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

This commit is contained in:
Kit Langton 2026-05-02 10:56:27 -04:00 committed by GitHub
parent 4c4860fb24
commit 78b3000031
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 75 additions and 11 deletions

View file

@ -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"]) {

View file

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

View file

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