aimeat 1.28.0 → 1.30.0

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 (173) hide show
  1. package/dist/locales/en.json +71 -2
  2. package/dist/locales/fi.json +71 -2
  3. package/dist/public/css/views/agents-detail.css +11 -0
  4. package/dist/public/css/views/inbox.css +62 -1
  5. package/dist/public/css/views/profile.css +87 -10
  6. package/dist/public/js/services/memory.js +62 -4
  7. package/dist/public/js/services/messages.js +4 -3
  8. package/dist/public/js/services/organisms.js +49 -3
  9. package/dist/public/js/services/workflows.js +4 -3
  10. package/dist/public/llms-template.txt +38 -1
  11. package/dist/public/spa.html +3 -0
  12. package/dist/public/views/command-palette.js +145 -0
  13. package/dist/public/views/profile/agents-tab.js +1 -0
  14. package/dist/public/views/profile/inbox-tab.js +175 -28
  15. package/dist/public/views/profile/knowledge-tab.js +6 -9
  16. package/dist/public/views/profile/landing-page.js +74 -29
  17. package/dist/public/views/profile/memory-tab.js +251 -26
  18. package/dist/public/views/profile/organisms/agents.js +6 -11
  19. package/dist/public/views/profile/organisms/home.js +1 -1
  20. package/dist/public/views/profile/organisms/members.js +9 -5
  21. package/dist/public/views/profile/organisms/mindmap.js +157 -59
  22. package/dist/public/views/profile/organisms/panels.js +36 -19
  23. package/dist/public/views/profile/organisms/timeline-panel.js +30 -1
  24. package/dist/public/views/profile/organisms/workspace-comments.js +15 -5
  25. package/dist/public/views/profile/organisms/workspace-list.js +35 -23
  26. package/dist/public/views/profile/organisms/workspace.js +166 -31
  27. package/dist/public/views/profile/organisms-tab.js +78 -5
  28. package/dist/public/views/profile/workflows-tab.js +6 -6
  29. package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
  30. package/dist/src/cli/connect/mcp/local-server.js +28 -0
  31. package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
  32. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +13 -0
  33. package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -0
  34. package/dist/src/cli/connect/mcp/tools/dm-messages.js +62 -0
  35. package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -0
  36. package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
  37. package/dist/src/cli/connect/mcp/tools/index.js +2 -0
  38. package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
  39. package/dist/src/generated/api-types.d.ts +527 -12
  40. package/dist/src/generated/api-types.d.ts.map +1 -1
  41. package/dist/src/generated/prisma-postgres/edge.js +4 -3
  42. package/dist/src/generated/prisma-postgres/index-browser.js +1 -0
  43. package/dist/src/generated/prisma-postgres/index.d.ts +30 -1
  44. package/dist/src/generated/prisma-postgres/index.js +4 -3
  45. package/dist/src/generated/prisma-postgres/package.json +1 -1
  46. package/dist/src/generated/prisma-postgres/schema.prisma +1 -0
  47. package/dist/src/generated/prisma-postgres/wasm.js +4 -3
  48. package/dist/src/index.js +2 -0
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/mcp/annotations.d.ts.map +1 -1
  51. package/dist/src/mcp/annotations.js +3 -0
  52. package/dist/src/mcp/annotations.js.map +1 -1
  53. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  54. package/dist/src/mcp/catalog/definitions.js +33 -0
  55. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  56. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  57. package/dist/src/mcp/catalog/scopes.js +6 -1
  58. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  59. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  60. package/dist/src/mcp/catalog/surfaces.js +1 -0
  61. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  62. package/dist/src/mcp/dm-messages.d.ts +20 -0
  63. package/dist/src/mcp/dm-messages.d.ts.map +1 -0
  64. package/dist/src/mcp/dm-messages.js +120 -0
  65. package/dist/src/mcp/dm-messages.js.map +1 -0
  66. package/dist/src/mcp/index.d.ts +2 -1
  67. package/dist/src/mcp/index.d.ts.map +1 -1
  68. package/dist/src/mcp/index.js +3 -1
  69. package/dist/src/mcp/index.js.map +1 -1
  70. package/dist/src/models/message-schemas.d.ts +3 -0
  71. package/dist/src/models/message-schemas.d.ts.map +1 -1
  72. package/dist/src/models/message-schemas.js +4 -0
  73. package/dist/src/models/message-schemas.js.map +1 -1
  74. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  75. package/dist/src/routes/agent-tasks.js +13 -0
  76. package/dist/src/routes/agent-tasks.js.map +1 -1
  77. package/dist/src/routes/app-grants.d.ts.map +1 -1
  78. package/dist/src/routes/app-grants.js +2 -0
  79. package/dist/src/routes/app-grants.js.map +1 -1
  80. package/dist/src/routes/bootstrap.js +1 -1
  81. package/dist/src/routes/bootstrap.js.map +1 -1
  82. package/dist/src/routes/catalogue.d.ts.map +1 -1
  83. package/dist/src/routes/catalogue.js +17 -8
  84. package/dist/src/routes/catalogue.js.map +1 -1
  85. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  86. package/dist/src/routes/federation-sync.js +1 -0
  87. package/dist/src/routes/federation-sync.js.map +1 -1
  88. package/dist/src/routes/memory.d.ts +3 -0
  89. package/dist/src/routes/memory.d.ts.map +1 -1
  90. package/dist/src/routes/memory.js +242 -14
  91. package/dist/src/routes/memory.js.map +1 -1
  92. package/dist/src/routes/messages.d.ts.map +1 -1
  93. package/dist/src/routes/messages.js +22 -19
  94. package/dist/src/routes/messages.js.map +1 -1
  95. package/dist/src/routes/organisms.d.ts.map +1 -1
  96. package/dist/src/routes/organisms.js +321 -3
  97. package/dist/src/routes/organisms.js.map +1 -1
  98. package/dist/src/routes/public-stats.d.ts +2 -0
  99. package/dist/src/routes/public-stats.d.ts.map +1 -1
  100. package/dist/src/routes/public-stats.js +93 -104
  101. package/dist/src/routes/public-stats.js.map +1 -1
  102. package/dist/src/routes/stats.d.ts.map +1 -1
  103. package/dist/src/routes/stats.js +6 -0
  104. package/dist/src/routes/stats.js.map +1 -1
  105. package/dist/src/routes/usage.d.ts +17 -0
  106. package/dist/src/routes/usage.d.ts.map +1 -0
  107. package/dist/src/routes/usage.js +35 -0
  108. package/dist/src/routes/usage.js.map +1 -0
  109. package/dist/src/routes/wallet.d.ts +2 -0
  110. package/dist/src/routes/wallet.d.ts.map +1 -1
  111. package/dist/src/routes/wallet.js +13 -6
  112. package/dist/src/routes/wallet.js.map +1 -1
  113. package/dist/src/routes/workflows.d.ts.map +1 -1
  114. package/dist/src/routes/workflows.js +13 -2
  115. package/dist/src/routes/workflows.js.map +1 -1
  116. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  117. package/dist/src/server-bootstrap/routes-loader.js +18 -2
  118. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  119. package/dist/src/services/cache.d.ts +61 -0
  120. package/dist/src/services/cache.d.ts.map +1 -0
  121. package/dist/src/services/cache.js +143 -0
  122. package/dist/src/services/cache.js.map +1 -0
  123. package/dist/src/services/connect-tunnel.d.ts +11 -0
  124. package/dist/src/services/connect-tunnel.d.ts.map +1 -1
  125. package/dist/src/services/connect-tunnel.js +36 -0
  126. package/dist/src/services/connect-tunnel.js.map +1 -1
  127. package/dist/src/services/message-delivery.d.ts.map +1 -1
  128. package/dist/src/services/message-delivery.js +1 -0
  129. package/dist/src/services/message-delivery.js.map +1 -1
  130. package/dist/src/services/message-send.d.ts +13 -0
  131. package/dist/src/services/message-send.d.ts.map +1 -1
  132. package/dist/src/services/message-send.js +28 -5
  133. package/dist/src/services/message-send.js.map +1 -1
  134. package/dist/src/services/organism-search.d.ts +12 -7
  135. package/dist/src/services/organism-search.d.ts.map +1 -1
  136. package/dist/src/services/organism-search.js +93 -60
  137. package/dist/src/services/organism-search.js.map +1 -1
  138. package/dist/src/services/usage-summary.d.ts +77 -0
  139. package/dist/src/services/usage-summary.d.ts.map +1 -0
  140. package/dist/src/services/usage-summary.js +85 -0
  141. package/dist/src/services/usage-summary.js.map +1 -0
  142. package/dist/src/services/workspace-enrichment.d.ts +76 -0
  143. package/dist/src/services/workspace-enrichment.d.ts.map +1 -0
  144. package/dist/src/services/workspace-enrichment.js +144 -0
  145. package/dist/src/services/workspace-enrichment.js.map +1 -0
  146. package/dist/src/storage/interface.d.ts +5 -1
  147. package/dist/src/storage/interface.d.ts.map +1 -1
  148. package/dist/src/storage/providers/mongodb/index.d.ts +16 -0
  149. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  150. package/dist/src/storage/providers/mongodb/index.js +51 -1
  151. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  152. package/dist/src/storage/providers/sqlite/index.d.ts +16 -0
  153. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  154. package/dist/src/storage/providers/sqlite/index.js +20 -2
  155. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  156. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +15 -0
  157. package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
  158. package/dist/src/storage/providers/sqlite/repos/direct-message.js +28 -2
  159. package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
  160. package/dist/src/storage/providers/sqlite/repos/memory.d.ts +1 -0
  161. package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
  162. package/dist/src/storage/providers/sqlite/repos/memory.js +4 -0
  163. package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
  164. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  165. package/dist/src/storage/providers/sqlite/schema.js +4 -0
  166. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  167. package/dist/src/storage/repositories/direct-message.repository.d.ts +20 -0
  168. package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
  169. package/dist/src/storage/repositories/memory.repository.d.ts +1 -0
  170. package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
  171. package/package.json +1 -1
  172. package/prisma/schema.postgres.prisma +1 -0
  173. package/prisma/schema.prisma +1 -0
