send images to opencode, which never needed the fork

B4 properly. The composer gate was the honest stopgap; this is the fix. opencode run takes
attachments with --file, so images work on the subprocess path we already use — the parity
doc had them down as phase 4, behind the serve migration, and they were not.

The bug was one omission: handleOpenCodeChat`s msg type had no images field, so the browser
sent them, the bubble rendered them, and they stopped at that signature. Nothing reported a
loss anywhere.

Attachments are paths, not inline data, so the sidecar spills each image to a temp file for
the length of the turn and removes it in settle — the same place every other per-turn
resource is released, so a killed or superseded turn cleans up too.

The load-bearing detail is `--` before the prompt: --file is an array option, so without the
separator the prompt is eaten as another filename and the turn dies with "File not found:"
followed by the entire message. Confirmed against the binary, and pinned by a test that
records argv from a stub.

list-models now reports each model own capability instead of a hardcoded false — opencode
publishes capabilities.input.image per model and nothing had ever read it. Defaults to false,
so a model that does not declare it keeps the affordance hidden.

Verified end to end: a red png sent over the chat socket to opencode/claude-sonnet-4-6 came
back "Red".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:49:39 +01:00
co-authored by Claude Opus 5
parent a41abb4b0f
commit 73b8111216
6 changed files with 149 additions and 18 deletions
+3 -1
View File
@@ -1,4 +1,4 @@
import type { TurnMessage } from '@@/api/chat/types';
import type { PromptImage, TurnMessage } from '@@/api/chat/types';
import { logger } from '@@/api/chat/logger';
import * as sidecar from '@@/sidecar-registry';
import { getOpenCodeSession } from '@@/api/chat/opencode/state';
@@ -20,6 +20,7 @@ type OpenCodeStreamingParams = {
role?: string;
resumeSessionId?: string;
durable?: boolean;
images?: PromptImage[];
// Finished turn messages, already committed by the sidecar; `seq` is the cursor id to deliver them under.
onMessage: (msg: TurnMessage, seq?: number) => void;
};
@@ -59,6 +60,7 @@ export async function sendOpenCodeStreaming(params: OpenCodeStreamingParams): Pr
model: params.model,
resumeSessionId,
durable: params.durable,
images: params.images,
});
} catch (err) {
unsub();