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.
Files changed (27) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1 -0
  4. package/node_modules/@groove-dev/daemon/src/journalist.js +83 -42
  5. package/node_modules/@groove-dev/daemon/src/rotator.js +6 -1
  6. package/node_modules/@groove-dev/daemon/test/journalist.test.js +59 -0
  7. package/node_modules/@groove-dev/daemon/test/rotator.test.js +22 -4
  8. package/node_modules/@groove-dev/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
  9. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  10. package/node_modules/@groove-dev/gui/package.json +1 -1
  11. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +36 -4
  12. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +25 -0
  13. package/node_modules/@groove-dev/gui/src/lib/logpaths.js +45 -0
  14. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +11 -0
  15. package/package.json +1 -1
  16. package/packages/cli/package.json +1 -1
  17. package/packages/daemon/package.json +1 -1
  18. package/packages/daemon/src/api.js +1 -0
  19. package/packages/daemon/src/journalist.js +83 -42
  20. package/packages/daemon/src/rotator.js +6 -1
  21. package/packages/gui/dist/assets/{index-DEZwM4Hb.js → index-DOOaCFRS.js} +60 -60
  22. package/packages/gui/dist/index.html +1 -1
  23. package/packages/gui/package.json +1 -1
  24. package/packages/gui/src/components/agents/agent-feed.jsx +36 -4
  25. package/packages/gui/src/components/editor/terminal.jsx +25 -0
  26. package/packages/gui/src/lib/logpaths.js +45 -0
  27. 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-DEZwM4Hb.js"></script>
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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.185",
3
+ "version": "0.27.186",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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 { peer, direction, kind } = msg.innerchat || {};
350
- const outbound = direction === 'out';
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} → ${peer?.name}` : `${peer?.name} → ${me}`}
366
+ {outbound ? `${me} → ${peerName}` : `${peerName} → ${me}`}
360
367
  </span>
361
368
  <span className="text-2xs text-text-4 font-sans">
362
- {kind === 'answer' ? 'answer' : outbound ? 'asked' : 'asks'}
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));