@imdeadpool/guardex 7.0.26 → 7.0.31

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.
@@ -3,11 +3,13 @@ const path = require('node:path');
3
3
  const cp = require('node:child_process');
4
4
  const vscode = require('vscode');
5
5
  const {
6
+ clearWorktreeActivityCache,
6
7
  formatElapsedFrom,
7
8
  readActiveSessions,
8
9
  readRepoChanges,
9
10
  readSessionInspectData,
10
11
  sanitizeBranchForFile,
12
+ sessionFilePathForBranch,
11
13
  } = require('./session-schema.js');
12
14
 
13
15
  const SESSION_DECORATION_SCHEME = 'gitguardex-agent';
@@ -54,6 +56,7 @@ const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
54
56
  const SESSION_ACTIVITY_GROUPS = [
55
57
  { kind: 'blocked', label: 'BLOCKED' },
56
58
  { kind: 'working', label: 'WORKING NOW' },
59
+ { kind: 'finished', label: 'FINISHED' },
57
60
  { kind: 'idle', label: 'THINKING' },
58
61
  { kind: 'stalled', label: 'STALLED' },
59
62
  { kind: 'dead', label: 'DEAD' },
@@ -61,10 +64,12 @@ const SESSION_ACTIVITY_GROUPS = [
61
64
  const SESSION_ACTIVITY_ICON_IDS = {
62
65
  blocked: 'warning',
63
66
  working: 'loading~spin',
67
+ finished: 'pass-filled',
64
68
  idle: 'comment-discussion',
65
69
  stalled: 'clock',
66
70
  dead: 'error',
67
71
  };
72
+ const DISMISSABLE_SESSION_ACTIVITY_KINDS = new Set(['stalled', 'dead']);
68
73
  const SESSION_PROVIDER_BRANDS = {
69
74
  openai: {
70
75
  id: 'openai',
@@ -105,6 +110,10 @@ function iconColorId(iconId) {
105
110
  return 'terminal.ansiCyan';
106
111
  case 'list-tree':
107
112
  return 'terminal.ansiBlue';
113
+ case 'pass-filled':
114
+ case 'pass':
115
+ case 'check':
116
+ return 'testing.iconPassed';
108
117
  default:
109
118
  return '';
110
119
  }
@@ -465,9 +474,15 @@ function agentBadgeFromBranch(branch) {
465
474
 
466
475
  function buildActiveAgentsStatusSummary(summary) {
467
476
  const workingCount = summary?.workingCount || 0;
477
+ const finishedCount = summary?.finishedCount || 0;
468
478
  const idleCount = summary?.idleCount || 0;
469
- if (workingCount > 0 || idleCount > 0) {
470
- return `$(git-branch) ${workingCount} working · ${idleCount} idle`;
479
+ if (workingCount > 0 || finishedCount > 0 || idleCount > 0) {
480
+ const parts = [`${workingCount} working`];
481
+ if (finishedCount > 0) {
482
+ parts.push(`${finishedCount} finished`);
483
+ }
484
+ parts.push(`${idleCount} idle`);
485
+ return `$(git-branch) ${parts.join(' · ')}`;
471
486
  }
472
487
  return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
473
488
  }
@@ -487,6 +502,7 @@ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
487
502
  return [
488
503
  formatCountLabel(activeCount, 'active agent'),
489
504
  formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
505
+ formatCountLabel(summary?.finishedCount || 0, 'finished session'),
490
506
  formatCountLabel(summary?.idleCount || 0, 'idle session'),
491
507
  formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
492
508
  formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
@@ -531,6 +547,10 @@ function countWorkingSessions(sessions) {
531
547
  )).length;
532
548
  }
533
549
 
550
+ function countFinishedSessions(sessions) {
551
+ return sessions.filter((session) => session.activityKind === 'finished').length;
552
+ }
553
+
534
554
  function countIdleSessions(sessions) {
535
555
  return sessions.filter((session) => (
536
556
  session.activityKind === 'idle' || session.activityKind === 'stalled'
@@ -568,6 +588,9 @@ function sessionFreshnessLabel(session, now = Date.now()) {
568
588
  if (session.activityKind === 'blocked') {
569
589
  return 'Needs attention';
570
590
  }
591
+ if (session.activityKind === 'finished') {
592
+ return 'Finished';
593
+ }
571
594
  if (session.activityKind === 'stalled') {
572
595
  return 'Possibly stale';
573
596
  }
@@ -595,6 +618,8 @@ function sessionStatusLabel(session) {
595
618
  return 'Blocked';
596
619
  case 'working':
597
620
  return 'Working';
621
+ case 'finished':
622
+ return 'Finished';
598
623
  case 'idle':
599
624
  return 'Idle';
600
625
  case 'stalled':
@@ -693,6 +718,14 @@ function changeRiskBadges(change) {
693
718
  ].filter(Boolean));
694
719
  }
695
720
 
721
+ function changeNeedsWarningIcon(change) {
722
+ return Boolean(
723
+ change?.protectedBranch
724
+ || change?.hasForeignLock
725
+ || (!change?.hasForeignLock && change?.lockOwnerBranch),
726
+ );
727
+ }
728
+
696
729
  function buildSessionCardDescription(session) {
697
730
  const provider = resolveSessionProvider(session);
698
731
  const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`;
@@ -793,6 +826,7 @@ function buildWorktreeBranchDescription(sessions) {
793
826
  function buildOverviewDescription(summary) {
794
827
  return [
795
828
  formatCountLabel(summary?.workingCount || 0, 'working agent'),
829
+ formatCountLabel(summary?.finishedCount || 0, 'finished agent'),
796
830
  formatCountLabel(summary?.idleCount || 0, 'idle agent'),
797
831
  formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
798
832
  formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
@@ -912,6 +946,9 @@ function workingSessionSortKey(session) {
912
946
  if (session.deltaLabel === 'New') {
913
947
  return 3;
914
948
  }
949
+ if (session.activityKind === 'finished') {
950
+ return 5;
951
+ }
915
952
  return 4;
916
953
  }
917
954
 
@@ -1289,7 +1326,7 @@ class SessionItem extends vscode.TreeItem {
1289
1326
  : buildSessionCardDescription(session);
1290
1327
  this.tooltip = buildSessionTooltip(session, this.description);
1291
1328
  this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind));
1292
- this.contextValue = 'gitguardex.session';
1329
+ this.contextValue = sessionContextValue(session);
1293
1330
  this.command = {
1294
1331
  command: 'gitguardex.activeAgents.openWorktree',
1295
1332
  title: 'Open Agent Worktree',
@@ -1298,6 +1335,35 @@ class SessionItem extends vscode.TreeItem {
1298
1335
  }
1299
1336
  }
1300
1337
 
1338
+ function sessionContextValue(session) {
1339
+ const activityKind = typeof session?.activityKind === 'string' ? session.activityKind.trim() : '';
1340
+ return activityKind
1341
+ ? `gitguardex.session.${activityKind}`
1342
+ : 'gitguardex.session';
1343
+ }
1344
+
1345
+ function canDismissSession(session) {
1346
+ return DISMISSABLE_SESSION_ACTIVITY_KINDS.has(session?.activityKind);
1347
+ }
1348
+
1349
+ function buildDismissSessionDetail(session, statePath) {
1350
+ const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
1351
+ const relativeStatePath = repoRoot
1352
+ ? path.relative(repoRoot, statePath) || path.basename(statePath)
1353
+ : path.basename(statePath);
1354
+ const detailParts = [
1355
+ `Remove ${relativeStatePath} and hide this session from Active Agents.`,
1356
+ ];
1357
+
1358
+ if (session?.activityKind === 'stalled') {
1359
+ detailParts.push('This dismisses the stale sidebar row only; use Stop if you want to interrupt a live agent.');
1360
+ } else {
1361
+ detailParts.push('This clears the stale session record from the sidebar.');
1362
+ }
1363
+
1364
+ return detailParts.join(' ');
1365
+ }
1366
+
1301
1367
  class FolderItem extends vscode.TreeItem {
1302
1368
  constructor(label, relativePath, items, options = {}) {
1303
1369
  super(
@@ -1845,6 +1911,51 @@ async function stopSession(session, refresh) {
1845
1911
  }
1846
1912
  }
1847
1913
 
1914
+ async function dismissSession(session, refresh) {
1915
+ if (!canDismissSession(session)) {
1916
+ showSessionMessage('Only stalled or dead sessions can be dismissed.');
1917
+ return;
1918
+ }
1919
+
1920
+ const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
1921
+ if (!repoRoot) {
1922
+ showSessionMessage('Cannot dismiss session: missing repo root.');
1923
+ return;
1924
+ }
1925
+ if (!session?.branch) {
1926
+ showSessionMessage('Cannot dismiss session: missing branch name.');
1927
+ return;
1928
+ }
1929
+
1930
+ const statePath = sessionFilePathForBranch(repoRoot, session.branch);
1931
+ if (!fs.existsSync(statePath)) {
1932
+ clearWorktreeActivityCache(session.worktreePath);
1933
+ refresh();
1934
+ showSessionMessage(`Session record already gone for ${sessionDisplayLabel(session)}.`);
1935
+ return;
1936
+ }
1937
+
1938
+ const confirmed = await vscode.window.showWarningMessage(
1939
+ `Dismiss ${sessionDisplayLabel(session)}?`,
1940
+ {
1941
+ modal: true,
1942
+ detail: buildDismissSessionDetail(session, statePath),
1943
+ },
1944
+ 'Dismiss',
1945
+ );
1946
+ if (confirmed !== 'Dismiss') {
1947
+ return;
1948
+ }
1949
+
1950
+ try {
1951
+ fs.unlinkSync(statePath);
1952
+ clearWorktreeActivityCache(session.worktreePath);
1953
+ refresh();
1954
+ } catch (error) {
1955
+ showSessionMessage(`Failed to dismiss session ${sessionDisplayLabel(session)}: ${error.message}`);
1956
+ }
1957
+ }
1958
+
1848
1959
  function readGitDirPath(targetPath) {
1849
1960
  const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : '';
1850
1961
  if (!normalizedTargetPath) {
@@ -2405,6 +2516,7 @@ function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
2405
2516
  return {
2406
2517
  sessionCount: sessions.length,
2407
2518
  workingCount: countWorkingSessions(sessions),
2519
+ finishedCount: countFinishedSessions(sessions),
2408
2520
  idleCount: countIdleSessions(sessions),
2409
2521
  unassignedChangeCount: (unassignedChanges || []).length,
2410
2522
  lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0,
@@ -2719,7 +2831,7 @@ function buildUnassignedChangeNodes(changes) {
2719
2831
  return sortUnassignedChanges(changes).map((change) => new ChangeItem(change, {
2720
2832
  label: compactRelativePath(change.relativePath),
2721
2833
  description: buildUnassignedChangeDescription(change),
2722
- iconId: changeRiskBadges(change).length > 0 ? 'warning' : undefined,
2834
+ iconId: changeNeedsWarningIcon(change) ? 'warning' : undefined,
2723
2835
  }));
2724
2836
  }
2725
2837
 
@@ -2775,6 +2887,7 @@ class ActiveAgentsProvider {
2775
2887
  this.viewSummary = {
2776
2888
  sessionCount: 0,
2777
2889
  workingCount: 0,
2890
+ finishedCount: 0,
2778
2891
  idleCount: 0,
2779
2892
  unassignedChangeCount: 0,
2780
2893
  lockedFileCount: 0,
@@ -2793,6 +2906,7 @@ class ActiveAgentsProvider {
2793
2906
  this.updateViewState({
2794
2907
  sessionCount: 0,
2795
2908
  workingCount: 0,
2909
+ finishedCount: 0,
2796
2910
  idleCount: 0,
2797
2911
  unassignedChangeCount: 0,
2798
2912
  lockedFileCount: 0,
@@ -2915,6 +3029,10 @@ class ActiveAgentsProvider {
2915
3029
  const summary = {
2916
3030
  sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0),
2917
3031
  workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0),
3032
+ finishedCount: repoEntries.reduce(
3033
+ (total, entry) => total + (entry.overview.finishedCount || 0),
3034
+ 0,
3035
+ ),
2918
3036
  idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0),
2919
3037
  unassignedChangeCount: repoEntries.reduce(
2920
3038
  (total, entry) => total + entry.overview.unassignedChangeCount,
@@ -2964,6 +3082,7 @@ class ActiveAgentsProvider {
2964
3082
  }),
2965
3083
  ], {
2966
3084
  description: '1',
3085
+ collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
2967
3086
  }),
2968
3087
  ];
2969
3088
 
@@ -2971,6 +3090,7 @@ class ActiveAgentsProvider {
2971
3090
  if (workingNowItems.length > 0) {
2972
3091
  sectionItems.push(new SectionItem('Working now', workingNowItems, {
2973
3092
  description: String(workingNowItems.length),
3093
+ collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
2974
3094
  iconId: 'loading~spin',
2975
3095
  }));
2976
3096
  }
@@ -3011,7 +3131,7 @@ class ActiveAgentsProvider {
3011
3131
  if (advancedItems.length > 0) {
3012
3132
  sectionItems.push(new SectionItem('Advanced details', advancedItems, {
3013
3133
  description: String(advancedItems.length),
3014
- collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
3134
+ collapsedState: vscode.TreeItemCollapsibleState.Expanded,
3015
3135
  iconId: 'list-tree',
3016
3136
  }));
3017
3137
  }
@@ -3358,6 +3478,7 @@ function activate(context) {
3358
3478
  vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
3359
3479
  vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
3360
3480
  vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
3481
+ vscode.commands.registerCommand('gitguardex.activeAgents.dismissSession', (session) => dismissSession(session, refresh)),
3361
3482
  vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged),
3362
3483
  activeSessionsWatcher,
3363
3484
  lockWatcher,
@@ -3,7 +3,7 @@
3
3
  "displayName": "GitGuardex Active Agents",
4
4
  "description": "Shows live Guardex sandbox sessions and repo changes in a dedicated VS Code Active Agents sidebar.",
5
5
  "publisher": "recodeee",
6
- "version": "0.0.17",
6
+ "version": "0.0.19",
7
7
  "license": "MIT",
8
8
  "icon": "icon.png",
9
9
  "engines": {
@@ -65,6 +65,11 @@
65
65
  "title": "Stop",
66
66
  "icon": "$(debug-stop)"
67
67
  },
68
+ {
69
+ "command": "gitguardex.activeAgents.dismissSession",
70
+ "title": "Dismiss",
71
+ "icon": "$(trash)"
72
+ },
68
73
  {
69
74
  "command": "gitguardex.activeAgents.showSessionTerminal",
70
75
  "title": "Show Terminal",
@@ -125,32 +130,37 @@
125
130
  "view/item/context": [
126
131
  {
127
132
  "command": "gitguardex.activeAgents.openWorktree",
128
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
133
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
129
134
  "group": "inline"
130
135
  },
131
136
  {
132
137
  "command": "gitguardex.activeAgents.inspect",
133
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
138
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
134
139
  "group": "inline"
135
140
  },
136
141
  {
137
142
  "command": "gitguardex.activeAgents.showSessionTerminal",
138
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
143
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
139
144
  "group": "inline"
140
145
  },
141
146
  {
142
147
  "command": "gitguardex.activeAgents.finishSession",
143
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
148
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
144
149
  "group": "inline"
145
150
  },
146
151
  {
147
152
  "command": "gitguardex.activeAgents.syncSession",
148
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
153
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
149
154
  "group": "inline"
150
155
  },
151
156
  {
152
157
  "command": "gitguardex.activeAgents.stopSession",
153
- "when": "view == gitguardex.activeAgents && viewItem == gitguardex.session",
158
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session(\\.|$)/",
159
+ "group": "inline"
160
+ },
161
+ {
162
+ "command": "gitguardex.activeAgents.dismissSession",
163
+ "when": "view == gitguardex.activeAgents && viewItem =~ /^gitguardex\\.session\\.(stalled|dead)$/",
154
164
  "group": "inline"
155
165
  }
156
166
  ]
@@ -700,17 +700,35 @@ function deriveSessionActivity(session, options = {}) {
700
700
  .filter(Boolean))]
701
701
  .sort((left, right) => left.localeCompare(right));
702
702
 
703
+ const workingLatestFileActivityMs = deriveLatestWorktreeFileActivity(session.worktreePath, {
704
+ now,
705
+ useCache: options.useCache,
706
+ });
707
+ const workingLastFileActivityAt = Number.isFinite(workingLatestFileActivityMs)
708
+ ? new Date(workingLatestFileActivityMs).toISOString()
709
+ : '';
710
+ const workingLastFileActivityLabel = workingLastFileActivityAt
711
+ ? formatElapsedFrom(workingLastFileActivityAt, now)
712
+ : '';
713
+ const workingFileActivityAgeMs = Number.isFinite(workingLatestFileActivityMs)
714
+ ? Math.max(0, now - workingLatestFileActivityMs)
715
+ : null;
716
+ const isFinishedUncommitted = workingFileActivityAgeMs !== null
717
+ && workingFileActivityAgeMs > IDLE_ACTIVITY_WINDOW_MS;
718
+
703
719
  return {
704
- activityKind: 'working',
705
- activityLabel: 'working',
720
+ activityKind: isFinishedUncommitted ? 'finished' : 'working',
721
+ activityLabel: isFinishedUncommitted ? 'finished' : 'working',
706
722
  activityCountLabel: formatFileCount(worktreeChangedPaths.length),
707
- activitySummary: previewChangedPaths(worktreeChangedPaths),
723
+ activitySummary: isFinishedUncommitted && workingLastFileActivityLabel
724
+ ? `${previewChangedPaths(worktreeChangedPaths)} · idle ${workingLastFileActivityLabel}`
725
+ : previewChangedPaths(worktreeChangedPaths),
708
726
  changeCount: worktreeChangedPaths.length,
709
727
  changedPaths,
710
728
  worktreeChangedPaths: worktreeRelativePaths,
711
729
  pidAlive,
712
- lastFileActivityAt: '',
713
- lastFileActivityLabel: '',
730
+ lastFileActivityAt: workingLastFileActivityAt,
731
+ lastFileActivityLabel: workingLastFileActivityLabel,
714
732
  };
715
733
  }
716
734