an opencode chat you can reopen: it has a name, its messages, and its model

three complaints, one root cause and two small ones.

THE TRANSCRIPT CAME BACK EMPTY because the client read `/session/<id>/message`
and this build answers that with `[]` — 200, well-formed, and wrong. the same
session returns 4 messages on `/api/session/<id>/message`. an empty transcript is
indistinguishable from a new conversation, so reopening a chat said "send a
message to start" and every message the owner had sent looked deleted.

that also explains the third complaint. the model was read from the assistant
turns, so no messages meant no model, so the detail reported a bare `opencode`,
which the composer cannot match — and it fell back to claude code and sonnet. it
now also reads the model off the session record, which is populated before any
assistant turn lands.

the message shape moved with the endpoint: `type` where it was `info.role`,
`content` where it was `parts`, and a user message's text sits on the message
rather than in a part. both shapes are accepted rather than pinned to today's
build — this reader is the only thing between a working transcript and a chat
that looks erased, and a host on a different opencode should lose features, never
its history.

AND THE ORDER WAS BACKWARDS. the endpoint returns newest first and nothing
downstream re-sorted, so the answer sat above the question that produced it.
sorted here, because this is the only place that still has timestamps.

NAMING: opencode is meant to title a session from its contents and does not, on
this surface — sessions stay at `New session - <timestamp>` through a finished
turn and past it, which is why the list was a column of "(untitled)".
`isPlaceholderOpenCodeTitle` exists precisely because that name leaks into the UI.
so the sidecar names a NEW session from the first line of its first prompt, which
is what the claude side shows. a resumed session keeps what it has, and if
opencode ever does title one it writes after us and wins.

verified live on the session that prompted this: 4 messages, in order, model
opencode/kimi-k3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 13:56:37 +00:00
co-authored by Claude Opus 5
parent 3b1d4cea84
commit 02f20d419d
3 changed files with 106 additions and 17 deletions
@@ -161,6 +161,33 @@ function permissionRequest(raw: unknown): { sessionID: string; requestID: string
return { sessionID, requestID };
}
/*
Name the conversation from its opening message.
OpenCode titles a session from its contents — eventually, and better than anything we would write. It
is not happening on this surface: sessions stay at `New session - <timestamp>` through a finished turn
and past it, so the owner's session list is a column of "(untitled)" with nothing to tell one from
another. `isPlaceholderOpenCodeTitle` exists precisely because that name leaks into the UI.
So: the first line of the first prompt, which is what the Claude side shows and what a person would
have picked. Fire-and-forget on session CREATE only — a resumed session keeps whatever it has, and if
OpenCode ever does title one, it is writing after us and wins.
*/
async function nameSession(config: ServeConfig, sessionID: string, prompt: string, cwd: string): Promise<void> {
const title = prompt.split('\n')[0]?.trim().slice(0, 60);
if (!title) return;
try {
await serveJson(config, `/session/${sessionID}`, {
method: 'PATCH',
body: JSON.stringify({ title }),
cwd,
});
} catch (err) {
// A conversation that works and is badly named is not worth failing a turn over.
console.error('[opencode] could not name the session', err instanceof Error ? err.message : err);
}
}
function handleServeEvent(raw: unknown, config?: ServeConfig): void {
const ask = permissionRequest(raw);
if (ask && config) {
@@ -292,6 +319,7 @@ export async function runOpenCodeTurnOnServe(
});
if (!created?.id) throw new Error('session create returned no id');
openCodeSessionId = created.id;
void nameSession(config, openCodeSessionId, params.prompt, cwd);
}
if (params.model) {