groove-dev 0.26.20 → 0.26.22
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/node_modules/@groove-dev/daemon/src/introducer.js +8 -2
- package/node_modules/@groove-dev/daemon/src/process.js +94 -32
- package/node_modules/@groove-dev/gui/dist/assets/{index-CBgUozrt.js → index-DOejqkiH.js} +23 -23
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHRPn_ls.css → index-DomJ4Dgb.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/ui/thinking-indicator.jsx +35 -73
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -14
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +49 -18
- package/package.json +1 -1
- package/packages/daemon/src/introducer.js +8 -2
- package/packages/daemon/src/process.js +94 -32
- package/packages/gui/dist/assets/{index-CBgUozrt.js → index-DOejqkiH.js} +23 -23
- package/packages/gui/dist/assets/{index-CHRPn_ls.css → index-DomJ4Dgb.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +5 -5
- package/packages/gui/src/components/ui/thinking-indicator.jsx +35 -73
- package/packages/gui/src/stores/groove.js +39 -14
- package/packages/gui/src/views/agents.jsx +49 -18
|
@@ -1,89 +1,51 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'Planning approach',
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
['Mapping task scope...', 'Identifying constraints...', 'Outlining steps...'],
|
|
16
|
-
['Tracing logic flow...', 'Considering edge cases...', 'Weighing approaches...'],
|
|
17
|
-
['Comparing alternatives...', 'Checking tradeoffs...', 'Selecting best path...'],
|
|
4
|
+
const MESSAGES = [
|
|
5
|
+
'Reading through the codebase...',
|
|
6
|
+
'Thinking through your request...',
|
|
7
|
+
'Planning the approach...',
|
|
8
|
+
'Running tool calls...',
|
|
9
|
+
'Working through the problem...',
|
|
10
|
+
'Reasoning step by step...',
|
|
11
|
+
'Reviewing context...',
|
|
12
|
+
'Considering options...',
|
|
13
|
+
'Analyzing the code...',
|
|
14
|
+
'Making progress...',
|
|
18
15
|
];
|
|
19
16
|
|
|
20
17
|
export function ThinkingIndicator({ agent, className }) {
|
|
21
|
-
const [
|
|
22
|
-
const [
|
|
23
|
-
|
|
24
|
-
// Cycle phases every 3.5s
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const t = setInterval(() => setPhase((p) => (p + 1) % PHASES.length), 3500);
|
|
27
|
-
return () => clearInterval(t);
|
|
28
|
-
}, []);
|
|
18
|
+
const [idx, setIdx] = useState(0);
|
|
19
|
+
const [fade, setFade] = useState(true);
|
|
29
20
|
|
|
30
|
-
// Elapsed timer
|
|
31
21
|
useEffect(() => {
|
|
32
|
-
const t = setInterval(() =>
|
|
22
|
+
const t = setInterval(() => {
|
|
23
|
+
setFade(false);
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
setIdx((i) => (i + 1) % MESSAGES.length);
|
|
26
|
+
setFade(true);
|
|
27
|
+
}, 250);
|
|
28
|
+
}, 2800);
|
|
33
29
|
return () => clearInterval(t);
|
|
34
30
|
}, []);
|
|
35
31
|
|
|
36
|
-
const secs = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
|
|
37
|
-
|
|
38
32
|
return (
|
|
39
|
-
<div className={
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
<span className="absolute inset-0 rounded-full border-2 border-accent/15 animate-ping" style={{ animationDuration: '2.5s' }} />
|
|
49
|
-
<span className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin" style={{ animationDuration: '1.2s' }} />
|
|
50
|
-
<span className="absolute inset-[6px] rounded-full bg-accent/10" />
|
|
51
|
-
</div>
|
|
52
|
-
|
|
53
|
-
<div className="flex-1 min-w-0">
|
|
54
|
-
{agent ? (
|
|
55
|
-
<>
|
|
56
|
-
<p className="text-xs font-sans font-semibold text-text-1 truncate leading-tight">{agent.name}</p>
|
|
57
|
-
<p className="text-[10px] font-mono text-accent leading-tight mt-0.5">thinking</p>
|
|
58
|
-
</>
|
|
59
|
-
) : (
|
|
60
|
-
<p className="text-xs font-sans font-semibold text-text-1 leading-tight">Thinking</p>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<span className="text-xs font-mono text-text-2 tabular-nums flex-shrink-0">{secs}</span>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Phase label — re-keyed to replay fade-in on phase change */}
|
|
68
|
-
<div className="pl-3 border-l-2 border-accent/25 mb-3">
|
|
69
|
-
<span key={phase} className="block text-[13px] font-sans font-medium text-text-1 animate-phase-in">
|
|
70
|
-
{PHASES[phase]}
|
|
71
|
-
</span>
|
|
72
|
-
</div>
|
|
73
|
-
|
|
74
|
-
{/* Staggered detail lines — re-keyed to replay cascade on phase change */}
|
|
75
|
-
<div className="space-y-1.5 pl-3 border-l border-border-subtle">
|
|
76
|
-
{DETAILS[phase].map((line, i) => (
|
|
77
|
-
<div
|
|
78
|
-
key={`${phase}-${i}`}
|
|
79
|
-
className="flex items-center gap-2 animate-cascade-in"
|
|
80
|
-
style={{ animationDelay: `${150 + i * 200}ms` }}
|
|
81
|
-
>
|
|
82
|
-
<span className="w-1 h-1 rounded-full bg-accent/35 flex-shrink-0" />
|
|
83
|
-
<span className="text-[11px] font-mono text-text-3">{line}</span>
|
|
84
|
-
</div>
|
|
85
|
-
))}
|
|
33
|
+
<div className={`${className || ''}`}>
|
|
34
|
+
<div className="flex items-center gap-2 mb-1">
|
|
35
|
+
<span className="text-2xs font-semibold text-text-1 font-sans">{agent?.name || 'Agent'}</span>
|
|
36
|
+
<span className="text-2xs text-accent font-mono">thinking</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="border-l border-accent/40 pl-3.5 py-1 flex items-center gap-2.5">
|
|
39
|
+
{/* Spinning ring */}
|
|
40
|
+
<div className="relative w-3.5 h-3.5 flex-shrink-0">
|
|
41
|
+
<span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
|
|
86
42
|
</div>
|
|
43
|
+
<span
|
|
44
|
+
className="text-[12px] font-sans text-text-3 transition-opacity duration-[250ms]"
|
|
45
|
+
style={{ opacity: fade ? 1 : 0 }}
|
|
46
|
+
>
|
|
47
|
+
{MESSAGES[idx]}
|
|
48
|
+
</span>
|
|
87
49
|
</div>
|
|
88
50
|
</div>
|
|
89
51
|
);
|
|
@@ -136,15 +136,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
136
136
|
case 'agent:output': {
|
|
137
137
|
const { agentId, data } = msg;
|
|
138
138
|
|
|
139
|
-
// Clear thinking indicator when agent responds
|
|
140
|
-
if (get().thinkingAgents.has(agentId)) {
|
|
141
|
-
set((s) => {
|
|
142
|
-
const next = new Set(s.thinkingAgents);
|
|
143
|
-
next.delete(agentId);
|
|
144
|
-
return { thinkingAgents: next };
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
// Separate text content from tool calls
|
|
149
140
|
let chatText = '';
|
|
150
141
|
let activityText = '';
|
|
@@ -177,6 +168,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
177
168
|
(data.type === 'activity' && typeof data.data === 'string')
|
|
178
169
|
);
|
|
179
170
|
if (showAsChat) {
|
|
171
|
+
// Clear thinking indicator only when actual text renders as a chat bubble
|
|
172
|
+
if (get().thinkingAgents.has(agentId)) {
|
|
173
|
+
set((s) => {
|
|
174
|
+
const next = new Set(s.thinkingAgents);
|
|
175
|
+
next.delete(agentId);
|
|
176
|
+
return { thinkingAgents: next };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
180
|
const trimmed = chatText.trim();
|
|
181
181
|
const history = { ...get().chatHistory };
|
|
182
182
|
if (!history[agentId]) history[agentId] = [];
|
|
@@ -516,21 +516,46 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
516
516
|
async checkRecommendedTeam() {
|
|
517
517
|
try {
|
|
518
518
|
const data = await api.get('/recommended-team');
|
|
519
|
-
if (data
|
|
520
|
-
set({ recommendedTeam: data });
|
|
521
|
-
} else {
|
|
519
|
+
if (!data || !data.agents?.length) {
|
|
522
520
|
set({ recommendedTeam: null });
|
|
521
|
+
return;
|
|
523
522
|
}
|
|
523
|
+
|
|
524
|
+
// Check if all recommended roles already exist in the planner's team.
|
|
525
|
+
// If so, auto-delegate instead of showing the "Launch Team" modal.
|
|
526
|
+
const planners = get().agents.filter((a) => a.role === 'planner');
|
|
527
|
+
const planner = planners.sort((a, b) => (b.lastActivity || '').localeCompare(a.lastActivity || ''))[0];
|
|
528
|
+
const teamId = planner?.teamId;
|
|
529
|
+
|
|
530
|
+
if (teamId) {
|
|
531
|
+
const teamAgents = get().agents.filter((a) => a.teamId === teamId && a.role !== 'planner');
|
|
532
|
+
const phase1Roles = data.agents.filter((a) => !a.phase || a.phase === 1).map((a) => a.role);
|
|
533
|
+
const allExist = phase1Roles.every((role) => teamAgents.some((a) => a.role === role));
|
|
534
|
+
|
|
535
|
+
if (allExist && phase1Roles.length > 0) {
|
|
536
|
+
// Auto-delegate — all agents already exist in the team
|
|
537
|
+
set({ recommendedTeam: null });
|
|
538
|
+
const result = await api.post('/recommended-team/launch');
|
|
539
|
+
const names = result.agents?.map((a) => a.name).join(', ') || '';
|
|
540
|
+
get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
|
|
541
|
+
api.post('/cleanup').catch(() => {});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// New agents needed — show the modal for approval
|
|
547
|
+
set({ recommendedTeam: data });
|
|
524
548
|
} catch {
|
|
525
549
|
set({ recommendedTeam: null });
|
|
526
550
|
}
|
|
527
551
|
},
|
|
528
552
|
|
|
529
|
-
async launchRecommendedTeam() {
|
|
553
|
+
async launchRecommendedTeam(modifiedAgents) {
|
|
530
554
|
try {
|
|
531
555
|
set({ recommendedTeam: null }); // Dismiss modal immediately
|
|
532
556
|
get().addToast('info', 'Launching team...');
|
|
533
|
-
const
|
|
557
|
+
const body = modifiedAgents ? { agents: modifiedAgents } : undefined;
|
|
558
|
+
const result = await api.post('/recommended-team/launch', body);
|
|
534
559
|
const sub = [
|
|
535
560
|
result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
|
|
536
561
|
result.projectDir ? `→ ${result.projectDir}/` : '',
|
|
@@ -474,11 +474,17 @@ function EmptyState({ onPlanner, onSpawn }) {
|
|
|
474
474
|
|
|
475
475
|
const ROLE_ICONS = { backend: Server, frontend: Monitor, fullstack: Code2, testing: TestTube, security: Shield };
|
|
476
476
|
|
|
477
|
+
const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
478
|
+
|
|
479
|
+
function sanitizeName(raw) {
|
|
480
|
+
return raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
|
|
481
|
+
}
|
|
482
|
+
|
|
477
483
|
function RecommendedTeamCard() {
|
|
478
484
|
const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
|
|
479
485
|
const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
|
|
480
|
-
const checkRecommendedTeam = useGrooveStore((s) => s.checkRecommendedTeam);
|
|
481
486
|
const [launching, setLaunching] = useState(false);
|
|
487
|
+
const [editedAgents, setEditedAgents] = useState(null);
|
|
482
488
|
|
|
483
489
|
if (!recommendedTeam?.agents?.length) return null;
|
|
484
490
|
|
|
@@ -486,10 +492,23 @@ function RecommendedTeamCard() {
|
|
|
486
492
|
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
487
493
|
const phase2 = agents.filter((a) => a.phase === 2);
|
|
488
494
|
|
|
495
|
+
// Initialize edits lazily so we get fresh data if recommendedTeam changes
|
|
496
|
+
const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
|
|
497
|
+
|
|
498
|
+
function handleNameChange(i, raw) {
|
|
499
|
+
const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
|
|
500
|
+
setEditedAgents(next);
|
|
501
|
+
}
|
|
502
|
+
|
|
489
503
|
async function handleLaunch() {
|
|
490
504
|
setLaunching(true);
|
|
491
505
|
try {
|
|
492
|
-
|
|
506
|
+
// Merge edited phase1 names back with phase2 agents
|
|
507
|
+
const modified = [
|
|
508
|
+
...agentEdits,
|
|
509
|
+
...phase2,
|
|
510
|
+
];
|
|
511
|
+
await launchRecommendedTeam(modified);
|
|
493
512
|
} catch { /* toast handles */ }
|
|
494
513
|
setLaunching(false);
|
|
495
514
|
}
|
|
@@ -507,26 +526,38 @@ function RecommendedTeamCard() {
|
|
|
507
526
|
<button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
|
|
508
527
|
</div>
|
|
509
528
|
|
|
510
|
-
<div className="px-4 py-3 space-y-
|
|
511
|
-
{/* Phase 1 agents */}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
529
|
+
<div className="px-4 py-3 space-y-1.5">
|
|
530
|
+
{/* Phase 1 agents — editable rows */}
|
|
531
|
+
{agentEdits.map((a, i) => {
|
|
532
|
+
const Icon = ROLE_ICONS[a.role] || Code2;
|
|
533
|
+
const nameValid = !a.name || NAME_RE.test(a.name);
|
|
534
|
+
return (
|
|
535
|
+
<div key={i} className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-surface-4 border border-border-subtle">
|
|
536
|
+
<Icon size={12} className="text-text-2 shrink-0" />
|
|
537
|
+
<input
|
|
538
|
+
type="text"
|
|
539
|
+
value={a.name}
|
|
540
|
+
onChange={(e) => handleNameChange(i, e.target.value)}
|
|
541
|
+
placeholder={a.role}
|
|
542
|
+
className={cn(
|
|
543
|
+
'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
|
|
544
|
+
!nameValid && 'text-red-400',
|
|
521
545
|
)}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
546
|
+
maxLength={64}
|
|
547
|
+
spellCheck={false}
|
|
548
|
+
/>
|
|
549
|
+
{a.scope?.length > 0 && (
|
|
550
|
+
<span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
|
|
551
|
+
{a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
|
|
552
|
+
</span>
|
|
553
|
+
)}
|
|
554
|
+
</div>
|
|
555
|
+
);
|
|
556
|
+
})}
|
|
526
557
|
|
|
527
558
|
{/* Project dir indicator */}
|
|
528
559
|
{recommendedTeam.projectDir && (
|
|
529
|
-
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono">
|
|
560
|
+
<div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
|
|
530
561
|
<span className="text-text-4">Project:</span>
|
|
531
562
|
<span className="text-accent">{recommendedTeam.projectDir}/</span>
|
|
532
563
|
</div>
|