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