summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-05-02 10:56:27 -0400
committerGitHub <[email protected]>2026-05-02 10:56:27 -0400
commit78b3000031d3224d46da667dc631a04e7647d0f6 (patch)
treec57c6636e3a7d58c261887cfae4bb333d8a90fd6
parent4c4860fb24603ce2e1044bc9d2c98953ce2d78af (diff)
downloadopencode-78b3000031d3224d46da667dc631a04e7647d0f6.tar.gz
opencode-78b3000031d3224d46da667dc631a04e7647d0f6.zip
fix(tui): keep shell-mode prompt editable (#25419)
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx17
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/traits.ts31
-rw-r--r--packages/opencode/test/cli/cmd/tui/prompt-traits.test.ts38
3 files changed, 75 insertions, 11 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 1f93a4394..79034a01b 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/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"]) {
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/traits.ts b/packages/opencode/src/cli/cmd/tui/component/prompt/traits.ts
new file mode 100644
index 000000000..e47a1aeba
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/packages/opencode/test/cli/cmd/tui/prompt-traits.test.ts b/packages/opencode/test/cli/cmd/tui/prompt-traits.test.ts
new file mode 100644
index 000000000..34a16aedd
--- /dev/null
+++ b/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)
+ })
+})