@imdeadpool/guardex 7.0.22 → 7.0.24

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.
@@ -6,6 +6,7 @@ const {
6
6
  formatElapsedFrom,
7
7
  readActiveSessions,
8
8
  readRepoChanges,
9
+ readSessionInspectData,
9
10
  sanitizeBranchForFile,
10
11
  } = require('./session-schema.js');
11
12
 
@@ -16,6 +17,7 @@ const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
16
17
  const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
17
18
  const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
18
19
  const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
20
+ const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log';
19
21
  const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
20
22
  const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
21
23
  const SESSION_SCAN_LIMIT = 200;
@@ -24,17 +26,19 @@ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agen
24
26
  const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
25
27
  const RELOAD_WINDOW_ACTION = 'Reload Window';
26
28
  const UPDATE_LATER_ACTION = 'Later';
29
+ const REFRESH_POLL_INTERVAL_MS = 30_000;
30
+ const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
27
31
  const SESSION_ACTIVITY_GROUPS = [
28
32
  { kind: 'blocked', label: 'BLOCKED' },
29
33
  { kind: 'working', label: 'WORKING NOW' },
30
- { kind: 'idle', label: 'IDLE' },
34
+ { kind: 'idle', label: 'THINKING' },
31
35
  { kind: 'stalled', label: 'STALLED' },
32
36
  { kind: 'dead', label: 'DEAD' },
33
37
  ];
34
38
  const SESSION_ACTIVITY_ICON_IDS = {
35
39
  blocked: 'warning',
36
- working: 'edit',
37
- idle: 'loading~spin',
40
+ working: 'loading~spin',
41
+ idle: 'comment-discussion',
38
42
  stalled: 'clock',
39
43
  dead: 'error',
40
44
  };
@@ -168,6 +172,134 @@ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
168
172
  ].filter(Boolean).join('\n');
169
173
  }
170
174
 
175
+ function escapeHtml(value) {
176
+ return String(value || '')
177
+ .replace(/&/g, '&')
178
+ .replace(/</g, '&lt;')
179
+ .replace(/>/g, '&gt;')
180
+ .replace(/"/g, '&quot;')
181
+ .replace(/'/g, '&#39;');
182
+ }
183
+
184
+ function formatInspectBranchSummary(inspectData) {
185
+ if (Number.isInteger(inspectData?.aheadCount) && Number.isInteger(inspectData?.behindCount)) {
186
+ return `${inspectData.aheadCount} ahead · ${inspectData.behindCount} behind vs ${inspectData.compareRef}`;
187
+ }
188
+ return `Branch comparison unavailable vs ${inspectData?.compareRef || 'origin/dev'}`;
189
+ }
190
+
191
+ function inspectPanelTitle(session) {
192
+ return `Inspect ${sessionDisplayLabel(session)}`;
193
+ }
194
+
195
+ function renderInspectPanelHtml(session, inspectData) {
196
+ const heldLocksMarkup = Array.isArray(inspectData?.heldLocks) && inspectData.heldLocks.length > 0
197
+ ? `<ul>${inspectData.heldLocks.map((entry) => (
198
+ `<li><code>${escapeHtml(entry.relativePath)}</code>${entry.allowDelete ? ' <span class="pill">delete ok</span>' : ''}${entry.claimedAt ? ` <span class="muted">${escapeHtml(entry.claimedAt)}</span>` : ''}</li>`
199
+ )).join('')}</ul>`
200
+ : '<p class="muted">No held locks recorded for this session.</p>';
201
+ const logContent = inspectData?.logTailText
202
+ ? escapeHtml(inspectData.logTailText)
203
+ : 'No log output available.';
204
+
205
+ return `<!DOCTYPE html>
206
+ <html lang="en">
207
+ <head>
208
+ <meta charset="utf-8" />
209
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
210
+ <style>
211
+ :root {
212
+ color-scheme: light dark;
213
+ font-family: var(--vscode-font-family);
214
+ }
215
+ body {
216
+ padding: 16px;
217
+ color: var(--vscode-foreground);
218
+ background: var(--vscode-editor-background);
219
+ }
220
+ h1, h2 {
221
+ margin: 0 0 12px;
222
+ font-weight: 600;
223
+ }
224
+ h2 {
225
+ margin-top: 20px;
226
+ font-size: 13px;
227
+ text-transform: uppercase;
228
+ letter-spacing: 0.04em;
229
+ color: var(--vscode-descriptionForeground);
230
+ }
231
+ .grid {
232
+ display: grid;
233
+ grid-template-columns: minmax(140px, 220px) 1fr;
234
+ gap: 8px 12px;
235
+ margin: 0;
236
+ }
237
+ dt {
238
+ color: var(--vscode-descriptionForeground);
239
+ }
240
+ dd {
241
+ margin: 0;
242
+ word-break: break-word;
243
+ }
244
+ code, pre {
245
+ font-family: var(--vscode-editor-font-family, monospace);
246
+ font-size: 12px;
247
+ }
248
+ pre {
249
+ margin: 0;
250
+ padding: 12px;
251
+ border-radius: 8px;
252
+ overflow: auto;
253
+ background: var(--vscode-textCodeBlock-background, rgba(127, 127, 127, 0.12));
254
+ border: 1px solid var(--vscode-editorWidget-border, transparent);
255
+ white-space: pre-wrap;
256
+ word-break: break-word;
257
+ }
258
+ ul {
259
+ margin: 0;
260
+ padding-left: 20px;
261
+ }
262
+ li + li {
263
+ margin-top: 6px;
264
+ }
265
+ .muted {
266
+ color: var(--vscode-descriptionForeground);
267
+ }
268
+ .pill {
269
+ display: inline-block;
270
+ margin-left: 6px;
271
+ padding: 1px 6px;
272
+ border-radius: 999px;
273
+ background: var(--vscode-badge-background);
274
+ color: var(--vscode-badge-foreground);
275
+ font-size: 11px;
276
+ }
277
+ </style>
278
+ </head>
279
+ <body>
280
+ <h1>${escapeHtml(sessionIdentityLabel(session))}</h1>
281
+ <dl class="grid">
282
+ <dt>Branch</dt>
283
+ <dd><code>${escapeHtml(session.branch)}</code></dd>
284
+ <dt>Worktree</dt>
285
+ <dd><code>${escapeHtml(session.worktreePath)}</code></dd>
286
+ <dt>Base branch</dt>
287
+ <dd><code>${escapeHtml(inspectData?.baseBranch || 'dev')}</code></dd>
288
+ <dt>Divergence</dt>
289
+ <dd>${escapeHtml(formatInspectBranchSummary(inspectData))}</dd>
290
+ <dt>Held locks</dt>
291
+ <dd>${Array.isArray(inspectData?.heldLocks) ? inspectData.heldLocks.length : 0}</dd>
292
+ <dt>Log file</dt>
293
+ <dd><code>${escapeHtml(inspectData?.logPath || 'Unavailable')}</code></dd>
294
+ </dl>
295
+ <h2>Held Locks</h2>
296
+ ${heldLocksMarkup}
297
+ <h2>Agent Log Tail</h2>
298
+ <pre>${logContent}</pre>
299
+ </body>
300
+ </html>`;
301
+ }
302
+
171
303
  class SessionDecorationProvider {
172
304
  constructor(nowProvider = () => Date.now()) {
173
305
  this.nowProvider = nowProvider;
@@ -265,8 +397,9 @@ class RepoItem extends vscode.TreeItem {
265
397
  if (workingCount > 0) {
266
398
  descriptionParts.push(`${workingCount} working`);
267
399
  }
268
- if (changes.length > 0) {
269
- descriptionParts.push(`${changes.length} changed`);
400
+ const changedCount = countChangedPaths(repoRoot, sessions, changes);
401
+ if (changedCount > 0) {
402
+ descriptionParts.push(`${changedCount} changed`);
270
403
  }
271
404
  this.description = descriptionParts.join(' · ');
272
405
  this.tooltip = [
@@ -288,11 +421,49 @@ class SectionItem extends vscode.TreeItem {
288
421
  }
289
422
  }
290
423
 
424
+ class WorktreeItem extends vscode.TreeItem {
425
+ constructor(worktreePath, sessions, items = [], options = {}) {
426
+ const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
427
+ const sessionList = Array.isArray(sessions) ? sessions : [];
428
+ const changedCount = Number.isInteger(options.changedCount)
429
+ ? options.changedCount
430
+ : sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
431
+ const descriptionParts = [formatCountLabel(sessionList.length, 'agent')];
432
+ if (changedCount > 0) {
433
+ descriptionParts.push(`${changedCount} changed`);
434
+ }
435
+ super(
436
+ path.basename(normalizedWorktreePath || '') || 'worktree',
437
+ items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
438
+ );
439
+ this.worktreePath = normalizedWorktreePath;
440
+ this.sessions = sessionList;
441
+ this.items = items;
442
+ this.description = options.description || descriptionParts.join(' · ');
443
+ this.tooltip = [
444
+ normalizedWorktreePath,
445
+ ...sessionList.map((session) => session.branch).filter(Boolean),
446
+ ].filter(Boolean).join('\n');
447
+ this.iconPath = new vscode.ThemeIcon('folder');
448
+ this.contextValue = 'gitguardex.worktree';
449
+ if (sessionList[0]?.worktreePath) {
450
+ this.command = {
451
+ command: 'gitguardex.activeAgents.openWorktree',
452
+ title: 'Open Agent Worktree',
453
+ arguments: [sessionList[0]],
454
+ };
455
+ }
456
+ }
457
+ }
458
+
291
459
  class SessionItem extends vscode.TreeItem {
292
- constructor(session, items = []) {
460
+ constructor(session, items = [], options = {}) {
293
461
  const lockCount = Number.isFinite(session.lockCount) ? session.lockCount : 0;
462
+ const label = typeof options.label === 'string' && options.label.trim()
463
+ ? options.label.trim()
464
+ : session.label;
294
465
  super(
295
- `${session.label} 🔒 ${lockCount}`,
466
+ label,
296
467
  items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
297
468
  );
298
469
  this.session = session;
@@ -303,6 +474,9 @@ class SessionItem extends vscode.TreeItem {
303
474
  descriptionParts.push(session.activityCountLabel);
304
475
  }
305
476
  descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
477
+ if (lockCount > 0) {
478
+ descriptionParts.push(`${lockCount} $(lock)`);
479
+ }
306
480
  this.description = descriptionParts.join(' · ');
307
481
  const tooltipLines = [
308
482
  session.branch,
@@ -315,6 +489,7 @@ class SessionItem extends vscode.TreeItem {
315
489
  ? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
316
490
  : session.activitySummary,
317
491
  `Locks ${lockCount}`,
492
+ session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
318
493
  Number.isInteger(session.pid) && session.pid > 0
319
494
  ? session.pidAlive === false
320
495
  ? `PID ${session.pid} not alive`
@@ -378,10 +553,39 @@ function shellQuote(value) {
378
553
  return `'${normalized.replace(/'/g, "'\"'\"'")}'`;
379
554
  }
380
555
 
556
+ function readPackageJson(repoRoot) {
557
+ const packageJsonPath = path.join(repoRoot, 'package.json');
558
+ try {
559
+ return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
560
+ } catch (_error) {
561
+ return null;
562
+ }
563
+ }
564
+
565
+ function resolveStartAgentCommand(repoRoot, details) {
566
+ const taskArg = shellQuote(details.taskName);
567
+ const agentArg = shellQuote(details.agentName);
568
+ const localCodexAgentPath = path.join(repoRoot, 'scripts', 'codex-agent.sh');
569
+ if (fs.existsSync(localCodexAgentPath)) {
570
+ return `bash ./scripts/codex-agent.sh ${taskArg} ${agentArg}`;
571
+ }
572
+
573
+ const agentCodexScript = readPackageJson(repoRoot)?.scripts?.['agent:codex'];
574
+ if (typeof agentCodexScript === 'string' && agentCodexScript.trim().length > 0) {
575
+ return `npm run agent:codex -- ${taskArg} ${agentArg}`;
576
+ }
577
+
578
+ return `gx branch start ${taskArg} ${agentArg}`;
579
+ }
580
+
381
581
  function sessionDisplayLabel(session) {
382
582
  return session?.taskName || session?.label || session?.branch || path.basename(session?.worktreePath || '') || 'session';
383
583
  }
384
584
 
585
+ function sessionTreeLabel(session) {
586
+ return session?.branch || sessionDisplayLabel(session);
587
+ }
588
+
385
589
  function sessionWorktreePath(session) {
386
590
  return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
387
591
  }
@@ -435,16 +639,34 @@ function syncSession(session) {
435
639
  runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
436
640
  }
437
641
 
642
+ function execFileAsync(command, args, options = {}) {
643
+ return new Promise((resolve, reject) => {
644
+ cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
645
+ if (error) {
646
+ error.stdout = stdout;
647
+ error.stderr = stderr;
648
+ reject(error);
649
+ return;
650
+ }
651
+ resolve({ stdout, stderr });
652
+ });
653
+ });
654
+ }
655
+
438
656
  async function stopSession(session, refresh) {
439
657
  const pid = Number(session?.pid);
440
658
  if (!Number.isInteger(pid) || pid <= 0) {
441
659
  showSessionMessage('Cannot stop session: missing pid.');
442
660
  return;
443
661
  }
662
+ if (!session?.branch) {
663
+ showSessionMessage('Cannot stop session: missing branch name.');
664
+ return;
665
+ }
444
666
 
445
667
  const confirmed = await vscode.window.showWarningMessage(
446
668
  `Stop ${sessionDisplayLabel(session)}?`,
447
- { modal: true, detail: `Send SIGTERM to pid ${pid}.` },
669
+ { modal: true, detail: `Run gx agents stop --pid ${pid}.` },
448
670
  'Stop',
449
671
  );
450
672
  if (confirmed !== 'Stop') {
@@ -452,42 +674,87 @@ async function stopSession(session, refresh) {
452
674
  }
453
675
 
454
676
  try {
455
- process.kill(pid, 'SIGTERM');
677
+ const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
678
+ const args = ['agents', 'stop', '--pid', String(pid)];
679
+ if (session?.repoRoot) {
680
+ args.push('--target', session.repoRoot);
681
+ }
682
+ await execFileAsync('gx', args, {
683
+ cwd: commandCwd,
684
+ encoding: 'utf8',
685
+ maxBuffer: 1024 * 1024,
686
+ });
456
687
  refresh();
457
688
  } catch (error) {
458
689
  showSessionMessage(
459
- `Failed to stop session ${sessionDisplayLabel(session)}: ${error?.message || String(error)}`,
690
+ `Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`,
460
691
  );
461
692
  }
462
693
  }
463
694
 
695
+ function sessionChangedPaths(session) {
696
+ const directPaths = Array.isArray(session?.changedPaths)
697
+ ? session.changedPaths.map(normalizeRelativePath).filter(Boolean)
698
+ : [];
699
+ if (directPaths.length > 0) {
700
+ return [...new Set(directPaths)];
701
+ }
702
+ if (!session?.repoRoot || !session?.branch) {
703
+ return [];
704
+ }
705
+
706
+ const liveSession = readActiveSessions(session.repoRoot)
707
+ .find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(session));
708
+ return Array.isArray(liveSession?.changedPaths)
709
+ ? [...new Set(liveSession.changedPaths.map(normalizeRelativePath).filter(Boolean))]
710
+ : [];
711
+ }
712
+
713
+ async function pickSessionDiffPath(session) {
714
+ const changedPaths = sessionChangedPaths(session);
715
+ if (changedPaths.length === 0) {
716
+ return '';
717
+ }
718
+ if (changedPaths.length === 1 || !vscode.window.showQuickPick) {
719
+ return changedPaths[0];
720
+ }
721
+
722
+ const picks = changedPaths.map((relativePath) => ({
723
+ label: path.basename(relativePath),
724
+ description: relativePath,
725
+ relativePath,
726
+ }));
727
+ const selection = await vscode.window.showQuickPick(picks, {
728
+ placeHolder: `Select a changed file for ${sessionDisplayLabel(session)}`,
729
+ ignoreFocusOut: true,
730
+ });
731
+ return selection?.relativePath || '';
732
+ }
733
+
464
734
  async function openSessionDiff(session) {
465
735
  const worktreePath = ensureSessionWorktree(session, 'open diff');
466
736
  if (!worktreePath) {
467
737
  return;
468
738
  }
469
739
 
470
- let diffOutput = '';
471
- try {
472
- diffOutput = cp.execFileSync('git', ['-C', worktreePath, 'diff'], {
473
- encoding: 'utf8',
474
- stdio: ['ignore', 'pipe', 'pipe'],
475
- });
476
- } catch (error) {
477
- const detail = [
478
- error?.stdout,
479
- error?.stderr,
480
- error?.message,
481
- ].find((value) => typeof value === 'string' && value.trim().length > 0) || 'git diff failed.';
482
- showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${detail.trim()}`);
740
+ const relativePath = await pickSessionDiffPath(session);
741
+ if (!relativePath) {
742
+ showSessionMessage(`No changed files to diff for ${sessionDisplayLabel(session)}.`);
483
743
  return;
484
744
  }
485
745
 
486
- const document = await vscode.workspace.openTextDocument({
487
- language: 'diff',
488
- content: diffOutput,
489
- });
490
- await vscode.window.showTextDocument(document, { preview: false });
746
+ const repoRoot = session?.repoRoot || worktreePath;
747
+ const absolutePath = path.resolve(repoRoot, relativePath);
748
+ const resourceUri = vscode.Uri.file(absolutePath);
749
+ try {
750
+ await vscode.commands.executeCommand('git.openChange', resourceUri);
751
+ } catch (error) {
752
+ if (fs.existsSync(absolutePath)) {
753
+ await vscode.commands.executeCommand('vscode.open', resourceUri);
754
+ return;
755
+ }
756
+ showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`);
757
+ }
491
758
  }
492
759
 
493
760
  function repoRootFromSessionFile(filePath) {
@@ -674,7 +941,7 @@ async function maybeAutoUpdateActiveAgentsExtension(context) {
674
941
  }
675
942
 
676
943
  const selection = await vscode.window.showInformationMessage?.(
677
- `GitGuardex Active Agents updated to ${candidate.version}. Reload Window to use the newest companion.`,
944
+ `GitGuardex Active Agents updated to ${candidate.version}. Reload this window now, then reload any other already-open VS Code windows to use the newest companion.`,
678
945
  RELOAD_WINDOW_ACTION,
679
946
  UPDATE_LATER_ACTION,
680
947
  );
@@ -684,9 +951,12 @@ async function maybeAutoUpdateActiveAgentsExtension(context) {
684
951
  }
685
952
 
686
953
  function decorateSession(session, lockRegistry) {
954
+ const touchedChanges = buildSessionTouchedChanges(session, lockRegistry);
687
955
  return {
688
956
  ...session,
689
957
  lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
958
+ touchedChanges,
959
+ conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length,
690
960
  };
691
961
  }
692
962
 
@@ -700,6 +970,28 @@ function decorateChange(change, lockRegistry, owningBranch) {
700
970
  };
701
971
  }
702
972
 
973
+ function buildSessionTouchedChanges(session, lockRegistry) {
974
+ const changedPaths = Array.isArray(session.worktreeChangedPaths)
975
+ ? session.worktreeChangedPaths
976
+ : [];
977
+ return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))]
978
+ .sort((left, right) => left.localeCompare(right))
979
+ .map((relativePath) => {
980
+ const lockEntry = lockRegistry.entriesByPath.get(relativePath);
981
+ const lockOwnerBranch = lockEntry?.branch || '';
982
+ return {
983
+ relativePath,
984
+ absolutePath: path.join(session.worktreePath, relativePath),
985
+ originalPath: '',
986
+ statusCode: 'M',
987
+ statusLabel: 'M',
988
+ statusText: 'Touched',
989
+ lockOwnerBranch,
990
+ hasForeignLock: Boolean(lockOwnerBranch) && lockOwnerBranch !== session.branch,
991
+ };
992
+ });
993
+ }
994
+
703
995
  function isPathWithin(parentPath, targetPath) {
704
996
  const relativePath = path.relative(parentPath, targetPath);
705
997
  return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
@@ -876,6 +1168,61 @@ function countWorkingSessions(sessions) {
876
1168
  return sessions.filter((session) => session.activityKind === 'working').length;
877
1169
  }
878
1170
 
1171
+ function countChangedPaths(repoRoot, sessions, changes) {
1172
+ const changedKeys = new Set();
1173
+
1174
+ for (const change of changes || []) {
1175
+ if (change?.relativePath) {
1176
+ changedKeys.add(normalizeRelativePath(change.relativePath));
1177
+ }
1178
+ }
1179
+
1180
+ for (const session of sessions || []) {
1181
+ for (const change of session.touchedChanges || []) {
1182
+ const absolutePath = change?.absolutePath
1183
+ || path.join(session.worktreePath || '', change?.relativePath || '');
1184
+ const normalizedRelativePath = absolutePath && isPathWithin(repoRoot, absolutePath)
1185
+ ? normalizeRelativePath(path.relative(repoRoot, absolutePath))
1186
+ : `${session.branch}:${normalizeRelativePath(change?.relativePath)}`;
1187
+ if (normalizedRelativePath) {
1188
+ changedKeys.add(normalizedRelativePath);
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ return changedKeys.size;
1194
+ }
1195
+
1196
+ function groupSessionsByWorktree(sessions) {
1197
+ const sessionsByWorktree = new Map();
1198
+
1199
+ for (const session of sessions || []) {
1200
+ const worktreePath = sessionWorktreePath(session);
1201
+ const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`;
1202
+ if (!sessionsByWorktree.has(key)) {
1203
+ sessionsByWorktree.set(key, {
1204
+ worktreePath,
1205
+ sessions: [],
1206
+ });
1207
+ }
1208
+ sessionsByWorktree.get(key).sessions.push(session);
1209
+ }
1210
+
1211
+ return [...sessionsByWorktree.values()]
1212
+ .map((entry) => ({
1213
+ ...entry,
1214
+ sessions: entry.sessions.sort((left, right) => (
1215
+ sessionTreeLabel(left).localeCompare(sessionTreeLabel(right))
1216
+ )),
1217
+ }))
1218
+ .sort((left, right) => {
1219
+ const leftLabel = path.basename(left.worktreePath || '') || '';
1220
+ const rightLabel = path.basename(right.worktreePath || '') || '';
1221
+ return leftLabel.localeCompare(rightLabel)
1222
+ || (left.worktreePath || '').localeCompare(right.worktreePath || '');
1223
+ });
1224
+ }
1225
+
879
1226
  function buildGroupedChangeTreeNodes(sessions, changes) {
880
1227
  const changesBySession = new Map();
881
1228
  const sessionByChangedPath = new Map();
@@ -908,15 +1255,22 @@ function buildGroupedChangeTreeNodes(sessions, changes) {
908
1255
  changesBySession.get(session.branch).push(localizedChange);
909
1256
  }
910
1257
 
911
- const items = sessions
912
- .map((session) => {
913
- const sessionChanges = changesBySession.get(session.branch) || [];
914
- if (sessionChanges.length === 0) {
915
- return null;
916
- }
917
- return new SessionItem(session, buildChangeTreeNodes(sessionChanges));
918
- })
919
- .filter(Boolean);
1258
+ const items = groupSessionsByWorktree(
1259
+ sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0),
1260
+ ).map(({ worktreePath, sessions: worktreeSessions }) => {
1261
+ const sessionItems = worktreeSessions.map((session) => (
1262
+ new SessionItem(
1263
+ session,
1264
+ buildChangeTreeNodes(changesBySession.get(session.branch) || []),
1265
+ { label: sessionTreeLabel(session) },
1266
+ )
1267
+ ));
1268
+ const changedCount = worktreeSessions.reduce(
1269
+ (total, session) => total + ((changesBySession.get(session.branch) || []).length),
1270
+ 0,
1271
+ );
1272
+ return new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount });
1273
+ });
920
1274
 
921
1275
  if (repoRootChanges.length > 0) {
922
1276
  items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
@@ -956,14 +1310,14 @@ async function pickRepoRoot() {
956
1310
  repoRoot: folder.uri.fsPath,
957
1311
  }));
958
1312
  const selection = await vscode.window.showQuickPick?.(picks, {
959
- placeHolder: 'Select the Guardex repo where gx branch start should run.',
1313
+ placeHolder: 'Select the Guardex repo where the Start agent launcher should run.',
960
1314
  });
961
1315
  return selection?.repoRoot || null;
962
1316
  }
963
1317
 
964
1318
  async function promptStartAgentDetails() {
965
1319
  const taskName = await vscode.window.showInputBox?.({
966
- prompt: 'Task for gx branch start',
1320
+ prompt: 'Task for the Guardex agent launcher',
967
1321
  placeHolder: 'vscode active agents welcome view',
968
1322
  ignoreFocusOut: true,
969
1323
  validateInput: (value) => value.trim() ? undefined : 'Task is required.',
@@ -973,7 +1327,7 @@ async function promptStartAgentDetails() {
973
1327
  }
974
1328
 
975
1329
  const agentName = await vscode.window.showInputBox?.({
976
- prompt: 'Agent name for gx branch start',
1330
+ prompt: 'Agent name for the Guardex agent launcher',
977
1331
  placeHolder: 'codex',
978
1332
  value: 'codex',
979
1333
  ignoreFocusOut: true,
@@ -1005,10 +1359,7 @@ async function startAgentFromPrompt(refresh) {
1005
1359
  cwd: repoRoot,
1006
1360
  });
1007
1361
  terminal?.show(true);
1008
- terminal?.sendText(
1009
- `gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`,
1010
- true,
1011
- );
1362
+ terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true);
1012
1363
  refresh();
1013
1364
  }
1014
1365
 
@@ -1047,11 +1398,20 @@ function commitWorktree(worktreePath, message) {
1047
1398
  function buildActiveAgentGroupNodes(sessions) {
1048
1399
  const groups = [];
1049
1400
  for (const group of SESSION_ACTIVITY_GROUPS) {
1050
- const groupSessions = sessions
1051
- .filter((session) => session.activityKind === group.kind)
1052
- .map((session) => new SessionItem(session));
1053
- if (groupSessions.length > 0) {
1054
- groups.push(new SectionItem(group.label, groupSessions));
1401
+ const groupSessions = sessions.filter((session) => session.activityKind === group.kind);
1402
+ const worktreeItems = groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => (
1403
+ new WorktreeItem(
1404
+ worktreePath,
1405
+ worktreeSessions,
1406
+ worktreeSessions.map((session) => new SessionItem(
1407
+ session,
1408
+ buildChangeTreeNodes(session.touchedChanges || []),
1409
+ { label: sessionTreeLabel(session) },
1410
+ )),
1411
+ )
1412
+ ));
1413
+ if (worktreeItems.length > 0) {
1414
+ groups.push(new SectionItem(group.label, worktreeItems));
1055
1415
  }
1056
1416
  }
1057
1417
 
@@ -1072,6 +1432,7 @@ class ActiveAgentsProvider {
1072
1432
  sessionCount: 0,
1073
1433
  workingCount: 0,
1074
1434
  deadCount: 0,
1435
+ conflictCount: 0,
1075
1436
  };
1076
1437
  }
1077
1438
 
@@ -1118,7 +1479,7 @@ class ActiveAgentsProvider {
1118
1479
  this.setSelectedSession(nextSession || null);
1119
1480
  }
1120
1481
 
1121
- updateViewState(sessionCount, workingCount, deadCount) {
1482
+ updateViewState(sessionCount, workingCount, deadCount, conflictCount = 0) {
1122
1483
  if (!this.treeView) {
1123
1484
  return;
1124
1485
  }
@@ -1128,7 +1489,10 @@ class ActiveAgentsProvider {
1128
1489
  sessionCount,
1129
1490
  workingCount,
1130
1491
  deadCount,
1492
+ conflictCount,
1131
1493
  };
1494
+ void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0);
1495
+ void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0);
1132
1496
  const badgeTooltipParts = [];
1133
1497
  if (activeCount > 0) {
1134
1498
  badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
@@ -1139,6 +1503,9 @@ class ActiveAgentsProvider {
1139
1503
  if (workingCount > 0) {
1140
1504
  badgeTooltipParts.push(`${workingCount} working now`);
1141
1505
  }
1506
+ if (conflictCount > 0) {
1507
+ badgeTooltipParts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
1508
+ }
1142
1509
 
1143
1510
  this.treeView.badge = sessionCount > 0
1144
1511
  ? {
@@ -1146,9 +1513,7 @@ class ActiveAgentsProvider {
1146
1513
  tooltip: badgeTooltipParts.join(' · '),
1147
1514
  }
1148
1515
  : undefined;
1149
- this.treeView.message = sessionCount > 0
1150
- ? undefined
1151
- : 'Start a sandbox session to populate this view.';
1516
+ this.treeView.message = undefined;
1152
1517
  }
1153
1518
 
1154
1519
  async syncRepoEntries() {
@@ -1162,8 +1527,12 @@ class ActiveAgentsProvider {
1162
1527
  (total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
1163
1528
  0,
1164
1529
  );
1530
+ const conflictCount = repoEntries.reduce(
1531
+ (total, entry) => total + countEntryConflicts(entry),
1532
+ 0,
1533
+ );
1165
1534
 
1166
- this.updateViewState(sessionCount, workingCount, deadCount);
1535
+ this.updateViewState(sessionCount, workingCount, deadCount, conflictCount);
1167
1536
  this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
1168
1537
  this.decorationProvider?.updateLockEntries(repoEntries);
1169
1538
  return repoEntries;
@@ -1189,20 +1558,6 @@ class ActiveAgentsProvider {
1189
1558
  this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
1190
1559
  }
1191
1560
 
1192
- readLockRegistryForRepo(repoRoot) {
1193
- const lockRegistry = readLockRegistry(repoRoot);
1194
- this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
1195
- return lockRegistry;
1196
- }
1197
-
1198
- getLockRegistryForRepo(repoRoot) {
1199
- return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
1200
- }
1201
-
1202
- refreshLockRegistryForFile(filePath) {
1203
- this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
1204
- }
1205
-
1206
1561
  async getChildren(element) {
1207
1562
  if (element instanceof RepoItem) {
1208
1563
  const sectionItems = [
@@ -1218,7 +1573,7 @@ class ActiveAgentsProvider {
1218
1573
  return sectionItems;
1219
1574
  }
1220
1575
 
1221
- if (element instanceof SectionItem || element instanceof FolderItem || element instanceof SessionItem) {
1576
+ if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) {
1222
1577
  return element.items;
1223
1578
  }
1224
1579
 
@@ -1250,9 +1605,95 @@ class ActiveAgentsProvider {
1250
1605
  }
1251
1606
  }
1252
1607
 
1608
+ function countEntryConflicts(entry) {
1609
+ const sessionConflicts = entry.sessions.reduce(
1610
+ (total, session) => total + (session.conflictCount || 0),
1611
+ 0,
1612
+ );
1613
+ const changeConflicts = entry.changes.filter((change) => change.hasForeignLock).length;
1614
+ return sessionConflicts + changeConflicts;
1615
+ }
1616
+
1617
+ class SessionInspectPanelManager {
1618
+ constructor() {
1619
+ this.panel = null;
1620
+ this.session = null;
1621
+ }
1622
+
1623
+ open(session) {
1624
+ const targetSession = session?.branch ? { ...session } : null;
1625
+ if (!targetSession?.repoRoot || !targetSession?.branch) {
1626
+ showSessionMessage('Pick an Active Agents session first.');
1627
+ return;
1628
+ }
1629
+ if (!vscode.window.createWebviewPanel) {
1630
+ showSessionMessage('Inspect panel is unavailable in this VS Code build.');
1631
+ return;
1632
+ }
1633
+
1634
+ this.session = targetSession;
1635
+ if (!this.panel) {
1636
+ this.panel = vscode.window.createWebviewPanel(
1637
+ INSPECT_PANEL_VIEW_TYPE,
1638
+ inspectPanelTitle(targetSession),
1639
+ vscode.ViewColumn?.Beside,
1640
+ {
1641
+ enableFindWidget: true,
1642
+ enableScripts: false,
1643
+ retainContextWhenHidden: true,
1644
+ },
1645
+ );
1646
+ this.panel.onDidDispose(() => {
1647
+ this.panel = null;
1648
+ this.session = null;
1649
+ });
1650
+ } else {
1651
+ this.panel.reveal?.(vscode.ViewColumn?.Beside);
1652
+ }
1653
+
1654
+ this.render();
1655
+ }
1656
+
1657
+ resolveSession() {
1658
+ if (!this.session?.repoRoot || !this.session?.branch) {
1659
+ return this.session ? { ...this.session } : null;
1660
+ }
1661
+
1662
+ return readActiveSessions(this.session.repoRoot, { includeStale: true })
1663
+ .find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(this.session))
1664
+ || { ...this.session };
1665
+ }
1666
+
1667
+ render() {
1668
+ if (!this.panel || !this.session) {
1669
+ return;
1670
+ }
1671
+
1672
+ const session = this.resolveSession();
1673
+ if (!session) {
1674
+ return;
1675
+ }
1676
+
1677
+ this.session = { ...session };
1678
+ this.panel.title = inspectPanelTitle(session);
1679
+ this.panel.webview.html = renderInspectPanelHtml(session, readSessionInspectData(session));
1680
+ }
1681
+
1682
+ refresh() {
1683
+ this.render();
1684
+ }
1685
+
1686
+ dispose() {
1687
+ this.panel?.dispose();
1688
+ this.panel = null;
1689
+ this.session = null;
1690
+ }
1691
+ }
1692
+
1253
1693
  class ActiveAgentsRefreshController {
1254
- constructor(provider) {
1694
+ constructor(provider, inspectPanelManager = null) {
1255
1695
  this.provider = provider;
1696
+ this.inspectPanelManager = inspectPanelManager;
1256
1697
  this.refreshTimer = null;
1257
1698
  this.sessionWatchers = new Map();
1258
1699
  }
@@ -1270,6 +1711,7 @@ class ActiveAgentsRefreshController {
1270
1711
  async refreshNow() {
1271
1712
  await this.syncSessionWatchers();
1272
1713
  await this.provider.refresh();
1714
+ this.inspectPanelManager?.refresh();
1273
1715
  }
1274
1716
 
1275
1717
  async syncSessionWatchers() {
@@ -1320,7 +1762,8 @@ class ActiveAgentsRefreshController {
1320
1762
  function activate(context) {
1321
1763
  const decorationProvider = new SessionDecorationProvider();
1322
1764
  const provider = new ActiveAgentsProvider(decorationProvider);
1323
- const refreshController = new ActiveAgentsRefreshController(provider);
1765
+ const inspectPanelManager = new SessionInspectPanelManager();
1766
+ const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager);
1324
1767
  const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
1325
1768
  treeDataProvider: provider,
1326
1769
  showCollapseAll: true,
@@ -1338,6 +1781,7 @@ function activate(context) {
1338
1781
  const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
1339
1782
  const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
1340
1783
  const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
1784
+ const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB);
1341
1785
  const updateCommitInput = (session) => {
1342
1786
  sourceControl.inputBox.enabled = true;
1343
1787
  sourceControl.inputBox.visible = true;
@@ -1397,7 +1841,7 @@ function activate(context) {
1397
1841
  command: 'gitguardex.activeAgents.commitSelectedSession',
1398
1842
  title: 'Commit Selected Session',
1399
1843
  };
1400
- const interval = setInterval(refresh, 5_000);
1844
+ const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS);
1401
1845
  const refreshLockRegistry = (uri) => {
1402
1846
  if (uri?.fsPath) {
1403
1847
  provider.refreshLockRegistryForFile(uri.fsPath);
@@ -1419,6 +1863,7 @@ function activate(context) {
1419
1863
  treeView,
1420
1864
  sourceControl,
1421
1865
  activeAgentsStatusItem,
1866
+ inspectPanelManager,
1422
1867
  refreshController,
1423
1868
  vscode.window.registerFileDecorationProvider(decorationProvider),
1424
1869
  vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
@@ -1450,6 +1895,9 @@ function activate(context) {
1450
1895
 
1451
1896
  await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
1452
1897
  }),
1898
+ vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
1899
+ inspectPanelManager.open(session || provider.getSelectedSession());
1900
+ }),
1453
1901
  vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
1454
1902
  vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
1455
1903
  vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
@@ -1458,6 +1906,7 @@ function activate(context) {
1458
1906
  activeSessionsWatcher,
1459
1907
  lockWatcher,
1460
1908
  worktreeLockWatcher,
1909
+ logWatcher,
1461
1910
  { dispose: () => clearInterval(interval) },
1462
1911
  );
1463
1912
 
@@ -1465,6 +1914,7 @@ function activate(context) {
1465
1914
  ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
1466
1915
  ...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
1467
1916
  ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
1917
+ ...bindRefreshWatcher(logWatcher, scheduleRefresh),
1468
1918
  );
1469
1919
  void refreshController.refreshNow();
1470
1920
  void maybeAutoUpdateActiveAgentsExtension(context);