groove-dev 0.27.91 → 0.27.93
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/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +228 -3
- 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 +8 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +33 -4
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -1
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +8 -1
- 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/{codemirror-BBL3i_JW.js → codemirror-CFF1Lrnz.js} +10 -10
- package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-VB4_k5Pz.js +8653 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -3
- 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 +15 -5
- 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 +32 -26
- 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 +151 -12
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +720 -38
- 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 +228 -3
- 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 +8 -0
- package/packages/daemon/src/providers/codex.js +33 -4
- package/packages/daemon/src/providers/gemini.js +14 -1
- package/packages/daemon/src/providers/grok.js +8 -1
- 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/{codemirror-BBL3i_JW.js → codemirror-CFF1Lrnz.js} +10 -10
- package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/packages/gui/dist/assets/index-VB4_k5Pz.js +8653 -0
- package/packages/gui/dist/index.html +3 -3
- 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 +15 -5
- package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
- package/packages/gui/src/components/editor/code-editor.jsx +32 -26
- 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 +151 -12
- package/packages/gui/src/views/agents.jsx +720 -38
- package/workspace.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-MLIZRMj1.js +0 -8642
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/packages/gui/dist/assets/index-MLIZRMj1.js +0 -8642
|
@@ -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,
|
|
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, 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 || '__default__'} onValueChange={(v) => {
|
|
978
|
+
const pv = v === '__default__' ? null : v;
|
|
979
|
+
const p = providers.find((x) => x.id === pv);
|
|
980
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
981
|
+
updateRole(i, { provider: pv, model: pModels[0]?.id || null });
|
|
982
|
+
}}>
|
|
983
|
+
<SelectTrigger placeholder="Team Default" className="bg-surface-3 h-7 text-xs" />
|
|
984
|
+
<SelectContent>
|
|
985
|
+
<SelectItem value="__default__">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 || '__default__'} onValueChange={(v) => updateRole(i, { model: v === '__default__' ? null : v })}>
|
|
995
|
+
<SelectTrigger placeholder="Default" className="bg-surface-3 h-7 text-xs" />
|
|
996
|
+
<SelectContent>
|
|
997
|
+
<SelectItem value="__default__">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 || '__default__'} onValueChange={(v) => handleSettingsProviderChange(v === '__default__' ? '' : v)}>
|
|
1083
|
+
<SelectTrigger placeholder="Default" className="bg-surface-3 h-7 text-xs" />
|
|
1084
|
+
<SelectContent>
|
|
1085
|
+
<SelectItem value="__default__">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 || '__default__'} onValueChange={(v) => setSettings({ model: v === '__default__' ? '' : v })}>
|
|
1095
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
|
|
1096
|
+
<SelectContent>
|
|
1097
|
+
<SelectItem value="__default__">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,31 +1194,33 @@ function EmptyState({ onPlanner, onSpawn }) {
|
|
|
619
1194
|
</div>
|
|
620
1195
|
</button>
|
|
621
1196
|
|
|
622
|
-
<
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
<
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
<div className="
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1197
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1198
|
+
<button
|
|
1199
|
+
onClick={onTeamBuilder}
|
|
1200
|
+
className="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"
|
|
1201
|
+
>
|
|
1202
|
+
<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">
|
|
1203
|
+
<UserPlus size={20} className="text-purple" />
|
|
1204
|
+
</div>
|
|
1205
|
+
<div className="min-w-0">
|
|
1206
|
+
<div className="text-sm font-semibold text-text-0 font-sans">Build a Team</div>
|
|
1207
|
+
<div className="text-xs text-text-3 font-sans mt-0.5">Pick roles and configure</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
</button>
|
|
634
1210
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1211
|
+
<button
|
|
1212
|
+
onClick={onSpawn}
|
|
1213
|
+
className="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"
|
|
1214
|
+
>
|
|
1215
|
+
<div className="w-10 h-10 rounded-lg bg-surface-4 flex items-center justify-center group-hover:scale-110 transition-transform flex-shrink-0">
|
|
1216
|
+
<Plus size={20} className="text-text-1" />
|
|
1217
|
+
</div>
|
|
1218
|
+
<div className="min-w-0">
|
|
1219
|
+
<div className="text-sm font-semibold text-text-0 font-sans">Spawn Agent</div>
|
|
1220
|
+
<div className="text-xs text-text-3 font-sans mt-0.5">Choose a role and configure</div>
|
|
1221
|
+
</div>
|
|
1222
|
+
</button>
|
|
1223
|
+
</div>
|
|
647
1224
|
</div>
|
|
648
1225
|
|
|
649
1226
|
{window.groove?.openFolder && (
|
|
@@ -687,8 +1264,24 @@ function sanitizeName(raw) {
|
|
|
687
1264
|
function RecommendedTeamCard() {
|
|
688
1265
|
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
689
1266
|
const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
|
|
1267
|
+
const teamLaunchConfig = useGrooveStore((s) => s.teamLaunchConfig);
|
|
1268
|
+
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
690
1269
|
const [launching, setLaunching] = useState(false);
|
|
691
1270
|
const [editedAgents, setEditedAgents] = useState(null);
|
|
1271
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
1272
|
+
const [providers, setProviders] = useState([]);
|
|
1273
|
+
|
|
1274
|
+
// Team settings — pre-populated from planner spawn config or defaults
|
|
1275
|
+
const [tsProvider, setTsProvider] = useState(teamLaunchConfig?.provider || '');
|
|
1276
|
+
const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
|
|
1277
|
+
const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
|
|
1278
|
+
const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
|
|
1279
|
+
|
|
1280
|
+
useEffect(() => {
|
|
1281
|
+
fetchProviders().then((list) => {
|
|
1282
|
+
if (Array.isArray(list)) setProviders(list.filter((p) => p.installed));
|
|
1283
|
+
}).catch(() => {});
|
|
1284
|
+
}, []);
|
|
692
1285
|
|
|
693
1286
|
if (!recommendedTeam?.agents?.length) return null;
|
|
694
1287
|
|
|
@@ -696,22 +1289,38 @@ function RecommendedTeamCard() {
|
|
|
696
1289
|
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
697
1290
|
const phase2 = agents.filter((a) => a.phase === 2);
|
|
698
1291
|
|
|
699
|
-
// Initialize edits lazily so we get fresh data if recommendedTeam changes
|
|
700
1292
|
const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
|
|
701
1293
|
|
|
1294
|
+
const selectedProvider = providers.find((p) => p.id === tsProvider);
|
|
1295
|
+
const tsModels = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1296
|
+
const showTemp = PROVIDER_TEMP_SUPPORT.has(tsProvider);
|
|
1297
|
+
|
|
702
1298
|
function handleNameChange(i, raw) {
|
|
703
1299
|
const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
|
|
704
1300
|
setEditedAgents(next);
|
|
705
1301
|
}
|
|
706
1302
|
|
|
1303
|
+
function handleTsProviderChange(id) {
|
|
1304
|
+
setTsProvider(id);
|
|
1305
|
+
const p = providers.find((x) => x.id === id);
|
|
1306
|
+
const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
|
|
1307
|
+
setTsModel(pModels[0]?.id || '');
|
|
1308
|
+
}
|
|
1309
|
+
|
|
707
1310
|
async function handleLaunch() {
|
|
708
1311
|
setLaunching(true);
|
|
1312
|
+
// Save overrides to store so launchRecommendedTeam sends them
|
|
1313
|
+
if (tsProvider) {
|
|
1314
|
+
useGrooveStore.setState({
|
|
1315
|
+
teamLaunchConfig: {
|
|
1316
|
+
provider: tsProvider, model: tsModel,
|
|
1317
|
+
reasoningEffort: tsReasoning,
|
|
1318
|
+
...(showTemp && { temperature: tsTemp }),
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
709
1322
|
try {
|
|
710
|
-
|
|
711
|
-
const modified = [
|
|
712
|
-
...agentEdits,
|
|
713
|
-
...phase2,
|
|
714
|
-
];
|
|
1323
|
+
const modified = [...agentEdits, ...phase2];
|
|
715
1324
|
await launchRecommendedTeam(modified);
|
|
716
1325
|
} catch { /* toast handles */ }
|
|
717
1326
|
setLaunching(false);
|
|
@@ -730,8 +1339,66 @@ function RecommendedTeamCard() {
|
|
|
730
1339
|
<button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
|
|
731
1340
|
</div>
|
|
732
1341
|
|
|
1342
|
+
{/* Collapsible Team Settings */}
|
|
1343
|
+
<div className="border-b border-border-subtle">
|
|
1344
|
+
<button
|
|
1345
|
+
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
1346
|
+
className="w-full flex items-center gap-2 px-4 py-2 text-left cursor-pointer hover:bg-surface-3/50 transition-colors"
|
|
1347
|
+
>
|
|
1348
|
+
<ChevronDown size={12} className={cn('text-text-4 transition-transform duration-200', !settingsOpen && '-rotate-90')} />
|
|
1349
|
+
<Settings2 size={12} className="text-text-3" />
|
|
1350
|
+
<span className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wider">Team Settings</span>
|
|
1351
|
+
{tsProvider && (
|
|
1352
|
+
<span className="ml-auto text-2xs text-accent font-mono">{tsProvider}{tsModel ? ` / ${tsModel}` : ''}</span>
|
|
1353
|
+
)}
|
|
1354
|
+
</button>
|
|
1355
|
+
{settingsOpen && (
|
|
1356
|
+
<div className="px-4 pb-3 space-y-3">
|
|
1357
|
+
<div className="flex gap-3">
|
|
1358
|
+
<div className="flex-1 space-y-1">
|
|
1359
|
+
<label className="text-2xs text-text-3 font-sans">Provider</label>
|
|
1360
|
+
<Select value={tsProvider} onValueChange={handleTsProviderChange}>
|
|
1361
|
+
<SelectTrigger placeholder="Default" className="bg-surface-4 h-7 text-xs" />
|
|
1362
|
+
<SelectContent>
|
|
1363
|
+
{providers.map((p) => (
|
|
1364
|
+
<SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
|
|
1365
|
+
))}
|
|
1366
|
+
</SelectContent>
|
|
1367
|
+
</Select>
|
|
1368
|
+
</div>
|
|
1369
|
+
<div className="flex-1 space-y-1">
|
|
1370
|
+
<label className="text-2xs text-text-3 font-sans">Model</label>
|
|
1371
|
+
<Select value={tsModel} onValueChange={setTsModel}>
|
|
1372
|
+
<SelectTrigger placeholder="Auto" className="bg-surface-4 h-7 text-xs" />
|
|
1373
|
+
<SelectContent>
|
|
1374
|
+
<SelectItem value="auto">Auto</SelectItem>
|
|
1375
|
+
{tsModels.map((m) => (
|
|
1376
|
+
<SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
|
|
1377
|
+
))}
|
|
1378
|
+
</SelectContent>
|
|
1379
|
+
</Select>
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>
|
|
1382
|
+
<TuningSlider
|
|
1383
|
+
label="Reasoning"
|
|
1384
|
+
value={tsReasoning}
|
|
1385
|
+
onChange={setTsReasoning}
|
|
1386
|
+
min={0} max={100} step={1}
|
|
1387
|
+
/>
|
|
1388
|
+
{showTemp && (
|
|
1389
|
+
<TuningSlider
|
|
1390
|
+
label="Temperature"
|
|
1391
|
+
value={tsTemp}
|
|
1392
|
+
onChange={setTsTemp}
|
|
1393
|
+
min={0} max={1} step={0.01}
|
|
1394
|
+
formatValue={(v) => v.toFixed(2)}
|
|
1395
|
+
/>
|
|
1396
|
+
)}
|
|
1397
|
+
</div>
|
|
1398
|
+
)}
|
|
1399
|
+
</div>
|
|
1400
|
+
|
|
733
1401
|
<div className="px-4 py-3 space-y-1.5">
|
|
734
|
-
{/* Phase 1 agents — editable rows */}
|
|
735
1402
|
{agentEdits.map((a, i) => {
|
|
736
1403
|
const Icon = ROLE_ICONS[a.role] || Code2;
|
|
737
1404
|
const nameValid = !a.name || NAME_RE.test(a.name);
|
|
@@ -759,7 +1426,6 @@ function RecommendedTeamCard() {
|
|
|
759
1426
|
);
|
|
760
1427
|
})}
|
|
761
1428
|
|
|
762
|
-
{/* Project dir indicator */}
|
|
763
1429
|
{recommendedTeam.projectDir && (
|
|
764
1430
|
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
765
1431
|
<span className="text-text-4">Project:</span>
|
|
@@ -767,7 +1433,6 @@ function RecommendedTeamCard() {
|
|
|
767
1433
|
</div>
|
|
768
1434
|
)}
|
|
769
1435
|
|
|
770
|
-
{/* Phase 2 indicator */}
|
|
771
1436
|
{phase2.length > 0 && (
|
|
772
1437
|
<div className="flex items-center gap-1.5 text-2xs text-text-3 font-sans">
|
|
773
1438
|
<Shield size={10} />
|
|
@@ -802,6 +1467,9 @@ export default function AgentsView() {
|
|
|
802
1467
|
const togglePreviewInAgents = useGrooveStore((s) => s.togglePreviewInAgents);
|
|
803
1468
|
const workspaceMode = useGrooveStore((s) => s.workspaceMode);
|
|
804
1469
|
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
1470
|
+
const openTeamBuilder = useGrooveStore((s) => s.openTeamBuilder);
|
|
1471
|
+
|
|
1472
|
+
const [plannerConfigOpen, setPlannerConfigOpen] = useState(false);
|
|
805
1473
|
|
|
806
1474
|
// Poll for recommended team while a planner is running
|
|
807
1475
|
useEffect(() => {
|
|
@@ -811,9 +1479,21 @@ export default function AgentsView() {
|
|
|
811
1479
|
return () => clearInterval(interval);
|
|
812
1480
|
}, [allAgents, checkRecommendedTeam]);
|
|
813
1481
|
|
|
814
|
-
|
|
1482
|
+
function openPlannerConfig() {
|
|
1483
|
+
setPlannerConfigOpen(true);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async function handlePlannerLaunch(config) {
|
|
1487
|
+
setPlannerConfigOpen(false);
|
|
815
1488
|
try {
|
|
816
|
-
const agent = await spawnAgent({
|
|
1489
|
+
const agent = await spawnAgent({
|
|
1490
|
+
role: 'planner',
|
|
1491
|
+
provider: config.provider,
|
|
1492
|
+
model: config.model,
|
|
1493
|
+
reasoningEffort: config.reasoningEffort,
|
|
1494
|
+
temperature: config.temperature,
|
|
1495
|
+
verbosity: config.verbosity,
|
|
1496
|
+
});
|
|
817
1497
|
if (agent?.id) {
|
|
818
1498
|
selectAgent(agent.id);
|
|
819
1499
|
}
|
|
@@ -851,7 +1531,7 @@ export default function AgentsView() {
|
|
|
851
1531
|
<p className="text-xs text-text-3 font-sans mt-1">Syncing with daemon...</p>
|
|
852
1532
|
</div>
|
|
853
1533
|
) : teamAgents.length === 0 ? (
|
|
854
|
-
<EmptyState onPlanner={
|
|
1534
|
+
<EmptyState onPlanner={openPlannerConfig} onSpawn={() => openDetail({ type: 'spawn' })} onTeamBuilder={openTeamBuilder} />
|
|
855
1535
|
) : workspaceMode ? (
|
|
856
1536
|
<WorkspaceMode />
|
|
857
1537
|
) : showPreviewInAgents && previewState.url && previewState.teamId === activeTeamId ? (
|
|
@@ -892,6 +1572,8 @@ export default function AgentsView() {
|
|
|
892
1572
|
{showPreviewInAgents ? <><Users size={14} /> Team</> : <><Eye size={14} /> Preview</>}
|
|
893
1573
|
</button>
|
|
894
1574
|
)}
|
|
1575
|
+
<PlannerConfigDialog open={plannerConfigOpen} onOpenChange={setPlannerConfigOpen} onLaunch={handlePlannerLaunch} />
|
|
1576
|
+
<TeamBuilder />
|
|
895
1577
|
</div>
|
|
896
1578
|
);
|
|
897
1579
|
}
|