@qiaolei81/copilot-session-viewer 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,59 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.1] - 2026-03-07
9
+
10
+ ### Fixed
11
+ - **Session Deduplication** - VSCode sessions with the same ID across multiple workspaces are now deduplicated (keeps most recently updated)
12
+ - **WIP Status Accuracy** - VSCode agentic sessions now also check file mtime for WIP detection; threshold increased from 5 to 15 minutes
13
+ - **Timeline Bar Positioning** - UserReq rows with 0 tools no longer render at the start of the timeline
14
+ - **Tag Isolation** - Tags now use filePath-based storage to prevent shared directory collisions (Claude, Pi-Mono, Copilot CLI)
15
+ - **Per-Session Insight Files** - Agent review files use `{sessionId}.agent-review.md` naming to avoid collisions
16
+ - **Export All Sources** - Session export works for all sources including VSCode; file-based exports include `.tags.json`
17
+ - **Inline References in Markdown** - VSCode `inlineReference` items (file/folder links) now rendered as code references instead of being silently dropped, fixing broken markdown tables
18
+
19
+ ### Performance
20
+ - **60s Cache + Request Dedup** - `SessionRepository.findAll()` results cached for 60 seconds with concurrent request deduplication, reducing TTFB from ~11s to <100ms on cache hit
21
+
22
+ ## [0.3.0] - 2026-03-07
23
+
24
+ ### Added
25
+ - **VSCode Copilot Chat Support** - Full support for VSCode Copilot Chat sessions as a new source (`vscode`)
26
+ - Session cards show model badge + repo basename
27
+ - Copilot Chat extension version badge
28
+ - WIP badge for active sessions
29
+ - SubAgent name badges on assistant messages (replaces generic ASSISTANT badge)
30
+ - UserReq rows in Gantt timeline
31
+ - Turn divider shows start time + duration
32
+ - **Dynamic Source Path Hints** - Clicking filter pills shows the source directory path (cross-platform)
33
+ - **Multi-Tool Branding** - Homepage wording updated to reflect multi-tool support (Copilot CLI, Claude Code, Pi-Mono, Copilot Chat)
34
+
35
+ ### Changed
36
+ - **Source Display Names** - `copilot` → "Copilot CLI", `vscode` → "Copilot Chat"
37
+ - **Session Info Layout** - Shows Model + Repo basename instead of CWD hash path
38
+ - **Tool Input Display** - URI objects simplified to filename only; edits collapsed to count
39
+ - **Tool Call Width** - Truncation increased to 200 chars with flex-wrap
40
+ - **SubAgent Badge** - Shows name only (no emoji prefix)
41
+ - **System Messages** - System-sourced `user.message` converted to `system.notification` type with SYSTEM badge
42
+
43
+ ### Fixed
44
+ - Claude `tool_result` user messages filtered from display
45
+ - VSCode parser: `resultDetails` iteration crash, request timestamps for subagent events
46
+ - VSCode subagent dedup by `subAgentId` instead of name
47
+ - VSCode `findById` uses `selectedModel` + `resolveWorkspacePath`
48
+ - VSCode duration estimation fix
49
+ - Session detail repo dedup, skill table width
50
+ - SESSION INFO duplicate Model/label
51
+ - SubAgent badge uses `agentName` from `toolSpecificData`
52
+ - Source path hints from server (cross-platform Windows/macOS/Linux)
53
+
54
+ ## [0.2.7] - 2026-03-05
55
+
56
+ ### Added
57
+ - **Session Tagging** - Add, remove, and filter sessions by custom tags from the session list and detail pages
58
+ - **Unit Tests for Tagging** - 70 new tests covering `tagService` and `tagController` (608 total)
59
+ - **E2E Tests for Tagging** - Playwright tests covering tagging API and UI flows
60
+
8
61
  ## [0.2.6] - 2026-03-05
9
62
 
