Add the dasHerd detachable terminal foundation#3528
Conversation
There was a problem hiding this comment.
Pull request overview
This PR lays down the foundation for dasHerd’s detachable terminal host (a persistent watcher that owns PTYs + durable logs, controllable over authenticated HTTP/WebSocket), along with benchmarking probes, an offline xterm.js browser control panel, and a native ImGui client. It also extends the dashv WebSocket client API to preserve binary frames (including embedded \0) and adjusts terminal erase behavior to better match real-terminal expectations.
Changes:
- Add the dasHerd watcher (core session lifecycle + HTTP/WS control server), plus a browser control panel and an ImGui “rich client”.
- Add benchmark “chunk” probes measuring Git-through-terminal overhead and process-per-session overhead.
- Extend
dasHVWebSocket client callbacks to a losslessonMessageFrameAPI (opcode + authoritative byte count), add binary-frame tests/examples, and adjust terminal erase semantics + oracle corpus.
Reviewed changes
Copilot reviewed 28 out of 38 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/dasHerd/WORKTREES_AND_TASK_TERMINALS.md | Design draft describing worktree + task-terminal + detachable host direction and staged plan. |
| utils/dasHerd/watcher/watcher_server.das | HTTP + WebSocket control server for the watcher (auth, session ops, snapshots, raw replay). |
| utils/dasHerd/watcher/watcher_core.das | Core session lifecycle (launch, poll, drain, timeout, controller lease, raw history + log writing). |
| utils/dasHerd/watcher/vendor/xterm/xterm.css | Vendored xterm.css asset for offline browser terminal UI. |
| utils/dasHerd/watcher/vendor/xterm/README.md | Notes and versioning for vendored xterm.js assets. |
| utils/dasHerd/watcher/vendor/xterm/LICENSE.xterm.txt | License for vendored xterm.js. |
| utils/dasHerd/watcher/vendor/xterm/LICENSE.addon-web-links.txt | License for vendored web-links addon. |
| utils/dasHerd/watcher/vendor/xterm/LICENSE.addon-unicode11.txt | License for vendored unicode11 addon. |
| utils/dasHerd/watcher/vendor/xterm/LICENSE.addon-fit.txt | License for vendored fit addon. |
| utils/dasHerd/watcher/vendor/xterm/addon-web-links.js | Vendored addon-web-links.js bundle. |
| utils/dasHerd/watcher/vendor/xterm/addon-unicode11.js | Vendored addon-unicode11.js bundle. |
| utils/dasHerd/watcher/vendor/xterm/addon-fit.js | Vendored addon-fit.js bundle. |
| utils/dasHerd/watcher/tests/test_watcher_core.das | Watcher core tests covering lifecycle, drain/exit races, timeouts, controller arbitration. |
| utils/dasHerd/watcher/rich_client.das | Native ImGui client that renders raw watcher bytes via the daScript terminal emulator and sends input back. |
| utils/dasHerd/watcher/README.md | Run/usage docs for watcher + clients and their responsibilities/logs. |
| utils/dasHerd/watcher/main.das | Watcher process lifecycle host (init/update/shutdown, loopback binding, GC scheduling). |
| utils/dasHerd/watcher/control.html | Offline browser control panel using xterm.js, authenticated via token query param. |
| utils/dasHerd/chunk1/session_host_worker.das | Worker for “fresh process per session” benchmark lane. |
| utils/dasHerd/chunk1/README.md | Documentation + first measurement results for chunk1 overhead probe. |
| utils/dasHerd/chunk1/benchmark_session_host.das | Benchmark harness comparing direct vs in-process terminal vs fresh worker-host lanes. |
| utils/dasHerd/chunk0/README.md | Documentation + first measurement results for chunk0 terminal overhead probe. |
| utils/dasHerd/chunk0/git_terminal_window.das | Visible terminal probe UI (ImGui) for manual inspection + logging. |
| utils/dasHerd/chunk0/git_terminal_probe.das | Shared probe implementation for direct capture vs terminal capture lanes. |
| utils/dasHerd/chunk0/benchmark_git_terminal.das | Headless benchmark runner for chunk0 probes. |
| tutorials/dasHV/06_websockets.das | Tutorial code updated to the new onMessageFrame-based client callback. |
| tests/dasHV/test_websockets.das | Adds coverage for binary WebSocket frames preserving opcode/length/embedded zero bytes. |
| modules/dasTerminal/tests/terminal_semantics.das | Adds an assertion for wide grapheme handling (“中”). |
| modules/dasTerminal/tests/app_compat/scenarios.json | Adds an OpenCode background-color erase scenario to the oracle corpus. |
| modules/dasTerminal/tests/app_compat/scenario-source.mjs | Adds source definition for the new OpenCode scenario. |
| modules/dasTerminal/tests/app_compat/oracle-snapshots.json | Adds expected oracle snapshot for the new OpenCode scenario. |
| modules/dasTerminal/daslib/terminal.das | Changes erase/blanking semantics to respect the active pen background/attributes. |
| modules/dasHV/src/dasHV.h | Adds C++ API declarations for channel close + server bind host selection. |
| modules/dasHV/src/dasHV.cpp | Routes WebSocket client delivery through opcode-aware onMessageFrame; adds close/bind-host externs. |
| modules/dasHV/src/dashv_gen.inc | Updates generated adapter offsets for the new onMessageFrame method. |
| modules/dasHV/src/aot_hv.h | Adds AOT declarations for the new close/bind-host externs. |
| modules/dasHV/dashv/dashv_boost.das | Exposes onMessageFrame + set_bind_host at the daScript wrapper level. |
| examples/hv/ws_chat_client.das | Example updated to handle the new onMessageFrame callback. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def private observe_exit(var session : WatcherSession?; now_ms : int64) { | ||
| if (session.exit_seen_ms >= 0l || !terminal_process_exited(session.terminal)) return | ||
| session.exit_seen_ms = now_ms | ||
| session.exit_code = terminal_exit_code(session.terminal) | ||
| session.state = "exited" | ||
| session.status_revision++ | ||
| if (session.last_data_ms == 0l) { | ||
| session.last_data_ms = now_ms | ||
| } | ||
| append_event(session, "process_exited", "exit_code={session.exit_code}") | ||
| } |
| void onMessageFrame ( const string & msg, ws_opcode opcode ) { | ||
| if ( auto fnOnMessageFrame = get_onMessageFrame(classPtr) ) { | ||
| invoke_onMessageFrame(context, fnOnMessageFrame, classPtr, | ||
| (char *)msg.data(), static_cast<int32_t>(msg.size()), opcode); | ||
| } | ||
| } |
| let error = watcher_input(input.session_id, input.text) | ||
| if (!empty(error)) return self.respond_error(resp, http_status.CONFLICT, error) | ||
| return resp |> JSON(sprint_json(WatcherOk(), false), http_status.OK) |
| let error = watcher_resize(resize.session_id, resize.columns, resize.rows, 0) | ||
| if (!empty(error)) return self.respond_error(resp, http_status.CONFLICT, error) | ||
| return resp |> JSON(sprint_json(WatcherOk(), false), http_status.OK) |
| def private input_session(var session : WatcherSession?; text : string; client_id : int) : string { | ||
| if (session.controller_id != 0 && session.controller_id != client_id) return "controller lease is held by another client" | ||
| if (session.state != "running" || session.drained) return "session is not running" | ||
| if (!terminal_write(session.terminal, text)) return terminal_transport_error(session.terminal) | ||
| session.input_bytes += int64(length(text)) | ||
| return "" | ||
| } |
| def private resize_session(var session : WatcherSession?; columns, rows, client_id : int) : string { | ||
| if (session.controller_id != 0 && session.controller_id != client_id) return "controller lease is held by another client" | ||
| terminal_resize(session.terminal, clamp(columns, 20, 400), clamp(rows, 5, 200)) | ||
| append_event(session, "resized", "{session.terminal.columns}x{session.terminal.rows}") | ||
| return "" | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 42 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
utils/dasHerd/watcher/watcher_core.das:268
- observe_exit overwrites session.state to "exited" for any session once terminal_process_exited() becomes true. That can hide real failure states (e.g. transport_error sets state="failed" and drained=true; the next tick’s drained-path calls observe_exit and will flip it to "exited"). Preserve "failed" while still recording exit_code/exit_seen_ms.
Summary
Companion renderer changes: borisbat/dasImgui#229
Validation
A\0Bbinary payload preservation@xterm/headlessoracle is current and app compatibility tests pass (13 scenarios)Notes
This is intentionally a substantial work-in-progress foundation PR. The watcher/browser/native-client path is usable, while higher-level worktree and agent herding UI remains follow-up work.