@imdeadpool/guardex 7.0.20 → 7.0.22

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.
@@ -218,18 +218,48 @@ fi
218
218
 
219
219
  get_worktree_for_branch() {
220
220
  local branch="$1"
221
- git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" '
221
+ git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" -v probe_prefix="${agent_worktree_root}/__source-probe-" '
222
222
  $1 == "worktree" { wt = $2 }
223
- $1 == "branch" && $2 == target { print wt; exit }
223
+ $1 == "branch" && $2 == target {
224
+ if (index(wt, probe_prefix) != 1) {
225
+ print wt
226
+ exit
227
+ }
228
+ }
224
229
  '
225
230
  }
226
231
 
232
+ remove_stale_source_probe_worktrees() {
233
+ local branch="$1"
234
+ local stale_probe=""
235
+
236
+ while IFS= read -r stale_probe; do
237
+ [[ -z "$stale_probe" ]] && continue
238
+ [[ "$stale_probe" == "$current_worktree" ]] && continue
239
+
240
+ echo "[agent-branch-finish] Removing stale source-probe worktree for '${branch}': ${stale_probe}" >&2
241
+ git -C "$stale_probe" rebase --abort >/dev/null 2>&1 || true
242
+ git -C "$stale_probe" merge --abort >/dev/null 2>&1 || true
243
+ git -C "$repo_root" worktree remove "$stale_probe" --force >/dev/null 2>&1 || true
244
+ done < <(
245
+ git -C "$repo_root" worktree list --porcelain | awk -v target="refs/heads/${branch}" -v probe_prefix="${agent_worktree_root}/__source-probe-" '
246
+ $1 == "worktree" { wt = $2 }
247
+ $1 == "branch" && $2 == target {
248
+ if (index(wt, probe_prefix) == 1) {
249
+ print wt
250
+ }
251
+ }
252
+ '
253
+ )
254
+ }
255
+
227
256
  is_clean_worktree() {
228
257
  local wt="$1"
229
258
  git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
230
259
  && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json"
231
260
  }
232
261
 
262
+ remove_stale_source_probe_worktrees "$SOURCE_BRANCH"
233
263
  source_worktree="$(get_worktree_for_branch "$SOURCE_BRANCH")"
234
264
  created_source_probe=0
235
265
  source_probe_path=""
@@ -295,8 +325,13 @@ if [[ "$should_require_sync" -eq 1 ]] && git -C "$repo_root" show-ref --verify -
295
325
 
296
326
  echo "[agent-sync-guard] Auto-sync failed while rebasing '${SOURCE_BRANCH}' onto origin/${BASE_BRANCH}." >&2
297
327
  if [[ "$rebase_active" -eq 1 ]]; then
298
- echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
299
- echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
328
+ if [[ "$created_source_probe" -eq 1 ]]; then
329
+ echo "[agent-sync-guard] Temporary source-probe worktree will be cleaned up on exit." >&2
330
+ echo "[agent-sync-guard] Reattach '${SOURCE_BRANCH}' in a regular worktree, then rebase it onto origin/${BASE_BRANCH} manually." >&2
331
+ else
332
+ echo "[agent-sync-guard] Resolve conflicts, then run: git -C \"$source_worktree\" rebase --continue" >&2
333
+ echo "[agent-sync-guard] Or abort: git -C \"$source_worktree\" rebase --abort" >&2
334
+ fi
300
335
  fi
301
336
  exit 1
302
337
  fi
@@ -366,6 +401,14 @@ is_local_branch_delete_error() {
366
401
  return 1
367
402
  }
368
403
 
