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
@@ -13,6 +13,8 @@
13
13
  * import KnowledgeTab from './knowledge-tab.js';
14
14
  * html`<${KnowledgeTab} session=${session} showToast=${showToast} onStats=${onStats} />`
15
15
  * @version-history
16
+ * v1.1.0 — 2026-06-22 — Drop the per-package /v1/memory/{key} hydration fallback — the memory list
17
+ * already returns each package's full value inline.
16
18
  * v1.0.0 — 2026-03-17 — Refactor: remove all inline style="" attributes, use CSS
17
19
  * classes (pf-ref-row, pf-ref-icon, pf-external-icon, pf-ref-title, pf-badge-xs,
18
20
  * pf-sharing-box, pf-sharing-heading, kpkg-detail-loading). Replace inline
@@ -68,16 +70,11 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
68
70
  const loadPackages = useCallback(async ({ showSpinner = true } = {}) => {
69
71
  try {
70
72
  if (showSpinner) setLoading(true);
73
+ // The memory list (GET /v1/memory) already returns each entry's full `value`, so there's no need
74
+ // for a per-package /v1/memory/{key} hydration fetch (that fallback never fired in practice).
71
75
  const list = await knowledgeService.listMyPackages();
72
- const hydrated = await Promise.all(list.map(async (pkg) => {
73
- if (pkg.value) return pkg;
74
- try {
75
- const resp = await apiGet('/v1/memory/' + encodeURIComponent(pkg.key));
76
- return { ...pkg, value: resp?.data?.value };
77
- } catch { return pkg; }
78
- }));
79
- setPackages(hydrated);
80
- onStats?.({ knowledge: hydrated.length });
76
+ setPackages(list);
77
+ onStats?.({ knowledge: list.length });
81
78
  } catch { if (showSpinner) setPackages([]); } // keep old list on a transient live-update refetch
82
79
  finally { setLoading(false); }
83
80
  }, [onStats]);
@@ -15,6 +15,11 @@
15
15
  * - PresencePill + PresenceDialog — header status pill that opens the availability settings dialog
16
16
  * - LandingPage — main orchestrator (default export)
17
17
  * @version-history
18
+ * v3.8.0 — 2026-06-22 — Home UsageCard: quota usage bars (memory / files / micro-memory) + resource
19
+ * counts (agents, organisms, apps, connected apps, extensions, cortexes, services), backed by the
20
+ * cached GET /v1/owner/usage endpoint (60s server-side TTL).
21
+ * v3.7.0 — 2026-06-22 — WaitingForYou uses one getWaiting() call (server-aggregated) instead of the
22
+ * per-organism listApprovals + listJoinRequests + listWorkspaces fan-out.
18
23
  * v3.6.0 — 2026-06-21 — Change-password modal handles OAuth accounts with no password yet
19
24
  * (e.g. Google sign-in): fetches has_password from /v1/ghii/me and, when none is set, switches
20
25
  * to a "set a password" flow — drops the current-password field, shows an explanatory hint, and
