diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7269803..aecc641 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,7 +74,13 @@ This repo uses `gitleaks` for local hooks and CI history scanning with a version Install `gitleaks` once: ```bash +# macOS brew install gitleaks + +# Windows (scoop) +scoop install gitleaks + +# Or download from https://github.com/gitleaks/gitleaks/releases ``` Useful commands: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3197299..818b8c3 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -131,14 +131,17 @@ npx serve out ./scripts/build-release.sh web --serve ``` -### Desktop (macOS DMG) +### Desktop (Windows, macOS, Linux) ```bash -# Apple Silicon (aarch64) DMG +# Build for current platform ./scripts/build-release.sh desktop + +# macOS: Universal binary (arm64 + Intel) +./scripts/build-release.sh desktop --universal ``` -The DMG is output to `src-tauri/target/.../bundle/dmg/`. +Output location: `src-tauri/target/.../bundle/` (`.msi`/`.exe` on Windows, `.app`/`.dmg` on macOS, `.deb`/`.AppImage` on Linux). **First build takes 5–10 minutes** (Rust compilation). Subsequent builds are cached and much faster. @@ -199,19 +202,21 @@ The `release.yml` workflow: ## Keyboard Shortcuts -| Shortcut | Action | -| -------- | --------------------------- | -| `⌘B` | Toggle file explorer | -| `⌘J` | Toggle terminal | -| `⌘\` | Toggle sidebar | -| `⌘P` | Quick open file | -| `⌘⇧I` | Isolate component (preview) | -| `⌘K` | Inline edit | -| `⌘L` | Send selection to chat | -| `⌘S` | Save file | -| `⌘⇧F` | Global search | -| `⌘⇧P` | Command palette | -| `Esc` | Close overlays | +Use **Cmd** on macOS, **Ctrl** on Windows/Linux. + +| Shortcut | Action | +| ------------- | --------------------------- | +| `Cmd/Ctrl+B` | Toggle file explorer | +| `Cmd/Ctrl+J` | Toggle terminal | +| `Cmd/Ctrl+\` | Toggle sidebar | +| `Cmd/Ctrl+P` | Quick open file | +| `Cmd/Ctrl+⇧I` | Isolate component (preview) | +| `Cmd/Ctrl+K` | Inline edit | +| `Cmd/Ctrl+L` | Send selection to chat | +| `Cmd/Ctrl+S` | Save file | +| `Cmd/Ctrl+⇧F` | Global search | +| `Cmd/Ctrl+⇧P` | Command palette | +| `Esc` | Close overlays | --- @@ -237,11 +242,11 @@ To add a new theme: ## Preview System -The preview panel (`⌘3` or click Preview tab) connects to any local dev server: +The preview panel (`Cmd/Ctrl+3` or click Preview tab) connects to any local dev server: - **URL bar** — type `localhost:5173` or any dev server URL - **Device Carousel** — see your app on iPhone, Pixel, iPad, MacBook, Desktop simultaneously -- **Component Isolation** (`⌘⇧I`) — isolate a React component from the active file +- **Component Isolation** (`Cmd/Ctrl+⇧I`) — isolate a React component from the active file - **Picture-in-Picture** — float the preview over your code while editing - **Agent Annotations** — when the AI makes changes, glowing highlights show what changed diff --git a/README.md b/README.md index 8334226..76c6ed9 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A lightweight, AI-native code editor powered by [OpenClaw](https://github.com/op ┌──────────────┬──────────────────────────┬─────────────────┐ │ File Tree │ Monaco Editor │ Agent Panel │ │ │ (multi-tab, vim mode) │ (chat + diff) │ -│ ⌘B toggle │ ⌘K inline edit │ ⌘J toggle │ -│ │ ⌘P quick open │ │ +│ Cmd/Ctrl+B │ Cmd/Ctrl+K inline edit │ Cmd/Ctrl+J │ +│ │ Cmd/Ctrl+P quick open │ │ ├──────────────┴──────────────────────────┴─────────────────┤ │ Terminal (xterm.js) │ └───────────────────────────────────────────────────────────┘ @@ -37,11 +37,15 @@ A lightweight, AI-native code editor powered by [OpenClaw](https://github.com/op ## Quick Start -### Desktop (macOS) +### Desktop (Windows, macOS, Linux) -Download the [latest release](https://github.com/OpenKnots/code-editor/releases/latest) (.dmg). +Download the [latest release](https://github.com/OpenKnots/code-editor/releases/latest): -After installing, macOS may show _"KnotCode is damaged"_ — this is because the app isn't notarized with Apple (yet). Fix it with: +- **Windows** — `.msi` or `.exe` installer +- **macOS** — `.dmg` (Apple Silicon + Intel) +- **Linux** — `.deb`, `.AppImage`, or `.rpm` (varies by distro) + +**macOS only:** After installing, macOS may show _"KnotCode is damaged"_ — this is because the app isn't notarized with Apple (yet). Fix it with: ```bash xattr -cr /Applications/KnotCode.app @@ -81,7 +85,7 @@ Copy `.env.example` to `.env` and configure. All variables are optional — the - **Agent Builder** — Choose a persona, customize your system prompt, configure behaviors - **Inline Edits** — Agent proposes changes, you review diffs and accept/reject per-hunk - **7 Themes** — Obsidian, Bone, Neon, Catppuccin, VooDoo, CyberNord, PrettyPink -- **Monaco Editor** — Multi-tab, Vim mode, syntax highlighting, ⌘P quick open +- **Monaco Editor** — Multi-tab, Vim mode, syntax highlighting, Cmd/Ctrl+P quick open - **GitHub Integration** — Device flow auth, commit, push, branch switching - **Terminal** — Integrated xterm.js with gateway slash commands - **Spotify + YouTube** — Built-in music and video plugins @@ -108,15 +112,17 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the technical architecture, ## Keyboard Shortcuts -| Shortcut | Action | -| -------- | ------------------------------ | -| `⌘P` | Quick file open (fuzzy search) | -| `⌘K` | Inline edit at selection | -| `⌘B` | Toggle file explorer | -| `⌘I` | Toggle agent panel | -| `⌘J` | Toggle terminal | -| `Enter` | Send message / Start chat | -| `Esc` | Close overlays | +Use **Cmd** on macOS, **Ctrl** on Windows/Linux. + +| Shortcut | Action | +| ------------ | ------------------------------ | +| `Cmd/Ctrl+P` | Quick file open (fuzzy search) | +| `Cmd/Ctrl+K` | Inline edit at selection | +| `Cmd/Ctrl+B` | Toggle file explorer | +| `Cmd/Ctrl+I` | Toggle agent panel | +| `Cmd/Ctrl+J` | Toggle terminal | +| `Enter` | Send message / Start chat | +| `Esc` | Close overlays | ## Tech Stack diff --git a/__tests__/platform.test.ts b/__tests__/platform.test.ts new file mode 100644 index 0000000..6a8e792 --- /dev/null +++ b/__tests__/platform.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { getPlatform, resetPlatformCache, formatShortcut, formatShortcutKeys } from '@/lib/platform' + +describe('platform', () => { + beforeEach(() => { + resetPlatformCache() + }) + + describe('getPlatform', () => { + it('returns one of mac, windows, or linux', () => { + const platform = getPlatform() + expect(['mac', 'windows', 'linux']).toContain(platform) + }) + }) + + describe('formatShortcut', () => { + it('formats meta+P with platform-appropriate modifier', () => { + const result = formatShortcut('meta+P') + const isMac = getPlatform() === 'mac' + expect(result).toMatch(isMac ? /⌘.*P/ : /Ctrl\+P/) + }) + + it('formats meta+shift+P with shift modifier', () => { + const result = formatShortcut('meta+shift+P') + expect(result).toContain('P') + expect(result.length).toBeGreaterThan(2) + }) + + it('formats single key combos as-is', () => { + expect(formatShortcut('?')).toBe('?') + }) + + it('formats special keys like Enter', () => { + const result = formatShortcut('meta+Enter') + expect(result.length).toBeGreaterThan(2) + expect(result).toMatch(/⌘|Ctrl/) + }) + }) + + describe('formatShortcutKeys', () => { + it('returns non-empty array of key parts', () => { + const keys = formatShortcutKeys('meta+P') + expect(Array.isArray(keys)).toBe(true) + expect(keys.length).toBeGreaterThanOrEqual(2) + expect(keys).toContain('P') + }) + }) +}) diff --git a/app/page.tsx b/app/page.tsx index f76fc35..d697b8b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -14,6 +14,7 @@ import { useAppMode } from '@/context/app-mode-context' import { WorkspaceSidebar } from '@/components/workspace-sidebar' import { FloatingPanel } from '@/components/floating-panel' import { isTauri } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' import { fetchFileContentsByName as fetchFileContents, commitFilesByName as commitFiles, @@ -461,7 +462,7 @@ export default function EditorLayout() { '--color': isActive ? 'var(--text-primary)' : 'var(--text-disabled)', } as React.CSSProperties } - title={`${VIEW_ICONS[v].label} (\u2318${i + 1})`} + title={`${VIEW_ICONS[v].label} (${formatShortcut(`meta+${i + 1}`)})`} whileTap={{ scale: 0.95 }} layout > @@ -538,7 +539,7 @@ export default function EditorLayout() { ? 'bg-[var(--bg)] text-[var(--text-primary)] shadow-[0_2px_6px_rgba(0,0,0,0.3),0_1px_0_rgba(255,255,255,0.08)_inset]' : 'text-[var(--text-disabled)] hover:text-[var(--text-secondary)] hover:bg-[color-mix(in_srgb,var(--text-primary)_6%,transparent)] active:shadow-[inset_0_1px_2px_rgba(0,0,0,0.2)] hover:scale-105' }`} - title={`${m.label} mode (⌘⇧${['classic', 'chat', 'tui'].indexOf(m.id) + 1})`} + title={`${m.label} mode (${formatShortcut(`meta+shift+${['classic', 'chat', 'tui'].indexOf(m.id) + 1}`)})`} > diff --git a/components/agent-panel.tsx b/components/agent-panel.tsx index a91454c..1716392 100644 --- a/components/agent-panel.tsx +++ b/components/agent-panel.tsx @@ -27,6 +27,7 @@ import { MessageList } from '@/components/chat/message-list' import { ChatInputBar } from '@/components/chat/chat-input-bar' import { emit, on } from '@/lib/events' import { copyToClipboard } from '@/lib/clipboard' +import { formatShortcut } from '@/lib/platform' import type { PlanStep } from '@/components/plan-view' import { navigateToLine } from '@/lib/line-links' import { useChatAppearance, FONT_OPTIONS } from '@/context/chat-appearance-context' @@ -1522,7 +1523,7 @@ export function AgentPanel() { id: crypto.randomUUID(), role: 'user', type: 'text', - content: `⌘K: ${instruction}`, + content: `${formatShortcut('meta+K')}: ${instruction}`, timestamp: Date.now(), }) @@ -1893,7 +1894,7 @@ export function AgentPanel() { @@ -1903,7 +1904,7 @@ export function AgentPanel() {
@@ -325,18 +333,18 @@ function WelcomeView() {
{[ - ['⌘P', 'Quick Open'], - ['⌘B', 'Toggle Explorer'], - ['⌘J', 'Toggle Agent'], - ['⌘K', 'Inline Edit'], - ['⌘S', 'Save'], - ['⌘⇧F', 'Search Files'], - ['⌘\`', 'Terminal'], + ['meta+P', 'Quick Open'], + ['meta+B', 'Toggle Explorer'], + ['meta+J', 'Toggle Agent'], + ['meta+K', 'Inline Edit'], + ['meta+S', 'Save'], + ['meta+shift+F', 'Search Files'], + ['meta+`', 'Terminal'], ['?', 'All Shortcuts'], - ].map(([key, label]) => ( -
+ ].map(([combo, label]) => ( +
- {key} + {formatShortcut(combo)} {label}
@@ -1235,7 +1243,7 @@ export function CodeEditor() { Reject - ⌘⏎ accept · Esc reject + {formatShortcut('meta+Enter')} accept · Esc reject
)} @@ -1250,7 +1258,7 @@ export function CodeEditor() { {[ { icon: 'lucide:message-square', - tip: 'Add to Chat (⌘L)', + tip: `Add to Chat (${formatShortcut('meta+L')})`, ev: 'add-to-chat', detail: { path: activeFile || 'untitled', @@ -1261,7 +1269,7 @@ export function CodeEditor() { }, { icon: 'lucide:pencil', - tip: 'Edit (⌘K)', + tip: `Edit (${formatShortcut('meta+K')})`, ev: 'inline-edit-request', detail: { text: selToolbar.text }, }, diff --git a/components/command-palette.tsx b/components/command-palette.tsx index defcaa0..c999fc8 100644 --- a/components/command-palette.tsx +++ b/components/command-palette.tsx @@ -5,6 +5,7 @@ import { Icon } from '@iconify/react' import { cn } from '@/lib/utils' import { useView, type ViewId } from '@/context/view-context' import { isTauri } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' type CommandId = | 'find-files' @@ -53,7 +54,7 @@ interface CommandItem { hint: string keywords: string[] icon: string - shortcut?: string + combo?: string group: 'search' | 'layout' | 'preset' | 'navigate' | 'git' | 'pr' | 'preview' } @@ -65,7 +66,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Open quick file search', keywords: ['file', 'quick', 'open'], icon: 'lucide:file-search', - shortcut: '\u2318P', + combo: 'meta+P', group: 'search', }, { @@ -74,7 +75,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Save the active file', keywords: ['save', 'write', 'file'], icon: 'lucide:save', - shortcut: '\u2318S', + combo: 'meta+S', group: 'search', }, { @@ -91,7 +92,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Open editor search', keywords: ['find', 'search', 'match'], icon: 'lucide:search', - shortcut: '\u2318F', + combo: 'meta+F', group: 'search', }, { @@ -100,7 +101,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Open replace widget', keywords: ['replace', 'search', 'find'], icon: 'lucide:replace', - shortcut: '\u2318H', + combo: 'meta+H', group: 'search', }, { @@ -135,7 +136,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Show or hide the file tree', keywords: ['files', 'tree', 'explorer', 'sidebar'], icon: 'lucide:folder', - shortcut: '\u2318B', + combo: 'meta+B', group: 'layout', }, { @@ -144,7 +145,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Show or hide the terminal panel', keywords: ['terminal', 'shell', 'console'], icon: 'lucide:terminal', - shortcut: '\u2318J', + combo: 'meta+J', group: 'layout', }, { @@ -153,7 +154,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Show or hide the AI agent panel', keywords: ['chat', 'agent', 'ai', 'assistant'], icon: 'lucide:message-square', - shortcut: '\u2318I', + combo: 'meta+I', group: 'layout', }, { @@ -170,7 +171,7 @@ const COMMANDS: CommandItem[] = [ hint: 'Minimize editor to icon rail', keywords: ['collapse', 'minimize', 'hide', 'editor'], icon: 'lucide:minimize-2', - shortcut: '\u2318E', + combo: 'meta+E', group: 'layout', }, @@ -577,9 +578,9 @@ export function CommandPalette({ open, onClose, onRun }: CommandPaletteProps) { {command.hint}

- {command.shortcut && ( + {command.combo && ( - {command.shortcut} + {formatShortcut(command.combo)} )} diff --git a/components/editor-tabs.tsx b/components/editor-tabs.tsx index 98183ba..8670b9a 100644 --- a/components/editor-tabs.tsx +++ b/components/editor-tabs.tsx @@ -3,6 +3,7 @@ import { useState, useCallback, useRef } from 'react' import { Icon } from '@iconify/react' import { useEditor } from '@/context/editor-context' +import { formatShortcut } from '@/lib/platform' const EXT_ICONS: Record = { ts: { icon: 'lucide:file-code', color: '#3178c6' }, @@ -151,7 +152,7 @@ export function EditorTabs() { closeFile(file.path) }} className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-[color-mix(in_srgb,var(--text-primary)_10%,transparent)] transition-all cursor-pointer ml-1 hover:scale-110" - title="Close (⌘W)" + title={`Close (${formatShortcut('meta+W')})`} > diff --git a/components/keyboard-handler.tsx b/components/keyboard-handler.tsx index 8a374a2..b4cdd89 100644 --- a/components/keyboard-handler.tsx +++ b/components/keyboard-handler.tsx @@ -37,32 +37,32 @@ export function useKeyboardShortcuts({ const handler = (e: KeyboardEvent) => { const meta = e.metaKey || e.ctrlKey - // ⌘P — Quick open + // meta+P — Quick open if (meta && e.key === 'p' && !e.shiftKey) { e.preventDefault() onQuickOpen() } - // ⌘⇧P — Command palette + // meta+shift+P — Command palette if (meta && e.shiftKey && e.key === 'p') { e.preventDefault() onCommandPalette() } - // ⌘⇧F — Global search + // meta+shift+F — Global search if (meta && e.shiftKey && e.key === 'f') { e.preventDefault() onGlobalSearch() } - // ⌘\\ — Toggle sidebar + // meta+\ — Toggle sidebar if (meta && e.key === '\\') { e.preventDefault() layout.toggle('sidebar') } - // ⌘J / ⌘` — Toggle terminal + // meta+J / meta+` — Toggle terminal if (meta && (e.key === 'j' || e.key === '`') && !e.shiftKey) { e.preventDefault() layout.toggle('terminal') } - // ⌘L — Open side chat panel and focus input + // meta+L — Open side chat panel and focus input if (meta && e.key === 'l' && !e.shiftKey) { e.preventDefault() if (mode === 'chat') { @@ -73,7 +73,7 @@ export function useKeyboardShortcuts({ } requestAnimationFrame(() => emit('focus-agent-input')) } - // ⌘⌥1-4 — Focus key regions (explorer/editor/chat/terminal) + // meta+alt+1-4 — Focus key regions (explorer/editor/chat/terminal) if (meta && e.altKey && ['1', '2', '3', '4'].includes(e.key)) { e.preventDefault() if (activeViewRef.current !== 'editor') setView('editor') @@ -94,7 +94,7 @@ export function useKeyboardShortcuts({ } } // Esc — Close overlays (handled by each overlay individually via props) - // ⌘⇧1/2/3 — Mode switching + // meta+shift+1/2/3 — Mode switching if (meta && e.shiftKey && ['1', '2', '3'].includes(e.key)) { e.preventDefault() const modes: AppMode[] = ['classic', 'chat', 'tui'] @@ -102,7 +102,7 @@ export function useKeyboardShortcuts({ if (target) setMode(target) return } - // ⌘1..N — View switching (mode-aware) + // meta+1..N — View switching (mode-aware) if (meta && e.key >= '1' && e.key <= String(visibleViews.length)) { e.preventDefault() const target = visibleViews[parseInt(e.key) - 1] @@ -126,7 +126,7 @@ export function useKeyboardShortcuts({ onFlashTab, ]) - // ─── ⌘S — Save file ─────────────────────────────────── + // ─── meta+S — Save file ─────────────────────────────── useEffect(() => { const keyHandler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 's') { diff --git a/components/onboarding-tour.tsx b/components/onboarding-tour.tsx index 55f7d17..486145e 100644 --- a/components/onboarding-tour.tsx +++ b/components/onboarding-tour.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react' import { Icon } from '@iconify/react' +import { formatShortcut } from '@/lib/platform' export const ONBOARDING_KEY = 'ce:onboarding:v1' @@ -29,12 +30,12 @@ export function OnboardingTour({ open, onClose }: { open: boolean; onClose: () = }, { title: 'Keyboard-first navigation', - body: 'Use ⌘P to open files, ⌘⇧P for the command palette, and ⌘⌥1–4 to jump focus (Files / Editor / Chat / Terminal).', + body: `Use ${formatShortcut('meta+P')} to open files, ${formatShortcut('meta+shift+P')} for the command palette, and ${formatShortcut('meta+alt+1')}–4 to jump focus (Files / Editor / Chat / Terminal).`, icon: 'lucide:keyboard', }, { title: 'Panels & layout', - body: 'Toggle Explorer with ⌘B, Chat with ⌘I, Terminal with ⌘J (or ⌘`). On smaller screens, panels open as drawers to avoid clipping.', + body: `Toggle Explorer with ${formatShortcut('meta+B')}, Chat with ${formatShortcut('meta+I')}, Terminal with ${formatShortcut('meta+J')} (or ${formatShortcut('meta+`')}). On smaller screens, panels open as drawers to avoid clipping.`, icon: 'lucide:layout-panel-left', }, { diff --git a/components/preview/preview-panel.tsx b/components/preview/preview-panel.tsx index a588b94..75af0cb 100644 --- a/components/preview/preview-panel.tsx +++ b/components/preview/preview-panel.tsx @@ -14,6 +14,7 @@ import { useEditor } from '@/context/editor-context' import { useView } from '@/context/view-context' import { useLocal } from '@/context/local-context' import { isTauri, tauriInvoke } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' import { emit } from '@/lib/events' /* ── Script metadata ─────────────────────────────────────────── */ @@ -705,7 +706,7 @@ function SingleDeviceZoomBar({ @@ -717,7 +718,7 @@ function SingleDeviceZoomBar({ }} className="p-0.5 rounded hover:bg-[var(--bg-subtle)] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" disabled={zoom <= ZOOM_MIN} - title="Zoom out (⌘−)" + title={`Zoom out (${formatShortcut('meta+-')})`} > @@ -753,7 +754,7 @@ function SingleDeviceZoomBar({ }} className="p-0.5 rounded hover:bg-[var(--bg-subtle)] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" disabled={zoom >= ZOOM_MAX} - title="Zoom in (⌘+)" + title={`Zoom in (${formatShortcut('meta+=')})`} > @@ -814,7 +815,7 @@ function SingleDeviceZoomBar({ className="w-full flex items-center gap-2 px-3 py-1.5 text-[11px] text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] transition-colors cursor-pointer" > - Fit to screen (⌘1) + Fit to screen ({formatShortcut('meta+1')})
diff --git a/components/settings-panel.tsx b/components/settings-panel.tsx index 4f2509b..0fa3f4b 100644 --- a/components/settings-panel.tsx +++ b/components/settings-panel.tsx @@ -10,6 +10,7 @@ import { AgentBuilder, AgentSummary } from '@/components/agent-builder' import { SkillsInterface } from '@/components/skills/skills-interface' import { type AgentConfig, getAgentConfig, clearAgentConfig } from '@/lib/agent-session' import { isTauri, tauriReadFileBase64 } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' interface Props { open: boolean @@ -151,20 +152,20 @@ export function SettingsPanel({ open, onClose, initialTab }: Props) { ] const shortcuts = [ - { keys: '⌘B', desc: 'Toggle explorer' }, - { keys: '⌘J', desc: 'Toggle terminal' }, - { keys: '⌘\\', desc: 'Toggle sidebar' }, - { keys: '⌘P', desc: 'Quick open file' }, - { keys: '⌘K', desc: 'Inline edit' }, - { keys: '⌘L', desc: 'Send selection to chat' }, - { keys: '⌘⌥1', desc: 'Focus explorer' }, - { keys: '⌘⌥2', desc: 'Focus editor' }, - { keys: '⌘⌥3', desc: 'Focus chat' }, - { keys: '⌘⌥4', desc: 'Focus terminal' }, - { keys: '⌘S', desc: 'Save file' }, - { keys: '⌘⇧F', desc: 'Global search' }, - { keys: '⌘⇧P', desc: 'Command palette' }, - { keys: 'Esc', desc: 'Close overlays' }, + { combo: 'meta+B', desc: 'Toggle explorer' }, + { combo: 'meta+J', desc: 'Toggle terminal' }, + { combo: 'meta+\\', desc: 'Toggle sidebar' }, + { combo: 'meta+P', desc: 'Quick open file' }, + { combo: 'meta+K', desc: 'Inline edit' }, + { combo: 'meta+L', desc: 'Send selection to chat' }, + { combo: 'meta+alt+1', desc: 'Focus explorer' }, + { combo: 'meta+alt+2', desc: 'Focus editor' }, + { combo: 'meta+alt+3', desc: 'Focus chat' }, + { combo: 'meta+alt+4', desc: 'Focus terminal' }, + { combo: 'meta+S', desc: 'Save file' }, + { combo: 'meta+shift+F', desc: 'Global search' }, + { combo: 'meta+shift+P', desc: 'Command palette' }, + { combo: 'Escape', desc: 'Close overlays' }, ] return ( @@ -625,12 +626,12 @@ export function SettingsPanel({ open, onClose, initialTab }: Props) {
{shortcuts.map((s) => (
{s.desc} - {s.keys} + {formatShortcut(s.combo)}
))} diff --git a/components/shortcuts-overlay.tsx b/components/shortcuts-overlay.tsx index 2e8da6b..9d17d2a 100644 --- a/components/shortcuts-overlay.tsx +++ b/components/shortcuts-overlay.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useMemo } from 'react' import { Icon } from '@iconify/react' import { isTauri } from '@/lib/tauri' +import { formatShortcutKeys } from '@/lib/platform' interface ShortcutsOverlayProps { open: boolean @@ -10,31 +11,31 @@ interface ShortcutsOverlayProps { } const NAV_SHORTCUTS = [ - { keys: ['⌘', 'K'], desc: 'Command palette' }, - { keys: ['⌘', 'P'], desc: 'Quick file open' }, - { keys: ['⌘', 'B'], desc: 'Toggle file explorer' }, - { keys: ['⌘', 'J'], desc: 'Toggle agent panel' }, - { keys: ['⌘', '⇧', 'E'], desc: 'Toggle Gateway Engine' }, - { keys: ['⌘', '⌥', '1'], desc: 'Focus Explorer' }, - { keys: ['⌘', '⌥', '2'], desc: 'Focus Editor' }, - { keys: ['⌘', '⌥', '3'], desc: 'Focus Chat' }, - { keys: ['?'], desc: 'This shortcuts overlay' }, + { combo: 'meta+shift+P', desc: 'Command palette' }, + { combo: 'meta+P', desc: 'Quick file open' }, + { combo: 'meta+B', desc: 'Toggle file explorer' }, + { combo: 'meta+J', desc: 'Toggle agent panel' }, + { combo: 'meta+shift+E', desc: 'Toggle Gateway Engine' }, + { combo: 'meta+alt+1', desc: 'Focus Explorer' }, + { combo: 'meta+alt+2', desc: 'Focus Editor' }, + { combo: 'meta+alt+3', desc: 'Focus Chat' }, + { combo: '?', desc: 'This shortcuts overlay' }, ] const NAV_TERMINAL_SHORTCUTS = [ - { keys: ['⌘', '`'], desc: 'Toggle terminal' }, - { keys: ['⌘', '⌥', '4'], desc: 'Focus Terminal' }, + { combo: 'meta+`', desc: 'Toggle terminal' }, + { combo: 'meta+alt+4', desc: 'Focus Terminal' }, ] const STATIC_SECTIONS = [ { title: 'Editing', shortcuts: [ - { keys: ['⌘', '⇧', 'K'], desc: 'Inline edit at selection' }, - { keys: ['⌘', '⇧', 'V'], desc: 'Cycle markdown edit/preview/split' }, - { keys: ['⌘', 'S'], desc: 'Save (commit) file' }, - { keys: ['⌘', 'Z'], desc: 'Undo' }, - { keys: ['⌘', '⇧', 'Z'], desc: 'Redo' }, + { combo: 'meta+shift+K', desc: 'Inline edit at selection' }, + { combo: 'meta+shift+V', desc: 'Cycle markdown edit/preview/split' }, + { combo: 'meta+S', desc: 'Save (commit) file' }, + { combo: 'meta+Z', desc: 'Undo' }, + { combo: 'meta+shift+Z', desc: 'Redo' }, ], }, { @@ -51,7 +52,7 @@ const STATIC_SECTIONS = [ { keys: ['/undo'], desc: 'Undo last commit' }, ], }, -] +] as const export function ShortcutsOverlay({ open, onClose }: ShortcutsOverlayProps) { const [isDesktop, setIsDesktop] = useState(false) @@ -119,30 +120,33 @@ export function ShortcutsOverlay({ open, onClose }: ShortcutsOverlayProps) {
- {section.shortcuts.map((s) => ( -
- - {s.desc} - -
- {s.keys.map((key, i) => ( - - {key} - - ))} + {section.shortcuts.map((s) => { + const keys = 'combo' in s ? formatShortcutKeys(s.combo) : s.keys + return ( +
+ + {s.desc} + +
+ {keys.map((key, i) => ( + + {key} + + ))} +
-
- ))} + ) + })}
))} diff --git a/components/status-bar.tsx b/components/status-bar.tsx index 375cb56..5aaa09b 100644 --- a/components/status-bar.tsx +++ b/components/status-bar.tsx @@ -10,6 +10,7 @@ import { useAppMode } from '@/context/app-mode-context' import { PluginSlotRenderer } from '@/context/plugin-context' import { BranchPicker } from '@/components/branch-picker' import { FolderIndicator } from '@/components/source-switcher' +import { formatShortcut } from '@/lib/platform' // ─── Activity Pulse Ring ───────────────────────────── function ActivityPulseRing({ status, agentActive }: { status: string; agentActive: boolean }) { @@ -124,7 +125,7 @@ export function StatusBar({ agentActive }: StatusBarProps) { ? 'text-[var(--brand)]' : 'text-[var(--text-disabled)] hover:text-[var(--text-secondary)]' }`} - title={`${terminalVisible ? 'Hide' : 'Show'} Terminal (⌘J)`} + title={`${terminalVisible ? 'Hide' : 'Show'} Terminal (${formatShortcut('meta+J')})`} > diff --git a/components/views/editor-view.tsx b/components/views/editor-view.tsx index c96bdb0..7c43248 100644 --- a/components/views/editor-view.tsx +++ b/components/views/editor-view.tsx @@ -14,6 +14,7 @@ import { FloatingPanel } from '@/components/floating-panel' import { KnotLogo } from '@/components/knot-logo' import { KnotBackground } from '@/components/knot-background' import { isTauri } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' import { emit } from '@/lib/events' const FileExplorer = dynamic( @@ -32,15 +33,15 @@ const AgentPanel = dynamic( const PANEL_SPRING = { type: 'spring' as const, stiffness: 500, damping: 35 } const QUICK_ACTIONS = [ - { icon: 'lucide:file-search', label: 'File', shortcut: '\u2318P', event: 'quick-open' }, - { icon: 'lucide:folder', label: 'Browse', shortcut: '\u2318B', event: 'toggle-tree' }, + { icon: 'lucide:file-search', label: 'File', combo: 'meta+P', event: 'quick-open' }, + { icon: 'lucide:folder', label: 'Browse', combo: 'meta+B', event: 'toggle-tree' }, { icon: 'lucide:message-square', label: 'Chat', - shortcut: '\u2318L', + combo: 'meta+L', event: 'open-side-chat', }, - { icon: 'lucide:terminal', label: 'Terminal', shortcut: '\u2318J', event: 'toggle-terminal' }, + { icon: 'lucide:terminal', label: 'Terminal', combo: 'meta+J', event: 'toggle-terminal' }, ] export function EditorView() { @@ -150,7 +151,7 @@ export function EditorView() { @@ -160,7 +161,7 @@ export function EditorView() { layout.show('tree') }} className="mt-1.5 p-2.5 rounded-xl hover:bg-[var(--bg-subtle)] text-[var(--text-disabled)] hover:text-[var(--text-tertiary)] transition-colors cursor-pointer" - title="Open explorer (⌘B)" + title={`Open explorer (${formatShortcut('meta+B')})`} > @@ -196,7 +197,7 @@ export function EditorView() { @@ -227,7 +228,7 @@ export function EditorView() { @@ -328,7 +329,7 @@ export function EditorView() { {item.label}
- {item.shortcut} + {formatShortcut(item.combo)} ), @@ -350,7 +351,7 @@ export function EditorView() { /> Press{' '} - ⌘P + {formatShortcut('meta+P')} {' '} to quickly find any file @@ -372,7 +373,7 @@ export function EditorView() { diff --git a/components/workspace-sidebar.tsx b/components/workspace-sidebar.tsx index 37bf154..3110d88 100644 --- a/components/workspace-sidebar.tsx +++ b/components/workspace-sidebar.tsx @@ -5,6 +5,7 @@ import { motion } from 'framer-motion' import { Icon } from '@iconify/react' import { useLayout, usePanelResize } from '@/context/layout-context' import { isTauri } from '@/lib/tauri' +import { formatShortcut } from '@/lib/platform' import { emit } from '@/lib/events' const SIDEBAR_SPRING = { type: 'spring' as const, stiffness: 500, damping: 35 } @@ -120,7 +121,7 @@ export function WorkspaceSidebar({ collapsed, onToggle, repoName }: Props) { diff --git a/docs/DESKTOP.md b/docs/DESKTOP.md index 8a2c859..dae2cab 100644 --- a/docs/DESKTOP.md +++ b/docs/DESKTOP.md @@ -2,13 +2,13 @@ ## Overview -KnotCode ships as a native macOS desktop application via [Tauri v2](https://v2.tauri.app). Tauri wraps the system's native WebView (WebKit on macOS) instead of bundling Chromium, resulting in a ~10MB binary vs ~150MB for Electron. +KnotCode ships as a native desktop application for **Windows, macOS, and Linux** via [Tauri v2](https://v2.tauri.app). Tauri wraps the system's native WebView (WebKit on macOS, WebView2 on Windows) instead of bundling Chromium, resulting in a ~10MB binary vs ~150MB for Electron. ## Architecture ``` ┌─────────────────────────────────┐ -│ macOS .app Bundle │ +│ Native Desktop Bundle │ │ │ │ ┌───────────────────────────┐ │ │ │ System WebKit View │ │ @@ -36,7 +36,7 @@ KnotCode ships as a native macOS desktop application via [Tauri v2](https://v2.t # Install Rust via rustup curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -# Add to shell profile (~/.zshrc for macOS) +# Add to shell profile (~/.zshrc on macOS, ~/.bashrc on Linux, $PROFILE on Windows) echo '. "$HOME/.cargo/env"' >> ~/.zshrc source ~/.zshrc @@ -45,11 +45,11 @@ rustc --version # rustc 1.x.x cargo --version # cargo 1.x.x ``` -### 2. Xcode Command Line Tools +### 2. Platform-specific tools -```bash -xcode-select --install -``` +- **macOS:** Xcode Command Line Tools — `xcode-select --install` +- **Windows:** Visual Studio Build Tools (for Rust MSVC target) +- **Linux:** Build essentials — `sudo apt install build-essential` (Debian/Ubuntu) or equivalent ### 3. Node.js + pnpm @@ -66,7 +66,7 @@ pnpm --version # any recent version pnpm install # Start Tauri dev mode -# This runs Next.js dev server + opens a native macOS window +# This runs Next.js dev server + opens a native desktop window pnpm tauri:dev ``` @@ -77,19 +77,11 @@ The dev window connects to `http://localhost:3080` with full hot reload. Code ch ## Production Build ```bash -# Build .app bundle + .dmg installer +# Build native installer (.msi/.exe on Windows, .app/.dmg on macOS, .deb/.AppImage on Linux) pnpm tauri:build ``` -Output location: - -``` -src-tauri/target/release/bundle/ -├── macos/ -│ └── KnotCode.app # macOS application bundle -└── dmg/ - └── KnotCode_0.1.0_aarch64.dmg # Installer -``` +Output location: `src-tauri/target/release/bundle/` — structure varies by platform (e.g. `macos/KnotCode.app`, `windows/*.msi`, `linux/*.AppImage`). ## Configuration @@ -188,6 +180,5 @@ src-tauri/ - [ ] **Auto-updater** — check for updates on launch - [ ] **Native notifications** — build status, agent replies - [ ] **Deep linking** — `code-editor://open?repo=...` URL scheme -- [ ] **Touch Bar** — common actions (save, run, commit) -- [ ] **Spotlight integration** — search files via macOS Spotlight -- [ ] **Windows + Linux** — Tauri supports all platforms natively +- [ ] **Touch Bar** (macOS) — common actions (save, run, commit) +- [ ] **Spotlight integration** (macOS) — search files via macOS Spotlight diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 7476fd4..23c8c2e 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -88,12 +88,12 @@ ### Desktop App (Tauri) -- [x] **macOS native** — Tauri v2 with system WebKit +- [x] **Windows, macOS, Linux** — Tauri v2 with native WebView (WebKit on macOS, WebView2 on Windows) - [x] **~10MB binary** — no bundled Chromium - [x] **Custom icons** — OpenKnot logo at all sizes -- [x] **macOS titlebar overlay** — native feel +- [x] **Native titlebar** — platform-appropriate styling - [x] **Dev mode** — `pnpm tauri:dev` with hot reload -- [x] **Production build** — `pnpm tauri:build` → .app + .dmg +- [x] **Production build** — `pnpm tauri:build` → .msi/.exe, .app/.dmg, or .deb/.AppImage --- @@ -103,7 +103,7 @@ - [ ] **Git status in file tree** — green (new), orange (modified), red (deleted) indicators - [ ] **Modified files diff** — view all changes before committing -- [ ] **⌘S Save shortcut** — quick commit current file +- [ ] **Cmd/Ctrl+S Save shortcut** — quick commit current file - [ ] **Agent-initiated file navigation** — agent says "open file X" → editor opens it - [ ] **Selection-aware /explain** — explain selected code, not whole file - [ ] **Recent files** — quick access to recently opened files diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 912401f..b086da1 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -7,6 +7,7 @@ **Symptom:** Build fails with various webpack-related errors. **Fix:** Always use the webpack flag: + ```bash pnpm build --webpack ``` @@ -20,11 +21,13 @@ The default Turbopack bundler can crash with port binding errors. Webpack is the **Symptom:** Missing dependency during build. **Fix:** Install the missing package: + ```bash pnpm add ``` Common ones that may need manual installation: + - `@workos-inc/authkit-nextjs` - `create-markdown` - `tw-animate-css` @@ -39,6 +42,7 @@ Common ones that may need manual installation: **Context:** Monaco's TypeScript types are deprecated in newer versions. **Fix:** Use `beforeMount` callback with optional chaining: + ```typescript const handleBeforeMount: BeforeMount = (monaco) => { monaco.languages.typescript?.typescriptDefaults?.setDiagnosticsOptions({ @@ -68,17 +72,20 @@ const handleBeforeMount: BeforeMount = (monaco) => { **Fix:** 1. Ensure Rust is installed: + ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ``` 2. Add to your shell profile (`~/.zshrc` for macOS): + ```bash echo '. "$HOME/.cargo/env"' >> ~/.zshrc source ~/.zshrc ``` 3. Verify: + ```bash cargo --version # should print cargo 1.x.x rustc --version # should print rustc 1.x.x @@ -90,7 +97,7 @@ const handleBeforeMount: BeforeMount = (monaco) => { ### First Tauri build takes very long -**Expected behavior.** The first build compiles all Rust dependencies (~300 crates). This takes 2-5 minutes on M-series Macs, longer on older hardware. +**Expected behavior.** The first build compiles all Rust dependencies (~300 crates). This takes 2-5 minutes on modern hardware, longer on older machines. Subsequent builds only recompile changed code and are much faster (<30 seconds). @@ -101,11 +108,13 @@ Subsequent builds only recompile changed code and are much faster (<30 seconds). **Symptom:** Compilation errors mentioning Xcode, SDK, or system frameworks. **Fix:** Ensure Xcode Command Line Tools are installed: + ```bash xcode-select --install ``` If already installed, try resetting: + ```bash sudo xcode-select --reset ``` @@ -117,6 +126,7 @@ sudo xcode-select --reset **Possible causes:** 1. **Dev server not running:** `pnpm tauri:dev` should start Next.js dev server automatically. Check that port 3080 is free: + ```bash lsof -i :3080 ``` @@ -140,6 +150,7 @@ sudo xcode-select --reset **Cause:** The code-editor's origin is not in the gateway's allowed origins list. **Fix:** Add the origin to `~/.openclaw/openclaw.json`: + ```json { "gateway": { @@ -155,6 +166,7 @@ sudo xcode-select --reset ``` Then restart the gateway: + ```bash openclaw gateway restart ``` @@ -168,6 +180,7 @@ openclaw gateway restart **Possible causes:** 1. **Gateway not running:** + ```bash openclaw gateway status # If stopped: @@ -190,6 +203,7 @@ openclaw gateway restart **Symptom:** Login shows pairing instructions. **Fix:** On the gateway host machine: + ```bash openclaw devices list # find the pending request openclaw devices approve @@ -208,7 +222,8 @@ Then click Connect again in the editor. **Cause:** Monaco tries to type-check TypeScript/JavaScript files but has no `tsconfig` or type definitions. **Fix:** This is already handled — `beforeMount` disables semantic validation. If you still see red lines: -1. Hard refresh the page (⌘⇧R) + +1. Hard refresh the page (Cmd/Ctrl+Shift+R) 2. Check that the `handleBeforeMount` callback is being called --- @@ -264,7 +279,7 @@ Then click Connect again in the editor. **Symptom:** Page keeps redirecting to WorkOS login and back. **Fix:** Check that `WORKOS_CLIENT_ID`, `WORKOS_API_KEY`, and `WORKOS_REDIRECT_URI` are set correctly in environment variables. - + For local development, the redirect URI should be `http://localhost:3080/callback`. --- @@ -276,6 +291,7 @@ For local development, the redirect URI should be `http://localhost:3080/callbac **Cause:** `ALLOWED_USER_EMAIL` or `ALLOWED_USER_ID` is set and your account doesn't match. **Fix:** Either: + - Update the env var to match your email/user ID - Remove the env var to allow all authenticated users @@ -288,6 +304,7 @@ For local development, the redirect URI should be `http://localhost:3080/callbac **Cause:** Large repos (10,000+ files) create a heavy tree in memory. **Workarounds:** + - Use the search to filter before browsing - Tree rendering uses virtual-ish approach (dirs collapse by default) - Consider filtering tree API response server-side for very large repos @@ -297,6 +314,7 @@ For local development, the redirect URI should be `http://localhost:3080/callbac ### Agent responses are slow **Possible causes:** + 1. **Model choice:** Larger models (Opus) take longer than smaller ones (Haiku) 2. **Context size:** Files >8KB are truncated in context injection to prevent overload 3. **Gateway load:** Multiple sessions competing for the same gateway @@ -305,12 +323,12 @@ For local development, the redirect URI should be `http://localhost:3080/callbac ## Common Environment Variables -| Variable | Purpose | Required | -|----------|---------|----------| -| `GITHUB_TOKEN` | GitHub API access | Yes | -| `WORKOS_CLIENT_ID` | WorkOS OAuth | Yes (web) | -| `WORKOS_API_KEY` | WorkOS server auth | Yes (web) | -| `WORKOS_REDIRECT_URI` | OAuth callback URL | Yes (web) | -| `ALLOWED_USER_EMAIL` | Restrict to one user | No | -| `ALLOWED_USER_ID` | Restrict to one user | No | -| `ALLOWED_IPS` | IP allowlist (CIDR) | No (`*` = disabled) | +| Variable | Purpose | Required | +| --------------------- | -------------------- | ------------------- | +| `GITHUB_TOKEN` | GitHub API access | Yes | +| `WORKOS_CLIENT_ID` | WorkOS OAuth | Yes (web) | +| `WORKOS_API_KEY` | WorkOS server auth | Yes (web) | +| `WORKOS_REDIRECT_URI` | OAuth callback URL | Yes (web) | +| `ALLOWED_USER_EMAIL` | Restrict to one user | No | +| `ALLOWED_USER_ID` | Restrict to one user | No | +| `ALLOWED_IPS` | IP allowlist (CIDR) | No (`*` = disabled) | diff --git a/lib/platform.ts b/lib/platform.ts new file mode 100644 index 0000000..26616f9 --- /dev/null +++ b/lib/platform.ts @@ -0,0 +1,111 @@ +/** + * Platform detection and shortcut labeling for cross-platform UI. + * Uses navigator.userAgent (works in browser and Tauri WebView). + */ + +export type Platform = 'mac' | 'windows' | 'linux' + +let _platform: Platform | null = null + +/** Reset cached platform (for testing). */ +export function resetPlatformCache(): void { + _platform = null +} + +/** Detect current OS. Cached for consistency. */ +export function getPlatform(): Platform { + if (_platform) return _platform + if (typeof navigator === 'undefined') return 'windows' // SSR fallback + const ua = navigator.userAgent.toLowerCase() + if (ua.includes('win')) _platform = 'windows' + else if (ua.includes('mac')) _platform = 'mac' + else _platform = 'linux' + return _platform +} + +export function isMac(): boolean { + return getPlatform() === 'mac' +} + +export function isWindows(): boolean { + return getPlatform() === 'windows' +} + +export function isLinux(): boolean { + return getPlatform() === 'linux' +} + +/** Modifier symbols for macOS (glyphs) vs Windows/Linux (text) */ +const MAC = { meta: '⌘', shift: '⇧', alt: '⌥' } as const +const WIN = { meta: 'Ctrl', shift: 'Shift', alt: 'Alt' } as const + +/** + * Format an abstract shortcut combo for display. + * Combos use: meta+, shift+, alt+, and the key (e.g. P, 1, `, Enter). + * Examples: 'meta+P', 'meta+shift+P', 'meta+alt+1', 'meta+`' , '?' + */ +export function formatShortcut(combo: string): string { + const mac = isMac() + const mods = mac ? MAC : WIN + + const parts = combo.toLowerCase().split('+').filter(Boolean) + if (parts.length === 0) return combo + if (parts.length === 1 && !['meta', 'shift', 'alt'].includes(parts[0]!)) { + return formatKey(parts[0]!, mac) + } + + const key = parts.filter((p) => !['meta', 'shift', 'alt'].includes(p))[0] + const hasMeta = parts.includes('meta') + const hasShift = parts.includes('shift') + const hasAlt = parts.includes('alt') + + const out: string[] = [] + if (hasMeta) out.push(mods.meta) + if (hasShift) out.push(mods.shift) + if (hasAlt) out.push(mods.alt) + if (key) out.push(formatKey(key, mac)) + + return mac ? out.join('') : out.join('+') +} + +/** Format a single key for display (e.g. backtick, enter). */ +function formatKey(key: string, mac: boolean): string { + const special: Record = { + '`': '`', + '\\': '\\', + enter: '⏎', + escape: 'Esc', + ' ': 'Space', + '-': '−', + '=': '+', + } + return special[key] ?? key.toUpperCase() +} + +/** + * Format a shortcut as an array of key parts for rendering in elements. + * e.g. formatShortcutKeys('meta+P') -> ['⌘', 'P'] on Mac, ['Ctrl', 'P'] on Windows + */ +export function formatShortcutKeys(combo: string): string[] { + const mac = isMac() + const mods = mac ? MAC : WIN + + const parts = combo.toLowerCase().split('+').filter(Boolean) + if (parts.length === 0) return [combo] + if (parts.length === 1 && !['meta', 'shift', 'alt'].includes(parts[0]!)) { + return [formatKey(parts[0]!, mac)] + } + + const key = parts.filter((p) => !['meta', 'shift', 'alt'].includes(p))[0] + const hasMeta = parts.includes('meta') + const hasShift = parts.includes('shift') + const hasAlt = parts.includes('alt') + + const out: string[] = [] + if (hasMeta) out.push(mods.meta) + if (hasShift) out.push(mods.shift) + if (hasAlt) out.push(mods.alt) + if (key) out.push(formatKey(key, mac)) + + return out +} diff --git a/package.json b/package.json index 6441b30..c4f03d4 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "scripts": { "frontend:dev": "next dev --turbopack", "frontend:build": "next build", - "desktop:dev": "PORT=3080 tauri dev", + "desktop:dev": "cross-env PORT=3080 tauri dev", "desktop:check": "tsc --noEmit", - "desktop:build": "CI=true tauri build --target aarch64-apple-darwin", + "desktop:build": "cross-env CI=true tauri build", "desktop:build:debug": "tauri build --debug", "desktop:release": "pnpm desktop:check && pnpm desktop:build", "desktop:sign": "./scripts/sign-and-deploy.sh", @@ -22,9 +22,9 @@ "lint": "eslint .", "format": "prettier --write .", "format:check": "prettier --check .", - "secrets:scan": "scripts/secrets-scan.sh history", - "secrets:scan:staged": "scripts/secrets-scan.sh staged", - "secrets:baseline": "scripts/secrets-scan.sh baseline", + "secrets:scan": "bash ./scripts/secrets-scan.sh history", + "secrets:scan:staged": "bash ./scripts/secrets-scan.sh staged", + "secrets:baseline": "bash ./scripts/secrets-scan.sh baseline", "policy:skill-first": "node scripts/preflight-skill-first.mjs staged", "policy:skill-first:ci": "node scripts/preflight-skill-first.mjs ci", "prepare": "husky" @@ -75,6 +75,7 @@ "eslint": "^10.0.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", + "cross-env": "^7.0.3", "husky": "^9.1.7", "jsdom": "^28.1.0", "lint-staged": "^16.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bcdbc1..255f96e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: '@vitest/coverage-v8': specifier: ^4.0.18 version: 4.0.18(vitest@4.0.18(@types/node@25.3.3)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(yaml@2.8.2)) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 eslint: specifier: ^10.0.2 version: 10.0.2(jiti@2.6.1) @@ -1365,6 +1368,11 @@ packages: shiki: optional: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3294,6 +3302,10 @@ snapshots: optionalDependencies: react: 19.2.4 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/scripts/secrets-scan.sh b/scripts/secrets-scan.sh index 00cb5ac..af15d1a 100755 --- a/scripts/secrets-scan.sh +++ b/scripts/secrets-scan.sh @@ -12,6 +12,14 @@ resolve_gitleaks() { echo "$ROOT/.tools/bin/gitleaks" return 0 fi + if [ -n "${LOCALAPPDATA:-}" ]; then + for candidate in "$LOCALAPPDATA"/Microsoft/WinGet/Packages/*/gitleaks.exe; do + if [ -x "$candidate" ]; then + echo "$candidate" + return 0 + fi + done + fi if command -v gitleaks >/dev/null 2>&1; then command -v gitleaks return 0