404
+ is_remote_branch_missing_error() {
405
+ local output="$1"
406
+ if [[ "$output" == *"remote ref does not exist"* ]] || [[ "$output" == *"failed to push some refs"* ]]; then
407
+ return 0
408
+ fi
409
+ return 1
410
+ }
411
+
369
412
  read_pr_state() {
370
413
  local state_line
371
414
  state_line="$("$GH_BIN" pr view "$SOURCE_BRANCH" --json state,mergedAt,url --jq '[.state, (.mergedAt // ""), (.url // "")] | join("\u001f")' 2>/dev/null || true)"
@@ -568,7 +611,15 @@ if [[ "$CLEANUP_AFTER_MERGE" -eq 1 ]]; then
568
611
 
569
612
  if [[ "$PUSH_ENABLED" -eq 1 && "$DELETE_REMOTE_BRANCH" -eq 1 ]]; then
570
613
  if git -C "$repo_root" ls-remote --exit-code --heads origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
571
- git -C "$repo_root" push origin --delete "$SOURCE_BRANCH"
614
+ remote_delete_output=""
615
+ if ! remote_delete_output="$(git -C "$repo_root" push origin --delete "$SOURCE_BRANCH" 2>&1)"; then
616
+ if is_remote_branch_missing_error "$remote_delete_output"; then
617
+ echo "[agent-branch-finish] Remote branch '${SOURCE_BRANCH}' was already deleted; continuing cleanup." >&2
618
+ else
619
+ echo "$remote_delete_output" >&2
620
+ exit 1
621
+ fi
622
+ fi
572
623
  fi
573
624
  fi
574
625
 
@@ -111,6 +111,13 @@ is_managed_worktree_path() {
111
111
  return 1
112
112
  }
113
113
 
114
+ is_temporary_worktree_path() {
115
+ local entry="$1"
116
+ local name
117
+ name="$(basename "$entry")"
118
+ [[ "$name" == __agent_integrate-* || "$name" == __source-probe-* ]]
119
+ }
120
+
114
121
  resolve_base_branch() {
115
122
  local configured=""
116
123
  local current=""
@@ -425,7 +432,9 @@ process_entry() {
425
432
  local remove_reason=""
426
433
  local branch_delete_mode="safe"
427
434
 
428
- if [[ -z "$branch_ref" ]]; then
435
+ if is_temporary_worktree_path "$wt"; then
436
+ remove_reason="temporary-worktree"
437
+ elif [[ -z "$branch_ref" ]]; then
429
438
  remove_reason="detached-worktree"
430
439
  elif ! git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}"; then
431
440
  remove_reason="missing-branch"
@@ -452,6 +461,11 @@ process_entry() {
452
461
  return
453
462
  fi
454
463
 
464
+ if [[ "$remove_reason" == "temporary-worktree" ]]; then
465
+ git -C "$wt" rebase --abort >/dev/null 2>&1 || true
466
+ git -C "$wt" merge --abort >/dev/null 2>&1 || true
467
+ fi
468
+
455
469
  if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
456
470
  skipped_dirty=$((skipped_dirty + 1))
457
471
  echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
@@ -90,6 +90,13 @@ string_has_lightweight_prefix() {
90
90
  return 1
91
91
  }
92
92
 
93
+ task_requires_full_change_workspace() {
94
+ local text="$1"
95
+ string_contains_any "$text" \
96
+ "cleanup evidence" "merged cleanup" "merged state" "pr url" \
97
+ "cleanup pipeline" "finish pipeline" "sandbox cleanup" "tasks.md"
98
+ }
99
+
93
100
  derive_task_mode_from_tier() {
94
101
  case "$1" in
95
102
  T0|T1) printf 'caveman' ;;
@@ -128,8 +135,13 @@ decide_task_routing() {
128
135
  fi
129
136
  TASK_ROUTING_REASON="explicit tier override"
130
137
  elif string_has_lightweight_prefix "$task_lower"; then
131
- OPENSPEC_TIER="T1"
132
- TASK_ROUTING_REASON="explicit lightweight prefix"
138
+ if task_requires_full_change_workspace "$task_lower"; then
139
+ OPENSPEC_TIER="T2"
140
+ TASK_ROUTING_REASON="cleanup-evidence artifact wording overrides lightweight prefix"
141
+ else
142
+ OPENSPEC_TIER="T1"
143
+ TASK_ROUTING_REASON="explicit lightweight prefix"
144
+ fi
133
145
  elif string_contains_any "$task_lower" \
134
146
  "ralph" "autopilot" "ultrawork" "ultraqa" "ralplan" "deep interview" "ouroboros" \
135
147
  "migration" "refactor" "architecture" "re-architect" "cross-cutting" "multi-agent" \
@@ -17,11 +17,13 @@ node scripts/install-vscode-active-agents-extension.js
17
17
 
18
18
  What it does:
19
19
 
20
+ - Bundles a local GitGuardex icon so repo installs show branded extension metadata inside VS Code.
20
21
  - Adds an `Active Agents` view to the Source Control container.
21
22
  - Renders one repo node per live Guardex workspace with grouped `ACTIVE AGENTS` and `CHANGES` sections.
22
23
  - Splits live sessions inside `ACTIVE AGENTS` into `BLOCKED`, `WORKING NOW`, `IDLE`, `STALLED`, and `DEAD` groups so stuck, active, and inactive lanes stand out immediately.
24
+ - Mirrors the same live state in the VS Code status bar so the selected session or active-agent count stays visible outside the tree.
23
25
  - Shows one row per live Guardex sandbox session inside those activity groups.
24
26
  - Shows repo-root git changes in a sibling `CHANGES` section when the guarded repo itself is dirty.
25
27
  - Derives session state from dirty worktree status, git conflict markers, PID liveness, and recent file mtimes, surfaces working/dead counts in the repo/header summary, and shows changed-file counts for active edits.
26
28
  - Uses distinct VS Code codicons for each session state: `warning`, `edit`, `loading~spin`, `clock`, and `error`.
27
- - Reads repo-local presence files from `.omx/state/active-sessions/`.
29
+ - Reads repo-local presence files from `.omx/state/active-sessions/` and falls back to managed worktree-root `AGENT.lock` telemetry when the launcher session file is absent.
@@ -15,9 +15,15 @@ const IDLE_ERROR_MS = 30 * 60 * 1000;
15
15
  const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
16
16
  const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
17
17
  const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
18
+ const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
18
19
  const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
20
+ const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
19
21
  const SESSION_SCAN_LIMIT = 200;
20
22
  const REFRESH_DEBOUNCE_MS = 250;
23
+ const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
24
+ const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
25
+ const RELOAD_WINDOW_ACTION = 'Reload Window';
26
+ const UPDATE_LATER_ACTION = 'Later';
21
27
  const SESSION_ACTIVITY_GROUPS = [
22
28
  { kind: 'blocked', label: 'BLOCKED' },
23
29
  { kind: 'working', label: 'WORKING NOW' },
@@ -91,10 +97,83 @@ function sessionIdleDecoration(session, now = Date.now()) {
91
97
  return undefined;
92
98
  }
93
99
 
100
+ function formatCountLabel(count, singular, plural = `${singular}s`) {
101
+ return `${count} ${count === 1 ? singular : plural}`;
102
+ }
103
+
104
+ function sessionIdentityLabel(session) {
105
+ const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
106
+ const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
107
+ const label = typeof session?.label === 'string' ? session.label.trim() : '';
108
+
109
+ if (agentName && taskName) {
110
+ return `${agentName} · ${taskName}`;
111
+ }
112
+ if (agentName && label) {
113
+ return `${agentName} · ${label}`;
114
+ }
115
+
116
+ return agentName || taskName || label || 'session';
117
+ }
118
+
119
+ function sessionCommitPlaceholder(session) {
120
+ if (!session?.branch) {
121
+ return 'Pick an Active Agents session to commit its worktree.';
122
+ }
123
+
124
+ return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')} (Ctrl+Enter)`;
125
+ }
126
+
127
+ function agentNameFromBranch(branch) {
128
+ const segments = String(branch || '')
129
+ .split('/')
130
+ .map((segment) => segment.trim())
131
+ .filter(Boolean);
132
+ if (segments[0] === 'agent' && segments[1]) {
133
+ return segments[1];
134
+ }
135
+ return segments[0] || 'lock';
136
+ }
137
+
138
+ function agentBadgeFromBranch(branch) {
139
+ const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, '');
140
+ return normalized.slice(0, 2) || 'LK';
141
+ }
142
+
143
+ function buildActiveAgentsStatusSummary(summary) {
144
+ const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
145
+ if (activeCount > 0) {
146
+ return `$(git-branch) ${formatCountLabel(activeCount, 'active agent')}`;
147
+ }
148
+ return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
149
+ }
150
+
151
+ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
152
+ if (selectedSession?.branch) {
153
+ return [
154
+ selectedSession.branch,
155
+ sessionIdentityLabel(selectedSession),
156
+ formatCountLabel(selectedSession.lockCount || 0, 'lock'),
157
+ selectedSession.worktreePath,
158
+ 'Click to open Source Control.',
159
+ ].filter(Boolean).join('\n');
160
+ }
161
+
162
+ const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
163
+ return [
164
+ formatCountLabel(activeCount, 'active agent'),
165
+ formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
166
+ summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
167
+ 'Click to open Source Control.',
168
+ ].filter(Boolean).join('\n');
169
+ }
170
+
94
171
  class SessionDecorationProvider {
95
172
  constructor(nowProvider = () => Date.now()) {
96
173
  this.nowProvider = nowProvider;
97
174
  this.sessionsByUri = new Map();
175
+ this.lockEntriesByFileUri = new Map();
176
+ this.selectedBranch = '';
98
177
  this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
99
178
  this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
100
179
  }
@@ -105,13 +184,54 @@ class SessionDecorationProvider {
105
184
  );
106
185
  }
107
186
 
187
+ updateLockEntries(repoEntries) {
188
+ const nextEntriesByUri = new Map();
189
+ for (const entry of repoEntries || []) {
190
+ for (const [relativePath, lockEntry] of entry.lockEntries || []) {
191
+ nextEntriesByUri.set(
192
+ vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(),
193
+ { branch: lockEntry.branch },
194
+ );
195
+ }
196
+ }
197
+ this.lockEntriesByFileUri = nextEntriesByUri;
198
+ }
199
+
200
+ setSelectedBranch(branch) {
201
+ this.selectedBranch = typeof branch === 'string' ? branch.trim() : '';
202
+ }
203
+
108
204
  refresh() {
109
205
  this.onDidChangeFileDecorationsEmitter.fire();
110
206
  }
111
207
 
112
208
  provideFileDecoration(uri) {
113
209
  if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
114
- return undefined;
210
+ if (!uri || uri.scheme !== 'file') {
211
+ return undefined;
212
+ }
213
+
214
+ const lockEntry = this.lockEntriesByFileUri.get(uri.toString());
215
+ if (!lockEntry?.branch) {
216
+ return undefined;
217
+ }
218
+
219
+ const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch;
220
+ return {
221
+ badge: agentBadgeFromBranch(lockEntry.branch),
222
+ tooltip: ownsSelectedSession
223
+ ? `Locked by selected session ${lockEntry.branch}`
224
+ : this.selectedBranch
225
+ ? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})`
226
+ : `Locked by ${lockEntry.branch}`,
227
+ color: new vscode.ThemeColor(
228
+ ownsSelectedSession
229
+ ? 'gitDecoration.modifiedResourceForeground'
230
+ : this.selectedBranch
231
+ ? 'list.errorForeground'
232
+ : 'list.warningForeground',
233
+ ),
234
+ };
115
235
  }
116
236
 
117
237
  return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
@@ -187,14 +307,23 @@ class SessionItem extends vscode.TreeItem {
187
307
  const tooltipLines = [
188
308
  session.branch,
189
309
  `${session.agentName} · ${session.taskName}`,
310
+ session.latestTaskPreview && session.latestTaskPreview !== session.taskName
311
+ ? `Live task ${session.latestTaskPreview}`
312
+ : '',
190
313
  `Status ${this.description}`,
191
314
  session.changeCount > 0
192
315
  ? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
193
316
  : session.activitySummary,
194
317
  `Locks ${lockCount}`,
195
- session.pidAlive === false ? `PID ${session.pid} not alive` : `PID ${session.pid} alive`,
318
+ Number.isInteger(session.pid) && session.pid > 0
319
+ ? session.pidAlive === false
320
+ ? `PID ${session.pid} not alive`
321
+ : `PID ${session.pid} alive`
322
+ : '',
196
323
  session.lastFileActivityAt ? `Last file activity ${session.lastFileActivityAt}` : '',
197
- `Started ${session.startedAt}`,
324
+ session.sourceKind === 'worktree-lock'
325
+ ? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
326
+ : `Started ${session.startedAt}`,
198
327
  session.worktreePath,
199
328
  ];
200
329
  this.tooltip = tooltipLines.filter(Boolean).join('\n');
@@ -365,6 +494,10 @@ function repoRootFromSessionFile(filePath) {
365
494
  return path.resolve(path.dirname(filePath), '..', '..', '..');
366
495
  }
367
496
 
497
+ function repoRootFromWorktreeLockFile(filePath) {
498
+ return path.resolve(path.dirname(filePath), '..', '..', '..');
499
+ }
500
+
368
501
  function repoRootFromLockFile(filePath) {
369
502
  return path.resolve(path.dirname(filePath), '..', '..');
370
503
  }
@@ -436,6 +569,120 @@ function readCurrentBranch(repoRoot) {
436
569
  }
437
570
  }
438
571
 
572
+ function parseSimpleSemver(version) {
573
+ const parts = String(version || '')
574
+ .split('.')
575
+ .map((part) => Number.parseInt(part, 10));
576
+ if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
577
+ return null;
578
+ }
579
+ return parts;
580
+ }
581
+
582
+ function compareSimpleSemver(left, right) {
583
+ const leftParts = parseSimpleSemver(left);
584
+ const rightParts = parseSimpleSemver(right);
585
+ if (!leftParts || !rightParts) {
586
+ return 0;
587
+ }
588
+
589
+ for (let index = 0; index < leftParts.length; index += 1) {
590
+ if (leftParts[index] !== rightParts[index]) {
591
+ return leftParts[index] - rightParts[index];
592
+ }
593
+ }
594
+
595
+ return 0;
596
+ }
597
+
598
+ function readJsonFile(filePath) {
599
+ try {
600
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
601
+ } catch (_error) {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ function resolveActiveAgentsAutoUpdateCandidate(installedVersion) {
607
+ const candidates = [];
608
+
609
+ for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
610
+ const repoRoot = workspaceFolder?.uri?.fsPath;
611
+ if (!repoRoot) {
612
+ continue;
613
+ }
614
+
615
+ const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE);
616
+ const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE);
617
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) {
618
+ continue;
619
+ }
620
+
621
+ const manifest = readJsonFile(manifestPath);
622
+ const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : '';
623
+ if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) {
624
+ continue;
625
+ }
626
+
627
+ candidates.push({ repoRoot, installScriptPath, version: nextVersion });
628
+ }
629
+
630
+ candidates.sort((left, right) => compareSimpleSemver(right.version, left.version));
631
+ return candidates[0] || null;
632
+ }
633
+
634
+ function runActiveAgentsInstallScript(repoRoot, installScriptPath) {
635
+ return new Promise((resolve, reject) => {
636
+ cp.execFile(
637
+ process.execPath,
638
+ [installScriptPath],
639
+ { cwd: repoRoot, encoding: 'utf8' },
640
+ (error, stdout, stderr) => {
641
+ if (error) {
642
+ reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed'));
643
+ return;
644
+ }
645
+ resolve({ stdout, stderr });
646
+ },
647
+ );
648
+ });
649
+ }
650
+
651
+ async function maybeAutoUpdateActiveAgentsExtension(context) {
652
+ const installedVersion = typeof context?.extension?.packageJSON?.version === 'string'
653
+ ? context.extension.packageJSON.version.trim()
654
+ : '';
655
+ if (!installedVersion) {
656
+ return;
657
+ }
658
+
659
+ const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion);
660
+ if (!candidate) {
661
+ return;
662
+ }
663
+
664
+ try {
665
+ await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath);
666
+ } catch (error) {
667
+ const failure = typeof error?.message === 'string' && error.message.trim()
668
+ ? error.message.trim()
669
+ : 'install failed';
670
+ vscode.window.showWarningMessage?.(
671
+ `GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`,
672
+ );
673
+ return;
674
+ }
675
+
676
+ const selection = await vscode.window.showInformationMessage?.(
677
+ `GitGuardex Active Agents updated to ${candidate.version}. Reload Window to use the newest companion.`,
678
+ RELOAD_WINDOW_ACTION,
679
+ UPDATE_LATER_ACTION,
680
+ );
681
+ if (selection === RELOAD_WINDOW_ACTION) {
682
+ await vscode.commands.executeCommand('workbench.action.reloadWindow');
683
+ }
684
+ }
685
+
439
686
  function decorateSession(session, lockRegistry) {
440
687
  return {
441
688
  ...session,
@@ -479,16 +726,29 @@ function localizeChangeForSession(session, change) {
479
726
  }
480
727
 
481
728
  async function findRepoSessionEntries() {
482
- const sessionFiles = await vscode.workspace.findFiles(
483
- ACTIVE_SESSION_FILES_GLOB,
484
- SESSION_SCAN_EXCLUDE_GLOB,
485
- SESSION_SCAN_LIMIT,
486
- );
729
+ const [sessionFiles, worktreeLockFiles] = await Promise.all([
730
+ vscode.workspace.findFiles(
731
+ ACTIVE_SESSION_FILES_GLOB,
732
+ SESSION_SCAN_EXCLUDE_GLOB,
733
+ SESSION_SCAN_LIMIT,
734
+ ),
735
+ vscode.workspace.findFiles(
736
+ WORKTREE_AGENT_LOCKS_GLOB,
737
+ WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
738
+ SESSION_SCAN_LIMIT,
739
+ ),
740
+ ]);
487
741
 
488
742
  const repoRoots = new Set();
489
743
  for (const uri of sessionFiles) {
490
744
  repoRoots.add(repoRootFromSessionFile(uri.fsPath));
491
745
  }
746
+ for (const uri of worktreeLockFiles) {
747
+ if (path.basename(uri.fsPath) !== 'AGENT.lock') {
748
+ continue;
749
+ }
750
+ repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath));
751
+ }
492
752
 
493
753
  if (repoRoots.size === 0) {
494
754
  for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
@@ -808,6 +1068,11 @@ class ActiveAgentsProvider {
808
1068
  this.treeView = null;
809
1069
  this.lockRegistryByRepoRoot = new Map();
810
1070
  this.selectedSession = null;
1071
+ this.viewSummary = {
1072
+ sessionCount: 0,
1073
+ workingCount: 0,
1074
+ deadCount: 0,
1075
+ };
811
1076
  }
812
1077
 
813
1078
  getTreeItem(element) {
@@ -828,6 +1093,7 @@ class ActiveAgentsProvider {
828
1093
  const currentKey = sessionSelectionKey(this.selectedSession);
829
1094
  const nextKey = sessionSelectionKey(nextSession);
830
1095
  this.selectedSession = nextSession;
1096
+ this.decorationProvider?.setSelectedBranch(nextSession?.branch || '');
831
1097
  if (currentKey !== nextKey) {
832
1098
  this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
833
1099
  }
@@ -837,6 +1103,10 @@ class ActiveAgentsProvider {
837
1103
  return this.selectedSession ? { ...this.selectedSession } : null;
838
1104
  }
839
1105
 
1106
+ getViewSummary() {
1107
+ return { ...this.viewSummary };
1108
+ }
1109
+
840
1110
  syncSelectedSession(repoEntries) {
841
1111
  if (!this.selectedSession) {
842
1112
  return;
@@ -854,6 +1124,11 @@ class ActiveAgentsProvider {
854
1124
  }
855
1125
 
856
1126
  const activeCount = Math.max(0, sessionCount - deadCount);
1127
+ this.viewSummary = {
1128
+ sessionCount,
1129
+ workingCount,
1130
+ deadCount,
1131
+ };
857
1132
  const badgeTooltipParts = [];
858
1133
  if (activeCount > 0) {
859
1134
  badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
@@ -890,6 +1165,7 @@ class ActiveAgentsProvider {
890
1165
 
891
1166
  this.updateViewState(sessionCount, workingCount, deadCount);
892
1167
  this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
1168
+ this.decorationProvider?.updateLockEntries(repoEntries);
893
1169
  return repoEntries;
894
1170
  }
895
1171
 
@@ -968,6 +1244,7 @@ class ActiveAgentsProvider {
968
1244
  changes: readRepoChanges(repoRoot).map((change) => (
969
1245
  decorateChange(change, lockRegistry, currentBranch)
970
1246
  )),
1247
+ lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
971
1248
  };
972
1249
  });
973
1250
  }
@@ -1052,19 +1329,36 @@ function activate(context) {
1052
1329
  'gitguardex.activeAgents.commitInput',
1053
1330
  'Active Agents Commit',
1054
1331
  );
1332
+ const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
1333
+ activeAgentsStatusItem.name = 'GitGuardex Active Agents';
1334
+ activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
1055
1335
  provider.attachTreeView(treeView);
1056
1336
  const scheduleRefresh = () => refreshController.scheduleRefresh();
1057
1337
  const refresh = () => void refreshController.refreshNow();
1058
1338
  const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
1059
1339
  const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
1340
+ const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
1060
1341
  const updateCommitInput = (session) => {
1061
1342
  sourceControl.inputBox.enabled = true;
1062
1343
  sourceControl.inputBox.visible = true;
1063
- sourceControl.inputBox.placeholder = session?.label
1064
- ? `Commit ${session.label} (Ctrl+Enter)`
1065
- : 'Pick an Active Agents session to commit its worktree.';
1344
+ sourceControl.inputBox.placeholder = sessionCommitPlaceholder(session);
1345
+ };
1346
+ const updateStatusBar = () => {
1347
+ const selectedSession = provider.getSelectedSession();
1348
+ const summary = provider.getViewSummary();
1349
+ if ((summary.sessionCount || 0) <= 0) {
1350
+ activeAgentsStatusItem.hide();
1351
+ return;
1352
+ }
1353
+
1354
+ activeAgentsStatusItem.text = selectedSession?.branch
1355
+ ? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}`
1356
+ : buildActiveAgentsStatusSummary(summary);
1357
+ activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary);
1358
+ activeAgentsStatusItem.show();
1066
1359
  };
1067
1360
  updateCommitInput(null);
1361
+ updateStatusBar();
1068
1362
  const commitSelectedSession = async () => {
1069
1363
  const selectedSession = provider.getSelectedSession();
1070
1364
  if (!selectedSession?.worktreePath) {
@@ -1111,15 +1405,27 @@ function activate(context) {
1111
1405
  scheduleRefresh();
1112
1406
  };
1113
1407
 
1114
- provider.onDidChangeSelectedSession(updateCommitInput);
1408
+ provider.onDidChangeSelectedSession((session) => {
1409
+ updateCommitInput(session);
1410
+ updateStatusBar();
1411
+ decorationProvider.refresh();
1412
+ });
1413
+ provider.onDidChangeTreeData(() => {
1414
+ updateCommitInput(provider.getSelectedSession());
1415
+ updateStatusBar();
1416
+ });
1115
1417
 
1116
1418
  context.subscriptions.push(
1117
1419
  treeView,
1118
1420
  sourceControl,
1421
+ activeAgentsStatusItem,
1119
1422
  refreshController,
1120
1423
  vscode.window.registerFileDecorationProvider(decorationProvider),
1121
1424
  vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
1122
1425
  vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
1426
+ vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
1427
+ await vscode.commands.executeCommand('workbench.view.scm');
1428
+ }),
1123
1429
  vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
1124
1430
  vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
1125
1431
  if (!session?.worktreePath) {
@@ -1151,14 +1457,17 @@ function activate(context) {
1151
1457
  vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
1152
1458
  activeSessionsWatcher,
1153
1459
  lockWatcher,
1460
+ worktreeLockWatcher,
1154
1461
  { dispose: () => clearInterval(interval) },
1155
1462
  );
1156
1463
 
1157
1464
  context.subscriptions.push(
1158
1465
  ...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
1159
1466
  ...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
1467
+ ...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
1160
1468
  );
1161
1469
  void refreshController.refreshNow();
1470
+ void maybeAutoUpdateActiveAgentsExtension(context);
1162
1471
  }
1163
1472
 
1164
1473
  function deactivate() {}