Compare commits

..
Author SHA1 Message Date
rekram1-node 2f60a0b2a2 fix(tui): apply cursor config in mini 2026-08-19 21:47:19 +00:00
27 changed files with 102 additions and 226 deletions
-12
View File
@@ -105,16 +105,4 @@ describe("extractPromptFromMessage", () => {
expect(extractPromptFromMessage(message)[0]).toMatchObject({ type: "text", content: "model text" })
})
test("restores command invocation text", () => {
const message = {
id: "msg_1",
type: "user",
text: "expanded command template",
command: { name: "command", arguments: "input" },
time: { created: 1 },
} satisfies SessionMessageUser
expect(extractPromptFromMessage(message)[0]).toMatchObject({ type: "text", content: "/command input" })
})
})
+1 -3
View File
@@ -44,9 +44,7 @@ export function extractPromptFromMessage(
message: SessionMessageUser,
opts?: { directory?: string; attachmentName?: string },
): Prompt {
const text = message.command
? `/${message.command.name}${message.command.arguments ? ` ${message.command.arguments}` : ""}`
: (readPromptPresentation(message.metadata)?.displayText ?? message.text)
const text = readPromptPresentation(message.metadata)?.displayText ?? message.text
const directory = opts?.directory
const attachmentName = opts?.attachmentName ?? "attachment"
const toRelative = (path: string) => {
@@ -50,18 +50,6 @@ describe("session message presentation", () => {
})
})
test("projects command invocation text", () => {
const message = {
id: "msg_user",
type: "user",
text: "expanded command template",
command: { name: "command", arguments: "input" },
time: { created: 1 },
} satisfies SessionMessageUser
expect(presentUserParts("ses_1", message)[0]).toMatchObject({ type: "text", text: "/command input" })
})
test("projects current assistant content for existing DOM tools", () => {
const message = {
id: "msg_assistant",
+1 -3
View File
@@ -57,9 +57,7 @@ export function presentUserMessage(
export function presentUserParts(sessionID: string, message: SessionMessageUser): Part[] {
const presentation = readPromptPresentation(message.metadata)
const text = message.command
? `/${message.command.name}${message.command.arguments ? ` ${message.command.arguments}` : ""}`
: (presentation?.displayText ?? message.text)
const text = presentation?.displayText ?? message.text
return [
...(text ? [textPart(sessionID, message.id, 0, text)] : []),
...(message.files ?? []).map(
@@ -30,8 +30,6 @@ 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 }
@@ -1686,7 +1684,6 @@ export type SessionMessageUser = {
metadata?: { [x: string]: JsonValue }
time: { created: number }
text: string
command?: PromptCommandInvocation
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
@@ -1695,7 +1692,6 @@ export type SessionMessageUser = {
export type SessionInboxUserPayload = {
text: string
command?: PromptCommandInvocation
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
@@ -1704,7 +1700,6 @@ export type SessionInboxUserPayload = {
export type SessionInboxUserPayload1 = {
text: string
command?: PromptCommandInvocation
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
@@ -2557,7 +2552,6 @@ export type SessionImportInput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly text: string
readonly command?: { readonly name: string; readonly arguments: string }
readonly files?: ReadonlyArray<{
readonly data: string
readonly mime: string
@@ -2827,7 +2821,6 @@ export type SessionImportInput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly text: string
readonly command?: { readonly name: string; readonly arguments: string }
readonly files?: ReadonlyArray<{
readonly data: string
readonly mime: string
@@ -3097,7 +3090,6 @@ export type SessionImportInput = {
readonly metadata?: { readonly [x: string]: JsonValue }
readonly time: { readonly created: number }
readonly text: string
readonly command?: { readonly name: string; readonly arguments: string }
readonly files?: ReadonlyArray<{
readonly data: string
readonly mime: string
+7 -11
View File
@@ -222,7 +222,6 @@ export interface Interface {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
text: string
command?: Prompt["command"]
files?: PromptInput.Prompt["files"]
agents?: PromptInput.Prompt["agents"]
skills?: PromptInput.Prompt["skills"]
@@ -587,7 +586,11 @@ const layer = Layer.effect(
return yield* Image.Service
}).pipe(Effect.provide(locations.get(session.location)))
const skills = Skill.Service.pipe(Effect.provide(locations.get(session.location)))
const prompt = yield* resolvePrompt(input, image, skills).pipe(Effect.provideService(FSUtil.Service, fs))
const prompt = yield* resolvePrompt(
{ text: input.text, files: input.files, agents: input.agents, skills: input.skills },
image,
skills,
).pipe(Effect.provideService(FSUtil.Service, fs))
const messageID = input.id ?? SessionMessage.ID.create()
const admittedInput = SessionInbox.Item.make({
type: "user",
@@ -654,7 +657,6 @@ const layer = Layer.effect(
id: input.id,
sessionID: input.sessionID,
text: evaluated.text,
command: { name: input.command, arguments: input.arguments ?? "" },
files: input.files,
agents: input.agents,
skills: input.skills,
@@ -962,7 +964,7 @@ function synthesizeTerminalShellInfo(started: ShellSchema.Info): ShellSchema.Inf
}
const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* (
input: PromptInput.Prompt & Pick<Prompt, "command">,
input: PromptInput.Prompt,
image: Effect.Effect<Image.Interface>,
skills: Effect.Effect<Skill.Interface>,
) {
@@ -985,13 +987,7 @@ const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* (
})
})
})
return Prompt.fromUserMessage({
text: input.text,
command: input.command,
agents: input.agents,
files,
skills: selected?.length ? selected : undefined,
})
return Prompt.make({ text: input.text, agents: input.agents, files, skills: selected?.length ? selected : undefined })
})
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024
+11 -9
View File
@@ -20,7 +20,6 @@ import { FSUtil } from "@opencode-ai/util/fs-util"
import { Money } from "@opencode-ai/schema/money"
import { Worktree } from "@opencode-ai/schema/worktree"
import { Project } from "@opencode-ai/schema/project"
import { Prompt } from "@opencode-ai/schema/prompt"
import { AbsolutePath, RelativePath } from "../schema.js"
import type { SessionSchema } from "./schema.js"
@@ -527,14 +526,17 @@ const layer = Layer.effectDiscard(
yield* insertMessage(
db,
event,
input.type === "user"
? {
...Prompt.fromUserMessage(input.payload),
id: input.id,
type: "user",
metadata: input.payload.metadata,
time: { created: DateTime.makeUnsafe(event.created) },
}
input.type === "user"
? {
id: input.id,
type: "user",
metadata: input.payload.metadata,
text: input.payload.text,
files: input.payload.files,
agents: input.payload.agents,
skills: input.payload.skills,
time: { created: DateTime.makeUnsafe(event.created) },
}
: {
id: input.id,
type: "synthetic",
+2 -14
View File
@@ -323,11 +323,7 @@ describe("SessionProjector", () => {
const admitted = yield* SessionInbox.admit(db, bus, {
id,
sessionID,
item: {
type: "user",
payload: { text: "expanded command template", command: { name: "command", arguments: "input" } },
delivery: "steer",
},
item: { type: "user", payload: { text: "promote me" }, delivery: "steer" },
})
if (!admitted) return yield* Effect.die("Prompt admission failed")
@@ -341,15 +337,7 @@ describe("SessionProjector", () => {
).toBeUndefined()
expect(
yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, id)).get().pipe(Effect.orDie),
).toMatchObject({
session_id: sessionID,
type: "user",
seq: event.durable?.seq,
data: {
text: "expanded command template",
command: { name: "command", arguments: "input" },
},
})
).toMatchObject({ session_id: sessionID, type: "user", seq: event.durable?.seq })
}),
)
+1 -3
View File
@@ -235,18 +235,16 @@ describe("Session.prompt", () => {
const message = yield* session.prompt({
sessionID,
text: "Fix the failing tests",
command: { name: "fix", arguments: "tests" },
resume: false,
})
expect(message.payload.text).toBe("Fix the failing 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", command: { name: "fix", arguments: "tests" } },
payload: { text: "Fix the failing tests" },
delivery: "steer",
})
}),
+1 -9
View File
@@ -61,16 +61,9 @@ 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,
command: CommandInvocation.pipe(optional),
files: Schema.Array(FileAttachment).pipe(optional),
agents: Schema.Array(AgentAttachment).pipe(optional),
skills: Schema.Array(SkillAttachment).pipe(optional),
@@ -79,10 +72,9 @@ export const Prompt = Schema.Struct({
.pipe(
statics((schema) => ({
equivalence: Schema.toEquivalence(schema),
fromUserMessage: (input: Pick<Prompt, "text" | "command" | "files" | "agents" | "skills">) =>
fromUserMessage: (input: Pick<Prompt, "text" | "files" | "agents" | "skills">) =>
schema.make({
text: input.text,
...(input.command === undefined ? {} : { command: input.command }),
...(input.files === undefined ? {} : { files: input.files }),
...(input.agents === undefined ? {} : { agents: input.agents }),
...(input.skills === undefined ? {} : { skills: input.skills }),
+4 -1
View File
@@ -72,7 +72,10 @@ export const LocationSwitched = Schema.Struct({
export interface User extends Schema.Schema.Type<typeof User> {}
export const User = Schema.Struct({
...Base,
...Prompt.fields,
text: Prompt.fields.text,
files: Prompt.fields.files,
agents: Prompt.fields.agents,
skills: Prompt.fields.skills,
type: Schema.tag("user"),
}).annotate({ identifier: "Session.Message.User" })
-13
View File
@@ -1,13 +0,0 @@
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",
}
}
+3 -1
View File
@@ -33,11 +33,12 @@ import {
} from "./form.shared"
import type { FormBodyState } from "./form.shared"
import type { RunFooterTheme } from "./theme"
import type { FormCancel, FormReply, MiniFormRequest } from "./types"
import type { FormCancel, FormReply, MiniFormRequest, RunTuiConfig } from "./types"
export function RunFormBody(props: {
request: MiniFormRequest
theme: RunFooterTheme
cursor?: RunTuiConfig["cursor"]
onReply: (input: FormReply) => void | Promise<void>
onCancel: (input: FormCancel) => void | Promise<void>
openExternal?: (url: string) => Promise<unknown>
@@ -320,6 +321,7 @@ export function RunFormBody(props: {
backgroundColor={props.theme.surface}
focusedBackgroundColor={props.theme.surface}
cursorColor={props.theme.text}
cursorStyle={props.cursor}
focused
onSubmit={commitInput}
onContentChange={() => {
+5 -1
View File
@@ -31,7 +31,7 @@ import {
import { footerWidthPolicy } from "./footer.width"
import { toolFiletype } from "./tool"
import { transparent, type RunBlockTheme, type RunFooterTheme } from "./theme"
import type { MiniPermissionRequest, PermissionReply } from "./types"
import type { MiniPermissionRequest, PermissionReply, RunTuiConfig } from "./types"
import { PatchDiff } from "../component/patch-diff"
function buttons(
@@ -74,6 +74,7 @@ function buttons(
/** @internal Exported to test managed textarea submission without permission navigation. */
export function RejectField(props: {
theme: RunFooterTheme
cursor?: RunTuiConfig["cursor"]
text: string
disabled: boolean
onChange: (text: string) => void
@@ -113,6 +114,7 @@ export function RejectField(props: {
backgroundColor={props.theme.surface}
focusedBackgroundColor={props.theme.surface}
cursorColor={props.theme.text}
cursorStyle={props.cursor}
focused={!props.disabled}
onSubmit={props.onConfirm}
onContentChange={() => {
@@ -139,6 +141,7 @@ export function RunPermissionBody(props: {
request: MiniPermissionRequest
directory?: () => string
theme: RunFooterTheme
cursor?: RunTuiConfig["cursor"]
block: RunBlockTheme
onReply: (input: PermissionReply) => void | Promise<void>
mono?: boolean
@@ -327,6 +330,7 @@ export function RunPermissionBody(props: {
<box width={narrow() ? "100%" : undefined} flexGrow={1} flexShrink={1}>
<RejectField
theme={props.theme}
cursor={props.cursor}
text={state().message}
disabled={busy()}
onChange={(text) => {
+3
View File
@@ -42,6 +42,7 @@ import type {
RunPrompt,
RunPromptPart,
RunReference,
RunTuiConfig,
} from "./types"
const AUTOCOMPLETE_ROWS = FOOTER_MENU_ROWS
@@ -175,6 +176,7 @@ export function selectedCommand(text: string, command: RunPrompt["command"]) {
export function RunPromptBody(props: {
theme: () => RunFooterTheme
cursor?: RunTuiConfig["cursor"]
background: () => ColorInput
placeholder: () => StyledText | string
onSubmit: () => void
@@ -239,6 +241,7 @@ export function RunPromptBody(props: {
backgroundColor={props.background()}
focusedBackgroundColor={props.background()}
cursorColor={props.theme().text}
cursorStyle={props.cursor}
onSubmit={props.onSubmit}
onKeyDown={props.onKeyDown}
onPaste={() => {
+1
View File
@@ -335,6 +335,7 @@ export class RunFooter implements FooterApi {
variants: footer.variants,
currentVariant: footer.currentVariant,
theme: footer.theme,
cursor: options.tuiConfig.cursor,
mono: options.mono,
miniSettings: footer.miniSettings,
history: footer.history,
+5
View File
@@ -55,6 +55,7 @@ import type {
RunPrompt,
RunProvider,
RunReference,
RunTuiConfig,
} from "./types"
import type { RunTheme } from "./theme"
@@ -92,6 +93,7 @@ type RunFooterViewProps = {
subagent?: () => FooterSubagentState
queuedPrompts?: () => FooterQueuedPrompt[]
theme: () => RunTheme
cursor?: RunTuiConfig["cursor"]
mono: boolean
miniSettings: () => MiniSettings
history?: () => RunPrompt[]
@@ -733,6 +735,7 @@ export function RunFooterView(props: RunFooterViewProps) {
<Match when={active().type === "prompt" && route().type === "composer"}>
<RunPromptBody
theme={theme}
cursor={props.cursor}
background={() => runTheme().background}
placeholder={composer.placeholder}
onSubmit={composer.onSubmit}
@@ -882,6 +885,7 @@ export function RunFooterView(props: RunFooterViewProps) {
request={permission()!.request}
directory={props.directory}
theme={theme()}
cursor={props.cursor}
block={block()}
onReply={props.onPermissionReply}
mono={props.mono}
@@ -893,6 +897,7 @@ export function RunFooterView(props: RunFooterViewProps) {
<RunFormBody
request={value.request}
theme={theme()}
cursor={props.cursor}
state={formStates.get(value.request.id) ?? createFormBodyState(value.request)}
onState={(state) => {
if (!formsAbsent && !settledForms.has(state.formID))
+7 -11
View File
@@ -12,7 +12,6 @@ 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
@@ -174,16 +173,13 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
}
if (sent.mode !== "shell") {
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)
const commit = {
kind: "user",
text: sent.text,
phase: "start",
source: "system",
messageID: sent.messageID,
} as const
input.trace?.write("ui.commit", commit)
input.footer.append(commit)
}
+10 -13
View File
@@ -21,7 +21,6 @@ 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,
@@ -904,17 +903,13 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
state.shown = true
state.history.push({ ...prompt, delivery: undefined })
if (prompt.mode !== "shell" && delivery === "steer") {
rememberLocal(
prompt.command && prompt.command.source !== "skill"
? commandCommit(prompt.messageID, prompt.command)
: {
kind: "user",
text: prompt.text,
phase: "start",
source: "system",
messageID: prompt.messageID,
},
)
rememberLocal({
kind: "user",
text: prompt.text,
phase: "start",
source: "system",
messageID: prompt.messageID,
})
}
},
admit: async (prompt, delivery, signal) => {
@@ -1049,7 +1044,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
admitted,
)
if (prompt.messageID) {
state.localRows = state.localRows.filter((row) => row.commit.messageID !== prompt.messageID)
state.localRows = state.localRows.filter(
(row) => row.commit.kind !== "user" || row.commit.messageID !== prompt.messageID,
)
}
// Shell and skill turns never send CLI file attachments; keep them
// pending for the next prompt-shaped turn.
+1 -2
View File
@@ -1,7 +1,6 @@
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
@@ -23,7 +22,7 @@ export type RunSession = {
function messagePrompt(message: SessionMessageUser): RunPrompt {
return {
text: message.command ? commandText(message.command) : message.text,
text: message.text,
parts: [
...(message.files ?? []).map((file) => ({
type: "file" as const,
+20 -48
View File
@@ -17,8 +17,6 @@ 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,
@@ -188,12 +186,7 @@ function pendingPrompt(item: SessionInboxInfo): FooterQueuedPrompt | undefined {
if (item.type !== "user") return undefined
return {
messageID: item.id,
prompt: {
messageID: item.id,
text: item.payload.command ? commandText(item.payload.command) : item.payload.text,
parts: [],
command: item.payload.command,
},
prompt: { messageID: item.id, text: item.payload.text, parts: [] },
delivery: item.delivery,
}
}
@@ -662,22 +655,9 @@ 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.text,
phase: "start",
messageID: message.id,
},
{ kind: "user", source: "system", text: message.text, phase: "start", messageID: message.id },
])
return
}
@@ -967,19 +947,15 @@ 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(
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([
{
kind: "user",
source: "system",
text: pending.prompt.text,
phase: "start",
messageID: event.data.inboxID,
},
])
}
write([], { phase: "running", status: "waiting for assistant" })
return
@@ -992,19 +968,15 @@ 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(
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([
{
kind: "user",
source: "system",
text: pending.prompt.text,
phase: "start",
messageID: event.data.inboxID,
},
])
return
}
if (event.type === "session.inbox.cancelled") {
+1 -1
View File
@@ -392,7 +392,7 @@ export type FormCancel = {
location?: LocationRef
}
export type RunTuiConfig = Pick<Config.Resolved, "keybinds" | "leader" | "theme" | "mini" | "session">
export type RunTuiConfig = Pick<Config.Resolved, "keybinds" | "leader" | "theme" | "mini" | "session" | "cursor">
export type MiniSettings = {
thinking: "show" | "hide"
+3 -33
View File
@@ -100,7 +100,6 @@ 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"
@@ -206,11 +205,7 @@ 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.command ? commandText(item.payload.command) : item.payload.text }]
: [],
),
pendingUsers().flatMap((item) => (item.delivery === "queue" ? [{ id: item.id, text: item.payload.text }] : [])),
)
const [composer, setComposer] = createStore({
open: false,
@@ -2183,29 +2178,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
backgroundColor={hover() ? theme.raise(theme.background.default) : theme.background.default}
flexShrink={0}
>
<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>
<text fg={theme.text.default}>{props.message.text}</text>
<Show when={skills().length}>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={skills()}>
@@ -3639,10 +3612,7 @@ 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.command ? commandText(message.command) : message.text}`,
]
if (message.type === "user") return [`## User\n\n${message.text}`]
if (message.type === "shell")
return [`## Shell\n\n\`\`\`\n$ ${message.command}\n${message.output?.output ?? ""}\n\`\`\``]
if (message.type !== "assistant") return []
-3
View File
@@ -1,3 +0,0 @@
export function commandText(command: { name: string; arguments: string }) {
return `/${command.name}${command.arguments ? ` ${command.arguments}` : ""}`
}
@@ -169,6 +169,7 @@ async function renderFooter(
subagent={subagents}
queuedPrompts={() => input.queuedPrompts ?? []}
theme={input.theme ?? (() => RUN_THEME_FALLBACK)}
cursor={config.cursor}
mono={input.mono ?? false}
miniSettings={miniSettings}
onSubmit={input.onSubmit ?? (() => true)}
@@ -342,6 +343,18 @@ test("direct footer composer area does not adopt footer surface", async () => {
}
})
test("direct footer composer uses the configured cursor style", async () => {
const cursor = { style: "underline" as const, blinking: false }
const app = await renderFooter({ tuiConfig: { ...tuiConfig, cursor } })
try {
await app.renderOnce()
expect(app.renderer.currentFocusedEditor?.cursorStyle).toEqual(cursor)
} finally {
app.cleanup()
}
})
test("run entry content updates when live commit text changes", async () => {
const [commit, setCommit] = createSignal<StreamCommit>({
kind: "tool",
@@ -103,16 +103,6 @@ describe("run session shared", () => {
})
})
test("uses presentation text for command history", () => {
const out = createSession([
userMessage("msg-user-1", "expanded command template", {
command: { name: "command", arguments: "input" },
}),
])
expect(out.turns[0]?.prompt.text).toBe("/command input")
})
test("dedupes consecutive history entries, drops blanks, and copies prompt parts", () => {
const parts = [
{
@@ -667,10 +667,7 @@ describe("V2 mini transport", () => {
sessionID: "ses_1",
timeCreated: 1,
type: "user",
payload: {
text: "expanded command template",
command: { name: "command", arguments: "input" },
},
payload: { text: "follow up" },
delivery: "queue",
},
{
@@ -710,7 +707,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: "system", messageID: "msg_queued", text: '→ Command "/command input"' }),
expect.objectContaining({ kind: "user", messageID: "msg_queued", text: "follow up" }),
)
expect(pending()).toEqual([["msg_cancelled", "queue"]])
events.push({