groove-dev 0.27.185 → 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/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 +1 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +83 -42
- package/node_modules/@groove-dev/daemon/src/rotator.js +6 -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/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +36 -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/slices/ui-slice.js +11 -0
- 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 +1 -0
- package/packages/daemon/src/journalist.js +83 -42
- package/packages/daemon/src/rotator.js +6 -1
- package/packages/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +36 -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/slices/ui-slice.js +11 -0
|
@@ -6,7 +6,7 @@
|
|
|
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">
|
|
@@ -12,6 +12,7 @@ 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
|
|
|
@@ -346,8 +347,14 @@ function UserMessage({ msg }) {
|
|
|
346
347
|
function InnerChatMessage({ msg, agent }) {
|
|
347
348
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 400);
|
|
348
349
|
const isLong = msg.text?.length > 400;
|
|
349
|
-
const
|
|
350
|
-
|
|
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';
|
|
351
358
|
const me = agent?.name || 'this agent';
|
|
352
359
|
|
|
353
360
|
return (
|
|
@@ -356,10 +363,10 @@ function InnerChatMessage({ msg, agent }) {
|
|
|
356
363
|
<ArrowLeftRight size={10} className="text-indigo flex-shrink-0" />
|
|
357
364
|
<span className="text-2xs font-semibold text-indigo font-sans">InnerChat</span>
|
|
358
365
|
<span className="text-2xs text-text-3 font-sans truncate">
|
|
359
|
-
{outbound ? `${me} → ${
|
|
366
|
+
{outbound ? `${me} → ${peerName}` : `${peerName} → ${me}`}
|
|
360
367
|
</span>
|
|
361
368
|
<span className="text-2xs text-text-4 font-sans">
|
|
362
|
-
{
|
|
369
|
+
{isAnswer ? 'answer' : outbound ? 'asked' : 'asks'}
|
|
363
370
|
</span>
|
|
364
371
|
<span className="text-[10px] text-text-4 font-sans ml-auto flex-shrink-0">{timeAgo(msg.timestamp)}</span>
|
|
365
372
|
</div>
|
|
@@ -378,6 +385,30 @@ function InnerChatMessage({ msg, agent }) {
|
|
|
378
385
|
);
|
|
379
386
|
}
|
|
380
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
|
+
|
|
381
412
|
function AgentMessage({ msg, agent, answeredTo }) {
|
|
382
413
|
const [collapsed, setCollapsed] = useState(msg.text?.length > 600);
|
|
383
414
|
const isLong = msg.text?.length > 600;
|
|
@@ -399,6 +430,7 @@ function AgentMessage({ msg, agent, answeredTo }) {
|
|
|
399
430
|
<div className={cn('pl-3.5 py-1 border-l', answeredTo ? 'border-indigo/50' : 'border-accent')}>
|
|
400
431
|
<StructuredMessage text={collapsed ? msg.text.slice(0, 600) + '...' : msg.text} />
|
|
401
432
|
</div>
|
|
433
|
+
<LogChips text={msg.text} />
|
|
402
434
|
{collapsed && (
|
|
403
435
|
<button
|
|
404
436
|
onClick={() => setCollapsed(false)}
|
|
@@ -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
|
+
}
|
|
@@ -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));
|