agentleFS
Sign inSign up

gridwatch

BinaryMeadow/gridwatch/.github/copilot-instructions.md

GridWatch is a cross-platform Electron desktop app that reads GitHub Copilot CLI's local session data (~/.copilot/session-state/) and presents it as a real-time dashboard. It uses a retro Tron-inspired design with neon cyan, electric blue, and orange accents on near-black backgrounds. Stack: Electron + React 19 + TypeScript + Vite + CSS Modules + Recharts Author: Faesel Saeed License: MIT

Copilot instructions35 starsChanged 7 months ago
# GitHub Copilot Instructions for GridWatch

## Project overview

**GridWatch** is a cross-platform Electron desktop app that reads GitHub Copilot CLI's local session data (`~/.copilot/session-state/`) and presents it as a real-time dashboard. It uses a retro Tron-inspired design with neon cyan, electric blue, and orange accents on near-black backgrounds.

**Stack:** Electron + React 19 + TypeScript + Vite + CSS Modules + Recharts  
**Author:** Faesel Saeed  
**License:** MIT

---

## Architecture

### Process model

```
Main process (electron/main.ts)
  └── All file system access
  └── All IPC handlers (ipcMain.handle)
  └── Window management

Preload (electron/preload.ts)
  └── contextBridge.exposeInMainWorld('gridwatchAPI', {...})
  └── Also exposes webFrame.setZoomFactor / getZoomFactor

Renderer (src/)
  └── React app — NO direct Node.js access
  └── Communicates with main only via window.gridwatchAPI
```

### Data sources (read-only, never modified by GridWatch)

| File | Contents |
|---|---|
| `~/.copilot/session-state/<uuid>/workspace.yaml` | id, cwd, repository, branch, summary, created_at, updated_at |
| `~/.copilot/session-state/<uuid>/events.jsonl` | session.start, user.message (field: `data.content`), tool.execution_start, and **token data**: `session.compaction_complete.data.preCompactionTokens` (context occupancy per compaction) + `session.compaction_start.data.model` |
| `~/.copilot/session-state/<uuid>/rewind-snapshots/index.json` | Checkpoint snapshots with user prompts |
| `~/.copilot/session-state/<uuid>/research/*.md` | Markdown reports generated by Copilot's research agent |
| `~/.copilot/logs/process-<ms-timestamp>-<pid>.log` | Compaction start/complete detail lines (`saved ~N tokens`), persisted checkpoint summaries, and `Registering/Unregistering foreground session: <uuid>` lines. **Note:** as of Copilot CLI ~1.0.70 these logs no longer contain `Utilization X% (used/total tokens)` lines — token counts moved to `events.jsonl` |

### Custom data (written by GridWatch)

| File | Contents |
|---|---|
| `~/.copilot/session-state/<uuid>/gridwatch.json` | `{ "tags": [...], "notes": "...", "tokenStats": { "peakTokens", "peakUtilisation", "contextWindow", "initialTokens", "initialUtilisation", "compactions", "capturedAt" } }` — `tokenStats` is a snapshot persisted so token numbers survive Copilot's ~14-day log pruning |
| `~/.copilot/skills/<name>/gridwatch.json` | `{ "tags": [...], "childSkills": [...], "linkedAgents": [...], "orchestration"?: {...} }` — skill tags, nested child-skill folder names, linked agent ids, and (for orchestrators) the orchestration config: `{ isOrchestrator, mode: 'sequential'\|'parallel', outputDir, children: { <name>: { inputDir, outputDir, outputFile } }, finalOutput?, generatedHash, generatedAt }` |
| `~/.copilot/skills-disabled/<name>/` | Skills moved here when disabled via GridWatch toggle |
| `~/.copilot/gridwatch-mcp-tools-cache.json` | Disk-persisted cache of MCP tool lists queried via JSON-RPC `tools/list` |
| `~/.copilot/gridwatch-lsp-disabled.json` | LSP servers moved here when disabled via GridWatch toggle |
| `~/.copilot/gridwatch-autotag-rules.json` | `{ "rules": [{ "id", "path", "tags": [...] }] }` — directory→tags auto-tagging rules |
| `~/.copilot/agents/<name>.agent.md` | Custom agent profiles with YAML frontmatter (read-only) |
| `localStorage` (renderer) | `gridwatch-settings` — zoom, fontSize, spacing |

