route a resumed opencode session to opencode

Resuming an OpenCode conversation dispatched it to the Claude CLI: a `ses_…` id handed to
`claude --resume`. Wrong harness, not degraded output.

The whole six-hop chain, confirmed rather than inferred, because the endpoints alone do not show which
hop drops the value:

  /chat/:id fetches detail and sets `selected.model` = `opencode/big-pickle`  ← the value exists
  ChatDetailPanel renders <NewChat …> without a `model` prop                  ← dropped here
  NewChat reads `initialModel={locationState?.model}`                         ← unrelated source
  nothing in the tree ever writes `location.state.model`                      ← so always undefined
  useChat therefore holds no model and the socket sends none
  websocket.ts falls back to the user default, `isClaudeModel` is true

So the model was resolved correctly at the top and read from somewhere else at the bottom. `selected`
has carried `model` all along.

`locationState.model` stays as a fallback rather than being deleted: it is declared on
ChatLocationState and costs nothing to keep for a caller that navigates with one deliberately.

docs/opencode-parity.md B2, which flagged this as the one to verify hop by hop. Its account is accurate;
the added detail is that the value is produced and then dropped, not never produced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 02:22:47 +00:00
co-authored by Claude Opus 5
parent 22bcd7d4b9
commit 492509ae52
@@ -123,6 +123,15 @@ function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, on
} }
type NewChatProps = { type NewChatProps = {
/**
* The harness that owns this conversation, as `harness/model` — e.g. `opencode/big-pickle`.
*
* Load-bearing on resume, not cosmetic. The server routes by model: anything starting `claude-code`
* goes to the Claude sidecar, everything else to OpenCode. Send nothing and it falls back to the
* user's default, which is how a `ses_…` id ended up being handed to `claude --resume` — a
* wrong-harness dispatch rather than a degradation.
*/
model?: string | null;
resumeSummary?: string; resumeSummary?: string;
resumeSessionId?: string; resumeSessionId?: string;
initialMessages?: ChatMessage[]; initialMessages?: ChatMessage[];
@@ -191,7 +200,10 @@ function NewChat(props: NewChatProps) {
<EmbeddableChat <EmbeddableChat
chat={chat} chat={chat}
sessionId={undefined} sessionId={undefined}
initialModel={locationState?.model ?? undefined} // The session's own model wins. `locationState.model` is declared on ChatLocationState and
// written by nothing, so it was the only source and always undefined on resume; it stays as a
// fallback for a future caller that navigates with one deliberately.
initialModel={props.model ?? locationState?.model ?? undefined}
initialMessage={initialMessage} initialMessage={initialMessage}
defaultInput={locationState?.prefillInput ?? ''} defaultInput={locationState?.prefillInput ?? ''}
cwd={cwd} cwd={cwd}
@@ -241,6 +253,7 @@ export const ChatDetailPanel = () => {
initialMessages={selected.initialMessages} initialMessages={selected.initialMessages}
total={selected.total} total={selected.total}
initialOffset={selected.initialOffset} initialOffset={selected.initialOffset}
model={selected.model}
sessionCwd={selected.cwd} sessionCwd={selected.cwd}
sessionTitle={title} sessionTitle={title}
partCount={selected.partCount} partCount={selected.partCount}