agentuity-vscode 0.1.6 → 0.1.8

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