@@ -11,6 +11,13 @@
11
11
  * v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
12
12
  * v1.1.0 — 2026-06-22 — Add the free-form README panel + the interactive workspace mindmap; rename
13
13
  * the structure overview to "table of contents" (Osa A/C).
14
+ * v1.2.0 — 2026-06-22 — Batch + debounce: one /comments/batch covers all visible threads (records +
15
+ * open doc) instead of per-thread fetches; the main load + comments refresh are debounced (1.5s)
16
+ * so an agent-driven 'organisms'/'memory' event burst is a single reload.
17
+ * v1.3.0 — 2026-06-22 — Record readability: fields render label-on-top, left-aligned, wrapping, with
18
+ * markdown for text / lists for arrays / pretty JSON for objects (was a right-aligned one-liner);
19
+ * editing happens INLINE in the record's own card (one editor at a time) instead of a form at the
20
+ * top of the space; each record is a separated card with breathing room.
14
21
  */
15
22
  import { h } from 'preact';
16
23
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -21,9 +28,9 @@ import { t, getLocale } from '/js/i18n.js';
21
28
  import { Spinner, KebabMenu } from '/views/profile/shared.js';
22
29
  import { useConfirm } from '/components/Modal.js';
23
30
  import { EmptyState } from '/components/EmptyState.js';
24
- import { KeyValueRow } from '/components/KeyValueRow.js';
31
+ import { SearchBar } from '/components/SearchBar.js';
25
32
  import { Mermaid } from '/components/Mermaid.js';
26
- import { slugifyHeading } from '/components/Markdown.js';
33
+ import { Markdown, slugifyHeading } from '/components/Markdown.js';
27
34
  import * as orgService from '/js/services/organisms.js';
28
35
  import { getGhii } from '/js/services/auth.js';
29
36
  import { copyToClipboard } from '/js/utils.js';
@@ -194,6 +201,31 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
194
201
  };
195
202
  const onWsMapNav = (target) => { if (target?.type === 'space' && target.space) setTab('space:' + target.space); };
196
203
 
