@imdeadpool/guardex 7.0.41 → 7.0.43
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 +68 -13
- package/package.json +2 -1
- package/skills/gitguardex/SKILL.md +13 -0
- package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
- package/src/agents/cleanup-sessions.js +126 -0
- package/src/agents/detect.js +160 -0
- package/src/agents/finish.js +172 -0
- package/src/agents/inspect.js +189 -0
- package/src/agents/launch.js +240 -0
- package/src/agents/registry.js +133 -0
- package/src/agents/selection-panel.js +571 -0
- package/src/agents/sessions.js +151 -0
- package/src/agents/start.js +591 -0
- package/src/agents/status.js +143 -0
- package/src/agents/terminal.js +152 -0
- package/src/budget/index.js +343 -0
- package/src/ci-init/index.js +265 -0
- package/src/cli/args.js +305 -1
- package/src/cli/main.js +262 -132
- package/src/cockpit/action-runner.js +3 -0
- package/src/cockpit/actions.js +80 -0
- package/src/cockpit/control.js +1121 -0
- package/src/cockpit/index.js +426 -0
- package/src/cockpit/keybindings.js +224 -0
- package/src/cockpit/kitty-layout.js +549 -0
- package/src/cockpit/kitty-tree.js +144 -0
- package/src/cockpit/layout.js +224 -0
- package/src/cockpit/logs-reader.js +182 -0
- package/src/cockpit/menu.js +204 -0
- package/src/cockpit/pane-actions.js +597 -0
- package/src/cockpit/pane-menu.js +387 -0
- package/src/cockpit/projects-finder.js +178 -0
- package/src/cockpit/render.js +215 -0
- package/src/cockpit/settings-render.js +128 -0
- package/src/cockpit/settings.js +124 -0
- package/src/cockpit/shortcuts.js +24 -0
- package/src/cockpit/sidebar.js +311 -0
- package/src/cockpit/state.js +72 -0
- package/src/cockpit/theme.js +128 -0
- package/src/cockpit/welcome.js +266 -0
- package/src/context.js +76 -33
- package/src/doctor/index.js +3 -2
- package/src/finish/index.js +39 -2
- package/src/git/index.js +65 -0
- package/src/kitty/command.js +101 -0
- package/src/kitty/runtime.js +250 -0
- package/src/output/index.js +1 -1
- package/src/pr-review.js +241 -0
- package/src/scaffold/index.js +19 -0
- package/src/submodule/index.js +288 -0
- package/src/terminal/index.js +120 -0
- package/src/terminal/kitty.js +622 -0
- package/src/terminal/tmux.js +126 -0
- package/src/tmux/command.js +27 -0
- package/src/tmux/session.js +89 -0
- package/templates/AGENTS.multiagent-safety.md +27 -1
- package/templates/codex/skills/gitguardex/SKILL.md +2 -0
- package/templates/githooks/pre-commit +22 -1
- package/templates/github/workflows/README.md +87 -0
- package/templates/github/workflows/ci-full.yml +55 -0
- package/templates/github/workflows/ci.yml +56 -0
- package/templates/github/workflows/cr.yml +20 -1
- package/templates/scripts/agent-branch-finish.sh +544 -26
- package/templates/scripts/agent-branch-start.sh +89 -22
- package/templates/scripts/agent-preflight.sh +89 -0
- package/templates/scripts/agent-worktree-prune.sh +96 -5
- package/templates/scripts/codex-agent.sh +41 -6
- package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
- package/templates/scripts/review-bot-watch.sh +31 -2
- package/templates/scripts/agent-session-state.js +0 -171
- package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
- package/templates/vscode/guardex-active-agents/README.md +0 -34
- package/templates/vscode/guardex-active-agents/extension.js +0 -3782
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
- package/templates/vscode/guardex-active-agents/package.json +0 -169
- package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
|
@@ -1,3782 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
const cp = require('node:child_process');
|
|
4
|
-
const http = require('node:http');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const vscode = require('vscode');
|
|
7
|
-
const {
|
|
8
|
-
clearWorktreeActivityCache,
|
|
9
|
-
formatElapsedFrom,
|
|
10
|
-
readActiveSessions,
|
|
11
|
-
readRepoChanges,
|
|
12
|
-
readSessionInspectData,
|
|
13
|
-
sanitizeBranchForFile,
|
|
14
|
-
sessionFilePathForBranch,
|
|
15
|
-
} = require('./session-schema.js');
|
|
16
|
-
|
|
17
|
-
const SESSION_DECORATION_SCHEME = 'gitguardex-agent';
|
|
18
|
-
const IDLE_WARNING_MS = 10 * 60 * 1000;
|
|
19
|
-
const IDLE_ERROR_MS = 30 * 60 * 1000;
|
|
20
|
-
const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
21
|
-
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
22
|
-
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
23
|
-
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
24
|
-
const MANAGED_WORKTREE_GIT_FILES_GLOB = '**/{.omx,.omc}/agent-worktrees/*/.git';
|
|
25
|
-
const MANAGED_WORKTREE_RELATIVE_ROOTS = [
|
|
26
|
-
path.join('.omx', 'agent-worktrees'),
|
|
27
|
-
path.join('.omc', 'agent-worktrees'),
|
|
28
|
-
];
|
|
29
|
-
const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log';
|
|
30
|
-
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
31
|
-
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
32
|
-
const MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB = '**/node_modules/**';
|
|
33
|
-
const SESSION_SCAN_LIMIT = 200;
|
|
34
|
-
const REFRESH_DEBOUNCE_MS = 250;
|
|
35
|
-
const RECENTLY_ACTIVE_WINDOW_MS = 10 * 60 * 1000;
|
|
36
|
-
const SESSION_TOP_FILE_COUNT = 3;
|
|
37
|
-
const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
|
|
38
|
-
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
|
|
39
|
-
const RELOAD_WINDOW_ACTION = 'Reload Window';
|
|
40
|
-
const UPDATE_LATER_ACTION = 'Later';
|
|
41
|
-
const ACTIVE_AGENTS_EXTENSION_ID = 'Recodee.gitguardex-active-agents';
|
|
42
|
-
const RESTART_EXTENSION_HOST_COMMAND = 'workbench.action.restartExtensionHost';
|
|
43
|
-
const REFRESH_POLL_INTERVAL_MS = 30_000;
|
|
44
|
-
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
|
|
45
|
-
const COLONY_DEFAULT_PORT = 37777;
|
|
46
|
-
const COLONY_SNAPSHOT_TTL_MS = 5_000;
|
|
47
|
-
const COLONY_FETCH_TIMEOUT_MS = 800;
|
|
48
|
-
|
|
49
|
-
function colonyDataDir() {
|
|
50
|
-
return process.env.COLONY_HOME
|
|
51
|
-
|| process.env.CAVEMEM_HOME
|
|
52
|
-
|| path.join(os.homedir(), '.colony');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readColonyPort() {
|
|
56
|
-
try {
|
|
57
|
-
const raw = fs.readFileSync(path.join(colonyDataDir(), 'settings.json'), 'utf8');
|
|
58
|
-
const parsed = JSON.parse(raw);
|
|
59
|
-
const port = Number(parsed?.workerPort);
|
|
60
|
-
return Number.isFinite(port) && port > 0 ? port : COLONY_DEFAULT_PORT;
|
|
61
|
-
} catch (_error) {
|
|
62
|
-
return COLONY_DEFAULT_PORT;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function fetchColonyJson(urlPath) {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const req = http.get(
|
|
69
|
-
{
|
|
70
|
-
hostname: '127.0.0.1',
|
|
71
|
-
port: readColonyPort(),
|
|
72
|
-
path: urlPath,
|
|
73
|
-
timeout: COLONY_FETCH_TIMEOUT_MS,
|
|
74
|
-
},
|
|
75
|
-
(res) => {
|
|
76
|
-
if (res.statusCode !== 200) {
|
|
77
|
-
res.resume();
|
|
78
|
-
resolve(null);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
let body = '';
|
|
82
|
-
res.setEncoding('utf8');
|
|
83
|
-
res.on('data', (chunk) => {
|
|
84
|
-
body += chunk;
|
|
85
|
-
});
|
|
86
|
-
res.on('end', () => {
|
|
87
|
-
try {
|
|
88
|
-
resolve(JSON.parse(body));
|
|
89
|
-
} catch (_error) {
|
|
90
|
-
resolve(null);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
req.on('error', () => resolve(null));
|
|
96
|
-
req.on('timeout', () => {
|
|
97
|
-
req.destroy();
|
|
98
|
-
resolve(null);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const colonyTasksCache = new Map();
|
|
104
|
-
|
|
105
|
-
async function readColonyTasksForRepo(repoRoot) {
|
|
106
|
-
const cached = colonyTasksCache.get(repoRoot);
|
|
107
|
-
if (cached && Date.now() - cached.at < COLONY_SNAPSHOT_TTL_MS) {
|
|
108
|
-
return cached.tasks;
|
|
109
|
-
}
|
|
110
|
-
const tasks = await fetchColonyJson(
|
|
111
|
-
`/api/colony/tasks?repo_root=${encodeURIComponent(repoRoot)}`,
|
|
112
|
-
);
|
|
113
|
-
const resolved = Array.isArray(tasks) ? tasks : [];
|
|
114
|
-
colonyTasksCache.set(repoRoot, { at: Date.now(), tasks: resolved });
|
|
115
|
-
return resolved;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function compactColonyBranchLabel(branch) {
|
|
119
|
-
if (typeof branch !== 'string' || !branch) return 'unknown';
|
|
120
|
-
const parts = branch.split('/').filter(Boolean);
|
|
121
|
-
return parts.length > 2 ? parts.slice(-2).join('/') : branch;
|
|
122
|
-
}
|
|
123
|
-
const GIT_CONFIGURATION_SECTION = 'git';
|
|
124
|
-
const REPO_SCAN_IGNORED_FOLDERS_SETTING = 'repositoryScanIgnoredFolders';
|
|
125
|
-
const BUNDLED_FILE_ICONS_MANIFEST_RELATIVE = path.join('fileicons', 'gitguardex-fileicons.json');
|
|
126
|
-
const MANAGED_REPO_SCAN_IGNORED_FOLDERS = [
|
|
127
|
-
'.omx/agent-worktrees',
|
|
128
|
-
'**/.omx/agent-worktrees',
|
|
129
|
-
'.omx/.tmp-worktrees',
|
|
130
|
-
'**/.omx/.tmp-worktrees',
|
|
131
|
-
'.omc/agent-worktrees',
|
|
132
|
-
'**/.omc/agent-worktrees',
|
|
133
|
-
'.omc/.tmp-worktrees',
|
|
134
|
-
'**/.omc/.tmp-worktrees',
|
|
135
|
-
];
|
|
136
|
-
const SESSION_ACTIVITY_GROUPS = [
|
|
137
|
-
{ kind: 'blocked', label: 'BLOCKED' },
|
|
138
|
-
{ kind: 'working', label: 'WORKING NOW' },
|
|
139
|
-
{ kind: 'finished', label: 'NEEDS CLEANUP' },
|
|
140
|
-
{ kind: 'idle', label: 'THINKING' },
|
|
141
|
-
{ kind: 'stalled', label: 'STALLED' },
|
|
142
|
-
{ kind: 'dead', label: 'DEAD' },
|
|
143
|
-
];
|
|
144
|
-
const SESSION_ACTIVITY_ICON_IDS = {
|
|
145
|
-
blocked: 'warning',
|
|
146
|
-
working: 'loading~spin',
|
|
147
|
-
finished: 'pass-filled',
|
|
148
|
-
idle: 'comment-discussion',
|
|
149
|
-
stalled: 'clock',
|
|
150
|
-
dead: 'error',
|
|
151
|
-
};
|
|
152
|
-
const DISMISSABLE_SESSION_ACTIVITY_KINDS = new Set(['stalled', 'dead']);
|
|
153
|
-
const SESSION_PROVIDER_BRANDS = {
|
|
154
|
-
openai: {
|
|
155
|
-
id: 'openai',
|
|
156
|
-
label: 'OpenAI',
|
|
157
|
-
badge: 'AI',
|
|
158
|
-
},
|
|
159
|
-
claude: {
|
|
160
|
-
id: 'claude',
|
|
161
|
-
label: 'Claude',
|
|
162
|
-
badge: 'CL',
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
let bundledTreeIconThemeCache = null;
|
|
166
|
-
|
|
167
|
-
function iconColorId(iconId) {
|
|
168
|
-
switch (iconId) {
|
|
169
|
-
case 'warning':
|
|
170
|
-
case 'clock':
|
|
171
|
-
return 'list.warningForeground';
|
|
172
|
-
case 'error':
|
|
173
|
-
return 'list.errorForeground';
|
|
174
|
-
case 'loading~spin':
|
|
175
|
-
return 'gitDecoration.addedResourceForeground';
|
|
176
|
-
case 'comment-discussion':
|
|
177
|
-
case 'info':
|
|
178
|
-
case 'repo':
|
|
179
|
-
case 'folder':
|
|
180
|
-
case 'graph':
|
|
181
|
-
case 'history':
|
|
182
|
-
case 'dashboard':
|
|
183
|
-
case 'inbox':
|
|
184
|
-
case 'file-directory':
|
|
185
|
-
case 'settings-gear':
|
|
186
|
-
case 'folder-library':
|
|
187
|
-
return 'textLink.foreground';
|
|
188
|
-
case 'git-branch':
|
|
189
|
-
return 'gitDecoration.modifiedResourceForeground';
|
|
190
|
-
case 'account':
|
|
191
|
-
return 'terminal.ansiYellow';
|
|
192
|
-
case 'debug-pause':
|
|
193
|
-
return 'terminal.ansiYellow';
|
|
194
|
-
case 'sparkle':
|
|
195
|
-
case 'rocket':
|
|
196
|
-
return 'terminal.ansiMagenta';
|
|
197
|
-
case 'list-flat':
|
|
198
|
-
case 'device-camera':
|
|
199
|
-
return 'terminal.ansiCyan';
|
|
200
|
-
case 'list-tree':
|
|
201
|
-
case 'telescope':
|
|
202
|
-
return 'terminal.ansiBlue';
|
|
203
|
-
case 'organization':
|
|
204
|
-
return 'terminal.ansiGreen';
|
|
205
|
-
case 'pass-filled':
|
|
206
|
-
case 'pass':
|
|
207
|
-
case 'check':
|
|
208
|
-
return 'testing.iconPassed';
|
|
209
|
-
default:
|
|
210
|
-
return '';
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function themeIcon(iconId, colorId = iconColorId(iconId)) {
|
|
215
|
-
if (!iconId) {
|
|
216
|
-
return undefined;
|
|
217
|
-
}
|
|
218
|
-
return colorId
|
|
219
|
-
? new vscode.ThemeIcon(iconId, new vscode.ThemeColor(colorId))
|
|
220
|
-
: new vscode.ThemeIcon(iconId);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function sessionDecorationUri(branch) {
|
|
224
|
-
return vscode.Uri.parse(`${SESSION_DECORATION_SCHEME}://${sanitizeBranchForFile(branch)}`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function emptyBundledTreeIconTheme() {
|
|
228
|
-
return {
|
|
229
|
-
iconPathById: new Map(),
|
|
230
|
-
fileNames: {},
|
|
231
|
-
folderNames: {},
|
|
232
|
-
fileExtensions: {},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function loadBundledTreeIconTheme() {
|
|
237
|
-
if (bundledTreeIconThemeCache) {
|
|
238
|
-
return bundledTreeIconThemeCache;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const manifestPath = path.join(__dirname, BUNDLED_FILE_ICONS_MANIFEST_RELATIVE);
|
|
242
|
-
try {
|
|
243
|
-
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
244
|
-
const manifestDir = path.dirname(manifestPath);
|
|
245
|
-
const iconPathById = new Map();
|
|
246
|
-
for (const [iconId, definition] of Object.entries(parsed?.iconDefinitions || {})) {
|
|
247
|
-
if (typeof definition?.iconPath !== 'string' || !definition.iconPath.trim()) {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
const iconUri = vscode.Uri.file(path.resolve(manifestDir, definition.iconPath));
|
|
251
|
-
iconPathById.set(iconId, {
|
|
252
|
-
light: iconUri,
|
|
253
|
-
dark: iconUri,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
bundledTreeIconThemeCache = {
|
|
257
|
-
iconPathById,
|
|
258
|
-
fileNames: parsed?.fileNames || {},
|
|
259
|
-
folderNames: parsed?.folderNames || {},
|
|
260
|
-
fileExtensions: parsed?.fileExtensions || {},
|
|
261
|
-
};
|
|
262
|
-
} catch (_error) {
|
|
263
|
-
bundledTreeIconThemeCache = emptyBundledTreeIconTheme();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return bundledTreeIconThemeCache;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function resolveBundledTreeItemIconId(relativePath, kind = 'file') {
|
|
270
|
-
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
271
|
-
const entryName = path.posix.basename(normalizedRelativePath || '');
|
|
272
|
-
if (!entryName) {
|
|
273
|
-
return '';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const bundledTheme = loadBundledTreeIconTheme();
|
|
277
|
-
if (kind === 'folder') {
|
|
278
|
-
return bundledTheme.folderNames[entryName] || '';
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (bundledTheme.fileNames[entryName]) {
|
|
282
|
-
return bundledTheme.fileNames[entryName];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const matchingExtension = Object.keys(bundledTheme.fileExtensions)
|
|
286
|
-
.sort((left, right) => right.length - left.length)
|
|
287
|
-
.find((extension) => entryName === extension || entryName.endsWith(`.${extension}`));
|
|
288
|
-
return matchingExtension ? bundledTheme.fileExtensions[matchingExtension] : '';
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function resolveBundledTreeItemIcon(relativePath, kind = 'file') {
|
|
292
|
-
const bundledTheme = loadBundledTreeIconTheme();
|
|
293
|
-
const iconId = resolveBundledTreeItemIconId(relativePath, kind);
|
|
294
|
-
return iconId ? bundledTheme.iconPathById.get(iconId) : undefined;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function sessionIdleDecoration(session, now = Date.now()) {
|
|
298
|
-
if (!session) {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (session.activityKind === 'blocked') {
|
|
303
|
-
return {
|
|
304
|
-
badge: '!',
|
|
305
|
-
tooltip: 'blocked',
|
|
306
|
-
color: new vscode.ThemeColor('list.warningForeground'),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
if (session.activityKind === 'dead') {
|
|
310
|
-
return {
|
|
311
|
-
badge: 'x',
|
|
312
|
-
tooltip: 'dead',
|
|
313
|
-
color: new vscode.ThemeColor('list.errorForeground'),
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
if (session.activityKind === 'stalled') {
|
|
317
|
-
return {
|
|
318
|
-
badge: '!',
|
|
319
|
-
tooltip: 'stalled',
|
|
320
|
-
color: new vscode.ThemeColor('list.errorForeground'),
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
if (session.activityKind === 'working') {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const startedAtMs = Date.parse(session.startedAt);
|
|
328
|
-
if (!Number.isFinite(startedAtMs)) {
|
|
329
|
-
return undefined;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const elapsedMs = now - startedAtMs;
|
|
333
|
-
if (elapsedMs > IDLE_ERROR_MS) {
|
|
334
|
-
return {
|
|
335
|
-
badge: '30m+',
|
|
336
|
-
tooltip: 'idle 30m+',
|
|
337
|
-
color: new vscode.ThemeColor('list.errorForeground'),
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
if (elapsedMs > IDLE_WARNING_MS) {
|
|
341
|
-
return {
|
|
342
|
-
badge: '10m+',
|
|
343
|
-
tooltip: 'idle 10m+',
|
|
344
|
-
color: new vscode.ThemeColor('list.warningForeground'),
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
return undefined;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function formatCountLabel(count, singular, plural = `${singular}s`) {
|
|
352
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function branchSegments(branch) {
|
|
356
|
-
return String(branch || '')
|
|
357
|
-
.split('/')
|
|
358
|
-
.map((segment) => segment.trim())
|
|
359
|
-
.filter(Boolean);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function compactBranchLabel(branch) {
|
|
363
|
-
const segments = branchSegments(branch);
|
|
364
|
-
if (segments.length >= 3 && segments[0] === 'agent') {
|
|
365
|
-
return `${segments[1]}/${segments.slice(2).join('/')}`;
|
|
366
|
-
}
|
|
367
|
-
return segments.join('/');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function sessionFileCountLabel(session) {
|
|
371
|
-
const activityCountLabel = typeof session?.activityCountLabel === 'string'
|
|
372
|
-
? session.activityCountLabel.trim()
|
|
373
|
-
: '';
|
|
374
|
-
if (activityCountLabel) {
|
|
375
|
-
return activityCountLabel;
|
|
376
|
-
}
|
|
377
|
-
if ((session?.changeCount || 0) > 0) {
|
|
378
|
-
return formatCountLabel(session.changeCount, 'file');
|
|
379
|
-
}
|
|
380
|
-
return '';
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function uniqueStringList(values) {
|
|
384
|
-
const seen = new Set();
|
|
385
|
-
const result = [];
|
|
386
|
-
|
|
387
|
-
for (const value of values) {
|
|
388
|
-
if (typeof value !== 'string' || seen.has(value)) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
seen.add(value);
|
|
392
|
-
result.push(value);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return result;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function normalizeSessionProviderToken(value) {
|
|
399
|
-
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function resolveSessionProvider(session) {
|
|
403
|
-
const signals = [
|
|
404
|
-
session?.cliName,
|
|
405
|
-
session?.agentName,
|
|
406
|
-
session?.branch,
|
|
407
|
-
]
|
|
408
|
-
.map(normalizeSessionProviderToken)
|
|
409
|
-
.filter(Boolean);
|
|
410
|
-
|
|
411
|
-
if (signals.some((value) => value.includes('claude'))) {
|
|
412
|
-
return {
|
|
413
|
-
...SESSION_PROVIDER_BRANDS.claude,
|
|
414
|
-
cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
if (signals.some((value) => value.includes('codex') || value.includes('openai'))) {
|
|
418
|
-
return {
|
|
419
|
-
...SESSION_PROVIDER_BRANDS.openai,
|
|
420
|
-
cliName: typeof session?.cliName === 'string' ? session.cliName.trim() : '',
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function sessionProviderDecoration(session) {
|
|
427
|
-
const provider = resolveSessionProvider(session);
|
|
428
|
-
if (!provider) {
|
|
429
|
-
return undefined;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const cliName = provider.cliName || provider.id;
|
|
433
|
-
return {
|
|
434
|
-
badge: provider.badge,
|
|
435
|
-
tooltip: `${provider.label} session via ${cliName}`,
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function normalizeSnapshotIdentityValue(value) {
|
|
440
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function sessionSnapshotDisplayName(session) {
|
|
444
|
-
return normalizeSnapshotIdentityValue(session?.snapshotName)
|
|
445
|
-
|| normalizeSnapshotIdentityValue(session?.snapshotEmail);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function sessionSnapshotBadge(session) {
|
|
449
|
-
const displayName = sessionSnapshotDisplayName(session);
|
|
450
|
-
const match = displayName.match(/[a-z0-9]/i);
|
|
451
|
-
return match ? match[0].toUpperCase() : '';
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function sessionSnapshotDescription(session) {
|
|
455
|
-
const displayName = sessionSnapshotDisplayName(session);
|
|
456
|
-
return displayName ? `snapshot ${displayName}` : '';
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function sessionSnapshotDecoration(session) {
|
|
460
|
-
const badge = sessionSnapshotBadge(session);
|
|
461
|
-
const displayName = sessionSnapshotDisplayName(session);
|
|
462
|
-
if (!badge || !displayName) {
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
badge,
|
|
468
|
-
tooltip: `Snapshot ${displayName}`,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function sessionIdentityDecoration(session) {
|
|
473
|
-
return sessionSnapshotDecoration(session) || sessionProviderDecoration(session);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function stringListsEqual(left, right) {
|
|
477
|
-
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return left.every((value, index) => value === right[index]);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
async function ensureManagedRepoScanIgnores() {
|
|
485
|
-
if (typeof vscode.workspace.getConfiguration !== 'function') {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
|
490
|
-
if (workspaceFolders.length === 0) {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const workspaceFolderTarget = workspaceFolders.length > 1
|
|
495
|
-
? vscode.ConfigurationTarget?.WorkspaceFolder
|
|
496
|
-
: vscode.ConfigurationTarget?.Workspace;
|
|
497
|
-
if (workspaceFolderTarget === undefined) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
for (const workspaceFolder of workspaceFolders) {
|
|
502
|
-
const gitConfig = vscode.workspace.getConfiguration(GIT_CONFIGURATION_SECTION, workspaceFolder);
|
|
503
|
-
const configuredIgnoredFolders = gitConfig.get(REPO_SCAN_IGNORED_FOLDERS_SETTING);
|
|
504
|
-
const existingIgnoredFolders = Array.isArray(configuredIgnoredFolders)
|
|
505
|
-
? configuredIgnoredFolders
|
|
506
|
-
: [];
|
|
507
|
-
const nextIgnoredFolders = uniqueStringList([
|
|
508
|
-
...existingIgnoredFolders,
|
|
509
|
-
...MANAGED_REPO_SCAN_IGNORED_FOLDERS,
|
|
510
|
-
]);
|
|
511
|
-
|
|
512
|
-
if (stringListsEqual(existingIgnoredFolders, nextIgnoredFolders)) {
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
await gitConfig.update(
|
|
518
|
-
REPO_SCAN_IGNORED_FOLDERS_SETTING,
|
|
519
|
-
nextIgnoredFolders,
|
|
520
|
-
workspaceFolderTarget,
|
|
521
|
-
);
|
|
522
|
-
} catch {
|
|
523
|
-
// Leave the extension usable even when the current workspace settings cannot be updated.
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function sessionIdentityLabel(session) {
|
|
529
|
-
const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
|
|
530
|
-
const taskName = sessionDisplayLabel(session);
|
|
531
|
-
const label = typeof session?.label === 'string' ? session.label.trim() : '';
|
|
532
|
-
|
|
533
|
-
if (agentName && taskName) {
|
|
534
|
-
return `${agentName} · ${taskName}`;
|
|
535
|
-
}
|
|
536
|
-
if (agentName && label) {
|
|
537
|
-
return `${agentName} · ${label}`;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return agentName || taskName || label || 'session';
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function sessionCommitPlaceholder(session) {
|
|
544
|
-
if (!session?.branch) {
|
|
545
|
-
return 'Pick an Active Agents session to commit its worktree.';
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')}`;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function agentNameFromBranch(branch) {
|
|
552
|
-
const segments = String(branch || '')
|
|
553
|
-
.split('/')
|
|
554
|
-
.map((segment) => segment.trim())
|
|
555
|
-
.filter(Boolean);
|
|
556
|
-
if (segments[0] === 'agent' && segments[1]) {
|
|
557
|
-
return segments[1];
|
|
558
|
-
}
|
|
559
|
-
return segments[0] || 'lock';
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
function agentBadgeFromBranch(branch) {
|
|
563
|
-
const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
564
|
-
return normalized.slice(0, 2) || 'LK';
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function buildActiveAgentsStatusSummary(summary) {
|
|
568
|
-
const workingCount = summary?.workingCount || 0;
|
|
569
|
-
const finishedCount = summary?.finishedCount || 0;
|
|
570
|
-
const idleCount = summary?.idleCount || 0;
|
|
571
|
-
if (workingCount > 0 || finishedCount > 0 || idleCount > 0) {
|
|
572
|
-
const parts = [`${workingCount} working`];
|
|
573
|
-
if (finishedCount > 0) {
|
|
574
|
-
parts.push(`${finishedCount} needs cleanup`);
|
|
575
|
-
}
|
|
576
|
-
parts.push(`${idleCount} idle`);
|
|
577
|
-
return `$(git-branch) ${parts.join(' · ')}`;
|
|
578
|
-
}
|
|
579
|
-
return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
583
|
-
if (selectedSession?.branch) {
|
|
584
|
-
return [
|
|
585
|
-
selectedSession.branch,
|
|
586
|
-
sessionIdentityLabel(selectedSession),
|
|
587
|
-
formatCountLabel(selectedSession.lockCount || 0, 'lock'),
|
|
588
|
-
selectedSession.worktreePath,
|
|
589
|
-
'Click to open Active Agents.',
|
|
590
|
-
].filter(Boolean).join('\n');
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
|
|
594
|
-
return [
|
|
595
|
-
formatCountLabel(activeCount, 'active agent'),
|
|
596
|
-
formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
|
|
597
|
-
formatCountLabel(summary?.finishedCount || 0, 'needs cleanup session'),
|
|
598
|
-
formatCountLabel(summary?.idleCount || 0, 'idle session'),
|
|
599
|
-
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
600
|
-
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
601
|
-
summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
|
|
602
|
-
'Click to open Active Agents.',
|
|
603
|
-
].filter(Boolean).join('\n');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function compactRelativePath(relativePath) {
|
|
607
|
-
const normalized = normalizeRelativePath(relativePath);
|
|
608
|
-
if (!normalized) {
|
|
609
|
-
return '';
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const segments = normalized.split('/').filter(Boolean);
|
|
613
|
-
if (segments.length <= 2) {
|
|
614
|
-
return normalized;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return `${segments[0]}/.../${segments[segments.length - 1]}`;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function summarizeCompactPaths(paths, maxCount = SESSION_TOP_FILE_COUNT) {
|
|
621
|
-
const compactPaths = uniqueStringList((paths || [])
|
|
622
|
-
.map(normalizeRelativePath)
|
|
623
|
-
.filter(Boolean)
|
|
624
|
-
.map((relativePath) => compactRelativePath(relativePath)))
|
|
625
|
-
.slice(0, maxCount);
|
|
626
|
-
if (compactPaths.length === 0) {
|
|
627
|
-
return '';
|
|
628
|
-
}
|
|
629
|
-
return compactPaths.join(', ');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function isProtectedBranchName(branch) {
|
|
633
|
-
return branch === 'main' || branch === 'dev';
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function countWorkingSessions(sessions) {
|
|
637
|
-
return sessions.filter((session) => (
|
|
638
|
-
session.activityKind === 'working' || session.activityKind === 'blocked'
|
|
639
|
-
)).length;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function countFinishedSessions(sessions) {
|
|
643
|
-
return sessions.filter((session) => session.activityKind === 'finished').length;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function countIdleSessions(sessions) {
|
|
647
|
-
return sessions.filter((session) => (
|
|
648
|
-
session.activityKind === 'idle' || session.activityKind === 'stalled'
|
|
649
|
-
)).length;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function sessionLastActiveAt(session) {
|
|
653
|
-
return [
|
|
654
|
-
session?.lastHeartbeatAt,
|
|
655
|
-
session?.lastFileActivityAt,
|
|
656
|
-
session?.telemetryUpdatedAt,
|
|
657
|
-
session?.startedAt,
|
|
658
|
-
].find((value) => typeof value === 'string' && value.trim().length > 0) || '';
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function sessionLastActiveLabel(session) {
|
|
662
|
-
const lastActiveAt = sessionLastActiveAt(session);
|
|
663
|
-
if (!lastActiveAt) {
|
|
664
|
-
return '';
|
|
665
|
-
}
|
|
666
|
-
return formatElapsedFrom(lastActiveAt);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function sessionLastActiveAgeMs(session, now = Date.now()) {
|
|
670
|
-
const lastActiveAt = sessionLastActiveAt(session);
|
|
671
|
-
const timestamp = Date.parse(lastActiveAt);
|
|
672
|
-
if (!Number.isFinite(timestamp)) {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
return Math.max(0, now - timestamp);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function sessionFreshnessLabel(session, now = Date.now()) {
|
|
679
|
-
const ageMs = sessionLastActiveAgeMs(session, now);
|
|
680
|
-
if (session.activityKind === 'blocked') {
|
|
681
|
-
return 'Needs attention';
|
|
682
|
-
}
|
|
683
|
-
if (session.activityKind === 'finished') {
|
|
684
|
-
return 'Needs cleanup';
|
|
685
|
-
}
|
|
686
|
-
if (session.activityKind === 'stalled') {
|
|
687
|
-
return 'Possibly stale';
|
|
688
|
-
}
|
|
689
|
-
if (session.activityKind === 'dead') {
|
|
690
|
-
return 'Stopped';
|
|
691
|
-
}
|
|
692
|
-
if (ageMs === null) {
|
|
693
|
-
return '';
|
|
694
|
-
}
|
|
695
|
-
if (ageMs <= IDLE_WARNING_MS) {
|
|
696
|
-
return 'Fresh';
|
|
697
|
-
}
|
|
698
|
-
if (ageMs <= RECENTLY_ACTIVE_WINDOW_MS) {
|
|
699
|
-
return 'Recently active';
|
|
700
|
-
}
|
|
701
|
-
if (session.activityKind === 'idle') {
|
|
702
|
-
return 'Idle';
|
|
703
|
-
}
|
|
704
|
-
return 'Recently active';
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function sessionStatusLabel(session) {
|
|
708
|
-
switch (session.activityKind) {
|
|
709
|
-
case 'blocked':
|
|
710
|
-
return 'Blocked';
|
|
711
|
-
case 'working':
|
|
712
|
-
return 'Working';
|
|
713
|
-
case 'finished':
|
|
714
|
-
return 'Needs cleanup';
|
|
715
|
-
case 'idle':
|
|
716
|
-
return 'Idle';
|
|
717
|
-
case 'stalled':
|
|
718
|
-
return 'Stale';
|
|
719
|
-
case 'dead':
|
|
720
|
-
return 'Dead';
|
|
721
|
-
default:
|
|
722
|
-
return 'Thinking';
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function sessionHealthScore(session) {
|
|
727
|
-
return Number.isInteger(session?.sessionHealth?.score) ? session.sessionHealth.score : null;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function buildSessionHealthCompactLabel(session) {
|
|
731
|
-
const score = sessionHealthScore(session);
|
|
732
|
-
return score === null ? '' : `${score}/100`;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function buildSessionHealthSummary(session) {
|
|
736
|
-
const compactLabel = buildSessionHealthCompactLabel(session);
|
|
737
|
-
if (!compactLabel) {
|
|
738
|
-
return '';
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const label = typeof session?.sessionHealth?.label === 'string'
|
|
742
|
-
? session.sessionHealth.label.trim()
|
|
743
|
-
: '';
|
|
744
|
-
return label ? `${compactLabel} · ${label}` : compactLabel;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function buildSessionHealthDriversSummary(session) {
|
|
748
|
-
const primaryDriver = typeof session?.sessionHealth?.primaryDriver === 'string'
|
|
749
|
-
? session.sessionHealth.primaryDriver.trim()
|
|
750
|
-
: '';
|
|
751
|
-
const secondaries = uniqueStringList(Array.isArray(session?.sessionHealth?.secondaries)
|
|
752
|
-
? session.sessionHealth.secondaries.map((value) => String(value || '').trim())
|
|
753
|
-
: []);
|
|
754
|
-
return [
|
|
755
|
-
primaryDriver ? `Primary: ${primaryDriver}` : '',
|
|
756
|
-
secondaries.length > 0 ? `Secondary: ${secondaries.join(', ')}` : '',
|
|
757
|
-
].filter(Boolean).join(' | ');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function buildSessionHealthTooltip(session) {
|
|
761
|
-
const outputLine = typeof session?.sessionHealth?.outputLine === 'string'
|
|
762
|
-
? session.sessionHealth.outputLine.trim()
|
|
763
|
-
: '';
|
|
764
|
-
if (outputLine) {
|
|
765
|
-
return outputLine;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
return [
|
|
769
|
-
buildSessionHealthSummary(session),
|
|
770
|
-
buildSessionHealthDriversSummary(session),
|
|
771
|
-
].filter(Boolean).join('\n');
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function buildSessionTopFiles(session) {
|
|
775
|
-
return uniqueStringList((session?.worktreeChangedPaths || [])
|
|
776
|
-
.map(normalizeRelativePath)
|
|
777
|
-
.filter(Boolean))
|
|
778
|
-
.slice(0, SESSION_TOP_FILE_COUNT);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function buildSessionRecentChangeSummary(session) {
|
|
782
|
-
if (session?.latestTaskPreview && session.latestTaskPreview !== session.taskName) {
|
|
783
|
-
return session.latestTaskPreview;
|
|
784
|
-
}
|
|
785
|
-
const topFiles = summarizeCompactPaths(session?.worktreeChangedPaths || []);
|
|
786
|
-
if (topFiles) {
|
|
787
|
-
return `Changed ${topFiles}`;
|
|
788
|
-
}
|
|
789
|
-
if (session?.activitySummary) {
|
|
790
|
-
return session.activitySummary;
|
|
791
|
-
}
|
|
792
|
-
return 'No recent change summary.';
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function sessionRiskBadges(session) {
|
|
796
|
-
return uniqueStringList([
|
|
797
|
-
session?.activityKind === 'blocked' ? 'Blocked' : '',
|
|
798
|
-
session?.activityKind === 'stalled' ? 'Stale' : '',
|
|
799
|
-
session?.conflictCount > 0 ? 'Conflict' : '',
|
|
800
|
-
session?.lockCount > 0 ? 'Locked' : '',
|
|
801
|
-
].filter(Boolean));
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
function changeRiskBadges(change) {
|
|
805
|
-
return uniqueStringList([
|
|
806
|
-
change?.protectedBranch ? 'Protected branch' : '',
|
|
807
|
-
change?.hasForeignLock ? 'Conflict' : '',
|
|
808
|
-
!change?.hasForeignLock && change?.lockOwnerBranch ? 'Locked' : '',
|
|
809
|
-
change?.deltaLabel || '',
|
|
810
|
-
].filter(Boolean));
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function changeNeedsWarningIcon(change) {
|
|
814
|
-
return Boolean(
|
|
815
|
-
change?.protectedBranch
|
|
816
|
-
|| change?.hasForeignLock
|
|
817
|
-
|| (!change?.hasForeignLock && change?.lockOwnerBranch),
|
|
818
|
-
);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function buildSessionCardDescription(session) {
|
|
822
|
-
const provider = resolveSessionProvider(session);
|
|
823
|
-
const statusAgentLabel = `${sessionStatusLabel(session)}: ${session.agentName || 'agent'}`;
|
|
824
|
-
const descriptionParts = [
|
|
825
|
-
statusAgentLabel,
|
|
826
|
-
provider?.label ? `via ${provider.label}` : '',
|
|
827
|
-
sessionSnapshotDescription(session),
|
|
828
|
-
session.deltaLabel || '',
|
|
829
|
-
session.changeCount > 0 ? formatCountLabel(session.changeCount, 'changed file') : '',
|
|
830
|
-
session.lockCount > 0 ? formatCountLabel(session.lockCount, 'lock') : '',
|
|
831
|
-
buildSessionHealthCompactLabel(session),
|
|
832
|
-
session.freshnessLabel || '',
|
|
833
|
-
session.lastActiveLabel ? `${session.lastActiveLabel} ago` : '',
|
|
834
|
-
].filter(Boolean);
|
|
835
|
-
return descriptionParts.join(' · ');
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function buildRawSessionDescription(session) {
|
|
839
|
-
const provider = resolveSessionProvider(session);
|
|
840
|
-
const descriptionParts = [sessionStatusLabel(session)];
|
|
841
|
-
const fileCountLabel = sessionFileCountLabel(session);
|
|
842
|
-
if (fileCountLabel) {
|
|
843
|
-
descriptionParts.push(fileCountLabel);
|
|
844
|
-
}
|
|
845
|
-
if (provider?.label) {
|
|
846
|
-
descriptionParts.push(provider.label);
|
|
847
|
-
}
|
|
848
|
-
const snapshot = sessionSnapshotDescription(session);
|
|
849
|
-
if (snapshot) {
|
|
850
|
-
descriptionParts.push(snapshot);
|
|
851
|
-
}
|
|
852
|
-
descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
|
|
853
|
-
const sessionHealthLabel = buildSessionHealthCompactLabel(session);
|
|
854
|
-
if (sessionHealthLabel) {
|
|
855
|
-
descriptionParts.push(sessionHealthLabel);
|
|
856
|
-
}
|
|
857
|
-
if (session.lockCount > 0) {
|
|
858
|
-
descriptionParts.push(formatCountLabel(session.lockCount, 'lock'));
|
|
859
|
-
}
|
|
860
|
-
return descriptionParts.join(' · ');
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function buildSessionTooltip(session, description) {
|
|
864
|
-
const provider = resolveSessionProvider(session);
|
|
865
|
-
const riskSummary = uniqueStringList([
|
|
866
|
-
...(session?.riskBadges || []),
|
|
867
|
-
session?.deltaLabel || '',
|
|
868
|
-
].filter(Boolean)).join(', ');
|
|
869
|
-
const topFiles = session?.topChangedFilesLabel || summarizeCompactPaths(session?.worktreeChangedPaths || []);
|
|
870
|
-
const sessionHealthSummary = buildSessionHealthSummary(session);
|
|
871
|
-
const sessionHealthDrivers = buildSessionHealthDriversSummary(session);
|
|
872
|
-
return [
|
|
873
|
-
session.branch,
|
|
874
|
-
provider?.label
|
|
875
|
-
? `Provider ${provider.label}${provider.cliName ? ` (${provider.cliName})` : ''}`
|
|
876
|
-
: '',
|
|
877
|
-
sessionSnapshotDisplayName(session) ? `Snapshot ${sessionSnapshotDisplayName(session)}` : '',
|
|
878
|
-
`${session.agentName} · ${sessionDisplayLabel(session)}`,
|
|
879
|
-
`Status ${description}`,
|
|
880
|
-
sessionHealthSummary ? `Session health ${sessionHealthSummary}` : '',
|
|
881
|
-
sessionHealthDrivers ? `Drivers ${sessionHealthDrivers}` : '',
|
|
882
|
-
session.recentChangeSummary ? `Recent ${session.recentChangeSummary}` : '',
|
|
883
|
-
topFiles ? `Top files ${topFiles}` : '',
|
|
884
|
-
riskSummary ? `Signals ${riskSummary}` : '',
|
|
885
|
-
session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
|
|
886
|
-
session.lastActiveAt ? `Last active ${session.lastActiveAt}` : '',
|
|
887
|
-
session.sourceKind === 'worktree-lock'
|
|
888
|
-
? `Telemetry updated ${session.telemetryUpdatedAt || session.startedAt}`
|
|
889
|
-
: `Started ${session.startedAt}`,
|
|
890
|
-
session.worktreePath,
|
|
891
|
-
].filter(Boolean).join('\n');
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function buildUnassignedChangeDescription(change) {
|
|
895
|
-
return [
|
|
896
|
-
change.statusLabel,
|
|
897
|
-
...changeRiskBadges(change),
|
|
898
|
-
].filter(Boolean).join(' · ');
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function buildWorktreeBranchDescription(sessions) {
|
|
902
|
-
const sessionList = Array.isArray(sessions) ? sessions : [];
|
|
903
|
-
const primarySession = sessionList[0] || null;
|
|
904
|
-
if (!primarySession) {
|
|
905
|
-
return '';
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const descriptionParts = [
|
|
909
|
-
`${sessionStatusLabel(primarySession).toLowerCase()}: ${primarySession.agentName || 'agent'}`,
|
|
910
|
-
sessionSnapshotDescription(primarySession),
|
|
911
|
-
];
|
|
912
|
-
if (sessionList.length > 1) {
|
|
913
|
-
descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
|
|
914
|
-
}
|
|
915
|
-
return descriptionParts.filter(Boolean).join(' · ');
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function buildOverviewDescription(summary) {
|
|
919
|
-
return [
|
|
920
|
-
formatCountLabel(summary?.workingCount || 0, 'working agent'),
|
|
921
|
-
formatCountLabel(summary?.finishedCount || 0, 'needs cleanup agent'),
|
|
922
|
-
formatCountLabel(summary?.idleCount || 0, 'idle agent'),
|
|
923
|
-
summary?.colonyTaskCount
|
|
924
|
-
? formatCountLabel(summary.colonyTaskCount, 'colony task')
|
|
925
|
-
: '',
|
|
926
|
-
summary?.pendingHandoffCount
|
|
927
|
-
? formatCountLabel(summary.pendingHandoffCount, 'pending handoff')
|
|
928
|
-
: '',
|
|
929
|
-
formatCountLabel(summary?.unassignedChangeCount || 0, 'unassigned change'),
|
|
930
|
-
formatCountLabel(summary?.lockedFileCount || 0, 'locked file'),
|
|
931
|
-
formatCountLabel(summary?.conflictCount || 0, 'conflict'),
|
|
932
|
-
]
|
|
933
|
-
.filter(Boolean)
|
|
934
|
-
.join(' · ');
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
function buildRepoDescription(summary) {
|
|
938
|
-
return buildOverviewDescription(summary);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function buildRepoTooltip(repoRoot, summary) {
|
|
942
|
-
return [
|
|
943
|
-
repoRoot,
|
|
944
|
-
buildOverviewDescription(summary),
|
|
945
|
-
].join('\n');
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function repoRootDisplayLabel(repoRoot) {
|
|
949
|
-
const normalizedRepoRoot = path.resolve(repoRoot);
|
|
950
|
-
const matchingWorkspaceRoots = (vscode.workspace.workspaceFolders || [])
|
|
951
|
-
.map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : ''))
|
|
952
|
-
.filter((workspaceRoot) => workspaceRoot && isPathWithin(workspaceRoot, normalizedRepoRoot))
|
|
953
|
-
.sort((left, right) => right.length - left.length);
|
|
954
|
-
|
|
955
|
-
const workspaceRoot = matchingWorkspaceRoots[0];
|
|
956
|
-
if (!workspaceRoot) {
|
|
957
|
-
return path.basename(normalizedRepoRoot);
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const workspaceLabel = path.basename(workspaceRoot);
|
|
961
|
-
const relativePath = normalizeRelativePath(path.relative(workspaceRoot, normalizedRepoRoot));
|
|
962
|
-
if (!relativePath) {
|
|
963
|
-
return workspaceLabel;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return [
|
|
967
|
-
workspaceLabel,
|
|
968
|
-
...relativePath.split('/').filter(Boolean),
|
|
969
|
-
].join('/');
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
function sessionSnapshotKey(session) {
|
|
973
|
-
return `${session?.repoRoot || ''}::${session?.branch || ''}`;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
function changeSnapshotKey(repoRoot, change) {
|
|
977
|
-
return `${repoRoot || ''}::${normalizeRelativePath(change?.relativePath)}`;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function buildSessionSnapshot(session) {
|
|
981
|
-
return {
|
|
982
|
-
activityKind: session.activityKind,
|
|
983
|
-
changeCount: session.changeCount || 0,
|
|
984
|
-
conflictCount: session.conflictCount || 0,
|
|
985
|
-
lockCount: session.lockCount || 0,
|
|
986
|
-
changedPaths: [...(session.changedPaths || [])],
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
function buildChangeSnapshot(change) {
|
|
991
|
-
return {
|
|
992
|
-
statusLabel: change.statusLabel,
|
|
993
|
-
hasForeignLock: Boolean(change.hasForeignLock),
|
|
994
|
-
lockOwnerBranch: change.lockOwnerBranch || '',
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function deriveSessionDelta(previousSnapshot, currentSession) {
|
|
999
|
-
if (!previousSnapshot) {
|
|
1000
|
-
return '';
|
|
1001
|
-
}
|
|
1002
|
-
if (currentSession.conflictCount > previousSnapshot.conflictCount) {
|
|
1003
|
-
return 'Conflict';
|
|
1004
|
-
}
|
|
1005
|
-
if (currentSession.activityKind !== previousSnapshot.activityKind) {
|
|
1006
|
-
return sessionStatusLabel(currentSession);
|
|
1007
|
-
}
|
|
1008
|
-
if (
|
|
1009
|
-
currentSession.changeCount !== previousSnapshot.changeCount
|
|
1010
|
-
|| !stringListsEqual(currentSession.changedPaths || [], previousSnapshot.changedPaths || [])
|
|
1011
|
-
) {
|
|
1012
|
-
return 'New';
|
|
1013
|
-
}
|
|
1014
|
-
if (currentSession.lockCount !== previousSnapshot.lockCount) {
|
|
1015
|
-
return 'Updated';
|
|
1016
|
-
}
|
|
1017
|
-
return '';
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
function deriveChangeDelta(previousSnapshot, currentChange) {
|
|
1021
|
-
if (!previousSnapshot) {
|
|
1022
|
-
return '';
|
|
1023
|
-
}
|
|
1024
|
-
if (currentChange.hasForeignLock && !previousSnapshot.hasForeignLock) {
|
|
1025
|
-
return 'Conflict';
|
|
1026
|
-
}
|
|
1027
|
-
if (
|
|
1028
|
-
currentChange.statusLabel !== previousSnapshot.statusLabel
|
|
1029
|
-
|| currentChange.lockOwnerBranch !== previousSnapshot.lockOwnerBranch
|
|
1030
|
-
) {
|
|
1031
|
-
return 'Updated';
|
|
1032
|
-
}
|
|
1033
|
-
return '';
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function workingSessionSortKey(session) {
|
|
1037
|
-
if (session.activityKind === 'blocked') {
|
|
1038
|
-
return 0;
|
|
1039
|
-
}
|
|
1040
|
-
if (session.conflictCount > 0) {
|
|
1041
|
-
return 1;
|
|
1042
|
-
}
|
|
1043
|
-
if (session.deltaLabel === 'Conflict') {
|
|
1044
|
-
return 2;
|
|
1045
|
-
}
|
|
1046
|
-
if (session.deltaLabel === 'New') {
|
|
1047
|
-
return 3;
|
|
1048
|
-
}
|
|
1049
|
-
if (session.activityKind === 'finished') {
|
|
1050
|
-
return 5;
|
|
1051
|
-
}
|
|
1052
|
-
return 4;
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function idleSessionSortKey(session) {
|
|
1056
|
-
if (session.activityKind === 'stalled') {
|
|
1057
|
-
return 0;
|
|
1058
|
-
}
|
|
1059
|
-
if (session.activityKind === 'idle') {
|
|
1060
|
-
return 1;
|
|
1061
|
-
}
|
|
1062
|
-
if (session.activityKind === 'dead') {
|
|
1063
|
-
return 2;
|
|
1064
|
-
}
|
|
1065
|
-
return 3;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function sortSessionsForWorkingNow(sessions) {
|
|
1069
|
-
return [...sessions].sort((left, right) => {
|
|
1070
|
-
const keyDelta = workingSessionSortKey(left) - workingSessionSortKey(right);
|
|
1071
|
-
if (keyDelta !== 0) {
|
|
1072
|
-
return keyDelta;
|
|
1073
|
-
}
|
|
1074
|
-
const timeDelta = sessionLastActiveAgeMs(left) - sessionLastActiveAgeMs(right);
|
|
1075
|
-
if (Number.isFinite(timeDelta) && timeDelta !== 0) {
|
|
1076
|
-
return timeDelta;
|
|
1077
|
-
}
|
|
1078
|
-
const changeDelta = (right.changeCount || 0) - (left.changeCount || 0);
|
|
1079
|
-
if (changeDelta !== 0) {
|
|
1080
|
-
return changeDelta;
|
|
1081
|
-
}
|
|
1082
|
-
return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right));
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function sortSessionsForIdleThinking(sessions) {
|
|
1087
|
-
return [...sessions].sort((left, right) => {
|
|
1088
|
-
const keyDelta = idleSessionSortKey(left) - idleSessionSortKey(right);
|
|
1089
|
-
if (keyDelta !== 0) {
|
|
1090
|
-
return keyDelta;
|
|
1091
|
-
}
|
|
1092
|
-
const timeDelta = sessionLastActiveAgeMs(right) - sessionLastActiveAgeMs(left);
|
|
1093
|
-
if (Number.isFinite(timeDelta) && timeDelta !== 0) {
|
|
1094
|
-
return timeDelta;
|
|
1095
|
-
}
|
|
1096
|
-
return sessionDisplayLabel(left).localeCompare(sessionDisplayLabel(right));
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function sortUnassignedChanges(changes) {
|
|
1101
|
-
return [...changes].sort((left, right) => {
|
|
1102
|
-
const leftBadges = changeRiskBadges(left).length;
|
|
1103
|
-
const rightBadges = changeRiskBadges(right).length;
|
|
1104
|
-
if (leftBadges !== rightBadges) {
|
|
1105
|
-
return rightBadges - leftBadges;
|
|
1106
|
-
}
|
|
1107
|
-
return normalizeRelativePath(left.relativePath).localeCompare(normalizeRelativePath(right.relativePath));
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function escapeHtml(value) {
|
|
1112
|
-
return String(value || '')
|
|
1113
|
-
.replace(/&/g, '&')
|
|
1114
|
-
.replace(/</g, '<')
|
|
1115
|
-
.replace(/>/g, '>')
|
|
1116
|
-
.replace(/"/g, '"')
|
|
1117
|
-
.replace(/'/g, ''');
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function formatInspectBranchSummary(inspectData) {
|
|
1121
|
-
if (Number.isInteger(inspectData?.aheadCount) && Number.isInteger(inspectData?.behindCount)) {
|
|
1122
|
-
return `${inspectData.aheadCount} ahead · ${inspectData.behindCount} behind vs ${inspectData.compareRef}`;
|
|
1123
|
-
}
|
|
1124
|
-
return `Branch comparison unavailable vs ${inspectData?.compareRef || 'origin/dev'}`;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function inspectPanelTitle(session) {
|
|
1128
|
-
return `Inspect ${sessionDisplayLabel(session)}`;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
function renderInspectPanelHtml(session, inspectData) {
|
|
1132
|
-
const heldLocksMarkup = Array.isArray(inspectData?.heldLocks) && inspectData.heldLocks.length > 0
|
|
1133
|
-
? `<ul>${inspectData.heldLocks.map((entry) => (
|
|
1134
|
-
`<li><code>${escapeHtml(entry.relativePath)}</code>${entry.allowDelete ? ' <span class="pill">delete ok</span>' : ''}${entry.claimedAt ? ` <span class="muted">${escapeHtml(entry.claimedAt)}</span>` : ''}</li>`
|
|
1135
|
-
)).join('')}</ul>`
|
|
1136
|
-
: '<p class="muted">No held locks recorded for this session.</p>';
|
|
1137
|
-
const logContent = inspectData?.logTailText
|
|
1138
|
-
? escapeHtml(inspectData.logTailText)
|
|
1139
|
-
: 'No log output available.';
|
|
1140
|
-
|
|
1141
|
-
return `<!DOCTYPE html>
|
|
1142
|
-
<html lang="en">
|
|
1143
|
-
<head>
|
|
1144
|
-
<meta charset="utf-8" />
|
|
1145
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1146
|
-
<style>
|
|
1147
|
-
:root {
|
|
1148
|
-
color-scheme: light dark;
|
|
1149
|
-
font-family: var(--vscode-font-family);
|
|
1150
|
-
}
|
|
1151
|
-
body {
|
|
1152
|
-
padding: 16px;
|
|
1153
|
-
color: var(--vscode-foreground);
|
|
1154
|
-
background: var(--vscode-editor-background);
|
|
1155
|
-
}
|
|
1156
|
-
h1, h2 {
|
|
1157
|
-
margin: 0 0 12px;
|
|
1158
|
-
font-weight: 600;
|
|
1159
|
-
}
|
|
1160
|
-
h2 {
|
|
1161
|
-
margin-top: 20px;
|
|
1162
|
-
font-size: 13px;
|
|
1163
|
-
text-transform: uppercase;
|
|
1164
|
-
letter-spacing: 0.04em;
|
|
1165
|
-
color: var(--vscode-descriptionForeground);
|
|
1166
|
-
}
|
|
1167
|
-
.grid {
|
|
1168
|
-
display: grid;
|
|
1169
|
-
grid-template-columns: minmax(140px, 220px) 1fr;
|
|
1170
|
-
gap: 8px 12px;
|
|
1171
|
-
margin: 0;
|
|
1172
|
-
}
|
|
1173
|
-
dt {
|
|
1174
|
-
color: var(--vscode-descriptionForeground);
|
|
1175
|
-
}
|
|
1176
|
-
dd {
|
|
1177
|
-
margin: 0;
|
|
1178
|
-
word-break: break-word;
|
|
1179
|
-
}
|
|
1180
|
-
code, pre {
|
|
1181
|
-
font-family: var(--vscode-editor-font-family, monospace);
|
|
1182
|
-
font-size: 12px;
|
|
1183
|
-
}
|
|
1184
|
-
pre {
|
|
1185
|
-
margin: 0;
|
|
1186
|
-
padding: 12px;
|
|
1187
|
-
border-radius: 8px;
|
|
1188
|
-
overflow: auto;
|
|
1189
|
-
background: var(--vscode-textCodeBlock-background, rgba(127, 127, 127, 0.12));
|
|
1190
|
-
border: 1px solid var(--vscode-editorWidget-border, transparent);
|
|
1191
|
-
white-space: pre-wrap;
|
|
1192
|
-
word-break: break-word;
|
|
1193
|
-
}
|
|
1194
|
-
ul {
|
|
1195
|
-
margin: 0;
|
|
1196
|
-
padding-left: 20px;
|
|
1197
|
-
}
|
|
1198
|
-
li + li {
|
|
1199
|
-
margin-top: 6px;
|
|
1200
|
-
}
|
|
1201
|
-
.muted {
|
|
1202
|
-
color: var(--vscode-descriptionForeground);
|
|
1203
|
-
}
|
|
1204
|
-
.pill {
|
|
1205
|
-
display: inline-block;
|
|
1206
|
-
margin-left: 6px;
|
|
1207
|
-
padding: 1px 6px;
|
|
1208
|
-
border-radius: 999px;
|
|
1209
|
-
background: var(--vscode-badge-background);
|
|
1210
|
-
color: var(--vscode-badge-foreground);
|
|
1211
|
-
font-size: 11px;
|
|
1212
|
-
}
|
|
1213
|
-
</style>
|
|
1214
|
-
</head>
|
|
1215
|
-
<body>
|
|
1216
|
-
<h1>${escapeHtml(sessionIdentityLabel(session))}</h1>
|
|
1217
|
-
<dl class="grid">
|
|
1218
|
-
<dt>Branch</dt>
|
|
1219
|
-
<dd><code>${escapeHtml(session.branch)}</code></dd>
|
|
1220
|
-
<dt>Worktree</dt>
|
|
1221
|
-
<dd><code>${escapeHtml(session.worktreePath)}</code></dd>
|
|
1222
|
-
<dt>Base branch</dt>
|
|
1223
|
-
<dd><code>${escapeHtml(inspectData?.baseBranch || 'dev')}</code></dd>
|
|
1224
|
-
<dt>Divergence</dt>
|
|
1225
|
-
<dd>${escapeHtml(formatInspectBranchSummary(inspectData))}</dd>
|
|
1226
|
-
<dt>Held locks</dt>
|
|
1227
|
-
<dd>${Array.isArray(inspectData?.heldLocks) ? inspectData.heldLocks.length : 0}</dd>
|
|
1228
|
-
<dt>Log file</dt>
|
|
1229
|
-
<dd><code>${escapeHtml(inspectData?.logPath || 'Unavailable')}</code></dd>
|
|
1230
|
-
</dl>
|
|
1231
|
-
<h2>Held Locks</h2>
|
|
1232
|
-
${heldLocksMarkup}
|
|
1233
|
-
<h2>Agent Log Tail</h2>
|
|
1234
|
-
<pre>${logContent}</pre>
|
|
1235
|
-
</body>
|
|
1236
|
-
</html>`;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
class SessionDecorationProvider {
|
|
1240
|
-
constructor(nowProvider = () => Date.now()) {
|
|
1241
|
-
this.nowProvider = nowProvider;
|
|
1242
|
-
this.sessionsByUri = new Map();
|
|
1243
|
-
this.lockEntriesByFileUri = new Map();
|
|
1244
|
-
this.selectedBranch = '';
|
|
1245
|
-
this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
|
|
1246
|
-
this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
updateSessions(sessions) {
|
|
1250
|
-
this.sessionsByUri = new Map(
|
|
1251
|
-
sessions.map((session) => [sessionDecorationUri(session.branch).toString(), session]),
|
|
1252
|
-
);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
updateLockEntries(repoEntries) {
|
|
1256
|
-
const nextEntriesByUri = new Map();
|
|
1257
|
-
for (const entry of repoEntries || []) {
|
|
1258
|
-
for (const [relativePath, lockEntry] of entry.lockEntries || []) {
|
|
1259
|
-
nextEntriesByUri.set(
|
|
1260
|
-
vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(),
|
|
1261
|
-
{ branch: lockEntry.branch },
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
this.lockEntriesByFileUri = nextEntriesByUri;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
setSelectedBranch(branch) {
|
|
1269
|
-
this.selectedBranch = typeof branch === 'string' ? branch.trim() : '';
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
refresh() {
|
|
1273
|
-
this.onDidChangeFileDecorationsEmitter.fire();
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
provideFileDecoration(uri) {
|
|
1277
|
-
if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
|
|
1278
|
-
if (!uri || uri.scheme !== 'file') {
|
|
1279
|
-
return undefined;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
const lockEntry = this.lockEntriesByFileUri.get(uri.toString());
|
|
1283
|
-
if (!lockEntry?.branch) {
|
|
1284
|
-
return undefined;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch;
|
|
1288
|
-
return {
|
|
1289
|
-
badge: agentBadgeFromBranch(lockEntry.branch),
|
|
1290
|
-
tooltip: ownsSelectedSession
|
|
1291
|
-
? `Locked by selected session ${lockEntry.branch}`
|
|
1292
|
-
: this.selectedBranch
|
|
1293
|
-
? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})`
|
|
1294
|
-
: `Locked by ${lockEntry.branch}`,
|
|
1295
|
-
color: new vscode.ThemeColor(
|
|
1296
|
-
ownsSelectedSession
|
|
1297
|
-
? 'gitDecoration.modifiedResourceForeground'
|
|
1298
|
-
: this.selectedBranch
|
|
1299
|
-
? 'list.errorForeground'
|
|
1300
|
-
: 'list.warningForeground',
|
|
1301
|
-
),
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
const session = this.sessionsByUri.get(uri.toString());
|
|
1306
|
-
const idleDecoration = sessionIdleDecoration(session, this.nowProvider());
|
|
1307
|
-
if (idleDecoration) {
|
|
1308
|
-
return idleDecoration;
|
|
1309
|
-
}
|
|
1310
|
-
return sessionIdentityDecoration(session);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
class InfoItem extends vscode.TreeItem {
|
|
1315
|
-
constructor(label, description = '') {
|
|
1316
|
-
super(label, vscode.TreeItemCollapsibleState.None);
|
|
1317
|
-
this.description = description;
|
|
1318
|
-
this.iconPath = themeIcon('info');
|
|
1319
|
-
this.tooltip = [label, description].filter(Boolean).join('\n');
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
class DetailItem extends vscode.TreeItem {
|
|
1324
|
-
constructor(label, description = '', options = {}) {
|
|
1325
|
-
super(label, vscode.TreeItemCollapsibleState.None);
|
|
1326
|
-
this.description = description;
|
|
1327
|
-
this.tooltip = options.tooltip || [label, description].filter(Boolean).join('\n');
|
|
1328
|
-
this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
class RepoItem extends vscode.TreeItem {
|
|
1333
|
-
constructor(repoRoot, sessions, changes, options = {}) {
|
|
1334
|
-
const label = typeof options.label === 'string' && options.label.trim()
|
|
1335
|
-
? options.label.trim()
|
|
1336
|
-
: repoRootDisplayLabel(repoRoot);
|
|
1337
|
-
super(label, vscode.TreeItemCollapsibleState.Expanded);
|
|
1338
|
-
this.repoRoot = repoRoot;
|
|
1339
|
-
this.sessions = sessions;
|
|
1340
|
-
this.changes = changes;
|
|
1341
|
-
this.unassignedChanges = options.unassignedChanges || [];
|
|
1342
|
-
this.lockEntries = options.lockEntries || [];
|
|
1343
|
-
this.colonyTasks = Array.isArray(options.colonyTasks) ? options.colonyTasks : [];
|
|
1344
|
-
this.overview = options.overview
|
|
1345
|
-
|| buildRepoOverview(sessions, this.unassignedChanges, this.lockEntries, this.colonyTasks);
|
|
1346
|
-
this.description = buildRepoDescription(this.overview);
|
|
1347
|
-
this.tooltip = buildRepoTooltip(repoRoot, this.overview);
|
|
1348
|
-
this.iconPath = themeIcon('repo');
|
|
1349
|
-
this.contextValue = 'gitguardex.repo';
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
class SectionItem extends vscode.TreeItem {
|
|
1354
|
-
constructor(label, items, options = {}) {
|
|
1355
|
-
const collapsibleState = items.length > 0
|
|
1356
|
-
? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded)
|
|
1357
|
-
: vscode.TreeItemCollapsibleState.None;
|
|
1358
|
-
super(label, collapsibleState);
|
|
1359
|
-
this.items = items;
|
|
1360
|
-
this.description = options.description
|
|
1361
|
-
|| (items.length > 0 ? String(items.length) : '');
|
|
1362
|
-
this.tooltip = options.tooltip || [label, this.description].filter(Boolean).join('\n');
|
|
1363
|
-
this.iconPath = options.iconId ? themeIcon(options.iconId, options.iconColorId) : undefined;
|
|
1364
|
-
this.contextValue = 'gitguardex.section';
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
class WorktreeItem extends vscode.TreeItem {
|
|
1369
|
-
constructor(worktreePath, sessions, items = [], options = {}) {
|
|
1370
|
-
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
|
|
1371
|
-
const sessionList = Array.isArray(sessions) ? sessions : [];
|
|
1372
|
-
const primarySession = options.resourceSession || sessionList[0] || null;
|
|
1373
|
-
const changedCount = Number.isInteger(options.changedCount)
|
|
1374
|
-
? options.changedCount
|
|
1375
|
-
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
|
|
1376
|
-
const label = typeof options.label === 'string' && options.label.trim()
|
|
1377
|
-
? options.label.trim()
|
|
1378
|
-
: worktreeDisplayLabel(normalizedWorktreePath, sessionList);
|
|
1379
|
-
super(
|
|
1380
|
-
label,
|
|
1381
|
-
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
|
|
1382
|
-
);
|
|
1383
|
-
this.worktreePath = normalizedWorktreePath;
|
|
1384
|
-
this.sessions = sessionList;
|
|
1385
|
-
this.items = items;
|
|
1386
|
-
this.description = options.description || buildWorktreeDescription(sessionList, changedCount);
|
|
1387
|
-
this.tooltip = [
|
|
1388
|
-
normalizedWorktreePath,
|
|
1389
|
-
...sessionList.map((session) => session.branch).filter(Boolean),
|
|
1390
|
-
].filter(Boolean).join('\n');
|
|
1391
|
-
this.iconPath = themeIcon(options.iconId || 'folder', options.iconColorId);
|
|
1392
|
-
if (options.useSessionDecoration && primarySession?.branch) {
|
|
1393
|
-
this.resourceUri = sessionDecorationUri(primarySession.branch);
|
|
1394
|
-
}
|
|
1395
|
-
this.contextValue = 'gitguardex.worktree';
|
|
1396
|
-
if (primarySession?.worktreePath) {
|
|
1397
|
-
this.command = {
|
|
1398
|
-
command: 'gitguardex.activeAgents.openWorktree',
|
|
1399
|
-
title: 'Open Agent Worktree',
|
|
1400
|
-
arguments: [primarySession],
|
|
1401
|
-
};
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
class SessionItem extends vscode.TreeItem {
|
|
1407
|
-
constructor(session, items = [], options = {}) {
|
|
1408
|
-
const variant = options.variant === 'raw' ? 'raw' : 'card';
|
|
1409
|
-
const label = typeof options.label === 'string' && options.label.trim()
|
|
1410
|
-
? options.label.trim()
|
|
1411
|
-
: (variant === 'raw' ? session.label : sessionDisplayLabel(session));
|
|
1412
|
-
const collapsibleState = items.length > 0
|
|
1413
|
-
? (options.collapsedState ?? (
|
|
1414
|
-
variant === 'raw'
|
|
1415
|
-
? vscode.TreeItemCollapsibleState.Expanded
|
|
1416
|
-
: vscode.TreeItemCollapsibleState.Collapsed
|
|
1417
|
-
))
|
|
1418
|
-
: vscode.TreeItemCollapsibleState.None;
|
|
1419
|
-
super(
|
|
1420
|
-
label,
|
|
1421
|
-
collapsibleState,
|
|
1422
|
-
);
|
|
1423
|
-
this.session = session;
|
|
1424
|
-
this.items = items;
|
|
1425
|
-
this.resourceUri = sessionDecorationUri(session.branch);
|
|
1426
|
-
this.description = variant === 'raw'
|
|
1427
|
-
? buildRawSessionDescription(session)
|
|
1428
|
-
: buildSessionCardDescription(session);
|
|
1429
|
-
this.tooltip = buildSessionTooltip(session, this.description);
|
|
1430
|
-
this.iconPath = themeIcon(resolveSessionActivityIconId(session.activityKind));
|
|
1431
|
-
this.contextValue = sessionContextValue(session);
|
|
1432
|
-
this.command = {
|
|
1433
|
-
command: 'gitguardex.activeAgents.openWorktree',
|
|
1434
|
-
title: 'Open Agent Worktree',
|
|
1435
|
-
arguments: [session],
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
function sessionContextValue(session) {
|
|
1441
|
-
const activityKind = typeof session?.activityKind === 'string' ? session.activityKind.trim() : '';
|
|
1442
|
-
return activityKind
|
|
1443
|
-
? `gitguardex.session.${activityKind}`
|
|
1444
|
-
: 'gitguardex.session';
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function canDismissSession(session) {
|
|
1448
|
-
return DISMISSABLE_SESSION_ACTIVITY_KINDS.has(session?.activityKind);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
function buildDismissSessionDetail(session, statePath) {
|
|
1452
|
-
const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
|
|
1453
|
-
const relativeStatePath = repoRoot
|
|
1454
|
-
? path.relative(repoRoot, statePath) || path.basename(statePath)
|
|
1455
|
-
: path.basename(statePath);
|
|
1456
|
-
const detailParts = [
|
|
1457
|
-
`Remove ${relativeStatePath} and hide this session from Active Agents.`,
|
|
1458
|
-
];
|
|
1459
|
-
|
|
1460
|
-
if (session?.activityKind === 'stalled') {
|
|
1461
|
-
detailParts.push('This dismisses the stale sidebar row only; use Stop if you want to interrupt a live agent.');
|
|
1462
|
-
} else {
|
|
1463
|
-
detailParts.push('This clears the stale session record from the sidebar.');
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
return detailParts.join(' ');
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
class FolderItem extends vscode.TreeItem {
|
|
1470
|
-
constructor(label, relativePath, items, options = {}) {
|
|
1471
|
-
super(
|
|
1472
|
-
label,
|
|
1473
|
-
items.length > 0
|
|
1474
|
-
? (options.collapsedState ?? vscode.TreeItemCollapsibleState.Expanded)
|
|
1475
|
-
: vscode.TreeItemCollapsibleState.None,
|
|
1476
|
-
);
|
|
1477
|
-
this.relativePath = relativePath;
|
|
1478
|
-
this.items = items;
|
|
1479
|
-
this.description = typeof options.description === 'string' ? options.description : '';
|
|
1480
|
-
this.tooltip = options.tooltip || relativePath || label;
|
|
1481
|
-
this.iconPath = options.iconPath
|
|
1482
|
-
|| (!options.iconId ? resolveBundledTreeItemIcon(relativePath || label, 'folder') : undefined)
|
|
1483
|
-
|| themeIcon(options.iconId || 'folder', options.iconColorId);
|
|
1484
|
-
this.contextValue = options.contextValue || 'gitguardex.folder';
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
class ChangeItem extends vscode.TreeItem {
|
|
1489
|
-
constructor(change, options = {}) {
|
|
1490
|
-
const label = typeof options.label === 'string' && options.label.trim()
|
|
1491
|
-
? options.label.trim()
|
|
1492
|
-
: path.basename(change.relativePath);
|
|
1493
|
-
super(label, vscode.TreeItemCollapsibleState.None);
|
|
1494
|
-
this.change = change;
|
|
1495
|
-
this.description = typeof options.description === 'string'
|
|
1496
|
-
? options.description
|
|
1497
|
-
: change.statusLabel;
|
|
1498
|
-
this.tooltip = [
|
|
1499
|
-
change.relativePath,
|
|
1500
|
-
`Summary ${this.description}`,
|
|
1501
|
-
`Status ${change.statusText}`,
|
|
1502
|
-
change.originalPath ? `Renamed from ${change.originalPath}` : '',
|
|
1503
|
-
change.hasForeignLock ? `Locked by ${change.lockOwnerBranch}` : '',
|
|
1504
|
-
change.absolutePath,
|
|
1505
|
-
].filter(Boolean).join('\n');
|
|
1506
|
-
this.resourceUri = vscode.Uri.file(change.absolutePath);
|
|
1507
|
-
if (options.iconId || change.hasForeignLock) {
|
|
1508
|
-
this.iconPath = themeIcon(options.iconId || 'warning', options.iconColorId || 'list.warningForeground');
|
|
1509
|
-
} else {
|
|
1510
|
-
this.iconPath = options.iconPath || resolveBundledTreeItemIcon(change.relativePath || label, 'file');
|
|
1511
|
-
}
|
|
1512
|
-
this.contextValue = 'gitguardex.change';
|
|
1513
|
-
this.command = {
|
|
1514
|
-
command: 'gitguardex.activeAgents.openChange',
|
|
1515
|
-
title: 'Open Changed File',
|
|
1516
|
-
arguments: [change],
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function shellQuote(value) {
|
|
1522
|
-
const normalized = String(value || '');
|
|
1523
|
-
return `'${normalized.replace(/'/g, "'\"'\"'")}'`;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
function readPackageJson(repoRoot) {
|
|
1527
|
-
const packageJsonPath = path.join(repoRoot, 'package.json');
|
|
1528
|
-
try {
|
|
1529
|
-
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
1530
|
-
} catch (_error) {
|
|
1531
|
-
return null;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function resolveStartAgentCommand(repoRoot, details) {
|
|
1536
|
-
const taskArg = shellQuote(details.taskName);
|
|
1537
|
-
const agentArg = shellQuote(details.agentName);
|
|
1538
|
-
const localCodexAgentPath = path.join(repoRoot, 'scripts', 'codex-agent.sh');
|
|
1539
|
-
if (fs.existsSync(localCodexAgentPath)) {
|
|
1540
|
-
return `bash ./scripts/codex-agent.sh ${taskArg} ${agentArg}`;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
const agentCodexScript = readPackageJson(repoRoot)?.scripts?.['agent:codex'];
|
|
1544
|
-
if (typeof agentCodexScript === 'string' && agentCodexScript.trim().length > 0) {
|
|
1545
|
-
return `npm run agent:codex -- ${taskArg} ${agentArg}`;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
return `gx branch start ${taskArg} ${agentArg}`;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
function sessionTaskLabel(session) {
|
|
1552
|
-
const latestTaskPreview = typeof session?.latestTaskPreview === 'string'
|
|
1553
|
-
? session.latestTaskPreview.trim()
|
|
1554
|
-
: '';
|
|
1555
|
-
if (latestTaskPreview) {
|
|
1556
|
-
return latestTaskPreview;
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
|
|
1560
|
-
if (taskName) {
|
|
1561
|
-
return taskName;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
return '';
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function sessionDisplayLabel(session) {
|
|
1568
|
-
return sessionTaskLabel(session)
|
|
1569
|
-
|| session?.label
|
|
1570
|
-
|| compactBranchLabel(session?.branch)
|
|
1571
|
-
|| session?.branch
|
|
1572
|
-
|| path.basename(session?.worktreePath || '')
|
|
1573
|
-
|| 'session';
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function sessionTreeLabel(session) {
|
|
1577
|
-
return sessionTaskLabel(session) || compactBranchLabel(session?.branch) || sessionDisplayLabel(session);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function worktreeDisplayLabel(worktreePath, sessions) {
|
|
1581
|
-
const sessionList = Array.isArray(sessions)
|
|
1582
|
-
? sessions.filter(Boolean)
|
|
1583
|
-
: [];
|
|
1584
|
-
if (sessionList.length === 1) {
|
|
1585
|
-
return sessionDisplayLabel(sessionList[0]);
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
return path.basename(String(worktreePath || '').trim()) || 'worktree';
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
function buildWorktreeDescription(sessions, changedCount) {
|
|
1592
|
-
const sessionList = Array.isArray(sessions)
|
|
1593
|
-
? sessions.filter(Boolean)
|
|
1594
|
-
: [];
|
|
1595
|
-
const primarySession = sessionList.length === 1 ? sessionList[0] : null;
|
|
1596
|
-
const totalLocks = sessionList.reduce((total, session) => total + (session.lockCount || 0), 0);
|
|
1597
|
-
const descriptionParts = [];
|
|
1598
|
-
|
|
1599
|
-
if (primarySession?.agentName) {
|
|
1600
|
-
descriptionParts.push(primarySession.agentName);
|
|
1601
|
-
} else {
|
|
1602
|
-
descriptionParts.push(formatCountLabel(sessionList.length, 'agent'));
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const fileCountLabel = primarySession
|
|
1606
|
-
? sessionFileCountLabel(primarySession)
|
|
1607
|
-
: changedCount > 0
|
|
1608
|
-
? formatCountLabel(changedCount, 'file')
|
|
1609
|
-
: '';
|
|
1610
|
-
if (fileCountLabel) {
|
|
1611
|
-
descriptionParts.push(fileCountLabel);
|
|
1612
|
-
}
|
|
1613
|
-
if (totalLocks > 0) {
|
|
1614
|
-
descriptionParts.push(formatCountLabel(totalLocks, 'lock'));
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
return descriptionParts.join(' · ');
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
function sessionWorktreePath(session) {
|
|
1621
|
-
return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
function resolveSessionProjectRelativePath(session) {
|
|
1625
|
-
const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
|
|
1626
|
-
if (!repoRoot) {
|
|
1627
|
-
return '';
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
const resolveCandidate = (candidatePath) => {
|
|
1631
|
-
const normalizedCandidate = typeof candidatePath === 'string' ? candidatePath.trim() : '';
|
|
1632
|
-
if (!normalizedCandidate) {
|
|
1633
|
-
return '';
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
const absolutePath = path.isAbsolute(normalizedCandidate)
|
|
1637
|
-
? path.resolve(normalizedCandidate)
|
|
1638
|
-
: path.resolve(repoRoot, normalizedCandidate);
|
|
1639
|
-
if (!isPathWithin(repoRoot, absolutePath) || !fs.existsSync(absolutePath)) {
|
|
1640
|
-
return '';
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
return normalizeRelativePath(path.relative(repoRoot, absolutePath));
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
|
-
const isManagedWorktreeRelativePath = (relativePath) => {
|
|
1647
|
-
const normalizedRelativePath = normalizeRelativePath(relativePath);
|
|
1648
|
-
return MANAGED_WORKTREE_RELATIVE_ROOTS.some((managedRoot) => {
|
|
1649
|
-
const normalizedManagedRoot = normalizeRelativePath(managedRoot);
|
|
1650
|
-
return normalizedRelativePath === normalizedManagedRoot
|
|
1651
|
-
|| normalizedRelativePath.startsWith(`${normalizedManagedRoot}/`);
|
|
1652
|
-
});
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
const explicitProjectPath = resolveCandidate(session?.projectPath);
|
|
1656
|
-
if (explicitProjectPath && !isManagedWorktreeRelativePath(explicitProjectPath)) {
|
|
1657
|
-
return explicitProjectPath;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
const namedProjectPath = resolveCandidate(session?.projectName);
|
|
1661
|
-
if (namedProjectPath && !isManagedWorktreeRelativePath(namedProjectPath)) {
|
|
1662
|
-
return namedProjectPath;
|
|
1663
|
-
}
|
|
1664
|
-
return '';
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
function worktreeProjectRelativePath(sessions) {
|
|
1668
|
-
const projectPaths = uniqueStringList((sessions || [])
|
|
1669
|
-
.map((session) => resolveSessionProjectRelativePath(session))
|
|
1670
|
-
.filter(Boolean));
|
|
1671
|
-
return projectPaths.length === 1 ? projectPaths[0] : '';
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
function repoEntryDisplayLabel(repoRoot, sessions) {
|
|
1675
|
-
const repoLabel = repoRootDisplayLabel(repoRoot);
|
|
1676
|
-
const projectPaths = uniqueStringList((sessions || [])
|
|
1677
|
-
.map((session) => resolveSessionProjectRelativePath(session))
|
|
1678
|
-
.filter(Boolean));
|
|
1679
|
-
if (projectPaths.length !== 1) {
|
|
1680
|
-
return repoLabel;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
const [projectRelativePath] = projectPaths;
|
|
1684
|
-
const hasRootScopedSession = (sessions || []).some(
|
|
1685
|
-
(session) => !resolveSessionProjectRelativePath(session),
|
|
1686
|
-
);
|
|
1687
|
-
if (!projectRelativePath || hasRootScopedSession) {
|
|
1688
|
-
return repoLabel;
|
|
1689
|
-
}
|
|
1690
|
-
if (repoLabel.endsWith(`/${projectRelativePath}`)) {
|
|
1691
|
-
return repoLabel;
|
|
1692
|
-
}
|
|
1693
|
-
return `${repoLabel}/${projectRelativePath}`;
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function buildProjectScopedDescription(entries) {
|
|
1697
|
-
const sessions = (entries || []).flatMap((entry) => Array.isArray(entry?.sessions) ? entry.sessions : []);
|
|
1698
|
-
if (sessions.length === 0) {
|
|
1699
|
-
return '';
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
const changedCount = sessions.reduce((total, session) => total + (session.changeCount || 0), 0);
|
|
1703
|
-
const lockCount = sessions.reduce((total, session) => total + (session.lockCount || 0), 0);
|
|
1704
|
-
const descriptionParts = [formatCountLabel(sessions.length, 'agent')];
|
|
1705
|
-
if (changedCount > 0) {
|
|
1706
|
-
descriptionParts.push(formatCountLabel(changedCount, 'file'));
|
|
1707
|
-
}
|
|
1708
|
-
if (lockCount > 0) {
|
|
1709
|
-
descriptionParts.push(formatCountLabel(lockCount, 'lock'));
|
|
1710
|
-
}
|
|
1711
|
-
return descriptionParts.join(' · ');
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function buildProjectScopedItems(entries, options = {}) {
|
|
1715
|
-
const normalizedEntries = Array.isArray(entries)
|
|
1716
|
-
? entries.filter((entry) => entry?.item)
|
|
1717
|
-
: [];
|
|
1718
|
-
const projectRoots = [];
|
|
1719
|
-
const rootEntries = [];
|
|
1720
|
-
let hasProjectFolders = false;
|
|
1721
|
-
|
|
1722
|
-
function sortFolders(nodes) {
|
|
1723
|
-
nodes.sort((left, right) => left.label.localeCompare(right.label));
|
|
1724
|
-
for (const node of nodes) {
|
|
1725
|
-
sortFolders(node.children);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
for (const entry of normalizedEntries) {
|
|
1730
|
-
const projectRelativePath = normalizeRelativePath(entry.projectRelativePath);
|
|
1731
|
-
if (!projectRelativePath) {
|
|
1732
|
-
rootEntries.push(entry);
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
hasProjectFolders = true;
|
|
1737
|
-
let nodes = projectRoots;
|
|
1738
|
-
let folderPath = '';
|
|
1739
|
-
let parentNode = null;
|
|
1740
|
-
for (const segment of projectRelativePath.split('/').filter(Boolean)) {
|
|
1741
|
-
folderPath = folderPath ? path.posix.join(folderPath, segment) : segment;
|
|
1742
|
-
let folderNode = nodes.find((node) => node.relativePath === folderPath);
|
|
1743
|
-
if (!folderNode) {
|
|
1744
|
-
folderNode = {
|
|
1745
|
-
label: segment,
|
|
1746
|
-
relativePath: folderPath,
|
|
1747
|
-
children: [],
|
|
1748
|
-
entries: [],
|
|
1749
|
-
directEntries: [],
|
|
1750
|
-
};
|
|
1751
|
-
nodes.push(folderNode);
|
|
1752
|
-
}
|
|
1753
|
-
folderNode.entries.push(entry);
|
|
1754
|
-
parentNode = folderNode;
|
|
1755
|
-
nodes = folderNode.children;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
if (parentNode) {
|
|
1759
|
-
parentNode.directEntries.push(entry);
|
|
1760
|
-
} else {
|
|
1761
|
-
rootEntries.push(entry);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
if (!hasProjectFolders) {
|
|
1766
|
-
return rootEntries.map((entry) => entry.item);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
sortFolders(projectRoots);
|
|
1770
|
-
|
|
1771
|
-
function materialize(nodes) {
|
|
1772
|
-
return nodes.map((node) => new FolderItem(
|
|
1773
|
-
node.label,
|
|
1774
|
-
node.relativePath,
|
|
1775
|
-
[
|
|
1776
|
-
...materialize(node.children),
|
|
1777
|
-
...node.directEntries.map((entry) => entry.item),
|
|
1778
|
-
],
|
|
1779
|
-
{
|
|
1780
|
-
description: buildProjectScopedDescription(node.entries),
|
|
1781
|
-
tooltip: [node.relativePath, buildProjectScopedDescription(node.entries)].filter(Boolean).join('\n'),
|
|
1782
|
-
},
|
|
1783
|
-
));
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
const items = materialize(projectRoots);
|
|
1787
|
-
if (rootEntries.length === 0) {
|
|
1788
|
-
return items;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
const rootLabel = typeof options.rootLabel === 'string' ? options.rootLabel.trim() : '';
|
|
1792
|
-
if (!rootLabel) {
|
|
1793
|
-
items.push(...rootEntries.map((entry) => entry.item));
|
|
1794
|
-
return items;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
items.push(new FolderItem(
|
|
1798
|
-
rootLabel,
|
|
1799
|
-
'',
|
|
1800
|
-
rootEntries.map((entry) => entry.item),
|
|
1801
|
-
{
|
|
1802
|
-
description: buildProjectScopedDescription(rootEntries),
|
|
1803
|
-
tooltip: rootLabel,
|
|
1804
|
-
},
|
|
1805
|
-
));
|
|
1806
|
-
return items;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
function showSessionMessage(message) {
|
|
1810
|
-
vscode.window.showInformationMessage?.(message);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function ensureSessionWorktree(session, actionLabel) {
|
|
1814
|
-
const worktreePath = sessionWorktreePath(session);
|
|
1815
|
-
if (!worktreePath) {
|
|
1816
|
-
showSessionMessage(`Cannot ${actionLabel}: missing worktree path.`);
|
|
1817
|
-
return '';
|
|
1818
|
-
}
|
|
1819
|
-
if (!fs.existsSync(worktreePath)) {
|
|
1820
|
-
showSessionMessage(`Cannot ${actionLabel}: worktree is no longer on disk: ${worktreePath}`);
|
|
1821
|
-
return '';
|
|
1822
|
-
}
|
|
1823
|
-
return worktreePath;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
function runSessionTerminalCommand(session, actionLabel, iconId, commandText) {
|
|
1827
|
-
const worktreePath = ensureSessionWorktree(session, actionLabel.toLowerCase());
|
|
1828
|
-
if (!worktreePath) {
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
const terminal = vscode.window.createTerminal({
|
|
1833
|
-
name: `GitGuardex ${actionLabel}: ${sessionDisplayLabel(session)}`,
|
|
1834
|
-
cwd: worktreePath,
|
|
1835
|
-
iconPath: new vscode.ThemeIcon(iconId),
|
|
1836
|
-
});
|
|
1837
|
-
terminal.show();
|
|
1838
|
-
terminal.sendText(commandText, true);
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
function sessionTerminalLabel(session) {
|
|
1842
|
-
return `GitGuardex Terminal: ${sessionDisplayLabel(session)}`;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
function listWindowTerminals() {
|
|
1846
|
-
return Array.isArray(vscode.window.terminals) ? vscode.window.terminals : [];
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
function focusTerminal(terminal) {
|
|
1850
|
-
terminal?.show?.(false);
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
async function terminalProcessId(terminal) {
|
|
1854
|
-
if (!terminal?.processId) {
|
|
1855
|
-
return null;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
try {
|
|
1859
|
-
const pid = await terminal.processId;
|
|
1860
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
1861
|
-
} catch (_error) {
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
function findFallbackSessionTerminal(session) {
|
|
1867
|
-
const label = sessionTerminalLabel(session);
|
|
1868
|
-
return listWindowTerminals().find((terminal) => terminal?.name === label) || null;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
async function findSessionTerminal(session) {
|
|
1872
|
-
const pid = Number(session?.pid);
|
|
1873
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1874
|
-
return null;
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
for (const terminal of listWindowTerminals()) {
|
|
1878
|
-
if (await terminalProcessId(terminal) === pid) {
|
|
1879
|
-
return terminal;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
return null;
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
function openFallbackSessionTerminal(session, worktreePath) {
|
|
1887
|
-
const existingTerminal = findFallbackSessionTerminal(session);
|
|
1888
|
-
if (existingTerminal) {
|
|
1889
|
-
focusTerminal(existingTerminal);
|
|
1890
|
-
return existingTerminal;
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
const terminal = vscode.window.createTerminal({
|
|
1894
|
-
name: sessionTerminalLabel(session),
|
|
1895
|
-
cwd: worktreePath,
|
|
1896
|
-
iconPath: new vscode.ThemeIcon('terminal'),
|
|
1897
|
-
});
|
|
1898
|
-
focusTerminal(terminal);
|
|
1899
|
-
return terminal;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
async function showSessionTerminal(session) {
|
|
1903
|
-
const worktreePath = ensureSessionWorktree(session, 'show terminal');
|
|
1904
|
-
if (!worktreePath) {
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
const terminal = await findSessionTerminal(session);
|
|
1909
|
-
if (terminal) {
|
|
1910
|
-
focusTerminal(terminal);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
openFallbackSessionTerminal(session, worktreePath);
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
function finishSession(session) {
|
|
1918
|
-
if (!session?.branch) {
|
|
1919
|
-
showSessionMessage('Cannot finish session: missing branch name.');
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
runSessionTerminalCommand(
|
|
1923
|
-
session,
|
|
1924
|
-
'Finish',
|
|
1925
|
-
'check',
|
|
1926
|
-
`gx branch finish --branch ${shellQuote(session.branch)}`,
|
|
1927
|
-
);
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
function syncSession(session) {
|
|
1931
|
-
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
async function restartActiveAgents(extensionId) {
|
|
1935
|
-
if (extensionId && extensionId !== ACTIVE_AGENTS_EXTENSION_ID) {
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
await vscode.commands.executeCommand(RESTART_EXTENSION_HOST_COMMAND);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
function execFileAsync(command, args, options = {}) {
|
|
1942
|
-
return new Promise((resolve, reject) => {
|
|
1943
|
-
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
|
|
1944
|
-
if (error) {
|
|
1945
|
-
error.stdout = stdout;
|
|
1946
|
-
error.stderr = stderr;
|
|
1947
|
-
reject(error);
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
resolve({ stdout, stderr });
|
|
1951
|
-
});
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
function buildStopSessionCommandText(session, pid) {
|
|
1956
|
-
const parts = ['gx', 'agents', 'stop', '--pid', String(pid)];
|
|
1957
|
-
if (session?.repoRoot) {
|
|
1958
|
-
parts.push('--target', session.repoRoot);
|
|
1959
|
-
}
|
|
1960
|
-
return parts.map(shellQuote).join(' ');
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
async function stopSession(session, refresh) {
|
|
1964
|
-
const pid = Number(session?.pid);
|
|
1965
|
-
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1966
|
-
showSessionMessage('Cannot stop session: missing pid.');
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
1969
|
-
if (!session?.branch) {
|
|
1970
|
-
showSessionMessage('Cannot stop session: missing branch name.');
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
const sessionTerminal = await findSessionTerminal(session);
|
|
1975
|
-
const stopCommandText = buildStopSessionCommandText(session, pid);
|
|
1976
|
-
const confirmed = await vscode.window.showWarningMessage(
|
|
1977
|
-
`Stop ${sessionDisplayLabel(session)}?`,
|
|
1978
|
-
{
|
|
1979
|
-
modal: true,
|
|
1980
|
-
detail: sessionTerminal
|
|
1981
|
-
? 'Send Ctrl+C to the live session terminal.'
|
|
1982
|
-
: `No live session terminal found. Run ${stopCommandText}.`,
|
|
1983
|
-
},
|
|
1984
|
-
'Stop',
|
|
1985
|
-
);
|
|
1986
|
-
if (confirmed !== 'Stop') {
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
if (sessionTerminal) {
|
|
1991
|
-
focusTerminal(sessionTerminal);
|
|
1992
|
-
sessionTerminal.sendText('\u0003', false);
|
|
1993
|
-
refresh();
|
|
1994
|
-
return;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
try {
|
|
1998
|
-
const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
|
|
1999
|
-
const args = ['agents', 'stop', '--pid', String(pid)];
|
|
2000
|
-
if (session?.repoRoot) {
|
|
2001
|
-
args.push('--target', session.repoRoot);
|
|
2002
|
-
}
|
|
2003
|
-
await execFileAsync('gx', args, {
|
|
2004
|
-
cwd: commandCwd,
|
|
2005
|
-
encoding: 'utf8',
|
|
2006
|
-
maxBuffer: 1024 * 1024,
|
|
2007
|
-
});
|
|
2008
|
-
refresh();
|
|
2009
|
-
} catch (error) {
|
|
2010
|
-
showSessionMessage(
|
|
2011
|
-
`Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`,
|
|
2012
|
-
);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
async function dismissSession(session, refresh) {
|
|
2017
|
-
if (!canDismissSession(session)) {
|
|
2018
|
-
showSessionMessage('Only stalled or dead sessions can be dismissed.');
|
|
2019
|
-
return;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
const repoRoot = typeof session?.repoRoot === 'string' ? session.repoRoot.trim() : '';
|
|
2023
|
-
if (!repoRoot) {
|
|
2024
|
-
showSessionMessage('Cannot dismiss session: missing repo root.');
|
|
2025
|
-
return;
|
|
2026
|
-
}
|
|
2027
|
-
if (!session?.branch) {
|
|
2028
|
-
showSessionMessage('Cannot dismiss session: missing branch name.');
|
|
2029
|
-
return;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
const statePath = sessionFilePathForBranch(repoRoot, session.branch);
|
|
2033
|
-
if (!fs.existsSync(statePath)) {
|
|
2034
|
-
clearWorktreeActivityCache(session.worktreePath);
|
|
2035
|
-
refresh();
|
|
2036
|
-
showSessionMessage(`Session record already gone for ${sessionDisplayLabel(session)}.`);
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
const confirmed = await vscode.window.showWarningMessage(
|
|
2041
|
-
`Dismiss ${sessionDisplayLabel(session)}?`,
|
|
2042
|
-
{
|
|
2043
|
-
modal: true,
|
|
2044
|
-
detail: buildDismissSessionDetail(session, statePath),
|
|
2045
|
-
},
|
|
2046
|
-
'Dismiss',
|
|
2047
|
-
);
|
|
2048
|
-
if (confirmed !== 'Dismiss') {
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
try {
|
|
2053
|
-
fs.unlinkSync(statePath);
|
|
2054
|
-
clearWorktreeActivityCache(session.worktreePath);
|
|
2055
|
-
refresh();
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
showSessionMessage(`Failed to dismiss session ${sessionDisplayLabel(session)}: ${error.message}`);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function readGitDirPath(targetPath) {
|
|
2062
|
-
const normalizedTargetPath = typeof targetPath === 'string' ? targetPath.trim() : '';
|
|
2063
|
-
if (!normalizedTargetPath) {
|
|
2064
|
-
return '';
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
const gitPath = path.join(path.resolve(normalizedTargetPath), '.git');
|
|
2068
|
-
try {
|
|
2069
|
-
if (fs.statSync(gitPath).isDirectory()) {
|
|
2070
|
-
return gitPath;
|
|
2071
|
-
}
|
|
2072
|
-
} catch (_error) {
|
|
2073
|
-
return '';
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
try {
|
|
2077
|
-
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
2078
|
-
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
2079
|
-
if (match?.[1]) {
|
|
2080
|
-
return path.resolve(path.dirname(gitPath), match[1].trim());
|
|
2081
|
-
}
|
|
2082
|
-
} catch (_error) {
|
|
2083
|
-
return '';
|
|
2084
|
-
}
|
|
2085
|
-
|
|
2086
|
-
return '';
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
function resolveRepoRootFromGitDir(targetPath) {
|
|
2090
|
-
const gitDir = readGitDirPath(targetPath);
|
|
2091
|
-
if (!gitDir) {
|
|
2092
|
-
return '';
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
let commonDir = gitDir;
|
|
2096
|
-
try {
|
|
2097
|
-
const commonDirPath = path.join(gitDir, 'commondir');
|
|
2098
|
-
if (fs.existsSync(commonDirPath)) {
|
|
2099
|
-
const rawCommonDir = fs.readFileSync(commonDirPath, 'utf8').trim();
|
|
2100
|
-
if (rawCommonDir) {
|
|
2101
|
-
commonDir = path.resolve(gitDir, rawCommonDir);
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
} catch (_error) {
|
|
2105
|
-
// Fall back to the direct git dir when commondir is unreadable.
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
return path.basename(commonDir) === '.git'
|
|
2109
|
-
? path.resolve(path.dirname(commonDir))
|
|
2110
|
-
: '';
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
function readGitTopLevel(targetPath) {
|
|
2114
|
-
try {
|
|
2115
|
-
return cp.execFileSync('git', ['-C', targetPath, 'rev-parse', '--show-toplevel'], {
|
|
2116
|
-
encoding: 'utf8',
|
|
2117
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2118
|
-
}).trim();
|
|
2119
|
-
} catch (_error) {
|
|
2120
|
-
return '';
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
function resolveWorkspaceFolderRepoRoot(workspacePath) {
|
|
2125
|
-
const normalizedWorkspacePath = typeof workspacePath === 'string' ? workspacePath.trim() : '';
|
|
2126
|
-
if (!normalizedWorkspacePath) {
|
|
2127
|
-
return '';
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
const absoluteWorkspacePath = path.resolve(normalizedWorkspacePath);
|
|
2131
|
-
const directRepoRoot = resolveRepoRootFromGitDir(absoluteWorkspacePath);
|
|
2132
|
-
if (directRepoRoot) {
|
|
2133
|
-
return directRepoRoot;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
const gitTopLevel = readGitTopLevel(absoluteWorkspacePath);
|
|
2137
|
-
if (!gitTopLevel) {
|
|
2138
|
-
return absoluteWorkspacePath;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
return resolveRepoRootFromGitDir(gitTopLevel) || path.resolve(gitTopLevel);
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
function repoRootFromSessionFile(filePath) {
|
|
2145
|
-
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
function repoRootFromWorktreeLockFile(filePath) {
|
|
2149
|
-
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
function repoRootFromManagedWorktreeGitFile(filePath) {
|
|
2153
|
-
return path.resolve(path.dirname(filePath), '..', '..', '..');
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
function repoRootFromLockFile(filePath) {
|
|
2157
|
-
return path.resolve(path.dirname(filePath), '..', '..');
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function normalizeRelativePath(relativePath) {
|
|
2161
|
-
return String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
function emptyLockRegistry() {
|
|
2165
|
-
return {
|
|
2166
|
-
entriesByPath: new Map(),
|
|
2167
|
-
countsByBranch: new Map(),
|
|
2168
|
-
};
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
function readLockRegistry(repoRoot) {
|
|
2172
|
-
const lockPath = path.join(repoRoot, LOCK_FILE_RELATIVE);
|
|
2173
|
-
if (!fs.existsSync(lockPath)) {
|
|
2174
|
-
return emptyLockRegistry();
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
let parsed;
|
|
2178
|
-
try {
|
|
2179
|
-
parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
2180
|
-
} catch (_error) {
|
|
2181
|
-
return emptyLockRegistry();
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
const locks = parsed?.locks;
|
|
2185
|
-
if (!locks || typeof locks !== 'object' || Array.isArray(locks)) {
|
|
2186
|
-
return emptyLockRegistry();
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
const entriesByPath = new Map();
|
|
2190
|
-
const countsByBranch = new Map();
|
|
2191
|
-
for (const [rawRelativePath, entry] of Object.entries(locks)) {
|
|
2192
|
-
if (!entry || typeof entry !== 'object') {
|
|
2193
|
-
continue;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
const relativePath = normalizeRelativePath(rawRelativePath);
|
|
2197
|
-
const branch = typeof entry.branch === 'string' ? entry.branch.trim() : '';
|
|
2198
|
-
if (!relativePath || !branch) {
|
|
2199
|
-
continue;
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
entriesByPath.set(relativePath, {
|
|
2203
|
-
branch,
|
|
2204
|
-
claimedAt: typeof entry.claimed_at === 'string' ? entry.claimed_at : '',
|
|
2205
|
-
allowDelete: Boolean(entry.allow_delete),
|
|
2206
|
-
});
|
|
2207
|
-
countsByBranch.set(branch, (countsByBranch.get(branch) || 0) + 1);
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
return {
|
|
2211
|
-
entriesByPath,
|
|
2212
|
-
countsByBranch,
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
function readCurrentBranch(repoRoot) {
|
|
2217
|
-
try {
|
|
2218
|
-
return cp.execFileSync('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
2219
|
-
encoding: 'utf8',
|
|
2220
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2221
|
-
}).trim();
|
|
2222
|
-
} catch (_error) {
|
|
2223
|
-
return '';
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
function parseSimpleSemver(version) {
|
|
2228
|
-
const parts = String(version || '')
|
|
2229
|
-
.split('.')
|
|
2230
|
-
.map((part) => Number.parseInt(part, 10));
|
|
2231
|
-
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
|
|
2232
|
-
return null;
|
|
2233
|
-
}
|
|
2234
|
-
return parts;
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
function compareSimpleSemver(left, right) {
|
|
2238
|
-
const leftParts = parseSimpleSemver(left);
|
|
2239
|
-
const rightParts = parseSimpleSemver(right);
|
|
2240
|
-
if (!leftParts || !rightParts) {
|
|
2241
|
-
return 0;
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
for (let index = 0; index < leftParts.length; index += 1) {
|
|
2245
|
-
if (leftParts[index] !== rightParts[index]) {
|
|
2246
|
-
return leftParts[index] - rightParts[index];
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
return 0;
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
function readJsonFile(filePath) {
|
|
2254
|
-
try {
|
|
2255
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2256
|
-
} catch (_error) {
|
|
2257
|
-
return null;
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
function resolveActiveAgentsAutoUpdateCandidate(installedVersion) {
|
|
2262
|
-
const candidates = [];
|
|
2263
|
-
|
|
2264
|
-
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
2265
|
-
const repoRoot = workspaceFolder?.uri?.fsPath;
|
|
2266
|
-
if (!repoRoot) {
|
|
2267
|
-
continue;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE);
|
|
2271
|
-
const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE);
|
|
2272
|
-
if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) {
|
|
2273
|
-
continue;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
const manifest = readJsonFile(manifestPath);
|
|
2277
|
-
const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : '';
|
|
2278
|
-
if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) {
|
|
2279
|
-
continue;
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
candidates.push({ repoRoot, installScriptPath, version: nextVersion });
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
candidates.sort((left, right) => compareSimpleSemver(right.version, left.version));
|
|
2286
|
-
return candidates[0] || null;
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
function runActiveAgentsInstallScript(repoRoot, installScriptPath) {
|
|
2290
|
-
return new Promise((resolve, reject) => {
|
|
2291
|
-
cp.execFile(
|
|
2292
|
-
process.execPath,
|
|
2293
|
-
[installScriptPath],
|
|
2294
|
-
{ cwd: repoRoot, encoding: 'utf8' },
|
|
2295
|
-
(error, stdout, stderr) => {
|
|
2296
|
-
if (error) {
|
|
2297
|
-
reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed'));
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2300
|
-
resolve({ stdout, stderr });
|
|
2301
|
-
},
|
|
2302
|
-
);
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
async function maybeAutoUpdateActiveAgentsExtension(context) {
|
|
2307
|
-
const installedVersion = typeof context?.extension?.packageJSON?.version === 'string'
|
|
2308
|
-
? context.extension.packageJSON.version.trim()
|
|
2309
|
-
: '';
|
|
2310
|
-
if (!installedVersion) {
|
|
2311
|
-
return;
|
|
2312
|
-
}
|
|
2313
|
-
|
|
2314
|
-
const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion);
|
|
2315
|
-
if (!candidate) {
|
|
2316
|
-
return;
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
try {
|
|
2320
|
-
await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath);
|
|
2321
|
-
} catch (error) {
|
|
2322
|
-
const failure = typeof error?.message === 'string' && error.message.trim()
|
|
2323
|
-
? error.message.trim()
|
|
2324
|
-
: 'install failed';
|
|
2325
|
-
vscode.window.showWarningMessage?.(
|
|
2326
|
-
`GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`,
|
|
2327
|
-
);
|
|
2328
|
-
return;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
const selection = await vscode.window.showInformationMessage?.(
|
|
2332
|
-
`GitGuardex Active Agents updated to ${candidate.version}. Reload this window now, then reload any other already-open VS Code windows to use the newest companion.`,
|
|
2333
|
-
RELOAD_WINDOW_ACTION,
|
|
2334
|
-
UPDATE_LATER_ACTION,
|
|
2335
|
-
);
|
|
2336
|
-
if (selection === RELOAD_WINDOW_ACTION) {
|
|
2337
|
-
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
function decorateSession(session, lockRegistry) {
|
|
2342
|
-
const touchedChanges = buildSessionTouchedChanges(session, lockRegistry);
|
|
2343
|
-
const decorated = {
|
|
2344
|
-
...session,
|
|
2345
|
-
lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
|
|
2346
|
-
touchedChanges,
|
|
2347
|
-
conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length,
|
|
2348
|
-
};
|
|
2349
|
-
decorated.lastActiveAt = sessionLastActiveAt(decorated);
|
|
2350
|
-
decorated.lastActiveLabel = sessionLastActiveLabel(decorated);
|
|
2351
|
-
decorated.freshnessLabel = sessionFreshnessLabel(decorated);
|
|
2352
|
-
decorated.topChangedFiles = buildSessionTopFiles(decorated);
|
|
2353
|
-
decorated.topChangedFilesLabel = summarizeCompactPaths(decorated.topChangedFiles);
|
|
2354
|
-
decorated.recentChangeSummary = buildSessionRecentChangeSummary(decorated);
|
|
2355
|
-
decorated.riskBadges = sessionRiskBadges(decorated);
|
|
2356
|
-
return decorated;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
function decorateChange(change, lockRegistry, owningBranch) {
|
|
2360
|
-
const lockEntry = lockRegistry.entriesByPath.get(normalizeRelativePath(change.relativePath));
|
|
2361
|
-
const lockOwnerBranch = lockEntry?.branch || '';
|
|
2362
|
-
const decorated = {
|
|
2363
|
-
...change,
|
|
2364
|
-
lockOwnerBranch,
|
|
2365
|
-
hasForeignLock: Boolean(lockOwnerBranch) && (!owningBranch || lockOwnerBranch !== owningBranch),
|
|
2366
|
-
protectedBranch: isProtectedBranchName(owningBranch),
|
|
2367
|
-
};
|
|
2368
|
-
decorated.riskBadges = changeRiskBadges(decorated);
|
|
2369
|
-
return decorated;
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
function buildSessionTouchedChanges(session, lockRegistry) {
|
|
2373
|
-
const changedPaths = Array.isArray(session.worktreeChangedPaths)
|
|
2374
|
-
? session.worktreeChangedPaths
|
|
2375
|
-
: [];
|
|
2376
|
-
return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
2377
|
-
.map((relativePath) => {
|
|
2378
|
-
const lockEntry = lockRegistry.entriesByPath.get(relativePath);
|
|
2379
|
-
const lockOwnerBranch = lockEntry?.branch || '';
|
|
2380
|
-
return {
|
|
2381
|
-
relativePath,
|
|
2382
|
-
absolutePath: path.join(session.worktreePath, relativePath),
|
|
2383
|
-
originalPath: '',
|
|
2384
|
-
statusCode: 'M',
|
|
2385
|
-
statusLabel: 'M',
|
|
2386
|
-
statusText: 'Touched',
|
|
2387
|
-
lockOwnerBranch,
|
|
2388
|
-
hasForeignLock: Boolean(lockOwnerBranch) && lockOwnerBranch !== session.branch,
|
|
2389
|
-
};
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
function isPathWithin(parentPath, targetPath) {
|
|
2394
|
-
const relativePath = path.relative(parentPath, targetPath);
|
|
2395
|
-
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
function normalizeAbsolutePath(value) {
|
|
2399
|
-
return typeof value === 'string' && value.trim() ? path.resolve(value) : '';
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
function isManagedWorktreePath(worktreePath) {
|
|
2403
|
-
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
|
|
2404
|
-
if (!normalizedWorktreePath) {
|
|
2405
|
-
return false;
|
|
2406
|
-
}
|
|
2407
|
-
|
|
2408
|
-
return MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => {
|
|
2409
|
-
const normalizedRelativeRoot = path.normalize(relativeRoot);
|
|
2410
|
-
const marker = `${path.sep}${normalizedRelativeRoot}${path.sep}`;
|
|
2411
|
-
return normalizedWorktreePath.includes(marker);
|
|
2412
|
-
});
|
|
2413
|
-
}
|
|
2414
|
-
|
|
2415
|
-
function removeDeletedWorktreeWorkspaceFolder(worktreePath) {
|
|
2416
|
-
if (typeof vscode.workspace.updateWorkspaceFolders !== 'function') {
|
|
2417
|
-
return false;
|
|
2418
|
-
}
|
|
2419
|
-
|
|
2420
|
-
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
|
|
2421
|
-
if (!normalizedWorktreePath) {
|
|
2422
|
-
return false;
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
|
2426
|
-
const folderIndex = workspaceFolders.findIndex((folder) => (
|
|
2427
|
-
normalizeAbsolutePath(folder?.uri?.fsPath) === normalizedWorktreePath
|
|
2428
|
-
));
|
|
2429
|
-
if (folderIndex < 0) {
|
|
2430
|
-
return false;
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
try {
|
|
2434
|
-
return vscode.workspace.updateWorkspaceFolders(folderIndex, 1) === true;
|
|
2435
|
-
} catch (_error) {
|
|
2436
|
-
return false;
|
|
2437
|
-
}
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
async function closeDeletedWorktreeRepository(worktreePath) {
|
|
2441
|
-
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
|
|
2442
|
-
if (!normalizedWorktreePath || fs.existsSync(normalizedWorktreePath)) {
|
|
2443
|
-
return false;
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
try {
|
|
2447
|
-
await vscode.commands.executeCommand('git.close', vscode.Uri.file(normalizedWorktreePath));
|
|
2448
|
-
} catch (_error) {
|
|
2449
|
-
// The Git extension may have already removed this repository.
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
removeDeletedWorktreeWorkspaceFolder(normalizedWorktreePath);
|
|
2453
|
-
return true;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
function findDeletedManagedWorkspaceFolders() {
|
|
2457
|
-
return (vscode.workspace.workspaceFolders || [])
|
|
2458
|
-
.map((folder) => normalizeAbsolutePath(folder?.uri?.fsPath))
|
|
2459
|
-
.filter((workspacePath) => (
|
|
2460
|
-
workspacePath
|
|
2461
|
-
&& !fs.existsSync(workspacePath)
|
|
2462
|
-
&& isManagedWorktreePath(workspacePath)
|
|
2463
|
-
));
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
function localizeChangeForSession(session, change) {
|
|
2467
|
-
if (!change?.absolutePath || !isPathWithin(session.worktreePath, change.absolutePath)) {
|
|
2468
|
-
return null;
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
let originalPath = change.originalPath;
|
|
2472
|
-
if (originalPath) {
|
|
2473
|
-
const originalAbsolutePath = path.join(session.repoRoot, originalPath);
|
|
2474
|
-
if (isPathWithin(session.worktreePath, originalAbsolutePath)) {
|
|
2475
|
-
originalPath = normalizeRelativePath(path.relative(session.worktreePath, originalAbsolutePath));
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
return {
|
|
2480
|
-
...change,
|
|
2481
|
-
relativePath: normalizeRelativePath(path.relative(session.worktreePath, change.absolutePath)),
|
|
2482
|
-
originalPath,
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
async function findRepoSessionEntries() {
|
|
2487
|
-
const [sessionFiles, worktreeLockFiles, managedWorktreeGitFiles] = await Promise.all([
|
|
2488
|
-
vscode.workspace.findFiles(
|
|
2489
|
-
ACTIVE_SESSION_FILES_GLOB,
|
|
2490
|
-
SESSION_SCAN_EXCLUDE_GLOB,
|
|
2491
|
-
SESSION_SCAN_LIMIT,
|
|
2492
|
-
),
|
|
2493
|
-
vscode.workspace.findFiles(
|
|
2494
|
-
WORKTREE_AGENT_LOCKS_GLOB,
|
|
2495
|
-
WORKTREE_LOCK_SCAN_EXCLUDE_GLOB,
|
|
2496
|
-
SESSION_SCAN_LIMIT,
|
|
2497
|
-
),
|
|
2498
|
-
vscode.workspace.findFiles(
|
|
2499
|
-
MANAGED_WORKTREE_GIT_FILES_GLOB,
|
|
2500
|
-
MANAGED_WORKTREE_GIT_SCAN_EXCLUDE_GLOB,
|
|
2501
|
-
SESSION_SCAN_LIMIT,
|
|
2502
|
-
),
|
|
2503
|
-
]);
|
|
2504
|
-
|
|
2505
|
-
const repoRoots = new Set();
|
|
2506
|
-
const addRepoRootCandidate = (repoRoot) => {
|
|
2507
|
-
if (typeof repoRoot !== 'string' || !repoRoot.trim()) {
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
const normalizedRepoRoot = path.resolve(repoRoot);
|
|
2512
|
-
const isInsideWorkspaceManagedWorktree = (vscode.workspace.workspaceFolders || [])
|
|
2513
|
-
.map((folder) => (typeof folder?.uri?.fsPath === 'string' ? path.resolve(folder.uri.fsPath) : ''))
|
|
2514
|
-
.filter(Boolean)
|
|
2515
|
-
.some((workspaceRoot) => MANAGED_WORKTREE_RELATIVE_ROOTS.some((relativeRoot) => (
|
|
2516
|
-
isPathWithin(path.join(workspaceRoot, relativeRoot), normalizedRepoRoot)
|
|
2517
|
-
)));
|
|
2518
|
-
if (!isInsideWorkspaceManagedWorktree) {
|
|
2519
|
-
repoRoots.add(normalizedRepoRoot);
|
|
2520
|
-
}
|
|
2521
|
-
};
|
|
2522
|
-
|
|
2523
|
-
for (const uri of sessionFiles) {
|
|
2524
|
-
addRepoRootCandidate(repoRootFromSessionFile(uri.fsPath));
|
|
2525
|
-
}
|
|
2526
|
-
for (const uri of worktreeLockFiles) {
|
|
2527
|
-
if (path.basename(uri.fsPath) !== 'AGENT.lock') {
|
|
2528
|
-
continue;
|
|
2529
|
-
}
|
|
2530
|
-
addRepoRootCandidate(repoRootFromWorktreeLockFile(uri.fsPath));
|
|
2531
|
-
}
|
|
2532
|
-
for (const uri of managedWorktreeGitFiles) {
|
|
2533
|
-
if (path.basename(uri.fsPath) !== '.git') {
|
|
2534
|
-
continue;
|
|
2535
|
-
}
|
|
2536
|
-
addRepoRootCandidate(repoRootFromManagedWorktreeGitFile(uri.fsPath));
|
|
2537
|
-
}
|
|
2538
|
-
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
2539
|
-
if (workspaceFolder?.uri?.fsPath) {
|
|
2540
|
-
addRepoRootCandidate(resolveWorkspaceFolderRepoRoot(workspaceFolder.uri.fsPath));
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
const repoEntries = [];
|
|
2545
|
-
for (const repoRoot of repoRoots) {
|
|
2546
|
-
const sessions = readActiveSessions(repoRoot, { includeStale: true });
|
|
2547
|
-
if (sessions.length > 0) {
|
|
2548
|
-
repoEntries.push({ repoRoot, sessions });
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
repoEntries.sort((left, right) => left.repoRoot.localeCompare(right.repoRoot));
|
|
2553
|
-
return repoEntries;
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
function resolveSessionWatcherKey(session) {
|
|
2557
|
-
return `${path.resolve(session.repoRoot)}::${session.branch}::${path.resolve(session.worktreePath)}`;
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
function resolveSessionGitIndexPath(worktreePath) {
|
|
2561
|
-
const gitPath = path.join(worktreePath, '.git');
|
|
2562
|
-
const defaultIndexPath = path.join(gitPath, 'index');
|
|
2563
|
-
|
|
2564
|
-
try {
|
|
2565
|
-
if (fs.statSync(gitPath).isDirectory()) {
|
|
2566
|
-
return defaultIndexPath;
|
|
2567
|
-
}
|
|
2568
|
-
} catch (_error) {
|
|
2569
|
-
return defaultIndexPath;
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
try {
|
|
2573
|
-
const gitPointer = fs.readFileSync(gitPath, 'utf8');
|
|
2574
|
-
const match = gitPointer.match(/^gitdir:\s*(.+)$/m);
|
|
2575
|
-
if (match?.[1]) {
|
|
2576
|
-
return path.resolve(worktreePath, match[1].trim(), 'index');
|
|
2577
|
-
}
|
|
2578
|
-
} catch (_error) {
|
|
2579
|
-
return defaultIndexPath;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
return defaultIndexPath;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
function bindRefreshWatcher(watcher, refresh) {
|
|
2586
|
-
return [
|
|
2587
|
-
watcher.onDidCreate(refresh),
|
|
2588
|
-
watcher.onDidChange(refresh),
|
|
2589
|
-
watcher.onDidDelete(refresh),
|
|
2590
|
-
];
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
function disposeAll(disposables) {
|
|
2594
|
-
for (const disposable of disposables) {
|
|
2595
|
-
disposable?.dispose?.();
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
function buildChangeTreeNodes(changes) {
|
|
2600
|
-
const root = [];
|
|
2601
|
-
|
|
2602
|
-
function sortNodes(nodes) {
|
|
2603
|
-
nodes.sort((left, right) => {
|
|
2604
|
-
const leftIsFolder = left.kind === 'folder';
|
|
2605
|
-
const rightIsFolder = right.kind === 'folder';
|
|
2606
|
-
if (leftIsFolder !== rightIsFolder) {
|
|
2607
|
-
return leftIsFolder ? -1 : 1;
|
|
2608
|
-
}
|
|
2609
|
-
return left.label.localeCompare(right.label);
|
|
2610
|
-
});
|
|
2611
|
-
|
|
2612
|
-
for (const node of nodes) {
|
|
2613
|
-
if (node.kind === 'folder') {
|
|
2614
|
-
sortNodes(node.children);
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
|
-
for (const change of changes) {
|
|
2620
|
-
const segments = change.relativePath.split(/[\\/]+/).filter(Boolean);
|
|
2621
|
-
if (segments.length <= 1) {
|
|
2622
|
-
root.push({ kind: 'change', label: change.relativePath, change });
|
|
2623
|
-
continue;
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
let nodes = root;
|
|
2627
|
-
let folderPath = '';
|
|
2628
|
-
for (const segment of segments.slice(0, -1)) {
|
|
2629
|
-
folderPath = folderPath ? path.posix.join(folderPath, segment) : segment;
|
|
2630
|
-
let folderNode = nodes.find((node) => node.kind === 'folder' && node.relativePath === folderPath);
|
|
2631
|
-
if (!folderNode) {
|
|
2632
|
-
folderNode = {
|
|
2633
|
-
kind: 'folder',
|
|
2634
|
-
label: segment,
|
|
2635
|
-
relativePath: folderPath,
|
|
2636
|
-
children: [],
|
|
2637
|
-
};
|
|
2638
|
-
nodes.push(folderNode);
|
|
2639
|
-
}
|
|
2640
|
-
nodes = folderNode.children;
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
nodes.push({ kind: 'change', label: change.relativePath, change });
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
sortNodes(root);
|
|
2647
|
-
|
|
2648
|
-
function materialize(nodes) {
|
|
2649
|
-
return nodes.map((node) => {
|
|
2650
|
-
if (node.kind === 'folder') {
|
|
2651
|
-
return new FolderItem(node.label, node.relativePath, materialize(node.children));
|
|
2652
|
-
}
|
|
2653
|
-
return new ChangeItem(node.change);
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
return materialize(root);
|
|
2658
|
-
}
|
|
2659
|
-
|
|
2660
|
-
function countChangedPaths(repoRoot, sessions, changes) {
|
|
2661
|
-
const changedKeys = new Set();
|
|
2662
|
-
|
|
2663
|
-
for (const change of changes || []) {
|
|
2664
|
-
if (change?.relativePath) {
|
|
2665
|
-
changedKeys.add(normalizeRelativePath(change.relativePath));
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
for (const session of sessions || []) {
|
|
2670
|
-
for (const change of session.touchedChanges || []) {
|
|
2671
|
-
const absolutePath = change?.absolutePath
|
|
2672
|
-
|| path.join(session.worktreePath || '', change?.relativePath || '');
|
|
2673
|
-
const normalizedRelativePath = absolutePath && isPathWithin(repoRoot, absolutePath)
|
|
2674
|
-
? normalizeRelativePath(path.relative(repoRoot, absolutePath))
|
|
2675
|
-
: `${session.branch}:${normalizeRelativePath(change?.relativePath)}`;
|
|
2676
|
-
if (normalizedRelativePath) {
|
|
2677
|
-
changedKeys.add(normalizedRelativePath);
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
return changedKeys.size;
|
|
2683
|
-
}
|
|
2684
|
-
|
|
2685
|
-
function buildRepoOverview(sessions, unassignedChanges, lockEntries, colonyTasks = []) {
|
|
2686
|
-
const colonyTaskList = Array.isArray(colonyTasks) ? colonyTasks : [];
|
|
2687
|
-
return {
|
|
2688
|
-
sessionCount: sessions.length,
|
|
2689
|
-
workingCount: countWorkingSessions(sessions),
|
|
2690
|
-
finishedCount: countFinishedSessions(sessions),
|
|
2691
|
-
idleCount: countIdleSessions(sessions),
|
|
2692
|
-
unassignedChangeCount: (unassignedChanges || []).length,
|
|
2693
|
-
lockedFileCount: Array.isArray(lockEntries) ? lockEntries.length : 0,
|
|
2694
|
-
conflictCount: sessions.reduce(
|
|
2695
|
-
(total, session) => total + (session.conflictCount || 0),
|
|
2696
|
-
0,
|
|
2697
|
-
) + (unassignedChanges || []).filter((change) => change.hasForeignLock).length,
|
|
2698
|
-
colonyTaskCount: colonyTaskList.length,
|
|
2699
|
-
pendingHandoffCount: colonyTaskList.reduce(
|
|
2700
|
-
(total, task) => total + (task.pending_handoff_count || 0),
|
|
2701
|
-
0,
|
|
2702
|
-
),
|
|
2703
|
-
};
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
function groupSessionsByWorktree(sessions) {
|
|
2707
|
-
const sessionsByWorktree = new Map();
|
|
2708
|
-
|
|
2709
|
-
for (const session of sessions || []) {
|
|
2710
|
-
const worktreePath = sessionWorktreePath(session);
|
|
2711
|
-
const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`;
|
|
2712
|
-
if (!sessionsByWorktree.has(key)) {
|
|
2713
|
-
sessionsByWorktree.set(key, {
|
|
2714
|
-
worktreePath,
|
|
2715
|
-
sessions: [],
|
|
2716
|
-
});
|
|
2717
|
-
}
|
|
2718
|
-
sessionsByWorktree.get(key).sessions.push(session);
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
return [...sessionsByWorktree.values()]
|
|
2722
|
-
.map((entry) => ({
|
|
2723
|
-
...entry,
|
|
2724
|
-
sessions: entry.sessions.sort((left, right) => (
|
|
2725
|
-
sessionTreeLabel(left).localeCompare(sessionTreeLabel(right))
|
|
2726
|
-
)),
|
|
2727
|
-
}))
|
|
2728
|
-
.sort((left, right) => {
|
|
2729
|
-
const leftLabel = path.basename(left.worktreePath || '') || '';
|
|
2730
|
-
const rightLabel = path.basename(right.worktreePath || '') || '';
|
|
2731
|
-
return leftLabel.localeCompare(rightLabel)
|
|
2732
|
-
|| (left.worktreePath || '').localeCompare(right.worktreePath || '');
|
|
2733
|
-
});
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
function partitionChangesByOwnership(sessions, changes) {
|
|
2737
|
-
const changesBySession = new Map();
|
|
2738
|
-
const sessionByChangedPath = new Map();
|
|
2739
|
-
const repoRootChanges = [];
|
|
2740
|
-
|
|
2741
|
-
for (const session of sessions) {
|
|
2742
|
-
changesBySession.set(session.branch, []);
|
|
2743
|
-
for (const changedPath of session.changedPaths || []) {
|
|
2744
|
-
if (!sessionByChangedPath.has(changedPath)) {
|
|
2745
|
-
sessionByChangedPath.set(changedPath, session);
|
|
2746
|
-
}
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
for (const change of changes) {
|
|
2751
|
-
const normalizedRelativePath = normalizeRelativePath(change.relativePath);
|
|
2752
|
-
const session = sessionByChangedPath.get(normalizedRelativePath)
|
|
2753
|
-
|| sessions.find((candidate) => isPathWithin(candidate.worktreePath, change.absolutePath));
|
|
2754
|
-
if (!session) {
|
|
2755
|
-
repoRootChanges.push(change);
|
|
2756
|
-
continue;
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
const localizedChange = localizeChangeForSession(session, change);
|
|
2760
|
-
if (!localizedChange) {
|
|
2761
|
-
repoRootChanges.push(change);
|
|
2762
|
-
continue;
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
changesBySession.get(session.branch).push(localizedChange);
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
return {
|
|
2769
|
-
changesBySession,
|
|
2770
|
-
repoRootChanges,
|
|
2771
|
-
};
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
2775
|
-
const { changesBySession, repoRootChanges } = partitionChangesByOwnership(sessions, changes);
|
|
2776
|
-
|
|
2777
|
-
const items = buildProjectScopedItems(
|
|
2778
|
-
groupSessionsByWorktree(
|
|
2779
|
-
sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0),
|
|
2780
|
-
).map(({ worktreePath, sessions: worktreeSessions }) => {
|
|
2781
|
-
const sessionItems = worktreeSessions.map((session) => (
|
|
2782
|
-
new SessionItem(
|
|
2783
|
-
session,
|
|
2784
|
-
buildChangeTreeNodes(changesBySession.get(session.branch) || []),
|
|
2785
|
-
{
|
|
2786
|
-
label: sessionTreeLabel(session),
|
|
2787
|
-
variant: 'raw',
|
|
2788
|
-
},
|
|
2789
|
-
)
|
|
2790
|
-
));
|
|
2791
|
-
const changedCount = worktreeSessions.reduce(
|
|
2792
|
-
(total, session) => total + ((changesBySession.get(session.branch) || []).length),
|
|
2793
|
-
0,
|
|
2794
|
-
);
|
|
2795
|
-
return {
|
|
2796
|
-
projectRelativePath: worktreeProjectRelativePath(worktreeSessions),
|
|
2797
|
-
sessions: worktreeSessions,
|
|
2798
|
-
item: new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount }),
|
|
2799
|
-
};
|
|
2800
|
-
}),
|
|
2801
|
-
);
|
|
2802
|
-
|
|
2803
|
-
if (repoRootChanges.length > 0) {
|
|
2804
|
-
items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
|
|
2805
|
-
description: String(repoRootChanges.length),
|
|
2806
|
-
}));
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
return items;
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
function countActiveSessions(sessions) {
|
|
2813
|
-
return sessions.filter((session) => session.activityKind !== 'dead').length;
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
function countSessionsByActivityKind(sessions, activityKind) {
|
|
2817
|
-
return sessions.filter((session) => session.activityKind === activityKind).length;
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
function resolveSessionActivityIconId(activityKind) {
|
|
2821
|
-
return SESSION_ACTIVITY_ICON_IDS[activityKind] || 'loading~spin';
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
async function pickRepoRoot() {
|
|
2825
|
-
const workspaceFolders = vscode.workspace.workspaceFolders || [];
|
|
2826
|
-
if (workspaceFolders.length === 0) {
|
|
2827
|
-
vscode.window.showInformationMessage?.('Open a Guardex workspace folder to start an agent.');
|
|
2828
|
-
return null;
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
if (workspaceFolders.length === 1) {
|
|
2832
|
-
return workspaceFolders[0].uri.fsPath;
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
const picks = workspaceFolders.map((folder) => ({
|
|
2836
|
-
label: path.basename(folder.uri.fsPath),
|
|
2837
|
-
description: folder.uri.fsPath,
|
|
2838
|
-
repoRoot: folder.uri.fsPath,
|
|
2839
|
-
}));
|
|
2840
|
-
const selection = await vscode.window.showQuickPick?.(picks, {
|
|
2841
|
-
placeHolder: 'Select the Guardex repo where the Start agent launcher should run.',
|
|
2842
|
-
});
|
|
2843
|
-
return selection?.repoRoot || null;
|
|
2844
|
-
}
|
|
2845
|
-
|
|
2846
|
-
async function promptStartAgentDetails() {
|
|
2847
|
-
const taskName = await vscode.window.showInputBox?.({
|
|
2848
|
-
prompt: 'Task for the Guardex agent launcher',
|
|
2849
|
-
placeHolder: 'vscode active agents welcome view',
|
|
2850
|
-
ignoreFocusOut: true,
|
|
2851
|
-
validateInput: (value) => value.trim() ? undefined : 'Task is required.',
|
|
2852
|
-
});
|
|
2853
|
-
if (!taskName) {
|
|
2854
|
-
return null;
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
const agentName = await vscode.window.showInputBox?.({
|
|
2858
|
-
prompt: 'Agent name for the Guardex agent launcher',
|
|
2859
|
-
placeHolder: 'codex',
|
|
2860
|
-
value: 'codex',
|
|
2861
|
-
ignoreFocusOut: true,
|
|
2862
|
-
validateInput: (value) => value.trim() ? undefined : 'Agent name is required.',
|
|
2863
|
-
});
|
|
2864
|
-
if (!agentName) {
|
|
2865
|
-
return null;
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
|
-
return {
|
|
2869
|
-
taskName: taskName.trim(),
|
|
2870
|
-
agentName: agentName.trim(),
|
|
2871
|
-
};
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
async function startAgentFromPrompt(refresh) {
|
|
2875
|
-
const repoRoot = await pickRepoRoot();
|
|
2876
|
-
if (!repoRoot) {
|
|
2877
|
-
return;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
const details = await promptStartAgentDetails();
|
|
2881
|
-
if (!details) {
|
|
2882
|
-
return;
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
const terminal = vscode.window.createTerminal?.({
|
|
2886
|
-
name: `GitGuardex: ${path.basename(repoRoot)}`,
|
|
2887
|
-
cwd: repoRoot,
|
|
2888
|
-
});
|
|
2889
|
-
terminal?.show(true);
|
|
2890
|
-
terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true);
|
|
2891
|
-
refresh();
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
function sessionSelectionKey(session) {
|
|
2895
|
-
if (!session?.repoRoot || !session?.branch) {
|
|
2896
|
-
return '';
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
return `${session.repoRoot}::${session.branch}`;
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
function formatGitCommandFailure(error) {
|
|
2903
|
-
for (const value of [error?.stderr, error?.stdout, error?.message]) {
|
|
2904
|
-
if (typeof value === 'string' && value.trim().length > 0) {
|
|
2905
|
-
return value.trim();
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
return 'Git command failed.';
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
function runGitCommand(worktreePath, args) {
|
|
2912
|
-
return cp.execFileSync('git', ['-C', worktreePath, ...args], {
|
|
2913
|
-
encoding: 'utf8',
|
|
2914
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2915
|
-
});
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
function stageWorktreeForCommit(worktreePath) {
|
|
2919
|
-
runGitCommand(worktreePath, ['add', '-A', '--', '.', `:(exclude)${LOCK_FILE_RELATIVE}`]);
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
function commitWorktree(worktreePath, message) {
|
|
2923
|
-
runGitCommand(worktreePath, ['commit', '-m', message]);
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
function buildSessionDetailItems(session) {
|
|
2927
|
-
const provider = resolveSessionProvider(session);
|
|
2928
|
-
const snapshot = sessionSnapshotDisplayName(session);
|
|
2929
|
-
const projectRelativePath = resolveSessionProjectRelativePath(session);
|
|
2930
|
-
const badgeSummary = uniqueStringList([
|
|
2931
|
-
...(session.riskBadges || []),
|
|
2932
|
-
session.deltaLabel || '',
|
|
2933
|
-
].filter(Boolean)).join(', ');
|
|
2934
|
-
const sessionHealthSummary = buildSessionHealthSummary(session);
|
|
2935
|
-
const items = [
|
|
2936
|
-
new DetailItem('Recent change', session.recentChangeSummary || 'No recent change summary.', {
|
|
2937
|
-
iconId: 'history',
|
|
2938
|
-
}),
|
|
2939
|
-
new DetailItem('Top files', session.topChangedFilesLabel || 'No tracked file edits.', {
|
|
2940
|
-
iconId: 'list-flat',
|
|
2941
|
-
}),
|
|
2942
|
-
];
|
|
2943
|
-
if (badgeSummary) {
|
|
2944
|
-
items.push(new DetailItem('Signals', badgeSummary, {
|
|
2945
|
-
iconId: 'warning',
|
|
2946
|
-
}));
|
|
2947
|
-
}
|
|
2948
|
-
if (sessionHealthSummary) {
|
|
2949
|
-
items.push(new DetailItem('Session health', sessionHealthSummary, {
|
|
2950
|
-
iconId: 'pulse',
|
|
2951
|
-
tooltip: buildSessionHealthTooltip(session) || sessionHealthSummary,
|
|
2952
|
-
}));
|
|
2953
|
-
}
|
|
2954
|
-
if (provider?.label) {
|
|
2955
|
-
items.push(new DetailItem('Provider', provider.label, {
|
|
2956
|
-
iconId: 'rocket',
|
|
2957
|
-
}));
|
|
2958
|
-
}
|
|
2959
|
-
if (snapshot) {
|
|
2960
|
-
items.push(new DetailItem('Snapshot', snapshot, {
|
|
2961
|
-
iconId: 'device-camera',
|
|
2962
|
-
}));
|
|
2963
|
-
}
|
|
2964
|
-
if (projectRelativePath) {
|
|
2965
|
-
items.push(new DetailItem('Project', projectRelativePath, {
|
|
2966
|
-
iconId: 'folder',
|
|
2967
|
-
tooltip: projectRelativePath,
|
|
2968
|
-
}));
|
|
2969
|
-
}
|
|
2970
|
-
items.push(new DetailItem('Branch', session.branch, {
|
|
2971
|
-
iconId: 'git-branch',
|
|
2972
|
-
}));
|
|
2973
|
-
items.push(new DetailItem('Worktree', session.worktreePath, {
|
|
2974
|
-
iconId: 'folder-library',
|
|
2975
|
-
tooltip: session.worktreePath,
|
|
2976
|
-
}));
|
|
2977
|
-
return items;
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
function buildWorkingNowNodes(sessions) {
|
|
2981
|
-
const sessionEntries = sortSessionsForWorkingNow(
|
|
2982
|
-
sessions.filter((session) => (
|
|
2983
|
-
session.activityKind === 'working' || session.activityKind === 'blocked'
|
|
2984
|
-
)),
|
|
2985
|
-
).map((session) => ({
|
|
2986
|
-
projectRelativePath: resolveSessionProjectRelativePath(session),
|
|
2987
|
-
sessions: [session],
|
|
2988
|
-
item: new SessionItem(session, buildSessionDetailItems(session)),
|
|
2989
|
-
}));
|
|
2990
|
-
return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' });
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
function buildIdleThinkingNodes(sessions) {
|
|
2994
|
-
const sessionEntries = sortSessionsForIdleThinking(
|
|
2995
|
-
sessions.filter((session) => !(
|
|
2996
|
-
session.activityKind === 'working'
|
|
2997
|
-
|| session.activityKind === 'blocked'
|
|
2998
|
-
|| session.activityKind === 'finished'
|
|
2999
|
-
)),
|
|
3000
|
-
).map((session) => ({
|
|
3001
|
-
projectRelativePath: resolveSessionProjectRelativePath(session),
|
|
3002
|
-
sessions: [session],
|
|
3003
|
-
item: new SessionItem(session, buildSessionDetailItems(session)),
|
|
3004
|
-
}));
|
|
3005
|
-
return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' });
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
function buildNeedsCleanupNodes(sessions) {
|
|
3009
|
-
const sessionEntries = sessions
|
|
3010
|
-
.filter((session) => session.activityKind === 'finished')
|
|
3011
|
-
.map((session) => ({
|
|
3012
|
-
projectRelativePath: resolveSessionProjectRelativePath(session),
|
|
3013
|
-
sessions: [session],
|
|
3014
|
-
item: new SessionItem(session, buildSessionDetailItems(session)),
|
|
3015
|
-
}));
|
|
3016
|
-
return buildProjectScopedItems(sessionEntries, { rootLabel: 'Repo root' });
|
|
3017
|
-
}
|
|
3018
|
-
|
|
3019
|
-
function buildUnassignedChangeNodes(changes) {
|
|
3020
|
-
return sortUnassignedChanges(changes).map((change) => new ChangeItem(change, {
|
|
3021
|
-
label: compactRelativePath(change.relativePath),
|
|
3022
|
-
description: buildUnassignedChangeDescription(change),
|
|
3023
|
-
iconId: changeNeedsWarningIcon(change) ? 'warning' : undefined,
|
|
3024
|
-
}));
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
function buildRawActiveAgentGroupNodes(sessions) {
|
|
3028
|
-
const groups = [];
|
|
3029
|
-
for (const group of SESSION_ACTIVITY_GROUPS) {
|
|
3030
|
-
const groupSessions = sessions.filter((session) => session.activityKind === group.kind);
|
|
3031
|
-
const worktreeItems = buildProjectScopedItems(
|
|
3032
|
-
groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => ({
|
|
3033
|
-
projectRelativePath: worktreeProjectRelativePath(worktreeSessions),
|
|
3034
|
-
sessions: worktreeSessions,
|
|
3035
|
-
item: new WorktreeItem(
|
|
3036
|
-
worktreePath,
|
|
3037
|
-
worktreeSessions,
|
|
3038
|
-
worktreeSessions.map((session) => new SessionItem(
|
|
3039
|
-
session,
|
|
3040
|
-
buildChangeTreeNodes(session.touchedChanges || []),
|
|
3041
|
-
{
|
|
3042
|
-
label: sessionTreeLabel(session),
|
|
3043
|
-
variant: 'raw',
|
|
3044
|
-
},
|
|
3045
|
-
)),
|
|
3046
|
-
{
|
|
3047
|
-
description: buildWorktreeBranchDescription(worktreeSessions),
|
|
3048
|
-
iconId: 'git-branch',
|
|
3049
|
-
resourceSession: worktreeSessions[0],
|
|
3050
|
-
useSessionDecoration: true,
|
|
3051
|
-
},
|
|
3052
|
-
),
|
|
3053
|
-
})),
|
|
3054
|
-
{ rootLabel: 'Repo root' },
|
|
3055
|
-
);
|
|
3056
|
-
if (worktreeItems.length > 0) {
|
|
3057
|
-
groups.push(new SectionItem(group.label, worktreeItems, {
|
|
3058
|
-
iconId: resolveSessionActivityIconId(group.kind),
|
|
3059
|
-
}));
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
|
|
3063
|
-
return groups;
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
class ActiveAgentsProvider {
|
|
3067
|
-
constructor(decorationProvider) {
|
|
3068
|
-
this.decorationProvider = decorationProvider;
|
|
3069
|
-
this.onDidChangeTreeDataEmitter = new vscode.EventEmitter();
|
|
3070
|
-
this.onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
|
|
3071
|
-
this.onDidChangeSelectedSessionEmitter = new vscode.EventEmitter();
|
|
3072
|
-
this.onDidChangeSelectedSession = this.onDidChangeSelectedSessionEmitter.event;
|
|
3073
|
-
this.treeView = null;
|
|
3074
|
-
this.lockRegistryByRepoRoot = new Map();
|
|
3075
|
-
this.selectedSession = null;
|
|
3076
|
-
this.viewSummary = {
|
|
3077
|
-
sessionCount: 0,
|
|
3078
|
-
workingCount: 0,
|
|
3079
|
-
finishedCount: 0,
|
|
3080
|
-
idleCount: 0,
|
|
3081
|
-
unassignedChangeCount: 0,
|
|
3082
|
-
lockedFileCount: 0,
|
|
3083
|
-
deadCount: 0,
|
|
3084
|
-
conflictCount: 0,
|
|
3085
|
-
};
|
|
3086
|
-
this.previousSnapshot = null;
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
getTreeItem(element) {
|
|
3090
|
-
return element;
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
attachTreeView(treeView) {
|
|
3094
|
-
this.treeView = treeView;
|
|
3095
|
-
this.updateViewState({
|
|
3096
|
-
sessionCount: 0,
|
|
3097
|
-
workingCount: 0,
|
|
3098
|
-
finishedCount: 0,
|
|
3099
|
-
idleCount: 0,
|
|
3100
|
-
unassignedChangeCount: 0,
|
|
3101
|
-
lockedFileCount: 0,
|
|
3102
|
-
deadCount: 0,
|
|
3103
|
-
conflictCount: 0,
|
|
3104
|
-
});
|
|
3105
|
-
treeView.onDidChangeSelection?.((event) => {
|
|
3106
|
-
const sessionItem = event.selection.find((item) => item instanceof SessionItem);
|
|
3107
|
-
this.setSelectedSession(sessionItem?.session || null);
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
setSelectedSession(session) {
|
|
3112
|
-
const nextSession = session?.worktreePath ? { ...session } : null;
|
|
3113
|
-
const currentKey = sessionSelectionKey(this.selectedSession);
|
|
3114
|
-
const nextKey = sessionSelectionKey(nextSession);
|
|
3115
|
-
this.selectedSession = nextSession;
|
|
3116
|
-
this.decorationProvider?.setSelectedBranch(nextSession?.branch || '');
|
|
3117
|
-
if (currentKey !== nextKey) {
|
|
3118
|
-
this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
getSelectedSession() {
|
|
3123
|
-
return this.selectedSession ? { ...this.selectedSession } : null;
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
getViewSummary() {
|
|
3127
|
-
return { ...this.viewSummary };
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
syncSelectedSession(repoEntries) {
|
|
3131
|
-
if (!this.selectedSession) {
|
|
3132
|
-
return;
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
const nextSession = repoEntries
|
|
3136
|
-
.flatMap((entry) => entry.sessions)
|
|
3137
|
-
.find((session) => sessionSelectionKey(session) === sessionSelectionKey(this.selectedSession));
|
|
3138
|
-
this.setSelectedSession(nextSession || null);
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
|
-
updateViewState(summary) {
|
|
3142
|
-
if (!this.treeView) {
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
const sessionCount = summary?.sessionCount || 0;
|
|
3147
|
-
const conflictCount = summary?.conflictCount || 0;
|
|
3148
|
-
this.viewSummary = { ...summary };
|
|
3149
|
-
void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0);
|
|
3150
|
-
void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0);
|
|
3151
|
-
|
|
3152
|
-
this.treeView.badge = sessionCount > 0
|
|
3153
|
-
? {
|
|
3154
|
-
value: sessionCount,
|
|
3155
|
-
tooltip: buildOverviewDescription(summary),
|
|
3156
|
-
}
|
|
3157
|
-
: undefined;
|
|
3158
|
-
this.treeView.message = undefined;
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
annotateRepoEntries(repoEntries) {
|
|
3162
|
-
const hasPreviousSnapshot = Boolean(this.previousSnapshot);
|
|
3163
|
-
const nextSnapshot = {
|
|
3164
|
-
sessions: new Map(),
|
|
3165
|
-
changes: new Map(),
|
|
3166
|
-
};
|
|
3167
|
-
|
|
3168
|
-
const annotatedEntries = repoEntries.map((entry) => {
|
|
3169
|
-
const sessions = entry.sessions.map((session) => {
|
|
3170
|
-
const snapshotKey = sessionSnapshotKey(session);
|
|
3171
|
-
nextSnapshot.sessions.set(snapshotKey, buildSessionSnapshot(session));
|
|
3172
|
-
const deltaLabel = hasPreviousSnapshot
|
|
3173
|
-
? deriveSessionDelta(this.previousSnapshot.sessions.get(snapshotKey), session)
|
|
3174
|
-
: '';
|
|
3175
|
-
return {
|
|
3176
|
-
...session,
|
|
3177
|
-
deltaLabel,
|
|
3178
|
-
riskBadges: uniqueStringList([
|
|
3179
|
-
...(session.riskBadges || []),
|
|
3180
|
-
deltaLabel,
|
|
3181
|
-
].filter(Boolean)),
|
|
3182
|
-
};
|
|
3183
|
-
});
|
|
3184
|
-
|
|
3185
|
-
const changes = entry.changes.map((change) => {
|
|
3186
|
-
const snapshotKey = changeSnapshotKey(entry.repoRoot, change);
|
|
3187
|
-
nextSnapshot.changes.set(snapshotKey, buildChangeSnapshot(change));
|
|
3188
|
-
const deltaLabel = hasPreviousSnapshot
|
|
3189
|
-
? deriveChangeDelta(this.previousSnapshot.changes.get(snapshotKey), change)
|
|
3190
|
-
: '';
|
|
3191
|
-
return {
|
|
3192
|
-
...change,
|
|
3193
|
-
deltaLabel,
|
|
3194
|
-
riskBadges: changeRiskBadges({
|
|
3195
|
-
...change,
|
|
3196
|
-
deltaLabel,
|
|
3197
|
-
}),
|
|
3198
|
-
};
|
|
3199
|
-
});
|
|
3200
|
-
|
|
3201
|
-
const { repoRootChanges } = partitionChangesByOwnership(sessions, changes);
|
|
3202
|
-
const unassignedChanges = sortUnassignedChanges(repoRootChanges);
|
|
3203
|
-
const colonyTasks = Array.isArray(entry.colonyTasks) ? entry.colonyTasks : [];
|
|
3204
|
-
return {
|
|
3205
|
-
...entry,
|
|
3206
|
-
sessions,
|
|
3207
|
-
changes,
|
|
3208
|
-
unassignedChanges,
|
|
3209
|
-
colonyTasks,
|
|
3210
|
-
overview: buildRepoOverview(sessions, unassignedChanges, entry.lockEntries, colonyTasks),
|
|
3211
|
-
};
|
|
3212
|
-
});
|
|
3213
|
-
|
|
3214
|
-
this.previousSnapshot = nextSnapshot;
|
|
3215
|
-
return annotatedEntries;
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
async syncRepoEntries() {
|
|
3219
|
-
const repoEntries = this.annotateRepoEntries(await this.loadRepoEntries());
|
|
3220
|
-
const summary = {
|
|
3221
|
-
sessionCount: repoEntries.reduce((total, entry) => total + entry.sessions.length, 0),
|
|
3222
|
-
workingCount: repoEntries.reduce((total, entry) => total + entry.overview.workingCount, 0),
|
|
3223
|
-
finishedCount: repoEntries.reduce(
|
|
3224
|
-
(total, entry) => total + (entry.overview.finishedCount || 0),
|
|
3225
|
-
0,
|
|
3226
|
-
),
|
|
3227
|
-
idleCount: repoEntries.reduce((total, entry) => total + entry.overview.idleCount, 0),
|
|
3228
|
-
unassignedChangeCount: repoEntries.reduce(
|
|
3229
|
-
(total, entry) => total + entry.overview.unassignedChangeCount,
|
|
3230
|
-
0,
|
|
3231
|
-
),
|
|
3232
|
-
lockedFileCount: repoEntries.reduce((total, entry) => total + entry.overview.lockedFileCount, 0),
|
|
3233
|
-
deadCount: repoEntries.reduce(
|
|
3234
|
-
(total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
|
|
3235
|
-
0,
|
|
3236
|
-
),
|
|
3237
|
-
conflictCount: repoEntries.reduce((total, entry) => total + entry.overview.conflictCount, 0),
|
|
3238
|
-
};
|
|
3239
|
-
|
|
3240
|
-
this.updateViewState(summary);
|
|
3241
|
-
this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
|
|
3242
|
-
this.decorationProvider?.updateLockEntries(repoEntries);
|
|
3243
|
-
return repoEntries;
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3246
|
-
async refresh() {
|
|
3247
|
-
await this.syncRepoEntries();
|
|
3248
|
-
this.onDidChangeTreeDataEmitter.fire();
|
|
3249
|
-
this.decorationProvider?.refresh();
|
|
3250
|
-
}
|
|
3251
|
-
|
|
3252
|
-
readLockRegistryForRepo(repoRoot) {
|
|
3253
|
-
const lockRegistry = readLockRegistry(repoRoot);
|
|
3254
|
-
this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
|
|
3255
|
-
return lockRegistry;
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
getLockRegistryForRepo(repoRoot) {
|
|
3259
|
-
return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
refreshLockRegistryForFile(filePath) {
|
|
3263
|
-
this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
async getChildren(element) {
|
|
3267
|
-
if (element instanceof RepoItem) {
|
|
3268
|
-
const sectionItems = [
|
|
3269
|
-
new SectionItem('Overview', [
|
|
3270
|
-
new DetailItem('Summary', buildOverviewDescription(element.overview), {
|
|
3271
|
-
iconId: 'dashboard',
|
|
3272
|
-
tooltip: buildRepoTooltip(element.repoRoot, element.overview),
|
|
3273
|
-
}),
|
|
3274
|
-
], {
|
|
3275
|
-
description: '1',
|
|
3276
|
-
iconId: 'telescope',
|
|
3277
|
-
}),
|
|
3278
|
-
];
|
|
3279
|
-
|
|
3280
|
-
const workingNowItems = buildWorkingNowNodes(element.sessions);
|
|
3281
|
-
if (workingNowItems.length > 0) {
|
|
3282
|
-
sectionItems.push(new SectionItem('Working now', workingNowItems, {
|
|
3283
|
-
description: String(workingNowItems.length),
|
|
3284
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3285
|
-
iconId: 'loading~spin',
|
|
3286
|
-
}));
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
const needsCleanupItems = buildNeedsCleanupNodes(element.sessions);
|
|
3290
|
-
if (needsCleanupItems.length > 0) {
|
|
3291
|
-
sectionItems.push(new SectionItem('Needs cleanup', needsCleanupItems, {
|
|
3292
|
-
description: String(needsCleanupItems.length),
|
|
3293
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3294
|
-
iconId: 'pass-filled',
|
|
3295
|
-
}));
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
const idleThinkingItems = buildIdleThinkingNodes(element.sessions);
|
|
3299
|
-
if (idleThinkingItems.length > 0) {
|
|
3300
|
-
sectionItems.push(new SectionItem('Idle / thinking', idleThinkingItems, {
|
|
3301
|
-
description: String(idleThinkingItems.length),
|
|
3302
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3303
|
-
iconId: 'debug-pause',
|
|
3304
|
-
}));
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
if (element.unassignedChanges.length > 0) {
|
|
3308
|
-
sectionItems.push(new SectionItem('Unassigned changes', buildUnassignedChangeNodes(element.unassignedChanges), {
|
|
3309
|
-
description: String(element.unassignedChanges.length),
|
|
3310
|
-
iconId: 'inbox',
|
|
3311
|
-
}));
|
|
3312
|
-
}
|
|
3313
|
-
|
|
3314
|
-
const advancedItems = [];
|
|
3315
|
-
const rawActiveAgents = buildRawActiveAgentGroupNodes(element.sessions);
|
|
3316
|
-
if (rawActiveAgents.length > 0) {
|
|
3317
|
-
advancedItems.push(new SectionItem('Active agent tree', rawActiveAgents, {
|
|
3318
|
-
description: String(element.sessions.length),
|
|
3319
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3320
|
-
iconId: 'organization',
|
|
3321
|
-
}));
|
|
3322
|
-
}
|
|
3323
|
-
const rawChangeTree = buildGroupedChangeTreeNodes(element.sessions, element.changes);
|
|
3324
|
-
if (rawChangeTree.length > 0) {
|
|
3325
|
-
advancedItems.push(new SectionItem('Raw path tree', rawChangeTree, {
|
|
3326
|
-
description: String(element.changes.length),
|
|
3327
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3328
|
-
iconId: 'file-directory',
|
|
3329
|
-
}));
|
|
3330
|
-
}
|
|
3331
|
-
const colonyTaskList = Array.isArray(element.colonyTasks) ? element.colonyTasks : [];
|
|
3332
|
-
if (colonyTaskList.length > 0) {
|
|
3333
|
-
const colonyItems = colonyTaskList.map((task) => {
|
|
3334
|
-
const pendingLabel = task.pending_handoff_count > 0
|
|
3335
|
-
? formatCountLabel(task.pending_handoff_count, 'pending handoff')
|
|
3336
|
-
: 'quiet';
|
|
3337
|
-
const participantLabel =
|
|
3338
|
-
(task.participants || []).map((p) => p.agent).filter(Boolean).join(', ')
|
|
3339
|
-
|| 'no participants';
|
|
3340
|
-
return new DetailItem(
|
|
3341
|
-
`#${task.id} · ${compactColonyBranchLabel(task.branch)}`,
|
|
3342
|
-
`${participantLabel} · ${pendingLabel}`,
|
|
3343
|
-
{
|
|
3344
|
-
iconId: task.pending_handoff_count > 0 ? 'warning' : 'comment-discussion',
|
|
3345
|
-
tooltip: [
|
|
3346
|
-
task.branch,
|
|
3347
|
-
`task #${task.id}`,
|
|
3348
|
-
participantLabel,
|
|
3349
|
-
task.pending_handoff_count > 0
|
|
3350
|
-
? formatCountLabel(task.pending_handoff_count, 'pending handoff')
|
|
3351
|
-
: '',
|
|
3352
|
-
].filter(Boolean).join('\n'),
|
|
3353
|
-
},
|
|
3354
|
-
);
|
|
3355
|
-
});
|
|
3356
|
-
advancedItems.push(new SectionItem('Colony tasks', colonyItems, {
|
|
3357
|
-
description: String(colonyItems.length),
|
|
3358
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3359
|
-
iconId: 'organization',
|
|
3360
|
-
}));
|
|
3361
|
-
}
|
|
3362
|
-
if (advancedItems.length > 0) {
|
|
3363
|
-
sectionItems.push(new SectionItem('Advanced details', advancedItems, {
|
|
3364
|
-
description: String(advancedItems.length),
|
|
3365
|
-
collapsedState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
3366
|
-
iconId: 'settings-gear',
|
|
3367
|
-
}));
|
|
3368
|
-
}
|
|
3369
|
-
return sectionItems;
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) {
|
|
3373
|
-
return element.items;
|
|
3374
|
-
}
|
|
3375
|
-
|
|
3376
|
-
const repoEntries = await this.syncRepoEntries();
|
|
3377
|
-
this.syncSelectedSession(repoEntries);
|
|
3378
|
-
|
|
3379
|
-
if (repoEntries.length === 0) {
|
|
3380
|
-
return [new InfoItem('No active Guardex agents', 'Open or start a sandbox session.')];
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3383
|
-
return repoEntries.map((entry) => new RepoItem(entry.repoRoot, entry.sessions, entry.changes, {
|
|
3384
|
-
label: repoEntryDisplayLabel(entry.repoRoot, entry.sessions),
|
|
3385
|
-
overview: entry.overview,
|
|
3386
|
-
unassignedChanges: entry.unassignedChanges,
|
|
3387
|
-
lockEntries: entry.lockEntries,
|
|
3388
|
-
colonyTasks: entry.colonyTasks,
|
|
3389
|
-
}));
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
async loadRepoEntries() {
|
|
3393
|
-
const repoEntries = await findRepoSessionEntries();
|
|
3394
|
-
return Promise.all(
|
|
3395
|
-
repoEntries.map(async (entry) => {
|
|
3396
|
-
const repoRoot = entry.repoRoot;
|
|
3397
|
-
const lockRegistry = this.getLockRegistryForRepo(repoRoot);
|
|
3398
|
-
const currentBranch = readCurrentBranch(repoRoot);
|
|
3399
|
-
const colonyTasks = await readColonyTasksForRepo(repoRoot);
|
|
3400
|
-
return {
|
|
3401
|
-
repoRoot,
|
|
3402
|
-
sessions: entry.sessions.map((session) => decorateSession(session, lockRegistry)),
|
|
3403
|
-
changes: readRepoChanges(repoRoot).map((change) => (
|
|
3404
|
-
decorateChange(change, lockRegistry, currentBranch)
|
|
3405
|
-
)),
|
|
3406
|
-
lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
|
|
3407
|
-
colonyTasks,
|
|
3408
|
-
};
|
|
3409
|
-
}),
|
|
3410
|
-
);
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
function countEntryConflicts(entry) {
|
|
3415
|
-
const sessionConflicts = entry.sessions.reduce(
|
|
3416
|
-
(total, session) => total + (session.conflictCount || 0),
|
|
3417
|
-
0,
|
|
3418
|
-
);
|
|
3419
|
-
const changeConflicts = entry.changes.filter((change) => change.hasForeignLock).length;
|
|
3420
|
-
return sessionConflicts + changeConflicts;
|
|
3421
|
-
}
|
|
3422
|
-
|
|
3423
|
-
class SessionInspectPanelManager {
|
|
3424
|
-
constructor() {
|
|
3425
|
-
this.panel = null;
|
|
3426
|
-
this.session = null;
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
open(session) {
|
|
3430
|
-
const targetSession = session?.branch ? { ...session } : null;
|
|
3431
|
-
if (!targetSession?.repoRoot || !targetSession?.branch) {
|
|
3432
|
-
showSessionMessage('Pick an Active Agents session first.');
|
|
3433
|
-
return;
|
|
3434
|
-
}
|
|
3435
|
-
if (!vscode.window.createWebviewPanel) {
|
|
3436
|
-
showSessionMessage('Inspect panel is unavailable in this VS Code build.');
|
|
3437
|
-
return;
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
this.session = targetSession;
|
|
3441
|
-
if (!this.panel) {
|
|
3442
|
-
this.panel = vscode.window.createWebviewPanel(
|
|
3443
|
-
INSPECT_PANEL_VIEW_TYPE,
|
|
3444
|
-
inspectPanelTitle(targetSession),
|
|
3445
|
-
vscode.ViewColumn?.Beside,
|
|
3446
|
-
{
|
|
3447
|
-
enableFindWidget: true,
|
|
3448
|
-
enableScripts: false,
|
|
3449
|
-
retainContextWhenHidden: true,
|
|
3450
|
-
},
|
|
3451
|
-
);
|
|
3452
|
-
this.panel.onDidDispose(() => {
|
|
3453
|
-
this.panel = null;
|
|
3454
|
-
this.session = null;
|
|
3455
|
-
});
|
|
3456
|
-
} else {
|
|
3457
|
-
this.panel.reveal?.(vscode.ViewColumn?.Beside);
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
this.render();
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
resolveSession() {
|
|
3464
|
-
if (!this.session?.repoRoot || !this.session?.branch) {
|
|
3465
|
-
return this.session ? { ...this.session } : null;
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
|
-
return readActiveSessions(this.session.repoRoot, { includeStale: true })
|
|
3469
|
-
.find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(this.session))
|
|
3470
|
-
|| { ...this.session };
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
render() {
|
|
3474
|
-
if (!this.panel || !this.session) {
|
|
3475
|
-
return;
|
|
3476
|
-
}
|
|
3477
|
-
|
|
3478
|
-
const session = this.resolveSession();
|
|
3479
|
-
if (!session) {
|
|
3480
|
-
return;
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
this.session = { ...session };
|
|
3484
|
-
this.panel.title = inspectPanelTitle(session);
|
|
3485
|
-
this.panel.webview.html = renderInspectPanelHtml(session, readSessionInspectData(session));
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
|
-
refresh() {
|
|
3489
|
-
this.render();
|
|
3490
|
-
}
|
|
3491
|
-
|
|
3492
|
-
dispose() {
|
|
3493
|
-
this.panel?.dispose();
|
|
3494
|
-
this.panel = null;
|
|
3495
|
-
this.session = null;
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
class ActiveAgentsRefreshController {
|
|
3500
|
-
constructor(provider, inspectPanelManager = null) {
|
|
3501
|
-
this.provider = provider;
|
|
3502
|
-
this.inspectPanelManager = inspectPanelManager;
|
|
3503
|
-
this.refreshTimer = null;
|
|
3504
|
-
this.sessionWatchers = new Map();
|
|
3505
|
-
this.closedMissingWorktreeRepositories = new Set();
|
|
3506
|
-
this.observedWorktreePaths = new Set();
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
scheduleRefresh() {
|
|
3510
|
-
if (this.refreshTimer) {
|
|
3511
|
-
clearTimeout(this.refreshTimer);
|
|
3512
|
-
}
|
|
3513
|
-
this.refreshTimer = setTimeout(() => {
|
|
3514
|
-
this.refreshTimer = null;
|
|
3515
|
-
void this.refreshNow();
|
|
3516
|
-
}, REFRESH_DEBOUNCE_MS);
|
|
3517
|
-
}
|
|
3518
|
-
|
|
3519
|
-
async refreshNow() {
|
|
3520
|
-
await this.syncSessionWatchers();
|
|
3521
|
-
await this.provider.refresh();
|
|
3522
|
-
this.inspectPanelManager?.refresh();
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
async syncSessionWatchers() {
|
|
3526
|
-
const repoEntries = await findRepoSessionEntries();
|
|
3527
|
-
const liveSessionKeys = new Set();
|
|
3528
|
-
|
|
3529
|
-
for (const workspacePath of findDeletedManagedWorkspaceFolders()) {
|
|
3530
|
-
await this.closeMissingWorktreeRepository(workspacePath);
|
|
3531
|
-
}
|
|
3532
|
-
|
|
3533
|
-
for (const entry of repoEntries) {
|
|
3534
|
-
for (const session of entry.sessions) {
|
|
3535
|
-
const worktreePath = sessionWorktreePath(session);
|
|
3536
|
-
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
|
|
3537
|
-
if (normalizedWorktreePath && !fs.existsSync(normalizedWorktreePath)) {
|
|
3538
|
-
await this.closeMissingWorktreeRepository(normalizedWorktreePath);
|
|
3539
|
-
continue;
|
|
3540
|
-
}
|
|
3541
|
-
if (normalizedWorktreePath) {
|
|
3542
|
-
this.closedMissingWorktreeRepositories.delete(normalizedWorktreePath);
|
|
3543
|
-
this.observedWorktreePaths.add(normalizedWorktreePath);
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
const sessionKey = resolveSessionWatcherKey(session);
|
|
3547
|
-
liveSessionKeys.add(sessionKey);
|
|
3548
|
-
if (this.sessionWatchers.has(sessionKey)) {
|
|
3549
|
-
continue;
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
const watcher = vscode.workspace.createFileSystemWatcher(
|
|
3553
|
-
resolveSessionGitIndexPath(session.worktreePath),
|
|
3554
|
-
);
|
|
3555
|
-
const disposables = bindRefreshWatcher(watcher, () => this.scheduleRefresh());
|
|
3556
|
-
this.sessionWatchers.set(sessionKey, {
|
|
3557
|
-
watcher,
|
|
3558
|
-
disposables,
|
|
3559
|
-
worktreePath: normalizedWorktreePath,
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3562
|
-
}
|
|
3563
|
-
|
|
3564
|
-
for (const observedWorktreePath of this.observedWorktreePaths) {
|
|
3565
|
-
if (fs.existsSync(observedWorktreePath)) {
|
|
3566
|
-
this.closedMissingWorktreeRepositories.delete(observedWorktreePath);
|
|
3567
|
-
continue;
|
|
3568
|
-
}
|
|
3569
|
-
await this.closeMissingWorktreeRepository(observedWorktreePath);
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
for (const [sessionKey, entry] of this.sessionWatchers) {
|
|
3573
|
-
if (liveSessionKeys.has(sessionKey)) {
|
|
3574
|
-
continue;
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
if (entry.worktreePath && !fs.existsSync(entry.worktreePath)) {
|
|
3578
|
-
await this.closeMissingWorktreeRepository(entry.worktreePath);
|
|
3579
|
-
}
|
|
3580
|
-
disposeAll(entry.disposables);
|
|
3581
|
-
entry.watcher.dispose();
|
|
3582
|
-
this.sessionWatchers.delete(sessionKey);
|
|
3583
|
-
}
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
async closeMissingWorktreeRepository(worktreePath) {
|
|
3587
|
-
const normalizedWorktreePath = normalizeAbsolutePath(worktreePath);
|
|
3588
|
-
if (!normalizedWorktreePath || this.closedMissingWorktreeRepositories.has(normalizedWorktreePath)) {
|
|
3589
|
-
return;
|
|
3590
|
-
}
|
|
3591
|
-
|
|
3592
|
-
this.closedMissingWorktreeRepositories.add(normalizedWorktreePath);
|
|
3593
|
-
await closeDeletedWorktreeRepository(normalizedWorktreePath);
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
dispose() {
|
|
3597
|
-
if (this.refreshTimer) {
|
|
3598
|
-
clearTimeout(this.refreshTimer);
|
|
3599
|
-
this.refreshTimer = null;
|
|
3600
|
-
}
|
|
3601
|
-
|
|
3602
|
-
for (const entry of this.sessionWatchers.values()) {
|
|
3603
|
-
disposeAll(entry.disposables);
|
|
3604
|
-
entry.watcher.dispose();
|
|
3605
|
-
}
|
|
3606
|
-
this.sessionWatchers.clear();
|
|
3607
|
-
}
|
|
3608
|
-
}
|
|
3609
|
-
|
|
3610
|
-
function activate(context) {
|
|
3611
|
-
const decorationProvider = new SessionDecorationProvider();
|
|
3612
|
-
const provider = new ActiveAgentsProvider(decorationProvider);
|
|
3613
|
-
const inspectPanelManager = new SessionInspectPanelManager();
|
|
3614
|
-
const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager);
|
|
3615
|
-
const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
|
|
3616
|
-
treeDataProvider: provider,
|
|
3617
|
-
showCollapseAll: true,
|
|
3618
|
-
});
|
|
3619
|
-
const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
|
|
3620
|
-
activeAgentsStatusItem.name = 'GitGuardex Active Agents';
|
|
3621
|
-
activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
|
|
3622
|
-
provider.attachTreeView(treeView);
|
|
3623
|
-
const scheduleRefresh = () => refreshController.scheduleRefresh();
|
|
3624
|
-
const handleWorkspaceFoldersChanged = () => {
|
|
3625
|
-
scheduleRefresh();
|
|
3626
|
-
void ensureManagedRepoScanIgnores();
|
|
3627
|
-
};
|
|
3628
|
-
const refresh = () => void refreshController.refreshNow();
|
|
3629
|
-
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
3630
|
-
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
3631
|
-
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
3632
|
-
const managedWorktreeGitWatcher = vscode.workspace.createFileSystemWatcher(MANAGED_WORKTREE_GIT_FILES_GLOB);
|
|
3633
|
-
const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB);
|
|
3634
|
-
const updateStatusBar = () => {
|
|
3635
|
-
const selectedSession = provider.getSelectedSession();
|
|
3636
|
-
const summary = provider.getViewSummary();
|
|
3637
|
-
if ((summary.sessionCount || 0) <= 0) {
|
|
3638
|
-
activeAgentsStatusItem.hide();
|
|
3639
|
-
return;
|
|
3640
|
-
}
|
|
3641
|
-
|
|
3642
|
-
activeAgentsStatusItem.text = selectedSession?.branch
|
|
3643
|
-
? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}`
|
|
3644
|
-
: buildActiveAgentsStatusSummary(summary);
|
|
3645
|
-
activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary);
|
|
3646
|
-
activeAgentsStatusItem.show();
|
|
3647
|
-
};
|
|
3648
|
-
updateStatusBar();
|
|
3649
|
-
const readCommitMessageForSession = async (session) => {
|
|
3650
|
-
const rawMessage = await vscode.window.showInputBox?.({
|
|
3651
|
-
prompt: `Commit ${sessionIdentityLabel(session)} worktree`,
|
|
3652
|
-
placeHolder: sessionCommitPlaceholder(session),
|
|
3653
|
-
ignoreFocusOut: true,
|
|
3654
|
-
});
|
|
3655
|
-
if (rawMessage === undefined) {
|
|
3656
|
-
return undefined;
|
|
3657
|
-
}
|
|
3658
|
-
return String(rawMessage).trim();
|
|
3659
|
-
};
|
|
3660
|
-
const commitSelectedSession = async () => {
|
|
3661
|
-
const selectedSession = provider.getSelectedSession();
|
|
3662
|
-
if (!selectedSession?.worktreePath) {
|
|
3663
|
-
vscode.window.showInformationMessage?.('Pick an Active Agents session first.');
|
|
3664
|
-
return;
|
|
3665
|
-
}
|
|
3666
|
-
|
|
3667
|
-
if (!fs.existsSync(selectedSession.worktreePath)) {
|
|
3668
|
-
vscode.window.showInformationMessage?.(
|
|
3669
|
-
`Selected session worktree is no longer on disk: ${selectedSession.worktreePath}`,
|
|
3670
|
-
);
|
|
3671
|
-
return;
|
|
3672
|
-
}
|
|
3673
|
-
|
|
3674
|
-
const message = await readCommitMessageForSession(selectedSession);
|
|
3675
|
-
if (message === undefined) {
|
|
3676
|
-
return;
|
|
3677
|
-
}
|
|
3678
|
-
if (!message) {
|
|
3679
|
-
vscode.window.showInformationMessage?.('Enter a commit message first.');
|
|
3680
|
-
return;
|
|
3681
|
-
}
|
|
3682
|
-
|
|
3683
|
-
try {
|
|
3684
|
-
stageWorktreeForCommit(selectedSession.worktreePath);
|
|
3685
|
-
commitWorktree(selectedSession.worktreePath, message);
|
|
3686
|
-
refresh();
|
|
3687
|
-
} catch (error) {
|
|
3688
|
-
const failure = formatGitCommandFailure(error);
|
|
3689
|
-
if (/nothing to commit|no changes added to commit/i.test(failure)) {
|
|
3690
|
-
vscode.window.showInformationMessage?.(`No changes to commit in ${selectedSession.label}.`);
|
|
3691
|
-
return;
|
|
3692
|
-
}
|
|
3693
|
-
vscode.window.showErrorMessage?.(`Active Agents commit failed: ${failure}`);
|
|
3694
|
-
}
|
|
3695
|
-
};
|
|
3696
|
-
const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS);
|
|
3697
|
-
const refreshLockRegistry = (uri) => {
|
|
3698
|
-
if (uri?.fsPath) {
|
|
3699
|
-
provider.refreshLockRegistryForFile(uri.fsPath);
|
|
3700
|
-
}
|
|
3701
|
-
scheduleRefresh();
|
|
3702
|
-
};
|
|
3703
|
-
|
|
3704
|
-
provider.onDidChangeSelectedSession((session) => {
|
|
3705
|
-
updateStatusBar();
|
|
3706
|
-
decorationProvider.refresh();
|
|
3707
|
-
});
|
|
3708
|
-
provider.onDidChangeTreeData(() => {
|
|
3709
|
-
updateStatusBar();
|
|
3710
|
-
});
|
|
3711
|
-
|
|
3712
|
-
context.subscriptions.push(
|
|
3713
|
-
treeView,
|
|
3714
|
-
activeAgentsStatusItem,
|
|
3715
|
-
inspectPanelManager,
|
|
3716
|
-
refreshController,
|
|
3717
|
-
vscode.window.registerFileDecorationProvider(decorationProvider),
|
|
3718
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
|
|
3719
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
|
|
3720
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.restart', restartActiveAgents),
|
|
3721
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
|
|
3722
|
-
await vscode.commands.executeCommand('workbench.view.extension.gitguardex-active-agents-container');
|
|
3723
|
-
}),
|
|
3724
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
|
|
3725
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
|
|
3726
|
-
if (!session?.worktreePath) {
|
|
3727
|
-
return;
|
|
3728
|
-
}
|
|
3729
|
-
|
|
3730
|
-
await vscode.commands.executeCommand(
|
|
3731
|
-
'vscode.openFolder',
|
|
3732
|
-
vscode.Uri.file(session.worktreePath),
|
|
3733
|
-
{ forceNewWindow: true },
|
|
3734
|
-
);
|
|
3735
|
-
}),
|
|
3736
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.openChange', async (change) => {
|
|
3737
|
-
if (!change?.absolutePath) {
|
|
3738
|
-
return;
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3741
|
-
if (!fs.existsSync(change.absolutePath)) {
|
|
3742
|
-
vscode.window.showInformationMessage?.(`Changed path is no longer on disk: ${change.relativePath}`);
|
|
3743
|
-
return;
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
|
|
3747
|
-
}),
|
|
3748
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
|
|
3749
|
-
inspectPanelManager.open(session || provider.getSelectedSession());
|
|
3750
|
-
}),
|
|
3751
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.showSessionTerminal', showSessionTerminal),
|
|
3752
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
|
|
3753
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
|
|
3754
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
|
|
3755
|
-
vscode.commands.registerCommand('gitguardex.activeAgents.dismissSession', (session) => dismissSession(session, refresh)),
|
|
3756
|
-
vscode.workspace.onDidChangeWorkspaceFolders(handleWorkspaceFoldersChanged),
|
|
3757
|
-
activeSessionsWatcher,
|
|
3758
|
-
lockWatcher,
|
|
3759
|
-
worktreeLockWatcher,
|
|
3760
|
-
managedWorktreeGitWatcher,
|
|
3761
|
-
logWatcher,
|
|
3762
|
-
{ dispose: () => clearInterval(interval) },
|
|
3763
|
-
);
|
|
3764
|
-
|
|
3765
|
-
context.subscriptions.push(
|
|
3766
|
-
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
3767
|
-
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
3768
|
-
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
3769
|
-
...bindRefreshWatcher(managedWorktreeGitWatcher, scheduleRefresh),
|
|
3770
|
-
...bindRefreshWatcher(logWatcher, scheduleRefresh),
|
|
3771
|
-
);
|
|
3772
|
-
void ensureManagedRepoScanIgnores();
|
|
3773
|
-
void refreshController.refreshNow();
|
|
3774
|
-
void maybeAutoUpdateActiveAgentsExtension(context);
|
|
3775
|
-
}
|
|
3776
|
-
|
|
3777
|
-
function deactivate() {}
|
|
3778
|
-
|
|
3779
|
-
module.exports = {
|
|
3780
|
-
activate,
|
|
3781
|
-
deactivate,
|
|
3782
|
-
};
|