groove-dev 0.27.169 → 0.27.172

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.
Files changed (40) hide show
  1. package/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
  5. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
  6. package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
  7. package/node_modules/@groove-dev/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
  8. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  9. package/node_modules/@groove-dev/gui/package.json +1 -1
  10. package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
  11. package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
  12. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
  13. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
  14. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
  15. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
  16. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
  17. package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
  18. package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
  19. package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
  20. package/package.json +1 -1
  21. package/packages/cli/package.json +1 -1
  22. package/packages/daemon/package.json +1 -1
  23. package/packages/daemon/src/routes/files.js +18 -5
  24. package/packages/daemon/src/tunnel-manager.js +16 -6
  25. package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
  26. package/packages/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
  27. package/packages/gui/dist/index.html +2 -2
  28. package/packages/gui/package.json +1 -1
  29. package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
  30. package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
  31. package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
  32. package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
  33. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
  34. package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
  35. package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
  36. package/packages/gui/src/views/agents.jsx +2 -11
  37. package/packages/gui/src/views/editor.jsx +63 -2
  38. package/packages/gui/src/views/settings.jsx +2 -1
  39. package/node_modules/@groove-dev/gui/dist/assets/index-BvXojcnr.css +0 -1
  40. package/packages/gui/dist/assets/index-BvXojcnr.css +0 -1
@@ -81,29 +81,29 @@ function ToggleSwitch({ value, onChange }) {
81
81
 
82
82
  function FieldCard({ icon: Icon, title, children }) {
83
83
  return (
84
- <div className="rounded-xl border border-border-subtle bg-surface-1/80 px-5 py-4 flex flex-col gap-2.5">
85
- <div className="flex items-center gap-2.5">
86
- <div className="w-7 h-7 rounded-lg bg-accent/10 border border-accent/10 flex items-center justify-center flex-shrink-0">
87
- <Icon size={13} className="text-accent" />
84
+ <div className="flex flex-col gap-3">
85
+ <div className="flex items-center gap-3">
86
+ <div className="w-8 h-8 rounded bg-[rgba(255,255,255,0.04)] border border-[#2c313a] flex items-center justify-center flex-shrink-0">
87
+ <Icon size={15} className="text-[#8b95a5]" />
88
88
  </div>
89
- <span className="text-sm font-semibold text-text-0 font-sans">{title}</span>
89
+ <span className="text-[15px] font-semibold text-[#e6e8ed]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif", letterSpacing: '-0.2px' }}>{title}</span>
90
90
  </div>
91
- <div className="mt-0.5">{children}</div>
91
+ <div>{children}</div>
92
92
  </div>
93
93
  );
94
94
  }
95
95
 
