From e5e7c0347e74ddbd59edac79f241070a04cedea1 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:47:41 +0800 Subject: [PATCH 1/2] docs(cli): document the DSH_TOOLS_MODE contract in the owning README Responding to ds-review-bot round 2 on #648: the env seam's accepted values, native default, process-wide scope, loud-failure behavior, and temporary status now live in apps/cli/README.md next to the Web/headless surface it configures, not only in the cordis.yml comment. --- apps/cli/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/cli/README.md b/apps/cli/README.md index 4ff9034dd3..50571632c0 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -14,6 +14,8 @@ The TUI surface: The Web and headless surfaces boot one shared composition (`cordis.yml`): both treat the invoking directory as the default project and Workspace root, create named Workspaces beneath that root unless `--workspace-root ` overrides it, load applicable `AGENTS.md`/`CLAUDE.md` instructions into each agent-loop request prefix with a 65,536-byte render budget, and opt into first-message model titles. Headless differs only in listening on an OS-assigned port (parallel `dsh -p` runs never collide; the stderr-printed URL opens the live session in a browser). Both need the frontend dist and client bundles built (`pnpm run build && pnpm run build:web`). +`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode). + ## Install (developer machine) Symlink the source-running launcher onto your PATH; it resolves the checkout through its own real path, so code changes apply on the next launch with no build step: From acf0d42ed89759810c4ca09081eb2e2a1541db1d Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:51:03 +0800 Subject: [PATCH 2/2] fix(client-runtime): settled-only dispatch index carries null callTime; README matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Responding to ds-review-bot round 2 on #653: the tool/code-dispatch event is appended at settlement, so using its time as callTime fabricated a zero-duration call for duration-aware consumers — it is now null (start unknown) per the ToolResultNode contract, pinned in the session spec. The README's codeDispatches section described the PR3 running→settled lifecycle a stack ahead of this tree; it now documents the settled-only index this PR ships (the running shape lands with the start event in #658, which already merges cleanly over this). --- packages/client/runtime/README.md | 2 +- packages/client/runtime/src/client/sessions/session.ts | 5 ++++- packages/client/runtime/tests/session.spec.ts | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 12bfaa459b..af164de824 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -14,7 +14,7 @@ SlotsService gives the renderer separate bare observables for `useSessions` and ## Code Mode sub-dispatch index -`ConversationSnapshot.codeDispatches` groups a `run_code` call's sub-dispatches under their parent callId, in start order, using the native call-block shapes: a started-but-unsettled sub-call is a `RunningToolCall` (rows derive the running ring from the shape) and its `tool/code-dispatch` settlement replaces it in place with the `ToolResultNode` form, `callTime` carrying the paired start's time. Live mux frames and history replay build the identical index; sub-calls never join the surface `nodes` flow; per-parent array and map references are memo-stable across unrelated snapshot swaps. +`ConversationSnapshot.codeDispatches` groups a `run_code` call's sub-dispatches under their parent callId, in dispatch order, as settled `ToolResultNode` entries (the native result shape): each `tool/code-dispatch` event appends one. The event carries only the settle timestamp, so `callTime` is `null` (start unknown) — no duration claim is possible from this index yet. Live mux frames and history replay build the identical index; sub-calls never join the surface `nodes` flow; per-parent array and map references are memo-stable across unrelated snapshot swaps. ## Session title projection diff --git a/packages/client/runtime/src/client/sessions/session.ts b/packages/client/runtime/src/client/sessions/session.ts index 322a2049fd..1c2eac7ea3 100644 --- a/packages/client/runtime/src/client/sessions/session.ts +++ b/packages/client/runtime/src/client/sessions/session.ts @@ -638,7 +638,10 @@ export class Session implements ObservableSnapshot { kind: 'tool-result', seq: event.seq, time: event.time, callId: data.subCallId, call: { name: data.name, argsRaw: JSON.stringify(data.arguments) }, - callTime: event.time, + // The settle event is the only timestamp this event carries; the + // start time is unknown (null per the ToolResultNode contract), so + // duration-aware consumers never see a fabricated zero-duration call. + callTime: null, content: data.content, isError: data.isError, callView: null, resultView: null, } diff --git a/packages/client/runtime/tests/session.spec.ts b/packages/client/runtime/tests/session.spec.ts index beceefce30..9cf153f105 100644 --- a/packages/client/runtime/tests/session.spec.ts +++ b/packages/client/runtime/tests/session.spec.ts @@ -659,6 +659,9 @@ describe('run_code sub-dispatch indexing', () => { expect(subs?.[0]).toMatchObject({ kind: 'tool-result', callId: 'p1:code:1', call: { name: 'bash', argsRaw: '{"command":"ls","description":"列目录"}' }, + // The settle event carries no start time: callTime stays null (never a + // fabricated zero-duration). + callTime: null, isError: false, content: [{ type: 'text', text: 'demo.txt' }], }) expect(subs?.[1]).toMatchObject({ callId: 'p1:code:2', isError: true })