---

## Project structure

```
gridwatch/
├── .github/
│   ├── copilot-instructions.md   ← this file
│   ├── CONTRIBUTING.md
│   └── ISSUE_TEMPLATE/
│       ├── bug_report.md
│       └── feature_request.md
├── electron/
│   ├── main.ts                   ← main process, all IPC handlers
│   └── preload.ts                ← contextBridge, webFrame zoom
├── src/
│   ├── pages/
│   │   ├── SessionsPage.tsx      ← session list + detail panel (rename/archive/delete/tags/history)
│   │   ├── TokensPage.tsx        ← token usage charts (line + bar + per-session table)
│   │   ├── ActivityPage.tsx      ← heatmap, top repos, tool usage, day-of-week chart
│   │   ├── SkillsPage.tsx        ← Copilot skills browser and editor (CRUD/import/export/toggle/tags)
│   │   ├── LspPage.tsx          ← LSP server dashboard (view/enable/disable)
│   │   └── SettingsPage.tsx      ← UI scale / font size / density; applySettings(), loadSettings()
│   ├── types/
│   │   ├── session.ts            ← SessionData, TokenDataPoint, CompactionEvent, RewindSnapshot interfaces
│   │   ├── skill.ts              ← SkillData, SkillFile interfaces
│   │   ├── agent.ts              ← CustomAgentData interface
│   │   ├── mcp.ts                ← McpServerData, McpTool, McpEnvVar interfaces
│   │   ├── lsp.ts                ← LspServerData interface
│   │   └── global.d.ts          ← Window.gridwatchAPI declarations
│   ├── App.tsx                   ← shell, sidebar (sidebarTop/sidebarBottom), auto-refresh, PageErrorBoundary
│   ├── App.module.css
│   └── index.css                 ← Tron CSS variables, global reset, density overrides
├── public/
│   └── icon.png                  ← 1024×1024 app icon
├── build/
│   └── icon.png                  ← same icon, for electron-builder
├── LICENSE                       ← MIT
└── README.md
```

---

## IPC API surface (`window.gridwatchAPI`)

| Method | IPC channel | Description |
|---|---|---|
| `getSessions()` | `sessions:get-all` | Returns `SessionData[]` sorted by updatedAt desc |
| `getLogTokens()` | `sessions:get-log-tokens` | Returns `{ date, tokens, utilisation }[]` — peak per calendar day aggregated across sessions' token history (events.jsonl-sourced) |
| `renameSession(id, summary)` | `sessions:rename` | Writes `name` + `user_named: true` (and `summary`) in workspace.yaml so the rename survives Copilot's own rewrites |
| `archiveSession(id)` | `sessions:archive` | Moves session dir to `~/.copilot/session-state-archived/` |
| `deleteSession(id)` | `sessions:delete` | `fs.rmSync` the session dir |
| `setTags(id, tags[])` | `sessions:set-tags` | Writes/merges tags into `gridwatch.json` |
| `showInFolder(path)` | `app:show-in-folder` | Reveals file in Finder/Explorer via `shell.showItemInFolder` |
| `setZoomFactor(n)` | — (webFrame) | Electron native zoom, correct viewport scaling |
| `getZoomFactor()` | — (webFrame) | Returns current zoom factor |

### Skills IPC