96
96
  function InfoCard({ icon: Icon, title, iconColor, children }) {
97
97
  return (
98
- <div className="rounded-xl border border-border-subtle bg-surface-1/80 px-5 py-4">
99
- <div className="flex items-center gap-2.5 mb-3">
98
+ <div>
99
+ <div className="flex items-center gap-3 mb-4">
100
100
  <div className={cn(
101
- 'w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0',
102
- iconColor || 'bg-accent/10',
101
+ 'w-8 h-8 rounded border border-[#2c313a] flex items-center justify-center flex-shrink-0',
102
+ iconColor || 'bg-[rgba(255,255,255,0.04)]',
103
103
  )}>
104
- <Icon size={13} className={iconColor ? undefined : 'text-accent'} />
104
+ <Icon size={15} className={iconColor ? undefined : 'text-[#8b95a5]'} />
105
105
  </div>
106
- <span className="text-sm font-semibold text-text-0 font-sans">{title}</span>
106
+ <span className="text-[15px] font-semibold text-[#e6e8ed]" style={{ fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif", letterSpacing: '-0.2px' }}>{title}</span>
107
107
  </div>
108
108
  {children}
109
109
  </div>
@@ -342,11 +342,13 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
342
342
  setConnecting(false);
343
343
  }
344
344
 
345
- const inputCls = 'h-9 px-3 text-xs bg-surface-0 border border-border-subtle rounded-lg text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/30 transition-colors';
346
- const monoInputCls = 'h-9 px-3 text-xs bg-surface-0 border border-border-subtle rounded-lg text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/30 transition-colors';
345
+ const inputCls = 'h-[38px] px-[13px] text-[13px] bg-[#1e2127] border border-[#2c313a] rounded text-[#e6e8ed] placeholder:text-[#4a5060] focus:outline-none focus:border-[#33afbc] focus:bg-[#1a1e25] transition-colors';
346
+ const monoInputCls = 'h-[38px] px-[13px] text-[13px] bg-[#1e2127] border border-[#2c313a] rounded text-[#e6e8ed] placeholder:text-[#4a5060] focus:outline-none focus:border-[#33afbc] focus:bg-[#1a1e25] transition-colors';
347
+ const inputStyle = { fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', system-ui, sans-serif" };
348
+ const monoStyle = { fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace" };
347
349
 
348
350
  return (
349
- <div className="p-5">
351
+ <div className="p-6">
350
352
  <StepIndicator
351
353
  steps={STEPS}
352
354
  currentStep={step}
@@ -359,22 +361,24 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
359
361
  <FieldCard icon={Server} title="Server Info">
360
362
  <div className="space-y-3">
361
363
  <div>
362
- <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Name</label>
364
+ <label className="block text-[10.5px] font-medium text-[#8b95a5] mb-1.5 uppercase tracking-wider" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace", letterSpacing: '0.6px' }}>Name</label>
363
365
  <input
364
366
  value={name}
365
367
  onChange={(e) => setName(e.target.value)}
366
368
  placeholder="api-vps"
367
369
  className={cn(inputCls, 'w-full')}
370
+ style={inputStyle}
368
371
  autoFocus
369
372
  />
370
373
  </div>
371
374
  <div>
372
- <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Host</label>
375
+ <label className="block text-[10.5px] font-medium text-[#8b95a5] mb-1.5 uppercase tracking-wider" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace", letterSpacing: '0.6px' }}>Host</label>
373
376
  <input
374
377
  value={host}
375
378
  onChange={(e) => setHost(e.target.value)}
376
379
  placeholder="165.22.180.45"
377
380
  className={cn(monoInputCls, 'w-full')}
381
+ style={monoStyle}
378
382
  />
379
383
  </div>
380
384
  </div>
@@ -383,21 +387,23 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
383
387
  <FieldCard icon={Settings} title="Connection">
384
388
  <div className="space-y-3">
385
389
  <div>
386
- <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">User</label>
390
+ <label className="block text-[10.5px] font-medium text-[#8b95a5] mb-1.5 uppercase tracking-wider" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace", letterSpacing: '0.6px' }}>User</label>
387
391
  <input
388
392
  value={user}
389
393
  onChange={(e) => setUser(e.target.value)}
390
394
  placeholder="root"
391
395
  className={cn(monoInputCls, 'w-full')}
396
+ style={monoStyle}
392
397
  />
393
398
  </div>
394
399
  <div>
395
- <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">SSH Port</label>
400
+ <label className="block text-[10.5px] font-medium text-[#8b95a5] mb-1.5 uppercase tracking-wider" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace", letterSpacing: '0.6px' }}>SSH Port</label>
396
401
  <input
397
402
  value={sshPort}
398
403
  onChange={(e) => setSshPort(Number(e.target.value) || 22)}
399
404
  type="number"
400
- className={cn(monoInputCls, 'w-24')}
405
+ className={cn(monoInputCls, 'w-28')}
406
+ style={monoStyle}
401
407
  />
402
408
  </div>
403
409
  </div>
@@ -410,74 +416,72 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
410
416
  <FieldCard icon={KeyRound} title="SSH Key">
411
417
  <div className="space-y-3">
412
418
  <div>
413
- <label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">Key Path</label>
414
- <div className="flex items-center gap-1.5">
419
+ <label className="block text-[10.5px] font-medium text-[#8b95a5] mb-1.5 uppercase tracking-wider" style={{ fontFamily: "ui-monospace, 'SF Mono', Monaco, monospace", letterSpacing: '0.6px' }}>Key Path</label>
420
+ <div className="flex items-center gap-2">
415
421
  <input
416
422
  value={sshKeyPath}
417
423
  onChange={(e) => setSshKeyPath(e.target.value)}
418
424
  placeholder="~/.ssh/id_ed25519"
419
425
  className={cn(monoInputCls, 'flex-1 min-w-0')}
426
+ style={monoStyle}
420
427
  autoFocus
421
428
  />
422
- <Button
423
- variant="secondary"
424
- size="sm"
429
+ <button
425
430
  onClick={() => setKeyBrowserOpen(true)}
426
- className="h-9 px-2.5 flex-shrink-0"
431
+ className="h-[38px] px-3 rounded bg-transparent border border-[#3e4451] text-[#8b95a5] hover:border-[#4a5060] hover:text-[#e6e8ed] hover:bg-[rgba(255,255,255,0.025)] cursor-pointer transition-colors flex-shrink-0"
427
432
  >
428
- <FolderSearch size={13} />
429
- </Button>
433
+ <FolderSearch size={15} />
434
+ </button>
430
435
  </div>
431
- <p className="text-2xs text-text-4 font-sans mt-1.5">
436
+ <p className="text-xs text-[#6e7681] mt-2" style={monoStyle}>
432
437
  Leave blank to use default SSH agent.
433
438
  </p>
434
439
  </div>
435
- <Button
436
- variant="secondary"
437
- size="sm"
440
+ <button
438
441
  onClick={handleTest}
439
442
  disabled={testLoading}
440
- className="h-8 text-2xs gap-1.5"
443
+ className="inline-flex items-center gap-2 h-9 px-4 rounded bg-transparent border border-[#3e4451] text-[#8b95a5] text-[12.5px] font-medium cursor-pointer transition-colors hover:border-[#4a5060] hover:text-[#e6e8ed] hover:bg-[rgba(255,255,255,0.025)] disabled:opacity-30 disabled:cursor-not-allowed"
444
+ style={inputStyle}
441
445
  >
442
- {testLoading ? <Loader2 size={11} className="animate-spin" /> : <Plug size={11} />}
446
+ {testLoading ? <Loader2 size={13} className="animate-spin" /> : <Plug size={13} />}
443
447
  Test Connection
444
- </Button>
448
+ </button>
445
449
  </div>
446
450
  </FieldCard>
447
451
 
448
452
  <div className="space-y-4">
449
453
  <InfoCard icon={Server} title="Target">
450
- <div className="space-y-2 text-2xs font-sans">
454
+ <div className="space-y-2.5 text-xs">
451
455
  <div className="flex items-center justify-between">
452
- <span className="text-text-3">Host</span>
453
- <span className="text-text-1 font-mono">{host || '—'}</span>
456
+ <span className="text-[#6e7681] uppercase text-[11px] tracking-wide" style={monoStyle}>Host</span>
457
+ <span className="text-[#e6e8ed] text-xs" style={monoStyle}>{host || '—'}</span>
454
458
  </div>
455
459
  <div className="flex items-center justify-between">
456
- <span className="text-text-3">User</span>
457
- <span className="text-text-1 font-mono">{user || '—'}</span>
460
+ <span className="text-[#6e7681] uppercase text-[11px] tracking-wide" style={monoStyle}>User</span>
461
+ <span className="text-[#e6e8ed] text-xs" style={monoStyle}>{user || '—'}</span>
458
462
  </div>
459
463
  <div className="flex items-center justify-between">
460
- <span className="text-text-3">Port</span>
461
- <span className="text-text-1 font-mono">{sshPort}</span>
464
+ <span className="text-[#6e7681] uppercase text-[11px] tracking-wide" style={monoStyle}>Port</span>
465
+ <span className="text-[#e6e8ed] text-xs" style={monoStyle}>{sshPort}</span>
462
466
  </div>
463
467
  </div>
464
468
  </InfoCard>
465
469
 
466
470
  {testResult && (
467
471
  <div className={cn(
468
- 'px-4 py-3 rounded-xl text-2xs font-sans flex items-start gap-2',
472
+ 'px-4 py-3.5 rounded text-xs flex items-start gap-2.5',
469
473
  testResult.error
470
- ? 'bg-danger/8 border border-danger/20 text-danger'
474
+ ? 'bg-[#1e2127] border border-[#ef4444]/25 text-[#ef4444]'
471
475
  : testResult.reachable
472
- ? 'bg-success/8 border border-success/20 text-success'
473
- : 'bg-warning/8 border border-warning/20 text-warning',
474
- )}>
476
+ ? 'bg-[#1e2127] border border-[#33afbc]/25 text-[#33afbc]'
477
+ : 'bg-[#1e2127] border border-[#fbbf24]/25 text-[#fbbf24]',
478
+ )} style={inputStyle}>
475
479
  {testResult.error ? (
476
- <><X size={11} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
480
+ <><X size={13} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
477
481
  ) : testResult.reachable ? (
478
- <><Check size={11} className="mt-0.5 flex-shrink-0" /> Server reachable</>
482
+ <><Check size={13} className="mt-0.5 flex-shrink-0" /> Server reachable</>
479
483
  ) : (
480
- <><AlertTriangle size={11} className="mt-0.5 flex-shrink-0" /> Host unreachable</>
484
+ <><AlertTriangle size={13} className="mt-0.5 flex-shrink-0" /> Host unreachable</>
481
485
  )}
482
486
  </div>
483
487
  )}
@@ -489,6 +493,8 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
489
493
  currentPath={sshKeyPath || '~/.ssh'}
490
494
  homePath={remoteHomedir}
491
495
  onSelect={(path) => setSshKeyPath(path)}
496
+ mode="file"
497
+ title="Select SSH Key"
492
498
  />
493
499
  </div>
494
500
  )}
@@ -601,48 +607,44 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
601
607
  </div>
602
608
  )}
