groove-dev 0.27.89 → 0.27.92
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/moe-training/client/parsers/claude-code.js +0 -2
- package/moe-training/client/session-attestation.js +2 -1
- package/moe-training/client/trajectory-capture.js +6 -0
- package/moe-training/test/client/parsers/claude-code.test.js +2 -2
- 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/api.js +244 -12
- package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
- package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
- package/node_modules/@groove-dev/daemon/src/process.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +9 -1
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -5
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +15 -2
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +10 -3
- package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -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/components/agents/agent-chat.jsx +26 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +16 -8
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +244 -12
- package/packages/daemon/src/conversations.js +32 -6
- package/packages/daemon/src/introducer.js +42 -0
- package/packages/daemon/src/process.js +5 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +9 -1
- package/packages/daemon/src/providers/codex.js +34 -5
- package/packages/daemon/src/providers/gemini.js +15 -2
- package/packages/daemon/src/providers/grok.js +10 -3
- package/packages/daemon/src/providers/local.js +8 -1
- package/packages/daemon/src/tunnel-manager.js +74 -5
- package/packages/daemon/src/validate.js +22 -1
- package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/packages/gui/src/components/chat/chat-header.jsx +3 -30
- package/packages/gui/src/components/chat/chat-input.jsx +163 -153
- package/packages/gui/src/components/chat/chat-view.jsx +16 -8
- package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
- package/packages/gui/src/components/editor/code-editor.jsx +29 -23
- package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
- package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/packages/gui/src/components/ui/slider.jsx +50 -0
- package/packages/gui/src/stores/groove.js +145 -9
- package/packages/gui/src/views/agents.jsx +707 -14
- package/workspace.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/packages/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
|
@@ -10,10 +10,15 @@ 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, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Radio, Eye } from 'lucide-react';
|
|
13
|
+
import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Radio, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Play, Clock, ListChecks, Layers } 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';
|
|
17
|
+
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
18
|
+
import { Select, SelectTrigger, SelectContent, SelectItem } from '../components/ui/select';
|
|
19
|
+
import { ScrollArea } from '../components/ui/scroll-area';
|
|
20
|
+
import { Tooltip } from '../components/ui/tooltip';
|
|
21
|
+
import { TuningSlider } from '../components/ui/slider';
|
|
17
22
|
|
|
18
23
|
const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
|
|
19
24
|
const NODE_W = 220;
|
|
@@ -582,9 +587,579 @@ function AgentTreeInner() {
|
|
|
582
587
|
);
|
|
583
588
|
}
|
|
584
589
|
|
|
590
|
+
/* ── Provider Config Helpers ──────────────────────────────── */
|
|
591
|
+
|
|
592
|
+
const PROVIDER_TEMP_SUPPORT = new Set(['codex', 'grok', 'local']);
|
|
593
|
+
const PROVIDER_VERBOSITY_SUPPORT = new Set(['codex']);
|
|
594
|
+
|
|
595
|
+
/* ── Planner Config Dialog ───────────────────────────────── */
|
|
596
|
+
|
|
597
|
+
function PlannerConfigDialog({ open, onOpenChange, onLaunch }) {
|
|
598
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
599
|
+
const [providers, setProviders] = useState([]);
|
|
600
|
+
const [provider, setProvider] = useState('');
|
|
601
|
+
const [model, setModel] = useState('');
|
|
602
|
+
const [reasoningEffort, setReasoningEffort] = useState(50);
|
|
603
|
+
const [temperature, setTemperature] = useState(0.5);
|
|
604
|
+
const [verbosity, setVerbosity] = useState(50);
|
|
605
|
+
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
if (!open) return;
|
|
608
|
+
fetchProviders().then((list) => {
|
|
609
|
+
if (!Array.isArray(list)) return;
|
|
610
|
+
const installed = list.filter((p) => p.installed);
|
|
611
|
+
setProviders(installed);
|
|
612
|
+
if (!provider && installed.length > 0) {
|
|
613
|
+
const def = installed.find((p) => p.isDefault) || installed[0];
|
|
614
|
+
setProvider(def.id);
|
|
615
|
+
const models = def.models?.filter((m) => m.type !== 'image') || [];
|
|
616
|
+
if (models.length > 0) setModel(models[0].id);
|
|
617
|
+
}
|
|
618
|
+
}).catch(() => {});
|
|
619
|
+
}, [open]);
|
|
620
|
+
|
|
621
|
+
const selectedProvider = providers.find((p) => p.id === provider);
|
|
622
|
+
const models = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
623
|
+
const showTemp = PROVIDER_TEMP_SUPPORT.has(provider);
|
|
624
|
+
const showVerbosity = PROVIDER_VERBOSITY_SUPPORT.has(provider);
|
|
625
|
+
|
|
626
|
+
function handleProviderChange(id) {
|
|
627
|
+
setProvider(id);
|
|
628
|
+
const p = providers.find((x) => x.id === id);
|
|
629
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
630
|
+
setModel(pModels[0]?.id || '');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function handleLaunch() {
|
|
634
|
+
const config = {
|
|
635
|
+
provider, model, reasoningEffort,
|
|
636
|
+
...(showTemp && { temperature }),
|
|
637
|
+
...(showVerbosity && { verbosity }),
|
|
638
|
+
};
|
|
639
|
+
useGrooveStore.setState({ teamLaunchConfig: config });
|
|
640
|
+
onLaunch(config);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
645
|
+
<DialogContent title="Configure Planner" description="Set provider, model, and tuning before launching the planner">
|
|
646
|
+
<div className="px-5 py-4 space-y-4">
|
|
647
|
+
<div className="space-y-1.5">
|
|
648
|
+
<label className="text-xs font-semibold text-text-2 font-sans">Provider</label>
|
|
649
|
+
<Select value={provider} onValueChange={handleProviderChange}>
|
|
650
|
+
<SelectTrigger placeholder="Select provider" className="bg-surface-3" />
|
|
651
|
+
<SelectContent>
|
|
652
|
+
{providers.map((p) => (
|
|
653
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
654
|
+
))}
|
|
655
|
+
</SelectContent>
|
|
656
|
+
</Select>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<div className="space-y-1.5">
|
|
660
|
+
<label className="text-xs font-semibold text-text-2 font-sans">Model</label>
|
|
661
|
+
<Select value={model} onValueChange={setModel}>
|
|
662
|
+
<SelectTrigger placeholder="Select model" className="bg-surface-3" />
|
|
663
|
+
<SelectContent>
|
|
664
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
665
|
+
{models.map((m) => (
|
|
666
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
667
|
+
))}
|
|
668
|
+
</SelectContent>
|
|
669
|
+
</Select>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
<div className="space-y-1 pt-1">
|
|
673
|
+
<TuningSlider
|
|
674
|
+
label="Reasoning Effort"
|
|
675
|
+
value={reasoningEffort}
|
|
676
|
+
onChange={setReasoningEffort}
|
|
677
|
+
min={0} max={100} step={1}
|
|
678
|
+
/>
|
|
679
|
+
{showTemp && (
|
|
680
|
+
<TuningSlider
|
|
681
|
+
label="Temperature"
|
|
682
|
+
value={temperature}
|
|
683
|
+
onChange={setTemperature}
|
|
684
|
+
min={0} max={1} step={0.01}
|
|
685
|
+
formatValue={(v) => v.toFixed(2)}
|
|
686
|
+
/>
|
|
687
|
+
)}
|
|
688
|
+
{showVerbosity && (
|
|
689
|
+
<TuningSlider
|
|
690
|
+
label="Verbosity"
|
|
691
|
+
value={verbosity}
|
|
692
|
+
onChange={setVerbosity}
|
|
693
|
+
min={0} max={100} step={1}
|
|
694
|
+
/>
|
|
695
|
+
)}
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
<div className="px-5 py-4 border-t border-border-subtle">
|
|
700
|
+
<Button variant="primary" size="md" onClick={handleLaunch} className="w-full gap-2">
|
|
701
|
+
<Zap size={14} />
|
|
702
|
+
Launch Planner
|
|
703
|
+
</Button>
|
|
704
|
+
</div>
|
|
705
|
+
</DialogContent>
|
|
706
|
+
</Dialog>
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* ── Team Builder ────────────────────────────────────────────── */
|
|
711
|
+
|
|
712
|
+
const TB_ROLE_ICONS = {
|
|
713
|
+
chat: MessageCircle, planner: Rocket, backend: Server, frontend: Monitor,
|
|
714
|
+
fullstack: Code2, testing: TestTube, devops: Cloud, docs: FileText,
|
|
715
|
+
security: Shield, database: Database, cmo: Megaphone, cfo: Calculator,
|
|
716
|
+
ea: UserCheck, support: Headphones, analyst: BarChart3, creative: Pen,
|
|
717
|
+
slides: Presentation, ambassador: Globe,
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const TB_ROLES = [
|
|
721
|
+
{ id: 'planner', label: 'Planner', desc: 'Analyzes tasks and designs team plans' },
|
|
722
|
+
{ id: 'frontend', label: 'Frontend', desc: 'React, UI components, views, styling' },
|
|
723
|
+
{ id: 'backend', label: 'Backend', desc: 'APIs, server logic, database, services' },
|
|
724
|
+
{ id: 'fullstack', label: 'Fullstack', desc: 'Cross-stack work, QC, integration testing' },
|
|
725
|
+
{ id: 'testing', label: 'Testing', desc: 'Test suites, coverage, quality assurance' },
|
|
726
|
+
{ id: 'devops', label: 'DevOps', desc: 'CI/CD, deployment, infrastructure' },
|
|
727
|
+
{ id: 'security', label: 'Security', desc: 'Security audits, vulnerability analysis' },
|
|
728
|
+
{ id: 'database', label: 'Database', desc: 'Schema design, queries, migrations' },
|
|
729
|
+
{ id: 'docs', label: 'Docs', desc: 'Documentation, guides, API docs' },
|
|
730
|
+
{ id: 'cmo', label: 'CMO', desc: 'Marketing strategy, campaigns, content' },
|
|
731
|
+
{ id: 'cfo', label: 'CFO', desc: 'Financial analysis, budgeting, forecasting' },
|
|
732
|
+
{ id: 'ea', label: 'EA', desc: 'Executive assistance, coordination, briefings' },
|
|
733
|
+
{ id: 'support', label: 'Support', desc: 'Customer support, issue triage' },
|
|
734
|
+
{ id: 'analyst', label: 'Analyst', desc: 'Data analysis, research, reporting' },
|
|
735
|
+
{ id: 'creative', label: 'Writer', desc: 'Design, copywriting, visual assets' },
|
|
736
|
+
{ id: 'slides', label: 'Slides', desc: 'Presentations, decks, pitch materials' },
|
|
737
|
+
];
|
|
738
|
+
|
|
739
|
+
const BUILT_IN_TEMPLATES = [
|
|
740
|
+
{ name: 'Dev Team', icon: Code2, roles: ['frontend', 'backend', 'testing'], desc: '3 agents' },
|
|
741
|
+
{ name: 'Full Stack', icon: Layers, roles: ['frontend', 'backend', 'fullstack', 'testing', 'devops'], desc: '5 agents' },
|
|
742
|
+
{ name: 'Marketing', icon: Megaphone, roles: ['cmo', 'creative', 'analyst'], desc: '3 agents' },
|
|
743
|
+
{ name: 'Business', icon: BarChart3, roles: ['cfo', 'analyst', 'ea'], desc: '3 agents' },
|
|
744
|
+
{ name: 'Security Audit', icon: Shield, roles: ['security', 'testing', 'devops'], desc: '3 agents' },
|
|
745
|
+
{ name: 'Docs', icon: FileText, roles: ['docs', 'frontend', 'analyst'], desc: '3 agents' },
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
function TeamBuilder() {
|
|
749
|
+
const open = useGrooveStore((s) => s.teamBuilderOpen);
|
|
750
|
+
const roles = useGrooveStore((s) => s.teamBuilderRoles);
|
|
751
|
+
const settings = useGrooveStore((s) => s.teamBuilderSettings);
|
|
752
|
+
const task = useGrooveStore((s) => s.teamBuilderTask);
|
|
753
|
+
const launchMode = useGrooveStore((s) => s.teamBuilderLaunchMode);
|
|
754
|
+
const templates = useGrooveStore((s) => s.teamTemplates);
|
|
755
|
+
const closeTeamBuilder = useGrooveStore((s) => s.closeTeamBuilder);
|
|
756
|
+
const addRole = useGrooveStore((s) => s.addTeamBuilderRole);
|
|
757
|
+
const removeRole = useGrooveStore((s) => s.removeTeamBuilderRole);
|
|
758
|
+
const updateRole = useGrooveStore((s) => s.updateTeamBuilderRole);
|
|
759
|
+
const applyTemplate = useGrooveStore((s) => s.applyTemplate);
|
|
760
|
+
const setSettings = useGrooveStore((s) => s.setTeamBuilderSettings);
|
|
761
|
+
const setTask = useGrooveStore((s) => s.setTeamBuilderTask);
|
|
762
|
+
const setLaunchMode = useGrooveStore((s) => s.setTeamBuilderLaunchMode);
|
|
763
|
+
const launchTeamBuilder = useGrooveStore((s) => s.launchTeamBuilder);
|
|
764
|
+
const saveTeamTemplate = useGrooveStore((s) => s.saveTeamTemplate);
|
|
765
|
+
const fetchTeamTemplates = useGrooveStore((s) => s.fetchTeamTemplates);
|
|
766
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
767
|
+
|
|
768
|
+
const [providers, setProviders] = useState([]);
|
|
769
|
+
const [search, setSearch] = useState('');
|
|
770
|
+
const [expandedIdx, setExpandedIdx] = useState(null);
|
|
771
|
+
const [launching, setLaunching] = useState(false);
|
|
772
|
+
const [activeTemplate, setActiveTemplate] = useState(null);
|
|
773
|
+
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
|
774
|
+
const [templateName, setTemplateName] = useState('');
|
|
775
|
+
|
|
776
|
+
useEffect(() => {
|
|
777
|
+
if (!open) return;
|
|
778
|
+
fetchProviders().then((list) => {
|
|
779
|
+
if (Array.isArray(list)) setProviders(list.filter((p) => p.installed));
|
|
780
|
+
}).catch(() => {});
|
|
781
|
+
fetchTeamTemplates();
|
|
782
|
+
}, [open]);
|
|
783
|
+
|
|
784
|
+
if (!open) return null;
|
|
785
|
+
|
|
786
|
+
const filteredRoles = search
|
|
787
|
+
? TB_ROLES.filter((r) => r.label.toLowerCase().includes(search.toLowerCase()) || r.desc.toLowerCase().includes(search.toLowerCase()))
|
|
788
|
+
: TB_ROLES;
|
|
789
|
+
|
|
790
|
+
const selectedProvider = providers.find((p) => p.id === settings.provider);
|
|
791
|
+
const settingsModels = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
792
|
+
const showTemp = PROVIDER_TEMP_SUPPORT.has(settings.provider);
|
|
793
|
+
|
|
794
|
+
function handleSettingsProviderChange(id) {
|
|
795
|
+
setSettings({ provider: id });
|
|
796
|
+
const p = providers.find((x) => x.id === id);
|
|
797
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
798
|
+
setSettings({ provider: id, model: pModels[0]?.id || '' });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function handleApplyTemplate(tmpl) {
|
|
802
|
+
applyTemplate(tmpl);
|
|
803
|
+
setActiveTemplate(tmpl.name);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function handleLaunch() {
|
|
807
|
+
setLaunching(true);
|
|
808
|
+
try {
|
|
809
|
+
await launchTeamBuilder();
|
|
810
|
+
} catch { /* toast handles */ }
|
|
811
|
+
setLaunching(false);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function handleSaveTemplate() {
|
|
815
|
+
const name = templateName.trim();
|
|
816
|
+
if (!name) return;
|
|
817
|
+
saveTeamTemplate(name);
|
|
818
|
+
setSaveDialogOpen(false);
|
|
819
|
+
setTemplateName('');
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const allTemplates = [...BUILT_IN_TEMPLATES, ...(templates.custom || []).map((t) => ({
|
|
823
|
+
...t, icon: Layers, desc: `${t.roles?.length || 0} agents`, custom: true,
|
|
824
|
+
}))];
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
|
828
|
+
<div className="w-full max-w-5xl max-h-[90vh] bg-surface-1 border border-border rounded-xl shadow-2xl flex flex-col overflow-hidden">
|
|
829
|
+
{/* Top Bar */}
|
|
830
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border-subtle">
|
|
831
|
+
<div className="flex items-center gap-3">
|
|
832
|
+
<div className="w-8 h-8 rounded-lg bg-purple/15 flex items-center justify-center">
|
|
833
|
+
<Users size={16} className="text-purple" />
|
|
834
|
+
</div>
|
|
835
|
+
<h2 className="text-lg font-bold text-text-0 font-sans">Team Builder</h2>
|
|
836
|
+
</div>
|
|
837
|
+
<button onClick={closeTeamBuilder} className="p-2 rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer">
|
|
838
|
+
<X size={18} />
|
|
839
|
+
</button>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
{/* Templates Row */}
|
|
843
|
+
<div className="px-6 py-3 border-b border-border-subtle">
|
|
844
|
+
<div className="flex items-center gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: 'none' }}>
|
|
845
|
+
{allTemplates.map((tmpl) => {
|
|
846
|
+
const TIcon = tmpl.icon || Layers;
|
|
847
|
+
const isActive = activeTemplate === tmpl.name;
|
|
848
|
+
return (
|
|
849
|
+
<button
|
|
850
|
+
key={tmpl.name}
|
|
851
|
+
onClick={() => handleApplyTemplate(tmpl)}
|
|
852
|
+
className={cn(
|
|
853
|
+
'flex flex-col items-center gap-1.5 px-4 py-2.5 rounded-lg border text-center transition-all cursor-pointer flex-shrink-0 min-w-[100px]',
|
|
854
|
+
isActive
|
|
855
|
+
? 'border-accent bg-accent/5'
|
|
856
|
+
: 'border-border-subtle bg-surface-3 hover:border-accent/30 hover:bg-surface-4',
|
|
857
|
+
)}
|
|
858
|
+
>
|
|
859
|
+
<TIcon size={16} className={isActive ? 'text-accent' : 'text-text-2'} />
|
|
860
|
+
<span className="text-2xs font-semibold text-text-0 font-sans">{tmpl.name}</span>
|
|
861
|
+
<span className="text-2xs text-text-4 font-sans">{tmpl.desc}</span>
|
|
862
|
+
</button>
|
|
863
|
+
);
|
|
864
|
+
})}
|
|
865
|
+
<Tooltip content="Save current roster as template">
|
|
866
|
+
<button
|
|
867
|
+
onClick={() => { setSaveDialogOpen(true); setTemplateName(''); }}
|
|
868
|
+
disabled={roles.length === 0}
|
|
869
|
+
className="flex flex-col items-center gap-1.5 px-4 py-2.5 rounded-lg border border-dashed border-border-subtle bg-surface-2 hover:border-accent/30 transition-all cursor-pointer flex-shrink-0 min-w-[100px] disabled:opacity-30 disabled:cursor-not-allowed"
|
|
870
|
+
>
|
|
871
|
+
<Save size={16} className="text-text-3" />
|
|
872
|
+
<span className="text-2xs font-semibold text-text-2 font-sans">Save</span>
|
|
873
|
+
<span className="text-2xs text-text-4 font-sans">Template</span>
|
|
874
|
+
</button>
|
|
875
|
+
</Tooltip>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
|
|
879
|
+
{/* Main Area */}
|
|
880
|
+
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
881
|
+
{/* Left: Available Roles */}
|
|
882
|
+
<div className="w-[40%] border-r border-border-subtle flex flex-col">
|
|
883
|
+
<div className="px-4 py-3 border-b border-border-subtle">
|
|
884
|
+
<div className="relative">
|
|
885
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
|
|
886
|
+
<input
|
|
887
|
+
type="text"
|
|
888
|
+
value={search}
|
|
889
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
890
|
+
placeholder="Filter roles..."
|
|
891
|
+
className="w-full h-8 pl-8 pr-3 text-xs bg-surface-3 border border-border-subtle rounded-md text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent font-sans"
|
|
892
|
+
/>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
<ScrollArea className="flex-1">
|
|
896
|
+
<div className="p-3 grid grid-cols-2 gap-2">
|
|
897
|
+
{filteredRoles.map((r) => {
|
|
898
|
+
const RIcon = TB_ROLE_ICONS[r.id] || Code2;
|
|
899
|
+
return (
|
|
900
|
+
<button
|
|
901
|
+
key={r.id}
|
|
902
|
+
onClick={() => addRole(r.id)}
|
|
903
|
+
className="flex items-start gap-2.5 p-2.5 rounded-lg border border-border-subtle bg-surface-2 hover:border-accent/30 hover:bg-surface-3 transition-all cursor-pointer text-left group"
|
|
904
|
+
>
|
|
905
|
+
<div className="w-7 h-7 rounded-md bg-surface-4 flex items-center justify-center flex-shrink-0 group-hover:bg-accent/15 transition-colors">
|
|
906
|
+
<RIcon size={14} className="text-text-2 group-hover:text-accent transition-colors" />
|
|
907
|
+
</div>
|
|
908
|
+
<div className="min-w-0 flex-1">
|
|
909
|
+
<div className="flex items-center justify-between">
|
|
910
|
+
<span className="text-xs font-semibold text-text-0 font-sans">{r.label}</span>
|
|
911
|
+
<Plus size={12} className="text-text-4 group-hover:text-accent transition-colors flex-shrink-0" />
|
|
912
|
+
</div>
|
|
913
|
+
<p className="text-2xs text-text-3 font-sans leading-tight mt-0.5">{r.desc}</p>
|
|
914
|
+
</div>
|
|
915
|
+
</button>
|
|
916
|
+
);
|
|
917
|
+
})}
|
|
918
|
+
</div>
|
|
919
|
+
</ScrollArea>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
{/* Right: Your Team */}
|
|
923
|
+
<div className="flex-1 flex flex-col">
|
|
924
|
+
<div className="px-4 py-3 border-b border-border-subtle flex items-center justify-between">
|
|
925
|
+
<span className="text-xs font-semibold text-text-1 font-sans uppercase tracking-wider">Your Team ({roles.length})</span>
|
|
926
|
+
</div>
|
|
927
|
+
<ScrollArea className="flex-1">
|
|
928
|
+
<div className="p-3 space-y-1.5">
|
|
929
|
+
{roles.length === 0 ? (
|
|
930
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
931
|
+
<Users size={32} className="text-text-4 mb-3" />
|
|
932
|
+
<p className="text-sm text-text-2 font-sans">Add roles from the left or pick a template above</p>
|
|
933
|
+
</div>
|
|
934
|
+
) : roles.map((r, i) => {
|
|
935
|
+
const RIcon = TB_ROLE_ICONS[r.role] || Code2;
|
|
936
|
+
const expanded = expandedIdx === i;
|
|
937
|
+
const roleProvider = r.provider ? providers.find((p) => p.id === r.provider) : null;
|
|
938
|
+
const roleModels = (roleProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
939
|
+
return (
|
|
940
|
+
<div key={i} className="rounded-lg border border-border-subtle bg-surface-2 overflow-hidden">
|
|
941
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
942
|
+
<GripVertical size={12} className="text-text-4 flex-shrink-0 cursor-grab" />
|
|
943
|
+
<div className="w-6 h-6 rounded-md bg-surface-4 flex items-center justify-center flex-shrink-0">
|
|
944
|
+
<RIcon size={12} className="text-text-1" />
|
|
945
|
+
</div>
|
|
946
|
+
<span className="text-xs font-semibold text-text-0 font-sans flex-1">{TB_ROLES.find((x) => x.id === r.role)?.label || r.role}</span>
|
|
947
|
+
<button
|
|
948
|
+
onClick={() => setExpandedIdx(expanded ? null : i)}
|
|
949
|
+
className="p-1 rounded text-text-4 hover:text-text-1 cursor-pointer"
|
|
950
|
+
>
|
|
951
|
+
<ChevronDown size={12} className={cn('transition-transform duration-200', expanded && 'rotate-180')} />
|
|
952
|
+
</button>
|
|
953
|
+
<button
|
|
954
|
+
onClick={() => { removeRole(i); if (expandedIdx === i) setExpandedIdx(null); else if (expandedIdx > i) setExpandedIdx(expandedIdx - 1); }}
|
|
955
|
+
className="p-1 rounded text-text-4 hover:text-danger cursor-pointer"
|
|
956
|
+
>
|
|
957
|
+
<X size={12} />
|
|
958
|
+
</button>
|
|
959
|
+
</div>
|
|
960
|
+
{expanded && (
|
|
961
|
+
<div className="px-3 pb-3 pt-1 space-y-3 border-t border-border-subtle bg-surface-1">
|
|
962
|
+
<div className="space-y-1">
|
|
963
|
+
<label className="text-2xs text-text-3 font-sans">Name Override</label>
|
|
964
|
+
<input
|
|
965
|
+
type="text"
|
|
966
|
+
value={r.name}
|
|
967
|
+
onChange={(e) => updateRole(i, { name: sanitizeName(e.target.value) })}
|
|
968
|
+
placeholder={r.role}
|
|
969
|
+
className="w-full h-7 px-2.5 text-xs bg-surface-3 border border-border-subtle rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
970
|
+
maxLength={64}
|
|
971
|
+
spellCheck={false}
|
|
972
|
+
/>
|
|
973
|
+
</div>
|
|
974
|
+
<div className="flex gap-2">
|
|
975
|
+
<div className="flex-1 space-y-1">
|
|
976
|
+
<label className="text-2xs text-text-3 font-sans">Provider</label>
|
|
977
|
+
<Select value={r.provider || ''} onValueChange={(v) => {
|
|
978
|
+
updateRole(i, { provider: v || null });
|
|
979
|
+
const p = providers.find((x) => x.id === v);
|
|
980
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
981
|
+
updateRole(i, { provider: v || null, model: pModels[0]?.id || null });
|
|
982
|
+
}}>
|
|
983
|
+
<SelectTrigger placeholder="Team Default" className="bg-surface-3 h-7 text-xs" />
|
|
984
|
+
<SelectContent>
|
|
985
|
+
<SelectItem value="">Team Default</SelectItem>
|
|
986
|
+
{providers.map((p) => (
|
|
987
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
988
|
+
))}
|
|
989
|
+
</SelectContent>
|
|
990
|
+
</Select>
|
|
991
|
+
</div>
|
|
992
|
+
<div className="flex-1 space-y-1">
|
|
993
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
994
|
+
<Select value={r.model || ''} onValueChange={(v) => updateRole(i, { model: v || null })}>
|
|
995
|
+
<SelectTrigger placeholder="Default" className="bg-surface-3 h-7 text-xs" />
|
|
996
|
+
<SelectContent>
|
|
997
|
+
<SelectItem value="">Default</SelectItem>
|
|
998
|
+
{roleModels.map((m) => (
|
|
999
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
1000
|
+
))}
|
|
1001
|
+
</SelectContent>
|
|
1002
|
+
</Select>
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
<TuningSlider
|
|
1006
|
+
label="Reasoning"
|
|
1007
|
+
value={r.reasoningEffort ?? settings.reasoningEffort}
|
|
1008
|
+
onChange={(v) => updateRole(i, { reasoningEffort: v })}
|
|
1009
|
+
min={0} max={100} step={1}
|
|
1010
|
+
/>
|
|
1011
|
+
{PROVIDER_TEMP_SUPPORT.has(r.provider || settings.provider) && (
|
|
1012
|
+
<TuningSlider
|
|
1013
|
+
label="Temperature"
|
|
1014
|
+
value={r.temperature ?? settings.temperature}
|
|
1015
|
+
onChange={(v) => updateRole(i, { temperature: v })}
|
|
1016
|
+
min={0} max={1} step={0.01}
|
|
1017
|
+
formatValue={(v) => v.toFixed(2)}
|
|
1018
|
+
/>
|
|
1019
|
+
)}
|
|
1020
|
+
<div className="space-y-1">
|
|
1021
|
+
<label className="text-2xs text-text-3 font-sans">Custom Prompt</label>
|
|
1022
|
+
<textarea
|
|
1023
|
+
value={r.prompt || ''}
|
|
1024
|
+
onChange={(e) => updateRole(i, { prompt: e.target.value })}
|
|
1025
|
+
placeholder="Optional instructions for this agent..."
|
|
1026
|
+
rows={2}
|
|
1027
|
+
className="w-full px-2.5 py-1.5 text-xs bg-surface-3 border border-border-subtle rounded-md text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent resize-none"
|
|
1028
|
+
/>
|
|
1029
|
+
</div>
|
|
1030
|
+
</div>
|
|
1031
|
+
)}
|
|
1032
|
+
</div>
|
|
1033
|
+
);
|
|
1034
|
+
})}
|
|
1035
|
+
</div>
|
|
1036
|
+
</ScrollArea>
|
|
1037
|
+
</div>
|
|
1038
|
+
</div>
|
|
1039
|
+
|
|
1040
|
+
{/* Bottom Bar */}
|
|
1041
|
+
<div className="border-t border-border-subtle px-6 py-4">
|
|
1042
|
+
<div className="flex gap-4">
|
|
1043
|
+
{/* Task + Launch Mode */}
|
|
1044
|
+
<div className="flex-1 space-y-3">
|
|
1045
|
+
<textarea
|
|
1046
|
+
value={task}
|
|
1047
|
+
onChange={(e) => setTask(e.target.value)}
|
|
1048
|
+
placeholder="Describe what this team should work on..."
|
|
1049
|
+
rows={2}
|
|
1050
|
+
className="w-full px-3 py-2 text-sm bg-surface-3 border border-border-subtle rounded-lg text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent resize-none"
|
|
1051
|
+
/>
|
|
1052
|
+
<div className="flex items-center gap-2">
|
|
1053
|
+
<span className="text-2xs text-text-3 font-sans mr-1">Launch Mode:</span>
|
|
1054
|
+
{[
|
|
1055
|
+
{ id: 'direct', label: 'Direct', icon: Play, tip: 'Agents start working immediately' },
|
|
1056
|
+
{ id: 'plan', label: 'Plan First', icon: ListChecks, tip: 'Planner designs prompts, then team launches' },
|
|
1057
|
+
{ id: 'await', label: 'Await', icon: Clock, tip: 'Agents spawn idle, await instructions' },
|
|
1058
|
+
].map((m) => (
|
|
1059
|
+
<Tooltip key={m.id} content={m.tip}>
|
|
1060
|
+
<button
|
|
1061
|
+
onClick={() => setLaunchMode(m.id)}
|
|
1062
|
+
className={cn(
|
|
1063
|
+
'flex items-center gap-1.5 px-2.5 py-1 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
1064
|
+
launchMode === m.id
|
|
1065
|
+
? 'bg-accent/15 text-accent border border-accent/30'
|
|
1066
|
+
: 'bg-surface-3 text-text-3 border border-border-subtle hover:text-text-1 hover:border-border',
|
|
1067
|
+
)}
|
|
1068
|
+
>
|
|
1069
|
+
<m.icon size={11} />
|
|
1070
|
+
{m.label}
|
|
1071
|
+
</button>
|
|
1072
|
+
</Tooltip>
|
|
1073
|
+
))}
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
|
|
1077
|
+
{/* Team Settings + Launch */}
|
|
1078
|
+
<div className="w-64 flex flex-col gap-2">
|
|
1079
|
+
<div className="flex gap-2">
|
|
1080
|
+
<div className="flex-1 space-y-0.5">
|
|
1081
|
+
<label className="text-2xs text-text-3 font-sans">Provider</label>
|
|
1082
|
+
<Select value={settings.provider || ''} onValueChange={handleSettingsProviderChange}>
|
|
1083
|
+
<SelectTrigger placeholder="Default" className="bg-surface-3 h-7 text-xs" />
|
|
1084
|
+
<SelectContent>
|
|
1085
|
+
<SelectItem value="">Default</SelectItem>
|
|
1086
|
+
{providers.map((p) => (
|
|
1087
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
1088
|
+
))}
|
|
1089
|
+
</SelectContent>
|
|
1090
|
+
</Select>
|
|
1091
|
+
</div>
|
|
1092
|
+
<div className="flex-1 space-y-0.5">
|
|
1093
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
1094
|
+
<Select value={settings.model || ''} onValueChange={(v) => setSettings({ model: v })}>
|
|
1095
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
|
|
1096
|
+
<SelectContent>
|
|
1097
|
+
<SelectItem value="">Auto</SelectItem>
|
|
1098
|
+
{settingsModels.map((m) => (
|
|
1099
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
1100
|
+
))}
|
|
1101
|
+
</SelectContent>
|
|
1102
|
+
</Select>
|
|
1103
|
+
</div>
|
|
1104
|
+
</div>
|
|
1105
|
+
<TuningSlider
|
|
1106
|
+
label="Reasoning"
|
|
1107
|
+
value={settings.reasoningEffort}
|
|
1108
|
+
onChange={(v) => setSettings({ reasoningEffort: v })}
|
|
1109
|
+
min={0} max={100} step={1}
|
|
1110
|
+
/>
|
|
1111
|
+
{showTemp && (
|
|
1112
|
+
<TuningSlider
|
|
1113
|
+
label="Temperature"
|
|
1114
|
+
value={settings.temperature}
|
|
1115
|
+
onChange={(v) => setSettings({ temperature: v })}
|
|
1116
|
+
min={0} max={1} step={0.01}
|
|
1117
|
+
formatValue={(v) => v.toFixed(2)}
|
|
1118
|
+
/>
|
|
1119
|
+
)}
|
|
1120
|
+
<Button
|
|
1121
|
+
variant="primary"
|
|
1122
|
+
size="md"
|
|
1123
|
+
onClick={handleLaunch}
|
|
1124
|
+
disabled={launching || roles.length === 0}
|
|
1125
|
+
className="w-full gap-2 mt-1"
|
|
1126
|
+
>
|
|
1127
|
+
<Zap size={14} />
|
|
1128
|
+
{launching ? 'Launching...' : `Launch Team (${roles.length})`}
|
|
1129
|
+
</Button>
|
|
1130
|
+
</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</div>
|
|
1134
|
+
|
|
1135
|
+
{/* Save Template Dialog */}
|
|
1136
|
+
{saveDialogOpen && (
|
|
1137
|
+
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40">
|
|
1138
|
+
<div className="w-full max-w-sm bg-surface-2 border border-border rounded-lg shadow-2xl p-5 space-y-4">
|
|
1139
|
+
<h3 className="text-sm font-semibold text-text-0 font-sans">Save as Template</h3>
|
|
1140
|
+
<input
|
|
1141
|
+
type="text"
|
|
1142
|
+
value={templateName}
|
|
1143
|
+
onChange={(e) => setTemplateName(e.target.value)}
|
|
1144
|
+
placeholder="Template name..."
|
|
1145
|
+
className="w-full h-8 px-3 text-sm bg-surface-3 border border-border-subtle rounded-md text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
1146
|
+
autoFocus
|
|
1147
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleSaveTemplate(); if (e.key === 'Escape') setSaveDialogOpen(false); }}
|
|
1148
|
+
/>
|
|
1149
|
+
<div className="flex justify-end gap-2">
|
|
1150
|
+
<Button variant="ghost" size="sm" onClick={() => setSaveDialogOpen(false)}>Cancel</Button>
|
|
1151
|
+
<Button variant="primary" size="sm" onClick={handleSaveTemplate} disabled={!templateName.trim()}>Save</Button>
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
)}
|
|
1156
|
+
</div>
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
585
1160
|
/* ── Empty State ───────────────────────────────────────────── */
|
|
586
1161
|
|
|
587
|
-
function EmptyState({ onPlanner, onSpawn }) {
|
|
1162
|
+
function EmptyState({ onPlanner, onSpawn, onTeamBuilder }) {
|
|
588
1163
|
return (
|
|
589
1164
|
<div className="w-full h-full flex items-center justify-center">
|
|
590
1165
|
<div className="max-w-2xl w-full text-center space-y-10 px-8">
|
|
@@ -619,6 +1194,19 @@ function EmptyState({ onPlanner, onSpawn }) {
|
|
|
619
1194
|
</div>
|
|
620
1195
|
</button>
|
|
621
1196
|
|
|
1197
|
+
<button
|
|
1198
|
+
onClick={onTeamBuilder}
|
|
1199
|
+
className="w-full flex items-center gap-3 p-4 rounded-lg border border-purple/25 bg-gradient-to-r from-purple/6 to-purple/2 hover:from-purple/12 hover:to-purple/5 hover:border-purple/35 transition-all cursor-pointer group text-left"
|
|
1200
|
+
>
|
|
1201
|
+
<div className="w-10 h-10 rounded-lg bg-purple/15 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
|
|
1202
|
+
<UserPlus size={20} className="text-purple" />
|
|
1203
|
+
</div>
|
|
1204
|
+
<div className="min-w-0">
|
|
1205
|
+
<div className="text-sm font-semibold text-text-0 font-sans">Build a Team</div>
|
|
1206
|
+
<div className="text-xs text-text-3 font-sans mt-0.5">Pick your roles, configure settings, and launch</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
</button>
|
|
1209
|
+
|
|
622
1210
|
<button
|
|
623
1211
|
onClick={onSpawn}
|
|
624
1212
|
className="w-full flex items-center gap-3 p-4 rounded-lg border border-border bg-surface-1 hover:bg-surface-2 hover:border-border transition-all cursor-pointer group text-left"
|
|
@@ -687,8 +1275,24 @@ function sanitizeName(raw) {
|
|
|
687
1275
|
function RecommendedTeamCard() {
|
|
688
1276
|
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
689
1277
|
const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
|
|
1278
|
+
const teamLaunchConfig = useGrooveStore((s) => s.teamLaunchConfig);
|
|
1279
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
690
1280
|
const [launching, setLaunching] = useState(false);
|
|
691
1281
|
const [editedAgents, setEditedAgents] = useState(null);
|
|
1282
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
1283
|
+
const [providers, setProviders] = useState([]);
|
|
1284
|
+
|
|
1285
|
+
// Team settings — pre-populated from planner spawn config or defaults
|
|
1286
|
+
const [tsProvider, setTsProvider] = useState(teamLaunchConfig?.provider || '');
|
|
1287
|
+
const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
|
|
1288
|
+
const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
|
|
1289
|
+
const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
|
|
1290
|
+
|
|
1291
|
+
useEffect(() => {
|
|
1292
|
+
fetchProviders().then((list) => {
|
|
1293
|
+
if (Array.isArray(list)) setProviders(list.filter((p) => p.installed));
|
|
1294
|
+
}).catch(() => {});
|
|
1295
|
+
}, []);
|
|
692
1296
|
|
|
693
1297
|
if (!recommendedTeam?.agents?.length) return null;
|
|
694
1298
|
|
|
@@ -696,22 +1300,38 @@ function RecommendedTeamCard() {
|
|
|
696
1300
|
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
697
1301
|
const phase2 = agents.filter((a) => a.phase === 2);
|
|
698
1302
|
|
|
699
|
-
// Initialize edits lazily so we get fresh data if recommendedTeam changes
|
|
700
1303
|
const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
|
|
701
1304
|
|
|
1305
|
+
const selectedProvider = providers.find((p) => p.id === tsProvider);
|
|
1306
|
+
const tsModels = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1307
|
+
const showTemp = PROVIDER_TEMP_SUPPORT.has(tsProvider);
|
|
1308
|
+
|
|
702
1309
|
function handleNameChange(i, raw) {
|
|
703
1310
|
const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
|
|
704
1311
|
setEditedAgents(next);
|
|
705
1312
|
}
|
|
706
1313
|
|
|
1314
|
+
function handleTsProviderChange(id) {
|
|
1315
|
+
setTsProvider(id);
|
|
1316
|
+
const p = providers.find((x) => x.id === id);
|
|
1317
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1318
|
+
setTsModel(pModels[0]?.id || '');
|
|
1319
|
+
}
|
|
1320
|
+
|
|
707
1321
|
async function handleLaunch() {
|
|
708
1322
|
setLaunching(true);
|
|
1323
|
+
// Save overrides to store so launchRecommendedTeam sends them
|
|
1324
|
+
if (tsProvider) {
|
|
1325
|
+
useGrooveStore.setState({
|
|
1326
|
+
teamLaunchConfig: {
|
|
1327
|
+
provider: tsProvider, model: tsModel,
|
|
1328
|
+
reasoningEffort: tsReasoning,
|
|
1329
|
+
...(showTemp && { temperature: tsTemp }),
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
709
1333
|
try {
|
|
710
|
-
|
|
711
|
-
const modified = [
|
|
712
|
-
...agentEdits,
|
|
713
|
-
...phase2,
|
|
714
|
-
];
|
|
1334
|
+
const modified = [...agentEdits, ...phase2];
|
|
715
1335
|
await launchRecommendedTeam(modified);
|
|
716
1336
|
} catch { /* toast handles */ }
|
|
717
1337
|
setLaunching(false);
|
|
@@ -730,8 +1350,66 @@ function RecommendedTeamCard() {
|
|
|
730
1350
|
<button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
|
|
731
1351
|
</div>
|
|
732
1352
|
|
|
1353
|
+
{/* Collapsible Team Settings */}
|
|
1354
|
+
<div className="border-b border-border-subtle">
|
|
1355
|
+
<button
|
|
1356
|
+
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
1357
|
+
className="w-full flex items-center gap-2 px-4 py-2 text-left cursor-pointer hover:bg-surface-3/50 transition-colors"
|
|
1358
|
+
>
|
|
1359
|
+
<ChevronDown size={12} className={cn('text-text-4 transition-transform duration-200', !settingsOpen && '-rotate-90')} />
|
|
1360
|
+
<Settings2 size={12} className="text-text-3" />
|
|
1361
|
+
<span className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wider">Team Settings</span>
|
|
1362
|
+
{tsProvider && (
|
|
1363
|
+
<span className="ml-auto text-2xs text-accent font-mono">{tsProvider}{tsModel ? ` / ${tsModel}` : ''}</span>
|
|
1364
|
+
)}
|
|
1365
|
+
</button>
|
|
1366
|
+
{settingsOpen && (
|
|
1367
|
+
<div className="px-4 pb-3 space-y-3">
|
|
1368
|
+
<div className="flex gap-3">
|
|
1369
|
+
<div className="flex-1 space-y-1">
|
|
1370
|
+
<label className="text-2xs text-text-3 font-sans">Provider</label>
|
|
1371
|
+
<Select value={tsProvider} onValueChange={handleTsProviderChange}>
|
|
1372
|
+
<SelectTrigger placeholder="Default" className="bg-surface-4 h-7 text-xs" />
|
|
1373
|
+
<SelectContent>
|
|
1374
|
+
{providers.map((p) => (
|
|
1375
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
1376
|
+
))}
|
|
1377
|
+
</SelectContent>
|
|
1378
|
+
</Select>
|
|
1379
|
+
</div>
|
|
1380
|
+
<div className="flex-1 space-y-1">
|
|
1381
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
1382
|
+
<Select value={tsModel} onValueChange={setTsModel}>
|
|
1383
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-4 h-7 text-xs" />
|
|
1384
|
+
<SelectContent>
|
|
1385
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
1386
|
+
{tsModels.map((m) => (
|
|
1387
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
1388
|
+
))}
|
|
1389
|
+
</SelectContent>
|
|
1390
|
+
</Select>
|
|
1391
|
+
</div>
|
|
1392
|
+
</div>
|
|
1393
|
+
<TuningSlider
|
|
1394
|
+
label="Reasoning"
|
|
1395
|
+
value={tsReasoning}
|
|
1396
|
+
onChange={setTsReasoning}
|
|
1397
|
+
min={0} max={100} step={1}
|
|
1398
|
+
/>
|
|
1399
|
+
{showTemp && (
|
|
1400
|
+
<TuningSlider
|
|
1401
|
+
label="Temperature"
|
|
1402
|
+
value={tsTemp}
|
|
1403
|
+
onChange={setTsTemp}
|
|
1404
|
+
min={0} max={1} step={0.01}
|
|
1405
|
+
formatValue={(v) => v.toFixed(2)}
|
|
1406
|
+
/>
|
|
1407
|
+
)}
|
|
1408
|
+
</div>
|
|
1409
|
+
)}
|
|
1410
|
+
</div>
|
|
1411
|
+
|
|
733
1412
|
<div className="px-4 py-3 space-y-1.5">
|
|
734
|
-
{/* Phase 1 agents — editable rows */}
|
|
735
1413
|
{agentEdits.map((a, i) => {
|
|
736
1414
|
const Icon = ROLE_ICONS[a.role] || Code2;
|
|
737
1415
|
const nameValid = !a.name || NAME_RE.test(a.name);
|
|
@@ -759,7 +1437,6 @@ function RecommendedTeamCard() {
|
|
|
759
1437
|
);
|
|
760
1438
|
})}
|
|
761
1439
|
|
|
762
|
-
{/* Project dir indicator */}
|
|
763
1440
|
{recommendedTeam.projectDir && (
|
|
764
1441
|
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
765
1442
|
<span className="text-text-4">Project:</span>
|
|
@@ -767,7 +1444,6 @@ function RecommendedTeamCard() {
|
|
|
767
1444
|
</div>
|
|
768
1445
|
)}
|
|
769
1446
|
|
|
770
|
-
{/* Phase 2 indicator */}
|
|
771
1447
|
{phase2.length > 0 && (
|
|
772
1448
|
<div className="flex items-center gap-1.5 text-2xs text-text-3 font-sans">
|
|
773
1449
|
<Shield size={10} />
|
|
@@ -802,6 +1478,9 @@ export default function AgentsView() {
|
|
|
802
1478
|
const togglePreviewInAgents = useGrooveStore((s) => s.togglePreviewInAgents);
|
|
803
1479
|
const workspaceMode = useGrooveStore((s) => s.workspaceMode);
|
|
804
1480
|
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
1481
|
+
const openTeamBuilder = useGrooveStore((s) => s.openTeamBuilder);
|
|
1482
|
+
|
|
1483
|
+
const [plannerConfigOpen, setPlannerConfigOpen] = useState(false);
|
|
805
1484
|
|
|
806
1485
|
// Poll for recommended team while a planner is running
|
|
807
1486
|
useEffect(() => {
|
|
@@ -811,9 +1490,21 @@ export default function AgentsView() {
|
|
|
811
1490
|
return () => clearInterval(interval);
|
|
812
1491
|
}, [allAgents, checkRecommendedTeam]);
|
|
813
1492
|
|
|
814
|
-
|
|
1493
|
+
function openPlannerConfig() {
|
|
1494
|
+
setPlannerConfigOpen(true);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async function handlePlannerLaunch(config) {
|
|
1498
|
+
setPlannerConfigOpen(false);
|
|
815
1499
|
try {
|
|
816
|
-
const agent = await spawnAgent({
|
|
1500
|
+
const agent = await spawnAgent({
|
|
1501
|
+
role: 'planner',
|
|
1502
|
+
provider: config.provider,
|
|
1503
|
+
model: config.model,
|
|
1504
|
+
reasoningEffort: config.reasoningEffort,
|
|
1505
|
+
temperature: config.temperature,
|
|
1506
|
+
verbosity: config.verbosity,
|
|
1507
|
+
});
|
|
817
1508
|
if (agent?.id) {
|
|
818
1509
|
selectAgent(agent.id);
|
|
819
1510
|
}
|
|
@@ -851,7 +1542,7 @@ export default function AgentsView() {
|
|
|
851
1542
|
<p className="text-xs text-text-3 font-sans mt-1">Syncing with daemon...</p>
|
|
852
1543
|
</div>
|
|
853
1544
|
) : teamAgents.length === 0 ? (
|
|
854
|
-
<EmptyState onPlanner={
|
|
1545
|
+
<EmptyState onPlanner={openPlannerConfig} onSpawn={() => openDetail({ type: 'spawn' })} onTeamBuilder={openTeamBuilder} />
|
|
855
1546
|
) : workspaceMode ? (
|
|
856
1547
|
<WorkspaceMode />
|
|
857
1548
|
) : showPreviewInAgents && previewState.url && previewState.teamId === activeTeamId ? (
|
|
@@ -892,6 +1583,8 @@ export default function AgentsView() {
|
|
|
892
1583
|
{showPreviewInAgents ? <><Users size={14} /> Team</> : <><Eye size={14} /> Preview</>}
|
|
893
1584
|
</button>
|
|
894
1585
|
)}
|
|
1586
|
+
<PlannerConfigDialog open={plannerConfigOpen} onOpenChange={setPlannerConfigOpen} onLaunch={handlePlannerLaunch} />
|
|
1587
|
+
<TeamBuilder />
|
|
895
1588
|
</div>
|
|
896
1589
|
);
|
|
897
1590
|
}
|