| Method | IPC channel | Description |
|---|---|---|
| `getSkills()` | `skills:get-all` | Scans `~/.copilot/skills/` and `skills-disabled/`, returns `SkillData[]` |
| `getSkillFile(name, file)` | `skills:get-file` | Reads a single file from a skill folder |
| `saveSkillFile(name, file, content)` | `skills:save-file` | Writes content to a file in a skill folder |
| `createSkill(name, desc)` | `skills:create` | Scaffolds a new skill folder with SKILL.md template |
| `deleteSkill(name)` | `skills:delete` | Removes a skill folder recursively |
| `duplicateSkill(name, newName)` | `skills:duplicate` | Copies a skill folder with updated frontmatter |
| `renameSkillFolder(name, newName)` | `skills:rename-folder` | Renames the skill directory (not frontmatter) |
| `toggleSkill(name)` | `skills:toggle` | Moves between `skills/` and `skills-disabled/` |
| `exportSkill(name)` | `skills:export` | Creates a zip archive via save dialog |
| `importSkill()` | `skills:import` | Opens file/folder dialog, copies into `skills/` |
| `setSkillTags(name, tags[])` | `skills:set-tags` | Writes/merges tags into skill's `gridwatch.json` |
| `setSkillRelations(name, childSkills[], linkedAgents[])` | `skills:set-relations` | Writes/merges child-skill and linked-agent relationships into skill's `gridwatch.json`. Validates that children/agents exist and rejects self-reference |
| `setSkillOrchestration(name, config)` | `skills:set-orchestration` | Writes/merges the `orchestration` block into the parent skill's `gridwatch.json`. Preserves provenance (`generatedHash`/`generatedAt`), which are owned by the generate step |
| `previewOrchestrator(name)` | `skills:preview-orchestrator` | Renders the managed workflow block from the saved config + `childSkills` order without writing — returns `{ ok, block?, error? }` |
| `generateOrchestrator(name)` | `skills:generate-orchestrator` | Renders and merges the managed block into the orchestrator's own `SKILL.md` (backs up to `SKILL.md.bak`), stores `generatedHash`. Refuses and returns `status:'broken'` if the managed markers are damaged |
| `pickDirectory()` | `app:pick-directory` | Opens a native folder picker, returns `{ ok, path? }` |

