Skip to content

Commit eac9e61

Browse files
committed
Docs: transform write-only docs to something live
- Delete old specs and notes. They'll be retained in git history. - Create CLAUDE.md files throughout the repo, and `architecture.md` that maps them. - Move user-docs to apps/desktop where it belongs - Add a Claude rule to keep agents maintaining the docs - Update AGENTS.md with new docs-related info
1 parent 26443c1 commit eac9e61

131 files changed

Lines changed: 1627 additions & 21194 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/docs-maintenance.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
paths:
3+
- "apps/**"
4+
- "scripts/**"
5+
---
6+
7+
When modifying code in a directory that contains a `CLAUDE.md` file, check whether your changes affect the documented
8+
architecture, key decisions, or gotchas. If they do, update the `CLAUDE.md` to stay in sync.
9+
Skip this for trivial changes like bug fixes, formatting, small refactors that don't change the architecture).

AGENTS.md

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ Core structure:
3131
- `license-server/` - Cloudflare Worker (Hono). Receives Paddle webhooks, generates&validates Ed25519-signed keys.
3232
- `website/` - Marketing website (getcmdr.com)
3333
- `/scripts/check/` - Go-based unified check runner (replaces individual scripts)
34-
- `/docs/` - Docs including `style-guide.md`
35-
- `artifacts/` - Development byproducts kept for reference. They describe the history of the system, not its state.
36-
- `adr/` - Architecture decisions
37-
- `notes/` - Other notes
38-
- `specs/` - Temporary spec docs and task lists kept during development
39-
- `features/` - Description of each major feature of the system
34+
- `/docs/` - Dev docs
35+
- `adr/` - Architecture decision records
4036
- `guides/` - How-to guides
41-
- `tooling/` - Like "features", but for internal tooling
42-
- `user-docs/` - The rest of `/docs` are all dev docs. These are user-facing, written with that audience in mind.
37+
- `tooling/` - Internal tooling docs
38+
- `architecture.md` - Map of all subsystems with pointers to colocated `CLAUDE.md` files
39+
- `style-guide.md` - Writing and code style rules
40+
- `security.md` - Security policies
41+
- Feature-level docs live in **colocated `CLAUDE.md` files** next to the code (for example,
42+
`src/lib/settings/CLAUDE.md`). Claude Code auto-discovers these. See `docs/architecture.md` for the full map.
4343

4444
## Testing & checking
4545

@@ -108,15 +108,39 @@ There are two MCP servers available to you:
108108
## Things to avoid
109109

110110
- ❌ Don't touch git, user handles commits manually. Unless explicitly asked to.
111-
- ❌ Don't use classes in TypeScript (use functional components/modules)
112111
- ❌ Don't add JSDoc that just repeats types or obvious function names
113-
- ❌ Don't use `any` type (ESLint will error)
114112
- ❌ Don't ignore linter warnings (fix them or justify with a comment)
115113
- ❌ Don't add dependencies without checking license compatibility (`cargo deny check`)
116114

