groove-dev 0.27.209 → 0.27.210

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.
@@ -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-217ZVIOc.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-YHDeARYl.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.209",
3
+ "version": "0.27.210",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.209",
3
+ "version": "0.27.210",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.209",
3
+ "version": "0.27.210",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.209",
3
+ "version": "0.27.210",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -32,6 +32,22 @@ function withBlessedEnv(launch) {
32
32
  return { ...launch, env: { ...BLESSED_ENV, ...(launch.env || {}) } };
33
33
  }
34
34
 
35
+ // A chat title is the opening message, trimmed to a glanceable length. It is
36
+ // a QUOTE, not a summary: GROOVE has no business paraphrasing what the user
37
+ // said, and an em-dash ellipsis makes the truncation visible rather than
38
+ // pretending the sentence ended there.
39
+ const TITLE_MAX = 48;
40
+ export function summarizeForTitle(text) {
41
+ if (typeof text !== 'string') return null;
42
+ const flat = text.replace(/\s+/g, ' ').trim();
43
+ if (!flat) return null;
44
+ if (flat.length <= TITLE_MAX) return flat;
45
+ // Prefer a word boundary so titles don't end mid-word.
46
+ const cut = flat.slice(0, TITLE_MAX);
47
+ const space = cut.lastIndexOf(' ');
48
+ return `${(space > TITLE_MAX * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
49
+ }
50
+
35
51
  // Single-quote for a POSIX shell. The spec is the user's own, but it crosses
36
52
  // an ssh command line — an unquoted path or value must not be able to end the
37
53
  // command and start another.
@@ -329,13 +345,29 @@ export class AxomRuntimes {
329
345
  this._save();
330
346
  }
331
347
 
348
+ // A chat titles itself from what it started with — "Chat 3" tells you
349
+ // nothing when you have six of them. Only ever replaces a PLACEHOLDER title:
350
+ // a name the user typed, or one already derived from the opening message, is
351
+ // never overwritten by a later turn.
352
+ titleFromFirstMessage(session, text) {
353
+ const chat = this.getChat(session);
354
+ if (!chat || chat.titled || chat.renamed) return null;
355
+ const title = summarizeForTitle(text);
356
+ if (!title) return null;
357
+ this._putChat({ ...chat, label: title, titled: true });
358
+ this.broadcastChats();
359
+ return title;
360
+ }
361
+
332
362
  renameChat(session, label) {
333
363
  const chat = this.getChat(session);
334
364
  if (!chat) throw new Error(`no chat "${session}"`);
335
365
  if (typeof label !== 'string' || !label.trim() || label.length > 80) {
336
366
  throw new Error('label must be a non-empty string of at most 80 chars');
337
367
  }
338
- this._putChat({ ...chat, label: label.trim() });
368
+ // `renamed` is sticky: once the user names a chat, no later auto-title
369
+ // may take it back.
370
+ this._putChat({ ...chat, label: label.trim(), renamed: true });
339
371
  this.broadcastChats();
340
372
  return this.getChat(session);
341
373
  }
@@ -348,10 +380,44 @@ export class AxomRuntimes {
348
380
  const chat = this.getChat(session);
349
381
  if (!chat) throw new Error(`no chat "${session}"`);
350
382
  this._putChat({ ...chat, hidden: true });
383
+ this._forgetPrompts(session);
384
+ this._save();
351
385
  this.broadcastChats();
352
386
  return { hidden: true, session, note: 'removed from the list; the conversation remains in Axom\'s memory' };
353
387
  }
354
388
 
389
+ // ── Prompts — what GROOVE sent, remembered where the events are ──────────
390
+ //
391
+ // The runtime's `pipeline_start` carries no prompt text, so the user's own
392
+ // words exist only in GROOVE. Keeping them in the browser meant a reload
393
+ // replayed every turn from the daemon's ring with its bubble gone — the
394
+ // answer with no question above it. This is OUR record of what WE sent, not
395
+ // invented telemetry, so the daemon is the right place for it.
396
+ recordPrompt(session, ref, text) {
397
+ if (!session || !ref) return null;
398
+ const all = this._cfg().prompts || {};
399
+ const forSession = (all[session] || []).filter((p) => p.ref !== ref);
400
+ // Bounded per session: a transcript this long is scrollback, not memory.
401
+ const next = [...forSession, { ref, text, ts: Date.now() }].slice(-200);
402
+ this._cfg().prompts = { ...all, [session]: next };
403
+ this._save();
404
+ return { ref, text };
405
+ }
406
+
407
+ prompts(session) {
408
+ return (this._cfg().prompts || {})[session] || [];
409
+ }
410
+
411
+ // A hidden chat's prompts go with it — the list is tidied, the ledger keeps
412
+ // the conversation itself.
413
+ _forgetPrompts(session) {
414
+ const all = this._cfg().prompts || {};
415
+ if (!all[session]) return;
416
+ const next = { ...all };
417
+ delete next[session];
418
+ this._cfg().prompts = next;
419
+ }
420
+
355
421
  broadcastChats() {
356
422
  this.daemon.broadcast({ type: 'axom:chats', data: { chats: this.chats() } });
357
423
  }
@@ -46,6 +46,16 @@ export function registerAxomRoutes(app, daemon) {
46
46
  return res.status(400).json({ error: 'clientRef must be a string of at most 64 chars' });
47
47
  }
48
48
  const result = await daemon.axom.message(endpoint, req.params.id, text, clientRef);
49
+ // Title the chat from its opening message — but only once the runtime
50
+ // ACCEPTED the turn. A message rejected with 409/413 never ran, so it
51
+ // must not name the conversation it failed to start.
52
+ if (result.status === 202) {
53
+ daemon.axomRuntimes.titleFromFirstMessage(req.params.id, text);
54
+ // Remember what we sent, keyed by the §15 ref the runtime echoes in
55
+ // pipeline_start. This is what lets a reloaded tab put the user's own
56
+ // words back above the answer instead of "prompt not identified".
57
+ if (clientRef) daemon.axomRuntimes.recordPrompt(req.params.id, clientRef, text);
58
+ }
49
59
  daemon.audit.log('axom.message', { session: req.params.id, chars: text.length, status: result.status });
50
60
  res.status(result.status).json(result.body);
51
61
  } catch (err) {
@@ -167,6 +177,13 @@ export function registerAxomRoutes(app, daemon) {
167
177
  res.json({ chats: daemon.axomRuntimes.chats() });
168
178
  });
169
179
 
180
+ // What GROOVE sent on this session, so a reloaded tab can restore the user's
181
+ // bubbles. Only ever OUR OWN sends — a turn started from the REPL or another
182
+ // client has no entry here and must still render without a bubble.
183
+ app.get('/api/axom/sessions/:id/prompts', (req, res) => {
184
+ res.json({ prompts: daemon.axomRuntimes.prompts(req.params.id) });
185
+ });
186
+
170
187
  app.patch('/api/axom/chats/:session', (req, res) => {
171
188
  try {
172
189
  res.json(daemon.axomRuntimes.renameChat(req.params.session, req.body?.label));