@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 +53 -0
- package/README.md +20 -17
- package/lib/parsers/index.js +2 -2
- package/lib/parsers/vscode-parser.js +280 -102
- package/package.json +1 -1
- package/src/controllers/sessionController.js +88 -46
- package/src/models/Session.js +3 -2
- package/src/services/insightService.js +16 -9
- package/src/services/sessionRepository.js +145 -31
- package/src/services/sessionService.js +180 -21
- package/src/services/tagService.js +13 -3
- package/src/utils/helpers.js +4 -1
- package/views/index.ejs +24 -12
- package/views/session-vue.ejs +70 -13
- package/views/time-analyze.ejs +310 -19
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
|
[](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
|

|
|
@@ -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
|
-
- **
|
|
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** -
|
|
157
|
+
2. **Unit Tests** - 622 Jest tests with coverage
|
|
157
158
|
3. **Mock Data Generation** - Reproducible test session fixtures
|
|
158
|
-
4. **E2E Tests** -
|
|
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
|
-
│ •
|
|
192
|
-
│ •
|
|
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.
|
|
308
|
-
- ✨
|
|
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
|
-
-
|
|
312
|
-
- 🧪 470+ tests with CI/CD integration
|
|
315
|
+
- 🧪 700+ tests with CI/CD integration
|
|
313
316
|
- 📚 Comprehensive documentation
|
|
314
317
|
|
|
315
318
|
---
|
package/lib/parsers/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
for
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
*
|
|
324
|
-
*
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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,
|