115+
### TypeScript
116+
117+
- Only functional components and modules. No classes.
118+
- Don't use classes. Use functional components/modules.
119+
- Don't use `any` type. ESLint will error.
120+
- Prefer functional programming (map, reduce, some, forEach) and pure functions wherever it makes sense.
121+
- Use `const` for everything, unless it makes the code unnecessarily verbose.
122+
- Start function names with a verb, unless unidiomatic in the specific case.
123+
- Use `camelCase` for variable and constant names, including module-level constants.
124+
- Put constants closest to where they are used. If a constant is only used in one function, put it in that function.
125+
- For maps, try to name them like `somethingToSomeethingElseMap`. That avoids unnecessary comments.
126+
- Keep interfaces minimal: only export what you must export.
127+
128+
### Rust
129+
130+
- Max 120 char lines, 4-space indent, cognitive complexity threshold: 15, enforced by clippy.
131+
132+
### CSS
133+
134+
- `html { font-size: 16px; }` is set so `1rem = 16px`. Use `px` by default but can use `rem` if it's more descriptive.
135+
- Use variables for colors, spacing, and the such, in `app.css`.
136+
- Always think about accessibility when designing, and dark+light modes.
137+
117138
## Planning
118-
- When coming up with a plan for a development, save it to `docs/specs/{feature}-plan.md in this repo.
119-
- Also create an accompanying task list that fully covers but doesn't duplicate the plan on a high level.
139+
140+
- When getting oriented, consider the docs: `docs` folder and `CLAUDE.md` files in each directory.
141+
- When coming up with a plan for a development, save it to `docs/specs/{feature}-plan.md` in this repo (we clean out old
142+
plans every few weeks/months, git history remembers them).
143+
- Also create an accompanying task list that fully covers but doesn't duplicate the plan on a high level.
120144
If all items on the task list are honestly marked as done, the plan is fully implemented in great quality.
121145
Tasks should be one-liners, grouped by milestones. Include docs, testing, and running all necessary checks.
122146

@@ -126,6 +150,10 @@ There are two MCP servers available to you:
126150
- When testing, consider using Rust/Go tests, Vitest, Playwright, and manual tests with the MCP servers, whatever is
127151
needed to feel confident about the development. Do this per milestone. Don't go overboard with unit tests. Test
128152
exactly so that you feel confident.
153+
- **Keep docs alive**: When modifying a feature directory that has a `CLAUDE.md`, check if the doc still matches the
154+
code. Update it if your changes affect architecture, key decisions, or gotchas. Don't update for trivial changes.
155+
If there is no `CLAUDE.md` file yet, but you want to capture high-level info about a module or feature, create one.
156+
Make it faster for the next person or agent to get oriented.
129157

130158
Always do a last round of checks before wrapping up:
131159

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# File system listing module
2+
3+
Backend directory reading, caching, sorting, and streaming for the file explorer. Handles 100k+ file directories with non-blocking I/O and progress events.
4+
5+
## Architecture
6+
7+
### Module structure
8+
9+
- **mod.rs** – Public API exports, re-exports for crate-internal use
10+
- **reading.rs** – Low-level disk I/O (`list_directory_core()`, `get_single_entry()`, macOS metadata)
11+
- **streaming.rs** – Async streaming with progress events, cancellation
12+
- **operations.rs** – Synchronous frontend-facing API (lifecycle, cache accessors)
13+
- **caching.rs**`LISTING_CACHE` global state, `CachedListing` struct
14+
- **sorting.rs**`SortColumn`, `SortOrder`, `sort_entries()`
15+
- **metadata.rs**`FileEntry` struct, macOS extended metadata
16+
17+
### Data flow
18+
19+
```
20+
Frontend Backend
21+
| |
22+
|-- listDirectoryStartStreaming -->| (returns immediately)
23+
|<-- { listingId, status: loading }|
24+
| |
25+
| [background task spawns]
26+
| |
27+
|<--- listing-opening event --------| (just before read_dir)
28+
|<--- listing-progress event -------| (every 500ms)
29+
| { listingId, loadedCount } |
30+
| |
31+
|<--- listing-read-complete event --| (when read_dir finishes)
32+
| { listingId, totalCount } |
33+
| |
34+
| [sorting + caching + watcher start]
35+
| |
36+
|<--- listing-complete event -------| (ready for use)
37+
| { listingId, totalCount, |
38+
| maxFilenameWidth } |
39+
| |
40+
|-- getFileRange(listingId, ...) -->| (on-demand fetching)
41+
|<-- [FileEntry, FileEntry, ...] |
42+
```
43+
44+
### Caching strategy
45+
46+
**LISTING_CACHE**: Global `RwLock<HashMap<String, CachedListing>>`
47+
**Key**: `listing_id` (UUID per navigation)
48+
**Value**: `CachedListing { volume_id, path, entries, sort_by, sort_order }`
49+
50+
**Lifecycle**:
51+
1. `list_directory_start_streaming()` generates ID, spawns task
52+
2. Background task reads directory, sorts, stores in cache
53+
3. Frontend calls `get_file_range()` for visible entries (on-demand)
54+
4. `list_directory_end()` stops watcher, removes from cache
55+
56+
**Concurrency**: Multiple listings can coexist (different panes, rapid navigation). Each has unique ID.
57+
58+
## Key decisions
59+
60+
**Decision**: Streaming with background task, not chunked IPC
61+
**Why**: Chunked approach required multiple IPC calls, complex state tracking. Streaming spawns `tokio::task::spawn_blocking()`, emits events. Frontend stays responsive—Tab works, ESC cancels.
62+
63+
**Decision**: Cancellation via `AtomicBool` checked per-entry
64+
**Why**: Network folders iterate slowly (seconds per entry). Checking on each iteration ensures responsive cancellation. ESC → cancel within ~100ms.
65+
66+
**Decision**: Three-stage progress: opening → progress → read-complete → complete
67+
**Why**: Gives user fine-grained feedback:
68+
- `listing-opening`: "About to start slow I/O" (for network folders)
69+
- `listing-progress`: "Loaded N files..." (every 500ms)
70+
- `listing-read-complete`: "All files read, sorting now"
71+
- `listing-complete`: "Ready to render"
72+
73+
**Decision**: Sorting happens AFTER read, BEFORE caching
74+
**Why**: Frontend expects sorted order. Sorting 50k entries takes ~15ms (fast enough). Done in background task after all entries collected.
75+
76+
**Decision**: Hidden files filtering in Rust, not frontend
77+
**Why**: Cannot know visible count until all files read. APIs accept `include_hidden: bool`, filter during `get_file_range()` iteration.
78+
79+
**Decision**: Font metrics in Rust binary cache, not frontend canvas measurement
80+
**Why**: Measuring 50k filenames in JS is slow. Rust precomputes metrics for system fonts, stores in `.bin` cache. `calculate_max_width()` is a hash lookup.
81+
82+
**Decision**: File watcher starts AFTER listing complete
83+
**Why**: Watcher diffs rely on cached entries. Starting before cache is populated would miss initial state.
84+
85+
## Gotchas
86+
87+
**Gotcha**: Background task runs to completion even if cancelled on frontend
88+
**Why**: `loadGeneration` discards stale results, but Rust keeps iterating. Mitigation: `AtomicBool` checked per-entry stops early.
89+
90+
**Gotcha**: `get_file_range()` with `include_hidden=false` skips hidden entries
91+
**Why**: Indices are for VISIBLE items only. If item 5 is hidden, index 5 in `include_hidden=false` mode is actually item 6 in the full list. Backend handles filtering, frontend sees dense array.
92+
93+
**Gotcha**: Watcher diffs must update both cache AND emit events
94+
**Why**: Cache is source of truth for `get_file_range()`. Events notify frontend to re-fetch visible range. Missing either = stale data or no UI update.
95+
96+
**Gotcha**: Sorting changes invalidate cached range on frontend
97+
**Why**: Frontend cache holds entries in old sort order. Backend re-sorts, but frontend must re-fetch. `cacheGeneration` bump triggers this.
98+
99+
**Gotcha**: macOS extended metadata (addedAt, openedAt) requires extra syscalls
100+
**Why**: `list_directory_core()` uses fast `fs::read_dir()` + `metadata()`. Extended metadata needs `listxattr()`/`getxattr()`. Available via `get_extended_metadata_batch()` but not wired into streaming path yet.
101+
102+
**Gotcha**: `CANCELLATION_POLL_INTERVAL` is 100ms, but check happens per-entry
103+
**Why**: Named confusingly. The interval is for waiting on channels, not polling the flag. Actual cancellation is checked on EVERY entry iteration.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# File viewer module (Rust backend)
2+
3+
Provides three backend strategies for serving file content line-by-line with instant open, virtual scrolling, and background search.
4+
5+
## Key files
6+
7+
- `mod.rs` — public API, constants (1MB threshold, 256-line checkpoints, 8KB backward scan limit)
8+
- `session.rs` — session orchestration, backend switching, search state
9+
- `full_load.rs` — loads entire file into `String` (<1MB files)
10+
- `byte_seek.rs` — seeks by byte offset, scans backward for newline (instant open)
11+
- `line_index.rs` — sparse newline index (1 checkpoint per 256 lines), SIMD-accelerated via `memchr`
12+
13+
## Backend selection logic
14+
15+
```rust
16+
if file_size < 1MB {
17+
FullLoadBackend
18+
} else {
19+
// Start with ByteSeek (instant)
20+
ByteSeekBackend
21+
// Spawn background thread to build LineIndex
22+
// Upgrade to LineIndexBackend when ready
23+
}
24+
```
25+
26+
## Tauri commands
27+
28+
- `viewer_open(path)``ViewerOpenResult` (session ID, metadata, initial lines, backend type)
29+
- `viewer_get_lines(session_id, target_type, target_value, count)``LineChunk`
30+
- `viewer_search_start(session_id, query)` → starts background search
31+
- `viewer_search_poll(session_id)``SearchPollResult` (matches, progress, status)
32+
- `viewer_search_cancel(session_id)` → cancels running search
33+
- `viewer_close(session_id)` → frees resources
34+
- `viewer_setup_menu(label)` — builds viewer menu with word wrap item
35+
- `viewer_set_word_wrap(label, checked)` — syncs menu state
36+
37+
## Gotchas
38+
39+
- **VIEWER_SESSIONS is unbounded** — grows with each `viewer_open`. Must call `viewer_close` when window closes (not automatic).
40+
- **LineIndex build is async**`ViewerSession` upgrades backend when ready. Frontend sees backend type change via status query.
41+
- **Search state per session** — only one search can run per session. Starting a new search cancels the previous one.
42+
- **UTF-16 offsets for JS compatibility**`SearchMatch.column` and `.length` are in UTF-16 code units, matching JS `String.substring()`.
43+
- **ByteSeek backward scan limit** — 8KB max. If newline not found, line starts at scan boundary (truncated).
44+
- **LineIndex memory** — O(total_lines / 256) for checkpoints. For a 100M line file: ~390K checkpoints × 8 bytes = ~3MB.
45+
46+
## Performance targets
47+
48+
- **Open latency**: <10ms for any file size (ByteSeek), <50ms for 1GB file after LineIndex builds
49+
- **Scroll latency**: <16ms (60fps) for 50-line fetch
50+
- **Search**: ~500MB/s (SIMD-accelerated), progress updates every 10MB
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# MCP server
2+
3+
## Purpose
4+
5+
Expose Cmdr functionality to AI agents via the Model Context Protocol (MCP). Agents can control the app using the same capabilities available to users—no more, no less.
6+
7+
## Architecture
8+
9+
### Server (`server.rs`)
10+
11+
- Runs in a background tokio task spawned at app startup
12+
- Binds to `127.0.0.1:9224` (localhost only for security)
13+
- Streamable HTTP transport (MCP spec 2025-11-25)
14+
- Endpoints: `POST /mcp` (JSON-RPC), `GET /mcp/sse` (optional SSE), `GET /mcp/health`
15+
16+
### Protocol (`protocol.rs`)
17+
18+
- JSON-RPC 2.0 message parsing
19+
- Routes to `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`
20+
- Session management (though most clients don't use sessions)
21+
22+
### Tools (`tools.rs`)
23+
24+
18 semantic tools grouped by category:
25+
- Navigation (6): `select_volume`, `nav_to_path`, `move_cursor`, etc.
26+
- Cursor/Selection (3): `move_cursor`, `open_under_cursor`, `select`
27+
- File operations (3): `copy`, `mkdir`, `refresh`
28+
- View (3): `sort`, `toggle_hidden`, `set_view_mode`
29+
- Dialogs (1): `dialog` (unified open/focus/close)
30+
- App (2): `switch_pane`, `quit`
31+
32+
### Resources (`resources.rs`)
33+
34+
- `cmdr://state`: Complete app state in YAML (both panes, volumes, dialogs)
35+
- `cmdr://dialogs/available`: Static metadata about available dialogs
36+
37+
### Executor (`executor.rs`)
38+
39+
Routes tool calls to implementations. Most tools emit Tauri events that trigger the same code paths as keyboard shortcuts or menu clicks.
40+
41+
### State stores
42+
43+
- `PaneStateStore`: Current state of left/right panes (path, files, cursor, selection)
44+
- `SoftDialogTracker`: Which dialogs MCP thinks are open
45+
- `SettingsStateStore`: Current settings window state (section, settings, shortcuts)
46+
47+
Frontend syncs state to these stores via Tauri commands (`update_left_pane_state`, `mcp_update_settings_sections`, etc.).
48+
49+
## Key decisions
50+
51+
### Why agent-centric API?
52+
53+
The original design mirrored keyboard shortcuts (43 tools like `nav_up`, `nav_down`). This forced agents to make dozens of calls to find a file. The agent-centric redesign (Jan 2026) consolidated to 18 semantic tools (`move_cursor(index=42)`, `nav_to_path("/Users")`). This reduced round-trips from 6+ reads to 1 (`cmdr://state` resource).
54+
55+
### Why YAML over JSON for resources?
56+
57+
LLMs consume resources, not machines. YAML is 30-40% smaller and more readable. The `cmdr://state` resource is optimized for LLM token usage, not parsing speed.
58+
59+
### Why plain text responses?
60+
61+
Tool results are plain text (`"OK: Navigated to /Users"`, `"ERROR: Path not found"`), not JSON objects. This reduces token usage and is easier for LLMs to parse. Errors are still JSON-RPC error objects, but the `content` field is plain text.
62+
63+
### Why stateful architecture?
64+
65+
Without state, resources would need to query the frontend on every read (slow, async). Storing state in Rust allows synchronous reads. The frontend syncs state after meaningful changes (file load, cursor move, selection).
66+
67+
### Why no file system access?
68+
69+
Security via parity: agents can only do what users can do. Giving agents `fs.read`/`fs.write` would violate this. Agents navigate the UI just like users, using `move_cursor`, `open_under_cursor`, etc.
70+
71+
### Why localhost only?
72+
73+
Binding to `0.0.0.0` would expose the server to the network. An attacker could quit the app, change settings, or navigate to sensitive directories. Localhost binding ensures only local processes can connect.
74+
75+
### Why separate state stores?
76+
77+
`PaneStateStore` is always synced (file pane changes frequently). `SettingsStateStore` is only synced when settings window is open (rare). `SoftDialogTracker` is updated by MCP tools themselves. Separating concerns keeps each store simple.
78+
79+
## Gotchas
80+
81+
### Server starts in background task
82+
83+
`start_mcp_server()` spawns a tokio task and returns immediately. If the server crashes, the app continues but MCP stops working. Check logs for "MCP server crashed" errors.
84+
85+
### State sync is best-effort
86+
87+
Frontend calls `update_left_pane_state()` after loading files, but there's no guarantee it completes before an MCP resource read. In practice, updates are fast and this isn't an issue. If stale data is a concern, add explicit sync waits.
88+
89+
### Dialog state is "soft"
90+
91+
`SoftDialogTracker` stores which dialogs MCP thinks are open, but if a dialog is closed manually (not via MCP), the tracker isn't updated. The `cmdr://state` resource double-checks reality by querying Tauri windows.
92+
93+
### View mode affects resource detail
94+
95+
`cmdr://state` shows file details differently based on view mode:
96+
- Full mode: all file info inline (`i:42 f package.json 1183b lm:2025-01-10`)
97+
- Brief mode: only cursor file gets details, rest are just names (`i:42 f package.json`)
98+
99+
This prevents overwhelming agents with data they can't see in the UI.
100+
101+
### Pane state includes pagination
102+
103+
Large directories (50k+ files) are paginated. The `totalFiles`, `loadedStart`, `loadedEnd` fields indicate what's currently loaded. Agents must use `scroll_to(index)` to load different regions.
104+
105+
### Resources don't require initialization
106+
107+
Unlike tools (which need a session via `initialize`), resources can be read immediately after server start. This is by design for debugging with curl.
108+
109+
### Settings state sync is window-specific
110+
111+
The settings window calls `syncSettingsState()` on mount and section changes. The main window doesn't sync settings state (it doesn't need to). This means `cmdr://state` only includes settings when the settings window is open.
112+
113+
### MCP-settings bridge vs MCP-shortcuts listener
114+
115+
Settings window: full bridge (`mcp-settings-bridge.ts`) syncs all state and handles all MCP events.
116+
Main window: lightweight listener (`mcp-shortcuts-listener.ts`) only handles shortcut changes.
117+
This separation keeps main window overhead minimal.
118+
119+
### Tool execution is synchronous
120+
121+
`execute_tool()` is a synchronous function. Tools that trigger async operations (like `copy`, `mkdir`) return immediately after emitting the event. The tool result doesn't wait for the operation to complete. This is intentional—tools return "OK: Copy dialog opened" not "OK: Files copied".
122+
123+
### Error codes are JSON-RPC standard
124+
125+
`INVALID_PARAMS = -32602`, `INTERNAL_ERROR = -32603`, etc. These are defined by the JSON-RPC spec, not MCP. Don't change them.
126+
127+
### Schema version doesn't apply to MCP state
128+
129+
MCP state stores don't have `_schemaVersion` fields. They're runtime-only, not persisted. If the state format changes, just restart the app.

0 commit comments

Comments
 (0)