add pane subsystem to the daemon + IPC event broadcast

Implements the Tier-1 pane subsystem from D-006: spawn / list / focus /
close / write / resize / tail commands, plus pane.spawned / output /
exit / resized / focused / closed events. PaneRegistry owns per-pane
PtySession lifecycles and id generation (p_N); a DaemonEventSink seam
keeps pane code decoupled from the IPC server package.

DaemonServer.broadcast() fans events out to every connected client.
Per-client subsystem/id filtering (`tail --filter pane:p_7`) is
deferred — Tier 1 broadcasts everything and the subscriber discards.

Panes carry a `kind:` field (terminal | claude). Step 7 (builtin.claude)
adds the claude-specific pane flow on top of this generic substrate —
the subsystem itself stays neutral.

14 new core tests: registry unit coverage (spawn → pane.spawned event,
output → base64 events, write/resize/close round-trips, idempotent
close, claude kind on the wire) plus dispatcher coverage (argv
validation, unknown-id → not-found, text vs bytes_b64, etc). All 37
core tests pass in ~3s under test-core.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-22 09:05:51 +02:00
co-authored by Claude
parent edd8a20e0d
commit 3715a2e191
10 changed files with 682 additions and 3 deletions
+60
View File
@@ -0,0 +1,60 @@
/// A single active pane in the daemon.
///
/// Owns a [PtySession] plus whatever pane-level metadata the UI +
/// CLI need. The `kind:` field distinguishes general-purpose terminal
/// panes from Claude panes (D-041, not yet landed) from whatever
/// future pane-shaped surface Tier 1+ grows.
library;
import '../pty/session.dart';
/// Kind of a pane. Keep this enum small and explicit — each kind
/// typically pairs with a bundled extension that manages its
/// lifecycle (`builtin.terminal`, `builtin.claude`).
enum PaneKind {
terminal,
claude;
String get wire => name;
static PaneKind parse(String s) {
return PaneKind.values.firstWhere(
(v) => v.wire == s,
orElse: () => throw ArgumentError.value(s, 'kind', 'unknown pane kind'),
);
}
}
/// A live pane. Thin wrapper over [PtySession] — the registry is what
/// owns the session lifecycle; consumers of this class read state and
/// call [write] / [resize] via the session.
class Pane {
Pane({
required this.id,
required this.kind,
required this.session,
required this.argv,
this.cwd,
this.title,
});
final String id;
final PaneKind kind;
final PtySession session;
final List<String> argv;
final String? cwd;
final String? title;
int get pid => session.pid;
bool get isClosed => session.isClosed;
Map<String, Object?> toJson() => {
'id': id,
'kind': kind.wire,
'pid': pid,
'argv': argv,
if (cwd != null) 'cwd': cwd,
if (title != null) 'title': title,
'closed': isClosed,
};
}