**Orchestrator skills:** A parent skill can be flagged an orchestrator that invokes its attached child skills (run order = `childSkills` order). The generated workflow lives in a GridWatch-managed block in the parent's `SKILL.md`, anchored by `<!-- gridwatch:orchestration:start -->` / `:end -->` markers wrapped in DO-NOT-EDIT banner comments; only that region is rewritten on regenerate, so prose outside it survives. `mode` is `'sequential'` (one-by-one) or `'parallel'` (all as background tasks). Per-child input/output folders are auto-wired on attach (`child.inputDir = previous child's outputDir`) and overridable. Status (`in-sync`/`edited`/`broken`/`missing`) is computed in `main.ts` during scan by hashing the normalised managed block against the stored `generatedHash`. v1 surfaces a status indicator only — no auto-repair. The editable graph (VIEW/EDIT toggle) lives in `SkillGraphPage.tsx`; pure rendering/merge/status logic is in `src/lib/orchestration.ts` (shared by main + renderer).

**Skill name validation:** lowercase alphanumeric and hyphens only (`/^[a-z0-9][a-z0-9-]*$/`).

**Guards:** archive and delete refuse if `updatedAt` is within 2 minutes of now (active session protection).

### MCP IPC

| Method | IPC channel | Description |
|---|---|---|
| `getMcpServers()` | `mcp:get-servers` | Returns `McpServerData[]` with tools queried via JSON-RPC for local servers |
| `toggleMcpServer(name)` | `mcp:toggle-server` | Moves server between `mcp-config.json` and `gridwatch-mcp-disabled.json` |
| `showMcpConfig()` | `mcp:show-config` | Reveals `mcp-config.json` in Finder/Explorer |

**MCP tool discovery:** Local servers are spawned briefly and queried via JSON-RPC `initialize` + `tools/list` to get canonical tool names, descriptions, and input schemas. Results are cached to `~/.copilot/gridwatch-mcp-tools-cache.json` (5 min in-memory TTL, persistent on disk). Remote/IDE servers fall back to log-based tool name extraction (no descriptions).

### LSP IPC

| Method | IPC channel | Description |
|---|---|---|
| `getLspServers()` | `lsp:get-servers` | Returns `LspServerData[]` from `lsp-config.json` and `gridwatch-lsp-disabled.json` |
| `toggleLspServer(name)` | `lsp:toggle-server` | Moves server between `lsp-config.json` and `gridwatch-lsp-disabled.json` |

**LSP caching:** In-memory cache with 10s TTL (same pattern as MCP). Cache invalidated on toggle.

### Agents IPC

| Method | IPC channel | Description |
|---|---|---|
| `getCustomAgents()` | `agents:get-all` | Scans `~/.copilot/agents/` for `*.agent.md` files, returns `CustomAgentData[]` |
| `getAgentFile(name, file)` | `agents:get-file` | Reads a single agent profile file (path-traversal protected) |

**Agent discovery:** Scans `~/.copilot/agents/` for files matching `*.agent.md`. Parses YAML frontmatter for `name` and `description`. The agent ID is derived from the filename (e.g., `security-reviewer.agent.md` → `security-reviewer`).

**Session linking:** Sessions are linked to custom agents by matching `agent_type` values extracted from `events.jsonl` against the agent's name and display name (case-insensitive).

### Auto-tag rules IPC

| Method | IPC channel | Description |
|---|---|---|
| `getAutoTagRules()` | `autotag:get-rules` | Returns `AutoTagRule[]` from `gridwatch-autotag-rules.json` |
| `getKnownTags()` | `autotag:get-known-tags` | Returns all distinct tags across sessions (typeahead source) |
| `pickAutoTagDirectory()` | `autotag:pick-directory` | Opens a native directory picker, returns the chosen absolute path |
| `addAutoTagRule(path, tags[])` | `autotag:add-rule` | Validates + appends a rule; rejects duplicate directories (platform-aware) |
| `removeAutoTagRule(id)` | `autotag:remove-rule` | Removes the rule with the given id |

**Auto-tag model:** Rules map a directory to a list of tags. At session load, each session whose `gitRoot` (falling back to `cwd`) equals or sits under a rule's directory receives that rule's tags as **derived** `autoTags` (computed in `loadAllSessions`, never written to the session's `gridwatch.json`). Tags already present in a session's manual `tags` are excluded from `autoTags`. Matching is case-insensitive on macOS/Windows and always includes subdirectories. Auto-tags are read-only in the Sessions view (managed only via Settings → Auto-tag rules) and rendered with a distinct ⛓ chip.



```typescript
interface SkillData {
  name: string              // folder name (canonical ID)
  displayName: string       // from YAML frontmatter `name`
  description: string       // from YAML frontmatter `description`
  license?: string          // from YAML frontmatter `license`
  files: SkillFile[]        // all files in the skill folder
  enabled: boolean          // true if in skills/, false if in skills-disabled/
  createdAt: string         // folder stat birthtime
  modifiedAt: string        // most recent file mtime
  usageCount?: number       // from session event cross-reference
  lastUsed?: string         // ISO date of last usage
  tags: string[]            // from gridwatch.json in the skill directory
  childSkills: string[]     // from gridwatch.json — canonical folder names of nested child skills
  linkedAgents: string[]    // from gridwatch.json — agent ids that run for this skill
}

interface SkillFile {
  name: string              // filename (e.g. "SKILL.md")
  path: string              // full path
  size: number
  modifiedAt: string
}
```

---

## McpServerData type

```typescript
interface McpTool {
  name: string              // tool identifier (e.g. "confluence_search")
  description?: string      // human-readable description from tools/list
  inputSchema?: Record<string, unknown>  // JSON Schema for parameters
}

