Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
1b46d02
feat(db): add subagent columns and migration v4
JeanBaptisteRenard May 21, 2026
271381e
feat(read-session-file): support subagent layout
JeanBaptisteRenard May 21, 2026
9e36ba2
feat(scan): index subagent transcripts in session cache
JeanBaptisteRenard May 21, 2026
2fee16f
feat(viewer): expand subagent transcripts inline via Agent block
JeanBaptisteRenard May 21, 2026
249749e
ci: add github actions workflow for npm test
JeanBaptisteRenard May 21, 2026
078e8cf
fix(get-projects): await scan when cache is empty post-migration
JeanBaptisteRenard May 21, 2026
a9357e5
fix(projects-payload): expose subagent fields to the renderer
JeanBaptisteRenard May 21, 2026
6db1951
Merge pull request #1 from JeanBaptisteRenard/feat/subagent-support
JeanBaptisteRenard May 21, 2026
2fd730f
fix(read-session-file): tolerate concurrent writes to JSONL files
JeanBaptisteRenard May 21, 2026
46d596c
fix(derive-project-path): collapse worktree cwd to parent project
JeanBaptisteRenard May 22, 2026
3d821b0
feat(transitions): detect subagent spawn/completion + live-tail IPC
JeanBaptisteRenard May 21, 2026
1fea1aa
feat(ui): hierarchical sidebar, grid badges, live indicator
JeanBaptisteRenard May 21, 2026
a6210a6
fix(transitions): silent cold-start to avoid IPC flood on attach
JeanBaptisteRenard May 22, 2026
bd5aff2
Merge pull request #2 from JeanBaptisteRenard/feat/subagent-observabi…
JeanBaptisteRenard May 22, 2026
1c235d1
feat(worktree): add 'Delete worktree' button that actually removes fr…
JeanBaptisteRenard May 21, 2026
fd82261
chore(ui): suffix app name with '(dev)' when not packaged
JeanBaptisteRenard May 22, 2026
f8124cf
test: cover resolveWorktreePath and detectSubagentTransitions cold-start
JeanBaptisteRenard May 22, 2026
0514607
Merge pull request #3 from JeanBaptisteRenard/feat/worktree-cleanup-ui
JeanBaptisteRenard May 22, 2026
92d70c6
Merge pull request #4 from JeanBaptisteRenard/tests/coverage-transiti…
JeanBaptisteRenard May 22, 2026
d58be56
perf(refresh): O(1) cached lookup in refreshFolder
JeanBaptisteRenard May 22, 2026
d1f901c
perf(refresh): targeted refresh — only stat files the watcher flagged
JeanBaptisteRenard May 22, 2026
d32dcae
perf(refresh): bump-only update for huge cached files
JeanBaptisteRenard May 22, 2026
ba1d55a
perf(refresh): header-only read for cached sessions, full read for ne…
JeanBaptisteRenard May 22, 2026
65c30da
feat(search): explicit reindex via Enter or refresh button
JeanBaptisteRenard May 22, 2026
206f393
fix(sidebar): destructure subagentIndex (empty-sidebar regression)
JeanBaptisteRenard May 22, 2026
50f8743
feat(sidebar): collapsible 'Orphan subagents' section, collapsed by d…
JeanBaptisteRenard May 22, 2026
69476e9
fix(sidebar): scope projectPath into buildSessionsList for orphan toggle
JeanBaptisteRenard May 22, 2026
e76a3df
perf(ui): throttle projects-changed notifies + renderer debounce
JeanBaptisteRenard May 22, 2026
b4d1c48
Merge pull request #5 from JeanBaptisteRenard/fix/refresh-folder-O1-l…
JeanBaptisteRenard May 22, 2026
1a6256f
chore: add Taskfile.yaml as standard task entrypoint
JeanBaptisteRenard May 22, 2026
8314f69
test(lint): add ESLint flat config + jsdom renderer tests for sidebar
JeanBaptisteRenard May 22, 2026
3a4cd04
chore: pre-commit hook runs task check (lint + tests)
JeanBaptisteRenard May 22, 2026
bf18dc1
feat(db): SWITCHBOARD_DATA_DIR env var to isolate dev DB from AppImage
JeanBaptisteRenard May 22, 2026
0001c83
Merge pull request #6 from JeanBaptisteRenard/chore/taskfile-tooling
JeanBaptisteRenard May 22, 2026
76aaaf3
fix(stats): source heatmap from session_cache instead of stats-cache.…
JeanBaptisteRenard May 22, 2026
0a24dce
fix(security+watcher): harden subagent IPCs, tighten worktree regex, …
JeanBaptisteRenard May 23, 2026
5f5e17b
feat(ui): subagent click opens read-only transcript instead of re-spa…
JeanBaptisteRenard May 23, 2026
641ebca
fix(ipc): use resolveJsonlPath in read-subagent-jsonl and start-subag…
JeanBaptisteRenard May 23, 2026
310a714
fix(read-session-file): reject colons in subagentSessionId args + test
JeanBaptisteRenard May 23, 2026
94567a9
fix(jsonl-viewer): use JSON.stringify key in agentMatchCounters to pr…
JeanBaptisteRenard May 23, 2026
b7a206a
chore: verify O(1) cachedMap lookup (d58be56) already on main — no ac…
JeanBaptisteRenard May 23, 2026
6d74fd1
feat(worktree): rich delete confirmation dialog with dirty-file status
JeanBaptisteRenard May 23, 2026
a5d9fbb
perf(transitions): readdir mtime cache + synthetic bootstrap spawn
JeanBaptisteRenard May 23, 2026
1afa99a
fix(sidebar): stable id for orphan-subagents group (morphdom flicker)
JeanBaptisteRenard May 23, 2026
92b119d
fix(sidebar): one-time GC for expandedSubagents localStorage key
JeanBaptisteRenard May 23, 2026
b658877
Merge pull request #10 from JeanBaptisteRenard/fix/pr47-cleanup
JeanBaptisteRenard May 23, 2026
1acfc71
Merge pull request #8 from JeanBaptisteRenard/fix/post-review-hardening
JeanBaptisteRenard May 23, 2026
de51d02
Merge pull request #12 from JeanBaptisteRenard/perf/pr48-observabilit…
JeanBaptisteRenard May 23, 2026
1ab01aa
Merge pull request #9 from JeanBaptisteRenard/feat/subagent-transcrip…
JeanBaptisteRenard May 23, 2026
2203596
Merge pull request #11 from JeanBaptisteRenard/feat/pr49-delete-workt…
JeanBaptisteRenard May 23, 2026
f843086
feat(main): requestSingleInstanceLock to prevent PTY loss on AppImage…
JeanBaptisteRenard May 23, 2026
2ff6660
Merge pull request #13 from JeanBaptisteRenard/feat/single-instance-lock
JeanBaptisteRenard May 24, 2026
04fe34a
feat(ui): per-project .work-files/ navigable tab
JeanBaptisteRenard May 24, 2026
6130013
Merge pull request #14 from JeanBaptisteRenard/feat/work-files-tab
JeanBaptisteRenard May 24, 2026
e1a99fc
fix(work-files): dedupe projects by projectPath in get-work-files IPC…
JeanBaptisteRenard May 24, 2026
d20a426
feat(work-files): add delete + JSON/JSONL format buttons (#16)
JeanBaptisteRenard May 24, 2026
1cc2510
fix(work-files): surgical removal on delete instead of full disk re-s…
JeanBaptisteRenard May 24, 2026
280b4e0
fix(clipboard): route terminal copy through main process for Wayland …
JeanBaptisteRenard May 30, 2026
dbbc8fa
fix(app): showSession() on openTerminal error path (#19)
JeanBaptisteRenard May 30, 2026
effeaeb
feat: detect missing project paths + remap (port of upstream #35) (#20)
JeanBaptisteRenard May 30, 2026
a9d6e75
docs: AI agent guidelines + AppImage replacement safety (#21)
JeanBaptisteRenard May 30, 2026
8b9ef68
docs(contexts): sub-system docs for AI navigation (#22)
JeanBaptisteRenard May 30, 2026
549b3a2
chore(ai): context_engineering_profile + AGENTS.md hint (#23)
JeanBaptisteRenard May 31, 2026
6196423
feat(main): trigger-watcher — file-based input injection for harness …
JeanBaptisteRenard May 31, 2026
783a933
docs: correct build-while-running invariant after 2026-05-31 incident…
JeanBaptisteRenard May 31, 2026
ffbeb44
feat(trigger-watcher): raise default idle timeout to 5min + add per-t…
JeanBaptisteRenard May 31, 2026
8746466
fix(trigger-watcher): check child-process liveness before write
JeanBaptisteRenard May 31, 2026
9dd4a66
feat(trigger-watcher): add chain field for sequential injection
JeanBaptisteRenard May 31, 2026
6a7e717
feat(stats): collect per-(session,date,model) token/tool/message metrics
JeanBaptisteRenard Jun 1, 2026
2b62009
feat(trigger-watcher): chain triggers + discrete-Enter submit
JeanBaptisteRenard Jun 1, 2026
528513e
fix(session-cache): prune stale cache rows on every startup
JeanBaptisteRenard Jun 1, 2026
6890a10
Merge pull request #33 from JeanBaptisteRenard/fix/trigger-watcher-di…
JeanBaptisteRenard Jun 2, 2026
c83043e
Merge pull request #32 from JeanBaptisteRenard/fix/stats-metrics-coll…
JeanBaptisteRenard Jun 2, 2026
13219eb
fix(stats): wire replaceSessionMetrics + touchCachedModified into ses…
JeanBaptisteRenard Jun 2, 2026
3b6adf0
Merge pull request #34 from JeanBaptisteRenard/fix/stats-ctx-db-wiring
JeanBaptisteRenard Jun 2, 2026
7c00cb6
perf: cut idle CPU from leaked watchers and unconditional polling
JeanBaptisteRenard Jun 2, 2026
4724527
Merge pull request #35 from JeanBaptisteRenard/perf/idle-cpu-watchers…
JeanBaptisteRenard Jun 2, 2026
5384778
Ignore the worktree option when resuming a session (fix at the source)
ymajoros Jun 4, 2026
51924a2
Reconcile cache with filesystem on get-projects so sessions stop goin…
ymajoros Jun 4, 2026
3324960
fix(trigger-watcher): verify submission and retry the Enter once
JeanBaptisteRenard Jun 4, 2026
ef1df24
Merge branch 'main' into fix/trigger-watcher-submit-verify
JeanBaptisteRenard Jun 4, 2026
2843ec7
Merge pull request #36 from JeanBaptisteRenard/fix/trigger-watcher-su…
JeanBaptisteRenard Jun 4, 2026
e5d860f
docs: cp over a running AppImage is NOT reliably safe (appimagelaunch…
JeanBaptisteRenard Jun 4, 2026
90a63dc
Merge pull request #37 from JeanBaptisteRenard/docs/cp-appimagelaunch…
JeanBaptisteRenard Jun 4, 2026
dec59bd
Merge pull request #31 from ymajoros/fix/no-worktree-on-resume-jbr
JeanBaptisteRenard Jun 4, 2026
abb4950
Merge pull request #29 from ymajoros/fix/reconcile-cache-jbr
JeanBaptisteRenard Jun 4, 2026
d9210b6
polish(reconcile): throttle back-to-back sweeps, document the sync ca…
JeanBaptisteRenard Jun 4, 2026
4046c67
Merge pull request #38 from JeanBaptisteRenard/polish/reconcile-throttle
JeanBaptisteRenard Jun 4, 2026
b5a4a38
feat(trigger-watcher): re-queue remaining chain steps on global-deadl…
JeanBaptisteRenard Jun 7, 2026
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
36 changes: 36 additions & 0 deletions .ai/contexts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Context engineering — Switchboard

Five sub-system docs, ~150 lines each, written for AI agents who need to make a focused change without re-reading 1800 LOC of `main.js`.

## When to read which

| Touching this area | Read |
|---|---|
| SQLite, indexing, search, heatmap aggregation, fs.watch | [session-cache](session-cache.md) |
| Cron schedules, schedule `.md` files, `claude --resume -p` spawn | [schedule-runner](schedule-runner.md) |
| Subagent sidebar grouping, transcript view, parent→child wiring | [subagent-observability](subagent-observability.md) |
| Plans/Memory/.work-files tabs, CodeMirror panel, format/delete buttons | [viewer-panel](viewer-panel.md) |
| New IPC, preload bridge changes, renderer ↔ main protocol | [ipc-bridge](ipc-bridge.md) |
| File-trigger watcher, harness input injection, idle-wait | [trigger-watcher](trigger-watcher.md) |

## Reading order for a new contributor (~30 min)

1. **`ipc-bridge`** — gives you the public surface of the whole app in one page
2. **`session-cache`** — gives you the data model
3. **`subagent-observability`** — the #1 fork-specific feature, touches everything
4. **`viewer-panel`** — the reusable read/edit-file framework
5. **`schedule-runner`** — least-cross-cutting; safe to skip if you're not touching schedules

## What's NOT in these contexts (intentionally)

- **Terminal management** — xterm.js + node-pty integration in `public/terminal-manager.js`. Not yet documented because it's largely upstream code with minor extensions. If you're working there, read the file directly.
- **MCP / IDE emulation** — file diff panel, OSC 8 hyperlinks, etc. Lives in `public/file-panel.js` and `main.js` MCP bridge handlers. Not yet documented; in-flight upstream work.
- **Settings UI** — per-project + global, lives in `public/settings-panel.js`. Mostly self-contained, low coupling.
- **Sidebar rendering details** — covered piecemeal in subagent-observability + session-cache; the full sidebar is `public/sidebar.js`. If you're doing UI work there, expect to read the file.
- **Build / electron-builder** — covered in [README.md](../../README.md). Not a code area an agent typically modifies.

## Updating these docs

When a feature lands, the corresponding context doc should change in the same PR. **If you can't decide which context owns a change, it probably means a new sub-system is emerging — write a new doc instead of stretching an existing one.**

Pre-existing observations / nits found while writing these docs are captured in [_issues.md](_issues.md). They're not blockers but worth a follow-up when adjacent work happens.
94 changes: 94 additions & 0 deletions .ai/contexts/_issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Observations from writing the context docs

Captured 2026-05-30 while writing the .ai/contexts/*.md files. None are blockers; they're follow-ups worth picking up when adjacent work happens.

## Architecture

- **`main.js` is 1849 LOC**, vs upstream's ~350. It mixes IPC handlers (the bulk), app-lifecycle wiring, scheduling glue, MCP bridge, native-module instantiation, and updater integration. A modest refactor would split it into:
- `ipc/sessions.js`, `ipc/projects.js`, `ipc/work-files.js`, `ipc/stats.js`, `ipc/updater.js`
- `lifecycle.js` (app.on, BrowserWindow, single-instance lock, requestSingleInstanceLock)
- `mcp-bridge.js` (already partial)

Risk: lots of churn for diff readability. Worth doing once, all at once, **not piecemeal**.

- **No subagent-observability module** — the feature is implemented across `session-cache.js` (indexing), `main.js` (4+ IPCs), `public/sidebar.js` (rendering), `public/jsonl-viewer.js` (transcript). Considered creating a `subagent/` directory but the cross-cuts are real (it's a feature, not a sub-system). The context doc cross-references the pieces.

## IPC surface

- **Inconsistent return shapes**: some IPCs return `{ok: true}`, others return raw values, others return `{error: '...'}` on failure with no `ok` field. No canonical contract. Examples:
- `delete-work-file` returns `{ok, error}` — well-shaped
- `read-work-file` returns a raw string OR a sentinel string like `'[binary file]'` — bad
- `remap-project` returns `{ok: true}` or `{error: '...'}` — well-shaped but no `ok: false` discriminator

Standardising would be a sweep-style PR; touchable by Sonnet in a couple of hours.

- **`get-stats` legacy**: kept for backward compat ("fallback") but no caller uses it since PR #7. Could be removed.

- **`updaterEvent` polymorphism**: one event type covers 5+ subtypes. Different from the per-event `onSubagentSpawned/Completed` pattern. Inconsistency tax with no migration cost — fix when next touching the updater.

## session-cache

- **`getCachedByFolder` is `SELECT *`** — convenient for additions but means any new column lands in every consumer's payload silently. Probably fine; just be aware.

- **`refreshFolder(folder, opts)` does NOT take a "force header-only" knob** — it picks header-only vs full read per file based on whether the file was already cached. For most paths this is right, but a manual cache-bust would need a way to force full read.

- **No tests for `enumerateSessionFiles` directly** — it's exercised transitively via `derive-project-path.test.js` and `remap-project.test.js`. A dedicated unit test for the file enumeration would be cheap and prevent future regressions.

- **FTS `'work-file'` entries include the full file body** for files ≤ 64 KB and non-`.jsonl`. No per-file gzip or content-hash; if a 60 KB markdown changes on every byte, the FTS table churns. Acceptable for personal use; not for large monorepos.

## schedule-runner

- **No DST handling**. 02:30 on spring-forward = silently skipped. 02:30 on fall-back = fires twice. Document fix would be `new Date().getTimezoneOffset()` watch; real fix is non-trivial.

- **Hand-rolled cron parser** has no `@daily`/`@hourly` aliases. Most users won't notice but it's surprising vs other cron implementations.

- **`runningTasks` is a Set per file path** — schedules can't share state. Fine for the current design.

- **No `kill timeout`** on detached child processes. A schedule that runs forever (or hangs on permission prompt despite `acceptEdits`) will silently consume a slot until process death.

## viewer-panel

- **`viewer-panel.js` constructor takes 9+ opts**. Could benefit from a builder or named-option grouping (clipboard, save, delete) but the current shape is workable.

- **No way to switch language mode after `open()`** — `language: 'auto'` infers from file extension at construction, but if you re-`open()` a `.md` file in a panel constructed with `language: 'auto'`, the editor mode may stale-cache.

- **`format` JSON-line joiner uses `\n---\n` as separator** — not valid JSON, intentional. Documented in the context doc, but a reader unfamiliar with the choice may file a bug.

- **No undo across `format` invocations** — CodeMirror's undo stack tracks the format as one large diff. Multiple format clicks accumulate. Acceptable.

## subagent-observability

- **The "Resume in terminal anyway" escape hatch is a footgun by design** — re-resuming a subagent that's done can corrupt context. A confirm dialog would prevent accidental clicks.

- **No persistence of which subagent transcripts the user has "seen"** — every reload shows them as fresh. Could be tracked via `localStorage` for a "new" badge.

- **Watch leak risk on rapid open/close** — `drainViewerWatches` handles the close case, but if the user opens 20 subagent transcripts in quick succession and only the last one is visible, 19 watchers may still tail their files until panel destroy. Worth profiling.

## Tests

- **No `enumerateSessionFiles` unit test** (see session-cache section).
- **No integration test for the full schedule-fire pathway** — `scanSchedules` is tested, `cronMatches` is tested, but the wired-up `setInterval + runScheduleCommand` path is not.
- **Worktree node_modules can have missing native modules** (morphdom-umd.js encountered 2026-05-30) — confused a code-reviewer agent into reporting false "pre-existing fails". Could be fixed by `npm install` after worktree creation as a harness step.

## Build pitfalls

- **`npm run build:linux` while Switchboard is running can kill the running instance.** Witnessed 2026-05-31 ~13:49 — the AppImage process stopped logging mid-window during a background `npm run build:linux`, no SIGTERM trace in `~/.config/switchboard/logs/main.log`, just a 9-minute silence then a fresh launch by the user. Confirmed harmless: the file `cp dist/*.AppImage ~/Applications/` AFTER the build completes (the cp itself is safe — the running process is mmap'd from `/tmp/.mount_*`).

**Suspected mechanism**: electron-builder invokes `@electron/rebuild` for native modules (`better-sqlite3`, `node-pty`). These modules are `dlopen()`-loaded by the running AppImage. If electron-rebuild uses `truncate+write` instead of `atomic rename`, the running process loses access to its `better_sqlite3.node` binding at the next SQLite call → segfault → kernel SIGKILL with no app-level trace. Not confirmed without sudo dmesg access.

**Mitigations to consider**:
1. **Skip native rebuild when running locally**: `npm config set npm_config_skip_electron_rebuild true` before build, or use `--config.npmRebuild=false` on electron-builder, or build only the asar (`electron-builder --linux AppImage --dir`).
2. **Build in a dedicated worktree with its own `node_modules/`**: full isolation, slower (extra `npm install`).
3. **Document loudly and tell the user before any background build**: lowest tech, highest reliability.

**The `cp` step itself is safe** — confirmed by replacing the file while the process kept running (mtime 17:36, PID 2620410 still alive). The running AppImage is fully extracted to `/tmp/.mount_*` at launch and does not page back from the on-disk file.

## Security

- **`clipboard-write-text` accepts any string from any renderer** — fine given the single-renderer, single-user threat model, but worth a sentence in the security model doc if one exists.
- **`remap-project` validates `newPath` via `lstatSync` (no symlink follow)** — correct since PR #20 fix. Worth a code comment noting why `lstatSync` (not `statSync`).
- **No CSP on the renderer** — likely fine for a local desktop app that doesn't load remote content, but worth a thought if the file-panel ever displays untrusted HTML.

---

*Use these as ammunition for opportunistic follow-ups, not as a TODO list. Most of them are "nice to have" — feature work should keep taking priority.*
133 changes: 133 additions & 0 deletions .ai/contexts/ipc-bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Context: ipc-bridge

**Purpose**: The trust boundary between the Electron main process (Node, full filesystem) and the renderer (Chromium, sandboxed). The renderer can only call what `preload.js` exposes via `contextBridge`; everything else is denied.

This file is the **canonical inventory** of the IPC surface. When you add a new IPC, you change three places — main handler, preload bridge, renderer caller — and every IPC name should appear here.

## Key files

| File | LOC | Role |
|---|---|---|
| `preload.js` | ~130 | The `contextBridge.exposeInMainWorld('api', {...})` block. Every renderer-facing function. |
| `main.js` | ~1850 | The `ipcMain.handle('<name>', ...)` and `ipcMain.on('<name>', ...)` handlers, scattered throughout. |

## Public surface (IPC inventory)

### Sessions (request/response)

| IPC | Args | Returns | Notes |
|---|---|---|---|
| `get-projects` | `(showArchived)` | `Project[]` | Sidebar payload. Reads from cache. |
| `get-active-sessions` | — | `Session[]` | Currently open PTY sessions |
| `get-active-terminals` | — | `Terminal[]` | Active PTY identifiers |
| `open-terminal` | `(id, projectPath, isNew, sessionOptions)` | `{ok, error?, mcpActive}` | Spawn or attach a PTY. |
| `stop-session` | `(id)` | `{ok}` | Kill the PTY for `id`. |
| `toggle-star` | `(id)` | `{ok}` | Star/unstar in session_meta. |
| `rename-session` | `(id, name)` | `{ok}` | Set customTitle. |
| `archive-session` | `(id, archived)` | `{ok}` | Move to archive. |
| `read-session-jsonl` | `(sessionId)` | `Entry[]` | Full transcript. |
| `read-subagent-jsonl` | `(parentSessionId, agentId)` | `Entry[]` | Subagent transcript. |
| `list-subagents` | `(parentSessionId)` | `Subagent[]` | All children of a parent. |
| `start-subagent-watch` | `(parentSessionId, agentId)` | `watchId` | Begin tailing. |
| `stop-subagent-watch` | `(watchId)` | `{ok}` | Tear down watch. |

### Projects + worktrees

| IPC | Args | Notes |
|---|---|---|
| `browse-folder` | — | Native folder picker |
| `add-project` | `(projectPath)` | Register a project (creates folder index) |
| `remove-project` | `(projectPath)` | Hide a project from sidebar |
| `remap-project` | `(oldPath, newPath)` | **Atomic JSONL `cwd` rewrite**, refuses if active sessions exist. See PR #20. |
| `delete-worktree` | `(worktreePath)` | `git worktree remove` |
| `worktree-status` | `(worktreePath)` | Dirty-file count |

### Tabs (Plans / Memory / .work-files / Stats)

| IPC | Returns |
|---|---|
| `get-plans` | `Plan[]` (reads `~/.claude/plans/*.md`) |
| `read-plan` / `save-plan` | content / `{ok}` |
| `get-memories` | `{global, projects}` |
| `read-memory` / `save-memory` | content / `{ok}` |
| `get-work-files` | `{projects: WorkFilesProject[]}` — **dedupes by projectPath** since PR #15. Walks `<projectPath>/.work-files/` recursively, capped at 200 files per project. |
| `read-work-file` / `delete-work-file` | content (with `.work-files/` path guard) / `{ok}` |
| `get-stats-from-db` | `{dailyActivity, totalMessages, totalSessions, firstSessionDate, lastComputedDate}` — heatmap source since PR #7 |
| `refresh-stats` | `{stats, usage}` — combined; calls `getDailyActivity` + `fetchAndTransformUsage` |
| `get-usage` | rate-limits payload from Claude `/usage` |
| `get-stats` | `~/.claude/stats-cache.json` raw (legacy; kept for fallback) |

### Search

| IPC | Args | Returns |
|---|---|---|
| `search` | `(type, query, titleOnly)` | FTS5 result rows. `type ∈ {session, subagent, plan, memory, work-file, null}` |
| `rebuild-cache` | — | Force a full re-index (heavy) |

### Settings

| IPC | Notes |
|---|---|
| `get-setting` / `set-setting` / `delete-setting` | Generic key/value over `settings` table |
| `get-effective-settings` | `(projectPath)` — resolves global + project overrides |
| `get-shell-profiles` | Configured shell list |
| `get-schedule-creator-command` / `create-schedule-session` / `run-schedule-now` | Schedule integration |

### File panel (IDE mode)

| IPC | Args |
|---|---|
| `read-file-for-panel` / `save-file-for-panel` | Arbitrary file IO inside the user's projects |
| `watch-file` / `unwatch-file` | fs.watch wrapper, emits `file-changed` event |

### Misc

| IPC | Notes |
|---|---|
| `open-external` | Opens https:// URLs in OS browser |
| `clipboard-write-text` | Main-process clipboard write (Wayland fix, PR #18) |
| `get-app-version` | From package.json |
| `updater-check` / `updater-download` / `updater-install` | electron-updater |

### Send (fire-and-forget, renderer → main)

| IPC | Notes |
|---|---|
| `terminal-input` | Forward keypress to PTY |
| `terminal-resize` | Resize PTY columns/rows |
| `close-terminal` | Renderer signals tab closed |
| `mcp-diff-response` | Diff accept/reject from MCP IDE mode |

### Events (main → renderer)

`terminal-data`, `session-detected`, `process-exited`, `terminal-notification`, `cli-busy-state`, `session-forked`, `subagent-spawned`, `subagent-completed`, `subagent-watch-event`, `projects-changed`, `status-update`, `file-changed`, `mcp-open-diff`, `mcp-open-file`, `mcp-close-all-diffs`, `mcp-close-tab`, `updater-event`

## Invariants

- **No `nodeIntegration` in renderer**. The renderer can only call what's in `window.api`. `contextIsolation: true` is mandatory in BrowserWindow options.
- **Every IPC must validate its arguments** at the main-side handler. The renderer is trusted-ish (single user, single window) but a compromised renderer should not be able to escape the user's working directories.
- **Path-touching IPCs (`read-work-file`, `delete-work-file`, `read-memory`, etc.) MUST guard their paths**. Pattern: `path.resolve(input).includes('/.work-files/')` for the work-files IPC. Audit every new path IPC.
- **Trust boundary is the contextBridge call**. Anything passed across must survive structured-clone serialization. No functions, no DOM nodes, no class instances — only plain JSON.
- **Async handlers return promises**. Renderer uses `await window.api.foo(...)`. Throws cross the boundary as rejected promises; return `{ok, error}` if you want graceful failure handling on the renderer side.

## Non-obvious behaviors

- **`preload.js` is the *single* surface the renderer sees**. If you add `ipcMain.handle('xyz', ...)` but forget to add `xyz: () => ipcRenderer.invoke('xyz')` in preload, the renderer can't call it. Symptom: `window.api.xyz is not a function`.
- **Webcontents `send` events vs `invoke`**: `invoke`/`handle` is request-response (returns a promise). `send`/`on` is fire-and-forget (no return). Pick based on whether the caller needs the result.
- **`webUtils.getPathForFile(file)`** is the only way to get the absolute path of a drag-and-dropped file in Electron 28+. Exposed at `window.api.getPathForFile`.
- **Updater events use a single `onUpdaterEvent(type, data)` callback** for all 5+ event types — different from the per-event onSubagentSpawned/Completed pattern. Inconsistency tax.

## If you change this, also check

- **Three places per new IPC**: handler in `main.js`, bridge entry in `preload.js`, caller in `public/*.js` (and maybe `eslint.config.js` if you expose a new global).
- `eslint.config.js` `rendererCrossFileGlobals` — renderer functions exposed across `<script>` tags must be declared
- Any new event needs both an `ipcRenderer.on` in preload and an `mainWindow.webContents.send` in main
- If you change argument shapes, the renderer callers break silently (no type check) — search for callers before changing signatures

## How to add a new IPC

1. `main.js`: `ipcMain.handle('my-thing', (_event, arg1, arg2) => { /* validate, do, return */ })` — place near related handlers, not at random.
2. `preload.js`: `myThing: (arg1, arg2) => ipcRenderer.invoke('my-thing', arg1, arg2)` — add to the alphabetical-ish block.
3. Renderer: `const result = await window.api.myThing(...)`
4. Test: prefer a unit test for the main-side logic (extract to a pure function the handler calls); jsdom integration tests for renderer side.
5. Document here.
Loading