@qiaolei81/copilot-session-viewer 0.2.7 → 0.3.0
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 +39 -0
- package/lib/parsers/index.js +2 -2
- package/lib/parsers/vscode-parser.js +272 -100
- package/package.json +1 -1
- package/src/controllers/sessionController.js +19 -1
- package/src/models/Session.js +2 -2
- package/src/services/insightService.js +6 -0
- package/src/services/sessionRepository.js +77 -28
- package/src/services/sessionService.js +180 -21
- 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 +296 -19
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ 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.0] - 2026-03-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **VSCode Copilot Chat Support** - Full support for VSCode Copilot Chat sessions as a new source (`vscode`)
|
|
12
|
+
- Session cards show model badge + repo basename
|
|
13
|
+
- Copilot Chat extension version badge
|
|
14
|
+
- WIP badge for active sessions
|
|
15
|
+
- SubAgent name badges on assistant messages (replaces generic ASSISTANT badge)
|
|
16
|
+
- UserReq rows in Gantt timeline
|
|
17
|
+
- Turn divider shows start time + duration
|
|
18
|
+
- **Dynamic Source Path Hints** - Clicking filter pills shows the source directory path (cross-platform)
|
|
19
|
+
- **Multi-Tool Branding** - Homepage wording updated to reflect multi-tool support (Copilot CLI, Claude Code, Pi-Mono, Copilot Chat)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Source Display Names** - `copilot` → "Copilot CLI", `vscode` → "Copilot Chat"
|
|
23
|
+
- **Session Info Layout** - Shows Model + Repo basename instead of CWD hash path
|
|
24
|
+
- **Tool Input Display** - URI objects simplified to filename only; edits collapsed to count
|
|
25
|
+
- **Tool Call Width** - Truncation increased to 200 chars with flex-wrap
|
|
26
|
+
- **SubAgent Badge** - Shows name only (no emoji prefix)
|
|
27
|
+
- **System Messages** - System-sourced `user.message` converted to `system.notification` type with SYSTEM badge
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Claude `tool_result` user messages filtered from display
|
|
31
|
+
- VSCode parser: `resultDetails` iteration crash, request timestamps for subagent events
|
|
32
|
+
- VSCode subagent dedup by `subAgentId` instead of name
|
|
33
|
+
- VSCode `findById` uses `selectedModel` + `resolveWorkspacePath`
|
|
34
|
+
- VSCode duration estimation fix
|
|
35
|
+
- Session detail repo dedup, skill table width
|
|
36
|
+
- SESSION INFO duplicate Model/label
|
|
37
|
+
- SubAgent badge uses `agentName` from `toolSpecificData`
|
|
38
|
+
- Source path hints from server (cross-platform Windows/macOS/Linux)
|
|
39
|
+
|
|
40
|
+
## [0.2.7] - 2026-03-05
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **Session Tagging** - Add, remove, and filter sessions by custom tags from the session list and detail pages
|
|
44
|
+
- **Unit Tests for Tagging** - 70 new tests covering `tagService` and `tagController` (608 total)
|
|
45
|
+
- **E2E Tests for Tagging** - Playwright tests covering tagging API and UI flows
|
|
46
|
+
|
|
8
47
|
## [0.2.6] - 2026-03-05
|
|
9
48
|
|
|
10
49
|
### Fixed
|
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
|
};
|
|
@@ -230,9 +354,15 @@ class VsCodeParser extends BaseSessionParser {
|
|
|
230
354
|
|
|
231
355
|
case 'toolInvocationSerialized': {
|
|
232
356
|
flushText();
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
357
|
+
// runSubagent items: toolId='runSubagent', toolCallId=subagent-id, subAgentInvocationId=null
|
|
358
|
+
// Regular tool items: toolId=e.g. 'copilot_readFile', subAgentInvocationId=owning-subagent-id
|
|
359
|
+
const sid = item.toolId === 'runSubagent'
|
|
360
|
+
? item.toolCallId // the subagent being launched
|
|
361
|
+
: item.subAgentInvocationId; // the subagent that launched this tool
|
|
362
|
+
if (item.toolId === 'runSubagent') {
|
|
363
|
+
// Mark current context as this subagent (for subsequent tool items)
|
|
364
|
+
currentSubAgentId = sid;
|
|
365
|
+
}
|
|
236
366
|
if (sid) currentSubAgentId = sid;
|
|
237
367
|
const tool = this._normalizeTool(item);
|
|
238
368
|
if (tool) {
|
|
@@ -296,49 +426,44 @@ class VsCodeParser extends BaseSessionParser {
|
|
|
296
426
|
}
|
|
297
427
|
|
|
298
428
|
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
429
|
}
|
|
318
430
|
|
|
319
431
|
return events;
|
|
320
432
|
}
|
|
321
433
|
|
|
322
434
|
/**
|
|
323
|
-
*
|
|
324
|
-
*
|
|
435
|
+
* Build a map of subagent id → agent name from runSubagent tool invocations.
|
|
436
|
+
* runSubagent items: toolId='runSubagent', toolCallId=subagent-id
|
|
437
|
+
* Agent name is extracted from invocationMessage or resultDetails.
|
|
325
438
|
*/
|
|
326
439
|
_buildSubAgentNameMap(items) {
|
|
327
440
|
const nameMap = {};
|
|
328
441
|
for (const item of items) {
|
|
329
442
|
if (!item || typeof item !== 'object') continue;
|
|
330
|
-
const sid = item.subAgentInvocationId;
|
|
331
|
-
if (!sid || nameMap[sid]) continue;
|
|
332
443
|
if (item.kind !== 'toolInvocationSerialized') continue;
|
|
444
|
+
if (item.toolId !== 'runSubagent') continue;
|
|
445
|
+
|
|
446
|
+
const sid = item.toolCallId;
|
|
447
|
+
if (!sid || nameMap[sid]) continue;
|
|
448
|
+
|
|
449
|
+
// Prefer agentName from toolSpecificData (e.g. "FoundationAgent")
|
|
450
|
+
const agentName = item.toolSpecificData?.agentName;
|
|
451
|
+
if (agentName) { nameMap[sid] = agentName; continue; }
|
|
333
452
|
|
|
334
|
-
//
|
|
453
|
+
// Fallback: Use invocationMessage as agent display name
|
|
335
454
|
const msgObj = item.invocationMessage;
|
|
336
|
-
const msg =
|
|
455
|
+
const msg = typeof msgObj === 'string' ? msgObj
|
|
456
|
+
: (msgObj && typeof msgObj === 'object') ? (msgObj.value || '') : '';
|
|
457
|
+
if (msg) { nameMap[sid] = msg; continue; }
|
|
458
|
+
|
|
459
|
+
// Fallback: try agent file path in message
|
|
337
460
|
let m = msg.match(/agents\/([^/\]]+?)\.agent\.md/);
|
|
338
461
|
if (m) { nameMap[sid] = m[1]; continue; }
|
|
339
462
|
|
|
340
463
|
// Try resultDetails
|
|
341
|
-
|
|
464
|
+
const resultDetails = item.resultDetails;
|
|
465
|
+
const rdList = Array.isArray(resultDetails) ? resultDetails : (resultDetails ? [resultDetails] : []);
|
|
466
|
+
for (const rd of rdList) {
|
|
342
467
|
if (typeof rd !== 'object') continue;
|
|
343
468
|
const fp = rd.fsPath || rd.path || '';
|
|
344
469
|
m = fp.match(/agents\/([^/]+?)\.agent\.md/);
|
|
@@ -366,10 +491,57 @@ class VsCodeParser extends BaseSessionParser {
|
|
|
366
491
|
|
|
367
492
|
// toolSpecificData may hold input/output depending on tool type
|
|
368
493
|
const tsd = item.toolSpecificData || {};
|
|
369
|
-
|
|
370
|
-
|
|
494
|
+
let input = tsd.input || tsd.parameters || tsd.request || {};
|
|
495
|
+
let result = tsd.output || tsd.result || null;
|
|
371
496
|
const isError = item.isConfirmed === false;
|
|
372
497
|
|
|
498
|
+
// vscode tools don't serialize input/result into toolSpecificData.
|
|
499
|
+
// Use invocationMessage as a human-readable description of what the tool did,
|
|
500
|
+
// and generatedTitle / resultDetails as the result summary.
|
|
501
|
+
if (!result && (item.generatedTitle || item.resultDetails)) {
|
|
502
|
+
// resultDetails is an array of URIs (e.g. files found/read)
|
|
503
|
+
if (item.resultDetails) {
|
|
504
|
+
const rdList = Array.isArray(item.resultDetails) ? item.resultDetails : [item.resultDetails];
|
|
505
|
+
const paths = rdList.map(rd => rd.fsPath || rd.path || rd.external || JSON.stringify(rd)).filter(Boolean);
|
|
506
|
+
result = paths.length > 0 ? paths.join('\n') : item.generatedTitle || null;
|
|
507
|
+
} else {
|
|
508
|
+
result = item.generatedTitle || null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// Use invocationMessage (plain text) as input description if input is empty
|
|
512
|
+
if (Object.keys(input).length === 0 && item.invocationMessage) {
|
|
513
|
+
const msg = item.invocationMessage;
|
|
514
|
+
const msgText = typeof msg === 'string' ? msg
|
|
515
|
+
: (msg && typeof msg === 'object') ? (msg.value || '') : '';
|
|
516
|
+
if (msgText) input = { description: msgText };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Simplify URI objects: replace {$mid, fsPath, external, path, scheme} with just the filename
|
|
520
|
+
const simplifyUri = (uri) => {
|
|
521
|
+
if (!uri || typeof uri !== 'object') return uri;
|
|
522
|
+
const p = uri.fsPath || uri.path || uri.external || '';
|
|
523
|
+
return p ? p.replace(/.*\//, '') : JSON.stringify(uri);
|
|
524
|
+
};
|
|
525
|
+
const simplifyInput = (obj) => {
|
|
526
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
527
|
+
const out = {};
|
|
528
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
529
|
+
if (k === '$mid' || k === 'external' || k === 'scheme') continue; // skip internal URI fields
|
|
530
|
+
if (k === 'fsPath' || k === 'path') { out['file'] = v.replace(/.*\//, ''); continue; }
|
|
531
|
+
if (v && typeof v === 'object' && ('fsPath' in v || '$mid' in v)) {
|
|
532
|
+
out[k] = simplifyUri(v);
|
|
533
|
+
} else if (Array.isArray(v) && k === 'edits') {
|
|
534
|
+
out['edits'] = `${v.length} edit(s)`;
|
|
535
|
+
} else {
|
|
536
|
+
out[k] = v;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
};
|
|
541
|
+
if (input && typeof input === 'object' && !input.description) {
|
|
542
|
+
input = simplifyInput(input);
|
|
543
|
+
}
|
|
544
|
+
|
|
373
545
|
return {
|
|
374
546
|
type: 'tool_use',
|
|
375
547
|
id: item.toolCallId,
|
package/package.json
CHANGED
|
@@ -15,11 +15,29 @@ class SessionController {
|
|
|
15
15
|
// Only load default pill (copilot) first 20 sessions
|
|
16
16
|
const paginationData = await this.sessionService.getPaginatedSessions(1, 20, 'copilot');
|
|
17
17
|
|
|
18
|
+
// Build source path hints from repository config
|
|
19
|
+
const sourceHints = {};
|
|
20
|
+
if (this.sessionService.sessionRepository && this.sessionService.sessionRepository.sources) {
|
|
21
|
+
for (const src of this.sessionService.sessionRepository.sources) {
|
|
22
|
+
// Replace home dir with ~ for display
|
|
23
|
+
const home = require('os').homedir();
|
|
24
|
+
let displayPath = src.dir;
|
|
25
|
+
if (displayPath.startsWith(home)) {
|
|
26
|
+
displayPath = '~' + displayPath.slice(home.length);
|
|
27
|
+
}
|
|
28
|
+
// Normalize path separators for display
|
|
29
|
+
displayPath = displayPath.replace(/\\/g, '/');
|
|
30
|
+
if (!displayPath.endsWith('/')) displayPath += '/';
|
|
31
|
+
sourceHints[src.type] = displayPath;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
// Pass data for infinite scroll
|
|
19
36
|
const templateData = {
|
|
20
37
|
sessions: paginationData.sessions,
|
|
21
38
|
hasMore: paginationData.hasNextPage,
|
|
22
|
-
totalSessions: paginationData.totalSessions
|
|
39
|
+
totalSessions: paginationData.totalSessions,
|
|
40
|
+
sourceHints: JSON.stringify(sourceHints)
|
|
23
41
|
};
|
|
24
42
|
|
|
25
43
|
res.render('index', templateData);
|
package/src/models/Session.js
CHANGED
|
@@ -123,10 +123,10 @@ class Session {
|
|
|
123
123
|
*/
|
|
124
124
|
_getSourceDisplayMetadata(source) {
|
|
125
125
|
const metadata = {
|
|
126
|
-
'copilot': { name: 'Copilot', badgeClass: 'source-copilot' },
|
|
126
|
+
'copilot': { name: 'Copilot CLI', badgeClass: 'source-copilot' },
|
|
127
127
|
'claude': { name: 'Claude', badgeClass: 'source-claude' },
|
|
128
128
|
'pi-mono': { name: 'Pi', badgeClass: 'source-pi-mono' },
|
|
129
|
-
'vscode': { name: '
|
|
129
|
+
'vscode': { name: 'Copilot Chat', badgeClass: 'source-vscode' }
|
|
130
130
|
};
|
|
131
131
|
return metadata[source] || { name: source, badgeClass: 'source-unknown' };
|
|
132
132
|
}
|
|
@@ -28,6 +28,12 @@ class InsightService {
|
|
|
28
28
|
args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
|
|
29
29
|
cwd: sessionPath
|
|
30
30
|
},
|
|
31
|
+
vscode: {
|
|
32
|
+
name: 'Copilot',
|
|
33
|
+
cli: 'copilot',
|
|
34
|
+
args: (tmpDir, prompt) => ['--config-dir', tmpDir, '--yolo', '-p', prompt],
|
|
35
|
+
cwd: sessionPath
|
|
36
|
+
},
|
|
31
37
|
claude: {
|
|
32
38
|
name: 'Claude Code',
|
|
33
39
|
cli: 'claude',
|