@imdeadpool/guardex 7.0.24 → 7.0.25
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.
- package/README.md +25 -3
- package/package.json +1 -1
- package/src/cli/args.js +9 -0
- package/src/context.js +22 -0
- package/src/doctor/index.js +158 -5
- package/src/finish/index.js +1 -0
- package/templates/AGENTS.multiagent-safety.md +3 -0
- package/templates/codex/skills/guardex-merge-skills-to-dev/SKILL.md +3 -2
- package/templates/scripts/agent-branch-finish.sh +106 -5
- package/templates/scripts/agent-worktree-prune.sh +22 -1
- package/templates/vscode/guardex-active-agents/README.md +6 -3
- package/templates/vscode/guardex-active-agents/extension.js +1706 -247
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +54 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +4 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +4 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +4 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +3 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +5 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +4 -0
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +4 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +14 -0
- package/templates/vscode/guardex-active-agents/package.json +39 -11
- package/templates/vscode/guardex-active-agents/session-schema.js +226 -8
|
@@ -17,17 +17,40 @@ const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
|
17
17
|
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
18
18
|
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
19
19
|
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
20
|
+
const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git';
|
|
21
|
+
const MANAGED_WORKTREE_RELATIVE_ROOTS = [
|
|
22
|
+
path.join('.omx', 'agent-worktrees'),
|
|
23
|
+
path.join('.omc', 'agent-worktrees'),
|
|
24
|
+
];
|
|
20
25
|
const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log';
|
|
21
26
|
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
22
27
|
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
28
|
+
const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**';
|
|
23
29
|
const SESSION_SCAN_LIMIT = 200;
|
|
24
30
|
const REFRESH_DEBOUNCE_MS = 250;
|
|
31
|
+
const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000;
|
|
32
|
+
const SESSION_TOP_FILE_COUNT = 3;
|
|
25
33
|
const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
|
|
26
34
|
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
|
|
27
35
|
const RELOAD_WINDOW_ACTION = 'Reload Window';
|
|
28
36
|
const UPDATE_LATER_ACTION = 'Later';
|
|
37
|
+
const ACTIVE_AGENTS_EXTENSION_ID = 'recodeee.gitguardex-active-agents';
|
|
38
|
+
const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
|
|
29
39
|
const REFRESH_POLL_INTERVAL_MS = 30_000;
|
|
30
40
|
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
|
|
41
|
+
const GIT_CONFIGURATION_SECTION = 'git';
|
|
42
|
+
const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders';
|
|
43
|
+
const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json');
|
|
44
|
+
const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
|
|
45
|
+
'.omx/agent-worktrees',
|
|
46
|
+
'**/.omx/agent-worktrees',
|
|
47
|
+
'.omx/.tmp-worktrees',
|
|
48
|
+
'**/.omx/.tmp-worktrees',
|
|
49
|
+
'.omc/agent-worktrees',
|
|
50
|
+
'**/.omc/agent-worktrees',
|
|
51
|
+
'.omc/.tmp-worktrees',
|
|
52
|
+
'**/.omc/.tmp-worktrees',
|
|
53
|
+
];
|
|
31
54
|
const SESSION_ACTIVITY_GROUPS = [
|
|
32
55
|
{ kind: 'blocked', label: 'BLOCKED' },
|
|
33
56
|
{ kind: 'working', label: 'WORKING NOW' },
|
|
@@ -42,11 +65,134 @@ const SESSION_ACTIVITY_ICON_IDS = {
|
|
|
42
65
|
stalled: 'clock',
|
|
43
66
|
dead: 'error',
|
|
44
67
|
};
|
|
68
|
+
const SESSION_PROVIDER_BRANDS = {
|
|
69
|
+
openai: {
|
|
70
|
+
id: 'openai',
|
|
71
|
+
label: 'OpenAI',
|
|
72
|
+
badge: 'AI',
|
|
73
|
+
},
|
|
74
|
+
claude: {
|
|
75
|
+
id: 'claude',
|
|
76
|
+
label: 'Claude',
|
|
77
|
+
badge: 'CL',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
let bundledTreeIconThemeCache = null;
|
|
81
|
+
|
|
82
|
+
function iconColorId(iconId) {
|
|
83
|
+
switch (iconId) {
|
|
84
|
+
case 'warning':
|
|
85
|
+
case 'clock':
|
|
86
|
+
return 'list.warningForeground';
|
|
87
|
+
case 'error':
|
|
88
|
+
return 'list.errorForeground';
|
|
89
|
+
case 'loading~spin':
|
|
90
|
+
return 'gitDecoration.addedResourceForeground';
|
|
91
|
+
case 'comment-discussion':
|
|
92
|
+
case 'info':
|
|
93
|
+
case 'repo':
|
|
94
|
+
case 'folder':
|
|
95
|
+
case 'graph':
|
|
96
|
+
case 'history':
|
|
97
|
+
return 'textLink.foreground';
|
|
98
|
+
case 'git-branch':
|
|
99
|
+
return 'gitDecoration.modifiedResourceForeground';
|
|
100
|
+
case 'account':
|
|
101
|
+
return 'terminal.ansiYellow';
|
|
102
|
+
case 'sparkle':
|
|
103
|
+
return 'terminal.ansiMagenta';
|
|
104
|
+
case 'list-flat':
|
|
105
|
+
return 'terminal.ansiCyan';
|
|
106
|
+
case 'list-tree':
|
|
107
|
+
return 'terminal.ansiBlue';
|
|
108
|
+
default:
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function themeIcon(iconId, colorId = iconColorId(iconId)) {
|
|
114
|
+
if (!iconId) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return colorId
|
|
118
|
+
? new vscode.ThemeIcon(iconId, new vscode.ThemeColor(colorId))
|
|
119
|
+
: new vscode.ThemeIcon(iconId);
|
|
120
|
+
}
|
|
45
121
|
|
|
46
122
|
function sessionDecorationUri(branch) {
|
|
47
123
|
return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`);
|
|
48
124
|
}
|
|
49
125
|
|
|
126
|
+
function emptyBundledTreeIconTheme() {
|
|
127
|
+
return {
|
|
128
|
+
iconPathById: new Map(),
|
|
129
|
+
fileNames: {},
|
|
130
|
+
folderNames: {},
|
|
131
|
+
fileExtensions: {},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function loadBundledTreeIconTheme() {
|
|
136
|
+
if (bundledTreeIconThemeCache) {
|
|
137
|
+
return bundledTreeIconThemeCache;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const manifestPath = path.join(__dirname, BUNDLED_FILE_ICONS_MANIFEST_RELATIVE);
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
143
|
+
const manifestDir = path.dirname(manifestPath);
|
|
144
|
+
const iconPathById = new Map();
|
|
145
|
+
for (const [iconId, definition] of Object.entries(parsed?.iconDefinitions || {})) {
|
|
146
|
+
if (typeof definition?.iconPath !== 'string' || !definition.iconPath.trim()) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const iconUri = vscode.Uri.file(path.resolve(manifestDir, definition.iconPath));
|
|
150
|
+
iconPathById.set(iconId, {
|
|
151
|
+
light: iconUri,
|
|
152
|
+
dark: iconUri,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
bundledTreeIconThemeCache = {
|
|
156
|
+
iconPathById,
|
|
157
|
+
fileNames: parsed?.fileNames || {},
|
|
158
|
+
folderNames: parsed?.folderNames || {},
|
|
159
|
+
fileExtensions: parsed?.fileExtensions || {},
|
|
160
|
+
};
|
|
161
|
+
} catch (_error) {
|
|
162
|
+
bundledTreeIconThemeCache = emptyBundledTreeIconTheme();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return bundledTreeIconThemeCache;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveBundledTreeItemIconId(relativePath, kind = 'file') {
|
|
169
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
170
|
+
const entryName = path.posix.basename(normalizedRelativePath || '');
|
|
171
|
+
if (!entryName) {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const bundledTheme = loadBundledTreeIconTheme();
|
|
176
|
+
if (kind === 'folder') {
|
|
177
|
+
return bundledTheme.folderNames[entryName] || '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (bundledTheme.fileNames[entryName]) {
|
|
181
|
+
return bundledTheme.fileNames[entryName];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const matchingExtension = Object.keys(bundledTheme.fileExtensions)
|
|
185
|
+
.sort((left, right) => right.length - left.length)
|
|
186
|
+
.find((extension) => entryName === extension || entryName.endsWith(`.${extension}`));
|
|
187
|
+
return matchingExtension ? bundledTheme.fileExtensions[matchingExtension] : '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function resolveBundledTreeItemIcon(relativePath, kind = 'file') {
|
|
191
|
+
const bundledTheme = loadBundledTreeIconTheme();
|
|
192
|
+
const iconId = resolveBundledTreeItemIconId(relativePath, kind);
|
|
193
|
+
return iconId ? bundledTheme.iconPathById.get(iconId) : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
50
196
|
function sessionIdleDecoration(session, now = Date.now()) {
|
|
51
197
|
if (!session) {
|
|
52
198
|
return undefined;
|
|
@@ -105,9 +251,182 @@ function formatCountLabel(count, singular, plural = `${singular}s`) {
|
|
|
105
251
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
106
252
|
}
|
|
107
253
|
|
|
254
|
+
function branchSegments(branch) {
|
|
255
|
+
return String(branch || '')
|
|
256
|
+
.split('/')
|
|
257
|
+
.map((segment) => segment.trim())
|
|
258
|
+
.filter(Boolean);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function compactBranchLabel(branch) {
|
|
262
|
+
const segments = branchSegments(branch);
|
|
263
|
+
if (segments.length >= 3 && segments[0] === 'agent') {
|
|
264
|
+
return `${segments[1]}/${segments.slice(2).join('/')}`;
|
|
265
|
+
}
|
|
266
|
+
return segments.join('/');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sessionFileCountLabel(session) {
|
|
270
|
+
const activityCountLabel = typeof session?.activityCountLabel === 'string'
|
|
271
|
+
? session.activityCountLabel.trim()
|
|
272
|
+
: '';
|
|
273
|
+
if (activityCountLabel) {
|
|
274
|
+
return activityCountLabel;
|
|
275
|
+
}
|
|
276
|
+
if ((session?.changeCount || 0) > 0) {
|
|
277
|
+
return formatCountLabel(session.changeCount, 'file');
|
|
278
|
+
}
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function uniqueStringList(values) {
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
const result = [];
|
|
285
|
+
|
|
286
|
+
for (const value of values) {
|
|
287
|
+
if (typeof value !== 'string' || seen.has(value)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
seen.add(value);
|
|
291
|
+
result.push(value);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeSessionProviderToken(value) {
|
|
298
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resolveSessionProvider(session) {
|
|
302
|
+
const signals = [
|
|
303
|
+
session?.cliName,
|
|
304
|
+
session?.agentName,
|
|
305
|
+
session?.branch,
|
|
306
|
+
]
|
|
307
|
+
.map(normalizeSessionProviderToken)
|
|
308
|
+
.filter(Boolean);
|
|
309
|
+
|
|
310
|
+
if (signals.some((value) => value.includes('claude'))) {
|
|
311
|
+
return {
|
|
312
|
+
...SESSION_PROVIDER_BRANDS.claude,
|
|
313
|
+
cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (signals.some((value) => value.includes('codex') || value.includes('openai'))) {
|
|
317
|
+
return {
|
|
318
|
+
...SESSION_PROVIDER_BRANDS.openai,
|
|
319
|
+
cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function sessionProviderDecoration(session) {
|
|
326
|
+
const provider = resolveSessionProvider(session);
|
|
327
|
+
if (!provider) {
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const cliName = provider.cliName || provider.id;
|
|
332
|
+
return {
|
|
333
|
+
badge: provider.badge,
|
|
334
|
+
tooltip: `${provider.label} session via ${cliName}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function normalizeSnapshotIdentityValue(value) {
|
|
339
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function sessionSnapshotDisplayName(session) {
|
|
343
|
+
return normalizeSnapshotIdentityValue(session?.snapshotName)
|
|
344
|
+
|| normalizeSnapshotIdentityValue(session?.snapshotEmail);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function sessionSnapshotBadge(session) {
|
|
348
|
+
const displayName = sessionSnapshotDisplayName(session);
|
|
349
|
+
const match = displayName.match(/[a-z0-9]/i);
|
|
350
|
+
return match ? match[0].toUpperCase() : '';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function sessionSnapshotDescription(session) {
|
|
354
|
+
const displayName = sessionSnapshotDisplayName(session);
|
|
355
|
+
return displayName ? `snapshot ${displayName}` : '';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sessionSnapshotDecoration(session) {
|
|
359
|
+
const badge = sessionSnapshotBadge(session);
|
|
360
|
+
const displayName = sessionSnapshotDisplayName(session);
|
|
361
|
+
if (!badge || !displayName) {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
badge,
|
|
367
|
+
tooltip: `Snapshot ${displayName}`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function sessionIdentityDecoration(session) {
|
|
372
|
+
return sessionSnapshotDecoration(session) || sessionProviderDecoration(session);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function stringListsEqual(left, right) {
|
|
376
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return left.every((value, index) => value === right[index]);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function ensureManagedRepoScanIgnores() {
|
|
384
|
+
if (typeof vscode.workspace.getConfiguration !== 'function') {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
|
389
|
+
if (workspaceFolders.length === 0) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const workspaceFolderTarget = workspaceFolders.length > 1
|
|
394
|
+
? vscode.ConfigurationTarget?.WorkspaceFolder
|
|
395
|
+
: vscode.ConfigurationTarget?.Workspace;
|
|
396
|
+
if (workspaceFolderTarget === undefined) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const workspaceFolder of workspaceFolders) {
|
|
401
|
+
const gitConfig = vscode.workspace.getConfiguration(GIT_CONFIGURATION_SECTION, workspaceFolder);
|
|
402
|
+
const configuredIgnoredFolders = gitConfig.get(REPO_SCAN_IGNORED_FOLDERS_SETTING);
|
|
403
|
+
const existingIgnoredFolders = Array.isArray(configuredIgnoredFolders)
|
|
404
|
+
? configuredIgnoredFolders
|
|
405
|
+
: [];
|
|
406
|
+
const nextIgnoredFolders = uniqueStringList([
|
|
407
|
+
...existingIgnoredFolders,
|
|
408
|
+
...MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
409
|
+
]);
|
|
410
|
+
|
|
411
|
+
if (stringListsEqual(existingIgnoredFolders, nextIgnoredFolders)) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
await gitConfig.update(
|
|
417
|
+
REPO_SCAN_IGNORED_FOLDERS_SETTING,
|
|
418
|
+
nextIgnoredFolders,
|
|
419
|
+
workspaceFolderTarget,
|
|
420
|
+
);
|
|
421
|
+
} catch {
|
|
422
|
+
// Leave the extension usable even when the current workspace settings cannot be updated.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
108
427
|
function sessionIdentityLabel(session) {
|
|
109
428
|
const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
|
|
110
|
-
const taskName =
|
|
429
|
+
const taskName = sessionDisplayLabel(session);
|
|
111
430
|
const label = typeof session?.label === 'string' ? session.label.trim() : '';
|
|
112
431
|
|
|
113
432
|
if (agentName && taskName) {
|
|
@@ -145,9 +464,10 @@ function agentBadgeFromBranch(branch) {
|
|
|
145
464
|
}
|
|
146
465
|
|
|
147
466
|
function buildActiveAgentsStatusSummary(summary) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
467
|
+
const workingCount = summary?.workingCount || 0;
|
|
468
|
+
const idleCount = summary?.idleCount || 0;
|
|
469
|
+
if (workingCount > 0 || idleCount > 0) {
|
|
470
|
+
return `$(git-branch) ${workingCount} working · ${idleCount} idle`;
|
|
151
471
|
}
|
|
152
472
|
return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
|
|
153
473
|
}
|
|
@@ -159,7 +479,7 @@ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
|
159
479
|
sessionIdentityLabel(selectedSession),
|
|
160
480
|
formatCountLabel(selectedSession.lockCount || 0, 'lock'),
|
|
161
481
|
selectedSession.worktreePath,
|
|
162
|
-
'Click to open
|
|
482
|
+
'Click to open Active Agents.',
|
|
163
483
|
].filter(Boolean).join('\n');
|
|
164
484
|
}
|
|
165
485
|
|
|
@@ -167,11 +487,490 @@ function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
|
167
487
|
return [
|
|
168
488
|
formatCountLabel(activeCount, 'active agent'),
|
|
169
489
|
formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
|
|
490
|
+
formatCountLabel(summary?.idleCount || 0, 'idle session'),
|
|
491
|
+
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
492
|
+
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
170
493
|
summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
|
|
171
|
-
'Click to open
|
|
494
|
+
'Click to open Active Agents.',
|
|
495
|
+
].filter(Boolean).join('\n');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function compactRelativePath(relativePath) {
|
|
499
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
500
|
+
if (!normalized) {
|
|
501
|
+
return '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
505
|
+
if (segments.length <= 2) {
|
|
506
|
+
return normalized;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return `${segments[0]}/.../${segments[segments.length - 1]}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) {
|
|
513
|
+
const compactPaths = uniqueStringList((paths || [])
|
|
514
|
+
.map(normalizeRelativePath)
|
|
515
|
+
.filter(Boolean)
|
|
516
|
+
.map((relativePath) => compactRelativePath(relativePath)))
|
|
517
|
+
.slice(0, maxCount);
|
|
518
|
+
if (compactPaths.length === 0) {
|
|
519
|
+
return '';
|
|
520
|
+
}
|
|
521
|
+
return compactPaths.join(', ');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function isProtectedBranchName(branch) {
|
|
525
|
+
return branch === 'main' || branch === 'dev';
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function countWorkingSessions(sessions) {
|
|
529
|
+
return sessions.filter((session) => (
|
|
530
|
+
session.activityKind === 'working' || session.activityKind === 'blocked'
|
|
531
|
+
)).length;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function countIdleSessions(sessions) {
|
|
535
|
+
return sessions.filter((session) => (
|
|
536
|
+
session.activityKind === 'idle' || session.activityKind === 'stalled'
|
|
537
|
+
)).length;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function sessionLastActiveAt(session) {
|
|
541
|
+
return [
|
|
542
|
+
session?.lastHeartbeatAt,
|
|
543
|
+
session?.lastFileActivityAt,
|
|
544
|
+
session?.telemetryUpdatedAt,
|
|
545
|
+
session?.startedAt,
|
|
546
|
+
].find((value) => typeof value === 'string' && value.trim().length > 0) || '';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function sessionLastActiveLabel(session) {
|
|
550
|
+
const lastActiveAt = sessionLastActiveAt(session);
|
|
551
|
+
if (!lastActiveAt) {
|
|
552
|
+
return '';
|
|
553
|
+
}
|
|
554
|
+
return formatElapsedFrom(lastActiveAt);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function sessionLastActiveAgeMs(session, now = Date.now()) {
|
|
558
|
+
const lastActiveAt = sessionLastActiveAt(session);
|
|
559
|
+
const timestamp = Date.parse(lastActiveAt);
|
|
560
|
+
if (!Number.isFinite(timestamp)) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
return Math.max(0, now - timestamp);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function sessionFreshnessLabel(session, now = Date.now()) {
|
|
567
|
+
const ageMs = sessionLastActiveAgeMs(session, now);
|
|
568
|
+
if (session.activityKind === 'blocked') {
|
|
569
|
+
return 'Needs attention';
|
|
570
|
+
}
|
|
571
|
+
if (session.activityKind === 'stalled') {
|
|
572
|
+
return 'Possibly stale';
|
|
573
|
+
}
|
|
574
|
+
if (session.activityKind === 'dead') {
|
|
575
|
+
return 'Stopped';
|
|
576
|
+
}
|
|
577
|
+
if (ageMs === null) {
|
|
578
|
+
return '';
|
|
579
|
+
}
|
|
580
|
+
if (ageMs <= IDLE_WARNING_MS) {
|
|
581
|
+
return 'Fresh';
|
|
582
|
+
}
|
|
583
|
+
if (ageMs <= RECENTLY_ACTIVE_WINDOW_MS) {
|
|
584
|
+
return 'Recently active';
|
|
585
|
+
}
|
|
586
|
+
if (session.activityKind === 'idle') {
|
|
587
|
+
return 'Idle';
|
|
588
|
+
}
|
|
589
|
+
return 'Recently active';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function sessionStatusLabel(session) {
|
|
593
|
+
switch (session.activityKind) {
|
|
594
|
+
case 'blocked':
|
|
595
|
+
return 'Blocked';
|
|
596
|
+
case 'working':
|
|
597
|
+
return 'Working';
|
|
598
|
+
case 'idle':
|
|
599
|
+
return 'Idle';
|
|
600
|
+
case 'stalled':
|
|
601
|
+
return 'Stale';
|
|
602
|
+
case 'dead':
|
|
603
|
+
return 'Dead';
|
|
604
|
+
default:
|
|
605
|
+
return 'Thinking';
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function sessionHealthScore(session) {
|
|
610
|
+
return Number.isInteger(session?.sessionHealth?.score) ? session.sessionHealth.score : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function buildSessionHealthCompactLabel(session) {
|
|
614
|
+
const score = sessionHealthScore(session);
|
|
615
|
+
return score === null ? '' : `${score}/100`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildSessionHealthSummary(session) {
|
|
619
|
+
const compactLabel = buildSessionHealthCompactLabel(session);
|
|
620
|
+
if (!compactLabel) {
|
|
621
|
+
return '';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const label = typeof session?.sessionHealth?.label === 'string'
|
|
625
|
+
? session.sessionHealth.label.trim()
|
|
626
|
+
: '';
|
|
627
|
+
return label ? `${compactLabel} · ${label}` : compactLabel;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function buildSessionHealthDriversSummary(session) {
|
|
631
|
+
const primaryDriver = typeof session?.sessionHealth?.primaryDriver === 'string'
|
|
632
|
+
? session.sessionHealth.primaryDriver.trim()
|
|
633
|
+
: '';
|
|
634
|
+
const secondaries = uniqueStringList(Array.isArray(session?.sessionHealth?.secondaries)
|
|
635
|
+
? session.sessionHealth.secondaries.map((value) => String(value || '').trim())
|
|
636
|
+
: []);
|
|
637
|
+
return [
|
|
638
|
+
primaryDriver ? `Primary: ${primaryDriver}` : '',
|
|
639
|
+
secondaries.length > 0 ? `Secondary: ${secondaries.join(', ')}` : '',
|
|
640
|
+
].filter(Boolean).join(' | ');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function buildSessionHealthTooltip(session) {
|
|
644
|
+
const outputLine = typeof session?.sessionHealth?.outputLine === 'string'
|
|
645
|
+
? session.sessionHealth.outputLine.trim()
|
|
646
|
+
: '';
|
|
647
|
+
if (outputLine) {
|
|
648
|
+
return outputLine;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return [
|
|
652
|
+
buildSessionHealthSummary(session),
|
|
653
|
+
buildSessionHealthDriversSummary(session),
|
|
172
654
|
].filter(Boolean).join('\n');
|
|
173
655
|
}
|
|
174
656
|
|
|
657
|
+
function buildSessionTopFiles(session) {
|
|
658
|
+
return uniqueStringList((session?.worktreeChangedPaths || [])
|
|
659
|
+
.map(normalizeRelativePath)
|
|
660
|
+
.filter(Boolean))
|
|
661
|
+
.slice(0, SESSION_TOP_FILE_COUNT);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function buildSessionRecentChangeSummary(session) {
|
|
665
|
+
if (session?.latestTaskPreview && session.latestTaskPreview !== session.taskName) {
|
|
666
|
+
return session.latestTaskPreview;
|
|
667
|
+
}
|
|
668
|
+
const topFiles = summarizeCompactPaths(session?.worktreeChangedPaths || []);
|
|
669
|
+
if (topFiles) {
|
|
670
|
+
return `Changed ${topFiles}`;
|
|
671
|
+
}
|
|
672
|
+
if (session?.activitySummary) {
|
|
673
|
+
return session.activitySummary;
|
|
674
|
+
}
|
|
675
|
+
return 'No recent change summary.';
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function sessionRiskBadges(session) {
|
|
679
|
+
return uniqueStringList([
|
|
680
|
+
session?.activityKind === 'blocked' ? 'Blocked' : '',
|
|
681
|
+
session?.activityKind === 'stalled' ? 'Stale' : '',
|
|
682
|
+
session?.conflictCount > 0 ? 'Conflict' : '',
|
|
683
|
+
session?.lockCount > 0 ? 'Locked' : '',
|
|
684
|
+
].filter(Boolean));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function changeRiskBadges(change) {
|
|
688
|
+
return uniqueStringList([
|
|
689
|
+
change?.protectedBranch ? 'Protected branch' : '',
|
|
690
|
+
change?.hasForeignLock ? 'Conflict' : '',
|
|
691
|
+
!change?.hasForeignLock && change?.lockOwnerBranch ? 'Locked' : '',
|
|
692
|
+
change?.deltaLabel || '',
|
|
693
|
+
].filter(Boolean));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function buildSessionCardDescription(session) {
|
|
697
|
+
const provider = resolveSessionProvider(session);
|
|
698
|
+
const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`;
|
|
699
|
+
const descriptionParts = [
|
|
700
|
+
statusAgentLabel,
|
|
701
|
+
provider?.label ? `via ${provider.label}` : '',
|
|
702
|
+
sessionSnapshotDescription(session),
|
|
703
|
+
session.deltaLabel || '',
|
|
704
|
+
session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '',
|
|
705
|
+
session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '',
|
|
706
|
+
buildSessionHealthCompactLabel(session),
|
|
707
|
+
session.freshnessLabel || '',
|
|
708
|
+
session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '',
|
|
709
|
+
].filter(Boolean);
|
|
710
|
+
return descriptionParts.join(' · ');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildRawSessionDescription(session) {
|
|
714
|
+
const provider = resolveSessionProvider(session);
|
|
715
|
+
const descriptionParts = [sessionStatusLabel(session)];
|
|
716
|
+
const fileCountLabel = sessionFileCountLabel(session);
|
|
717
|
+
if (fileCountLabel) {
|
|
718
|
+
descriptionParts.push(fileCountLabel);
|
|
719
|
+
}
|
|
720
|
+
if (provider?.label) {
|
|
721
|
+
descriptionParts.push(provider.label);
|
|
722
|
+
}
|
|
723
|
+
const snapshot = sessionSnapshotDescription(session);
|
|
724
|
+
if (snapshot) {
|
|
725
|
+
descriptionParts.push(snapshot);
|
|
726
|
+
}
|
|
727
|
+
descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
|
|
728
|
+
const sessionHealthLabel = buildSessionHealthCompactLabel(session);
|
|
729
|
+
if (sessionHealthLabel) {
|
|
730
|
+
descriptionParts.push(sessionHealthLabel);
|
|
731
|
+
}
|
|
732
|
+
if (session.lockCount > 0) {
|
|
733
|
+
descriptionParts.push(formatCountLabel(session.lockCount, 'lock'));
|
|
734
|
+
}
|
|
735
|
+
return descriptionParts.join(' · ');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function buildSessionTooltip(session, description) {
|
|
739
|
+
const provider = resolveSessionProvider(session);
|
|
740
|
+
const riskSummary = uniqueStringList([
|
|
741
|
+
...(session?.riskBadges || []),
|
|
742
|
+
session?.deltaLabel || '',
|
|
743
|
+
].filter(Boolean)).join(', ');
|
|
744
|
+
const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []);
|
|
745
|
+
const sessionHealthSummary = buildSessionHealthSummary(session);
|
|
746
|
+
const sessionHealthDrivers = buildSessionHealthDriversSummary(session);
|
|
747
|
+
return [
|
|
748
|
+
session.branch,
|
|
749
|
+
provider?.label
|
|
750
|
+
? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}`
|
|
751
|
+
: '',
|
|
752
|
+
sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '',
|
|
753
|
+
`${session.agentName} · ${sessionDisplayLabel(session)}`,
|
|
754
|
+
`Status ${description}`,
|
|
755
|
+
sessionHealthSummary ? `Session health ${sessionHealthSummary}` : '',
|
|
756
|
+
sessionHealthDrivers ? `Drivers ${sessionHealthDrivers}` : '',
|
|
757
|
+
session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '',
|
|
758
|
+
topFiles ? `Top files ${topFiles}` : '',
|
|
759
|
+
riskSummary ? `Signals ${riskSummary}` : '',
|
|
760
|
+
session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
|
|
761
|
+
session.lastActiveAt ? `Last active ${session.lastActiveAt}` : '',
|
|
762
|
+
session.sourceKind === 'worktree-lock'
|
|
763
|
+
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
764
|
+
: `Started ${session.startedAt}`,
|
|
765
|
+
session.worktreePath,
|
|
766
|
+
].filter(Boolean).join('\n');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function buildUnassignedChangeDescription(change) {
|
|
770
|
+
return [
|
|
771
|
+
change.statusLabel,
|
|
772
|
+
...changeRiskBadges(change),
|
|
773
|
+
].filter(Boolean).join(' · ');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function buildWorktreeBranchDescription(sessions) {
|
|
777
|
+
const sessionList = Array.isArray(sessions) ? sessions : [];
|
|
778
|
+
const primarySession = sessionList[0] || null;
|
|
779
|
+
if (!primarySession) {
|
|
780
|
+
return '';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const descriptionParts = [
|
|
784
|
+
`${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`,
|
|
785
|
+
sessionSnapshotDescription(primarySession),
|
|
786
|
+
];
|
|
787
|
+
if (sessionList.length > 1) {
|
|
788
|
+
descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
|
|
789
|
+
}
|
|
790
|
+
return descriptionParts.filter(Boolean).join(' · ');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function buildOverviewDescription(summary) {
|
|
794
|
+
return [
|
|
795
|
+
formatCountLabel(summary?.workingCount || 0, 'working agent'),
|
|
796
|
+
formatCountLabel(summary?.idleCount || 0, 'idle agent'),
|
|
797
|
+
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
798
|
+
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
799
|
+
formatCountLabel(summary?.conflictCount || 0, 'conflict'),
|
|
800
|
+
].join(' · ');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildRepoDescription(summary) {
|
|
804
|
+
return buildOverviewDescription(summary);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function buildRepoTooltip(repoRoot, summary) {
|
|
808
|
+
return [
|
|
809
|
+
repoRoot,
|
|
810
|
+
buildOverviewDescription(summary),
|
|
811
|
+
].join('\n');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function repoRootDisplayLabel(repoRoot) {
|
|
815
|
+
const normalizedRepoRoot = path.resolve(repoRoot);
|
|
816
|
+
const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || [])
|
|
817
|
+
.map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : ''))
|
|
818
|
+
.filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot))
|
|
819
|
+
.sort((left, right) => right.length - left.length);
|
|
820
|
+
|
|
821
|
+
const workspaceRoot = matchingWorkspaceRoots[0];
|
|
822
|
+
if (!workspaceRoot) {
|
|
823
|
+
return path.basename(normalizedRepoRoot);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const workspaceLabel = path.basename(workspaceRoot);
|
|
827
|
+
const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot));
|
|
828
|
+
if (!relativePath) {
|
|
829
|
+
return workspaceLabel;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return [
|
|
833
|
+
workspaceLabel,
|
|
834
|
+
...relativePath.split('/').filter(Boolean),
|
|
835
|
+
].join('/');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function sessionSnapshotKey(session) {
|
|
839
|
+
return `${session?.repoRoot || ''}::${session?.branch || ''}`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function changeSnapshotKey(repoRoot, change) {
|
|
843
|
+
return `${repoRoot || ''}::${normalizeRelativePath(change?.relativePath)}`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function buildSessionSnapshot(session) {
|
|
847
|
+
return {
|
|
848
|
+
activityKind: session.activityKind,
|
|
849
|
+
changeCount: session.changeCount || 0,
|
|
850
|
+
conflictCount: session.conflictCount || 0,
|
|
851
|
+
lockCount: session.lockCount || 0,
|
|
852
|
+
changedPaths: [...(session.changedPaths || [])],
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function buildChangeSnapshot(change) {
|
|
857
|
+
return {
|
|
858
|
+
statusLabel: change.statusLabel,
|
|
859
|
+
hasForeignLock: Boolean(change.hasForeignLock),
|
|
860
|
+
lockOwnerBranch: change.lockOwnerBranch || '',
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function deriveSessionDelta(previousSnapshot, currentSession) {
|
|
865
|
+
if (!previousSnapshot) {
|
|
866
|
+
return '';
|
|
867
|
+
}
|
|
868
|
+
if (currentSession.conflictCount > previousSnapshot.conflictCount) {
|
|
869
|
+
return 'Conflict';
|
|
870
|
+
}
|
|
871
|
+
if (currentSession.activityKind !== previousSnapshot.activityKind) {
|
|
872
|
+
return sessionStatusLabel(currentSession);
|
|
873
|
+
}
|
|
874
|
+
if (
|
|
875
|
+
currentSession.changeCount !== previousSnapshot.changeCount
|
|
876
|
+
|| !stringListsEqual(currentSession.changedPaths || [], previousSnapshot.changedPaths || [])
|
|
877
|
+
) {
|
|
878
|
+
return 'New';
|
|
879
|
+
}
|
|
880
|
+
if (currentSession.lockCount !== previousSnapshot.lockCount) {
|
|
881
|
+
return 'Updated';
|
|
882
|
+
}
|
|
883
|
+
return '';
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function deriveChangeDelta(previousSnapshot, currentChange) {
|
|
887
|
+
if (!previousSnapshot) {
|
|
888
|
+
return '';
|
|
889
|
+
}
|
|
890
|
+
if (currentChange.hasForeignLock && !previousSnapshot.hasForeignLock) {
|
|
891
|
+
return 'Conflict';
|
|
892
|
+
}
|
|
893
|
+
if (
|
|
894
|
+
currentChange.statusLabel !== previousSnapshot.statusLabel
|
|
895
|
+
|| currentChange.lockOwnerBranch !== previousSnapshot.lockOwnerBranch
|
|
896
|
+
) {
|
|
897
|
+
return 'Updated';
|
|
898
|
+
}
|
|
899
|
+
return '';
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function workingSessionSortKey(session) {
|
|
903
|
+
if (session.activityKind === 'blocked') {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
if (session.conflictCount > 0) {
|
|
907
|
+
return 1;
|
|
908
|
+
}
|
|
909
|
+
if (session.deltaLabel === 'Conflict') {
|
|
910
|
+
return 2;
|
|
911
|
+
}
|
|
912
|
+
if (session.deltaLabel === 'New') {
|
|
913
|
+
return 3;
|
|
914
|
+
}
|
|
915
|
+
return 4;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function idleSessionSortKey(session) {
|
|
919
|
+
if (session.activityKind === 'stalled') {
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
if (session.activityKind === 'idle') {
|
|
923
|
+
return 1;
|
|
924
|
+
}
|
|
925
|
+
if (session.activityKind === 'dead') {
|
|
926
|
+
return 2;
|
|
927
|
+
}
|
|
928
|
+
return 3;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function sortSessionsForWorkingNow(sessions) {
|
|
932
|
+
return [...sessions].sort((left, right) => {
|
|
933
|
+
const keyDelta = workingSessionSortKey(left) - workingSessionSortKey(right);
|
|
934
|
+
if (keyDelta !== 0) {
|
|
935
|
+
return keyDelta;
|
|
936
|
+
}
|
|
937
|
+
const timeDelta = sessionLastActiveAgeMs(left) - sessionLastActiveAgeMs(right);
|
|
938
|
+
if (Number.isFinite(timeDelta) && timeDelta !== 0) {
|
|
939
|
+
return timeDelta;
|
|
940
|
+
}
|
|
941
|
+
const changeDelta = (right.changeCount || 0) - (left.changeCount || 0);
|
|
942
|
+
if (changeDelta !== 0) {
|
|
943
|
+
return changeDelta;
|
|
944
|
+
}
|
|
945
|
+
return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right));
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function sortSessionsForIdleThinking(sessions) {
|
|
950
|
+
return [...sessions].sort((left, right) => {
|
|
951
|
+
const keyDelta = idleSessionSortKey(left) - idleSessionSortKey(right);
|
|
952
|
+
if (keyDelta !== 0) {
|
|
953
|
+
return keyDelta;
|
|
954
|
+
}
|
|
955
|
+
const timeDelta = sessionLastActiveAgeMs(right) - sessionLastActiveAgeMs(left);
|
|
956
|
+
if (Number.isFinite(timeDelta) && timeDelta !== 0) {
|
|
957
|
+
return timeDelta;
|
|
958
|
+
}
|
|
959
|
+
return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right));
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function sortUnassignedChanges(changes) {
|
|
964
|
+
return [...changes].sort((left, right) => {
|
|
965
|
+
const leftBadges = changeRiskBadges(left).length;
|
|
966
|
+
const rightBadges = changeRiskBadges(right).length;
|
|
967
|
+
if (leftBadges !== rightBadges) {
|
|
968
|
+
return rightBadges - leftBadges;
|
|
969
|
+
}
|
|
970
|
+
return normalizeRelativePath(left.relativePath).localeCompare(normalizeRelativePath(right.relativePath));
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
175
974
|
function escapeHtml(value) {
|
|
176
975
|
return String(value || '')
|
|
177
976
|
.replace(/&/g, '&')
|
|
@@ -366,7 +1165,12 @@ class SessionDecorationProvider {
|
|
|
366
1165
|
};
|
|
367
1166
|
}
|
|
368
1167
|
|
|
369
|
-
|
|
1168
|
+
const session = this.sessionsByUri.get(uri.toString());
|
|
1169
|
+
const idleDecoration = sessionIdleDecoration(session, this.nowProvider());
|
|
1170
|
+
if (idleDecoration) {
|
|
1171
|
+
return idleDecoration;
|
|
1172
|
+
}
|
|
1173
|
+
return sessionIdentityDecoration(session);
|
|
370
1174
|
}
|
|
371
1175
|
}
|
|
372
1176
|
|
|
@@ -374,49 +1178,50 @@ class InfoItem extends vscode.TreeItem {
|
|
|
374
1178
|
constructor(label, description = '') {
|
|
375
1179
|
super(label, vscode.TreeItemCollapsibleState.None);
|
|
376
1180
|
this.description = description;
|
|
377
|
-
this.iconPath =
|
|
1181
|
+
this.iconPath = themeIcon('info');
|
|
1182
|
+
this.tooltip = [label, description].filter(Boolean).join('\n');
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
class DetailItem extends vscode.TreeItem {
|
|
1187
|
+
constructor(label, description = '', options = {}) {
|
|
1188
|
+
super(label, vscode.TreeItemCollapsibleState.None);
|
|
1189
|
+
this.description = description;
|
|
1190
|
+
this.tooltip = options.tooltip || [label, description].filter(Boolean).join('\n');
|
|
1191
|
+
this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined;
|
|
378
1192
|
}
|
|
379
1193
|
}
|
|
380
1194
|
|
|
381
1195
|
class RepoItem extends vscode.TreeItem {
|
|
382
|
-
constructor(repoRoot, sessions, changes) {
|
|
383
|
-
|
|
1196
|
+
constructor(repoRoot, sessions, changes, options = {}) {
|
|
1197
|
+
const label = typeof options.label === 'string' && options.label.trim()
|
|
1198
|
+
? options.label.trim()
|
|
1199
|
+
: repoRootDisplayLabel(repoRoot);
|
|
1200
|
+
super(label, vscode.TreeItemCollapsibleState.Expanded);
|
|
384
1201
|
this.repoRoot = repoRoot;
|
|
385
1202
|
this.sessions = sessions;
|
|
386
1203
|
this.changes = changes;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
if (deadCount > 0) {
|
|
395
|
-
descriptionParts.push(`${deadCount} dead`);
|
|
396
|
-
}
|
|
397
|
-
if (workingCount > 0) {
|
|
398
|
-
descriptionParts.push(`${workingCount} working`);
|
|
399
|
-
}
|
|
400
|
-
const changedCount = countChangedPaths(repoRoot, sessions, changes);
|
|
401
|
-
if (changedCount > 0) {
|
|
402
|
-
descriptionParts.push(`${changedCount} changed`);
|
|
403
|
-
}
|
|
404
|
-
this.description = descriptionParts.join(' · ');
|
|
405
|
-
this.tooltip = [
|
|
406
|
-
repoRoot,
|
|
407
|
-
this.description,
|
|
408
|
-
].join('\n');
|
|
409
|
-
this.iconPath = new vscode.ThemeIcon('repo');
|
|
1204
|
+
this.unassignedChanges = options.unassignedChanges || [];
|
|
1205
|
+
this.lockEntries = options.lockEntries || [];
|
|
1206
|
+
this.overview = options.overview || buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries);
|
|
1207
|
+
this.description = buildRepoDescription(this.overview);
|
|
1208
|
+
this.tooltip = buildRepoTooltip(repoRoot, this.overview);
|
|
1209
|
+
this.iconPath = themeIcon('repo');
|
|
410
1210
|
this.contextValue = 'gitguardex.repo';
|
|
411
1211
|
}
|
|
412
1212
|
}
|
|
413
1213
|
|
|
414
1214
|
class SectionItem extends vscode.TreeItem {
|
|
415
1215
|
constructor(label, items, options = {}) {
|
|
416
|
-
|
|
1216
|
+
const collapsibleState = items.length > 0
|
|
1217
|
+
? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded)
|
|
1218
|
+
: vscode.TreeItemCollapsibleState.None;
|
|
1219
|
+
super(label, collapsibleState);
|
|
417
1220
|
this.items = items;
|
|
418
1221
|
this.description = options.description
|
|
419
1222
|
|| (items.length > 0 ? String(items.length) : '');
|
|
1223
|
+
this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n');
|
|
1224
|
+
this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined;
|
|
420
1225
|
this.contextValue = 'gitguardex.section';
|
|
421
1226
|
}
|
|
422
1227
|
}
|
|
@@ -425,32 +1230,35 @@ class WorktreeItem extends vscode.TreeItem {
|
|
|
425
1230
|
constructor(worktreePath, sessions, items = [], options = {}) {
|
|
426
1231
|
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
|
|
427
1232
|
const sessionList = Array.isArray(sessions) ? sessions : [];
|
|
1233
|
+
const primarySession = options.resourceSession || sessionList[0] || null;
|
|
428
1234
|
const changedCount = Number.isInteger(options.changedCount)
|
|
429
1235
|
? options.changedCount
|
|
430
1236
|
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
1237
|
+
const label = typeof options.label === 'string' && options.label.trim()
|
|
1238
|
+
? options.label.trim()
|
|
1239
|
+
: worktreeDisplayLabel(normalizedWorktreePath, sessionList);
|
|
435
1240
|
super(
|
|
436
|
-
|
|
1241
|
+
label,
|
|
437
1242
|
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
|
|
438
1243
|
);
|
|
439
1244
|
this.worktreePath = normalizedWorktreePath;
|
|
440
1245
|
this.sessions = sessionList;
|
|
441
1246
|
this.items = items;
|
|
442
|
-
this.description = options.description ||
|
|
1247
|
+
this.description = options.description || buildWorktreeDescription(sessionList, changedCount);
|
|
443
1248
|
this.tooltip = [
|
|
444
1249
|
normalizedWorktreePath,
|
|
445
1250
|
...sessionList.map((session) => session.branch).filter(Boolean),
|
|
446
1251
|
].filter(Boolean).join('\n');
|
|
447
|
-
this.iconPath =
|
|
1252
|
+
this.iconPath = themeIcon(options.iconId || 'folder', options.iconColorId);
|
|
1253
|
+
if (options.useSessionDecoration && primarySession?.branch) {
|
|
1254
|
+
this.resourceUri = sessionDecorationUri(primarySession.branch);
|
|
1255
|
+
}
|
|
448
1256
|
this.contextValue = 'gitguardex.worktree';
|
|
449
|
-
if (
|
|
1257
|
+
if (primarySession?.worktreePath) {
|
|
450
1258
|
this.command = {
|
|
451
1259
|
command: 'gitguardex.activeAgents.openWorktree',
|
|
452
1260
|
title: 'Open Agent Worktree',
|
|
453
|
-
arguments: [
|
|
1261
|
+
arguments: [primarySession],
|
|
454
1262
|
};
|
|
455
1263
|
}
|
|
456
1264
|
}
|
|
@@ -458,51 +1266,29 @@ class WorktreeItem extends vscode.TreeItem {
|
|
|
458
1266
|
|
|
459
1267
|
class SessionItem extends vscode.TreeItem {
|
|
460
1268
|
constructor(session, items = [], options = {}) {
|
|
461
|
-
const
|
|
1269
|
+
const variant = options.variant === 'raw' ? 'raw' : 'card';
|
|
462
1270
|
const label = typeof options.label === 'string' && options.label.trim()
|
|
463
1271
|
? options.label.trim()
|
|
464
|
-
: session.label;
|
|
1272
|
+
: (variant === 'raw' ? session.label : sessionDisplayLabel(session));
|
|
1273
|
+
const collapsibleState = items.length > 0
|
|
1274
|
+
? (options.collapsedState ?? (
|
|
1275
|
+
variant === 'raw'
|
|
1276
|
+
? vscode.TreeItemCollapsibleState.Expanded
|
|
1277
|
+
: vscode.TreeItemCollapsibleState.Collapsed
|
|
1278
|
+
))
|
|
1279
|
+
: vscode.TreeItemCollapsibleState.None;
|
|
465
1280
|
super(
|
|
466
1281
|
label,
|
|
467
|
-
|
|
1282
|
+
collapsibleState,
|
|
468
1283
|
);
|
|
469
1284
|
this.session = session;
|
|
470
1285
|
this.items = items;
|
|
471
1286
|
this.resourceUri = sessionDecorationUri(session.branch);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (lockCount > 0) {
|
|
478
|
-
descriptionParts.push(`${lockCount} $(lock)`);
|
|
479
|
-
}
|
|
480
|
-
this.description = descriptionParts.join(' · ');
|
|
481
|
-
const tooltipLines = [
|
|
482
|
-
session.branch,
|
|
483
|
-
`${session.agentName} · ${session.taskName}`,
|
|
484
|
-
session.latestTaskPreview && session.latestTaskPreview !== session.taskName
|
|
485
|
-
? `Live task ${session.latestTaskPreview}`
|
|
486
|
-
: '',
|
|
487
|
-
`Status ${this.description}`,
|
|
488
|
-
session.changeCount > 0
|
|
489
|
-
? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
|
|
490
|
-
: session.activitySummary,
|
|
491
|
-
`Locks ${lockCount}`,
|
|
492
|
-
session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
|
|
493
|
-
Number.isInteger(session.pid) && session.pid > 0
|
|
494
|
-
? session.pidAlive === false
|
|
495
|
-
? `PID ${session.pid} not alive`
|
|
496
|
-
: `PID ${session.pid} alive`
|
|
497
|
-
: '',
|
|
498
|
-
session.lastFileActivityAt ? `Last file activity ${session.lastFileActivityAt}` : '',
|
|
499
|
-
session.sourceKind === 'worktree-lock'
|
|
500
|
-
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
501
|
-
: `Started ${session.startedAt}`,
|
|
502
|
-
session.worktreePath,
|
|
503
|
-
];
|
|
504
|
-
this.tooltip = tooltipLines.filter(Boolean).join('\n');
|
|
505
|
-
this.iconPath = new vscode.ThemeIcon(resolveSessionActivityIconId(session.activityKind));
|
|
1287
|
+
this.description = variant === 'raw'
|
|
1288
|
+
? buildRawSessionDescription(session)
|
|
1289
|
+
: buildSessionCardDescription(session);
|
|
1290
|
+
this.tooltip = buildSessionTooltip(session, this.description);
|
|
1291
|
+
this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind));
|
|
506
1292
|
this.contextValue = 'gitguardex.session';
|
|
507
1293
|
this.command = {
|
|
508
1294
|
command: 'gitguardex.activeAgents.openWorktree',
|
|
@@ -513,31 +1299,47 @@ class SessionItem extends vscode.TreeItem {
|
|
|
513
1299
|
}
|
|
514
1300
|
|
|
515
1301
|
class FolderItem extends vscode.TreeItem {
|
|
516
|
-
constructor(label, relativePath, items) {
|
|
517
|
-
super(
|
|
1302
|
+
constructor(label, relativePath, items, options = {}) {
|
|
1303
|
+
super(
|
|
1304
|
+
label,
|
|
1305
|
+
items.length > 0
|
|
1306
|
+
? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded)
|
|
1307
|
+
: vscode.TreeItemCollapsibleState.None,
|
|
1308
|
+
);
|
|
518
1309
|
this.relativePath = relativePath;
|
|
519
1310
|
this.items = items;
|
|
520
|
-
this.
|
|
521
|
-
this.
|
|
522
|
-
this.
|
|
1311
|
+
this.description = typeof options.description === 'string' ? options.description : '';
|
|
1312
|
+
this.tooltip = options.tooltip || relativePath || label;
|
|
1313
|
+
this.iconPath = options.iconPath
|
|
1314
|
+
|| (!options.iconId ? resolveBundledTreeItemIcon(relativePath || label, 'folder') : undefined)
|
|
1315
|
+
|| themeIcon(options.iconId || 'folder', options.iconColorId);
|
|
1316
|
+
this.contextValue = options.contextValue || 'gitguardex.folder';
|
|
523
1317
|
}
|
|
524
1318
|
}
|
|
525
1319
|
|
|
526
1320
|
class ChangeItem extends vscode.TreeItem {
|
|
527
|
-
constructor(change) {
|
|
528
|
-
|
|
1321
|
+
constructor(change, options = {}) {
|
|
1322
|
+
const label = typeof options.label === 'string' && options.label.trim()
|
|
1323
|
+
? options.label.trim()
|
|
1324
|
+
: path.basename(change.relativePath);
|
|
1325
|
+
super(label, vscode.TreeItemCollapsibleState.None);
|
|
529
1326
|
this.change = change;
|
|
530
|
-
this.description =
|
|
1327
|
+
this.description = typeof options.description === 'string'
|
|
1328
|
+
? options.description
|
|
1329
|
+
: change.statusLabel;
|
|
531
1330
|
this.tooltip = [
|
|
532
1331
|
change.relativePath,
|
|
1332
|
+
`Summary ${this.description}`,
|
|
533
1333
|
`Status ${change.statusText}`,
|
|
534
1334
|
change.originalPath ? `Renamed from ${change.originalPath}` : '',
|
|
535
1335
|
change.hasForeignLock ? `Locked by ${change.lockOwnerBranch}` : '',
|
|
536
1336
|
change.absolutePath,
|
|
537
1337
|
].filter(Boolean).join('\n');
|
|
538
1338
|
this.resourceUri = vscode.Uri.file(change.absolutePath);
|
|
539
|
-
if (change.hasForeignLock) {
|
|
540
|
-
this.iconPath =
|
|
1339
|
+
if (options.iconId || change.hasForeignLock) {
|
|
1340
|
+
this.iconPath = themeIcon(options.iconId || 'warning', options.iconColorId || 'list.warningForeground');
|
|
1341
|
+
} else {
|
|
1342
|
+
this.iconPath = options.iconPath || resolveBundledTreeItemIcon(change.relativePath || label, 'file');
|
|
541
1343
|
}
|
|
542
1344
|
this.contextValue = 'gitguardex.change';
|
|
543
1345
|
this.command = {
|
|
@@ -562,32 +1364,278 @@ function readPackageJson(repoRoot) {
|
|
|
562
1364
|
}
|
|
563
1365
|
}
|
|
564
1366
|
|
|
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}`;
|
|
1367
|
+
function resolveStartAgentCommand(repoRoot, details) {
|
|
1368
|
+
const taskArg = shellQuote(details.taskName);
|
|
1369
|
+
const agentArg = shellQuote(details.agentName);
|
|
1370
|
+
const localCodexAgentPath = path.join(repoRoot, 'scripts', 'codex-agent.sh');
|
|
1371
|
+
if (fs.existsSync(localCodexAgentPath)) {
|
|
1372
|
+
return `bash ./scripts/codex-agent.sh ${taskArg} ${agentArg}`;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const agentCodexScript = readPackageJson(repoRoot)?.scripts?.['agent:codex'];
|
|
1376
|
+
if (typeof agentCodexScript === 'string' && agentCodexScript.trim().length > 0) {
|
|
1377
|
+
return `npm run agent:codex -- ${taskArg} ${agentArg}`;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return `gx branch start ${taskArg} ${agentArg}`;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
function sessionTaskLabel(session) {
|
|
1384
|
+
const latestTaskPreview = typeof session?.latestTaskPreview === 'string'
|
|
1385
|
+
? session.latestTaskPreview.trim()
|
|
1386
|
+
: '';
|
|
1387
|
+
if (latestTaskPreview) {
|
|
1388
|
+
return latestTaskPreview;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
|
|
1392
|
+
if (taskName) {
|
|
1393
|
+
return taskName;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
return '';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function sessionDisplayLabel(session) {
|
|
1400
|
+
return sessionTaskLabel(session)
|
|
1401
|
+
|| session?.label
|
|
1402
|
+
|| compactBranchLabel(session?.branch)
|
|
1403
|
+
|| session?.branch
|
|
1404
|
+
|| path.basename(session?.worktreePath || '')
|
|
1405
|
+
|| 'session';
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function sessionTreeLabel(session) {
|
|
1409
|
+
return sessionTaskLabel(session) || compactBranchLabel(session?.branch) || sessionDisplayLabel(session);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function worktreeDisplayLabel(worktreePath, sessions) {
|
|
1413
|
+
const sessionList = Array.isArray(sessions)
|
|
1414
|
+
? sessions.filter(Boolean)
|
|
1415
|
+
: [];
|
|
1416
|
+
if (sessionList.length === 1) {
|
|
1417
|
+
return sessionDisplayLabel(sessionList[0]);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return path.basename(String(worktreePath || '').trim()) || 'worktree';
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function buildWorktreeDescription(sessions, changedCount) {
|
|
1424
|
+
const sessionList = Array.isArray(sessions)
|
|
1425
|
+
? sessions.filter(Boolean)
|
|
1426
|
+
: [];
|
|
1427
|
+
const primarySession = sessionList.length === 1 ? sessionList[0] : null;
|
|
1428
|
+
const totalLocks = sessionList.reduce((total, session) => total + (session.lockCount || 0), 0);
|
|
1429
|
+
const descriptionParts = [];
|
|
1430
|
+
|
|
1431
|
+
if (primarySession?.agentName) {
|
|
1432
|
+
descriptionParts.push(primarySession.agentName);
|
|
1433
|
+
} else {
|
|
1434
|
+
descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const fileCountLabel = primarySession
|
|
1438
|
+
? sessionFileCountLabel(primarySession)
|
|
1439
|
+
: changedCount > 0
|
|
1440
|
+
? formatCountLabel(changedCount, 'file')
|
|
1441
|
+
: '';
|
|
1442
|
+
if (fileCountLabel) {
|
|
1443
|
+
descriptionParts.push(fileCountLabel);
|
|
1444
|
+
}
|
|
1445
|
+
if (totalLocks > 0) {
|
|
1446
|
+
descriptionParts.push(formatCountLabel(totalLocks, 'lock'));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return descriptionParts.join(' · ');
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function sessionWorktreePath(session) {
|
|
1453
|
+
return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function resolveSessionProjectRelativePath(session) {
|
|
1457
|
+
const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
|
|
1458
|
+
if (!repoRoot) {
|
|
1459
|
+
return '';
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const resolveCandidate = (candidatePath) => {
|
|
1463
|
+
const normalizedCandidate = typeof candidatePath === 'string' ? candidatePath.trim() : '';
|
|
1464
|
+
if (!normalizedCandidate) {
|
|
1465
|
+
return '';
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const absolutePath = path.isAbsolute(normalizedCandidate)
|
|
1469
|
+
? path.resolve(normalizedCandidate)
|
|
1470
|
+
: path.resolve(repoRoot, normalizedCandidate);
|
|
1471
|
+
if (!isPathWithin(repoRoot, absolutePath) || !fs.existsSync(absolutePath)) {
|
|
1472
|
+
return '';
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return normalizeRelativePath(path.relative(repoRoot, absolutePath));
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
const isManagedWorktreeRelativePath = (relativePath) => {
|
|
1479
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
1480
|
+
return MANAGED_WORKTREE_RELATIVE_ROOTS.some((managedRoot) => {
|
|
1481
|
+
const normalizedManagedRoot = normalizeRelativePath(managedRoot);
|
|
1482
|
+
return normalizedRelativePath === normalizedManagedRoot
|
|
1483
|
+
|| normalizedRelativePath.startsWith(`${normalizedManagedRoot}/`);
|
|
1484
|
+
});
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
const explicitProjectPath = resolveCandidate(session?.projectPath);
|
|
1488
|
+
if (explicitProjectPath && !isManagedWorktreeRelativePath(explicitProjectPath)) {
|
|
1489
|
+
return explicitProjectPath;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
const namedProjectPath = resolveCandidate(session?.projectName);
|
|
1493
|
+
if (namedProjectPath && !isManagedWorktreeRelativePath(namedProjectPath)) {
|
|
1494
|
+
return namedProjectPath;
|
|
1495
|
+
}
|
|
1496
|
+
return '';
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function worktreeProjectRelativePath(sessions) {
|
|
1500
|
+
const projectPaths = uniqueStringList((sessions || [])
|
|
1501
|
+
.map((session) => resolveSessionProjectRelativePath(session))
|
|
1502
|
+
.filter(Boolean));
|
|
1503
|
+
return projectPaths.length === 1 ? projectPaths[0] : '';
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function repoEntryDisplayLabel(repoRoot, sessions) {
|
|
1507
|
+
const repoLabel = repoRootDisplayLabel(repoRoot);
|
|
1508
|
+
const projectPaths = uniqueStringList((sessions || [])
|
|
1509
|
+
.map((session) => resolveSessionProjectRelativePath(session))
|
|
1510
|
+
.filter(Boolean));
|
|
1511
|
+
if (projectPaths.length !== 1) {
|
|
1512
|
+
return repoLabel;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const [projectRelativePath] = projectPaths;
|
|
1516
|
+
const hasRootScopedSession = (sessions || []).some(
|
|
1517
|
+
(session) => !resolveSessionProjectRelativePath(session),
|
|
1518
|
+
);
|
|
1519
|
+
if (!projectRelativePath || hasRootScopedSession) {
|
|
1520
|
+
return repoLabel;
|
|
1521
|
+
}
|
|
1522
|
+
if (repoLabel.endsWith(`/${projectRelativePath}`)) {
|
|
1523
|
+
return repoLabel;
|
|
1524
|
+
}
|
|
1525
|
+
return `${repoLabel}/${projectRelativePath}`;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function buildProjectScopedDescription(entries) {
|
|
1529
|
+
const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []);
|
|
1530
|
+
if (sessions.length === 0) {
|
|
1531
|
+
return '';
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const changedCount = sessions.reduce((total, session) => total + (session.changeCount || 0), 0);
|
|
1535
|
+
const lockCount = sessions.reduce((total, session) => total + (session.lockCount || 0), 0);
|
|
1536
|
+
const descriptionParts = [formatCountLabel(sessions.length, 'agent')];
|
|
1537
|
+
if (changedCount > 0) {
|
|
1538
|
+
descriptionParts.push(formatCountLabel(changedCount, 'file'));
|
|
1539
|
+
}
|
|
1540
|
+
if (lockCount > 0) {
|
|
1541
|
+
descriptionParts.push(formatCountLabel(lockCount, 'lock'));
|
|
1542
|
+
}
|
|
1543
|
+
return descriptionParts.join(' · ');
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function buildProjectScopedItems(entries, options = {}) {
|
|
1547
|
+
const normalizedEntries = Array.isArray(entries)
|
|
1548
|
+
? entries.filter((entry) => entry?.item)
|
|
1549
|
+
: [];
|
|
1550
|
+
const projectRoots = [];
|
|
1551
|
+
const rootEntries = [];
|
|
1552
|
+
let hasProjectFolders = false;
|
|
1553
|
+
|
|
1554
|
+
function sortFolders(nodes) {
|
|
1555
|
+
nodes.sort((left, right) => left.label.localeCompare(right.label));
|
|
1556
|
+
for (const node of nodes) {
|
|
1557
|
+
sortFolders(node.children);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
for (const entry of normalizedEntries) {
|
|
1562
|
+
const projectRelativePath = normalizeRelativePath(entry.projectRelativePath);
|
|
1563
|
+
if (!projectRelativePath) {
|
|
1564
|
+
rootEntries.push(entry);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
hasProjectFolders = true;
|
|
1569
|
+
let nodes = projectRoots;
|
|
1570
|
+
let folderPath = '';
|
|
1571
|
+
let parentNode = null;
|
|
1572
|
+
for (const segment of projectRelativePath.split('/').filter(Boolean)) {
|
|
1573
|
+
folderPath = folderPath ? path.posix.join(folderPath, segment) : segment;
|
|
1574
|
+
let folderNode = nodes.find((node) => node.relativePath === folderPath);
|
|
1575
|
+
if (!folderNode) {
|
|
1576
|
+
folderNode = {
|
|
1577
|
+
label: segment,
|
|
1578
|
+
relativePath: folderPath,
|
|
1579
|
+
children: [],
|
|
1580
|
+
entries: [],
|
|
1581
|
+
directEntries: [],
|
|
1582
|
+
};
|
|
1583
|
+
nodes.push(folderNode);
|
|
1584
|
+
}
|
|
1585
|
+
folderNode.entries.push(entry);
|
|
1586
|
+
parentNode = folderNode;
|
|
1587
|
+
nodes = folderNode.children;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
if (parentNode) {
|
|
1591
|
+
parentNode.directEntries.push(entry);
|
|
1592
|
+
} else {
|
|
1593
|
+
rootEntries.push(entry);
|
|
1594
|
+
}
|
|
571
1595
|
}
|
|
572
1596
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return `npm run agent:codex -- ${taskArg} ${agentArg}`;
|
|
1597
|
+
if (!hasProjectFolders) {
|
|
1598
|
+
return rootEntries.map((entry) => entry.item);
|
|
576
1599
|
}
|
|
577
1600
|
|
|
578
|
-
|
|
579
|
-
}
|
|
1601
|
+
sortFolders(projectRoots);
|
|
580
1602
|
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
|
|
1603
|
+
function materialize(nodes) {
|
|
1604
|
+
return nodes.map((node) => new FolderItem(
|
|
1605
|
+
node.label,
|
|
1606
|
+
node.relativePath,
|
|
1607
|
+
[
|
|
1608
|
+
...materialize(node.children),
|
|
1609
|
+
...node.directEntries.map((entry) => entry.item),
|
|
1610
|
+
],
|
|
1611
|
+
{
|
|
1612
|
+
description: buildProjectScopedDescription(node.entries),
|
|
1613
|
+
tooltip: [node.relativePath, buildProjectScopedDescription(node.entries)].filter(Boolean).join('\n'),
|
|
1614
|
+
},
|
|
1615
|
+
));
|
|
1616
|
+
}
|
|
584
1617
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1618
|
+
const items = materialize(projectRoots);
|
|
1619
|
+
if (rootEntries.length === 0) {
|
|
1620
|
+
return items;
|
|
1621
|
+
}
|
|
588
1622
|
|
|
589
|
-
|
|
590
|
-
|
|
1623
|
+
const rootLabel = typeof options.rootLabel === 'string' ? options.rootLabel.trim() : '';
|
|
1624
|
+
if (!rootLabel) {
|
|
1625
|
+
items.push(...rootEntries.map((entry) => entry.item));
|
|
1626
|
+
return items;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
items.push(new FolderItem(
|
|
1630
|
+
rootLabel,
|
|
1631
|
+
'',
|
|
1632
|
+
rootEntries.map((entry) => entry.item),
|
|
1633
|
+
{
|
|
1634
|
+
description: buildProjectScopedDescription(rootEntries),
|
|
1635
|
+
tooltip: rootLabel,
|
|
1636
|
+
},
|
|
1637
|
+
));
|
|
1638
|
+
return items;
|
|
591
1639
|
}
|
|
592
1640
|
|
|
593
1641
|
function showSessionMessage(message) {
|
|
@@ -622,6 +1670,82 @@ function runSessionTerminalCommand(session, actionLabel, iconId, commandText) {
|
|
|
622
1670
|
terminal.sendText(commandText, true);
|
|
623
1671
|
}
|
|
624
1672
|
|
|
1673
|
+
function sessionTerminalLabel(session) {
|
|
1674
|
+
return `GitGuardex Terminal: ${sessionDisplayLabel(session)}`;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function listWindowTerminals() {
|
|
1678
|
+
return Array.isArray(vscode.window.terminals) ? vscode.window.terminals : [];
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function focusTerminal(terminal) {
|
|
1682
|
+
terminal?.show?.(false);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function terminalProcessId(terminal) {
|
|
1686
|
+
if (!terminal?.processId) {
|
|
1687
|
+
return null;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
const pid = await terminal.processId;
|
|
1692
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
1693
|
+
} catch (_error) {
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function findFallbackSessionTerminal(session) {
|
|
1699
|
+
const label = sessionTerminalLabel(session);
|
|
1700
|
+
return listWindowTerminals().find((terminal) => terminal?.name === label) || null;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
async function findSessionTerminal(session) {
|
|
1704
|
+
const pid = Number(session?.pid);
|
|
1705
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
for (const terminal of listWindowTerminals()) {
|
|
1710
|
+
if (await terminalProcessId(terminal) === pid) {
|
|
1711
|
+
return terminal;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function openFallbackSessionTerminal(session, worktreePath) {
|
|
1719
|
+
const existingTerminal = findFallbackSessionTerminal(session);
|
|
1720
|
+
if (existingTerminal) {
|
|
1721
|
+
focusTerminal(existingTerminal);
|
|
1722
|
+
return existingTerminal;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const terminal = vscode.window.createTerminal({
|
|
1726
|
+
name: sessionTerminalLabel(session),
|
|
1727
|
+
cwd: worktreePath,
|
|
1728
|
+
iconPath: new vscode.ThemeIcon('terminal'),
|
|
1729
|
+
});
|
|
1730
|
+
focusTerminal(terminal);
|
|
1731
|
+
return terminal;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async function showSessionTerminal(session) {
|
|
1735
|
+
const worktreePath = ensureSessionWorktree(session, 'show terminal');
|
|
1736
|
+
if (!worktreePath) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
const terminal = await findSessionTerminal(session);
|
|
1741
|
+
if (terminal) {
|
|
1742
|
+
focusTerminal(terminal);
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
openFallbackSessionTerminal(session, worktreePath);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
625
1749
|
function finishSession(session) {
|
|
626
1750
|
if (!session?.branch) {
|
|
627
1751
|
showSessionMessage('Cannot finish session: missing branch name.');
|
|
@@ -639,6 +1763,13 @@ function syncSession(session) {
|
|
|
639
1763
|
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
|
|
640
1764
|
}
|
|
641
1765
|
|
|
1766
|
+
async function restartActiveAgents(extensionId) {
|
|
1767
|
+
if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
642
1773
|
function execFileAsync(command, args, options = {}) {
|
|
643
1774
|
return new Promise((resolve, reject) => {
|
|
644
1775
|
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
|
|
@@ -653,6 +1784,14 @@ function execFileAsync(command, args, options = {}) {
|
|
|
653
1784
|
});
|
|
654
1785
|
}
|
|
655
1786
|
|
|
1787
|
+
function buildStopSessionCommandText(session, pid) {
|
|
1788
|
+
const parts = ['gx', 'agents', 'stop', '--pid', String(pid)];
|
|
1789
|
+
if (session?.repoRoot) {
|
|
1790
|
+
parts.push('--target', session.repoRoot);
|
|
1791
|
+
}
|
|
1792
|
+
return parts.map(shellQuote).join(' ');
|
|
1793
|
+
}
|
|
1794
|
+
|
|
656
1795
|
async function stopSession(session, refresh) {
|
|
657
1796
|
const pid = Number(session?.pid);
|
|
658
1797
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
@@ -664,15 +1803,29 @@ async function stopSession(session, refresh) {
|
|
|
664
1803
|
return;
|
|
665
1804
|
}
|
|
666
1805
|
|
|
1806
|
+
const sessionTerminal = await findSessionTerminal(session);
|
|
1807
|
+
const stopCommandText = buildStopSessionCommandText(session, pid);
|
|
667
1808
|
const confirmed = await vscode.window.showWarningMessage(
|
|
668
1809
|
`Stop ${sessionDisplayLabel(session)}?`,
|
|
669
|
-
{
|
|
1810
|
+
{
|
|
1811
|
+
modal: true,
|
|
1812
|
+
detail: sessionTerminal
|
|
1813
|
+
? 'Send Ctrl+C to the live session terminal.'
|
|
1814
|
+
: `No live session terminal found. Run ${stopCommandText}.`,
|
|
1815
|
+
},
|
|
670
1816
|
'Stop',
|
|
671
1817
|
);
|
|
672
1818
|
if (confirmed !== 'Stop') {
|
|
673
1819
|
return;
|
|
674
1820
|
}
|
|
675
1821
|
|
|
1822
|
+
if (sessionTerminal) {
|
|
1823
|
+
focusTerminal(sessionTerminal);
|
|
1824
|
+
sessionTerminal.sendText('\u0003', false);
|
|
1825
|
+
refresh();
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
676
1829
|
try {
|
|
677
1830
|
const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
|
|
678
1831
|
const args = ['agents', 'stop', '--pid', String(pid)];
|
|
@@ -692,69 +1845,87 @@ async function stopSession(session, refresh) {
|
|
|
692
1845
|
}
|
|
693
1846
|
}
|
|
694
1847
|
|
|
695
|
-
function
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (directPaths.length > 0) {
|
|
700
|
-
return [...new Set(directPaths)];
|
|
1848
|
+
function readGitDirPath(targetPath) {
|
|
1849
|
+
const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : '';
|
|
1850
|
+
if (!normalizedTargetPath) {
|
|
1851
|
+
return '';
|
|
701
1852
|
}
|
|
702
|
-
|
|
703
|
-
|
|
1853
|
+
|
|
1854
|
+
const gitPath = path.join(path.resolve(normalizedTargetPath), '.git');
|
|
1855
|
+
try {
|
|
1856
|
+
if (fs.statSync(gitPath).isDirectory()) {
|
|
1857
|
+
return gitPath;
|
|
1858
|
+
}
|
|
1859
|
+
} catch (_error) {
|
|
1860
|
+
return '';
|
|
704
1861
|
}
|
|
705
1862
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1863
|
+
try {
|
|
1864
|
+
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
1865
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
1866
|
+
if (match?.[1]) {
|
|
1867
|
+
return path.resolve(path.dirname(gitPath), match[1].trim());
|
|
1868
|
+
}
|
|
1869
|
+
} catch (_error) {
|
|
1870
|
+
return '';
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
return '';
|
|
711
1874
|
}
|
|
712
1875
|
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
1876
|
+
function resolveRepoRootFromGitDir(targetPath) {
|
|
1877
|
+
const gitDir = readGitDirPath(targetPath);
|
|
1878
|
+
if (!gitDir) {
|
|
716
1879
|
return '';
|
|
717
1880
|
}
|
|
718
|
-
|
|
719
|
-
|
|
1881
|
+
|
|
1882
|
+
let commonDir = gitDir;
|
|
1883
|
+
try {
|
|
1884
|
+
const commonDirPath = path.join(gitDir, 'commondir');
|
|
1885
|
+
if (fs.existsSync(commonDirPath)) {
|
|
1886
|
+
const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim();
|
|
1887
|
+
if (rawCommonDir) {
|
|
1888
|
+
commonDir = path.resolve(gitDir, rawCommonDir);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
} catch (_error) {
|
|
1892
|
+
// Fall back to the direct git dir when commondir is unreadable.
|
|
720
1893
|
}
|
|
721
1894
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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 || '';
|
|
1895
|
+
return path.basename(commonDir) === '.git'
|
|
1896
|
+
? path.resolve(path.dirname(commonDir))
|
|
1897
|
+
: '';
|
|
732
1898
|
}
|
|
733
1899
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1900
|
+
function readGitTopLevel(targetPath) {
|
|
1901
|
+
try {
|
|
1902
|
+
return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], {
|
|
1903
|
+
encoding: 'utf8',
|
|
1904
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1905
|
+
}).trim();
|
|
1906
|
+
} catch (_error) {
|
|
1907
|
+
return '';
|
|
738
1908
|
}
|
|
1909
|
+
}
|
|
739
1910
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
return;
|
|
1911
|
+
function resolveWorkspaceFolderRepoRoot(workspacePath) {
|
|
1912
|
+
const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : '';
|
|
1913
|
+
if (!normalizedWorkspacePath) {
|
|
1914
|
+
return '';
|
|
744
1915
|
}
|
|
745
1916
|
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
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)}`);
|
|
1917
|
+
const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath);
|
|
1918
|
+
const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath);
|
|
1919
|
+
if (directRepoRoot) {
|
|
1920
|
+
return directRepoRoot;
|
|
757
1921
|
}
|
|
1922
|
+
|
|
1923
|
+
const gitTopLevel = readGitTopLevel(absoluteWorkspacePath);
|
|
1924
|
+
if (!gitTopLevel) {
|
|
1925
|
+
return absoluteWorkspacePath;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel);
|
|
758
1929
|
}
|
|
759
1930
|
|
|
760
1931
|
function repoRootFromSessionFile(filePath) {
|
|
@@ -765,6 +1936,10 @@ function repoRootFromWorktreeLockFile(filePath) {
|
|
|
765
1936
|
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
766
1937
|
}
|
|
767
1938
|
|
|
1939
|
+
function repoRootFromManagedWorktreeGitFile(filePath) {
|
|
1940
|
+
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
1941
|
+
}
|
|
1942
|
+
|
|
768
1943
|
function repoRootFromLockFile(filePath) {
|
|
769
1944
|
return path.resolve(path.dirname(filePath), '..', '..');
|
|
770
1945
|
}
|
|
@@ -952,22 +2127,33 @@ async function maybeAutoUpdateActiveAgentsExtension(context) {
|
|
|
952
2127
|
|
|
953
2128
|
function decorateSession(session, lockRegistry) {
|
|
954
2129
|
const touchedChanges = buildSessionTouchedChanges(session, lockRegistry);
|
|
955
|
-
|
|
2130
|
+
const decorated = {
|
|
956
2131
|
...session,
|
|
957
2132
|
lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
|
|
958
2133
|
touchedChanges,
|
|
959
2134
|
conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length,
|
|
960
2135
|
};
|
|
2136
|
+
decorated.lastActiveAt = sessionLastActiveAt(decorated);
|
|
2137
|
+
decorated.lastActiveLabel = sessionLastActiveLabel(decorated);
|
|
2138
|
+
decorated.freshnessLabel = sessionFreshnessLabel(decorated);
|
|
2139
|
+
decorated.topChangedFiles = buildSessionTopFiles(decorated);
|
|
2140
|
+
decorated.topChangedFilesLabel = summarizeCompactPaths(decorated.topChangedFiles);
|
|
2141
|
+
decorated.recentChangeSummary = buildSessionRecentChangeSummary(decorated);
|
|
2142
|
+
decorated.riskBadges = sessionRiskBadges(decorated);
|
|
2143
|
+
return decorated;
|
|
961
2144
|
}
|
|
962
2145
|
|
|
963
2146
|
function decorateChange(change, lockRegistry, owningBranch) {
|
|
964
2147
|
const lockEntry = lockRegistry.entriesByPath.get(normalizeRelativePath(change.relativePath));
|
|
965
2148
|
const lockOwnerBranch = lockEntry?.branch || '';
|
|
966
|
-
|
|
2149
|
+
const decorated = {
|
|
967
2150
|
...change,
|
|
968
2151
|
lockOwnerBranch,
|
|
969
2152
|
hasForeignLock: Boolean(lockOwnerBranch) && (!owningBranch || lockOwnerBranch !== owningBranch),
|
|
2153
|
+
protectedBranch: isProtectedBranchName(owningBranch),
|
|
970
2154
|
};
|
|
2155
|
+
decorated.riskBadges = changeRiskBadges(decorated);
|
|
2156
|
+
return decorated;
|
|
971
2157
|
}
|
|
972
2158
|
|
|
973
2159
|
function buildSessionTouchedChanges(session, lockRegistry) {
|
|
@@ -975,7 +2161,6 @@ function buildSessionTouchedChanges(session, lockRegistry) {
|
|
|
975
2161
|
? session.worktreeChangedPaths
|
|
976
2162
|
: [];
|
|
977
2163
|
return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
978
|
-
.sort((left, right) => left.localeCompare(right))
|
|
979
2164
|
.map((relativePath) => {
|
|
980
2165
|
const lockEntry = lockRegistry.entriesByPath.get(relativePath);
|
|
981
2166
|
const lockOwnerBranch = lockEntry?.branch || '';
|
|
@@ -1018,7 +2203,7 @@ function localizeChangeForSession(session, change) {
|
|
|
1018
2203
|
}
|
|
1019
2204
|
|
|
1020
2205
|
async function findRepoSessionEntries() {
|
|
1021
|
-
const [sessionFiles, worktreeLockFiles] = await Promise.all([
|
|
2206
|
+
const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([
|
|
1022
2207
|
vscode.workspace.findFiles(
|
|
1023
2208
|
ACTIVE_SESSION_FILES_GLOB,
|
|
1024
2209
|
SESSION_SCAN_EXCLUDE_GLOB,
|
|
@@ -1029,22 +2214,49 @@ async function findRepoSessionEntries() {
|
|
|
1029
2214
|
WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
|
|
1030
2215
|
SESSION_SCAN_LIMIT,
|
|
1031
2216
|
),
|
|
2217
|
+
vscode.workspace.findFiles(
|
|
2218
|
+
MANAGED_WORKTREE_GIT_FILES_GLOB,
|
|
2219
|
+
MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB,
|
|
2220
|
+
SESSION_SCAN_LIMIT,
|
|
2221
|
+
),
|
|
1032
2222
|
]);
|
|
1033
2223
|
|
|
1034
2224
|
const repoRoots = new Set();
|
|
2225
|
+
const addRepoRootCandidate = (repoRoot) => {
|
|
2226
|
+
if (typeof repoRoot !== 'string' || !repoRoot.trim()) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const normalizedRepoRoot = path.resolve(repoRoot);
|
|
2231
|
+
const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || [])
|
|
2232
|
+
.map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : ''))
|
|
2233
|
+
.filter(Boolean)
|
|
2234
|
+
.some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => (
|
|
2235
|
+
isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot)
|
|
2236
|
+
)));
|
|
2237
|
+
if (!isInsideWorkspaceManagedWorktree) {
|
|
2238
|
+
repoRoots.add(normalizedRepoRoot);
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
|
|
1035
2242
|
for (const uri of sessionFiles) {
|
|
1036
|
-
|
|
2243
|
+
addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath));
|
|
1037
2244
|
}
|
|
1038
2245
|
for (const uri of worktreeLockFiles) {
|
|
1039
2246
|
if (path.basename(uri.fsPath) !== 'AGENT.lock') {
|
|
1040
2247
|
continue;
|
|
1041
2248
|
}
|
|
1042
|
-
|
|
2249
|
+
addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath));
|
|
1043
2250
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
2251
|
+
for (const uri of managedWorktreeGitFiles) {
|
|
2252
|
+
if (path.basename(uri.fsPath) !== '.git') {
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
2255
|
+
addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath));
|
|
2256
|
+
}
|
|
2257
|
+
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
2258
|
+
if (workspaceFolder?.uri?.fsPath) {
|
|
2259
|
+
addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath));
|
|
1048
2260
|
}
|
|
1049
2261
|
}
|
|
1050
2262
|
|
|
@@ -1164,10 +2376,6 @@ function buildChangeTreeNodes(changes) {
|
|
|
1164
2376
|
return materialize(root);
|
|
1165
2377
|
}
|
|
1166
2378
|
|
|
1167
|
-
function countWorkingSessions(sessions) {
|
|
1168
|
-
return sessions.filter((session) => session.activityKind === 'working').length;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
2379
|
function countChangedPaths(repoRoot, sessions, changes) {
|
|
1172
2380
|
const changedKeys = new Set();
|
|
1173
2381
|
|
|
@@ -1193,6 +2401,20 @@ function countChangedPaths(repoRoot, sessions, changes) {
|
|
|
1193
2401
|
return changedKeys.size;
|
|
1194
2402
|
}
|
|
1195
2403
|
|
|
2404
|
+
function buildRepoOverview(sessions, unassignedChanges, lockEntries) {
|
|
2405
|
+
return {
|
|
2406
|
+
sessionCount: sessions.length,
|
|
2407
|
+
workingCount: countWorkingSessions(sessions),
|
|
2408
|
+
idleCount: countIdleSessions(sessions),
|
|
2409
|
+
unassignedChangeCount: (unassignedChanges || []).length,
|
|
2410
|
+
lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0,
|
|
2411
|
+
conflictCount: sessions.reduce(
|
|
2412
|
+
(total, session) => total + (session.conflictCount || 0),
|
|
2413
|
+
0,
|
|
2414
|
+
) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length,
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
|
|
1196
2418
|
function groupSessionsByWorktree(sessions) {
|
|
1197
2419
|
const sessionsByWorktree = new Map();
|
|
1198
2420
|
|
|
@@ -1223,7 +2445,7 @@ function groupSessionsByWorktree(sessions) {
|
|
|
1223
2445
|
});
|
|
1224
2446
|
}
|
|
1225
2447
|
|
|
1226
|
-
function
|
|
2448
|
+
function partitionChangesByOwnership(sessions, changes) {
|
|
1227
2449
|
const changesBySession = new Map();
|
|
1228
2450
|
const sessionByChangedPath = new Map();
|
|
1229
2451
|
const repoRootChanges = [];
|
|
@@ -1255,22 +2477,40 @@ function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
|
1255
2477
|
changesBySession.get(session.branch).push(localizedChange);
|
|
1256
2478
|
}
|
|
1257
2479
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
(
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
2480
|
+
return {
|
|
2481
|
+
changesBySession,
|
|
2482
|
+
repoRootChanges,
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
2487
|
+
const { changesBySession, repoRootChanges } = partitionChangesByOwnership(sessions, changes);
|
|
2488
|
+
|
|
2489
|
+
const items = buildProjectScopedItems(
|
|
2490
|
+
groupSessionsByWorktree(
|
|
2491
|
+
sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0),
|
|
2492
|
+
).map(({ worktreePath, sessions: worktreeSessions }) => {
|
|
2493
|
+
const sessionItems = worktreeSessions.map((session) => (
|
|
2494
|
+
new SessionItem(
|
|
2495
|
+
session,
|
|
2496
|
+
buildChangeTreeNodes(changesBySession.get(session.branch) || []),
|
|
2497
|
+
{
|
|
2498
|
+
label: sessionTreeLabel(session),
|
|
2499
|
+
variant: 'raw',
|
|
2500
|
+
},
|
|
2501
|
+
)
|
|
2502
|
+
));
|
|
2503
|
+
const changedCount = worktreeSessions.reduce(
|
|
2504
|
+
(total, session) => total + ((changesBySession.get(session.branch) || []).length),
|
|
2505
|
+
0,
|
|
2506
|
+
);
|
|
2507
|
+
return {
|
|
2508
|
+
projectRelativePath: worktreeProjectRelativePath(worktreeSessions),
|
|
2509
|
+
sessions: worktreeSessions,
|
|
2510
|
+
item: new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount }),
|
|
2511
|
+
};
|
|
2512
|
+
}),
|
|
2513
|
+
);
|
|
1274
2514
|
|
|
1275
2515
|
if (repoRootChanges.length > 0) {
|
|
1276
2516
|
items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
|
|
@@ -1395,23 +2635,127 @@ function commitWorktree(worktreePath, message) {
|
|
|
1395
2635
|
runGitCommand(worktreePath, ['commit', '-m', message]);
|
|
1396
2636
|
}
|
|
1397
2637
|
|
|
1398
|
-
function
|
|
2638
|
+
function buildSessionDetailItems(session) {
|
|
2639
|
+
const provider = resolveSessionProvider(session);
|
|
2640
|
+
const snapshot = sessionSnapshotDisplayName(session);
|
|
2641
|
+
const projectRelativePath = resolveSessionProjectRelativePath(session);
|
|
2642
|
+
const badgeSummary = uniqueStringList([
|
|
2643
|
+
...(session.riskBadges || []),
|
|
2644
|
+
session.deltaLabel || '',
|
|
2645
|
+
].filter(Boolean)).join(', ');
|
|
2646
|
+
const sessionHealthSummary = buildSessionHealthSummary(session);
|
|
2647
|
+
const items = [
|
|
2648
|
+
new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', {
|
|
2649
|
+
iconId: 'history',
|
|
2650
|
+
}),
|
|
2651
|
+
new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', {
|
|
2652
|
+
iconId: 'list-flat',
|
|
2653
|
+
}),
|
|
2654
|
+
];
|
|
2655
|
+
if (badgeSummary) {
|
|
2656
|
+
items.push(new DetailItem('Signals', badgeSummary, {
|
|
2657
|
+
iconId: 'warning',
|
|
2658
|
+
}));
|
|
2659
|
+
}
|
|
2660
|
+
if (sessionHealthSummary) {
|
|
2661
|
+
items.push(new DetailItem('Session health', sessionHealthSummary, {
|
|
2662
|
+
iconId: 'pulse',
|
|
2663
|
+
tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary,
|
|
2664
|
+
}));
|
|
2665
|
+
}
|
|
2666
|
+
if (provider?.label) {
|
|
2667
|
+
items.push(new DetailItem('Provider', provider.label, {
|
|
2668
|
+
iconId: 'sparkle',
|
|
2669
|
+
}));
|
|
2670
|
+
}
|
|
2671
|
+
if (snapshot) {
|
|
2672
|
+
items.push(new DetailItem('Snapshot', snapshot, {
|
|
2673
|
+
iconId: 'account',
|
|
2674
|
+
}));
|
|
2675
|
+
}
|
|
2676
|
+
if (projectRelativePath) {
|
|
2677
|
+
items.push(new DetailItem('Project', projectRelativePath, {
|
|
2678
|
+
iconId: 'folder',
|
|
2679
|
+
tooltip: projectRelativePath,
|
|
2680
|
+
}));
|
|
2681
|
+
}
|
|
2682
|
+
items.push(new DetailItem('Branch', session.branch, {
|
|
2683
|
+
iconId: 'git-branch',
|
|
2684
|
+
}));
|
|
2685
|
+
items.push(new DetailItem('Worktree', session.worktreePath, {
|
|
2686
|
+
iconId: 'folder',
|
|
2687
|
+
tooltip: session.worktreePath,
|
|
2688
|
+
}));
|
|
2689
|
+
return items;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
function buildWorkingNowNodes(sessions) {
|
|
2693
|
+
const sessionEntries = sortSessionsForWorkingNow(
|
|
2694
|
+
sessions.filter((session) => (
|
|
2695
|
+
session.activityKind === 'working' || session.activityKind === 'blocked'
|
|
2696
|
+
)),
|
|
2697
|
+
).map((session) => ({
|
|
2698
|
+
projectRelativePath: resolveSessionProjectRelativePath(session),
|
|
2699
|
+
sessions: [session],
|
|
2700
|
+
item: new SessionItem(session, buildSessionDetailItems(session)),
|
|
2701
|
+
}));
|
|
2702
|
+
return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' });
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
function buildIdleThinkingNodes(sessions) {
|
|
2706
|
+
const sessionEntries = sortSessionsForIdleThinking(
|
|
2707
|
+
sessions.filter((session) => !(
|
|
2708
|
+
session.activityKind === 'working' || session.activityKind === 'blocked'
|
|
2709
|
+
)),
|
|
2710
|
+
).map((session) => ({
|
|
2711
|
+
projectRelativePath: resolveSessionProjectRelativePath(session),
|
|
2712
|
+
sessions: [session],
|
|
2713
|
+
item: new SessionItem(session, buildSessionDetailItems(session)),
|
|
2714
|
+
}));
|
|
2715
|
+
return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' });
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function buildUnassignedChangeNodes(changes) {
|
|
2719
|
+
return sortUnassignedChanges(changes).map((change) => new ChangeItem(change, {
|
|
2720
|
+
label: compactRelativePath(change.relativePath),
|
|
2721
|
+
description: buildUnassignedChangeDescription(change),
|
|
2722
|
+
iconId: changeRiskBadges(change).length > 0 ? 'warning' : undefined,
|
|
2723
|
+
}));
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function buildRawActiveAgentGroupNodes(sessions) {
|
|
1399
2727
|
const groups = [];
|
|
1400
2728
|
for (const group of SESSION_ACTIVITY_GROUPS) {
|
|
1401
2729
|
const groupSessions = sessions.filter((session) => session.activityKind === group.kind);
|
|
1402
|
-
const worktreeItems =
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
worktreeSessions,
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
2730
|
+
const worktreeItems = buildProjectScopedItems(
|
|
2731
|
+
groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => ({
|
|
2732
|
+
projectRelativePath: worktreeProjectRelativePath(worktreeSessions),
|
|
2733
|
+
sessions: worktreeSessions,
|
|
2734
|
+
item: new WorktreeItem(
|
|
2735
|
+
worktreePath,
|
|
2736
|
+
worktreeSessions,
|
|
2737
|
+
worktreeSessions.map((session) => new SessionItem(
|
|
2738
|
+
session,
|
|
2739
|
+
buildChangeTreeNodes(session.touchedChanges || []),
|
|
2740
|
+
{
|
|
2741
|
+
label: sessionTreeLabel(session),
|
|
2742
|
+
variant: 'raw',
|
|
2743
|
+
},
|
|
2744
|
+
)),
|
|
2745
|
+
{
|
|
2746
|
+
description: buildWorktreeBranchDescription(worktreeSessions),
|
|
2747
|
+
iconId: 'git-branch',
|
|
2748
|
+
resourceSession: worktreeSessions[0],
|
|
2749
|
+
useSessionDecoration: true,
|
|
2750
|
+
},
|
|
2751
|
+
),
|
|
2752
|
+
})),
|
|
2753
|
+
{ rootLabel: 'Repo root' },
|
|
2754
|
+
);
|
|
1413
2755
|
if (worktreeItems.length > 0) {
|
|
1414
|
-
groups.push(new SectionItem(group.label, worktreeItems
|
|
2756
|
+
groups.push(new SectionItem(group.label, worktreeItems, {
|
|
2757
|
+
iconId: resolveSessionActivityIconId(group.kind),
|
|
2758
|
+
}));
|
|
1415
2759
|
}
|
|
1416
2760
|
}
|
|
1417
2761
|
|
|
@@ -1431,9 +2775,13 @@ class ActiveAgentsProvider {
|
|
|
1431
2775
|
this.viewSummary = {
|
|
1432
2776
|
sessionCount: 0,
|
|
1433
2777
|
workingCount: 0,
|
|
2778
|
+
idleCount: 0,
|
|
2779
|
+
unassignedChangeCount: 0,
|
|
2780
|
+
lockedFileCount: 0,
|
|
1434
2781
|
deadCount: 0,
|
|
1435
2782
|
conflictCount: 0,
|
|
1436
2783
|
};
|
|
2784
|
+
this.previousSnapshot = null;
|
|
1437
2785
|
}
|
|
1438
2786
|
|
|
1439
2787
|
getTreeItem(element) {
|
|
@@ -1442,7 +2790,15 @@ class ActiveAgentsProvider {
|
|
|
1442
2790
|
|
|
1443
2791
|
attachTreeView(treeView) {
|
|
1444
2792
|
this.treeView = treeView;
|
|
1445
|
-
this.updateViewState(
|
|
2793
|
+
this.updateViewState({
|
|
2794
|
+
sessionCount: 0,
|
|
2795
|
+
workingCount: 0,
|
|
2796
|
+
idleCount: 0,
|
|
2797
|
+
unassignedChangeCount: 0,
|
|
2798
|
+
lockedFileCount: 0,
|
|
2799
|
+
deadCount: 0,
|
|
2800
|
+
conflictCount: 0,
|
|
2801
|
+
});
|
|
1446
2802
|
treeView.onDidChangeSelection?.((event) => {
|
|
1447
2803
|
const sessionItem = event.selection.find((item) => item instanceof SessionItem);
|
|
1448
2804
|
this.setSelectedSession(sessionItem?.session || null);
|
|
@@ -1479,60 +2835,100 @@ class ActiveAgentsProvider {
|
|
|
1479
2835
|
this.setSelectedSession(nextSession || null);
|
|
1480
2836
|
}
|
|
1481
2837
|
|
|
1482
|
-
updateViewState(
|
|
2838
|
+
updateViewState(summary) {
|
|
1483
2839
|
if (!this.treeView) {
|
|
1484
2840
|
return;
|
|
1485
2841
|
}
|
|
1486
2842
|
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
workingCount,
|
|
1491
|
-
deadCount,
|
|
1492
|
-
conflictCount,
|
|
1493
|
-
};
|
|
2843
|
+
const sessionCount = summary?.sessionCount || 0;
|
|
2844
|
+
const conflictCount = summary?.conflictCount || 0;
|
|
2845
|
+
this.viewSummary = { ...summary };
|
|
1494
2846
|
void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0);
|
|
1495
2847
|
void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0);
|
|
1496
|
-
const badgeTooltipParts = [];
|
|
1497
|
-
if (activeCount > 0) {
|
|
1498
|
-
badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
|
|
1499
|
-
}
|
|
1500
|
-
if (deadCount > 0) {
|
|
1501
|
-
badgeTooltipParts.push(`${deadCount} dead`);
|
|
1502
|
-
}
|
|
1503
|
-
if (workingCount > 0) {
|
|
1504
|
-
badgeTooltipParts.push(`${workingCount} working now`);
|
|
1505
|
-
}
|
|
1506
|
-
if (conflictCount > 0) {
|
|
1507
|
-
badgeTooltipParts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
|
|
1508
|
-
}
|
|
1509
2848
|
|
|
1510
2849
|
this.treeView.badge = sessionCount > 0
|
|
1511
2850
|
? {
|
|
1512
2851
|
value: sessionCount,
|
|
1513
|
-
tooltip:
|
|
2852
|
+
tooltip: buildOverviewDescription(summary),
|
|
1514
2853
|
}
|
|
1515
2854
|
: undefined;
|
|
1516
2855
|
this.treeView.message = undefined;
|
|
1517
2856
|
}
|
|
1518
2857
|
|
|
2858
|
+
annotateRepoEntries(repoEntries) {
|
|
2859
|
+
const hasPreviousSnapshot = Boolean(this.previousSnapshot);
|
|
2860
|
+
const nextSnapshot = {
|
|
2861
|
+
sessions: new Map(),
|
|
2862
|
+
changes: new Map(),
|
|
2863
|
+
};
|
|
2864
|
+
|
|
2865
|
+
const annotatedEntries = repoEntries.map((entry) => {
|
|
2866
|
+
const sessions = entry.sessions.map((session) => {
|
|
2867
|
+
const snapshotKey = sessionSnapshotKey(session);
|
|
2868
|
+
nextSnapshot.sessions.set(snapshotKey, buildSessionSnapshot(session));
|
|
2869
|
+
const deltaLabel = hasPreviousSnapshot
|
|
2870
|
+
? deriveSessionDelta(this.previousSnapshot.sessions.get(snapshotKey), session)
|
|
2871
|
+
: '';
|
|
2872
|
+
return {
|
|
2873
|
+
...session,
|
|
2874
|
+
deltaLabel,
|
|
2875
|
+
riskBadges: uniqueStringList([
|
|
2876
|
+
...(session.riskBadges || []),
|
|
2877
|
+
deltaLabel,
|
|
2878
|
+
].filter(Boolean)),
|
|
2879
|
+
};
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
const changes = entry.changes.map((change) => {
|
|
2883
|
+
const snapshotKey = changeSnapshotKey(entry.repoRoot, change);
|
|
2884
|
+
nextSnapshot.changes.set(snapshotKey, buildChangeSnapshot(change));
|
|
2885
|
+
const deltaLabel = hasPreviousSnapshot
|
|
2886
|
+
? deriveChangeDelta(this.previousSnapshot.changes.get(snapshotKey), change)
|
|
2887
|
+
: '';
|
|
2888
|
+
return {
|
|
2889
|
+
...change,
|
|
2890
|
+
deltaLabel,
|
|
2891
|
+
riskBadges: changeRiskBadges({
|
|
2892
|
+
...change,
|
|
2893
|
+
deltaLabel,
|
|
2894
|
+
}),
|
|
2895
|
+
};
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
const { repoRootChanges } = partitionChangesByOwnership(sessions, changes);
|
|
2899
|
+
const unassignedChanges = sortUnassignedChanges(repoRootChanges);
|
|
2900
|
+
return {
|
|
2901
|
+
...entry,
|
|
2902
|
+
sessions,
|
|
2903
|
+
changes,
|
|
2904
|
+
unassignedChanges,
|
|
2905
|
+
overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries),
|
|
2906
|
+
};
|
|
2907
|
+
});
|
|
2908
|
+
|
|
2909
|
+
this.previousSnapshot = nextSnapshot;
|
|
2910
|
+
return annotatedEntries;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
1519
2913
|
async syncRepoEntries() {
|
|
1520
|
-
const repoEntries = await this.loadRepoEntries();
|
|
1521
|
-
const
|
|
1522
|
-
|
|
1523
|
-
(total, entry) => total +
|
|
1524
|
-
0,
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2914
|
+
const repoEntries = this.annotateRepoEntries(await this.loadRepoEntries());
|
|
2915
|
+
const summary = {
|
|
2916
|
+
sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0),
|
|
2917
|
+
workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0),
|
|
2918
|
+
idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0),
|
|
2919
|
+
unassignedChangeCount: repoEntries.reduce(
|
|
2920
|
+
(total, entry) => total + entry.overview.unassignedChangeCount,
|
|
2921
|
+
0,
|
|
2922
|
+
),
|
|
2923
|
+
lockedFileCount: repoEntries.reduce((total, entry) => total + entry.overview.lockedFileCount, 0),
|
|
2924
|
+
deadCount: repoEntries.reduce(
|
|
2925
|
+
(total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
|
|
2926
|
+
0,
|
|
2927
|
+
),
|
|
2928
|
+
conflictCount: repoEntries.reduce((total, entry) => total + entry.overview.conflictCount, 0),
|
|
2929
|
+
};
|
|
1534
2930
|
|
|
1535
|
-
this.updateViewState(
|
|
2931
|
+
this.updateViewState(summary);
|
|
1536
2932
|
this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
|
|
1537
2933
|
this.decorationProvider?.updateLockEntries(repoEntries);
|
|
1538
2934
|
return repoEntries;
|
|
@@ -1561,13 +2957,62 @@ class ActiveAgentsProvider {
|
|
|
1561
2957
|
async getChildren(element) {
|
|
1562
2958
|
if (element instanceof RepoItem) {
|
|
1563
2959
|
const sectionItems = [
|
|
1564
|
-
new SectionItem('
|
|
1565
|
-
|
|
2960
|
+
new SectionItem('Overview', [
|
|
2961
|
+
new DetailItem('Summary', buildOverviewDescription(element.overview), {
|
|
2962
|
+
iconId: 'graph',
|
|
2963
|
+
tooltip: buildRepoTooltip(element.repoRoot, element.overview),
|
|
2964
|
+
}),
|
|
2965
|
+
], {
|
|
2966
|
+
description: '1',
|
|
1566
2967
|
}),
|
|
1567
2968
|
];
|
|
1568
|
-
|
|
1569
|
-
|
|
2969
|
+
|
|
2970
|
+
const workingNowItems = buildWorkingNowNodes(element.sessions);
|
|
2971
|
+
if (workingNowItems.length > 0) {
|
|
2972
|
+
sectionItems.push(new SectionItem('Working now', workingNowItems, {
|
|
2973
|
+
description: String(workingNowItems.length),
|
|
2974
|
+
iconId: 'loading~spin',
|
|
2975
|
+
}));
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
const idleThinkingItems = buildIdleThinkingNodes(element.sessions);
|
|
2979
|
+
if (idleThinkingItems.length > 0) {
|
|
2980
|
+
sectionItems.push(new SectionItem('Idle / thinking', idleThinkingItems, {
|
|
2981
|
+
description: String(idleThinkingItems.length),
|
|
2982
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
2983
|
+
iconId: 'comment-discussion',
|
|
2984
|
+
}));
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
if (element.unassignedChanges.length > 0) {
|
|
2988
|
+
sectionItems.push(new SectionItem('Unassigned changes', buildUnassignedChangeNodes(element.unassignedChanges), {
|
|
2989
|
+
description: String(element.unassignedChanges.length),
|
|
2990
|
+
iconId: 'warning',
|
|
2991
|
+
}));
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const advancedItems = [];
|
|
2995
|
+
const rawActiveAgents = buildRawActiveAgentGroupNodes(element.sessions);
|
|
2996
|
+
if (rawActiveAgents.length > 0) {
|
|
2997
|
+
advancedItems.push(new SectionItem('Active agent tree', rawActiveAgents, {
|
|
2998
|
+
description: String(element.sessions.length),
|
|
2999
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3000
|
+
iconId: 'git-branch',
|
|
3001
|
+
}));
|
|
3002
|
+
}
|
|
3003
|
+
const rawChangeTree = buildGroupedChangeTreeNodes(element.sessions, element.changes);
|
|
3004
|
+
if (rawChangeTree.length > 0) {
|
|
3005
|
+
advancedItems.push(new SectionItem('Raw path tree', rawChangeTree, {
|
|
1570
3006
|
description: String(element.changes.length),
|
|
3007
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3008
|
+
iconId: 'list-tree',
|
|
3009
|
+
}));
|
|
3010
|
+
}
|
|
3011
|
+
if (advancedItems.length > 0) {
|
|
3012
|
+
sectionItems.push(new SectionItem('Advanced details', advancedItems, {
|
|
3013
|
+
description: String(advancedItems.length),
|
|
3014
|
+
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3015
|
+
iconId: 'list-tree',
|
|
1571
3016
|
}));
|
|
1572
3017
|
}
|
|
1573
3018
|
return sectionItems;
|
|
@@ -1584,7 +3029,12 @@ class ActiveAgentsProvider {
|
|
|
1584
3029
|
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
|
|
1585
3030
|
}
|
|
1586
3031
|
|
|
1587
|
-
return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes
|
|
3032
|
+
return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, {
|
|
3033
|
+
label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions),
|
|
3034
|
+
overview: entry.overview,
|
|
3035
|
+
unassignedChanges: entry.unassignedChanges,
|
|
3036
|
+
lockEntries: entry.lockEntries,
|
|
3037
|
+
}));
|
|
1588
3038
|
}
|
|
1589
3039
|
|
|
1590
3040
|
async loadRepoEntries() {
|
|
@@ -1777,10 +3227,15 @@ function activate(context) {
|
|
|
1777
3227
|
activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
|
|
1778
3228
|
provider.attachTreeView(treeView);
|
|
1779
3229
|
const scheduleRefresh = () => refreshController.scheduleRefresh();
|
|
3230
|
+
const handleWorkspaceFoldersChanged = () => {
|
|
3231
|
+
scheduleRefresh();
|
|
3232
|
+
void ensureManagedRepoScanIgnores();
|
|
3233
|
+
};
|
|
1780
3234
|
const refresh = () => void refreshController.refreshNow();
|
|
1781
3235
|
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
1782
3236
|
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
1783
3237
|
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
3238
|
+
const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB);
|
|
1784
3239
|
const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB);
|
|
1785
3240
|
const updateCommitInput = (session) => {
|
|
1786
3241
|
sourceControl.inputBox.enabled = true;
|
|
@@ -1868,8 +3323,9 @@ function activate(context) {
|
|
|
1868
3323
|
vscode.window.registerFileDecorationProvider(decorationProvider),
|
|
1869
3324
|
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
|
|
1870
3325
|
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
|
|
3326
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents),
|
|
1871
3327
|
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
|
|
1872
|
-
await vscode.commands.executeCommand('workbench.view.
|
|
3328
|
+
await vscode.commands.executeCommand('workbench.view.extension.gitguardex.activeAgentsContainer');
|
|
1873
3329
|
}),
|
|
1874
3330
|
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
|
|
1875
3331
|
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
|
|
@@ -1898,14 +3354,15 @@ function activate(context) {
|
|
|
1898
3354
|
vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
|
|
1899
3355
|
inspectPanelManager.open(session || provider.getSelectedSession());
|
|
1900
3356
|
}),
|
|
3357
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.showSessionTerminal', showSessionTerminal),
|
|
1901
3358
|
vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
|
|
1902
3359
|
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
|
|
1903
3360
|
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
|
|
1904
|
-
vscode.
|
|
1905
|
-
vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
|
|
3361
|
+
vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged),
|
|
1906
3362
|
activeSessionsWatcher,
|
|
1907
3363
|
lockWatcher,
|
|
1908
3364
|
worktreeLockWatcher,
|
|
3365
|
+
managedWorktreeGitWatcher,
|
|
1909
3366
|
logWatcher,
|
|
1910
3367
|
{ dispose: () => clearInterval(interval) },
|
|
1911
3368
|
);
|
|
@@ -1914,8 +3371,10 @@ function activate(context) {
|
|
|
1914
3371
|
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
1915
3372
|
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
1916
3373
|
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
3374
|
+
...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh),
|
|
1917
3375
|
...bindRefreshWatcher(logWatcher, scheduleRefresh),
|
|
1918
3376
|
);
|
|
3377
|
+
void ensureManagedRepoScanIgnores();
|
|
1919
3378
|
void refreshController.refreshNow();
|
|
1920
3379
|
void maybeAutoUpdateActiveAgentsExtension(context);
|
|
1921
3380
|
}
|