chore: generate

This commit is contained in:
opencode-agent[bot]
2026-05-08 10:18:22 +00:00
parent 7f2b5ee8c2
commit ac7a885ae9
23 changed files with 283 additions and 282 deletions
+1 -8
View File
@@ -18,14 +18,7 @@ import path from "path"
import type { Event, ToolPart } from "@opencode-ai/sdk/v2" import type { Event, ToolPart } from "@opencode-ai/sdk/v2"
import { createSessionData, reduceSessionData, type SessionData } from "./session-data" import { createSessionData, reduceSessionData, type SessionData } from "./session-data"
import { writeSessionOutput } from "./stream" import { writeSessionOutput } from "./stream"
import type { import type { FooterApi, PermissionReply, QuestionReject, QuestionReply, RunPrompt, StreamCommit } from "./types"
FooterApi,
PermissionReply,
QuestionReject,
QuestionReply,
RunPrompt,
StreamCommit,
} from "./types"
const KINDS = [ const KINDS = [
"markdown", "markdown",
@@ -180,13 +180,7 @@ function PanelShell(props: {
children: JSX.Element children: JSX.Element
}) { }) {
return ( return (
<box <box id={props.id} width="100%" flexDirection="column" backgroundColor="transparent" flexShrink={0}>
id={props.id}
width="100%"
flexDirection="column"
backgroundColor="transparent"
flexShrink={0}
>
<box <box
width="100%" width="100%"
flexDirection="column" flexDirection="column"
@@ -333,14 +327,14 @@ export function RunCommandMenuBody(props: {
category: item.source === "mcp" ? "MCP Commands" : "Project Commands", category: item.source === "mcp" ? "MCP Commands" : "Project Commands",
name: item.name, name: item.name,
display: item.name, display: item.name,
footer: `/${item.name}`, footer: `/${item.name}`,
keywords: keywords:
item.source === "mcp" item.source === "mcp"
? `/${item.name} ${item.name} mcp ${item.description ?? ""}` ? `/${item.name} ${item.name} mcp ${item.description ?? ""}`
: `/${item.name} ${item.name} ${item.description ?? ""}`, : `/${item.name} ${item.name} ${item.description ?? ""}`,
}) satisfies CommandEntry, }) satisfies CommandEntry,
) )
.sort((a, b) => categoryRank(a.category) - categoryRank(b.category) || a.display.localeCompare(b.display)), .sort((a, b) => categoryRank(a.category) - categoryRank(b.category) || a.display.localeCompare(b.display)),
{ action: "exit", category: "System", display: "Exit", footer: "/exit", keywords: "/exit exit" }, { action: "exit", category: "System", display: "Exit", footer: "/exit", keywords: "/exit exit" },
] ]
}) })
@@ -161,10 +161,7 @@ export function RunFooterMenu(props: {
return return
} }
const dir = const dir = props.selected() === previous + 1 ? 1 : props.selected() === previous - 1 ? -1 : undefined
props.selected() === previous + 1 ? 1
: props.selected() === previous - 1 ? -1
: undefined
setGroupOffset((value) => setGroupOffset((value) =>
dir dir
? moveOffset(value, { count: all.length, limit: limit(), selected, dir }) ? moveOffset(value, { count: all.length, limit: limit(), selected, dir })
@@ -175,11 +172,14 @@ export function RunFooterMenu(props: {
const rows = createMemo<RunFooterMenuRow[]>(() => { const rows = createMemo<RunFooterMenuRow[]>(() => {
if (!props.grouped) { if (!props.grouped) {
return props.items().slice(props.offset(), props.offset() + limit()).map((item, index) => ({ return props
type: "item", .items()
item, .slice(props.offset(), props.offset() + limit())
index: index + props.offset(), .map((item, index) => ({
})) type: "item",
item,
index: index + props.offset(),
}))
} }
const all = groupedRows() const all = groupedRows()
@@ -187,7 +187,13 @@ export function RunFooterMenu(props: {
return all.slice(start, start + limit()) return all.slice(start, start + limit())
}) })
const descriptionColumn = createMemo(() => { const descriptionColumn = createMemo(() => {
const width = Math.max(0, ...props.items().filter((item) => item.description).map((item) => Bun.stringWidth(item.display))) const width = Math.max(
0,
...props
.items()
.filter((item) => item.description)
.map((item) => Bun.stringWidth(item.display)),
)
return width === 0 ? 0 : width + 2 return width === 0 ? 0 : width + 2
}) })
const descriptionPad = (item: RunFooterMenuItem) => { const descriptionPad = (item: RunFooterMenuItem) => {
@@ -264,7 +270,12 @@ export function RunFooterMenu(props: {
backgroundColor={active() ? props.theme().highlight : props.theme().surface} backgroundColor={active() ? props.theme().highlight : props.theme().surface}
> >
<box width="100%" flexDirection="row" justifyContent="space-between" gap={1}> <box width="100%" flexDirection="row" justifyContent="space-between" gap={1}>
<text fg={active() ? props.theme().surface : props.theme().text} wrapMode="none" truncate flexGrow={1}> <text
fg={active() ? props.theme().surface : props.theme().text}
wrapMode="none"
truncate
flexGrow={1}
>
{row.item.display} {row.item.display}
{row.item.description ? ( {row.item.description ? (
<span style={{ fg: active() ? props.theme().surface : props.theme().muted }}> <span style={{ fg: active() ? props.theme().surface : props.theme().muted }}>
@@ -274,7 +285,12 @@ export function RunFooterMenu(props: {
) : undefined} ) : undefined}
</text> </text>
{row.item.footer ? ( {row.item.footer ? (
<text fg={active() ? props.theme().surface : props.theme().muted} wrapMode="none" truncate flexShrink={0}> <text
fg={active() ? props.theme().surface : props.theme().muted}
wrapMode="none"
truncate
flexShrink={0}
>
{row.item.footer} {row.item.footer}
</text> </text>
) : undefined} ) : undefined}
@@ -36,10 +36,7 @@ function statusIcon(status: FooterSubagentTab["status"]) {
} }
function tabText(tab: FooterSubagentTab, slot: string, count: number, width: number) { function tabText(tab: FooterSubagentTab, slot: string, count: number, width: number) {
const perTab = Math.max( const perTab = Math.max(1, Math.floor((width - 4 - Math.max(0, count - 1) * 3) / Math.max(1, count)))
1,
Math.floor((width - 4 - Math.max(0, count - 1) * 3) / Math.max(1, count)),
)
if (count >= 8 || perTab < 12) { if (count >= 8 || perTab < 12) {
return `[${slot}]` return `[${slot}]`
} }
@@ -96,7 +93,9 @@ export function RunFooterSubagentTabs(props: {
flexDirection="row" flexDirection="row"
flexShrink={0} flexShrink={0}
> >
<box flexDirection="row" gap={3} flexShrink={1} flexGrow={1}>{items()}</box> <box flexDirection="row" gap={3} flexShrink={1} flexGrow={1}>
{items()}
</box>
</box> </box>
) )
} }
+3 -3
View File
@@ -567,9 +567,9 @@ export class RunFooter implements FooterApi {
? 1 + tabs + MODEL_ROWS ? 1 + tabs + MODEL_ROWS
: this.promptRoute.type === "variant" : this.promptRoute.type === "variant"
? 1 + tabs + VARIANT_ROWS ? 1 + tabs + VARIANT_ROWS
: this.promptRoute.type === "subagent" : this.promptRoute.type === "subagent"
? this.base + tabs + SUBAGENT_INSPECTOR_ROWS ? this.base + tabs + SUBAGENT_INSPECTOR_ROWS
: Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows)) : Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
if (height !== this.renderer.footerHeight) { if (height !== this.renderer.footerHeight) {
this.renderer.footerHeight = height this.renderer.footerHeight = height
+7 -9
View File
@@ -103,15 +103,13 @@ export function withRunSpan<A>(
attributes: attributes(input), attributes: attributes(input),
}) })
return context.with( return context.with(trace.setSpan(context.active(), span), () =>
trace.setSpan(context.active(), span), finish(
() => span,
finish( new Promise<A>((resolve) => {
span, resolve(fn(span))
new Promise<A>((resolve) => { }),
resolve(fn(span)) ),
}),
),
) )
}, },
() => fn(noop), () => fn(noop),
@@ -23,13 +23,7 @@ export type PromptHistoryState = {
draft: string draft: string
} }
export function promptInfo(event: { export function promptInfo(event: { name: string; ctrl?: boolean; meta?: boolean; shift?: boolean; super?: boolean }) {
name: string
ctrl?: boolean
meta?: boolean
shift?: boolean
super?: boolean
}) {
return { return {
name: event.name === " " ? "space" : event.name, name: event.name === " " ? "space" : event.name,
ctrl: !!event.ctrl, ctrl: !!event.ctrl,
@@ -123,7 +117,11 @@ export function promptBindings(bindings: FooterKeybinds["commandList"], leader:
}) })
} }
function mapInputBindings(bindings: FooterKeybinds["inputSubmit"], leader: string, action: "submit" | "newline"): KeyBinding[] { function mapInputBindings(
bindings: FooterKeybinds["inputSubmit"],
leader: string,
action: "submit" | "newline",
): KeyBinding[] {
return promptBindings(bindings, leader).flatMap((key) => { return promptBindings(bindings, leader).flatMap((key) => {
if (key.leader) { if (key.leader) {
return [] return []
@@ -248,7 +248,12 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
process.on("SIGINT", sigint) process.on("SIGINT", sigint)
let closed = false let closed = false
const close = async (next: { showExit: boolean; sessionTitle?: string; sessionID?: string; history?: RunPrompt[] }) => { const close = async (next: {
showExit: boolean
sessionTitle?: string
sessionID?: string
history?: RunPrompt[]
}) => {
if (closed) { if (closed) {
return return
} }
+4 -7
View File
@@ -39,10 +39,7 @@ type CreateSessionInput = {
variant: string | undefined variant: string | undefined
} }
type CreateSession = ( type CreateSession = (sdk: RunInput["sdk"], input: CreateSessionInput) => Promise<{ id: string; title?: string }>
sdk: RunInput["sdk"],
input: CreateSessionInput,
) => Promise<{ id: string; title?: string }>
type RunRuntimeInput = { type RunRuntimeInput = {
boot: () => Promise<BootContext> boot: () => Promise<BootContext>
@@ -548,9 +545,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
state.sessionTitle = created.sessionTitle state.sessionTitle = created.sessionTitle
state.agent = created.agent ?? state.agent state.agent = created.agent ?? state.agent
state.history = [] state.history = []
includeFiles = true includeFiles = true
state.demo = input.demo state.demo = input.demo
? createRunDemo({ ? createRunDemo({
footer, footer,
sessionID: state.sessionID, sessionID: state.sessionID,
thinking: input.thinking, thinking: input.thinking,
@@ -118,37 +118,37 @@ export class RunScrollbackStream {
const renderable = const renderable =
body.type === "text" body.type === "text"
? new TextRenderable(surface.renderContext, { ? new TextRenderable(surface.renderContext, {
id,
content: "",
width: "100%",
wrapMode: "word",
fg: style.fg,
attributes: style.attrs,
})
: body.type === "code"
? new CodeRenderable(surface.renderContext, {
id, id,
content: "", content: "",
filetype: body.filetype,
syntaxStyle: entrySyntax(commit, this.theme),
width: "100%", width: "100%",
wrapMode: "word", wrapMode: "word",
drawUnstyledText: false, fg: style.fg,
streaming: true, attributes: style.attrs,
fg: entryColor(commit, this.theme),
treeSitterClient: this.treeSitterClient,
}) })
: body.type === "code"
? new CodeRenderable(surface.renderContext, {
id,
content: "",
filetype: body.filetype,
syntaxStyle: entrySyntax(commit, this.theme),
width: "100%",
wrapMode: "word",
drawUnstyledText: false,
streaming: true,
fg: entryColor(commit, this.theme),
treeSitterClient: this.treeSitterClient,
})
: new MarkdownRenderable(surface.renderContext, { : new MarkdownRenderable(surface.renderContext, {
id, id,
content: "", content: "",
syntaxStyle: entrySyntax(commit, this.theme), syntaxStyle: entrySyntax(commit, this.theme),
width: "100%", width: "100%",
streaming: true, streaming: true,
internalBlockMode: "top-level", internalBlockMode: "top-level",
tableOptions: { widthMode: "content" }, tableOptions: { widthMode: "content" },
fg: entryColor(commit, this.theme), fg: entryColor(commit, this.theme),
treeSitterClient: this.treeSitterClient, treeSitterClient: this.treeSitterClient,
}) })
surface.root.add(renderable) surface.root.add(renderable)
@@ -326,8 +326,7 @@ export class RunScrollbackStream {
if ( if (
body.type !== "structured" && body.type !== "structured" &&
(entryCanStream(commit, body) || (entryCanStream(commit, body) || (commit.kind === "tool" && commit.phase === "final" && body.type === "markdown"))
(commit.kind === "tool" && commit.phase === "final" && body.type === "markdown"))
) { ) {
await this.writeStreaming(commit, body) await this.writeStreaming(commit, body)
if (entryDone(commit)) { if (entryDone(commit)) {
@@ -55,7 +55,12 @@ export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody
return "block" return "block"
} }
if (commit.phase === "progress" && commit.toolState === "completed" && body.type === "text" && body.content.includes("\n")) { if (
commit.phase === "progress" &&
commit.toolState === "completed" &&
body.type === "text" &&
body.content.includes("\n")
) {
return "block" return "block"
} }
@@ -205,10 +205,7 @@ function out(data: SessionData, commits: SessionCommit[], footer?: FooterOutput)
} }
} }
export function pickBlockerView(input: { export function pickBlockerView(input: { permission?: PermissionRequest; question?: QuestionRequest }): FooterView {
permission?: PermissionRequest
question?: QuestionRequest
}): FooterView {
if (input.permission) { if (input.permission) {
return { type: "permission", request: input.permission } return { type: "permission", request: input.permission }
} }
+9 -1
View File
@@ -259,7 +259,15 @@ function build(input: SplashWriterInput, kind: "entry" | "exit", ctx: Scrollback
} }
push(lines, body_left, top + 1, label, left, undefined, TextAttributes.DIM) push(lines, body_left, top + 1, label, left, undefined, TextAttributes.DIM)
push(lines, body_left + label.length, top + 1, `opencode run -i -s ${meta.session_id}`, right, undefined, TextAttributes.BOLD) push(
lines,
body_left + label.length,
top + 1,
`opencode run -i -s ${meta.session_id}`,
right,
undefined,
TextAttributes.BOLD,
)
height = top + mark.length height = top + mark.length
} }
@@ -189,9 +189,7 @@ function waitTurn(done: Wait["done"], signal: AbortSignal) {
signal.addEventListener("abort", onAbort, { once: true }) signal.addEventListener("abort", onAbort, { once: true })
return Effect.sync(() => signal.removeEventListener("abort", onAbort)) return Effect.sync(() => signal.removeEventListener("abort", onAbort))
}).pipe(Effect.exit), }).pipe(Effect.exit),
]).pipe( ]).pipe(Effect.flatMap((exit) => (Exit.isFailure(exit) ? Effect.failCause(exit.cause) : Effect.succeed(exit.value))))
Effect.flatMap((exit) => (Exit.isFailure(exit) ? Effect.failCause(exit.cause) : Effect.succeed(exit.value))),
)
} }
export function formatUnknownError(error: unknown): string { export function formatUnknownError(error: unknown): string {
@@ -380,7 +378,7 @@ function createLayer(input: StreamInput) {
(events) => (events) =>
Effect.sync(() => { Effect.sync(() => {
void events.stream.return(undefined).catch(() => {}) void events.stream.return(undefined).catch(() => {})
}), }),
), ),
) )
closeStream = () => { closeStream = () => {
@@ -422,10 +420,7 @@ function createLayer(input: StreamInput) {
return return
} }
if ( if (event.properties.sessionID !== input.sessionID && !state.subagent.tabs.has(event.properties.sessionID)) {
event.properties.sessionID !== input.sessionID &&
!state.subagent.tabs.has(event.properties.sessionID)
) {
return return
} }
@@ -178,10 +178,12 @@ function createLayer(fs = AppFileSystem.defaultLayer) {
delete next[key] delete next[key]
} }
yield* file.writeJson(MODEL_FILE, { yield* file
...current, .writeJson(MODEL_FILE, {
variant: next, ...current,
}).pipe(Effect.orElseSucceed(() => undefined)) variant: next,
})
.pipe(Effect.orElseSucceed(() => undefined))
}) })
return Service.of({ return Service.of({
@@ -191,8 +191,8 @@ describe("run entry body", () => {
deletions: 0, deletions: 0,
}, },
], ],
},
}, },
},
] satisfies Array<{ name: string; commit: StreamCommit; snapshot: ToolSnapshot }>) { ] satisfies Array<{ name: string; commit: StreamCommit; snapshot: ToolSnapshot }>) {
test(item.name, () => { test(item.name, () => {
expect(structured(item.commit)).toEqual(item.snapshot) expect(structured(item.commit)).toEqual(item.snapshot)
@@ -335,26 +335,16 @@ describe("run entry body", () => {
tool: "bash", tool: "bash",
phase: "progress", phase: "progress",
toolState: "completed", toolState: "completed",
text: [ text: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join("\n"),
"/tmp/demo",
"git status",
"On branch demo",
"nothing to commit, working tree clean",
"",
].join("\n"),
state: { state: {
status: "completed", status: "completed",
input: { input: {
command: "git status", command: "git status",
workdir: "/tmp/demo", workdir: "/tmp/demo",
}, },
output: [ output: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join(
"/tmp/demo", "\n",
"git status", ),
"On branch demo",
"nothing to commit, working tree clean",
"",
].join("\n"),
title: "git status", title: "git status",
metadata: { metadata: {
exitCode: 0, exitCode: 0,
@@ -2,7 +2,12 @@
import { expect, test } from "bun:test" import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid" import { testRender } from "@opentui/solid"
import { createSignal } from "solid-js" import { createSignal } from "solid-js"
import { RUN_COMMAND_PANEL_ROWS, RunCommandMenuBody, RunModelSelectBody, RunVariantSelectBody } from "@/cli/cmd/run/footer.command" import {
RUN_COMMAND_PANEL_ROWS,
RunCommandMenuBody,
RunModelSelectBody,
RunVariantSelectBody,
} from "@/cli/cmd/run/footer.command"
import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer" import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer"
import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme" import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
import type { FooterKeybinds, RunCommand, RunInput, RunProvider, StreamCommit } from "@/cli/cmd/run/types" import type { FooterKeybinds, RunCommand, RunInput, RunProvider, StreamCommit } from "@/cli/cmd/run/types"
@@ -117,14 +122,17 @@ test("run entry content updates when live commit text changes", async () => {
tool: "bash", tool: "bash",
}) })
const app = await testRender(() => ( const app = await testRender(
<box width={80} height={4}> () => (
<RunEntryContent commit={commit()} theme={RUN_THEME_FALLBACK} width={80} /> <box width={80} height={4}>
</box> <RunEntryContent commit={commit()} theme={RUN_THEME_FALLBACK} width={80} />
), { </box>
width: 80, ),
height: 4, {
}) width: 80,
height: 4,
},
)
try { try {
await app.renderOnce() await app.renderOnce()
@@ -155,26 +163,29 @@ test("direct command panel renders grouped command palette", async () => {
]) ])
const [variants] = createSignal(["high", "minimal"]) const [variants] = createSignal(["high", "minimal"])
const app = await testRender(() => ( const app = await testRender(
<box width={100} height={RUN_COMMAND_PANEL_ROWS}> () => (
<RunCommandMenuBody <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
theme={() => RUN_THEME_FALLBACK.footer} <RunCommandMenuBody
commands={commands} theme={() => RUN_THEME_FALLBACK.footer}
variants={variants} commands={commands}
keybinds={keybinds} variants={variants}
onClose={() => {}} keybinds={keybinds}
onModel={() => {}} onClose={() => {}}
onVariant={() => {}} onModel={() => {}}
onVariantCycle={() => {}} onVariant={() => {}}
onCommand={() => {}} onVariantCycle={() => {}}
onNew={() => {}} onCommand={() => {}}
onExit={() => {}} onNew={() => {}}
/> onExit={() => {}}
</box> />
), { </box>
width: 100, ),
height: RUN_COMMAND_PANEL_ROWS, {
}) width: 100,
height: RUN_COMMAND_PANEL_ROWS,
},
)
try { try {
await app.renderOnce() await app.renderOnce()
@@ -207,20 +218,23 @@ test("direct model panel renders current model selector", async () => {
const [providers] = createSignal<RunProvider[] | undefined>([provider()]) const [providers] = createSignal<RunProvider[] | undefined>([provider()])
const [current] = createSignal<RunInput["model"]>({ providerID: "opencode", modelID: "gpt-5" }) const [current] = createSignal<RunInput["model"]>({ providerID: "opencode", modelID: "gpt-5" })
const app = await testRender(() => ( const app = await testRender(
<box width={100} height={RUN_COMMAND_PANEL_ROWS}> () => (
<RunModelSelectBody <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
theme={() => RUN_THEME_FALLBACK.footer} <RunModelSelectBody
providers={providers} theme={() => RUN_THEME_FALLBACK.footer}
current={current} providers={providers}
onClose={() => {}} current={current}
onSelect={() => {}} onClose={() => {}}
/> onSelect={() => {}}
</box> />
), { </box>
width: 100, ),
height: RUN_COMMAND_PANEL_ROWS, {
}) width: 100,
height: RUN_COMMAND_PANEL_ROWS,
},
)
try { try {
await app.renderOnce() await app.renderOnce()
@@ -243,20 +257,23 @@ test("direct variant panel renders current variant selector", async () => {
const [variants] = createSignal(["high", "minimal"]) const [variants] = createSignal(["high", "minimal"])
const [current] = createSignal<string | undefined>("high") const [current] = createSignal<string | undefined>("high")
const app = await testRender(() => ( const app = await testRender(
<box width={100} height={RUN_COMMAND_PANEL_ROWS}> () => (
<RunVariantSelectBody <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
theme={() => RUN_THEME_FALLBACK.footer} <RunVariantSelectBody
variants={variants} theme={() => RUN_THEME_FALLBACK.footer}
current={current} variants={variants}
onClose={() => {}} current={current}
onSelect={() => {}} onClose={() => {}}
/> onSelect={() => {}}
</box> />
), { </box>
width: 100, ),
height: RUN_COMMAND_PANEL_ROWS, {
}) width: 100,
height: RUN_COMMAND_PANEL_ROWS,
},
)
try { try {
await app.renderOnce() await app.renderOnce()
@@ -7,11 +7,7 @@ import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
import { formatBindings } from "@/cli/cmd/run/keymap.shared" import { formatBindings } from "@/cli/cmd/run/keymap.shared"
import { KeymapSectionNames, keymapBindingDefaults, type KeymapSection } from "@/cli/cmd/tui/config/tui-schema" import { KeymapSectionNames, keymapBindingDefaults, type KeymapSection } from "@/cli/cmd/tui/config/tui-schema"
import { ConfigKeybinds } from "@/config/keybinds" import { ConfigKeybinds } from "@/config/keybinds"
import { import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
resolveDiffStyle,
resolveFooterKeybinds,
resolveModelInfo,
} from "@/cli/cmd/run/runtime.boot"
type RunBinding = Binding<Renderable, KeyEvent> type RunBinding = Binding<Renderable, KeyEvent>
@@ -299,5 +295,4 @@ describe("run runtime boot", () => {
}, },
}) })
}) })
}) })
@@ -58,10 +58,12 @@ function destroy(commits: ClaimedCommit[]) {
} }
} }
async function setup(input: { async function setup(
width?: number input: {
wrote?: boolean width?: number
} = {}) { wrote?: boolean
} = {},
) {
const out = await createTestRenderer({ const out = await createTestRenderer({
width: input.width ?? 80, width: input.width ?? 80,
screenMode: "split-footer", screenMode: "split-footer",
@@ -150,7 +152,8 @@ function toolCommit(input: {
} }
test("finalizes markdown tables for streamed and coalesced input", async () => { test("finalizes markdown tables for streamed and coalesced input", async () => {
const text = "| Column 1 | Column 2 | Column 3 |\n|---|---|---|\n| Row 1 | Value 1 | Value 2 |\n| Row 2 | Value 3 | Value 4 |" const text =
"| Column 1 | Column 2 | Column 3 |\n|---|---|---|\n| Row 1 | Value 1 | Value 2 |\n| Row 2 | Value 3 | Value 4 |"
for (const chunks of [[text], [...text]]) { for (const chunks of [[text], [...text]]) {
const out = await setup() const out = await setup()
@@ -182,7 +185,9 @@ test("holds markdown code blocks until final commit and keeps newline ownership"
try { try {
await out.scrollback.append( await out.scrollback.append(
assistant('# Markdown Sample\n\n- Item 1\n- Item 2\n\n```js\nconst message = "Hello, markdown"\nconsole.log(message)\n```'), assistant(
'# Markdown Sample\n\n- Item 1\n- Item 2\n\n```js\nconst message = "Hello, markdown"\nconsole.log(message)\n```',
),
) )
const progress = claim(out.renderer) const progress = claim(out.renderer)
@@ -543,7 +548,7 @@ test("inserts a spacer before the next tool after completed multiline bash outpu
take() take()
const output = lines.join("\n") const output = lines.join("\n")
expect(output).toContain("total 4\n\n✱ Glob \"**/*tool*\" in src/cli/cmd") expect(output).toContain('total 4\n\n✱ Glob "**/*tool*" in src/cli/cmd')
} finally { } finally {
out.scrollback.destroy() out.scrollback.destroy()
} }
@@ -48,12 +48,7 @@ function user(id: string) {
} }
} }
function text(input: { function text(input: { id: string; messageID: string; text: string; time?: Record<string, number> }) {
id: string
messageID: string
text: string
time?: Record<string, number>
}) {
return { return {
type: "message.part.updated", type: "message.part.updated",
properties: { properties: {
@@ -98,13 +93,7 @@ function delta(messageID: string, partID: string, value: string) {
} }
} }
function tool(input: { function tool(input: { id: string; messageID: string; tool: string; state: Record<string, unknown>; callID?: string }) {
id: string
messageID: string
tool: string
state: Record<string, unknown>
callID?: string
}) {
return { return {
type: "message.part.updated", type: "message.part.updated",
properties: { properties: {
@@ -144,11 +144,7 @@ function statusMap(busy: boolean): SessionStatusMap {
return {} return {}
} }
function assistantMessage(input: { function assistantMessage(input: { sessionID: string; id: string; parts: SessionMessage["parts"] }): SessionMessage {
sessionID: string
id: string
parts: SessionMessage["parts"]
}): SessionMessage {
return { return {
info: { info: {
id: input.id, id: input.id,
@@ -334,16 +330,18 @@ function footer(fn?: (commit: StreamCommit) => void) {
return { api, commits, events } return { api, commits, events }
} }
function sdk(input: { function sdk(
stream?: EventStream input: {
subscribe?: OpencodeClient["event"]["subscribe"] stream?: EventStream
promptAsync?: OpencodeClient["session"]["promptAsync"] subscribe?: OpencodeClient["event"]["subscribe"]
status?: OpencodeClient["session"]["status"] promptAsync?: OpencodeClient["session"]["promptAsync"]
messages?: OpencodeClient["session"]["messages"] status?: OpencodeClient["session"]["status"]
children?: OpencodeClient["session"]["children"] messages?: OpencodeClient["session"]["messages"]
permissions?: OpencodeClient["permission"]["list"] children?: OpencodeClient["session"]["children"]
questions?: OpencodeClient["question"]["list"] permissions?: OpencodeClient["permission"]["list"]
} = {}) { questions?: OpencodeClient["question"]["list"]
} = {},
) {
const client = new OpencodeClient() const client = new OpencodeClient()
const subscribe: OpencodeClient["event"]["subscribe"] = input.subscribe ?? (() => sse(input.stream ?? emptyStream())) const subscribe: OpencodeClient["event"]["subscribe"] = input.subscribe ?? (() => sse(input.stream ?? emptyStream()))
@@ -375,51 +373,52 @@ describe("run stream transport", () => {
messages: async ({ sessionID }) => { messages: async ({ sessionID }) => {
if (sessionID === "session-1") { if (sessionID === "session-1") {
return ok([ return ok([
assistantMessage({
sessionID: "session-1",
id: "msg-1",
parts: [
runningTool({
sessionID: "session-1",
messageID: "msg-1",
id: "task-1",
callID: "call-1",
tool: "task",
body: {
description: "Explore run folder",
subagent_type: "explore",
},
metadata: {
sessionId: "child-1",
},
}),
],
}),
])
}
return ok([
assistantMessage({ assistantMessage({
sessionID: "child-1", sessionID: "session-1",
id: "msg-child-1", id: "msg-1",
parts: [ parts: [
runningTool({ runningTool({
sessionID: "child-1", sessionID: "session-1",
messageID: "msg-child-1", messageID: "msg-1",
id: "edit-1", id: "task-1",
callID: "call-edit-1", callID: "call-1",
tool: "edit", tool: "task",
body: { body: {
filePath: "src/run/subagent-data.ts", description: "Explore run folder",
diff: "@@ -1 +1 @@", subagent_type: "explore",
},
metadata: {
sessionId: "child-1",
}, },
}), }),
], ],
}), }),
]) ])
}
return ok([
assistantMessage({
sessionID: "child-1",
id: "msg-child-1",
parts: [
runningTool({
sessionID: "child-1",
messageID: "msg-child-1",
id: "edit-1",
callID: "call-edit-1",
tool: "edit",
body: {
filePath: "src/run/subagent-data.ts",
diff: "@@ -1 +1 @@",
},
}),
],
}),
])
}, },
children: async () => ok([child("child-1")]), children: async () => ok([child("child-1")]),
permissions: async () => ok([ permissions: async () =>
ok([
{ {
id: "perm-1", id: "perm-1",
sessionID: "child-1", sessionID: "child-1",
@@ -743,7 +742,8 @@ describe("run stream transport", () => {
expect( expect(
ui.events.some( ui.events.some(
(event) => event.type === "stream.view" && event.view.type === "question" && event.view.request.id === request.id, (event) =>
event.type === "stream.view" && event.view.type === "question" && event.view.request.id === request.id,
), ),
).toBe(false) ).toBe(false)
@@ -982,11 +982,14 @@ describe("run stream transport", () => {
const transport = await createSessionTransport({ const transport = await createSessionTransport({
sdk: sdk({ sdk: sdk({
subscribe: () => sse((async function* (): AsyncGenerator<SdkEvent> { subscribe: () =>
await ready.promise sse(
yield busy() (async function* (): AsyncGenerator<SdkEvent> {
throw new Error("boom") await ready.promise
})()), yield busy()
throw new Error("boom")
})(),
),
promptAsync: async () => { promptAsync: async () => {
ready.resolve() ready.resolve()
return ok(undefined) return ok(undefined)
+8 -15
View File
@@ -2,16 +2,7 @@ import { expect, test } from "bun:test"
import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core" import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core"
import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "@/cli/cmd/run/theme" import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "@/cli/cmd/run/theme"
const palette = [ const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
"#15161e",
"#f7768e",
"#9ece6a",
"#e0af68",
"#7aa2f7",
"#bb9af7",
"#7dcfff",
"#c0caf5",
] as const
function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors { function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
return { return {
@@ -28,11 +19,13 @@ function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
} }
} }
function renderer(input: { function renderer(
themeMode?: "dark" | "light" input: {
colors?: TerminalColors themeMode?: "dark" | "light"
fail?: boolean colors?: TerminalColors
} = {}) { fail?: boolean
} = {},
) {
return { return {
themeMode: input.themeMode, themeMode: input.themeMode,
getPalette: async () => { getPalette: async () => {
@@ -79,7 +79,10 @@ const providers: RunProvider[] = [
}, },
] ]
function userMessage(id: string, input: { providerID: string; modelID: string; variant?: string }): SessionMessages[number] { function userMessage(
id: string,
input: { providerID: string; modelID: string; variant?: string },
): SessionMessages[number] {
return { return {
info: { info: {
id, id,