فهرست منبع

chore: generate

opencode-agent[bot] 3 ماه پیش
والد
کامیت
ac7a885ae9
23فایلهای تغییر یافته به همراه283 افزوده شده و 282 حذف شده
  1. 1 8
      packages/opencode/src/cli/cmd/run/demo.ts
  2. 9 15
      packages/opencode/src/cli/cmd/run/footer.command.tsx
  3. 28 12
      packages/opencode/src/cli/cmd/run/footer.menu.tsx
  4. 4 5
      packages/opencode/src/cli/cmd/run/footer.subagent.tsx
  5. 3 3
      packages/opencode/src/cli/cmd/run/footer.ts
  6. 7 9
      packages/opencode/src/cli/cmd/run/otel.ts
  7. 6 8
      packages/opencode/src/cli/cmd/run/prompt.shared.ts
  8. 6 1
      packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts
  9. 4 7
      packages/opencode/src/cli/cmd/run/runtime.ts
  10. 26 27
      packages/opencode/src/cli/cmd/run/scrollback.surface.ts
  11. 6 1
      packages/opencode/src/cli/cmd/run/scrollback.writer.tsx
  12. 1 4
      packages/opencode/src/cli/cmd/run/session-data.ts
  13. 9 1
      packages/opencode/src/cli/cmd/run/splash.ts
  14. 3 8
      packages/opencode/src/cli/cmd/run/stream.transport.ts
  15. 6 4
      packages/opencode/src/cli/cmd/run/variant.shared.ts
  16. 5 15
      packages/opencode/test/cli/run/entry.body.test.ts
  17. 74 57
      packages/opencode/test/cli/run/footer.view.test.tsx
  18. 1 6
      packages/opencode/test/cli/run/runtime.boot.test.ts
  19. 12 7
      packages/opencode/test/cli/run/scrollback.surface.test.ts
  20. 2 13
      packages/opencode/test/cli/run/session-data.test.ts
  21. 58 55
      packages/opencode/test/cli/run/stream.transport.test.ts
  22. 8 15
      packages/opencode/test/cli/run/theme.test.ts
  23. 4 1
      packages/opencode/test/cli/run/variant.shared.test.ts

+ 1 - 8
packages/opencode/src/cli/cmd/run/demo.ts

@@ -18,14 +18,7 @@ import path from "path"
 import type { Event, ToolPart } from "@opencode-ai/sdk/v2"
 import { createSessionData, reduceSessionData, type SessionData } from "./session-data"
 import { writeSessionOutput } from "./stream"
-import type {
-  FooterApi,
-  PermissionReply,
-  QuestionReject,
-  QuestionReply,
-  RunPrompt,
-  StreamCommit,
-} from "./types"
+import type { FooterApi, PermissionReply, QuestionReject, QuestionReply, RunPrompt, StreamCommit } from "./types"
 
 const KINDS = [
   "markdown",

+ 9 - 15
packages/opencode/src/cli/cmd/run/footer.command.tsx

@@ -180,13 +180,7 @@ function PanelShell(props: {
   children: JSX.Element
 }) {
   return (
-    <box
-      id={props.id}
-      width="100%"
-      flexDirection="column"
-      backgroundColor="transparent"
-      flexShrink={0}
-    >
+    <box id={props.id} width="100%" flexDirection="column" backgroundColor="transparent" flexShrink={0}>
       <box
         width="100%"
         flexDirection="column"
@@ -333,14 +327,14 @@ export function RunCommandMenuBody(props: {
               category: item.source === "mcp" ? "MCP Commands" : "Project Commands",
               name: item.name,
               display: item.name,
-               footer: `/${item.name}`,
-               keywords:
-                 item.source === "mcp"
-                   ? `/${item.name} ${item.name} mcp ${item.description ?? ""}`
-                   : `/${item.name} ${item.name} ${item.description ?? ""}`,
-             }) satisfies CommandEntry,
-         )
-         .sort((a, b) => categoryRank(a.category) - categoryRank(b.category) || a.display.localeCompare(b.display)),
+              footer: `/${item.name}`,
+              keywords:
+                item.source === "mcp"
+                  ? `/${item.name} ${item.name} mcp ${item.description ?? ""}`
+                  : `/${item.name} ${item.name} ${item.description ?? ""}`,
+            }) satisfies CommandEntry,
+        )
+        .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" },
     ]
   })