204
+ // ── In-workspace content search (Kerros 1): indexed FTS scoped to this workspace. While a query is
205
+ // active the space tabs are filtered to the ones with matches (+ counts) and a grouped result list
206
+ // replaces the space view; clicking a hit jumps straight to that record/document. ──
207
+ const [wsQuery, setWsQuery] = useState('');
208
+ const [wsHits, setWsHits] = useState(null); // null = not searching; [] = searched, no matches
209
+ const [wsSearching, setWsSearching] = useState(false);
210
+ useEffect(() => {
211
+ const q = wsQuery.trim();
212
+ if (q.length < 2) { setWsHits(null); setWsSearching(false); return undefined; }
213
+ let cancelled = false;
214
+ setWsSearching(true);
215
+ const tid = setTimeout(async () => {
216
+ const r = await orgService.searchOrganism(orgId, q, wsId).catch(() => null);
217
+ if (!cancelled) { setWsHits((r?.data?.results) || []); setWsSearching(false); }
218
+ }, 220); // debounce — instant feel without a request per keystroke
219
+ return () => { cancelled = true; clearTimeout(tid); };
220
+ }, [wsQuery, orgId, wsId]);
221
+ const wsSearchCounts = wsHits ? wsHits.reduce((m, h) => { m[h.space] = (m[h.space] || 0) + 1; return m; }, {}) : null;
222
+ // One-shot: the command palette (Cmd-K) can pre-fill this workspace's search so you land on the
223
+ // filtered result list for the query you jumped from.
224
+ useEffect(() => {
225
+ const k = `aimeat.ws.${orgId}.${wsId}.search`;
226
+ try { const v = sessionStorage.getItem(k); if (v) { setWsQuery(v); sessionStorage.removeItem(k); } } catch { /* noop */ }
227
+ }, [orgId, wsId]);
228
+
197
229
  // Deep-link from the librarian "Open" button: aimeat.ws.{org}.{ws}.openDoc = { namespace, id }.
198
230
  // Once the manifest is loaded we resolve the namespace → object-type name, open that space tab and
199
231
  // its document, then clear the key (one-shot).
@@ -216,8 +248,44 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
216
248
  } catch (e) { /* noop */ }
217
249
  }, [activeDoc, docKey]);
218
250
 
251
+ // Subscribe to 'organisms' ONLY — NOT the global 'memory' firehose (every one of dozens of agents'
252
+ // unrelated memory writes was waking this view even when nothing in THIS workspace changed). Organism
253
+ // content writes now also emit 'organisms' (see the memory route), so real workspace changes still
254
+ // refresh; debounced to coalesce a burst into one reload.
219
255
  const liveRef = useRef(load); liveRef.current = load;
220
- useEffect(() => onLiveUpdate(['organisms', 'memory'], () => liveRef.current()), []);
256
+ useEffect(() => {
257
+ let timer = null;
258
+ const off = onLiveUpdate(['organisms'], () => { clearTimeout(timer); timer = setTimeout(() => liveRef.current(), 1500); });
259
+ return () => { clearTimeout(timer); off(); };
260
+ }, []);
261
+
262
+ // ── Comments: ONE /comments/batch request covers every visible thread (the open document + each
263
+ // expanded record), instead of every <WorkspaceComments> self-fetching AND re-fetching on every
264
+ // 'organisms' event (the per-document comments storm). Debounced like the main load. ──
265
+ const [commentsByKey, setCommentsByKey] = useState({});
266
+ const cKey = orgService.commentBatchKey;
267
+ const reloadComments = useCallback(async () => {
268
+ const instances = [];
269
+ for (const k of Object.keys(expandedRec)) {
270
+ if (!expandedRec[k]) continue;
271
+ const i = k.indexOf(':');
272
+ if (i < 0) continue;
273
+ instances.push({ ws: wsId, space: k.slice(0, i), instance_id: k.slice(i + 1) });
274
+ }
275
+ if (activeDoc?.type && activeDoc.page?.id) instances.push({ ws: wsId, space: activeDoc.type, instance_id: activeDoc.page.id });
276
+ if (!instances.length) { setCommentsByKey({}); return; }
277
+ const res = await orgService.listCommentsBatch(orgId, instances);
278
+ const map = {};
279
+ for (const [key, val] of Object.entries(res)) map[key] = val.comments || [];
280
+ setCommentsByKey(map);
281
+ }, [orgId, wsId, expandedRec, activeDoc]);
282
+ useEffect(() => { reloadComments(); }, [reloadComments]);
283
+ const commentsLiveRef = useRef(reloadComments); commentsLiveRef.current = reloadComments;
284
+ useEffect(() => {
285
+ let timer = null;
286
+ const off = onLiveUpdate(['organisms'], () => { clearTimeout(timer); timer = setTimeout(() => commentsLiveRef.current(), 1500); });
287
+ return () => { clearTimeout(timer); off(); };
288
+ }, []);
221
289
 
222
290
  // Clicking a single space inside a stacked group view opens that group, then scrolls to (and
223
291
  // expands) the clicked space's section. The anchor wrapper is id="ws-sec-<name>"; a short timeout
@@ -336,12 +404,33 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
336
404
  const lang = getLocale();
337
405
  return (m[lang] && m[lang][key]) || (m.en && m.en[key]) || '';
338
406
  }, [ws]);
407
+ // Render ONE field value, type-aware + left-aligned + wrapping. Strings that look like markdown (or
408
+ // are multi-line) render through the safe Markdown component; plain strings wrap as text; arrays
409
+ // become a bullet list; objects pretty-print in a wrapped <pre>. (Was a right-aligned KeyValueRow
410
+ // with everything String()'d onto one line — unreadable for real record data.)
411
+ const looksMarkdown = (s) => /\n/.test(s) || /(^|\s)[-*]\s/.test(s) || /[#`>|]|\[[^\]]+\]\(|\*\*/.test(s);
412
+ const renderFieldVal = (v) => {
413
+ if (Array.isArray(v)) {
414
+ return html`<ul class="pj-rec-field-list">${v.map((it, i) => html`<li key=${i}>${
415
+ (it && typeof it === 'object') ? html`<pre class="pj-rec-json">${JSON.stringify(it, null, 2)}</pre>` : String(it)
416
+ }</li>`)}</ul>`;
417
+ }
418
+ if (v && typeof v === 'object') return html`<pre class="pj-rec-json">${JSON.stringify(v, null, 2)}</pre>`;
419
+ if (typeof v === 'string') {
420
+ return looksMarkdown(v)
421
+ ? html`<div class="pj-rec-md"><${Markdown} text=${v} /></div>`
422
+ : html`<span class="pj-rec-field-text">${v}</span>`;
423
+ }
424
+ return html`<span class="pj-rec-field-text">${String(v)}</span>`;
425
+ };
339
426
  const recordFields = (ot, rec) => {
340
427
  const rows = Object.entries(rec || {}).filter(([k, v]) =>
341
428
  !k.startsWith('_') && v !== undefined && v !== null && v !== '' && !(Array.isArray(v) && v.length === 0));
342
429
  if (!rows.length) return html`<div class="pj-muted pj-rec-empty">${t('organisms.noFields') || 'No fields'}</div>`;
343
- return rows.map(([k, v]) => html`<${KeyValueRow} key=${k} label=${wsT(`${ot.namespace}.${k}`) || k}
344
- value=${Array.isArray(v) ? v.join(', ') : (typeof v === 'object' ? JSON.stringify(v) : String(v))} />`);
430
+ return rows.map(([k, v]) => html`<div class="pj-rec-field" key=${k}>
431
+ <div class="pj-rec-field-label">${wsT(`${ot.namespace}.${k}`) || k}</div>
432
+ <div class="pj-rec-field-val">${renderFieldVal(v)}</div>
433
+ </div>`);
345
434
  };
346
435
 
347
436
  const saveDraft = useCallback(async (ot, value) => {
@@ -801,7 +890,8 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
801
890
  if (target) { setActiveDoc({ type: ot.name, mode: 'view', page: target }); scrollToAnchor(); }
802
891
  else showToast((t('organisms.docNotFound') || 'No document titled “{title}”').replace('{title}', title));
803
892
  }} />
804
- <${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${livePage.id} showToast=${showToast} />`;
893
+ <${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${livePage.id} showToast=${showToast}
894
+ batched=${true} initialComments=${commentsByKey[cKey(wsId, ot.name, livePage.id)]} onReload=${reloadComments} />`;
805
895
  })()}
806
896
  </div>
807
897
  </div>
@@ -817,10 +907,10 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
817
907
  </div>
818
908
  ${spaceDesc(ot) ? html`<div class="section-desc">${spaceDesc(ot)}</div>` : null}
819
909
 
820
- ${adding === ot.name && (addingSchema
821
- ? html`<${SchemaForm} key=${'sf-' + (addingId || 'new')} schema=${addingSchema} busy=${busy} initial=${addingInitial}
910
+ ${adding === ot.name && !addingId && (addingSchema
911
+ ? html`<div class="pj-rec-edit pj-rec-edit-new">${html`<${SchemaForm} key=${'sf-new'} schema=${addingSchema} busy=${busy} initial=${addingInitial}
822
912
  idPrefix=${ot.name} namespace=${ot.namespace} wsT=${wsT}
823
- onSave=${(v) => saveDraft(ot, addingId ? { ...v, id: addingId } : v)} onCancel=${cancelForm} />`
913
+ onSave=${(v) => saveDraft(ot, v)} onCancel=${cancelForm} />`}</div>`
824
914
  : html`<${Spinner} />`)}
825
915
 
826
916
  ${draftsFor(ot.name).map((d, i) => html`
@@ -832,7 +922,13 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
832
922
  <button class="btn-primary btn-sm" onClick=${() => publish(ot, d.id)} disabled=${busy}>${t('organisms.publish') || 'Publish'}</button>
833
923
  <button class="pj-icon-btn" title=${t('organisms.delete') || 'Delete'} disabled=${busy} onClick=${() => removeObject(ot.namespace, d.id, String(d[PRIMARY_FIELD[ot.name] || 'title'] || d.id))}>🗑</button>
834
924
  </div>
835
- ${expandedRec[ot.name + ':' + d.id] ? html`<div class="pj-rec-fields">${recordFields(ot, d)}</div><${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${d.id} showToast=${showToast} />` : null}
925
+ ${adding === ot.name && addingId === d.id
926
+ ? html`<div class="pj-rec-edit">${addingSchema
927
+ ? html`<${SchemaForm} key=${'sf-' + d.id} schema=${addingSchema} busy=${busy} initial=${addingInitial}
928
+ idPrefix=${ot.name} namespace=${ot.namespace} wsT=${wsT}
929
+ onSave=${(v) => saveDraft(ot, { ...v, id: addingId })} onCancel=${cancelForm} />`
930
+ : html`<${Spinner} />`}</div>`
931
+ : (expandedRec[ot.name + ':' + d.id] ? html`<div class="pj-rec-fields">${recordFields(ot, d)}</div><${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${d.id} showToast=${showToast} batched=${true} initialComments=${commentsByKey[cKey(wsId, ot.name, d.id)]} onReload=${reloadComments} />` : null)}
836
932
  </div>
837
933
  `)}
838
934
 
@@ -847,7 +943,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
847
943
  <button class="btn-ghost btn-sm" title=${t('organisms.reopenEditHint') || 'Reopen for editing — creates an editable draft from the published version'} disabled=${busy} onClick=${() => reopen(ot, o.id)}>${t('organisms.edit') || 'Edit'}</button>` : null}
848
944
  <button class="pj-icon-btn" title=${t('organisms.delete') || 'Delete'} disabled=${busy} onClick=${() => removeObject(ot.namespace, o.id, String(o[PRIMARY_FIELD[ot.name] || 'title'] || o.id))}>🗑</button>
849
945
  </div>
850
- ${expandedRec[ot.name + ':' + o.id] ? html`<div class="pj-rec-fields">${recordFields(ot, o)}</div><${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${o.id} showToast=${showToast} />` : null}
946
+ ${expandedRec[ot.name + ':' + o.id] ? html`<div class="pj-rec-fields">${recordFields(ot, o)}</div><${WorkspaceComments} orgId=${orgId} ws=${wsId} space=${ot.name} instanceId=${o.id} showToast=${showToast} batched=${true} initialComments=${commentsByKey[cKey(wsId, ot.name, o.id)]} onReload=${reloadComments} />` : null}
851
947
  </div>
852
948
  `)
853
949
  }
@@ -958,6 +1054,32 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
958
1054
  pickTab('space:' + target.name);
959
1055
  };
960
1056
  const openOvRec = (ot, r) => { setExpandedRec(s => ({ ...s, [ot.name + ':' + r.id]: true })); pickTab('space:' + ot.name); };
1057
+ // Jump from a search hit to its record/document in the right space, then close the search.
1058
+ const gotoHit = (hit) => {
1059
+ const target = allTypes.find(o => o.name === hit.space || o.namespace === hit.namespace);
1060
+ if (!target) return;
1061
+ if (isDocSpace(target)) setActiveDoc({ type: target.name, mode: 'view', page: { id: hit.id } });
1062
+ else setExpandedRec(s => ({ ...s, [target.name + ':' + hit.id]: true }));
1063
+ pickTab('space:' + target.name);
1064
+ setWsQuery(''); setWsHits(null);
1065
+ };
1066
+ const renderWsSearchResults = () => {
1067
+ if (wsSearching && !wsHits) return html`<${Spinner} text=${t('organisms.loading') || 'Loading...'} />`;
1068
+ if (!wsHits || !wsHits.length) return html`<${EmptyState} text=${t('search.noMatches') || 'No matches'} />`;
1069
+ const bySpace = {};
1070
+ for (const h of wsHits) (bySpace[h.space] = bySpace[h.space] || []).push(h);
1071
+ return html`<div class="pj-search-results">
1072
+ ${Object.entries(bySpace).map(([space, hits]) => html`
1073
+ <div class="pj-search-group" key=${space}>
1074
+ <div class="pj-search-group-head">${cap(wsT('type.' + space) || space)}<span class="pj-org-tab-count">${hits.length}</span></div>
1075
+ ${hits.map(h => html`
1076
+ <button class="pj-search-hit" key=${h.id} onClick=${() => gotoHit(h)}>
1077
+ <span class="pj-search-hit-title">${h.title}</span>
1078
+ <span class="pj-search-hit-snippet">${h.snippet}</span>
1079
+ </button>`)}
1080
+ </div>`)}
1081
+ </div>`;
1082
+ };
961
1083
  // A document opens in its space tab on desktop; on mobile it expands INLINE right here —
962
1084
  // view and edit both — so no window juggling is ever needed on a phone.
963
1085
  const openOvDoc = (ot, d) => {
@@ -1075,7 +1197,7 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
1075
1197
  <${ReadmePanel} markdown=${ws.readme || ''} canEdit=${wsCanEdit} kind="workspace" name=${ws.manifest?.name || 'Workspace'}
1076
1198
  aiPromptSeed=${wsTocSeed} onSave=${saveWsReadme} />
1077
1199
 
1078
- <${StructureMindmap} scope="workspace" graph=${wsGraph} onNavigate=${onWsMapNav} />
1200
+ <${StructureMindmap} scope="workspace" graph=${wsGraph} onNavigate=${onWsMapNav} storageKey=${'ws.' + orgId + '.' + wsId} />
1079
1201
 
1080
1202
  <${StructureOverview} label=${t('organisms.structureOverviewWs') || 'Workspace structure — table of contents'}
1081
1203
  load=${() => orgService.getWorkspaceOverview(orgId, wsId)} />
@@ -1089,10 +1211,19 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
1089
1211
  <button class="btn-outline btn-sm" onClick=${() => setTab('review')}>${t('organisms.reviewQueue') || 'Review queue'}</button>
1090
1212
  </div>` : null}
1091
1213
 
1214
+ <div class="pj-ws-searchbar">
1215
+ <${SearchBar} value=${wsQuery} onInput=${e => setWsQuery(e.target.value)}
1216
+ placeholder=${t('search.wsPlaceholder') || 'Search this workspace…'} ariaLabel=${t('search.wsPlaceholder') || 'Search this workspace'} />
1217
+ ${wsHits !== null ? html`<button class="btn-ghost btn-sm" onClick=${() => { setWsQuery(''); setWsHits(null); }}>${t('search.clear') || 'Clear'}</button>` : null}
1218
+ </div>
1219
+
1092
1220
  <div class="pj-org-groups" role="tablist">
1093
1221
  ${groups.map(g => {
1094
1222
  const stacked = g.kind === 'stacked';
1095
1223
  const groupActive = activeTab === g.id;
1224
+ // While searching, a stacked (content) group shows only the spaces that have matches.
1225
+ const members = (wsSearchCounts && stacked) ? g.members.filter(tb => wsSearchCounts[tb.ot.name]) : g.members;
1226
+ if (wsSearchCounts && stacked && !members.length) return null;
1096
1227
  return html`
1097
1228
  <div class="pj-org-group ${groupActive ? 'active' : ''}" key=${g.id}>
1098
1229
  ${stacked
@@ -1101,14 +1232,16 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
1101
1232
  </button>`
1102
1233
  : html`<span class="pj-org-group-cap pj-org-group-cap-static">${g.label}</span>`}
1103
1234
  <div class="pj-org-group-tabs">
1104
- ${g.members.map(tb => {
1235
+ ${members.map(tb => {
1105
1236
  // Related members are independent panels; a stacked member scrolls within its group.
1106
1237
  const isActive = activeTab === tb.id;
1107
1238
  const u = isActive ? 0 : unseenOf(tb.id);
1239
+ const matchCount = (wsSearchCounts && stacked) ? wsSearchCounts[tb.ot.name] : null;
1108
1240
  const onClick = stacked ? () => scrollToSpace(g.id, tb.ot.name) : () => pickTab(tb.id);
1109
1241
  return html`
1110
1242
  <button class="pj-org-tab ${isActive ? 'active' : ''}" role="tab" aria-selected=${isActive} key=${tb.id} onClick=${onClick}>
1111
- ${(tb.label)}${tb.count !== null && tb.count !== undefined ? html`<span class="pj-org-tab-count">${tb.count}</span>` : null}
1243
+ ${(tb.label)}${matchCount != null ? html`<span class="pj-org-tab-count pj-org-tab-match">${matchCount}</span>`
1244
+ : (tb.count !== null && tb.count !== undefined ? html`<span class="pj-org-tab-count">${tb.count}</span>` : null)}
1112
1245
  ${u > 0 ? html`<span class="pj-org-tab-unseen" title=${t('organisms.unseenHint') || 'Changed since your last visit'}>${u}</span>` : null}
1113
1246
  </button>`;
1114
1247
  })}
@@ -1197,26 +1330,28 @@ export function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
1197
1330
  </div>
1198
1331
  `}
1199
1332
 
1200
- ${REL_DESC[activeTab] ? html`<div class="section-desc pj-tab-desc">${REL_DESC[activeTab]}</div>` : null}
1333
+ ${wsHits !== null ? renderWsSearchResults() : html`
1334
+ ${REL_DESC[activeTab] ? html`<div class="section-desc pj-tab-desc">${REL_DESC[activeTab]}</div>` : null}
1201
1335
 
1202
- ${activeTab === 'overview' ? renderOverview() : null}
1336
+ ${activeTab === 'overview' ? renderOverview() : null}
1203
1337
 
1204
- ${activeGroup ? html`
1205
- <div class="pj-ov pj-group-view">
1206
- <div class="pj-group-head">
1207
- <div class="section-title">${activeGroup.label}</div>
1208
- ${activeGroup.desc ? html`<div class="section-desc">${activeGroup.desc}</div>` : null}
1209
- </div>
1210
- ${activeGroup.spaces.map(ot => html`
1211
- <div class="pj-group-sec" id=${'ws-sec-' + ot.name} key=${'gs-' + ot.name}>${renderOvSection(ot)}</div>`)}
1212
- ${activeGroup.spaces.length === 0 ? html`<${EmptyState} text=${t('organisms.noneYet') || 'none yet'} />` : null}
1213
- </div>` : null}
1338
+ ${activeGroup ? html`
1339
+ <div class="pj-ov pj-group-view">
1340
+ <div class="pj-group-head">
1341
+ <div class="section-title">${activeGroup.label}</div>
1342
+ ${activeGroup.desc ? html`<div class="section-desc">${activeGroup.desc}</div>` : null}
1343
+ </div>
1344
+ ${activeGroup.spaces.map(ot => html`
1345
+ <div class="pj-group-sec" id=${'ws-sec-' + ot.name} key=${'gs-' + ot.name}>${renderOvSection(ot)}</div>`)}
1346
+ ${activeGroup.spaces.length === 0 ? html`<${EmptyState} text=${t('organisms.noneYet') || 'none yet'} />` : null}
1347
+ </div>` : null}
1214
1348
 
1215
- ${activeSpace
1216
- ? (!orgService.isMemorySpace(activeSpace)
1217
- ? renderSpaceNotice(activeSpace)
1218
- : (isDocSpace(activeSpace) ? renderDocSpace(activeSpace) : renderRecordSpace(activeSpace)))
1219
- : null}
1349
+ ${activeSpace
1350
+ ? (!orgService.isMemorySpace(activeSpace)
1351
+ ? renderSpaceNotice(activeSpace)
1352
+ : (isDocSpace(activeSpace) ? renderDocSpace(activeSpace) : renderRecordSpace(activeSpace)))
1353
+ : null}
1354
+ `}
1220
1355
 
1221
1356
  ${activeTab === 'sources' ? html`<${SourcesPanel} orgId=${orgId} wsId=${wsId} showToast=${showToast} />` : null}
1222
1357
 
@@ -17,6 +17,11 @@
17
17
  * import OrganismsTab from '/views/profile/organisms-tab.js';
18
18
  * <OrganismsTab session={session} showToast={showToast} onStats={onStats} />
19
19
  * @version-history
20
+ * v2.2.0 — 2026-06-22 — Cross-organism content search on the list view: one box searches ALL my
21
+ * organisms (indexed librarian FTS), results grouped per organism; a hit opens that organism +
22
+ * workspace with the in-workspace search pre-filled. Also listens for the Cmd-K aimeat-open-organism.
23
+ * v2.1.0 — 2026-06-22 — Workspace counts come inline via listOrganisms({include:'counts'}) instead
24
+ * of a per-organism discoverWorkspaces fan-out.
20
25
  * v1.31.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking.
21
26
  * v2.0.0 — 2026-06-19 — Split the ~3825-line module into cohesive sub-modules under ./organisms/*
22
27
  * (no behaviour/visual change). This file keeps only OrganismsTab + the list-row renderers.
@@ -32,6 +37,8 @@ import { onLiveUpdate } from '/lib/live-updates.js';
32
37
  const html = htm.bind(h);
33
38
  import { t } from '/js/i18n.js';
34
39
  import { Spinner, KebabMenu } from './shared.js';
40
+ import { SearchBar } from '/components/SearchBar.js';
41
+ import { EmptyState } from '/components/EmptyState.js';
35
42
  import { useConfirm } from '/components/Modal.js';
36
43
  import * as orgService from '/js/services/organisms.js';
37
44
  import * as memoryService from '/js/services/memory.js';
@@ -56,6 +63,12 @@ export default function OrganismsTab({ session, showToast, onStats }) {
56
63
  const [sortMode, setSortMode] = useState('custom');
57
64
  const [customOrder, setCustomOrder] = useState([]);
58
65
  const [openSettings, setOpenSettings] = useState(false); // open OrganismHome with the Settings panel showing
66
+ // Cross-organism content search (all my organisms at once) — indexed librarian FTS, grouped per
67
+ // organism. Replaces the org lists while a query is active; a hit opens that organism + workspace
68
+ // with the in-workspace search pre-filled to the same query.
69
+ const [gQuery, setGQuery] = useState('');
70
+ const [gHits, setGHits] = useState(null); // null = not searching
71
+ const [gBusy, setGBusy] = useState(false);
59
72
  const dragIdRef = useRef(null);
60
73
  const [dragOverId, setDragOverId] = useState(null);
61
74
 
@@ -73,7 +86,7 @@ export default function OrganismsTab({ session, showToast, onStats }) {
73
86
  const loadData = useCallback(async () => {
74
87
  try {
75
88
  const [myResp, pubResp] = await Promise.all([
76
- ghii ? orgService.listOrganisms({ member: ghii }) : Promise.resolve({ data: { organisms: [] } }),
89
+ ghii ? orgService.listOrganisms({ member: ghii, include: 'counts' }) : Promise.resolve({ data: { organisms: [] } }),
77
90
  orgService.listOrganisms({ visibility: 'public' }),
78
91
  ]);
79
92
  const mine = myResp?.data?.organisms || [];
@@ -82,10 +95,8 @@ export default function OrganismsTab({ session, showToast, onStats }) {
82
95
  setMyOrganisms(mine);
83
96
  setPublicOrganisms(discover);
84
97
  onStats?.({ organisms: mine.length });
85
- // Workspace counts for the 📁 row stat (member-gated endpoint, so own orgs only).
86
- Promise.all(mine.map(async (o) => [o.id, (await orgService.discoverWorkspaces(o.id)).length]))
87
- .then(entries => setWsCounts(Object.fromEntries(entries)))
88
- .catch(() => {});
98
+ // Workspace counts for the 📁 row stat come inline via ?include=counts (no per-org fan-out).
99
+ setWsCounts(Object.fromEntries(mine.map(o => [o.id, o.workspace_count ?? 0])));
89
100
  } catch {
90
101
  setMyOrganisms([]);
91
102
  setPublicOrganisms([]);
@@ -172,6 +183,31 @@ export default function OrganismsTab({ session, showToast, onStats }) {
172
183
  if (openId && myOrganisms && !myOrganisms.some(o => o.id === openId)) { setOpenId(null); setOpenWs(null); }
173
184
  }, [openId, myOrganisms]);
174
185
 
186
+ // Jump from the global command palette (Cmd-K) when this tab is already mounted — open the chosen
187
+ // organism (+ workspace). A cold navigation instead reads sessionStorage on mount (above).
188
+ useEffect(() => {
189
+ const onOpen = (e) => {
190
+ const d = e.detail || {};
191
+ if (!d.orgId) return;
192
+ setOpenId(d.orgId); setOpenWs(d.wsId || null); setOpenSettings(false);
193
+ };
194
+ window.addEventListener('aimeat-open-organism', onOpen);
195
+ return () => window.removeEventListener('aimeat-open-organism', onOpen);
196
+ }, []);
197
+
198
+ // Cross-organism content search (debounced) — librarian FTS across all my own content.
199
+ useEffect(() => {
200
+ const query = gQuery.trim();
201
+ if (query.length < 2) { setGHits(null); setGBusy(false); return undefined; }
202
+ let cancelled = false;
203
+ setGBusy(true);
204
+ const tid = setTimeout(async () => {
205
+ const hits = await memoryService.librarianSearch(query, 60, 'own').catch(() => []);
206
+ if (!cancelled) { setGHits(hits || []); setGBusy(false); }
207
+ }, 220);
208
+ return () => { cancelled = true; clearTimeout(tid); };
209
+ }, [gQuery]);
210
+
175
211
  // Live update listener
176
212
  const liveRef = useRef(loadData);
177
213
  liveRef.current = loadData;
@@ -364,6 +400,20 @@ export default function OrganismsTab({ session, showToast, onStats }) {
364
400
 
365
401
  if (!myOrganisms) return html`<${Spinner} text=${t('organisms.loading') || 'Loading organisms...'} />`;
366
402
 
403
+ // Open a cross-organism search hit: jump to its organism + workspace, pre-filling the in-workspace
404
+ // search with the query so you land on the filtered result list (reuses Kerros 1).
405
+ const openGHit = (hit) => {
406
+ try { if (hit.workspaceId) sessionStorage.setItem(`aimeat.ws.${hit.organismId}.${hit.workspaceId}.search`, gQuery.trim()); } catch { /* noop */ }
407
+ setOpenId(hit.organismId);
408
+ setOpenWs(hit.workspaceId || null);
409
+ setOpenSettings(false);
410
+ };
411
+ // Group the librarian hits under each organism (name from my list / discover), for the results view.
412
+ const orgNameOf = (id) => (myOrganisms || []).find(o => o.id === id)?.name
413
+ || publicOrganisms.find(o => o.id === id)?.name || id;
414
+ const gGroups = {};
415
+ for (const hh of (gHits || [])) { if (!hh.organismId) continue; (gGroups[hh.organismId] = gGroups[hh.organismId] || []).push(hh); }
416
+
367
417
  return html`
368
418
  <div class="section-title">${t('organisms.title') || 'Organisms'}</div>
369
419
  <div class="section-desc">${t('organisms.desc') || 'Organisms are groups — communities, teams, clubs, or projects. Create one or join existing ones to share knowledge, coordinate work, and build together.'}</div>
@@ -415,6 +465,28 @@ export default function OrganismsTab({ session, showToast, onStats }) {
415
465
  `}
416
466
  </div>
417
467
 
468
+ <!-- Cross-organism content search: one box over ALL my organisms, results grouped per organism -->
469
+ <div class="pj-org-globalsearch">
470
+ <${SearchBar} value=${gQuery} onInput=${e => setGQuery(e.target.value)}
471
+ placeholder=${t('search.allOrgsPlaceholder') || 'Search across all organisms…'} ariaLabel=${t('search.allOrgsPlaceholder') || 'Search across all organisms'} />
472
+ ${gHits !== null ? html`<button class="btn-ghost btn-sm" onClick=${() => setGQuery('')}>${t('search.clear') || 'Clear'}</button>` : null}
473
+ </div>
474
+
475
+ ${gHits !== null ? html`
476
+ <div class="pj-search-results">
477
+ ${gBusy && !gHits.length ? html`<${Spinner} text=${t('search.searching') || 'Searching…'} />` : null}
478
+ ${!gHits.length && !gBusy ? html`<${EmptyState} text=${t('search.noMatches') || 'No matches'} />` : null}
479
+ ${Object.entries(gGroups).map(([orgId, hits]) => html`
480
+ <div class="pj-search-group" key=${orgId}>
481
+ <div class="pj-search-group-head">${orgNameOf(orgId)}<span class="pj-org-tab-count">${hits.length}</span></div>
482
+ ${hits.map(hh => html`
483
+ <button class="pj-search-hit" key=${hh.key} onClick=${() => openGHit(hh)}>
484
+ <span class="pj-search-hit-title">${hh.title || hh.key}${hh.workspaceId ? html` <span class="pj-mini">· ${hh.workspaceId}</span>` : null}</span>
485
+ <span class="pj-search-hit-snippet">${hh.snippet || ''}</span>
486
+ </button>`)}
487
+ </div>`)}
488
+ </div>
489
+ ` : html`
418
490
  <!-- Incoming invitations -->
419
491
  <${IncomingInvitations} showToast=${showToast} onChanged=${loadData} />
420
492
 
@@ -448,6 +520,7 @@ export default function OrganismsTab({ session, showToast, onStats }) {
448
520
  <div class="section-title section-title-spaced">${t('organisms.discover') || 'Discover'}</div>
449
521
  <div class="pj-org-list">${publicOrganisms.map(org => renderOrgRow(org, false))}</div>
450
522
  `}
523
+ `}
451
524
  <${ConfirmUI} />
452
525
  `;
453
526
  }
@@ -9,6 +9,8 @@
9
9
  * @structure WorkflowsTab (default) — view state machine (list | detail | run)
10
10
  * @usage Registered in profile.js TABS as { id:'workflows', component: WorkflowsTab }.
11
11
  * @version-history
12
+ * v1.2.0 -- 2026-06-22 -- List uses listWorkflows({include:'health'}) so health arrives inline in
13
+ * one request instead of a per-workflow getHealth fan-out.
12
14
  * v1.0.0 -- 2026-06-13 -- Phase 9: list + blueprint + runs + run-now + health.
13
15
  * v1.1.0 -- 2026-06-13 -- Run view renders image deliverables: a step whose observed output / writes
14
16
  * hold an image (a /v1/pub URL or { url, mime } value) shows inline thumbnails via the shared
@@ -20,7 +22,7 @@ import htm from 'htm';
20
22
  import { onLiveUpdate } from '/lib/live-updates.js';
21
23
  import { t } from '/js/i18n.js';
22
24
  import { timeAgo } from '/js/utils.js';
23
- import { listWorkflows, getWorkflow, getBlueprint, getHealth, listRuns, getRun, runWorkflow, cancelRun } from '/js/services/workflows.js';
25
+ import { listWorkflows, getWorkflow, getBlueprint, listRuns, getRun, runWorkflow, cancelRun } from '/js/services/workflows.js';
24
26
  import { collectImages, ImageStrip } from '/components/ImageDeliverable.js';