603
609
 
604
- <div className="flex items-center justify-between mt-5">
605
- <Button
606
- variant="ghost"
607
- size="sm"
610
+ <div className="flex items-center justify-between mt-6">
611
+ <button
608
612
  onClick={step === 0 ? onCancel : step === 3 ? onCancel : handleBack}
609
- className="h-8 text-xs px-4 text-text-3"
613
+ className="inline-flex items-center h-9 px-4 rounded bg-transparent border border-[#3e4451] text-[#8b95a5] text-[12.5px] font-medium cursor-pointer transition-colors hover:border-[#4a5060] hover:text-[#e6e8ed] hover:bg-[rgba(255,255,255,0.025)]"
614
+ style={inputStyle}
610
615
  >
611
616
  {step === 0 ? 'Cancel' : step === 3 ? 'Done' : 'Back'}
612
- </Button>
617
+ </button>
613
618
  {step < 3 && (
614
619
  <div className="flex gap-2">
615
620
  {step === 2 ? (
616
- <Button
617
- variant="primary"
618
- size="sm"
621
+ <button
619
622
  onClick={handleConnect}
620
623
  disabled={connecting || saving}
621
- className="h-8 text-xs px-4 gap-1.5"
624
+ className="inline-flex items-center gap-2 h-9 px-5 rounded bg-[#33afbc] text-[#0a0c10] text-[12.5px] font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed"
625
+ style={inputStyle}
622
626
  >
623
- {connecting ? <Loader2 size={12} className="animate-spin" /> : <Plug size={12} />}
627
+ {connecting ? <Loader2 size={14} className="animate-spin" /> : <Plug size={14} />}
624
628
  {connecting ? 'Connecting...' : 'Connect'}
625
- </Button>
629
+ </button>
626
630
  ) : step === 1 ? (
627
- <Button
628
- variant="primary"
629
- size="sm"
631
+ <button
630
632
  onClick={handleSaveAndSetup}
631
633
  disabled={saving}
632
- className="h-8 text-xs px-4"
634
+ className="inline-flex items-center h-9 px-5 rounded bg-[#33afbc] text-[#0a0c10] text-[12.5px] font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed"
635
+ style={inputStyle}
633
636
  >
634
637
  {saving ? 'Saving...' : 'Next'}
635
- </Button>
638
+ </button>
636
639
  ) : (
637
- <Button
638
- variant="primary"
639
- size="sm"
640
+ <button
640
641
  onClick={handleNext}
641
642
  disabled={!canAdvanceStep0()}
642
- className="h-8 text-xs px-4"
643
+ className="inline-flex items-center h-9 px-5 rounded bg-[#33afbc] text-[#0a0c10] text-[12.5px] font-semibold cursor-pointer transition-opacity hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed"
644
+ style={inputStyle}
643
645
  >
644
646
  Next
645
- </Button>
647
+ </button>
646
648
  )}
647
649
  </div>
648
650
  )}