interface McpServerData {
  name: string              // server name from config or logs
  type: 'local' | 'remote'
  command?: string          // local: command to spawn
  args?: string[]           // local: command arguments
  url?: string              // remote: HTTP endpoint
  envVars: McpEnvVar[]      // env vars (with secret detection)
  toolCount?: number
  tools: McpTool[]          // queried via JSON-RPC for local, log-scraped for remote
  connectionTime?: number   // ms, from most recent log
  enabled: boolean          // false if in gridwatch-mcp-disabled.json
}
```

---

## LspServerData type

```typescript
interface LspServerData {
  name: string              // server name from config key (e.g. "terraform", "typescript")
  command: string           // command to spawn (e.g. "terraform-ls")
  args: string[]            // command arguments (e.g. ["serve"])
  fileExtensions: Record<string, string>  // map of extension to language ID (e.g. ".ts" → "typescript")
  enabled: boolean          // false if in gridwatch-lsp-disabled.json
}
```

---

## CustomAgentData type

```typescript
interface CustomAgentData {
  name: string              // derived from filename (e.g. "security-reviewer")
  displayName: string       // from YAML frontmatter `name`
  description: string       // from YAML frontmatter `description`
  files: { name: string; path: string; size: number; modifiedAt: string }[]
  createdAt: string         // file stat birthtime
  modifiedAt: string        // file stat mtime
}
```

---

## SessionData type

```typescript
interface SessionData {
  id: string
  cwd: string
  gitRoot?: string
  repository?: string
  branch?: string
  summary?: string
  summaryCount: number
  createdAt: string        // always ISO string — js-yaml returns Date objects, convert with new Date(...).toISOString()
  updatedAt: string        // same
  turnCount: number
  toolsUsed: string[]
  copilotVersion?: string
  lastUserMessage?: string
  userMessages: UserMessage[]   // all user.message events, field: event.data.content
  tags: string[]           // from gridwatch.json
  autoTags: string[]       // derived at load from gridwatch-autotag-rules.json; never persisted
  notes: string
  rewindSnapshots: RewindSnapshot[]
  filesModified: string[]
  peakTokens: number
  peakUtilisation: number
  contextWindow?: number          // inferred from model (events.jsonl) or legacy log denominator (varies by model; NOT always 128K)
  initialTokens?: number          // first recorded token count for the session
  initialUtilisation?: number     // first recorded utilisation %
  tokenLogPruned: boolean         // true when no live log, no events snapshot, no saved snapshot, and session predates oldest log
  tokenStatsCached?: boolean      // true when token stats came from the gridwatch.json snapshot, not a live source
  tokenHistory: TokenDataPoint[]
  compactions: CompactionEvent[]  // from events.jsonl compaction events, enriched with process-log detail when available
  isResearch: boolean             // true if first user.message starts with "Researching: "
  isReview: boolean               // true if events.jsonl contains "agent_type":"code-review"
  agentTypes: string[]            // all agent_type values found in events.jsonl (e.g. ["explore", "security-reviewer"])
  researchReports: string[]       // full paths to markdown reports in session's research/ dir
}

