CLAUDE.md
CLAUDE.mdCLAUDE.mdroot
Quality
69/100
Scores the file, not the repository.Length
2,958 words
18 headings · 3 code blocksRepository
1.3k
— · pushed 7 days agoLast changed
3 days ago
First indexed 3 days ago.1# CLAUDE.md23This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.45## What This Is67A terminal-centric IDE desktop app built on Electron that wraps CLI tool sessions. Users manage projects and sessions, each backed by a PTY running a CLI tool (currently Claude Code, with an abstraction layer for future providers like Copilot CLI and Gemini CLI), rendered via xterm.js.89## Build & Run1011```bash12npm run build # Compile all three targets (main, preload, renderer) + copy assets13npm start # Build then launch Electron app (alias: npm run dev)14```1516No hot reload — changes require rebuild + app restart.1718Requires Node v24 (see `.nvmrc`). No lint tooling is configured.1920Cross-platform: builds and runs on macOS, Linux, and Windows. Release artifacts (via electron-builder) include `.dmg`/`.zip` (mac), `.deb`/`.AppImage` (linux), and NSIS installer + portable `.exe` (win). CI covers all three platforms.2122## Testing2324```bash25npm test # Run all tests once26npm run test:watch # Watch mode (re-runs on file changes)27npm run test:coverage # Run with coverage report (terminal + HTML)28```2930Uses **Vitest** with v8 coverage. Tests are co-located with source files as `*.test.ts`. Coverage HTML report outputs to `coverage/index.html`.3132Test files are excluded from production builds via `exclude` in `tsconfig.main.json` and `tsconfig.renderer.json`.3334Three renderer modules (`session-cost.ts`, `session-activity.ts`, `session-context.ts`) expose `_resetForTesting()` to clear module-level state between tests. Main process tests mock `fs`, `child_process`, `node-pty`, and `os` via `vi.mock()`.3536## Architecture3738Three-process Electron architecture with strict context isolation:3940- **Main process** (`src/main/`) — Node.js side: window creation, PTY lifecycle via `node-pty`, filesystem access, persistent state (`~/.vibeyard/state.json`). IPC handlers in `ipc-handlers.ts` dispatch to `pty-manager.ts` and `store.ts`. CLI tool behavior is abstracted via the provider system (`src/main/providers/`).41- **Preload** (`src/preload/preload.ts`) — Secure bridge exposing `window.vibeyard` API via `contextBridge` with namespaces: `pty`, `session`, `store`, `profiles`, `fs`, `provider`, `menu`.42- **Renderer** (`src/renderer/`) — Vanilla TypeScript DOM UI (no framework). `AppState` singleton in `state.ts` uses an event emitter pattern; components in `components/` subscribe to state changes.4344### Data Flow4546Renderer → IPC invoke/send → Main process → PTY/filesystem → IPC send back → Renderer updates xterm terminal.4748### Build Targets4950Each process has its own `tsconfig.*.json`. Main and preload compile via `tsc` (CommonJS). Renderer bundles via esbuild (IIFE format, browser platform, with sourcemaps).5152### CLI Provider System5354CLI-specific behavior is encapsulated behind a `CliProvider` interface (`src/main/providers/provider.ts`). Each provider handles binary resolution, env vars, args, hooks, config reading, and cleanup. Providers are registered in a registry (`src/main/providers/registry.ts`) at app startup.5556- **Provider per-session**: Each `SessionRecord` has a `providerId` (defaults to `'claude'`). A project can contain sessions from multiple providers.57- **Capabilities pattern**: Providers declare what they support via `CliProviderCapabilities`. UI can conditionally enable features per-session.58- **Current providers**: `ClaudeProvider` (`src/main/providers/claude-provider.ts`) — extracts all Claude-specific logic from `pty-manager.ts`, `prerequisites.ts`, `claude-cli.ts`, and `hook-status.ts`.59- **System prompt**: `buildArgs` accepts `systemPrompt?: string` and every provider must honor it (used by the Team feature). Claude maps it to `--append-system-prompt`; Codex to `-c developer_instructions=<value>`; Copilot/Gemini to `--system-prompt`. The renderer passes it via the transient `pendingSystemPrompt` field on `SessionRecord`, which is consumed once on the first PTY spawn and stripped from `state.json` so it is never re-injected on resume.60- **Profiles** (multi-license): a `Profile` (`{ id, name, providerId, configDir, managed }`, top-level `state.profiles`) backs a CLI session with an isolated config directory so a user can run separate logins/licenses (work vs personal). `buildEnv(sessionId, baseEnv, opts?: { configDir? })` injects it — Claude sets `CLAUDE_CONFIG_DIR` (which relocates *everything* Claude reads/writes: credentials, `settings.json`, hooks, status line, transcripts, agents); other providers accept the param but don't use it yet. The effective profile (explicit choice > project `defaultProfileId` > `preferences.defaultProfileId`, provider-matched) is resolved **once at session creation** via `resolveProfile` (`src/renderer/state/specialized-sessions.ts`) and **pinned** onto `SessionRecord.profileId` — it is NOT re-derived at spawn, so changing a default later never redirects an existing session's resume. At spawn, `split-layout.ts` looks up the config dir from the sticky `session.profileId` only (provider-checked) and threads the `configDir` string through `createTerminalPane` → `pty.create` → `pty:create` → `spawnPty` → `buildEnv`. `profileId` is persisted on `SessionRecord`/`ArchivedSession` (and on `TranscriptDescriptor`/`DeepSearchResult` so global-search resume reopens under the right dir); `removeProfile` clears it from sessions, projects, history, and prefs. Because `CLAUDE_CONFIG_DIR` relocates `settings.json`, `spawnPty` installs Vibeyard's hooks + status line into the profile dir on each Claude spawn via the config-dir-parameterized `installHooksOnly(configDir)` / `installStatusLine(configDir)` in `claude-cli.ts` (the status-line *script* stays global in `~/.vibeyard/run/`; only per-dir `settings.json` differs). `getTranscriptPath(cliSessionId, projectPath, configDir?)` and `discoverTranscripts()` are profile-aware — the latter unions `~/.claude/projects` with every claude profile's `<configDir>/projects` (read from `loadState()`). Managed profile dirs live at `~/.vibeyard/profiles/<id>/` (auto-created), with an optional custom path; provisioning is `src/main/profiles.ts` exposed via the `profiles:provision` IPC. Profile management UI is the Preferences "Profiles" section; selection is in the New Session dialog and the sidebar project "Project Settings…" menu. v1 leaves `getConfig` (Overview agents/skills/MCP) and the config-watcher on the default `~/.claude`. macOS keychain caveat + guardrail: on macOS Claude stores credentials in the system Keychain (not `<configDir>/.credentials.json`) under a per-config-dir service name `Claude Code-credentials-<first 8 hex of sha256(configDir)>` (verified on 2.1.159), so profiles isolate logins only on Claude builds new enough to namespace that entry. Older builds (≤2.1.19, anthropics/claude-code#20553) reuse one shared `Claude Code-credentials` entry, bleeding logins across profiles. `src/main/claude-keychain.ts` (`getKeychainIsolationStatus`, exposed via IPC `profiles:keychainStatus` → `window.vibeyard.profiles.keychainStatus()`) guards this: empirically `supported` if any known profile already has a namespaced Keychain entry, else version-gated (`≤2.1.19` ⇒ `unsupported`; newer-but-unconfirmed ⇒ `unknown`, allowed — never false-blocked); non-macOS is always `supported` (isolation is file-based). On `unsupported`, `spawnPty` skips the profile session and writes an in-pane message (it does not throw — the renderer fires `pty.create` un-awaited), and the Preferences "Profiles" section blocks profile creation and shows a warning banner. The shared semver helpers (`parseSemver`, `semverGte`) live in `claude-hook-versions.ts`.61- **Agent files**: providers expose optional `agentsDir()`, `installAgent(slug, content)`, and `removeAgent(slug)` methods (default impls delegate to `src/main/providers/agent-files.ts`, which accepts an optional extension — Copilot passes `.agent.md`; everyone else uses the default `.md`). Each provider's user-global agents directory is `~/.<cli>/agents/` (e.g. `~/.claude/agents/`). The Team feature uses these via the `provider:installAgent` / `provider:removeAgent` IPC channels to mirror a `TeamMember` (with `installAsAgent: true`) as a `<slug>.md` (or `<slug>.agent.md` for Copilot) file across every installed provider, making it invokable as `/<slug>` inside CLI sessions. Slug is sticky on the member (`agentSlug` field) so renames preserve the same file. Filename collisions with non-Vibeyard agents at the same slug will overwrite — the renderer only deduplicates within team members.6263### Key Components6465- `terminal-pane.ts` — xterm.js wrapper per session, handles PTY data streaming and WebGL rendering with software fallback66- `state.ts` — Reactive AppState singleton; debounced persistence (300ms) to `~/.vibeyard/state.json`67- `split-layout.ts` — Manages tab mode (single terminal) vs split mode (side-by-side)68- `session-activity.ts` — Tracks working/waiting/idle status with debounced transitions69- `session-cost.ts` — Structured cost tracking via Claude CLI status line (`statusLine` setting), with regex fallback for older CLI versions. Provides per-session and aggregate cost data (USD, tokens, cache, duration)70- `components/active-sessions-panel.ts` — Global, cross-project **Active Sessions** list rendered into `#sidebar-active-sessions` (a persistent sidebar block above Discussions). `selectActiveSessions(projects, statusOf, activeStatuses)` is a DOM-free selector that collects open CLI sessions (`isCliSession`) across `appState.projects` whose live `getStatus()` is in the configured set, ordered by `STATUS_PRIORITY` (from `project-status.ts`, now exported) then project name. `initActiveSessions()` subscribes to `session-activity.onChange` + the relevant `appState` events and re-renders; rows route clicks through `setActiveProject` + `setActiveSession`. Visibility is gated by `preferences.sidebarViews.activeSessions` **and** requires more than one project (with a single project the rows just duplicate its own tab bar); the "active" status set comes from `preferences.activeSessionStatuses` (`{ working, waiting, input, completed }`, default working/input/completed via `resolveActiveStatuses`). Both preferences are edited in the Preferences → Appearance section. Styles live in `styles/sidebar.css`; status dots reuse the existing `.project-status.<status>` classes.71- `components/git-panel.ts` — Git changes for the **active project**, rendered inside its sidebar card as a `git` panel-toggle tab (a third `ProjectPanel` alongside `history`/`files` in `sidebar.ts`), not a standalone bottom panel. The "Git" tab carries a `.project-action-badge` count of total changes (badge + click-to-open); `Cmd/Ctrl+Shift+G` (`toggleGitPanel`, exported from `sidebar.ts`) toggles it. `mountGitPanel(project, container)` reparents a single persistent node into `.project-panel-git` so file rows/scroll survive sidebar re-renders; `closeGitPanel()` detaches it. The shared `loadFiles`/worktree-selector logic is unchanged; `sidebarViews.gitPanel` now gates the tab button. There is no `#git-panel` node in `index.html`.72- `browser-tab/` — Browser tab pane split into focused modules: `types.ts`, `instance.ts` (registry + preload path), `navigation.ts`, `viewport.ts`, `selector-ui.ts`, `inspect-mode.ts`, `flow-recording.ts`, `flow-picker.ts`, `session-integration.ts`, and `pane.ts` (DOM build + event wiring). `browser-tab-pane.ts` is a re-export shim for backward compatibility.73- `board-state.ts` — Kanban board CRUD: tasks, columns, tags, reorder. Mutates `appState.activeProject.board` in place, calls `appState.notifyBoardChanged()`.74- `board-filter.ts` — Module-level search query and tag filter state for the board. Observer pattern via `onFilterChange()`.75- `board-session-sync.ts` — Listens to session lifecycle events and auto-moves board tasks (e.g. to Done on session complete).76- `components/board/` — Board UI: `board-view.ts` (container + header with the search box and "New task" button in a right-aligned actions group, plus a dedicated "Filter" tag row below), `board-column.ts` (column with header/rename), `board-card.ts` (card with run/resume/focus), `board-task-modal.ts` (create/edit dialog with tags), `board-dnd.ts` (drag-and-drop with injected DOM drop targets), `board-context-menu.ts`.77- `styles/kanban.css` — All kanban board styles including cards, columns, DnD drop targets, tag pills, and filter UI.78- `components/team/` — Team tab: `instance.ts` + `pane.ts` (tab plumbing mirroring kanban), `team-view.ts` (header + card grid + empty state), `member-card.ts` (Chat/Edit/Sessions/Delete actions), `member-modal.ts` (create/edit form using the shared `showModal`), `predefined-picker.ts` (fetches suggestions from this repo's `personas/` folder, marks already-installed members), `github-fetcher.ts` (Contents API + raw download, 1 hour cache), `frontmatter.ts` (Markdown → `TeamMember` parser).79- `styles/team.css` — Team grid, cards, predefined-picker dialog. Uses CSS variables only.80- Team state lives at the top level of `~/.vibeyard/state.json` as `state.team.members` (global, not per-project). Predefined suggestions cache at `state.team.predefinedCache`. Predefined personas live in the top-level `personas/` directory of this repo and are fetched at runtime via the GitHub Contents API; the location is configured by the single constant `TEAM_MEMBERS_REPO` in `src/shared/team-config.ts` — flip its `owner`/`repo`/`path` to retarget.81- `components/project-tab/` — Customizable Overview page driven by a gridstack.js drag-and-drop grid. `pane.ts` builds the toolbar (`+ Add Widget`, `Edit layout` toggle) + grid root. `grid.ts` wraps gridstack and owns tile chrome (header, drag handle, refresh/settings/remove buttons). Per-project layout persists at `ProjectRecord.overviewLayout` with widget records `{ id, type, x, y, w, h, config? }` — lazy-defaulted on first render to mirror the legacy 2-column layout, no migration code. Each widget is a `WidgetFactory` registered in `widgets/widget-registry.ts`; current types: `readiness`, `provider-tools` (refactors of the old columns), `github-prs`, `github-issues`, `team` (reuses `createMemberCard` from `components/team/member-card.ts`, listens to `'team-changed'`), `kanban` (reuses `createCardElement` from `components/board/board-card.ts`, groups by column, listens only to `'board-changed'` — live per-session metrics are intentionally omitted to avoid full-rerender storms; the full kanban tab covers that), `sessions` (active CLI sessions + recent archived split into two sections; filtered through `isCliSession`; click-to-focus calls `setActiveSession`, click-to-resume calls `resumeFromHistory`; subscribes to `session-activity`/`session-cost`/`session-unread` observers with surgical row updates for status/cost ticks; settings modal at `widgets/sessions-settings-modal.ts` with config in `widgets/sessions-types.ts` lets users tune `recentLimit`), `usage-stats` (Claude-Code-only summary of sessions/messages/model token usage and a 7-day + by-hour activity heatmap; reads `~/.claude/stats-cache.json` via `window.vibeyard.stats.getCache()`; styled with `styles/usage.css`; refresh wired through the widget chrome's refresh button — no in-content button). GitHub widgets use `window.vibeyard.github.*` IPC backed by `src/main/github-cli.ts` which shells out to the user's local `gh` CLI (auto-detected; PATH is the augmented `getFullPath()` from `pty-manager`). Repo defaults to the project's git origin via `getGitRemoteUrl`; per-widget settings (`widgets/github-settings-modal.ts`) override repo, state, max items, and refresh interval. Read/unread tracking lives in `github-unread.ts` (Set + observer mirroring `session-unread.ts`); per-item lastSeen timestamps persist at `ProjectRecord.githubLastSeen`. The tab-bar surfaces unread by branching on `project-tab` to consult `hasUnreadInProject`. Gridstack CSS is bundled by copying `node_modules/gridstack/dist/gridstack.min.css` to `dist/renderer/vendor/` (esbuild has no CSS loader); `<link>`ed from `index.html`. `styles/widgets.css` holds shared widget chrome.82- `terminal-context-menu.ts` — Right-click context menu for terminal panes (Copy, Paste, Select All). DOM-based using shared `tab-context-menu` CSS classes. `showTerminalContextMenu()` / `hideTerminalContextMenu()` exports; integrated via `contextmenu` listener on `xtermWrap` in `terminal-pane.ts`. Every action handler calls `terminal.focus()` after closing the menu so keyboard input resumes immediately (clicking a menu item otherwise leaves focus on the document, e.g. typing after Paste would be dropped). Note: the flex layout (`display:flex; justify-content:space-between; align-items:center`) lives on the shared `.tab-context-menu-item` selector in `styles/tabs.css`, so it affects every menu reusing that class — existing single-label items are unaffected since `space-between` has nothing to push.83- **i18n** — lightweight in-house internationalization (no third-party library). `src/renderer/i18n.ts` exports `t(key)` (dot-path lookup with English fallback + `console.warn` for missing keys), `getLocale()`, `setLocale(locale)` (unknown locales rejected), and `onLocaleChange(cb)` returning an unsubscribe handle. Catalogs at `src/renderer/locales/{en,zh-CN}.json` are statically imported by esbuild (zero runtime fetches). `src/renderer/system-locale.ts` resolves `navigator.language` (`/^zh/i` → `zh-CN`, else `en`). The persisted choice lives at `Preferences.locale` (`'en' | 'zh-CN'`, defined inline in `src/shared/types.ts` rather than imported from `i18n.ts` to avoid a main↔renderer import cycle). Persistence flows through the existing `setPreference('locale', loc)` → 300ms-debounced main-process save. `initI18n()` in `src/renderer/index.ts` runs right after `appState.load()`; on first launch (no saved `locale`) it persists the OS-resolved choice. The `preferences-changed` listener in `index.ts` calls `i18n.setLocale(newLocale)` and `rerenderOpenPreferencesModal()` (exported from `src/renderer/components/preferences-modal.ts`) so the open Preferences modal re-translates in place. The Language switcher itself lives at the top of Preferences → General as a `createCustomSelect` row backed by `appState.setLocale` (live, no Confirm required). Translated sections in v1: Preferences sidebar labels + General + Appearance. Out-of-modal strings (sidebar buttons, tab titles) stay English until a follow-up PR.8485### Platform Checks8687Platform detection is centralized in `src/main/platform.ts`. Import88`isWin`/`isMac`/`isLinux` (and derived constants `pathSep`, `whichCmd`,89`pythonBin`) from there — do **not** inline `process.platform === 'win32'`90or redefine `isWin`/`isMac` locally in source or test files. The91three-way managed-path branch in `claude-cli.ts` is the one intentional92exception.9394### Cross-platform paths in tests9596When asserting on a path that the implementation produced via `path.join`,97`path.resolve`, or `path.normalize`, **never hardcode forward-slash literals**98like `'/repo/foo.ts'` in the assertion — they pass on macOS/Linux but fail99on `windows-latest` because Node yields `\repo\foo.ts` there. Build the100expected value with the same primitive the implementation uses:101102```ts103import * as path from 'path';104// good — matches whatever path.join produces on the running platform105expect(mockRm).toHaveBeenCalledWith(path.join('/repo', 'foo.ts'), opts);106107// bad — hidden Windows-only failure108expect(mockRm).toHaveBeenCalledWith('/repo/foo.ts', opts);109```110111This applies to any assertion on arguments to `fs.*`, `child_process` calls,112or anything else that flows a joined path through. CI runs on all three113platforms, so a forward-slash literal will eventually fail on Windows.114115### File Watching116117Live filesystem updates for the file tree, file reader, and diff viewer are backed by a single **chokidar** watcher in `src/main/file-watcher.ts` (chokidar is ESM-only but `require`-able under Electron's Node ≥22.12). It watches **directories non-recursively** (`depth: 0`), ref-counted per dir via `watchDir(dir)` / `unwatchDir(dir)` (IPC `fs:watchDir` / `fs:unwatchDir`). Watching the parent dir — not the file inode — is deliberate: it survives atomic save/replace (write-temp + rename), which silently kills an inode watch. Changes are coalesced and debounced (150ms) into a single batched `fs:changed` IPC carrying `FsChange[]` (`{ path, dir, type }`, type ∈ add/addDir/change/unlink/unlinkDir; see `src/shared/types.ts`). The renderer subscribes via `window.vibeyard.fs.onFsChange`. `stopAllFileWatchers()` is wired into `main.ts` teardown (window `closed` + `before-quit`).118119Scope is **lazy**: only expanded tree folders and the parent dirs of open reader/viewer files are watched (cheap on huge repos). The file tree (`file-tree.ts`) reacts by **incremental reconciliation** (`reconcileChildren`), not full rebuilds: rows are keyed by `data-entry-path`, vanished entries are removed (unwatching their subtree via `unwatchSubtree`), new entries are inserted at the sorted position, and unchanged rows — with their expanded subtrees, scroll, and selection — are left untouched. Change bursts are flushed once per `requestAnimationFrame`. Cross-platform path helpers (`dirname`, `isPathUnder`) live in `src/shared/platform.ts`. Note: this is separate from `git-watcher.ts`, which watches the whole working tree and emits `git:changed` for the git-status UI. To avoid the macOS FSEvents teardown storm on project switch (#142), `git-watcher.ts` uses a **single recursive `fs.watch`** on macOS + Windows (`watchRecursiveWorkingTree`, one OS handle, events filtered via `hasIgnoredSegment`) and keeps the **capped per-dir BFS** (`walkAndWatch`, `MAX_WATCHES`) on Linux, where recursive `fs.watch` is unsupported and previously leaked inotify watches (#139). Fine-grained `.git` watches and the 60s git-status poll are unchanged.120121### State Persistence122123App state (projects, sessions, layout) persists to `~/.vibeyard/state.json` via the main process store. Saves are debounced and flushed on quit. Sessions track `cliSessionId` for CLI session resume capability.124125## UI Development126127When working on renderer/UI code, the `/ui-dev` skill is automatically invoked. It documents all custom components (dropdowns, modals, alerts, badges), CSS theming variables, styling conventions, and component architecture patterns. Always follow it — never use native `<select>`, never hardcode colors, always reuse existing components.128129## Planning130131When entering plan mode for a new feature, consider whether the feature (or aspects of it) should be exposed as a user-configurable option in Preferences. If it's relevant, ask the user whether they'd like it added as a config in the prefs before finalizing the plan.132133## Post-Implementation134135After completing an implementation task, always:1361371. Run `/code-review` to review changed code for correctness bugs and reuse/quality/efficiency cleanups. Run it automatically — do not ask the user for permission first.1382. Run `/simplify` to apply reuse, simplification, efficiency, and altitude cleanups to the changed code. Run it automatically — do not ask the user for permission first.1393. Add or update tests as needed to cover the changes.140141## Git Workflow142143Always use the `/commit` command when committing changes to this project. Do not create commits manually.144145Never commit, push, or create pull requests unless the user explicitly asks for it.146147`CHANGELOG.md` is auto-generated by the release action (via the `/release` / release-notes flow). Do not edit it as part of coding tasks — changes there will be overwritten at release time.148149## Maintaining This File150151When your changes affect the architecture, build process, key components, data flow, or any other information documented above, update this CLAUDE.md to reflect the new state. This includes adding/removing/renaming files, changing IPC namespaces, modifying the build pipeline, or introducing new patterns. Keep this file accurate so future sessions start with correct context.152
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 3 days ago | |
| livewire/livewireCLAUDE.md · 24k | CLAUDE.md | setupbuildteststyle+4 | 100/100 | 3 days ago | |
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| filamentphp/filamentCLAUDE.md · 32k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today |
