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.
- package/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
- package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/routes/files.js +18 -5
- package/packages/daemon/src/tunnel-manager.js +16 -6
- package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/packages/gui/dist/assets/{index-BcWo4sq-.js → index-BsCp-oqa.js} +226 -221
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
- package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
- package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/packages/gui/src/views/agents.jsx +2 -11
- package/packages/gui/src/views/editor.jsx +63 -2
- package/packages/gui/src/views/settings.jsx +2 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BvXojcnr.css +0 -1
- 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="
|
|
85
|
-
<div className="flex items-center gap-
|
|
86
|
-
<div className="w-
|
|
87
|
-
<Icon size={
|
|
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-
|
|
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
|
|
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
|
|
99
|
-
<div className="flex items-center gap-
|
|
98
|
+
<div>
|
|
99
|
+
<div className="flex items-center gap-3 mb-4">
|
|
100
100
|
<div className={cn(
|
|
101
|
-
'w-
|
|
102
|
-
iconColor || 'bg-
|
|
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={
|
|
104
|
+
<Icon size={15} className={iconColor ? undefined : 'text-[#8b95a5]'} />
|
|
105
105
|
</div>
|
|
106
|
-
<span className="text-
|
|
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-
|
|
346
|
-
const monoInputCls = 'h-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
414
|
-
<div className="flex items-center gap-
|
|
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
|
-
<
|
|
423
|
-
variant="secondary"
|
|
424
|
-
size="sm"
|
|
429
|
+
<button
|
|
425
430
|
onClick={() => setKeyBrowserOpen(true)}
|
|
426
|
-
className="h-
|
|
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={
|
|
429
|
-
</
|
|
433
|
+
<FolderSearch size={15} />
|
|
434
|
+
</button>
|
|
430
435
|
</div>
|
|
431
|
-
<p className="text-
|
|
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
|
-
<
|
|
436
|
-
variant="secondary"
|
|
437
|
-
size="sm"
|
|
440
|
+
<button
|
|
438
441
|
onClick={handleTest}
|
|
439
442
|
disabled={testLoading}
|
|
440
|
-
className="h-
|
|
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={
|
|
446
|
+
{testLoading ? <Loader2 size={13} className="animate-spin" /> : <Plug size={13} />}
|
|
443
447
|
Test Connection
|
|
444
|
-
</
|
|
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-
|
|
454
|
+
<div className="space-y-2.5 text-xs">
|
|
451
455
|
<div className="flex items-center justify-between">
|
|
452
|
-
<span className="text-text-
|
|
453
|
-
<span className="text-text-
|
|
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-
|
|
457
|
-
<span className="text-text-
|
|
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-
|
|
461
|
-
<span className="text-text-
|
|
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
|
|
472
|
+
'px-4 py-3.5 rounded text-xs flex items-start gap-2.5',
|
|
469
473
|
testResult.error
|
|
470
|
-
? 'bg-
|
|
474
|
+
? 'bg-[#1e2127] border border-[#ef4444]/25 text-[#ef4444]'
|
|
471
475
|
: testResult.reachable
|
|
472
|
-
? 'bg-
|
|
473
|
-
: 'bg-
|
|
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={
|
|
480
|
+
<><X size={13} className="mt-0.5 flex-shrink-0" /> {testResult.error}</>
|
|
477
481
|
) : testResult.reachable ? (
|
|
478
|
-
<><Check size={
|
|
482
|
+
<><Check size={13} className="mt-0.5 flex-shrink-0" /> Server reachable</>
|
|
479
483
|
) : (
|
|
480
|
-
<><AlertTriangle size={
|
|
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-
|
|
605
|
-
<
|
|
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="
|
|
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
|
-
</
|
|
617
|
+
</button>
|
|
613
618
|
{step < 3 && (
|
|
614
619
|
<div className="flex gap-2">
|
|
615
620
|
{step === 2 ? (
|
|
616
|
-
<
|
|
617
|
-
variant="primary"
|
|
618
|
-
size="sm"
|
|
621
|
+
<button
|
|
619
622
|
onClick={handleConnect}
|
|
620
623
|
disabled={connecting || saving}
|
|
621
|
-
className="h-
|
|
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={
|
|
627
|
+
{connecting ? <Loader2 size={14} className="animate-spin" /> : <Plug size={14} />}
|
|
624
628
|
{connecting ? 'Connecting...' : 'Connect'}
|
|
625
|
-
</
|
|
629
|
+
</button>
|
|
626
630
|
) : step === 1 ? (
|
|
627
|
-
<
|
|
628
|
-
variant="primary"
|
|
629
|
-
size="sm"
|
|
631
|
+
<button
|
|
630
632
|
onClick={handleSaveAndSetup}
|
|
631
633
|
disabled={saving}
|
|
632
|
-
className="h-
|
|
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
|
-
</
|
|
638
|
+
</button>
|
|
636
639
|
) : (
|
|
637
|
-
<
|
|
638
|
-
variant="primary"
|
|
639
|
-
size="sm"
|
|
640
|
+
<button
|
|
640
641
|
onClick={handleNext}
|
|
641
642
|
disabled={!canAdvanceStep0()}
|
|
642
|
-
className="h-
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.172",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -156,22 +156,35 @@ export function registerFileRoutes(app, daemon) {
|
|
|
156
156
|
if (absPath.includes('\0')) return res.status(400).json({ error: 'Invalid path' });
|
|
157
157
|
if (!existsSync(absPath)) return res.status(404).json({ error: 'Not found' });
|
|
158
158
|
|
|
159
|
+
const showFiles = req.query.files === 'true';
|
|
160
|
+
const showHidden = req.query.hidden === 'true';
|
|
161
|
+
|
|
159
162
|
try {
|
|
160
|
-
const
|
|
161
|
-
|
|
163
|
+
const raw = readdirSync(absPath, { withFileTypes: true });
|
|
164
|
+
|
|
165
|
+
const dirs = raw
|
|
166
|
+
.filter((e) => e.isDirectory() && (showHidden || !e.name.startsWith('.')) && e.name !== 'node_modules')
|
|
162
167
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
163
168
|
.map((e) => {
|
|
164
169
|
const full = resolve(absPath, e.name);
|
|
165
170
|
let hasChildren = false;
|
|
166
171
|
try {
|
|
167
172
|
hasChildren = readdirSync(full, { withFileTypes: true })
|
|
168
|
-
.some((c) => c.isDirectory() && !c.name.startsWith('.') && c.name !== 'node_modules');
|
|
173
|
+
.some((c) => c.isDirectory() && (showHidden || !c.name.startsWith('.')) && c.name !== 'node_modules');
|
|
169
174
|
} catch { /* unreadable */ }
|
|
170
|
-
return { name: e.name, path: full, hasChildren };
|
|
175
|
+
return { name: e.name, path: full, hasChildren, type: 'dir' };
|
|
171
176
|
});
|
|
172
177
|
|
|
178
|
+
let files = [];
|
|
179
|
+
if (showFiles) {
|
|
180
|
+
files = raw
|
|
181
|
+
.filter((e) => e.isFile() && (showHidden || !e.name.startsWith('.')))
|
|
182
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
183
|
+
.map((e) => ({ name: e.name, path: resolve(absPath, e.name), type: 'file' }));
|
|
184
|
+
}
|
|
185
|
+
|
|
173
186
|
const parent = absPath === '/' ? null : resolve(absPath, '..');
|
|
174
|
-
res.json({ current: absPath, parent, dirs
|
|
187
|
+
res.json({ current: absPath, parent, dirs, files });
|
|
175
188
|
} catch (err) {
|
|
176
189
|
res.status(500).json({ error: err.message });
|
|
177
190
|
}
|
|
@@ -34,7 +34,8 @@ function validateField(value, name) {
|
|
|
34
34
|
|
|
35
35
|
function sshCmd(cmd) {
|
|
36
36
|
const nvmProbe = 'export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; ';
|
|
37
|
-
|
|
37
|
+
const npmGlobalProbe = '[ -d "$HOME/.npm-global/bin" ] && export PATH="$HOME/.npm-global/bin:$PATH"; ';
|
|
38
|
+
return `bash -lc '${nvmProbe}${npmGlobalProbe}${cmd}'`;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function npmGlobalInstall(pkg, user) {
|
|
@@ -354,8 +355,19 @@ export class TunnelManager {
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
if (!tunnelUp) {
|
|
357
|
-
|
|
358
|
-
|
|
358
|
+
// Remote daemon likely not running — start it and retry the port check
|
|
359
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
360
|
+
try { await this.autoStart(id); } catch { /* best effort */ }
|
|
361
|
+
for (let elapsed = 0; elapsed < 15000; elapsed += 500) {
|
|
362
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
363
|
+
if (tunnel.exitCode !== null) break;
|
|
364
|
+
tunnelUp = await this._isPortInUse(localPort);
|
|
365
|
+
if (tunnelUp) break;
|
|
366
|
+
}
|
|
367
|
+
if (!tunnelUp) {
|
|
368
|
+
try { process.kill(tunnel.pid); } catch { /* ignore */ }
|
|
369
|
+
throw new Error(`SSH tunnel started but remote daemon not reachable${stderrBuf.trim() ? ': ' + stderrBuf.trim() : ''}`);
|
|
370
|
+
}
|
|
359
371
|
}
|
|
360
372
|
|
|
361
373
|
tunnel.unref();
|
|
@@ -379,7 +391,7 @@ export class TunnelManager {
|
|
|
379
391
|
remoteAlive = probe.ok;
|
|
380
392
|
} catch { /* not reachable */ }
|
|
381
393
|
|
|
382
|
-
if (!remoteAlive
|
|
394
|
+
if (!remoteAlive) {
|
|
383
395
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
384
396
|
await this.autoStart(id);
|
|
385
397
|
for (let i = 0; i < 5; i++) {
|
|
@@ -391,8 +403,6 @@ export class TunnelManager {
|
|
|
391
403
|
if (retry.ok) { remoteAlive = true; break; }
|
|
392
404
|
} catch { /* retry */ }
|
|
393
405
|
}
|
|
394
|
-
} else if (!remoteAlive && !config.autoStart) {
|
|
395
|
-
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'waiting', message: 'Remote daemon not running. Start it manually or enable auto-start.' } });
|
|
396
406
|
}
|
|
397
407
|
|
|
398
408
|
// Auto-upgrade: check version through tunnel, upgrade if behind
|