@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.6
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/README.md +3 -3
- package/bin/copilot-session-viewer +2 -2
- package/dist/server.min.js +99 -0
- package/package.json +5 -17
- package/public/js/homepage.min.js +9 -9
- package/public/js/session-detail.min.js +36 -7
- package/public/vendor/marked.umd.min.js +8 -0
- package/public/vendor/purify.min.js +3 -0
- package/public/vendor/vue-virtual-scroller.css +1 -0
- package/public/vendor/vue-virtual-scroller.min.js +2 -0
- package/public/vendor/vue.global.prod.min.js +19 -0
- package/views/session-vue.ejs +31 -6
- package/views/time-analyze.ejs +2 -2
- package/lib/parsers/README.md +0 -239
- package/lib/parsers/base-parser.js +0 -53
- package/lib/parsers/claude-parser.js +0 -181
- package/lib/parsers/copilot-parser.js +0 -143
- package/lib/parsers/index.js +0 -15
- package/lib/parsers/parser-factory.js +0 -77
- package/lib/parsers/pi-mono-parser.js +0 -119
- package/lib/parsers/vscode-parser.js +0 -591
- package/server.js +0 -29
- package/src/app.js +0 -129
- package/src/config/index.js +0 -27
- package/src/controllers/insightController.js +0 -136
- package/src/controllers/sessionController.js +0 -449
- package/src/controllers/tagController.js +0 -113
- package/src/controllers/uploadController.js +0 -648
- package/src/middleware/common.js +0 -67
- package/src/middleware/rateLimiting.js +0 -62
- package/src/models/Session.js +0 -146
- package/src/routes/api.js +0 -11
- package/src/routes/insights.js +0 -12
- package/src/routes/pages.js +0 -12
- package/src/routes/uploads.js +0 -14
- package/src/schemas/event.schema.js +0 -73
- package/src/services/eventNormalizer.js +0 -291
- package/src/services/insightService.js +0 -535
- package/src/services/sessionRepository.js +0 -1092
- package/src/services/sessionService.js +0 -1919
- package/src/services/tagService.js +0 -205
- package/src/telemetry.js +0 -152
- package/src/utils/fileUtils.js +0 -305
- package/src/utils/helpers.js +0 -45
- package/src/utils/processManager.js +0 -85
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi-Mono session parser
|
|
3
|
-
* Parses ~/.pi/agent/sessions/ format
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const BaseParser = require('./base-parser');
|
|
7
|
-
|
|
8
|
-
class PiMonoParser extends BaseParser {
|
|
9
|
-
constructor() {
|
|
10
|
-
super('pi-mono');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Parse Pi-Mono session directory
|
|
15
|
-
* @param {string} sessionDir - e.g., ~/.pi/agent/sessions/--project-path--/
|
|
16
|
-
* @returns {Object|null} - Session metadata or null if invalid
|
|
17
|
-
*/
|
|
18
|
-
async parseSessionDir(sessionDir) {
|
|
19
|
-
const fs = require('fs').promises;
|
|
20
|
-
const path = require('path');
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
// List .jsonl files in directory
|
|
24
|
-
const entries = await fs.readdir(sessionDir);
|
|
25
|
-
const jsonlFiles = entries.filter(f => f.endsWith('.jsonl'));
|
|
26
|
-
|
|
27
|
-
if (jsonlFiles.length === 0) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Use the latest file for metadata
|
|
32
|
-
jsonlFiles.sort().reverse();
|
|
33
|
-
const latestFile = path.join(sessionDir, jsonlFiles[0]);
|
|
34
|
-
const firstLine = await this._readFirstLine(latestFile);
|
|
35
|
-
|
|
36
|
-
if (!firstLine) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const sessionEvent = JSON.parse(firstLine);
|
|
41
|
-
|
|
42
|
-
if (sessionEvent.type !== 'session') {
|
|
43
|
-
console.warn(`Pi-Mono file ${latestFile} doesn't start with session event`);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Extract project name from directory name
|
|
48
|
-
const dirName = path.basename(sessionDir);
|
|
49
|
-
const projectPath = dirName.replace(/^--/, '').replace(/--$/, '');
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
id: sessionEvent.id,
|
|
53
|
-
type: 'pi-mono',
|
|
54
|
-
source: 'pi-mono',
|
|
55
|
-
cwd: sessionEvent.cwd || projectPath,
|
|
56
|
-
createdAt: new Date(sessionEvent.timestamp),
|
|
57
|
-
updatedAt: new Date(sessionEvent.timestamp), // Will be updated when scanning all files
|
|
58
|
-
summary: `Pi-Mono: ${projectPath}`,
|
|
59
|
-
fileCount: jsonlFiles.length
|
|
60
|
-
};
|
|
61
|
-
} catch (err) {
|
|
62
|
-
console.error(`Error parsing Pi-Mono session dir ${sessionDir}:`, err.message);
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Read first line of a file
|
|
69
|
-
*/
|
|
70
|
-
async _readFirstLine(filePath) {
|
|
71
|
-
const fs = require('fs');
|
|
72
|
-
const readline = require('readline');
|
|
73
|
-
|
|
74
|
-
return new Promise((resolve) => {
|
|
75
|
-
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
76
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
77
|
-
|
|
78
|
-
rl.on('line', (line) => {
|
|
79
|
-
rl.close();
|
|
80
|
-
stream.destroy();
|
|
81
|
-
resolve(line.trim());
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
rl.on('close', () => resolve(null));
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Parse Pi-Mono events from .jsonl file
|
|
90
|
-
* @param {string} filePath
|
|
91
|
-
* @returns {Array} - Array of parsed events
|
|
92
|
-
*/
|
|
93
|
-
async parseEvents(filePath) {
|
|
94
|
-
const fs = require('fs');
|
|
95
|
-
const readline = require('readline');
|
|
96
|
-
|
|
97
|
-
const events = [];
|
|
98
|
-
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
99
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
100
|
-
|
|
101
|
-
let lineIndex = 0;
|
|
102
|
-
for await (const line of rl) {
|
|
103
|
-
lineIndex++;
|
|
104
|
-
const trimmed = line.trim();
|
|
105
|
-
if (!trimmed) continue;
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const event = JSON.parse(trimmed);
|
|
109
|
-
events.push(event);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
console.error(`Error parsing Pi-Mono line ${lineIndex}:`, err.message);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return events;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
module.exports = PiMonoParser;
|
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
const BaseSessionParser = require('./base-parser');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* VSCode Copilot Chat Session Parser
|
|
5
|
-
*
|
|
6
|
-
* Parses chat sessions stored by VS Code's GitHub Copilot Chat extension.
|
|
7
|
-
* Location: ~/Library/Application Support/Code/User/workspaceStorage/<hash>/chatSessions/<uuid>.json
|
|
8
|
-
*
|
|
9
|
-
* Format: A single JSON object with a `requests` array. Each request has
|
|
10
|
-
* a `message` (user input) and a `response` array of typed content items.
|
|
11
|
-
*/
|
|
12
|
-
class VsCodeParser extends BaseSessionParser {
|
|
13
|
-
/**
|
|
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
|
|
19
|
-
*/
|
|
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
|
-
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse a VSCode chat session JSON object into the normalised format.
|
|
39
|
-
* @param {Object} sessionJson - Parsed JSON from chatSessions/<uuid>.json
|
|
40
|
-
* @returns {Object} Normalised session data
|
|
41
|
-
*/
|
|
42
|
-
parseVsCode(sessionJson) {
|
|
43
|
-
const metadata = this._getMetadata(sessionJson);
|
|
44
|
-
const events = this._toEvents(sessionJson);
|
|
45
|
-
return {
|
|
46
|
-
metadata,
|
|
47
|
-
turns: this._extractTurns(events),
|
|
48
|
-
toolCalls: this._extractToolCalls(events),
|
|
49
|
-
allEvents: events,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
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
|
-
}
|
|
219
|
-
|
|
220
|
-
// ---- private helpers ----
|
|
221
|
-
|
|
222
|
-
_getMetadata(sessionJson) {
|
|
223
|
-
const requests = sessionJson.requests || [];
|
|
224
|
-
const firstReq = requests[0] || {};
|
|
225
|
-
const lastReq = requests[requests.length - 1] || {};
|
|
226
|
-
|
|
227
|
-
// Derive workspace label from agent name or fallback
|
|
228
|
-
const agentName = firstReq.agent?.name || firstReq.agent?.id || 'vscode-copilot';
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
sessionId: sessionJson.sessionId,
|
|
232
|
-
startTime: sessionJson.creationDate
|
|
233
|
-
? new Date(sessionJson.creationDate).toISOString()
|
|
234
|
-
: (firstReq.timestamp ? new Date(firstReq.timestamp).toISOString() : null),
|
|
235
|
-
endTime: sessionJson.lastMessageDate
|
|
236
|
-
? new Date(sessionJson.lastMessageDate).toISOString()
|
|
237
|
-
: (lastReq.timestamp ? new Date(lastReq.timestamp).toISOString() : null),
|
|
238
|
-
model: firstReq.modelId || null,
|
|
239
|
-
producer: 'vscode-copilot-chat',
|
|
240
|
-
version: firstReq.agent?.extensionVersion || null,
|
|
241
|
-
agentName,
|
|
242
|
-
requestCount: requests.length,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Convert VSCode session JSON into a flat event array that matches the
|
|
248
|
-
* normalised event schema used by the rest of the viewer.
|
|
249
|
-
*/
|
|
250
|
-
_toEvents(sessionJson) {
|
|
251
|
-
const events = [];
|
|
252
|
-
const requests = sessionJson.requests || [];
|
|
253
|
-
|
|
254
|
-
// session.start synthetic event
|
|
255
|
-
events.push({
|
|
256
|
-
type: 'session.start',
|
|
257
|
-
id: `${sessionJson.sessionId}-start`,
|
|
258
|
-
timestamp: sessionJson.creationDate
|
|
259
|
-
? new Date(sessionJson.creationDate).toISOString()
|
|
260
|
-
: null,
|
|
261
|
-
data: {
|
|
262
|
-
sessionId: sessionJson.sessionId,
|
|
263
|
-
producer: 'vscode-copilot-chat',
|
|
264
|
-
selectedModel: requests[0]?.modelId || null,
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
for (const req of requests) {
|
|
269
|
-
const ts = req.timestamp ? new Date(req.timestamp).toISOString() : null;
|
|
270
|
-
// Use completedAt for assistant events — more accurate than request start time
|
|
271
|
-
const completedAt = req.modelState?.completedAt
|
|
272
|
-
? new Date(req.modelState.completedAt).toISOString()
|
|
273
|
-
: ts;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Build subAgentInvocationId → agent name map from this request's response items
|
|
277
|
-
const responseItems = Array.isArray(req.response) ? req.response : [];
|
|
278
|
-
const subAgentNames = this._buildSubAgentNameMap(responseItems);
|
|
279
|
-
|
|
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.
|
|
284
|
-
|
|
285
|
-
// user.message
|
|
286
|
-
const userText = this._extractUserText(req.message);
|
|
287
|
-
events.push({
|
|
288
|
-
type: 'user.message',
|
|
289
|
-
id: req.requestId,
|
|
290
|
-
timestamp: ts,
|
|
291
|
-
data: {
|
|
292
|
-
message: userText,
|
|
293
|
-
content: userText,
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// assistant.turn_start
|
|
298
|
-
events.push({
|
|
299
|
-
type: 'assistant.turn_start',
|
|
300
|
-
id: `${req.requestId}-turn`,
|
|
301
|
-
timestamp: ts,
|
|
302
|
-
parentId: req.requestId,
|
|
303
|
-
data: {},
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
let assistantText = '';
|
|
307
|
-
let itemIndex = 0;
|
|
308
|
-
let currentSubAgentId = null;
|
|
309
|
-
|
|
310
|
-
const flushText = () => {
|
|
311
|
-
const trimmed = assistantText.trim().replace(/^`{3,}$/gm, '').trim();
|
|
312
|
-
assistantText = '';
|
|
313
|
-
if (!trimmed) return;
|
|
314
|
-
const sid = currentSubAgentId;
|
|
315
|
-
const agentName = sid ? (subAgentNames[sid] || sid.slice(0, 8)) : null;
|
|
316
|
-
events.push({
|
|
317
|
-
type: 'assistant.message',
|
|
318
|
-
id: `${req.requestId}-text-${itemIndex}`,
|
|
319
|
-
timestamp: completedAt,
|
|
320
|
-
parentId: req.requestId,
|
|
321
|
-
data: {
|
|
322
|
-
message: trimmed,
|
|
323
|
-
content: trimmed,
|
|
324
|
-
tools: [],
|
|
325
|
-
subAgentId: sid || null,
|
|
326
|
-
subAgentName: agentName,
|
|
327
|
-
parentToolCallId: null,
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
for (const item of responseItems) {
|
|
333
|
-
itemIndex++;
|
|
334
|
-
switch (item.kind) {
|
|
335
|
-
case 'thinking': {
|
|
336
|
-
const text = item.content?.value || item.content || '';
|
|
337
|
-
if (text) assistantText += text + '\n';
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
case 'markdownContent': {
|
|
342
|
-
const text = item.content?.value || item.content || '';
|
|
343
|
-
if (text) assistantText += text + '\n';
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
case undefined:
|
|
348
|
-
case null: {
|
|
349
|
-
// Plain markdown text item (no kind field, has 'value')
|
|
350
|
-
const text = item.value || '';
|
|
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 + '`';
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
case 'toolInvocationSerialized': {
|
|
363
|
-
flushText();
|
|
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
|
-
}
|
|
373
|
-
if (sid) currentSubAgentId = sid;
|
|
374
|
-
const tool = this._normalizeTool(item);
|
|
375
|
-
if (tool) {
|
|
376
|
-
const agentName = sid ? (subAgentNames[sid] || sid.slice(0, 8)) : null;
|
|
377
|
-
events.push({
|
|
378
|
-
type: 'tool.invocation',
|
|
379
|
-
id: tool.id || `${req.requestId}-tool-${itemIndex}`,
|
|
380
|
-
timestamp: completedAt,
|
|
381
|
-
parentId: req.requestId,
|
|
382
|
-
data: {
|
|
383
|
-
tool,
|
|
384
|
-
subAgentId: sid || null,
|
|
385
|
-
subAgentName: agentName,
|
|
386
|
-
parentToolCallId: sid || null,
|
|
387
|
-
badgeLabel: tool.name,
|
|
388
|
-
badgeClass: tool.status === 'error' ? 'badge-error' : 'badge-tool',
|
|
389
|
-
},
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
break;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
case 'textEditGroup': {
|
|
396
|
-
flushText();
|
|
397
|
-
const edits = item.edits || item.uri ? [item] : [];
|
|
398
|
-
events.push({
|
|
399
|
-
type: 'tool.invocation',
|
|
400
|
-
id: `${req.requestId}-edit-${itemIndex}`,
|
|
401
|
-
timestamp: completedAt,
|
|
402
|
-
parentId: req.requestId,
|
|
403
|
-
data: {
|
|
404
|
-
tool: {
|
|
405
|
-
type: 'tool_use',
|
|
406
|
-
id: `${req.requestId}-edit-${itemIndex}`,
|
|
407
|
-
name: 'textEdit',
|
|
408
|
-
startTime: ts,
|
|
409
|
-
endTime: ts,
|
|
410
|
-
status: 'completed',
|
|
411
|
-
input: { uri: item.uri, edits },
|
|
412
|
-
result: 'file edit',
|
|
413
|
-
error: null,
|
|
414
|
-
},
|
|
415
|
-
badgeLabel: 'textEdit',
|
|
416
|
-
badgeClass: 'badge-tool',
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
case 'prepareToolInvocation':
|
|
423
|
-
case 'undoStop':
|
|
424
|
-
case 'codeblockUri':
|
|
425
|
-
case 'mcpServersStarting':
|
|
426
|
-
// Skip non-visible items
|
|
427
|
-
break;
|
|
428
|
-
|
|
429
|
-
default:
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
flushText(); // flush any trailing text
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return events;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
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.
|
|
444
|
-
*/
|
|
445
|
-
_buildSubAgentNameMap(items) {
|
|
446
|
-
const nameMap = {};
|
|
447
|
-
for (const item of items) {
|
|
448
|
-
if (!item || typeof item !== 'object') continue;
|
|
449
|
-
if (item.kind !== 'toolInvocationSerialized') continue;
|
|
450
|
-
if (item.toolId !== 'runSubagent') continue;
|
|
451
|
-
|
|
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
|
|
460
|
-
const msgObj = item.invocationMessage;
|
|
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
|
|
466
|
-
let m = msg.match(/agents\/([^/\]]+?)\.agent\.md/);
|
|
467
|
-
if (m) { nameMap[sid] = m[1]; continue; }
|
|
468
|
-
|
|
469
|
-
// Try resultDetails
|
|
470
|
-
const resultDetails = item.resultDetails;
|
|
471
|
-
const rdList = Array.isArray(resultDetails) ? resultDetails : (resultDetails ? [resultDetails] : []);
|
|
472
|
-
for (const rd of rdList) {
|
|
473
|
-
if (typeof rd !== 'object') continue;
|
|
474
|
-
const fp = rd.fsPath || rd.path || '';
|
|
475
|
-
m = fp.match(/agents\/([^/]+?)\.agent\.md/);
|
|
476
|
-
if (m) { nameMap[sid] = m[1]; break; }
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return nameMap;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
_extractUserText(message) {
|
|
483
|
-
if (!message) return '';
|
|
484
|
-
if (typeof message.text === 'string') return message.text;
|
|
485
|
-
// parts[] may contain text fragments
|
|
486
|
-
if (Array.isArray(message.parts)) {
|
|
487
|
-
return message.parts
|
|
488
|
-
.filter(p => p.kind === 'text')
|
|
489
|
-
.map(p => p.text || '')
|
|
490
|
-
.join('');
|
|
491
|
-
}
|
|
492
|
-
return '';
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
_normalizeTool(item) {
|
|
496
|
-
if (!item.toolCallId) return null;
|
|
497
|
-
|
|
498
|
-
// toolSpecificData may hold input/output depending on tool type
|
|
499
|
-
const tsd = item.toolSpecificData || {};
|
|
500
|
-
let input = tsd.input || tsd.parameters || tsd.request || {};
|
|
501
|
-
let result = tsd.output || tsd.result || null;
|
|
502
|
-
const isError = item.isConfirmed === false;
|
|
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
|
-
|
|
551
|
-
return {
|
|
552
|
-
type: 'tool_use',
|
|
553
|
-
id: item.toolCallId,
|
|
554
|
-
name: item.toolId || 'unknown',
|
|
555
|
-
startTime: null,
|
|
556
|
-
endTime: null,
|
|
557
|
-
status: isError ? 'error' : (item.isComplete ? 'completed' : 'pending'),
|
|
558
|
-
input,
|
|
559
|
-
result: typeof result === 'string' ? result : JSON.stringify(result),
|
|
560
|
-
error: isError ? (item.resultDetails || 'Tool invocation not confirmed') : null,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
_extractTurns(events) {
|
|
565
|
-
const turns = [];
|
|
566
|
-
let current = null;
|
|
567
|
-
|
|
568
|
-
for (const event of events) {
|
|
569
|
-
if (event.type === 'user.message') {
|
|
570
|
-
if (current) turns.push(current);
|
|
571
|
-
current = { userMessage: event, assistantMessages: [], toolCalls: [] };
|
|
572
|
-
} else if (current) {
|
|
573
|
-
if (event.type === 'assistant.message') {
|
|
574
|
-
current.assistantMessages.push(event);
|
|
575
|
-
} else if (event.type === 'tool.invocation') {
|
|
576
|
-
current.toolCalls.push(event.data?.tool);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (current) turns.push(current);
|
|
581
|
-
return turns;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
_extractToolCalls(events) {
|
|
585
|
-
return events
|
|
586
|
-
.filter(e => e.type === 'tool.invocation' && e.data?.tool)
|
|
587
|
-
.map(e => e.data.tool);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
module.exports = VsCodeParser;
|