decouple Pane from PtySession so the web build compiles
test / unit + widget + golden + a11y (push) Failing after 37s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped

Pane is now a pure data class — id, kind, pid, argv, cwd, title,
isClosed. The daemon-side PaneRegistry holds a parallel map of
PtySession keyed on id; registry methods look up both sides when
writing / resizing / closing.

The `clide.dart` barrel no longer re-exports `src/pty/*`,
`src/panes/registry.dart`, or the `*_commands.dart` modules — all
three transitively import `dart:ffi` which isn't available when
compiling to WebAssembly. The daemon entrypoint (bin/clide.dart) +
core tests import them via deep paths now. Pane / PaneKind /
DaemonEventSink / RecordingEventSink stay in the barrel since
they're pure data the Flutter app references over IPC.

Verified: `dart analyze` clean, 53 core tests green, 174 app tests
green, `make ui-smoke` compiles + serves + Playwright smoke passes,
daemon boots + ping round-trips + SIGTERMs cleanly.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-22 10:01:06 +02:00
co-authored by Claude
parent b9fbcd43a2
commit 37108230f2
8 changed files with 44 additions and 27 deletions
+13 -6
View File
@@ -20,6 +20,7 @@ class PaneRegistry {
final DaemonEventSink events;
final Map<String, Pane> _panes = {};
final Map<String, PtySession> _sessions = {};
final Map<String, StreamSubscription<Uint8List>> _subs = {};
int _nextId = 1;
@@ -55,12 +56,13 @@ class PaneRegistry {
final pane = Pane(
id: id,
kind: kind,
session: session,
pid: session.pid,
argv: argv,
cwd: cwd,
title: title,
);
_panes[id] = pane;
_sessions[id] = session;
_emit('pane.spawned', id, pane.toJson());
@@ -77,15 +79,17 @@ class PaneRegistry {
/// Send bytes to a pane's stdin.
int write(String id, List<int> bytes) {
final p = _panes[id];
if (p == null || p.isClosed) return 0;
return p.session.write(bytes);
final s = _sessions[id];
if (p == null || p.isClosed || s == null) return 0;
return s.write(bytes);
}
/// Resize a pane + emit `pane.resized`.
void resize(String id, {required int cols, required int rows}) {
final p = _panes[id];
if (p == null || p.isClosed) return;
p.session.resize(cols: cols, rows: rows);
final s = _sessions[id];
if (p == null || p.isClosed || s == null) return;
s.resize(cols: cols, rows: rows);
_emit('pane.resized', id, {'cols': cols, 'rows': rows});
}
@@ -93,9 +97,11 @@ class PaneRegistry {
Future<void> close(String id) async {
final p = _panes[id];
if (p == null) return;
await p.session.close();
final s = _sessions[id];
if (s != null) await s.close();
await _subs[id]?.cancel();
_subs.remove(id);
_sessions.remove(id);
_panes.remove(id);
_emit('pane.closed', id, const {});
}
@@ -111,6 +117,7 @@ class PaneRegistry {
void _onExit(Pane p) {
if (_panes.containsKey(p.id)) {
p.isClosed = true;
_emit('pane.exit', p.id, const {});
// Don't auto-close — keep the pane entry so `list` can show the
// exited state until the consumer explicitly closes. A future