@@ -19,6 +19,7 @@ import { Select, SelectTrigger, SelectContent, SelectItem } from '../components/
19
19
  import { ScrollArea } from '../components/ui/scroll-area';
20
20
  import { Tooltip } from '../components/ui/tooltip';
21
21
  import { TuningSlider } from '../components/ui/slider';
22
+ import { RecommendedTeamCard } from '../components/agents/recommended-team-card';
22
23
 
23
24
  const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
24
25
  const NODE_W = 220;
@@ -1233,17 +1234,7 @@ function EmptyState({ onPlanner, onSpawn, onTeamBuilder }) {
1233
1234
  );
1234
1235
  }
1235
1236
 
1236
- /* ── Recommended Team Launch Card ─────────────────────────── */
1237
-
1238
- const ROLE_ICONS = { backend: Server, frontend: Monitor, fullstack: Code2, testing: TestTube, security: Shield };
1239
-
1240
- const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
1241
-
1242
- function sanitizeName(raw) {
1243
- return raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
1244
- }
1245
-
1246
- function RecommendedTeamCard() {
1237
+ function _RecommendedTeamCard_removed() { return null; /* extracted to components/agents/recommended-team-card.jsx */
1247
1238
  const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
1248
1239
  const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
1249
1240
  const teamLaunchConfig = useGrooveStore((s) => s.teamLaunchConfig);
@@ -8,7 +8,7 @@ import { MediaViewer, isMediaFile } from '../components/editor/media-viewer';
8
8
  import { EditorStatusBar } from '../components/editor/editor-status-bar';
9
9
  import { GotoLine } from '../components/editor/goto-line';
10
10
  import { Breadcrumbs } from '../components/editor/breadcrumbs';
11
- import { Code2, Eye, FileCode, PanelLeftOpen } from 'lucide-react';
11
+ import { Code2, Eye, FileCode, PanelLeftOpen, Upload } from 'lucide-react';
12
12
  import { api } from '../lib/api';
13
13
  import { cn } from '../lib/cn';
14
14
 
@@ -37,6 +37,11 @@ export default function EditorView() {
37
37
  const [showGotoLine, setShowGotoLine] = useState(false);
38
38
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
39
39
 
40
+ const [dragOver, setDragOver] = useState(false);
41
+ const dragCounter = useRef(0);
42
+ const addToast = useGrooveStore((s) => s.addToast);
43
+ const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
44
+
40
45
  const editorViewRef = useRef(null);
41
46
  const dragging = useRef(false);
42
47
  const startX = useRef(0);
@@ -62,6 +67,47 @@ export default function EditorView() {
62
67
  return () => document.removeEventListener('keydown', handleKeyDown);
63
68
  }, []);
64
69
 
70
+ // External file drop handler — upload files dropped anywhere in the editor
71
+ function handleDragEnter(e) {
72
+ e.preventDefault();
73
+ dragCounter.current++;
74
+ if (e.dataTransfer?.types?.includes('Files')) setDragOver(true);
75
+ }
76
+ function handleDragLeave(e) {
77
+ e.preventDefault();
78
+ dragCounter.current--;
79
+ if (dragCounter.current <= 0) { dragCounter.current = 0; setDragOver(false); }
80
+ }
81
+ function handleDragOver(e) {
82
+ e.preventDefault();
83
+ e.dataTransfer.dropEffect = 'copy';
84
+ }
85
+ async function handleDrop(e) {
86
+ e.preventDefault();
87
+ dragCounter.current = 0;
88
+ setDragOver(false);
89
+ const nativeFiles = e.dataTransfer?.files;
90
+ if (!nativeFiles?.length) return;
91
+
92
+ const toUpload = [];
93
+ for (const file of Array.from(nativeFiles)) {
94
+ const base64 = await new Promise((resolve, reject) => {
95
+ const reader = new FileReader();
96
+ reader.onload = () => resolve(reader.result.split(',')[1]);
97
+ reader.onerror = reject;
98
+ reader.readAsDataURL(file);
99
+ });
100
+ toUpload.push({ name: file.name, content: base64 });
101
+ }
102
+ try {
103
+ const result = await api.post('/files/upload', { dir: '', files: toUpload });
104
+ addToast('success', `Uploaded ${result.total} file${result.total !== 1 ? 's' : ''}`);
105
+ fetchTreeDir('');
106
+ } catch (err) {
107
+ addToast('error', 'Upload failed', err.message);
108
+ }
109
+ }
110
+
65
111
  // Sidebar resize handlers
66
112
  const onSidebarMouseDown = useCallback((e) => {
67
113
  e.preventDefault();
@@ -129,7 +175,22 @@ export default function EditorView() {
129
175
  )}
130
176
 
131
177
  {/* Editor area */}
132
- <div className="flex-1 flex flex-col min-w-0 bg-surface-1">
178
+ <div
179
+ className="flex-1 flex flex-col min-w-0 bg-surface-1 relative"
180
+ onDragEnter={handleDragEnter}
181
+ onDragLeave={handleDragLeave}
182
+ onDragOver={handleDragOver}
183
+ onDrop={handleDrop}
184
+ >
185
+ {/* Drop overlay */}
186
+ {dragOver && (
187
+ <div className="absolute inset-0 z-50 bg-accent/8 border-2 border-dashed border-accent/40 rounded-lg flex items-center justify-center pointer-events-none">
188
+ <div className="flex flex-col items-center gap-2 text-accent">
189
+ <Upload size={32} />
190
+ <span className="text-sm font-semibold font-sans">Drop files to upload</span>
191
+ </div>
192
+ </div>
193
+ )}
133
194
 
134
195
  {/* Tab bar */}
135
196
  <EditorTabs />
@@ -1580,6 +1580,7 @@ export default function SettingsView() {
1580
1580
  const [folderBrowserOpen, setFolderBrowserOpen] = useState(false);
1581
1581
  const addToast = useGrooveStore((s) => s.addToast);
1582
1582
  const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
1583
+ const tunneled = useGrooveStore((s) => s.tunneled);
1583
1584
  const providerRefreshTick = useGrooveStore((s) => s._providerRefreshTick);
1584
1585
 
1585
1586
  function loadProviders() {
@@ -1764,7 +1765,7 @@ export default function SettingsView() {
1764
1765
  {config.defaultWorkingDir || 'Project root'}
1765
1766
  </code>
1766
1767
  <Button variant="secondary" size="sm" onClick={async () => {
1767
- if (window.groove?.folders?.select) {
1768
+ if (window.groove?.folders?.select && !tunneled) {
1768
1769
  const dir = await window.groove.folders.select({
1769
1770
  title: 'Select Working Directory',
1770
1771
  defaultPath: config?.defaultWorkingDir || undefined,