groove-dev 0.27.7 → 0.27.11

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 (127) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/src/api.js +496 -44
  3. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
  4. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  7. package/node_modules/@groove-dev/daemon/src/process.js +128 -104
  8. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  9. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  10. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  11. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  13. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  14. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  15. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  20. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
  28. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  29. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  32. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  33. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  34. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  36. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  37. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  38. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  39. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  41. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  42. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  43. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  44. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  46. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  47. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  48. package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
  49. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
  50. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  51. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  52. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  53. package/package.json +7 -2
  54. package/packages/daemon/src/api.js +496 -44
  55. package/packages/daemon/src/gateways/manager.js +25 -12
  56. package/packages/daemon/src/index.js +7 -0
  57. package/packages/daemon/src/introducer.js +72 -4
  58. package/packages/daemon/src/journalist.js +66 -11
  59. package/packages/daemon/src/process.js +128 -104
  60. package/packages/daemon/src/registry.js +1 -1
  61. package/packages/daemon/src/repo-import.js +541 -0
  62. package/packages/daemon/src/rotator.js +28 -1
  63. package/packages/daemon/src/supervisor.js +2 -1
  64. package/packages/daemon/src/tunnel-manager.js +504 -0
  65. package/packages/daemon/src/validate.js +13 -0
  66. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  67. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  68. package/packages/gui/dist/index.html +2 -2
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  71. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  72. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  75. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  76. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  77. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  78. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  80. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  81. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  82. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  83. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  84. package/packages/gui/src/app.css +14 -0
  85. package/packages/gui/src/app.jsx +13 -0
  86. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  87. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  88. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  89. package/packages/gui/src/components/agents/agent-node.jsx +16 -17
  90. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  91. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  92. package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
  93. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  94. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  95. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  96. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  97. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  98. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  99. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  100. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  101. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  102. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  103. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  104. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  105. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  106. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  107. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  108. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  109. package/packages/gui/src/components/ui/toast.jsx +1 -1
  110. package/packages/gui/src/lib/edition.js +4 -0
  111. package/packages/gui/src/lib/electron.js +17 -0
  112. package/packages/gui/src/lib/status.js +1 -0
  113. package/packages/gui/src/stores/groove.js +150 -6
  114. package/packages/gui/src/views/dashboard.jsx +39 -40
  115. package/packages/gui/src/views/marketplace.jsx +82 -0
  116. package/packages/gui/src/views/settings.jsx +66 -0
  117. package/packages/gui/vite.config.js +3 -0
  118. package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  119. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
  120. package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  121. package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
  122. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  123. package/test-slack.mjs +0 -28
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  126. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  127. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -5,7 +5,7 @@ import {
5
5
  Gauge, FolderSearch, Key, Check, Eye, EyeOff,
6
6
  AlertCircle, Layers, Activity,
7
7
  RotateCw, Skull, Copy, Trash2,
8
- Sparkles, Calendar, Plug,
8
+ Sparkles, Calendar, Plug, MessageCircle, Save, GitBranch,
9
9
  } from 'lucide-react';
10
10
  import { useGrooveStore } from '../../stores/groove';
11
11
  import { Badge } from '../ui/badge';
@@ -156,15 +156,21 @@ export function AgentConfig({ agent }) {
156
156
  const [expandedProvider, setExpandedProvider] = useState(null);
157
157
  const [routingMode, setRoutingMode] = useState(agent.routingMode || 'auto');
158
158
  const [installedSkills, setInstalledSkills] = useState([]);
159
+ const [importedRepos, setImportedRepos] = useState([]);
159
160
  const [scheduleUnit, setScheduleUnit] = useState('hr');
160
161
  const [scheduleCount, setScheduleCount] = useState('1');
161
162
  const [scheduling, setScheduling] = useState(false);
163
+ const [personalityContent, setPersonalityContent] = useState('');
164
+ const [personalityLoaded, setPersonalityLoaded] = useState(false);
165
+ const [personalities, setPersonalities] = useState([]);
166
+ const [savingPersonality, setSavingPersonality] = useState(false);
162
167
 
163
168
  const isAlive = agent.status === 'running' || agent.status === 'starting';
164
169
 
165
170
  useEffect(() => {
166
171
  loadProviders();
167
172
  api.get('/skills/installed').then((data) => setInstalledSkills(Array.isArray(data) ? data : data.skills || [])).catch(() => {});
173
+ api.get('/repos/imported').then((data) => setImportedRepos((Array.isArray(data) ? data : []).filter((r) => r.status === 'active'))).catch(() => {});
168
174
  function onChanged() { loadProviders(); }
169
175
  window.addEventListener('groove:providers-changed', onChanged);
170
176
  return () => window.removeEventListener('groove:providers-changed', onChanged);
@@ -181,6 +187,20 @@ export function AgentConfig({ agent }) {
181
187
  }).catch(() => {});
182
188
  }, [agent.id, agent.model]);
