Просмотр исходного кода

fix(tui): separate question checkmark labels (#28558)

Kit Langton 3 месяцев назад
Родитель
Сommit
ddd6eb4496

+ 2 - 2
packages/opencode/src/cli/cmd/run/footer.question.tsx

@@ -416,7 +416,7 @@ export function RunQuestionBody(props: {
                               </text>
                             </box>
                             <Show when={!info()?.multiple}>
-                              <text fg={props.theme.success}>{hit() ? "✓" : ""}</text>
+                              <text fg={props.theme.success}>{hit() ? " ✓" : ""}</text>
                             </Show>
                           </box>
                           <box paddingLeft={3}>
@@ -466,7 +466,7 @@ export function RunQuestionBody(props: {
                           </text>
                         </box>
                         <Show when={!info()?.multiple}>
-                          <text fg={props.theme.success}>{picked() ? "✓" : ""}</text>
+                          <text fg={props.theme.success}>{picked() ? " ✓" : ""}</text>
                         </Show>
                       </box>
                       <Show

+ 2 - 2
packages/opencode/src/cli/cmd/tui/routes/session/question.tsx

@@ -375,7 +375,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
                           </text>
                         </box>
                         <Show when={!multi()}>
-                          <text fg={theme.success}>{picked() ? "✓" : ""}</text>
+                          <text fg={theme.success}>{picked() ? " ✓" : ""}</text>
                         </Show>
                       </box>
 
@@ -408,7 +408,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
                     </box>
 
                     <Show when={!multi()}>
-                      <text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
+                      <text fg={theme.success}>{customPicked() ? " ✓" : ""}</text>
                     </Show>
                   </box>
                   <Show when={store.editing}>

+ 49 - 0
packages/opencode/test/cli/run/footer.view.test.tsx

@@ -2,6 +2,7 @@
 import { expect, test } from "bun:test"
 import { testRender } from "@opentui/solid"
 import { createSignal } from "solid-js"
+import type { QuestionRequest } from "@opencode-ai/sdk/v2"
 import {
   RUN_COMMAND_PANEL_ROWS,
   RUN_SUBAGENT_PANEL_ROWS,
@@ -24,6 +25,7 @@ import type {
   RunProvider,
   StreamCommit,
 } from "@/cli/cmd/run/types"
+import { RunQuestionBody } from "@/cli/cmd/run/footer.question"
 
 function bindings(...keys: string[]) {
   return keys.map((key) => ({ key }))
@@ -401,6 +403,53 @@ test("direct footer shows subagent indicator while prompt is running", async ()
   }
 })
 
+test("direct question body separates single-select checkmark from label", async () => {
+  const request = {
+    id: "question-1",
+    sessionID: "session-1",
+    questions: [
+      {
+        question: "Which categorical concept is often described as a universal way to combine two objects?",
+        header: "Universal Product",
+        options: [
+          { label: "Product", description: "A product comes with projections." },
+          { label: "Equalizer", description: "An equalizer selects morphisms where arrows agree." },
+        ],
+      },
+    ],
+  } satisfies QuestionRequest
+  const replies: unknown[] = []
+
+  const app = await testRender(
+    () => (
+      <box width={100} height={12}>
+        <RunQuestionBody
+          request={request}
+          theme={RUN_THEME_FALLBACK.footer}
+          onReply={(input) => {
+            replies.push(input)
+          }}
+          onReject={() => {}}
+        />
+      </box>
+    ),
+    {
+      width: 100,
+      height: 12,
+    },
+  )
+
+  try {
+    app.mockInput.pressEnter()
+    await app.renderOnce()
+
+    expect(replies).toHaveLength(1)
+    expect(app.captureCharFrame()).toContain("Product ✓")
+  } finally {
+    app.renderer.destroy()
+  }
+})
+
 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" })

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

@@ -171,7 +171,7 @@ function globalSse(stream: GlobalEventStream) {
 function wrapGlobalStream(stream: EventStream): GlobalEventStream {
   return (async function* (): GlobalEventStream {
     for await (const event of stream) {
-      yield globalEvent(event)
+      yield globalEvent(event as GlobalEvent["payload"])
     }
     return StreamClosed
   })()
@@ -339,11 +339,11 @@ function child(id: string): SessionChild {
   }
 }
 
-function globalEvent(payload: GlobalEvent["payload"]): GlobalEvent {
+function globalEvent(payload: SdkEvent | GlobalEvent["payload"]): GlobalEvent {
   return {
     directory: "/tmp",
     project: "project-1",
-    payload,
+    payload: payload as GlobalEvent["payload"],
   }
 }
 

+ 10 - 7
packages/opencode/test/cli/tui/use-event.test.tsx

@@ -1,7 +1,7 @@
 /** @jsxImportSource @opentui/solid */
 import { describe, expect, test } from "bun:test"
 import { testRender } from "@opentui/solid"
-import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2"
+import type { GlobalEvent } from "@opencode-ai/sdk/v2"
 import { onMount } from "solid-js"
 import { ProjectProvider, useProject } from "../../../src/cli/cmd/tui/context/project"
 import { SDKProvider } from "../../../src/cli/cmd/tui/context/sdk"
@@ -17,7 +17,10 @@ async function wait(fn: () => boolean, timeout = 2000) {
   }
 }
 
-function event(payload: Event, input: { directory: string; project?: string; workspace?: string }): GlobalEvent {
+function event(
+  payload: GlobalEvent["payload"],
+  input: { directory: string; project?: string; workspace?: string },
+): GlobalEvent {
   return {
     directory: input.directory,
     project: input.project,
@@ -26,7 +29,7 @@ function event(payload: Event, input: { directory: string; project?: string; wor
   }
 }
 
-function vcs(branch: string): Event {
+function vcs(branch: string): GlobalEvent["payload"] {
   return {
     id: `evt_vcs_${branch}`,
     type: "vcs.branch.updated",
@@ -36,7 +39,7 @@ function vcs(branch: string): Event {
   }
 }
 
-function update(version: string): Event {
+function update(version: string): GlobalEvent["payload"] {
   return {
     id: `evt_update_${version}`,
     type: "installation.update-available",
@@ -67,7 +70,7 @@ function createSource() {
 
 async function mount() {
   const source = createSource()
-  const seen: Event[] = []
+  const seen: GlobalEvent["payload"][] = []
   const workspaces: Array<string | undefined> = []
   const fetch = (async (input: RequestInfo | URL) => {
     const url = new URL(input instanceof Request ? input.url : String(input))
@@ -102,7 +105,7 @@ async function mount() {
 }
 
 function Probe(props: {
-  seen: Event[]
+  seen: GlobalEvent["payload"][]
   workspaces: Array<string | undefined>
   onReady: (ctx: { project: ReturnType<typeof useProject> }) => void
 }) {
@@ -111,7 +114,7 @@ function Probe(props: {
 
   onMount(() => {
     event.subscribe((evt, { workspace }) => {
-      props.seen.push(evt)
+      props.seen.push(evt as GlobalEvent["payload"])
       props.workspaces.push(workspace)
     })
     props.onReady({ project })