mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
fix(tui): render commands as attachments
This commit is contained in:
@@ -408,6 +408,7 @@ describe("prompt submit worktree selection", () => {
|
||||
files: [],
|
||||
agents: [],
|
||||
metadata: {
|
||||
displayText: "ls",
|
||||
comments: [],
|
||||
agent: "agent",
|
||||
model: { providerID: "provider", modelID: "model", variant: "high" },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<PromptInput.FileAttachment> | undefined
|
||||
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
|
||||
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
skills?: Array<PromptSkillAttachment>
|
||||
@@ -1693,7 +1695,7 @@ export type SessionMessageUser = {
|
||||
|
||||
export type SessionInboxUserPayload = {
|
||||
text: string
|
||||
displayText?: string
|
||||
command?: PromptCommandInvocation
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
skills?: Array<PromptSkillAttachment>
|
||||
@@ -1702,7 +1704,7 @@ export type SessionInboxUserPayload = {
|
||||
|
||||
export type SessionInboxUserPayload1 = {
|
||||
text: string
|
||||
displayText?: string
|
||||
command?: PromptCommandInvocation
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
skills?: Array<PromptSkillAttachment>
|
||||
@@ -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
|
||||
|
||||
@@ -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<Prompt, "command">,
|
||||
image: Effect.Effect<Image.Interface>,
|
||||
skills: Effect.Effect<Skill.Interface>,
|
||||
) {
|
||||
@@ -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,
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -61,10 +61,16 @@ export const SkillAttachment = Schema.Struct({
|
||||
mention: PromptMention.pipe(optional),
|
||||
}).annotate({ identifier: "Prompt.SkillAttachment" })
|
||||
|
||||
export interface CommandInvocation extends Schema.Schema.Type<typeof CommandInvocation> {}
|
||||
export const CommandInvocation = Schema.Struct({
|
||||
name: Schema.String,
|
||||
arguments: Schema.String,
|
||||
}).annotate({ identifier: "Prompt.CommandInvocation" })
|
||||
|
||||
export interface Prompt extends Schema.Schema.Type<typeof Prompt> {}
|
||||
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<Prompt, "text" | "displayText" | "files" | "agents" | "skills">) =>
|
||||
fromUserMessage: (input: Pick<Prompt, "text" | "command" | "files" | "agents" | "skills">) =>
|
||||
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 }),
|
||||
|
||||
@@ -72,11 +72,7 @@ export const LocationSwitched = Schema.Struct({
|
||||
export interface User extends Schema.Schema.Type<typeof User> {}
|
||||
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" })
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Sessio
|
||||
state.messageIDs.add(message.id)
|
||||
if (!render) return
|
||||
if (reuseVisibleWait && waiting) return
|
||||
if (message.command) {
|
||||
write([
|
||||
commandCommit(message.id, message.command),
|
||||
...(message.skills ?? []).map((skill) => 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<Sessio
|
||||
const visible = state.messageIDs.has(event.data.inboxID)
|
||||
if (waiting || pending) state.messageIDs.add(event.data.inboxID)
|
||||
if (!waiting && pending && !visible) {
|
||||
write([
|
||||
{
|
||||
kind: "user",
|
||||
source: "system",
|
||||
text: pending.prompt.text,
|
||||
phase: "start",
|
||||
messageID: event.data.inboxID,
|
||||
},
|
||||
])
|
||||
write(
|
||||
pending.prompt.command
|
||||
? [commandCommit(event.data.inboxID, pending.prompt.command)]
|
||||
: [
|
||||
{
|
||||
kind: "user",
|
||||
source: "system",
|
||||
text: pending.prompt.text,
|
||||
phase: "start",
|
||||
messageID: event.data.inboxID,
|
||||
},
|
||||
],
|
||||
)
|
||||
}
|
||||
write([], { phase: "running", status: "waiting for assistant" })
|
||||
return
|
||||
@@ -974,15 +992,19 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||
if (event.data.delivery === "queue") return
|
||||
if (state.messageIDs.has(event.data.inboxID)) return
|
||||
state.messageIDs.add(event.data.inboxID)
|
||||
write([
|
||||
{
|
||||
kind: "user",
|
||||
source: "system",
|
||||
text: pending.prompt.text,
|
||||
phase: "start",
|
||||
messageID: event.data.inboxID,
|
||||
},
|
||||
])
|
||||
write(
|
||||
pending.prompt.command
|
||||
? [commandCommit(event.data.inboxID, pending.prompt.command)]
|
||||
: [
|
||||
{
|
||||
kind: "user",
|
||||
source: "system",
|
||||
text: pending.prompt.text,
|
||||
phase: "start",
|
||||
messageID: event.data.inboxID,
|
||||
},
|
||||
],
|
||||
)
|
||||
return
|
||||
}
|
||||
if (event.type === "session.inbox.cancelled") {
|
||||
|
||||
@@ -100,6 +100,7 @@ import {
|
||||
import { switchLabel } from "../../util/model"
|
||||
import { findMessageBoundary, messageNavigationSlack } from "./message-navigation"
|
||||
import { stringWidth } from "../../util/string-width"
|
||||
import { commandText } from "../../util/command"
|
||||
import { useArgs } from "../../context/args"
|
||||
import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback"
|
||||
import { useSessionTabs } from "../../context/session-tabs"
|
||||
@@ -205,7 +206,11 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
)
|
||||
const pendingDeliveries = createMemo(() => 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}
|
||||
>
|
||||
<text fg={theme.text.default}>{props.message.displayText ?? props.message.text}</text>
|
||||
<Show when={!props.message.command}>
|
||||
<text fg={theme.text.default}>{props.message.text}</text>
|
||||
</Show>
|
||||
<Show when={props.message.command}>
|
||||
{(command) => (
|
||||
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<text fg={theme.text.default}>
|
||||
<span
|
||||
style={{
|
||||
bg: theme.hue.accent[mode() === "light" ? 700 : 200],
|
||||
fg: theme.background.default,
|
||||
bold: true,
|
||||
}}
|
||||
>
|
||||
{" command "}
|
||||
</span>
|
||||
<span style={{ bg: theme.raise(theme.background.default), fg: theme.text.subdued }}>
|
||||
{` ${commandText(command())} `}
|
||||
</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={skills().length}>
|
||||
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={skills()}>
|
||||
@@ -3612,7 +3639,10 @@ function recordValue(value: unknown): Record<string, unknown> | 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 []
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function commandText(command: { name: string; arguments: string }) {
|
||||
return `/${command.name}${command.arguments ? ` ${command.arguments}` : ""}`
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user