groove-dev 0.27.183 → 0.27.185
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/.watch-test-flag-1784767927904 +1 -0
- package/.watch-test-flag-1784768055729 +1 -0
- package/.watch-test-flag-1784768066364 +1 -0
- 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/src/api.js +2 -0
- package/node_modules/@groove-dev/daemon/src/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +96 -0
- package/node_modules/@groove-dev/daemon/src/innerchat.js +265 -96
- package/node_modules/@groove-dev/daemon/src/introducer.js +31 -1
- package/node_modules/@groove-dev/daemon/src/process.js +36 -6
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +109 -13
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +11 -0
- package/node_modules/@groove-dev/daemon/src/routes/watch.js +42 -0
- package/node_modules/@groove-dev/daemon/src/teams.js +28 -0
- package/node_modules/@groove-dev/daemon/src/watcher.js +258 -0
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +222 -179
- package/node_modules/@groove-dev/daemon/test/teams.test.js +53 -0
- package/node_modules/@groove-dev/daemon/test/watcher.test.js +158 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CU8L_r5f.css +1 -0
- package/{packages/gui/dist/assets/index-DPjGBQ5X.js → node_modules/@groove-dev/gui/dist/assets/index-DEZwM4Hb.js} +233 -228
- 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/app.css +1 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +60 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +1 -29
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +1 -15
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +29 -11
- package/node_modules/@groove-dev/gui/src/stores/groove.js +36 -16
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +6 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +22 -0
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +56 -31
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +2 -0
- package/packages/daemon/src/index.js +17 -0
- package/packages/daemon/src/innerchat-docs.js +96 -0
- package/packages/daemon/src/innerchat.js +265 -96
- package/packages/daemon/src/introducer.js +31 -1
- package/packages/daemon/src/process.js +36 -6
- package/packages/daemon/src/routes/innerchat.js +109 -13
- package/packages/daemon/src/routes/teams.js +11 -0
- package/packages/daemon/src/routes/watch.js +42 -0
- package/packages/daemon/src/teams.js +28 -0
- package/packages/daemon/src/watcher.js +258 -0
- package/packages/gui/dist/assets/index-CU8L_r5f.css +1 -0
- package/{node_modules/@groove-dev/gui/dist/assets/index-DPjGBQ5X.js → packages/gui/dist/assets/index-DEZwM4Hb.js} +233 -228
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +1 -0
- package/packages/gui/src/components/agents/agent-feed.jsx +60 -4
- package/packages/gui/src/components/agents/agent-panel.jsx +1 -29
- package/packages/gui/src/components/fleet/fleet-agent-row.jsx +18 -2
- package/packages/gui/src/components/fleet/fleet-pane.jsx +1 -15
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +29 -11
- package/packages/gui/src/stores/groove.js +36 -16
- package/packages/gui/src/stores/slices/agents-slice.js +6 -0
- package/packages/gui/src/stores/slices/teams-slice.js +22 -0
- package/packages/gui/src/views/memory.jsx +56 -31
- package/node_modules/@groove-dev/gui/dist/assets/index-CiOy7wVS.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/innerchat-relay.jsx +0 -145
- package/packages/gui/dist/assets/index-CiOy7wVS.css +0 -1
- package/packages/gui/src/components/agents/innerchat-relay.jsx +0 -145
|
@@ -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-DEZwM4Hb.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-CU8L_r5f.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
Loader2, MessageSquare, SendHorizontal, Pause,
|
|
5
5
|
FileEdit, Search, Terminal, CheckCircle2, AlertCircle,
|
|
6
6
|
RotateCw, Zap, Wrench, Eye, Code2, Bug,
|
|
7
|
-
ChevronDown, Paperclip, GripHorizontal,
|
|
7
|
+
ChevronDown, Paperclip, GripHorizontal, ArrowLeftRight,
|
|
8
8
|
FileCode, X, File, Image as ImageIcon, Film, Upload,
|
|
9
9
|
} from 'lucide-react';
|
|
10
10
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
@@ -16,6 +16,7 @@ import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
|
16
16
|
import { TableTree } from '../ui/table-tree';
|
|
17
17
|
|
|
18
18
|
const EMPTY = [];
|
|
19
|
+
const EMPTY_MAP = {};
|
|
19
20
|
const KEEPER_RE = /(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|#[\w/.-]+)/gi;
|
|
20
21
|
const KEEPER_CMD_RE = /^\[(?:save|append|update|delete|view|doc|link|read|instruct)\]$/i;
|
|
21
22
|
const KEEPER_TAG_RE = /^#[\w/.-]+$/;
|
|
@@ -339,7 +340,45 @@ function UserMessage({ msg }) {
|
|
|
339
340
|
);
|
|
340
341
|
}
|
|
341
342
|
|
|
342
|
-
|
|
343
|
+
// An agent-to-agent turn. Deliberately styled apart from the normal feed —
|
|
344
|
+
// amber rail and an explicit "X → Y" header — so it reads as traffic between
|
|
345
|
+
// agents rather than something addressed to the user.
|
|
346
|
+
function InnerChatMessage({ msg, agent }) {
|
|
347
|
+
const [collapsed, setCollapsed] = useState(msg.text?.length > 400);
|
|
348
|
+
const isLong = msg.text?.length > 400;
|
|
349
|
+
const { peer, direction, kind } = msg.innerchat || {};
|
|
350
|
+
const outbound = direction === 'out';
|
|
351
|
+
const me = agent?.name || 'this agent';
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className="rounded-md bg-indigo/[0.04] border border-indigo/15 px-2.5 py-2">
|
|
355
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
356
|
+
<ArrowLeftRight size={10} className="text-indigo flex-shrink-0" />
|
|
357
|
+
<span className="text-2xs font-semibold text-indigo font-sans">InnerChat</span>
|
|
358
|
+
<span className="text-2xs text-text-3 font-sans truncate">
|
|
359
|
+
{outbound ? `${me} → ${peer?.name}` : `${peer?.name} → ${me}`}
|
|
360
|
+
</span>
|
|
361
|
+
<span className="text-2xs text-text-4 font-sans">
|
|
362
|
+
{kind === 'answer' ? 'answer' : outbound ? 'asked' : 'asks'}
|
|
363
|
+
</span>
|
|
364
|
+
<span className="text-[10px] text-text-4 font-sans ml-auto flex-shrink-0">{timeAgo(msg.timestamp)}</span>
|
|
365
|
+
</div>
|
|
366
|
+
<div className="border-l-2 border-indigo/40 pl-3">
|
|
367
|
+
<StructuredMessage text={collapsed ? msg.text.slice(0, 400) + '...' : msg.text} />
|
|
368
|
+
</div>
|
|
369
|
+
{isLong && (
|
|
370
|
+
<button
|
|
371
|
+
onClick={() => setCollapsed((c) => !c)}
|
|
372
|
+
className="mt-1 text-2xs text-text-4 hover:text-indigo font-sans cursor-pointer"
|
|
373
|
+
>
|
|
374
|
+
{collapsed ? 'Show more' : 'Show less'}
|
|
375
|
+
</button>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function AgentMessage({ msg, agent, answeredTo }) {
|
|
343
382
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 600);
|
|
344
383
|
const isLong = msg.text?.length > 600;
|
|
345
384
|
|
|
@@ -348,9 +387,16 @@ function AgentMessage({ msg, agent }) {
|
|
|
348
387
|
<div className="flex items-center gap-2 mb-1">
|
|
349
388
|
<span className="text-2xs font-semibold text-text-1 font-sans">{agent?.name || 'Agent'}</span>
|
|
350
389
|
<span className="text-2xs text-text-4 font-sans">{agent?.role}</span>
|
|
390
|
+
{/* This reply went to another agent, not to the user */}
|
|
391
|
+
{answeredTo && (
|
|
392
|
+
<span className="flex items-center gap-1 px-1.5 py-px rounded bg-indigo/10 text-2xs font-medium text-indigo font-sans">
|
|
393
|
+
<ArrowLeftRight size={9} />
|
|
394
|
+
to {answeredTo.name}
|
|
395
|
+
</span>
|
|
396
|
+
)}
|
|
351
397
|
<span className="text-[10px] text-text-4 font-sans ml-auto">{timeAgo(msg.timestamp)}</span>
|
|
352
398
|
</div>
|
|
353
|
-
<div className=
|
|
399
|
+
<div className={cn('pl-3.5 py-1 border-l', answeredTo ? 'border-indigo/50' : 'border-accent')}>
|
|
354
400
|
<StructuredMessage text={collapsed ? msg.text.slice(0, 600) + '...' : msg.text} />
|
|
355
401
|
</div>
|
|
356
402
|
{collapsed && (
|
|
@@ -606,6 +652,8 @@ export function AgentFeed({ agent, readOnly = false }) {
|
|
|
606
652
|
const rawActivityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
|
|
607
653
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
608
654
|
const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
|
|
655
|
+
// Replies this agent sent to other agents, keyed by text — see innerchatAnswers.
|
|
656
|
+
const answerPeers = useGrooveStore((s) => s.innerchatAnswers?.[agent.id]) || EMPTY_MAP;
|
|
609
657
|
const cachedChatRef = useRef(EMPTY);
|
|
610
658
|
const cachedActivityRef = useRef(EMPTY);
|
|
611
659
|
const cachedAgentIdRef = useRef(agent.id);
|
|
@@ -886,7 +934,15 @@ export function AgentFeed({ agent, readOnly = false }) {
|
|
|
886
934
|
}
|
|
887
935
|
if (item.from === 'user') return <UserMessage key={`user-${item._chatIdx}`} msg={item} />;
|
|
888
936
|
if (item.from === 'system') return <SystemMessage key={`sys-${item._chatIdx}`} msg={item} />;
|
|
889
|
-
return <
|
|
937
|
+
if (item.from === 'innerchat') return <InnerChatMessage key={`ic-${item._chatIdx}`} msg={item} agent={agent} />;
|
|
938
|
+
return (
|
|
939
|
+
<AgentMessage
|
|
940
|
+
key={`agent-${item._chatIdx}`}
|
|
941
|
+
msg={item}
|
|
942
|
+
agent={agent}
|
|
943
|
+
answeredTo={answerPeers[item.text?.trim()]?.peer}
|
|
944
|
+
/>
|
|
945
|
+
);
|
|
890
946
|
})}
|
|
891
947
|
<AnimatePresence>
|
|
892
948
|
{(sending || isThinking) && (
|
|
@@ -6,7 +6,6 @@ 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 { InnerChatRelay } from './innerchat-relay';
|
|
10
9
|
import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X } from 'lucide-react';
|
|
11
10
|
import { fmtNum, fmtUptime } from '../../lib/format';
|
|
12
11
|
import { cn } from '../../lib/cn';
|
|
@@ -86,8 +85,6 @@ export function AgentPanel() {
|
|
|
86
85
|
const [activeTab, setActiveTab] = useState('command');
|
|
87
86
|
const cachedAgentRef = useRef(null);
|
|
88
87
|
|
|
89
|
-
const [innerChatOpen, setInnerChatOpen] = useState(false);
|
|
90
|
-
|
|
91
88
|
const agentId = detailPanel?.type === 'agent' ? detailPanel.agentId : null;
|
|
92
89
|
const liveAgent = agentId ? agents.find((a) => a.id === agentId) : null;
|
|
93
90
|
if (liveAgent) cachedAgentRef.current = liveAgent;
|
|
@@ -159,37 +156,12 @@ export function AgentPanel() {
|
|
|
159
156
|
</button>
|
|
160
157
|
);
|
|
161
158
|
})}
|
|
162
|
-
<div className="flex-1" />
|
|
163
|
-
{activeTab === 'command' && (
|
|
164
|
-
<button
|
|
165
|
-
onClick={() => setInnerChatOpen((v) => !v)}
|
|
166
|
-
className={cn(
|
|
167
|
-
'flex items-center gap-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-colors cursor-pointer border-b-2 -mb-px',
|
|
168
|
-
innerChatOpen
|
|
169
|
-
? 'text-warning border-warning/60'
|
|
170
|
-
: 'text-text-4 border-transparent hover:text-warning',
|
|
171
|
-
)}
|
|
172
|
-
title="InnerChat — relay message to another agent"
|
|
173
|
-
>
|
|
174
|
-
<MessageSquare size={11} />
|
|
175
|
-
Relay
|
|
176
|
-
</button>
|
|
177
|
-
)}
|
|
178
159
|
</div>
|
|
179
160
|
</div>
|
|
180
161
|
|
|
181
162
|
{/* ── Tab Content ────────────────────────────────────── */}
|
|
182
163
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
183
|
-
{activeTab === 'command' &&
|
|
184
|
-
<>
|
|
185
|
-
<div className="flex-1 min-h-0 overflow-hidden">
|
|
186
|
-
<AgentFeed agent={agent} />
|
|
187
|
-
</div>
|
|
188
|
-
{innerChatOpen && (
|
|
189
|
-
<InnerChatRelay fromAgent={agent} onClose={() => setInnerChatOpen(false)} />
|
|
190
|
-
)}
|
|
191
|
-
</>
|
|
192
|
-
)}
|
|
164
|
+
{activeTab === 'command' && <AgentFeed agent={agent} />}
|
|
193
165
|
{activeTab === 'config' && <AgentConfig agent={agent} />}
|
|
194
166
|
{activeTab === 'telemetry' && <AgentTelemetry agent={agent} />}
|
|
195
167
|
{activeTab === 'mdfiles' && <AgentMdFiles agent={agent} />}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import { X } from 'lucide-react';
|
|
3
|
+
import { X, Pencil } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { api } from '../../lib/api';
|
|
@@ -26,6 +26,7 @@ export function FleetAgentRow({ agent }) {
|
|
|
26
26
|
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
27
27
|
const fleetMarkRead = useGrooveStore((s) => s.fleetMarkRead);
|
|
28
28
|
const killAgent = useGrooveStore((s) => s.killAgent);
|
|
29
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
29
30
|
const unreadTs = useGrooveStore((s) => s.fleetUnreadMap[agent.id]);
|
|
30
31
|
const chatHistory = useGrooveStore((s) => s.chatHistory[agent.id]);
|
|
31
32
|
|
|
@@ -69,7 +70,10 @@ export function FleetAgentRow({ agent }) {
|
|
|
69
70
|
if (!trimmed || trimmed === agent.name) return;
|
|
70
71
|
try {
|
|
71
72
|
await api.patch(`/agents/${agent.id}`, { name: trimmed });
|
|
72
|
-
} catch {
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Renames can legitimately fail — duplicate name, illegal characters.
|
|
75
|
+
addToast('error', 'Rename failed', err.message);
|
|
76
|
+
}
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
function handleKeyDown(e) {
|
|
@@ -158,6 +162,18 @@ export function FleetAgentRow({ agent }) {
|
|
|
158
162
|
<span className="group-hover:hidden w-1.5 h-1.5 rounded-full bg-accent flex-shrink-0" />
|
|
159
163
|
)}
|
|
160
164
|
|
|
165
|
+
{/* Rename — an explicit affordance, since double-click competes with
|
|
166
|
+
the row's drag handler and click-to-select */}
|
|
167
|
+
{!editing && !confirmKill && (
|
|
168
|
+
<button
|
|
169
|
+
onClick={(e) => { e.stopPropagation(); setEditing(true); setEditName(agent.name); }}
|
|
170
|
+
className="hidden group-hover:flex items-center justify-center p-0.5 rounded text-text-4 hover:text-accent transition-colors cursor-pointer flex-shrink-0"
|
|
171
|
+
title="Rename agent"
|
|
172
|
+
>
|
|
173
|
+
<Pencil size={11} />
|
|
174
|
+
</button>
|
|
175
|
+
)}
|
|
176
|
+
|
|
161
177
|
{/* Remove button (visible on hover) */}
|
|
162
178
|
{!editing && (
|
|
163
179
|
<button
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { X, PanelRight
|
|
3
|
+
import { X, PanelRight } 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';
|
|
9
8
|
import { fmtNum } from '../../lib/format';
|
|
10
9
|
|
|
11
10
|
const STATUS_VARIANT = {
|
|
@@ -40,7 +39,6 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
40
39
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
41
40
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
42
41
|
const isPanelOpen = detailPanel?.type === 'agent' && detailPanel?.agentId === resolvedId;
|
|
43
|
-
const [relayOpen, setRelayOpen] = useState(false);
|
|
44
42
|
|
|
45
43
|
const lastAgentRef = useRef(liveAgent);
|
|
46
44
|
const [gone, setGone] = useState(false);
|
|
@@ -97,16 +95,6 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
97
95
|
)}>
|
|
98
96
|
{ctxPct}%
|
|
99
97
|
</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>
|
|
110
98
|
<button
|
|
111
99
|
onClick={() => openDetail({ type: 'agent', agentId: resolvedId })}
|
|
112
100
|
className={cn(
|
|
@@ -130,8 +118,6 @@ export function FleetPane({ agentId, paneIndex, readOnly = false }) {
|
|
|
130
118
|
<div className="flex-1 min-h-0">
|
|
131
119
|
<AgentFeed agent={agent} readOnly={readOnly} />
|
|
132
120
|
</div>
|
|
133
|
-
|
|
134
|
-
{relayOpen && <InnerChatRelay fromAgent={agent} onClose={() => setRelayOpen(false)} />}
|
|
135
121
|
</div>
|
|
136
122
|
);
|
|
137
123
|
}
|
|
@@ -5,6 +5,9 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { FleetAgentRow } from './fleet-agent-row';
|
|
7
7
|
|
|
8
|
+
const AGENT_DRAG = 'application/x-fleet-agent';
|
|
9
|
+
const TEAM_DRAG = 'application/x-fleet-team';
|
|
10
|
+
|
|
8
11
|
function teamStatusDot(agents) {
|
|
9
12
|
if (agents.some((a) => a.status === 'crashed')) return 'bg-danger';
|
|
10
13
|
if (agents.some((a) => a.status === 'running' || a.status === 'starting')) return 'bg-accent';
|
|
@@ -27,6 +30,7 @@ export function FleetSidebar({ width }) {
|
|
|
27
30
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
28
31
|
|
|
29
32
|
const moveAgentToTeam = useGrooveStore((s) => s.moveAgentToTeam);
|
|
33
|
+
const reorderTeams = useGrooveStore((s) => s.reorderTeams);
|
|
30
34
|
|
|
31
35
|
const [dropTeamId, setDropTeamId] = useState(null);
|
|
32
36
|
const [confirmDeleteTeam, setConfirmDeleteTeam] = useState(null);
|
|
@@ -128,20 +132,27 @@ export function FleetSidebar({ width }) {
|
|
|
128
132
|
setRenamingTeamId(null);
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
//
|
|
135
|
+
// A team block accepts two kinds of drop: an agent row (reassign its team)
|
|
136
|
+
// or another team header (reorder the list).
|
|
132
137
|
function handleDragOver(e, teamId) {
|
|
133
|
-
|
|
138
|
+
const { types } = e.dataTransfer;
|
|
139
|
+
const isAgent = types.includes(AGENT_DRAG);
|
|
140
|
+
const isTeam = types.includes(TEAM_DRAG);
|
|
141
|
+
if (!isAgent && !isTeam) return;
|
|
134
142
|
e.preventDefault();
|
|
135
|
-
e.dataTransfer.dropEffect = 'link';
|
|
143
|
+
e.dataTransfer.dropEffect = isAgent ? 'link' : 'move';
|
|
136
144
|
if (dropTeamId !== teamId) setDropTeamId(teamId);
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
function handleDrop(e, teamId) {
|
|
140
|
-
const agentId = e.dataTransfer.getData('application/x-fleet-agent');
|
|
141
148
|
setDropTeamId(null);
|
|
142
|
-
|
|
149
|
+
const agentId = e.dataTransfer.getData(AGENT_DRAG);
|
|
150
|
+
const draggedTeamId = e.dataTransfer.getData(TEAM_DRAG);
|
|
151
|
+
if (!agentId && !draggedTeamId) return;
|
|
143
152
|
e.preventDefault();
|
|
144
|
-
|
|
153
|
+
|
|
154
|
+
if (agentId) moveAgentToTeam(agentId, teamId);
|
|
155
|
+
else if (draggedTeamId !== teamId) reorderTeams(draggedTeamId, teamId);
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
function startRename(e, team) {
|
|
@@ -200,11 +211,18 @@ export function FleetSidebar({ width }) {
|
|
|
200
211
|
}}
|
|
201
212
|
onDrop={(e) => handleDrop(e, team.id)}
|
|
202
213
|
>
|
|
203
|
-
{/* Team header */}
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
214
|
+
{/* Team header — draggable to reorder the sidebar list */}
|
|
215
|
+
<div
|
|
216
|
+
draggable={renamingTeamId !== team.id}
|
|
217
|
+
onDragStart={(e) => {
|
|
218
|
+
e.dataTransfer.setData(TEAM_DRAG, team.id);
|
|
219
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
220
|
+
}}
|
|
221
|
+
className={cn(
|
|
222
|
+
'w-full flex items-center gap-1 px-2 py-1.5 rounded-md hover:bg-surface-2 transition-colors group',
|
|
223
|
+
isConfirming && 'bg-danger/10 hover:bg-danger/20',
|
|
224
|
+
)}
|
|
225
|
+
>
|
|
208
226
|
{renamingTeamId === team.id ? (
|
|
209
227
|
<div className="flex items-center gap-1.5 flex-1 min-w-0 pl-1">
|
|
210
228
|
<input
|
|
@@ -372,36 +372,50 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
372
372
|
case 'innerchat:turn': {
|
|
373
373
|
const { thread, turn } = msg.data;
|
|
374
374
|
if (turn.status === 'failed') {
|
|
375
|
-
get().addToast('error', `
|
|
375
|
+
get().addToast('error', `InnerChat to ${turn.to.name} failed`, turn.error);
|
|
376
|
+
break;
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
// The turn lands in the recipient's chat, badged with who sent it.
|
|
379
379
|
set((s) => {
|
|
380
380
|
const history = { ...s.chatHistory };
|
|
381
|
-
const
|
|
382
|
-
arr.push({
|
|
381
|
+
const entry = (peer, direction) => ({
|
|
383
382
|
from: 'innerchat',
|
|
384
383
|
text: turn.text,
|
|
385
384
|
timestamp: turn.timestamp,
|
|
386
385
|
isQuery: false,
|
|
387
|
-
innerchat: {
|
|
388
|
-
turnId: turn.id,
|
|
389
|
-
threadId: thread.id,
|
|
390
|
-
kind: turn.kind,
|
|
391
|
-
fromAgent: turn.from,
|
|
392
|
-
},
|
|
386
|
+
innerchat: { turnId: turn.id, threadId: thread.id, kind: turn.kind, peer, direction },
|
|
393
387
|
});
|
|
394
|
-
|
|
388
|
+
const push = (agentId, item) => {
|
|
389
|
+
history[agentId] = [...(history[agentId] || []), item].slice(-100);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (turn.kind === 'ask') {
|
|
393
|
+
// Both sides see the question — the asker as outgoing, the
|
|
394
|
+
// target as incoming — so neither feed has an unexplained gap.
|
|
395
|
+
push(turn.from.id, entry(turn.to, 'out'));
|
|
396
|
+
push(turn.to.id, entry(turn.from, 'in'));
|
|
397
|
+
} else {
|
|
398
|
+
// The answer reaches the asker over HTTP, so only it needs a
|
|
399
|
+
// feed entry. The responder already shows this text as its own
|
|
400
|
+
// agent message; that one gets badged via innerchatAnswers
|
|
401
|
+
// rather than duplicated here.
|
|
402
|
+
push(turn.to.id, entry(turn.from, 'in'));
|
|
403
|
+
}
|
|
404
|
+
|
|
395
405
|
persistJSON('groove:chatHistory', history);
|
|
396
406
|
|
|
407
|
+
const answers = { ...s.innerchatAnswers };
|
|
408
|
+
if (turn.kind === 'answer') {
|
|
409
|
+
answers[turn.from.id] = {
|
|
410
|
+
...(answers[turn.from.id] || {}),
|
|
411
|
+
[turn.text.trim()]: { peer: turn.to, threadId: thread.id },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
397
415
|
const threads = { ...s.innerchatThreads, [thread.id]: thread };
|
|
398
416
|
persistJSON('groove:innerchatThreads', threads);
|
|
399
|
-
return { chatHistory: history, innerchatThreads: threads };
|
|
417
|
+
return { chatHistory: history, innerchatThreads: threads, innerchatAnswers: answers };
|
|
400
418
|
});
|
|
401
|
-
|
|
402
|
-
if (turn.kind === 'reply' && turn.status !== 'failed') {
|
|
403
|
-
get().addToast('info', `${turn.from.name} replied`, `Forwarded to ${turn.to.name}`);
|
|
404
|
-
}
|
|
405
419
|
break;
|
|
406
420
|
}
|
|
407
421
|
|
|
@@ -605,6 +619,12 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
605
619
|
get().fetchTeams();
|
|
606
620
|
break;
|
|
607
621
|
|
|
622
|
+
// Reordering already applied optimistically on the client that did it;
|
|
623
|
+
// this keeps every other window in sync.
|
|
624
|
+
case 'teams:reordered':
|
|
625
|
+
if (msg.teams) set({ teams: msg.teams });
|
|
626
|
+
break;
|
|
627
|
+
|
|
608
628
|
case 'approval:request':
|
|
609
629
|
set((s) => ({ pendingApprovals: [...s.pendingApprovals, msg.data] }));
|
|
610
630
|
break;
|
|
@@ -123,6 +123,12 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
123
123
|
|
|
124
124
|
innerchatThreads: loadJSON('groove:innerchatThreads') || {},
|
|
125
125
|
|
|
126
|
+
// agentId -> { <answer text>: { peer, threadId } }. An agent's answer to
|
|
127
|
+
// another agent arrives as an ordinary result, indistinguishable in its own
|
|
128
|
+
// feed from a message meant for the user — this is what lets the feed badge
|
|
129
|
+
// it. Keyed by text so it works regardless of which event lands first.
|
|
130
|
+
innerchatAnswers: {},
|
|
131
|
+
|
|
126
132
|
// Relay a message from one agent to another. Pass threadId to continue an
|
|
127
133
|
// existing exchange rather than opening a new one.
|
|
128
134
|
async sendInnerChat(fromId, toId, message, threadId = null) {
|
|
@@ -173,6 +173,28 @@ export const createTeamsSlice = (set, get) => ({
|
|
|
173
173
|
}
|
|
174
174
|
},
|
|
175
175
|
|
|
176
|
+
// Move a team to a new index in the sidebar. Applied optimistically so the
|
|
177
|
+
// drop lands instantly, then reconciled with whatever the daemon persisted.
|
|
178
|
+
async reorderTeams(draggedId, targetId) {
|
|
179
|
+
const current = get().teams;
|
|
180
|
+
const from = current.findIndex((t) => t.id === draggedId);
|
|
181
|
+
const to = current.findIndex((t) => t.id === targetId);
|
|
182
|
+
if (from === -1 || to === -1 || from === to) return;
|
|
183
|
+
|
|
184
|
+
const next = [...current];
|
|
185
|
+
const [moved] = next.splice(from, 1);
|
|
186
|
+
next.splice(to, 0, moved);
|
|
187
|
+
set({ teams: next });
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const { teams } = await api.post('/teams/reorder', { orderedIds: next.map((t) => t.id) });
|
|
191
|
+
if (teams) set({ teams });
|
|
192
|
+
} catch (err) {
|
|
193
|
+
set({ teams: current });
|
|
194
|
+
get().addToast('error', 'Failed to reorder teams', err.message);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
176
198
|
async promoteTeam(id) {
|
|
177
199
|
try {
|
|
178
200
|
const team = await api.post(`/teams/${encodeURIComponent(id)}/promote`);
|
|
@@ -6,16 +6,19 @@ import { ScrollArea } from '../components/ui/scroll-area';
|
|
|
6
6
|
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
7
7
|
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical, CornerLeftUp } from 'lucide-react';
|
|
8
8
|
|
|
9
|
+
// `cmd` is the bare word; the UI always renders it bracketed so the real
|
|
10
|
+
// syntax — [cmd] #tag — is never ambiguous. `example` shows a full, copyable
|
|
11
|
+
// line so a new user can see exactly what to type.
|
|
9
12
|
const COMMANDS = [
|
|
10
|
-
{ cmd: 'save',
|
|
11
|
-
{ cmd: 'append',
|
|
12
|
-
{ cmd: 'update',
|
|
13
|
-
{ cmd: '
|
|
14
|
-
{ cmd: '
|
|
15
|
-
{ cmd: '
|
|
16
|
-
{ cmd: '
|
|
17
|
-
{ cmd: '
|
|
18
|
-
{ cmd: '
|
|
13
|
+
{ cmd: 'save', example: '[save] #api-auth', desc: 'Save the current message as a new memory under #api-auth' },
|
|
14
|
+
{ cmd: 'append', example: '[append] #api-auth', desc: 'Add the current message to the existing #api-auth memory' },
|
|
15
|
+
{ cmd: 'update', example: '[update] #api-auth', desc: 'Open the editor to change the #api-auth memory' },
|
|
16
|
+
{ cmd: 'view', example: '[view] #api-auth', desc: 'Open a memory to read it, without sending it to the agent' },
|
|
17
|
+
{ cmd: 'read', example: '[read] #api-auth #db-schema', desc: 'Send one or more memories to the agent as context (chat stays clean)' },
|
|
18
|
+
{ cmd: 'doc', example: '[doc] #api-auth', desc: 'Let the AI synthesize the conversation into a memory' },
|
|
19
|
+
{ cmd: 'link', example: '[link] #api-auth docs/spec.md', desc: 'Link a memory to a NORTHSTAR or external document' },
|
|
20
|
+
{ cmd: 'delete', example: '[delete] #api-auth', desc: 'Delete a memory permanently' },
|
|
21
|
+
{ cmd: 'instruct', example: '[instruct]', desc: 'Show this command reference' },
|
|
19
22
|
];
|
|
20
23
|
|
|
21
24
|
function formatRelative(iso) {
|
|
@@ -219,36 +222,58 @@ function EditorModal({ open, onOpenChange, editing, onSave, onRename }) {
|
|
|
219
222
|
function InstructModal({ open, onOpenChange }) {
|
|
220
223
|
return (
|
|
221
224
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
222
|
-
<DialogContent title="
|
|
225
|
+
<DialogContent title="Memory Commands" description="How to save and recall tagged memories" className="max-w-lg">
|
|
223
226
|
<div className="p-5 space-y-4">
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
227
|
+
{/* Format primer — the part new users miss */}
|
|
228
|
+
<div className="rounded-lg bg-surface-2 border border-border-subtle p-3 space-y-2">
|
|
229
|
+
<p className="text-xs text-text-2 leading-relaxed">
|
|
230
|
+
Type a command in any agent chat. The format is always the command in
|
|
231
|
+
<span className="font-mono text-accent"> [square brackets]</span>, then a
|
|
232
|
+
<span className="font-mono text-accent"> #tag</span>:
|
|
233
|
+
</p>
|
|
234
|
+
<div className="flex items-center gap-2 font-mono text-sm">
|
|
235
|
+
<span className="px-1.5 py-0.5 rounded bg-accent/15 text-accent font-semibold">[save]</span>
|
|
236
|
+
<span className="px-1.5 py-0.5 rounded bg-info/10 text-info font-semibold">#api-auth</span>
|
|
237
|
+
<span className="text-text-3 text-xs font-sans">your message here</span>
|
|
238
|
+
</div>
|
|
239
|
+
<p className="text-xs text-text-4 leading-relaxed">
|
|
240
|
+
These commands are handled by GROOVE — the agent never sees them.
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div className="space-y-1">
|
|
228
245
|
{COMMANDS.map((c) => (
|
|
229
246
|
<div key={c.cmd} className="flex items-start gap-3 py-1.5 border-b border-border-subtle last:border-0">
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
<code className="flex-shrink-0 w-44 text-xs font-mono text-text-1 pt-0.5">
|
|
248
|
+
{c.example.split(/(\[[^\]]+\]|#[\w/-]+)/).map((part, i) => {
|
|
249
|
+
if (/^\[.+\]$/.test(part)) return <span key={i} className="text-accent font-semibold">{part}</span>;
|
|
250
|
+
if (/^#/.test(part)) return <span key={i} className="text-info">{part}</span>;
|
|
251
|
+
return <span key={i} className="text-text-4">{part}</span>;
|
|
252
|
+
})}
|
|
253
|
+
</code>
|
|
234
254
|
<span className="text-xs text-text-3 pt-0.5">{c.desc}</span>
|
|
235
255
|
</div>
|
|
236
256
|
))}
|
|
237
257
|
</div>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
|
|
259
|
+
<div className="pt-1 space-y-2.5">
|
|
260
|
+
<div>
|
|
261
|
+
<h3 className="text-xs font-semibold text-text-1 mb-1">Nested tags</h3>
|
|
262
|
+
<p className="text-xs text-text-3 leading-relaxed">
|
|
263
|
+
Use <span className="font-mono text-accent">/</span> to nest:
|
|
264
|
+
<span className="font-mono text-info"> #groove/memory</span> lives under
|
|
265
|
+
<span className="font-mono text-info"> #groove</span>. Reading the parent tag pulls every memory beneath it.
|
|
266
|
+
</p>
|
|
267
|
+
</div>
|
|
268
|
+
<div>
|
|
269
|
+
<h3 className="text-xs font-semibold text-text-1 mb-1">Give a new agent context</h3>
|
|
270
|
+
<p className="text-xs text-text-3 leading-relaxed">
|
|
271
|
+
Use <span className="font-mono"><span className="text-accent">[read]</span> <span className="text-info">#tag</span></span> to
|
|
272
|
+
drop saved memories into an agent's chat as background — useful right after spawning one.
|
|
273
|
+
</p>
|
|
274
|
+
</div>
|
|
251
275
|
</div>
|
|
276
|
+
|
|
252
277
|
<div className="flex justify-end pt-1">
|
|
253
278
|
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>Close</Button>
|
|
254
279
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.185",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|