+ 28 - 12
packages/opencode/src/cli/cmd/run/footer.menu.tsx

@@ -161,10 +161,7 @@ export function RunFooterMenu(props: {
       return
     }
 
-    const dir =
-      props.selected() === previous + 1 ? 1
-      : props.selected() === previous - 1 ? -1
-      : undefined
+    const dir = props.selected() === previous + 1 ? 1 : props.selected() === previous - 1 ? -1 : undefined
     setGroupOffset((value) =>
       dir
         ? moveOffset(value, { count: all.length, limit: limit(), selected, dir })
@@ -175,11 +172,14 @@ export function RunFooterMenu(props: {
 
   const rows = createMemo<RunFooterMenuRow[]>(() => {
     if (!props.grouped) {
-      return props.items().slice(props.offset(), props.offset() + limit()).map((item, index) => ({
-        type: "item",
-        item,
-        index: index + props.offset(),
-      }))
+      return props
+        .items()
+        .slice(props.offset(), props.offset() + limit())
+        .map((item, index) => ({
+          type: "item",
+          item,
+          index: index + props.offset(),
+        }))
     }
 
     const all = groupedRows()
@@ -187,7 +187,13 @@ export function RunFooterMenu(props: {
     return all.slice(start, start + limit())
   })
   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
   })
   const descriptionPad = (item: RunFooterMenuItem) => {
@@ -264,7 +270,12 @@ export function RunFooterMenu(props: {
                   backgroundColor={active() ? props.theme().highlight : props.theme().surface}
                 >
                   <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.description ? (
                         <span style={{ fg: active() ? props.theme().surface : props.theme().muted }}>
@@ -274,7 +285,12 @@ export function RunFooterMenu(props: {
                       ) : undefined}
                     </text>
                     {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}
                       </text>
                     ) : undefined}

+ 4 - 5
packages/opencode/src/cli/cmd/run/footer.subagent.tsx

@@ -36,10 +36,7 @@ function statusIcon(status: FooterSubagentTab["status"]) {
 }
 
 function tabText(tab: FooterSubagentTab, slot: string, count: number, width: number) {
-  const perTab = Math.max(
-    1,
-    Math.floor((width - 4 - Math.max(0, count - 1) * 3) / Math.max(1, count)),
-  )
+  const perTab = Math.max(1, Math.floor((width - 4 - Math.max(0, count - 1) * 3) / Math.max(1, count)))
   if (count >= 8 || perTab < 12) {
     return `[${slot}]`
   }
@@ -96,7 +93,9 @@ export function RunFooterSubagentTabs(props: {
       flexDirection="row"
       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>
   )
 }

+ 3 - 3
packages/opencode/src/cli/cmd/run/footer.ts

@@ -567,9 +567,9 @@ export class RunFooter implements FooterApi {
               ? 1 + tabs + MODEL_ROWS
               : this.promptRoute.type === "variant"
                 ? 1 + tabs + VARIANT_ROWS
-              : this.promptRoute.type === "subagent"
-                ? this.base + tabs + SUBAGENT_INSPECTOR_ROWS
-                : Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
+                : this.promptRoute.type === "subagent"
+                  ? this.base + tabs + SUBAGENT_INSPECTOR_ROWS
+                  : Math.max(base + TEXTAREA_MIN_ROWS, Math.min(base + PROMPT_MAX_ROWS, base + this.rows))
 
     if (height !== this.renderer.footerHeight) {
       this.renderer.footerHeight = height

+ 7 - 9
packages/opencode/src/cli/cmd/run/otel.ts

@@ -103,15 +103,13 @@ export function withRunSpan<A>(
         attributes: attributes(input),
       })
 
-      return context.with(
-        trace.setSpan(context.active(), span),
-        () =>
-          finish(
-            span,
-            new Promise<A>((resolve) => {
-              resolve(fn(span))
-            }),
-          ),
+      return context.with(trace.setSpan(context.active(), span), () =>
+        finish(
+          span,
+          new Promise<A>((resolve) => {
+            resolve(fn(span))
+          }),
+        ),
       )
     },
     () => fn(noop),

+ 6 - 8
packages/opencode/src/cli/cmd/run/prompt.shared.ts

@@ -23,13 +23,7 @@ export type PromptHistoryState = {
   draft: string
 }
 
-export function promptInfo(event: {
-  name: string
-  ctrl?: boolean
-  meta?: boolean
-  shift?: boolean
-  super?: boolean
-}) {
+export function promptInfo(event: { name: string; ctrl?: boolean; meta?: boolean; shift?: boolean; super?: boolean }) {
   return {
     name: event.name === " " ? "space" : event.name,
     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) => {
     if (key.leader) {
       return []

+ 6 - 1
packages/opencode/src/cli/cmd/run/runtime.lifecycle.ts

@@ -248,7 +248,12 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
         process.on("SIGINT", sigint)
 
         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) {
             return
           }

+ 4 - 7
packages/opencode/src/cli/cmd/run/runtime.ts

@@ -39,10 +39,7 @@ type CreateSessionInput = {
   variant: string | undefined
 }
 
-type CreateSession = (
-  sdk: RunInput["sdk"],
-  input: CreateSessionInput,
-) => Promise<{ id: string; title?: string }>
+type CreateSession = (sdk: RunInput["sdk"], input: CreateSessionInput) => Promise<{ id: string; title?: string }>
 
 type RunRuntimeInput = {
   boot: () => Promise<BootContext>
@@ -548,9 +545,9 @@ async function runInteractiveRuntime(input: RunRuntimeInput): Promise<void> {
                   state.sessionTitle = created.sessionTitle
                   state.agent = created.agent ?? state.agent
                   state.history = []
-                   includeFiles = true
-                   state.demo = input.demo
-                     ? createRunDemo({
+                  includeFiles = true
+                  state.demo = input.demo
+                    ? createRunDemo({
                         footer,
                         sessionID: state.sessionID,
                         thinking: input.thinking,

+ 26 - 27
packages/opencode/src/cli/cmd/run/scrollback.surface.ts

@@ -118,37 +118,37 @@ export class RunScrollbackStream {
     const renderable =
       body.type === "text"
         ? new TextRenderable(surface.renderContext, {
-          id,
-          content: "",
-          width: "100%",
-          wrapMode: "word",
-          fg: style.fg,
-          attributes: style.attrs,
-        })
-        : 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,
+            fg: style.fg,
+            attributes: style.attrs,
           })
+        : 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, {
-            id,
-            content: "",
-            syntaxStyle: entrySyntax(commit, this.theme),
-            width: "100%",
-            streaming: true,
-            internalBlockMode: "top-level",
-            tableOptions: { widthMode: "content" },
-            fg: entryColor(commit, this.theme),
-            treeSitterClient: this.treeSitterClient,
-          })
+              id,
+              content: "",
+              syntaxStyle: entrySyntax(commit, this.theme),
+              width: "100%",
+              streaming: true,
+              internalBlockMode: "top-level",
+              tableOptions: { widthMode: "content" },
+              fg: entryColor(commit, this.theme),
+              treeSitterClient: this.treeSitterClient,
+            })
 
     surface.root.add(renderable)
 
@@ -326,8 +326,7 @@ export class RunScrollbackStream {
 
     if (
       body.type !== "structured" &&
-      (entryCanStream(commit, body) ||
-        (commit.kind === "tool" && commit.phase === "final" && body.type === "markdown"))
+      (entryCanStream(commit, body) || (commit.kind === "tool" && commit.phase === "final" && body.type === "markdown"))
     ) {
       await this.writeStreaming(commit, body)
       if (entryDone(commit)) {

+ 6 - 1
packages/opencode/src/cli/cmd/run/scrollback.writer.tsx

@@ -55,7 +55,12 @@ export function entryLayout(commit: StreamCommit, body: RunEntryBody = entryBody
       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"
     }
 

+ 1 - 4
packages/opencode/src/cli/cmd/run/session-data.ts

@@ -205,10 +205,7 @@ function out(data: SessionData, commits: SessionCommit[], footer?: FooterOutput)
   }
 }
 
-export function pickBlockerView(input: {
-  permission?: PermissionRequest
-  question?: QuestionRequest
-}): FooterView {
+export function pickBlockerView(input: { permission?: PermissionRequest; question?: QuestionRequest }): FooterView {
   if (input.permission) {
     return { type: "permission", request: input.permission }
   }

+ 9 - 1
packages/opencode/src/cli/cmd/run/splash.ts

@@ -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 + 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
   }
 

+ 3 - 8
packages/opencode/src/cli/cmd/run/stream.transport.ts

@@ -189,9 +189,7 @@ function waitTurn(done: Wait["done"], signal: AbortSignal) {
       signal.addEventListener("abort", onAbort, { once: true })
       return Effect.sync(() => signal.removeEventListener("abort", onAbort))
     }).pipe(Effect.exit),
-  ]).pipe(
-    Effect.flatMap((exit) => (Exit.isFailure(exit) ? Effect.failCause(exit.cause) : Effect.succeed(exit.value))),
-  )
+  ]).pipe(Effect.flatMap((exit) => (Exit.isFailure(exit) ? Effect.failCause(exit.cause) : Effect.succeed(exit.value))))
 }
 
 export function formatUnknownError(error: unknown): string {
@@ -380,7 +378,7 @@ function createLayer(input: StreamInput) {
             (events) =>
               Effect.sync(() => {
                 void events.stream.return(undefined).catch(() => {})
-                }),
+              }),
           ),
         )
         closeStream = () => {
@@ -422,10 +420,7 @@ function createLayer(input: StreamInput) {
             return
           }
 
-          if (
-            event.properties.sessionID !== input.sessionID &&
-            !state.subagent.tabs.has(event.properties.sessionID)
-          ) {
+          if (event.properties.sessionID !== input.sessionID && !state.subagent.tabs.has(event.properties.sessionID)) {
             return
           }
 

+ 6 - 4
packages/opencode/src/cli/cmd/run/variant.shared.ts

@@ -178,10 +178,12 @@ function createLayer(fs = AppFileSystem.defaultLayer) {
             delete next[key]
           }
 
-          yield* file.writeJson(MODEL_FILE, {
-            ...current,
-            variant: next,
-          }).pipe(Effect.orElseSucceed(() => undefined))
+          yield* file
+            .writeJson(MODEL_FILE, {
+              ...current,
+              variant: next,
+            })
+            .pipe(Effect.orElseSucceed(() => undefined))
         })
 
         return Service.of({

+ 5 - 15
packages/opencode/test/cli/run/entry.body.test.ts

@@ -191,8 +191,8 @@ describe("run entry body", () => {
             deletions: 0,
           },
         ],
-        },
       },
+    },
   ] satisfies Array<{ name: string; commit: StreamCommit; snapshot: ToolSnapshot }>) {
     test(item.name, () => {
       expect(structured(item.commit)).toEqual(item.snapshot)
@@ -335,26 +335,16 @@ describe("run entry body", () => {
           tool: "bash",
           phase: "progress",
           toolState: "completed",
-          text: [
-            "/tmp/demo",
-            "git status",
-            "On branch demo",
-            "nothing to commit, working tree clean",
-            "",
-          ].join("\n"),
+          text: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join("\n"),
           state: {
             status: "completed",
             input: {
               command: "git status",
               workdir: "/tmp/demo",
             },
-            output: [
-              "/tmp/demo",
-              "git status",
-              "On branch demo",
-              "nothing to commit, working tree clean",
-              "",
-            ].join("\n"),
+            output: ["/tmp/demo", "git status", "On branch demo", "nothing to commit, working tree clean", ""].join(
+              "\n",
+            ),
             title: "git status",
             metadata: {
               exitCode: 0,

+ 74 - 57
packages/opencode/test/cli/run/footer.view.test.tsx

@@ -2,7 +2,12 @@
 import { expect, test } from "bun:test"
 import { testRender } from "@opentui/solid"
 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 { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
 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",
   })
 
-  const app = await testRender(() => (
-    <box width={80} height={4}>
-      <RunEntryContent commit={commit()} theme={RUN_THEME_FALLBACK} width={80} />
-    </box>
-  ), {
-    width: 80,
-    height: 4,
-  })
+  const app = await testRender(
+    () => (
+      <box width={80} height={4}>
+        <RunEntryContent commit={commit()} theme={RUN_THEME_FALLBACK} width={80} />
+      </box>
+    ),
+    {
+      width: 80,
+      height: 4,
+    },
+  )
 
   try {
     await app.renderOnce()
@@ -155,26 +163,29 @@ test("direct command panel renders grouped command palette", async () => {
   ])
   const [variants] = createSignal(["high", "minimal"])
 
-  const app = await testRender(() => (
-    <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
-      <RunCommandMenuBody
-        theme={() => RUN_THEME_FALLBACK.footer}
-        commands={commands}
-        variants={variants}
-        keybinds={keybinds}
-        onClose={() => {}}
-        onModel={() => {}}
-        onVariant={() => {}}
-        onVariantCycle={() => {}}
-        onCommand={() => {}}
-        onNew={() => {}}
-        onExit={() => {}}
-      />
-    </box>
-  ), {
-    width: 100,
-    height: RUN_COMMAND_PANEL_ROWS,
-  })
+  const app = await testRender(
+    () => (
+      <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
+        <RunCommandMenuBody
+          theme={() => RUN_THEME_FALLBACK.footer}
+          commands={commands}
+          variants={variants}
+          keybinds={keybinds}
+          onClose={() => {}}
+          onModel={() => {}}
+          onVariant={() => {}}
+          onVariantCycle={() => {}}
+          onCommand={() => {}}
+          onNew={() => {}}
+          onExit={() => {}}
+        />
+      </box>
+    ),
+    {
+      width: 100,
+      height: RUN_COMMAND_PANEL_ROWS,
+    },
+  )
 
   try {
     await app.renderOnce()
@@ -207,20 +218,23 @@ test("direct model panel renders current model selector", async () => {
   const [providers] = createSignal<RunProvider[] | undefined>([provider()])
   const [current] = createSignal<RunInput["model"]>({ providerID: "opencode", modelID: "gpt-5" })
 
-  const app = await testRender(() => (
-    <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
-      <RunModelSelectBody
-        theme={() => RUN_THEME_FALLBACK.footer}
-        providers={providers}
-        current={current}
-        onClose={() => {}}
-        onSelect={() => {}}
-      />
-    </box>
-  ), {
-    width: 100,
-    height: RUN_COMMAND_PANEL_ROWS,
-  })
+  const app = await testRender(
+    () => (
+      <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
+        <RunModelSelectBody
+          theme={() => RUN_THEME_FALLBACK.footer}
+          providers={providers}
+          current={current}
+          onClose={() => {}}
+          onSelect={() => {}}
+        />
+      </box>
+    ),
+    {
+      width: 100,
+      height: RUN_COMMAND_PANEL_ROWS,
+    },
+  )
 
   try {
     await app.renderOnce()
@@ -243,20 +257,23 @@ test("direct variant panel renders current variant selector", async () => {
   const [variants] = createSignal(["high", "minimal"])
   const [current] = createSignal<string | undefined>("high")
 
-  const app = await testRender(() => (
-    <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
-      <RunVariantSelectBody
-        theme={() => RUN_THEME_FALLBACK.footer}
-        variants={variants}
-        current={current}
-        onClose={() => {}}
-        onSelect={() => {}}
-      />
-    </box>
-  ), {
-    width: 100,
-    height: RUN_COMMAND_PANEL_ROWS,
-  })
+  const app = await testRender(
+    () => (
+      <box width={100} height={RUN_COMMAND_PANEL_ROWS}>
+        <RunVariantSelectBody
+          theme={() => RUN_THEME_FALLBACK.footer}
+          variants={variants}
+          current={current}
+          onClose={() => {}}
+          onSelect={() => {}}
+        />
+      </box>
+    ),
+    {
+      width: 100,
+      height: RUN_COMMAND_PANEL_ROWS,
+    },
+  )
 
   try {
     await app.renderOnce()

+ 1 - 6
packages/opencode/test/cli/run/runtime.boot.test.ts

@@ -7,11 +7,7 @@ import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
 import { formatBindings } from "@/cli/cmd/run/keymap.shared"
 import { KeymapSectionNames, keymapBindingDefaults, type KeymapSection } from "@/cli/cmd/tui/config/tui-schema"
 import { ConfigKeybinds } from "@/config/keybinds"
-import {
-  resolveDiffStyle,
-  resolveFooterKeybinds,
-  resolveModelInfo,
-} from "@/cli/cmd/run/runtime.boot"
+import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
 
 type RunBinding = Binding<Renderable, KeyEvent>
 
@@ -299,5 +295,4 @@ describe("run runtime boot", () => {
       },
     })
   })
-
 })

+ 12 - 7
packages/opencode/test/cli/run/scrollback.surface.test.ts

@@ -58,10 +58,12 @@ function destroy(commits: ClaimedCommit[]) {
   }
 }
 
-async function setup(input: {
-  width?: number
-  wrote?: boolean
-} = {}) {
+async function setup(
+  input: {
+    width?: number
+    wrote?: boolean
+  } = {},
+) {
   const out = await createTestRenderer({
     width: input.width ?? 80,
     screenMode: "split-footer",
@@ -150,7 +152,8 @@ function toolCommit(input: {
 }
 
 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]]) {
     const out = await setup()
@@ -182,7 +185,9 @@ test("holds markdown code blocks until final commit and keeps newline ownership"
 
   try {
     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)
@@ -543,7 +548,7 @@ test("inserts a spacer before the next tool after completed multiline bash outpu
     take()
 
     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 {
     out.scrollback.destroy()
   }

+ 2 - 13
packages/opencode/test/cli/run/session-data.test.ts

@@ -48,12 +48,7 @@ function user(id: string) {
   }
 }
 
-function text(input: {
-  id: string
-  messageID: string
-  text: string
-  time?: Record<string, number>
-}) {
+function text(input: { id: string; messageID: string; text: string; time?: Record<string, number> }) {
   return {
     type: "message.part.updated",
     properties: {
@@ -98,13 +93,7 @@ function delta(messageID: string, partID: string, value: string) {
   }
 }
 
-function tool(input: {
-  id: string
-  messageID: string
-  tool: string
-  state: Record<string, unknown>
-  callID?: string
-}) {
+function tool(input: { id: string; messageID: string; tool: string; state: Record<string, unknown>; callID?: string }) {
   return {
     type: "message.part.updated",
     properties: {

+ 58 - 55
packages/opencode/test/cli/run/stream.transport.test.ts

@@ -144,11 +144,7 @@ function statusMap(busy: boolean): SessionStatusMap {
   return {}
 }
 
-function assistantMessage(input: {
-  sessionID: string
-  id: string
-  parts: SessionMessage["parts"]
-}): SessionMessage {
+function assistantMessage(input: { sessionID: string; id: string; parts: SessionMessage["parts"] }): SessionMessage {
   return {
     info: {
       id: input.id,
@@ -334,16 +330,18 @@ function footer(fn?: (commit: StreamCommit) => void) {
   return { api, commits, events }
 }
 
-function sdk(input: {
-  stream?: EventStream
-  subscribe?: OpencodeClient["event"]["subscribe"]
-  promptAsync?: OpencodeClient["session"]["promptAsync"]
-  status?: OpencodeClient["session"]["status"]
-  messages?: OpencodeClient["session"]["messages"]
-  children?: OpencodeClient["session"]["children"]
-  permissions?: OpencodeClient["permission"]["list"]
-  questions?: OpencodeClient["question"]["list"]
-} = {}) {
+function sdk(
+  input: {
+    stream?: EventStream
+    subscribe?: OpencodeClient["event"]["subscribe"]
+    promptAsync?: OpencodeClient["session"]["promptAsync"]
+    status?: OpencodeClient["session"]["status"]
+    messages?: OpencodeClient["session"]["messages"]
+    children?: OpencodeClient["session"]["children"]
+    permissions?: OpencodeClient["permission"]["list"]
+    questions?: OpencodeClient["question"]["list"]
+  } = {},
+) {
   const client = new OpencodeClient()
 
   const subscribe: OpencodeClient["event"]["subscribe"] = input.subscribe ?? (() => sse(input.stream ?? emptyStream()))
@@ -375,51 +373,52 @@ describe("run stream transport", () => {
         messages: async ({ sessionID }) => {
           if (sessionID === "session-1") {
             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({
-                sessionID: "child-1",
-                id: "msg-child-1",
+                sessionID: "session-1",
+                id: "msg-1",
                 parts: [
                   runningTool({
-                    sessionID: "child-1",
-                    messageID: "msg-child-1",
-                    id: "edit-1",
-                    callID: "call-edit-1",
-                    tool: "edit",
+                    sessionID: "session-1",
+                    messageID: "msg-1",
+                    id: "task-1",
+                    callID: "call-1",
+                    tool: "task",
                     body: {
-                      filePath: "src/run/subagent-data.ts",
-                      diff: "@@ -1 +1 @@",
+                      description: "Explore run folder",
+                      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")]),
-        permissions: async () => ok([
+        permissions: async () =>
+          ok([
             {
               id: "perm-1",
               sessionID: "child-1",
@@ -743,7 +742,8 @@ describe("run stream transport", () => {
 
       expect(
         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)
 
@@ -982,11 +982,14 @@ describe("run stream transport", () => {
 
     const transport = await createSessionTransport({
       sdk: sdk({
-        subscribe: () => sse((async function* (): AsyncGenerator<SdkEvent> {
-            await ready.promise
-            yield busy()
-            throw new Error("boom")
-          })()),
+        subscribe: () =>
+          sse(
+            (async function* (): AsyncGenerator<SdkEvent> {
+              await ready.promise
+              yield busy()
+              throw new Error("boom")
+            })(),
+          ),
         promptAsync: async () => {
           ready.resolve()
           return ok(undefined)

+ 8 - 15
packages/opencode/test/cli/run/theme.test.ts

@@ -2,16 +2,7 @@ import { expect, test } from "bun:test"
 import { RGBA, type CliRenderer, type TerminalColors } from "@opentui/core"
 import { RUN_THEME_FALLBACK, generateSystem, resolveRunTheme, resolveTheme } from "@/cli/cmd/run/theme"
 
-const palette = [
-  "#15161e",
-  "#f7768e",
-  "#9ece6a",
-  "#e0af68",
-  "#7aa2f7",
-  "#bb9af7",
-  "#7dcfff",
-  "#c0caf5",
-] as const
+const palette = ["#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5"] as const
 
 function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
   return {
@@ -28,11 +19,13 @@ function terminalColors(input: Partial<TerminalColors> = {}): TerminalColors {
   }
 }
 
-function renderer(input: {
-  themeMode?: "dark" | "light"
-  colors?: TerminalColors
-  fail?: boolean
-} = {}) {
+function renderer(
+  input: {
+    themeMode?: "dark" | "light"
+    colors?: TerminalColors
+    fail?: boolean
+  } = {},
+) {
   return {
     themeMode: input.themeMode,
     getPalette: async () => {

+ 4 - 1
packages/opencode/test/cli/run/variant.shared.test.ts

@@ -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 {
     info: {
       id,