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 ba6df1d6bbdd..a5cf25ca5689 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -120,7 +120,33 @@ export function Prompt(props: PromptProps) { event.on(TuiEvent.PromptAppend.type, (evt) => { if (!input || input.isDestroyed) return + const oldLength = input.plainText.length input.insertText(evt.properties.text) + + if (evt.properties.parts.length > 0) { + for (const part of evt.properties.parts) { + const extmarkId = + part.source && + input.extmarks.create({ + start: oldLength + (part.type === "file" ? part.source.text.start : part.source.start), + end: oldLength + (part.type === "file" ? part.source.text.end : part.source.end), + virtual: true, + styleId: part.type === "agent" ? agentStyleId : fileStyleId, + typeId: promptPartTypeId, + }) + + setStore( + produce((draft) => { + const partIndex = draft.prompt.parts.length + draft.prompt.parts.push(part) + if (extmarkId !== undefined) { + draft.extmarkToPartIndex.set(extmarkId, partIndex) + } + }), + ) + } + } + setTimeout(() => { // setTimeout is a workaround and needs to be addressed properly if (!input || input.isDestroyed) return diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index b2e4b92c5513..c4cc7b7f59d8 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -1,10 +1,22 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { SessionID } from "@/session/schema" +import { MessageV2 } from "@/session/message-v2" import z from "zod" export const TuiEvent = { - PromptAppend: BusEvent.define("tui.prompt.append", z.object({ text: z.string() })), + PromptAppend: BusEvent.define( + "tui.prompt.append", + z.object({ + text: z.string(), + parts: z.array( + z.union([ + MessageV2.AgentPart.omit({ id: true, sessionID: true, messageID: true }), + MessageV2.FilePart.omit({ id: true, sessionID: true, messageID: true }), + ]), + ), + }), + ), CommandExecute: BusEvent.define( "tui.command.execute", z.object({ diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts index 8650a0cccf76..bb73d0a37552 100644 --- a/packages/opencode/src/server/routes/tui.ts +++ b/packages/opencode/src/server/routes/tui.ts @@ -3,6 +3,7 @@ import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { Bus } from "../../bus" import { Session } from "../../session" +import { SessionPrompt } from "../../session/prompt" import { TuiEvent } from "@/cli/cmd/tui/event" import { AsyncQueue } from "../../util/queue" import { errors } from "../error" @@ -95,9 +96,15 @@ export const TuiRoutes = lazy(() => ...errors(400), }, }), - validator("json", TuiEvent.PromptAppend.properties), + validator("json", z.object({ text: z.string() })), async (c) => { - await Bus.publish(TuiEvent.PromptAppend, c.req.valid("json")) + const { text } = c.req.valid("json") + await Bus.publish(TuiEvent.PromptAppend, { + text, + parts: (await SessionPrompt.resolvePromptParts(text)).filter( + (part) => part.type === "agent" || part.type === "file", + ), + }) return c.json(true) }, ) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 2b092fc8fe2f..9cddb54da265 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -132,15 +132,34 @@ export namespace SessionPrompt { const info = yield* fsys.stat(filepath).pipe(Effect.option) if (Option.isNone(info)) { const found = yield* agents.get(name) - if (found) parts.push({ type: "agent", name: found.name }) + if (found) { + parts.push({ + type: "agent" as const, + name: found.name, + source: { + start: match.index!, + end: match.index! + match[0].length, + value: match[0], + }, + }) + } return } const stat = info.value parts.push({ - type: "file", - url: pathToFileURL(filepath).href, - filename: name, + type: "file" as const, mime: stat.type === "Directory" ? "application/x-directory" : "text/plain", + filename: name, + url: pathToFileURL(filepath).href, + source: { + type: "file" as const, + path: name, + text: { + start: match.index!, + end: match.index! + match[0].length, + value: match[0], + }, + }, }) }), { concurrency: "unbounded", discard: true }, diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 8eefe5bfe985..8f7feeeaf3f2 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -615,6 +615,24 @@ export type EventTuiPromptAppend = { type: "tui.prompt.append" properties: { text: string + parts: Array< + | { + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } + } + | { + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + } + > } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7ca6ea05c85e..3aa1f7f61f42 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -241,6 +241,24 @@ export type EventTuiPromptAppend = { type: "tui.prompt.append" properties: { text: string + parts: Array< + | { + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } + } + | { + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource + } + > } }