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
@@ -1,16 +1,21 @@
1
1
  /**
2
2
  * @file mindmap.js
3
- * @description Interactive, DETERMINISTIC structure mindmap for an organism or a single workspace.
4
- * Reuses the offers "Kartta" pattern: a strict-security Mermaid diagram (no Mermaid click handlers)
5
- * whose node ids we generate ourselves (`ws{i}`, `ws{i}_sp{j}`, `mem{i}`, `ag{i}`, `sp{j}`) and then
6
- * resolve from a click on the rendered SVG → onNavigate(target). Options (depth level, show users,
7
- * show activity counts, heatmap) regenerate the source, so the map re-renders live. This is SEPARATE
8
- * from the README and the OKF overview: it is generated from the /graph data, not authored, and not
9
- * part of OKF. Heatmap colours are read from theme CSS variables (no hardcoded colours).
10
- * @structure StructureMindmap({ scope, graph, onNavigate, label }); buildOrganismMindmap; buildWorkspaceMindmap
3
+ * @description Interactive, DETERMINISTIC structure map for an organism or a single workspace, with
4
+ * selectable chart types — **mindmap** (radial, the default; most compact / best at-a-glance),
5
+ * **flowchart LR** and **flowchart TD** (familiar tree views). Reuses the offers "Kartta" pattern:
6
+ * strict-security Mermaid (no Mermaid click handlers); flowchart nodes carry ids we generate
7
+ * (`ws{i}`, `ws{i}_sp{j}`, `mem{i}`, `ag{i}`, `sp{j}`) and a click resolves them by id, while mindmap
8
+ * nodes resolve by their label text (mindmap can't carry arbitrary ids). Options (chart type, detail
9
+ * level, show users, show activity, heatmap) regenerate the source and are PERSISTED per user per
10
+ * organism/workspace in localStorage. Generated from the /graph data — separate from the README and
11
+ * the OKF outline, and never persisted server-side. Heatmap colours come from theme CSS variables.
12
+ * @structure StructureMindmap({ scope, graph, onNavigate, label, storageKey });
13
+ * buildOrganismMindmap / buildWorkspaceMindmap (chart-type aware, used here + by the timeline)
11
14
  * @usage import { StructureMindmap } from '/views/profile/organisms/mindmap.js';
12
15
  * @version-history
13
16
  * v1.0.0 — 2026-06-22 — Initial: clickable organism/workspace mindmap with level/users/activity/heatmap.
17
+ * v1.1.0 — 2026-06-22 — Chart types (mindmap default + flowchart LR/TD), per-org/ws localStorage
18
+ * persistence, label-text click resolution for mindmap mode.
14
19
  */
15
20
  import { h } from 'preact';
16
21
  import { useState } from 'preact/hooks';
@@ -19,12 +24,16 @@ const html = htm.bind(h);
19
24
  import { t } from '/js/i18n.js';
20
25
  import { Mermaid } from '/components/Mermaid.js';
21
26
 