10
63
  ### Fixed
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)](https://nodejs.org/)
7
7
 
8
8
  **Multi-Tool Session Log Viewer & Analyzer**
9
- View and analyze AI coding assistant sessions from **GitHub Copilot CLI**, **Claude Code CLI**, and **Pi-Mono** with time analysis, virtual scrolling, and AI-powered insights.
9
+ View and analyze AI coding assistant sessions from **GitHub Copilot CLI**, **Copilot Chat (VSCode)**, **Claude Code CLI**, and **Pi-Mono** with time analysis, virtual scrolling, and AI-powered insights.
10
10
 
11
- A modern web-based viewer for analyzing AI coding assistant session logs with virtual scrolling, infinite loading, time analysis, and AI-powered insights. Supports **GitHub Copilot CLI**, **Claude Code CLI**, and **Pi-Mono** sessions.
11
+ A modern web-based viewer for analyzing AI coding assistant session logs with virtual scrolling, infinite loading, time analysis, and AI-powered insights. Supports **GitHub Copilot CLI**, **Copilot Chat (VSCode)**, **Claude Code CLI**, and **Pi-Mono** sessions.
12
12
 
13
13
  ### Session List
14
14
  ![Session List](https://raw.githubusercontent.com/qiaolei81/copilot-session-viewer/main/docs/images/homepage.png)
@@ -57,7 +57,7 @@ copilot-session-viewer
57
57
  - **🚀 Virtual Scrolling** - Handle 1000+ events smoothly
58
58
  - **♾️ Infinite Scroll** - Progressive session loading for better performance
59
59
  - **🤖 AI Insights** - LLM-powered session analysis
60
- - **🎭 Multi-Format Support** - Copilot, Claude Code, and Pi-Mono sessions
60
+ - **🎭 Multi-Format Support** - Copilot CLI, Copilot Chat (VSCode), Claude Code, and Pi-Mono sessions
61
61
 
62
62
  ### 🎨 **User Experience**
63
63
  - **🌙 Dark Theme** - GitHub-inspired interface
@@ -69,7 +69,7 @@ copilot-session-viewer
69
69
  - **Vue 3** - Reactive virtual scrolling
70
70
  - **Express.js** - Robust backend API
71
71
  - **ZIP Import/Export** - Session sharing capabilities with security validation
72
- - **Multi-Source Support** - Copilot (`~/.copilot/session-state/`), Claude (`~/.claude/projects/`), Pi-Mono (`~/.pi/agent/sessions/`)
72
+ - **Multi-Source Support** - Copilot CLI (`~/.copilot/session-state/`), Copilot Chat (`~/Library/Application Support/Code/User/workspaceStorage/`), Claude (`~/.claude/projects/`), Pi-Mono (`~/.pi/agent/sessions/`)
73
73
  - **Unified Event Format** - Consistent schema across all sources
74
74
  - **Memory Pagination** - Efficient handling of large sessions
75
75
  - **XSS Protection** - DOMPurify-based HTML sanitization
@@ -81,7 +81,8 @@ copilot-session-viewer
81
81
 
82
82
  1. **Generate Sessions** - Use GitHub Copilot CLI, Claude Code CLI, or Pi-Mono to create session logs
83
83
  2. **Auto-Discovery** - Sessions are automatically detected from:
84
- - Copilot: `~/.copilot/session-state/`
84
+ - Copilot CLI: `~/.copilot/session-state/`
85
+ - Copilot Chat: `~/Library/Application Support/Code/User/workspaceStorage/`
85
86
  - Claude: `~/.claude/projects/`
86
87
  - Pi-Mono: `~/.pi/agent/sessions/`
87
88
  3. **Browse & Analyze** - View sessions with infinite scroll and detailed event streams
@@ -124,8 +125,8 @@ This project includes comprehensive unit and E2E test coverage with CI/CD integr
124
125
 
125
126
  ### Test Coverage
126
127
 
127
- - **470+ Tests** (411 unit + 59 E2E)
128
- - **Unified Format Tests** - Mock data validation for all sources (Copilot, Claude, Pi-Mono)
128
+ - **700+ Tests** (622 unit + 80 E2E)
129
+ - **Unified Format Tests** - Mock data validation for all sources (Copilot CLI, Copilot Chat, Claude, Pi-Mono)
129
130
  - **Security Tests** - XSS prevention, ZIP bomb defense
130
131
  - **Integration Tests** - Session import/export, file operations
131
132
  - **CI-Friendly** - Mock data generation for reproducible tests
@@ -153,15 +154,15 @@ npm run test:all
153
154
 
154
155
  GitHub Actions workflow includes:
155
156
  1. **Linting** - ESLint code quality checks
156
- 2. **Unit Tests** - 411 Jest tests with coverage
157
+ 2. **Unit Tests** - 622 Jest tests with coverage
157
158
  3. **Mock Data Generation** - Reproducible test session fixtures
158
- 4. **E2E Tests** - 59 Playwright tests with Chromium
159
+ 4. **E2E Tests** - 80 Playwright tests with Chromium
159
160
  5. **Artifact Upload** - Test results on failure
160
161
 
161
162
  **Test Data Strategy:**
162
163
  - ✅ CI uses generated mock data (fast, reliable, no external dependencies)
163
164
  - ✅ Local development can use real sessions for integration testing
164
- - ✅ Fixtures cover all event formats (Copilot, Claude, Pi-Mono)
165
+ - ✅ Fixtures cover all event formats (Copilot CLI, Copilot Chat, Claude, Pi-Mono)
165
166
 
166
167
  ---
167
168
 
@@ -187,9 +188,10 @@ GitHub Actions workflow includes:
187
188
  ↕ File System
188
189
  ┌─────────────────────────────────────────────────┐
189
190
  │ Data Layer (Multi-Source) │
190
- │ • Copilot: ~/.copilot/session-state/
191
- │ • Claude: ~/.claude/projects/
192
- │ • Pi-Mono: ~/.pi/agent/sessions/
191
+ │ • Copilot CLI: ~/.copilot/session-state/
192
+ │ • Copilot Chat: ~/Library/.../workspaceStorage/
193
+ │ • Claude: ~/.claude/projects/
194
+ │ • Pi-Mono: ~/.pi/agent/sessions/ │
193
195
  └─────────────────────────────────────────────────┘
194
196
  ```
195
197
 
@@ -304,12 +306,13 @@ MIT License - see [LICENSE](LICENSE) file for details
304
306
  - [DOMPurify](https://github.com/cure53/DOMPurify) - XSS protection
305
307
  - [Playwright](https://playwright.dev/) - E2E testing
306
308
 
307
- **Recent Updates (v0.1.9+):**
308
- - ✨ Multi-source support (Copilot, Claude, Pi-Mono)
309
+ **Recent Updates (v0.3.0):**
310
+ - ✨ VSCode Copilot Chat support (4th source)
311
+ - 🏷️ Session tagging system
312
+ - 🔄 Multi-tool branding (Copilot CLI, Copilot Chat, Claude Code, Pi-Mono)
309
313
  - 🔒 XSS protection with DOMPurify
310
314
  - 🛡️ ZIP bomb defense (4-layer validation)
311
- - 📄 Memory pagination API
312
- - 🧪 470+ tests with CI/CD integration
315
+ - 🧪 700+ tests with CI/CD integration
313
316
  - 📚 Comprehensive documentation
314
317
 
315
318
  ---
@@ -2,7 +2,7 @@ const BaseSessionParser = require('./base-parser');
2
2
  const CopilotSessionParser = require('./copilot-parser');
3
3
  const ClaudeSessionParser = require('./claude-parser');
4
4
  const PiMonoParser = require('./pi-mono-parser');
5
- // const VsCodeParser = require('./vscode-parser'); // TODO: VSCode parser disabled
5
+ const VsCodeParser = require('./vscode-parser');
6
6
  const ParserFactory = require('./parser-factory');
7
7
 
8
8
  module.exports = {
@@ -10,6 +10,6 @@ module.exports = {
10
10
  CopilotSessionParser,
11
11
  ClaudeSessionParser,
12
12
  PiMonoParser,
13
- // VsCodeParser, // TODO: VSCode parser disabled
13
+ VsCodeParser,
14
14
  ParserFactory
15
15
  };
@@ -11,14 +11,26 @@ const BaseSessionParser = require('./base-parser');
11
11
  */
12
12
  class VsCodeParser extends BaseSessionParser {
13
13
  /**
14
- * VSCode sessions are passed as a plain JS object (already parsed JSON),
15
- * not as an array of events. We detect them by the presence of `requests`.
16
- * The ParserFactory passes raw events arrays for JSONL sources, so we
17
- * keep canParse() returning false — VSCode sessions are loaded differently.
14
+ * VSCode sessions can be either:
15
+ * 1. Old format: Plain JSON object with `requests` array
16
+ * 2. New format: JSONL with ObjectMutationLog entries (kind: 0|1|2|3)
17
+ *
18
+ * For JSONL format, the first line has kind=0 with v.sessionId
18
19
  */
19
- canParse(_events) {
20
- // VSCode sessions are JSON objects, not event arrays.
21
- // SessionRepository calls parseVsCode() directly; canParse is unused here.
20
+ canParse(lines) {
21
+ if (!Array.isArray(lines) || lines.length === 0) return false;
22
+
23
+ // Check if this is JSONL format (array of parsed lines)
24
+ const firstLine = lines[0];
25
+
26
+ // New JSONL format: first line has kind=0 and v.sessionId
27
+ if (firstLine && typeof firstLine === 'object' &&
28
+ firstLine.kind === 0 &&
29
+ firstLine.v &&
30
+ firstLine.v.sessionId) {
31
+ return true;
32
+ }
33
+
22
34
  return false;
23
35
  }
24
36
 
@@ -38,11 +50,172 @@ class VsCodeParser extends BaseSessionParser {
38
50
  };
39
51
  }
40
52
 
41
- // ---- required abstract methods (not used for VsCode path) ----
42
- parse(_events) { return null; }
43
- getMetadata(_events) { return null; }
44
- extractTurns(_events) { return []; }
45
- extractToolCalls(_events) { return []; }
53
+ /**
54
+ * Parse JSONL format (ObjectMutationLog) - new VS Code format
55
+ * @param {Array} lines - Array of parsed JSON objects from JSONL file
56
+ * @returns {Object} Normalised session data
57
+ */
58
+ parseJsonl(lines) {
59
+ // Replay mutations to reconstruct the session state
60
+ const sessionState = this.replayMutations(lines);
61
+
62
+ // Use existing methods to convert to events
63
+ const metadata = this._getMetadata(sessionState);
64
+ const events = this._toEvents(sessionState);
65
+
66
+ return {
67
+ metadata,
68
+ turns: this._extractTurns(events),
69
+ toolCalls: this._extractToolCalls(events),
70
+ allEvents: events,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Replay ObjectMutationLog entries to reconstruct session state
76
+ * @param {Array} lines - Array of mutation entries { kind, k, v, i }
77
+ * @returns {Object} Reconstructed session state
78
+ */
79
+ replayMutations(lines) {
80
+ let state = null;
81
+
82
+ for (const entry of lines) {
83
+ if (!entry || typeof entry !== 'object') continue;
84
+
85
+ const { kind, k, v, i } = entry;
86
+
87
+ switch (kind) {
88
+ case 0: // Initial - set entire state
89
+ state = v;
90
+ break;
91
+
92
+ case 1: // Set - update property at path
93
+ if (k && Array.isArray(k)) {
94
+ this._applySet(state, k, v);
95
+ }
96
+ break;
97
+
98
+ case 2: // Push - append to array (with optional truncate)
99
+ if (k && Array.isArray(k)) {
100
+ this._applyPush(state, k, v, i);
101
+ }
102
+ break;
103
+
104
+ case 3: // Delete - remove property at path
105
+ if (k && Array.isArray(k)) {
106
+ this._applyDelete(state, k);
107
+ }
108
+ break;
109
+
110
+ default:
111
+ console.warn(`[VsCodeParser] Unknown mutation kind: ${kind}`);
112
+ }
113
+ }
114
+
115
+ return state;
116
+ }
117
+
118
+ /**
119
+ * Apply Set mutation: set value at path k in state
120
+ * @private
121
+ */
122
+ _applySet(state, path, value) {
123
+ if (!state || path.length === 0) return;
124
+
125
+ let current = state;
126
+ for (let i = 0; i < path.length - 1; i++) {
127
+ const key = path[i];
128
+ if (!current[key]) {
129
+ // Create intermediate object or array based on next key type
130
+ current[key] = typeof path[i + 1] === 'number' ? [] : {};
131
+ }
132
+ current = current[key];
133
+ }
134
+
135
+ const lastKey = path[path.length - 1];
136
+ current[lastKey] = value;
137
+ }
138
+
139
+ /**
140
+ * Apply Push mutation: append items to array at path k
141
+ * If i is set, truncate array to index i first
142
+ * @private
143
+ */
144
+ _applyPush(state, path, values, startIndex) {
145
+ if (!state || path.length === 0) return;
146
+
147
+ let current = state;
148
+ for (let i = 0; i < path.length - 1; i++) {
149
+ const key = path[i];
150
+ if (!current[key]) {
151
+ current[key] = typeof path[i + 1] === 'number' ? [] : {};
152
+ }
153
+ current = current[key];
154
+ }
155
+
156
+ const lastKey = path[path.length - 1];
157
+ if (!current[lastKey]) {
158
+ current[lastKey] = [];
159
+ }
160
+
161
+ const arr = current[lastKey];
162
+ if (!Array.isArray(arr)) {
163
+ console.warn(`[VsCodeParser] Push target is not an array: ${path.join('.')}`);
164
+ return;
165
+ }
166
+
167
+ // Truncate if startIndex is specified
168
+ if (startIndex !== undefined && startIndex !== null) {
169
+ arr.length = startIndex;
170
+ }
171
+
172
+ // Append new values
173
+ if (values && Array.isArray(values) && values.length > 0) {
174
+ arr.push(...values);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Apply Delete mutation: remove property at path k
180
+ * @private
181
+ */
182
+ _applyDelete(state, path) {
183
+ if (!state || path.length === 0) return;
184
+
185
+ let current = state;
186
+ for (let i = 0; i < path.length - 1; i++) {
187
+ const key = path[i];
188
+ if (!current[key]) return; // Path doesn't exist
189
+ current = current[key];
190
+ }
191
+
192
+ const lastKey = path[path.length - 1];
193
+ delete current[lastKey];
194
+ }
195
+
196
+ // ---- required abstract methods (for ParserFactory interface) ----
197
+ parse(events) {
198
+ // For JSONL format, use parseJsonl
199
+ if (Array.isArray(events) && events.length > 0 && this.canParse(events)) {
200
+ return this.parseJsonl(events);
201
+ }
202
+ return null;
203
+ }
204
+
205
+ getMetadata(events) {
206
+ const parsed = this.parse(events);
207
+ return parsed ? parsed.metadata : null;
208
+ }
209
+
210
+ extractTurns(events) {
211
+ const parsed = this.parse(events);
212
+ return parsed ? parsed.turns : [];
213
+ }
214
+
215
+ extractToolCalls(events) {
216
+ const parsed = this.parse(events);
217
+ return parsed ? parsed.toolCalls : [];
218
+ }
46
219
 
47
220
  // ---- private helpers ----
48
221
 
@@ -98,42 +271,16 @@ class VsCodeParser extends BaseSessionParser {
98
271
  const completedAt = req.modelState?.completedAt
99
272
  ? new Date(req.modelState.completedAt).toISOString()
100
273
  : ts;
101
- const reqStartMs = ts ? new Date(ts).getTime() : null;
102
- const reqEndMs = completedAt ? new Date(completedAt).getTime() : reqStartMs;
103
- const reqDurationMs = (reqStartMs && reqEndMs) ? (reqEndMs - reqStartMs) : 0;
274
+
104
275
 
105
276
  // Build subAgentInvocationId → agent name map from this request's response items
106
- const responseItems = req.response || [];
277
+ const responseItems = Array.isArray(req.response) ? req.response : [];
107
278
  const subAgentNames = this._buildSubAgentNameMap(responseItems);
108
279
 
109
- // Count items per subagent (in first-appearance order) for proportional time estimation
110
- const subAgentItemCounts = new Map(); // sid → count
111
- const subAgentOrder = []; // ordered unique sids
112
- for (const item of responseItems) {
113
- if (!item || typeof item !== 'object') continue;
114
- const sid = item.subAgentInvocationId;
115
- if (!sid) continue;
116
- if (!subAgentItemCounts.has(sid)) { subAgentItemCounts.set(sid, 0); subAgentOrder.push(sid); }
117
- subAgentItemCounts.set(sid, subAgentItemCounts.get(sid) + 1);
118
- }
119
- const totalSubAgentItems = [...subAgentItemCounts.values()].reduce((a, b) => a + b, 0);
120
-
121
- // Compute estimated start/end timestamps per subagent (proportional to item count)
122
- const subAgentTimestamps = new Map(); // sid → { startTime, endTime }
123
- if (reqStartMs && totalSubAgentItems > 0) {
124
- let cursor = reqStartMs;
125
- for (const sid of subAgentOrder) {
126
- const fraction = subAgentItemCounts.get(sid) / totalSubAgentItems;
127
- const duration = Math.round(reqDurationMs * fraction);
128
- subAgentTimestamps.set(sid, {
129
- startTime: new Date(cursor).toISOString(),
130
- endTime: new Date(cursor + duration).toISOString(),
131
- });
132
- cursor += duration;
133
- }
134
- }
135
-
136
- const seenSubAgents = new Set();
280
+ // Note: VS Code JSONL does not record per-subagent timestamps.
281
+ // All subagents in a request span the same wall-clock window (they run in parallel).
282
+ // Use request start as START and request completedAt as COMPLETE for all subagents,
283
+ // which is accurate for parallel dispatch and avoids interleaved/crossed timelines.
137
284
 
138
285
  // user.message
139
286
  const userText = this._extractUserText(req.message);
@@ -165,7 +312,7 @@ class VsCodeParser extends BaseSessionParser {
165
312
  assistantText = '';
166
313
  if (!trimmed) return;
167
314
  const sid = currentSubAgentId;
168
- const _agentName = sid ? (subAgentNames[sid] || sid.slice(0, 8)) : null;
315
+ const agentName = sid ? (subAgentNames[sid] || sid.slice(0, 8)) : null;
169
316
  events.push({
170
317
  type: 'assistant.message',
171
318
  id: `${req.requestId}-text-${itemIndex}`,
@@ -175,32 +322,9 @@ class VsCodeParser extends BaseSessionParser {
175
322
  message: trimmed,
176
323
  content: trimmed,
177
324
  tools: [],
178
- subAgentId: null,
179
- subAgentName: null,
180
- parentToolCallId: null,
181
- },
182
- });
183
- };
184
-
185
- const emitSubAgentStart = (sid, _itemIdx) => {
186
- if (!sid || seenSubAgents.has(sid)) return;
187
- seenSubAgents.add(sid);
188
- const agentName = subAgentNames[sid] || `subagent-${sid.slice(0, 8)}`;
189
- const times = subAgentTimestamps.get(sid);
190
- const startTs = times?.startTime || completedAt;
191
- events.push({
192
- type: 'subagent.started',
193
- id: `${req.requestId}-subagent-${sid}`,
194
- timestamp: startTs,
195
- parentId: req.requestId,
196
- data: {
197
- subAgentId: sid,
325
+ subAgentId: sid || null,
198
326
  subAgentName: agentName,
199
- agentName: agentName,
200
- agentDisplayName: agentName,
201
- toolCallId: sid,
202
- badgeLabel: agentName,
203
- badgeClass: 'badge-subagent',
327
+ parentToolCallId: null,
204
328
  },
205
329
  });
206
330
  };
@@ -224,15 +348,28 @@ class VsCodeParser extends BaseSessionParser {
224
348
  case null: {
225
349
  // Plain markdown text item (no kind field, has 'value')
226
350
  const text = item.value || '';
227
- if (text) assistantText += text + '\n';
351
+ if (text) assistantText += text;
352
+ break;
353
+ }
354
+
355
+ case 'inlineReference': {
356
+ // File/folder reference inline in markdown — append name as code reference
357
+ const name = item.name || '';
358
+ if (name) assistantText += '`' + name + '`';
228
359
  break;
229
360
  }
230
361
 
231
362
  case 'toolInvocationSerialized': {
232
363
  flushText();
233
- // Emit subagent.start on first appearance of this subagent
234
- const sid = item.subAgentInvocationId;
235
- emitSubAgentStart(sid, itemIndex);
364
+ // runSubagent items: toolId='runSubagent', toolCallId=subagent-id, subAgentInvocationId=null
365
+ // Regular tool items: toolId=e.g. 'copilot_readFile', subAgentInvocationId=owning-subagent-id
366
+ const sid = item.toolId === 'runSubagent'
367
+ ? item.toolCallId // the subagent being launched
368
+ : item.subAgentInvocationId; // the subagent that launched this tool
369
+ if (item.toolId === 'runSubagent') {
370
+ // Mark current context as this subagent (for subsequent tool items)
371
+ currentSubAgentId = sid;
372
+ }
236
373
  if (sid) currentSubAgentId = sid;
237
374
  const tool = this._normalizeTool(item);
238
375
  if (tool) {
@@ -283,7 +420,6 @@ class VsCodeParser extends BaseSessionParser {
283
420
  }
284
421
 
285
422
  case 'prepareToolInvocation':
286
- case 'inlineReference':
287
423
  case 'undoStop':
288
424
  case 'codeblockUri':
289
425
  case 'mcpServersStarting':
@@ -296,49 +432,44 @@ class VsCodeParser extends BaseSessionParser {
296
432
  }
297
433
 
298
434
  flushText(); // flush any trailing text
299
-
300
- // Emit subagent.completed events (proportional estimated endTime) after all items
301
- for (const sid of seenSubAgents) {
302
- const agentName = subAgentNames[sid] || `subagent-${sid.slice(0, 8)}`;
303
- const times = subAgentTimestamps.get(sid);
304
- const endTs = times?.endTime || completedAt;
305
- events.push({
306
- type: 'subagent.completed',
307
- id: `${req.requestId}-subagent-${sid}-end`,
308
- timestamp: endTs,
309
- parentId: req.requestId,
310
- data: {
311
- toolCallId: sid,
312
- agentDisplayName: agentName,
313
- agentName: agentName,
314
- },
315
- });
316
- }
317
435
  }
318
436
 
319
437
  return events;
320
438
  }
321
439
 
322
440
  /**
323
- * Extract agent name from the first toolInvocationSerialized item for a given subAgentInvocationId.
324
- * Looks for .agent.md filename in invocationMessage or resultDetails URIs.
441
+ * Build a map of subagent id agent name from runSubagent tool invocations.
442
+ * runSubagent items: toolId='runSubagent', toolCallId=subagent-id
443
+ * Agent name is extracted from invocationMessage or resultDetails.
325
444
  */
326
445
  _buildSubAgentNameMap(items) {
327
446
  const nameMap = {};
328
447
  for (const item of items) {
329
448
  if (!item || typeof item !== 'object') continue;
330
- const sid = item.subAgentInvocationId;
331
- if (!sid || nameMap[sid]) continue;
332
449
  if (item.kind !== 'toolInvocationSerialized') continue;
450
+ if (item.toolId !== 'runSubagent') continue;
333
451
 
334
- // Try invocationMessage text for agent file path
452
+ const sid = item.toolCallId;
453
+ if (!sid || nameMap[sid]) continue;
454
+
455
+ // Prefer agentName from toolSpecificData (e.g. "FoundationAgent")
456
+ const agentName = item.toolSpecificData?.agentName;
457
+ if (agentName) { nameMap[sid] = agentName; continue; }
458
+
459
+ // Fallback: Use invocationMessage as agent display name
335
460
  const msgObj = item.invocationMessage;
336
- const msg = (msgObj && typeof msgObj === 'object') ? (msgObj.value || '') : '';
461
+ const msg = typeof msgObj === 'string' ? msgObj
462
+ : (msgObj && typeof msgObj === 'object') ? (msgObj.value || '') : '';
463
+ if (msg) { nameMap[sid] = msg; continue; }
464
+
465
+ // Fallback: try agent file path in message
337
466
  let m = msg.match(/agents\/([^/\]]+?)\.agent\.md/);
338
467
  if (m) { nameMap[sid] = m[1]; continue; }
339
468
 
340
469
  // Try resultDetails
341
- for (const rd of (item.resultDetails || [])) {
470
+ const resultDetails = item.resultDetails;
471
+ const rdList = Array.isArray(resultDetails) ? resultDetails : (resultDetails ? [resultDetails] : []);
472
+ for (const rd of rdList) {
342
473
  if (typeof rd !== 'object') continue;
343
474
  const fp = rd.fsPath || rd.path || '';
344
475
  m = fp.match(/agents\/([^/]+?)\.agent\.md/);
@@ -366,10 +497,57 @@ class VsCodeParser extends BaseSessionParser {
366
497
 
367
498
  // toolSpecificData may hold input/output depending on tool type
368
499
  const tsd = item.toolSpecificData || {};
369
- const input = tsd.input || tsd.parameters || tsd.request || {};
370
- const result = tsd.output || tsd.result || item.resultDetails || null;
500
+ let input = tsd.input || tsd.parameters || tsd.request || {};
501
+ let result = tsd.output || tsd.result || null;
371
502
  const isError = item.isConfirmed === false;
372
503
 
504
+ // vscode tools don't serialize input/result into toolSpecificData.
505
+ // Use invocationMessage as a human-readable description of what the tool did,
506
+ // and generatedTitle / resultDetails as the result summary.
507
+ if (!result && (item.generatedTitle || item.resultDetails)) {
508
+ // resultDetails is an array of URIs (e.g. files found/read)
509
+ if (item.resultDetails) {
510
+ const rdList = Array.isArray(item.resultDetails) ? item.resultDetails : [item.resultDetails];
511
+ const paths = rdList.map(rd => rd.fsPath || rd.path || rd.external || JSON.stringify(rd)).filter(Boolean);
512
+ result = paths.length > 0 ? paths.join('\n') : item.generatedTitle || null;
513
+ } else {
514
+ result = item.generatedTitle || null;
515
+ }
516
+ }
517
+ // Use invocationMessage (plain text) as input description if input is empty
518
+ if (Object.keys(input).length === 0 && item.invocationMessage) {
519
+ const msg = item.invocationMessage;
520
+ const msgText = typeof msg === 'string' ? msg
521
+ : (msg && typeof msg === 'object') ? (msg.value || '') : '';
522
+ if (msgText) input = { description: msgText };
523
+ }
524
+
525
+ // Simplify URI objects: replace {$mid, fsPath, external, path, scheme} with just the filename
526
+ const simplifyUri = (uri) => {
527
+ if (!uri || typeof uri !== 'object') return uri;
528
+ const p = uri.fsPath || uri.path || uri.external || '';
529
+ return p ? p.replace(/.*\//, '') : JSON.stringify(uri);
530
+ };
531
+ const simplifyInput = (obj) => {
532
+ if (!obj || typeof obj !== 'object') return obj;
533
+ const out = {};
534
+ for (const [k, v] of Object.entries(obj)) {
535
+ if (k === '$mid' || k === 'external' || k === 'scheme') continue; // skip internal URI fields
536
+ if (k === 'fsPath' || k === 'path') { out['file'] = v.replace(/.*\//, ''); continue; }
537
+ if (v && typeof v === 'object' && ('fsPath' in v || '$mid' in v)) {
538
+ out[k] = simplifyUri(v);
539
+ } else if (Array.isArray(v) && k === 'edits') {
540
+ out['edits'] = `${v.length} edit(s)`;
541
+ } else {
542
+ out[k] = v;
543
+ }
544
+ }
545
+ return out;
546
+ };
547
+ if (input && typeof input === 'object' && !input.description) {
548
+ input = simplifyInput(input);
549
+ }
550
+
373
551
  return {
374
552
  type: 'tool_use',
375
553
  id: item.toolCallId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaolei81/copilot-session-viewer",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "description": "Web UI for viewing GitHub Copilot CLI session logs",
5
5
  "author": "Lei Qiao <qiaolei81@gmail.com>",
6
6
  "license": "MIT",