Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 23 additions & 18 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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 |

---

Expand All @@ -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

Expand Down
36 changes: 21 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) │
└───────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
48 changes: 48 additions & 0 deletions __tests__/platform.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
5 changes: 3 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
>
Expand Down Expand Up @@ -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}`)})`}
>
<Icon icon={m.icon} width={15} height={15} />
</button>
Expand Down
7 changes: 4 additions & 3 deletions components/agent-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(),
})

Expand Down Expand Up @@ -1893,7 +1894,7 @@ export function AgentPanel() {
<button
onClick={decreaseFontSize}
className="flex h-6 w-6 items-center justify-center rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] transition-colors cursor-pointer"
title="Decrease text size (⌘-)"
title={`Decrease text size (${formatShortcut('meta+-')})`}
>
<Icon icon="lucide:minus" width={12} height={12} />
</button>
Expand All @@ -1903,7 +1904,7 @@ export function AgentPanel() {
<button
onClick={increaseFontSize}
className="flex h-6 w-6 items-center justify-center rounded text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-subtle)] transition-colors cursor-pointer"
title="Increase text size (⌘+)"
title={`Increase text size (${formatShortcut('meta+=')})`}
>
<Icon icon="lucide:plus" width={12} height={12} />
</button>
Expand Down
24 changes: 15 additions & 9 deletions components/chat/chat-input-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
useState,
useCallback,
useEffect,
useMemo,
type KeyboardEvent,
type DragEvent,
type ClipboardEvent,
} from 'react'
import { Icon } from '@iconify/react'
import { formatShortcut } from '@/lib/platform'
import { ModeSelector } from '@/components/mode-selector'
import type { AgentMode } from '@/components/mode-selector'

Expand Down Expand Up @@ -113,12 +115,14 @@ function formatFileSize(dataUrl: string): string {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}

const PLACEHOLDER_HINTS = [
'Ask anything...',
'Ask anything... \u2318L to add selection',
'Ask anything... @ to mention a file',
'Ask anything... /commit to save changes',
]
function getPlaceholderHints(): string[] {
return [
'Ask anything...',
`Ask anything... ${formatShortcut('meta+L')} to add selection`,
'Ask anything... @ to mention a file',
'Ask anything... /commit to save changes',
]
}

export function ChatInputBar({
input,
Expand Down Expand Up @@ -160,17 +164,19 @@ export function ChatInputBar({
const [inputDragOver, setInputDragOver] = useState(false)
const inputDragCounter = useRef(0)

const placeholderHints = useMemo(() => getPlaceholderHints(), [])

useEffect(() => {
if (input) return
const interval = setInterval(() => {
setPlaceholderIdx((i) => (i + 1) % PLACEHOLDER_HINTS.length)
setPlaceholderIdx((i) => (i + 1) % placeholderHints.length)
}, 4000)
return () => clearInterval(interval)
}, [input])
}, [input, placeholderHints.length])

const currentPlaceholder = activeFile
? `Ask about ${activeFile.split('/').pop()}...`
: PLACEHOLDER_HINTS[placeholderIdx]
: placeholderHints[placeholderIdx]

const handleInputDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
e.preventDefault()
Expand Down
Loading