183
189
 
190
+ useEffect(() => {
191
+ setPersonalityLoaded(false);
192
+ api.get(`/personalities/${agent.name}`).then((data) => {
193
+ setPersonalityContent(data?.content || '');
194
+ setPersonalityLoaded(true);
195
+ }).catch(() => {
196
+ setPersonalityContent('');
197
+ setPersonalityLoaded(true);
198
+ });
199
+ api.get('/personalities').then((data) => {
200
+ setPersonalities(Array.isArray(data) ? data : data.personalities || []);
201
+ }).catch(() => {});
202
+ }, [agent.id, agent.name]);
203
+
184
204
  const currentProvider = providers.find((p) => p.id === agent.provider);
185
205
 
186
206
  async function handleModelSwap(providerId, modelId) {
@@ -612,6 +632,60 @@ export function AgentConfig({ agent }) {
612
632
  </div>
613
633
  </ConfigSection>
614
634
 
635
+ {/* ── Repos ─────────────────────────────────────────── */}
636
+ {importedRepos.length > 0 && (
637
+ <ConfigSection label="Repos" icon={GitBranch} description="Attach imported repos so this agent knows where they are.">
638
+ <div className="flex flex-wrap gap-1.5">
639
+ {(agent.repos || []).map((importId) => {
640
+ const repo = importedRepos.find((r) => r.id === importId);
641
+ return (
642
+ <Badge key={importId} variant="accent" className="font-mono text-xs gap-1.5 px-2.5 py-1">
643
+ {repo?.name || importId}
644
+ <button
645
+ onClick={async () => {
646
+ try {
647
+ await api.delete(`/agents/${agent.id}/repos/${importId}`);
648
+ addToast('success', `Detached ${repo?.name || importId}`);
649
+ } catch (err) { addToast('error', 'Detach failed', err.message); }
650
+ }}
651
+ className="hover:text-danger cursor-pointer"
652
+ >
653
+ <X size={10} />
654
+ </button>
655
+ </Badge>
656
+ );
657
+ })}
658
+ {importedRepos.filter((r) => !(agent.repos || []).includes(r.id)).length > 0 && (
659
+ <div className="relative group">
660
+ <button className="w-7 h-7 flex items-center justify-center rounded-md bg-surface-4 border border-border-subtle text-text-3 hover:text-accent cursor-pointer transition-colors">
661
+ <Plus size={12} />
662
+ </button>
663
+ <div className="absolute top-full left-0 mt-1 z-20 hidden group-hover:block bg-surface-2 border border-border-subtle rounded-lg shadow-xl py-1 min-w-[200px]">
664
+ {importedRepos.filter((r) => !(agent.repos || []).includes(r.id)).map((repo) => (
665
+ <button
666
+ key={repo.id}
667
+ onClick={async () => {
668
+ try {
669
+ await api.post(`/agents/${agent.id}/repos/${repo.id}`);
670
+ addToast('success', `Attached ${repo.name || repo.id}`);
671
+ } catch (err) { addToast('error', 'Attach failed', err.message); }
672
+ }}
673
+ className="w-full text-left px-3 py-1.5 text-xs font-sans text-text-1 hover:bg-surface-4 cursor-pointer transition-colors"
674
+ >
675
+ <div className="font-semibold">{repo.name || repo.repo}</div>
676
+ <div className="text-2xs text-text-4 font-mono truncate">{repo.clonedTo}</div>
677
+ </button>
678
+ ))}
679
+ </div>
680
+ </div>
681
+ )}
682
+ {(agent.repos || []).length === 0 && (
683
+ <span className="text-2xs text-text-4 font-sans">No repos attached — import one from the Marketplace</span>
684
+ )}
685
+ </div>
686
+ </ConfigSection>
687
+ )}
688
+
615
689
  {/* ── Schedule ──────────────────────────────────────── */}
616
690
  <ConfigSection label="Schedule" icon={Calendar} description="Run this agent on a recurring schedule.">
617
691
  <div className="flex items-center gap-2">
@@ -686,6 +760,61 @@ export function AgentConfig({ agent }) {
686
760
  </div>
687
761
  </ConfigSection>
688
762
 
763
+ {/* ── Personality ──────────────────────────────────── */}
764
+ <ConfigSection label="Personality" icon={MessageCircle} description="Injected into every prompt. Changes apply on next spawn or rotation.">
765
+ <textarea
766
+ value={personalityContent}
767
+ onChange={(e) => setPersonalityContent(e.target.value)}
768
+ placeholder={personalityLoaded ? 'Describe this agent\'s personality, tone, and behavior...' : 'Loading...'}
769
+ rows={4}
770
+ className="w-full min-h-[4rem] max-h-[10rem] resize-y bg-surface-0 border border-border-subtle rounded-md p-2 text-xs font-mono text-text-1 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
771
+ />
772
+ <div className="flex items-center gap-2">
773
+ <Button
774
+ variant="primary"
775
+ size="sm"
776
+ disabled={savingPersonality}
777
+ onClick={async () => {
778
+ setSavingPersonality(true);
779
+ try {
780
+ await api.put(`/personalities/${agent.name}`, { content: personalityContent });
781
+ addToast('success', 'Personality saved');
782
+ } catch (err) {
783
+ addToast('error', 'Save failed', err.message);
784
+ }
785
+ setSavingPersonality(false);
786
+ }}
787
+ className="h-7 px-3 text-2xs gap-1"
788
+ >
789
+ <Save size={10} />
790
+ {savingPersonality ? 'Saving...' : 'Save'}
791
+ </Button>
792
+ {personalities.length > 0 && (
793
+ <div className="relative">
794
+ <select
795
+ value=""
796
+ onChange={(e) => {
797
+ if (!e.target.value) return;
798
+ const p = personalities.find((x) => (x.name || x) === e.target.value);
799
+ if (p) {
800
+ api.get(`/personalities/${p.name || p}`).then((data) => {
801
+ if (data?.content) setPersonalityContent(data.content);
802
+ }).catch(() => {});
803
+ }
804
+ }}
805
+ className="h-7 px-2 pr-7 text-2xs rounded-md bg-surface-1 border border-border-subtle text-text-2 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent"
806
+ >
807
+ <option value="">Clone from...</option>
808
+ {personalities.filter((p) => (p.name || p) !== agent.name).map((p) => (
809
+ <option key={p.name || p} value={p.name || p}>{p.name || p}</option>
810
+ ))}
811
+ </select>
812
+ <ChevronDown size={10} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
813
+ </div>
814
+ )}
815
+ </div>
816
+ </ConfigSection>
817
+
689
818
  {/* ── Original Prompt ────────────────────────────────── */}
690
819
  {agent.prompt && (
691
820
  <ConfigSection label="Original Prompt" icon={Activity}>
@@ -369,9 +369,9 @@ function StreamingBar({ agent }) {
369
369
  <div className="flex items-center gap-3 flex-shrink-0">
370
370
  <span className="text-[10px] text-text-4 font-mono">{fmtTokens(agent.tokensUsed)}</span>
371
371
  <div className="flex items-center gap-1.5">
372
- <div className="w-14 h-1 rounded-full bg-surface-4 overflow-hidden">
372
+ <div className="w-14 h-0.5 rounded-sm bg-surface-4 overflow-hidden">
373
373
  <div
374
- className="h-full rounded-full transition-all duration-500"
374
+ className="h-full rounded-sm transition-all duration-500"
375
375
  style={{
376
376
  width: `${ctxPct}%`,
377
377
  background: ctxPct >= 75 ? 'var(--color-danger)' : ctxPct >= 50 ? 'var(--color-warning)' : 'var(--color-accent)',
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
3
3
  import { api } from '../../lib/api';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
- import { FileText, Save, X, RefreshCw, ChevronLeft } from 'lucide-react';
6
+ import { FileText, Save, X, RefreshCw, ChevronLeft, Plus } from 'lucide-react';
7
7
 
8
8
  export function AgentMdFiles({ agent }) {
9
9
  const addToast = useGrooveStore((s) => s.addToast);
@@ -13,6 +13,8 @@ export function AgentMdFiles({ agent }) {
13
13
  const [content, setContent] = useState('');
14
14
  const [original, setOriginal] = useState('');
15
15
  const [saving, setSaving] = useState(false);
16
+ const [creating, setCreating] = useState(false);
17
+ const [newFileName, setNewFileName] = useState('');
16
18
 
17
19
  async function fetchFiles() {
18
20
  try {
@@ -48,6 +50,20 @@ export function AgentMdFiles({ agent }) {
48
50
  setSaving(false);
49
51
  }
50
52
 
53
+ async function createFile() {
54
+ const name = newFileName.trim();
55
+ if (!name) return;
56
+ try {
57
+ await api.post(`/agents/${agent.id}/mdfiles/create`, { name });
58
+ setNewFileName('');
59
+ setCreating(false);
60
+ addToast('success', `Created ${name}.md`);
61
+ fetchFiles();
62
+ } catch (err) {
63
+ addToast('error', 'Create failed', err.message);
64
+ }
65
+ }
66
+
51
67
  const isDirty = content !== original;
52
68
 
53
69
  // File list view
@@ -57,6 +73,9 @@ export function AgentMdFiles({ agent }) {
57
73
  <div className="flex items-center gap-2 px-4 py-2.5 border-b border-border-subtle">
58
74
  <FileText size={12} className="text-text-3" />
59
75
  <span className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wider flex-1">Markdown Files</span>
76
+ <button onClick={() => setCreating(true)} className="p-1 text-text-4 hover:text-accent cursor-pointer" title="Create file">
77
+ <Plus size={12} />
78
+ </button>
60
79
  <button onClick={fetchFiles} className="p-1 text-text-4 hover:text-text-1 cursor-pointer">
61
80
  <RefreshCw size={11} />
62
81
  </button>
@@ -68,6 +87,23 @@ export function AgentMdFiles({ agent }) {
68
87
  </div>
69
88
  )}
70
89
 
90
+ {creating && (
91
+ <div className="flex items-center gap-1.5 px-4 py-2 border-b border-border-subtle bg-surface-0">
92
+ <FileText size={11} className="text-accent flex-shrink-0" />
93
+ <input
94
+ autoFocus
95
+ value={newFileName}
96
+ onChange={(e) => setNewFileName(e.target.value)}
97
+ onKeyDown={(e) => { if (e.key === 'Enter') createFile(); if (e.key === 'Escape') { setCreating(false); setNewFileName(''); } }}
98
+ placeholder="filename"
99
+ className="flex-1 bg-transparent text-xs text-text-0 font-mono outline-none placeholder:text-text-4"
100
+ />
101
+ <span className="text-[10px] text-text-4 font-mono">.md</span>
102
+ <button onClick={createFile} className="p-0.5 text-accent hover:text-accent/80 cursor-pointer"><Save size={11} /></button>
103
+ <button onClick={() => { setCreating(false); setNewFileName(''); }} className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"><X size={11} /></button>
104
+ </div>
105
+ )}
106
+
71
107
  <div className="flex-1 overflow-y-auto">
72
108
  {files.length === 0 ? (
73
109
  <div className="flex flex-col items-center justify-center h-full text-center px-4">
@@ -91,6 +127,12 @@ export function AgentMdFiles({ agent }) {
91
127
  <span className="text-[10px] text-text-4 font-mono flex-shrink-0">
92
128
  {f.size > 1024 ? `${(f.size / 1024).toFixed(1)}K` : `${f.size}B`}
93
129
  </span>
130
+ {f.source === 'personality' && (
131
+ <span className="text-[9px] font-mono text-purple bg-purple/10 px-1 py-px rounded flex-shrink-0">personality</span>
132
+ )}
133
+ {f.source === 'user' && (
134
+ <span className="text-[9px] font-mono text-accent bg-accent/10 px-1 py-px rounded flex-shrink-0">custom</span>
135
+ )}
94
136
  </button>
95
137
  ))}
96
138
  </div>
@@ -1,6 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { memo, useState, useMemo, useRef, useEffect } from 'react';
2
+ import { memo, useMemo, useRef, useEffect } from 'react';
3
3
  import { Handle, Position } from '@xyflow/react';
4
+ import { Maximize2, X } from 'lucide-react';
4
5
  import { useGrooveStore } from '../../stores/groove';
5
6
  import { statusColor } from '../../lib/status';
6
7
  import { fmtNum, fmtDollar, fmtUptime } from '../../lib/format';
@@ -58,13 +59,14 @@ const AgentNode = memo(({ data, selected }) => {
58
59
  const contextPct = Math.round((agent.contextUsage || 0) * 100);
59
60
  const sColor = statusColor(agent.status);
60
61
  const tokens = agent.tokensUsed || 0;
61
- const [hovered, setHovered] = useState(false);
62
62
  const nodeRef = useRef(null);
63
+ const expanded = useGrooveStore((s) => !!s.expandedNodes[agent.id]);
64
+ const toggleExpanded = useGrooveStore((s) => s.toggleNodeExpanded);
63
65
 
64
66
  useEffect(() => {
65
67
  const rfNode = nodeRef.current?.closest('.react-flow__node');
66
- if (rfNode) rfNode.style.zIndex = hovered ? '1000' : '';
67
- }, [hovered]);
68
+ if (rfNode) rfNode.style.zIndex = expanded ? '1000' : '';
69
+ }, [expanded]);
68
70
 
69
71
  const activityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
70
72
  const tokenTimeline = useGrooveStore((s) => s.tokenTimeline[agent.id]) || EMPTY;
@@ -84,18 +86,9 @@ const AgentNode = memo(({ data, selected }) => {
84
86
  : 0;
85
87
 
86
88
  return (
87
- <div
88
- ref={nodeRef}
89
- onMouseEnter={() => setHovered(true)}
90
- onMouseLeave={() => setHovered(false)}
91
- >
89
+ <div ref={nodeRef}>
92
90
  <div
93
- className="w-[220px] overflow-hidden transition-all duration-200 ease-out"
94
- style={{
95
- background: hovered ? '#141720' : '#1c1f26',
96
- border: `1px solid ${hovered ? '#2e3640' : selected ? '#2e323a' : '#262a32'}`,
97
- borderRadius: 4,
98
- }}
91
+ className={`w-[220px] overflow-hidden rounded-[4px] transition-all duration-200 ease-out bg-[#1c1f26] hover:bg-[#141720] border border-solid ${selected ? 'border-[#2e323a]' : 'border-[#262a32]'} hover:border-[#2e3640]`}
99
92
  >
100
93
  {/* Handles */}
101
94
  <Handle id="top" type="target" position={Position.Top} className="!w-1 !h-1 !bg-transparent !border-0" />
@@ -137,6 +130,12 @@ const AgentNode = memo(({ data, selected }) => {
137
130
  >
138
131
  {STATUS_SHORT[agent.status] || agent.status}
139
132
  </span>
133
+ <button
134
+ className="text-[#505862] hover:text-[#8b929e] cursor-pointer transition-colors flex-shrink-0"
135
+ onClick={(e) => { e.stopPropagation(); toggleExpanded(agent.id); }}
136
+ >
137
+ {expanded ? <X size={10} /> : <Maximize2 size={10} />}
138
+ </button>
140
139
  </div>
141
140
 
142
141
  <div className="flex items-center gap-1.5 mt-1.5">
@@ -169,10 +168,10 @@ const AgentNode = memo(({ data, selected }) => {
169
168
  </div>
170
169
  </div>
171
170
 
172
- {/* ── Expanded stats (morphs on hover) ────────── */}
171
+ {/* ── Expanded stats (click-to-open) ─────────── */}
173
172
  <div
174
173
  className="grid transition-[grid-template-rows] duration-200 ease-out"
175
- style={{ gridTemplateRows: hovered ? '1fr' : '0fr' }}
174
+ style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
176
175
  >
177
176
  <div className="overflow-hidden">
178
177
  <div className="mx-3 border-t border-white/[0.04]" />
@@ -11,7 +11,7 @@ import {
11
11
  Server, Monitor, Code2, TestTube, Cloud, FileText,
12
12
  Shield, Database, Megaphone, Calculator, UserCheck,
13
13
  Headphones, BarChart3, Rocket, ChevronDown, Pen, Presentation,
14
- Sparkles, X, Search, AlertTriangle, Plug,
14
+ Sparkles, X, Search, AlertTriangle, Plug, MessageCircle, GitBranch,
15
15
  } from 'lucide-react';
16
16
  import { api } from '../../lib/api';
17
17
  import { Dialog, DialogContent } from '../ui/dialog';
@@ -53,6 +53,7 @@ const INTEGRATION_LOGOS = {
53
53
  };
54
54
 
55
55
  const ROLE_PRESETS = [
56
+ { id: 'chat', label: 'Chat', desc: 'Companion, assistant, conversation', icon: MessageCircle, tier: 'Medium' },
56
57
  { id: 'planner', label: 'Planner', desc: 'Plans the team and tasks', icon: Rocket, tier: 'Heavy' },
57
58
  { id: 'backend', label: 'Backend', desc: 'APIs, services, databases', icon: Server, tier: 'Medium' },
58
59
  { id: 'frontend', label: 'Frontend', desc: 'UI, components, styling', icon: Monitor, tier: 'Medium' },
@@ -103,6 +104,13 @@ export function SpawnWizard() {
103
104
  const [integrationModalOpen, setIntegrationModalOpen] = useState(false);
104
105
  const [integrationSearch, setIntegrationSearch] = useState('');
105
106
  const [integrationApproval, setIntegrationApproval] = useState('manual');
107
+ const [importedRepos, setImportedRepos] = useState([]);
108
+ const [selectedRepos, setSelectedRepos] = useState([]);
109
+ const [repoModalOpen, setRepoModalOpen] = useState(false);
110
+ const [repoSearch, setRepoSearch] = useState('');
111
+ const [personalities, setPersonalities] = useState([]);
112
+ const [selectedPersonality, setSelectedPersonality] = useState('');
113
+ const [showAdvanced, setShowAdvanced] = useState(false);
106
114
  const [spawning, setSpawning] = useState(false);
107
115
 
108
116
  useEffect(() => {
@@ -124,10 +132,19 @@ export function SpawnWizard() {
124
132
  api.get('/integrations/installed').then((data) => {
125
133
  setInstalledIntegrations(Array.isArray(data) ? data : []);
126
134
  }).catch(() => {});
135
+ api.get('/repos/imported').then((data) => {
136
+ setImportedRepos((Array.isArray(data) ? data : []).filter((r) => r.status === 'active'));
137
+ }).catch(() => {});
138
+ api.get('/personalities').then((data) => {
139
+ setPersonalities(Array.isArray(data) ? data : data.personalities || []);
140
+ }).catch(() => {});
127
141
  setRole(''); setCustomRole(''); setName(''); setProvider(''); setModel(''); setPrompt('');
128
142
  setSelectedSkills([]);
129
143
  setSelectedIntegrations([]);
130
144
  setIntegrationApproval('manual');
145
+ setSelectedRepos([]);
146
+ setSelectedPersonality('');
147
+ setShowAdvanced(false);
131
148
  }
132
149
  }, [open, fetchProviders]);
133
150
 
@@ -149,6 +166,8 @@ export function SpawnWizard() {
149
166
  ...(selectedSkills.length > 0 && { skills: selectedSkills }),
150
167
  ...(selectedIntegrations.length > 0 && { integrations: selectedIntegrations }),
151
168
  ...(selectedIntegrations.length > 0 && { integrationApproval }),
169
+ ...(selectedRepos.length > 0 && { repos: selectedRepos }),
170
+ ...(selectedPersonality && { personality: selectedPersonality }),
152
171
  };
153
172
  await spawnAgent(config);
154
173
  closeDetail();
@@ -539,6 +558,127 @@ export function SpawnWizard() {
539
558
  </div>
540
559
  )}
541
560
 
561
+ {/* Repos */}
562
+ {importedRepos.length > 0 && (
563
+ <div className="space-y-1.5">
564
+ <label className="text-xs font-medium text-text-2 font-sans">Repos</label>
565
+ <div className="flex flex-wrap items-center gap-1.5">
566
+ {selectedRepos.map((importId) => {
567
+ const repo = importedRepos.find((r) => r.id === importId);
568
+ return (
569
+ <span
570
+ key={importId}
571
+ className="inline-flex items-center gap-1 px-2 py-1 rounded bg-accent/12 text-accent border border-accent/25 text-2xs font-sans"
572
+ >
573
+ <GitBranch size={9} />
574
+ {repo?.name || importId}
575
+ <button
576
+ onClick={() => setSelectedRepos((prev) => prev.filter((r) => r !== importId))}
577
+ className="ml-0.5 hover:text-text-0 cursor-pointer"
578
+ >
579
+ <X size={9} />
580
+ </button>
581
+ </span>
582
+ );
583
+ })}
584
+ <button
585
+ onClick={() => { setRepoModalOpen(true); setRepoSearch(''); }}
586
+ className={cn(
587
+ 'inline-flex items-center gap-1.5 px-2.5 py-1 rounded text-2xs font-sans transition-colors cursor-pointer',
588
+ 'bg-surface-0 text-text-2 border border-border-subtle hover:border-border hover:text-text-0',
589
+ )}
590
+ >
591
+ <GitBranch size={10} />
592
+ {selectedRepos.length > 0 ? 'Add repo' : 'Attach repo'}
593
+ </button>
594
+ </div>
595
+ </div>
596
+ )}
597
+
598
+ {/* Repo picker modal */}
599
+ <Dialog open={repoModalOpen} onOpenChange={setRepoModalOpen}>
600
+ <DialogContent title="Select Repository" className="max-w-sm">
601
+ <div className="space-y-3 p-4">
602
+ {importedRepos.length > 1 && (
603
+ <div className="relative">
604
+ <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-4" />
605
+ <input
606
+ value={repoSearch}
607
+ onChange={(e) => setRepoSearch(e.target.value)}
608
+ placeholder="Search repos..."
609
+ autoFocus
610
+ className="w-full h-8 pl-8 pr-3 text-xs rounded-md bg-surface-0 border border-border text-text-0 font-sans focus:outline-none focus:ring-1 focus:ring-accent"
611
+ />
612
+ </div>
613
+ )}
614
+ <div className="max-h-64 overflow-y-auto space-y-1">
615
+ {importedRepos
616
+ .filter((r) => {
617
+ if (!repoSearch) return true;
618
+ const q = repoSearch.toLowerCase();
619
+ return (r.name || r.repo || r.id).toLowerCase().includes(q);
620
+ })
621
+ .map((repo) => {
622
+ const active = selectedRepos.includes(repo.id);
623
+ return (
624
+ <button
625
+ key={repo.id}
626
+ onClick={() => {
627
+ setSelectedRepos((prev) =>
628
+ active ? prev.filter((r) => r !== repo.id) : [...prev, repo.id]
629
+ );
630
+ }}
631
+ className={cn(
632
+ 'w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-left transition-colors cursor-pointer',
633
+ active
634
+ ? 'bg-accent/10 border border-accent/25'
635
+ : 'hover:bg-surface-3 border border-transparent',
636
+ )}
637
+ >
638
+ <GitBranch size={12} className={active ? 'text-accent' : 'text-text-3'} />
639
+ <div className="flex-1 min-w-0">
640
+ <div className="text-xs font-semibold text-text-0 font-sans truncate">{repo.name || repo.repo}</div>
641
+ <div className="text-2xs text-text-4 font-mono truncate">{repo.clonedTo}</div>
642
+ </div>
643
+ {active && <CheckMark />}
644
+ </button>
645
+ );
646
+ })}
647
+ </div>
648
+ </div>
649
+ </DialogContent>
650
+ </Dialog>
651
+
652
+ {/* Personality — shown for chat role, or via Advanced toggle for others */}
653
+ {(role === 'chat' || showAdvanced) && (
654
+ <div className="space-y-1.5">
655
+ <label className="text-xs font-medium text-text-2 font-sans">Personality</label>
656
+ <div className="relative">
657
+ <select
658
+ value={selectedPersonality}
659
+ onChange={(e) => setSelectedPersonality(e.target.value)}
660
+ className="w-full h-8 px-3 pr-8 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent"
661
+ >
662
+ <option value="">None (blank)</option>
663
+ {personalities.map((p) => (
664
+ <option key={p.name || p} value={p.name || p}>{p.name || p}</option>
665
+ ))}
666
+ </select>
667
+ <ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none" />
668
+ </div>
669
+ <p className="text-2xs text-text-4 font-sans">Personality is injected into every prompt for this agent.</p>
670
+ </div>
671
+ )}
672
+
673
+ {role !== 'chat' && !showAdvanced && (
674
+ <button
675
+ onClick={() => setShowAdvanced(true)}
676
+ className="text-2xs text-text-3 hover:text-accent font-sans transition-colors cursor-pointer"
677
+ >
678
+ + Advanced options
679
+ </button>
680
+ )}
681
+
542
682
  </div>
543
683
  )}
544
684
  </div>
@@ -94,16 +94,16 @@ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
94
94
  {/* Context bar */}
95
95
  <div className="flex items-center gap-2">
96
96
  <div
97
- className="relative flex-1 h-[4px] rounded-full overflow-visible"
97
+ className="relative flex-1 h-0.5 rounded-sm overflow-visible"
98
98
  style={{ background: hexAlpha(HEX.accent, 0.12) }}
99
99
  >
100
100
  <div
101
- className="absolute inset-y-0 left-0 rounded-full transition-all duration-700"
101
+ className="absolute inset-y-0 left-0 rounded-sm transition-all duration-700"
102
102
  style={{ width: `${Math.max(contextPct, 1)}%`, background: barColor }}
103
103
  />
104
104
  {thresholdPct && (
105
105
  <div
106
- className="absolute top-[-2px] w-px h-[8px]"
106
+ className="absolute top-[-1px] w-px h-[4px]"
107
107
  style={{ left: `${thresholdPct}%`, background: HEX.purple }}
108
108
  title={`Rotation at ${thresholdPct}%`}
109
109
  />
@@ -61,8 +61,8 @@ function QualityBar({ score }) {
61
61
  const pct = Math.max(0, Math.min(100, score || 0));
62
62
  const color = qualityColor(score);
63
63
  return (
64
- <div className="h-1 rounded-full overflow-hidden flex-1" style={{ background: 'rgba(51,175,188,0.08)' }}>
65
- <div className="h-full rounded-full transition-all duration-700" style={{ width: `${pct}%`, background: color }} />
64
+ <div className="h-0.5 rounded-sm overflow-hidden flex-1" style={{ background: 'rgba(51,175,188,0.08)' }}>
65
+ <div className="h-full rounded-sm transition-all duration-700" style={{ width: `${pct}%`, background: color }} />
66
66
  </div>
67
67
  );
68
68
  }
@@ -81,9 +81,9 @@ function ProgressBar({ label, value, total, color }) {
81
81
  <span className="text-2xs font-mono text-text-3 tabular-nums w-10 text-right">{fmtNum(value)}</span>
82
82
  </div>
83
83
  </div>
84
- <div className="h-[7px] rounded-full overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
84
+ <div className="h-0.5 rounded-sm overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
85
85
  <div
86
- className="h-full rounded-full transition-all duration-500"
86
+ className="h-full rounded-sm transition-all duration-500"
87
87
  style={{ width: `${Math.min(pct, 100)}%`, background: color }}
88
88
  />
89
89
  </div>
@@ -118,7 +118,7 @@ function HealthTab({ tokens, rotation, agentBreakdown }) {
118
118
  Quality
119
119
  <InfoTip text="Average session quality score (0-100). Based on error rate, tool failures, repetitions, and file churn. Below 40 triggers auto-rotation to prevent wasted tokens." />
120
120
  </div>
121
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: qualityColor(avgQuality) }}>
121
+ <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
122
122
  {avgQuality ?? '—'}
123
123
  </div>
124
124
  </div>
@@ -127,7 +127,7 @@ function HealthTab({ tokens, rotation, agentBreakdown }) {
127
127
  Rotations
128
128
  <InfoTip text="Context rotations: quality-based (q), context threshold (c), and natural compactions (n) from provider-managed context resets. Each rotation preserves progress via a journalist handoff brief." />
129
129
  </div>
130
- <div className="text-2xl font-mono font-bold text-text-0 tabular-nums leading-none">
130
+ <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
131
131
  {rotation?.totalRotations || 0}
132
132
  </div>
133
133
  {(rotation?.qualityRotations > 0 || rotation?.contextRotations > 0 || rotation?.naturalCompactions > 0) && (
@@ -141,7 +141,7 @@ function HealthTab({ tokens, rotation, agentBreakdown }) {
141
141
  Cache
142
142
  <InfoTip text="Prompt cache hit rate. Cache reads are ~90% cheaper than regular input tokens. Managed by your AI provider — GROOVE tracks it, doesn't control it." />
143
143
  </div>
144
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: HEX.accent }}>
144
+ <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
145
145
  {fmtPct((tokens?.cacheHitRate || 0) * 100)}
146
146
  </div>
147
147
  </div>
@@ -150,7 +150,7 @@ function HealthTab({ tokens, rotation, agentBreakdown }) {
150
150
  Success
151
151
  <InfoTip text="Agent completion rate. Completed agents vs. crashed agents. High success rate means agents are finishing tasks without errors." />
152
152
  </div>
153
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: completionRate >= 90 ? '#4ae168' : '#e5c07b' }}>
153
+ <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
154
154
  {completionRate}%
155
155
  </div>
156
156
  </div>
@@ -34,7 +34,7 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
34
34
  <span className="text-2xs font-mono text-text-4 ml-auto tabular-nums">{fmtNum(total)} decisions</span>
35
35
  </div>
36
36
  {/* Stacked horizontal bar */}
37
- <div className="h-[6px] bg-surface-4 rounded-full overflow-hidden flex">
37
+ <div className="h-0.5 bg-surface-2 rounded-sm overflow-hidden flex">
38
38
  {tiers.map((tier) => {
39
39
  const count = byTier[tier] || 0;
40
40
  if (count === 0) return null;
@@ -86,9 +86,9 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
86
86
  <span className="text-2xs font-mono text-text-3 tabular-nums">{usage.agents} agent{usage.agents !== 1 ? 's' : ''}</span>
87
87
  <span className="text-xs font-mono text-text-1 tabular-nums">{fmtNum(usage.tokens)}</span>
88
88
  </div>
89
- <div className="h-[3px] bg-surface-4 rounded-full overflow-hidden">
89
+ <div className="h-0.5 bg-surface-4 rounded-sm overflow-hidden">
90
90
  <div
91
- className="h-full rounded-full transition-all duration-500"
91
+ className="h-full rounded-sm transition-all duration-500"
92
92
  style={{ width: `${Math.max(barPct, 2)}%`, background: HEX.accent }}
93
93
  />
94
94
  </div>
@@ -18,7 +18,7 @@ export const TeamBurnPanel = memo(function TeamBurnPanel({ teams = [] }) {
18
18
  {teams.length === 0 ? (
19
19
  <div className="px-3 py-6 text-center text-xs text-text-3 font-mono">No team activity yet</div>
20
20
  ) : (
21
- <div className="px-3 pb-2 space-y-1.5">
21
+ <div className="px-3 py-3 space-y-3">
22
22
  {teams.map((t) => {
23
23
  const pct = maxTokens > 0 ? (t.totalTokens / maxTokens) * 100 : 0;
24
24
  return (