diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 94da6fea125..5397816afcb 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -408,6 +408,7 @@ describe("prompt submit worktree selection", () => { files: [], agents: [], metadata: { + displayText: "ls", comments: [], agent: "agent", model: { providerID: "provider", modelID: "model", variant: "high" }, diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 799ef5047ae..ecb9c2bfc82 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -136,10 +136,10 @@ export async function sendFollowupDraft(input: FollowupSendInput) { sessionID: input.draft.sessionID, id: messageID, text: request.text, - ...(request.displayText === request.text ? {} : { displayText: request.displayText }), files: request.files.map((file) => ({ uri: file.uri, name: file.name, mention: file.mention })), agents: request.agents, metadata: { + displayText: request.displayText, comments: request.comments, agent: input.draft.agent, model: { diff --git a/packages/app/src/utils/comment-note.ts b/packages/app/src/utils/comment-note.ts index 0c05e4908e3..72d47af1b29 100644 --- a/packages/app/src/utils/comment-note.ts +++ b/packages/app/src/utils/comment-note.ts @@ -57,9 +57,9 @@ export function readPromptPresentation(value: unknown) { if (!value || typeof value !== "object") return const displayText = (value as { displayText?: unknown }).displayText const comments = (value as { comments?: unknown }).comments - if (!Array.isArray(comments)) return + if (typeof displayText !== "string" || !Array.isArray(comments)) return return { - displayText: typeof displayText === "string" ? displayText : undefined, + displayText, comments: comments.flatMap((item): PromptComment[] => { if (!item || typeof item !== "object") return [] const path = (item as { path?: unknown }).path diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts index f96c4bc4dd8..ce281aa4d2a 100644 --- a/packages/app/src/utils/prompt.test.ts +++ b/packages/app/src/utils/prompt.test.ts @@ -106,17 +106,15 @@ describe("extractPromptFromMessage", () => { expect(extractPromptFromMessage(message)[0]).toMatchObject({ type: "text", content: "model text" }) }) - test("restores explicit presentation text", () => { + test("restores command invocation text", () => { const message = { id: "msg_1", type: "user", text: "expanded command template", - displayText: "/command input", - metadata: { comments: [{ path: "src/app.ts", comment: "check this" }] }, + command: { name: "command", arguments: "input" }, time: { created: 1 }, } satisfies SessionMessageUser expect(extractPromptFromMessage(message)[0]).toMatchObject({ type: "text", content: "/command input" }) - expect(extractPromptComments(message)).toEqual([{ path: "src/app.ts", comment: "check this" }]) }) }) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index d7f9f4db5cc..36f74a79f30 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -44,7 +44,9 @@ export function extractPromptFromMessage( message: SessionMessageUser, opts?: { directory?: string; attachmentName?: string }, ): Prompt { - const text = message.displayText ?? readPromptPresentation(message.metadata)?.displayText ?? message.text + const text = message.command + ? `/${message.command.name}${message.command.arguments ? ` ${message.command.arguments}` : ""}` + : (readPromptPresentation(message.metadata)?.displayText ?? message.text) const directory = opts?.directory const attachmentName = opts?.attachmentName ?? "attachment" const toRelative = (path: string) => { diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts index 611996eb307..2e5930641cf 100644 --- a/packages/app/src/utils/session-message.test.ts +++ b/packages/app/src/utils/session-message.test.ts @@ -50,12 +50,12 @@ describe("session message presentation", () => { }) }) - test("projects explicit presentation text", () => { + test("projects command invocation text", () => { const message = { id: "msg_user", type: "user", text: "expanded command template", - displayText: "/command input", + command: { name: "command", arguments: "input" }, time: { created: 1 }, } satisfies SessionMessageUser diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts index 8243bb61140..d3015903764 100644 --- a/packages/app/src/utils/session-message.ts +++ b/packages/app/src/utils/session-message.ts @@ -57,7 +57,9 @@ export function presentUserMessage( export function presentUserParts(sessionID: string, message: SessionMessageUser): Part[] { const presentation = readPromptPresentation(message.metadata) - const text = message.displayText ?? presentation?.displayText ?? message.text + const text = message.command + ? `/${message.command.name}${message.command.arguments ? ` ${message.command.arguments}` : ""}` + : (presentation?.displayText ?? message.text) return [ ...(text ? [textPart(sessionID, message.id, 0, text)] : []), ...(message.files ?? []).map( diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index a3346e70fa5..dbcc071fc2c 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -172,7 +172,6 @@ export type Endpoint5_12Input = { readonly sessionID: Session.ID readonly id?: SessionMessage.ID | undefined readonly text: string - readonly displayText?: string | undefined readonly files?: ReadonlyArray | undefined readonly agents?: ReadonlyArray | undefined readonly skills?: ReadonlyArray | undefined diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 32ca8a31285..dccea27fa09 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -397,7 +397,6 @@ const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12I payload: { id: input["id"], text: input["text"], - displayText: input["displayText"], files: input["files"], agents: input["agents"], skills: input["skills"], diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 5954e2ad23f..3d5b02b21e0 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -593,7 +593,6 @@ export function make(options: ClientOptions) { body: { id: input["id"], text: input["text"], - displayText: input["displayText"], files: input["files"], agents: input["agents"], skills: input["skills"], diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 78aadfc617e..f7f568664c4 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -30,6 +30,8 @@ export type FileDiffInfo = { status: "added" | "deleted" | "modified" } +export type PromptCommandInvocation = { name: string; arguments: string } + export type PromptBase64 = string export type PromptFileSource = { type: "inline" } | { type: "uri"; uri: string } @@ -1684,7 +1686,7 @@ export type SessionMessageUser = { metadata?: { [x: string]: JsonValue } time: { created: number } text: string - displayText?: string + command?: PromptCommandInvocation files?: Array agents?: Array skills?: Array @@ -1693,7 +1695,7 @@ export type SessionMessageUser = { export type SessionInboxUserPayload = { text: string - displayText?: string + command?: PromptCommandInvocation files?: Array agents?: Array skills?: Array @@ -1702,7 +1704,7 @@ export type SessionInboxUserPayload = { export type SessionInboxUserPayload1 = { text: string - displayText?: string + command?: PromptCommandInvocation files?: Array agents?: Array skills?: Array @@ -2555,7 +2557,7 @@ export type SessionImportInput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } readonly text: string - readonly displayText?: string + readonly command?: { readonly name: string; readonly arguments: string } readonly files?: ReadonlyArray<{ readonly data: string readonly mime: string @@ -2825,7 +2827,7 @@ export type SessionImportInput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } readonly text: string - readonly displayText?: string + readonly command?: { readonly name: string; readonly arguments: string } readonly files?: ReadonlyArray<{ readonly data: string readonly mime: string @@ -3095,7 +3097,7 @@ export type SessionImportInput = { readonly metadata?: { readonly [x: string]: JsonValue } readonly time: { readonly created: number } readonly text: string - readonly displayText?: string + readonly command?: { readonly name: string; readonly arguments: string } readonly files?: ReadonlyArray<{ readonly data: string readonly mime: string @@ -3371,7 +3373,6 @@ export type SessionPromptInput = { readonly id?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3393,7 +3394,6 @@ export type SessionPromptInput = { readonly text: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3412,32 +3412,9 @@ export type SessionPromptInput = { readonly delivery?: ("steer" | "queue") | null readonly resume?: boolean | null }["text"] - readonly displayText?: { - readonly id?: string | null - readonly text: string - readonly displayText?: string - readonly files?: ReadonlyArray<{ - readonly uri: string - readonly name?: string - readonly description?: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly agents?: ReadonlyArray<{ - readonly name: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly skills?: ReadonlyArray<{ - readonly id: string - readonly mention?: { readonly start: number; readonly end: number; readonly text: string } - }> - readonly metadata?: { readonly [x: string]: JsonValue } - readonly delivery?: ("steer" | "queue") | null - readonly resume?: boolean | null - }["displayText"] readonly files?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3459,7 +3436,6 @@ export type SessionPromptInput = { readonly agents?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3481,7 +3457,6 @@ export type SessionPromptInput = { readonly skills?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3503,7 +3478,6 @@ export type SessionPromptInput = { readonly metadata?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3525,7 +3499,6 @@ export type SessionPromptInput = { readonly delivery?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string @@ -3547,7 +3520,6 @@ export type SessionPromptInput = { readonly resume?: { readonly id?: string | null readonly text: string - readonly displayText?: string readonly files?: ReadonlyArray<{ readonly uri: string readonly name?: string diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4e8e56b8a79..a963d415219 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -222,7 +222,7 @@ export interface Interface { id?: SessionMessage.ID sessionID: SessionSchema.ID text: string - displayText?: string + command?: Prompt["command"] files?: PromptInput.Prompt["files"] agents?: PromptInput.Prompt["agents"] skills?: PromptInput.Prompt["skills"] @@ -654,7 +654,7 @@ const layer = Layer.effect( id: input.id, sessionID: input.sessionID, text: evaluated.text, - displayText: `/${input.command}${input.arguments ? ` ${input.arguments}` : ""}`, + command: { name: input.command, arguments: input.arguments ?? "" }, files: input.files, agents: input.agents, skills: input.skills, @@ -962,7 +962,7 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf } const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* ( - input: PromptInput.Prompt, + input: PromptInput.Prompt & Pick, image: Effect.Effect, skills: Effect.Effect, ) { @@ -985,9 +985,9 @@ const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* ( }) }) }) - return Prompt.make({ + return Prompt.fromUserMessage({ text: input.text, - displayText: input.displayText === input.text ? undefined : input.displayText, + command: input.command, agents: input.agents, files, skills: selected?.length ? selected : undefined, diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 7c3be1dc9a2..4ac606f451d 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -325,7 +325,7 @@ describe("SessionProjector", () => { sessionID, item: { type: "user", - payload: { text: "expanded command template", displayText: "/command input" }, + payload: { text: "expanded command template", command: { name: "command", arguments: "input" } }, delivery: "steer", }, }) @@ -345,7 +345,10 @@ describe("SessionProjector", () => { session_id: sessionID, type: "user", seq: event.durable?.seq, - data: { text: "expanded command template", displayText: "/command input" }, + data: { + text: "expanded command template", + command: { name: "command", arguments: "input" }, + }, }) }), ) diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index 0ba3981e6f3..1e2ff06bd7c 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/packages/core/test/session-prompt.test.ts @@ -235,18 +235,18 @@ describe("Session.prompt", () => { const message = yield* session.prompt({ sessionID, text: "Fix the failing tests", - displayText: "/fix tests", + command: { name: "fix", arguments: "tests" }, resume: false, }) expect(message.payload.text).toBe("Fix the failing tests") - expect(message.payload.displayText).toBe("/fix tests") + expect(message.payload.command).toEqual({ name: "fix", arguments: "tests" }) expect(yield* session.messages({ sessionID })).toEqual([]) expect(yield* admitted(message.id)).toMatchObject({ id: message.id, sessionID, type: "user", - payload: { text: "Fix the failing tests", displayText: "/fix tests" }, + payload: { text: "Fix the failing tests", command: { name: "fix", arguments: "tests" } }, delivery: "steer", }) }), diff --git a/packages/schema/src/prompt-input.ts b/packages/schema/src/prompt-input.ts index 99943bba475..c25adbf2dae 100644 --- a/packages/schema/src/prompt-input.ts +++ b/packages/schema/src/prompt-input.ts @@ -28,7 +28,6 @@ export const SkillAttachment = Schema.Struct({ export const Prompt = Schema.Struct({ text: Schema.String, - displayText: Schema.String.pipe(optional), files: Schema.Array(FileAttachment).pipe(optional), agents: Schema.Array(AgentAttachment).pipe(optional), skills: Schema.Array(SkillAttachment).pipe(optional), diff --git a/packages/schema/src/prompt.ts b/packages/schema/src/prompt.ts index e547b9fd3d0..3d8e216cf2e 100644 --- a/packages/schema/src/prompt.ts +++ b/packages/schema/src/prompt.ts @@ -61,10 +61,16 @@ export const SkillAttachment = Schema.Struct({ mention: PromptMention.pipe(optional), }).annotate({ identifier: "Prompt.SkillAttachment" }) +export interface CommandInvocation extends Schema.Schema.Type {} +export const CommandInvocation = Schema.Struct({ + name: Schema.String, + arguments: Schema.String, +}).annotate({ identifier: "Prompt.CommandInvocation" }) + export interface Prompt extends Schema.Schema.Type {} export const Prompt = Schema.Struct({ text: Schema.String, - displayText: Schema.String.pipe(optional), + command: CommandInvocation.pipe(optional), files: Schema.Array(FileAttachment).pipe(optional), agents: Schema.Array(AgentAttachment).pipe(optional), skills: Schema.Array(SkillAttachment).pipe(optional), @@ -73,10 +79,10 @@ export const Prompt = Schema.Struct({ .pipe( statics((schema) => ({ equivalence: Schema.toEquivalence(schema), - fromUserMessage: (input: Pick) => + fromUserMessage: (input: Pick) => schema.make({ text: input.text, - ...(input.displayText === undefined ? {} : { displayText: input.displayText }), + ...(input.command === undefined ? {} : { command: input.command }), ...(input.files === undefined ? {} : { files: input.files }), ...(input.agents === undefined ? {} : { agents: input.agents }), ...(input.skills === undefined ? {} : { skills: input.skills }), diff --git a/packages/schema/src/session-message.ts b/packages/schema/src/session-message.ts index a3cc40734b8..a70c12e9567 100644 --- a/packages/schema/src/session-message.ts +++ b/packages/schema/src/session-message.ts @@ -72,11 +72,7 @@ export const LocationSwitched = Schema.Struct({ export interface User extends Schema.Schema.Type {} export const User = Schema.Struct({ ...Base, - text: Prompt.fields.text, - displayText: Prompt.fields.displayText, - files: Prompt.fields.files, - agents: Prompt.fields.agents, - skills: Prompt.fields.skills, + ...Prompt.fields, type: Schema.tag("user"), }).annotate({ identifier: "Session.Message.User" }) diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index d4038854f35..e5e71eef312 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -328,7 +328,6 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl sessionID: ctx.params.sessionID, id: ctx.payload.id, text: ctx.payload.text, - displayText: ctx.payload.displayText, files: ctx.payload.files, agents: ctx.payload.agents, skills: ctx.payload.skills, diff --git a/packages/tui/src/mini/command.shared.ts b/packages/tui/src/mini/command.shared.ts new file mode 100644 index 00000000000..2c7f2f9cadf --- /dev/null +++ b/packages/tui/src/mini/command.shared.ts @@ -0,0 +1,13 @@ +import type { StreamCommit } from "./types" +import { commandText } from "../util/command" + +export function commandCommit(messageID: string | undefined, command: { name: string; arguments: string }): StreamCommit { + return { + kind: "system", + source: "system", + messageID, + partID: "command", + text: `→ Command "${commandText(command)}"`, + phase: "start", + } +} diff --git a/packages/tui/src/mini/runtime.queue.ts b/packages/tui/src/mini/runtime.queue.ts index 31e1f8c0c55..b1fbcb5c3e7 100644 --- a/packages/tui/src/mini/runtime.queue.ts +++ b/packages/tui/src/mini/runtime.queue.ts @@ -12,6 +12,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import { Locale } from "../util/locale" import { isCompactCommand, isExitCommand, isNewCommand } from "./prompt.shared" import type { FooterApi, FooterEvent, RunDelivery, RunPrompt } from "./types" +import { commandCommit } from "./command.shared" type Trace = { write(type: string, data?: unknown): void @@ -173,13 +174,16 @@ export async function runPromptQueue(input: QueueInput): Promise { } if (sent.mode !== "shell") { - const commit = { - kind: "user", - text: sent.text, - phase: "start", - source: "system", - messageID: sent.messageID, - } as const + const commit = + sent.command && sent.command.source !== "skill" + ? commandCommit(sent.messageID, sent.command) + : ({ + kind: "user", + text: sent.text, + phase: "start", + source: "system", + messageID: sent.messageID, + } as const) input.trace?.write("ui.commit", commit) input.footer.append(commit) } diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index 0a4dcf31c2f..0745f9979b2 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -21,6 +21,7 @@ import { resolveSessionInfo, } from "./runtime.boot" import { createRuntimeLifecycle } from "./runtime.lifecycle" +import { commandCommit } from "./command.shared" import { cycleVariant, formatModelLabel, resolveVariant } from "./variant.shared" import type { LocalReplayRow, @@ -903,13 +904,17 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep state.shown = true state.history.push({ ...prompt, delivery: undefined }) if (prompt.mode !== "shell" && delivery === "steer") { - rememberLocal({ - kind: "user", - text: prompt.text, - phase: "start", - source: "system", - messageID: prompt.messageID, - }) + rememberLocal( + prompt.command && prompt.command.source !== "skill" + ? commandCommit(prompt.messageID, prompt.command) + : { + kind: "user", + text: prompt.text, + phase: "start", + source: "system", + messageID: prompt.messageID, + }, + ) } }, admit: async (prompt, delivery, signal) => { @@ -1044,9 +1049,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep admitted, ) if (prompt.messageID) { - state.localRows = state.localRows.filter( - (row) => row.commit.kind !== "user" || row.commit.messageID !== prompt.messageID, - ) + state.localRows = state.localRows.filter((row) => row.commit.messageID !== prompt.messageID) } // Shell and skill turns never send CLI file attachments; keep them // pending for the next prompt-shaped turn. diff --git a/packages/tui/src/mini/session.shared.ts b/packages/tui/src/mini/session.shared.ts index e53eb4627a9..229aaf23a2e 100644 --- a/packages/tui/src/mini/session.shared.ts +++ b/packages/tui/src/mini/session.shared.ts @@ -1,6 +1,7 @@ import type { SessionMessageInfo, SessionMessageUser } from "@opencode-ai/client/promise" import { promptCopy, promptSame } from "./prompt.shared" import type { RunInput, RunPrompt } from "./types" +import { commandText } from "../util/command" const LIMIT = 200 @@ -22,7 +23,7 @@ export type RunSession = { function messagePrompt(message: SessionMessageUser): RunPrompt { return { - text: message.displayText ?? message.text, + text: message.command ? commandText(message.command) : message.text, parts: [ ...(message.files ?? []).map((file) => ({ type: "file" as const, diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 52fae27d600..30ad0a7004e 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -17,6 +17,8 @@ import { createFragmentReconciler, fragmentRef, type FragmentReconciler } from " import { createSubagentTracker, toolCommit, toolFinalPhase } from "./stream-v2.subagent" import { normalizeTool, toolOutputText } from "./tool" import { toolDisplayContent } from "../util/tool-display" +import { commandCommit } from "./command.shared" +import { commandText } from "../util/command" import type { FooterApi, FooterView, @@ -186,7 +188,12 @@ function pendingPrompt(item: SessionInboxInfo): FooterQueuedPrompt | undefined { if (item.type !== "user") return undefined return { messageID: item.id, - prompt: { messageID: item.id, text: item.payload.displayText ?? item.payload.text, parts: [] }, + prompt: { + messageID: item.id, + text: item.payload.command ? commandText(item.payload.command) : item.payload.text, + parts: [], + command: item.payload.command, + }, delivery: item.delivery, } } @@ -655,12 +662,19 @@ export async function createSessionTransport(input: StreamInput): Promise skillCommit(message.id, skill.name, skill.id)), + ]) + return + } write([ ...(message.skills ?? []).map((skill) => skillCommit(message.id, skill.name, skill.id)), { kind: "user", source: "system", - text: message.displayText ?? message.text, + text: message.text, phase: "start", messageID: message.id, }, @@ -953,15 +967,19 @@ export async function createSessionTransport(input: StreamInput): Promise new Map(pendingUsers().map((item) => [item.id, item.delivery]))) const queuedPrompts = createMemo(() => - pendingUsers().flatMap((item) => (item.delivery === "queue" ? [{ id: item.id, text: item.payload.text }] : [])), + pendingUsers().flatMap((item) => + item.delivery === "queue" + ? [{ id: item.id, text: item.payload.command ? commandText(item.payload.command) : item.payload.text }] + : [], + ), ) const [composer, setComposer] = createStore({ open: false, @@ -2178,7 +2183,29 @@ function UserMessage(props: { message: SessionMessageUser }) { backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default} flexShrink={0} > - {props.message.displayText ?? props.message.text} + + {props.message.text} + + + {(command) => ( + + + + {" command "} + + + {` ${commandText(command())} `} + + + + )} + @@ -3612,7 +3639,10 @@ function recordValue(value: unknown): Record | undefined { function formatSessionTranscript(session: SessionInfo, messages: SessionMessageInfo[], thinking: boolean) { const body = messages.flatMap((message) => { - if (message.type === "user") return [`## User\n\n${message.displayText ?? message.text}`] + if (message.type === "user") + return [ + `## User\n\n${message.command ? commandText(message.command) : message.text}`, + ] if (message.type === "shell") return [`## Shell\n\n\`\`\`\n$ ${message.command}\n${message.output?.output ?? ""}\n\`\`\``] if (message.type !== "assistant") return [] diff --git a/packages/tui/src/util/command.ts b/packages/tui/src/util/command.ts new file mode 100644 index 00000000000..4dc9f799b07 --- /dev/null +++ b/packages/tui/src/util/command.ts @@ -0,0 +1,3 @@ +export function commandText(command: { name: string; arguments: string }) { + return `/${command.name}${command.arguments ? ` ${command.arguments}` : ""}` +} diff --git a/packages/tui/test/mini/session.shared.test.ts b/packages/tui/test/mini/session.shared.test.ts index 287c7d9931e..6c1e3efdb02 100644 --- a/packages/tui/test/mini/session.shared.test.ts +++ b/packages/tui/test/mini/session.shared.test.ts @@ -105,7 +105,9 @@ describe("run session shared", () => { test("uses presentation text for command history", () => { const out = createSession([ - userMessage("msg-user-1", "expanded command template", { displayText: "/command input" }), + userMessage("msg-user-1", "expanded command template", { + command: { name: "command", arguments: "input" }, + }), ]) expect(out.turns[0]?.prompt.text).toBe("/command input") diff --git a/packages/tui/test/mini/stream-v2.transport.test.ts b/packages/tui/test/mini/stream-v2.transport.test.ts index 3f7dc1eee4d..fad0d42f0ee 100644 --- a/packages/tui/test/mini/stream-v2.transport.test.ts +++ b/packages/tui/test/mini/stream-v2.transport.test.ts @@ -667,7 +667,10 @@ describe("V2 mini transport", () => { sessionID: "ses_1", timeCreated: 1, type: "user", - payload: { text: "expanded command template", displayText: "/command input" }, + payload: { + text: "expanded command template", + command: { name: "command", arguments: "input" }, + }, delivery: "queue", }, { @@ -707,7 +710,7 @@ describe("V2 mini transport", () => { while (!ui.commits.some((item) => item.messageID === "msg_queued")) await Bun.sleep(0) expect(ui.commits).toContainEqual( - expect.objectContaining({ kind: "user", messageID: "msg_queued", text: "/command input" }), + expect.objectContaining({ kind: "system", messageID: "msg_queued", text: '→ Command "/command input"' }), ) expect(pending()).toEqual([["msg_cancelled", "queue"]]) events.push({