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,1387 @@
1
+ import * as vscode from 'vscode';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import * as fs from 'fs';
5
+ import { SandboxTreeDataProvider, SandboxTreeItem } from './sandboxTreeData';
6
+ import { onAuthStatusChanged } from '../../core/auth';
7
+ import {
8
+ getCliClient,
9
+ type SandboxCreateOptions,
10
+ type SnapshotInfo,
11
+ CliClient,
12
+ } from '../../core/cliClient';
13
+ import {
14
+ getSandboxManager,
15
+ initSandboxManager,
16
+ onLinkedSandboxesChanged,
17
+ formatBytes,
18
+ DEFAULT_SANDBOX_PATH,
19
+ } from '../../core/sandboxManager';
20
+ import { openReadonlyDocument } from '../../core/readonlyDocument';
21
+ import {
22
+ createSandboxStatusBar,
23
+ updateStatusBar,
24
+ showSyncProgress,
25
+ showSyncSuccess,
26
+ showSyncError,
27
+ disposeSandboxStatusBar,
28
+ } from './statusBar';
29
+
30
+ const sandboxTerminals: Map<string, vscode.Terminal> = new Map();
31
+
32
+ // Track sandbox files opened for editing: localPath -> { sandboxId, remotePath }
33
+ interface SandboxFileMapping {
34
+ sandboxId: string;
35
+ remotePath: string;
36
+ }
37
+ const sandboxFileMap: Map<string, SandboxFileMapping> = new Map();
38
+ let saveListener: vscode.Disposable | undefined;
39
+
40
+ // Debounce timers for file uploads
41
+ const uploadDebounceTimers: Map<string, NodeJS.Timeout> = new Map();
42
+ const UPLOAD_DEBOUNCE_MS = 1000; // 1 second debounce
43
+
44
+ export function registerSandboxExplorer(context: vscode.ExtensionContext): SandboxTreeDataProvider {
45
+ // Initialize sandbox manager
46
+ initSandboxManager(context);
47
+
48
+ const provider = new SandboxTreeDataProvider();
49
+
50
+ const treeView = vscode.window.createTreeView('agentuity.sandboxes', {
51
+ treeDataProvider: provider,
52
+ showCollapseAll: true,
53
+ });
54
+
55
+ // Create status bar
56
+ createSandboxStatusBar(context);
57
+
58
+ // Refresh on auth changes
59
+ const authSub = onAuthStatusChanged(() => {
60
+ provider.refresh();
61
+ updateStatusBar();
62
+ });
63
+
64
+ // Refresh when linked sandboxes change
65
+ const linkedSub = onLinkedSandboxesChanged(() => {
66
+ provider.refresh();
67
+ updateStatusBar();
68
+ });
69
+
70
+ // Register sandbox-specific commands (refresh is registered in extension.ts)
71
+ registerCommands(context, provider);
72
+
73
+ // Set up save listener for sandbox files with debouncing
74
+ saveListener = vscode.workspace.onDidSaveTextDocument((doc) => {
75
+ const mapping = sandboxFileMap.get(doc.uri.fsPath);
76
+ if (mapping) {
77
+ // Clear any existing timer for this file
78
+ const existingTimer = uploadDebounceTimers.get(doc.uri.fsPath);
79
+ if (existingTimer) {
80
+ clearTimeout(existingTimer);
81
+ }
82
+
83
+ // Set new debounced upload
84
+ const timer = setTimeout(async () => {
85
+ uploadDebounceTimers.delete(doc.uri.fsPath);
86
+ await uploadSavedFile(mapping.sandboxId, doc.uri.fsPath, mapping.remotePath, provider);
87
+ }, UPLOAD_DEBOUNCE_MS);
88
+
89
+ uploadDebounceTimers.set(doc.uri.fsPath, timer);
90
+ }
91
+ });
92
+
93
+ // Clean up file mappings and pending uploads when documents are closed
94
+ const closeListener = vscode.workspace.onDidCloseTextDocument((doc) => {
95
+ sandboxFileMap.delete(doc.uri.fsPath);
96
+ const timer = uploadDebounceTimers.get(doc.uri.fsPath);
97
+ if (timer) {
98
+ clearTimeout(timer);
99
+ uploadDebounceTimers.delete(doc.uri.fsPath);
100
+ }
101
+ });
102
+
103
+ context.subscriptions.push(
104
+ treeView,
105
+ authSub,
106
+ linkedSub,
107
+ saveListener,
108
+ closeListener,
109
+ { dispose: () => provider.dispose() },
110
+ { dispose: () => disposeTerminals() },
111
+ { dispose: () => disposeSandboxStatusBar() },
112
+ { dispose: () => sandboxFileMap.clear() },
113
+ {
114
+ dispose: () => {
115
+ // Clear all pending upload timers
116
+ for (const timer of uploadDebounceTimers.values()) {
117
+ clearTimeout(timer);
118
+ }
119
+ uploadDebounceTimers.clear();
120
+ },
121
+ }
122
+ );
123
+
124
+ return provider;
125
+ }
126
+
127
+ function registerCommands(
128
+ context: vscode.ExtensionContext,
129
+ provider: SandboxTreeDataProvider
130
+ ): void {
131
+ // Create sandbox
132
+ context.subscriptions.push(
133
+ vscode.commands.registerCommand('agentuity.sandbox.create', async () => {
134
+ await createSandbox(provider);
135
+ })
136
+ );
137
+
138
+ // Create sandbox from snapshot
139
+ context.subscriptions.push(
140
+ vscode.commands.registerCommand(
141
+ 'agentuity.sandbox.createFromSnapshot',
142
+ async (item?: SandboxTreeItem) => {
143
+ if (item?.snapshotData) {
144
+ await createSandboxFromSnapshot(item.snapshotData.snapshotId, provider);
145
+ } else {
146
+ // Prompt for snapshot
147
+ const snapshotId = await vscode.window.showInputBox({
148
+ prompt: 'Enter snapshot ID or tag',
149
+ placeHolder: 'snp_xxx or tag-name',
150
+ });
151
+ if (snapshotId) {
152
+ await createSandboxFromSnapshot(snapshotId, provider);
153
+ }
154
+ }
155
+ }
156
+ )
157
+ );
158
+
159
+ // Delete sandbox
160
+ context.subscriptions.push(
161
+ vscode.commands.registerCommand(
162
+ 'agentuity.sandbox.delete',
163
+ async (item?: SandboxTreeItem) => {
164
+ if (!item?.sandboxData) return;
165
+ await deleteSandbox(item.sandboxData.sandboxId, provider);
166
+ }
167
+ )
168
+ );
169
+
170
+ // Link sandbox
171
+ context.subscriptions.push(
172
+ vscode.commands.registerCommand('agentuity.sandbox.link', async (item?: SandboxTreeItem) => {
173
+ if (!item?.sandboxData) return;
174
+ await linkSandbox(item.sandboxData.sandboxId, provider);
175
+ })
176
+ );
177
+
178
+ // Unlink sandbox
179
+ context.subscriptions.push(
180
+ vscode.commands.registerCommand(
181
+ 'agentuity.sandbox.unlink',
182
+ async (item?: SandboxTreeItem) => {
183
+ if (!item?.sandboxData) return;
184
+ await unlinkSandbox(item.sandboxData.sandboxId, provider);
185
+ }
186
+ )
187
+ );
188
+
189
+ // Sync project to sandbox
190
+ context.subscriptions.push(
191
+ vscode.commands.registerCommand('agentuity.sandbox.sync', async (item?: SandboxTreeItem) => {
192
+ let sandboxId: string | undefined;
193
+
194
+ if (item?.sandboxData) {
195
+ sandboxId = item.sandboxData.sandboxId;
196
+ } else {
197
+ // Try to get from linked sandboxes
198
+ const linked = getSandboxManager().getLinkedSandboxes();
199
+ if (linked.length === 0) {
200
+ vscode.window.showWarningMessage(
201
+ 'No sandbox linked to this workspace. Link a sandbox first.'
202
+ );
203
+ return;
204
+ }
205
+ if (linked.length === 1) {
206
+ sandboxId = linked[0].sandboxId;
207
+ } else {
208
+ // Pick one
209
+ const picked = await vscode.window.showQuickPick(
210
+ linked.map((l) => ({
211
+ label: l.name || l.sandboxId,
212
+ description: l.sandboxId,
213
+ sandboxId: l.sandboxId,
214
+ })),
215
+ { placeHolder: 'Select sandbox to sync to' }
216
+ );
217
+ if (picked) {
218
+ sandboxId = picked.sandboxId;
219
+ }
220
+ }
221
+ }
222
+
223
+ if (sandboxId) {
224
+ await syncToSandbox(sandboxId, provider);
225
+ }
226
+ })
227
+ );
228
+
229
+ // Execute command in sandbox
230
+ context.subscriptions.push(
231
+ vscode.commands.registerCommand(
232
+ 'agentuity.sandbox.exec',
233
+ async (itemOrOptions?: SandboxTreeItem | { sandboxId: string; command?: string }) => {
234
+ let sandboxId: string | undefined;
235
+ let command: string | undefined;
236
+
237
+ if (itemOrOptions instanceof SandboxTreeItem) {
238
+ sandboxId = itemOrOptions.sandboxData?.sandboxId;
239
+ } else if (itemOrOptions && 'sandboxId' in itemOrOptions) {
240
+ sandboxId = itemOrOptions.sandboxId;
241
+ command = itemOrOptions.command;
242
+ }
243
+
244
+ if (!sandboxId) return;
245
+ await execInSandbox(sandboxId, command);
246
+ }
247
+ )
248
+ );
249
+
250
+ // View file
251
+ context.subscriptions.push(
252
+ vscode.commands.registerCommand(
253
+ 'agentuity.sandbox.viewFile',
254
+ async (item?: SandboxTreeItem) => {
255
+ if (!item?.parentSandboxId || !item?.filePath) return;
256
+ await viewSandboxFile(item.parentSandboxId, item.filePath);
257
+ }
258
+ )
259
+ );
260
+
261
+ // Download file/directory
262
+ context.subscriptions.push(
263
+ vscode.commands.registerCommand(
264
+ 'agentuity.sandbox.download',
265
+ async (item?: SandboxTreeItem) => {
266
+ if (!item?.parentSandboxId || !item?.filePath) return;
267
+ await downloadFromSandbox(
268
+ item.parentSandboxId,
269
+ item.filePath,
270
+ item.itemType === 'directory'
271
+ );
272
+ }
273
+ )
274
+ );
275
+
276
+ // Delete file
277
+ context.subscriptions.push(
278
+ vscode.commands.registerCommand(
279
+ 'agentuity.sandbox.deleteFile',
280
+ async (item?: SandboxTreeItem) => {
281
+ if (!item?.parentSandboxId || !item?.filePath) return;
282
+ await deleteFile(
283
+ item.parentSandboxId,
284
+ item.filePath,
285
+ item.itemType === 'directory',
286
+ provider
287
+ );
288
+ }
289
+ )
290
+ );
291
+
292
+ // Create new file in sandbox
293
+ context.subscriptions.push(
294
+ vscode.commands.registerCommand(
295
+ 'agentuity.sandbox.createFile',
296
+ async (item?: SandboxTreeItem) => {
297
+ // Get sandbox ID from item or category
298
+ const sandboxId = item?.parentSandboxId || item?.sandboxData?.sandboxId;
299
+ if (!sandboxId) return;
300
+
301
+ // Get parent directory path
302
+ let parentDir = '';
303
+ if (item?.itemType === 'directory' && item.filePath) {
304
+ parentDir = item.filePath;
305
+ } else if (item?.itemType === 'file' && item.filePath) {
306
+ parentDir = path.dirname(item.filePath);
307
+ }
308
+
309
+ await createSandboxFile(sandboxId, parentDir, provider);
310
+ }
311
+ )
312
+ );
313
+
314
+ // Create new folder in sandbox
315
+ context.subscriptions.push(
316
+ vscode.commands.registerCommand(
317
+ 'agentuity.sandbox.createFolder',
318
+ async (item?: SandboxTreeItem) => {
319
+ // Get sandbox ID from item or category
320
+ const sandboxId = item?.parentSandboxId || item?.sandboxData?.sandboxId;
321
+ if (!sandboxId) return;
322
+
323
+ // Get parent directory path
324
+ let parentDir = '';
325
+ if (item?.itemType === 'directory' && item.filePath) {
326
+ parentDir = item.filePath;
327
+ } else if (item?.itemType === 'file' && item.filePath) {
328
+ parentDir = path.dirname(item.filePath);
329
+ }
330
+
331
+ await createSandboxFolder(sandboxId, parentDir, provider);
332
+ }
333
+ )
334
+ );
335
+
336
+ // Copy path
337
+ context.subscriptions.push(
338
+ vscode.commands.registerCommand(
339
+ 'agentuity.sandbox.copyPath',
340
+ async (item?: SandboxTreeItem) => {
341
+ if (!item?.filePath) return;
342
+ await vscode.env.clipboard.writeText(item.filePath);
343
+ vscode.window.showInformationMessage(`Copied: ${item.filePath}`);
344
+ }
345
+ )
346
+ );
347
+
348
+ // View execution
349
+ context.subscriptions.push(
350
+ vscode.commands.registerCommand(
351
+ 'agentuity.sandbox.viewExecution',
352
+ async (item?: SandboxTreeItem) => {
353
+ if (!item?.executionData) return;
354
+ await viewExecution(item.executionData.executionId);
355
+ }
356
+ )
357
+ );
358
+
359
+ // Set environment variable
360
+ context.subscriptions.push(
361
+ vscode.commands.registerCommand(
362
+ 'agentuity.sandbox.setEnv',
363
+ async (item?: SandboxTreeItem) => {
364
+ if (!item?.sandboxData) return;
365
+ await setEnvVar(item.sandboxData.sandboxId);
366
+ }
367
+ )
368
+ );
369
+
370
+ // View environment
371
+ context.subscriptions.push(
372
+ vscode.commands.registerCommand(
373
+ 'agentuity.sandbox.viewEnv',
374
+ async (item?: SandboxTreeItem) => {
375
+ if (!item?.sandboxData) return;
376
+ await viewEnv(item.sandboxData.sandboxId);
377
+ }
378
+ )
379
+ );
380
+
381
+ // Sync .env file
382
+ context.subscriptions.push(
383
+ vscode.commands.registerCommand(
384
+ 'agentuity.sandbox.syncEnvFile',
385
+ async (item?: SandboxTreeItem) => {
386
+ if (!item?.sandboxData) return;
387
+ await syncEnvFile(item.sandboxData.sandboxId);
388
+ }
389
+ )
390
+ );
391
+
392
+ // Create snapshot
393
+ context.subscriptions.push(
394
+ vscode.commands.registerCommand(
395
+ 'agentuity.sandbox.snapshot.create',
396
+ async (item?: SandboxTreeItem) => {
397
+ if (!item?.sandboxData) return;
398
+ await createSnapshot(item.sandboxData.sandboxId, provider);
399
+ }
400
+ )
401
+ );
402
+
403
+ // Delete snapshot
404
+ context.subscriptions.push(
405
+ vscode.commands.registerCommand(
406
+ 'agentuity.sandbox.snapshot.delete',
407
+ async (item?: SandboxTreeItem) => {
408
+ if (!item?.snapshotData) return;
409
+ await deleteSnapshot(item.snapshotData.snapshotId, provider);
410
+ }
411
+ )
412
+ );
413
+
414
+ // Tag snapshot
415
+ context.subscriptions.push(
416
+ vscode.commands.registerCommand(
417
+ 'agentuity.sandbox.snapshot.tag',
418
+ async (item?: SandboxTreeItem) => {
419
+ if (!item?.snapshotData) return;
420
+ await tagSnapshot(item.snapshotData.snapshotId, provider);
421
+ }
422
+ )
423
+ );
424
+
425
+ // View snapshot details (JSON)
426
+ context.subscriptions.push(
427
+ vscode.commands.registerCommand(
428
+ 'agentuity.sandbox.snapshot.viewDetails',
429
+ async (item?: SandboxTreeItem) => {
430
+ if (!item?.snapshotData) return;
431
+ await viewSnapshotDetails(item.snapshotData.snapshotId);
432
+ }
433
+ )
434
+ );
435
+
436
+ // View snapshot file (readonly)
437
+ context.subscriptions.push(
438
+ vscode.commands.registerCommand(
439
+ 'agentuity.sandbox.snapshot.viewFile',
440
+ async (item?: SandboxTreeItem) => {
441
+ if (!item?.snapshotData || !item?.filePath) return;
442
+ await viewSnapshotFile(item.snapshotData, item.filePath);
443
+ }
444
+ )
445
+ );
446
+
447
+ // Upload from explorer (context menu)
448
+ context.subscriptions.push(
449
+ vscode.commands.registerCommand('agentuity.sandbox.upload', async (uri?: vscode.Uri) => {
450
+ if (!uri) return;
451
+ await uploadToSandbox(uri);
452
+ })
453
+ );
454
+ }
455
+
456
+ // ==================== Command Implementations ====================
457
+
458
+ async function createSandbox(provider: SandboxTreeDataProvider): Promise<void> {
459
+ const config = vscode.workspace.getConfiguration('agentuity');
460
+ const defaultMemory = config.get<string>('sandbox.defaultMemory', '512Mi');
461
+ const defaultCpu = config.get<string>('sandbox.defaultCpu', '500m');
462
+ const defaultNetwork = config.get<boolean>('sandbox.defaultNetwork', false);
463
+
464
+ // Quick pick for basic vs advanced
465
+ const mode = await vscode.window.showQuickPick(
466
+ [
467
+ { label: 'Quick Create', description: 'Use default settings' },
468
+ { label: 'Custom', description: 'Configure resources and options' },
469
+ ],
470
+ { placeHolder: 'How do you want to create the sandbox?' }
471
+ );
472
+
473
+ if (!mode) return;
474
+
475
+ let options: SandboxCreateOptions = {};
476
+
477
+ if (mode.label === 'Custom') {
478
+ // Memory
479
+ const memory = await vscode.window.showInputBox({
480
+ prompt: 'Memory limit',
481
+ value: defaultMemory,
482
+ placeHolder: 'e.g., 512Mi, 1Gi, 2Gi',
483
+ });
484
+ if (memory === undefined) return;
485
+ options.memory = memory || undefined;
486
+
487
+ // CPU
488
+ const cpu = await vscode.window.showInputBox({
489
+ prompt: 'CPU limit (millicores)',
490
+ value: defaultCpu,
491
+ placeHolder: 'e.g., 500m, 1000m',
492
+ });
493
+ if (cpu === undefined) return;
494
+ options.cpu = cpu || undefined;
495
+
496
+ // Network
497
+ const network = await vscode.window.showQuickPick(
498
+ [
499
+ { label: 'Disabled', description: 'No outbound network access', value: false },
500
+ { label: 'Enabled', description: 'Allow outbound network access', value: true },
501
+ ],
502
+ { placeHolder: 'Network access' }
503
+ );
504
+ if (!network) return;
505
+ options.network = network.value;
506
+
507
+ // Dependencies
508
+ const deps = await vscode.window.showInputBox({
509
+ prompt: 'APT packages to install (optional)',
510
+ placeHolder: 'e.g., python3 nodejs git',
511
+ });
512
+ if (deps) {
513
+ options.dependencies = deps.split(/\s+/).filter(Boolean);
514
+ }
515
+ } else {
516
+ options = {
517
+ memory: defaultMemory,
518
+ cpu: defaultCpu,
519
+ network: defaultNetwork,
520
+ };
521
+ }
522
+
523
+ await vscode.window.withProgress(
524
+ {
525
+ location: vscode.ProgressLocation.Notification,
526
+ title: 'Creating sandbox...',
527
+ cancellable: false,
528
+ },
529
+ async () => {
530
+ const cli = getCliClient();
531
+ const result = await cli.sandboxCreate(options);
532
+
533
+ if (result.success && result.data) {
534
+ vscode.window.showInformationMessage(`Sandbox created: ${result.data.sandboxId}`);
535
+ await provider.forceRefresh();
536
+ } else {
537
+ vscode.window.showErrorMessage(`Failed to create sandbox: ${result.error}`);
538
+ }
539
+ }
540
+ );
541
+ }
542
+
543
+ async function createSandboxFromSnapshot(
544
+ snapshotId: string,
545
+ provider: SandboxTreeDataProvider
546
+ ): Promise<void> {
547
+ await vscode.window.withProgress(
548
+ {
549
+ location: vscode.ProgressLocation.Notification,
550
+ title: 'Creating sandbox from snapshot...',
551
+ cancellable: false,
552
+ },
553
+ async () => {
554
+ const cli = getCliClient();
555
+ const result = await cli.sandboxCreate({ snapshot: snapshotId });
556
+
557
+ if (result.success && result.data) {
558
+ vscode.window.showInformationMessage(`Sandbox created: ${result.data.sandboxId}`);
559
+ await provider.forceRefresh();
560
+ } else {
561
+ vscode.window.showErrorMessage(`Failed to create sandbox: ${result.error}`);
562
+ }
563
+ }
564
+ );
565
+ }
566
+
567
+ async function deleteSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
568
+ const confirm = await vscode.window.showWarningMessage(
569
+ `Are you sure you want to delete sandbox ${sandboxId}?`,
570
+ { modal: true },
571
+ 'Delete'
572
+ );
573
+
574
+ if (confirm !== 'Delete') return;
575
+
576
+ await vscode.window.withProgress(
577
+ {
578
+ location: vscode.ProgressLocation.Notification,
579
+ title: 'Deleting sandbox...',
580
+ cancellable: false,
581
+ },
582
+ async () => {
583
+ const cli = getCliClient();
584
+ const result = await cli.sandboxDelete(sandboxId);
585
+
586
+ if (result.success) {
587
+ // Also unlink if linked
588
+ try {
589
+ await getSandboxManager().unlinkSandbox(sandboxId);
590
+ } catch {
591
+ // Ignore if not linked
592
+ }
593
+ vscode.window.showInformationMessage('Sandbox deleted');
594
+ await provider.forceRefresh();
595
+ } else {
596
+ vscode.window.showErrorMessage(`Failed to delete sandbox: ${result.error}`);
597
+ }
598
+ }
599
+ );
600
+ }
601
+
602
+ async function linkSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
603
+ const name = await vscode.window.showInputBox({
604
+ prompt: 'Enter a friendly name for this sandbox (optional)',
605
+ placeHolder: 'my-dev-sandbox',
606
+ });
607
+
608
+ const remotePath = await vscode.window.showInputBox({
609
+ prompt: 'Remote path for synced files',
610
+ value: DEFAULT_SANDBOX_PATH,
611
+ placeHolder: DEFAULT_SANDBOX_PATH,
612
+ });
613
+
614
+ if (remotePath === undefined) return;
615
+
616
+ try {
617
+ await getSandboxManager().linkSandbox(sandboxId, {
618
+ name: name || undefined,
619
+ remotePath: remotePath || DEFAULT_SANDBOX_PATH,
620
+ });
621
+ vscode.window.showInformationMessage(`Sandbox linked to workspace`);
622
+ provider.refresh();
623
+ } catch (err) {
624
+ vscode.window.showErrorMessage(
625
+ `Failed to link sandbox: ${err instanceof Error ? err.message : 'Unknown error'}`
626
+ );
627
+ }
628
+ }
629
+
630
+ async function unlinkSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
631
+ try {
632
+ await getSandboxManager().unlinkSandbox(sandboxId);
633
+ vscode.window.showInformationMessage('Sandbox unlinked from workspace');
634
+ provider.refresh();
635
+ } catch (err) {
636
+ vscode.window.showErrorMessage(
637
+ `Failed to unlink sandbox: ${err instanceof Error ? err.message : 'Unknown error'}`
638
+ );
639
+ }
640
+ }
641
+
642
+ async function syncToSandbox(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
643
+ // Show status bar sync indicator
644
+ showSyncProgress('Syncing to sandbox...');
645
+
646
+ await vscode.window.withProgress(
647
+ {
648
+ location: vscode.ProgressLocation.Notification,
649
+ title: 'Syncing files to sandbox...',
650
+ cancellable: false,
651
+ },
652
+ async () => {
653
+ try {
654
+ const manager = getSandboxManager();
655
+ const result = await manager.syncToSandbox(sandboxId);
656
+
657
+ vscode.window.showInformationMessage(
658
+ `Synced ${result.filesUploaded} files (${formatBytes(result.bytesTransferred)}) in ${(result.duration / 1000).toFixed(1)}s`
659
+ );
660
+
661
+ // Show success in status bar
662
+ showSyncSuccess(result.filesUploaded, result.bytesTransferred);
663
+
664
+ // Refresh files cache
665
+ provider.clearSandboxCache(sandboxId);
666
+ provider.refresh();
667
+ updateStatusBar();
668
+ } catch (err) {
669
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
670
+ vscode.window.showErrorMessage(`Failed to sync: ${errorMessage}`);
671
+ showSyncError(errorMessage);
672
+ }
673
+ }
674
+ );
675
+ }
676
+
677
+ async function execInSandbox(sandboxId: string, prefilledCommand?: string): Promise<void> {
678
+ const command =
679
+ prefilledCommand ??
680
+ (await vscode.window.showInputBox({
681
+ prompt: 'Enter command to execute',
682
+ placeHolder: 'npm test',
683
+ }));
684
+
685
+ if (!command) return;
686
+
687
+ executeInTerminal(sandboxId, command);
688
+ }
689
+
690
+ function executeInTerminal(sandboxId: string, command: string): void {
691
+ const cli = getCliClient();
692
+ const cliPath = cli.getCliPath();
693
+
694
+ // Get or create terminal
695
+ let terminal = sandboxTerminals.get(sandboxId);
696
+ if (!terminal || terminal.exitStatus !== undefined) {
697
+ terminal = vscode.window.createTerminal({
698
+ name: `Sandbox: ${sandboxId.slice(0, 8)}`,
699
+ iconPath: new vscode.ThemeIcon('vm'),
700
+ });
701
+ sandboxTerminals.set(sandboxId, terminal);
702
+ }
703
+
704
+ terminal.show();
705
+ terminal.sendText(
706
+ `${cliPath} cloud sandbox exec ${sandboxId} --region ${cli.getSandboxRegion()} -- ${command}`
707
+ );
708
+ }
709
+
710
+ function disposeTerminals(): void {
711
+ for (const terminal of sandboxTerminals.values()) {
712
+ terminal.dispose();
713
+ }
714
+ sandboxTerminals.clear();
715
+ }
716
+
717
+ async function viewSandboxFile(sandboxId: string, filePath: string): Promise<void> {
718
+ await vscode.window.withProgress(
719
+ {
720
+ location: vscode.ProgressLocation.Notification,
721
+ title: 'Fetching file...',
722
+ cancellable: false,
723
+ },
724
+ async () => {
725
+ const cli = getCliClient();
726
+ // Use a stable temp directory for sandbox files
727
+ const sandboxTmpDir = path.join(os.tmpdir(), 'agentuity-sandbox', sandboxId.slice(0, 12));
728
+ fs.mkdirSync(sandboxTmpDir, { recursive: true });
729
+
730
+ const fileName = path.basename(filePath);
731
+ const localPath = path.join(sandboxTmpDir, fileName);
732
+
733
+ // Build full remote path under sandbox home
734
+ const fullRemotePath = filePath.startsWith('/')
735
+ ? filePath
736
+ : `${CliClient.SANDBOX_HOME}/${filePath}`;
737
+
738
+ const result = await cli.sandboxCpFromSandbox(sandboxId, fullRemotePath, localPath);
739
+
740
+ if (result.success) {
741
+ // Track this file for save-back
742
+ sandboxFileMap.set(localPath, {
743
+ sandboxId,
744
+ remotePath: fullRemotePath,
745
+ });
746
+
747
+ const doc = await vscode.workspace.openTextDocument(localPath);
748
+ await vscode.window.showTextDocument(doc, { preview: false });
749
+ } else {
750
+ vscode.window.showErrorMessage(`Failed to fetch file: ${result.error}`);
751
+ }
752
+ }
753
+ );
754
+ }
755
+
756
+ async function uploadSavedFile(
757
+ sandboxId: string,
758
+ localPath: string,
759
+ remotePath: string,
760
+ provider: SandboxTreeDataProvider
761
+ ): Promise<void> {
762
+ const cli = getCliClient();
763
+ const result = await cli.sandboxCpToSandbox(sandboxId, localPath, remotePath);
764
+
765
+ if (result.success) {
766
+ vscode.window.showInformationMessage(`Saved to sandbox: ${path.basename(remotePath)}`);
767
+ provider.clearSandboxCache(sandboxId);
768
+ provider.refresh();
769
+ } else {
770
+ vscode.window.showErrorMessage(`Failed to save to sandbox: ${result.error}`);
771
+ }
772
+ }
773
+
774
+ async function createSandboxFile(
775
+ sandboxId: string,
776
+ parentDir: string,
777
+ _provider: SandboxTreeDataProvider
778
+ ): Promise<void> {
779
+ const fileName = await vscode.window.showInputBox({
780
+ prompt: 'Enter new file name',
781
+ placeHolder: 'newfile.ts',
782
+ validateInput: (value) => {
783
+ if (!value || value.trim() === '') {
784
+ return 'File name cannot be empty';
785
+ }
786
+ if (value.includes('/') || value.includes('\\')) {
787
+ return 'File name cannot contain path separators';
788
+ }
789
+ return undefined;
790
+ },
791
+ });
792
+
793
+ if (!fileName) return;
794
+
795
+ // Create temp file locally
796
+ const sandboxTmpDir = path.join(os.tmpdir(), 'agentuity-sandbox', sandboxId.slice(0, 12));
797
+ fs.mkdirSync(sandboxTmpDir, { recursive: true });
798
+ const localPath = path.join(sandboxTmpDir, fileName);
799
+
800
+ // Create empty file
801
+ fs.writeFileSync(localPath, '');
802
+
803
+ // Build remote path
804
+ const remotePath = parentDir
805
+ ? `${CliClient.SANDBOX_HOME}/${parentDir}/${fileName}`
806
+ : `${CliClient.SANDBOX_HOME}/${fileName}`;
807
+
808
+ // Track this file for save-back
809
+ sandboxFileMap.set(localPath, {
810
+ sandboxId,
811
+ remotePath,
812
+ });
813
+
814
+ // Open in editor - file will be uploaded on first save
815
+ const doc = await vscode.workspace.openTextDocument(localPath);
816
+ await vscode.window.showTextDocument(doc, { preview: false });
817
+
818
+ vscode.window.showInformationMessage(`New file will be created at ${remotePath} when you save`);
819
+ }
820
+
821
+ async function createSandboxFolder(
822
+ sandboxId: string,
823
+ parentDir: string,
824
+ provider: SandboxTreeDataProvider
825
+ ): Promise<void> {
826
+ const folderName = await vscode.window.showInputBox({
827
+ prompt: 'Enter new folder name',
828
+ placeHolder: 'newfolder',
829
+ validateInput: (value) => {
830
+ if (!value || value.trim() === '') {
831
+ return 'Folder name cannot be empty';
832
+ }
833
+ if (value.includes('/') || value.includes('\\')) {
834
+ return 'Folder name cannot contain path separators';
835
+ }
836
+ return undefined;
837
+ },
838
+ });
839
+
840
+ if (!folderName) return;
841
+
842
+ const remotePath = parentDir
843
+ ? `${CliClient.SANDBOX_HOME}/${parentDir}/${folderName}`
844
+ : `${CliClient.SANDBOX_HOME}/${folderName}`;
845
+
846
+ const cli = getCliClient();
847
+ const result = await cli.sandboxMkdir(sandboxId, remotePath, true);
848
+
849
+ if (result.success) {
850
+ vscode.window.showInformationMessage(`Created folder: ${folderName}`);
851
+ provider.clearSandboxCache(sandboxId);
852
+ provider.refresh();
853
+ } else {
854
+ vscode.window.showErrorMessage(`Failed to create folder: ${result.error}`);
855
+ }
856
+ }
857
+
858
+ async function downloadFromSandbox(
859
+ sandboxId: string,
860
+ remotePath: string,
861
+ isDirectory: boolean
862
+ ): Promise<void> {
863
+ const defaultUri = vscode.workspace.workspaceFolders?.[0]?.uri;
864
+
865
+ const saveUri = await vscode.window.showSaveDialog({
866
+ defaultUri: defaultUri
867
+ ? vscode.Uri.joinPath(defaultUri, path.basename(remotePath))
868
+ : undefined,
869
+ saveLabel: 'Download',
870
+ filters: isDirectory ? { Archives: ['tar.gz', 'zip'] } : {},
871
+ });
872
+
873
+ if (!saveUri) return;
874
+
875
+ await vscode.window.withProgress(
876
+ {
877
+ location: vscode.ProgressLocation.Notification,
878
+ title: 'Downloading...',
879
+ cancellable: false,
880
+ },
881
+ async () => {
882
+ const cli = getCliClient();
883
+ const result = await cli.sandboxCpFromSandbox(
884
+ sandboxId,
885
+ remotePath,
886
+ saveUri.fsPath,
887
+ isDirectory
888
+ );
889
+
890
+ if (result.success) {
891
+ vscode.window.showInformationMessage(`Downloaded to ${saveUri.fsPath}`);
892
+ } else {
893
+ vscode.window.showErrorMessage(`Failed to download: ${result.error}`);
894
+ }
895
+ }
896
+ );
897
+ }
898
+
899
+ async function deleteFile(
900
+ sandboxId: string,
901
+ filePath: string,
902
+ isDirectory: boolean,
903
+ provider: SandboxTreeDataProvider
904
+ ): Promise<void> {
905
+ const confirm = await vscode.window.showWarningMessage(
906
+ `Delete ${isDirectory ? 'directory' : 'file'} ${filePath}?`,
907
+ { modal: true },
908
+ 'Delete'
909
+ );
910
+
911
+ if (confirm !== 'Delete') return;
912
+
913
+ const cli = getCliClient();
914
+ const result = isDirectory
915
+ ? await cli.sandboxRmdir(sandboxId, filePath, true)
916
+ : await cli.sandboxRm(sandboxId, filePath);
917
+
918
+ if (result.success) {
919
+ vscode.window.showInformationMessage(`Deleted ${filePath}`);
920
+ provider.clearSandboxCache(sandboxId);
921
+ provider.refresh();
922
+ } else {
923
+ vscode.window.showErrorMessage(`Failed to delete: ${result.error}`);
924
+ }
925
+ }
926
+
927
+ async function viewExecution(executionId: string): Promise<void> {
928
+ await vscode.window.withProgress(
929
+ {
930
+ location: vscode.ProgressLocation.Notification,
931
+ title: 'Fetching execution details...',
932
+ cancellable: false,
933
+ },
934
+ async () => {
935
+ const cli = getCliClient();
936
+ const result = await cli.executionGet(executionId);
937
+
938
+ if (result.success && result.data) {
939
+ const exec = result.data;
940
+
941
+ // Build execution details
942
+ const lines: string[] = [
943
+ '='.repeat(60),
944
+ 'EXECUTION DETAILS',
945
+ '='.repeat(60),
946
+ `Execution: ${exec.executionId}`,
947
+ `Sandbox: ${exec.sandboxId || 'N/A'}`,
948
+ `Status: ${exec.status}`,
949
+ `Exit Code: ${exec.exitCode ?? 'N/A'}`,
950
+ `Duration: ${exec.durationMs ? `${exec.durationMs}ms` : 'N/A'}`,
951
+ `Started: ${exec.startedAt || 'N/A'}`,
952
+ `Completed: ${exec.completedAt || 'N/A'}`,
953
+ `Command: ${exec.command || 'N/A'}`,
954
+ ];
955
+
956
+ // Fetch stdout stream if available
957
+ if (exec.stdoutStreamUrl) {
958
+ lines.push('', '='.repeat(60), 'STDOUT', '='.repeat(60));
959
+ try {
960
+ const stdoutContent = await fetchStreamContent(exec.stdoutStreamUrl);
961
+ lines.push(stdoutContent || '(empty)');
962
+ } catch (err) {
963
+ lines.push(
964
+ `(failed to fetch: ${err instanceof Error ? err.message : 'unknown error'})`
965
+ );
966
+ }
967
+ }
968
+
969
+ // Fetch stderr stream if available
970
+ if (exec.stderrStreamUrl) {
971
+ lines.push('', '='.repeat(60), 'STDERR', '='.repeat(60));
972
+ try {
973
+ const stderrContent = await fetchStreamContent(exec.stderrStreamUrl);
974
+ lines.push(stderrContent || '(empty)');
975
+ } catch (err) {
976
+ lines.push(
977
+ `(failed to fetch: ${err instanceof Error ? err.message : 'unknown error'})`
978
+ );
979
+ }
980
+ }
981
+
982
+ await openReadonlyDocument(
983
+ lines.join('\n'),
984
+ 'log',
985
+ `execution-${executionId.slice(0, 8)}`
986
+ );
987
+ } else {
988
+ vscode.window.showErrorMessage(`Failed to get execution: ${result.error}`);
989
+ }
990
+ }
991
+ );
992
+ }
993
+
994
+ async function fetchStreamContent(url: string): Promise<string> {
995
+ // Use https module to fetch stream content
996
+ const https = await import('https');
997
+ const http = await import('http');
998
+
999
+ return new Promise((resolve, reject) => {
1000
+ const protocol = url.startsWith('https') ? https : http;
1001
+ const request = protocol.get(url, (response) => {
1002
+ if (response.statusCode !== 200) {
1003
+ reject(new Error(`HTTP ${response.statusCode}`));
1004
+ return;
1005
+ }
1006
+
1007
+ let data = '';
1008
+ response.on('data', (chunk: Buffer) => {
1009
+ data += chunk.toString();
1010
+ });
1011
+ response.on('end', () => {
1012
+ resolve(data);
1013
+ });
1014
+ response.on('error', reject);
1015
+ });
1016
+
1017
+ request.on('error', reject);
1018
+ request.setTimeout(10000, () => {
1019
+ request.destroy();
1020
+ reject(new Error('Request timeout'));
1021
+ });
1022
+ });
1023
+ }
1024
+
1025
+ async function setEnvVar(sandboxId: string): Promise<void> {
1026
+ const input = await vscode.window.showInputBox({
1027
+ prompt: 'Enter environment variable (KEY=value)',
1028
+ placeHolder: 'MY_VAR=my_value',
1029
+ });
1030
+
1031
+ if (!input) return;
1032
+
1033
+ const [key, ...valueParts] = input.split('=');
1034
+ const value = valueParts.join('=');
1035
+
1036
+ if (!key || value === undefined) {
1037
+ vscode.window.showErrorMessage('Invalid format. Use KEY=value');
1038
+ return;
1039
+ }
1040
+
1041
+ const cli = getCliClient();
1042
+ const result = await cli.sandboxEnvSet(sandboxId, { [key]: value });
1043
+
1044
+ if (result.success) {
1045
+ vscode.window.showInformationMessage(`Set ${key}=${value}`);
1046
+ } else {
1047
+ vscode.window.showErrorMessage(`Failed to set env var: ${result.error}`);
1048
+ }
1049
+ }
1050
+
1051
+ async function viewEnv(sandboxId: string): Promise<void> {
1052
+ await vscode.window.withProgress(
1053
+ {
1054
+ location: vscode.ProgressLocation.Notification,
1055
+ title: 'Fetching environment variables...',
1056
+ cancellable: false,
1057
+ },
1058
+ async () => {
1059
+ const cli = getCliClient();
1060
+ // Use exec to run 'env' command to get actual runtime environment
1061
+ const result = await cli.sandboxExec(sandboxId, ['env']);
1062
+
1063
+ if (result.success && result.data) {
1064
+ const content = result.data.output || '(no environment variables)';
1065
+ await openReadonlyDocument(
1066
+ content,
1067
+ 'properties',
1068
+ `sandbox-env-${sandboxId.slice(0, 8)}`
1069
+ );
1070
+ } else {
1071
+ vscode.window.showErrorMessage(`Failed to get env: ${result.error}`);
1072
+ }
1073
+ }
1074
+ );
1075
+ }
1076
+
1077
+ async function syncEnvFile(sandboxId: string): Promise<void> {
1078
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
1079
+ if (!workspaceFolder) {
1080
+ vscode.window.showWarningMessage('No workspace folder open');
1081
+ return;
1082
+ }
1083
+
1084
+ const envPath = path.join(workspaceFolder.uri.fsPath, '.env');
1085
+ try {
1086
+ const content = await vscode.workspace.fs.readFile(vscode.Uri.file(envPath));
1087
+ const text = new TextDecoder().decode(content);
1088
+
1089
+ const vars: Record<string, string> = {};
1090
+ for (const line of text.split('\n')) {
1091
+ const trimmed = line.trim();
1092
+ if (!trimmed || trimmed.startsWith('#')) continue;
1093
+ const [key, ...valueParts] = trimmed.split('=');
1094
+ if (key && valueParts.length > 0) {
1095
+ vars[key] = valueParts.join('=');
1096
+ }
1097
+ }
1098
+
1099
+ if (Object.keys(vars).length === 0) {
1100
+ vscode.window.showWarningMessage('No variables found in .env file');
1101
+ return;
1102
+ }
1103
+
1104
+ const cli = getCliClient();
1105
+ const result = await cli.sandboxEnvSet(sandboxId, vars);
1106
+
1107
+ if (result.success) {
1108
+ vscode.window.showInformationMessage(
1109
+ `Synced ${Object.keys(vars).length} environment variables`
1110
+ );
1111
+ } else {
1112
+ vscode.window.showErrorMessage(`Failed to sync env: ${result.error}`);
1113
+ }
1114
+ } catch {
1115
+ vscode.window.showWarningMessage('No .env file found in workspace root');
1116
+ }
1117
+ }
1118
+
1119
+ async function createSnapshot(sandboxId: string, provider: SandboxTreeDataProvider): Promise<void> {
1120
+ const tag = await vscode.window.showInputBox({
1121
+ prompt: 'Enter a tag for this snapshot (optional)',
1122
+ placeHolder: 'v1.0 or latest',
1123
+ });
1124
+
1125
+ await vscode.window.withProgress(
1126
+ {
1127
+ location: vscode.ProgressLocation.Notification,
1128
+ title: 'Creating snapshot...',
1129
+ cancellable: false,
1130
+ },
1131
+ async () => {
1132
+ const cli = getCliClient();
1133
+ const result = await cli.snapshotCreate(sandboxId, tag || undefined);
1134
+
1135
+ if (result.success && result.data) {
1136
+ vscode.window.showInformationMessage(
1137
+ `Snapshot created: ${result.data.snapshotId}${tag ? ` [${tag}]` : ''}`
1138
+ );
1139
+ provider.clearSandboxCache(sandboxId);
1140
+ provider.refresh();
1141
+ } else {
1142
+ vscode.window.showErrorMessage(`Failed to create snapshot: ${result.error}`);
1143
+ }
1144
+ }
1145
+ );
1146
+ }
1147
+
1148
+ async function deleteSnapshot(
1149
+ snapshotId: string,
1150
+ provider: SandboxTreeDataProvider
1151
+ ): Promise<void> {
1152
+ const confirm = await vscode.window.showWarningMessage(
1153
+ `Are you sure you want to delete snapshot ${snapshotId}?`,
1154
+ { modal: true },
1155
+ 'Delete'
1156
+ );
1157
+
1158
+ if (confirm !== 'Delete') return;
1159
+
1160
+ const cli = getCliClient();
1161
+ const result = await cli.snapshotDelete(snapshotId);
1162
+
1163
+ if (result.success) {
1164
+ vscode.window.showInformationMessage('Snapshot deleted');
1165
+ await provider.forceRefresh();
1166
+ } else {
1167
+ vscode.window.showErrorMessage(`Failed to delete snapshot: ${result.error}`);
1168
+ }
1169
+ }
1170
+
1171
+ async function tagSnapshot(snapshotId: string, provider: SandboxTreeDataProvider): Promise<void> {
1172
+ const tag = await vscode.window.showInputBox({
1173
+ prompt: 'Enter new tag (leave empty to remove tag)',
1174
+ placeHolder: 'v1.0 or latest',
1175
+ });
1176
+
1177
+ if (tag === undefined) return;
1178
+
1179
+ const cli = getCliClient();
1180
+ const result = await cli.snapshotTag(snapshotId, tag || null);
1181
+
1182
+ if (result.success) {
1183
+ vscode.window.showInformationMessage(tag ? `Tagged as: ${tag}` : 'Tag removed');
1184
+ await provider.forceRefresh();
1185
+ } else {
1186
+ vscode.window.showErrorMessage(`Failed to update tag: ${result.error}`);
1187
+ }
1188
+ }
1189
+
1190
+ async function viewSnapshotDetails(snapshotId: string): Promise<void> {
1191
+ await vscode.window.withProgress(
1192
+ {
1193
+ location: vscode.ProgressLocation.Notification,
1194
+ title: 'Fetching snapshot details...',
1195
+ cancellable: false,
1196
+ },
1197
+ async () => {
1198
+ const cli = getCliClient();
1199
+ const result = await cli.snapshotGet(snapshotId);
1200
+
1201
+ if (result.success && result.data) {
1202
+ const content = JSON.stringify(result.data, null, 2);
1203
+ await openReadonlyDocument(content, 'json', `snapshot-${snapshotId.slice(0, 8)}`);
1204
+ } else {
1205
+ vscode.window.showErrorMessage(`Failed to get snapshot: ${result.error}`);
1206
+ }
1207
+ }
1208
+ );
1209
+ }
1210
+
1211
+ async function viewSnapshotFile(snapshot: SnapshotInfo, filePath: string): Promise<void> {
1212
+ if (!snapshot.downloadUrl) {
1213
+ vscode.window.showErrorMessage('Snapshot does not have a download URL');
1214
+ return;
1215
+ }
1216
+
1217
+ await vscode.window.withProgress(
1218
+ {
1219
+ location: vscode.ProgressLocation.Notification,
1220
+ title: `Fetching ${path.basename(filePath)}...`,
1221
+ cancellable: false,
1222
+ },
1223
+ async () => {
1224
+ try {
1225
+ // Create temp directory for snapshot files
1226
+ const snapshotTmpDir = path.join(
1227
+ os.tmpdir(),
1228
+ 'agentuity-snapshots',
1229
+ snapshot.snapshotId.slice(0, 12)
1230
+ );
1231
+ fs.mkdirSync(snapshotTmpDir, { recursive: true });
1232
+
1233
+ const archivePath = path.join(snapshotTmpDir, 'snapshot.tar.gz');
1234
+ const extractDir = path.join(snapshotTmpDir, 'files');
1235
+
1236
+ // Download and extract the archive if not already cached
1237
+ if (!fs.existsSync(extractDir)) {
1238
+ // Download tar.gz
1239
+ await downloadFile(snapshot.downloadUrl!, archivePath);
1240
+
1241
+ // Extract using tar module
1242
+ fs.mkdirSync(extractDir, { recursive: true });
1243
+ const { extract } = await import('tar');
1244
+ await extract({
1245
+ file: archivePath,
1246
+ cwd: extractDir,
1247
+ });
1248
+ }
1249
+
1250
+ // Read the specific file
1251
+ const targetFile = path.join(extractDir, filePath);
1252
+ if (!fs.existsSync(targetFile)) {
1253
+ vscode.window.showErrorMessage(`File not found in snapshot: ${filePath}`);
1254
+ return;
1255
+ }
1256
+
1257
+ const content = fs.readFileSync(targetFile, 'utf-8');
1258
+ const ext = path.extname(filePath).slice(1) || 'txt';
1259
+ await openReadonlyDocument(
1260
+ content,
1261
+ ext,
1262
+ `snapshot-${snapshot.snapshotId.slice(0, 8)}-${path.basename(filePath)}`
1263
+ );
1264
+ } catch (err) {
1265
+ vscode.window.showErrorMessage(
1266
+ `Failed to view snapshot file: ${err instanceof Error ? err.message : 'unknown error'}`
1267
+ );
1268
+ }
1269
+ }
1270
+ );
1271
+ }
1272
+
1273
+ async function downloadFile(url: string, destPath: string): Promise<void> {
1274
+ const https = await import('https');
1275
+ const http = await import('http');
1276
+
1277
+ return new Promise((resolve, reject) => {
1278
+ const protocol = url.startsWith('https') ? https : http;
1279
+ const file = fs.createWriteStream(destPath);
1280
+
1281
+ const request = protocol.get(url, (response) => {
1282
+ // Handle redirects
1283
+ if (response.statusCode === 301 || response.statusCode === 302) {
1284
+ const redirectUrl = response.headers.location;
1285
+ if (redirectUrl) {
1286
+ file.close();
1287
+ fs.unlinkSync(destPath);
1288
+ downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
1289
+ return;
1290
+ }
1291
+ }
1292
+
1293
+ if (response.statusCode !== 200) {
1294
+ file.close();
1295
+ fs.unlinkSync(destPath);
1296
+ reject(new Error(`HTTP ${response.statusCode}`));
1297
+ return;
1298
+ }
1299
+
1300
+ response.pipe(file);
1301
+ file.on('finish', () => {
1302
+ file.close();
1303
+ resolve();
1304
+ });
1305
+ file.on('error', (err) => {
1306
+ fs.unlinkSync(destPath);
1307
+ reject(err);
1308
+ });
1309
+ });
1310
+
1311
+ request.on('error', (err) => {
1312
+ file.close();
1313
+ try {
1314
+ fs.unlinkSync(destPath);
1315
+ } catch {
1316
+ /* ignore cleanup errors */
1317
+ }
1318
+ reject(err);
1319
+ });
1320
+
1321
+ request.setTimeout(60000, () => {
1322
+ request.destroy();
1323
+ file.close();
1324
+ try {
1325
+ fs.unlinkSync(destPath);
1326
+ } catch {
1327
+ /* ignore cleanup errors */
1328
+ }
1329
+ reject(new Error('Download timeout'));
1330
+ });
1331
+ });
1332
+ }
1333
+
1334
+ async function uploadToSandbox(uri: vscode.Uri): Promise<void> {
1335
+ const linked = getSandboxManager().getLinkedSandboxes();
1336
+
1337
+ if (linked.length === 0) {
1338
+ vscode.window.showWarningMessage('No sandbox linked. Link a sandbox first.');
1339
+ return;
1340
+ }
1341
+
1342
+ let sandboxId: string;
1343
+ if (linked.length === 1) {
1344
+ sandboxId = linked[0].sandboxId;
1345
+ } else {
1346
+ const picked = await vscode.window.showQuickPick(
1347
+ linked.map((l) => ({
1348
+ label: l.name || l.sandboxId,
1349
+ description: l.sandboxId,
1350
+ sandboxId: l.sandboxId,
1351
+ })),
1352
+ { placeHolder: 'Select sandbox to upload to' }
1353
+ );
1354
+ if (!picked) return;
1355
+ sandboxId = picked.sandboxId;
1356
+ }
1357
+
1358
+ const remotePath = await vscode.window.showInputBox({
1359
+ prompt: 'Remote path',
1360
+ value: linked.find((l) => l.sandboxId === sandboxId)?.remotePath || DEFAULT_SANDBOX_PATH,
1361
+ });
1362
+
1363
+ if (!remotePath) return;
1364
+
1365
+ await vscode.window.withProgress(
1366
+ {
1367
+ location: vscode.ProgressLocation.Notification,
1368
+ title: 'Uploading...',
1369
+ cancellable: false,
1370
+ },
1371
+ async () => {
1372
+ const cli = getCliClient();
1373
+ const stats = await vscode.workspace.fs.stat(uri);
1374
+ const isDir = stats.type === vscode.FileType.Directory;
1375
+
1376
+ const result = await cli.sandboxCpToSandbox(sandboxId, uri.fsPath, remotePath, isDir);
1377
+
1378
+ if (result.success) {
1379
+ vscode.window.showInformationMessage(`Uploaded to ${remotePath}`);
1380
+ } else {
1381
+ vscode.window.showErrorMessage(`Failed to upload: ${result.error}`);
1382
+ }
1383
+ }
1384
+ );
1385
+ }
1386
+
1387
+ export { SandboxTreeDataProvider };