interface CompactionEvent {
  timestamp: string
  triggerUtilisation: number
  forced: boolean
  messagesReplaced?: number
  newMessages?: number
  tokensSaved?: number
  summary?: string              // from "Persisted checkpoint #N: <summary>" log line
}
```

---

## Critical gotchas

### js-yaml date conversion
js-yaml automatically converts ISO date strings in YAML to JavaScript `Date` objects. Electron IPC uses structured clone (not JSON), so `Date` objects arrive in the renderer as `Date` objects — NOT strings. Always convert in main.ts:
```typescript
const createdAt = new Date(workspace.created_at || Date.now()).toISOString()
```

### Zoom scaling
Use `webFrame.setZoomFactor()` (exposed via preload), NOT `document.body.style.zoom`. The latter distorts `100vh` causing content to overflow the viewport. `webFrame` correctly adjusts the viewport.

### Recharts ResponsiveContainer height
Always provide an explicit pixel `height` to `<ResponsiveContainer>` — never `height="100%"` unless the parent has an explicit pixel height.

### Token chart X-axis duplicates
Multiple log files can share the same date string (multiple sessions per day). Always aggregate `logTokens` by date before passing to charts:
```typescript
const lineMap = new Map<string, number>()
logTokens.forEach(e => { if (e.tokens > (lineMap.get(e.date) || 0)) lineMap.set(e.date, e.tokens) })
```

### events.jsonl user message field
The field is `event.data.content`, **not** `event.data.message`.

### NEVER read events.jsonl into memory
`events.jsonl` files are huge — over 1 GB across a mature `session-state/` tree, with individual
sessions exceeding 80 MB. Reading them with `fs.readFile`/`readFileSync` (and especially then
calling `.split('\n')`) across hundreds of sessions exhausted the V8 heap and crashed the main
process with a fatal `EXC_BREAKPOINT`. Always stream them with `forEachLine()` and fold each
event as it arrives — never accumulate parsed events into an array.

The session scan is additionally protected by three mechanisms in `main.ts`, all of which must
be preserved:

| Mechanism | Purpose |
|---|---|
| `mapWithConcurrency(entries, SESSION_SCAN_CONCURRENCY, …)` | Bounds in-flight file I/O; unbounded `Promise.all` over every session stacked hundreds of concurrent reads |
| `sessionParseCache` (size+mtime fingerprint per file) | Skips re-parsing unchanged sessions; a warm rescan is ~175 ms vs ~8 s cold |
| `sessionsInFlight` in `loadAllSessions()` | Coalesces concurrent scans — several IPC handlers funnel into it, and overlapping scans each held their own copy of the parsed tree |

Cached parse results (`ParsedEvents` / `ParsedRewind`) are shared across scans, so treat them as
immutable — copy before sorting or mutating (see the `compactionSnapshots` spread).

### Renderer refresh must preserve array identity
`App.tsx` polls `getSessionSummaries()` every 30 s. Always compare with `summariesEqual()` and
keep the previous array when nothing changed — installing a fresh array invalidates every
`useMemo([sessions])` on every page and causes a full recompute-and-re-render pulse. Polling is
also skipped while `document.hidden`, and resumes on `visibilitychange`.

### Token data source (changed in Copilot CLI ~1.0.70)
Copilot **stopped emitting** the `CompactionProcessor: Utilization X% (used/total tokens)` log lines. The **primary token source is now each session's `events.jsonl`**. Per-turn context occupancy is **no longer logged** (only `assistant.message.data.outputTokens`, the response size). Context-occupancy snapshots exist only at these events:
- `session.compaction_complete.data.preCompactionTokens` — context occupancy just before each compaction (present uniformly back to Feb 2026; never pruned). Each is a local peak (~80% threshold).
- `session.shutdown.data.currentTokens` — final context occupancy when the session ended. This is the peak signal for sessions that **never compacted**, so token data appears "straight up" without waiting for a compaction.
- **initial tokens** = `systemTokens + toolDefinitionsTokens` (the fixed startup context overhead, ~constant ~28–33K), taken from the earliest `session.compaction_start.data` or, failing that, `session.shutdown.data`.
- `session.compaction_start.data.model` (falling back to `session.shutdown` model, then the last `tool.execution_complete.data.model`) — used to infer the context window.
- Compaction detail lines (`replaced N messages … saved ~N tokens`, `Persisted checkpoint #N`) are **still** emitted in `process-*.log`, so `buildTokenIndex` is retained to enrich compaction events (tokensSaved/forced) when the log survives.

