Skip to content

Commit 78b3000

Browse files
authored
fix(tui): keep shell-mode prompt editable (#25419)
1 parent 4c4860f commit 78b3000

3 files changed

Lines changed: 75 additions & 11 deletions

File tree

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { MessageID, PartID } from "@/session/schema"
1717
import { createStore, produce, unwrap } from "solid-js/store"
1818
import { useKeybind } from "@tui/context/keybind"
1919
import { usePromptHistory, type PromptInfo } from "./history"
20+
import { computePromptTraits } from "./traits"
2021
import { assign } from "./part"
2122
import { usePromptStash } from "./stash"
2223
import { DialogStash } from "../dialog-stash"
@@ -557,17 +558,11 @@ export function Prompt(props: PromptProps) {
557558

558559
createEffect(() => {
559560
if (!input || input.isDestroyed) return
560-
const capture =
561-
store.mode === "normal"
562-
? auto()?.visible
563-
? (["escape", "navigate", "submit", "tab"] as const)
564-
: (["tab"] as const)
565-
: undefined
566-
input.traits = {
567-
capture,
568-
suspend: !!props.disabled || store.mode === "shell",
569-
status: store.mode === "shell" ? "SHELL" : undefined,
570-
}
561+
input.traits = computePromptTraits({
562+
mode: store.mode,
563+
disabled: !!props.disabled,
564+
autocompleteVisible: !!auto()?.visible,
565+
})
571566
})
572567

573568
function restoreExtmarksFromParts(parts: PromptInfo["parts"]) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { EditorTraits } from "@opentui/core"
2+
3+
export type PromptMode = "normal" | "shell"
4+
5+
export interface PromptTraitsInput {
6+
mode: PromptMode
7+
disabled: boolean
8+
autocompleteVisible: boolean
9+
}
10+
11+
/**
12+
* Compute the textarea editor traits for the prompt.
13+
*
14+
* `traits.suspend` gates the textarea's keybinding actions (backspace,
15+
* delete-word, arrow movement, undo/redo, etc.). Shell mode is an active
16+
* editing mode — only `disabled` should suspend the textarea, otherwise
17+
* users can type in shell mode but cannot delete or move the cursor.
18+
*/
19+
export function computePromptTraits(input: PromptTraitsInput): EditorTraits {
20+
const capture =
21+
input.mode === "normal"
22+
? input.autocompleteVisible
23+
? (["escape", "navigate", "submit", "tab"] as const)
24+
: (["tab"] as const)
25+
: undefined
26+
return {
27+
capture,
28+
suspend: input.disabled,
29+
status: input.mode === "shell" ? "SHELL" : undefined,
30+
}
31+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { computePromptTraits } from "../../../../src/cli/cmd/tui/component/prompt/traits"
3+
4+
describe("computePromptTraits", () => {
5+
test("normal mode without autocomplete only captures tab", () => {
6+
const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: false })
7+
expect(traits.capture).toEqual(["tab"])
8+
expect(traits.suspend).toBe(false)
9+
expect(traits.status).toBeUndefined()
10+
})
11+
12+
test("normal mode with autocomplete captures navigation keys", () => {
13+
const traits = computePromptTraits({ mode: "normal", disabled: false, autocompleteVisible: true })
14+
expect(traits.capture).toEqual(["escape", "navigate", "submit", "tab"])
15+
expect(traits.suspend).toBe(false)
16+
expect(traits.status).toBeUndefined()
17+
})
18+
19+
test("shell mode does not suspend the textarea", () => {
20+
// Suspending the textarea would gate every keybinding action
21+
// (backspace, delete-word-backward, arrow movement, etc.) — see
22+
// @opentui/core 0.2.x TextareaRenderable.handleKeyPress. Shell mode is
23+
// an active editing mode, so suspend must stay off.
24+
const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
25+
expect(traits.suspend).toBe(false)
26+
})
27+
28+
test("shell mode disables capture and labels the prompt", () => {
29+
const traits = computePromptTraits({ mode: "shell", disabled: false, autocompleteVisible: false })
30+
expect(traits.capture).toBeUndefined()
31+
expect(traits.status).toBe("SHELL")
32+
})
33+
34+
test("disabled suspends regardless of mode", () => {
35+
expect(computePromptTraits({ mode: "normal", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
36+
expect(computePromptTraits({ mode: "shell", disabled: true, autocompleteVisible: false }).suspend).toBe(true)
37+
})
38+
})

0 commit comments

Comments
 (0)