refactor(tui): remove legacy keymap layer (#37206)
Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
parent
e916b99742
commit
b4a4ef0b3c
19 changed files with 535 additions and 1025 deletions
|
|
@ -23,7 +23,7 @@ import {
|
|||
movePromptHistory,
|
||||
pushPromptHistory,
|
||||
} from "./prompt.shared"
|
||||
import { OPENCODE_BASE_MODE, useBindings } from "@opencode-ai/tui/keymap"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
import { realignEditorPromptParts, resolveEditorSlashValue } from "./prompt.editor"
|
||||
import { FOOTER_MENU_ROWS, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
|
||||
import type { RunFooterTheme } from "./theme"
|
||||
|
|
@ -993,93 +993,83 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
return true
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: baseBindingsEnabled(),
|
||||
commands: [
|
||||
{
|
||||
name: "prompt.clear",
|
||||
id: "prompt.clear",
|
||||
title: "Clear prompt or exit",
|
||||
category: "Prompt",
|
||||
group: "Prompt",
|
||||
run() {
|
||||
if (requestExit()) return
|
||||
return false
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("prompt.clear"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: input.prompt(),
|
||||
commands: [
|
||||
{
|
||||
name: "session.interrupt",
|
||||
id: "session.interrupt",
|
||||
title: "Interrupt session",
|
||||
category: "Session",
|
||||
group: "Session",
|
||||
run() {
|
||||
if (input.onInterrupt()) return
|
||||
return false
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("session.interrupt"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: input.prompt() && !visible(),
|
||||
commands: [
|
||||
{
|
||||
name: "prompt.editor",
|
||||
id: "prompt.editor",
|
||||
title: "Open editor",
|
||||
category: "Prompt",
|
||||
group: "Prompt",
|
||||
run() {
|
||||
void openEditor()
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: input.tuiConfig.keybinds.get("prompt.editor"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
Keymap.createLayer(() => ({
|
||||
priority: 1,
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
enabled: input.prompt() && !visible(),
|
||||
commands: [
|
||||
{
|
||||
name: "prompt.history.previous",
|
||||
id: "prompt.history.previous",
|
||||
title: "Previous prompt history",
|
||||
category: "Prompt",
|
||||
run(ctx: { event: KeyEvent }) {
|
||||
return historyCommand(-1, ctx.event)
|
||||
group: "Prompt",
|
||||
run(_input: string | undefined, event?: KeyEvent) {
|
||||
if (!event) return false
|
||||
return historyCommand(-1, event)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompt.history.next",
|
||||
id: "prompt.history.next",
|
||||
title: "Next prompt history",
|
||||
category: "Prompt",
|
||||
run(ctx: { event: KeyEvent }) {
|
||||
return historyCommand(1, ctx.event)
|
||||
group: "Prompt",
|
||||
run(_input: string | undefined, event?: KeyEvent) {
|
||||
if (!event) return false
|
||||
return historyCommand(1, event)
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
...input.tuiConfig.keybinds.get("prompt.history.previous"),
|
||||
...input.tuiConfig.keybinds.get("prompt.history.next"),
|
||||
],
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: input.prompt() && !visible(),
|
||||
bindings: [
|
||||
commands: [
|
||||
{
|
||||
key: "!",
|
||||
desc: "Shell mode",
|
||||
bind: "!",
|
||||
title: "Shell mode",
|
||||
group: "Prompt",
|
||||
cmd() {
|
||||
run() {
|
||||
if (shell()) return false
|
||||
if (!area || area.isDestroyed) return false
|
||||
if (area.cursorOffset !== 0) return false
|
||||
|
|
@ -1089,21 +1079,20 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
],
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: input.prompt() && shell() && !visible(),
|
||||
bindings: [
|
||||
commands: [
|
||||
{
|
||||
key: "escape",
|
||||
desc: "Exit shell mode",
|
||||
bind: "escape",
|
||||
title: "Exit shell mode",
|
||||
group: "Prompt",
|
||||
cmd: () => setShellMode(false),
|
||||
run: () => setShellMode(false),
|
||||
},
|
||||
{
|
||||
key: "backspace",
|
||||
desc: "Exit shell mode",
|
||||
bind: "backspace",
|
||||
title: "Exit shell mode",
|
||||
group: "Prompt",
|
||||
cmd() {
|
||||
run() {
|
||||
if (!area || area.isDestroyed) return false
|
||||
if (area.cursorOffset !== 0) return false
|
||||
setShellMode(false)
|
||||
|
|
@ -1112,32 +1101,31 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
],
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: input.prompt() && visible(),
|
||||
commands: [
|
||||
{
|
||||
name: "prompt.autocomplete.prev",
|
||||
id: "prompt.autocomplete.prev",
|
||||
title: "Previous autocomplete item",
|
||||
category: "Autocomplete",
|
||||
group: "Autocomplete",
|
||||
run: () => menu.move(-1),
|
||||
},
|
||||
{
|
||||
name: "prompt.autocomplete.next",
|
||||
id: "prompt.autocomplete.next",
|
||||
title: "Next autocomplete item",
|
||||
category: "Autocomplete",
|
||||
group: "Autocomplete",
|
||||
run: () => menu.move(1),
|
||||
},
|
||||
{
|
||||
name: "prompt.autocomplete.hide",
|
||||
id: "prompt.autocomplete.hide",
|
||||
title: "Hide autocomplete",
|
||||
category: "Autocomplete",
|
||||
group: "Autocomplete",
|
||||
run: cancelAutocomplete,
|
||||
},
|
||||
{
|
||||
name: "prompt.autocomplete.select",
|
||||
id: "prompt.autocomplete.select",
|
||||
title: "Select autocomplete item",
|
||||
category: "Autocomplete",
|
||||
group: "Autocomplete",
|
||||
run() {
|
||||
if (mode() === "slash" && options().length === 0) {
|
||||
hide()
|
||||
|
|
@ -1147,9 +1135,9 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "prompt.autocomplete.complete",
|
||||
id: "prompt.autocomplete.complete",
|
||||
title: "Complete autocomplete item",
|
||||
category: "Autocomplete",
|
||||
group: "Autocomplete",
|
||||
run() {
|
||||
if (mode() === "slash" && options().length === 0) {
|
||||
hide()
|
||||
|
|
@ -1164,13 +1152,6 @@ export function createPromptState(input: PromptInput): PromptState {
|
|||
},
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
"prompt.autocomplete.prev",
|
||||
"prompt.autocomplete.next",
|
||||
"prompt.autocomplete.hide",
|
||||
"prompt.autocomplete.select",
|
||||
"prompt.autocomplete.complete",
|
||||
].flatMap((command) => input.tuiConfig.keybinds.get(command)),
|
||||
}))
|
||||
|
||||
const onKeyDown = (event: KeyEvent) => {
|
||||
|
|
|
|||
|
|
@ -24,12 +24,11 @@
|
|||
// Ctrl-c clears a live prompt draft first; otherwise interrupt and exit use a
|
||||
// two-press pattern where the first press shows a hint and the second press
|
||||
// within 5 seconds actually fires the action.
|
||||
import { CliRenderEvents, type CliRenderer, type KeyEvent, type Renderable, type TreeSitterClient } from "@opentui/core"
|
||||
import type { Keymap } from "@opentui/keymap"
|
||||
import { CliRenderEvents, type CliRenderer, type TreeSitterClient } from "@opentui/core"
|
||||
import { render } from "@opentui/solid"
|
||||
import { createComponent, createSignal, type Accessor, type Setter } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { OpencodeKeymapProvider } from "@opencode-ai/tui/keymap"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
import { RUN_COMMAND_PANEL_ROWS, RUN_SUBAGENT_PANEL_ROWS } from "./footer.command"
|
||||
import { SUBAGENT_INSPECTOR_ROWS } from "./footer.subagent"
|
||||
import { PROMPT_MAX_ROWS, TEXTAREA_MIN_ROWS } from "./footer.prompt"
|
||||
|
|
@ -82,7 +81,6 @@ type RunFooterOptions = {
|
|||
first: boolean
|
||||
history?: RunPrompt[]
|
||||
theme: RunTheme
|
||||
keymap: Keymap<Renderable, KeyEvent>
|
||||
tuiConfig: RunTuiConfig
|
||||
diffStyle: RunDiffStyle
|
||||
onPermissionReply: (input: PermissionReply) => void | Promise<void>
|
||||
|
|
@ -305,8 +303,8 @@ export class RunFooter implements FooterApi {
|
|||
const footer = this
|
||||
void render(
|
||||
() =>
|
||||
createComponent(OpencodeKeymapProvider, {
|
||||
keymap: options.keymap,
|
||||
createComponent(Keymap.Provider, {
|
||||
config: options.tuiConfig,
|
||||
get children() {
|
||||
return createComponent(RunFooterView, {
|
||||
directory: options.directory,
|
||||
|
|
|
|||
|
|
@ -27,14 +27,8 @@ import { RunPromptBody, createPromptState } from "./footer.prompt"
|
|||
import { RunPermissionBody } from "./footer.permission"
|
||||
import { RunQuestionBody } from "./footer.question"
|
||||
import { footerWidthPolicy } from "./footer.width"
|
||||
import {
|
||||
OPENCODE_BASE_MODE,
|
||||
formatKeyBindings,
|
||||
formatKeySequence,
|
||||
useBindings,
|
||||
useKeymapSelector,
|
||||
type OpenTuiKeymap,
|
||||
} from "@opencode-ai/tui/keymap"
|
||||
import { Keymap } from "@opencode-ai/tui/context/keymap"
|
||||
|
||||
import type {
|
||||
FooterPromptRoute,
|
||||
FooterQueuedPrompt,
|
||||
|
|
@ -177,75 +171,15 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const current = route()
|
||||
return current.type === "subagent" ? subagent().details[current.sessionID] : undefined
|
||||
})
|
||||
const command = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["command.palette.show"] })
|
||||
.get("command.palette.show")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const subagentShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.child.first"] })
|
||||
.get("session.child.first")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const queuedShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.queued_prompts"] })
|
||||
.get("session.queued_prompts")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const backgroundShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.background"] })
|
||||
.get("session.background")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const subagentInterruptShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["subagent.interrupt"] })
|
||||
.get("subagent.interrupt")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const interrupt = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap
|
||||
.getCommandBindings({ visibility: "registered", commands: ["session.interrupt"] })
|
||||
.get("session.interrupt")?.[0]?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const variantCycle = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeyBindings(
|
||||
keymap.getCommandBindings({ visibility: "registered", commands: ["variant.cycle"] }).get("variant.cycle"),
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const clearShortcut = useKeymapSelector(
|
||||
(keymap: OpenTuiKeymap) =>
|
||||
formatKeySequence(
|
||||
keymap.getCommandBindings({ visibility: "registered", commands: ["prompt.clear"] }).get("prompt.clear")?.[0]
|
||||
?.sequence,
|
||||
props.tuiConfig,
|
||||
) ?? "",
|
||||
)
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const command = () => shortcuts.get("command.palette.show") ?? ""
|
||||
const subagentShortcut = () => shortcuts.get("session.child.first") ?? ""
|
||||
const queuedShortcut = () => shortcuts.get("session.queued_prompts") ?? ""
|
||||
const backgroundShortcut = () => shortcuts.get("session.background") ?? ""
|
||||
const subagentInterruptShortcut = () => shortcuts.get("subagent.interrupt") ?? ""
|
||||
const interrupt = () => shortcuts.get("session.interrupt") ?? ""
|
||||
const variantCycle = () => shortcuts.all("variant.cycle") ?? ""
|
||||
const clearShortcut = () => shortcuts.get("prompt.clear") ?? ""
|
||||
const busy = createMemo(() => props.state().phase === "running")
|
||||
const armed = createMemo(() => props.state().interrupt > 0)
|
||||
const exiting = createMemo(() => props.state().exit > 0)
|
||||
|
|
@ -504,74 +438,62 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
props.onRequestExit?.(undefined)
|
||||
})
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: active().type === "prompt" && route().type === "composer" && !composer.visible(),
|
||||
commands: [
|
||||
{
|
||||
name: "command.palette.show",
|
||||
id: "command.palette.show",
|
||||
title: "Open command palette",
|
||||
category: "Prompt",
|
||||
group: "Prompt",
|
||||
run: openCommand,
|
||||
},
|
||||
{
|
||||
name: "variant.cycle",
|
||||
id: "variant.cycle",
|
||||
title: "Cycle model variant",
|
||||
category: "Model",
|
||||
group: "Model",
|
||||
run: props.onCycle,
|
||||
},
|
||||
],
|
||||
bindings: [
|
||||
...props.tuiConfig.keybinds.get("command.palette.show"),
|
||||
...props.tuiConfig.keybinds.get("variant.cycle"),
|
||||
],
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: active().type === "prompt" && route().type === "composer" && foregroundSubagents() && !!props.onBackground,
|
||||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
name: "session.background",
|
||||
id: "session.background",
|
||||
title: "Background subagents",
|
||||
category: "Session",
|
||||
group: "Session",
|
||||
run: () => props.onBackground?.(),
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.background"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: active().type === "prompt" && route().type === "composer" && tabs().length > 0,
|
||||
commands: [
|
||||
{
|
||||
name: "session.child.first",
|
||||
id: "session.child.first",
|
||||
title: "View subagents",
|
||||
category: "Session",
|
||||
group: "Session",
|
||||
run: openSubagentMenu,
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.child.first"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled: active().type === "prompt" && route().type === "composer" && queuedPrompts().length > 0,
|
||||
commands: [
|
||||
{
|
||||
name: "session.queued_prompts",
|
||||
id: "session.queued_prompts",
|
||||
title: "Manage queued prompts",
|
||||
category: "Session",
|
||||
group: "Session",
|
||||
run: openQueuedMenu,
|
||||
},
|
||||
],
|
||||
bindings: props.tuiConfig.keybinds.get("session.queued_prompts"),
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
mode: OPENCODE_BASE_MODE,
|
||||
Keymap.createLayer(() => ({
|
||||
enabled:
|
||||
active().type === "prompt" &&
|
||||
route().type === "subagent" &&
|
||||
|
|
@ -580,9 +502,10 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
priority: 1,
|
||||
commands: [
|
||||
{
|
||||
name: "subagent.interrupt",
|
||||
id: "subagent.interrupt",
|
||||
title: "Interrupt subagent",
|
||||
category: "Session",
|
||||
group: "Session",
|
||||
bind: "ctrl+d",
|
||||
run: () => {
|
||||
const current = selectedTab()
|
||||
if (current?.status !== "running") {
|
||||
|
|
@ -593,7 +516,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
},
|
||||
},
|
||||
],
|
||||
bindings: [{ key: "ctrl+d", desc: "Interrupt subagent", group: "Subagents", cmd: "subagent.interrupt" }],
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@
|
|||
// back to the usual two-press exit sequence through RunFooter.requestExit().
|
||||
import path from "path"
|
||||
import { CliRenderEvents, createCliRenderer, type CliRenderer, type ScrollbackWriter } from "@opentui/core"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { registerOpencodeKeymap } from "@opencode-ai/tui/keymap"
|
||||
import { isDefaultTitle } from "@opencode-ai/tui/util/session"
|
||||
import { Locale } from "@opencode-ai/tui/util/locale"
|
||||
import { resolveInteractiveStdin } from "./runtime.stdin"
|
||||
|
|
@ -167,8 +165,6 @@ function queueSplash(
|
|||
export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lifecycle> {
|
||||
const source = resolveInteractiveStdin()
|
||||
const footerTask = import("./footer")
|
||||
let unregisterKeymap: (() => void) | undefined
|
||||
|
||||
try {
|
||||
const renderer = await createCliRenderer({
|
||||
stdin: source.stdin,
|
||||
|
|
@ -187,8 +183,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
})
|
||||
const [theme, tuiConfig] = await Promise.all([resolveRunTheme(renderer), input.tuiConfig])
|
||||
renderer.setBackgroundColor(theme.background)
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
unregisterKeymap = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
||||
const state: SplashState = {
|
||||
entry: false,
|
||||
exit: false,
|
||||
|
|
@ -233,7 +227,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
history: input.history,
|
||||
theme,
|
||||
wrote,
|
||||
keymap,
|
||||
tuiConfig,
|
||||
diffStyle: tuiConfig.diff_style ?? "auto",
|
||||
onPermissionReply: input.onPermissionReply,
|
||||
|
|
@ -333,7 +326,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
footer.close()
|
||||
await footer.idle().catch(() => {})
|
||||
footer.destroy()
|
||||
unregisterKeymap?.()
|
||||
shutdown(renderer)
|
||||
if (!wroteExit) {
|
||||
process.stdout.write("\n")
|
||||
|
|
@ -391,7 +383,6 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
|||
close,
|
||||
}
|
||||
} catch (error) {
|
||||
unregisterKeymap?.()
|
||||
source.cleanup?.()
|
||||
throw error
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue