@imdeadpool/guardex 7.0.21 → 7.0.23

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,21 +17,28 @@ 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;
22
24
  const REFRESH_DEBOUNCE_MS = 250;
25
+ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
26
+ const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
27
+ const RELOAD_WINDOW_ACTION = 'Reload Window';
28
+ const UPDATE_LATER_ACTION = 'Later';
29
+ const REFRESH_POLL_INTERVAL_MS = 30_000;
30
+ const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
23
31
  const SESSION_ACTIVITY_GROUPS = [
24
32
  { kind: 'blocked', label: 'BLOCKED' },
25
33
  { kind: 'working', label: 'WORKING NOW' },
26
- { kind: 'idle', label: 'IDLE' },
34
+ { kind: 'idle', label: 'THINKING' },
27
35
  { kind: 'stalled', label: 'STALLED' },
28
36
  { kind: 'dead', label: 'DEAD' },
29
37
  ];
30
38
  const SESSION_ACTIVITY_ICON_IDS = {
31
39
  blocked: 'warning',
32
- working: 'edit',
33
- idle: 'loading~spin',
40
+ working: 'loading~spin',
41
+ idle: 'comment-discussion',
34
42
  stalled: 'clock',
35
43
  dead: 'error',
36
44
  };
@@ -93,10 +101,211 @@ function sessionIdleDecoration(session, now = Date.now()) {
93
101
  return undefined;
94
102
  }
95
103
 
104
+ function formatCountLabel(count, singular, plural = `${singular}s`) {
105
+ return `${count} ${count === 1 ? singular : plural}`;
106
+ }
107
+
108
+ function sessionIdentityLabel(session) {
109
+ const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
110
+ const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
111
+ const label = typeof session?.label === 'string' ? session.label.trim() : '';
112
+
113
+ if (agentName && taskName) {
114
+ return `${agentName} · ${taskName}`;
115
+ }
116
+ if (agentName && label) {
117
+ return `${agentName} · ${label}`;
118
+ }
119
+
120
+ return agentName || taskName || label || 'session';
121
+ }
122
+
123
+ function sessionCommitPlaceholder(session) {
124
+ if (!session?.branch) {
125
+ return 'Pick an Active Agents session to commit its worktree.';
126
+ }
127
+
128
+ return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')} (Ctrl+Enter)`;
129
+ }
130
+
131
+ function agentNameFromBranch(branch) {
132
+ const segments = String(branch || '')
133
+ .split('/')
134
+ .map((segment) => segment.trim())
135
+ .filter(Boolean);
136
+ if (segments[0] === 'agent' && segments[1]) {
137
+ return segments[1];
138
+ }
139
+ return segments[0] || 'lock';
140
+ }
141
+
142
+ function agentBadgeFromBranch(branch) {
143
+ const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, '');
144
+ return normalized.slice(0, 2) || 'LK';
145
+ }
146
+
147
+ function buildActiveAgentsStatusSummary(summary) {
148
+ const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
149
+ if (activeCount > 0) {
150
+ return `$(git-branch) ${formatCountLabel(activeCount, 'active agent')}`;
151
+ }
152
+ return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
153
+ }
154
+
155
+ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
156
+ if (selectedSession?.branch) {
157
+ return [
158
+ selectedSession.branch,
159
+ sessionIdentityLabel(selectedSession),
160
+ formatCountLabel(selectedSession.lockCount || 0, 'lock'),
161
+ selectedSession.worktreePath,
162
+ 'Click to open Source Control.',
163
+ ].filter(Boolean).join('\n');
164
+ }
165
+
166
+ const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
167
+ return [
168
+ formatCountLabel(activeCount, 'active agent'),
169
+ formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
170
+ summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
171
+ 'Click to open Source Control.',
172
+ ].filter(Boolean).join('\n');
173
+ }
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
+
96
303
  class SessionDecorationProvider {
97
304
  constructor(nowProvider = () => Date.now()) {
98
305
  this.nowProvider = nowProvider;
99
306
  this.sessionsByUri = new Map();
307
+ this.lockEntriesByFileUri = new Map();
308
+ this.selectedBranch = '';
100
309
  this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
101
310
  this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
102
311
  }
@@ -107,13 +316,54 @@ class SessionDecorationProvider {
107
316
  );
108
317
  }
109
318
 
319
+ updateLockEntries(repoEntries) {
320
+ const nextEntriesByUri = new Map();
321
+ for (const entry of repoEntries || []) {
322
+ for (const [relativePath, lockEntry] of entry.lockEntries || []) {
323
+ nextEntriesByUri.set(
324
+ vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(),
325
+ { branch: lockEntry.branch },
326
+ );
327
+ }
328
+ }
329
+ this.lockEntriesByFileUri = nextEntriesByUri;
330
+ }
331
+
332
+ setSelectedBranch(branch) {
333
+ this.selectedBranch = typeof branch === 'string' ? branch.trim() : '';
334
+ }
335
+
110
336
  refresh() {
111
337
  this.onDidChangeFileDecorationsEmitter.fire();
112
338
  }
113
339
 
114
340
  provideFileDecoration(uri) {
115
341
  if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
116
- return undefined;
342
+ if (!uri || uri.scheme !== 'file') {
343
+ return undefined;
344
+ }
345
+
346
+ const lockEntry = this.lockEntriesByFileUri.get(uri.toString());
347
+ if (!lockEntry?.branch) {
348
+ return undefined;
349
+ }
350
+
351
+ const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch;
352
+ return {
353
+ badge: agentBadgeFromBranch(lockEntry.branch),
354
+ tooltip: ownsSelectedSession
355
+ ? `Locked by selected session ${lockEntry.branch}`
356
+ : this.selectedBranch
357
+ ? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})`
358
+ : `Locked by ${lockEntry.branch}`,
359
+ color: new vscode.ThemeColor(
360
+ ownsSelectedSession
361
+ ? 'gitDecoration.modifiedResourceForeground'
362
+ : this.selectedBranch
363
+ ? 'list.errorForeground'
364
+ : 'list.warningForeground',
365
+ ),
366
+ };
117
367
  }
118
368
 
119
369
  return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
@@ -147,8 +397,9 @@ class RepoItem extends vscode.TreeItem {
147
397
  if (workingCount > 0) {
148
398
  descriptionParts.push(`${workingCount} working`);
149
399
  }
150
- if (changes.length > 0) {
151
- descriptionParts.push(`${changes.length} changed`);
400
+ const changedCount = countChangedPaths(repoRoot, sessions, changes);
401
+ if (changedCount > 0) {
402
+ descriptionParts.push(`${changedCount} changed`);
152
403
  }
153
404
  this.description = descriptionParts.join(' · ');
154
405
  this.tooltip = [
@@ -170,11 +421,49 @@ class SectionItem extends vscode.TreeItem {
170
421
  }
171
422
  }
172
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
+
173
459
  class SessionItem extends vscode.TreeItem {
174
- constructor(session, items = []) {
460
+ constructor(session, items = [], options = {}) {
175
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;
176
465
  super(
177
- `${session.label} 🔒 ${lockCount}`,
466
+ label,
178
467
  items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
179
468
  );
180
469
  this.session = session;
@@ -185,6 +474,9 @@ class SessionItem extends vscode.TreeItem {
185
474
  descriptionParts.push(session.activityCountLabel);
186
475
  }
187
476
  descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
477
+ if (lockCount > 0) {
478
+ descriptionParts.push(`${lockCount} $(lock)`);
479
+ }
188
480
  this.description = descriptionParts.join(' · ');
189
481
  const tooltipLines = [
190
482
  session.branch,
@@ -197,6 +489,7 @@ class SessionItem extends vscode.TreeItem {
197
489
  ? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
198
490
  : session.activitySummary,
199
491
  `Locks ${lockCount}`,
492
+ session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
200
493
  Number.isInteger(session.pid) && session.pid > 0
201
494
  ? session.pidAlive === false
202
495
  ? `PID ${session.pid} not alive`
@@ -260,10 +553,39 @@ function shellQuote(value) {
260
553
  return `'${normalized.replace(/'/g, "'\"'\"'")}'`;
261
554
  }
262
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
+
263
581
  function sessionDisplayLabel(session) {
264
582
  return session?.taskName || session?.label || session?.branch || path.basename(session?.worktreePath || '') || 'session';
265
583
  }
266
584
 
585
+ function sessionTreeLabel(session) {
586
+ return session?.branch || sessionDisplayLabel(session);
587
+ }
588
+
267
589
  function sessionWorktreePath(session) {
268
590
  return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
269
591
  }
@@ -317,16 +639,34 @@ function syncSession(session) {
317
639
  runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
318
640
  }
319
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
+
320
656
  async function stopSession(session, refresh) {
321
657
  const pid = Number(session?.pid);
322
658
  if (!Number.isInteger(pid) || pid <= 0) {
323
659
  showSessionMessage('Cannot stop session: missing pid.');
324
660
  return;
325
661
  }
662
+ if (!session?.branch) {
663
+ showSessionMessage('Cannot stop session: missing branch name.');
664
+ return;
665
+ }
326
666
 
327
667
  const confirmed = await vscode.window.showWarningMessage(
328
668
  `Stop ${sessionDisplayLabel(session)}?`,
329
- { modal: true, detail: `Send SIGTERM to pid ${pid}.` },
669
+ { modal: true, detail: `Run gx agents stop --pid ${pid}.` },
330
670
  'Stop',
331
671
  );
332
672
  if (confirmed !== 'Stop') {
@@ -334,42 +674,87 @@ async function stopSession(session, refresh) {
334
674
  }
335
675
 
336
676
  try {
337
- 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
+ });
338
687
  refresh();
339
688
  } catch (error) {
340
689
  showSessionMessage(
341
- `Failed to stop session ${sessionDisplayLabel(session)}: ${error?.message || String(error)}`,
690
+ `Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`,
342
691
  );
343
692
  }
344
693
  }
345
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
+
346
734
  async function openSessionDiff(session) {
347
735
  const worktreePath = ensureSessionWorktree(session, 'open diff');
348
736
  if (!worktreePath) {
349
737
  return;
350
738
  }
351
739
 
352
- let diffOutput = '';
353
- try {
354
- diffOutput = cp.execFileSync('git', ['-C', worktreePath, 'diff'], {
355
- encoding: 'utf8',
356
- stdio: ['ignore', 'pipe', 'pipe'],
357
- });
358
- } catch (error) {
359
- const detail = [
360
- error?.stdout,
361
- error?.stderr,
362
- error?.message,
363
- ].find((value) => typeof value === 'string' && value.trim().length > 0) || 'git diff failed.';
364
- 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)}.`);
365
743
  return;
366
744
  }
367
745
 
368
- const document = await vscode.workspace.openTextDocument({
369
- language: 'diff',
370
- content: diffOutput,
371
- });
372
- 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
+ }
373
758
  }
374
759
 
375
760
  function repoRootFromSessionFile(filePath) {
@@ -451,10 +836,127 @@ function readCurrentBranch(repoRoot) {
451
836
  }
452
837
  }
453
838
 
839
+ function parseSimpleSemver(version) {
840
+ const parts = String(version || '')
841
+ .split('.')
842
+ .map((part) => Number.parseInt(part, 10));
843
+ if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
844
+ return null;
845
+ }
846
+ return parts;
847
+ }
848
+
849
+ function compareSimpleSemver(left, right) {
850
+ const leftParts = parseSimpleSemver(left);
851
+ const rightParts = parseSimpleSemver(right);
852
+ if (!leftParts || !rightParts) {
853
+ return 0;
854
+ }
855
+
856
+ for (let index = 0; index < leftParts.length; index += 1) {
857
+ if (leftParts[index] !== rightParts[index]) {
858
+ return leftParts[index] - rightParts[index];
859
+ }
860
+ }
861
+
862
+ return 0;
863
+ }
864
+
865
+ function readJsonFile(filePath) {
866
+ try {
867
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
868
+ } catch (_error) {
869
+ return null;
870
+ }
871
+ }
872
+
873
+ function resolveActiveAgentsAutoUpdateCandidate(installedVersion) {
874
+ const candidates = [];
875
+
876
+ for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
877
+ const repoRoot = workspaceFolder?.uri?.fsPath;
878
+ if (!repoRoot) {
879
+ continue;
880
+ }
881
+
882
+ const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE);
883
+ const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE);
884
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) {
885
+ continue;
886
+ }
887
+
888
+ const manifest = readJsonFile(manifestPath);
889
+ const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : '';
890
+ if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) {
891
+ continue;
892
+ }
893
+
894
+ candidates.push({ repoRoot, installScriptPath, version: nextVersion });
895
+ }
896
+
897
+ candidates.sort((left, right) => compareSimpleSemver(right.version, left.version));
898
+ return candidates[0] || null;
899
+ }
900
+
901
+ function runActiveAgentsInstallScript(repoRoot, installScriptPath) {
902
+ return new Promise((resolve, reject) => {
903
+ cp.execFile(
904
+ process.execPath,
905
+ [installScriptPath],
906
+ { cwd: repoRoot, encoding: 'utf8' },
907
+ (error, stdout, stderr) => {
908
+ if (error) {
909
+ reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed'));
910
+ return;
911
+ }
912
+ resolve({ stdout, stderr });
913
+ },
914
+ );
915
+ });
916
+ }
917
+
918
+ async function maybeAutoUpdateActiveAgentsExtension(context) {
919
+ const installedVersion = typeof context?.extension?.packageJSON?.version === 'string'
920
+ ? context.extension.packageJSON.version.trim()
921
+ : '';
922
+ if (!installedVersion) {
923
+ return;
924
+ }
925
+
926
+ const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion);
927
+ if (!candidate) {
928
+ return;
929
+ }
930
+
931
+ try {
932
+ await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath);
933
+ } catch (error) {
934
+ const failure = typeof error?.message === 'string' && error.message.trim()
935
+ ? error.message.trim()
936
+ : 'install failed';
937
+ vscode.window.showWarningMessage?.(
938
+ `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`,
939
+ );
940
+ return;
941
+ }
942
+
943
+ const selection = await vscode.window.showInformationMessage?.(
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.`,
945
+ RELOAD_WINDOW_ACTION,
946
+ UPDATE_LATER_ACTION,
947
+ );
948
+ if (selection === RELOAD_WINDOW_ACTION) {
949
+ await vscode.commands.executeCommand('workbench.action.reloadWindow');
950
+ }
951
+ }
952
+
454
953
  function decorateSession(session, lockRegistry) {
954
+ const touchedChanges = buildSessionTouchedChanges(session, lockRegistry);
455
955
  return {
456
956
  ...session,
457
957
  lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
958
+ touchedChanges,
959
+ conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length,
458
960
  };
459
961
  }
460
962
 
@@ -468,6 +970,28 @@ function decorateChange(change, lockRegistry, owningBranch) {
468
970
  };
469
971
  }
470
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
+
471
995
  function isPathWithin(parentPath, targetPath) {
472
996
  const relativePath = path.relative(parentPath, targetPath);
473
997
  return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
@@ -644,6 +1168,61 @@ function countWorkingSessions(sessions) {
644
1168
  return sessions.filter((session) => session.activityKind === 'working').length;
645
1169
  }
646
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
+
647
1226
  function buildGroupedChangeTreeNodes(sessions, changes) {
648
1227
  const changesBySession = new Map();
649
1228
  const sessionByChangedPath = new Map();
@@ -676,15 +1255,22 @@ function buildGroupedChangeTreeNodes(sessions, changes) {
676
1255
  changesBySession.get(session.branch).push(localizedChange);
677
1256
  }
678
1257
 
679
- const items = sessions
680
- .map((session) => {
681
- const sessionChanges = changesBySession.get(session.branch) || [];
682
- if (sessionChanges.length === 0) {
683
- return null;
684
- }
685
- return new SessionItem(session, buildChangeTreeNodes(sessionChanges));
686
- })
687
- .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
+ });
688
1274
 
689
1275
  if (repoRootChanges.length > 0) {
690
1276
  items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
@@ -724,14 +1310,14 @@ async function pickRepoRoot() {
724
1310
  repoRoot: folder.uri.fsPath,
725
1311
  }));
726
1312
  const selection = await vscode.window.showQuickPick?.(picks, {
727
- placeHolder: 'Select the Guardex repo where gx branch start should run.',
1313
+ placeHolder: 'Select the Guardex repo where the Start agent launcher should run.',
728
1314
  });
729
1315
  return selection?.repoRoot || null;
730
1316
  }
731
1317
 
732
1318
  async function promptStartAgentDetails() {
733
1319
  const taskName = await vscode.window.showInputBox?.({
734
- prompt: 'Task for gx branch start',
1320
+ prompt: 'Task for the Guardex agent launcher',
735
1321
  placeHolder: 'vscode active agents welcome view',
736
1322
  ignoreFocusOut: true,
737
1323
  validateInput: (value) => value.trim() ? undefined : 'Task is required.',
@@ -741,7 +1327,7 @@ async function promptStartAgentDetails() {
741
1327
  }
742
1328
 
743
1329
  const agentName = await vscode.window.showInputBox?.({
744
- prompt: 'Agent name for gx branch start',
1330
+ prompt: 'Agent name for the Guardex agent launcher',
745
1331
  placeHolder: 'codex',
746
1332
  value: 'codex',
747
1333
  ignoreFocusOut: true,
@@ -773,10 +1359,7 @@ async function startAgentFromPrompt(refresh) {
773
1359
  cwd: repoRoot,
774
1360
  });
775
1361
  terminal?.show(true);
776
- terminal?.sendText(
777
- `gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`,
778
- true,
779
- );
1362
+ terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true);
780
1363
  refresh();
781
1364
  }
782
1365
 
@@ -815,11 +1398,20 @@ function commitWorktree(worktreePath, message) {
815
1398
  function buildActiveAgentGroupNodes(sessions) {
816
1399
  const groups = [];
817
1400
  for (const group of SESSION_ACTIVITY_GROUPS) {
818
- const groupSessions = sessions
819
- .filter((session) => session.activityKind === group.kind)
820
- .map((session) => new SessionItem(session));
821
- if (groupSessions.length > 0) {
822
- 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));
823
1415
  }
824
1416
  }
825
1417
 
@@ -836,6 +1428,12 @@ class ActiveAgentsProvider {
836
1428
  this.treeView = null;
837
1429
  this.lockRegistryByRepoRoot = new Map();
838
1430
  this.selectedSession = null;
1431
+ this.viewSummary = {
1432
+ sessionCount: 0,
1433
+ workingCount: 0,
1434
+ deadCount: 0,
1435
+ conflictCount: 0,
1436
+ };
839
1437
  }
840
1438
 
841
1439
  getTreeItem(element) {
@@ -856,6 +1454,7 @@ class ActiveAgentsProvider {
856
1454
  const currentKey = sessionSelectionKey(this.selectedSession);
857
1455
  const nextKey = sessionSelectionKey(nextSession);
858
1456
  this.selectedSession = nextSession;
1457
+ this.decorationProvider?.setSelectedBranch(nextSession?.branch || '');
859
1458
  if (currentKey !== nextKey) {
860
1459
  this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
861
1460
  }
@@ -865,6 +1464,10 @@ class ActiveAgentsProvider {
865
1464
  return this.selectedSession ? { ...this.selectedSession } : null;
866
1465
  }
867
1466
 
1467
+ getViewSummary() {
1468
+ return { ...this.viewSummary };
1469
+ }
1470
+
868
1471
  syncSelectedSession(repoEntries) {
869
1472
  if (!this.selectedSession) {
870
1473
  return;
@@ -876,12 +1479,20 @@ class ActiveAgentsProvider {
876
1479
  this.setSelectedSession(nextSession || null);
877
1480
  }
878
1481
 
879
- updateViewState(sessionCount, workingCount, deadCount) {
1482
+ updateViewState(sessionCount, workingCount, deadCount, conflictCount = 0) {
880
1483
  if (!this.treeView) {
881
1484
  return;
882
1485
  }
883
1486
 
884
1487
  const activeCount = Math.max(0, sessionCount - deadCount);
1488
+ this.viewSummary = {
1489
+ sessionCount,
1490
+ workingCount,
1491
+ deadCount,
1492
+ conflictCount,
1493
+ };
1494
+ void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0);
1495
+ void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0);
885
1496
  const badgeTooltipParts = [];
886
1497
  if (activeCount > 0) {
887
1498
  badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
@@ -892,6 +1503,9 @@ class ActiveAgentsProvider {
892
1503
  if (workingCount > 0) {
893
1504
  badgeTooltipParts.push(`${workingCount} working now`);
894
1505
  }
1506
+ if (conflictCount > 0) {
1507
+ badgeTooltipParts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
1508
+ }
895
1509
 
896
1510
  this.treeView.badge = sessionCount > 0
897
1511
  ? {
@@ -899,9 +1513,7 @@ class ActiveAgentsProvider {
899
1513
  tooltip: badgeTooltipParts.join(' · '),
900
1514
  }
901
1515
  : undefined;
902
- this.treeView.message = sessionCount > 0
903
- ? undefined
904
- : 'Start a sandbox session to populate this view.';
1516
+ this.treeView.message = undefined;
905
1517
  }
906
1518
 
907
1519
  async syncRepoEntries() {
@@ -915,9 +1527,14 @@ class ActiveAgentsProvider {
915
1527
  (total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
916
1528
  0,
917
1529
  );
1530
+ const conflictCount = repoEntries.reduce(
1531
+ (total, entry) => total + countEntryConflicts(entry),
1532
+ 0,
1533
+ );
918
1534
 
919
- this.updateViewState(sessionCount, workingCount, deadCount);
1535
+ this.updateViewState(sessionCount, workingCount, deadCount, conflictCount);
920
1536
  this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
1537
+ this.decorationProvider?.updateLockEntries(repoEntries);
921
1538
  return repoEntries;
922
1539
  }
923
1540
 
@@ -941,20 +1558,6 @@ class ActiveAgentsProvider {
941
1558
  this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
942
1559
  }
943
1560
 
944
- readLockRegistryForRepo(repoRoot) {
945
- const lockRegistry = readLockRegistry(repoRoot);
946
- this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
947
- return lockRegistry;
948
- }
949
-
950
- getLockRegistryForRepo(repoRoot) {
951
- return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
952
- }
953
-
954
- refreshLockRegistryForFile(filePath) {
955
- this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
956
- }
957
-
958
1561
  async getChildren(element) {
959
1562
  if (element instanceof RepoItem) {
960
1563
  const sectionItems = [
@@ -970,7 +1573,7 @@ class ActiveAgentsProvider {
970
1573
  return sectionItems;
971
1574
  }
972
1575
 
973
- if (element instanceof SectionItem || element instanceof FolderItem || element instanceof SessionItem) {
1576
+ if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) {
974
1577
  return element.items;
975
1578
  }
976
1579
 
@@ -996,14 +1599,101 @@ class ActiveAgentsProvider {
996
1599
  changes: readRepoChanges(repoRoot).map((change) => (
997
1600
  decorateChange(change, lockRegistry, currentBranch)
998
1601
  )),
1602
+ lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
999
1603
  };
1000
1604
  });
1001
1605
  }
1002
1606
  }
1003
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
+
1004
1693
  class ActiveAgentsRefreshController {
1005
- constructor(provider) {
1694
+ constructor(provider, inspectPanelManager = null) {
1006
1695
  this.provider = provider;
1696
+ this.inspectPanelManager = inspectPanelManager;
1007
1697
  this.refreshTimer = null;
1008
1698
  this.sessionWatchers = new Map();
1009
1699
  }
@@ -1021,6 +1711,7 @@ class ActiveAgentsRefreshController {
1021
1711
  async refreshNow() {
1022
1712
  await this.syncSessionWatchers();
1023
1713
  await this.provider.refresh();
1714
+ this.inspectPanelManager?.refresh();
1024
1715
  }
1025
1716
 
1026
1717
  async syncSessionWatchers() {
@@ -1071,7 +1762,8 @@ class ActiveAgentsRefreshController {
1071
1762
  function activate(context) {
1072
1763
  const decorationProvider = new SessionDecorationProvider();
1073
1764
  const provider = new ActiveAgentsProvider(decorationProvider);
1074
- const refreshController = new ActiveAgentsRefreshController(provider);
1765
+ const inspectPanelManager = new SessionInspectPanelManager();
1766
+ const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager);
1075
1767
  const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
1076
1768
  treeDataProvider: provider,
1077
1769
  showCollapseAll: true,
@@ -1080,20 +1772,37 @@ function activate(context) {
1080
1772
  'gitguardex.activeAgents.commitInput',
1081
1773
  'Active Agents Commit',
1082
1774
  );
1775
+ const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
1776
+ activeAgentsStatusItem.name = 'GitGuardex Active Agents';
1777
+ activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
1083
1778
  provider.attachTreeView(treeView);
1084
1779
  const scheduleRefresh = () => refreshController.scheduleRefresh();
1085
1780
  const refresh = () => void refreshController.refreshNow();
1086
1781
  const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
1087
1782
  const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
1088
1783
  const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
1784
+ const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB);
1089
1785
  const updateCommitInput = (session) => {
1090
1786
  sourceControl.inputBox.enabled = true;
1091
1787
  sourceControl.inputBox.visible = true;
1092
- sourceControl.inputBox.placeholder = session?.label
1093
- ? `Commit ${session.label} (Ctrl+Enter)`
1094
- : 'Pick an Active Agents session to commit its worktree.';
1788
+ sourceControl.inputBox.placeholder = sessionCommitPlaceholder(session);
1789
+ };
1790
+ const updateStatusBar = () => {
1791
+ const selectedSession = provider.getSelectedSession();
1792
+ const summary = provider.getViewSummary();
1793
+ if ((summary.sessionCount || 0) <= 0) {
1794
+ activeAgentsStatusItem.hide();
1795
+ return;
1796
+ }
1797
+
1798
+ activeAgentsStatusItem.text = selectedSession?.branch
1799
+ ? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}`
1800
+ : buildActiveAgentsStatusSummary(summary);
1801
+ activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary);
1802
+ activeAgentsStatusItem.show();
1095
1803
  };
1096
1804
  updateCommitInput(null);
1805
+ updateStatusBar();
1097
1806
  const commitSelectedSession = async () => {
1098
1807
  const selectedSession = provider.getSelectedSession();
1099
1808
  if (!selectedSession?.worktreePath) {
@@ -1132,7 +1841,7 @@ function activate(context) {
1132
1841
  command: 'gitguardex.activeAgents.commitSelectedSession',
1133
1842
  title: 'Commit Selected Session',
1134
1843
  };
1135
- const interval = setInterval(refresh, 5_000);
1844
+ const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS);
1136
1845
  const refreshLockRegistry = (uri) => {
1137
1846
  if (uri?.fsPath) {
1138
1847
  provider.refreshLockRegistryForFile(uri.fsPath);
@@ -1140,15 +1849,28 @@ function activate(context) {
1140
1849
  scheduleRefresh();
1141
1850
  };
1142
1851
 
1143
- provider.onDidChangeSelectedSession(updateCommitInput);
1852
+ provider.onDidChangeSelectedSession((session) => {
1853
+ updateCommitInput(session);
1854
+ updateStatusBar();
1855
+ decorationProvider.refresh();
1856
+ });
1857
+ provider.onDidChangeTreeData(() => {
1858
+ updateCommitInput(provider.getSelectedSession());
1859
+ updateStatusBar();
1860
+ });
1144
1861
 
1145
1862
  context.subscriptions.push(
1146
1863
  treeView,
1147
1864
  sourceControl,
1865
+ activeAgentsStatusItem,
1866
+ inspectPanelManager,
1148
1867
  refreshController,
1149
1868
  vscode.window.registerFileDecorationProvider(decorationProvider),
1150
1869
  vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
1151
1870
  vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
1871
+ vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
1872
+ await vscode.commands.executeCommand('workbench.view.scm');
1873
+ }),
1152
1874
  vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
1153
1875
  vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
1154
1876
  if (!session?.worktreePath) {
@@ -1173,6 +1895,9 @@ function activate(context) {
1173
1895
 
1174
1896
  await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
1175
1897
  }),
1898
+ vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
1899
+ inspectPanelManager.open(session || provider.getSelectedSession());
1900
+ }),
1176
1901
  vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
1177
1902
  vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
1178
1903
  vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
@@ -1181,6 +1906,7 @@ function activate(context) {
1181
1906
  activeSessionsWatcher,
1182
1907
  lockWatcher,
1183
1908
  worktreeLockWatcher,
1909
+ logWatcher,
1184
1910
  { dispose: () => clearInterval(interval) },
1185
1911
  );
1186
1912
 
@@ -1188,8 +1914,10 @@ function activate(context) {
1188
1914
  ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
1189
1915
  ...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
1190
1916
  ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
1917
+ ...bindRefreshWatcher(logWatcher, scheduleRefresh),
1191
1918
  );
1192
1919
  void refreshController.refreshNow();
1920
+ void maybeAutoUpdateActiveAgentsExtension(context);
1193
1921
  }
1194
1922
 
1195
1923
  function deactivate() {}