@imdeadpool/guardex 7.0.19 → 7.0.21
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 +51 -13
- package/bin/multiagent-safety.js +2 -7861
- package/package.json +2 -1
- package/src/cli/args.js +809 -0
- package/src/cli/dispatch.js +86 -0
- package/src/cli/main.js +5564 -0
- package/src/context.js +517 -0
- package/src/core/runtime.js +119 -0
- package/src/finish/index.js +425 -0
- package/src/git/index.js +122 -0
- package/src/hooks/index.js +74 -0
- package/src/output/index.js +398 -0
- package/src/sandbox/index.js +68 -0
- package/src/scaffold/index.js +148 -0
- package/src/toolchain/index.js +223 -0
- package/templates/scripts/agent-branch-start.sh +52 -8
- package/templates/scripts/codex-agent.sh +155 -7
- package/templates/vscode/guardex-active-agents/README.md +16 -11
- package/templates/vscode/guardex-active-agents/extension.js +908 -65
- package/templates/vscode/guardex-active-agents/package.json +63 -1
- package/templates/vscode/guardex-active-agents/session-schema.js +440 -42
|
@@ -1,7 +1,124 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
+
const cp = require('node:child_process');
|
|
3
4
|
const vscode = require('vscode');
|
|
4
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
formatElapsedFrom,
|
|
7
|
+
readActiveSessions,
|
|
8
|
+
readRepoChanges,
|
|
9
|
+
sanitizeBranchForFile,
|
|
10
|
+
} = require('./session-schema.js');
|
|
11
|
+
|
|
12
|
+
const SESSION_DECORATION_SCHEME = 'gitguardex-agent';
|
|
13
|
+
const IDLE_WARNING_MS = 10 * 60 * 1000;
|
|
14
|
+
const IDLE_ERROR_MS = 30 * 60 * 1000;
|
|
15
|
+
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
16
|
+
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
17
|
+
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
18
|
+
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
19
|
+
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
20
|
+
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
21
|
+
const SESSION_SCAN_LIMIT = 200;
|
|
22
|
+
const REFRESH_DEBOUNCE_MS = 250;
|
|
23
|
+
const SESSION_ACTIVITY_GROUPS = [
|
|
24
|
+
{ kind: 'blocked', label: 'BLOCKED' },
|
|
25
|
+
{ kind: 'working', label: 'WORKING NOW' },
|
|
26
|
+
{ kind: 'idle', label: 'IDLE' },
|
|
27
|
+
{ kind: 'stalled', label: 'STALLED' },
|
|
28
|
+
{ kind: 'dead', label: 'DEAD' },
|
|
29
|
+
];
|
|
30
|
+
const SESSION_ACTIVITY_ICON_IDS = {
|
|
31
|
+
blocked: 'warning',
|
|
32
|
+
working: 'edit',
|
|
33
|
+
idle: 'loading~spin',
|
|
34
|
+
stalled: 'clock',
|
|
35
|
+
dead: 'error',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function sessionDecorationUri(branch) {
|
|
39
|
+
return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sessionIdleDecoration(session, now = Date.now()) {
|
|
43
|
+
if (!session) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (session.activityKind === 'blocked') {
|
|
48
|
+
return {
|
|
49
|
+
badge: '!',
|
|
50
|
+
tooltip: 'blocked',
|
|
51
|
+
color: new vscode.ThemeColor('list.warningForeground'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (session.activityKind === 'dead') {
|
|
55
|
+
return {
|
|
56
|
+
badge: 'x',
|
|
57
|
+
tooltip: 'dead',
|
|
58
|
+
color: new vscode.ThemeColor('list.errorForeground'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (session.activityKind === 'stalled') {
|
|
62
|
+
return {
|
|
63
|
+
badge: '!',
|
|
64
|
+
tooltip: 'stalled',
|
|
65
|
+
color: new vscode.ThemeColor('list.errorForeground'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (session.activityKind === 'working') {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const startedAtMs = Date.parse(session.startedAt);
|
|
73
|
+
if (!Number.isFinite(startedAtMs)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const elapsedMs = now - startedAtMs;
|
|
78
|
+
if (elapsedMs > IDLE_ERROR_MS) {
|
|
79
|
+
return {
|
|
80
|
+
badge: '30m+',
|
|
81
|
+
tooltip: 'idle 30m+',
|
|
82
|
+
color: new vscode.ThemeColor('list.errorForeground'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (elapsedMs > IDLE_WARNING_MS) {
|
|
86
|
+
return {
|
|
87
|
+
badge: '10m+',
|
|
88
|
+
tooltip: 'idle 10m+',
|
|
89
|
+
color: new vscode.ThemeColor('list.warningForeground'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class SessionDecorationProvider {
|
|
97
|
+
constructor(nowProvider = () => Date.now()) {
|
|
98
|
+
this.nowProvider = nowProvider;
|
|
99
|
+
this.sessionsByUri = new Map();
|
|
100
|
+
this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
|
|
101
|
+
this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
updateSessions(sessions) {
|
|
105
|
+
this.sessionsByUri = new Map(
|
|
106
|
+
sessions.map((session) => [sessionDecorationUri(session.branch).toString(), session]),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
refresh() {
|
|
111
|
+
this.onDidChangeFileDecorationsEmitter.fire();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
provideFileDecoration(uri) {
|
|
115
|
+
if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
5
122
|
|
|
6
123
|
class InfoItem extends vscode.TreeItem {
|
|
7
124
|
constructor(label, description = '') {
|
|
@@ -17,8 +134,16 @@ class RepoItem extends vscode.TreeItem {
|
|
|
17
134
|
this.repoRoot = repoRoot;
|
|
18
135
|
this.sessions = sessions;
|
|
19
136
|
this.changes = changes;
|
|
20
|
-
const descriptionParts = [
|
|
137
|
+
const descriptionParts = [];
|
|
138
|
+
const activeCount = countActiveSessions(sessions);
|
|
139
|
+
const deadCount = countSessionsByActivityKind(sessions, 'dead');
|
|
21
140
|
const workingCount = countWorkingSessions(sessions);
|
|
141
|
+
if (activeCount > 0) {
|
|
142
|
+
descriptionParts.push(`${activeCount} active`);
|
|
143
|
+
}
|
|
144
|
+
if (deadCount > 0) {
|
|
145
|
+
descriptionParts.push(`${deadCount} dead`);
|
|
146
|
+
}
|
|
22
147
|
if (workingCount > 0) {
|
|
23
148
|
descriptionParts.push(`${workingCount} working`);
|
|
24
149
|
}
|
|
@@ -46,9 +171,15 @@ class SectionItem extends vscode.TreeItem {
|
|
|
46
171
|
}
|
|
47
172
|
|
|
48
173
|
class SessionItem extends vscode.TreeItem {
|
|
49
|
-
constructor(session) {
|
|
50
|
-
|
|
174
|
+
constructor(session, items = []) {
|
|
175
|
+
const lockCount = Number.isFinite(session.lockCount) ? session.lockCount : 0;
|
|
176
|
+
super(
|
|
177
|
+
`${session.label} 🔒 ${lockCount}`,
|
|
178
|
+
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
|
|
179
|
+
);
|
|
51
180
|
this.session = session;
|
|
181
|
+
this.items = items;
|
|
182
|
+
this.resourceUri = sessionDecorationUri(session.branch);
|
|
52
183
|
const descriptionParts = [session.activityLabel || 'thinking'];
|
|
53
184
|
if (session.activityCountLabel) {
|
|
54
185
|
descriptionParts.push(session.activityCountLabel);
|
|
@@ -58,17 +189,27 @@ class SessionItem extends vscode.TreeItem {
|
|
|
58
189
|
const tooltipLines = [
|
|
59
190
|
session.branch,
|
|
60
191
|
`${session.agentName} · ${session.taskName}`,
|
|
192
|
+
session.latestTaskPreview && session.latestTaskPreview !== session.taskName
|
|
193
|
+
? `Live task ${session.latestTaskPreview}`
|
|
194
|
+
: '',
|
|
61
195
|
`Status ${this.description}`,
|
|
62
196
|
session.changeCount > 0
|
|
63
197
|
? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
|
|
64
198
|
: session.activitySummary,
|
|
65
|
-
`
|
|
199
|
+
`Locks ${lockCount}`,
|
|
200
|
+
Number.isInteger(session.pid) && session.pid > 0
|
|
201
|
+
? session.pidAlive === false
|
|
202
|
+
? `PID ${session.pid} not alive`
|
|
203
|
+
: `PID ${session.pid} alive`
|
|
204
|
+
: '',
|
|
205
|
+
session.lastFileActivityAt ? `Last file activity ${session.lastFileActivityAt}` : '',
|
|
206
|
+
session.sourceKind === 'worktree-lock'
|
|
207
|
+
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
208
|
+
: `Started ${session.startedAt}`,
|
|
66
209
|
session.worktreePath,
|
|
67
210
|
];
|
|
68
211
|
this.tooltip = tooltipLines.filter(Boolean).join('\n');
|
|
69
|
-
this.iconPath = session.activityKind
|
|
70
|
-
? new vscode.ThemeIcon('edit')
|
|
71
|
-
: new vscode.ThemeIcon('loading~spin');
|
|
212
|
+
this.iconPath = new vscode.ThemeIcon(resolveSessionActivityIconId(session.activityKind));
|
|
72
213
|
this.contextValue = 'gitguardex.session';
|
|
73
214
|
this.command = {
|
|
74
215
|
command: 'gitguardex.activeAgents.openWorktree',
|
|
@@ -98,9 +239,13 @@ class ChangeItem extends vscode.TreeItem {
|
|
|
98
239
|
change.relativePath,
|
|
99
240
|
`Status ${change.statusText}`,
|
|
100
241
|
change.originalPath ? `Renamed from ${change.originalPath}` : '',
|
|
242
|
+
change.hasForeignLock ? `Locked by ${change.lockOwnerBranch}` : '',
|
|
101
243
|
change.absolutePath,
|
|
102
244
|
].filter(Boolean).join('\n');
|
|
103
245
|
this.resourceUri = vscode.Uri.file(change.absolutePath);
|
|
246
|
+
if (change.hasForeignLock) {
|
|
247
|
+
this.iconPath = new vscode.ThemeIcon('warning');
|
|
248
|
+
}
|
|
104
249
|
this.contextValue = 'gitguardex.change';
|
|
105
250
|
this.command = {
|
|
106
251
|
command: 'gitguardex.activeAgents.openChange',
|
|
@@ -110,10 +255,330 @@ class ChangeItem extends vscode.TreeItem {
|
|
|
110
255
|
}
|
|
111
256
|
}
|
|
112
257
|
|
|
258
|
+
function shellQuote(value) {
|
|
259
|
+
const normalized = String(value || '');
|
|
260
|
+
return `'${normalized.replace(/'/g, "'\"'\"'")}'`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sessionDisplayLabel(session) {
|
|
264
|
+
return session?.taskName || session?.label || session?.branch || path.basename(session?.worktreePath || '') || 'session';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function sessionWorktreePath(session) {
|
|
268
|
+
return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function showSessionMessage(message) {
|
|
272
|
+
vscode.window.showInformationMessage?.(message);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function ensureSessionWorktree(session, actionLabel) {
|
|
276
|
+
const worktreePath = sessionWorktreePath(session);
|
|
277
|
+
if (!worktreePath) {
|
|
278
|
+
showSessionMessage(`Cannot ${actionLabel}: missing worktree path.`);
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
if (!fs.existsSync(worktreePath)) {
|
|
282
|
+
showSessionMessage(`Cannot ${actionLabel}: worktree is no longer on disk: ${worktreePath}`);
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
return worktreePath;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function runSessionTerminalCommand(session, actionLabel, iconId, commandText) {
|
|
289
|
+
const worktreePath = ensureSessionWorktree(session, actionLabel.toLowerCase());
|
|
290
|
+
if (!worktreePath) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const terminal = vscode.window.createTerminal({
|
|
295
|
+
name: `GitGuardex ${actionLabel}: ${sessionDisplayLabel(session)}`,
|
|
296
|
+
cwd: worktreePath,
|
|
297
|
+
iconPath: new vscode.ThemeIcon(iconId),
|
|
298
|
+
});
|
|
299
|
+
terminal.show();
|
|
300
|
+
terminal.sendText(commandText, true);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function finishSession(session) {
|
|
304
|
+
if (!session?.branch) {
|
|
305
|
+
showSessionMessage('Cannot finish session: missing branch name.');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
runSessionTerminalCommand(
|
|
309
|
+
session,
|
|
310
|
+
'Finish',
|
|
311
|
+
'check',
|
|
312
|
+
`gx branch finish --branch ${shellQuote(session.branch)}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function syncSession(session) {
|
|
317
|
+
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function stopSession(session, refresh) {
|
|
321
|
+
const pid = Number(session?.pid);
|
|
322
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
323
|
+
showSessionMessage('Cannot stop session: missing pid.');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const confirmed = await vscode.window.showWarningMessage(
|
|
328
|
+
`Stop ${sessionDisplayLabel(session)}?`,
|
|
329
|
+
{ modal: true, detail: `Send SIGTERM to pid ${pid}.` },
|
|
330
|
+
'Stop',
|
|
331
|
+
);
|
|
332
|
+
if (confirmed !== 'Stop') {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
process.kill(pid, 'SIGTERM');
|
|
338
|
+
refresh();
|
|
339
|
+
} catch (error) {
|
|
340
|
+
showSessionMessage(
|
|
341
|
+
`Failed to stop session ${sessionDisplayLabel(session)}: ${error?.message || String(error)}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function openSessionDiff(session) {
|
|
347
|
+
const worktreePath = ensureSessionWorktree(session, 'open diff');
|
|
348
|
+
if (!worktreePath) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let diffOutput = '';
|
|
353
|
+
try {
|
|
354
|
+
diffOutput = cp.execFileSync('git', ['-C', worktreePath, 'diff'], {
|
|
355
|
+
encoding: 'utf8',
|
|
356
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
357
|
+
});
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const detail = [
|
|
360
|
+
error?.stdout,
|
|
361
|
+
error?.stderr,
|
|
362
|
+
error?.message,
|
|
363
|
+
].find((value) => typeof value === 'string' && value.trim().length > 0) || 'git diff failed.';
|
|
364
|
+
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${detail.trim()}`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const document = await vscode.workspace.openTextDocument({
|
|
369
|
+
language: 'diff',
|
|
370
|
+
content: diffOutput,
|
|
371
|
+
});
|
|
372
|
+
await vscode.window.showTextDocument(document, { preview: false });
|
|
373
|
+
}
|
|
374
|
+
|
|
113
375
|
function repoRootFromSessionFile(filePath) {
|
|
114
376
|
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
115
377
|
}
|
|
116
378
|
|
|
379
|
+
function repoRootFromWorktreeLockFile(filePath) {
|
|
380
|
+
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function repoRootFromLockFile(filePath) {
|
|
384
|
+
return path.resolve(path.dirname(filePath), '..', '..');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function normalizeRelativePath(relativePath) {
|
|
388
|
+
return String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function emptyLockRegistry() {
|
|
392
|
+
return {
|
|
393
|
+
entriesByPath: new Map(),
|
|
394
|
+
countsByBranch: new Map(),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function readLockRegistry(repoRoot) {
|
|
399
|
+
const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
400
|
+
if (!fs.existsSync(lockPath)) {
|
|
401
|
+
return emptyLockRegistry();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let parsed;
|
|
405
|
+
try {
|
|
406
|
+
parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
407
|
+
} catch (_error) {
|
|
408
|
+
return emptyLockRegistry();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const locks = parsed?.locks;
|
|
412
|
+
if (!locks || typeof locks !== 'object' || Array.isArray(locks)) {
|
|
413
|
+
return emptyLockRegistry();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const entriesByPath = new Map();
|
|
417
|
+
const countsByBranch = new Map();
|
|
418
|
+
for (const [rawRelativePath, entry] of Object.entries(locks)) {
|
|
419
|
+
if (!entry || typeof entry !== 'object') {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const relativePath = normalizeRelativePath(rawRelativePath);
|
|
424
|
+
const branch = typeof entry.branch === 'string' ? entry.branch.trim() : '';
|
|
425
|
+
if (!relativePath || !branch) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
entriesByPath.set(relativePath, {
|
|
430
|
+
branch,
|
|
431
|
+
claimedAt: typeof entry.claimed_at === 'string' ? entry.claimed_at : '',
|
|
432
|
+
allowDelete: Boolean(entry.allow_delete),
|
|
433
|
+
});
|
|
434
|
+
countsByBranch.set(branch, (countsByBranch.get(branch) || 0) + 1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
entriesByPath,
|
|
439
|
+
countsByBranch,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function readCurrentBranch(repoRoot) {
|
|
444
|
+
try {
|
|
445
|
+
return cp.execFileSync('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
446
|
+
encoding: 'utf8',
|
|
447
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
448
|
+
}).trim();
|
|
449
|
+
} catch (_error) {
|
|
450
|
+
return '';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function decorateSession(session, lockRegistry) {
|
|
455
|
+
return {
|
|
456
|
+
...session,
|
|
457
|
+
lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function decorateChange(change, lockRegistry, owningBranch) {
|
|
462
|
+
const lockEntry = lockRegistry.entriesByPath.get(normalizeRelativePath(change.relativePath));
|
|
463
|
+
const lockOwnerBranch = lockEntry?.branch || '';
|
|
464
|
+
return {
|
|
465
|
+
...change,
|
|
466
|
+
lockOwnerBranch,
|
|
467
|
+
hasForeignLock: Boolean(lockOwnerBranch) && (!owningBranch || lockOwnerBranch !== owningBranch),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function isPathWithin(parentPath, targetPath) {
|
|
472
|
+
const relativePath = path.relative(parentPath, targetPath);
|
|
473
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function localizeChangeForSession(session, change) {
|
|
477
|
+
if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let originalPath = change.originalPath;
|
|
482
|
+
if (originalPath) {
|
|
483
|
+
const originalAbsolutePath = path.join(session.repoRoot, originalPath);
|
|
484
|
+
if (isPathWithin(session.worktreePath, originalAbsolutePath)) {
|
|
485
|
+
originalPath = normalizeRelativePath(path.relative(session.worktreePath, originalAbsolutePath));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
...change,
|
|
491
|
+
relativePath: normalizeRelativePath(path.relative(session.worktreePath, change.absolutePath)),
|
|
492
|
+
originalPath,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function findRepoSessionEntries() {
|
|
497
|
+
const [sessionFiles, worktreeLockFiles] = await Promise.all([
|
|
498
|
+
vscode.workspace.findFiles(
|
|
499
|
+
ACTIVE_SESSION_FILES_GLOB,
|
|
500
|
+
SESSION_SCAN_EXCLUDE_GLOB,
|
|
501
|
+
SESSION_SCAN_LIMIT,
|
|
502
|
+
),
|
|
503
|
+
vscode.workspace.findFiles(
|
|
504
|
+
WORKTREE_AGENT_LOCKS_GLOB,
|
|
505
|
+
WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
|
|
506
|
+
SESSION_SCAN_LIMIT,
|
|
507
|
+
),
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
const repoRoots = new Set();
|
|
511
|
+
for (const uri of sessionFiles) {
|
|
512
|
+
repoRoots.add(repoRootFromSessionFile(uri.fsPath));
|
|
513
|
+
}
|
|
514
|
+
for (const uri of worktreeLockFiles) {
|
|
515
|
+
if (path.basename(uri.fsPath) !== 'AGENT.lock') {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
repoRoots.add(repoRootFromWorktreeLockFile(uri.fsPath));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (repoRoots.size === 0) {
|
|
522
|
+
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
523
|
+
repoRoots.add(workspaceFolder.uri.fsPath);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const repoEntries = [];
|
|
528
|
+
for (const repoRoot of repoRoots) {
|
|
529
|
+
const sessions = readActiveSessions(repoRoot, { includeStale: true });
|
|
530
|
+
if (sessions.length > 0) {
|
|
531
|
+
repoEntries.push({ repoRoot, sessions });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
repoEntries.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot));
|
|
536
|
+
return repoEntries;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function resolveSessionWatcherKey(session) {
|
|
540
|
+
return `${path.resolve(session.repoRoot)}::${session.branch}::${path.resolve(session.worktreePath)}`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function resolveSessionGitIndexPath(worktreePath) {
|
|
544
|
+
const gitPath = path.join(worktreePath, '.git');
|
|
545
|
+
const defaultIndexPath = path.join(gitPath, 'index');
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
if (fs.statSync(gitPath).isDirectory()) {
|
|
549
|
+
return defaultIndexPath;
|
|
550
|
+
}
|
|
551
|
+
} catch (_error) {
|
|
552
|
+
return defaultIndexPath;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
557
|
+
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
558
|
+
if (match?.[1]) {
|
|
559
|
+
return path.resolve(worktreePath, match[1].trim(), 'index');
|
|
560
|
+
}
|
|
561
|
+
} catch (_error) {
|
|
562
|
+
return defaultIndexPath;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return defaultIndexPath;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function bindRefreshWatcher(watcher, refresh) {
|
|
569
|
+
return [
|
|
570
|
+
watcher.onDidCreate(refresh),
|
|
571
|
+
watcher.onDidChange(refresh),
|
|
572
|
+
watcher.onDidDelete(refresh),
|
|
573
|
+
];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function disposeAll(disposables) {
|
|
577
|
+
for (const disposable of disposables) {
|
|
578
|
+
disposable?.dispose?.();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
117
582
|
function buildChangeTreeNodes(changes) {
|
|
118
583
|
const root = [];
|
|
119
584
|
|
|
@@ -179,30 +644,198 @@ function countWorkingSessions(sessions) {
|
|
|
179
644
|
return sessions.filter((session) => session.activityKind === 'working').length;
|
|
180
645
|
}
|
|
181
646
|
|
|
182
|
-
function
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
|
|
647
|
+
function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
648
|
+
const changesBySession = new Map();
|
|
649
|
+
const sessionByChangedPath = new Map();
|
|
650
|
+
const repoRootChanges = [];
|
|
651
|
+
|
|
652
|
+
for (const session of sessions) {
|
|
653
|
+
changesBySession.set(session.branch, []);
|
|
654
|
+
for (const changedPath of session.changedPaths || []) {
|
|
655
|
+
if (!sessionByChangedPath.has(changedPath)) {
|
|
656
|
+
sessionByChangedPath.set(changedPath, session);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (const change of changes) {
|
|
662
|
+
const normalizedRelativePath = normalizeRelativePath(change.relativePath);
|
|
663
|
+
const session = sessionByChangedPath.get(normalizedRelativePath)
|
|
664
|
+
|| sessions.find((candidate) => isPathWithin(candidate.worktreePath, change.absolutePath));
|
|
665
|
+
if (!session) {
|
|
666
|
+
repoRootChanges.push(change);
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const localizedChange = localizeChangeForSession(session, change);
|
|
671
|
+
if (!localizedChange) {
|
|
672
|
+
repoRootChanges.push(change);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
changesBySession.get(session.branch).push(localizedChange);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const items = sessions
|
|
680
|
+
.map((session) => {
|
|
681
|
+
const sessionChanges = changesBySession.get(session.branch) || [];
|
|
682
|
+
if (sessionChanges.length === 0) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
return new SessionItem(session, buildChangeTreeNodes(sessionChanges));
|
|
686
|
+
})
|
|
687
|
+
.filter(Boolean);
|
|
688
|
+
|
|
689
|
+
if (repoRootChanges.length > 0) {
|
|
690
|
+
items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
|
|
691
|
+
description: String(repoRootChanges.length),
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return items;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function countActiveSessions(sessions) {
|
|
699
|
+
return sessions.filter((session) => session.activityKind !== 'dead').length;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function countSessionsByActivityKind(sessions, activityKind) {
|
|
703
|
+
return sessions.filter((session) => session.activityKind === activityKind).length;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function resolveSessionActivityIconId(activityKind) {
|
|
707
|
+
return SESSION_ACTIVITY_ICON_IDS[activityKind] || 'loading~spin';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function pickRepoRoot() {
|
|
711
|
+
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
|
712
|
+
if (workspaceFolders.length === 0) {
|
|
713
|
+
vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.');
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (workspaceFolders.length === 1) {
|
|
718
|
+
return workspaceFolders[0].uri.fsPath;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const picks = workspaceFolders.map((folder) => ({
|
|
722
|
+
label: path.basename(folder.uri.fsPath),
|
|
723
|
+
description: folder.uri.fsPath,
|
|
724
|
+
repoRoot: folder.uri.fsPath,
|
|
725
|
+
}));
|
|
726
|
+
const selection = await vscode.window.showQuickPick?.(picks, {
|
|
727
|
+
placeHolder: 'Select the Guardex repo where gx branch start should run.',
|
|
728
|
+
});
|
|
729
|
+
return selection?.repoRoot || null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function promptStartAgentDetails() {
|
|
733
|
+
const taskName = await vscode.window.showInputBox?.({
|
|
734
|
+
prompt: 'Task for gx branch start',
|
|
735
|
+
placeHolder: 'vscode active agents welcome view',
|
|
736
|
+
ignoreFocusOut: true,
|
|
737
|
+
validateInput: (value) => value.trim() ? undefined : 'Task is required.',
|
|
738
|
+
});
|
|
739
|
+
if (!taskName) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
190
742
|
|
|
191
|
-
|
|
192
|
-
|
|
743
|
+
const agentName = await vscode.window.showInputBox?.({
|
|
744
|
+
prompt: 'Agent name for gx branch start',
|
|
745
|
+
placeHolder: 'codex',
|
|
746
|
+
value: 'codex',
|
|
747
|
+
ignoreFocusOut: true,
|
|
748
|
+
validateInput: (value) => value.trim() ? undefined : 'Agent name is required.',
|
|
749
|
+
});
|
|
750
|
+
if (!agentName) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
taskName: taskName.trim(),
|
|
756
|
+
agentName: agentName.trim(),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function startAgentFromPrompt(refresh) {
|
|
761
|
+
const repoRoot = await pickRepoRoot();
|
|
762
|
+
if (!repoRoot) {
|
|
763
|
+
return;
|
|
193
764
|
}
|
|
194
|
-
|
|
195
|
-
|
|
765
|
+
|
|
766
|
+
const details = await promptStartAgentDetails();
|
|
767
|
+
if (!details) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const terminal = vscode.window.createTerminal?.({
|
|
772
|
+
name: `GitGuardex: ${path.basename(repoRoot)}`,
|
|
773
|
+
cwd: repoRoot,
|
|
774
|
+
});
|
|
775
|
+
terminal?.show(true);
|
|
776
|
+
terminal?.sendText(
|
|
777
|
+
`gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`,
|
|
778
|
+
true,
|
|
779
|
+
);
|
|
780
|
+
refresh();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function sessionSelectionKey(session) {
|
|
784
|
+
if (!session?.repoRoot || !session?.branch) {
|
|
785
|
+
return '';
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return `${session.repoRoot}::${session.branch}`;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function formatGitCommandFailure(error) {
|
|
792
|
+
for (const value of [error?.stderr, error?.stdout, error?.message]) {
|
|
793
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
794
|
+
return value.trim();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return 'Git command failed.';
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function runGitCommand(worktreePath, args) {
|
|
801
|
+
return cp.execFileSync('git', ['-C', worktreePath, ...args], {
|
|
802
|
+
encoding: 'utf8',
|
|
803
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function stageWorktreeForCommit(worktreePath) {
|
|
808
|
+
runGitCommand(worktreePath, ['add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function commitWorktree(worktreePath, message) {
|
|
812
|
+
runGitCommand(worktreePath, ['commit', '-m', message]);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function buildActiveAgentGroupNodes(sessions) {
|
|
816
|
+
const groups = [];
|
|
817
|
+
for (const group of SESSION_ACTIVITY_GROUPS) {
|
|
818
|
+
const groupSessions = sessions
|
|
819
|
+
.filter((session) => session.activityKind === group.kind)
|
|
820
|
+
.map((session) => new SessionItem(session));
|
|
821
|
+
if (groupSessions.length > 0) {
|
|
822
|
+
groups.push(new SectionItem(group.label, groupSessions));
|
|
823
|
+
}
|
|
196
824
|
}
|
|
197
825
|
|
|
198
826
|
return groups;
|
|
199
827
|
}
|
|
200
828
|
|
|
201
829
|
class ActiveAgentsProvider {
|
|
202
|
-
constructor() {
|
|
830
|
+
constructor(decorationProvider) {
|
|
831
|
+
this.decorationProvider = decorationProvider;
|
|
203
832
|
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
|
|
204
833
|
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
|
|
834
|
+
this.onDidChangeSelectedSessionEmitter = new vscode.EventEmitter();
|
|
835
|
+
this.onDidChangeSelectedSession = this.onDidChangeSelectedSessionEmitter.event;
|
|
205
836
|
this.treeView = null;
|
|
837
|
+
this.lockRegistryByRepoRoot = new Map();
|
|
838
|
+
this.selectedSession = null;
|
|
206
839
|
}
|
|
207
840
|
|
|
208
841
|
getTreeItem(element) {
|
|
@@ -211,19 +844,59 @@ class ActiveAgentsProvider {
|
|
|
211
844
|
|
|
212
845
|
attachTreeView(treeView) {
|
|
213
846
|
this.treeView = treeView;
|
|
214
|
-
this.updateViewState(0, 0);
|
|
847
|
+
this.updateViewState(0, 0, 0);
|
|
848
|
+
treeView.onDidChangeSelection?.((event) => {
|
|
849
|
+
const sessionItem = event.selection.find((item) => item instanceof SessionItem);
|
|
850
|
+
this.setSelectedSession(sessionItem?.session || null);
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
setSelectedSession(session) {
|
|
855
|
+
const nextSession = session?.worktreePath ? { ...session } : null;
|
|
856
|
+
const currentKey = sessionSelectionKey(this.selectedSession);
|
|
857
|
+
const nextKey = sessionSelectionKey(nextSession);
|
|
858
|
+
this.selectedSession = nextSession;
|
|
859
|
+
if (currentKey !== nextKey) {
|
|
860
|
+
this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
|
|
861
|
+
}
|
|
215
862
|
}
|
|
216
863
|
|
|
217
|
-
|
|
864
|
+
getSelectedSession() {
|
|
865
|
+
return this.selectedSession ? { ...this.selectedSession } : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
syncSelectedSession(repoEntries) {
|
|
869
|
+
if (!this.selectedSession) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const nextSession = repoEntries
|
|
874
|
+
.flatMap((entry) => entry.sessions)
|
|
875
|
+
.find((session) => sessionSelectionKey(session) === sessionSelectionKey(this.selectedSession));
|
|
876
|
+
this.setSelectedSession(nextSession || null);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
updateViewState(sessionCount, workingCount, deadCount) {
|
|
218
880
|
if (!this.treeView) {
|
|
219
881
|
return;
|
|
220
882
|
}
|
|
221
883
|
|
|
884
|
+
const activeCount = Math.max(0, sessionCount - deadCount);
|
|
885
|
+
const badgeTooltipParts = [];
|
|
886
|
+
if (activeCount > 0) {
|
|
887
|
+
badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
|
|
888
|
+
}
|
|
889
|
+
if (deadCount > 0) {
|
|
890
|
+
badgeTooltipParts.push(`${deadCount} dead`);
|
|
891
|
+
}
|
|
892
|
+
if (workingCount > 0) {
|
|
893
|
+
badgeTooltipParts.push(`${workingCount} working now`);
|
|
894
|
+
}
|
|
895
|
+
|
|
222
896
|
this.treeView.badge = sessionCount > 0
|
|
223
897
|
? {
|
|
224
898
|
value: sessionCount,
|
|
225
|
-
tooltip:
|
|
226
|
-
+ (workingCount > 0 ? ` · ${workingCount} working now` : ''),
|
|
899
|
+
tooltip: badgeTooltipParts.join(' · '),
|
|
227
900
|
}
|
|
228
901
|
: undefined;
|
|
229
902
|
this.treeView.message = sessionCount > 0
|
|
@@ -231,8 +904,55 @@ class ActiveAgentsProvider {
|
|
|
231
904
|
: 'Start a sandbox session to populate this view.';
|
|
232
905
|
}
|
|
233
906
|
|
|
234
|
-
|
|
907
|
+
async syncRepoEntries() {
|
|
908
|
+
const repoEntries = await this.loadRepoEntries();
|
|
909
|
+
const sessionCount = repoEntries.reduce((total, entry) => total + entry.sessions.length, 0);
|
|
910
|
+
const workingCount = repoEntries.reduce(
|
|
911
|
+
(total, entry) => total + countWorkingSessions(entry.sessions),
|
|
912
|
+
0,
|
|
913
|
+
);
|
|
914
|
+
const deadCount = repoEntries.reduce(
|
|
915
|
+
(total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
|
|
916
|
+
0,
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
this.updateViewState(sessionCount, workingCount, deadCount);
|
|
920
|
+
this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
|
|
921
|
+
return repoEntries;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async refresh() {
|
|
925
|
+
await this.syncRepoEntries();
|
|
235
926
|
this.onDidChangeTreeDataEmitter.fire();
|
|
927
|
+
this.decorationProvider?.refresh();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
readLockRegistryForRepo(repoRoot) {
|
|
931
|
+
const lockRegistry = readLockRegistry(repoRoot);
|
|
932
|
+
this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
|
|
933
|
+
return lockRegistry;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
getLockRegistryForRepo(repoRoot) {
|
|
937
|
+
return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
refreshLockRegistryForFile(filePath) {
|
|
941
|
+
this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
readLockRegistryForRepo(repoRoot) {
|
|
945
|
+
const lockRegistry = readLockRegistry(repoRoot);
|
|
946
|
+
this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
|
|
947
|
+
return lockRegistry;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
getLockRegistryForRepo(repoRoot) {
|
|
951
|
+
return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
refreshLockRegistryForFile(filePath) {
|
|
955
|
+
this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
|
|
236
956
|
}
|
|
237
957
|
|
|
238
958
|
async getChildren(element) {
|
|
@@ -243,22 +963,19 @@ class ActiveAgentsProvider {
|
|
|
243
963
|
}),
|
|
244
964
|
];
|
|
245
965
|
if (element.changes.length > 0) {
|
|
246
|
-
sectionItems.push(new SectionItem('CHANGES',
|
|
966
|
+
sectionItems.push(new SectionItem('CHANGES', buildGroupedChangeTreeNodes(element.sessions, element.changes), {
|
|
967
|
+
description: String(element.changes.length),
|
|
968
|
+
}));
|
|
247
969
|
}
|
|
248
970
|
return sectionItems;
|
|
249
971
|
}
|
|
250
972
|
|
|
251
|
-
if (element instanceof SectionItem || element instanceof FolderItem) {
|
|
973
|
+
if (element instanceof SectionItem || element instanceof FolderItem || element instanceof SessionItem) {
|
|
252
974
|
return element.items;
|
|
253
975
|
}
|
|
254
976
|
|
|
255
|
-
const repoEntries = await this.
|
|
256
|
-
|
|
257
|
-
const workingCount = repoEntries.reduce(
|
|
258
|
-
(total, entry) => total + countWorkingSessions(entry.sessions),
|
|
259
|
-
0,
|
|
260
|
-
);
|
|
261
|
-
this.updateViewState(sessionCount, workingCount);
|
|
977
|
+
const repoEntries = await this.syncRepoEntries();
|
|
978
|
+
this.syncSelectedSession(repoEntries);
|
|
262
979
|
|
|
263
980
|
if (repoEntries.length === 0) {
|
|
264
981
|
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
|
|
@@ -268,54 +985,171 @@ class ActiveAgentsProvider {
|
|
|
268
985
|
}
|
|
269
986
|
|
|
270
987
|
async loadRepoEntries() {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
988
|
+
const repoEntries = await findRepoSessionEntries();
|
|
989
|
+
return repoEntries.map((entry) => {
|
|
990
|
+
const repoRoot = entry.repoRoot;
|
|
991
|
+
const lockRegistry = this.getLockRegistryForRepo(repoRoot);
|
|
992
|
+
const currentBranch = readCurrentBranch(repoRoot);
|
|
993
|
+
return {
|
|
994
|
+
repoRoot,
|
|
995
|
+
sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
|
|
996
|
+
changes: readRepoChanges(repoRoot).map((change) => (
|
|
997
|
+
decorateChange(change, lockRegistry, currentBranch)
|
|
998
|
+
)),
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
276
1003
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
1004
|
+
class ActiveAgentsRefreshController {
|
|
1005
|
+
constructor(provider) {
|
|
1006
|
+
this.provider = provider;
|
|
1007
|
+
this.refreshTimer = null;
|
|
1008
|
+
this.sessionWatchers = new Map();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
scheduleRefresh() {
|
|
1012
|
+
if (this.refreshTimer) {
|
|
1013
|
+
clearTimeout(this.refreshTimer);
|
|
280
1014
|
}
|
|
1015
|
+
this.refreshTimer = setTimeout(() => {
|
|
1016
|
+
this.refreshTimer = null;
|
|
1017
|
+
void this.refreshNow();
|
|
1018
|
+
}, REFRESH_DEBOUNCE_MS);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async refreshNow() {
|
|
1022
|
+
await this.syncSessionWatchers();
|
|
1023
|
+
await this.provider.refresh();
|
|
1024
|
+
}
|
|
281
1025
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1026
|
+
async syncSessionWatchers() {
|
|
1027
|
+
const repoEntries = await findRepoSessionEntries();
|
|
1028
|
+
const liveSessionKeys = new Set();
|
|
1029
|
+
|
|
1030
|
+
for (const entry of repoEntries) {
|
|
1031
|
+
for (const session of entry.sessions) {
|
|
1032
|
+
const sessionKey = resolveSessionWatcherKey(session);
|
|
1033
|
+
liveSessionKeys.add(sessionKey);
|
|
1034
|
+
if (this.sessionWatchers.has(sessionKey)) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const watcher = vscode.workspace.createFileSystemWatcher(
|
|
1039
|
+
resolveSessionGitIndexPath(session.worktreePath),
|
|
1040
|
+
);
|
|
1041
|
+
const disposables = bindRefreshWatcher(watcher, () => this.scheduleRefresh());
|
|
1042
|
+
this.sessionWatchers.set(sessionKey, { watcher, disposables });
|
|
285
1043
|
}
|
|
286
1044
|
}
|
|
287
1045
|
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (sessions.length > 0) {
|
|
292
|
-
repoEntries.push({
|
|
293
|
-
repoRoot,
|
|
294
|
-
sessions,
|
|
295
|
-
changes: readRepoChanges(repoRoot),
|
|
296
|
-
});
|
|
1046
|
+
for (const [sessionKey, entry] of this.sessionWatchers) {
|
|
1047
|
+
if (liveSessionKeys.has(sessionKey)) {
|
|
1048
|
+
continue;
|
|
297
1049
|
}
|
|
1050
|
+
|
|
1051
|
+
disposeAll(entry.disposables);
|
|
1052
|
+
entry.watcher.dispose();
|
|
1053
|
+
this.sessionWatchers.delete(sessionKey);
|
|
298
1054
|
}
|
|
1055
|
+
}
|
|
299
1056
|
|
|
300
|
-
|
|
301
|
-
|
|
1057
|
+
dispose() {
|
|
1058
|
+
if (this.refreshTimer) {
|
|
1059
|
+
clearTimeout(this.refreshTimer);
|
|
1060
|
+
this.refreshTimer = null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
for (const entry of this.sessionWatchers.values()) {
|
|
1064
|
+
disposeAll(entry.disposables);
|
|
1065
|
+
entry.watcher.dispose();
|
|
1066
|
+
}
|
|
1067
|
+
this.sessionWatchers.clear();
|
|
302
1068
|
}
|
|
303
1069
|
}
|
|
304
1070
|
|
|
305
1071
|
function activate(context) {
|
|
306
|
-
const
|
|
1072
|
+
const decorationProvider = new SessionDecorationProvider();
|
|
1073
|
+
const provider = new ActiveAgentsProvider(decorationProvider);
|
|
1074
|
+
const refreshController = new ActiveAgentsRefreshController(provider);
|
|
307
1075
|
const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
|
|
308
1076
|
treeDataProvider: provider,
|
|
309
1077
|
showCollapseAll: true,
|
|
310
1078
|
});
|
|
1079
|
+
const sourceControl = vscode.scm.createSourceControl(
|
|
1080
|
+
'gitguardex.activeAgents.commitInput',
|
|
1081
|
+
'Active Agents Commit',
|
|
1082
|
+
);
|
|
311
1083
|
provider.attachTreeView(treeView);
|
|
312
|
-
const
|
|
313
|
-
const
|
|
1084
|
+
const scheduleRefresh = () => refreshController.scheduleRefresh();
|
|
1085
|
+
const refresh = () => void refreshController.refreshNow();
|
|
1086
|
+
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
1087
|
+
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
1088
|
+
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
1089
|
+
const updateCommitInput = (session) => {
|
|
1090
|
+
sourceControl.inputBox.enabled = true;
|
|
1091
|
+
sourceControl.inputBox.visible = true;
|
|
1092
|
+
sourceControl.inputBox.placeholder = session?.label
|
|
1093
|
+
? `Commit ${session.label} (Ctrl+Enter)`
|
|
1094
|
+
: 'Pick an Active Agents session to commit its worktree.';
|
|
1095
|
+
};
|
|
1096
|
+
updateCommitInput(null);
|
|
1097
|
+
const commitSelectedSession = async () => {
|
|
1098
|
+
const selectedSession = provider.getSelectedSession();
|
|
1099
|
+
if (!selectedSession?.worktreePath) {
|
|
1100
|
+
vscode.window.showInformationMessage?.('Pick an Active Agents session first.');
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const message = String(sourceControl.inputBox.value || '').trim();
|
|
1105
|
+
if (!message) {
|
|
1106
|
+
vscode.window.showInformationMessage?.('Enter a commit message first.');
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (!fs.existsSync(selectedSession.worktreePath)) {
|
|
1111
|
+
vscode.window.showInformationMessage?.(
|
|
1112
|
+
`Selected session worktree is no longer on disk: ${selectedSession.worktreePath}`,
|
|
1113
|
+
);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
try {
|
|
1118
|
+
stageWorktreeForCommit(selectedSession.worktreePath);
|
|
1119
|
+
commitWorktree(selectedSession.worktreePath, message);
|
|
1120
|
+
sourceControl.inputBox.value = '';
|
|
1121
|
+
refresh();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
const failure = formatGitCommandFailure(error);
|
|
1124
|
+
if (/nothing to commit|no changes added to commit/i.test(failure)) {
|
|
1125
|
+
vscode.window.showInformationMessage?.(`No changes to commit in ${selectedSession.label}.`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
vscode.window.showErrorMessage?.(`Active Agents commit failed: ${failure}`);
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
sourceControl.acceptInputCommand = {
|
|
1132
|
+
command: 'gitguardex.activeAgents.commitSelectedSession',
|
|
1133
|
+
title: 'Commit Selected Session',
|
|
1134
|
+
};
|
|
314
1135
|
const interval = setInterval(refresh, 5_000);
|
|
1136
|
+
const refreshLockRegistry = (uri) => {
|
|
1137
|
+
if (uri?.fsPath) {
|
|
1138
|
+
provider.refreshLockRegistryForFile(uri.fsPath);
|
|
1139
|
+
}
|
|
1140
|
+
scheduleRefresh();
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
provider.onDidChangeSelectedSession(updateCommitInput);
|
|
315
1144
|
|
|
316
1145
|
context.subscriptions.push(
|
|
317
1146
|
treeView,
|
|
1147
|
+
sourceControl,
|
|
1148
|
+
refreshController,
|
|
1149
|
+
vscode.window.registerFileDecorationProvider(decorationProvider),
|
|
1150
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
|
|
318
1151
|
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
|
|
1152
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
|
|
319
1153
|
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
|
|
320
1154
|
if (!session?.worktreePath) {
|
|
321
1155
|
return;
|
|
@@ -339,14 +1173,23 @@ function activate(context) {
|
|
|
339
1173
|
|
|
340
1174
|
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
|
|
341
1175
|
}),
|
|
342
|
-
vscode.
|
|
343
|
-
|
|
1176
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
|
|
1177
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
|
|
1178
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
|
|
1179
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.openSessionDiff', openSessionDiff),
|
|
1180
|
+
vscode.workspace.onDidChangeWorkspaceFolders(scheduleRefresh),
|
|
1181
|
+
activeSessionsWatcher,
|
|
1182
|
+
lockWatcher,
|
|
1183
|
+
worktreeLockWatcher,
|
|
344
1184
|
{ dispose: () => clearInterval(interval) },
|
|
345
1185
|
);
|
|
346
1186
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
1187
|
+
context.subscriptions.push(
|
|
1188
|
+
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
1189
|
+
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
1190
|
+
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
1191
|
+
);
|
|
1192
|
+
void refreshController.refreshNow();
|
|
350
1193
|
}
|
|
351
1194
|
|
|
352
1195
|
function deactivate() {}
|