agentuity-vscode 0.1.5 → 0.1.7
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/package.json +475 -2
- package/src/core/cliClient.ts +477 -0
- package/src/core/index.ts +1 -0
- package/src/core/readonlyDocument.ts +11 -0
- package/src/core/sandboxManager.ts +483 -0
- package/src/extension.ts +14 -0
- package/src/features/chat/agentTools.ts +254 -1
- package/src/features/sandboxExplorer/index.ts +1375 -0
- package/src/features/sandboxExplorer/sandboxTreeData.ts +803 -0
- package/src/features/sandboxExplorer/statusBar.ts +383 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import {
|
|
3
|
+
getCliClient,
|
|
4
|
+
type SandboxInfo,
|
|
5
|
+
type SandboxFileInfo,
|
|
6
|
+
type SnapshotInfo,
|
|
7
|
+
type ExecutionInfo,
|
|
8
|
+
type SandboxStatus,
|
|
9
|
+
} from '../../core/cliClient';
|
|
10
|
+
import { getAuthStatus } from '../../core/auth';
|
|
11
|
+
import { getSandboxManager, type LinkedSandbox } from '../../core/sandboxManager';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Types of items in the sandbox tree.
|
|
15
|
+
*/
|
|
16
|
+
export type SandboxItemType =
|
|
17
|
+
| 'sandbox'
|
|
18
|
+
| 'category'
|
|
19
|
+
| 'file'
|
|
20
|
+
| 'directory'
|
|
21
|
+
| 'snapshot'
|
|
22
|
+
| 'snapshotFile'
|
|
23
|
+
| 'execution'
|
|
24
|
+
| 'message'
|
|
25
|
+
| 'createSandbox';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A tree item in the sandbox explorer.
|
|
29
|
+
*/
|
|
30
|
+
export class SandboxTreeItem extends vscode.TreeItem {
|
|
31
|
+
constructor(
|
|
32
|
+
public readonly label: string,
|
|
33
|
+
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
|
34
|
+
public readonly itemType: SandboxItemType,
|
|
35
|
+
public readonly sandboxData?: SandboxInfo,
|
|
36
|
+
public readonly fileData?: SandboxFileInfo,
|
|
37
|
+
public readonly snapshotData?: SnapshotInfo,
|
|
38
|
+
public readonly executionData?: ExecutionInfo,
|
|
39
|
+
public readonly parentSandboxId?: string,
|
|
40
|
+
public readonly categoryType?: 'files' | 'snapshots' | 'executions',
|
|
41
|
+
public readonly linkedData?: LinkedSandbox,
|
|
42
|
+
public readonly filePath?: string
|
|
43
|
+
) {
|
|
44
|
+
super(label, collapsibleState);
|
|
45
|
+
this.setupItem();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private setupItem(): void {
|
|
49
|
+
switch (this.itemType) {
|
|
50
|
+
case 'sandbox':
|
|
51
|
+
this.setupSandboxItem();
|
|
52
|
+
break;
|
|
53
|
+
case 'category':
|
|
54
|
+
this.setupCategoryItem();
|
|
55
|
+
break;
|
|
56
|
+
case 'file':
|
|
57
|
+
case 'directory':
|
|
58
|
+
this.setupFileItem();
|
|
59
|
+
break;
|
|
60
|
+
case 'snapshot':
|
|
61
|
+
this.setupSnapshotItem();
|
|
62
|
+
break;
|
|
63
|
+
case 'snapshotFile':
|
|
64
|
+
this.setupSnapshotFileItem();
|
|
65
|
+
break;
|
|
66
|
+
case 'execution':
|
|
67
|
+
this.setupExecutionItem();
|
|
68
|
+
break;
|
|
69
|
+
case 'createSandbox':
|
|
70
|
+
this.setupCreateItem();
|
|
71
|
+
break;
|
|
72
|
+
case 'message':
|
|
73
|
+
this.iconPath = new vscode.ThemeIcon('info');
|
|
74
|
+
this.contextValue = 'message';
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private setupSandboxItem(): void {
|
|
80
|
+
if (!this.sandboxData) return;
|
|
81
|
+
|
|
82
|
+
const status = this.sandboxData.status;
|
|
83
|
+
const isLinked = this.linkedData !== undefined;
|
|
84
|
+
|
|
85
|
+
// Set icon based on status
|
|
86
|
+
this.iconPath = new vscode.ThemeIcon(
|
|
87
|
+
this.getStatusIcon(status),
|
|
88
|
+
this.getStatusColor(status)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Set context value for menu targeting
|
|
92
|
+
let contextValue = `sandbox.${status}`;
|
|
93
|
+
if (isLinked) {
|
|
94
|
+
contextValue += '.linked';
|
|
95
|
+
}
|
|
96
|
+
this.contextValue = contextValue;
|
|
97
|
+
|
|
98
|
+
// Set description
|
|
99
|
+
const statusLabel = status.charAt(0).toUpperCase() + status.slice(1);
|
|
100
|
+
this.description = isLinked ? `${statusLabel} [linked]` : statusLabel;
|
|
101
|
+
|
|
102
|
+
// Set tooltip
|
|
103
|
+
this.tooltip = this.formatSandboxTooltip();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private getStatusIcon(status: SandboxStatus): string {
|
|
107
|
+
switch (status) {
|
|
108
|
+
case 'idle':
|
|
109
|
+
return 'vm';
|
|
110
|
+
case 'running':
|
|
111
|
+
return 'vm-running';
|
|
112
|
+
case 'creating':
|
|
113
|
+
return 'loading~spin';
|
|
114
|
+
case 'terminated':
|
|
115
|
+
return 'vm-outline';
|
|
116
|
+
case 'failed':
|
|
117
|
+
return 'error';
|
|
118
|
+
default:
|
|
119
|
+
return 'vm';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private getStatusColor(status: SandboxStatus): vscode.ThemeColor | undefined {
|
|
124
|
+
switch (status) {
|
|
125
|
+
case 'idle':
|
|
126
|
+
return new vscode.ThemeColor('charts.blue');
|
|
127
|
+
case 'running':
|
|
128
|
+
return new vscode.ThemeColor('charts.green');
|
|
129
|
+
case 'failed':
|
|
130
|
+
return new vscode.ThemeColor('charts.red');
|
|
131
|
+
case 'terminated':
|
|
132
|
+
return new vscode.ThemeColor('disabledForeground');
|
|
133
|
+
default:
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private formatSandboxTooltip(): string {
|
|
139
|
+
if (!this.sandboxData) return '';
|
|
140
|
+
|
|
141
|
+
const lines = [
|
|
142
|
+
`ID: ${this.sandboxData.sandboxId}`,
|
|
143
|
+
`Status: ${this.sandboxData.status}`,
|
|
144
|
+
`Region: ${this.sandboxData.region}`,
|
|
145
|
+
`Created: ${new Date(this.sandboxData.createdAt).toLocaleString()}`,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
if (this.sandboxData.resources) {
|
|
149
|
+
const r = this.sandboxData.resources;
|
|
150
|
+
if (r.memory) lines.push(`Memory: ${r.memory}`);
|
|
151
|
+
if (r.cpu) lines.push(`CPU: ${r.cpu}`);
|
|
152
|
+
if (r.disk) lines.push(`Disk: ${r.disk}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.linkedData) {
|
|
156
|
+
lines.push('', '--- Linked ---');
|
|
157
|
+
lines.push(`Remote Path: ${this.linkedData.remotePath}`);
|
|
158
|
+
if (this.linkedData.lastSyncedAt) {
|
|
159
|
+
lines.push(`Last Synced: ${new Date(this.linkedData.lastSyncedAt).toLocaleString()}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private setupCategoryItem(): void {
|
|
167
|
+
switch (this.categoryType) {
|
|
168
|
+
case 'files':
|
|
169
|
+
this.iconPath = new vscode.ThemeIcon('folder');
|
|
170
|
+
this.contextValue = 'sandboxCategory.files';
|
|
171
|
+
break;
|
|
172
|
+
case 'snapshots':
|
|
173
|
+
this.iconPath = new vscode.ThemeIcon('device-camera');
|
|
174
|
+
this.contextValue = 'sandboxCategory.snapshots';
|
|
175
|
+
break;
|
|
176
|
+
case 'executions':
|
|
177
|
+
this.iconPath = new vscode.ThemeIcon('terminal');
|
|
178
|
+
this.contextValue = 'sandboxCategory.executions';
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private setupFileItem(): void {
|
|
184
|
+
if (this.itemType === 'directory') {
|
|
185
|
+
this.iconPath = new vscode.ThemeIcon('folder');
|
|
186
|
+
this.contextValue = 'sandboxFile.directory';
|
|
187
|
+
} else {
|
|
188
|
+
this.iconPath = new vscode.ThemeIcon('file');
|
|
189
|
+
this.contextValue = 'sandboxFile';
|
|
190
|
+
|
|
191
|
+
// Add click command to open file directly
|
|
192
|
+
this.command = {
|
|
193
|
+
command: 'agentuity.sandbox.viewFile',
|
|
194
|
+
title: 'Open File',
|
|
195
|
+
arguments: [this],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.fileData) {
|
|
200
|
+
this.description = this.formatFileSize(this.fileData.size);
|
|
201
|
+
this.tooltip = `${this.filePath}\nSize: ${this.formatFileSize(this.fileData.size)}\nModified: ${this.fileData.modTime}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private formatFileSize(bytes: number): string {
|
|
206
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
207
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
208
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
209
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private setupSnapshotItem(): void {
|
|
213
|
+
if (!this.snapshotData) return;
|
|
214
|
+
|
|
215
|
+
this.iconPath = new vscode.ThemeIcon('device-camera');
|
|
216
|
+
|
|
217
|
+
if (this.snapshotData.tag) {
|
|
218
|
+
this.contextValue = 'snapshot.tagged';
|
|
219
|
+
this.description = `[${this.snapshotData.tag}] ${this.snapshotData.fileCount} files`;
|
|
220
|
+
} else {
|
|
221
|
+
this.contextValue = 'snapshot';
|
|
222
|
+
this.description = `${this.snapshotData.fileCount} files`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.tooltip = [
|
|
226
|
+
`ID: ${this.snapshotData.snapshotId}`,
|
|
227
|
+
`Size: ${this.formatFileSize(this.snapshotData.sizeBytes)}`,
|
|
228
|
+
`Files: ${this.snapshotData.fileCount}`,
|
|
229
|
+
`Created: ${new Date(this.snapshotData.createdAt).toLocaleString()}`,
|
|
230
|
+
this.snapshotData.tag ? `Tag: ${this.snapshotData.tag}` : '',
|
|
231
|
+
'',
|
|
232
|
+
'Click to view snapshot details',
|
|
233
|
+
]
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.join('\n');
|
|
236
|
+
|
|
237
|
+
// Add click command to view snapshot details JSON
|
|
238
|
+
this.command = {
|
|
239
|
+
command: 'agentuity.sandbox.snapshot.viewDetails',
|
|
240
|
+
title: 'View Snapshot Details',
|
|
241
|
+
arguments: [this],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private setupSnapshotFileItem(): void {
|
|
246
|
+
this.iconPath = new vscode.ThemeIcon('file');
|
|
247
|
+
this.contextValue = 'snapshotFile';
|
|
248
|
+
|
|
249
|
+
if (this.fileData) {
|
|
250
|
+
this.description = this.formatFileSize(this.fileData.size);
|
|
251
|
+
this.tooltip = `${this.filePath}\nSize: ${this.formatFileSize(this.fileData.size)}\n\nClick to view file (readonly)`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Add click command to view snapshot file
|
|
255
|
+
this.command = {
|
|
256
|
+
command: 'agentuity.sandbox.snapshot.viewFile',
|
|
257
|
+
title: 'View Snapshot File',
|
|
258
|
+
arguments: [this],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private setupExecutionItem(): void {
|
|
263
|
+
if (!this.executionData) return;
|
|
264
|
+
|
|
265
|
+
const status = this.executionData.status;
|
|
266
|
+
const icon = this.getExecutionIcon(status);
|
|
267
|
+
const color = this.getExecutionColor(status);
|
|
268
|
+
|
|
269
|
+
this.iconPath = new vscode.ThemeIcon(icon, color);
|
|
270
|
+
this.contextValue = status === 'running' ? 'execution.running' : 'execution';
|
|
271
|
+
|
|
272
|
+
// Build description
|
|
273
|
+
const parts: string[] = [];
|
|
274
|
+
if (this.executionData.exitCode !== undefined) {
|
|
275
|
+
parts.push(`exit ${this.executionData.exitCode}`);
|
|
276
|
+
}
|
|
277
|
+
if (this.executionData.durationMs !== undefined) {
|
|
278
|
+
parts.push(`${(this.executionData.durationMs / 1000).toFixed(1)}s`);
|
|
279
|
+
}
|
|
280
|
+
this.description = parts.join(', ');
|
|
281
|
+
|
|
282
|
+
this.tooltip = [
|
|
283
|
+
`ID: ${this.executionData.executionId}`,
|
|
284
|
+
`Status: ${status}`,
|
|
285
|
+
this.executionData.exitCode !== undefined ? `Exit Code: ${this.executionData.exitCode}` : '',
|
|
286
|
+
this.executionData.durationMs !== undefined
|
|
287
|
+
? `Duration: ${(this.executionData.durationMs / 1000).toFixed(2)}s`
|
|
288
|
+
: '',
|
|
289
|
+
'',
|
|
290
|
+
'Click to view execution details',
|
|
291
|
+
]
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
.join('\n');
|
|
294
|
+
|
|
295
|
+
// Add click command to view execution details
|
|
296
|
+
this.command = {
|
|
297
|
+
command: 'agentuity.sandbox.viewExecution',
|
|
298
|
+
title: 'View Execution',
|
|
299
|
+
arguments: [this],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private getExecutionIcon(status: string): string {
|
|
304
|
+
switch (status) {
|
|
305
|
+
case 'completed':
|
|
306
|
+
return 'check';
|
|
307
|
+
case 'failed':
|
|
308
|
+
return 'x';
|
|
309
|
+
case 'running':
|
|
310
|
+
return 'loading~spin';
|
|
311
|
+
case 'queued':
|
|
312
|
+
return 'clock';
|
|
313
|
+
case 'timeout':
|
|
314
|
+
return 'watch';
|
|
315
|
+
case 'cancelled':
|
|
316
|
+
return 'circle-slash';
|
|
317
|
+
default:
|
|
318
|
+
return 'terminal';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private getExecutionColor(status: string): vscode.ThemeColor | undefined {
|
|
323
|
+
switch (status) {
|
|
324
|
+
case 'completed':
|
|
325
|
+
return new vscode.ThemeColor('charts.green');
|
|
326
|
+
case 'failed':
|
|
327
|
+
case 'timeout':
|
|
328
|
+
return new vscode.ThemeColor('charts.red');
|
|
329
|
+
case 'cancelled':
|
|
330
|
+
return new vscode.ThemeColor('charts.orange');
|
|
331
|
+
default:
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private setupCreateItem(): void {
|
|
337
|
+
this.iconPath = new vscode.ThemeIcon('add');
|
|
338
|
+
this.contextValue = 'createSandbox';
|
|
339
|
+
this.command = {
|
|
340
|
+
command: 'agentuity.sandbox.create',
|
|
341
|
+
title: 'Create Sandbox',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Tree data provider for the sandbox explorer.
|
|
348
|
+
*/
|
|
349
|
+
export class SandboxTreeDataProvider implements vscode.TreeDataProvider<SandboxTreeItem> {
|
|
350
|
+
private _onDidChangeTreeData = new vscode.EventEmitter<SandboxTreeItem | undefined | null | void>();
|
|
351
|
+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
|
352
|
+
|
|
353
|
+
private sandboxes: SandboxInfo[] = [];
|
|
354
|
+
private loading = false;
|
|
355
|
+
private sandboxesLoaded = false;
|
|
356
|
+
private error: string | undefined;
|
|
357
|
+
|
|
358
|
+
// Cache for lazy-loaded data
|
|
359
|
+
private snapshotsCache: Map<string, SnapshotInfo[]> = new Map();
|
|
360
|
+
private executionsCache: Map<string, ExecutionInfo[]> = new Map();
|
|
361
|
+
private filesCache: Map<string, SandboxFileInfo[]> = new Map();
|
|
362
|
+
|
|
363
|
+
refresh(): void {
|
|
364
|
+
this._onDidChangeTreeData.fire();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getTreeItem(element: SandboxTreeItem): vscode.TreeItem {
|
|
368
|
+
return element;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async getChildren(element?: SandboxTreeItem): Promise<SandboxTreeItem[]> {
|
|
372
|
+
// Root level
|
|
373
|
+
if (!element) {
|
|
374
|
+
return this.getRootChildren();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Sandbox children (categories)
|
|
378
|
+
if (element.itemType === 'sandbox' && element.sandboxData) {
|
|
379
|
+
return this.getSandboxCategories(element.sandboxData, element.linkedData);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Category children
|
|
383
|
+
if (element.itemType === 'category' && element.parentSandboxId) {
|
|
384
|
+
switch (element.categoryType) {
|
|
385
|
+
case 'files':
|
|
386
|
+
// Pass undefined for root listing (CLI defaults to sandbox home)
|
|
387
|
+
return this.getFilesChildren(element.parentSandboxId, undefined);
|
|
388
|
+
case 'snapshots':
|
|
389
|
+
return this.getSnapshotsChildren(element.parentSandboxId);
|
|
390
|
+
case 'executions':
|
|
391
|
+
return this.getExecutionsChildren(element.parentSandboxId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Directory children
|
|
396
|
+
if (element.itemType === 'directory' && element.parentSandboxId && element.filePath) {
|
|
397
|
+
return this.getFilesChildren(element.parentSandboxId, element.filePath);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Snapshot children (files from snapshot get)
|
|
401
|
+
if (element.itemType === 'snapshot' && element.snapshotData) {
|
|
402
|
+
return this.getSnapshotFilesChildren(element.snapshotData.snapshotId);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return [];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private async getRootChildren(): Promise<SandboxTreeItem[]> {
|
|
409
|
+
const authStatus = getAuthStatus();
|
|
410
|
+
|
|
411
|
+
if (authStatus.state === 'unknown') {
|
|
412
|
+
return [
|
|
413
|
+
new SandboxTreeItem('Checking auth...', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (authStatus.state === 'cli-missing') {
|
|
418
|
+
return [
|
|
419
|
+
new SandboxTreeItem('CLI not installed', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (authStatus.state === 'unauthenticated') {
|
|
424
|
+
return [
|
|
425
|
+
new SandboxTreeItem('Not logged in', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (this.loading) {
|
|
430
|
+
return [new SandboxTreeItem('Loading...', vscode.TreeItemCollapsibleState.None, 'message')];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (this.error) {
|
|
434
|
+
return [
|
|
435
|
+
new SandboxTreeItem(
|
|
436
|
+
`Error: ${this.error}`,
|
|
437
|
+
vscode.TreeItemCollapsibleState.None,
|
|
438
|
+
'message'
|
|
439
|
+
),
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Load sandboxes if not loaded
|
|
444
|
+
if (!this.sandboxesLoaded && !this.loading) {
|
|
445
|
+
await this.loadSandboxes();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const items: SandboxTreeItem[] = [];
|
|
449
|
+
|
|
450
|
+
// Get linked sandboxes for this workspace
|
|
451
|
+
let linkedSandboxes: LinkedSandbox[] = [];
|
|
452
|
+
try {
|
|
453
|
+
linkedSandboxes = getSandboxManager().getLinkedSandboxes();
|
|
454
|
+
} catch {
|
|
455
|
+
// SandboxManager not initialized yet
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Add sandbox items
|
|
459
|
+
for (const sandbox of this.sandboxes) {
|
|
460
|
+
const linked = linkedSandboxes.find((l) => l.sandboxId === sandbox.sandboxId);
|
|
461
|
+
const displayName = linked?.name || sandbox.sandboxId;
|
|
462
|
+
|
|
463
|
+
items.push(
|
|
464
|
+
new SandboxTreeItem(
|
|
465
|
+
displayName,
|
|
466
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
467
|
+
'sandbox',
|
|
468
|
+
sandbox,
|
|
469
|
+
undefined,
|
|
470
|
+
undefined,
|
|
471
|
+
undefined,
|
|
472
|
+
undefined,
|
|
473
|
+
undefined,
|
|
474
|
+
linked
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Add "Create Sandbox" action at the end
|
|
480
|
+
items.push(
|
|
481
|
+
new SandboxTreeItem(
|
|
482
|
+
'Create Sandbox',
|
|
483
|
+
vscode.TreeItemCollapsibleState.None,
|
|
484
|
+
'createSandbox'
|
|
485
|
+
)
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
if (items.length === 1) {
|
|
489
|
+
// Only the "Create Sandbox" item
|
|
490
|
+
return [
|
|
491
|
+
new SandboxTreeItem(
|
|
492
|
+
'No sandboxes found',
|
|
493
|
+
vscode.TreeItemCollapsibleState.None,
|
|
494
|
+
'message'
|
|
495
|
+
),
|
|
496
|
+
items[0],
|
|
497
|
+
];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return items;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private getSandboxCategories(
|
|
504
|
+
sandbox: SandboxInfo,
|
|
505
|
+
linked?: LinkedSandbox
|
|
506
|
+
): SandboxTreeItem[] {
|
|
507
|
+
return [
|
|
508
|
+
new SandboxTreeItem(
|
|
509
|
+
'Files',
|
|
510
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
511
|
+
'category',
|
|
512
|
+
sandbox,
|
|
513
|
+
undefined,
|
|
514
|
+
undefined,
|
|
515
|
+
undefined,
|
|
516
|
+
sandbox.sandboxId,
|
|
517
|
+
'files',
|
|
518
|
+
linked
|
|
519
|
+
),
|
|
520
|
+
new SandboxTreeItem(
|
|
521
|
+
'Snapshots',
|
|
522
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
523
|
+
'category',
|
|
524
|
+
sandbox,
|
|
525
|
+
undefined,
|
|
526
|
+
undefined,
|
|
527
|
+
undefined,
|
|
528
|
+
sandbox.sandboxId,
|
|
529
|
+
'snapshots',
|
|
530
|
+
linked
|
|
531
|
+
),
|
|
532
|
+
new SandboxTreeItem(
|
|
533
|
+
'Executions',
|
|
534
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
535
|
+
'category',
|
|
536
|
+
sandbox,
|
|
537
|
+
undefined,
|
|
538
|
+
undefined,
|
|
539
|
+
undefined,
|
|
540
|
+
sandbox.sandboxId,
|
|
541
|
+
'executions',
|
|
542
|
+
linked
|
|
543
|
+
),
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private async getFilesChildren(sandboxId: string, dirPath?: string): Promise<SandboxTreeItem[]> {
|
|
548
|
+
// Always fetch from root to get full file list, then filter
|
|
549
|
+
const cacheKey = `${sandboxId}:root`;
|
|
550
|
+
|
|
551
|
+
// Check cache first - always cache from root
|
|
552
|
+
if (!this.filesCache.has(cacheKey)) {
|
|
553
|
+
const cli = getCliClient();
|
|
554
|
+
// Always fetch from root (no path) to get complete file list
|
|
555
|
+
const result = await cli.sandboxLs(sandboxId);
|
|
556
|
+
|
|
557
|
+
if (result.success && result.data) {
|
|
558
|
+
this.filesCache.set(cacheKey, result.data);
|
|
559
|
+
} else {
|
|
560
|
+
return [
|
|
561
|
+
new SandboxTreeItem(
|
|
562
|
+
`Error: ${result.error || 'Failed to list files'}`,
|
|
563
|
+
vscode.TreeItemCollapsibleState.None,
|
|
564
|
+
'message'
|
|
565
|
+
),
|
|
566
|
+
];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const allFiles = this.filesCache.get(cacheKey) || [];
|
|
571
|
+
|
|
572
|
+
// Filter to only show direct children of the current directory
|
|
573
|
+
const directChildren = allFiles.filter((file) => {
|
|
574
|
+
const filePath = file.path;
|
|
575
|
+
|
|
576
|
+
if (!dirPath) {
|
|
577
|
+
// Root level: only show items without '/' in path
|
|
578
|
+
return !filePath.includes('/');
|
|
579
|
+
} else {
|
|
580
|
+
// Subdirectory: only show direct children
|
|
581
|
+
// File must start with dirPath/
|
|
582
|
+
if (!filePath.startsWith(dirPath + '/')) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
// The remaining part after dirPath/ should not contain another '/'
|
|
586
|
+
const remaining = filePath.slice(dirPath.length + 1);
|
|
587
|
+
return !remaining.includes('/');
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (directChildren.length === 0) {
|
|
592
|
+
return [
|
|
593
|
+
new SandboxTreeItem('(empty)', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
594
|
+
];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Sort: directories first, then files, alphabetically
|
|
598
|
+
const sorted = [...directChildren].sort((a, b) => {
|
|
599
|
+
if (a.isDir && !b.isDir) return -1;
|
|
600
|
+
if (!a.isDir && b.isDir) return 1;
|
|
601
|
+
return a.name.localeCompare(b.name);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return sorted.map((file) => {
|
|
605
|
+
return new SandboxTreeItem(
|
|
606
|
+
file.name,
|
|
607
|
+
file.isDir
|
|
608
|
+
? vscode.TreeItemCollapsibleState.Collapsed
|
|
609
|
+
: vscode.TreeItemCollapsibleState.None,
|
|
610
|
+
file.isDir ? 'directory' : 'file',
|
|
611
|
+
undefined,
|
|
612
|
+
file,
|
|
613
|
+
undefined,
|
|
614
|
+
undefined,
|
|
615
|
+
sandboxId,
|
|
616
|
+
undefined,
|
|
617
|
+
undefined,
|
|
618
|
+
file.path // Use the full path from CLI
|
|
619
|
+
);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async getSnapshotsChildren(sandboxId: string): Promise<SandboxTreeItem[]> {
|
|
624
|
+
if (!this.snapshotsCache.has(sandboxId)) {
|
|
625
|
+
const cli = getCliClient();
|
|
626
|
+
const result = await cli.snapshotList(sandboxId);
|
|
627
|
+
|
|
628
|
+
if (result.success && result.data) {
|
|
629
|
+
this.snapshotsCache.set(sandboxId, result.data);
|
|
630
|
+
} else {
|
|
631
|
+
return [
|
|
632
|
+
new SandboxTreeItem(
|
|
633
|
+
`Error: ${result.error || 'Failed to list snapshots'}`,
|
|
634
|
+
vscode.TreeItemCollapsibleState.None,
|
|
635
|
+
'message'
|
|
636
|
+
),
|
|
637
|
+
];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const snapshots = this.snapshotsCache.get(sandboxId) || [];
|
|
642
|
+
|
|
643
|
+
if (snapshots.length === 0) {
|
|
644
|
+
return [
|
|
645
|
+
new SandboxTreeItem('No snapshots', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return snapshots.map(
|
|
650
|
+
(snap) =>
|
|
651
|
+
new SandboxTreeItem(
|
|
652
|
+
snap.tag || snap.snapshotId.slice(0, 12),
|
|
653
|
+
// Make expandable to show files
|
|
654
|
+
vscode.TreeItemCollapsibleState.Collapsed,
|
|
655
|
+
'snapshot',
|
|
656
|
+
undefined,
|
|
657
|
+
undefined,
|
|
658
|
+
snap,
|
|
659
|
+
undefined,
|
|
660
|
+
sandboxId
|
|
661
|
+
)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private async getSnapshotFilesChildren(snapshotId: string): Promise<SandboxTreeItem[]> {
|
|
666
|
+
// Fetch snapshot details to get file list
|
|
667
|
+
const cli = getCliClient();
|
|
668
|
+
const result = await cli.snapshotGet(snapshotId);
|
|
669
|
+
|
|
670
|
+
if (!result.success || !result.data) {
|
|
671
|
+
return [
|
|
672
|
+
new SandboxTreeItem(
|
|
673
|
+
`Error: ${result.error || 'Failed to get snapshot'}`,
|
|
674
|
+
vscode.TreeItemCollapsibleState.None,
|
|
675
|
+
'message'
|
|
676
|
+
),
|
|
677
|
+
];
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const snapshotData = result.data;
|
|
681
|
+
const files = snapshotData.files || [];
|
|
682
|
+
|
|
683
|
+
if (files.length === 0) {
|
|
684
|
+
return [
|
|
685
|
+
new SandboxTreeItem('(no files)', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
686
|
+
];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Sort files alphabetically
|
|
690
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
691
|
+
|
|
692
|
+
return sorted.map((file) => {
|
|
693
|
+
const fileName = file.path.split('/').pop() || file.path;
|
|
694
|
+
return new SandboxTreeItem(
|
|
695
|
+
fileName,
|
|
696
|
+
vscode.TreeItemCollapsibleState.None,
|
|
697
|
+
'snapshotFile',
|
|
698
|
+
undefined,
|
|
699
|
+
{ path: file.path, name: fileName, size: file.size, isDir: false, mode: '', modTime: '' },
|
|
700
|
+
snapshotData, // Pass snapshot data so we have access to downloadUrl
|
|
701
|
+
undefined,
|
|
702
|
+
undefined,
|
|
703
|
+
undefined,
|
|
704
|
+
undefined,
|
|
705
|
+
file.path
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private async getExecutionsChildren(sandboxId: string): Promise<SandboxTreeItem[]> {
|
|
711
|
+
if (!this.executionsCache.has(sandboxId)) {
|
|
712
|
+
const cli = getCliClient();
|
|
713
|
+
const result = await cli.executionList(sandboxId);
|
|
714
|
+
|
|
715
|
+
if (result.success && result.data) {
|
|
716
|
+
this.executionsCache.set(sandboxId, result.data);
|
|
717
|
+
} else {
|
|
718
|
+
return [
|
|
719
|
+
new SandboxTreeItem(
|
|
720
|
+
`Error: ${result.error || 'Failed to list executions'}`,
|
|
721
|
+
vscode.TreeItemCollapsibleState.None,
|
|
722
|
+
'message'
|
|
723
|
+
),
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const executions = this.executionsCache.get(sandboxId) || [];
|
|
729
|
+
|
|
730
|
+
if (executions.length === 0) {
|
|
731
|
+
return [
|
|
732
|
+
new SandboxTreeItem('No executions', vscode.TreeItemCollapsibleState.None, 'message'),
|
|
733
|
+
];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return executions.map(
|
|
737
|
+
(exec) =>
|
|
738
|
+
new SandboxTreeItem(
|
|
739
|
+
exec.executionId.slice(0, 12),
|
|
740
|
+
vscode.TreeItemCollapsibleState.None,
|
|
741
|
+
'execution',
|
|
742
|
+
undefined,
|
|
743
|
+
undefined,
|
|
744
|
+
undefined,
|
|
745
|
+
exec,
|
|
746
|
+
sandboxId
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private async loadSandboxes(): Promise<void> {
|
|
752
|
+
this.loading = true;
|
|
753
|
+
this.error = undefined;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const cli = getCliClient();
|
|
757
|
+
const result = await cli.sandboxList();
|
|
758
|
+
|
|
759
|
+
if (result.success && result.data) {
|
|
760
|
+
this.sandboxes = Array.isArray(result.data) ? result.data : [];
|
|
761
|
+
} else {
|
|
762
|
+
this.error = result.error || 'Failed to load sandboxes';
|
|
763
|
+
this.sandboxes = [];
|
|
764
|
+
}
|
|
765
|
+
} catch (err) {
|
|
766
|
+
this.error = err instanceof Error ? err.message : 'Unknown error';
|
|
767
|
+
this.sandboxes = [];
|
|
768
|
+
} finally {
|
|
769
|
+
this.loading = false;
|
|
770
|
+
this.sandboxesLoaded = true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async forceRefresh(): Promise<void> {
|
|
775
|
+
this.sandboxes = [];
|
|
776
|
+
this.sandboxesLoaded = false;
|
|
777
|
+
this.error = undefined;
|
|
778
|
+
this.clearCaches();
|
|
779
|
+
await this.loadSandboxes();
|
|
780
|
+
this.refresh();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
clearCaches(): void {
|
|
784
|
+
this.snapshotsCache.clear();
|
|
785
|
+
this.executionsCache.clear();
|
|
786
|
+
this.filesCache.clear();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Clear cache for a specific sandbox.
|
|
791
|
+
*/
|
|
792
|
+
clearSandboxCache(sandboxId: string): void {
|
|
793
|
+
this.snapshotsCache.delete(sandboxId);
|
|
794
|
+
this.executionsCache.delete(sandboxId);
|
|
795
|
+
|
|
796
|
+
// Clear files cache for this sandbox
|
|
797
|
+
this.filesCache.delete(`${sandboxId}:root`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
dispose(): void {
|
|
801
|
+
this._onDidChangeTreeData.dispose();
|
|
802
|
+
}
|
|
803
|
+
}
|