# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this app is A macOS Tauri 2 desktop app (Vue 3 + Rust) for browsing, viewing, and trashing local session transcripts from coding agent CLIs — currently **Claude Code**, **Codex**, and **Gemini CLI**. Each CLI stores JSONL transcripts in its own on-disk layout; this app normalizes them all into the same project → sessions → chat UI, plus a soft-delete trash that survives across agents. The app is read-only against the original transcripts — deletion is a `move` into a trash dir, never `rm`. ## Commands ```bash npm run tauri dev # full dev (Tauri shell + Vite on :1420) npm run tauri build # bundle .app / .dmg into src-tauri/target/release/bundle/ npm run dev # web-only Vite preview; Tauri invokes will fail npm run build # vue-tsc --noEmit + vite build npm test # vitest watch mode npm run test:run # vitest single run (CI) npm run test:coverage # vitest single run + v8 coverage report ``` There is no linter wired up — `npm run build` (which runs `vue-tsc --noEmit` first) is the typecheck step. Unit tests run on **Vitest** (jsdom env) and live under `test/`, mirroring `src/`. They cover the agent-agnostic logic modules (`format`, `i18n`, `settings`, `chatToolbar`, `trashToolbar`, `sessionsToolbar`, `export`, `api`, `fly`, `tooltip`) and the leaf components (`DiffBlock`, `ToolResult`, `CollapsibleBox`, `Sidebar`, `SidebarTopbar`, `SessionsTopbar`, `TrashTopbar`, `SettingsModal`). Config is `vitest.config.ts` (separate from `vite.config.ts`); jsdom polyfills for `matchMedia` / `ResizeObserver` / `Element.animate` live in `test/setup.ts`. `App.vue`, `views/`, and `modals/` are stateful shells left to manual/e2e testing and excluded from coverage. `test/tsconfig.json` is IDE-only — the production build never type-checks `test/`. Vite is locked to port `1420` (strictPort) because `tauri.conf.json` hardcodes that URL. `src-tauri/**` is excluded from Vite's watcher; Rust changes are picked up by Tauri's own dev loop. ## Architecture ### Two-side split - **Frontend** (`src/`) is a thin Vue 3 SPA. State lives in `App.vue` refs; there is no store. All persistence besides `localStorage` (lang/theme/pin prefs) goes through Tauri. - **Backend** (`src-tauri/src/`) owns *all* filesystem I/O and JSONL parsing. Frontend calls it via the `#[tauri::command]` functions in `lib.rs`, wrapped by `src/api.ts`. The full handler list lives in `tauri::generate_handler!` at the bottom of `lib.rs`; keep it in sync. The backend is split into: ``` src-tauri/src/ ├── lib.rs // Tauri commands + macOS setup; pure dispatch, no parsing ├── types.rs // Serializable types shared with the frontend ├── util.rs // dirs / time / jsonl / text helpers (agent-agnostic) ├── trash.rs // soft-delete / restore / list / empty (agent-agnostic) └── agents/ ├── mod.rs // `SessionSource` trait + `source(agent)` dispatcher ├── claude.rs // ClaudeSource impl (~/.claude/projects/
`. The frontend does not parse diffs itself. If a future agent also emits structured diffs, give it its own parser in its agent module rather than hoisting `parse_structured_patch` into `util.rs`. ### Resume = AppleScript → Terminal `resume_session` shells out to `osascript` to open Terminal.app, `cd` into the project dir, and run a per-agent CLI (`claude --resume` / `codex resume ` / …). The CLI string comes from `SessionSource::resume_cli`, and `lib.rs` validates the session id with a strict allowlist (`[A-Za-z0-9-]+`) because the id is interpolated into a shell command. ### macOS titlebar / traffic lights The CSS topbar is 40px and shares background with the sidebar; the unified look depends on AppKit growing the native titlebar to match. `pin_traffic_lights` in `lib.rs` attaches an empty `NSToolbar` with `unifiedCompact` style — the *supported* AppKit way to extend the titlebar. The setup hook re-pins on `WindowEvent::Resized` (and intentionally *not* on Focused/ThemeChanged, which breaks click→drag tracking). Don't try to manually `setFrameOrigin` the window buttons; it visually works but corrupts drag-region tracking. ### Reactive i18n + theme - `src/settings.ts` holds `lang` and `theme` as `ref`s persisted to `localStorage`. `applyTheme()` is wrapped in `watchEffect`, so toggling theme/lang re-renders Vue templates that read those refs automatically. - `t(key, vars)` in `src/i18n.ts` reads `lang.value` — that read is what makes every template using `t()` reactive. Don't cache `t()` results outside of a computed/template. ### Design system `src/style.css` defines a Codex-inspired neutral token set (`--surface`, `--surface-hover`, `--border`, `--text`, `--accent`, ...) with a `:root` (light) and `:root.theme-dark` override block. Brand color (`--brand` = Claude orange or Codex green) is only used for tiny accents like the active-project count badge and the agent badge in the trash list — primary buttons and active surfaces use neutral foreground inversion (Codex style). Icons are inline SVG components in `src/components/icons.ts`. Do not introduce emoji icons in chrome — they were intentionally removed for a cleaner look. Tailwind v4 is installed but most styling uses the CSS-variable tokens above; new components should follow the existing class-name convention rather than inlining utility classes. Tooltips use the custom `v-tooltip` directive (registered in `src/main.ts`, implemented in `src/tooltip.ts`) rather than the native `title=` attribute — native tooltips render in a system font and look out of place in this UI. When adding a new button or icon, write `v-tooltip="t('...')"`, not `:title`.