groove-dev 0.27.89 → 0.27.92

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