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.
- package/CLAUDE.md +0 -7
- package/node_modules/@groove-dev/daemon/src/api.js +496 -44
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
- package/node_modules/@groove-dev/daemon/src/index.js +7 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
- package/node_modules/@groove-dev/daemon/src/process.js +128 -104
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
- package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +14 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
- package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
- package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
- package/node_modules/@groove-dev/gui/vite.config.js +3 -0
- package/package.json +7 -2
- package/packages/daemon/src/api.js +496 -44
- package/packages/daemon/src/gateways/manager.js +25 -12
- package/packages/daemon/src/index.js +7 -0
- package/packages/daemon/src/introducer.js +72 -4
- package/packages/daemon/src/journalist.js +66 -11
- package/packages/daemon/src/process.js +128 -104
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/repo-import.js +541 -0
- package/packages/daemon/src/rotator.js +28 -1
- package/packages/daemon/src/supervisor.js +2 -1
- package/packages/daemon/src/tunnel-manager.js +504 -0
- package/packages/daemon/src/validate.js +13 -0
- package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
- package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
- package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
- package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
- package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
- package/packages/gui/src/app.css +14 -0
- package/packages/gui/src/app.jsx +13 -0
- package/packages/gui/src/components/agents/agent-config.jsx +130 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/packages/gui/src/components/agents/agent-node.jsx +16 -17
- package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
- package/packages/gui/src/components/layout/app-shell.jsx +7 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/packages/gui/src/components/layout/command-palette.jsx +14 -4
- package/packages/gui/src/components/layout/status-bar.jsx +46 -11
- package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
- package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
- package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
- package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/lib/edition.js +4 -0
- package/packages/gui/src/lib/electron.js +17 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +150 -6
- package/packages/gui/src/views/dashboard.jsx +39 -40
- package/packages/gui/src/views/marketplace.jsx +82 -0
- package/packages/gui/src/views/settings.jsx +66 -0
- package/packages/gui/vite.config.js +3 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
- package/test-slack.mjs +0 -28
- /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
- /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-
|
|
372
|
+
<div className="w-14 h-0.5 rounded-sm bg-surface-4 overflow-hidden">
|
|
373
373
|
<div
|
|
374
|
-
className="h-full rounded-
|
|
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,
|
|
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 =
|
|
67
|
-
}, [
|
|
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=
|
|
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 (
|
|
171
|
+
{/* ── Expanded stats (click-to-open) ─────────── */}
|
|
173
172
|
<div
|
|
174
173
|
className="grid transition-[grid-template-rows] duration-200 ease-out"
|
|
175
|
-
style={{ gridTemplateRows:
|
|
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-
|
|
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-
|
|
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-[-
|
|
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-
|
|
65
|
-
<div className="h-full rounded-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
89
|
+
<div className="h-0.5 bg-surface-4 rounded-sm overflow-hidden">
|
|
90
90
|
<div
|
|
91
|
-
className="h-full rounded-
|
|
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
|
|
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 (
|