22
- /** Sanitize a label for a Mermaid quoted string (strip quotes/newlines/structural chars, clip). */
27
+ /** Sanitize a label for a Mermaid quoted flowchart string. */
23
28
  function mmdLabel(s) {
24
29
  return String(s ?? '').replace(/["\n\r]/g, ' ').replace(/[<>|{}[\]]/g, '').trim().slice(0, 48) || '—';
25
30
  }
31
+ /** Stricter sanitize for MINDMAP labels — also strips parentheses (mindmap treats `(...)` as a node
32
+ * shape, so a label like "Ledger (410)" would mis-parse). Counts are shown with " · N" instead. */
33
+ function mmLabel(s) {
34
+ return String(s ?? '').replace(/["\n\r]/g, ' ').replace(/[<>|{}[\]()]/g, '').trim().slice(0, 48) || '—';
35
+ }
26
36
 
27
- /** Bucket an item count into a heat class index (0 = none → no class). */
28
37
  function heatBucket(n) {
29
38
  if (!n) return 0;
30
39
  if (n <= 3) return 1;
@@ -32,7 +41,7 @@ function heatBucket(n) {
32
41
  return 3;
33
42
  }
34
43
 
35
- /** Read heat colours from theme CSS variables (source of truth stays in theme.css). */
44
+ /** Heat colours from theme CSS variables (source of truth = theme.css). */
36
45
  function heatColors() {
37
46
  const cs = typeof window !== 'undefined' ? getComputedStyle(document.documentElement) : null;
38
47
  const v = (n, fb) => ((cs && cs.getPropertyValue(n).trim()) || fb);
@@ -43,8 +52,8 @@ function heatColors() {
43
52
  };
44
53
  }
45
54
 
46
- /** Build the classDef + class lines for the heatmap (only for nodes with content). */
47
- function heatLines(assignments) {
55
+ /** classDef + class lines for the flowchart heatmap (mindmap uses :::class + CSS instead). */
56
+ function flowchartHeatLines(assignments) {
48
57
  if (!assignments.length) return [];
49
58
  const c = heatColors();
50
59
  const lines = [
@@ -56,20 +65,21 @@ function heatLines(assignments) {
56
65
  return lines;
57
66
  }
58
67
 
59
- /** Short activity label suffix for a node (counts + last update), when showActivity is on. */
60
68
  function activitySuffix(opts, { records, documents, lastActivity }) {
61
69
  if (!opts.showActivity) return '';
62
70
  const bits = [];
63
- if (documents != null && documents) bits.push(`${documents}d`);
64
- if (records != null && records) bits.push(`${records}r`);
71
+ if (documents) bits.push(`${documents}d`);
72
+ if (records) bits.push(`${records}r`);
65
73
  let s = bits.length ? ` · ${bits.join('+')}` : '';
66
74
  if (lastActivity) s += ` · ${String(lastActivity).slice(0, 10)}`;
67
75
  return s;
68
76
  }
69
77
 
70
- /** Build deterministic Mermaid source for an ORGANISM graph + render options. */
71
- export function buildOrganismMindmap(graph, opts) {
72
- const lines = ['flowchart LR', ` org["${mmdLabel(graph.name)}"]`];
78
+ /* ── Flowchart builders (LR / TD) — node ids encode the target for click resolution ── */
79
+
80
+ function buildOrgFlowchart(graph, opts) {
81
+ const dir = opts.chartType === 'flowchart-td' ? 'TD' : 'LR';
82
+ const lines = [`flowchart ${dir}`, ` org["${mmdLabel(graph.name)}"]`];
73
83
  const heat = [];
74
84
  (graph.workspaces || []).forEach((w, i) => {
75
85
  const wsTotal = (w.totalRecords || 0) + (w.totalDocuments || 0);
@@ -78,8 +88,8 @@ export function buildOrganismMindmap(graph, opts) {
78
88
  if (opts.heatmap) { const b = heatBucket(wsTotal); if (b) heat.push({ id: `ws${i}`, bucket: b }); }
79
89
  if (opts.level === 'spaces' || opts.level === 'counts') {
80
90
  (w.spaces || []).forEach((sp, j) => {
81
- const countSuffix = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
82
- lines.push(` ws${i} --> ws${i}_sp${j}["${mmdLabel(sp.name)}${countSuffix}"]`);
91
+ const c = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
92
+ lines.push(` ws${i} --> ws${i}_sp${j}["${mmdLabel(sp.name)}${c}"]`);
83
93
  if (opts.heatmap) { const b = heatBucket(sp.count || 0); if (b) heat.push({ id: `ws${i}_sp${j}`, bucket: b }); }
84
94
  });
85
95
  }
@@ -88,81 +98,163 @@ export function buildOrganismMindmap(graph, opts) {
88
98
  (graph.members || []).forEach((m, i) => lines.push(` org --> mem${i}["👤 ${mmdLabel(m.name)}"]`));
89
99
  (graph.agents || []).forEach((a, i) => lines.push(` org --> ag${i}["🤖 ${mmdLabel(a.name || a.gaii)}"]`));
90
100
  }
91
- return lines.concat(heatLines(heat)).join('\n');
101
+ return lines.concat(flowchartHeatLines(heat)).join('\n');
92
102
  }
93
103
 
94
- /** Build deterministic Mermaid source for a single WORKSPACE graph node + render options. */
95
- export function buildWorkspaceMindmap(node, opts) {
104
+ function buildWsFlowchart(node, opts) {
105
+ const dir = opts.chartType === 'flowchart-td' ? 'TD' : 'LR';
96
106
  const wsLabel = mmdLabel(node.name) + activitySuffix(opts, { records: node.totalRecords, documents: node.totalDocuments, lastActivity: node.lastActivity });
97
- const lines = ['flowchart LR', ` wsroot["${wsLabel}"]`];
107
+ const lines = [`flowchart ${dir}`, ` wsroot["${wsLabel}"]`];
98
108
  const heat = [];
99
109
  if (opts.level !== 'ws') {
100
110
  (node.spaces || []).forEach((sp, j) => {
101
- const countSuffix = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
102
- lines.push(` wsroot --> sp${j}["${mmdLabel(sp.name)}${countSuffix}"]`);
111
+ const c = opts.level === 'counts' ? ` (${sp.count || 0})` : '';
112
+ lines.push(` wsroot --> sp${j}["${mmdLabel(sp.name)}${c}"]`);
103
113
  if (opts.heatmap) { const b = heatBucket(sp.count || 0); if (b) heat.push({ id: `sp${j}`, bucket: b }); }
104
114
  });
105
115
  }
106
- return lines.concat(heatLines(heat)).join('\n');
116
+ return lines.concat(flowchartHeatLines(heat)).join('\n');
117
+ }
118
+
119
+ /* ── Mindmap builders (radial) — labels resolve clicks by text. Mermaid's mindmap does NOT support
120
+ * the `:::class` node-class syntax (it renders it as literal text), so the heatmap is shown with a
121
+ * trailing marker emoji instead of a coloured shape. ── */
122
+
123
+ const HEAT_MARK = { 1: '🟢', 2: '🔶', 3: '🔥' };
124
+ function heatMark(opts, n) { if (!opts.heatmap) return ''; const b = heatBucket(n); return b ? ` ${HEAT_MARK[b]}` : ''; }
125
+
126
+ function buildOrgMindmap(graph, opts) {
127
+ const lines = ['mindmap', ` root((${mmLabel(graph.name)}))`];
128
+ (graph.workspaces || []).forEach((w) => {
129
+ const wsTotal = (w.totalRecords || 0) + (w.totalDocuments || 0);
130
+ const wsLabel = mmLabel(w.name) + activitySuffix(opts, { records: w.totalRecords, documents: w.totalDocuments, lastActivity: w.lastActivity });
131
+ lines.push(` ${wsLabel}${heatMark(opts, wsTotal)}`);
132
+ if (opts.level === 'spaces' || opts.level === 'counts') {
133
+ (w.spaces || []).forEach((sp) => {
134
+ const c = opts.level === 'counts' ? ` · ${sp.count || 0}` : '';
135
+ lines.push(` ${mmLabel(sp.name)}${c}${heatMark(opts, sp.count || 0)}`);
136
+ });
137
+ }
138
+ });
139
+ if (opts.showUsers) {
140
+ (graph.members || []).forEach((m) => lines.push(` 👤 ${mmLabel(m.name)}`));
141
+ (graph.agents || []).forEach((a) => lines.push(` 🤖 ${mmLabel(a.name || a.gaii)}`));
142
+ }
143
+ return lines.join('\n');
144
+ }
145
+
146
+ function buildWsMindmap(node, opts) {
147
+ const wsLabel = mmLabel(node.name) + activitySuffix(opts, { records: node.totalRecords, documents: node.totalDocuments, lastActivity: node.lastActivity });
148
+ const lines = ['mindmap', ` root((${wsLabel}))`];
149
+ if (opts.level !== 'ws') {
150
+ (node.spaces || []).forEach((sp) => {
151
+ const c = opts.level === 'counts' ? ` · ${sp.count || 0}` : '';
152
+ lines.push(` ${mmLabel(sp.name)}${c}${heatMark(opts, sp.count || 0)}`);
153
+ });
154
+ }
155
+ return lines.join('\n');
107
156
  }
108
157
 
109
- /** Resolve a click on a rendered Mermaid node to a navigation target, by parsing the node's DOM id
110
- * (which embeds the id we generated). Returns null for the root / unknown nodes. */
111
- function resolveClick(scope, graph, domId) {
158
+ /** Build deterministic Mermaid source for an ORGANISM graph honouring opts.chartType. */
159
+ export function buildOrganismMindmap(graph, opts) {
160
+ return opts.chartType === 'mindmap' ? buildOrgMindmap(graph, opts) : buildOrgFlowchart(graph, opts);
161
+ }
162
+ /** Build deterministic Mermaid source for a single WORKSPACE graph node honouring opts.chartType. */
163
+ export function buildWorkspaceMindmap(node, opts) {
164
+ return opts.chartType === 'mindmap' ? buildWsMindmap(node, opts) : buildWsFlowchart(node, opts);
165
+ }
166
+
167
+ /* ── Click resolution ── */
168
+
169
+ /** Flowchart: parse the clicked node's DOM id (it embeds the generated id). */
170
+ function resolveFlowchartClick(scope, graph, domId) {
112
171
  if (scope === 'organism') {
113
172
  let m = /ws(\d+)_sp(\d+)/.exec(domId);
114
- if (m) {
115
- const w = graph.workspaces?.[+m[1]]; const sp = w?.spaces?.[+m[2]];
116
- if (w && sp) return { type: 'space', wsId: w.id, space: sp.name };
117
- return null;
118
- }
173
+ if (m) { const w = graph.workspaces?.[+m[1]]; const sp = w?.spaces?.[+m[2]]; return (w && sp) ? { type: 'space', wsId: w.id, space: sp.name } : null; }
119
174
  m = /ws(\d+)(?![_\d])/.exec(domId);
120
175
  if (m) { const w = graph.workspaces?.[+m[1]]; return w ? { type: 'workspace', wsId: w.id } : null; }
121
176
  if (/mem\d+/.test(domId) || /ag\d+/.test(domId)) return { type: 'members' };
122
177
  return null;
123
178
  }
124
- // workspace scope
125
179
  const m = /sp(\d+)/.exec(domId);
126
180
  if (m) { const sp = graph.spaces?.[+m[1]]; return sp ? { type: 'space', space: sp.name } : null; }
127
181
  return null;
128
182
  }
129
183
 
184
+ /** Mindmap: resolve by the clicked node's label text (mindmap nodes have no stable id). Strips the
185
+ * activity/count suffix and the user emoji, then matches the core name to a workspace / space /
186
+ * member. Workspace + member matches are exact; a space name is matched to the first workspace that
187
+ * has it (mindmap can't disambiguate a repeated space name across workspaces — opens that workspace). */
188
+ function resolveMindmapClick(scope, graph, text) {
189
+ const core = String(text || '').trim().split(' · ')[0].replace(/^👤\s*|^🤖\s*/, '').trim();
190
+ if (!core) return null;
191
+ if (scope === 'organism') {
192
+ for (const w of (graph.workspaces || [])) if (w.name === core) return { type: 'workspace', wsId: w.id };
193
+ for (const w of (graph.workspaces || [])) for (const sp of (w.spaces || [])) if (sp.name === core) return { type: 'workspace', wsId: w.id };
194
+ if ((graph.members || []).some(m => m.name === core) || (graph.agents || []).some(a => (a.name || a.gaii) === core)) return { type: 'members' };
195
+ return null;
196
+ }
197
+ for (const sp of (graph.spaces || [])) if (sp.name === core) return { type: 'space', space: sp.name };
198
+ return null;
199
+ }
200
+
201
+ const CHART_TYPES = [
202
+ { key: 'mindmap', label: 'mindmap.chartMindmap', fallback: 'Mindmap' },
203
+ { key: 'flowchart-lr', label: 'mindmap.chartLr', fallback: 'Flowchart →' },
204
+ { key: 'flowchart-td', label: 'mindmap.chartTd', fallback: 'Flowchart ↓' },
205
+ ];
130
206
  const LEVELS = [
131
207
  { key: 'ws', label: 'mindmap.levelWs', fallback: 'Workspaces' },
132
208
  { key: 'spaces', label: 'mindmap.levelSpaces', fallback: '+ Spaces' },
133
209
  { key: 'counts', label: 'mindmap.levelCounts', fallback: '+ Counts' },
134
210
  ];
135
211
 
212
+ const DEFAULTS = { chartType: 'mindmap', level: 'spaces', showUsers: false, showActivity: true, heatmap: true };
213
+
214
+ /** Load persisted options for this org/ws (per-user via localStorage), merged over defaults. */
215
+ function loadOpts(storageKey) {
216
+ if (!storageKey) return { ...DEFAULTS };
217
+ try {
218
+ const raw = localStorage.getItem(`aimeat.mm.${storageKey}`);
219
+ return raw ? { ...DEFAULTS, ...JSON.parse(raw) } : { ...DEFAULTS };
220
+ } catch { return { ...DEFAULTS }; }
221
+ }
222
+ function saveOpts(storageKey, opts) {
223
+ if (!storageKey) return;
224
+ try { localStorage.setItem(`aimeat.mm.${storageKey}`, JSON.stringify(opts)); } catch { /* quota/private mode — non-fatal */ }
225
+ }
226
+
136
227
  /**
137
- * StructureMindmap — collapsible interactive map. Default collapsed so the 3MB Mermaid bundle only
138
- * loads when asked for. scope 'organism' (root = organism, with workspaces → spaces + members/agents)
139
- * or 'workspace' (root = the workspace, with its spaces). onNavigate(target) receives:
140
- * { type:'workspace', wsId } | { type:'space', wsId?, space } | { type:'members' }
141
- * @param {{ scope: 'organism'|'workspace', graph: object, onNavigate: (t:object)=>void, label?: string }} props
228
+ * StructureMindmap — collapsible interactive map. Default collapsed so the Mermaid bundle only loads
229
+ * when asked for. scope 'organism' (root → workspaces → spaces + members/agents) or 'workspace'.
230
+ * onNavigate(target): { type:'workspace', wsId } | { type:'space', wsId?, space } | { type:'members' }.
231
+ * @param {{ scope:'organism'|'workspace', graph:object, onNavigate:(t:object)=>void, label?:string, storageKey?:string }} props
142
232
  */
143
- export function StructureMindmap({ scope, graph, onNavigate, label }) {
233
+ export function StructureMindmap({ scope, graph, onNavigate, label, storageKey }) {
144
234
  const [open, setOpen] = useState(false);
145
- const [level, setLevel] = useState(scope === 'workspace' ? 'spaces' : 'spaces');
146
- const [showUsers, setShowUsers] = useState(false);
147
- const [showActivity, setShowActivity] = useState(true);
148
- const [heatmap, setHeatmap] = useState(true);
235
+ const [opts, setOpts] = useState(() => loadOpts(storageKey));
149
236
 
150
237
  if (!graph) return null;
151
- const opts = { level, showUsers, showActivity, heatmap };
238
+ const set = (patch) => setOpts(prev => { const next = { ...prev, ...patch }; saveOpts(storageKey, next); return next; });
239
+
152
240
  const src = open
153
241
  ? (scope === 'organism' ? buildOrganismMindmap(graph, opts) : buildWorkspaceMindmap(graph, opts))
154
242
  : '';
155
243
 
156
244
  const onMapClick = (e) => {
157
- const node = e.target?.closest?.('.node');
158
- if (!node) return;
159
- const target = resolveClick(scope, graph, node.id || '');
160
- if (target) onNavigate?.(target);
245
+ if (opts.chartType === 'mindmap') {
246
+ const g = e.target?.closest?.('.mindmap-node, [class*="mindmap"], g');
247
+ const target = resolveMindmapClick(scope, graph, (g?.textContent) || e.target?.textContent || '');
248
+ if (target) onNavigate?.(target);
249
+ } else {
250
+ const node = e.target?.closest?.('.node');
251
+ if (!node) return;
252
+ const target = resolveFlowchartClick(scope, graph, node.id || '');
253
+ if (target) onNavigate?.(target);
254
+ }
161
255
  };
162
256
 
163
- const lbl = label || (scope === 'organism'
164
- ? (t('mindmap.titleOrg') || 'Organism map')
165
- : (t('mindmap.titleWs') || 'Workspace map'));
257
+ const lbl = label || (scope === 'organism' ? (t('mindmap.titleOrg') || 'Organism map') : (t('mindmap.titleWs') || 'Workspace map'));
166
258
 
167
259
  return html`
168
260
  <div class="pj-mindmap">
@@ -173,17 +265,23 @@ export function StructureMindmap({ scope, graph, onNavigate, label }) {
173
265
  ${open ? html`
174
266
  <div class="pj-mindmap-body card-detail">
175
267
  <div class="pj-mindmap-opts">
268
+ <label class="pj-mm-opt">
269
+ <span>${t('mindmap.chart') || 'Chart'}</span>
270
+ <select class="input-field input-sm" value=${opts.chartType} onChange=${e => set({ chartType: e.target.value })}>
271
+ ${CHART_TYPES.map(c => html`<option value=${c.key}>${t(c.label) || c.fallback}</option>`)}
272
+ </select>
273
+ </label>
176
274
  <label class="pj-mm-opt">
177
275
  <span>${t('mindmap.level') || 'Level'}</span>
178
- <select class="input-field input-sm" value=${level} onChange=${e => setLevel(e.target.value)}>
276
+ <select class="input-field input-sm" value=${opts.level} onChange=${e => set({ level: e.target.value })}>
179
277
  ${LEVELS.map(l => html`<option value=${l.key}>${t(l.label) || l.fallback}</option>`)}
180
278
  </select>
181
279
  </label>
182
280
  ${scope === 'organism' ? html`
183
- <label class="pj-mm-check"><input type="checkbox" checked=${showUsers} onChange=${e => setShowUsers(e.target.checked)} /> ${t('mindmap.showUsers') || 'Users'}</label>
281
+ <label class="pj-mm-check"><input type="checkbox" checked=${opts.showUsers} onChange=${e => set({ showUsers: e.target.checked })} /> ${t('mindmap.showUsers') || 'Users'}</label>
184
282
  ` : null}
185
- <label class="pj-mm-check"><input type="checkbox" checked=${showActivity} onChange=${e => setShowActivity(e.target.checked)} /> ${t('mindmap.showActivity') || 'Activity'}</label>
186
- <label class="pj-mm-check"><input type="checkbox" checked=${heatmap} onChange=${e => setHeatmap(e.target.checked)} /> ${t('mindmap.heatmap') || 'Heatmap'}</label>
283
+ <label class="pj-mm-check"><input type="checkbox" checked=${opts.showActivity} onChange=${e => set({ showActivity: e.target.checked })} /> ${t('mindmap.showActivity') || 'Activity'}</label>
284
+ <label class="pj-mm-check"><input type="checkbox" checked=${opts.heatmap} onChange=${e => set({ heatmap: e.target.checked })} /> ${t('mindmap.heatmap') || 'Heatmap'}</label>
187
285
  </div>
188
286
  <div class="pj-mindmap-canvas of-mapwrap--clickable" onClick=${onMapClick}>
189
287
  <${Mermaid} chart=${src} />
@@ -8,6 +8,8 @@
8
8
  * @usage import { OrgSearch, IncomingInvitations, BoardPreview } from '/views/profile/organisms/panels.js';
9
9
  * @version-history
10
10
  * v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
11
+ * v1.1.0 — 2026-06-22 — OrgSearch: instant (debounced) indexed search, results grouped by workspace,
12
+ * and clicking a hit deep-links to the exact record/document (via the workspace openDoc handoff).
11
13
  */
12
14
  import { h } from 'preact';
13
15
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -32,34 +34,49 @@ export function OrgSearch({ orgId, onOpenWorkspace }) {
32
34
  const [results, setResults] = useState(null);
33
35
  const [busy, setBusy] = useState(false);
34
36
 
35
- const doSearch = async () => {
37
+ // Instant (debounced) search across the organism — indexed FTS backend (GET /:id/search).
38
+ useEffect(() => {
36
39
  const query = q.trim();
37
- if (query.length < 2) return;
40
+ if (query.length < 2) { setResults(null); setBusy(false); return undefined; }
41
+ let cancelled = false;
38
42
  setBusy(true);
39
- try {
40
- const r = await orgService.searchOrganism(orgId, query);
41
- setResults(r?.data?.results || []);
42
- } catch { setResults([]); }
43
- finally { setBusy(false); }
43
+ const tid = setTimeout(async () => {
44
+ try { const r = await orgService.searchOrganism(orgId, query); if (!cancelled) setResults(r?.data?.results || []); }
45
+ catch { if (!cancelled) setResults([]); }
46
+ finally { if (!cancelled) setBusy(false); }
47
+ }, 220);
48
+ return () => { cancelled = true; clearTimeout(tid); };
49
+ }, [q, orgId]);
50
+
51
+ // Open a hit at the exact record/document: stash the deep-link the workspace reads on mount, then
52
+ // navigate to that workspace (its openDoc effect resolves namespace → space tab + opens the item).
53
+ const openHit = (r) => {
54
+ try { sessionStorage.setItem(`aimeat.ws.${orgId}.${r.ws}.openDoc`, JSON.stringify({ namespace: r.namespace, id: r.id })); } catch { /* noop */ }
55
+ onOpenWorkspace?.(r.ws);
44
56
  };
45
57
 
58
+ // Group hits by workspace so a big organism's results stay legible.
59
+ const byWs = {};
60
+ for (const r of (results || [])) (byWs[r.ws] = byWs[r.ws] || { name: r.wsName || r.ws, hits: [] }).hits.push(r);
61
+
46
62
  return html`
47
63
  <div class="pj-orgsearch">
48
64
  <div class="flex-row-wrap">
49
65
  <input class="input-field input-sm pj-orgsearch-input" placeholder=${t('organisms.searchPlaceholder') || 'Find records & documents…'} value=${q}
50
- onInput=${(e) => setQ(e.target.value)} onKeyDown=${(e) => { if (e.key === 'Enter') doSearch(); }} />
51
- <button class="btn-outline btn-sm" disabled=${busy || q.trim().length < 2} onClick=${doSearch}>${t('organisms.search') || 'Search'}</button>
66
+ onInput=${(e) => setQ(e.target.value)} />
67
+ ${busy ? html`<${Spinner} />` : null}
68
+ ${results !== null ? html`<button class="btn-ghost btn-sm" onClick=${() => setQ('')}>${t('search.clear') || 'Clear'}</button>` : null}
52
69
  </div>
53
- ${results !== null && results.length === 0 ? html`<div class="section-desc">${t('organisms.searchNoResults') || 'No matches.'}</div>` : null}
54
- ${(results || []).map(r => html`
55
- <div class="pj-access-row card-clickable" key=${r.ws + '/' + r.space + '/' + r.id} role="button" onClick=${() => onOpenWorkspace?.(r.ws)}>
56
- <span>
57
- <b>${(r.title)}</b>
58
- <span class="pj-mini"> — ${(r.wsName || r.ws)} · ${(r.space)}</span>
59
- <div class="pj-mini">${(r.snippet)}</div>
60
- </span>
61
- </div>
62
- `)}
70
+ ${results !== null && results.length === 0 && !busy ? html`<div class="section-desc">${t('search.noMatches') || 'No matches.'}</div>` : null}
71
+ ${Object.entries(byWs).map(([ws, grp]) => html`
72
+ <div class="pj-search-group" key=${ws}>
73
+ <div class="pj-search-group-head">${(grp.name)}<span class="pj-org-tab-count">${grp.hits.length}</span></div>
74
+ ${grp.hits.map(r => html`
75
+ <button class="pj-search-hit" key=${r.space + '/' + r.id} onClick=${() => openHit(r)}>
76
+ <span class="pj-search-hit-title">${(r.title)} <span class="pj-mini">· ${(r.space)}</span></span>
77
+ <span class="pj-search-hit-snippet">${(r.snippet)}</span>
78
+ </button>`)}
79
+ </div>`)}
63
80
  </div>
64
81
  `;
65
82
  }
@@ -9,6 +9,8 @@
9
9
  * @usage import { TimelinePanel } from '/views/profile/organisms/timeline-panel.js';
10
10
  * @version-history
11
11
  * v1.0.0 — 2026-06-22 — Initial: structure timeline view over trackable-memory history (Osa D3).
12
+ * v1.1.0 — 2026-06-22 — Add a Mermaid `timeline` diagram of the changes; the selected-snapshot map
13
+ * honours the chart type the user picked for this organism's mindmap (localStorage).
12
14
  */
13
15
  import { h } from 'preact';
14
16
  import { useState } from 'preact/hooks';
@@ -41,6 +43,30 @@ function totals(fp) {
41
43
  return { workspaces: ws.length, documents: docs, records: recs, members: fp.memberCount || 0 };
42
44
  }
43
45
 
46
+ /** The chart type the user picked for THIS organism's mindmap (shared with the snapshot map below). */
47
+ function readChartType(orgId) {
48
+ try {
49
+ const raw = localStorage.getItem(`aimeat.mm.org.${orgId}`);
50
+ return (raw && JSON.parse(raw).chartType) || 'mindmap';
51
+ } catch { return 'mindmap'; }
52
+ }
53
+
54
+ /** A Mermaid `timeline` diagram of the structural changes (chronological). `:` is the timeline
55
+ * separator, so it is stripped from event text; multiple events on one day share a row. */
56
+ function buildTimelineDiagram(rows) {
57
+ const chron = rows.slice().reverse(); // rows are newest-first → oldest-first for the time axis
58
+ const byDate = new Map();
59
+ for (const r of chron) {
60
+ const d = String(r.at).slice(0, 10) || '—';
61
+ const ev = String(r.event || '').replace(/[:\n\r]/g, ' ').replace(/[<>|{}]/g, '').trim().slice(0, 60) || 'muutos';
62
+ if (!byDate.has(d)) byDate.set(d, []);
63
+ byDate.get(d).push(ev);
64
+ }
65
+ const lines = ['timeline'];
66
+ for (const [d, evs] of byDate) lines.push(` ${d} : ${evs.join(' : ')}`);
67
+ return lines.join('\n');
68
+ }
69
+
44
70
  export function TimelinePanel({ orgId }) {
45
71
  const [open, setOpen] = useState(false);
46
72
  const [loaded, setLoaded] = useState(false);
@@ -79,6 +105,9 @@ export function TimelinePanel({ orgId }) {
79
105
  : (!rows.length
80
106
  ? html`<div class="section-desc">${t('timeline.empty') || 'No structural history yet.'}</div>`
81
107
  : html`
108
+ <div class="pj-timeline-diagram">
109
+ <${Mermaid} chart=${buildTimelineDiagram(rows)} />
110
+ </div>
82
111
  <div class="pj-timeline-grid">
83
112
  <ul class="pj-timeline-list">
84
113
  ${rows.map(r => {
@@ -95,7 +124,7 @@ export function TimelinePanel({ orgId }) {
95
124
  </ul>
96
125
  <div class="pj-timeline-map">
97
126
  ${selected
98
- ? html`<${Mermaid} chart=${buildOrganismMindmap({ name: t('timeline.snapshot') || 'Snapshot', workspaces: selected.workspaces || [], members: [], agents: [] }, { level: 'counts', showUsers: false, showActivity: true, heatmap: true })} />`
127
+ ? html`<${Mermaid} chart=${buildOrganismMindmap({ name: t('timeline.snapshot') || 'Snapshot', workspaces: selected.workspaces || [], members: [], agents: [] }, { chartType: readChartType(orgId), level: 'counts', showUsers: false, showActivity: true, heatmap: true })} />`
99
128
  : html`<div class="section-desc">${t('timeline.pick') || 'Pick a point to see the structure then.'}</div>`}
100
129
  </div>
101
130
  </div>`)}
@@ -7,6 +7,9 @@
7
7
  * @usage import { WorkspaceComments } from '/views/profile/organisms/workspace-comments.js';
8
8
  * @version-history
9
9
  * v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split.
10
+ * v1.1.0 — 2026-06-22 — Optional BATCHED mode (batched/initialComments/onReload): the parent fetches
11
+ * all visible threads in one /comments/batch request instead of each thread self-fetching +
12
+ * self-subscribing to 'organisms' events (which was the per-document comments request storm).
10
13
  */
11
14
  import { h } from 'preact';
12
15
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -23,8 +26,12 @@ import * as orgService from '/js/services/organisms.js';
23
26
  * (comment on a specific passage) and threaded replies (parentId). Backend: /v1/organisms/:id/comments.
24
27
  * Agents use the same endpoints via aimeat_workspace_comment(s).
25
28
  */
26
- export function WorkspaceComments({ orgId, ws, space, instanceId, showToast }) {
27
- const [comments, setComments] = useState(null);
29
+ export function WorkspaceComments({ orgId, ws, space, instanceId, showToast, batched, initialComments, onReload }) {
30
+ // BATCHED mode (parent owns the fetch): the container fetches every visible thread in one
31
+ // /comments/batch request and passes this thread's slice via `initialComments`; we don't self-fetch
32
+ // or self-subscribe (that per-thread fan-out was the comments request storm). STANDALONE mode
33
+ // (no `batched`): unchanged — self-fetch on mount + refresh on 'organisms' events.
34
+ const [comments, setComments] = useState(batched ? (initialComments ?? null) : null);
28
35
  const [body, setBody] = useState('');
29
36
  const [anchorQuote, setAnchorQuote] = useState('');
30
37
  const [replyTo, setReplyTo] = useState(null); // { id, body } of the comment being replied to
@@ -33,13 +40,16 @@ export function WorkspaceComments({ orgId, ws, space, instanceId, showToast }) {
33
40
  const mine = (author) => author && (author === me.gaii || author === me.ghii);
34
41
 
35
42
  const load = useCallback(async () => {
43
+ if (batched) { await onReload?.(); return; } // parent re-runs the batch → flows back via initialComments
36
44
  if (!ws || !space || !instanceId) return;
37
45
  const r = await orgService.listComments(orgId, ws, space, instanceId).catch(() => null);
38
46
  setComments(r?.data?.comments || []);
39
- }, [orgId, ws, space, instanceId]);
40
- useEffect(() => { load(); }, [load]);
47
+ }, [orgId, ws, space, instanceId, batched, onReload]);
48
+ // Batched: keep local view in sync with the prop (undefined = still loading → null). Standalone: self-fetch.
49
+ useEffect(() => { if (batched) setComments(initialComments ?? null); }, [batched, initialComments]);
50
+ useEffect(() => { if (!batched) load(); }, [load, batched]);
41
51
  const liveRef = useRef(load); liveRef.current = load;
42
- useEffect(() => onLiveUpdate(['organisms'], () => liveRef.current()), []);
52
+ useEffect(() => { if (batched) return undefined; return onLiveUpdate(['organisms'], () => liveRef.current()); }, [batched]);
43
53
 
44
54
  const submit = async () => {
45
55
  const text = body.trim();
@@ -10,6 +10,9 @@
10
10
  * @version-history
11
11
  * v1.0.0 — 2026-06-19 — Extracted from organisms-tab.js during the module split; the hidden file
12
12
  * input uses the .pj-hidden-input class instead of an inline style (Rule 8).
13
+ * v1.1.0 — 2026-06-22 — Kill the per-workspace fetch storm: one discoverWorkspaces({include:'enrichment'})
14
+ * replaces the 1 + 3N (getWorkspace+activity+participants) fan-out; pending-review counts come inline;
15
+ * the 'organisms' live refresh is debounced (1.5s) so an agent-driven event burst is one reload.
13
16
  */
14
17
  import { h } from 'preact';
15
18
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -48,32 +51,33 @@ export function WorkspaceList({ org, showToast, onOpen, onCount }) {
48
51
  // Discovery: every workspace in the org (membership-gated) with this user's access status. A member
49
52
  // sees workspaces they can't yet read (access:'none') so they can request access. Row enrichment
50
53
  // (record/doc counts, last event, review counter) loads per workspace afterwards, best effort.
54
+ // ONE enriched request replaces the old discover + per-row getWorkspace+activity+participants
55
+ // fan-out (1 + 3N requests, each re-scanning the same workspace memory). The server computes the
56
+ // same recs/docs/lastEvent/participants + pending-review counts and returns them inline.
51
57
  const load = useCallback(async () => {
52
- const l = await orgService.discoverWorkspaces(orgId);
58
+ const l = await orgService.discoverWorkspaces(orgId, { include: 'enrichment' });
53
59
  setList(l);
54
60
  onCount?.(Array.isArray(l) ? l.length : 0);
55
- orgService.listApprovals(orgId, 'pending').then(aps => {
56
- const by = {};
57
- for (const a of (aps || [])) { const w = a.arguments?.ws; if (w) by[w] = (by[w] || 0) + 1; }
58
- setApprByWs(by);
59
- }).catch(() => {});
60
- for (const w of (l || []).filter(x => x.access !== 'none')) {
61
- Promise.all([
62
- orgService.getWorkspace(orgId, w.id).catch(() => null),
63
- orgService.getWorkspaceActivity(orgId, w.id),
64
- orgService.getWorkspaceParticipants(orgId, w.id).catch(() => null),
65
- ]).then(([wsData, act, parts]) => {
66
- let recs = 0, docs = 0;
67
- for (const ot of (wsData?.manifest?.objectTypes || []).filter(orgService.isMemorySpace)) {
68
- const n = new Set([...(wsData.drafts?.[ot.name] || []), ...(wsData.objects?.[ot.name] || [])].map(d => d.id)).size;
69
- if (orgService.isDocSpace(ot)) docs += n; else recs += n;
70
- }
71
- const lastEv = (act?.events || []).reduce((b, e) => (!b || (e.at || '') > (b.at || '') ? e : b), null);
72
- const owners = [];
73
- for (const n of (parts?.nodes || [])) for (const o of (n.owners || [])) owners.push({ ...o, node: n.id, isLocalNode: n.isLocal });
74
- setWsStats(s => ({ ...s, [w.id]: { recs, docs, lastEv, hasManifest: !!wsData?.manifest, owners } }));
75
- }).catch(() => {});
61
+ const stats = {};
62
+ const reviews = {};
63
+ for (const w of (l || [])) {
64
+ const e = w.enrichment;
65
+ if (w.access === 'none' || !e) continue;
66
+ if (e.pendingReviews) reviews[w.id] = e.pendingReviews;
67
+ stats[w.id] = {
68
+ recs: e.recs || 0,
69
+ docs: e.docs || 0,
70
+ lastEv: e.lastEvent || null,
71
+ hasManifest: !!e.hasManifest,
72
+ // UI only reads owners[].length and per-owner {owner,isCreator,isSelf,isLocalNode,agents.length}.
73
+ owners: (e.participants || []).map(p => ({
74
+ owner: p.owner, node: p.node, isLocalNode: p.isLocalNode,
75
+ isCreator: p.isCreator, isSelf: p.isSelf, agents: new Array(p.agentsCount || 0),
76
+ })),
77
+ };
76
78
  }
79
+ setWsStats(stats);
80
+ setApprByWs(reviews);
77
81
  }, [orgId, onCount]);
78
82
  useEffect(() => { load(); }, [load]);
79
83
 
@@ -103,7 +107,15 @@ export function WorkspaceList({ org, showToast, onOpen, onCount }) {
103
107
  orgService.buildOrganismOverviewMermaid(orgId).then(c => { if (!cancelled) setOverview(c); }).catch(() => {});
104
108
  return () => { cancelled = true; };
105
109
  }, [orgId, list, showOverview]);
106
- useEffect(() => onLiveUpdate(['organisms'], () => load()), [load]);
110
+ // Re-load on organism changes, but DEBOUNCE: on a busy node dozens of agents emit 'organisms'
111
+ // events ~1/sec; a trailing debounce collapses a burst into one reload (the load is now a single
112
+ // cheap request). `onLiveUpdate` reads the Set-typed domains correctly — never via e.detail directly.
113
+ const liveRef = useRef(load); liveRef.current = load;
114
+ useEffect(() => {
115
+ let timer = null;
116
+ const off = onLiveUpdate(['organisms'], () => { clearTimeout(timer); timer = setTimeout(() => liveRef.current(), 1500); });
117
+ return () => { clearTimeout(timer); off(); };
118
+ }, []);
107
119
 
108
120
  const create = async () => {
109
121
  const name = newName.trim();