diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index b540f15726..60886839ae 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -66,8 +66,16 @@ import { createTuiApi } from "@/cli/cmd/tui/plugin/api" import type { RouteMap } from "@/cli/cmd/tui/plugin/api" import { createTuiAttention } from "@/cli/cmd/tui/attention" import { FormatError, FormatUnknownError } from "@/cli/error" -import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette" -import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap" +import { CommandPaletteDialog } from "./component/command-palette" +import { + COMMAND_PALETTE_COMMAND, + OPENCODE_BASE_MODE, + OpencodeKeymapProvider, + createOpencodeModeStack, + registerOpencodeKeymap, + useBindings, + useOpencodeKeymap, +} from "./keymap" import type { EventSource } from "./context/sdk" import { DialogVariant } from "./component/dialog-variant" @@ -180,6 +188,7 @@ export function tui(input: { } const onBeforeExit = async () => { offKeymap() + modeStack.dispose() await TuiPluginRuntime.dispose() TuiAudio.dispose() } @@ -190,6 +199,7 @@ export function tui(input: { const mode = (await renderer.waitForThemeMode(1000)) ?? "dark" const keymap = createDefaultOpenTuiKeymap(renderer) + const modeStack = createOpencodeModeStack(keymap) const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config) await render(() => { @@ -229,17 +239,15 @@ export function tui(input: { - - - - - - - - - - - + + + + + + + + + @@ -269,7 +277,6 @@ function App(props: { onSnapshot?: () => Promise }) { const dialog = useDialog() const local = useLocal() const kv = useKV() - const command = useCommandPalette() const keymap = useOpencodeKeymap() const event = useEvent() const sdk = useSDK() @@ -448,12 +455,12 @@ function App(props: { onSnapshot?: () => Promise }) { const appCommands = createMemo(() => [ { - name: "command.palette.show", + name: COMMAND_PALETTE_COMMAND, title: "Show command palette", category: "System", hidden: true, run: () => { - command.show() + dialog.replace(() => ) }, }, { @@ -836,7 +843,7 @@ function App(props: { onSnapshot?: () => Promise }) { })) useBindings(() => ({ - enabled: command.matcher, + opencodeMode: OPENCODE_BASE_MODE, bindings: tuiConfig.keybinds.gather( "app", Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING @@ -848,9 +855,8 @@ function App(props: { onSnapshot?: () => Promise }) { })) useBindings(() => ({ + opencodeMode: OPENCODE_BASE_MODE, enabled: () => { - const ok = command.matcher.get() - if (!ok) return false const current = promptRef.current if (!current?.focused) return true return current.current.input === "" @@ -859,7 +865,7 @@ function App(props: { onSnapshot?: () => Promise }) { })) event.on(TuiEvent.CommandExecute.type, (evt) => { - command.run(evt.properties.command) + keymap.dispatchCommand(evt.properties.command) }) event.on(TuiEvent.ToastShow.type, (evt) => { diff --git a/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx new file mode 100644 index 0000000000..ba2b1ebdc7 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/command-palette.tsx @@ -0,0 +1,80 @@ +import { createMemo } from "solid-js" +import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" +import { type DialogContext } from "@tui/ui/dialog" +import { + COMMAND_PALETTE_COMMAND, + formatKeyBindings, + type OpenTuiKeymap, + useKeymapSelector, + useOpencodeKeymap, +} from "../keymap" +import { useTuiConfig } from "../context/tui-config" + +type PaletteCommandEntry = ReturnType[number] + +function isVisiblePaletteCommand(entry: PaletteCommandEntry) { + return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND +} + +function isSuggestedPaletteCommand(entry: PaletteCommandEntry) { + const suggested = entry.command.suggested + if (typeof suggested === "boolean") return suggested + if (typeof suggested === "function") return suggested() === true + return false +} + +export function CommandPaletteDialog() { + const config = useTuiConfig() + const keymap = useOpencodeKeymap() + const entries = useKeymapSelector((keymap: OpenTuiKeymap) => { + const query = { + namespace: "palette", + } + const reachable = keymap + .getCommandEntries({ + ...query, + visibility: "reachable", + }) + .filter(isVisiblePaletteCommand) + const registeredBindings = keymap.getCommandBindings({ + visibility: "registered", + commands: reachable.map((entry) => entry.command.name), + }) + + return reachable.map((entry) => ({ + ...entry, + bindings: registeredBindings.get(entry.command.name) ?? entry.bindings, + })) + }) + const options = createMemo(() => + entries().map((entry) => ({ + title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name, + description: typeof entry.command.desc === "string" ? entry.command.desc : undefined, + category: typeof entry.command.category === "string" ? entry.command.category : undefined, + footer: formatKeyBindings(entry.bindings, config), + value: entry.command.name, + suggested: isSuggestedPaletteCommand(entry), + onSelect: (dialog: DialogContext) => { + dialog.clear() + keymap.dispatchCommand(entry.command.name) + }, + })), + ) + + let ref: DialogSelectRef + const list = () => { + if (ref?.filter) return options() + return [ + ...options() + .filter((option) => option.suggested) + .map((option) => ({ + ...option, + value: `suggested:${option.value}`, + category: "Suggested", + })), + ...options(), + ] + } + + return (ref = value)} title="Commands" options={list()} /> +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3f7604653c..b9e5aad74e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -12,12 +12,11 @@ import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" import { useTheme, selectedForeground } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" -import { useCommandPalette } from "../../context/command-palette" import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" -import { useBindings } from "../../keymap" +import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap" import { Reference } from "@/reference/reference" import type { Config } from "@/config/config" import { displayCharAt, mentionTriggerIndex } from "@/cli/cmd/prompt-display" @@ -85,7 +84,8 @@ export function Autocomplete(props: { const editor = useEditorContext() const sdk = useSDK() const sync = useSync() - const command = useCommandPalette() + const slashes = useCommandSlashes() + const modeStack = useOpencodeModeStack() const { theme } = useTheme() const dimensions = useTerminalDimensions() const frecency = useFrecency() @@ -99,6 +99,12 @@ export function Autocomplete(props: { const [positionTick, setPositionTick] = createSignal(0) + createEffect(() => { + if (!store.visible) return + const popMode = modeStack.push("autocomplete") + onCleanup(popMode) + }) + createEffect(() => { if (store.visible) { let lastPos = { x: 0, y: 0, width: 0 } @@ -365,7 +371,6 @@ export function Autocomplete(props: { const { filename, part } = createFilePart(item, lineRange) const index = store.visible === "@" ? store.index : props.input().cursorOffset - command.suspend(false) setStore("visible", false) setStore("index", index) insertPart(filename, part) @@ -536,7 +541,7 @@ export function Autocomplete(props: { ) const commands = createMemo((): AutocompleteOption[] => { - const results: AutocompleteOption[] = [...command.slashes()] + const results: AutocompleteOption[] = [...slashes()] for (const serverCommand of sync.data.command) { if (serverCommand.source === "skill") continue @@ -727,7 +732,6 @@ export function Autocomplete(props: { })) function show(mode: "@" | "/") { - command.suspend(true) setStore({ visible: mode, index: props.input().cursorOffset, @@ -744,7 +748,6 @@ export function Autocomplete(props: { draft.input = props.input().plainText }) } - command.suspend(false) setStore("visible", false) } 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 f6da08d6c0..a4cfc563f5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -59,8 +59,13 @@ import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable" import { useArgs } from "@tui/context/args" import { Flag } from "@opencode-ai/core/flag/flag" import { type WorkspaceStatus } from "../workspace-label" -import { useCommandPalette } from "../../context/command-palette" -import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap" +import { + OPENCODE_BASE_MODE, + useBindings, + useCommandShortcut, + useLeaderActive, + useOpencodeKeymap, +} from "../../keymap" import { useTuiConfig } from "../../context/tui-config" export type PromptProps = { @@ -152,7 +157,6 @@ export function Prompt(props: PromptProps) { const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" }) const history = usePromptHistory() const stash = usePromptStash() - const command = useCommandPalette() const keymap = useOpencodeKeymap() const agentShortcut = useCommandShortcut("agent.cycle") const paletteShortcut = useCommandShortcut("command.palette.show") @@ -640,7 +644,7 @@ export function Prompt(props: PromptProps) { })) useBindings(() => ({ - enabled: command.matcher, + opencodeMode: OPENCODE_BASE_MODE, bindings: tuiConfig.keybinds.gather("prompt.palette", [ "prompt.submit", "prompt.editor", diff --git a/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx b/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx deleted file mode 100644 index 07cca99074..0000000000 --- a/packages/opencode/src/cli/cmd/tui/context/command-palette.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { createContext, createMemo, createSignal, useContext, type Accessor, type ParentProps } from "solid-js" -import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" -import { useDialog, type DialogContext } from "@tui/ui/dialog" -import { - formatKeyBindings, - reactiveMatcherFromSignal, - type OpenTuiKeymap, - useKeymapSelector, - useOpencodeKeymap, -} from "../keymap" -import { useTuiConfig } from "./tui-config" - -type SlashEntry = { - display: string - description?: string - aliases?: string[] - onSelect: () => void -} - -type CommandPaletteContext = { - run(command: string): void - show(): void - slashes: Accessor - suspend(enabled: boolean): void - readonly suspended: boolean - matcher: ReturnType -} - -const COMMAND_PALETTE_DIALOG = "command.palette.show" -const ctx = createContext() -type PaletteCommandEntry = ReturnType[number] - -function isVisiblePaletteCommand(entry: PaletteCommandEntry) { - return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_DIALOG -} - -function isSuggestedPaletteCommand(entry: PaletteCommandEntry) { - const suggested = entry.command.suggested - if (typeof suggested === "boolean") return suggested - if (typeof suggested === "function") return suggested() === true - return false -} - -export function CommandPaletteProvider(props: ParentProps) { - const dialog = useDialog() - const keymap = useOpencodeKeymap() - const [suspendCount, setSuspendCount] = createSignal(0) - const entries = useKeymapSelector((keymap: OpenTuiKeymap) => - keymap - .getCommandEntries({ - visibility: "reachable", - namespace: "palette", - }) - .filter(isVisiblePaletteCommand), - ) - - const run = (command: string) => { - keymap.dispatchCommand(command) - } - - const slashes = createMemo(() => - entries().flatMap((entry) => { - const slashName = entry.command.slashName - if (typeof slashName !== "string" || !slashName) return [] - const slashAliases = entry.command.slashAliases - return { - display: `/${slashName}`, - description: - typeof entry.command.desc === "string" - ? entry.command.desc - : typeof entry.command.title === "string" - ? entry.command.title - : undefined, - aliases: Array.isArray(slashAliases) - ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`) - : undefined, - onSelect: () => run(entry.command.name), - } - }), - ) - - const value: CommandPaletteContext = { - run, - show() { - dialog.replace(() => ) - }, - slashes, - suspend(enabled: boolean) { - setSuspendCount((count) => Math.max(0, count + (enabled ? 1 : -1))) - }, - get suspended() { - return suspendCount() > 0 || dialog.stack.length > 0 - }, - matcher: reactiveMatcherFromSignal(() => suspendCount() === 0 && dialog.stack.length === 0), - } - - return {props.children} -} - -export function useCommandPalette() { - const value = useContext(ctx) - if (!value) throw new Error("CommandPalette context must be used within a CommandPaletteProvider") - return value -} - -function CommandPaletteDialog(props: { run(command: string): void }) { - const config = useTuiConfig() - const entries = useKeymapSelector((keymap: OpenTuiKeymap) => { - const query = { - namespace: "palette", - } - const reachable = keymap - .getCommandEntries({ - ...query, - visibility: "reachable", - }) - .filter(isVisiblePaletteCommand) - const registeredBindings = keymap.getCommandBindings({ - visibility: "registered", - commands: reachable.map((entry) => entry.command.name), - }) - - return reachable.map((entry) => ({ - ...entry, - bindings: registeredBindings.get(entry.command.name) ?? entry.bindings, - })) - }) - const options = createMemo(() => - entries().map((entry) => ({ - title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name, - description: typeof entry.command.desc === "string" ? entry.command.desc : undefined, - category: typeof entry.command.category === "string" ? entry.command.category : undefined, - footer: formatKeyBindings(entry.bindings, config), - value: entry.command.name, - suggested: isSuggestedPaletteCommand(entry), - onSelect: (dialog: DialogContext) => { - dialog.clear() - props.run(entry.command.name) - }, - })), - ) - - let ref: DialogSelectRef - const list = () => { - if (ref?.filter) return options() - return [ - ...options() - .filter((option) => option.suggested) - .map((option) => ({ - ...option, - value: `suggested:${option.value}`, - category: "Suggested", - })), - ...options(), - ] - } - - return (ref = value)} title="Commands" options={list()} /> -} - -export function useCommandSlashes(): Accessor { - return useCommandPalette().slashes -} diff --git a/packages/opencode/src/cli/cmd/tui/keymap.tsx b/packages/opencode/src/cli/cmd/tui/keymap.tsx index 289bb901d6..f68a88711f 100644 --- a/packages/opencode/src/cli/cmd/tui/keymap.tsx +++ b/packages/opencode/src/cli/cmd/tui/keymap.tsx @@ -7,24 +7,92 @@ import { } from "@opentui/keymap/extras" import { KeymapProvider, - reactiveMatcherFromSignal, useKeymap, useKeymapSelector, useBindings, } from "@opentui/keymap/solid" -import type { Accessor } from "solid-js" +import { createMemo, type Accessor } from "solid-js" import type { TuiConfig } from "./config/tui" import { useTuiConfig } from "./context/tui-config" import { TuiKeybind } from "./config/keybind" export const LEADER_TOKEN = "leader" +export const OPENCODE_BASE_MODE = "base" +export const COMMAND_PALETTE_COMMAND = "command.palette.show" + +const OPENCODE_MODE_KEY = "opencode.mode" export const OpencodeKeymapProvider = KeymapProvider export const useOpencodeKeymap = useKeymap -export { reactiveMatcherFromSignal, useBindings, useKeymapSelector } +export { useBindings, useKeymapSelector } export type OpenTuiKeymap = ReturnType +type OpencodeModeStack = ReturnType +type CommandSlashEntry = { + display: string + description?: string + aliases?: string[] + onSelect: () => void +} +type CommandEntry = ReturnType[number] + +const modeStacks = new WeakMap() + +function isVisiblePaletteCommand(entry: CommandEntry) { + return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND +} + +export function createOpencodeModeStack(keymap: OpenTuiKeymap) { + keymap.setData(OPENCODE_MODE_KEY, OPENCODE_BASE_MODE) + + const offFields = keymap.registerLayerFields({ + opencodeMode(value, ctx) { + ctx.require(OPENCODE_MODE_KEY, value) + }, + }) + + const stack: { id: symbol; mode: string }[] = [] + let disposed = false + + const update = () => { + keymap.setData(OPENCODE_MODE_KEY, stack.at(-1)?.mode ?? OPENCODE_BASE_MODE) + } + + const stackApi = { + push(mode: string) { + if (disposed) return () => {} + const id = Symbol(mode) + let active = true + stack.push({ id, mode }) + update() + + return () => { + if (!active) return + active = false + const index = stack.findIndex((item) => item.id === id) + if (index !== -1) stack.splice(index, 1) + update() + } + }, + dispose() { + if (disposed) return + disposed = true + stack.length = 0 + offFields() + keymap.setData(OPENCODE_MODE_KEY, undefined) + }, + } + + modeStacks.set(keymap, stackApi) + return stackApi +} + +export function useOpencodeModeStack() { + const value = modeStacks.get(useOpencodeKeymap()) + if (!value) throw new Error("Opencode mode stack is not registered for this keymap") + return value +} const KEY_ALIASES = { enter: "return", @@ -164,3 +232,36 @@ export function useCommandShortcut(command: string): Accessor { export function useLeaderActive(): Accessor { return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN) } + +export function useCommandSlashes(): Accessor { + const keymap = useOpencodeKeymap() + const entries = useKeymapSelector((keymap: OpenTuiKeymap) => + keymap + .getCommandEntries({ + visibility: "reachable", + namespace: "palette", + }) + .filter(isVisiblePaletteCommand), + ) + + return createMemo(() => + entries().flatMap((entry) => { + const slashName = entry.command.slashName + if (typeof slashName !== "string" || !slashName) return [] + const slashAliases = entry.command.slashAliases + return { + display: `/${slashName}`, + description: + typeof entry.command.desc === "string" + ? entry.command.desc + : typeof entry.command.title === "string" + ? entry.command.title + : undefined, + aliases: Array.isArray(slashAliases) + ? slashAliases.filter((alias): alias is string => typeof alias === "string").map((alias) => `/${alias}`) + : undefined, + onSelect: () => keymap.dispatchCommand(entry.command.name), + } + }), + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 70b5570ad5..1dfc67cb95 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -87,8 +87,7 @@ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime" import { DialogRetryAction } from "../../component/dialog-retry-action" import { SessionRetry } from "@/session/retry" import { getRevertDiffFiles } from "../../util/revert-diff" -import { useCommandPalette } from "../../context/command-palette" -import { useBindings, useCommandShortcut } from "../../keymap" +import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap" import { PathFormatterProvider, usePathFormatter } from "../../context/path-format" addDefaultParsers(parsers.parsers) @@ -306,7 +305,7 @@ export function Session() { seeded = true r.set(route.prompt) } - const command = useCommandPalette() + const keymap = useOpencodeKeymap() const dialog = useDialog() const renderer = useRenderer() @@ -1047,7 +1046,7 @@ export function Session() { })) useBindings(() => ({ - enabled: command.matcher, + opencodeMode: OPENCODE_BASE_MODE, bindings: tuiConfig.keybinds.gather("session", sessionBindingCommands), })) @@ -1123,7 +1122,6 @@ export function Session() { {(function () { - const command = useCommandPalette() const redoShortcut = useCommandShortcut("session.redo") const [hover, setHover] = createSignal(false) const dialog = useDialog() @@ -1135,7 +1133,7 @@ export function Session() { "Are you sure you want to restore the reverted messages?", ) if (confirmed) { - command.run("session.redo") + keymap.dispatchCommand("session.redo") } } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index 5b40c3c318..039adad482 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -16,7 +16,7 @@ import { webSearchProviderLabel } from "@/tool/websearch" import { useDialog } from "../../ui/dialog" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" -import { useBindings, useCommandShortcut } from "../../keymap" +import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap" import { usePathFormatter } from "../../context/path-format" type PermissionStage = "permission" | "always" | "reject" @@ -448,9 +448,8 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: ( const tuiConfig = useTuiConfig() const dimensions = useTerminalDimensions() const narrow = createMemo(() => dimensions().width < 80) - const dialog = useDialog() useBindings(() => ({ - enabled: dialog.stack.length === 0, + opencodeMode: OPENCODE_BASE_MODE, commands: [ { name: "app.exit", @@ -542,11 +541,10 @@ function Prompt>(props: { expanded: false, }) const narrow = createMemo(() => dimensions().width < 80) - const dialog = useDialog() const fullscreenHint = useCommandShortcut("permission.prompt.fullscreen") useBindings(() => ({ - enabled: dialog.stack.length === 0, + opencodeMode: OPENCODE_BASE_MODE, commands: [ { name: "app.exit", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx index 46fc220bdc..2fbd3baa40 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx @@ -6,9 +6,8 @@ import { selectedForeground, tint, useTheme } from "../../context/theme" import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2" import { useSDK } from "../../context/sdk" import { SplitBorder } from "../../component/border" -import { useDialog } from "../../ui/dialog" import { useTuiConfig } from "../../context/tui-config" -import { useBindings } from "../../keymap" +import { OPENCODE_BASE_MODE, useBindings } from "../../keymap" export function QuestionPrompt(props: { request: QuestionRequest }) { const sdk = useSDK() @@ -120,9 +119,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { pick(opt.label) } - const dialog = useDialog() - useBindings(() => ({ + opencodeMode: OPENCODE_BASE_MODE, enabled: store.editing && !confirm(), commands: [ { @@ -203,7 +201,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) { const max = Math.min(total, 9) return { - enabled: dialog.stack.length === 0 && !store.editing, + opencodeMode: OPENCODE_BASE_MODE, + enabled: !store.editing, commands: [ { name: "app.exit", diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx index f4a458b63d..e1be4286f1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/subagent-footer.tsx @@ -6,8 +6,7 @@ import { SplitBorder } from "@tui/component/border" import type { AssistantMessage } from "@opencode-ai/sdk/v2" import { Locale } from "@/util/locale" import { useTerminalDimensions } from "@opentui/solid" -import { useCommandPalette } from "../../context/command-palette" -import { useCommandShortcut } from "../../keymap" +import { useCommandShortcut, useOpencodeKeymap } from "../../keymap" export function SubagentFooter() { const route = useRouteData("session") @@ -56,7 +55,7 @@ export function SubagentFooter() { }) const { theme } = useTheme() - const command = useCommandPalette() + const keymap = useOpencodeKeymap() const parentShortcut = useCommandShortcut("session.parent") const previousShortcut = useCommandShortcut("session.child.previous") const nextShortcut = useCommandShortcut("session.child.next") @@ -98,7 +97,7 @@ export function SubagentFooter() { setHover("parent")} onMouseOut={() => setHover(null)} - onMouseUp={() => command.run("session.parent")} + onMouseUp={() => keymap.dispatchCommand("session.parent")} backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel} > @@ -108,7 +107,7 @@ export function SubagentFooter() { setHover("prev")} onMouseOut={() => setHover(null)} - onMouseUp={() => command.run("session.child.previous")} + onMouseUp={() => keymap.dispatchCommand("session.child.previous")} backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel} > @@ -118,7 +117,7 @@ export function SubagentFooter() { setHover("next")} onMouseOut={() => setHover(null)} - onMouseUp={() => command.run("session.child.next")} + onMouseUp={() => keymap.dispatchCommand("session.child.next")} backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel} > diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 3c74ded5fd..73d61c3ba6 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -1,12 +1,12 @@ import { useRenderer, useTerminalDimensions } from "@opentui/solid" -import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js" +import { batch, createContext, createEffect, onCleanup, Show, useContext, type JSX, type ParentProps } from "solid-js" import { useTheme } from "@tui/context/theme" import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { useToast } from "./toast" import { Flag } from "@opencode-ai/core/flag/flag" import * as Selection from "@tui/util/selection" -import { useBindings } from "../keymap" +import { useBindings, useOpencodeModeStack } from "../keymap" export function Dialog( props: ParentProps<{ @@ -73,6 +73,13 @@ function init() { }) const renderer = useRenderer() + const modeStack = useOpencodeModeStack() + + createEffect(() => { + if (store.stack.length === 0) return + const popMode = modeStack.push("modal") + onCleanup(popMode) + }) let focus: Renderable | null function refocus() {