diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb6bd972e..1e5f56ba6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -85,7 +85,6 @@ jobs: publish-tauri: needs: publish - if: inputs.bump || inputs.version continue-on-error: true strategy: fail-fast: false @@ -130,7 +129,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: install dependencies (ubuntu only) - if: startsWith(matrix.settings.host, 'ubuntu') + if: contains(matrix.settings.host, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf @@ -162,7 +161,7 @@ jobs: # Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage - if: startsWith(matrix.settings.host, 'ubuntu') + if: contains(matrix.settings.host, 'ubuntu') - name: Build and upload artifacts uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a @@ -180,7 +179,7 @@ jobs: with: projectPath: packages/tauri uploadWorkflowArtifacts: true - tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} + tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }} args: --target ${{ matrix.settings.target }} updaterJsonPreferNsis: true releaseId: ${{ needs.publish.outputs.releaseId }} 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 }) { ) 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, + keybinds: Record, 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 & { + 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("") }) + + 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",