Derivation: `initialTokens` = baseline (systemTokens+toolDefinitionsTokens); `peakTokens` = max(all preCompactionTokens, shutdown currentTokens); `contextWindow` = model-based (`contextWindowForModel`) else `snap(compactionPeak / 0.8)` — the shutdown value must **not** feed the `/0.8` inference (it isn't at the 80% threshold); `tokenHistory` = [baseline@createdAt, each compaction point, shutdown point] sorted.

Resolution is 4-tier in `loadAllSessions`: (1) legacy log Utilization history (only pre-1.0.70 sessions still in the log window), (2) `events.jsonl` (compaction snapshots and/or shutdown currentTokens + baseline), (3) persisted `gridwatch.json` snapshot (`tokenStatsCached`), (4) none. Coverage note: a **live/active** session that has neither compacted nor shut down has no context-occupancy snapshot → `peakTokens` 0 (no token panel) until it compacts or exits. Very old (pre-Feb-2026) sessions expose only cumulative `modelMetrics` (not context occupancy) → also no panel.

### Session → log token attribution (legacy log path)
Token/Utilisation lines in `process-*.log` carry **no** session id. Attribute them to the session named in the most recent `Registering foreground session: <uuid>` line (cleared by `Unregistering …`); skip lines when no session is active. A session can span multiple logs. This is done once in `buildTokenIndex(logDir)` before the per-session loop in `loadAllSessions` — never match by closest timestamp (that mis-binds pruned sessions to unrelated logs). Note: post-1.0.70 logs no longer contain Utilization lines, so this path now only yields compaction detail.

### Context window is not always 128K
Copilot's context window varies by model and is **no longer logged numerically**. Infer it via `contextWindowForModel()` (claude-* → 200K [verified: 162,697 ≈ 81% of 200K], gpt-5* → 272K, gpt-4* → 128K, gemini* → 1M); for unknown models fall back to snapping `peakTokens / 0.8` (the ~80% compaction threshold, `COMPACTION_THRESHOLD`) to the nearest of `KNOWN_CONTEXT_WINDOWS`. For the legacy log path, still parse the denominator from the `Utilization X% (used/total tokens)` line and keep `peakTokens`, `peakUtilisation` and `contextWindow` from the **same** (max-tokens) line so they stay coherent.

### Token log pruning + persisted snapshots
Copilot prunes `~/.copilot/logs` to ~14 days, but `events.jsonl` is session-owned and un-pruned, so the events-based source covers old sessions too. GridWatch still persists a `tokenStats` snapshot into each session's `gridwatch.json` for the first page (top `TOKEN_SNAPSHOT_LIMIT = 20` most-recently-updated sessions) on load, using the final resolved token fields (whatever tier produced them, skipping snapshot-sourced ones). `tokenLogPruned` is only true when there is no live log, no events snapshot, no saved snapshot, logs exist, and the session predates the oldest surviving log. Snapshot writes are change-gated, atomic (temp+rename) and serialised per session via `mutateSessionMeta` to avoid clobbering tags/notes.

---

## Design system

All design tokens are CSS custom properties in `src/index.css`:

```css
--tron-bg:          #060a14    /* main background */
--tron-panel:       #0a0e1f    /* card/panel background */
--tron-cyan:        #00f5ff    /* primary accent — titles, active state */
--tron-blue:        #0080ff    /* secondary accent — section labels, bars */
--tron-orange:      #ff6600    /* destructive actions, active nav indicator */
--tron-text:        #c0e8ff    /* body text */
--tron-text-dim:    #4a7a9b    /* muted/secondary text */
--tron-border:      #1a2a4a    /* borders */
--tron-glow-cyan:   0 0 8px rgba(0, 245, 255, 0.4)
--tron-glow-orange: 0 0 8px rgba(255, 102, 0, 0.4)
--sidebar-width:    160px
```

Density variants are applied via `data-density` attribute on `<html>`:
- `compact` — reduced padding on cards and nav items
- `default` — standard
- `comfortable` — increased padding

---

## App layout

```
┌─────────────────────────────────────────────────┐
│ Titlebar (44px, hiddenInset macOS, traffic 16,12)│
├──────────┬──────────────────────────────────────┤
│ Sidebar  │ Content (overflow-y: auto)            │
│ (160px)  │                                       │
│          │  <SessionsPage | TokensPage |          │
│ sidebarTop  ActivityPage | SkillsPage |          │
│ (scrolls)│  InsightsPage | SettingsPage>         │
│          │                                       │
│ ──────── │                                       │
│ sidebarBottom (fixed: version label + Settings)  │
└──────────┴──────────────────────────────────────┘
```

The sidebar uses two zones (`sidebarTop` = flex:1 overflow-y:auto, `sidebarBottom` = flex-shrink:0) so the Settings item is always visible regardless of zoom level.

---

## npm scripts

```bash
npm run dev          # Vite dev server + Electron (hot reload)
npm run dev:debug    # Same, but with DevTools auto-opened
npm run pack:mac     # tsc && vite build && electron-builder --mac
npm run pack:win     # tsc && vite build && electron-builder --win
npm run pack:all     # tsc && vite build && electron-builder --mac --win
```

**Note:** The build tool is `vite` (from `vite-plugin-electron`), NOT `electron-vite`. Do not use `electron-vite` in scripts.

---

## Coding conventions

1. **TypeScript strict** — no `any` without justification, no implicit `any`
2. **Null safety** — always guard array fields: `(s.toolsUsed ?? [])`, `(s.createdAt ?? '')`
3. **CSS Modules** — all component styles in `.module.css`, no inline styles for static values
4. **CSS variables** — never hardcode hex colours, always use `var(--tron-*)`
5. **Error boundaries** — `<PageErrorBoundary key={activePage}>` in App.tsx wraps all pages; add more where needed
6. **IPC guards** — wrap all `ipcMain.handle` bodies in try/catch, return safe defaults
7. **No Copilot file writes** — never write to `workspace.yaml`, `events.jsonl`, or any Copilot-owned file

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.