groove-dev 0.27.139 → 0.27.141
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +225 -16
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +76 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
- 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/app.jsx +4 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +225 -16
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +17 -1
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +277 -0
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +9 -0
- package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +4 -0
- package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
- package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/file-tree.jsx +2 -86
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
- package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +302 -20
- package/packages/gui/src/views/agents.jsx +118 -60
- package/packages/gui/src/views/editor.jsx +67 -219
- package/packages/gui/src/views/memory.jsx +460 -0
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +295 -0
- package/plan_files/keeper-memory-system.md +223 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
- package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
|
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
|
|
|
10
10
|
import { cn } from '../lib/cn';
|
|
11
11
|
import { Button } from '../components/ui/button';
|
|
12
12
|
import { Badge } from '../components/ui/badge';
|
|
13
|
-
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers,
|
|
13
|
+
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, LayoutGrid, Activity, Gauge, Cpu } from 'lucide-react';
|
|
14
14
|
import { PreviewWorkspace } from '../components/preview/preview-workspace';
|
|
15
15
|
import { WorkspaceMode } from '../components/agents/workspace-mode';
|
|
16
16
|
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
|
|
@@ -1258,7 +1258,7 @@ function RecommendedTeamCard() {
|
|
|
1258
1258
|
const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
|
|
1259
1259
|
const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
|
|
1260
1260
|
const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
|
|
1261
|
-
const [
|
|
1261
|
+
const [expandedAgent, setExpandedAgent] = useState(null);
|
|
1262
1262
|
|
|
1263
1263
|
useEffect(() => {
|
|
1264
1264
|
fetchProviders().then((list) => {
|
|
@@ -1283,6 +1283,15 @@ function RecommendedTeamCard() {
|
|
|
1283
1283
|
setEditedAgents(next);
|
|
1284
1284
|
}
|
|
1285
1285
|
|
|
1286
|
+
function handleAgentField(i, updates) {
|
|
1287
|
+
if (typeof updates === 'string') {
|
|
1288
|
+
const [field, value] = [updates, arguments[2]];
|
|
1289
|
+
setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, [field]: value } : a));
|
|
1290
|
+
} else {
|
|
1291
|
+
setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, ...updates } : a));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1286
1295
|
function handleTsProviderChange(id) {
|
|
1287
1296
|
setTsProvider(id);
|
|
1288
1297
|
const p = providers.find((x) => x.id === id);
|
|
@@ -1298,7 +1307,6 @@ function RecommendedTeamCard() {
|
|
|
1298
1307
|
...(tsProvider && { provider: tsProvider, model: tsModel }),
|
|
1299
1308
|
reasoningEffort: tsReasoning,
|
|
1300
1309
|
...(showTemp && { temperature: tsTemp }),
|
|
1301
|
-
mode: tsMode,
|
|
1302
1310
|
},
|
|
1303
1311
|
});
|
|
1304
1312
|
try {
|
|
@@ -1376,41 +1384,6 @@ function RecommendedTeamCard() {
|
|
|
1376
1384
|
formatValue={(v) => v.toFixed(2)}
|
|
1377
1385
|
/>
|
|
1378
1386
|
)}
|
|
1379
|
-
{/* Build Mode */}
|
|
1380
|
-
<div className="space-y-1">
|
|
1381
|
-
<label className="text-2xs text-text-3 font-sans">Build Mode</label>
|
|
1382
|
-
<div className="flex rounded-md bg-surface-4 border border-border-subtle p-0.5">
|
|
1383
|
-
<button
|
|
1384
|
-
onClick={() => setTsMode('sandbox')}
|
|
1385
|
-
className={cn(
|
|
1386
|
-
'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
|
|
1387
|
-
tsMode === 'sandbox'
|
|
1388
|
-
? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
|
|
1389
|
-
: 'text-text-3 hover:text-text-1',
|
|
1390
|
-
)}
|
|
1391
|
-
>
|
|
1392
|
-
<Box size={11} />
|
|
1393
|
-
Sandbox
|
|
1394
|
-
</button>
|
|
1395
|
-
<button
|
|
1396
|
-
onClick={() => setTsMode('production')}
|
|
1397
|
-
className={cn(
|
|
1398
|
-
'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
|
|
1399
|
-
tsMode === 'production'
|
|
1400
|
-
? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
|
|
1401
|
-
: 'text-text-3 hover:text-text-1',
|
|
1402
|
-
)}
|
|
1403
|
-
>
|
|
1404
|
-
<HardDrive size={11} />
|
|
1405
|
-
Production
|
|
1406
|
-
</button>
|
|
1407
|
-
</div>
|
|
1408
|
-
<p className="text-2xs text-text-4 font-sans">
|
|
1409
|
-
{tsMode === 'sandbox'
|
|
1410
|
-
? 'Files live in a team directory, removable with the team'
|
|
1411
|
-
: 'Files live in the project directory, persist forever'}
|
|
1412
|
-
</p>
|
|
1413
|
-
</div>
|
|
1414
1387
|
</div>
|
|
1415
1388
|
)}
|
|
1416
1389
|
</div>
|
|
@@ -1419,31 +1392,116 @@ function RecommendedTeamCard() {
|
|
|
1419
1392
|
{agentEdits.map((a, i) => {
|
|
1420
1393
|
const Icon = ROLE_ICONS[a.role] || Code2;
|
|
1421
1394
|
const nameValid = !a.name || NAME_RE.test(a.name);
|
|
1395
|
+
const isExpanded = expandedAgent === i;
|
|
1396
|
+
const agentProvider = providers.find((p) => p.id === (a.provider || tsProvider));
|
|
1397
|
+
const agentModels = (agentProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1422
1398
|
return (
|
|
1423
|
-
<div key={i} className="
|
|
1424
|
-
<
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1399
|
+
<div key={i} className="rounded-md bg-surface-4 border border-border-subtle overflow-hidden">
|
|
1400
|
+
<div
|
|
1401
|
+
className="flex items-center gap-2 px-2.5 py-1.5 cursor-pointer hover:bg-surface-5/50 transition-colors"
|
|
1402
|
+
onClick={() => setExpandedAgent(isExpanded ? null : i)}
|
|
1403
|
+
>
|
|
1404
|
+
<Icon size={12} className="text-text-2 shrink-0" />
|
|
1405
|
+
<input
|
|
1406
|
+
type="text"
|
|
1407
|
+
value={a.name}
|
|
1408
|
+
onChange={(e) => handleNameChange(i, e.target.value)}
|
|
1409
|
+
onClick={(e) => e.stopPropagation()}
|
|
1410
|
+
placeholder={a.role}
|
|
1411
|
+
className={cn(
|
|
1412
|
+
'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
|
|
1413
|
+
!nameValid && 'text-red-400',
|
|
1414
|
+
)}
|
|
1415
|
+
maxLength={64}
|
|
1416
|
+
spellCheck={false}
|
|
1417
|
+
/>
|
|
1418
|
+
{a.provider && a.provider !== tsProvider && (
|
|
1419
|
+
<span className="text-2xs text-accent font-mono shrink-0">{a.provider}</span>
|
|
1433
1420
|
)}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1421
|
+
{a.scope?.length > 0 && (
|
|
1422
|
+
<span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
|
|
1423
|
+
{a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
|
|
1424
|
+
</span>
|
|
1425
|
+
)}
|
|
1426
|
+
<ChevronDown size={10} className={cn('text-text-4 shrink-0 transition-transform duration-200', !isExpanded && '-rotate-90')} />
|
|
1427
|
+
</div>
|
|
1428
|
+
{isExpanded && (
|
|
1429
|
+
<div className="px-2.5 pb-2.5 pt-1 space-y-2.5 border-t border-border-subtle">
|
|
1430
|
+
<div className="flex gap-2">
|
|
1431
|
+
<div className="flex-1 space-y-1">
|
|
1432
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Cpu size={10} />Provider</label>
|
|
1433
|
+
<Select value={a.provider || ''} onValueChange={(id) => {
|
|
1434
|
+
const p = providers.find((x) => x.id === id);
|
|
1435
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1436
|
+
handleAgentField(i, { provider: id, model: pModels[0]?.id || '' });
|
|
1437
|
+
}}>
|
|
1438
|
+
<SelectTrigger placeholder="Team default" className="bg-surface-3 h-7 text-xs" />
|
|
1439
|
+
<SelectContent>
|
|
1440
|
+
<SelectItem value="">Team default</SelectItem>
|
|
1441
|
+
{providers.map((p) => (
|
|
1442
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
1443
|
+
))}
|
|
1444
|
+
</SelectContent>
|
|
1445
|
+
</Select>
|
|
1446
|
+
</div>
|
|
1447
|
+
<div className="flex-1 space-y-1">
|
|
1448
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
1449
|
+
<Select value={a.model || ''} onValueChange={(v) => handleAgentField(i, 'model', v)}>
|
|
1450
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
|
|
1451
|
+
<SelectContent>
|
|
1452
|
+
<SelectItem value="">Auto</SelectItem>
|
|
1453
|
+
{agentModels.map((m) => (
|
|
1454
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
1455
|
+
))}
|
|
1456
|
+
</SelectContent>
|
|
1457
|
+
</Select>
|
|
1458
|
+
</div>
|
|
1459
|
+
</div>
|
|
1460
|
+
<div className="space-y-1">
|
|
1461
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Activity size={10} />Model Routing</label>
|
|
1462
|
+
<div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
|
|
1463
|
+
{[{ value: 'fixed', label: 'Fixed' }, { value: 'auto', label: 'Auto' }, { value: 'auto-floor', label: 'Auto + Floor' }].map((opt) => (
|
|
1464
|
+
<button
|
|
1465
|
+
key={opt.value}
|
|
1466
|
+
onClick={() => handleAgentField(i, 'routingMode', opt.value)}
|
|
1467
|
+
className={cn(
|
|
1468
|
+
'flex-1 px-2 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
1469
|
+
(a.routingMode || 'auto') === opt.value
|
|
1470
|
+
? 'bg-accent/15 text-accent shadow-sm'
|
|
1471
|
+
: 'text-text-3 hover:text-text-1',
|
|
1472
|
+
)}
|
|
1473
|
+
>
|
|
1474
|
+
{opt.label}
|
|
1475
|
+
</button>
|
|
1476
|
+
))}
|
|
1477
|
+
</div>
|
|
1478
|
+
</div>
|
|
1479
|
+
<div className="space-y-1">
|
|
1480
|
+
<label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Gauge size={10} />Effort Level</label>
|
|
1481
|
+
<div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
|
|
1482
|
+
{[{ value: 'min', label: 'Min' }, { value: 'low', label: 'Low' }, { value: 'default', label: 'Default' }, { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }].map((opt) => (
|
|
1483
|
+
<button
|
|
1484
|
+
key={opt.value}
|
|
1485
|
+
onClick={() => handleAgentField(i, 'effort', opt.value)}
|
|
1486
|
+
className={cn(
|
|
1487
|
+
'flex-1 px-1.5 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
1488
|
+
(a.effort || 'default') === opt.value
|
|
1489
|
+
? 'bg-accent/15 text-accent shadow-sm'
|
|
1490
|
+
: 'text-text-3 hover:text-text-1',
|
|
1491
|
+
)}
|
|
1492
|
+
>
|
|
1493
|
+
{opt.label}
|
|
1494
|
+
</button>
|
|
1495
|
+
))}
|
|
1496
|
+
</div>
|
|
1497
|
+
</div>
|
|
1498
|
+
</div>
|
|
1441
1499
|
)}
|
|
1442
1500
|
</div>
|
|
1443
1501
|
);
|
|
1444
1502
|
})}
|
|
1445
1503
|
|
|
1446
|
-
{recommendedTeam.projectDir &&
|
|
1504
|
+
{recommendedTeam.projectDir && (
|
|
1447
1505
|
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
1448
1506
|
<span className="text-text-4">Project:</span>
|
|
1449
1507
|
<span className="text-accent">{recommendedTeam.projectDir}/</span>
|
|
@@ -1569,7 +1627,7 @@ export default function AgentsView() {
|
|
|
1569
1627
|
<Tooltip content="Spawn agent" side="top">
|
|
1570
1628
|
<button
|
|
1571
1629
|
onClick={() => openDetail({ type: 'spawn' })}
|
|
1572
|
-
className="flex items-center justify-center w-
|
|
1630
|
+
className="flex items-center justify-center w-8 h-8 rounded border border-text-3/20 bg-surface-2/50 text-text-3 hover:text-accent hover:border-accent/40 hover:bg-accent/10 transition-colors cursor-pointer"
|
|
1573
1631
|
>
|
|
1574
1632
|
<Plus size={15} />
|
|
1575
1633
|
</button>
|
|
@@ -1594,7 +1652,7 @@ export default function AgentsView() {
|
|
|
1594
1652
|
saveRoleLayout(layout);
|
|
1595
1653
|
addToast('success', 'Layout saved', 'Future spawns will use these positions');
|
|
1596
1654
|
}}
|
|
1597
|
-
className="flex items-center justify-center w-
|
|
1655
|
+
className="flex items-center justify-center w-8 h-8 rounded border border-text-3/20 bg-surface-2/50 text-text-3 hover:text-accent hover:border-accent/40 hover:bg-accent/10 transition-colors cursor-pointer"
|
|
1598
1656
|
>
|
|
1599
1657
|
<LayoutGrid size={15} />
|
|
1600
1658
|
</button>
|
|
@@ -1602,7 +1660,7 @@ export default function AgentsView() {
|
|
|
1602
1660
|
<Tooltip content="Workspace" side="top">
|
|
1603
1661
|
<button
|
|
1604
1662
|
onClick={() => setWorkspaceMode(true)}
|
|
1605
|
-
className="flex items-center justify-center w-
|
|
1663
|
+
className="flex items-center justify-center w-8 h-8 rounded border border-text-3/20 bg-surface-2/50 text-text-3 hover:text-accent hover:border-accent/40 hover:bg-accent/10 transition-colors cursor-pointer"
|
|
1606
1664
|
>
|
|
1607
1665
|
<Code2 size={15} />
|
|
1608
1666
|
</button>
|
|
@@ -1628,7 +1686,7 @@ export default function AgentsView() {
|
|
|
1628
1686
|
}
|
|
1629
1687
|
}}
|
|
1630
1688
|
className={cn(
|
|
1631
|
-
'flex items-center justify-center w-
|
|
1689
|
+
'flex items-center justify-center w-8 h-8 rounded border bg-surface-2/50 transition-colors cursor-pointer',
|
|
1632
1690
|
isActive
|
|
1633
1691
|
? 'border-text-3/25 text-text-3 hover:text-info hover:border-info/40 hover:bg-info/10'
|
|
1634
1692
|
: 'border-text-4/25 text-text-4 hover:text-warning hover:border-warning/40 hover:bg-warning/10',
|
|
@@ -7,13 +7,7 @@ import { CodeEditor } from '../components/editor/code-editor';
|
|
|
7
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
|
-
import {
|
|
11
|
-
import { AiPanel } from '../components/editor/ai-panel';
|
|
12
|
-
import { QuickSearch } from '../components/editor/quick-search';
|
|
13
|
-
import { SelectionMenu } from '../components/editor/selection-menu';
|
|
14
|
-
import { InlinePrompt } from '../components/editor/inline-prompt';
|
|
15
|
-
import { DiffViewer } from '../components/agents/diff-viewer';
|
|
16
|
-
import { CodeReview } from '../components/agents/code-review';
|
|
10
|
+
import { Breadcrumbs } from '../components/editor/breadcrumbs';
|
|
17
11
|
import { Code2, Eye, FileCode, PanelLeftOpen } from 'lucide-react';
|
|
18
12
|
import { api } from '../lib/api';
|
|
19
13
|
import { cn } from '../lib/cn';
|
|
@@ -34,11 +28,6 @@ export default function EditorView() {
|
|
|
34
28
|
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
35
29
|
const sidebarWidth = useGrooveStore((s) => s.editorSidebarWidth);
|
|
36
30
|
const setSidebarWidth = useGrooveStore((s) => s.setEditorSidebarWidth);
|
|
37
|
-
const viewMode = useGrooveStore((s) => s.editorViewMode);
|
|
38
|
-
const aiPanelOpen = useGrooveStore((s) => s.editorAiPanelOpen);
|
|
39
|
-
const aiPanelWidth = useGrooveStore((s) => s.editorAiPanelWidth);
|
|
40
|
-
const setAiPanelWidth = useGrooveStore((s) => s.setEditorAiPanelWidth);
|
|
41
|
-
const setQuickSearchOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
|
|
42
31
|
|
|
43
32
|
const projectDir = useGrooveStore((s) => s.projectDir);
|
|
44
33
|
const [rootDir, setRootDir] = useState('');
|
|
@@ -47,17 +36,11 @@ export default function EditorView() {
|
|
|
47
36
|
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
|
|
48
37
|
const [showGotoLine, setShowGotoLine] = useState(false);
|
|
49
38
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
50
|
-
const [selection, setSelection] = useState(null);
|
|
51
|
-
const [inlinePrompt, setInlinePrompt] = useState(null);
|
|
52
39
|
|
|
53
40
|
const editorViewRef = useRef(null);
|
|
54
|
-
const editorContainerRef = useRef(null);
|
|
55
41
|
const dragging = useRef(false);
|
|
56
42
|
const startX = useRef(0);
|
|
57
43
|
const startW = useRef(0);
|
|
58
|
-
const aiDragging = useRef(false);
|
|
59
|
-
const aiStartX = useRef(0);
|
|
60
|
-
const aiStartW = useRef(0);
|
|
61
44
|
|
|
62
45
|
// Fetch root dir on mount and when project directory changes
|
|
63
46
|
useEffect(() => {
|
|
@@ -79,72 +62,6 @@ export default function EditorView() {
|
|
|
79
62
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
80
63
|
}, []);
|
|
81
64
|
|
|
82
|
-
// AI panel resize handler
|
|
83
|
-
const onAiPanelMouseDown = useCallback((e) => {
|
|
84
|
-
e.preventDefault();
|
|
85
|
-
aiDragging.current = true;
|
|
86
|
-
aiStartX.current = e.clientX;
|
|
87
|
-
aiStartW.current = aiPanelWidth;
|
|
88
|
-
|
|
89
|
-
function onMouseMove(e) {
|
|
90
|
-
if (!aiDragging.current) return;
|
|
91
|
-
const delta = aiStartX.current - e.clientX;
|
|
92
|
-
const newW = Math.min(Math.max(aiStartW.current + delta, 280), 600);
|
|
93
|
-
setAiPanelWidth(newW);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function onMouseUp() {
|
|
97
|
-
aiDragging.current = false;
|
|
98
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
99
|
-
document.removeEventListener('mouseup', onMouseUp);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
103
|
-
document.addEventListener('mouseup', onMouseUp);
|
|
104
|
-
}, [aiPanelWidth, setAiPanelWidth]);
|
|
105
|
-
|
|
106
|
-
// Track text selection in the code editor
|
|
107
|
-
const handleEditorMouseUp = useCallback(() => {
|
|
108
|
-
const view = editorViewRef.current;
|
|
109
|
-
if (!view) return;
|
|
110
|
-
const sel = view.state.selection.main;
|
|
111
|
-
if (sel.empty) { setSelection(null); return; }
|
|
112
|
-
const text = view.state.sliceDoc(sel.from, sel.to);
|
|
113
|
-
if (!text.trim()) { setSelection(null); return; }
|
|
114
|
-
const fromLine = view.state.doc.lineAt(sel.from);
|
|
115
|
-
const toLine = view.state.doc.lineAt(sel.to);
|
|
116
|
-
const coords = view.coordsAtPos(sel.to);
|
|
117
|
-
if (coords) {
|
|
118
|
-
setSelection({
|
|
119
|
-
x: Math.min(coords.left + 10, window.innerWidth - 220),
|
|
120
|
-
y: coords.bottom + 4,
|
|
121
|
-
lineStart: fromLine.number,
|
|
122
|
-
lineEnd: toLine.number,
|
|
123
|
-
selectedCode: text,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
// Right-click on a selection opens the AI selection menu
|
|
129
|
-
const handleEditorContextMenu = useCallback((e) => {
|
|
130
|
-
const view = editorViewRef.current;
|
|
131
|
-
if (!view) return;
|
|
132
|
-
const sel = view.state.selection.main;
|
|
133
|
-
if (sel.empty) return;
|
|
134
|
-
const text = view.state.sliceDoc(sel.from, sel.to);
|
|
135
|
-
if (!text.trim()) return;
|
|
136
|
-
e.preventDefault();
|
|
137
|
-
const fromLine = view.state.doc.lineAt(sel.from);
|
|
138
|
-
const toLine = view.state.doc.lineAt(sel.to);
|
|
139
|
-
setSelection({
|
|
140
|
-
x: Math.min(e.clientX, window.innerWidth - 220),
|
|
141
|
-
y: e.clientY + 4,
|
|
142
|
-
lineStart: fromLine.number,
|
|
143
|
-
lineEnd: toLine.number,
|
|
144
|
-
selectedCode: text,
|
|
145
|
-
});
|
|
146
|
-
}, []);
|
|
147
|
-
|
|
148
65
|
// Sidebar resize handlers
|
|
149
66
|
const onSidebarMouseDown = useCallback((e) => {
|
|
150
67
|
e.preventDefault();
|
|
@@ -184,92 +101,6 @@ export default function EditorView() {
|
|
|
184
101
|
const isMedia = activeFile && isMediaFile(activeFile);
|
|
185
102
|
const isHtml = activeFile && isHtmlFile(activeFile);
|
|
186
103
|
|
|
187
|
-
const renderCodeContent = () => {
|
|
188
|
-
if (!activeFile) {
|
|
189
|
-
return (
|
|
190
|
-
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans">
|
|
191
|
-
<div className="text-center space-y-2">
|
|
192
|
-
<Code2 size={32} className="mx-auto" />
|
|
193
|
-
<p className="text-sm">Open a file from the tree</p>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (isMedia) return <MediaViewer path={activeFile} />;
|
|
200
|
-
|
|
201
|
-
if (isHtml) {
|
|
202
|
-
return (
|
|
203
|
-
<>
|
|
204
|
-
<div className="absolute top-0 right-4 z-10 flex items-center gap-0.5 mt-2 bg-surface-2 border border-border-subtle rounded-md p-0.5">
|
|
205
|
-
<button
|
|
206
|
-
onClick={() => setPreviewMode(false)}
|
|
207
|
-
className={cn(
|
|
208
|
-
'flex items-center gap-1.5 px-2.5 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
209
|
-
!previewMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
210
|
-
)}
|
|
211
|
-
>
|
|
212
|
-
<FileCode size={12} /> Code
|
|
213
|
-
</button>
|
|
214
|
-
<button
|
|
215
|
-
onClick={() => { setPreviewMode(true); setPreviewKey((k) => k + 1); }}
|
|
216
|
-
className={cn(
|
|
217
|
-
'flex items-center gap-1.5 px-2.5 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
218
|
-
previewMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
219
|
-
)}
|
|
220
|
-
>
|
|
221
|
-
<Eye size={12} /> Preview
|
|
222
|
-
</button>
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
{previewMode ? (
|
|
226
|
-
<iframe
|
|
227
|
-
key={previewKey}
|
|
228
|
-
src={`/api/files/raw?path=${encodeURIComponent(activeFile)}`}
|
|
229
|
-
className="w-full h-full border-0"
|
|
230
|
-
sandbox="allow-scripts allow-same-origin"
|
|
231
|
-
title="HTML Preview"
|
|
232
|
-
/>
|
|
233
|
-
) : (
|
|
234
|
-
file && (
|
|
235
|
-
<div className="w-full h-full" onMouseUp={handleEditorMouseUp} onContextMenu={handleEditorContextMenu}>
|
|
236
|
-
<CodeEditor
|
|
237
|
-
content={file.content}
|
|
238
|
-
language={file.language}
|
|
239
|
-
filePath={activeFile}
|
|
240
|
-
onChange={(content) => updateFileContent(activeFile, content)}
|
|
241
|
-
onSave={() => saveFile(activeFile)}
|
|
242
|
-
onCursorChange={setCursorPos}
|
|
243
|
-
onCmdK={({ line, coords }) => setInlinePrompt({ line, coords })}
|
|
244
|
-
viewRef={editorViewRef}
|
|
245
|
-
/>
|
|
246
|
-
</div>
|
|
247
|
-
)
|
|
248
|
-
)}
|
|
249
|
-
</>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (file) {
|
|
254
|
-
return (
|
|
255
|
-
<div className="w-full h-full" onMouseUp={handleEditorMouseUp} onContextMenu={handleEditorContextMenu}>
|
|
256
|
-
<CodeEditor
|
|
257
|
-
content={file.content}
|
|
258
|
-
language={file.language}
|
|
259
|
-
filePath={activeFile}
|
|
260
|
-
onChange={(content) => updateFileContent(activeFile, content)}
|
|
261
|
-
onSave={() => saveFile(activeFile)}
|
|
262
|
-
onCursorChange={setCursorPos}
|
|
263
|
-
onCmdK={({ line, coords }) => setInlinePrompt({ line, coords })}
|
|
264
|
-
viewRef={editorViewRef}
|
|
265
|
-
/>
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return null;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
104
|
return (
|
|
274
105
|
<div className="flex h-full">
|
|
275
106
|
{/* File tree sidebar */}
|
|
@@ -303,14 +134,11 @@ export default function EditorView() {
|
|
|
303
134
|
{/* Tab bar */}
|
|
304
135
|
<EditorTabs />
|
|
305
136
|
|
|
306
|
-
{/*
|
|
307
|
-
<
|
|
308
|
-
onCmdP={() => setQuickSearchOpen(true)}
|
|
309
|
-
onCmdK={() => setInlinePrompt({ line: cursorPos.line, coords: null })}
|
|
310
|
-
/>
|
|
137
|
+
{/* Breadcrumbs */}
|
|
138
|
+
{activeFile && !isMedia && <Breadcrumbs path={activeFile} />}
|
|
311
139
|
|
|
312
140
|
{/* Content */}
|
|
313
|
-
<div className="flex-1 relative min-h-0"
|
|
141
|
+
<div className="flex-1 relative min-h-0">
|
|
314
142
|
{/* Go to line dialog */}
|
|
315
143
|
{showGotoLine && (
|
|
316
144
|
<GotoLine
|
|
@@ -320,62 +148,82 @@ export default function EditorView() {
|
|
|
320
148
|
/>
|
|
321
149
|
)}
|
|
322
150
|
|
|
323
|
-
{/*
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
Open a file to see diff
|
|
151
|
+
{/* Editor / Media / Empty */}
|
|
152
|
+
{!activeFile && (
|
|
153
|
+
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans">
|
|
154
|
+
<div className="text-center space-y-2">
|
|
155
|
+
<Code2 size={32} className="mx-auto" />
|
|
156
|
+
<p className="text-sm">Open a file from the tree</p>
|
|
157
|
+
</div>
|
|
331
158
|
</div>
|
|
332
159
|
)}
|
|
333
|
-
{viewMode === 'review' && <CodeReview />}
|
|
334
160
|
|
|
335
|
-
{
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
161
|
+
{activeFile && isMedia && <MediaViewer path={activeFile} />}
|
|
162
|
+
|
|
163
|
+
{activeFile && !isMedia && isHtml && (
|
|
164
|
+
<>
|
|
165
|
+
{/* Code / Preview toggle for HTML files */}
|
|
166
|
+
<div className="absolute top-0 right-4 z-10 flex items-center gap-0.5 mt-2 bg-surface-2 border border-border-subtle rounded-md p-0.5">
|
|
167
|
+
<button
|
|
168
|
+
onClick={() => setPreviewMode(false)}
|
|
169
|
+
className={cn(
|
|
170
|
+
'flex items-center gap-1.5 px-2.5 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
171
|
+
!previewMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
<FileCode size={12} /> Code
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
onClick={() => { setPreviewMode(true); setPreviewKey((k) => k + 1); }}
|
|
178
|
+
className={cn(
|
|
179
|
+
'flex items-center gap-1.5 px-2.5 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
180
|
+
previewMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<Eye size={12} /> Preview
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{previewMode ? (
|
|
188
|
+
<iframe
|
|
189
|
+
key={previewKey}
|
|
190
|
+
src={`/api/files/raw?path=${encodeURIComponent(activeFile)}`}
|
|
191
|
+
className="w-full h-full border-0"
|
|
192
|
+
sandbox="allow-scripts allow-same-origin"
|
|
193
|
+
title="HTML Preview"
|
|
194
|
+
/>
|
|
195
|
+
) : (
|
|
196
|
+
file && (
|
|
197
|
+
<CodeEditor
|
|
198
|
+
content={file.content}
|
|
199
|
+
language={file.language}
|
|
200
|
+
onChange={(content) => updateFileContent(activeFile, content)}
|
|
201
|
+
onSave={() => saveFile(activeFile)}
|
|
202
|
+
onCursorChange={setCursorPos}
|
|
203
|
+
viewRef={editorViewRef}
|
|
204
|
+
/>
|
|
205
|
+
)
|
|
206
|
+
)}
|
|
207
|
+
</>
|
|
346
208
|
)}
|
|
347
209
|
|
|
348
|
-
{
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
210
|
+
{activeFile && !isMedia && !isHtml && file && (
|
|
211
|
+
<CodeEditor
|
|
212
|
+
content={file.content}
|
|
213
|
+
language={file.language}
|
|
214
|
+
onChange={(content) => updateFileContent(activeFile, content)}
|
|
215
|
+
onSave={() => saveFile(activeFile)}
|
|
216
|
+
onCursorChange={setCursorPos}
|
|
217
|
+
viewRef={editorViewRef}
|
|
355
218
|
/>
|
|
356
219
|
)}
|
|
357
220
|
</div>
|
|
358
221
|
|
|
359
222
|
{/* Status bar */}
|
|
360
|
-
{activeFile && !isMedia &&
|
|
223
|
+
{activeFile && !isMedia && (
|
|
361
224
|
<EditorStatusBar cursorPos={cursorPos} language={file?.language} />
|
|
362
225
|
)}
|
|
363
226
|
</div>
|
|
364
|
-
|
|
365
|
-
{/* AI Panel */}
|
|
366
|
-
{aiPanelOpen && (
|
|
367
|
-
<div className="relative flex-shrink-0" style={{ width: aiPanelWidth }}>
|
|
368
|
-
{/* Resize handle */}
|
|
369
|
-
<div
|
|
370
|
-
className="absolute top-0 left-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
371
|
-
onMouseDown={onAiPanelMouseDown}
|
|
372
|
-
/>
|
|
373
|
-
<AiPanel />
|
|
374
|
-
</div>
|
|
375
|
-
)}
|
|
376
|
-
|
|
377
|
-
{/* Quick Search modal */}
|
|
378
|
-
<QuickSearch />
|
|
379
227
|
</div>
|
|
380
228
|
);
|
|
381
229
|
}
|