Skip to content

Commit 4f81992

Browse files
Apply PR #26246: use keymap state for layer visibility
2 parents 7aad96c + 29b7871 commit 4f81992

11 files changed

Lines changed: 252 additions & 220 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,16 @@ import { TuiPluginRuntime } from "@/cli/cmd/tui/plugin/runtime"
6464
import { createTuiApi } from "@/cli/cmd/tui/plugin/api"
6565
import type { RouteMap } from "@/cli/cmd/tui/plugin/api"
6666
import { FormatError, FormatUnknownError } from "@/cli/error"
67-
import { CommandPaletteProvider, useCommandPalette } from "./context/command-palette"
68-
import { OpencodeKeymapProvider, registerOpencodeKeymap, useBindings, useOpencodeKeymap } from "./keymap"
67+
import { CommandPaletteDialog } from "./component/command-palette"
68+
import {
69+
COMMAND_PALETTE_COMMAND,
70+
OPENCODE_BASE_MODE,
71+
OpencodeKeymapProvider,
72+
createOpencodeModeStack,
73+
registerOpencodeKeymap,
74+
useBindings,
75+
useOpencodeKeymap,
76+
} from "./keymap"
6977

7078
import type { EventSource } from "./context/sdk"
7179
import { DialogVariant } from "./component/dialog-variant"
@@ -133,6 +141,7 @@ export function tui(input: {
133141

134142
const onBeforeExit = async () => {
135143
offKeymap()
144+
modeStack.dispose()
136145
await TuiPluginRuntime.dispose()
137146
}
138147

@@ -142,6 +151,7 @@ export function tui(input: {
142151
const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
143152

144153
const keymap = createDefaultOpenTuiKeymap(renderer)
154+
const modeStack = createOpencodeModeStack(keymap)
145155
const offKeymap = registerOpencodeKeymap(keymap, renderer, input.config)
146156

147157
await render(() => {
@@ -181,17 +191,15 @@ export function tui(input: {
181191
<LocalProvider>
182192
<PromptStashProvider>
183193
<DialogProvider>
184-
<CommandPaletteProvider>
185-
<FrecencyProvider>
186-
<PromptHistoryProvider>
187-
<PromptRefProvider>
188-
<EditorContextProvider>
189-
<App onSnapshot={input.onSnapshot} />
190-
</EditorContextProvider>
191-
</PromptRefProvider>
192-
</PromptHistoryProvider>
193-
</FrecencyProvider>
194-
</CommandPaletteProvider>
194+
<FrecencyProvider>
195+
<PromptHistoryProvider>
196+
<PromptRefProvider>
197+
<EditorContextProvider>
198+
<App onSnapshot={input.onSnapshot} />
199+
</EditorContextProvider>
200+
</PromptRefProvider>
201+
</PromptHistoryProvider>
202+
</FrecencyProvider>
195203
</DialogProvider>
196204
</PromptStashProvider>
197205
</LocalProvider>
@@ -224,7 +232,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
224232
const dialog = useDialog()
225233
const local = useLocal()
226234
const kv = useKV()
227-
const command = useCommandPalette()
228235
const keymap = useOpencodeKeymap()
229236
const event = useEvent()
230237
const sdk = useSDK()
@@ -397,11 +404,11 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
397404
const appCommands = createMemo(() =>
398405
[
399406
{
400-
name: "command.palette.show",
407+
name: COMMAND_PALETTE_COMMAND,
401408
title: "Show command palette",
402409
hidden: true,
403410
run: () => {
404-
command.show()
411+
dialog.replace(() => <CommandPaletteDialog />)
405412
},
406413
},
407414
{
@@ -758,12 +765,12 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
758765
}))
759766

760767
useBindings(() => ({
761-
enabled: command.matcher,
768+
opencodeMode: OPENCODE_BASE_MODE,
762769
bindings: sections.global,
763770
}))
764771

765772
event.on(TuiEvent.CommandExecute.type, (evt) => {
766-
command.run(evt.properties.command)
773+
keymap.dispatchCommand(evt.properties.command)
767774
})
768775

769776
event.on(TuiEvent.ToastShow.type, (evt) => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createMemo } from "solid-js"
2+
import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select"
3+
import { type DialogContext } from "@tui/ui/dialog"
4+
import {
5+
COMMAND_PALETTE_COMMAND,
6+
formatKeyBindings,
7+
type OpenTuiKeymap,
8+
useKeymapSelector,
9+
useOpencodeKeymap,
10+
} from "../keymap"
11+
import { useTuiConfig } from "../context/tui-config"
12+
13+
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
14+
15+
function isVisiblePaletteCommand(entry: PaletteCommandEntry) {
16+
return entry.command.hidden !== true && entry.command.name !== COMMAND_PALETTE_COMMAND
17+
}
18+
19+
function isSuggestedPaletteCommand(entry: PaletteCommandEntry) {
20+
const suggested = entry.command.suggested
21+
if (typeof suggested === "boolean") return suggested
22+
if (typeof suggested === "function") return suggested() === true
23+
return false
24+
}
25+
26+
export function CommandPaletteDialog() {
27+
const config = useTuiConfig()
28+
const keymap = useOpencodeKeymap()
29+
const entries = useKeymapSelector((keymap: OpenTuiKeymap) => {
30+
const query = {
31+
namespace: "palette",
32+
}
33+
const reachable = keymap
34+
.getCommandEntries({
35+
...query,
36+
visibility: "reachable",
37+
})
38+
.filter(isVisiblePaletteCommand)
39+
const registeredBindings = keymap.getCommandBindings({
40+
visibility: "registered",
41+
commands: reachable.map((entry) => entry.command.name),
42+
})
43+
44+
return reachable.map((entry) => ({
45+
...entry,
46+
bindings: registeredBindings.get(entry.command.name) ?? entry.bindings,
47+
}))
48+
})
49+
const options = createMemo(() =>
50+
entries().map((entry) => ({
51+
title: typeof entry.command.title === "string" ? entry.command.title : entry.command.name,
52+
description: typeof entry.command.desc === "string" ? entry.command.desc : undefined,
53+
category: typeof entry.command.category === "string" ? entry.command.category : undefined,
54+
footer: formatKeyBindings(entry.bindings, config),
55+
value: entry.command.name,
56+
suggested: isSuggestedPaletteCommand(entry),
57+
onSelect: (dialog: DialogContext) => {
58+
dialog.clear()
59+
keymap.dispatchCommand(entry.command.name)
60+
},
61+
})),
62+
)
63+
64+
let ref: DialogSelectRef<string>
65+
const list = () => {
66+
if (ref?.filter) return options()
67+
return [
68+
...options()
69+
.filter((option) => option.suggested)
70+
.map((option) => ({
71+
...option,
72+
value: `suggested:${option.value}`,
73+
category: "Suggested",
74+
})),
75+
...options(),
76+
]
77+
}
78+
79+
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
80+
}

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ import { getScrollAcceleration } from "../../util/scroll"
1212
import { useTuiConfig } from "../../context/tui-config"
1313
import { useTheme, selectedForeground } from "@tui/context/theme"
1414
import { SplitBorder } from "@tui/component/border"
15-
import { useCommandPalette } from "../../context/command-palette"
1615
import { useTerminalDimensions } from "@opentui/solid"
1716
import { Locale } from "@/util/locale"
1817
import type { PromptInfo } from "./history"
1918
import { useFrecency } from "./frecency"
20-
import { useBindings } from "../../keymap"
19+
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
2120

2221
function removeLineRange(input: string) {
2322
const hashIndex = input.lastIndexOf("#")
@@ -82,7 +81,8 @@ export function Autocomplete(props: {
8281
const editor = useEditorContext()
8382
const sdk = useSDK()
8483
const sync = useSync()
85-
const command = useCommandPalette()
84+
const slashes = useCommandSlashes()
85+
const modeStack = useOpencodeModeStack()
8686
const { theme } = useTheme()
8787
const dimensions = useTerminalDimensions()
8888
const frecency = useFrecency()
@@ -99,6 +99,12 @@ export function Autocomplete(props: {
9999

100100
const [positionTick, setPositionTick] = createSignal(0)
101101

102+
createEffect(() => {
103+
if (!store.visible) return
104+
const popMode = modeStack.push("autocomplete")
105+
onCleanup(popMode)
106+
})
107+
102108
createEffect(() => {
103109
if (store.visible) {
104110
let lastPos = { x: 0, y: 0, width: 0 }
@@ -284,7 +290,6 @@ export function Autocomplete(props: {
284290
const { filename, part } = createFilePart(item, lineRange)
285291
const index = store.visible === "@" ? store.index : props.input().cursorOffset
286292

287-
command.suspend(false)
288293
setStore("visible", false)
289294
setStore("index", index)
290295
insertPart(filename, part)
@@ -401,7 +406,7 @@ export function Autocomplete(props: {
401406
})
402407

403408
const commands = createMemo((): AutocompleteOption[] => {
404-
const results: AutocompleteOption[] = [...command.slashes()]
409+
const results: AutocompleteOption[] = [...slashes()]
405410

406411
for (const serverCommand of sync.data.command) {
407412
if (serverCommand.source === "skill") continue
@@ -569,7 +574,6 @@ export function Autocomplete(props: {
569574
}))
570575

571576
function show(mode: "@" | "/") {
572-
command.suspend(true)
573577
setStore({
574578
visible: mode,
575579
index: props.input().cursorOffset,
@@ -586,7 +590,6 @@ export function Autocomplete(props: {
586590
draft.input = props.input().plainText
587591
})
588592
}
589-
command.suspend(false)
590593
setStore("visible", false)
591594
}
592595

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ import { DialogWorkspaceUnavailable } from "../dialog-workspace-unavailable"
5959
import { useArgs } from "@tui/context/args"
6060
import { Flag } from "@opencode-ai/core/flag/flag"
6161
import { type WorkspaceStatus } from "../workspace-label"
62-
import { useCommandPalette } from "../../context/command-palette"
63-
import { useBindings, useCommandShortcut, useLeaderActive, useOpencodeKeymap } from "../../keymap"
62+
import {
63+
OPENCODE_BASE_MODE,
64+
useBindings,
65+
useCommandShortcut,
66+
useLeaderActive,
67+
useOpencodeKeymap,
68+
} from "../../keymap"
6469
import { useTuiConfig } from "../../context/tui-config"
6570

6671
export type PromptProps = {
@@ -153,7 +158,6 @@ export function Prompt(props: PromptProps) {
153158
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
154159
const history = usePromptHistory()
155160
const stash = usePromptStash()
156-
const command = useCommandPalette()
157161
const keymap = useOpencodeKeymap()
158162
const agentShortcut = useCommandShortcut("agent.cycle")
159163
const paletteShortcut = useCommandShortcut("command.palette.show")
@@ -639,7 +643,7 @@ export function Prompt(props: PromptProps) {
639643
}))
640644

641645
useBindings(() => ({
642-
enabled: command.matcher,
646+
opencodeMode: OPENCODE_BASE_MODE,
643647
bindings: keymapConfig.pick("prompt", [
644648
"prompt.submit",
645649
"prompt.editor",

0 commit comments

Comments
 (0)