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.
Files changed (70) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +228 -3
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
  5. package/node_modules/@groove-dev/daemon/src/process.js +5 -1
  6. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +8 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/codex.js +33 -4
  9. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -1
  10. package/node_modules/@groove-dev/daemon/src/providers/grok.js +8 -1
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
  13. package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BBL3i_JW.js → codemirror-CFF1Lrnz.js} +10 -10
  15. package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-VB4_k5Pz.js +8653 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
  24. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +15 -5
  25. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
  26. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +32 -26
  27. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
  28. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
  29. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
  30. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
  31. package/node_modules/@groove-dev/gui/src/stores/groove.js +151 -12
  32. package/node_modules/@groove-dev/gui/src/views/agents.jsx +720 -38
  33. package/package.json +1 -1
  34. package/packages/cli/package.json +1 -1
  35. package/packages/daemon/package.json +1 -1
  36. package/packages/daemon/src/api.js +228 -3
  37. package/packages/daemon/src/introducer.js +42 -0
  38. package/packages/daemon/src/process.js +5 -1
  39. package/packages/daemon/src/providers/base.js +4 -0
  40. package/packages/daemon/src/providers/claude-code.js +8 -0
  41. package/packages/daemon/src/providers/codex.js +33 -4
  42. package/packages/daemon/src/providers/gemini.js +14 -1
  43. package/packages/daemon/src/providers/grok.js +8 -1
  44. package/packages/daemon/src/providers/local.js +8 -1
  45. package/packages/daemon/src/tunnel-manager.js +74 -5
  46. package/packages/daemon/src/validate.js +22 -1
  47. package/packages/gui/dist/assets/{codemirror-BBL3i_JW.js → codemirror-CFF1Lrnz.js} +10 -10
  48. package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  49. package/packages/gui/dist/assets/index-VB4_k5Pz.js +8653 -0
  50. package/packages/gui/dist/index.html +3 -3
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
  53. package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
  54. package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
  55. package/packages/gui/src/components/chat/chat-header.jsx +3 -30
  56. package/packages/gui/src/components/chat/chat-input.jsx +163 -153
  57. package/packages/gui/src/components/chat/chat-view.jsx +15 -5
  58. package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
  59. package/packages/gui/src/components/editor/code-editor.jsx +32 -26
  60. package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
  61. package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
  62. package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
  63. package/packages/gui/src/components/ui/slider.jsx +50 -0
  64. package/packages/gui/src/stores/groove.js +151 -12
  65. package/packages/gui/src/views/agents.jsx +720 -38
  66. package/workspace.png +0 -0
  67. package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  68. package/node_modules/@groove-dev/gui/dist/assets/index-MLIZRMj1.js +0 -8642
  69. package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  70. 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, 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, 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
- <button
623
- onClick={onSpawn}
624
- 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"
625
- >
626
- <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">
627
- <Plus size={20} className="text-text-1" />
628
- </div>
629
- <div className="min-w-0">
630
- <div className="text-sm font-semibold text-text-0 font-sans">Spawn Agent</div>
631
- <div className="text-xs text-text-3 font-sans mt-0.5">Choose a role and configure</div>
632
- </div>
633
- </button>
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
- <button
636
- onClick={() => useGrooveStore.getState().toggleQuickConnect()}
637
- 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"
638
- >
639
- <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">
640
- <Radio size={20} className="text-text-1" />
641
- </div>
642
- <div className="min-w-0">
643
- <div className="text-sm font-semibold text-text-0 font-sans">Connect to Remote Server</div>
644
- <div className="text-xs text-text-3 font-sans mt-0.5">SSH tunnel to a remote machine</div>
645
- </div>
646
- </button>
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
- // Merge edited phase1 names back with phase2 agents
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
- async function launchPlanner() {
1482
+ function openPlannerConfig() {
1483
+ setPlannerConfigOpen(true);
1484
+ }
1485
+
1486
+ async function handlePlannerLaunch(config) {
1487
+ setPlannerConfigOpen(false);
815
1488
  try {
816
- const agent = await spawnAgent({ role: 'planner' });
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={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
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
  }