@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.
@@ -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
- // TODO: VSCode parser disabled
43
- // {
44
- // type: 'vscode',
45
- // dir: process.env.VSCODE_WORKSPACE_STORAGE_DIR ||
46
- // path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage')
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
- // } else if (source.type === 'vscode') { // TODO: VSCode disabled
115
- // if (stats.isDirectory()) {
116
- // return this._scanVsCodeWorkspaceDir(fullPath);
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
- // } else if (source.type === 'vscode') { // TODO: VSCode disabled
278
- // session = await this._findVsCodeSession(sessionId, source.dir);
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: updatedAt - createdAt,
487
- sessionStatus: 'completed',
488
- model: firstReq.modelId || null,
489
- workspace: { cwd: path.join(workspaceStorageDir, hash) },
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
- : (lastReq.timestamp ? new Date(lastReq.timestamp) : stats.mtime);
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: updatedAt - createdAt,
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
- // } else if (session.source === 'vscode') { // TODO: VSCode disabled
155
- // const { VsCodeParser } = require('../../lib/parsers');
156
- // const vscodeParser = new VsCodeParser();
157
- // try {
158
- // const raw = await fs.promises.readFile(session.filePath, 'utf-8');
159
- // let sessionJson;
160
- // if (session.filePath.endsWith('.jsonl')) {
161
- // sessionJson = this.sessionRepository._parseVsCodeJsonl(raw);
162
- // } else {
163
- // sessionJson = JSON.parse(raw);
164
- // }
165
- // if (!sessionJson) return [];
166
- // const parsed = vscodeParser.parseVsCode(sessionJson);
167
- // return this._expandVsCodeEvents(parsed.allEvents);
168
- // } catch (err) {
169
- // console.error('Error reading VSCode session:', err);
170
- // return [];
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
- // User messages naturally contain tool_result (responses to tool_use), keep them
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
- // Old format: request/response user/assistant
889
+
890
+ // system-sourced user.messagesystem.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;
@@ -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>Copilot Session Viewer</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} CLI">${sourceLabel}</span>`;
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 Copilot Insight">💡</span>';
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="Copilot CLI version">${escapeHtml(session.copilotVersion)}</span>`;
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]) {