Browse Source

Merge branch 'dev' of https://github.com/sst/opencode into dev

David Hill 2 months ago
parent
commit
afcd547a16

+ 3 - 4
.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 }}

+ 1 - 1
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 />)

+ 3 - 1
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,
   }))
 }

+ 4 - 14
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]

+ 2 - 2
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()

+ 30 - 7
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

+ 104 - 0
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",
+      },
+    ])
+  })
 })

+ 2 - 2
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": {

+ 23 - 24
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;
   }
 }
 

+ 2 - 2
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",