@imdeadpool/guardex 7.0.21 → 7.0.23
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 +39 -29
- package/package.json +5 -1
- package/src/cli/args.js +89 -0
- package/src/cli/main.js +645 -2873
- package/src/context.js +195 -31
- package/src/core/stdin.js +52 -0
- package/src/core/versions.js +33 -0
- package/src/doctor/index.js +1071 -0
- package/src/finish/index.js +456 -358
- package/src/git/index.js +604 -1
- package/src/hooks/index.js +64 -0
- package/src/output/index.js +72 -5
- package/src/report/session-severity.js +213 -0
- package/src/sandbox/index.js +301 -52
- package/src/scaffold/index.js +627 -0
- package/src/toolchain/index.js +559 -179
- package/templates/AGENTS.multiagent-safety.md +25 -0
- package/templates/scripts/agent-branch-finish.sh +86 -6
- package/templates/scripts/agent-session-state.js +62 -1
- package/templates/scripts/agent-worktree-prune.sh +15 -1
- package/templates/scripts/codex-agent.sh +38 -0
- package/templates/scripts/install-vscode-active-agents-extension.js +38 -11
- package/templates/scripts/openspec/init-plan-workspace.sh +34 -3
- package/templates/vscode/guardex-active-agents/README.md +9 -6
- package/templates/vscode/guardex-active-agents/extension.js +805 -77
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/package.json +15 -3
- package/templates/vscode/guardex-active-agents/session-schema.js +311 -4
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
formatElapsedFrom,
|
|
7
7
|
readActiveSessions,
|
|
8
8
|
readRepoChanges,
|
|
9
|
+
readSessionInspectData,
|
|
9
10
|
sanitizeBranchForFile,
|
|
10
11
|
} = require('./session-schema.js');
|
|
11
12
|
|
|
@@ -16,21 +17,28 @@ const LOCK_FILE_RELATIVE = path.join('.omx', 'state', 'agent-file-locks.json');
|
|
|
16
17
|
const ACTIVE_SESSION_FILES_GLOB = '**/.omx/state/active-sessions/*.json';
|
|
17
18
|
const AGENT_FILE_LOCKS_GLOB = '**/.omx/state/agent-file-locks.json';
|
|
18
19
|
const WORKTREE_AGENT_LOCKS_GLOB = '**/{.omx,.omc}/agent-worktrees/**/AGENT.lock';
|
|
20
|
+
const AGENT_LOG_FILES_GLOB = '**/.omx/logs/*.log';
|
|
19
21
|
const SESSION_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git,.omx/agent-worktrees,.omc/agent-worktrees}/**';
|
|
20
22
|
const WORKTREE_LOCK_SCAN_EXCLUDE_GLOB = '**/{node_modules,.git}/**';
|
|
21
23
|
const SESSION_SCAN_LIMIT = 200;
|
|
22
24
|
const REFRESH_DEBOUNCE_MS = 250;
|
|
25
|
+
const ACTIVE_AGENTS_MANIFEST_RELATIVE = path.join('vscode', 'guardex-active-agents', 'package.json');
|
|
26
|
+
const ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE = path.join('scripts', 'install-vscode-active-agents-extension.js');
|
|
27
|
+
const RELOAD_WINDOW_ACTION = 'Reload Window';
|
|
28
|
+
const UPDATE_LATER_ACTION = 'Later';
|
|
29
|
+
const REFRESH_POLL_INTERVAL_MS = 30_000;
|
|
30
|
+
const INSPECT_PANEL_VIEW_TYPE = 'gitguardex.activeAgents.inspect';
|
|
23
31
|
const SESSION_ACTIVITY_GROUPS = [
|
|
24
32
|
{ kind: 'blocked', label: 'BLOCKED' },
|
|
25
33
|
{ kind: 'working', label: 'WORKING NOW' },
|
|
26
|
-
{ kind: 'idle', label: '
|
|
34
|
+
{ kind: 'idle', label: 'THINKING' },
|
|
27
35
|
{ kind: 'stalled', label: 'STALLED' },
|
|
28
36
|
{ kind: 'dead', label: 'DEAD' },
|
|
29
37
|
];
|
|
30
38
|
const SESSION_ACTIVITY_ICON_IDS = {
|
|
31
39
|
blocked: 'warning',
|
|
32
|
-
working: '
|
|
33
|
-
idle: '
|
|
40
|
+
working: 'loading~spin',
|
|
41
|
+
idle: 'comment-discussion',
|
|
34
42
|
stalled: 'clock',
|
|
35
43
|
dead: 'error',
|
|
36
44
|
};
|
|
@@ -93,10 +101,211 @@ function sessionIdleDecoration(session, now = Date.now()) {
|
|
|
93
101
|
return undefined;
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
function formatCountLabel(count, singular, plural = `${singular}s`) {
|
|
105
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sessionIdentityLabel(session) {
|
|
109
|
+
const agentName = typeof session?.agentName === 'string' ? session.agentName.trim() : '';
|
|
110
|
+
const taskName = typeof session?.taskName === 'string' ? session.taskName.trim() : '';
|
|
111
|
+
const label = typeof session?.label === 'string' ? session.label.trim() : '';
|
|
112
|
+
|
|
113
|
+
if (agentName && taskName) {
|
|
114
|
+
return `${agentName} · ${taskName}`;
|
|
115
|
+
}
|
|
116
|
+
if (agentName && label) {
|
|
117
|
+
return `${agentName} · ${label}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return agentName || taskName || label || 'session';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sessionCommitPlaceholder(session) {
|
|
124
|
+
if (!session?.branch) {
|
|
125
|
+
return 'Pick an Active Agents session to commit its worktree.';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `Commit ${sessionIdentityLabel(session)} on ${session.branch} · ${formatCountLabel(session.lockCount || 0, 'lock')} (Ctrl+Enter)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function agentNameFromBranch(branch) {
|
|
132
|
+
const segments = String(branch || '')
|
|
133
|
+
.split('/')
|
|
134
|
+
.map((segment) => segment.trim())
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
if (segments[0] === 'agent' && segments[1]) {
|
|
137
|
+
return segments[1];
|
|
138
|
+
}
|
|
139
|
+
return segments[0] || 'lock';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function agentBadgeFromBranch(branch) {
|
|
143
|
+
const normalized = agentNameFromBranch(branch).toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
144
|
+
return normalized.slice(0, 2) || 'LK';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildActiveAgentsStatusSummary(summary) {
|
|
148
|
+
const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
|
|
149
|
+
if (activeCount > 0) {
|
|
150
|
+
return `$(git-branch) ${formatCountLabel(activeCount, 'active agent')}`;
|
|
151
|
+
}
|
|
152
|
+
return `$(git-branch) ${formatCountLabel(summary?.sessionCount || 0, 'tracked session')}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildActiveAgentsStatusTooltip(selectedSession, summary) {
|
|
156
|
+
if (selectedSession?.branch) {
|
|
157
|
+
return [
|
|
158
|
+
selectedSession.branch,
|
|
159
|
+
sessionIdentityLabel(selectedSession),
|
|
160
|
+
formatCountLabel(selectedSession.lockCount || 0, 'lock'),
|
|
161
|
+
selectedSession.worktreePath,
|
|
162
|
+
'Click to open Source Control.',
|
|
163
|
+
].filter(Boolean).join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const activeCount = Math.max(0, (summary?.sessionCount || 0) - (summary?.deadCount || 0));
|
|
167
|
+
return [
|
|
168
|
+
formatCountLabel(activeCount, 'active agent'),
|
|
169
|
+
formatCountLabel(summary?.workingCount || 0, 'working now session', 'working now sessions'),
|
|
170
|
+
summary?.deadCount ? formatCountLabel(summary.deadCount, 'dead session') : '',
|
|
171
|
+
'Click to open Source Control.',
|
|
172
|
+
].filter(Boolean).join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function escapeHtml(value) {
|
|
176
|
+
return String(value || '')
|
|
177
|
+
.replace(/&/g, '&')
|
|
178
|
+
.replace(/</g, '<')
|
|
179
|
+
.replace(/>/g, '>')
|
|
180
|
+
.replace(/"/g, '"')
|
|
181
|
+
.replace(/'/g, ''');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatInspectBranchSummary(inspectData) {
|
|
185
|
+
if (Number.isInteger(inspectData?.aheadCount) && Number.isInteger(inspectData?.behindCount)) {
|
|
186
|
+
return `${inspectData.aheadCount} ahead · ${inspectData.behindCount} behind vs ${inspectData.compareRef}`;
|
|
187
|
+
}
|
|
188
|
+
return `Branch comparison unavailable vs ${inspectData?.compareRef || 'origin/dev'}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function inspectPanelTitle(session) {
|
|
192
|
+
return `Inspect ${sessionDisplayLabel(session)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderInspectPanelHtml(session, inspectData) {
|
|
196
|
+
const heldLocksMarkup = Array.isArray(inspectData?.heldLocks) && inspectData.heldLocks.length > 0
|
|
197
|
+
? `<ul>${inspectData.heldLocks.map((entry) => (
|
|
198
|
+
`<li><code>${escapeHtml(entry.relativePath)}</code>${entry.allowDelete ? ' <span class="pill">delete ok</span>' : ''}${entry.claimedAt ? ` <span class="muted">${escapeHtml(entry.claimedAt)}</span>` : ''}</li>`
|
|
199
|
+
)).join('')}</ul>`
|
|
200
|
+
: '<p class="muted">No held locks recorded for this session.</p>';
|
|
201
|
+
const logContent = inspectData?.logTailText
|
|
202
|
+
? escapeHtml(inspectData.logTailText)
|
|
203
|
+
: 'No log output available.';
|
|
204
|
+
|
|
205
|
+
return `<!DOCTYPE html>
|
|
206
|
+
<html lang="en">
|
|
207
|
+
<head>
|
|
208
|
+
<meta charset="utf-8" />
|
|
209
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
210
|
+
<style>
|
|
211
|
+
:root {
|
|
212
|
+
color-scheme: light dark;
|
|
213
|
+
font-family: var(--vscode-font-family);
|
|
214
|
+
}
|
|
215
|
+
body {
|
|
216
|
+
padding: 16px;
|
|
217
|
+
color: var(--vscode-foreground);
|
|
218
|
+
background: var(--vscode-editor-background);
|
|
219
|
+
}
|
|
220
|
+
h1, h2 {
|
|
221
|
+
margin: 0 0 12px;
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
}
|
|
224
|
+
h2 {
|
|
225
|
+
margin-top: 20px;
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
text-transform: uppercase;
|
|
228
|
+
letter-spacing: 0.04em;
|
|
229
|
+
color: var(--vscode-descriptionForeground);
|
|
230
|
+
}
|
|
231
|
+
.grid {
|
|
232
|
+
display: grid;
|
|
233
|
+
grid-template-columns: minmax(140px, 220px) 1fr;
|
|
234
|
+
gap: 8px 12px;
|
|
235
|
+
margin: 0;
|
|
236
|
+
}
|
|
237
|
+
dt {
|
|
238
|
+
color: var(--vscode-descriptionForeground);
|
|
239
|
+
}
|
|
240
|
+
dd {
|
|
241
|
+
margin: 0;
|
|
242
|
+
word-break: break-word;
|
|
243
|
+
}
|
|
244
|
+
code, pre {
|
|
245
|
+
font-family: var(--vscode-editor-font-family, monospace);
|
|
246
|
+
font-size: 12px;
|
|
247
|
+
}
|
|
248
|
+
pre {
|
|
249
|
+
margin: 0;
|
|
250
|
+
padding: 12px;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
overflow: auto;
|
|
253
|
+
background: var(--vscode-textCodeBlock-background, rgba(127, 127, 127, 0.12));
|
|
254
|
+
border: 1px solid var(--vscode-editorWidget-border, transparent);
|
|
255
|
+
white-space: pre-wrap;
|
|
256
|
+
word-break: break-word;
|
|
257
|
+
}
|
|
258
|
+
ul {
|
|
259
|
+
margin: 0;
|
|
260
|
+
padding-left: 20px;
|
|
261
|
+
}
|
|
262
|
+
li + li {
|
|
263
|
+
margin-top: 6px;
|
|
264
|
+
}
|
|
265
|
+
.muted {
|
|
266
|
+
color: var(--vscode-descriptionForeground);
|
|
267
|
+
}
|
|
268
|
+
.pill {
|
|
269
|
+
display: inline-block;
|
|
270
|
+
margin-left: 6px;
|
|
271
|
+
padding: 1px 6px;
|
|
272
|
+
border-radius: 999px;
|
|
273
|
+
background: var(--vscode-badge-background);
|
|
274
|
+
color: var(--vscode-badge-foreground);
|
|
275
|
+
font-size: 11px;
|
|
276
|
+
}
|
|
277
|
+
</style>
|
|
278
|
+
</head>
|
|
279
|
+
<body>
|
|
280
|
+
<h1>${escapeHtml(sessionIdentityLabel(session))}</h1>
|
|
281
|
+
<dl class="grid">
|
|
282
|
+
<dt>Branch</dt>
|
|
283
|
+
<dd><code>${escapeHtml(session.branch)}</code></dd>
|
|
284
|
+
<dt>Worktree</dt>
|
|
285
|
+
<dd><code>${escapeHtml(session.worktreePath)}</code></dd>
|
|
286
|
+
<dt>Base branch</dt>
|
|
287
|
+
<dd><code>${escapeHtml(inspectData?.baseBranch || 'dev')}</code></dd>
|
|
288
|
+
<dt>Divergence</dt>
|
|
289
|
+
<dd>${escapeHtml(formatInspectBranchSummary(inspectData))}</dd>
|
|
290
|
+
<dt>Held locks</dt>
|
|
291
|
+
<dd>${Array.isArray(inspectData?.heldLocks) ? inspectData.heldLocks.length : 0}</dd>
|
|
292
|
+
<dt>Log file</dt>
|
|
293
|
+
<dd><code>${escapeHtml(inspectData?.logPath || 'Unavailable')}</code></dd>
|
|
294
|
+
</dl>
|
|
295
|
+
<h2>Held Locks</h2>
|
|
296
|
+
${heldLocksMarkup}
|
|
297
|
+
<h2>Agent Log Tail</h2>
|
|
298
|
+
<pre>${logContent}</pre>
|
|
299
|
+
</body>
|
|
300
|
+
</html>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
96
303
|
class SessionDecorationProvider {
|
|
97
304
|
constructor(nowProvider = () => Date.now()) {
|
|
98
305
|
this.nowProvider = nowProvider;
|
|
99
306
|
this.sessionsByUri = new Map();
|
|
307
|
+
this.lockEntriesByFileUri = new Map();
|
|
308
|
+
this.selectedBranch = '';
|
|
100
309
|
this.onDidChangeFileDecorationsEmitter = new vscode.EventEmitter();
|
|
101
310
|
this.onDidChangeFileDecorations = this.onDidChangeFileDecorationsEmitter.event;
|
|
102
311
|
}
|
|
@@ -107,13 +316,54 @@ class SessionDecorationProvider {
|
|
|
107
316
|
);
|
|
108
317
|
}
|
|
109
318
|
|
|
319
|
+
updateLockEntries(repoEntries) {
|
|
320
|
+
const nextEntriesByUri = new Map();
|
|
321
|
+
for (const entry of repoEntries || []) {
|
|
322
|
+
for (const [relativePath, lockEntry] of entry.lockEntries || []) {
|
|
323
|
+
nextEntriesByUri.set(
|
|
324
|
+
vscode.Uri.file(path.join(entry.repoRoot, relativePath)).toString(),
|
|
325
|
+
{ branch: lockEntry.branch },
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
this.lockEntriesByFileUri = nextEntriesByUri;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
setSelectedBranch(branch) {
|
|
333
|
+
this.selectedBranch = typeof branch === 'string' ? branch.trim() : '';
|
|
334
|
+
}
|
|
335
|
+
|
|
110
336
|
refresh() {
|
|
111
337
|
this.onDidChangeFileDecorationsEmitter.fire();
|
|
112
338
|
}
|
|
113
339
|
|
|
114
340
|
provideFileDecoration(uri) {
|
|
115
341
|
if (!uri || uri.scheme !== SESSION_DECORATION_SCHEME) {
|
|
116
|
-
|
|
342
|
+
if (!uri || uri.scheme !== 'file') {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const lockEntry = this.lockEntriesByFileUri.get(uri.toString());
|
|
347
|
+
if (!lockEntry?.branch) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const ownsSelectedSession = Boolean(this.selectedBranch) && lockEntry.branch === this.selectedBranch;
|
|
352
|
+
return {
|
|
353
|
+
badge: agentBadgeFromBranch(lockEntry.branch),
|
|
354
|
+
tooltip: ownsSelectedSession
|
|
355
|
+
? `Locked by selected session ${lockEntry.branch}`
|
|
356
|
+
: this.selectedBranch
|
|
357
|
+
? `Locked by ${lockEntry.branch} (selected session: ${this.selectedBranch})`
|
|
358
|
+
: `Locked by ${lockEntry.branch}`,
|
|
359
|
+
color: new vscode.ThemeColor(
|
|
360
|
+
ownsSelectedSession
|
|
361
|
+
? 'gitDecoration.modifiedResourceForeground'
|
|
362
|
+
: this.selectedBranch
|
|
363
|
+
? 'list.errorForeground'
|
|
364
|
+
: 'list.warningForeground',
|
|
365
|
+
),
|
|
366
|
+
};
|
|
117
367
|
}
|
|
118
368
|
|
|
119
369
|
return sessionIdleDecoration(this.sessionsByUri.get(uri.toString()), this.nowProvider());
|
|
@@ -147,8 +397,9 @@ class RepoItem extends vscode.TreeItem {
|
|
|
147
397
|
if (workingCount > 0) {
|
|
148
398
|
descriptionParts.push(`${workingCount} working`);
|
|
149
399
|
}
|
|
150
|
-
|
|
151
|
-
|
|
400
|
+
const changedCount = countChangedPaths(repoRoot, sessions, changes);
|
|
401
|
+
if (changedCount > 0) {
|
|
402
|
+
descriptionParts.push(`${changedCount} changed`);
|
|
152
403
|
}
|
|
153
404
|
this.description = descriptionParts.join(' · ');
|
|
154
405
|
this.tooltip = [
|
|
@@ -170,11 +421,49 @@ class SectionItem extends vscode.TreeItem {
|
|
|
170
421
|
}
|
|
171
422
|
}
|
|
172
423
|
|
|
424
|
+
class WorktreeItem extends vscode.TreeItem {
|
|
425
|
+
constructor(worktreePath, sessions, items = [], options = {}) {
|
|
426
|
+
const normalizedWorktreePath = typeof worktreePath === 'string' ? worktreePath.trim() : '';
|
|
427
|
+
const sessionList = Array.isArray(sessions) ? sessions : [];
|
|
428
|
+
const changedCount = Number.isInteger(options.changedCount)
|
|
429
|
+
? options.changedCount
|
|
430
|
+
: sessionList.reduce((total, session) => total + (session.changeCount || 0), 0);
|
|
431
|
+
const descriptionParts = [formatCountLabel(sessionList.length, 'agent')];
|
|
432
|
+
if (changedCount > 0) {
|
|
433
|
+
descriptionParts.push(`${changedCount} changed`);
|
|
434
|
+
}
|
|
435
|
+
super(
|
|
436
|
+
path.basename(normalizedWorktreePath || '') || 'worktree',
|
|
437
|
+
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
|
|
438
|
+
);
|
|
439
|
+
this.worktreePath = normalizedWorktreePath;
|
|
440
|
+
this.sessions = sessionList;
|
|
441
|
+
this.items = items;
|
|
442
|
+
this.description = options.description || descriptionParts.join(' · ');
|
|
443
|
+
this.tooltip = [
|
|
444
|
+
normalizedWorktreePath,
|
|
445
|
+
...sessionList.map((session) => session.branch).filter(Boolean),
|
|
446
|
+
].filter(Boolean).join('\n');
|
|
447
|
+
this.iconPath = new vscode.ThemeIcon('folder');
|
|
448
|
+
this.contextValue = 'gitguardex.worktree';
|
|
449
|
+
if (sessionList[0]?.worktreePath) {
|
|
450
|
+
this.command = {
|
|
451
|
+
command: 'gitguardex.activeAgents.openWorktree',
|
|
452
|
+
title: 'Open Agent Worktree',
|
|
453
|
+
arguments: [sessionList[0]],
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
173
459
|
class SessionItem extends vscode.TreeItem {
|
|
174
|
-
constructor(session, items = []) {
|
|
460
|
+
constructor(session, items = [], options = {}) {
|
|
175
461
|
const lockCount = Number.isFinite(session.lockCount) ? session.lockCount : 0;
|
|
462
|
+
const label = typeof options.label === 'string' && options.label.trim()
|
|
463
|
+
? options.label.trim()
|
|
464
|
+
: session.label;
|
|
176
465
|
super(
|
|
177
|
-
|
|
466
|
+
label,
|
|
178
467
|
items.length > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None,
|
|
179
468
|
);
|
|
180
469
|
this.session = session;
|
|
@@ -185,6 +474,9 @@ class SessionItem extends vscode.TreeItem {
|
|
|
185
474
|
descriptionParts.push(session.activityCountLabel);
|
|
186
475
|
}
|
|
187
476
|
descriptionParts.push(session.elapsedLabel || formatElapsedFrom(session.startedAt));
|
|
477
|
+
if (lockCount > 0) {
|
|
478
|
+
descriptionParts.push(`${lockCount} $(lock)`);
|
|
479
|
+
}
|
|
188
480
|
this.description = descriptionParts.join(' · ');
|
|
189
481
|
const tooltipLines = [
|
|
190
482
|
session.branch,
|
|
@@ -197,6 +489,7 @@ class SessionItem extends vscode.TreeItem {
|
|
|
197
489
|
? `Changed ${session.activityCountLabel}: ${session.activitySummary}`
|
|
198
490
|
: session.activitySummary,
|
|
199
491
|
`Locks ${lockCount}`,
|
|
492
|
+
session.conflictCount > 0 ? `Conflicts ${session.conflictCount}` : '',
|
|
200
493
|
Number.isInteger(session.pid) && session.pid > 0
|
|
201
494
|
? session.pidAlive === false
|
|
202
495
|
? `PID ${session.pid} not alive`
|
|
@@ -260,10 +553,39 @@ function shellQuote(value) {
|
|
|
260
553
|
return `'${normalized.replace(/'/g, "'\"'\"'")}'`;
|
|
261
554
|
}
|
|
262
555
|
|
|
556
|
+
function readPackageJson(repoRoot) {
|
|
557
|
+
const packageJsonPath = path.join(repoRoot, 'package.json');
|
|
558
|
+
try {
|
|
559
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
560
|
+
} catch (_error) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function resolveStartAgentCommand(repoRoot, details) {
|
|
566
|
+
const taskArg = shellQuote(details.taskName);
|
|
567
|
+
const agentArg = shellQuote(details.agentName);
|
|
568
|
+
const localCodexAgentPath = path.join(repoRoot, 'scripts', 'codex-agent.sh');
|
|
569
|
+
if (fs.existsSync(localCodexAgentPath)) {
|
|
570
|
+
return `bash ./scripts/codex-agent.sh ${taskArg} ${agentArg}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const agentCodexScript = readPackageJson(repoRoot)?.scripts?.['agent:codex'];
|
|
574
|
+
if (typeof agentCodexScript === 'string' && agentCodexScript.trim().length > 0) {
|
|
575
|
+
return `npm run agent:codex -- ${taskArg} ${agentArg}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return `gx branch start ${taskArg} ${agentArg}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
263
581
|
function sessionDisplayLabel(session) {
|
|
264
582
|
return session?.taskName || session?.label || session?.branch || path.basename(session?.worktreePath || '') || 'session';
|
|
265
583
|
}
|
|
266
584
|
|
|
585
|
+
function sessionTreeLabel(session) {
|
|
586
|
+
return session?.branch || sessionDisplayLabel(session);
|
|
587
|
+
}
|
|
588
|
+
|
|
267
589
|
function sessionWorktreePath(session) {
|
|
268
590
|
return typeof session?.worktreePath === 'string' ? session.worktreePath.trim() : '';
|
|
269
591
|
}
|
|
@@ -317,16 +639,34 @@ function syncSession(session) {
|
|
|
317
639
|
runSessionTerminalCommand(session, 'Sync', 'sync', 'gx sync');
|
|
318
640
|
}
|
|
319
641
|
|
|
642
|
+
function execFileAsync(command, args, options = {}) {
|
|
643
|
+
return new Promise((resolve, reject) => {
|
|
644
|
+
cp.execFile(command, args, options, (error, stdout = '', stderr = '') => {
|
|
645
|
+
if (error) {
|
|
646
|
+
error.stdout = stdout;
|
|
647
|
+
error.stderr = stderr;
|
|
648
|
+
reject(error);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
resolve({ stdout, stderr });
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
320
656
|
async function stopSession(session, refresh) {
|
|
321
657
|
const pid = Number(session?.pid);
|
|
322
658
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
323
659
|
showSessionMessage('Cannot stop session: missing pid.');
|
|
324
660
|
return;
|
|
325
661
|
}
|
|
662
|
+
if (!session?.branch) {
|
|
663
|
+
showSessionMessage('Cannot stop session: missing branch name.');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
326
666
|
|
|
327
667
|
const confirmed = await vscode.window.showWarningMessage(
|
|
328
668
|
`Stop ${sessionDisplayLabel(session)}?`,
|
|
329
|
-
{ modal: true, detail: `
|
|
669
|
+
{ modal: true, detail: `Run gx agents stop --pid ${pid}.` },
|
|
330
670
|
'Stop',
|
|
331
671
|
);
|
|
332
672
|
if (confirmed !== 'Stop') {
|
|
@@ -334,42 +674,87 @@ async function stopSession(session, refresh) {
|
|
|
334
674
|
}
|
|
335
675
|
|
|
336
676
|
try {
|
|
337
|
-
process.
|
|
677
|
+
const commandCwd = session?.repoRoot || sessionWorktreePath(session) || process.cwd();
|
|
678
|
+
const args = ['agents', 'stop', '--pid', String(pid)];
|
|
679
|
+
if (session?.repoRoot) {
|
|
680
|
+
args.push('--target', session.repoRoot);
|
|
681
|
+
}
|
|
682
|
+
await execFileAsync('gx', args, {
|
|
683
|
+
cwd: commandCwd,
|
|
684
|
+
encoding: 'utf8',
|
|
685
|
+
maxBuffer: 1024 * 1024,
|
|
686
|
+
});
|
|
338
687
|
refresh();
|
|
339
688
|
} catch (error) {
|
|
340
689
|
showSessionMessage(
|
|
341
|
-
`Failed to stop session ${sessionDisplayLabel(session)}: ${
|
|
690
|
+
`Failed to stop session ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`,
|
|
342
691
|
);
|
|
343
692
|
}
|
|
344
693
|
}
|
|
345
694
|
|
|
695
|
+
function sessionChangedPaths(session) {
|
|
696
|
+
const directPaths = Array.isArray(session?.changedPaths)
|
|
697
|
+
? session.changedPaths.map(normalizeRelativePath).filter(Boolean)
|
|
698
|
+
: [];
|
|
699
|
+
if (directPaths.length > 0) {
|
|
700
|
+
return [...new Set(directPaths)];
|
|
701
|
+
}
|
|
702
|
+
if (!session?.repoRoot || !session?.branch) {
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const liveSession = readActiveSessions(session.repoRoot)
|
|
707
|
+
.find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(session));
|
|
708
|
+
return Array.isArray(liveSession?.changedPaths)
|
|
709
|
+
? [...new Set(liveSession.changedPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
710
|
+
: [];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function pickSessionDiffPath(session) {
|
|
714
|
+
const changedPaths = sessionChangedPaths(session);
|
|
715
|
+
if (changedPaths.length === 0) {
|
|
716
|
+
return '';
|
|
717
|
+
}
|
|
718
|
+
if (changedPaths.length === 1 || !vscode.window.showQuickPick) {
|
|
719
|
+
return changedPaths[0];
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const picks = changedPaths.map((relativePath) => ({
|
|
723
|
+
label: path.basename(relativePath),
|
|
724
|
+
description: relativePath,
|
|
725
|
+
relativePath,
|
|
726
|
+
}));
|
|
727
|
+
const selection = await vscode.window.showQuickPick(picks, {
|
|
728
|
+
placeHolder: `Select a changed file for ${sessionDisplayLabel(session)}`,
|
|
729
|
+
ignoreFocusOut: true,
|
|
730
|
+
});
|
|
731
|
+
return selection?.relativePath || '';
|
|
732
|
+
}
|
|
733
|
+
|
|
346
734
|
async function openSessionDiff(session) {
|
|
347
735
|
const worktreePath = ensureSessionWorktree(session, 'open diff');
|
|
348
736
|
if (!worktreePath) {
|
|
349
737
|
return;
|
|
350
738
|
}
|
|
351
739
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
encoding: 'utf8',
|
|
356
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
357
|
-
});
|
|
358
|
-
} catch (error) {
|
|
359
|
-
const detail = [
|
|
360
|
-
error?.stdout,
|
|
361
|
-
error?.stderr,
|
|
362
|
-
error?.message,
|
|
363
|
-
].find((value) => typeof value === 'string' && value.trim().length > 0) || 'git diff failed.';
|
|
364
|
-
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${detail.trim()}`);
|
|
740
|
+
const relativePath = await pickSessionDiffPath(session);
|
|
741
|
+
if (!relativePath) {
|
|
742
|
+
showSessionMessage(`No changed files to diff for ${sessionDisplayLabel(session)}.`);
|
|
365
743
|
return;
|
|
366
744
|
}
|
|
367
745
|
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
746
|
+
const repoRoot = session?.repoRoot || worktreePath;
|
|
747
|
+
const absolutePath = path.resolve(repoRoot, relativePath);
|
|
748
|
+
const resourceUri = vscode.Uri.file(absolutePath);
|
|
749
|
+
try {
|
|
750
|
+
await vscode.commands.executeCommand('git.openChange', resourceUri);
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (fs.existsSync(absolutePath)) {
|
|
753
|
+
await vscode.commands.executeCommand('vscode.open', resourceUri);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
showSessionMessage(`Failed to open diff for ${sessionDisplayLabel(session)}: ${formatGitCommandFailure(error)}`);
|
|
757
|
+
}
|
|
373
758
|
}
|
|
374
759
|
|
|
375
760
|
function repoRootFromSessionFile(filePath) {
|
|
@@ -451,10 +836,127 @@ function readCurrentBranch(repoRoot) {
|
|
|
451
836
|
}
|
|
452
837
|
}
|
|
453
838
|
|
|
839
|
+
function parseSimpleSemver(version) {
|
|
840
|
+
const parts = String(version || '')
|
|
841
|
+
.split('.')
|
|
842
|
+
.map((part) => Number.parseInt(part, 10));
|
|
843
|
+
if (parts.length !== 3 || parts.some((part) => Number.isNaN(part))) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
return parts;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function compareSimpleSemver(left, right) {
|
|
850
|
+
const leftParts = parseSimpleSemver(left);
|
|
851
|
+
const rightParts = parseSimpleSemver(right);
|
|
852
|
+
if (!leftParts || !rightParts) {
|
|
853
|
+
return 0;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
857
|
+
if (leftParts[index] !== rightParts[index]) {
|
|
858
|
+
return leftParts[index] - rightParts[index];
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return 0;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function readJsonFile(filePath) {
|
|
866
|
+
try {
|
|
867
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
868
|
+
} catch (_error) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function resolveActiveAgentsAutoUpdateCandidate(installedVersion) {
|
|
874
|
+
const candidates = [];
|
|
875
|
+
|
|
876
|
+
for (const workspaceFolder of vscode.workspace.workspaceFolders || []) {
|
|
877
|
+
const repoRoot = workspaceFolder?.uri?.fsPath;
|
|
878
|
+
if (!repoRoot) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const manifestPath = path.join(repoRoot, ACTIVE_AGENTS_MANIFEST_RELATIVE);
|
|
883
|
+
const installScriptPath = path.join(repoRoot, ACTIVE_AGENTS_INSTALL_SCRIPT_RELATIVE);
|
|
884
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(installScriptPath)) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const manifest = readJsonFile(manifestPath);
|
|
889
|
+
const nextVersion = typeof manifest?.version === 'string' ? manifest.version.trim() : '';
|
|
890
|
+
if (!nextVersion || compareSimpleSemver(nextVersion, installedVersion) <= 0) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
candidates.push({ repoRoot, installScriptPath, version: nextVersion });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
candidates.sort((left, right) => compareSimpleSemver(right.version, left.version));
|
|
898
|
+
return candidates[0] || null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function runActiveAgentsInstallScript(repoRoot, installScriptPath) {
|
|
902
|
+
return new Promise((resolve, reject) => {
|
|
903
|
+
cp.execFile(
|
|
904
|
+
process.execPath,
|
|
905
|
+
[installScriptPath],
|
|
906
|
+
{ cwd: repoRoot, encoding: 'utf8' },
|
|
907
|
+
(error, stdout, stderr) => {
|
|
908
|
+
if (error) {
|
|
909
|
+
reject(new Error(String(stderr || stdout || error.message || '').trim() || 'install failed'));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
resolve({ stdout, stderr });
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function maybeAutoUpdateActiveAgentsExtension(context) {
|
|
919
|
+
const installedVersion = typeof context?.extension?.packageJSON?.version === 'string'
|
|
920
|
+
? context.extension.packageJSON.version.trim()
|
|
921
|
+
: '';
|
|
922
|
+
if (!installedVersion) {
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const candidate = resolveActiveAgentsAutoUpdateCandidate(installedVersion);
|
|
927
|
+
if (!candidate) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
await runActiveAgentsInstallScript(candidate.repoRoot, candidate.installScriptPath);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
const failure = typeof error?.message === 'string' && error.message.trim()
|
|
935
|
+
? error.message.trim()
|
|
936
|
+
: 'install failed';
|
|
937
|
+
vscode.window.showWarningMessage?.(
|
|
938
|
+
`GitGuardex Active Agents could not auto-update to ${candidate.version}: ${failure}`,
|
|
939
|
+
);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const selection = await vscode.window.showInformationMessage?.(
|
|
944
|
+
`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.`,
|
|
945
|
+
RELOAD_WINDOW_ACTION,
|
|
946
|
+
UPDATE_LATER_ACTION,
|
|
947
|
+
);
|
|
948
|
+
if (selection === RELOAD_WINDOW_ACTION) {
|
|
949
|
+
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
454
953
|
function decorateSession(session, lockRegistry) {
|
|
954
|
+
const touchedChanges = buildSessionTouchedChanges(session, lockRegistry);
|
|
455
955
|
return {
|
|
456
956
|
...session,
|
|
457
957
|
lockCount: lockRegistry.countsByBranch.get(session.branch) || 0,
|
|
958
|
+
touchedChanges,
|
|
959
|
+
conflictCount: touchedChanges.filter((change) => change.hasForeignLock).length,
|
|
458
960
|
};
|
|
459
961
|
}
|
|
460
962
|
|
|
@@ -468,6 +970,28 @@ function decorateChange(change, lockRegistry, owningBranch) {
|
|
|
468
970
|
};
|
|
469
971
|
}
|
|
470
972
|
|
|
973
|
+
function buildSessionTouchedChanges(session, lockRegistry) {
|
|
974
|
+
const changedPaths = Array.isArray(session.worktreeChangedPaths)
|
|
975
|
+
? session.worktreeChangedPaths
|
|
976
|
+
: [];
|
|
977
|
+
return [...new Set(changedPaths.map(normalizeRelativePath).filter(Boolean))]
|
|
978
|
+
.sort((left, right) => left.localeCompare(right))
|
|
979
|
+
.map((relativePath) => {
|
|
980
|
+
const lockEntry = lockRegistry.entriesByPath.get(relativePath);
|
|
981
|
+
const lockOwnerBranch = lockEntry?.branch || '';
|
|
982
|
+
return {
|
|
983
|
+
relativePath,
|
|
984
|
+
absolutePath: path.join(session.worktreePath, relativePath),
|
|
985
|
+
originalPath: '',
|
|
986
|
+
statusCode: 'M',
|
|
987
|
+
statusLabel: 'M',
|
|
988
|
+
statusText: 'Touched',
|
|
989
|
+
lockOwnerBranch,
|
|
990
|
+
hasForeignLock: Boolean(lockOwnerBranch) && lockOwnerBranch !== session.branch,
|
|
991
|
+
};
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
471
995
|
function isPathWithin(parentPath, targetPath) {
|
|
472
996
|
const relativePath = path.relative(parentPath, targetPath);
|
|
473
997
|
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
@@ -644,6 +1168,61 @@ function countWorkingSessions(sessions) {
|
|
|
644
1168
|
return sessions.filter((session) => session.activityKind === 'working').length;
|
|
645
1169
|
}
|
|
646
1170
|
|
|
1171
|
+
function countChangedPaths(repoRoot, sessions, changes) {
|
|
1172
|
+
const changedKeys = new Set();
|
|
1173
|
+
|
|
1174
|
+
for (const change of changes || []) {
|
|
1175
|
+
if (change?.relativePath) {
|
|
1176
|
+
changedKeys.add(normalizeRelativePath(change.relativePath));
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
for (const session of sessions || []) {
|
|
1181
|
+
for (const change of session.touchedChanges || []) {
|
|
1182
|
+
const absolutePath = change?.absolutePath
|
|
1183
|
+
|| path.join(session.worktreePath || '', change?.relativePath || '');
|
|
1184
|
+
const normalizedRelativePath = absolutePath && isPathWithin(repoRoot, absolutePath)
|
|
1185
|
+
? normalizeRelativePath(path.relative(repoRoot, absolutePath))
|
|
1186
|
+
: `${session.branch}:${normalizeRelativePath(change?.relativePath)}`;
|
|
1187
|
+
if (normalizedRelativePath) {
|
|
1188
|
+
changedKeys.add(normalizedRelativePath);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
return changedKeys.size;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function groupSessionsByWorktree(sessions) {
|
|
1197
|
+
const sessionsByWorktree = new Map();
|
|
1198
|
+
|
|
1199
|
+
for (const session of sessions || []) {
|
|
1200
|
+
const worktreePath = sessionWorktreePath(session);
|
|
1201
|
+
const key = worktreePath || session?.branch || `session-${sessionsByWorktree.size + 1}`;
|
|
1202
|
+
if (!sessionsByWorktree.has(key)) {
|
|
1203
|
+
sessionsByWorktree.set(key, {
|
|
1204
|
+
worktreePath,
|
|
1205
|
+
sessions: [],
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
sessionsByWorktree.get(key).sessions.push(session);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return [...sessionsByWorktree.values()]
|
|
1212
|
+
.map((entry) => ({
|
|
1213
|
+
...entry,
|
|
1214
|
+
sessions: entry.sessions.sort((left, right) => (
|
|
1215
|
+
sessionTreeLabel(left).localeCompare(sessionTreeLabel(right))
|
|
1216
|
+
)),
|
|
1217
|
+
}))
|
|
1218
|
+
.sort((left, right) => {
|
|
1219
|
+
const leftLabel = path.basename(left.worktreePath || '') || '';
|
|
1220
|
+
const rightLabel = path.basename(right.worktreePath || '') || '';
|
|
1221
|
+
return leftLabel.localeCompare(rightLabel)
|
|
1222
|
+
|| (left.worktreePath || '').localeCompare(right.worktreePath || '');
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
647
1226
|
function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
648
1227
|
const changesBySession = new Map();
|
|
649
1228
|
const sessionByChangedPath = new Map();
|
|
@@ -676,15 +1255,22 @@ function buildGroupedChangeTreeNodes(sessions, changes) {
|
|
|
676
1255
|
changesBySession.get(session.branch).push(localizedChange);
|
|
677
1256
|
}
|
|
678
1257
|
|
|
679
|
-
const items =
|
|
680
|
-
.
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1258
|
+
const items = groupSessionsByWorktree(
|
|
1259
|
+
sessions.filter((session) => (changesBySession.get(session.branch) || []).length > 0),
|
|
1260
|
+
).map(({ worktreePath, sessions: worktreeSessions }) => {
|
|
1261
|
+
const sessionItems = worktreeSessions.map((session) => (
|
|
1262
|
+
new SessionItem(
|
|
1263
|
+
session,
|
|
1264
|
+
buildChangeTreeNodes(changesBySession.get(session.branch) || []),
|
|
1265
|
+
{ label: sessionTreeLabel(session) },
|
|
1266
|
+
)
|
|
1267
|
+
));
|
|
1268
|
+
const changedCount = worktreeSessions.reduce(
|
|
1269
|
+
(total, session) => total + ((changesBySession.get(session.branch) || []).length),
|
|
1270
|
+
0,
|
|
1271
|
+
);
|
|
1272
|
+
return new WorktreeItem(worktreePath, worktreeSessions, sessionItems, { changedCount });
|
|
1273
|
+
});
|
|
688
1274
|
|
|
689
1275
|
if (repoRootChanges.length > 0) {
|
|
690
1276
|
items.push(new SectionItem('Repo root', buildChangeTreeNodes(repoRootChanges), {
|
|
@@ -724,14 +1310,14 @@ async function pickRepoRoot() {
|
|
|
724
1310
|
repoRoot: folder.uri.fsPath,
|
|
725
1311
|
}));
|
|
726
1312
|
const selection = await vscode.window.showQuickPick?.(picks, {
|
|
727
|
-
placeHolder: 'Select the Guardex repo where
|
|
1313
|
+
placeHolder: 'Select the Guardex repo where the Start agent launcher should run.',
|
|
728
1314
|
});
|
|
729
1315
|
return selection?.repoRoot || null;
|
|
730
1316
|
}
|
|
731
1317
|
|
|
732
1318
|
async function promptStartAgentDetails() {
|
|
733
1319
|
const taskName = await vscode.window.showInputBox?.({
|
|
734
|
-
prompt: 'Task for
|
|
1320
|
+
prompt: 'Task for the Guardex agent launcher',
|
|
735
1321
|
placeHolder: 'vscode active agents welcome view',
|
|
736
1322
|
ignoreFocusOut: true,
|
|
737
1323
|
validateInput: (value) => value.trim() ? undefined : 'Task is required.',
|
|
@@ -741,7 +1327,7 @@ async function promptStartAgentDetails() {
|
|
|
741
1327
|
}
|
|
742
1328
|
|
|
743
1329
|
const agentName = await vscode.window.showInputBox?.({
|
|
744
|
-
prompt: 'Agent name for
|
|
1330
|
+
prompt: 'Agent name for the Guardex agent launcher',
|
|
745
1331
|
placeHolder: 'codex',
|
|
746
1332
|
value: 'codex',
|
|
747
1333
|
ignoreFocusOut: true,
|
|
@@ -773,10 +1359,7 @@ async function startAgentFromPrompt(refresh) {
|
|
|
773
1359
|
cwd: repoRoot,
|
|
774
1360
|
});
|
|
775
1361
|
terminal?.show(true);
|
|
776
|
-
terminal?.sendText(
|
|
777
|
-
`gx branch start ${shellQuote(details.taskName)} ${shellQuote(details.agentName)}`,
|
|
778
|
-
true,
|
|
779
|
-
);
|
|
1362
|
+
terminal?.sendText(resolveStartAgentCommand(repoRoot, details), true);
|
|
780
1363
|
refresh();
|
|
781
1364
|
}
|
|
782
1365
|
|
|
@@ -815,11 +1398,20 @@ function commitWorktree(worktreePath, message) {
|
|
|
815
1398
|
function buildActiveAgentGroupNodes(sessions) {
|
|
816
1399
|
const groups = [];
|
|
817
1400
|
for (const group of SESSION_ACTIVITY_GROUPS) {
|
|
818
|
-
const groupSessions = sessions
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1401
|
+
const groupSessions = sessions.filter((session) => session.activityKind === group.kind);
|
|
1402
|
+
const worktreeItems = groupSessionsByWorktree(groupSessions).map(({ worktreePath, sessions: worktreeSessions }) => (
|
|
1403
|
+
new WorktreeItem(
|
|
1404
|
+
worktreePath,
|
|
1405
|
+
worktreeSessions,
|
|
1406
|
+
worktreeSessions.map((session) => new SessionItem(
|
|
1407
|
+
session,
|
|
1408
|
+
buildChangeTreeNodes(session.touchedChanges || []),
|
|
1409
|
+
{ label: sessionTreeLabel(session) },
|
|
1410
|
+
)),
|
|
1411
|
+
)
|
|
1412
|
+
));
|
|
1413
|
+
if (worktreeItems.length > 0) {
|
|
1414
|
+
groups.push(new SectionItem(group.label, worktreeItems));
|
|
823
1415
|
}
|
|
824
1416
|
}
|
|
825
1417
|
|
|
@@ -836,6 +1428,12 @@ class ActiveAgentsProvider {
|
|
|
836
1428
|
this.treeView = null;
|
|
837
1429
|
this.lockRegistryByRepoRoot = new Map();
|
|
838
1430
|
this.selectedSession = null;
|
|
1431
|
+
this.viewSummary = {
|
|
1432
|
+
sessionCount: 0,
|
|
1433
|
+
workingCount: 0,
|
|
1434
|
+
deadCount: 0,
|
|
1435
|
+
conflictCount: 0,
|
|
1436
|
+
};
|
|
839
1437
|
}
|
|
840
1438
|
|
|
841
1439
|
getTreeItem(element) {
|
|
@@ -856,6 +1454,7 @@ class ActiveAgentsProvider {
|
|
|
856
1454
|
const currentKey = sessionSelectionKey(this.selectedSession);
|
|
857
1455
|
const nextKey = sessionSelectionKey(nextSession);
|
|
858
1456
|
this.selectedSession = nextSession;
|
|
1457
|
+
this.decorationProvider?.setSelectedBranch(nextSession?.branch || '');
|
|
859
1458
|
if (currentKey !== nextKey) {
|
|
860
1459
|
this.onDidChangeSelectedSessionEmitter.fire(this.selectedSession);
|
|
861
1460
|
}
|
|
@@ -865,6 +1464,10 @@ class ActiveAgentsProvider {
|
|
|
865
1464
|
return this.selectedSession ? { ...this.selectedSession } : null;
|
|
866
1465
|
}
|
|
867
1466
|
|
|
1467
|
+
getViewSummary() {
|
|
1468
|
+
return { ...this.viewSummary };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
868
1471
|
syncSelectedSession(repoEntries) {
|
|
869
1472
|
if (!this.selectedSession) {
|
|
870
1473
|
return;
|
|
@@ -876,12 +1479,20 @@ class ActiveAgentsProvider {
|
|
|
876
1479
|
this.setSelectedSession(nextSession || null);
|
|
877
1480
|
}
|
|
878
1481
|
|
|
879
|
-
updateViewState(sessionCount, workingCount, deadCount) {
|
|
1482
|
+
updateViewState(sessionCount, workingCount, deadCount, conflictCount = 0) {
|
|
880
1483
|
if (!this.treeView) {
|
|
881
1484
|
return;
|
|
882
1485
|
}
|
|
883
1486
|
|
|
884
1487
|
const activeCount = Math.max(0, sessionCount - deadCount);
|
|
1488
|
+
this.viewSummary = {
|
|
1489
|
+
sessionCount,
|
|
1490
|
+
workingCount,
|
|
1491
|
+
deadCount,
|
|
1492
|
+
conflictCount,
|
|
1493
|
+
};
|
|
1494
|
+
void vscode.commands.executeCommand('setContext', 'guardex.hasAgents', sessionCount > 0);
|
|
1495
|
+
void vscode.commands.executeCommand('setContext', 'guardex.hasConflicts', conflictCount > 0);
|
|
885
1496
|
const badgeTooltipParts = [];
|
|
886
1497
|
if (activeCount > 0) {
|
|
887
1498
|
badgeTooltipParts.push(`${activeCount} active agent${activeCount === 1 ? '' : 's'}`);
|
|
@@ -892,6 +1503,9 @@ class ActiveAgentsProvider {
|
|
|
892
1503
|
if (workingCount > 0) {
|
|
893
1504
|
badgeTooltipParts.push(`${workingCount} working now`);
|
|
894
1505
|
}
|
|
1506
|
+
if (conflictCount > 0) {
|
|
1507
|
+
badgeTooltipParts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
|
|
1508
|
+
}
|
|
895
1509
|
|
|
896
1510
|
this.treeView.badge = sessionCount > 0
|
|
897
1511
|
? {
|
|
@@ -899,9 +1513,7 @@ class ActiveAgentsProvider {
|
|
|
899
1513
|
tooltip: badgeTooltipParts.join(' · '),
|
|
900
1514
|
}
|
|
901
1515
|
: undefined;
|
|
902
|
-
this.treeView.message =
|
|
903
|
-
? undefined
|
|
904
|
-
: 'Start a sandbox session to populate this view.';
|
|
1516
|
+
this.treeView.message = undefined;
|
|
905
1517
|
}
|
|
906
1518
|
|
|
907
1519
|
async syncRepoEntries() {
|
|
@@ -915,9 +1527,14 @@ class ActiveAgentsProvider {
|
|
|
915
1527
|
(total, entry) => total + countSessionsByActivityKind(entry.sessions, 'dead'),
|
|
916
1528
|
0,
|
|
917
1529
|
);
|
|
1530
|
+
const conflictCount = repoEntries.reduce(
|
|
1531
|
+
(total, entry) => total + countEntryConflicts(entry),
|
|
1532
|
+
0,
|
|
1533
|
+
);
|
|
918
1534
|
|
|
919
|
-
this.updateViewState(sessionCount, workingCount, deadCount);
|
|
1535
|
+
this.updateViewState(sessionCount, workingCount, deadCount, conflictCount);
|
|
920
1536
|
this.decorationProvider?.updateSessions(repoEntries.flatMap((entry) => entry.sessions));
|
|
1537
|
+
this.decorationProvider?.updateLockEntries(repoEntries);
|
|
921
1538
|
return repoEntries;
|
|
922
1539
|
}
|
|
923
1540
|
|
|
@@ -941,20 +1558,6 @@ class ActiveAgentsProvider {
|
|
|
941
1558
|
this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
|
|
942
1559
|
}
|
|
943
1560
|
|
|
944
|
-
readLockRegistryForRepo(repoRoot) {
|
|
945
|
-
const lockRegistry = readLockRegistry(repoRoot);
|
|
946
|
-
this.lockRegistryByRepoRoot.set(repoRoot, lockRegistry);
|
|
947
|
-
return lockRegistry;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
getLockRegistryForRepo(repoRoot) {
|
|
951
|
-
return this.lockRegistryByRepoRoot.get(repoRoot) || this.readLockRegistryForRepo(repoRoot);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
refreshLockRegistryForFile(filePath) {
|
|
955
|
-
this.readLockRegistryForRepo(repoRootFromLockFile(filePath));
|
|
956
|
-
}
|
|
957
|
-
|
|
958
1561
|
async getChildren(element) {
|
|
959
1562
|
if (element instanceof RepoItem) {
|
|
960
1563
|
const sectionItems = [
|
|
@@ -970,7 +1573,7 @@ class ActiveAgentsProvider {
|
|
|
970
1573
|
return sectionItems;
|
|
971
1574
|
}
|
|
972
1575
|
|
|
973
|
-
if (element instanceof SectionItem || element instanceof FolderItem || element instanceof SessionItem) {
|
|
1576
|
+
if (element instanceof SectionItem || element instanceof FolderItem || element instanceof WorktreeItem || element instanceof SessionItem) {
|
|
974
1577
|
return element.items;
|
|
975
1578
|
}
|
|
976
1579
|
|
|
@@ -996,14 +1599,101 @@ class ActiveAgentsProvider {
|
|
|
996
1599
|
changes: readRepoChanges(repoRoot).map((change) => (
|
|
997
1600
|
decorateChange(change, lockRegistry, currentBranch)
|
|
998
1601
|
)),
|
|
1602
|
+
lockEntries: Array.from(lockRegistry.entriesByPath.entries()),
|
|
999
1603
|
};
|
|
1000
1604
|
});
|
|
1001
1605
|
}
|
|
1002
1606
|
}
|
|
1003
1607
|
|
|
1608
|
+
function countEntryConflicts(entry) {
|
|
1609
|
+
const sessionConflicts = entry.sessions.reduce(
|
|
1610
|
+
(total, session) => total + (session.conflictCount || 0),
|
|
1611
|
+
0,
|
|
1612
|
+
);
|
|
1613
|
+
const changeConflicts = entry.changes.filter((change) => change.hasForeignLock).length;
|
|
1614
|
+
return sessionConflicts + changeConflicts;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
class SessionInspectPanelManager {
|
|
1618
|
+
constructor() {
|
|
1619
|
+
this.panel = null;
|
|
1620
|
+
this.session = null;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
open(session) {
|
|
1624
|
+
const targetSession = session?.branch ? { ...session } : null;
|
|
1625
|
+
if (!targetSession?.repoRoot || !targetSession?.branch) {
|
|
1626
|
+
showSessionMessage('Pick an Active Agents session first.');
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (!vscode.window.createWebviewPanel) {
|
|
1630
|
+
showSessionMessage('Inspect panel is unavailable in this VS Code build.');
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
this.session = targetSession;
|
|
1635
|
+
if (!this.panel) {
|
|
1636
|
+
this.panel = vscode.window.createWebviewPanel(
|
|
1637
|
+
INSPECT_PANEL_VIEW_TYPE,
|
|
1638
|
+
inspectPanelTitle(targetSession),
|
|
1639
|
+
vscode.ViewColumn?.Beside,
|
|
1640
|
+
{
|
|
1641
|
+
enableFindWidget: true,
|
|
1642
|
+
enableScripts: false,
|
|
1643
|
+
retainContextWhenHidden: true,
|
|
1644
|
+
},
|
|
1645
|
+
);
|
|
1646
|
+
this.panel.onDidDispose(() => {
|
|
1647
|
+
this.panel = null;
|
|
1648
|
+
this.session = null;
|
|
1649
|
+
});
|
|
1650
|
+
} else {
|
|
1651
|
+
this.panel.reveal?.(vscode.ViewColumn?.Beside);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
this.render();
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
resolveSession() {
|
|
1658
|
+
if (!this.session?.repoRoot || !this.session?.branch) {
|
|
1659
|
+
return this.session ? { ...this.session } : null;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
return readActiveSessions(this.session.repoRoot, { includeStale: true })
|
|
1663
|
+
.find((entry) => sessionSelectionKey(entry) === sessionSelectionKey(this.session))
|
|
1664
|
+
|| { ...this.session };
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
render() {
|
|
1668
|
+
if (!this.panel || !this.session) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const session = this.resolveSession();
|
|
1673
|
+
if (!session) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
this.session = { ...session };
|
|
1678
|
+
this.panel.title = inspectPanelTitle(session);
|
|
1679
|
+
this.panel.webview.html = renderInspectPanelHtml(session, readSessionInspectData(session));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
refresh() {
|
|
1683
|
+
this.render();
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
dispose() {
|
|
1687
|
+
this.panel?.dispose();
|
|
1688
|
+
this.panel = null;
|
|
1689
|
+
this.session = null;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1004
1693
|
class ActiveAgentsRefreshController {
|
|
1005
|
-
constructor(provider) {
|
|
1694
|
+
constructor(provider, inspectPanelManager = null) {
|
|
1006
1695
|
this.provider = provider;
|
|
1696
|
+
this.inspectPanelManager = inspectPanelManager;
|
|
1007
1697
|
this.refreshTimer = null;
|
|
1008
1698
|
this.sessionWatchers = new Map();
|
|
1009
1699
|
}
|
|
@@ -1021,6 +1711,7 @@ class ActiveAgentsRefreshController {
|
|
|
1021
1711
|
async refreshNow() {
|
|
1022
1712
|
await this.syncSessionWatchers();
|
|
1023
1713
|
await this.provider.refresh();
|
|
1714
|
+
this.inspectPanelManager?.refresh();
|
|
1024
1715
|
}
|
|
1025
1716
|
|
|
1026
1717
|
async syncSessionWatchers() {
|
|
@@ -1071,7 +1762,8 @@ class ActiveAgentsRefreshController {
|
|
|
1071
1762
|
function activate(context) {
|
|
1072
1763
|
const decorationProvider = new SessionDecorationProvider();
|
|
1073
1764
|
const provider = new ActiveAgentsProvider(decorationProvider);
|
|
1074
|
-
const
|
|
1765
|
+
const inspectPanelManager = new SessionInspectPanelManager();
|
|
1766
|
+
const refreshController = new ActiveAgentsRefreshController(provider, inspectPanelManager);
|
|
1075
1767
|
const treeView = vscode.window.createTreeView('gitguardex.activeAgents', {
|
|
1076
1768
|
treeDataProvider: provider,
|
|
1077
1769
|
showCollapseAll: true,
|
|
@@ -1080,20 +1772,37 @@ function activate(context) {
|
|
|
1080
1772
|
'gitguardex.activeAgents.commitInput',
|
|
1081
1773
|
'Active Agents Commit',
|
|
1082
1774
|
);
|
|
1775
|
+
const activeAgentsStatusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10);
|
|
1776
|
+
activeAgentsStatusItem.name = 'GitGuardex Active Agents';
|
|
1777
|
+
activeAgentsStatusItem.command = 'gitguardex.activeAgents.focus';
|
|
1083
1778
|
provider.attachTreeView(treeView);
|
|
1084
1779
|
const scheduleRefresh = () => refreshController.scheduleRefresh();
|
|
1085
1780
|
const refresh = () => void refreshController.refreshNow();
|
|
1086
1781
|
const activeSessionsWatcher = vscode.workspace.createFileSystemWatcher(ACTIVE_SESSION_FILES_GLOB);
|
|
1087
1782
|
const lockWatcher = vscode.workspace.createFileSystemWatcher(AGENT_FILE_LOCKS_GLOB);
|
|
1088
1783
|
const worktreeLockWatcher = vscode.workspace.createFileSystemWatcher(WORKTREE_AGENT_LOCKS_GLOB);
|
|
1784
|
+
const logWatcher = vscode.workspace.createFileSystemWatcher(AGENT_LOG_FILES_GLOB);
|
|
1089
1785
|
const updateCommitInput = (session) => {
|
|
1090
1786
|
sourceControl.inputBox.enabled = true;
|
|
1091
1787
|
sourceControl.inputBox.visible = true;
|
|
1092
|
-
sourceControl.inputBox.placeholder = session
|
|
1093
|
-
|
|
1094
|
-
|
|
1788
|
+
sourceControl.inputBox.placeholder = sessionCommitPlaceholder(session);
|
|
1789
|
+
};
|
|
1790
|
+
const updateStatusBar = () => {
|
|
1791
|
+
const selectedSession = provider.getSelectedSession();
|
|
1792
|
+
const summary = provider.getViewSummary();
|
|
1793
|
+
if ((summary.sessionCount || 0) <= 0) {
|
|
1794
|
+
activeAgentsStatusItem.hide();
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
activeAgentsStatusItem.text = selectedSession?.branch
|
|
1799
|
+
? `$(git-branch) ${sessionIdentityLabel(selectedSession)} · ${formatCountLabel(selectedSession.lockCount || 0, 'lock')}`
|
|
1800
|
+
: buildActiveAgentsStatusSummary(summary);
|
|
1801
|
+
activeAgentsStatusItem.tooltip = buildActiveAgentsStatusTooltip(selectedSession, summary);
|
|
1802
|
+
activeAgentsStatusItem.show();
|
|
1095
1803
|
};
|
|
1096
1804
|
updateCommitInput(null);
|
|
1805
|
+
updateStatusBar();
|
|
1097
1806
|
const commitSelectedSession = async () => {
|
|
1098
1807
|
const selectedSession = provider.getSelectedSession();
|
|
1099
1808
|
if (!selectedSession?.worktreePath) {
|
|
@@ -1132,7 +1841,7 @@ function activate(context) {
|
|
|
1132
1841
|
command: 'gitguardex.activeAgents.commitSelectedSession',
|
|
1133
1842
|
title: 'Commit Selected Session',
|
|
1134
1843
|
};
|
|
1135
|
-
const interval = setInterval(refresh,
|
|
1844
|
+
const interval = setInterval(refresh, REFRESH_POLL_INTERVAL_MS);
|
|
1136
1845
|
const refreshLockRegistry = (uri) => {
|
|
1137
1846
|
if (uri?.fsPath) {
|
|
1138
1847
|
provider.refreshLockRegistryForFile(uri.fsPath);
|
|
@@ -1140,15 +1849,28 @@ function activate(context) {
|
|
|
1140
1849
|
scheduleRefresh();
|
|
1141
1850
|
};
|
|
1142
1851
|
|
|
1143
|
-
provider.onDidChangeSelectedSession(
|
|
1852
|
+
provider.onDidChangeSelectedSession((session) => {
|
|
1853
|
+
updateCommitInput(session);
|
|
1854
|
+
updateStatusBar();
|
|
1855
|
+
decorationProvider.refresh();
|
|
1856
|
+
});
|
|
1857
|
+
provider.onDidChangeTreeData(() => {
|
|
1858
|
+
updateCommitInput(provider.getSelectedSession());
|
|
1859
|
+
updateStatusBar();
|
|
1860
|
+
});
|
|
1144
1861
|
|
|
1145
1862
|
context.subscriptions.push(
|
|
1146
1863
|
treeView,
|
|
1147
1864
|
sourceControl,
|
|
1865
|
+
activeAgentsStatusItem,
|
|
1866
|
+
inspectPanelManager,
|
|
1148
1867
|
refreshController,
|
|
1149
1868
|
vscode.window.registerFileDecorationProvider(decorationProvider),
|
|
1150
1869
|
vscode.commands.registerCommand('gitguardex.activeAgents.startAgent', () => startAgentFromPrompt(refresh)),
|
|
1151
1870
|
vscode.commands.registerCommand('gitguardex.activeAgents.refresh', refresh),
|
|
1871
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.focus', async () => {
|
|
1872
|
+
await vscode.commands.executeCommand('workbench.view.scm');
|
|
1873
|
+
}),
|
|
1152
1874
|
vscode.commands.registerCommand('gitguardex.activeAgents.commitSelectedSession', commitSelectedSession),
|
|
1153
1875
|
vscode.commands.registerCommand('gitguardex.activeAgents.openWorktree', async (session) => {
|
|
1154
1876
|
if (!session?.worktreePath) {
|
|
@@ -1173,6 +1895,9 @@ function activate(context) {
|
|
|
1173
1895
|
|
|
1174
1896
|
await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(change.absolutePath));
|
|
1175
1897
|
}),
|
|
1898
|
+
vscode.commands.registerCommand('gitguardex.activeAgents.inspect', (session) => {
|
|
1899
|
+
inspectPanelManager.open(session || provider.getSelectedSession());
|
|
1900
|
+
}),
|
|
1176
1901
|
vscode.commands.registerCommand('gitguardex.activeAgents.finishSession', finishSession),
|
|
1177
1902
|
vscode.commands.registerCommand('gitguardex.activeAgents.syncSession', syncSession),
|
|
1178
1903
|
vscode.commands.registerCommand('gitguardex.activeAgents.stopSession', (session) => stopSession(session, refresh)),
|
|
@@ -1181,6 +1906,7 @@ function activate(context) {
|
|
|
1181
1906
|
activeSessionsWatcher,
|
|
1182
1907
|
lockWatcher,
|
|
1183
1908
|
worktreeLockWatcher,
|
|
1909
|
+
logWatcher,
|
|
1184
1910
|
{ dispose: () => clearInterval(interval) },
|
|
1185
1911
|
);
|
|
1186
1912
|
|
|
@@ -1188,8 +1914,10 @@ function activate(context) {
|
|
|
1188
1914
|
...bindRefreshWatcher(activeSessionsWatcher, scheduleRefresh),
|
|
1189
1915
|
...bindRefreshWatcher(lockWatcher, refreshLockRegistry),
|
|
1190
1916
|
...bindRefreshWatcher(worktreeLockWatcher, scheduleRefresh),
|
|
1917
|
+
...bindRefreshWatcher(logWatcher, scheduleRefresh),
|
|
1191
1918
|
);
|
|
1192
1919
|
void refreshController.refreshNow();
|
|
1920
|
+
void maybeAutoUpdateActiveAgentsExtension(context);
|
|
1193
1921
|
}
|
|
1194
1922
|
|
|
1195
1923
|
function deactivate() {}
|