groove-dev 0.27.184 → 0.27.186
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 +3 -0
- package/node_modules/@groove-dev/daemon/src/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +60 -5
- package/node_modules/@groove-dev/daemon/src/innerchat.js +167 -54
- package/node_modules/@groove-dev/daemon/src/introducer.js +3 -2
- package/node_modules/@groove-dev/daemon/src/journalist.js +83 -42
- package/node_modules/@groove-dev/daemon/src/process.js +19 -10
- package/node_modules/@groove-dev/daemon/src/rotator.js +6 -1
- package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +80 -28
- package/node_modules/@groove-dev/daemon/src/routes/watch.js +42 -0
- package/node_modules/@groove-dev/daemon/src/watcher.js +258 -0
- package/node_modules/@groove-dev/daemon/test/innerchat.test.js +71 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +59 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +22 -4
- 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/node_modules/@groove-dev/gui/dist/assets/{index-BpOyN6Zf.js → index-DOOaCFRS.js} +227 -222
- 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 +92 -4
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +25 -0
- package/node_modules/@groove-dev/gui/src/lib/logpaths.js +45 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +30 -16
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +6 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +11 -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 +3 -0
- package/packages/daemon/src/index.js +17 -0
- package/packages/daemon/src/innerchat-docs.js +60 -5
- package/packages/daemon/src/innerchat.js +167 -54
- package/packages/daemon/src/introducer.js +3 -2
- package/packages/daemon/src/journalist.js +83 -42
- package/packages/daemon/src/process.js +19 -10
- package/packages/daemon/src/rotator.js +6 -1
- package/packages/daemon/src/routes/innerchat.js +80 -28
- package/packages/daemon/src/routes/watch.js +42 -0
- package/packages/daemon/src/watcher.js +258 -0
- package/packages/gui/dist/assets/index-CU8L_r5f.css +1 -0
- package/packages/gui/dist/assets/{index-BpOyN6Zf.js → index-DOOaCFRS.js} +227 -222
- 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 +92 -4
- package/packages/gui/src/components/editor/terminal.jsx +25 -0
- package/packages/gui/src/lib/logpaths.js +45 -0
- package/packages/gui/src/stores/groove.js +30 -16
- package/packages/gui/src/stores/slices/agents-slice.js +6 -0
- package/packages/gui/src/stores/slices/ui-slice.js +11 -0
- package/packages/gui/src/views/memory.jsx +56 -31
- package/node_modules/@groove-dev/gui/dist/assets/index-DiXB7yry.css +0 -1
- package/packages/gui/dist/assets/index-DiXB7yry.css +0 -1
|
@@ -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-DOOaCFRS.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>
|
package/packages/gui/src/app.css
CHANGED
|
@@ -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';
|
|
@@ -12,10 +12,12 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
12
12
|
import { cn } from '../../lib/cn';
|
|
13
13
|
import { timeAgo } from '../../lib/format';
|
|
14
14
|
import { api } from '../../lib/api';
|
|
15
|
+
import { extractLogPaths, logLabel } from '../../lib/logpaths';
|
|
15
16
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
16
17
|
import { TableTree } from '../ui/table-tree';
|
|
17
18
|
|
|
18
19
|
const EMPTY = [];
|
|
20
|
+
const EMPTY_MAP = {};
|
|
19
21
|
const KEEPER_RE = /(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|#[\w/.-]+)/gi;
|
|
20
22
|
const KEEPER_CMD_RE = /^\[(?:save|append|update|delete|view|doc|link|read|instruct)\]$/i;
|
|
21
23
|
const KEEPER_TAG_RE = /^#[\w/.-]+$/;
|
|
@@ -339,7 +341,75 @@ function UserMessage({ msg }) {
|
|
|
339
341
|
);
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
|
|
344
|
+
// An agent-to-agent turn. Deliberately styled apart from the normal feed —
|
|
345
|
+
// amber rail and an explicit "X → Y" header — so it reads as traffic between
|
|
346
|
+
// agents rather than something addressed to the user.
|
|
347
|
+
function InnerChatMessage({ msg, agent }) {
|
|
348
|
+
const [collapsed, setCollapsed] = useState(msg.text?.length > 400);
|
|
349
|
+
const isLong = msg.text?.length > 400;
|
|
350
|
+
const info = msg.innerchat || {};
|
|
351
|
+
// `peer` is the current field; `fromAgent` is the pre-`tell` schema still
|
|
352
|
+
// sitting in persisted chatHistory. Fall back so old bubbles never render
|
|
353
|
+
// "undefined".
|
|
354
|
+
const peer = info.peer || info.fromAgent;
|
|
355
|
+
const peerName = peer?.name || 'another agent';
|
|
356
|
+
const outbound = info.direction === 'out';
|
|
357
|
+
const isAnswer = info.kind === 'answer' || info.kind === 'reply';
|
|
358
|
+
const me = agent?.name || 'this agent';
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className="rounded-md bg-indigo/[0.04] border border-indigo/15 px-2.5 py-2">
|
|
362
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
363
|
+
<ArrowLeftRight size={10} className="text-indigo flex-shrink-0" />
|
|
364
|
+
<span className="text-2xs font-semibold text-indigo font-sans">InnerChat</span>
|
|
365
|
+
<span className="text-2xs text-text-3 font-sans truncate">
|
|
366
|
+
{outbound ? `${me} → ${peerName}` : `${peerName} → ${me}`}
|
|
367
|
+
</span>
|
|
368
|
+
<span className="text-2xs text-text-4 font-sans">
|
|
369
|
+
{isAnswer ? 'answer' : outbound ? 'asked' : 'asks'}
|
|
370
|
+
</span>
|
|
371
|
+
<span className="text-[10px] text-text-4 font-sans ml-auto flex-shrink-0">{timeAgo(msg.timestamp)}</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="border-l-2 border-indigo/40 pl-3">
|
|
374
|
+
<StructuredMessage text={collapsed ? msg.text.slice(0, 400) + '...' : msg.text} />
|
|
375
|
+
</div>
|
|
376
|
+
{isLong && (
|
|
377
|
+
<button
|
|
378
|
+
onClick={() => setCollapsed((c) => !c)}
|
|
379
|
+
className="mt-1 text-2xs text-text-4 hover:text-indigo font-sans cursor-pointer"
|
|
380
|
+
>
|
|
381
|
+
{collapsed ? 'Show more' : 'Show less'}
|
|
382
|
+
</button>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// One-click "tail" chips for any log paths the agent mentioned — saves asking
|
|
389
|
+
// "what's the log file?" and hand-copying it into a terminal.
|
|
390
|
+
function LogChips({ text }) {
|
|
391
|
+
const runInTerminal = useGrooveStore((s) => s.runInTerminal);
|
|
392
|
+
const paths = useMemo(() => extractLogPaths(text), [text]);
|
|
393
|
+
if (paths.length === 0) return null;
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div className="ml-3.5 mt-1.5 flex flex-wrap gap-1.5">
|
|
397
|
+
{paths.map((path) => (
|
|
398
|
+
<button
|
|
399
|
+
key={path}
|
|
400
|
+
onClick={() => runInTerminal(`tail -f ${path}`)}
|
|
401
|
+
title={`tail -f ${path}`}
|
|
402
|
+
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md bg-accent/10 hover:bg-accent/20 text-accent text-[11px] font-medium font-sans cursor-pointer transition-colors max-w-full"
|
|
403
|
+
>
|
|
404
|
+
<Terminal size={11} className="flex-shrink-0" />
|
|
405
|
+
<span className="truncate">tail {logLabel(path)}</span>
|
|
406
|
+
</button>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function AgentMessage({ msg, agent, answeredTo }) {
|
|
343
413
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 600);
|
|
344
414
|
const isLong = msg.text?.length > 600;
|
|
345
415
|
|
|
@@ -348,11 +418,19 @@ function AgentMessage({ msg, agent }) {
|
|
|
348
418
|
<div className="flex items-center gap-2 mb-1">
|
|
349
419
|
<span className="text-2xs font-semibold text-text-1 font-sans">{agent?.name || 'Agent'}</span>
|
|
350
420
|
<span className="text-2xs text-text-4 font-sans">{agent?.role}</span>
|
|
421
|
+
{/* This reply went to another agent, not to the user */}
|
|
422
|
+
{answeredTo && (
|
|
423
|
+
<span className="flex items-center gap-1 px-1.5 py-px rounded bg-indigo/10 text-2xs font-medium text-indigo font-sans">
|
|
424
|
+
<ArrowLeftRight size={9} />
|
|
425
|
+
to {answeredTo.name}
|
|
426
|
+
</span>
|
|
427
|
+
)}
|
|
351
428
|
<span className="text-[10px] text-text-4 font-sans ml-auto">{timeAgo(msg.timestamp)}</span>
|
|
352
429
|
</div>
|
|
353
|
-
<div className=
|
|
430
|
+
<div className={cn('pl-3.5 py-1 border-l', answeredTo ? 'border-indigo/50' : 'border-accent')}>
|
|
354
431
|
<StructuredMessage text={collapsed ? msg.text.slice(0, 600) + '...' : msg.text} />
|
|
355
432
|
</div>
|
|
433
|
+
<LogChips text={msg.text} />
|
|
356
434
|
{collapsed && (
|
|
357
435
|
<button
|
|
358
436
|
onClick={() => setCollapsed(false)}
|
|
@@ -606,6 +684,8 @@ export function AgentFeed({ agent, readOnly = false }) {
|
|
|
606
684
|
const rawActivityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
|
|
607
685
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
608
686
|
const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
|
|
687
|
+
// Replies this agent sent to other agents, keyed by text — see innerchatAnswers.
|
|
688
|
+
const answerPeers = useGrooveStore((s) => s.innerchatAnswers?.[agent.id]) || EMPTY_MAP;
|
|
609
689
|
const cachedChatRef = useRef(EMPTY);
|
|
610
690
|
const cachedActivityRef = useRef(EMPTY);
|
|
611
691
|
const cachedAgentIdRef = useRef(agent.id);
|
|
@@ -886,7 +966,15 @@ export function AgentFeed({ agent, readOnly = false }) {
|
|
|
886
966
|
}
|
|
887
967
|
if (item.from === 'user') return <UserMessage key={`user-${item._chatIdx}`} msg={item} />;
|
|
888
968
|
if (item.from === 'system') return <SystemMessage key={`sys-${item._chatIdx}`} msg={item} />;
|
|
889
|
-
return <
|
|
969
|
+
if (item.from === 'innerchat') return <InnerChatMessage key={`ic-${item._chatIdx}`} msg={item} agent={agent} />;
|
|
970
|
+
return (
|
|
971
|
+
<AgentMessage
|
|
972
|
+
key={`agent-${item._chatIdx}`}
|
|
973
|
+
msg={item}
|
|
974
|
+
agent={agent}
|
|
975
|
+
answeredTo={answerPeers[item.text?.trim()]?.peer}
|
|
976
|
+
/>
|
|
977
|
+
);
|
|
890
978
|
})}
|
|
891
979
|
<AnimatePresence>
|
|
892
980
|
{(sending || isThinking) && (
|
|
@@ -31,6 +31,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
31
31
|
const mountedRef = useRef(false);
|
|
32
32
|
const visibleRef = useRef(visible);
|
|
33
33
|
const lastSizeRef = useRef({ cols: 0, rows: 0 });
|
|
34
|
+
const outputReadyRef = useRef(false);
|
|
34
35
|
|
|
35
36
|
useEffect(() => {
|
|
36
37
|
registerKill?.(tabId, () => {
|
|
@@ -41,6 +42,28 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
41
42
|
});
|
|
42
43
|
}, [tabId, registerKill]);
|
|
43
44
|
|
|
45
|
+
// Run a command dropped into the store by another component (e.g. a "tail
|
|
46
|
+
// log" chip in chat). Only the visible tab handles it, and we poll until this
|
|
47
|
+
// instance's PTY is ready — a freshly opened terminal spawns asynchronously,
|
|
48
|
+
// and keystrokes sent before it's ready are dropped.
|
|
49
|
+
const pendingCommand = useGrooveStore((s) => s.terminalPendingCommand);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!pendingCommand || !visible) return;
|
|
52
|
+
let tries = 0;
|
|
53
|
+
let timer = null;
|
|
54
|
+
const send = () => {
|
|
55
|
+
const ws = useGrooveStore.getState().ws;
|
|
56
|
+
if (ws?.readyState === WebSocket.OPEN && termIdRef.current && outputReadyRef.current) {
|
|
57
|
+
ws.send(JSON.stringify({ type: 'terminal:input', id: termIdRef.current, data: pendingCommand.command + '\r' }));
|
|
58
|
+
useGrooveStore.getState().clearTerminalPendingCommand();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (tries++ < 40) timer = setTimeout(send, 150); // ~6s max, covers spawn
|
|
62
|
+
};
|
|
63
|
+
send();
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}, [pendingCommand, visible]);
|
|
66
|
+
|
|
44
67
|
useEffect(() => {
|
|
45
68
|
if (!containerRef.current || mountedRef.current) return;
|
|
46
69
|
mountedRef.current = true;
|
|
@@ -111,6 +134,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
111
134
|
// Wipe xterm so garbled output from wrong-sized PTY is never visible
|
|
112
135
|
term.reset();
|
|
113
136
|
outputReady = true;
|
|
137
|
+
outputReadyRef.current = true;
|
|
114
138
|
// Ask the shell to clear screen and redraw its prompt
|
|
115
139
|
const w2 = useGrooveStore.getState().ws;
|
|
116
140
|
if (w2?.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
@@ -121,6 +145,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
121
145
|
if (outputReady) term.write(msg.data);
|
|
122
146
|
} else if (msg.type === 'terminal:exit' && msg.id === termIdRef.current) {
|
|
123
147
|
outputReady = true;
|
|
148
|
+
outputReadyRef.current = false; // no live shell to accept commands
|
|
124
149
|
term.write('\r\n\x1b[90m[session ended]\x1b[0m\r\n');
|
|
125
150
|
termIdRef.current = null;
|
|
126
151
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
// Pull tailable log targets out of an agent's message so the chat can offer a
|
|
4
|
+
// one-click "tail" instead of the user asking "what's the log file?".
|
|
5
|
+
//
|
|
6
|
+
// Two signals, in priority order:
|
|
7
|
+
// 1. An explicit `tail -f <path>` the agent already wrote — highest confidence.
|
|
8
|
+
// 2. Any path-like token ending in .log.
|
|
9
|
+
// Both are conservative: a bare word like "changelog" is not a path and won't
|
|
10
|
+
// match, and results are de-duped by full path.
|
|
11
|
+
|
|
12
|
+
const TAIL_RE = /tail\s+(?:-[a-zA-Z]+\s+)*([~/]?[\w./-]+)/g;
|
|
13
|
+
const DOTLOG_RE = /(?:^|[\s'"`(=])([~/]?(?:[\w.-]+\/)*[\w.-]+\.log)\b/g;
|
|
14
|
+
|
|
15
|
+
function clean(p) {
|
|
16
|
+
return p.replace(/^['"`]+/, '').replace(/['"`.,;:)]+$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractLogPaths(text) {
|
|
20
|
+
if (!text || typeof text !== 'string') return [];
|
|
21
|
+
const out = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
|
|
24
|
+
const add = (raw) => {
|
|
25
|
+
const p = clean(raw);
|
|
26
|
+
// Require something path-shaped: a slash, a ~, or a .log extension. Guards
|
|
27
|
+
// against picking up plain words that followed "tail".
|
|
28
|
+
if (!p || p.length < 3) return;
|
|
29
|
+
if (!p.endsWith('.log') && !p.includes('/') && !p.startsWith('~')) return;
|
|
30
|
+
if (seen.has(p)) return;
|
|
31
|
+
seen.add(p);
|
|
32
|
+
out.push(p);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (const m of text.matchAll(TAIL_RE)) add(m[1]);
|
|
36
|
+
for (const m of text.matchAll(DOTLOG_RE)) add(m[1]);
|
|
37
|
+
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A short label for the chip — the basename, which is what the user recognizes.
|
|
42
|
+
export function logLabel(path) {
|
|
43
|
+
const base = path.replace(/\/+$/, '').split('/').pop() || path;
|
|
44
|
+
return base.length > 28 ? `…${base.slice(-27)}` : base;
|
|
45
|
+
}
|
|
@@ -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
|
|
|
@@ -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) {
|
|
@@ -106,6 +106,17 @@ export const createUiSlice = (set, get) => ({
|
|
|
106
106
|
set(updates);
|
|
107
107
|
localStorage.setItem('groove:terminalVisible', String(v));
|
|
108
108
|
},
|
|
109
|
+
|
|
110
|
+
// Cross-component channel for running a command in the terminal panel. The
|
|
111
|
+
// terminal id lives inside the active TerminalInstance, so components can't
|
|
112
|
+
// send to it directly — they drop a command here, the visible instance picks
|
|
113
|
+
// it up (waiting for its PTY to be ready) and clears it.
|
|
114
|
+
terminalPendingCommand: null,
|
|
115
|
+
runInTerminal(command) {
|
|
116
|
+
get().setTerminalVisible(true);
|
|
117
|
+
set({ terminalPendingCommand: { command, ts: Date.now() } });
|
|
118
|
+
},
|
|
119
|
+
clearTerminalPendingCommand() { set({ terminalPendingCommand: null }); },
|
|
109
120
|
setTerminalHeight(h) {
|
|
110
121
|
set({ terminalHeight: h });
|
|
111
122
|
localStorage.setItem('groove:terminalHeight', String(h));
|
|
@@ -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>
|