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

fix(tui): keep shell-mode prompt editable (#25419)

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

+ 6 - 11
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -17,6 +17,7 @@ import { MessageID, PartID } from "@/session/schema"
 import { createStore, produce, unwrap } from "solid-js/store"
 import { useKeybind } from "@tui/context/keybind"
 import { usePromptHistory, type PromptInfo } from "./history"
+import { computePromptTraits } from "./traits"
 import { assign } from "./part"
 import { usePromptStash } from "./stash"
 import { DialogStash } from "../dialog-stash"
@@ -557,17 +558,11 @@ export function Prompt(props: PromptProps) {
 
   createEffect(() => {
     if (!input || input.isDestroyed) return
-    const capture =
-      store.mode === "normal"
-        ? auto()?.visible
-          ? (["escape", "navigate", "submit", "tab"] as const)
-          : (["tab"] as const)
-        : undefined
-    input.traits = {
-      capture,
-      suspend: !!props.disabled || store.mode === "shell",
-      status: store.mode === "shell" ? "SHELL" : undefined,
-    }
+    input.traits = computePromptTraits({
+      mode: store.mode,
+      disabled: !!props.disabled,
+      autocompleteVisible: !!auto()?.visible,
+    })
   })
 
   function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {

+ 31 - 0
packages/opencode/src/cli/cmd/tui/component/prompt/traits.ts

@@ -0,0 +1,31 @@
+import type { EditorTraits } from "@opentui/core"
+
+export type PromptMode = "normal" | "shell"
+
+export interface PromptTraitsInput {
+  mode: PromptMode
+  disabled: boolean
+  autocompleteVisible: boolean
+}
+
+/**
+ * Compute the textarea editor traits for the prompt.
+ *
+ * `traits.suspend` gates the textarea's keybinding actions (backspace,
+ * delete-word, arrow movement, undo/redo, etc.). Shell mode is an active
+ * editing mode — only `disabled` should suspend the textarea, otherwise
+ * users can type in shell mode but cannot delete or move the cursor.
+ */
+export function computePromptTraits(input: PromptTraitsInput): EditorTraits {
+  const capture =
+    input.mode === "normal"
+      ? input.autocompleteVisible
+        ? (["escape", "navigate", "submit", "tab"] as const)
+        : (["tab"] as const)
+      : undefined
+  return {
+    capture,
+    suspend: input.disabled,
+    status: input.mode === "shell" ? "SHELL" : undefined,
+  }
+}

+ 38 - 0
packages/opencode/test/cli/cmd/tui/prompt-traits.test.ts

@@ -0,0 +1,38 @@
+import { describe, expect, test } from "bun:test"
+import { computePromptTraits } from "../../../../src/cli/cmd/tui/component/prompt/traits"
+
+describe("computePromptTraits", () => {
+  test("normal mode without autocomplete only captures tab", () => {
+    const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: false })
+    expect(traits.capture).toEqual(["tab"])
+    expect(traits.suspend).toBe(false)
+    expect(traits.status).toBeUndefined()
+  })
+
+  test("normal mode with autocomplete captures navigation keys", () => {
+    const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: true })
+    expect(traits.capture).toEqual(["escape", "navigate", "submit", "tab"])
+    expect(traits.suspend).toBe(false)
+    expect(traits.status).toBeUndefined()
+  })
+
+  test("shell mode does not suspend the textarea", () => {
+    // Suspending the textarea would gate every keybinding action
+    // (backspace, delete-word-backward, arrow movement, etc.) — see
+    // @opentui/core 0.2.x TextareaRenderable.handleKeyPress. Shell mode is
+    // an active editing mode, so suspend must stay off.
+    const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
+    expect(traits.suspend).toBe(false)
+  })
+
+  test("shell mode disables capture and labels the prompt", () => {
+    const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
+    expect(traits.capture).toBeUndefined()
+    expect(traits.status).toBe("SHELL")
+  })
+
+  test("disabled suspends regardless of mode", () => {
+    expect(computePromptTraits({ mode: "normal", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
+    expect(computePromptTraits({ mode: "shell", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
+  })
+})