@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.
Files changed (45) hide show
  1. package/README.md +3 -3
  2. package/bin/copilot-session-viewer +2 -2
  3. package/dist/server.min.js +99 -0
  4. package/package.json +5 -17
  5. package/public/js/homepage.min.js +9 -9
  6. package/public/js/session-detail.min.js +36 -7
  7. package/public/vendor/marked.umd.min.js +8 -0
  8. package/public/vendor/purify.min.js +3 -0
  9. package/public/vendor/vue-virtual-scroller.css +1 -0
  10. package/public/vendor/vue-virtual-scroller.min.js +2 -0
  11. package/public/vendor/vue.global.prod.min.js +19 -0
  12. package/views/session-vue.ejs +31 -6
  13. package/views/time-analyze.ejs +2 -2
  14. package/lib/parsers/README.md +0 -239
  15. package/lib/parsers/base-parser.js +0 -53
  16. package/lib/parsers/claude-parser.js +0 -181
  17. package/lib/parsers/copilot-parser.js +0 -143
  18. package/lib/parsers/index.js +0 -15
  19. package/lib/parsers/parser-factory.js +0 -77
  20. package/lib/parsers/pi-mono-parser.js +0 -119
  21. package/lib/parsers/vscode-parser.js +0 -591
  22. package/server.js +0 -29
  23. package/src/app.js +0 -129
  24. package/src/config/index.js +0 -27
  25. package/src/controllers/insightController.js +0 -136
  26. package/src/controllers/sessionController.js +0 -449
  27. package/src/controllers/tagController.js +0 -113
  28. package/src/controllers/uploadController.js +0 -648
  29. package/src/middleware/common.js +0 -67
  30. package/src/middleware/rateLimiting.js +0 -62
  31. package/src/models/Session.js +0 -146
  32. package/src/routes/api.js +0 -11
  33. package/src/routes/insights.js +0 -12
  34. package/src/routes/pages.js +0 -12
  35. package/src/routes/uploads.js +0 -14
  36. package/src/schemas/event.schema.js +0 -73
  37. package/src/services/eventNormalizer.js +0 -291
  38. package/src/services/insightService.js +0 -535
  39. package/src/services/sessionRepository.js +0 -1092
  40. package/src/services/sessionService.js +0 -1919
  41. package/src/services/tagService.js +0 -205
  42. package/src/telemetry.js +0 -152
  43. package/src/utils/fileUtils.js +0 -305
  44. package/src/utils/helpers.js +0 -45
  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;