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
+18 -2
View File
@@ -8,8 +8,7 @@ typedef RequestDispatcher = Future<IpcResponse> Function(IpcRequest request);
/// Unix-socket JSON-lines server. Each connection is an independent
/// bidirectional line-framed stream: client writes requests, daemon
/// writes responses (and events, later). Tier 0 handles request→response
/// only; event broadcasting lands with the first feature that emits.
/// writes responses + events on the same socket.
class DaemonServer {
DaemonServer({
required this.socketPath,
@@ -22,6 +21,23 @@ class DaemonServer {
ServerSocket? _server;
final Set<Socket> _clients = {};
/// Broadcast [event] to every currently-connected client.
///
/// Future tuning: per-client subsystem/id filter (`tail --filter
/// pane:p_7`). For Tier 1 every client sees everything. Sockets
/// that error on write are silently dropped; the client's read side
/// will notice the close.
void broadcast(IpcEvent event) {
final line = event.encode();
for (final c in List<Socket>.from(_clients)) {
try {
c.writeln(line);
} catch (_) {
_clients.remove(c);
}
}
}
Future<void> start() async {
final addr = InternetAddress(socketPath, type: InternetAddressType.unix);
try {