tui(run): use keymap instead of raw key events (#30077)
Co-authored-by: Sebastian Herrlinger <hasta84@gmail.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { createSignal } from "solid-js"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "@/cli/cmd/tui/keymap"
|
||||
import {
|
||||
RUN_COMMAND_PANEL_ROWS,
|
||||
RUN_SUBAGENT_PANEL_ROWS,
|
||||
@@ -15,7 +17,6 @@ import { RunFooterView } from "@/cli/cmd/run/footer.view"
|
||||
import { RunEntryContent } from "@/cli/cmd/run/scrollback.writer"
|
||||
import { RUN_THEME_FALLBACK } from "@/cli/cmd/run/theme"
|
||||
import type {
|
||||
FooterKeybinds,
|
||||
FooterState,
|
||||
FooterSubagentState,
|
||||
FooterSubagentTab,
|
||||
@@ -23,26 +24,14 @@ import type {
|
||||
RunCommand,
|
||||
RunInput,
|
||||
RunProvider,
|
||||
RunTuiConfig,
|
||||
StreamCommit,
|
||||
} from "@/cli/cmd/run/types"
|
||||
import { RunQuestionBody } from "@/cli/cmd/run/footer.question"
|
||||
import { RejectField } from "@/cli/cmd/run/footer.permission"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
function bindings(...keys: string[]) {
|
||||
return keys.map((key) => ({ key }))
|
||||
}
|
||||
|
||||
const keybinds: FooterKeybinds = {
|
||||
leader: "ctrl+x",
|
||||
leaderTimeout: 2000,
|
||||
commandList: bindings("ctrl+p"),
|
||||
variantCycle: bindings("ctrl+t"),
|
||||
interrupt: bindings("escape"),
|
||||
historyPrevious: bindings("up"),
|
||||
historyNext: bindings("down"),
|
||||
inputClear: bindings("ctrl+c"),
|
||||
inputSubmit: bindings("return"),
|
||||
inputNewline: bindings("shift+return,ctrl+return,alt+return,ctrl+j"),
|
||||
}
|
||||
const tuiConfig = createTuiResolvedConfig()
|
||||
|
||||
function command(input: { name: string; description: string; source?: "command" | "mcp" | "skill" }) {
|
||||
return {
|
||||
@@ -143,6 +132,90 @@ function subagent(input: {
|
||||
} satisfies FooterSubagentTab
|
||||
}
|
||||
|
||||
function footerState(input: Partial<FooterState> = {}) {
|
||||
return createSignal<FooterState>({
|
||||
phase: "idle",
|
||||
status: "",
|
||||
queue: 0,
|
||||
model: "gpt-5",
|
||||
duration: "",
|
||||
usage: "",
|
||||
first: false,
|
||||
interrupt: 0,
|
||||
exit: 0,
|
||||
...input,
|
||||
})[0]
|
||||
}
|
||||
|
||||
async function renderFooter(input: { tuiConfig?: RunTuiConfig; onCycle?: () => void } = {}) {
|
||||
const [view] = createSignal<FooterView>({ type: "prompt" })
|
||||
const [subagents] = createSignal<FooterSubagentState>({ tabs: [], details: {}, permissions: [], questions: [] })
|
||||
const state = footerState()
|
||||
const config = input.tuiConfig ?? tuiConfig
|
||||
let offKeymap: (() => void) | undefined
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
offKeymap = registerOpencodeKeymap(keymap, renderer, config)
|
||||
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<RunFooterView
|
||||
directory="/tmp"
|
||||
findFiles={async () => []}
|
||||
agents={() => []}
|
||||
resources={() => []}
|
||||
commands={() => []}
|
||||
providers={() => undefined}
|
||||
currentModel={() => undefined}
|
||||
variants={() => []}
|
||||
currentVariant={() => undefined}
|
||||
state={state}
|
||||
view={view}
|
||||
subagent={subagents}
|
||||
theme={RUN_THEME_FALLBACK}
|
||||
tuiConfig={config}
|
||||
agent="opencode"
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => { }}
|
||||
onQuestionReply={() => { }}
|
||||
onQuestionReject={() => { }}
|
||||
onCycle={input.onCycle ?? (() => { })}
|
||||
onInterrupt={() => false}
|
||||
onInputClear={() => { }}
|
||||
onExit={() => { }}
|
||||
onModelSelect={() => { }}
|
||||
onVariantSelect={() => { }}
|
||||
onRows={() => { }}
|
||||
onLayout={() => { }}
|
||||
onStatus={() => { }}
|
||||
/>
|
||||
</OpencodeKeymapProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={8}>
|
||||
<Harness />
|
||||
</box>
|
||||
),
|
||||
{ width: 100, height: 8, kittyKeyboard: true },
|
||||
)
|
||||
|
||||
return {
|
||||
...app,
|
||||
cleanup() {
|
||||
app.renderer.currentFocusedRenderable?.blur()
|
||||
app.renderer.currentFocusedEditor?.blur()
|
||||
offKeymap?.()
|
||||
offKeymap = undefined
|
||||
app.renderer.destroy()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test("run entry content updates when live commit text changes", async () => {
|
||||
const [commit, setCommit] = createSignal<StreamCommit>({
|
||||
kind: "tool",
|
||||
@@ -204,15 +277,15 @@ test("direct command panel renders grouped command palette", async () => {
|
||||
commands={commands}
|
||||
subagents={subagents}
|
||||
variants={variants}
|
||||
keybinds={keybinds}
|
||||
onClose={() => {}}
|
||||
onModel={() => {}}
|
||||
onSubagent={() => {}}
|
||||
onVariant={() => {}}
|
||||
onVariantCycle={() => {}}
|
||||
onCommand={() => {}}
|
||||
onNew={() => {}}
|
||||
onExit={() => {}}
|
||||
variantCycle="ctrl+t"
|
||||
onClose={() => { }}
|
||||
onModel={() => { }}
|
||||
onSubagent={() => { }}
|
||||
onVariant={() => { }}
|
||||
onVariantCycle={() => { }}
|
||||
onCommand={() => { }}
|
||||
onNew={() => { }}
|
||||
onExit={() => { }}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
@@ -262,15 +335,15 @@ test("direct command panel shows subagent entry when available", async () => {
|
||||
commands={commands}
|
||||
subagents={subagents}
|
||||
variants={variants}
|
||||
keybinds={keybinds}
|
||||
onClose={() => {}}
|
||||
onModel={() => {}}
|
||||
onSubagent={() => {}}
|
||||
onVariant={() => {}}
|
||||
onVariantCycle={() => {}}
|
||||
onCommand={() => {}}
|
||||
onNew={() => {}}
|
||||
onExit={() => {}}
|
||||
variantCycle="ctrl+t"
|
||||
onClose={() => { }}
|
||||
onModel={() => { }}
|
||||
onSubagent={() => { }}
|
||||
onVariant={() => { }}
|
||||
onVariantCycle={() => { }}
|
||||
onCommand={() => { }}
|
||||
onNew={() => { }}
|
||||
onExit={() => { }}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
@@ -306,8 +379,8 @@ test("direct subagent panel renders active subagents", async () => {
|
||||
theme={() => RUN_THEME_FALLBACK.footer}
|
||||
tabs={tabs}
|
||||
current={current}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
onClose={() => { }}
|
||||
onSelect={() => { }}
|
||||
onRows={(value) => {
|
||||
rows = value
|
||||
}}
|
||||
@@ -334,6 +407,61 @@ test("direct subagent panel renders active subagents", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
// OpenTUI currently segfaults when the full footer view suite creates several
|
||||
// keymap-backed test renderers in one process. Re-enable after the runtime fix.
|
||||
test.skip("direct footer opens command panel through keymap binding", async () => {
|
||||
const app = await renderFooter()
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("p", { ctrl: true })
|
||||
await app.renderOnce()
|
||||
|
||||
expect(app.captureCharFrame()).toContain("Commands")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test.skip("direct footer dispatches leader variant binding only when leader is registered", async () => {
|
||||
const calls: string[] = []
|
||||
const app = await renderFooter({
|
||||
tuiConfig: createTuiResolvedConfig({ keybinds: { leader: "ctrl+x", variant_cycle: "<leader>t" } }),
|
||||
onCycle: () => calls.push("cycle"),
|
||||
})
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("t")
|
||||
expect(calls).toEqual([])
|
||||
|
||||
app.mockInput.pressKey("x", { ctrl: true })
|
||||
app.mockInput.pressKey("t")
|
||||
expect(calls).toEqual(["cycle"])
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct footer keeps leader variant binding inactive when leader is disabled", async () => {
|
||||
const calls: string[] = []
|
||||
const app = await renderFooter({
|
||||
tuiConfig: createTuiResolvedConfig({ keybinds: { leader: "none", variant_cycle: "<leader>t" } }),
|
||||
onCycle: () => calls.push("cycle"),
|
||||
})
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("t")
|
||||
app.mockInput.pressKey("x", { ctrl: true })
|
||||
app.mockInput.pressKey("t")
|
||||
|
||||
expect(calls).toEqual([])
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct footer shows subagent indicator while prompt is running", async () => {
|
||||
const [state] = createSignal<FooterState>({
|
||||
phase: "running",
|
||||
@@ -353,10 +481,14 @@ test("direct footer shows subagent indicator while prompt is running", async ()
|
||||
permissions: [],
|
||||
questions: [],
|
||||
})
|
||||
let offKeymap: (() => void) | undefined
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
offKeymap = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={8}>
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<RunFooterView
|
||||
directory="/tmp"
|
||||
findFiles={async () => []}
|
||||
@@ -371,22 +503,30 @@ test("direct footer shows subagent indicator while prompt is running", async ()
|
||||
view={view}
|
||||
subagent={subagents}
|
||||
theme={RUN_THEME_FALLBACK}
|
||||
keybinds={keybinds}
|
||||
tuiConfig={tuiConfig}
|
||||
agent="opencode"
|
||||
onSubmit={() => true}
|
||||
onPermissionReply={() => {}}
|
||||
onQuestionReply={() => {}}
|
||||
onQuestionReject={() => {}}
|
||||
onCycle={() => {}}
|
||||
onPermissionReply={() => { }}
|
||||
onQuestionReply={() => { }}
|
||||
onQuestionReject={() => { }}
|
||||
onCycle={() => { }}
|
||||
onInterrupt={() => false}
|
||||
onInputClear={() => {}}
|
||||
onExit={() => {}}
|
||||
onModelSelect={() => {}}
|
||||
onVariantSelect={() => {}}
|
||||
onRows={() => {}}
|
||||
onLayout={() => {}}
|
||||
onStatus={() => {}}
|
||||
onInputClear={() => { }}
|
||||
onExit={() => { }}
|
||||
onModelSelect={() => { }}
|
||||
onVariantSelect={() => { }}
|
||||
onRows={() => { }}
|
||||
onLayout={() => { }}
|
||||
onStatus={() => { }}
|
||||
/>
|
||||
</OpencodeKeymapProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={8}>
|
||||
<Harness />
|
||||
</box>
|
||||
),
|
||||
{
|
||||
@@ -399,6 +539,9 @@ test("direct footer shows subagent indicator while prompt is running", async ()
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("interrupt · 1 agent · ↓ to view")
|
||||
} finally {
|
||||
app.renderer.currentFocusedRenderable?.blur()
|
||||
app.renderer.currentFocusedEditor?.blur()
|
||||
offKeymap?.()
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
@@ -429,7 +572,7 @@ test("direct question body separates single-select checkmark from label", async
|
||||
onReply={(input) => {
|
||||
replies.push(input)
|
||||
}}
|
||||
onReject={() => {}}
|
||||
onReject={() => { }}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
@@ -450,6 +593,120 @@ test("direct question body separates single-select checkmark from label", async
|
||||
}
|
||||
})
|
||||
|
||||
test("direct custom answer submits through keymap return binding", async () => {
|
||||
const question = {
|
||||
id: "question-1",
|
||||
sessionID: "session-1",
|
||||
questions: [
|
||||
{
|
||||
question: "Which answer should I use?",
|
||||
header: "Answer",
|
||||
options: [{ label: "Provided", description: "Use the listed answer." }],
|
||||
custom: true,
|
||||
},
|
||||
],
|
||||
} satisfies QuestionRequest
|
||||
const questions: unknown[] = []
|
||||
let off: (() => void) | undefined
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
off = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
||||
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<RunQuestionBody
|
||||
request={question}
|
||||
theme={RUN_THEME_FALLBACK.footer}
|
||||
onReply={(input) => {
|
||||
questions.push(input)
|
||||
}}
|
||||
onReject={() => { }}
|
||||
/>
|
||||
</OpencodeKeymapProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={18}>
|
||||
<Harness />
|
||||
</box>
|
||||
),
|
||||
{ width: 100, height: 18, kittyKeyboard: true },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("2")
|
||||
await app.renderOnce()
|
||||
"typed".split("").forEach((key) => app.mockInput.pressKey(key))
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressEnter()
|
||||
await app.renderOnce()
|
||||
expect(questions).toEqual([{ requestID: "question-1", answers: [["typed"]] }])
|
||||
} finally {
|
||||
app.renderer.currentFocusedRenderable?.blur()
|
||||
app.renderer.currentFocusedEditor?.blur()
|
||||
off?.()
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct permission rejection submits through keymap return binding", async () => {
|
||||
let text = ""
|
||||
const submits: string[] = []
|
||||
let off: (() => void) | undefined
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
off = registerOpencodeKeymap(keymap, renderer, tuiConfig)
|
||||
|
||||
return (
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<RejectField
|
||||
theme={RUN_THEME_FALLBACK.footer}
|
||||
text=""
|
||||
disabled={false}
|
||||
onChange={(input) => {
|
||||
text = input
|
||||
}}
|
||||
onConfirm={() => {
|
||||
submits.push(text)
|
||||
}}
|
||||
onCancel={() => { }}
|
||||
/>
|
||||
</OpencodeKeymapProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(
|
||||
() => (
|
||||
<box width={100} height={18}>
|
||||
<Harness />
|
||||
</box>
|
||||
),
|
||||
{ width: 100, height: 18, kittyKeyboard: true },
|
||||
)
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
"retry".split("").forEach((key) => app.mockInput.pressKey(key))
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("retry")
|
||||
app.mockInput.pressEnter()
|
||||
await app.renderOnce()
|
||||
expect(submits).toEqual(["retry"])
|
||||
} finally {
|
||||
app.renderer.currentFocusedRenderable?.blur()
|
||||
app.renderer.currentFocusedEditor?.blur()
|
||||
off?.()
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct model panel renders current model selector", async () => {
|
||||
const [providers] = createSignal<RunProvider[] | undefined>([provider()])
|
||||
const [current] = createSignal<RunInput["model"]>({ providerID: "opencode", modelID: "gpt-5" })
|
||||
@@ -461,8 +718,8 @@ test("direct model panel renders current model selector", async () => {
|
||||
theme={() => RUN_THEME_FALLBACK.footer}
|
||||
providers={providers}
|
||||
current={current}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
onClose={() => { }}
|
||||
onSelect={() => { }}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
@@ -500,8 +757,8 @@ test("direct variant panel renders current variant selector", async () => {
|
||||
theme={() => RUN_THEME_FALLBACK.footer}
|
||||
variants={variants}
|
||||
current={current}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
onClose={() => { }}
|
||||
onSelect={() => { }}
|
||||
/>
|
||||
</box>
|
||||
),
|
||||
|
||||
@@ -7,32 +7,10 @@ import {
|
||||
isNewCommand,
|
||||
mentionTriggerIndex,
|
||||
movePromptHistory,
|
||||
printableBinding,
|
||||
promptCycle,
|
||||
promptHit,
|
||||
promptInfo,
|
||||
promptKeys,
|
||||
pushPromptHistory,
|
||||
} from "@/cli/cmd/run/prompt.shared"
|
||||
import type { RunPrompt } from "@/cli/cmd/run/types"
|
||||
|
||||
function bindings(...keys: string[]) {
|
||||
return keys.map((key) => ({ key }))
|
||||
}
|
||||
|
||||
const keybinds = {
|
||||
leader: "ctrl+x",
|
||||
leaderTimeout: 2000,
|
||||
commandList: bindings("ctrl+p"),
|
||||
variantCycle: bindings("ctrl+t", "<leader>t"),
|
||||
interrupt: bindings("escape"),
|
||||
historyPrevious: bindings("up"),
|
||||
historyNext: bindings("down"),
|
||||
inputClear: bindings("ctrl+c"),
|
||||
inputSubmit: bindings("return"),
|
||||
inputNewline: bindings("shift+return,ctrl+return,alt+return,ctrl+j"),
|
||||
}
|
||||
|
||||
function prompt(text: string, parts: RunPrompt["parts"] = []): RunPrompt {
|
||||
return { text, parts }
|
||||
}
|
||||
@@ -141,39 +119,6 @@ describe("run prompt shared", () => {
|
||||
expect(mentionTriggerIndex("中文 @src file")).toBeUndefined()
|
||||
})
|
||||
|
||||
test("handles direct and leader-based variant cycling", () => {
|
||||
const keys = promptKeys(keybinds)
|
||||
|
||||
expect(promptHit(keys.clear, promptInfo({ name: "c", ctrl: true }))).toBe(true)
|
||||
|
||||
expect(promptCycle(false, promptInfo({ name: "x", ctrl: true }), keys.leaders, keys.cycles)).toEqual({
|
||||
arm: true,
|
||||
clear: false,
|
||||
cycle: false,
|
||||
consume: true,
|
||||
})
|
||||
|
||||
expect(promptCycle(true, promptInfo({ name: "t" }), keys.leaders, keys.cycles)).toEqual({
|
||||
arm: false,
|
||||
clear: true,
|
||||
cycle: true,
|
||||
consume: true,
|
||||
})
|
||||
|
||||
expect(promptCycle(false, promptInfo({ name: "t", ctrl: true }), keys.leaders, keys.cycles)).toEqual({
|
||||
arm: false,
|
||||
clear: false,
|
||||
cycle: true,
|
||||
consume: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("prints bindings with leader substitution and esc normalization", () => {
|
||||
expect(printableBinding(keybinds.variantCycle.slice(1), "ctrl+x")).toBe("ctrl+x t")
|
||||
expect(printableBinding(keybinds.interrupt, "ctrl+x")).toBe("esc")
|
||||
expect(printableBinding([], "ctrl+x")).toBe("")
|
||||
})
|
||||
|
||||
test("recognizes exit commands", () => {
|
||||
expect(isExitCommand("/exit")).toBe(true)
|
||||
expect(isExitCommand(" /Quit ")).toBe(true)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
|
||||
import { OpencodeClient, type Provider } from "@opencode-ai/sdk/v2"
|
||||
import { TuiConfig, type Resolved } from "@/cli/cmd/tui/config/tui"
|
||||
import { formatBindings } from "@/cli/cmd/run/keymap.shared"
|
||||
import { resolveDiffStyle, resolveFooterKeybinds, resolveModelInfo } from "@/cli/cmd/run/runtime.boot"
|
||||
import { resolveDiffStyle, resolveModelInfo, resolveRunTuiConfig } from "@/cli/cmd/run/runtime.boot"
|
||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||
|
||||
function model(id: string, providerID: string, context: number, variants?: Record<string, Record<string, never>>) {
|
||||
@@ -111,35 +110,44 @@ describe("run runtime boot", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await resolveFooterKeybinds()
|
||||
const result = await resolveRunTuiConfig()
|
||||
|
||||
expect(result.leader).toBe("ctrl+g")
|
||||
expect(result.leaderTimeout).toBe(2000)
|
||||
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
||||
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t, alt+t")
|
||||
expect(formatBindings(result.interrupt, result.leader)).toBe("ctrl+c")
|
||||
expect(formatBindings(result.historyPrevious, result.leader)).toBe("k")
|
||||
expect(formatBindings(result.historyNext, result.leader)).toBe("j")
|
||||
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+l")
|
||||
expect(formatBindings(result.inputSubmit, result.leader)).toBe("ctrl+s")
|
||||
expect(formatBindings(result.inputNewline, result.leader)).toBe("alt+return")
|
||||
expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+g")
|
||||
expect(result.leader_timeout).toBe(2000)
|
||||
expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p")
|
||||
expect(result.keybinds.get("variant.cycle").map((item) => item.key)).toEqual(["ctrl+t", "alt+t"])
|
||||
expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("ctrl+c")
|
||||
expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("k")
|
||||
expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("j")
|
||||
expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+l")
|
||||
expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("ctrl+s")
|
||||
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("alt+return")
|
||||
})
|
||||
|
||||
test("falls back to default keybinds when config load fails", async () => {
|
||||
test("falls back to default tui keymap config when config load fails", async () => {
|
||||
spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom"))
|
||||
|
||||
const result = await resolveFooterKeybinds()
|
||||
const result = await resolveRunTuiConfig()
|
||||
|
||||
expect(result.leader).toBe("ctrl+x")
|
||||
expect(result.leaderTimeout).toBe(2000)
|
||||
expect(formatBindings(result.commandList, result.leader)).toBe("ctrl+p")
|
||||
expect(formatBindings(result.variantCycle, result.leader)).toBe("ctrl+t")
|
||||
expect(formatBindings(result.interrupt, result.leader)).toBe("esc")
|
||||
expect(formatBindings(result.historyPrevious, result.leader)).toBe("up")
|
||||
expect(formatBindings(result.historyNext, result.leader)).toBe("down")
|
||||
expect(formatBindings(result.inputClear, result.leader)).toBe("ctrl+c")
|
||||
expect(formatBindings(result.inputSubmit, result.leader)).toBe("return")
|
||||
expect(formatBindings(result.inputNewline, result.leader)).toBe("shift+return, ctrl+return, alt+return, ctrl+j")
|
||||
expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+x")
|
||||
expect(result.leader_timeout).toBe(2000)
|
||||
expect(result.diff_style).toBe("auto")
|
||||
expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p")
|
||||
expect(result.keybinds.get("variant.cycle")?.[0]?.key).toBe("ctrl+t")
|
||||
expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("escape")
|
||||
expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("up")
|
||||
expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("down")
|
||||
expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+c")
|
||||
expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("return")
|
||||
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,alt+return,ctrl+j")
|
||||
})
|
||||
|
||||
test("preserves disabled leader from resolved tui config", async () => {
|
||||
spyOn(TuiConfig, "get").mockResolvedValue(config({ leader: "none" }))
|
||||
|
||||
const result = await resolveRunTuiConfig()
|
||||
|
||||
expect(result.keybinds.get("leader")).toEqual([])
|
||||
})
|
||||
|
||||
test("reads diff style and falls back to auto", async () => {
|
||||
|
||||
Reference in New Issue
Block a user