send images on the serve path too, before anyone switches to it

runOpenCodeTurnOnServe ignored params.images entirely, which is defect B4 rebuilt on the new
path: the image renders in your own bubble and the model never receives it, with nothing
reporting a loss. Fixed before the path is switched on for anyone rather than after.

data: URIs, not file://, and that is measured — the wrong choice is accepted with a 200 and
then dies inside the turn with "Anthropic Messages media must contain valid base64". The
data URI round-trips and the model describes the image.

Strictly better than the subprocess path here: no temp file to spill and nothing to clean up,
because the bytes travel in the request.

Both prompt paths carry them — an ordinary send and a mid-turn injection. Verified end to end
through the chat socket with the serve engine on: a red png came back "**Red**", with deltas
streaming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 19:25:48 +01:00
co-authored by Claude Opus 5
parent a058cbb3fd
commit b79eca45fb
+30 -2
View File
@@ -188,7 +188,10 @@ export async function runOpenCodeTurnOnServe(
try { try {
await serveJson(config, `/api/session/${live.openCodeSessionId}/prompt`, { await serveJson(config, `/api/session/${live.openCodeSessionId}/prompt`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ prompt: { text: params.prompt }, delivery: 'steer' }), body: JSON.stringify({
prompt: { text: params.prompt, files: promptFiles(params.images) },
delivery: 'steer',
}),
cwd, cwd,
}); });
} catch (err) { } catch (err) {
@@ -255,7 +258,10 @@ export async function runOpenCodeTurnOnServe(
// `delivery` is stated explicitly because it DEFAULTS to `"steer"`, which injects into a running // `delivery` is stated explicitly because it DEFAULTS to `"steer"`, which injects into a running
// turn. For an ordinary send that is the wrong default — two quick messages would merge into one // turn. For an ordinary send that is the wrong default — two quick messages would merge into one
// turn instead of running in order. `steer` is Phase C's job, wired to the button that means it. // turn instead of running in order. `steer` is Phase C's job, wired to the button that means it.
body: JSON.stringify({ prompt: { text: params.prompt }, delivery: 'queue' }), body: JSON.stringify({
prompt: { text: params.prompt, files: promptFiles(params.images) },
delivery: 'queue',
}),
cwd, cwd,
}); });
} catch (err) { } catch (err) {
@@ -273,6 +279,28 @@ function retire(turn: ServeTurn, event: ChatEvent | null): void {
const errText = (err: unknown): string => (err instanceof Error ? err.message : String(err)); const errText = (err: unknown): string => (err instanceof Error ? err.message : String(err));
/**
* Images as `prompt.files`, which this surface takes by URI.
*
* **`data:` URIs, not `file://`.** Measured, because the choice is not obvious and the wrong one fails
* at the provider rather than at the API: a `file://` attachment is accepted with a 200 and then dies
* inside the turn with `Anthropic Messages media must contain valid base64`. A `data:` URI round-trips
* and the model describes the image.
*
* This is strictly better than the subprocess path, which has to spill each image to a temp file for
* `--file` and delete it afterwards. Here the bytes go in the request and there is nothing to clean up.
*
* Silently dropping these is exactly defect B4 — the user sees their image in their own bubble and the
* model never receives it — so this exists before the serve path is switched on for anyone, not after.
*/
function promptFiles(images: OpenCodeRunParams['images']): { uri: string; name: string }[] | undefined {
if (!images?.length) return undefined;
return images.map((image, index) => ({
uri: `data:${image.mediaType || 'image/png'};base64,${image.data}`,
name: `attachment-${index + 1}`,
}));
}
/** The serve analog of `listRunningOpenCodeTurns`. Same shape, so the Live panel needs no changes. */ /** The serve analog of `listRunningOpenCodeTurns`. Same shape, so the Live panel needs no changes. */
export function listRunningServeTurns(): { sessionKey: string }[] { export function listRunningServeTurns(): { sessionKey: string }[] {
return [...bySessionKey.keys()].map((sessionKey) => ({ sessionKey })); return [...bySessionKey.keys()].map((sessionKey) => ({ sessionKey }));