Compare commits

...

2 Commits

Author SHA1 Message Date
Kit Langton 9144e6d01a fix(tui): preserve legacy option enter newlines 2026-08-07 21:16:06 +00:00
opencode-agent[bot] 0b84e24e65 fix(tui): standardize compact terminology (#41141) 2026-08-07 16:26:14 -04:00
10 changed files with 80 additions and 11 deletions
+3 -2
View File
@@ -18,6 +18,7 @@ const BindingObject = Schema.StructWithRest(
Schema.Struct({
key: Schema.Union([Schema.String, KeyStroke]),
event: Schema.optional(Schema.Literals(["press", "release"])),
source: Schema.optional(Schema.Literals(["raw", "kitty"])),
preventDefault: Schema.optional(Schema.Boolean),
fallthrough: Schema.optional(Schema.Boolean),
}),
@@ -162,7 +163,7 @@ export const Definitions = {
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
prompt_submit: keybind("none", "Submit prompt"),
prompt_queue: keybind("alt+return", "Queue prompt"),
prompt_queue: keybind({ key: "alt+return", source: "kitty" }, "Queue prompt"),
prompt_editor_context_clear: keybind("none", "Clear editor context"),
prompt_skills: keybind("none", "Open skill selector"),
prompt_stash: keybind("none", "Stash prompt"),
@@ -172,7 +173,7 @@ export const Definitions = {
input_clear: keybind("ctrl+c", "Clear input field"),
input_paste: keybind({ key: "ctrl+v", preventDefault: false }, "Paste from clipboard"),
input_submit: keybind("return", "Submit input"),
input_newline: keybind("shift+return,ctrl+return,ctrl+j", "Insert newline in input"),
input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"),
input_move_left: keybind("left,ctrl+b", "Move cursor left in input"),
input_move_right: keybind("right,ctrl+f", "Move cursor right in input"),
input_move_up: keybind("up", "Move cursor up in input"),
+26
View File
@@ -66,6 +66,32 @@ function Provider(props: ParentProps<{ config?: KeymapConfig }>) {
}
}
const dispose = [
keymap.registerBindingFields({
source: (source, context) => {
if (source !== "raw" && source !== "kitty") throw new Error(`Invalid key source: ${String(source)}`)
context.attr("source", source)
},
}),
keymap.appendBindingTransformer((binding, context) => {
const source = binding.source
if (source !== "raw" && source !== "kitty") return
const command = binding.cmd
if (!command) return
context.add({
...binding,
cmd: (commandContext) => {
if (commandContext.event.source !== source) return false
if (typeof command === "function") return command(commandContext)
return commandContext.keymap.runCommand(command, {
event: commandContext.event,
focused: commandContext.focused,
target: commandContext.target,
payload: commandContext.payload,
})
},
})
context.skipOriginal()
}),
registerCommaBindings(keymap),
keymap.appendBindingExpander((context) => {
const key = Object.entries({ enter: "return", esc: "escape", pgdown: "pagedown", pgup: "pageup" }).reduce(
+1 -1
View File
@@ -407,7 +407,7 @@ export function RunCommandMenuBody(props: {
name: "compact",
display: "Compact session",
footer: "/compact",
keywords: "compact summarize session context",
keywords: "compact session context",
},
{
action: "slash",
+1 -1
View File
@@ -406,7 +406,7 @@ export function createPromptState(input: PromptInput): PromptState {
kind: "slash",
name: "compact",
display: "/compact",
description: "summarize the session to reduce context usage",
description: "compact older session context to free space",
} satisfies SlashOption,
{ kind: "slash", name: "exit", display: "/exit", description: "close OpenCode" } satisfies SlashOption,
]
+1 -2
View File
@@ -54,8 +54,7 @@ export function isNewCommand(input: string): boolean {
}
export function isCompactCommand(input: string): boolean {
const text = input.trim().toLowerCase()
return text === "/compact" || text === "/summarize"
return input.trim().toLowerCase() === "/compact"
}
export function createPromptHistory(items?: RunPrompt[]): PromptHistoryState {
@@ -635,7 +635,6 @@ export function Session() {
group: "Session",
slash: {
name: "compact",
aliases: ["summarize"],
},
run: () => {
void client.api.session.compact({ sessionID: route.sessionID })
+37
View File
@@ -1,5 +1,6 @@
/** @jsxImportSource @opentui/solid */
import { testRender } from "@opentui/solid"
import type { TextareaRenderable } from "@opentui/core"
import { expect, test } from "bun:test"
import { ConfigProvider } from "../src/config"
import { Keymap } from "../src/context/keymap"
@@ -139,3 +140,39 @@ test("global commands stay reachable when the mode changes", async () => {
app.renderer.destroy()
}
})
test("queues explicit option enter and keeps raw option enter as newline", async () => {
async function exercise(kittyKeyboard: boolean) {
let area: TextareaRenderable | undefined
let queued = 0
function Harness() {
Keymap.createLayer(() => ({
priority: 1,
commands: [{ id: "prompt.queue", run: () => void queued++ }],
}))
return <textarea ref={(value) => (area = value)} focused />
}
const app = await testRender(
() => (
<ConfigProvider config={createTuiResolvedConfig()}>
<Keymap.Provider>
<Harness />
</Keymap.Provider>
</ConfigProvider>
),
{ kittyKeyboard },
)
try {
app.mockInput.pressEnter({ meta: true })
await app.renderOnce()
return { queued, text: area?.plainText }
} finally {
app.renderer.destroy()
}
}
expect(await exercise(true)).toEqual({ queued: 1, text: "" })
expect(await exercise(false)).toEqual({ queued: 0, text: "\n" })
})
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import {
createPromptHistory,
isCompactCommand,
isExitCommand,
isNewCommand,
movePromptHistory,
@@ -98,4 +99,10 @@ describe("run prompt shared", () => {
expect(isNewCommand(" /NEW ")).toBe(true)
expect(isNewCommand("/new now")).toBe(false)
})
test("recognizes only the compact command", () => {
expect(isCompactCommand("/compact")).toBe(true)
expect(isCompactCommand(" /COMPACT ")).toBe(true)
expect(isCompactCommand("/summarize")).toBe(false)
})
})
+2 -2
View File
@@ -82,8 +82,8 @@ describe("run runtime boot", () => {
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,ctrl+j")
expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("alt+return")
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,alt+return,ctrl+j")
expect(result.keybinds.get("prompt.queue")?.[0]).toMatchObject({ key: "alt+return", source: "kitty" })
})
test("preserves disabled leader from resolved tui config", async () => {
+2 -2
View File
@@ -80,7 +80,7 @@ describe("run runtime queue", () => {
])
})
test.each(["/compact", "/summarize"])("treats %s as a local compaction command", async (command) => {
test("treats /compact as a local compaction command", async () => {
const ui = createFooterApiFixture()
const seen: string[] = []
let compacted = 0
@@ -96,7 +96,7 @@ describe("run runtime queue", () => {
},
})
ui.submit(command)
ui.submit("/compact")
ui.submit("hello")
await task