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