CLI-parity sessions: permission-mode, engine lifecycle, streaming, queueing, CI#2
Merged
Merged
Conversation
added 8 commits
July 9, 2026 12:59
Node 20 + pinned pnpm via corepack. Runs install --frozen-lockfile, build (shared first), typecheck, and the keyless test suite (the live claude CLI test self-skips). Protects the session refactor that follows.
…sion-mode A hub session now behaves like a native `claude` session started in the same directory. Per-session config passes through to the CLI and inherits ~/.claude when unset: - permissionMode -> --permission-mode (canonical control) - model -> --model - settings -> --settings The legacy 3-way approvalMode dial becomes a preset alias that maps onto the CLI's native modes (approve_all->manual, approve_writes->acceptEdits, full_auto->bypassPermissions), and the UI exposes the CLI modes directly (incl. plan/dontAsk/auto) with an 'inherit' default. Permission policy is delegated to the engine: the CLI applies --permission-mode and only routes tools it wants asked to --permission-prompt-tool; the hub relays those to the user. This retires the hub's READ_ONLY_TOOLS tool-name policy (and its spoofing sharp edge) and the ApprovalBroker. The echo stub now applies the mode itself (permissionModeDisposition) to stay a faithful template. DB migration 2 adds permission_mode/requested_model/settings and backfills permission_mode from the legacy approval_mode preset. Verified live against claude 2.1.205: manual asks for Write, bypassPermissions runs it silently, --model sonnet reaches the CLI; migration applied cleanly to existing sessions.
Add a second state axis to SessionDto, independent of the interaction status:
- engineState: new | live | hibernated | dead
- enginePid: the live engine's OS pid (or null)
SessionRuntime tracks transitions: live on spawn, hibernated on idle-dispose,
stop, or boot reconcile, dead on an unexpected exit. A mid-turn crash also closes
the open turn with a done{error} instead of hanging in 'running'. New stop_engine
WS control tears the process down now while keeping the session resumable
(restart happens on the next message via --resume). Migration 3 adds engine_state.
UI: a per-session liveness dot in the sidebar, an engine indicator + 'stop engine'
button in the status bar, and the permission dial shows the CLI modes.
Verified live against claude 2.1.205: live->dead on external SIGKILL (idle and
mid-turn), dead->live on respawn, live->hibernated on stop_engine.
Import highlight.js/lib/core and register ~19 languages instead of the full bundle. Cuts the web bundle from 1.11 MB to 229 KB (362 KB -> 78 KB gzip) and clears the chunk-size warning. Unknown languages fall back to plaintext.
A user_message sent while a turn is running is now buffered (bounded at 100) and drained one at a time on each 'done', instead of throwing session_busy (409). interrupt/stop/dispose clear the queue (a deliberate stop discards pending work). The web composer now lets you queue a follow-up while busy (button reads 'Queue'). Covered by a new e2e test: two back-to-back messages run in order with two dones and no busy rejection.
Add --include-partial-messages and map the CLI's stream_event text_delta events
to onDelta, so assistant text streams token-by-token instead of arriving as a
whole block. Reconstructs the delta messageKey as ${messageId}:${textBlockIndex}
(mapping the absolute content-block index to normalize.ts's text-only ordinal) so
the final persisted message replaces the streaming buffer without duplication.
Verified live against claude 2.1.205: deltas arrive and concatenate exactly to
the final message text under the same messageKey.
Refresh README + CLAUDE.md: the approval broker / READ_ONLY_TOOLS 'safety dial' is replaced by per-session --permission-mode delegated to the engine; document the engineState/enginePid lifecycle axis, stop_engine, mid-turn queueing, and Claude partial-message streaming. Add handoff2.md. (CLAUDE.md was previously untracked; committing the now-accurate architecture guide.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Continues the
handoff1.mdnext-steps. The through-line: a hub session should behave like a nativeclaudesession started in the same directory — it inherits~/.claudeby default and is controlled via the CLI's own flags, not a parallel system the hub invents. Six focused, independently-reviewable commits plus a docs refresh.Every change was driven end-to-end against the real Claude CLI 2.1.205, not just unit-tested.
What's in here
cifeat(sessions)permissionMode→--permission-mode,model→--model,settings→--settings, each inheriting~/.claudewhen unset. The legacy 3-wayapprovalModebecomes a preset alias (approve_all→manual,approve_writes→acceptEdits,full_auto→bypassPermissions). Permission policy is delegated to the engine — theApprovalBroker/READ_ONLY_TOOLStool-name policy (and its spoofing sharp edge from the handoff) is retired; the hub just relays what the CLI surfaces. DB migration 2.feat(sessions)SessionDtogainsengineState(new/live/hibernated/dead) +enginePid, a second state axis independent of the interactionstatus. Newstop_engineWS control; a mid-turn crash closes the turn withdone{error}instead of hanging. DB migration 3. UI: per-session liveness dots, an engine indicator + "stop engine" button, permission dropdown showing the CLI modes.perf(web)highlight.js/lib/core+ a language subset: web bundle 1.11 MB → 229 KB (362 → 78 KB gzip); size warning gone.feat(sessions)user_messagesent while busy is buffered (bounded at 100) and drained ondoneinstead of throwingsession_busy(409). Web composer offers a "Queue" button.feat(claude)--include-partial-messages;stream_eventtext deltas map toonDelta(key${messageId}:${textBlockIndex}) so assistant text streams and the final message reconciles the buffer.docshandoff2.md. (CLAUDE.md was previously untracked; the now-accurate architecture guide is added to the repo.)Notable outcomes
READ_ONLY_TOOLSspoofing sharp edge for free — the CLI does the write/read distinction natively viaacceptEdits.PreToolUsehook auto-allowing a tool so the hub never saw it) is now intended, documented CLI-parity behavior rather than a bug.Verification
pnpm install/build/typecheck/testgreen: 19 passing (+2 live gated), typecheck clean, web bundle rebuilt.manualasks for Write whilebypassPermissionsruns it silently;--model sonnetreaches the CLI; migration 2/3 applied cleanly to existing sessions; enginelive→deadon external SIGKILL (idle and mid-turn),dead→liverespawn,live→hibernatedonstop_engine; streaming deltas concatenate exactly to the final message text under the samemessageKey.Not in scope
Approval affordances (
permission_suggestions), roadmap items (Copilot/Codex adapters, session fork, scheduling), event-replay pagination. Seehandoff2.md.