/chrome command output visibility in a fresh session

August 14, 2026 · View on GitHub

Problem

Running /chrome status (or doctor, authorize, …) in a brand-new session executed correctly and appended command/run + command/done, but the page stayed on the empty hero and showed nothing. The same commands rendered fine in a session that already had conversation content.

Root cause

This is a deliberate harness behaviour, not a bridge bug.

ComposerPhase activates a conversation only when the Chat window holds a visible non-command Node:

// packages/client/runtime/src/client/sessions/session.ts
function hasVisibleConversationContent(chat: ChatSnapshot): boolean {
  return chat.order.some(key => chat.nodes.get(key)?.kind !== 'command')
}

Generic command rows are control-plane content and are excluded on purpose, so a command-only session stays blank and the hero hides the conversation view.

/goal is not special-cased in that predicate — it works because ui-goal ships a feature-owned Conversation Definition that projects the command input as a separate non-command Chat Node. The harness Agent Note 2026-08-01-goal-command-input-projection records the decision and explicitly rejects widening the generic rule:

Render every command input as a user bubble. Rejected because existing control commands deliberately leave a fresh session on the hero; changing them would broaden interaction semantics without a feature-owned Conversation Definition.

So the sanctioned fix for /chrome is a feature-owned projection in this package, not a change to the harness render layer.

Fix

dsh-chrome now ships a browser half (src/client/) mirroring ui-goal:

  • chrome-command-input.ts — a ConversationNodeDefinition matching command/run where name === 'chrome', building a chrome-command-input Chat Node anchored at seq - 0.1 (just before the run).
  • ChromeCommandInputView.tsx — the keyed conversation.chat.node renderer, a right-aligned bubble reusing the shared --dsw-* tokens.
  • index.tsapply registering the Definition plus the renderer through ctx.slots.inject('conversation.chat.node', …).

Because the projected Node's kind is chrome-command-input (not command), it satisfies hasVisibleConversationContent, flips the session to active, and the generic result row that the command Definition already produced becomes visible alongside it.

Build

The harness clientBundle tsdown preset is repository-internal, so the closure factory format is reproduced locally in scripts/build-client.mjs with esbuild:

window.__ModuleLoader__.load({ id: "dsh-chrome", factory: (require) => { … } })

Only react and react/jsx-runtime stay external — they are resolved from the shell's frozen module table. Every other import is type-only and erased, so the bundle carries no cross-plugin runtime identity. src/client/ is excluded from the node-half tsconfig.json and type-checked by tsconfig.client.json.

Operational note

ClientModuleRegistry caches negative dsh.client verdicts for the process lifetime ("plugin-set changes take effect on restart"). Adding the client half to an already-running dsh web therefore requires a server restart before the row appears in window.__DSH_BOOT__; rebuilding the bundle alone is not enough.