groove-dev 0.27.8 → 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 (124) hide show
  1. package/node_modules/@groove-dev/daemon/src/api.js +460 -25
  2. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  3. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  5. package/node_modules/@groove-dev/daemon/src/process.js +67 -7
  6. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  8. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  9. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  10. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  11. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  12. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  13. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  18. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  23. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  24. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +4 -4
  25. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  27. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  28. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  29. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  30. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  32. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  33. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  34. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  35. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  36. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  37. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  38. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  39. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  40. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  41. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  43. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  44. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  45. package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
  46. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
  47. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  48. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  49. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  50. package/package.json +7 -2
  51. package/packages/daemon/src/api.js +460 -25
  52. package/packages/daemon/src/index.js +7 -0
  53. package/packages/daemon/src/introducer.js +72 -4
  54. package/packages/daemon/src/journalist.js +66 -11
  55. package/packages/daemon/src/process.js +67 -7
  56. package/packages/daemon/src/registry.js +1 -1
  57. package/packages/daemon/src/repo-import.js +541 -0
  58. package/packages/daemon/src/rotator.js +28 -1
  59. package/packages/daemon/src/supervisor.js +2 -1
  60. package/packages/daemon/src/tunnel-manager.js +504 -0
  61. package/packages/daemon/src/validate.js +13 -0
  62. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  63. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  64. package/packages/gui/dist/index.html +2 -2
  65. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  69. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  71. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  72. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  73. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  74. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  75. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  76. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  77. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  78. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  80. package/packages/gui/src/app.css +14 -0
  81. package/packages/gui/src/app.jsx +13 -0
  82. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  83. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  84. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  85. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  86. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  87. package/packages/gui/src/components/dashboard/intel-panel.jsx +4 -4
  88. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  89. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  90. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  91. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  92. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  93. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  94. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  95. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  96. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  97. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  98. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  99. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  100. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  101. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  102. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  103. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  104. package/packages/gui/src/components/ui/toast.jsx +1 -1
  105. package/packages/gui/src/lib/edition.js +4 -0
  106. package/packages/gui/src/lib/electron.js +17 -0
  107. package/packages/gui/src/lib/status.js +1 -0
  108. package/packages/gui/src/stores/groove.js +139 -6
  109. package/packages/gui/src/views/dashboard.jsx +38 -39
  110. package/packages/gui/src/views/marketplace.jsx +82 -0
  111. package/packages/gui/src/views/settings.jsx +66 -0
  112. package/packages/gui/vite.config.js +3 -0
  113. package/integrations/FEDERATION_PLAN.md +0 -583
  114. package/integrations/VOICE_PLAN.md +0 -232
  115. package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
  116. package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
  117. package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
  118. package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
  119. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  120. package/test-slack.mjs +0 -28
  121. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  122. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  123. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-DiCjVtQL.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-zdzOLAZM.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-CwmR3-HY.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BE6lYcd7.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -201,6 +201,20 @@ html {
201
201
  filter: drop-shadow(0 0 4px rgba(97, 175, 239, 0.4));
202
202
  }
203
203
 
204
+ /* ── Electron Desktop App ────────────────────────────────── */
205
+
206
+ .electron-drag {
207
+ -webkit-app-region: drag;
208
+ }
209
+
210
+ .electron-no-drag-children button,
211
+ .electron-no-drag-children input,
212
+ .electron-no-drag-children a,
213
+ .electron-no-drag-children kbd,
214
+ .electron-no-drag-children select {
215
+ -webkit-app-region: no-drag;
216
+ }
217
+
204
218
  /* ── React Flow Overrides ─────────────────────────────────── */
205
219
 
206
220
  /* Suppress node fly-in animation — nodes appear in position instantly */
@@ -99,8 +99,21 @@ function LoadingScreen() {
99
99
  export default function App() {
100
100
  const connect = useGrooveStore((s) => s.connect);
101
101
  const hydrated = useGrooveStore((s) => s.hydrated);
102
+ const tunneled = useGrooveStore((s) => s.tunneled);
102
103
  useEffect(() => { connect(); }, [connect]);
103
104
 
105
+ useEffect(() => {
106
+ const params = new URLSearchParams(window.location.search);
107
+ const instance = params.get('instance');
108
+ if (instance) {
109
+ document.title = `${instance} — Groove`;
110
+ } else if (tunneled) {
111
+ document.title = 'Remote — Groove';
112
+ } else {
113
+ document.title = 'Groove';
114
+ }
115
+ }, [tunneled]);
116
+
104
117
  if (!hydrated) return <LoadingScreen />;
105
118
 
106
119
  return (
@@ -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>
@@ -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>
@@ -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 (
@@ -27,13 +27,13 @@ export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel
27
27
  <button
28
28
  onClick={() => onNavigate(item.id)}
29
29
  className={cn(
30
- 'w-10 h-10 flex items-center justify-center rounded-md transition-colors cursor-pointer',
30
+ 'w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer',
31
31
  activeView === item.id
32
32
  ? 'text-text-0 bg-surface-5'
33
33
  : 'text-text-3 hover:text-text-1 hover:bg-surface-4',
34
34
  )}
35
35
  >
36
- <item.icon size={20} strokeWidth={activeView === item.id ? 2 : 1.5} />
36
+ <item.icon size={16} strokeWidth={activeView === item.id ? 2 : 1.5} />
37
37
  </button>
38
38
  </Tooltip>
39
39
  ))}
@@ -53,13 +53,13 @@ export function ActivityBar({ activeView, detailPanel, onNavigate, onTogglePanel
53
53
  <button
54
54
  onClick={() => item.panel ? onTogglePanel(item.id) : onNavigate(item.id)}
55
55
  className={cn(
56
- 'w-10 h-10 flex items-center justify-center rounded-md transition-colors cursor-pointer',
56
+ 'w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer',
57
57
  isActive
58
58
  ? 'text-text-0 bg-surface-5'
59
59
  : 'text-text-3 hover:text-text-1 hover:bg-surface-4',
60
60
  )}
61
61
  >
62
- <item.icon size={20} strokeWidth={isActive ? 2 : 1.5} />
62
+ <item.icon size={16} strokeWidth={isActive ? 2 : 1.5} />
63
63
  </button>
64
64
  </Tooltip>
65
65
  );