diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 02f584b99c6..d4cefb384d2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1093,6 +1093,13 @@ export const PromptInput: Component = (props) => { agent, model: `${model.providerID}/${model.modelID}`, variant, + parts: images.map((attachment) => ({ + id: Identifier.ascending("part"), + type: "file" as const, + mime: attachment.mime, + url: attachment.dataUrl, + filename: attachment.filename, + })), }) .catch((err) => { showToast({ @@ -1188,6 +1195,7 @@ export const PromptInput: Component = (props) => { filename: attachment.filename, })) + const messageID = Identifier.ascending("message") const textPart = { id: Identifier.ascending("part"), diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 02863b4cae3..4558914cb7e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -559,6 +559,12 @@ export function Prompt(props: PromptProps) { model: `${selectedModel.providerID}/${selectedModel.modelID}`, messageID, variant, + parts: nonTextParts + .filter((x) => x.type === "file") + .map((x) => ({ + id: Identifier.ascending("part"), + ...x, + })), }) } else { sdk.client.session.prompt({ diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fd7f8aa72a5..87b53f526bb 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1422,10 +1422,23 @@ export namespace SessionPrompt { arguments: z.string(), command: z.string(), variant: z.string().optional(), + parts: z + .array( + z.discriminatedUnion("type", [ + MessageV2.FilePart.omit({ + messageID: true, + sessionID: true, + }).partial({ + id: true, + }), + ]), + ) + .optional(), }) export type CommandInput = z.infer const bashRegex = /!`([^`]+)`/g - const argsRegex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g + // Match [Image N] as single token, quoted strings, or non-space sequences + const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi const placeholderRegex = /\$(\d+)/g const quoteTrimRegex = /^["']|["']$/g /** @@ -1516,6 +1529,7 @@ export namespace SessionPrompt { throw error } + const templateParts = await resolvePromptParts(template) const parts = (agent.mode === "subagent" && command.subtask !== false) || command.subtask === true ? [ @@ -1525,10 +1539,10 @@ export namespace SessionPrompt { description: command.description ?? "", command: input.command, // TODO: how can we make task tool accept a more complex input? - prompt: await resolvePromptParts(template).then((x) => x.find((y) => y.type === "text")?.text ?? ""), + prompt: templateParts.find((y) => y.type === "text")?.text ?? "", }, ] - : await resolvePromptParts(template) + : [...templateParts, ...(input.parts ?? [])] const result = (await prompt({ sessionID: input.sessionID, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index ac5ea12113c..a26cefb176f 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -24,6 +24,7 @@ import type { ExperimentalResourceListResponses, FileListResponses, FilePartInput, + FilePartSource, FileReadResponses, FileStatusResponses, FindFilesResponses, @@ -1451,6 +1452,14 @@ export class Session extends HeyApiClient { arguments?: string command?: string variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> }, options?: Options, ) { @@ -1467,6 +1476,7 @@ export class Session extends HeyApiClient { { in: "body", key: "arguments" }, { in: "body", key: "command" }, { in: "body", key: "variant" }, + { in: "body", key: "parts" }, ], }, ], diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 431135db3d1..596e52ad9f6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3292,6 +3292,14 @@ export type SessionCommandData = { arguments: string command: string variant?: string + parts?: Array<{ + id?: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + }> } path: { /** diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3e7bd5e08da..a32be164c23 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -2667,6 +2667,38 @@ }, "variant": { "type": "string" + }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "file" + }, + "mime": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "url": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FilePartSource" + } + }, + "required": ["type", "mime", "url"] + } + ] + } } }, "required": ["arguments", "command"]