summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-12-11 19:34:34 +0000
committerDavid Hill <[email protected]>2025-12-11 19:34:34 +0000
commitafcd547a16ab3c8cf9f4ab72995a8bcd08980c11 (patch)
treedd763d766107684c460080aaac1d933e4977cb46 /packages
parent521fbb93cd41e60b1afcbba2ce4d940543c10a51 (diff)
parent9d73096db0ea2eb9e11c48b287855574651f33af (diff)
downloadopencode-afcd547a16ab3c8cf9f4ab72995a8bcd08980c11.tar.gz
opencode-afcd547a16ab3c8cf9f4ab72995a8bcd08980c11.zip
Merge branch 'dev' of https://github.com/sst/opencode into dev
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx4
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/keybind.tsx18
-rw-r--r--packages/opencode/src/config/config.ts4
-rw-r--r--packages/opencode/src/util/keybind.ts37
-rw-r--r--packages/opencode/test/keybind.test.ts104
-rw-r--r--packages/sdk/openapi.json4
-rw-r--r--packages/ui/src/styles/theme.css47
-rw-r--r--packages/web/src/content/docs/keybinds.mdx4
9 files changed, 171 insertions, 53 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
index 0ea4cbd68..38fd57458 100644
--- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx
@@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) {
<DialogSelect
keybind={[
{
- keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
+ keybind: Keybind.parse("ctrl+a")[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)
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 a5b6051ed..2e1ec3e42 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
import { Identifier } from "@/id/id"
import { createStore, produce } from "solid-js/store"
import { useKeybind } from "@tui/context/keybind"
+import { Keybind } from "@/util/keybind"
import { usePromptHistory, type PromptInfo } from "./history"
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
import { useCommandDialog } from "../dialog-command"
@@ -85,7 +86,7 @@ const TEXTAREA_ACTIONS = [
] as const
function mapTextareaKeybindings(
- keybinds: Record<string, { ctrl: boolean; meta: boolean; shift: boolean; leader: boolean; name: string }[]>,
+ keybinds: Record<string, Keybind.Info[]>,
action: (typeof TEXTAREA_ACTIONS)[number],
): KeyBinding[] {
const configKey = `input_${action.replace(/-/g, "_")}`
@@ -96,6 +97,7 @@ function mapTextareaKeybindings(
ctrl: binding.ctrl || undefined,
meta: binding.meta || undefined,
shift: binding.shift || undefined,
+ super: binding.super || undefined,
action,
}))
}
diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
index 50a29d2c5..4c82e594c 100644
--- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx
@@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
return store.leader
},
parse(evt: ParsedKey): Keybind.Info {
- if (evt.name === "\x1F")
- return {
- ctrl: true,
- name: "_",
- shift: false,
- leader: false,
- meta: false,
- }
- return {
- ctrl: evt.ctrl,
- name: evt.name,
- shift: evt.shift,
- leader: store.leader,
- meta: evt.meta,
+ // Handle special case for Ctrl+Underscore (represented as \x1F)
+ if (evt.name === "\x1F") {
+ return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
}
+ return Keybind.fromParsedKey(evt, store.leader)
},
match(key: keyof KeybindsConfig, evt: ParsedKey) {
const keybind = keybinds()[key]
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 25e9589f8..be1949c3b 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -512,8 +512,8 @@ export namespace Config {
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
- input_undo: z.string().optional().default("ctrl+-").describe("Undo in input"),
- input_redo: z.string().optional().default("ctrl+.").describe("Redo in input"),
+ input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
+ input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
input_word_forward: z
.string()
.optional()
diff --git a/packages/opencode/src/util/keybind.ts b/packages/opencode/src/util/keybind.ts
index 5beaf9aab..69fef28f0 100644
--- a/packages/opencode/src/util/keybind.ts
+++ b/packages/opencode/src/util/keybind.ts
@@ -1,16 +1,35 @@
import { isDeepEqual } from "remeda"
+import type { ParsedKey } from "@opentui/core"
export namespace Keybind {
- export type Info = {
- ctrl: boolean
- meta: boolean
- shift: boolean
- leader: boolean
- name: string
+ /**
+ * Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
+ * This ensures type compatibility and catches missing fields at compile time.
+ */
+ export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
+ leader: boolean // our custom field
}
export function match(a: Info, b: Info): boolean {
- return isDeepEqual(a, b)
+ // Normalize super field (undefined and false are equivalent)
+ const normalizedA = { ...a, super: a.super ?? false }
+ const normalizedB = { ...b, super: b.super ?? false }
+ return isDeepEqual(normalizedA, normalizedB)
+ }
+
+ /**
+ * Convert OpenTUI's ParsedKey to our Keybind.Info format.
+ * This helper ensures all required fields are present and avoids manual object creation.
+ */
+ export function fromParsedKey(key: ParsedKey, leader = false): Info {
+ return {
+ name: key.name,
+ ctrl: key.ctrl,
+ meta: key.meta,
+ shift: key.shift,
+ super: key.super ?? false,
+ leader,
+ }
}
export function toString(info: Info): string {
@@ -18,6 +37,7 @@ export namespace Keybind {
if (info.ctrl) parts.push("ctrl")
if (info.meta) parts.push("alt")
+ if (info.super) parts.push("super")
if (info.shift) parts.push("shift")
if (info.name) {
if (info.name === "delete") parts.push("del")
@@ -58,6 +78,9 @@ export namespace Keybind {
case "option":
info.meta = true
break
+ case "super":
+ info.super = true
+ break
case "shift":
info.shift = true
break
diff --git a/packages/opencode/test/keybind.test.ts b/packages/opencode/test/keybind.test.ts
index c09d6cbd3..4ca1f1697 100644
--- a/packages/opencode/test/keybind.test.ts
+++ b/packages/opencode/test/keybind.test.ts
@@ -68,6 +68,31 @@ describe("Keybind.toString", () => {
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: true, name: "" }
expect(Keybind.toString(info)).toBe("<leader>")
})
+
+ test("should convert super modifier to string", () => {
+ const info: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
+ expect(Keybind.toString(info)).toBe("super+z")
+ })
+
+ test("should convert super+shift modifier to string", () => {
+ const info: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
+ expect(Keybind.toString(info)).toBe("super+shift+z")
+ })
+
+ test("should handle super with ctrl modifier", () => {
+ const info: Keybind.Info = { ctrl: true, meta: false, shift: false, super: true, leader: false, name: "a" }
+ expect(Keybind.toString(info)).toBe("ctrl+super+a")
+ })
+
+ test("should handle super with all modifiers", () => {
+ const info: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "x" }
+ expect(Keybind.toString(info)).toBe("ctrl+alt+super+shift+x")
+ })
+
+ test("should handle undefined super field (omitted)", () => {
+ const info: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
+ expect(Keybind.toString(info)).toBe("ctrl+c")
+ })
})
describe("Keybind.match", () => {
@@ -118,6 +143,36 @@ describe("Keybind.match", () => {
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "a" }
expect(Keybind.match(a, b)).toBe(true)
})
+
+ test("should match super modifier keybinds", () => {
+ const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
+ const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
+ expect(Keybind.match(a, b)).toBe(true)
+ })
+
+ test("should not match super vs non-super", () => {
+ const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
+ const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: false, leader: false, name: "z" }
+ expect(Keybind.match(a, b)).toBe(false)
+ })
+
+ test("should match undefined super with false super", () => {
+ const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
+ const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "c" }
+ expect(Keybind.match(a, b)).toBe(true)
+ })
+
+ test("should match super+shift combination", () => {
+ const a: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
+ const b: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
+ expect(Keybind.match(a, b)).toBe(true)
+ })
+
+ test("should not match when only super differs", () => {
+ const a: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "a" }
+ const b: Keybind.Info = { ctrl: true, meta: true, shift: true, super: false, leader: false, name: "a" }
+ expect(Keybind.match(a, b)).toBe(false)
+ })
})
describe("Keybind.parse", () => {
@@ -314,4 +369,53 @@ describe("Keybind.parse", () => {
},
])
})
+
+ test("should parse super modifier", () => {
+ const result = Keybind.parse("super+z")
+ expect(result).toEqual([
+ {
+ ctrl: false,
+ meta: false,
+ shift: false,
+ super: true,
+ leader: false,
+ name: "z",
+ },
+ ])
+ })
+
+ test("should parse super with shift modifier", () => {
+ const result = Keybind.parse("super+shift+z")
+ expect(result).toEqual([
+ {
+ ctrl: false,
+ meta: false,
+ shift: true,
+ super: true,
+ leader: false,
+ name: "z",
+ },
+ ])
+ })
+
+ test("should parse multiple keybinds with super", () => {
+ const result = Keybind.parse("ctrl+-,super+z")
+ expect(result).toEqual([
+ {
+ ctrl: true,
+ meta: false,
+ shift: false,
+ leader: false,
+ name: "-",
+ },
+ {
+ ctrl: false,
+ meta: false,
+ shift: false,
+ super: true,
+ leader: false,
+ name: "z",
+ },
+ ])
+ })
})
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 937ab72e7..7c7c216f5 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -7333,12 +7333,12 @@
},
"input_undo": {
"description": "Undo in input",
- "default": "ctrl+-",
+ "default": "ctrl+-,super+z",
"type": "string"
},
"input_redo": {
"description": "Redo in input",
- "default": "ctrl+.",
+ "default": "ctrl+.,super+shift+z",
"type": "string"
},
"input_word_forward": {
diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css
index c5bb27497..2da926673 100644
--- a/packages/ui/src/styles/theme.css
+++ b/packages/ui/src/styles/theme.css
@@ -307,19 +307,18 @@
--border-weaker-focus: var(--smoke-light-alpha-6);
--button-ghost-hover: var(--smoke-light-alpha-2);
--button-ghost-hover2: var(--smoke-light-alpha-3);
- --avatar-background-pink: #FEEEF8;
- --avatar-background-mint: #E1FBF4 ;
- --avatar-background-orange: #FFF1E7 ;
- --avatar-background-purple: #F9F1FE;
- --avatar-background-cyan: #E7F9FB;
- --avatar-background-lime: #EEFADC;
- --avatar-text-pink: #CD1D8D;
- --avatar-text-mint: #147D6F ;
- --avatar-text-orange: #ED5F00 ;
- --avatar-text-purple: #8445BC;
- --avatar-text-cyan: #0894B3;
- --avatar-text-lime: #5D770D;
-
+ --avatar-background-pink: #feeef8;
+ --avatar-background-mint: #e1fbf4;
+ --avatar-background-orange: #fff1e7;
+ --avatar-background-purple: #f9f1fe;
+ --avatar-background-cyan: #e7f9fb;
+ --avatar-background-lime: #eefadc;
+ --avatar-text-pink: #cd1d8d;
+ --avatar-text-mint: #147d6f;
+ --avatar-text-orange: #ed5f00;
+ --avatar-text-purple: #8445bc;
+ --avatar-text-cyan: #0894b3;
+ --avatar-text-lime: #5d770d;
@media (prefers-color-scheme: dark) {
color-scheme: dark;
@@ -564,18 +563,18 @@
--border-weaker-focus: var(--smoke-dark-alpha-6);
--button-ghost-hover: var(--smoke-dark-alpha-2);
--button-ghost-hover2: var(--smoke-dark-alpha-3);
- --avatar-background-pink: #501B3F;
- --avatar-background-mint: #033A34;
- --avatar-background-orange: #5F2A06;
+ --avatar-background-pink: #501b3f;
+ --avatar-background-mint: #033a34;
+ --avatar-background-orange: #5f2a06;
--avatar-background-purple: #432155;
- --avatar-background-cyan: #0F3058;
- --avatar-background-lime: #2B3711;
- --avatar-text-pink: #E34BA9;
- --avatar-text-mint: #95F3D9 ;
- --avatar-text-orange: #FF802B ;
- --avatar-text-purple: #9D5BD2;
- --avatar-text-cyan: #369EFF;
- --avatar-text-lime: #C4F042;
+ --avatar-background-cyan: #0f3058;
+ --avatar-background-lime: #2b3711;
+ --avatar-text-pink: #e34ba9;
+ --avatar-text-mint: #95f3d9;
+ --avatar-text-orange: #ff802b;
+ --avatar-text-purple: #9d5bd2;
+ --avatar-text-cyan: #369eff;
+ --avatar-text-lime: #c4f042;
}
}
diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx
index 273ecf524..d9e99cd03 100644
--- a/packages/web/src/content/docs/keybinds.mdx
+++ b/packages/web/src/content/docs/keybinds.mdx
@@ -73,8 +73,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
"input_delete_to_line_start": "ctrl+u",
"input_backspace": "backspace,shift+backspace",
"input_delete": "ctrl+d,delete,shift+delete",
- "input_undo": "ctrl+-,cmd+z",
- "input_redo": "ctrl+.,cmd+shift+z",
+ "input_undo": "ctrl+-,super+z",
+ "input_redo": "ctrl+.,super+shift+z",
"input_word_forward": "alt+f,alt+right,ctrl+right",
"input_word_backward": "alt+b,alt+left,ctrl+left",
"input_select_word_forward": "alt+shift+f,alt+shift+right",