@@ -434,36 +439,12 @@ function ChangePasswordModal({ onClose, onChanged }) {
434
439
  /* "Waiting for you" — everything that needs the user's decision, aggregated across organisms:
435
440
  * pending publish approvals (per workspace), pending join requests (orgs they manage), and
436
441
  * incoming organism invitations. Renders nothing when there is nothing to do. */
437
- function WaitingForYou({ owner }) {
442
+ function WaitingForYou() {
438
443
  const [items, setItems] = useState(null);
439
- const load = useCallback(async () => {
440
- try {
441
- const out = [];
442
- const orgsResp = await orgService.listOrganisms({ member: owner }).catch(() => null);
443
- const orgs = orgsResp?.data?.organisms || [];
444
- await Promise.all(orgs.map(async (org) => {
445
- const canManage = org.creatorGhii === owner || (org.admins || []).includes(owner);
446
- const [aps, reqs] = await Promise.all([
447
- orgService.listApprovals(org.id, 'pending').catch(() => []),
448
- canManage ? orgService.listJoinRequests(org.id).catch(() => null) : Promise.resolve(null),
449
- ]);
450
- if ((aps || []).length > 0) {
451
- const byWs = {};
452
- for (const a of aps) { const w = a.arguments?.ws || ''; byWs[w] = (byWs[w] || 0) + 1; }
453
- let names = {};
454
- try { names = Object.fromEntries((await orgService.listWorkspaces(org.id)).map(w => [w.id, w.name])); } catch { /* ids will do */ }
455
- for (const [wsId, n] of Object.entries(byWs)) {
456
- out.push({ kind: 'review', n, orgId: org.id, orgName: org.name, wsId, wsName: names[wsId] || wsId });
457
- }
458
- }
459
- const pend = ((reqs?.data?.join_requests) || []).filter(r => r.status === 'pending').length;
460
- if (pend > 0) out.push({ kind: 'join', n: pend, orgId: org.id, orgName: org.name });
461
- }));
462
- const inv = await orgService.listMyInvitations().catch(() => null);
463
- for (const e of (inv?.data?.invitations || [])) out.push({ kind: 'invite', orgName: e?.organism?.name || '' });
464
- setItems(out);
465
- } catch { setItems([]); }
466
- }, [owner]);
444
+ // ONE aggregated request replaces the old per-org fan-out (listOrganisms → per-org listApprovals +
445
+ // listJoinRequests + listWorkspaces + a final listMyInvitations). The server returns the same flat
446
+ // {kind:'review'|'join'|'invite', …} items this widget renders.
447
+ const load = useCallback(async () => { setItems(await orgService.getWaiting()); }, []);
467
448
  useEffect(() => { load(); }, [load]);
468
449
  const liveRef = useRef(load); liveRef.current = load;
469
450
  // Only re-run the per-organism approvals/join-requests fan-out when ORGANISMS actually
@@ -577,6 +558,69 @@ function AgentsCard({ owner }) {
577
558
  `;
578
559
  }
579
560
 
561
+ /* "Usage" — quota usage bars (memory / storage / micro-memory) + resource counts. Backed by the
562
+ * cached GET /v1/owner/usage endpoint (60s server-side TTL), so it's cheap to refetch on each
563
+ * live-update. Surfaces the same kind of quota bar the Memory tab shows, for the whole account. */
564
+ function fmtBytes(n) {
565
+ if (n == null) return '';
566
+ if (n < 1024) return n + ' B';
567
+ if (n < 1024 * 1024) return (n / 1024).toFixed(n < 10240 ? 1 : 0) + ' KB';
568
+ if (n < 1024 * 1024 * 1024) return (n / (1024 * 1024)).toFixed(1) + ' MB';
569
+ return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
570
+ }
571
+
572
+ function UsageCard({ switchTab }) {
573
+ const [u, setU] = useState(null);
574
+ const load = useCallback(async () => {
575
+ try { const r = await apiGet('/v1/owner/usage'); setU(r?.data || null); } catch { setU(null); }
576
+ }, []);
577
+ useEffect(() => { load(); }, [load]);
578
+ const liveRef = useRef(load); liveRef.current = load;
579
+ useEffect(() => onLiveUpdate(['memory', 'files', 'agents', 'apps', 'organisms'], () => liveRef.current()), []);
580
+
581
+ if (!u) return null;
582
+
583
+ const bar = (label, q, usedText) => {
584
+ const pct = q.percent >= 0 ? Math.max(0, Math.min(100, q.percent)) : 0;
585
+ return html`
586
+ <div class="pf-usage-row">
587
+ <div class="pf-usage-head">
588
+ <span class="pf-usage-label">${label}</span>
589
+ <span class="text-meta-sm">${usedText}</span>
590
+ </div>
591
+ <div class="pf-usage-bar"><div class="pf-usage-fill ${pct >= 90 ? 'pf-usage-fill--danger' : ''}" style=${`width:${pct}%`}></div></div>
592
+ </div>`;
593
+ };
594
+
595
+ const chip = (label, value, tab) => html`
596
+ <button class="pf-usage-chip" onClick=${tab ? () => switchTab(tab) : undefined} disabled=${!tab}>
597
+ <span class="pf-usage-chip-val">${value}</span>
598
+ <span class="pf-usage-chip-label">${label}</span>
599
+ </button>`;
600
+
601
+ const c = u.counts;
602
+ return html`
603
+ <div class="pf-home-card pf-usage-card">
604
+ <div class="pf-home-card-title">${t('profile.landing.usageTitle') || 'Usage & quotas'}</div>
605
+ ${bar(t('profile.landing.usageMemory') || 'Memory', u.memory,
606
+ `${u.memory.used_keys}/${u.memory.max_keys} ${t('profile.memory.keysWord') || 'keys'} · ${fmtBytes(u.memory.used_bytes)} / ${fmtBytes(u.memory.max_bytes)}`)}
607
+ ${bar(t('profile.landing.usageStorage') || 'Files', u.storage,
608
+ `${u.storage.used_files} ${t('profile.landing.usageFilesWord') || 'files'} · ${fmtBytes(u.storage.used_bytes)} / ${fmtBytes(u.storage.max_bytes)}`)}
609
+ ${bar(t('profile.landing.usageMicro') || 'Micro-memory', u.micro_memory,
610
+ `${u.micro_memory.used_sets}/${u.micro_memory.max_sets} ${t('profile.landing.usageSetsWord') || 'sets'} · ${fmtBytes(u.micro_memory.used_bytes)} / ${fmtBytes(u.micro_memory.max_bytes)}`)}
611
+ <div class="pf-usage-chips">
612
+ ${chip(t('profile.landing.usageAgents') || 'Agents', c.agents, 'agents')}
613
+ ${chip(t('profile.landing.usageOrganisms') || 'Organisms', c.organisms, 'organisms')}
614
+ ${chip(t('profile.landing.usageApps') || 'Apps', `${c.apps.used}/${c.apps.max}`, 'apps')}
615
+ ${chip(t('profile.landing.usageEcoApps') || 'Connected apps', c.ecosystem_apps, 'ecosystem')}
616
+ ${chip(t('profile.landing.usageExtensions') || 'Extensions', `${c.extensions.used}/${c.extensions.max}`, 'extensions')}
617
+ ${chip(t('profile.landing.usageCortexes') || 'Cortexes', c.cortexes, 'extensions')}
618
+ ${chip(t('profile.landing.usageServices') || 'Services', `${c.services.used}/${c.services.max}`, 'offers')}
619
+ </div>
620
+ </div>
621
+ `;
622
+ }
623
+
580
624
  /* "Presence" — the owner's own availability control. Lives as a compact status
581
625
  * pill in the ProfileCard header (<PresencePill>); clicking it opens the settings
582
626
  * <PresenceDialog>. The dot the pill shows is the same <PresenceDot> rendered next
@@ -1204,6 +1248,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
1204
1248
  <${NextSteps} switchTab=${(id) => open(id, 'main')}
1205
1249
  hasApps=${appsLoaded ? apps.length > 0 : undefined} />
1206
1250
  <div class="pf-home-grid">
1251
+ <${UsageCard} switchTab=${(id) => open(id, 'main')} />
1207
1252
  <${ContinueCard} />
1208
1253
  <${AgentsCard} owner=${owner} />
1209
1254
  </div>
@@ -34,6 +34,13 @@
34
34
  * per-row checkboxes + a bulk bar (change visibility incl. group / delete with confirm /
35
35
  * clear). The detail-row select and edit-modal select remain as alternate paths.
36
36
  * v2.2.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
37
+ * v2.3.0 — 2026-06-22 — Scale to thousands of keys: default load is metadata-only (include=meta,
38
+ * no values) with per-key lazy value fetch on expand + "Load all contents"; server-side content
39
+ * search (key/value/tags) with per-group scoping; storage-usage bar; value-size column + "largest
40
+ * first" sort; bulk-delete a whole group; JSON export/import (skip/overwrite/rename).
41
+ * v2.3.1 — 2026-06-22 — Toolbar layout: data tools (Load all / Export / Import) grouped in their
42
+ * own bordered .mem-tools-section up top; content-search + filter on one row; sort + "New memory"
43
+ * moved to a bottom bar (New-memory pushed right).
37
44
  */
38
45
  import { h } from 'preact';
39
46
  import { useState, useEffect, useRef } from 'preact/hooks';
@@ -92,6 +99,19 @@ export default function MemoryTab({ session, showToast, onStats }) {
92
99
  const [browseError, setBrowseError] = useState(null);
93
100
  const [pullingKeys, setPullingKeys] = useState(new Set());
94
101
  const [sortBy, setSortBy] = useState('updated');
102
+ // Scalable Memory tab (v2.3): the list loads metadata-only by default (no values) so thousands of
103
+ // keys open fast; values are fetched per-key on expand. memQuota drives the storage-usage bar.
104
+ const [memQuota, setMemQuota] = useState(null);
105
+ const [fullLoaded, setFullLoaded] = useState(false); // true once "Load all contents" pulls values
106
+ const [valueCache, setValueCache] = useState({}); // key → value (lazy-fetched in meta mode)
107
+ const [loadingValueKeys, setLoadingValueKeys] = useState(new Set());
108
+ // Server-side content search (matches key + value + tags) — separate from the instant client filter.
109
+ const [searchInput, setSearchInput] = useState('');
110
+ const [searchResults, setSearchResults] = useState(null); // null = not searching; array = results
111
+ const [searchScopePrefix, setSearchScopePrefix] = useState(''); // optional namespace/group scope
112
+ const [searchLoading, setSearchLoading] = useState(false);
113
+ const [importMode, setImportMode] = useState('skip'); // skip | overwrite | rename
114
+ const [importing, setImporting] = useState(false);
95
115
  const [discoverEntries, setDiscoverEntries] = useState(null);
96
116
  const [discoverLoading, setDiscoverLoading] = useState(false);
97
117
  const [discoverError, setDiscoverError] = useState(null);
@@ -206,15 +226,151 @@ export default function MemoryTab({ session, showToast, onStats }) {
206
226
  } catch { /* ignore */ }
207
227
  }
208
228
 
229
+ // Refresh the list in whatever mode is active: metadata-only by default (fast for thousands of
230
+ // keys), or full values once the user pressed "Load all contents".
209
231
  async function loadMemories() {
210
232
  try {
211
233
  const agentGaii = selectedAgent || undefined;
212
- const list = await memoryService.listMemories(agentGaii);
213
- setMemories(Array.isArray(list) ? list : []);
214
- onStats?.({ memory: Array.isArray(list) ? list.length : 0 });
234
+ if (fullLoaded) {
235
+ const list = await memoryService.listMemories(agentGaii);
236
+ setMemories(Array.isArray(list) ? list : []);
237
+ onStats?.({ memory: Array.isArray(list) ? list.length : 0 });
238
+ } else {
239
+ const { items, quota } = await memoryService.listMemoriesMeta(agentGaii);
240
+ setMemories(items);
241
+ setMemQuota(quota);
242
+ setValueCache({});
243
+ onStats?.({ memory: items.length });
244
+ }
215
245
  } catch { setMemories([]); }
216
246
  }
217
247
 
248
+ // "Load all contents" — pull every entry's value so the instant client filter can search values too.
249
+ async function loadFullContents() {
250
+ setFullLoaded(true);
251
+ try {
252
+ const list = await memoryService.listMemories(selectedAgent || undefined);
253
+ setMemories(Array.isArray(list) ? list : []);
254
+ } catch { /* keep the meta list we already have */ }
255
+ }
256
+
257
+ // Lazy value fetch (meta mode): on row expand, fetch the value once and cache it.
258
+ async function ensureValue(key) {
259
+ if (fullLoaded || valueCache[key] !== undefined) return;
260
+ setLoadingValueKeys(prev => new Set(prev).add(key));
261
+ try {
262
+ const resp = await memoryService.getMemory(key, { soft: true, agent: selectedAgent || undefined });
263
+ setValueCache(prev => ({ ...prev, [key]: resp?.data?.value ?? null }));
264
+ } catch {
265
+ setValueCache(prev => ({ ...prev, [key]: null }));
266
+ } finally {
267
+ setLoadingValueKeys(prev => { const n = new Set(prev); n.delete(key); return n; });
268
+ }
269
+ }
270
+
271
+ // The effective value for a row: from the full list, the lazy cache, or undefined (still loading).
272
+ const valueOf = (m) => (fullLoaded ? m.value : valueCache[m.key]);
273
+
274
+ // Server-side content search (key + value + tags), optionally scoped to a namespace/group prefix.
275
+ async function runServerSearch(query, prefix) {
276
+ const q = (query ?? searchInput).trim();
277
+ setSearchScopePrefix(prefix || '');
278
+ if (!q) { setSearchResults(null); return; }
279
+ setSearchLoading(true);
280
+ try {
281
+ const results = await memoryService.searchMemory(q, selectedAgent || undefined, prefix || undefined);
282
+ const list = Array.isArray(results) ? results : [];
283
+ setSearchResults(list);
284
+ // Search results carry their values — cache them so expanding a result shows the value at once.
285
+ setValueCache(prev => { const next = { ...prev }; for (const r of list) next[r.key] = r.value; return next; });
286
+ } catch (e) {
287
+ showToast(e.message || t('profile.error'), true);
288
+ setSearchResults([]);
289
+ } finally { setSearchLoading(false); }
290
+ }
291
+
292
+ function clearServerSearch() {
293
+ setSearchInput('');
294
+ setSearchResults(null);
295
+ setSearchScopePrefix('');
296
+ }
297
+
298
+ // Export all (or a prefix) of the caller's memory as a downloaded JSON backup.
299
+ async function handleExport(prefix) {
300
+ try {
301
+ const data = await memoryService.exportMemory(selectedAgent || undefined, prefix || undefined);
302
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
303
+ const url = URL.createObjectURL(blob);
304
+ const a = document.createElement('a');
305
+ const stamp = new Date().toISOString().slice(0, 10);
306
+ a.href = url;
307
+ a.download = `aimeat-memory-${stamp}.json`;
308
+ document.body.appendChild(a);
309
+ a.click();
310
+ a.remove();
311
+ URL.revokeObjectURL(url);
312
+ showToast((t('profile.memory.exportDone') || 'Exported {n} entries').replace('{n}', String(data.count ?? (data.entries || []).length)));
313
+ } catch (e) { showToast(e.message || t('profile.error'), true); }
314
+ }
315
+
316
+ // Import a JSON backup; the user picks a conflict mode (skip/overwrite/rename) before it runs.
317
+ const importFileRef = useRef(null);
318
+ function triggerImport() { importFileRef.current?.click(); }
319
+ async function handleImportFile(e) {
320
+ const file = e.target.files?.[0];
321
+ e.target.value = ''; // allow re-picking the same file
322
+ if (!file) return;
323
+ let parsed;
324
+ try {
325
+ const text = await file.text();
326
+ parsed = JSON.parse(text);
327
+ } catch { showToast(t('profile.memory.importBadJson') || 'Not a valid JSON backup', true); return; }
328
+ const entries = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.entries) ? parsed.entries : null);
329
+ if (!entries || entries.length === 0) { showToast(t('profile.memory.importEmpty') || 'No entries found in file', true); return; }
330
+
331
+ const modeLabel = (m) => t('profile.memory.importMode.' + m) || m;
332
+ confirm(
333
+ (t('profile.memory.importConfirm') || 'Import {n} entries? Existing keys are handled by mode: {mode}.')
334
+ .replace('{n}', String(entries.length)).replace('{mode}', modeLabel(importMode)),
335
+ async () => {
336
+ setImporting(true);
337
+ try {
338
+ const resp = await memoryService.importMemory(entries, importMode, selectedAgent || undefined);
339
+ if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
340
+ const s = resp.data || {};
341
+ showToast((t('profile.memory.importDone') || 'Imported: {c} new, {u} updated, {s} skipped')
342
+ .replace('{c}', String(s.created || 0)).replace('{u}', String(s.updated || 0)).replace('{s}', String(s.skipped || 0))
343
+ + ((s.failed && s.failed.length) ? ` · ${s.failed.length} ${t('profile.error') || 'failed'}` : ''),
344
+ !!(s.failed && s.failed.length));
345
+ loadMemories();
346
+ } catch (e) { showToast(e.message || t('profile.error'), true); }
347
+ finally { setImporting(false); }
348
+ },
349
+ );
350
+ }
351
+
352
+ // Delete a whole namespace/group at once (server-side bulk-delete by prefix).
353
+ function deleteGroup(g, count) {
354
+ const prefix = g.kind === 'organism' ? 'organism.' + g.uuid + '.'
355
+ : g.kind === 'plain' ? g.id + '.'
356
+ : null;
357
+ if (!prefix) { showToast(t('profile.memory.deleteGroupUnsupported') || 'This group cannot be bulk-deleted', true); return; }
358
+ confirm(
359
+ (t('profile.memory.deleteGroupConfirm') || 'Delete all {n} entries under "{g}"? This cannot be undone.')
360
+ .replace('{n}', String(count)).replace('{g}', groupLabel(g)),
361
+ async () => {
362
+ try {
363
+ const resp = await memoryService.bulkDeleteMemory({ prefix, agentGaii: selectedAgent || undefined });
364
+ if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
365
+ showToast((t('profile.memory.bulkDeleted') || '{n} deleted').replace('{n}', String(resp.data?.deleted ?? 0)));
366
+ setExpandedMem(null);
367
+ loadMemories();
368
+ } catch (e) { showToast(e.message || t('profile.error'), true); }
369
+ },
370
+ { danger: true },
371
+ );
372
+ }
373
+
218
374
  async function loadFiles() {
219
375
  try {
220
376
  const list = await memoryService.listFiles();
@@ -412,6 +568,13 @@ export default function MemoryTab({ session, showToast, onStats }) {
412
568
  }
413
569
  }
414
570
 
571
+ function formatBytes(n) {
572
+ if (n == null) return '';
573
+ if (n < 1024) return n + ' B';
574
+ if (n < 1024 * 1024) return (n / 1024).toFixed(n < 10240 ? 1 : 0) + ' KB';
575
+ return (n / (1024 * 1024)).toFixed(1) + ' MB';
576
+ }
577
+
415
578
  function formatRelativeTime(isoStr) {
416
579
  if (!isoStr) return '';
417
580
  const d = new Date(isoStr);
@@ -436,6 +599,8 @@ export default function MemoryTab({ session, showToast, onStats }) {
436
599
  return sorted.sort((a, b) => +new Date(b.created_at || 0) - +new Date(a.created_at || 0));
437
600
  case 'alpha':
438
601
  return sorted.sort((a, b) => a.key.localeCompare(b.key));
602
+ case 'size':
603
+ return sorted.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0));
439
604
  default:
440
605
  return sorted;
441
606
  }
@@ -492,7 +657,9 @@ export default function MemoryTab({ session, showToast, onStats }) {
492
657
  if (!ft) return true;
493
658
  if (m.key.toLowerCase().includes(ft)) return true;
494
659
  if (m.tags && m.tags.some(tag => tag.toLowerCase().includes(ft))) return true;
495
- try { return JSON.stringify(m.value).toLowerCase().includes(ft); } catch { return false; }
660
+ const v = valueOf(m); // undefined in meta mode (value not loaded) → key/tag filter only
661
+ if (v === undefined) return false;
662
+ try { return JSON.stringify(v).toLowerCase().includes(ft); } catch { return false; }
496
663
  }));
497
664
 
498
665
  const toggleMemTag = (tag) => {
@@ -517,10 +684,11 @@ export default function MemoryTab({ session, showToast, onStats }) {
517
684
 
518
685
  const renderRow = (m, g) => html`
519
686
  <div key=${m.key}>
520
- <div class="mem-item mem-item--grouped" onClick=${() => setExpandedMem(expandedMem === m.key ? null : m.key)}>
687
+ <div class="mem-item mem-item--grouped" onClick=${() => { const opening = expandedMem !== m.key; setExpandedMem(opening ? m.key : null); if (opening) ensureValue(m.key); }}>
521
688
  <input type="checkbox" class="mem-row-check" checked=${selectedKeys.has(m.key)}
522
689
  onClick=${(e) => e.stopPropagation()} onChange=${() => toggleSelected(m.key)} />
523
690
  <span class="mem-key" title=${m.key}>${escHtml(displayRemainder(m.key, g))}</span>
691
+ ${typeof m.bytes === 'number' && html`<span class="pf-mem-size" title=${t('profile.memory.sizeLabel') || 'Value size'}>${formatBytes(m.bytes)}</span>`}
524
692
  <span class="mem-time" title="${m.created_at ? new Date(m.created_at).toLocaleString() : ''} / ${m.updated_at ? new Date(m.updated_at).toLocaleString() : ''}">
525
693
  ${formatRelativeTime(m.updated_at || m.created_at)}
526
694
  </span>
@@ -549,8 +717,12 @@ export default function MemoryTab({ session, showToast, onStats }) {
549
717
  ${expandedMem === m.key && html`
550
718
  <div class="mem-detail">
551
719
  <div class="mem-detail-key" title=${m.key}>${escHtml(m.key)}</div>
552
- ${(() => { const im = detectImage(m.value, m.key); return im ? html`<${ImageView} desc=${im} />` : null; })()}
553
- <pre>${typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || '')}</pre>
720
+ ${(!fullLoaded && valueOf(m) === undefined)
721
+ ? html`<div class="text-meta-sm">${loadingValueKeys.has(m.key) ? (t('profile.memory.loadingValue') || 'Loading value…') : '…'}</div>`
722
+ : html`
723
+ ${(() => { const v = valueOf(m); const im = detectImage(v, m.key); return im ? html`<${ImageView} desc=${im} />` : null; })()}
724
+ <pre>${(() => { const v = valueOf(m); return typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v ?? ''); })()}</pre>
725
+ `}
554
726
  <div class="mem-detail-visrow mb-half">
555
727
  <span class="text-meta-sm">${t('profile.memory.visLabel')}</span>
556
728
  <select class="input-field mem-vis-select" value=${m.visibility || 'private'}
@@ -592,7 +764,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
592
764
  </div>
593
765
  `}
594
766
  <div class="mem-actions">
595
- <button class="btn-sm" onClick=${() => setEditModal({ key: m.key, value: typeof m.value === 'object' ? JSON.stringify(m.value, null, 2) : String(m.value || ''), visibility: m.visibility || 'private', version: m.version, isJson: typeof m.value === 'object' })}>${t('profile.memory.editBtn')}</button>
767
+ <button class="btn-sm" onClick=${() => { const v = valueOf(m); setEditModal({ key: m.key, value: typeof v === 'object' && v !== null ? JSON.stringify(v, null, 2) : String(v ?? ''), visibility: m.visibility || 'private', version: m.version, isJson: typeof v === 'object' && v !== null }); }}>${t('profile.memory.editBtn')}</button>
596
768
  ${fedConsents[m.key]
597
769
  ? html`<button class="btn-outline btn-sm" disabled=${togglingFed === m.key}
598
770
  onClick=${() => handleStopSharing(m.key)}>
@@ -618,22 +790,58 @@ export default function MemoryTab({ session, showToast, onStats }) {
618
790
  </div>
619
791
  `;
620
792
 
793
+ const quotaPct = memQuota && memQuota.max_bytes ? Math.min(100, Math.round((memQuota.used_bytes / memQuota.max_bytes) * 100)) : 0;
794
+
621
795
  return html`
796
+ ${memQuota && html`
797
+ <div class="pf-mem-quota">
798
+ <div class="pf-mem-quota-row">
799
+ <span class="text-meta-sm">${t('profile.memory.storageUsed') || 'Storage'}: ${memQuota.used_keys}/${memQuota.max_keys} ${t('profile.memory.keysWord') || 'keys'} · ${formatBytes(memQuota.used_bytes)} / ${formatBytes(memQuota.max_bytes)}</span>
800
+ </div>
801
+ <div class="pf-mem-quota-bar"><div class="pf-mem-quota-fill ${quotaPct >= 90 ? 'pf-mem-quota-fill--danger' : ''}" style=${`width:${quotaPct}%`}></div></div>
802
+ </div>
803
+ `}
804
+ <div class="mem-tools-section">
805
+ <span class="mem-tools-label">${t('profile.memory.toolsLabel') || 'Tools'}</span>
806
+ <div class="mem-tools-actions">
807
+ ${!fullLoaded && html`<button class="btn-outline btn-sm" onClick=${loadFullContents}>${t('profile.memory.loadContents') || 'Load all contents'}</button>`}
808
+ <span class="mem-import-group">
809
+ <button class="btn-outline btn-sm" onClick=${() => handleExport()}>${t('profile.memory.exportBtn') || 'Export'}</button>
810
+ <button class="btn-outline btn-sm" disabled=${importing} onClick=${triggerImport}>${importing ? '…' : (t('profile.memory.importBtn') || 'Import')}</button>
811
+ <select class="input-field mem-vis-select" value=${importMode} onChange=${e => setImportMode(e.target.value)} title=${t('profile.memory.importModeLabel') || 'Conflict handling'}>
812
+ <option value="skip">${t('profile.memory.importMode.skip') || 'Skip existing'}</option>
813
+ <option value="overwrite">${t('profile.memory.importMode.overwrite') || 'Overwrite'}</option>
814
+ <option value="rename">${t('profile.memory.importMode.rename') || 'Import as new'}</option>
815
+ </select>
816
+ <input type="file" accept="application/json,.json" ref=${importFileRef} class="pf-hidden" onChange=${handleImportFile} />
817
+ </span>
818
+ </div>
819
+ </div>
622
820
  <div class="action-bar">
821
+ <div class="search-bar">
822
+ <input type="text" class="input-field" placeholder=${t('profile.memory.searchContents') || 'Search content or key…'}
823
+ value=${searchInput} onInput=${e => setSearchInput(e.target.value)}
824
+ onKeyDown=${e => { if (e.key === 'Enter') runServerSearch(searchInput, searchScopePrefix); }} />
825
+ <button class="btn-sm" disabled=${searchLoading} onClick=${() => runServerSearch(searchInput, searchScopePrefix)}>${searchLoading ? '…' : (t('profile.memory.searchBtn') || 'Search')}</button>
826
+ ${searchResults !== null && html`<button class="btn-ghost btn-sm" onClick=${clearServerSearch}>✕</button>`}
827
+ </div>
623
828
  <div class="search-bar">
624
829
  <input type="text" class="input-field" placeholder=${t('profile.memory.filterType')}
625
830
  value=${filterText} onInput=${e => setFilterText(e.target.value)} />
626
831
  ${filterText && html`<button class="btn-ghost btn-sm" onClick=${() => setFilterText('')}>✕</button>`}
627
832
  </div>
833
+ </div>
834
+ <div class="action-bar mem-bottom-bar">
628
835
  <div class="mem-sort-bar">
629
836
  <label class="text-meta-sm">${t('profile.memory.sortLabel')}</label>
630
837
  <select class="input-field mem-sort-select" value=${sortBy} onChange=${e => setSortBy(e.target.value)}>
631
838
  <option value="updated">${t('profile.memory.sortUpdated')}</option>
632
839
  <option value="created">${t('profile.memory.sortCreated')}</option>
633
840
  <option value="alpha">${t('profile.memory.sortAlpha')}</option>
841
+ <option value="size">${t('profile.memory.sortSize') || 'Largest first'}</option>
634
842
  </select>
635
843
  </div>
636
- <button class="btn-primary" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
844
+ <button class="btn-primary mem-new-btn" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
637
845
  </div>
638
846
  <${TagCloud} tags=${tagsByFreq} selected=${memTagFilter} onToggle=${toggleMemTag} onClear=${() => setMemTagFilter(new Set())} limit=${10} />
639
847
  ${showMemForm && html`<${MemoryForm} onSave=${handleCreateMemory} onCancel=${() => setShowMemForm(false)} groups=${groups} />`}
@@ -649,22 +857,39 @@ export default function MemoryTab({ session, showToast, onStats }) {
649
857
  <button class="btn-ghost btn-sm" onClick=${() => setSelectedKeys(new Set())}>✕ ${t('profile.memory.bulkClear') || 'Clear selection'}</button>
650
858
  </div>
651
859
  `}
652
- ${filtered.length === 0
653
- ? html`<div class="empty">${memories.length > 0 ? (t('tags.noMatch') || 'No items match selected tags') : t('profile.memory.empty')}</div>`
654
- : groupsOrdered.map(g => {
655
- const collapsed = !filtering && collapsedGroups.has(g.id);
656
- return html`
657
- <div class="mem-group" key=${g.id}>
658
- <button class="mem-group-header" onClick=${() => toggleGroupCollapsed(g.id)}>
659
- <span class="pf-chevron ${collapsed ? '' : 'pf-chevron-open'}">▼</span>
660
- <span class="mem-group-name">${escHtml(groupLabel(g))}</span>
661
- <span class="mem-group-count">${g.items.length === 1 ? (t('profile.memory.keysOne') || '1 key') : (t('profile.memory.keysCount') || '{n} keys').replace('{n}', String(g.items.length))}</span>
662
- ${g.kind === 'organism' && orgNames[g.uuid] && html`<span class="mem-group-sub">${shortTok(g.uuid)}</span>`}
663
- </button>
664
- ${!collapsed && g.items.map(m => renderRow(m, g))}
665
- </div>
666
- `;
667
- })
860
+ ${searchResults !== null
861
+ ? html`
862
+ <div class="mem-search-summary">
863
+ <span class="text-meta-sm">${(t('profile.memory.searchResultCount') || '{n} matches').replace('{n}', String(searchResults.length))}${searchScopePrefix ? ` · ${escHtml(searchScopePrefix)}` : ''}</span>
864
+ <button class="btn-ghost btn-sm" onClick=${clearServerSearch}>✕ ${t('profile.memory.searchClear') || 'Clear search'}</button>
865
+ </div>
866
+ ${searchResults.length === 0
867
+ ? html`<div class="empty">${t('profile.memory.searchEmpty') || 'No matches'}</div>`
868
+ : sortEntries(searchResults).map(m => renderRow(m, groupOfKey(m.key)))}
869
+ `
870
+ : filtered.length === 0
871
+ ? html`<div class="empty">${memories.length > 0 ? (t('tags.noMatch') || 'No items match selected tags') : t('profile.memory.empty')}</div>`
872
+ : groupsOrdered.map(g => {
873
+ const collapsed = !filtering && collapsedGroups.has(g.id);
874
+ const groupPrefix = g.kind === 'organism' ? 'organism.' + g.uuid + '.' : g.kind === 'plain' ? g.id + '.' : null;
875
+ return html`
876
+ <div class="mem-group" key=${g.id}>
877
+ <div class="mem-group-header" role="button" tabindex="0" onClick=${() => toggleGroupCollapsed(g.id)}>
878
+ <span class="pf-chevron ${collapsed ? '' : 'pf-chevron-open'}">▼</span>
879
+ <span class="mem-group-name">${escHtml(groupLabel(g))}</span>
880
+ <span class="mem-group-count">${g.items.length === 1 ? (t('profile.memory.keysOne') || '1 key') : (t('profile.memory.keysCount') || '{n} keys').replace('{n}', String(g.items.length))}</span>
881
+ ${g.kind === 'organism' && orgNames[g.uuid] && html`<span class="mem-group-sub">${shortTok(g.uuid)}</span>`}
882
+ <span class="mem-group-actions">
883
+ ${groupPrefix && html`<button class="btn-ghost btn-sm" title=${t('profile.memory.searchInGroup') || 'Search in this group'}
884
+ onClick=${(e) => { e.stopPropagation(); setSearchInput(''); setSearchScopePrefix(groupPrefix); showToast((t('profile.memory.searchInGroupHint') || 'Type a query to search within {g}').replace('{g}', groupLabel(g))); }}>🔍</button>`}
885
+ ${groupPrefix && html`<button class="btn-ghost btn-sm" title=${t('profile.memory.deleteGroup') || 'Delete group'}
886
+ onClick=${(e) => { e.stopPropagation(); deleteGroup(g, g.items.length); }}>🗑️</button>`}
887
+ </span>
888
+ </div>
889
+ ${!collapsed && g.items.map(m => renderRow(m, g))}
890
+ </div>
891
+ `;
892
+ })
668
893
  }`;
669
894
  };
670
895
 
@@ -1056,7 +1281,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
1056
1281
  <div class="agent-selector mb-half">
1057
1282
  <label class="text-meta pf-mr-half">${t('profile.memory.agent') || 'Agent'}:</label>
1058
1283
  <select class="input-field pf-select-inline"
1059
- value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); }}>
1284
+ value=${selectedAgent} onChange=${e => { setSelectedAgent(e.target.value); setExpandedMem(null); setMemTagFilter(new Set()); setFullLoaded(false); clearServerSearch(); }}>
1060
1285
  <option value="">${t('profile.memory.defaultAgent') || 'Default agent'}</option>
1061
1286
  ${agents.map(a => html`<option key=${a.gaii} value=${a.gaii}>${escHtml(a.name || a.gaii)}${a.display_name ? ` — ${escHtml(a.display_name)}` : ''}</option>`)}
1062
1287
  </select>
@@ -8,6 +8,8 @@
8
8
  * @usage import { OrgAgentsPanel } from '/views/profile/organisms/agents.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 — Agent activity context comes from one getAgentsActivity(orgId) call instead
12
+ * of a per-workspace getWorkspaceActivity fan-out.
11
13
  */
12
14
  import { h } from 'preact';
13
15
  import { useState, useEffect } from 'preact/hooks';
@@ -41,22 +43,15 @@ export function OrgAgentsPanel({ org, ghii, canManage, showToast, onChanged }) {
41
43
  listAgents(ghii).then(a => setMine((a || []).filter(x => !String(x.name || '').startsWith('session-')))).catch(() => setMine([]));
42
44
  }, [ghii]);
43
45
 
44
- // Best-effort activity context: which workspaces each agent has touched, and when last.
46
+ // Best-effort activity context: which workspaces each agent has touched, and when last. ONE
47
+ // aggregated request replaces the old per-workspace getWorkspaceActivity fan-out.
45
48
  useEffect(() => {
46
49
  let cancelled = false;
47
50
  (async () => {
48
51
  try {
49
- const wss = (await orgService.discoverWorkspaces(orgId)).filter(w => w.access !== 'none');
52
+ const agents = await orgService.getAgentsActivity(orgId);
50
53
  const map = {};
51
- await Promise.all(wss.map(async (w) => {
52
- const a = await orgService.getWorkspaceActivity(orgId, w.id);
53
- for (const e of (a.events || [])) {
54
- if (!e.agent) continue;
55
- const m = map[e.agent] || (map[e.agent] = { count: 0, lastAt: '', ws: new Set() });
56
- m.count++; m.ws.add(w.name || w.id);
57
- if (!m.lastAt || e.at > m.lastAt) m.lastAt = e.at;
58
- }
59
- }));
54
+ for (const [name, v] of Object.entries(agents)) map[name] = { count: v.count, lastAt: v.lastAt, ws: new Set(v.workspaces || []) };
60
55
  if (!cancelled) setActed(map);
61
56
  } catch { /* context is optional */ }
62
57
  })();
@@ -326,7 +326,7 @@ export function OrganismHome({ org, ghii, showToast, initialSettings, onOpenWs,
326
326
  <${ReadmePanel} markdown=${readme} canEdit=${canEdit} kind="organism" name=${org.name}
327
327
  aiPromptSeed=${tocSeed} onSave=${saveReadme} />
328
328
 
329
- <${StructureMindmap} scope="organism" graph=${graph} onNavigate=${onMapNav} />
329
+ <${StructureMindmap} scope="organism" graph=${graph} onNavigate=${onMapNav} storageKey=${'org.' + org.id} />
330
330
 
331
331
  <${StructureOverview} label=${t('organisms.structureOverviewOrg') || 'Organism structure — table of contents'}
332
332
  load=${() => orgService.getOrganismOverview(org.id)} />
@@ -8,6 +8,8 @@
8
8
  * @usage import { OrgMemberManager } from '/views/profile/organisms/members.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 — Per-member workspace access uses one getWorkspaceAccessAll(orgId) call
12
+ * instead of a per-owned-workspace getWorkspaceAccess fan-out.
11
13
  */
12
14
  import { h } from 'preact';
13
15
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -71,7 +73,12 @@ export function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, c
71
73
  let cancelled = false;
72
74
  (async () => {
73
75
  try {
74
- const wss = await orgService.discoverWorkspaces(orgId);
76
+ // discoverWorkspaces → creators of every workspace; getWorkspaceAccessAll → members of MY
77
+ // workspaces in ONE request (was one getWorkspaceAccess per owned workspace).
78
+ const [wss, accessAll] = await Promise.all([
79
+ orgService.discoverWorkspaces(orgId),
80
+ orgService.getWorkspaceAccessAll(orgId),
81
+ ]);
75
82
  const map = {};
76
83
  const add = (who, wsName, role) => {
77
84
  const bare = String(who || '').split('@')[0];
@@ -80,10 +87,7 @@ export function OrgMemberManager({ org, ghii, canManage, isCreator, showToast, c
80
87
  if (!list.some(x => x.ws === wsName)) list.push({ ws: wsName, role });
81
88
  };
82
89
  for (const w of wss) if (w.created_by) add(w.created_by, w.name || w.id, 'owner');
83
- await Promise.all(wss.filter(w => w.access === 'owner').map(async (w) => {
84
- const acc = await orgService.getWorkspaceAccess(orgId, w.id).catch(() => null);
85
- for (const m of (acc?.members || [])) add(m.owner, w.name || w.id, m.role);
86
- }));
90
+ for (const w of accessAll) for (const m of (w.members || [])) add(m.owner, w.name || w.ws, m.role);
87
91
  if (!cancelled) setWsAccess(map);
88
92
  } catch { /* the access line is optional */ }
89
93
  })();