@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
|
@@ -20,31 +20,30 @@ class SessionRepository {
|
|
|
20
20
|
} else if (Array.isArray(sessionDirs)) {
|
|
21
21
|
this.sources = sessionDirs;
|
|
22
22
|
} else {
|
|
23
|
-
// Default: Copilot + Claude + Pi-Mono
|
|
23
|
+
// Default: Copilot + Claude + Pi-Mono + VSCode
|
|
24
24
|
// Support environment variables for each source (useful for testing/CI)
|
|
25
25
|
this.sources = [
|
|
26
26
|
{
|
|
27
27
|
type: 'copilot',
|
|
28
|
-
dir: process.env.COPILOT_SESSION_DIR ||
|
|
28
|
+
dir: process.env.COPILOT_SESSION_DIR ||
|
|
29
29
|
process.env.SESSION_DIR || // Legacy fallback
|
|
30
30
|
path.join(os.homedir(), '.copilot', 'session-state')
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
type: 'claude',
|
|
34
|
-
dir: process.env.CLAUDE_SESSION_DIR ||
|
|
34
|
+
dir: process.env.CLAUDE_SESSION_DIR ||
|
|
35
35
|
path.join(os.homedir(), '.claude', 'projects')
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
type: 'pi-mono',
|
|
39
|
-
dir: process.env.PI_MONO_SESSION_DIR ||
|
|
39
|
+
dir: process.env.PI_MONO_SESSION_DIR ||
|
|
40
40
|
path.join(os.homedir(), '.pi', 'agent', 'sessions')
|
|
41
41
|
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// }
|
|
42
|
+
{
|
|
43
|
+
type: 'vscode',
|
|
44
|
+
dir: process.env.VSCODE_WORKSPACE_STORAGE_DIR ||
|
|
45
|
+
path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage')
|
|
46
|
+
}
|
|
48
47
|
];
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -111,10 +110,11 @@ class SessionRepository {
|
|
|
111
110
|
if (stats.isDirectory()) {
|
|
112
111
|
return this._scanPiMonoDir(fullPath, entry);
|
|
113
112
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
} else if (source.type === 'vscode') {
|
|
114
|
+
// VSCode: workspace hash directories containing chatSessions/*.jsonl
|
|
115
|
+
if (stats.isDirectory()) {
|
|
116
|
+
return this._scanVsCodeWorkspaceDir(fullPath);
|
|
117
|
+
}
|
|
118
118
|
}
|
|
119
119
|
return null;
|
|
120
120
|
});
|
|
@@ -274,8 +274,8 @@ class SessionRepository {
|
|
|
274
274
|
session = await this._findClaudeSession(sessionId, source.dir);
|
|
275
275
|
} else if (source.type === 'pi-mono') {
|
|
276
276
|
session = await this._findPiMonoSession(sessionId, source.dir);
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
} else if (source.type === 'vscode') {
|
|
278
|
+
session = await this._findVsCodeSession(sessionId, source.dir);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
if (session) return session;
|
|
@@ -470,23 +470,46 @@ class SessionRepository {
|
|
|
470
470
|
const createdAt = sessionJson.creationDate
|
|
471
471
|
? new Date(sessionJson.creationDate)
|
|
472
472
|
: (firstReq.timestamp ? new Date(firstReq.timestamp) : stats.birthtime);
|
|
473
|
+
const lastReqTime2 = requests[requests.length - 1].timestamp ? new Date(requests[requests.length - 1].timestamp) : null;
|
|
473
474
|
const updatedAt = sessionJson.lastMessageDate
|
|
474
475
|
? new Date(sessionJson.lastMessageDate)
|
|
475
|
-
: stats.mtime;
|
|
476
|
+
: (lastReqTime2 || stats.mtime);
|
|
476
477
|
const userText = this._extractVsCodeUserText(firstReq.message);
|
|
477
478
|
|
|
479
|
+
const copilotChatVersion = firstReq.agent?.extensionVersion || null;
|
|
480
|
+
const realWorkspacePath = await this._resolveVsCodeWorkspacePath(path.join(workspaceStorageDir, hash));
|
|
481
|
+
|
|
482
|
+
// Same estimation logic as scan path
|
|
483
|
+
const _fileMtime2 = stats.mtime;
|
|
484
|
+
// Count tools for estimation
|
|
485
|
+
let toolCount2 = 0;
|
|
486
|
+
for (const req of requests) {
|
|
487
|
+
toolCount2 += (req.response || []).filter(r => r.kind === 'toolInvocationSerialized').length;
|
|
488
|
+
}
|
|
489
|
+
let effectiveEnd2;
|
|
490
|
+
if (toolCount2 > 10 && lastReqTime2) {
|
|
491
|
+
const estimatedDurationMs2 = Math.max(toolCount2 * 3500, 60000);
|
|
492
|
+
effectiveEnd2 = new Date(createdAt.getTime() + estimatedDurationMs2);
|
|
493
|
+
} else if (lastReqTime2) {
|
|
494
|
+
effectiveEnd2 = lastReqTime2;
|
|
495
|
+
} else {
|
|
496
|
+
effectiveEnd2 = updatedAt;
|
|
497
|
+
}
|
|
498
|
+
|
|
478
499
|
return new Session(sessionId, 'file', {
|
|
479
500
|
source: 'vscode',
|
|
480
501
|
filePath: fullPath,
|
|
502
|
+
workspaceHash: hash,
|
|
481
503
|
createdAt,
|
|
482
|
-
updatedAt,
|
|
504
|
+
updatedAt: effectiveEnd2,
|
|
483
505
|
summary: userText ? userText.slice(0, 120) : `VSCode chat (${requests.length} requests)`,
|
|
484
506
|
hasEvents: true,
|
|
485
507
|
eventCount: requests.reduce((s, r) => s + (r.response || []).length, 0) + requests.length * 2 + 1,
|
|
486
|
-
duration:
|
|
487
|
-
sessionStatus: 'completed',
|
|
488
|
-
|
|
489
|
-
|
|
508
|
+
duration: effectiveEnd2.getTime() - createdAt.getTime(),
|
|
509
|
+
sessionStatus: (Date.now() - effectiveEnd2.getTime()) < 5 * 60 * 1000 ? 'wip' : 'completed',
|
|
510
|
+
selectedModel: firstReq.modelId || null,
|
|
511
|
+
copilotVersion: copilotChatVersion,
|
|
512
|
+
workspace: { cwd: realWorkspacePath || path.join(workspaceStorageDir, hash) },
|
|
490
513
|
});
|
|
491
514
|
}
|
|
492
515
|
} catch {
|
|
@@ -845,6 +868,9 @@ class SessionRepository {
|
|
|
845
868
|
return []; // No chatSessions subfolder — skip silently
|
|
846
869
|
}
|
|
847
870
|
|
|
871
|
+
// Extract workspace hash from directory name
|
|
872
|
+
const workspaceHash = path.basename(workspaceHashDir);
|
|
873
|
+
|
|
848
874
|
// Resolve the real project path from workspace.json
|
|
849
875
|
const realWorkspacePath = await this._resolveVsCodeWorkspacePath(workspaceHashDir);
|
|
850
876
|
|
|
@@ -876,18 +902,39 @@ class SessionRepository {
|
|
|
876
902
|
const createdAt = sessionJson.creationDate
|
|
877
903
|
? new Date(sessionJson.creationDate)
|
|
878
904
|
: (firstReq.timestamp ? new Date(firstReq.timestamp) : stats.birthtime);
|
|
905
|
+
const lastReqTime = lastReq.timestamp ? new Date(lastReq.timestamp) : null;
|
|
879
906
|
const updatedAt = sessionJson.lastMessageDate
|
|
880
907
|
? new Date(sessionJson.lastMessageDate)
|
|
881
|
-
: (
|
|
908
|
+
: (lastReqTime || stats.mtime);
|
|
882
909
|
|
|
883
|
-
// Count tool invocations across all requests
|
|
910
|
+
// Count tool invocations across all requests (must be before effectiveEndTime calc)
|
|
884
911
|
let toolCount = 0;
|
|
885
912
|
for (const req of requests) {
|
|
886
913
|
toolCount += (req.response || []).filter(r => r.kind === 'toolInvocationSerialized').length;
|
|
887
914
|
}
|
|
888
915
|
|
|
916
|
+
// For VSCode agentic sessions, file mtime may be more accurate than last request timestamp
|
|
917
|
+
// because sub-agents write incremental updates over time without new request timestamps.
|
|
918
|
+
// BUT: VSCode may touch/sync all files at once, creating misleading mtimes.
|
|
919
|
+
// Strategy:
|
|
920
|
+
// - If session has many tool invocations (agentic), estimate duration from tool count
|
|
921
|
+
// - Otherwise fall back to request timestamps
|
|
922
|
+
const _fileMtime = stats.mtime;
|
|
923
|
+
let effectiveEndTime;
|
|
924
|
+
if (toolCount > 10 && lastReqTime) {
|
|
925
|
+
// Agentic session: estimate ~3.5s per tool invocation as a rough heuristic
|
|
926
|
+
const estimatedDurationMs = Math.max(toolCount * 3500, 60000); // at least 1 min
|
|
927
|
+
const estimatedEnd = new Date(createdAt.getTime() + estimatedDurationMs);
|
|
928
|
+
effectiveEndTime = estimatedEnd;
|
|
929
|
+
} else if (lastReqTime) {
|
|
930
|
+
effectiveEndTime = lastReqTime;
|
|
931
|
+
} else {
|
|
932
|
+
effectiveEndTime = updatedAt;
|
|
933
|
+
}
|
|
934
|
+
|
|
889
935
|
const model = firstReq.modelId || null;
|
|
890
936
|
const agentId = firstReq.agent?.id || 'vscode-copilot';
|
|
937
|
+
const copilotChatVersion = firstReq.agent?.extensionVersion || null;
|
|
891
938
|
const userText = this._extractVsCodeUserText(firstReq.message);
|
|
892
939
|
|
|
893
940
|
const session = new Session(
|
|
@@ -896,16 +943,18 @@ class SessionRepository {
|
|
|
896
943
|
{
|
|
897
944
|
source: 'vscode',
|
|
898
945
|
filePath: fullPath,
|
|
946
|
+
workspaceHash,
|
|
899
947
|
createdAt,
|
|
900
|
-
updatedAt,
|
|
948
|
+
updatedAt: effectiveEndTime,
|
|
901
949
|
summary: userText ? userText.slice(0, 120) : `VSCode chat (${requests.length} requests)`,
|
|
902
950
|
hasEvents: true,
|
|
903
951
|
eventCount: requests.reduce((s, r) => s + (r.response || []).length, 0) + requests.length * 2 + 1,
|
|
904
|
-
duration:
|
|
905
|
-
sessionStatus: 'completed',
|
|
906
|
-
model,
|
|
952
|
+
duration: effectiveEndTime.getTime() - createdAt.getTime(),
|
|
953
|
+
sessionStatus: (Date.now() - effectiveEndTime.getTime()) < 5 * 60 * 1000 ? 'wip' : 'completed',
|
|
954
|
+
selectedModel: model,
|
|
907
955
|
agentId,
|
|
908
956
|
toolCount,
|
|
957
|
+
copilotVersion: copilotChatVersion,
|
|
909
958
|
workspace: { cwd: realWorkspacePath || workspaceHashDir },
|
|
910
959
|
}
|
|
911
960
|
);
|
|
@@ -151,24 +151,72 @@ class SessionService {
|
|
|
151
151
|
console.error('Error searching Pi-Mono sessions:', err);
|
|
152
152
|
return [];
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
154
|
+
} else if (session.source === 'vscode') {
|
|
155
|
+
// VSCode format: Read JSONL file and parse with VsCodeParser
|
|
156
|
+
const { VsCodeParser } = require('../../lib/parsers');
|
|
157
|
+
const vscodeParser = new VsCodeParser();
|
|
158
|
+
try {
|
|
159
|
+
const raw = await fs.promises.readFile(session.filePath, 'utf-8');
|
|
160
|
+
const lines = raw.trim().split('\n').filter(line => line.trim());
|
|
161
|
+
const parsedLines = lines.map(line => {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(line);
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}).filter(l => l !== null);
|
|
168
|
+
|
|
169
|
+
if (parsedLines.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
// Use parseJsonl for new JSONL format, or parseVsCode for old JSON format
|
|
172
|
+
let parsed;
|
|
173
|
+
if (vscodeParser.canParse(parsedLines)) {
|
|
174
|
+
// New JSONL format
|
|
175
|
+
parsed = vscodeParser.parseJsonl(parsedLines);
|
|
176
|
+
} else {
|
|
177
|
+
// Old JSON format (single object)
|
|
178
|
+
const sessionJson = JSON.parse(raw);
|
|
179
|
+
parsed = vscodeParser.parseVsCode(sessionJson);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let events = this._expandVsCodeEvents(parsed.allEvents);
|
|
183
|
+
|
|
184
|
+
// Bug fix #2: Use file modification time as session end time
|
|
185
|
+
// VSCode sessions only record request.timestamp, not completion time
|
|
186
|
+
// For agentic sessions with sub-agents, actual work takes much longer
|
|
187
|
+
if (events.length > 0) {
|
|
188
|
+
try {
|
|
189
|
+
const stats = await fs.promises.stat(session.filePath);
|
|
190
|
+
const fileMtime = new Date(stats.mtime).toISOString();
|
|
191
|
+
|
|
192
|
+
// Check if the last event timestamp is much earlier than file mtime
|
|
193
|
+
const lastEvent = events[events.length - 1];
|
|
194
|
+
if (lastEvent && lastEvent.timestamp) {
|
|
195
|
+
const lastEventTime = new Date(lastEvent.timestamp).getTime();
|
|
196
|
+
const fileTime = new Date(fileMtime).getTime();
|
|
197
|
+
const diffSeconds = (fileTime - lastEventTime) / 1000;
|
|
198
|
+
|
|
199
|
+
// If difference is more than 10 seconds, use file mtime as end time
|
|
200
|
+
if (diffSeconds > 10) {
|
|
201
|
+
// Update the last event's timestamp to file modification time
|
|
202
|
+
lastEvent.timestamp = fileMtime;
|
|
203
|
+
console.log(`[VSCode] Updated session ${session.id} end time to file mtime (${diffSeconds.toFixed(0)}s difference)`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error('[VSCode] Error getting file mtime:', err);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Bug fix #1: Generate tool.execution_start/complete events from data.tools array
|
|
212
|
+
// This expansion must happen here before returning, since VSCode bypasses the normal pipeline
|
|
213
|
+
events = this._expandVsCodeToTimelineFormat(events);
|
|
214
|
+
|
|
215
|
+
return events;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.error('Error reading VSCode session:', err);
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
172
220
|
}
|
|
173
221
|
|
|
174
222
|
|
|
@@ -247,6 +295,7 @@ class SessionService {
|
|
|
247
295
|
// But we need to merge toolResult events into their parent assistant messages
|
|
248
296
|
this._mergePiMonoToolResults(events);
|
|
249
297
|
}
|
|
298
|
+
// Note: VSCode expansion is handled in the VSCode block above (before early return)
|
|
250
299
|
|
|
251
300
|
// Clean up events for timeline rendering
|
|
252
301
|
events = events.filter(e => {
|
|
@@ -500,6 +549,17 @@ class SessionService {
|
|
|
500
549
|
}
|
|
501
550
|
});
|
|
502
551
|
|
|
552
|
+
// Mark user events that are entirely tool_result responses (will be filtered out in normalizer)
|
|
553
|
+
events.forEach(event => {
|
|
554
|
+
if (event.type === 'user' && Array.isArray(event.message?.content)) {
|
|
555
|
+
const allToolResults = event.message.content.length > 0 &&
|
|
556
|
+
event.message.content.every(block => block?.type === 'tool_result');
|
|
557
|
+
if (allToolResults) {
|
|
558
|
+
event._isToolResultWrapper = true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
503
563
|
// Match tool_use with tool_result
|
|
504
564
|
events.forEach(event => {
|
|
505
565
|
if (event.data?.tools) {
|
|
@@ -523,9 +583,15 @@ class SessionService {
|
|
|
523
583
|
});
|
|
524
584
|
|
|
525
585
|
// Bug fix: Only remove tool_result from assistant messages
|
|
526
|
-
//
|
|
586
|
+
// Mark user messages that consist entirely of tool_results as wrappers (will be filtered out)
|
|
527
587
|
if (event.type === 'assistant' || event.type === 'assistant.message') {
|
|
528
588
|
event.data.tools = event.data.tools.filter(tool => tool.type !== 'tool_result');
|
|
589
|
+
} else if (event.type === 'user' || event.type === 'user.message') {
|
|
590
|
+
const allToolResults = event.data.tools.length > 0 &&
|
|
591
|
+
event.data.tools.every(tool => tool.type === 'tool_result');
|
|
592
|
+
if (allToolResults) {
|
|
593
|
+
event._isToolResultWrapper = true;
|
|
594
|
+
}
|
|
529
595
|
}
|
|
530
596
|
}
|
|
531
597
|
});
|
|
@@ -782,6 +848,11 @@ class SessionService {
|
|
|
782
848
|
normalized.data.badgeClass = 'badge-session';
|
|
783
849
|
return;
|
|
784
850
|
}
|
|
851
|
+
if (type === 'system.notification') {
|
|
852
|
+
normalized.data.badgeLabel = 'SYSTEM';
|
|
853
|
+
normalized.data.badgeClass = 'badge-system';
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
785
856
|
|
|
786
857
|
// Extract category from type (e.g., 'user.message' → 'user')
|
|
787
858
|
const parts = (type || '').split('.');
|
|
@@ -815,8 +886,17 @@ class SessionService {
|
|
|
815
886
|
|
|
816
887
|
if (source === 'copilot') {
|
|
817
888
|
// Copilot format normalization
|
|
818
|
-
|
|
819
|
-
//
|
|
889
|
+
|
|
890
|
+
// system-sourced user.message → system.notification (separate type for display)
|
|
891
|
+
if (event.type === 'user.message' && event.data?.source === 'system') {
|
|
892
|
+
normalized.type = 'system.notification';
|
|
893
|
+
// Extract text from <system_notification>...</system_notification> tag if present
|
|
894
|
+
const raw = event.data.content || event.data.message || '';
|
|
895
|
+
const match = raw.match(/<system_notification>([\s\S]*?)<\/system_notification>/);
|
|
896
|
+
normalized.data.message = match ? match[1].trim() : raw.trim();
|
|
897
|
+
this._generateBadgeInfo(normalized);
|
|
898
|
+
return normalized;
|
|
899
|
+
}
|
|
820
900
|
if (event.type === 'request') {
|
|
821
901
|
normalized.type = 'user';
|
|
822
902
|
// Extract message from payload.messages (Anthropic API format)
|
|
@@ -1463,6 +1543,8 @@ class SessionService {
|
|
|
1463
1543
|
let pendingParentId = null;
|
|
1464
1544
|
let pendingTs = null;
|
|
1465
1545
|
let pendingIdx = 0;
|
|
1546
|
+
let pendingSubAgentId = null;
|
|
1547
|
+
let pendingSubAgentName = null;
|
|
1466
1548
|
|
|
1467
1549
|
const flushTools = () => {
|
|
1468
1550
|
if (pendingTools.length === 0) return;
|
|
@@ -1475,19 +1557,30 @@ class SessionService {
|
|
|
1475
1557
|
message: '',
|
|
1476
1558
|
content: '',
|
|
1477
1559
|
tools: pendingTools,
|
|
1560
|
+
subAgentId: pendingSubAgentId,
|
|
1561
|
+
subAgentName: pendingSubAgentName,
|
|
1478
1562
|
},
|
|
1479
1563
|
_synthetic: true,
|
|
1480
1564
|
});
|
|
1481
1565
|
pendingTools = [];
|
|
1566
|
+
pendingSubAgentId = null;
|
|
1567
|
+
pendingSubAgentName = null;
|
|
1482
1568
|
};
|
|
1483
1569
|
|
|
1484
1570
|
for (let i = 0; i < events.length; i++) {
|
|
1485
1571
|
const ev = events[i];
|
|
1486
1572
|
if (ev.type === 'tool.invocation') {
|
|
1573
|
+
const evSubAgentId = ev.data?.subAgentId || null;
|
|
1574
|
+
// Flush if switching to a different subagent's tool group
|
|
1575
|
+
if (pendingTools.length > 0 && evSubAgentId !== pendingSubAgentId) {
|
|
1576
|
+
flushTools();
|
|
1577
|
+
}
|
|
1487
1578
|
if (pendingTools.length === 0) {
|
|
1488
1579
|
pendingParentId = ev.parentId;
|
|
1489
1580
|
pendingTs = ev.timestamp;
|
|
1490
1581
|
pendingIdx = i;
|
|
1582
|
+
pendingSubAgentId = evSubAgentId;
|
|
1583
|
+
pendingSubAgentName = ev.data?.subAgentName || null;
|
|
1491
1584
|
}
|
|
1492
1585
|
if (ev.data?.tool) pendingTools.push(ev.data.tool);
|
|
1493
1586
|
} else {
|
|
@@ -1755,6 +1848,72 @@ class SessionService {
|
|
|
1755
1848
|
|
|
1756
1849
|
return expanded;
|
|
1757
1850
|
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* Expand VSCode format to timeline format with tool.execution_start/complete events
|
|
1854
|
+
* VSCode events already have assistant.message events with data.tools arrays (from _expandVsCodeEvents)
|
|
1855
|
+
* This method generates tool.execution_start/complete events for the time-analyze page
|
|
1856
|
+
* @private
|
|
1857
|
+
* @param {Array} events - VSCode events with assistant.message events containing data.tools
|
|
1858
|
+
* @returns {Array} Expanded events with tool execution events
|
|
1859
|
+
*/
|
|
1860
|
+
_expandVsCodeToTimelineFormat(events) {
|
|
1861
|
+
const expanded = [];
|
|
1862
|
+
|
|
1863
|
+
for (let i = 0; i < events.length; i++) {
|
|
1864
|
+
const event = events[i];
|
|
1865
|
+
|
|
1866
|
+
// Keep the original event
|
|
1867
|
+
expanded.push(event);
|
|
1868
|
+
|
|
1869
|
+
// Generate tool.execution_start and tool.execution_complete for assistant.message events with tools
|
|
1870
|
+
if (event.type === 'assistant.message' && event.data?.tools && event.data.tools.length > 0) {
|
|
1871
|
+
event.data.tools.forEach((tool, idx) => {
|
|
1872
|
+
// Skip if tool doesn't have required fields
|
|
1873
|
+
if (!tool.id || !tool.name) return;
|
|
1874
|
+
|
|
1875
|
+
const toolStartTime = tool.startTime || event.timestamp;
|
|
1876
|
+
const toolEndTime = tool.endTime || event.timestamp;
|
|
1877
|
+
|
|
1878
|
+
// Generate tool.execution_start event
|
|
1879
|
+
expanded.push({
|
|
1880
|
+
type: 'tool.execution_start',
|
|
1881
|
+
id: `${tool.id}-start`,
|
|
1882
|
+
timestamp: toolStartTime,
|
|
1883
|
+
parentId: event.id,
|
|
1884
|
+
data: {
|
|
1885
|
+
toolCallId: tool.id,
|
|
1886
|
+
toolName: tool.name,
|
|
1887
|
+
tool: tool.name, // Alias for compatibility
|
|
1888
|
+
arguments: tool.input || {}
|
|
1889
|
+
},
|
|
1890
|
+
_synthetic: true,
|
|
1891
|
+
_fileIndex: event._fileIndex ? event._fileIndex + 0.1 + (idx * 0.02) : undefined
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
// Generate tool.execution_complete event
|
|
1895
|
+
expanded.push({
|
|
1896
|
+
type: 'tool.execution_complete',
|
|
1897
|
+
id: `${tool.id}-complete`,
|
|
1898
|
+
timestamp: toolEndTime,
|
|
1899
|
+
parentId: tool.id,
|
|
1900
|
+
data: {
|
|
1901
|
+
toolCallId: tool.id,
|
|
1902
|
+
toolName: tool.name,
|
|
1903
|
+
tool: tool.name, // Alias
|
|
1904
|
+
result: tool.result || null,
|
|
1905
|
+
error: tool.error || (tool.status === 'error' ? 'Tool execution failed' : null),
|
|
1906
|
+
isError: tool.status === 'error'
|
|
1907
|
+
},
|
|
1908
|
+
_synthetic: true,
|
|
1909
|
+
_fileIndex: event._fileIndex ? event._fileIndex + 0.15 + (idx * 0.02) : undefined
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
return expanded;
|
|
1916
|
+
}
|
|
1758
1917
|
}
|
|
1759
1918
|
|
|
1760
1919
|
module.exports = SessionService;
|
package/src/utils/helpers.js
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
* @returns {Object} Metadata object
|
|
9
9
|
*/
|
|
10
10
|
function buildMetadata(session) {
|
|
11
|
+
const json = session.toJSON ? session.toJSON() : {};
|
|
11
12
|
return {
|
|
12
13
|
type: session.type,
|
|
13
14
|
source: session.source, // 'copilot' or 'claude'
|
|
15
|
+
sourceName: json.sourceName || session.source,
|
|
16
|
+
sourceBadgeClass: json.sourceBadgeClass || 'source-unknown',
|
|
14
17
|
summary: session.summary,
|
|
15
|
-
model: session.model,
|
|
18
|
+
model: session.selectedModel || session.model,
|
|
16
19
|
repo: session.workspace?.repository,
|
|
17
20
|
branch: session.workspace?.branch,
|
|
18
21
|
cwd: session.workspace?.cwd,
|
package/views/index.ejs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>Session Viewer</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
9
|
body {
|
|
@@ -489,7 +489,7 @@
|
|
|
489
489
|
<body>
|
|
490
490
|
<div class="container">
|
|
491
491
|
<h1>🤖 Session Viewer</h1>
|
|
492
|
-
<p class="subtitle">View session logs from Copilot, Claude Code, and Pi-Mono</p>
|
|
492
|
+
<p class="subtitle">View session logs from Copilot CLI, Copilot Chat, Claude Code, and Pi-Mono</p>
|
|
493
493
|
|
|
494
494
|
<form id="sessionForm">
|
|
495
495
|
<div class="input-group">
|
|
@@ -505,10 +505,6 @@
|
|
|
505
505
|
</div>
|
|
506
506
|
</form>
|
|
507
507
|
|
|
508
|
-
<p class="hint">
|
|
509
|
-
Session ID can be found in <span class="hint-code">~/.copilot/session-state/</span>
|
|
510
|
-
</p>
|
|
511
|
-
|
|
512
508
|
<% if (sessions && sessions.length > 0) { %>
|
|
513
509
|
<div class="recent-sessions">
|
|
514
510
|
<div class="sessions-header">
|
|
@@ -518,11 +514,12 @@
|
|
|
518
514
|
<a class="import-link" id="importLink">Import session from zip</a>
|
|
519
515
|
</div>
|
|
520
516
|
<div class="filter-pills">
|
|
521
|
-
<button class="filter-pill active" data-source="copilot">Copilot</button>
|
|
517
|
+
<button class="filter-pill active" data-source="copilot">Copilot CLI</button>
|
|
518
|
+
<button class="filter-pill" data-source="vscode">Copilot Chat</button>
|
|
522
519
|
<button class="filter-pill" data-source="claude">Claude</button>
|
|
523
520
|
<button class="filter-pill" data-source="pi-mono">Pi</button>
|
|
524
|
-
<!-- <button class="filter-pill" data-source="vscode">VSCode</button> -->
|
|
525
521
|
</div>
|
|
522
|
+
<p class="hint source-hint" id="sourceHint"></p>
|
|
526
523
|
<input
|
|
527
524
|
type="file"
|
|
528
525
|
id="fileInput"
|
|
@@ -803,7 +800,7 @@
|
|
|
803
800
|
// Add source badge (use backend-provided metadata - Violation #3 & #5 fix)
|
|
804
801
|
const sourceClass = session.sourceBadgeClass || 'source-copilot';
|
|
805
802
|
const sourceLabel = session.sourceName || 'Copilot';
|
|
806
|
-
badges += `<span class="status-badge ${sourceClass}" title="${sourceLabel}
|
|
803
|
+
badges += `<span class="status-badge ${sourceClass}" title="${sourceLabel}">${sourceLabel}</span>`;
|
|
807
804
|
|
|
808
805
|
if (session.sessionStatus === 'wip') {
|
|
809
806
|
badges += '<span class="status-badge wip" title="Session in progress">🔄 WIP</span>';
|
|
@@ -812,7 +809,7 @@
|
|
|
812
809
|
badges += '<span class="status-badge imported" title="Imported session">📥</span>';
|
|
813
810
|
}
|
|
814
811
|
if (session.hasInsight) {
|
|
815
|
-
badges += '<span class="status-badge insight" title="Has
|
|
812
|
+
badges += '<span class="status-badge insight" title="Has Agent Review">💡</span>';
|
|
816
813
|
}
|
|
817
814
|
// Add model and version badges
|
|
818
815
|
if (session.selectedModel) {
|
|
@@ -828,7 +825,7 @@
|
|
|
828
825
|
badges += `<span class="status-badge model ${modelClass}" title="Model: ${escapeHtml(session.selectedModel)}">${escapeHtml(modelShort)}</span>`;
|
|
829
826
|
}
|
|
830
827
|
if (session.copilotVersion) {
|
|
831
|
-
badges += `<span class="status-badge version" title="
|
|
828
|
+
badges += `<span class="status-badge version" title="CLI version">${escapeHtml(session.copilotVersion)}</span>`;
|
|
832
829
|
}
|
|
833
830
|
|
|
834
831
|
let summaryHtml = '';
|
|
@@ -843,7 +840,7 @@
|
|
|
843
840
|
workspaceHtml = `
|
|
844
841
|
<div class="session-info-item workspace" title="${escapeHtml(session.workspace.cwd)}">
|
|
845
842
|
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z"></path></svg>
|
|
846
|
-
<span class="session-info-value">${escapeHtml(session.workspace.cwd)}</span>
|
|
843
|
+
<span class="session-info-value">${escapeHtml(session.workspace.cwd.replace(/^\/Users\/[^/]+/, '~'))}</span>
|
|
847
844
|
</div>
|
|
848
845
|
`;
|
|
849
846
|
}
|
|
@@ -957,14 +954,29 @@
|
|
|
957
954
|
});
|
|
958
955
|
}
|
|
959
956
|
|
|
957
|
+
// Source directory hints (from server, platform-aware)
|
|
958
|
+
const sourceHints = <%- sourceHints || '{}' %>;
|
|
959
|
+
|
|
960
|
+
function updateSourceHint(source) {
|
|
961
|
+
const hint = document.getElementById('sourceHint');
|
|
962
|
+
if (hint && sourceHints[source]) {
|
|
963
|
+
hint.innerHTML = 'Sessions from <span class="hint-code">' + sourceHints[source] + '</span>';
|
|
964
|
+
} else if (hint) {
|
|
965
|
+
hint.textContent = '';
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
960
969
|
// Filter pill click handler
|
|
961
970
|
function setupFilterPills() {
|
|
962
971
|
const filterPills = document.querySelectorAll('.filter-pill');
|
|
972
|
+
// Show hint for initial active pill
|
|
973
|
+
updateSourceHint(currentSourceFilter);
|
|
963
974
|
filterPills.forEach(pill => {
|
|
964
975
|
pill.addEventListener('click', async () => {
|
|
965
976
|
filterPills.forEach(p => p.classList.remove('active'));
|
|
966
977
|
pill.classList.add('active');
|
|
967
978
|
currentSourceFilter = pill.getAttribute('data-source');
|
|
979
|
+
updateSourceHint(currentSourceFilter);
|
|
968
980
|
|
|
969
981
|
// Init per-source state if first visit
|
|
970
982
|
if (!sourceState[currentSourceFilter]) {
|