mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 23:52:06 +00:00
Apply PR #26246: use keymap state for layer visibility
This commit is contained in:
@@ -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: {
|
||||
<LocalProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandPaletteProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandPaletteProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</LocalProvider>
|
||||
@@ -269,7 +277,6 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
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<string[]> }) {
|
||||
const appCommands = createMemo(() =>
|
||||
[
|
||||
{
|
||||
name: "command.palette.show",
|
||||
name: COMMAND_PALETTE_COMMAND,
|
||||
title: "Show command palette",
|
||||
category: "System",
|
||||
hidden: true,
|
||||
run: () => {
|
||||
command.show()
|
||||
dialog.replace(() => <CommandPaletteDialog />)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -846,21 +853,18 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: command.matcher,
|
||||
opencodeMode: OPENCODE_BASE_MODE,
|
||||
bindings: tuiConfig.keybinds.gather(
|
||||
"app",
|
||||
Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
|
||||
? appBindingCommands
|
||||
: appBindingCommands.filter(
|
||||
(c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch"),
|
||||
),
|
||||
: appBindingCommands.filter((c) => !c.startsWith("session.cycle_recent") && !c.startsWith("session.quick_switch")),
|
||||
),
|
||||
}))
|
||||
|
||||
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 === ""
|
||||
@@ -869,7 +873,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
}))
|
||||
|
||||
event.on(TuiEvent.CommandExecute.type, (evt) => {
|
||||
command.run(evt.properties.command)
|
||||
keymap.dispatchCommand(evt.properties.command)
|
||||
})
|
||||
|
||||
event.on(TuiEvent.ToastShow.type, (evt) => {
|
||||
|
||||
@@ -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<OpenTuiKeymap["getCommandEntries"]>[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<string>
|
||||
const list = () => {
|
||||
if (ref?.filter) return options()
|
||||
return [
|
||||
...options()
|
||||
.filter((option) => option.suggested)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
value: `suggested:${option.value}`,
|
||||
category: "Suggested",
|
||||
})),
|
||||
...options(),
|
||||
]
|
||||
}
|
||||
|
||||
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -630,7 +634,7 @@ export function Prompt(props: PromptProps) {
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: command.matcher,
|
||||
opencodeMode: OPENCODE_BASE_MODE,
|
||||
bindings: tuiConfig.keybinds.gather("prompt.palette", [
|
||||
"prompt.submit",
|
||||
"prompt.editor",
|
||||
|
||||
@@ -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<readonly SlashEntry[]>
|
||||
suspend(enabled: boolean): void
|
||||
readonly suspended: boolean
|
||||
matcher: ReturnType<typeof reactiveMatcherFromSignal>
|
||||
}
|
||||
|
||||
const COMMAND_PALETTE_DIALOG = "command.palette.show"
|
||||
const ctx = createContext<CommandPaletteContext>()
|
||||
type PaletteCommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[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<SlashEntry[]>(() =>
|
||||
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(() => <CommandPaletteDialog run={run} />)
|
||||
},
|
||||
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 <ctx.Provider value={value}>{props.children}</ctx.Provider>
|
||||
}
|
||||
|
||||
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<string>
|
||||
const list = () => {
|
||||
if (ref?.filter) return options()
|
||||
return [
|
||||
...options()
|
||||
.filter((option) => option.suggested)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
value: `suggested:${option.value}`,
|
||||
category: "Suggested",
|
||||
})),
|
||||
...options(),
|
||||
]
|
||||
}
|
||||
|
||||
return <DialogSelect ref={(value) => (ref = value)} title="Commands" options={list()} />
|
||||
}
|
||||
|
||||
export function useCommandSlashes(): Accessor<readonly SlashEntry[]> {
|
||||
return useCommandPalette().slashes
|
||||
}
|
||||
@@ -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<typeof useKeymap>
|
||||
type OpencodeModeStack = ReturnType<typeof createOpencodeModeStack>
|
||||
type CommandSlashEntry = {
|
||||
display: string
|
||||
description?: string
|
||||
aliases?: string[]
|
||||
onSelect: () => void
|
||||
}
|
||||
type CommandEntry = ReturnType<OpenTuiKeymap["getCommandEntries"]>[number]
|
||||
|
||||
const modeStacks = new WeakMap<OpenTuiKeymap, OpencodeModeStack>()
|
||||
|
||||
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<string> {
|
||||
export function useLeaderActive(): Accessor<boolean> {
|
||||
return useKeymapSelector((keymap: OpenTuiKeymap) => keymap.getPendingSequence()[0]?.tokenName === LEADER_TOKEN)
|
||||
}
|
||||
|
||||
export function useCommandSlashes(): Accessor<readonly CommandSlashEntry[]> {
|
||||
const keymap = useOpencodeKeymap()
|
||||
const entries = useKeymapSelector((keymap: OpenTuiKeymap) =>
|
||||
keymap
|
||||
.getCommandEntries({
|
||||
visibility: "reachable",
|
||||
namespace: "palette",
|
||||
})
|
||||
.filter(isVisiblePaletteCommand),
|
||||
)
|
||||
|
||||
return createMemo<CommandSlashEntry[]>(() =>
|
||||
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),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -87,9 +87,8 @@ 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 { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
|
||||
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -100,23 +99,6 @@ const GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW = "go_upsell_account_rate_limit_don
|
||||
const GO_UPSELL_WINDOW = 86_400_000 // 24 hrs
|
||||
const GO_UPSELL_PROVIDERS = new Set(["opencode", "opencode-go"])
|
||||
|
||||
function goUpsellKeys(action: SessionRetry.Retryable["action"]) {
|
||||
if (!action) return
|
||||
if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
|
||||
if (action.reason === "free_tier_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
|
||||
}
|
||||
}
|
||||
if (action.reason === "account_rate_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sessionBindingCommands = [
|
||||
"session.share",
|
||||
"session.rename",
|
||||
@@ -153,6 +135,23 @@ const sessionBindingCommands = [
|
||||
"session.child.previous",
|
||||
] as const
|
||||
|
||||
function goUpsellKeys(action: SessionRetry.Retryable["action"]) {
|
||||
if (!action) return
|
||||
if (!GO_UPSELL_PROVIDERS.has(action.provider)) return
|
||||
if (action.reason === "free_tier_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_FREE_TIER_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_FREE_TIER_DONT_SHOW,
|
||||
}
|
||||
}
|
||||
if (action.reason === "account_rate_limit") {
|
||||
return {
|
||||
lastSeenAt: GO_UPSELL_ACCOUNT_RATE_LIMIT_LAST_SEEN_AT,
|
||||
dontShow: GO_UPSELL_ACCOUNT_RATE_LIMIT_DONT_SHOW,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = createContext<{
|
||||
width: number
|
||||
sessionID: string
|
||||
@@ -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() {
|
||||
<Switch>
|
||||
<Match when={message.id === revert()?.messageID}>
|
||||
{(function () {
|
||||
const command = useCommandPalette()
|
||||
const redoShortcut = useCommandShortcut("session.redo")
|
||||
const [hover, setHover] = createSignal(false)
|
||||
const dialog = useDialog()
|
||||
@@ -1132,12 +1130,12 @@ export function Session() {
|
||||
const confirmed = await DialogConfirm.show(
|
||||
dialog,
|
||||
"Confirm Redo",
|
||||
"Are you sure you want to restore the reverted messages?",
|
||||
)
|
||||
if (confirmed) {
|
||||
command.run("session.redo")
|
||||
}
|
||||
"Are you sure you want to restore the reverted messages?",
|
||||
)
|
||||
if (confirmed) {
|
||||
keymap.dispatchCommand("session.redo")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<box
|
||||
|
||||
@@ -13,11 +13,10 @@ import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { ShellID } from "@/tool/shell/id"
|
||||
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 { usePathFormatter } from "../../context/path-format"
|
||||
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
|
||||
|
||||
type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
@@ -448,9 +447,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 +540,10 @@ function Prompt<const T extends Record<string, string>>(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",
|
||||
|
||||
@@ -5,9 +5,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()
|
||||
@@ -118,9 +117,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||
pick(opt.label)
|
||||
}
|
||||
|
||||
const dialog = useDialog()
|
||||
|
||||
useBindings(() => ({
|
||||
opencodeMode: OPENCODE_BASE_MODE,
|
||||
enabled: store.editing && !confirm(),
|
||||
commands: [
|
||||
{
|
||||
@@ -201,7 +199,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",
|
||||
|
||||
@@ -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() {
|
||||
<box
|
||||
onMouseOver={() => setHover("parent")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.run("session.parent")}
|
||||
onMouseUp={() => keymap.dispatchCommand("session.parent")}
|
||||
backgroundColor={hover() === "parent" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
@@ -108,7 +107,7 @@ export function SubagentFooter() {
|
||||
<box
|
||||
onMouseOver={() => setHover("prev")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.run("session.child.previous")}
|
||||
onMouseUp={() => keymap.dispatchCommand("session.child.previous")}
|
||||
backgroundColor={hover() === "prev" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
@@ -118,7 +117,7 @@ export function SubagentFooter() {
|
||||
<box
|
||||
onMouseOver={() => setHover("next")}
|
||||
onMouseOut={() => setHover(null)}
|
||||
onMouseUp={() => command.run("session.child.next")}
|
||||
onMouseUp={() => keymap.dispatchCommand("session.child.next")}
|
||||
backgroundColor={hover() === "next" ? theme.backgroundElement : theme.backgroundPanel}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user