groove-dev 0.27.181 → 0.27.183
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/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/scripts/repair-tokens.mjs +78 -0
- package/node_modules/@groove-dev/daemon/src/api.js +1 -1
- package/node_modules/@groove-dev/daemon/src/deliver.js +130 -0
- package/node_modules/@groove-dev/daemon/src/index.js +8 -3
- package/node_modules/@groove-dev/daemon/src/innerchat.js +213 -62
- package/node_modules/@groove-dev/daemon/src/journalist.js +15 -8
- package/node_modules/@groove-dev/daemon/src/process.js +59 -34
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +9 -1
- package/node_modules/@groove-dev/daemon/src/registry.js +5 -1
- package/node_modules/@groove-dev/daemon/src/rename.js +72 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +62 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +22 -100
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +12 -9
- package/node_modules/@groove-dev/daemon/src/teams.js +7 -1
- package/node_modules/@groove-dev/daemon/src/tokentracker.js +46 -6
- package/node_modules/@groove-dev/daemon/test/claude-code-provider.test.js +52 -0
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +197 -111
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +4 -2
- package/node_modules/@groove-dev/daemon/test/rename.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +131 -0
- package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +4 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DTFtRtkx.css → index-CiOy7wVS.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-CTer01Vg.js → index-DPjGBQ5X.js} +225 -225
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +3 -68
- package/node_modules/@groove-dev/gui/src/components/agents/innerchat-relay.jsx +145 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +3 -1
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +15 -1
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +33 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +29 -44
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +27 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/scripts/repair-tokens.mjs +78 -0
- package/packages/daemon/src/api.js +1 -1
- package/packages/daemon/src/deliver.js +130 -0
- package/packages/daemon/src/index.js +8 -3
- package/packages/daemon/src/innerchat.js +213 -62
- package/packages/daemon/src/journalist.js +15 -8
- package/packages/daemon/src/process.js +59 -34
- package/packages/daemon/src/providers/claude-code.js +9 -1
- package/packages/daemon/src/registry.js +5 -1
- package/packages/daemon/src/rename.js +72 -0
- package/packages/daemon/src/rotator.js +62 -5
- package/packages/daemon/src/routes/agents.js +22 -100
- package/packages/daemon/src/routes/innerchat.js +12 -9
- package/packages/daemon/src/teams.js +7 -1
- package/packages/daemon/src/tokentracker.js +46 -6
- package/packages/gui/dist/assets/{index-DTFtRtkx.css → index-CiOy7wVS.css} +1 -1
- package/packages/gui/dist/assets/{index-CTer01Vg.js → index-DPjGBQ5X.js} +225 -225
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-panel.jsx +3 -68
- package/packages/gui/src/components/agents/innerchat-relay.jsx +145 -0
- package/packages/gui/src/components/chat/chat-messages.jsx +3 -1
- package/packages/gui/src/components/fleet/fleet-pane.jsx +15 -1
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +33 -1
- package/packages/gui/src/stores/groove.js +29 -44
- package/packages/gui/src/stores/slices/agents-slice.js +27 -4
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DPjGBQ5X.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CiOy7wVS.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -6,7 +6,8 @@ import { AgentFeed } from './agent-feed';
|
|
|
6
6
|
import { AgentConfig } from './agent-config';
|
|
7
7
|
import { AgentTelemetry } from './agent-telemetry';
|
|
8
8
|
import { AgentMdFiles } from './agent-mdfiles';
|
|
9
|
-
import {
|
|
9
|
+
import { InnerChatRelay } from './innerchat-relay';
|
|
10
|
+
import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X } from 'lucide-react';
|
|
10
11
|
import { fmtNum, fmtUptime } from '../../lib/format';
|
|
11
12
|
import { cn } from '../../lib/cn';
|
|
12
13
|
import { roleColor } from '../../lib/status';
|
|
@@ -77,72 +78,6 @@ function InlineName({ agent }) {
|
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
function InnerChatCompose({ fromAgent, onClose }) {
|
|
81
|
-
const agents = useGrooveStore((s) => s.agents);
|
|
82
|
-
const sendInnerChat = useGrooveStore((s) => s.sendInnerChat);
|
|
83
|
-
const addToast = useGrooveStore((s) => s.addToast);
|
|
84
|
-
const [toId, setToId] = useState('');
|
|
85
|
-
const [message, setMessage] = useState('');
|
|
86
|
-
const [sending, setSending] = useState(false);
|
|
87
|
-
|
|
88
|
-
const others = agents.filter((a) => a.id !== fromAgent.id && (a.status === 'running' || a.status === 'starting' || a.status === 'completed'));
|
|
89
|
-
|
|
90
|
-
async function handleSend() {
|
|
91
|
-
if (!toId || !message.trim() || sending) return;
|
|
92
|
-
setSending(true);
|
|
93
|
-
try {
|
|
94
|
-
await sendInnerChat(fromAgent.id, toId, message.trim());
|
|
95
|
-
const target = agents.find((a) => a.id === toId);
|
|
96
|
-
addToast('success', `Sent to ${target?.name || toId}`, 'InnerChat message delivered');
|
|
97
|
-
setMessage('');
|
|
98
|
-
onClose();
|
|
99
|
-
} catch (err) {
|
|
100
|
-
addToast('error', 'InnerChat failed', err.message);
|
|
101
|
-
} finally {
|
|
102
|
-
setSending(false);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="flex-shrink-0 border-t border-border-subtle bg-surface-0 px-3 py-2.5 space-y-2">
|
|
108
|
-
<div className="flex items-center justify-between">
|
|
109
|
-
<span className="text-2xs font-semibold text-warning font-sans">InnerChat — relay to agent</span>
|
|
110
|
-
<button onClick={onClose} className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"><X size={12} /></button>
|
|
111
|
-
</div>
|
|
112
|
-
<div className="relative">
|
|
113
|
-
<select
|
|
114
|
-
value={toId}
|
|
115
|
-
onChange={(e) => setToId(e.target.value)}
|
|
116
|
-
className="w-full h-7 pl-2 pr-6 text-xs bg-surface-3 border border-border-subtle rounded text-text-0 font-sans focus:outline-none focus:border-accent/40 appearance-none cursor-pointer"
|
|
117
|
-
>
|
|
118
|
-
<option value="">Select target agent…</option>
|
|
119
|
-
{others.map((a) => (
|
|
120
|
-
<option key={a.id} value={a.id}>{a.name} ({a.role})</option>
|
|
121
|
-
))}
|
|
122
|
-
</select>
|
|
123
|
-
<ChevronDown size={10} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
|
|
124
|
-
</div>
|
|
125
|
-
<div className="flex gap-1.5">
|
|
126
|
-
<textarea
|
|
127
|
-
value={message}
|
|
128
|
-
onChange={(e) => setMessage(e.target.value)}
|
|
129
|
-
onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleSend(); }}
|
|
130
|
-
placeholder="Message to relay… (⌘↵ to send)"
|
|
131
|
-
rows={3}
|
|
132
|
-
className="flex-1 px-2 py-1.5 text-xs bg-surface-3 border border-border-subtle rounded text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:border-accent/40 resize-none"
|
|
133
|
-
/>
|
|
134
|
-
<button
|
|
135
|
-
onClick={handleSend}
|
|
136
|
-
disabled={!toId || !message.trim() || sending}
|
|
137
|
-
className="w-8 flex-shrink-0 flex items-center justify-center rounded bg-accent/15 text-accent hover:bg-accent/25 disabled:opacity-30 disabled:cursor-not-allowed transition-colors cursor-pointer"
|
|
138
|
-
>
|
|
139
|
-
<SendHorizontal size={14} />
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
81
|
export function AgentPanel() {
|
|
147
82
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
148
83
|
const agents = useGrooveStore((s) => s.agents);
|
|
@@ -251,7 +186,7 @@ export function AgentPanel() {
|
|
|
251
186
|
<AgentFeed agent={agent} />
|
|
252
187
|
</div>
|
|
253
188
|
{innerChatOpen && (
|
|
254
|
-
<
|
|
189
|
+
<InnerChatRelay fromAgent={agent} onClose={() => setInnerChatOpen(false)} />
|
|
255
190
|
)}
|
|
256
191
|
</>
|
|
257
192
|
)}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { SendHorizontal, ChevronDown, X } from 'lucide-react';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
|
|
7
|
+
// Relay compose panel — pick any agent on any team and send a message on this
|
|
8
|
+
// agent's behalf. The reply is forwarded back automatically, so the user only
|
|
9
|
+
// drives the outbound hop.
|
|
10
|
+
export function InnerChatRelay({ fromAgent, onClose }) {
|
|
11
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
12
|
+
const teams = useGrooveStore((s) => s.teams);
|
|
13
|
+
const threadsMap = useGrooveStore((s) => s.innerchatThreads);
|
|
14
|
+
const sendInnerChat = useGrooveStore((s) => s.sendInnerChat);
|
|
15
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
16
|
+
|
|
17
|
+
const [toId, setToId] = useState('');
|
|
18
|
+
const [threadId, setThreadId] = useState('');
|
|
19
|
+
const [message, setMessage] = useState('');
|
|
20
|
+
const [sending, setSending] = useState(false);
|
|
21
|
+
|
|
22
|
+
// Every agent except this one, grouped by team. Stopped agents are included:
|
|
23
|
+
// delivery resumes them the same way a user chat message would.
|
|
24
|
+
const grouped = useMemo(() => {
|
|
25
|
+
const byTeam = new Map();
|
|
26
|
+
for (const a of agents) {
|
|
27
|
+
if (a.id === fromAgent.id) continue;
|
|
28
|
+
const key = a.teamId || 'untimed';
|
|
29
|
+
if (!byTeam.has(key)) byTeam.set(key, []);
|
|
30
|
+
byTeam.get(key).push(a);
|
|
31
|
+
}
|
|
32
|
+
return Array.from(byTeam.entries()).map(([teamId, list]) => ({
|
|
33
|
+
teamId,
|
|
34
|
+
name: teams.find((t) => t.id === teamId)?.name || 'No team',
|
|
35
|
+
agents: list.sort((a, b) => a.name.localeCompare(b.name)),
|
|
36
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
37
|
+
}, [agents, teams, fromAgent.id]);
|
|
38
|
+
|
|
39
|
+
// Existing exchanges between this agent and the selected target.
|
|
40
|
+
const threads = useMemo(() => {
|
|
41
|
+
if (!toId) return [];
|
|
42
|
+
return Object.values(threadsMap)
|
|
43
|
+
.filter((t) => t.participants.some((p) => p.id === fromAgent.id)
|
|
44
|
+
&& t.participants.some((p) => p.id === toId))
|
|
45
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
46
|
+
}, [threadsMap, toId, fromAgent.id]);
|
|
47
|
+
|
|
48
|
+
const target = agents.find((a) => a.id === toId);
|
|
49
|
+
const willResume = target && target.status !== 'running' && target.status !== 'starting';
|
|
50
|
+
|
|
51
|
+
async function handleSend() {
|
|
52
|
+
if (!toId || !message.trim() || sending) return;
|
|
53
|
+
setSending(true);
|
|
54
|
+
try {
|
|
55
|
+
await sendInnerChat(fromAgent.id, toId, message.trim(), threadId || null);
|
|
56
|
+
addToast('success', `Relayed to ${target?.name || toId}`,
|
|
57
|
+
willResume ? 'Target is being resumed to receive it' : undefined);
|
|
58
|
+
setMessage('');
|
|
59
|
+
onClose();
|
|
60
|
+
} catch {
|
|
61
|
+
// sendInnerChat already surfaced the error
|
|
62
|
+
} finally {
|
|
63
|
+
setSending(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex-shrink-0 border-t border-border-subtle bg-surface-0 px-3 py-2.5 space-y-2">
|
|
69
|
+
<div className="flex items-center justify-between">
|
|
70
|
+
<span className="text-2xs font-semibold text-warning font-sans">
|
|
71
|
+
Relay from {fromAgent.name}
|
|
72
|
+
</span>
|
|
73
|
+
<button onClick={onClose} className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer">
|
|
74
|
+
<X size={12} />
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="relative">
|
|
79
|
+
<select
|
|
80
|
+
value={toId}
|
|
81
|
+
onChange={(e) => { setToId(e.target.value); setThreadId(''); }}
|
|
82
|
+
className="w-full h-7 pl-2 pr-6 text-xs bg-surface-3 border border-border-subtle rounded text-text-0 font-sans focus:outline-none focus:border-accent/40 appearance-none cursor-pointer"
|
|
83
|
+
>
|
|
84
|
+
<option value="">Select target agent…</option>
|
|
85
|
+
{grouped.map((g) => (
|
|
86
|
+
<optgroup key={g.teamId} label={g.name}>
|
|
87
|
+
{g.agents.map((a) => (
|
|
88
|
+
<option key={a.id} value={a.id}>
|
|
89
|
+
{a.name} ({a.role}){a.status === 'running' ? '' : ` — ${a.status}`}
|
|
90
|
+
</option>
|
|
91
|
+
))}
|
|
92
|
+
</optgroup>
|
|
93
|
+
))}
|
|
94
|
+
</select>
|
|
95
|
+
<ChevronDown size={10} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{threads.length > 0 && (
|
|
99
|
+
<div className="relative">
|
|
100
|
+
<select
|
|
101
|
+
value={threadId}
|
|
102
|
+
onChange={(e) => setThreadId(e.target.value)}
|
|
103
|
+
className="w-full h-7 pl-2 pr-6 text-xs bg-surface-3 border border-border-subtle rounded text-text-2 font-sans focus:outline-none focus:border-accent/40 appearance-none cursor-pointer"
|
|
104
|
+
>
|
|
105
|
+
<option value="">Start a new thread</option>
|
|
106
|
+
{threads.map((t) => (
|
|
107
|
+
<option key={t.id} value={t.id}>
|
|
108
|
+
Continue thread ({t.turns.length} turn{t.turns.length === 1 ? '' : 's'})
|
|
109
|
+
</option>
|
|
110
|
+
))}
|
|
111
|
+
</select>
|
|
112
|
+
<ChevronDown size={10} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-4 pointer-events-none" />
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<div className="flex gap-1.5">
|
|
117
|
+
<textarea
|
|
118
|
+
value={message}
|
|
119
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
120
|
+
onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleSend(); }}
|
|
121
|
+
placeholder={`What should ${fromAgent.name} say? (⌘↵ to send)`}
|
|
122
|
+
rows={3}
|
|
123
|
+
className="flex-1 px-2 py-1.5 text-xs bg-surface-3 border border-border-subtle rounded text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:border-accent/40 resize-none"
|
|
124
|
+
/>
|
|
125
|
+
<button
|
|
126
|
+
onClick={handleSend}
|
|
127
|
+
disabled={!toId || !message.trim() || sending}
|
|
128
|
+
className={cn(
|
|
129
|
+
'w-8 flex-shrink-0 flex items-center justify-center rounded transition-colors cursor-pointer',
|
|
130
|
+
'bg-accent/15 text-accent hover:bg-accent/25',
|
|
131
|
+
'disabled:opacity-30 disabled:cursor-not-allowed',
|
|
132
|
+
)}
|
|
133
|
+
>
|
|
134
|
+
<SendHorizontal size={14} />
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{willResume && (
|
|
139
|
+
<p className="text-2xs text-text-4 font-sans">
|
|
140
|
+
{target.name} is {target.status} — it will be resumed to receive this.
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -399,7 +399,9 @@ function InnerChatMessage({ msg }) {
|
|
|
399
399
|
<div className="max-w-[85%]">
|
|
400
400
|
<div className="flex items-center gap-1.5 mb-1">
|
|
401
401
|
<MessageSquare size={11} className="text-warning" />
|
|
402
|
-
<span className="text-2xs font-sans font-medium text-warning">
|
|
402
|
+
<span className="text-2xs font-sans font-medium text-warning">
|
|
403
|
+
{msg.innerchat?.kind === 'reply' ? 'InnerChat reply' : 'InnerChat'}
|
|
404
|
+
</span>
|
|
403
405
|
<span className="text-2xs font-sans text-text-3">from</span>
|
|
404
406
|
<span className="text-2xs font-sans font-medium text-text-1">{senderName}</span>
|
|
405
407
|
{senderRole && <span className="text-2xs font-sans text-text-3">{senderRole}</span>}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { X, PanelRight } from 'lucide-react';
|
|
3
|
+
import { X, PanelRight, MessageSquare } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { Badge } from '../ui/badge';
|
|
7
7
|
import { AgentFeed } from '../agents/agent-feed';
|
|
8
|
+
import { InnerChatRelay } from '../agents/innerchat-relay';
|
|
8
9
|
import { fmtNum } from '../../lib/format';
|
|
9
10
|
|
|
10
11
|
const STATUS_VARIANT = {
|
|
@@ -39,6 +40,7 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
39
40
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
40
41
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
41
42
|
const isPanelOpen = detailPanel?.type === 'agent' && detailPanel?.agentId === resolvedId;
|
|
43
|
+
const [relayOpen, setRelayOpen] = useState(false);
|
|
42
44
|
|
|
43
45
|
const lastAgentRef = useRef(liveAgent);
|
|
44
46
|
const [gone, setGone] = useState(false);
|
|
@@ -95,6 +97,16 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
95
97
|
)}>
|
|
96
98
|
{ctxPct}%
|
|
97
99
|
</span>
|
|
100
|
+
<button
|
|
101
|
+
onClick={() => setRelayOpen((v) => !v)}
|
|
102
|
+
className={cn(
|
|
103
|
+
'p-1 rounded-md transition-colors cursor-pointer',
|
|
104
|
+
relayOpen ? 'text-warning' : 'text-text-3 hover:text-warning hover:bg-surface-3',
|
|
105
|
+
)}
|
|
106
|
+
title="Relay a message to another agent"
|
|
107
|
+
>
|
|
108
|
+
<MessageSquare size={14} />
|
|
109
|
+
</button>
|
|
98
110
|
<button
|
|
99
111
|
onClick={() => openDetail({ type: 'agent', agentId: resolvedId })}
|
|
100
112
|
className={cn(
|
|
@@ -118,6 +130,8 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
118
130
|
<div className="flex-1 min-h-0">
|
|
119
131
|
<AgentFeed agent={agent} readOnly={readOnly} />
|
|
120
132
|
</div>
|
|
133
|
+
|
|
134
|
+
{relayOpen && <InnerChatRelay fromAgent={agent} onClose={() => setRelayOpen(false)} />}
|
|
121
135
|
</div>
|
|
122
136
|
);
|
|
123
137
|
}
|
|
@@ -26,6 +26,9 @@ export function FleetSidebar({ width }) {
|
|
|
26
26
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
27
27
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
28
28
|
|
|
29
|
+
const moveAgentToTeam = useGrooveStore((s) => s.moveAgentToTeam);
|
|
30
|
+
|
|
31
|
+
const [dropTeamId, setDropTeamId] = useState(null);
|
|
29
32
|
const [confirmDeleteTeam, setConfirmDeleteTeam] = useState(null);
|
|
30
33
|
const [renamingTeamId, setRenamingTeamId] = useState(null);
|
|
31
34
|
const [renameValue, setRenameValue] = useState('');
|
|
@@ -125,6 +128,22 @@ export function FleetSidebar({ width }) {
|
|
|
125
128
|
setRenamingTeamId(null);
|
|
126
129
|
}
|
|
127
130
|
|
|
131
|
+
// Team headers accept agent rows dragged from any other team.
|
|
132
|
+
function handleDragOver(e, teamId) {
|
|
133
|
+
if (!e.dataTransfer.types.includes('application/x-fleet-agent')) return;
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
e.dataTransfer.dropEffect = 'link';
|
|
136
|
+
if (dropTeamId !== teamId) setDropTeamId(teamId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleDrop(e, teamId) {
|
|
140
|
+
const agentId = e.dataTransfer.getData('application/x-fleet-agent');
|
|
141
|
+
setDropTeamId(null);
|
|
142
|
+
if (!agentId) return;
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
moveAgentToTeam(agentId, teamId);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
function startRename(e, team) {
|
|
129
148
|
e.stopPropagation();
|
|
130
149
|
setRenamingTeamId(team.id);
|
|
@@ -167,7 +186,20 @@ export function FleetSidebar({ width }) {
|
|
|
167
186
|
const isConfirming = confirmDeleteTeam === team.id;
|
|
168
187
|
|
|
169
188
|
return (
|
|
170
|
-
<div
|
|
189
|
+
<div
|
|
190
|
+
key={team.id}
|
|
191
|
+
className={cn(
|
|
192
|
+
'mb-0.5 rounded-md transition-colors',
|
|
193
|
+
dropTeamId === team.id && 'bg-accent/10 ring-1 ring-accent/40',
|
|
194
|
+
)}
|
|
195
|
+
onDragOver={(e) => handleDragOver(e, team.id)}
|
|
196
|
+
onDragLeave={(e) => {
|
|
197
|
+
// Ignore moves onto descendants — they still count as "inside".
|
|
198
|
+
if (e.currentTarget.contains(e.relatedTarget)) return;
|
|
199
|
+
setDropTeamId((id) => (id === team.id ? null : id));
|
|
200
|
+
}}
|
|
201
|
+
onDrop={(e) => handleDrop(e, team.id)}
|
|
202
|
+
>
|
|
171
203
|
{/* Team header */}
|
|
172
204
|
<div className={cn(
|
|
173
205
|
'w-full flex items-center gap-1 px-2 py-1.5 rounded-md hover:bg-surface-2 transition-colors group',
|
|
@@ -369,54 +369,39 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
369
369
|
break;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
case 'innerchat:
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
icMsgs[ic.id] = ic;
|
|
378
|
-
persistJSON('groove:innerchatMessages', icMsgs);
|
|
379
|
-
return { innerchatMessages: icMsgs };
|
|
380
|
-
});
|
|
381
|
-
// Tag the chat message with innerchat metadata
|
|
382
|
-
set((s) => {
|
|
383
|
-
const history = { ...s.chatHistory };
|
|
384
|
-
const arr = history[ic.to.id] || [];
|
|
385
|
-
if (arr.length > 0) {
|
|
386
|
-
const last = arr[arr.length - 1];
|
|
387
|
-
if (last.from === 'innerchat' && last.text === ic.message) {
|
|
388
|
-
arr[arr.length - 1] = { ...last, innerchat: { messageId: ic.id, fromAgent: ic.from } };
|
|
389
|
-
history[ic.to.id] = arr;
|
|
390
|
-
persistJSON('groove:chatHistory', history);
|
|
391
|
-
return { chatHistory: history };
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return {};
|
|
395
|
-
});
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
372
|
+
case 'innerchat:turn': {
|
|
373
|
+
const { thread, turn } = msg.data;
|
|
374
|
+
if (turn.status === 'failed') {
|
|
375
|
+
get().addToast('error', `Relay to ${turn.to.name} failed`, turn.error);
|
|
376
|
+
}
|
|
398
377
|
|
|
399
|
-
|
|
400
|
-
const ic = msg.data;
|
|
401
|
-
get().addChatMessage(ic.from.id, 'innerchat', ic.response, false);
|
|
378
|
+
// The turn lands in the recipient's chat, badged with who sent it.
|
|
402
379
|
set((s) => {
|
|
403
|
-
const icMsgs = { ...s.innerchatMessages };
|
|
404
|
-
icMsgs[ic.id] = ic;
|
|
405
|
-
persistJSON('groove:innerchatMessages', icMsgs);
|
|
406
|
-
// Tag the response chat message with innerchat metadata
|
|
407
380
|
const history = { ...s.chatHistory };
|
|
408
|
-
const arr = history[
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
381
|
+
const arr = [...(history[turn.to.id] || [])];
|
|
382
|
+
arr.push({
|
|
383
|
+
from: 'innerchat',
|
|
384
|
+
text: turn.text,
|
|
385
|
+
timestamp: turn.timestamp,
|
|
386
|
+
isQuery: false,
|
|
387
|
+
innerchat: {
|
|
388
|
+
turnId: turn.id,
|
|
389
|
+
threadId: thread.id,
|
|
390
|
+
kind: turn.kind,
|
|
391
|
+
fromAgent: turn.from,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
history[turn.to.id] = arr.slice(-100);
|
|
395
|
+
persistJSON('groove:chatHistory', history);
|
|
396
|
+
|
|
397
|
+
const threads = { ...s.innerchatThreads, [thread.id]: thread };
|
|
398
|
+
persistJSON('groove:innerchatThreads', threads);
|
|
399
|
+
return { chatHistory: history, innerchatThreads: threads };
|
|
418
400
|
});
|
|
419
|
-
|
|
401
|
+
|
|
402
|
+
if (turn.kind === 'reply' && turn.status !== 'failed') {
|
|
403
|
+
get().addToast('info', `${turn.from.name} replied`, `Forwarded to ${turn.to.name}`);
|
|
404
|
+
}
|
|
420
405
|
break;
|
|
421
406
|
}
|
|
422
407
|
|
|
@@ -104,20 +104,43 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
106
|
|
|
107
|
+
// Move an agent to another team. Teams are an organizational grouping —
|
|
108
|
+
// this re-labels the agent and does NOT relocate its working directory, so a
|
|
109
|
+
// running process keeps working where it is.
|
|
110
|
+
async moveAgentToTeam(agentId, teamId) {
|
|
111
|
+
const agent = get().agents.find((a) => a.id === agentId);
|
|
112
|
+
if (!agent || agent.teamId === teamId) return;
|
|
113
|
+
const team = get().teams.find((t) => t.id === teamId);
|
|
114
|
+
try {
|
|
115
|
+
await api.patch(`/agents/${encodeURIComponent(agentId)}`, { teamId });
|
|
116
|
+
get().addToast('success', `Moved ${agent.name} → ${team?.name || 'team'}`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
get().addToast('error', 'Move failed', err.message);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
107
122
|
// ── InnerChat ─────────────────────────────────────────────
|
|
108
123
|
|
|
109
|
-
|
|
124
|
+
innerchatThreads: loadJSON('groove:innerchatThreads') || {},
|
|
110
125
|
|
|
111
|
-
|
|
126
|
+
// Relay a message from one agent to another. Pass threadId to continue an
|
|
127
|
+
// existing exchange rather than opening a new one.
|
|
128
|
+
async sendInnerChat(fromId, toId, message, threadId = null) {
|
|
112
129
|
try {
|
|
113
|
-
|
|
114
|
-
return msg;
|
|
130
|
+
return await api.post('/innerchat/send', { from: fromId, to: toId, message, threadId });
|
|
115
131
|
} catch (err) {
|
|
116
132
|
get().addToast('error', 'InnerChat failed', err.message);
|
|
117
133
|
throw err;
|
|
118
134
|
}
|
|
119
135
|
},
|
|
120
136
|
|
|
137
|
+
// Threads this agent is a participant in, most recent first.
|
|
138
|
+
innerchatThreadsFor(agentId) {
|
|
139
|
+
return Object.values(get().innerchatThreads)
|
|
140
|
+
.filter((t) => t.participants.some((p) => p.id === agentId))
|
|
141
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
142
|
+
},
|
|
143
|
+
|
|
121
144
|
// ── Chat ──────────────────────────────────────────────────
|
|
122
145
|
|
|
123
146
|
addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
|