25
27
  import WorkflowForm from './workflows-form.js';
26
28
 
@@ -74,12 +76,10 @@ export default function WorkflowsTab({ showToast }) {
74
76
 
75
77
  const loadList = useCallback(async () => {
76
78
  try {
77
- const res = await listWorkflows();
79
+ const res = await listWorkflows({ include: 'health' });
78
80
  const defs = res?.data?.workflows || [];
79
- const withHealth = await Promise.all(defs.map(async (def) => {
80
- const hr = await getHealth(def.id).catch(() => null);
81
- return { def, health: hr?.data || null };
82
- }));
81
+ // Health arrives inline now (one request) — split it back out of each def.
82
+ const withHealth = defs.map((w) => { const { health = null, ...def } = w; return { def, health }; });
83
83
  setItems(withHealth);
84
84
  setError(null);
85
85
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"local-server.d.ts","sourceRoot":"","sources":["../../../../../src/cli/connect/mcp/local-server.ts"],"names":[],"mappings":"AAqDA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,sBAAsB,CAAC;AAM3E,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAChD;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,UAAU,EAAE,uBAAuB,EAAE,CAAC;IACtC,iGAAiG;IACjG,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAkID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,aAAa,CAAC;IACxB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,SAAS,CAAC;CAC3B;AAQD,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoY5E"}
1
+ {"version":3,"file":"local-server.d.ts","sourceRoot":"","sources":["../../../../../src/cli/connect/mcp/local-server.ts"],"names":[],"mappings":"AAwDA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,sBAAsB,CAAC;AAM3E,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAChD;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,OAAO,GAAG,WAAW,CAAC;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,UAAU,EAAE,uBAAuB,EAAE,CAAC;IACtC,iGAAiG;IACjG,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AA4ID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,aAAa,CAAC;IACxB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,SAAS,CAAC;CAC3B;AAQD,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiZ5E"}
@@ -44,6 +44,9 @@
44
44
  * SEPARATE from tasks, so record wakes never intermix with real tasks). `workspace.record` delivers
45
45
  * route to the new queue; `/local/status` exposes per-agent subscriptions + reconnect count (the
46
46
  * consumer's catch-up signal).
47
+ * v1.3.0 — 2026-06-22 — P3 cancellation push: `task.cancelled` delivers populate a per-agent
48
+ * cancelled-set, exposed at `GET /local/cancelled` so the daemon checks a loopback set instead of
49
+ * scanning owner-scoped `agents.cancel.*` memory before every dispatch.
47
50
  */
48
51
  import express from 'express';
49
52
  import { randomBytes } from 'node:crypto';
@@ -82,6 +85,9 @@ class AgentChannel {
82
85
  waiters = [];
83
86
  recordQueue = [];
84
87
  recordWaiters = [];
88
+ /** Task ids the node pushed as cancelled (P3) — checked by the daemon instead of polling the
89
+ * owner-scoped `agents.cancel.*` memory before every dispatch. Bounded by a daemon's task volume. */
90
+ cancelledIds = new Set();
85
91
  /** Spaces the agent asked to subscribe to — held so the daemon re-sends them on each reconnect. */
86
92
  subscriptions = [];
87
93
  constructor(entry) {
@@ -89,6 +95,13 @@ class AgentChannel {
89
95
  }
90
96
  setSubscriptions(spaces) { this.subscriptions = spaces; }
91
97
  getSubscriptions() { return this.subscriptions; }
98
+ /** Record a pushed task cancellation (P3 `task.cancelled` deliver). */
99
+ handleCancelled(payload) {
100
+ const id = payload?.id;
101
+ if (typeof id === 'string' && id)
102
+ this.cancelledIds.add(id);
103
+ }
104
+ getCancelledIds() { return [...this.cancelledIds]; }
92
105
  /** A `workspace.record` event arrived — surface it on the record long-poll (no dedup: each write
93
106
  * is a distinct wake; storage stays the source of truth for content via an authorized read). */
94
107
  handleRecord(payload) {
@@ -221,6 +234,8 @@ export async function runServeDaemon(opts) {
221
234
  ch.handleTask(payload, 'deliver');
222
235
  else if (kind === 'workspace.record')
223
236
  ch.handleRecord(payload);
237
+ else if (kind === 'task.cancelled')
238
+ ch.handleCancelled(payload);
224
239
  },
225
240
  onBacklog: ({ tasks, messages }) => {
226
241
  for (const t of tasks)
@@ -418,6 +433,19 @@ export async function runServeDaemon(opts) {
418
433
  data: { agent: entry.agent, owner: entry.owner, received_at: item.receivedAt, reconnects: channels.get(entry.agent).reconnects, event: item.event },
419
434
  });
420
435
  });
436
+ // GET /local/cancelled?agent= — task ids the node has pushed as cancelled (P3). The daemon checks
437
+ // this loopback set before a dispatch instead of scanning the owner-scoped `agents.cancel.*` memory.
438
+ app.get('/local/cancelled', (req, res) => {
439
+ let entry;
440
+ try {
441
+ entry = resolveAgent(req);
442
+ }
443
+ catch (err) {
444
+ res.status(400).json({ ok: false, error: { code: 'UNKNOWN_AGENT', message: err.message } });
445
+ return;
446
+ }
447
+ res.json({ ok: true, data: { agent: entry.agent, cancelled: channels.get(entry.agent).getCancelledIds() } });
448
+ });
421
449
  // ── Tool-call surface: deterministic shell-callable tool dispatch over the
422
450
  // tunnel. Same handler registry as `aimeat connect call`, but routed through
423
451
  // the agent's tunnel-backed client — one loopback POST, no per-call subprocess