|
| 1 | +--- |
| 2 | +description: "Add support for a new CLI-based coding environment (e.g. a new terminal agent) so its sessions appear in all extension views: session list, log viewer, charts, usage analysis, and diagnostics." |
| 3 | +name: "New Editor Support" |
| 4 | +tools: ["execute/runInTerminal", "execute/getTerminalOutput", "search/codebase", "read/problems"] |
| 5 | +--- |
| 6 | + |
| 7 | +# New Editor Support |
| 8 | + |
| 9 | +Integrate a new CLI-based coding environment into the extension so its session data appears in the session list, log viewer, charts, usage analysis, and diagnostics panels. |
| 10 | + |
| 11 | +## When to Use This Agent |
| 12 | + |
| 13 | +Trigger this agent when: |
| 14 | +- A new terminal-based coding agent (like OpenCode, Crush, Continue, etc.) needs to be added as a tracked editor |
| 15 | +- Users want token/interaction stats from a tool that stores data outside VS Code's AppData |
| 16 | +- A new session data format (SQLite DB, JSON files, JSONL, etc.) needs to be parsed |
| 17 | + |
| 18 | +## Architecture Overview |
| 19 | + |
| 20 | +The extension uses a **pipeline** from raw session files to all displays: |
| 21 | + |
| 22 | +``` |
| 23 | +Session Discovery → Cache → Token/Interaction Counting → Stats Aggregation → UI |
| 24 | +``` |
| 25 | + |
| 26 | +Every new editor must plug into **each stage** of this pipeline. The integration is deliberately layered so each layer has one responsibility. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## Step-by-Step Integration |
| 31 | + |
| 32 | +### Step 1 — Explore the Data Source |
| 33 | + |
| 34 | +Before writing any code, understand the new editor's storage layout: |
| 35 | + |
| 36 | +1. **Find the config/data directories** — check OS-specific locations (Windows: `%APPDATA%`, `%LOCALAPPDATA%`; Linux/macOS: `~/.config`, `~/.local/share`, `XDG_*` env vars). |
| 37 | +2. **Identify session files** — are sessions stored as individual JSON files, a single SQLite DB, per-project DBs, or JSONL? |
| 38 | +3. **Inspect the schema** — for SQLite, dump `.tables` and `PRAGMA table_info(table)`. For JSON, read a real session file. |
| 39 | +4. **Locate token counts** — does the schema have per-message tokens, per-session totals, or none? Note whether thinking/reasoning tokens are separately tracked. |
| 40 | +5. **Locate model info** — which field holds the model name/ID? Is it per-session or per-message? |
| 41 | +6. **Understand timestamps** — are they Unix epoch seconds, milliseconds, or ISO 8601 strings? (This is a common source of bugs — epoch seconds must be multiplied by 1000 for JS Date.) |
| 42 | +7. **Locate a projects registry** — if the editor stores one DB per project, there is usually a global index file (e.g. `projects.json`) that lists all known projects with their data directories. |
| 43 | + |
| 44 | +> **Lesson learned:** Always verify whether timestamps are in seconds or milliseconds before writing any date conversion code. Crush's SQLite stores epoch *seconds*; JS Date needs *milliseconds*. Getting this wrong silently corrupts all timestamps. |
| 45 | +
|
| 46 | +### Step 2 — Create a Dedicated Data Access Class |
| 47 | + |
| 48 | +Create `src/<editorname>.ts` modelled on `src/opencode.ts` and `src/crush.ts`. **Do not modify `opencode.ts`** — each editor gets its own file. |
| 49 | + |
| 50 | +The class must expose: |
| 51 | + |
| 52 | +| Method | Purpose | |
| 53 | +|---|---| |
| 54 | +| `getConfigDir(): string` | OS-aware path to the editor's config/data root | |
| 55 | +| `isSessionFile(filePath: string): boolean` | Returns true for any path belonging to this editor (normalise backslashes before checking) | |
| 56 | +| `statSessionFile(virtualPath: string): Promise<fs.Stats>` | Stats the underlying DB/file (needed for virtual paths that point into a DB) | |
| 57 | +| `discoverSessions(): Promise<string[]>` | Returns all virtual session paths | |
| 58 | +| `readSession(virtualPath): Promise<any \| null>` | Reads session metadata (title, timestamps, token totals) | |
| 59 | +| `getMessages(virtualPath): Promise<any[]>` | Returns all messages/turns ordered by time | |
| 60 | +| `getTokens(virtualPath): Promise<{ tokens: number; thinkingTokens: number }>` | Returns total tokens for the session | |
| 61 | +| `countInteractions(virtualPath): Promise<number>` | Count of user-role messages (= turns) | |
| 62 | +| `getModelUsage(virtualPath): Promise<ModelUsage>` | Per-model `{ inputTokens, outputTokens }` breakdown | |
| 63 | + |
| 64 | +**Virtual path scheme** (for DB-backed editors): use `<db_file_path>#<session_id>` so the file path remains a string throughout the pipeline. Example: `C:\repo\.crush\crush.db#<uuid>`. This mirrors OpenCode's `opencode.db#ses_<id>` convention. |
| 65 | + |
| 66 | +**Always normalise backslashes** in `isSessionFile()`: |
| 67 | +```ts |
| 68 | +isCrushSessionFile(filePath: string): boolean { |
| 69 | + return filePath.replace(/\\/g, '/').includes('/.crush/crush.db#'); |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +### Step 3 — Register Path Detection in `workspaceHelpers.ts` |
| 74 | + |
| 75 | +Two functions need updating in `src/workspaceHelpers.ts`: |
| 76 | + |
| 77 | +- **`getEditorTypeFromPath()`** — add a check *before* the generic `'/code/'` check (it will false-positive on any path containing the word `code`). Normalise backslashes first with `.replace(/\\/g, '/')`. |
| 78 | +- **`detectEditorSource()`** — same guard, same placement rule. |
| 79 | + |
| 80 | +> **Lesson learned:** The generic `'/code/'` check in `getEditorTypeFromPath` / `detectEditorSource` catches paths that contain a folder literally named `code` — e.g. `C:\Users\RobBos\code\repos\...`. Any new editor whose virtual paths run through a user's `code` directory *must* be checked **before** this generic match, or it gets misclassified as VS Code. |
| 81 | +
|
| 82 | +Also update **`getEditorNameFromRoot()`** — add a check for the new editor's identifier before the generic `code` match. This function is used when reconstructing editor names from cached data. |
| 83 | + |
| 84 | +### Step 4 — Fix `enrichDetailsWithEditorInfo()` in `extension.ts` |
| 85 | + |
| 86 | +`enrichDetailsWithEditorInfo()` derives `editorRoot` and `editorName` by splitting the file path on the `User` directory component. This **breaks** for editors that: |
| 87 | +- Store data outside VS Code's AppData (no `User` directory) |
| 88 | +- Use virtual paths that happen to pass through the user's home directory hierarchy |
| 89 | + |
| 90 | +**Add an early-return guard** for each new editor at the *top* of `enrichDetailsWithEditorInfo()`: |
| 91 | + |
| 92 | +```ts |
| 93 | +if (this.newEditor.isNewEditorSessionFile(sessionFile)) { |
| 94 | + details.editorRoot = path.dirname(this.newEditor.getDbPath(sessionFile)); |
| 95 | + details.editorName = 'NewEditor'; |
| 96 | + return; |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +This guard is critical — it also fixes the **cache reconstruction path** in `getSessionFileDetailsFromCache()`, which calls `enrichDetailsWithEditorInfo()` when rebuilding from cache. Without it, stale cached sessions get the wrong editor name on every reload. |
| 101 | + |
| 102 | +### Step 5 — Register in `sessionDiscovery.ts` |
| 103 | + |
| 104 | +1. Add `newEditor: NewEditorDataAccess` to the `SessionDiscoveryDeps` interface. |
| 105 | +2. In `getDiagnosticCandidatePaths()` — add candidate paths (config file + per-project DB paths). These appear in the Diagnostics panel's "Scanned Paths" table. |
| 106 | +3. In `getCopilotSessionFiles()` — add a discovery loop after OpenCode's loop. Call `discoverSessions()` and push virtual paths into the results array. |
| 107 | + |
| 108 | +> **Lesson learned:** If the editor stores one DB per project (like Crush), you need a two-level discovery: first read the global project registry, then enumerate sessions in each project's DB. Calling the data access class's registry reader here keeps discovery and parsing separated. |
| 109 | +
|
| 110 | +### Step 6 — Wire Into `extension.ts` |
| 111 | + |
| 112 | +Add the new editor at **eight locations** in `extension.ts`: |
| 113 | + |
| 114 | +1. **Import** — `import { NewEditorDataAccess } from './neweditor';` |
| 115 | +2. **Class field** — `private newEditor: NewEditorDataAccess;` |
| 116 | +3. **Constructor** — `this.newEditor = new NewEditorDataAccess(extensionUri);` + pass it to `SessionDiscovery({ ..., newEditor: this.newEditor })` |
| 117 | +4. **`usageAnalysisDeps` getter** — add `newEditor: this.newEditor` |
| 118 | +5. **`statSessionFile()` method** — add the new editor as the first guard in the router method that delegates stat calls (this avoids `fs.promises.stat()` failing on virtual paths) |
| 119 | +6. **`estimateTokensFromSession()`** — add a branch returning actual token counts from the DB |
| 120 | +7. **`countInteractionsInSession()`** — add a branch |
| 121 | +8. **`extractSessionMetadata()`** — add a branch reading title + timestamps; convert epoch seconds to milliseconds here |
| 122 | +9. **`getSessionFileDetails()`** — add a branch after the OpenCode block; explicitly set `details.editorRoot`, `details.editorName` |
| 123 | +10. **`getSessionLogData()`** — add a branch building `ChatTurn[]` from messages; distribute session-level token totals evenly across turns when per-turn tokens are unavailable |
| 124 | + |
| 125 | +### Step 7 — Wire Into `usageAnalysis.ts` |
| 126 | + |
| 127 | +1. Add `import type { NewEditorDataAccess } from './neweditor';` |
| 128 | +2. Add `newEditor?: NewEditorDataAccess` to `UsageAnalysisDeps` (optional to avoid breaking callers) |
| 129 | +3. **`getModelUsageFromSession()`** — add a guard routing to the editor's `getModelUsage()` method. Also update the `Pick<>` type signature to include `'newEditor'` |
| 130 | +4. **`analyzeSessionUsage()`** — add a branch after the OpenCode block; count tool calls from message parts, set mode, build model switching stats |
| 131 | + |
| 132 | +### Step 8 — Update the Diagnostics Webview (`main.ts`) |
| 133 | + |
| 134 | +1. **`getEditorIcon()`** — add a case *before* all existing checks (the `crush` case must be before any generic word matches). Pick a distinctive emoji matching the tool's brand colour. |
| 135 | +2. **`getEditorBadgeClass()`** — add a CSS class name for the editor's brand colours. |
| 136 | +3. Add a **`.editor-badge-<name>`** CSS rule in `styles.css` with the brand colours. |
| 137 | +4. For editors that produce **many candidate paths** (one per project), consider grouping them into a single row in `buildCandidatePathsElement()` rather than one row per project. See the Crush implementation for the grouping pattern. |
| 138 | + |
| 139 | +> **Lesson learned:** The icon is shown in two places: the editor filter panel (via `getEditorIcon()`) and per-session-row badges. All badge rendering sites (`buildCandidatePathsElement`, the session table row, the folder stats table, the dynamically-built DOM version) must be updated to use `getEditorBadgeClass()` and include the icon prefix. Search for `editor-badge` in `main.ts` to find all four sites. |
| 140 | +
|
| 141 | +--- |
| 142 | + |
| 143 | +## Common Pitfalls |
| 144 | + |
| 145 | +| Pitfall | Fix | |
| 146 | +|---|---| |
| 147 | +| Timestamps show as year 1970 | Multiply epoch-seconds values by 1000 before passing to `new Date()` | |
| 148 | +| Editor shows as "VS Code" in session list | The path passes through a folder called `code` — add an early-return guard in `enrichDetailsWithEditorInfo()` and check before the generic `/code/` guard in detection helpers | |
| 149 | +| Sessions discovered but tokens show 0 | Check `estimateTokensFromSession()` — the branch may be missing or not returning early | |
| 150 | +| Cache returns stale editor name | `getSessionFileDetailsFromCache()` calls `enrichDetailsWithEditorInfo()` — ensure the guard there applies too | |
| 151 | +| Virtual paths fail `fs.promises.stat()` | Route through `statSessionFile()` which resolves virtual paths to real DB file paths | |
| 152 | +| Icon only appears in filter panel, not badge | Update all four `editor-badge` render sites in `main.ts` — there are DOM-creation and template-string-based variants | |
| 153 | +| Discovery loop finds 0 sessions even though DB exists | Verify the project registry reader returns the correct `data_dir` (not the project `path`) and that `path.join(data_dir, '<db>.db')` matches the actual file | |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## Checklist |
| 158 | + |
| 159 | +- [ ] `src/<editor>.ts` created with all required methods |
| 160 | +- [ ] `workspaceHelpers.ts` — both detection helpers updated, new check before generic `/code/` match |
| 161 | +- [ ] `workspaceHelpers.ts` — `getEditorNameFromRoot()` updated |
| 162 | +- [ ] `extension.ts` — `enrichDetailsWithEditorInfo()` has early-return guard |
| 163 | +- [ ] `extension.ts` — all 10 integration points wired |
| 164 | +- [ ] `sessionDiscovery.ts` — deps interface, candidate paths, discovery loop |
| 165 | +- [ ] `usageAnalysis.ts` — deps interface, `getModelUsageFromSession()`, `analyzeSessionUsage()` |
| 166 | +- [ ] `webview/diagnostics/main.ts` — icon, badge class, all 4 badge render sites, candidate path grouping if needed |
| 167 | +- [ ] `webview/diagnostics/styles.css` — brand colour CSS rule added |
| 168 | +- [ ] `npm run compile` passes (TypeScript + ESLint + esbuild) |
| 169 | +- [ ] Sessions appear in the session list with the correct editor name and icon |
| 170 | +- [ ] Token counts are non-zero and plausible |
| 171 | +- [ ] Timestamps are correct (not 1970) |
| 172 | +- [ ] Diagnostics "Scanned Paths" table shows the new editor's paths |
0 commit comments