Skip to content

Commit 847e985

Browse files
authored
Merge pull request #452 from rajbos/new-coding-tool-agent
Add support for a new CLI-based coding environment in the extension
2 parents 199e1ac + 1f989b8 commit 847e985

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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

Comments
 (0)