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.
- package/dist/locales/en.json +71 -2
- package/dist/locales/fi.json +71 -2
- package/dist/public/css/views/agents-detail.css +11 -0
- package/dist/public/css/views/inbox.css +62 -1
- package/dist/public/css/views/profile.css +87 -10
- package/dist/public/js/services/memory.js +62 -4
- package/dist/public/js/services/messages.js +4 -3
- package/dist/public/js/services/organisms.js +49 -3
- package/dist/public/js/services/workflows.js +4 -3
- package/dist/public/llms-template.txt +38 -1
- package/dist/public/spa.html +3 -0
- package/dist/public/views/command-palette.js +145 -0
- package/dist/public/views/profile/agents-tab.js +1 -0
- package/dist/public/views/profile/inbox-tab.js +175 -28
- package/dist/public/views/profile/knowledge-tab.js +6 -9
- package/dist/public/views/profile/landing-page.js +74 -29
- package/dist/public/views/profile/memory-tab.js +251 -26
- package/dist/public/views/profile/organisms/agents.js +6 -11
- package/dist/public/views/profile/organisms/home.js +1 -1
- package/dist/public/views/profile/organisms/members.js +9 -5
- package/dist/public/views/profile/organisms/mindmap.js +157 -59
- package/dist/public/views/profile/organisms/panels.js +36 -19
- package/dist/public/views/profile/organisms/timeline-panel.js +30 -1
- package/dist/public/views/profile/organisms/workspace-comments.js +15 -5
- package/dist/public/views/profile/organisms/workspace-list.js +35 -23
- package/dist/public/views/profile/organisms/workspace.js +166 -31
- package/dist/public/views/profile/organisms-tab.js +78 -5
- package/dist/public/views/profile/workflows-tab.js +6 -6
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.js +28 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +13 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +62 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/index.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +527 -12
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +4 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +1 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +30 -1
- package/dist/src/generated/prisma-postgres/index.js +4 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +1 -0
- package/dist/src/generated/prisma-postgres/wasm.js +4 -3
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +3 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +33 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +6 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/dm-messages.d.ts +20 -0
- package/dist/src/mcp/dm-messages.d.ts.map +1 -0
- package/dist/src/mcp/dm-messages.js +120 -0
- package/dist/src/mcp/dm-messages.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +3 -1
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/models/message-schemas.d.ts +3 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +4 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +13 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +2 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/bootstrap.js +1 -1
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/catalogue.d.ts.map +1 -1
- package/dist/src/routes/catalogue.js +17 -8
- package/dist/src/routes/catalogue.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +1 -0
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/memory.d.ts +3 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +242 -14
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +22 -19
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +321 -3
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +2 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +93 -104
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +6 -0
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/usage.d.ts +17 -0
- package/dist/src/routes/usage.d.ts.map +1 -0
- package/dist/src/routes/usage.js +35 -0
- package/dist/src/routes/usage.js.map +1 -0
- package/dist/src/routes/wallet.d.ts +2 -0
- package/dist/src/routes/wallet.d.ts.map +1 -1
- package/dist/src/routes/wallet.js +13 -6
- package/dist/src/routes/wallet.js.map +1 -1
- package/dist/src/routes/workflows.d.ts.map +1 -1
- package/dist/src/routes/workflows.js +13 -2
- package/dist/src/routes/workflows.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +18 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/cache.d.ts +61 -0
- package/dist/src/services/cache.d.ts.map +1 -0
- package/dist/src/services/cache.js +143 -0
- package/dist/src/services/cache.js.map +1 -0
- package/dist/src/services/connect-tunnel.d.ts +11 -0
- package/dist/src/services/connect-tunnel.d.ts.map +1 -1
- package/dist/src/services/connect-tunnel.js +36 -0
- package/dist/src/services/connect-tunnel.js.map +1 -1
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +1 -0
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/message-send.d.ts +13 -0
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +28 -5
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/organism-search.d.ts +12 -7
- package/dist/src/services/organism-search.d.ts.map +1 -1
- package/dist/src/services/organism-search.js +93 -60
- package/dist/src/services/organism-search.js.map +1 -1
- package/dist/src/services/usage-summary.d.ts +77 -0
- package/dist/src/services/usage-summary.d.ts.map +1 -0
- package/dist/src/services/usage-summary.js +85 -0
- package/dist/src/services/usage-summary.js.map +1 -0
- package/dist/src/services/workspace-enrichment.d.ts +76 -0
- package/dist/src/services/workspace-enrichment.d.ts.map +1 -0
- package/dist/src/services/workspace-enrichment.js +144 -0
- package/dist/src/services/workspace-enrichment.js.map +1 -0
- package/dist/src/storage/interface.d.ts +5 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +16 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +51 -1
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +16 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +20 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +15 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +28 -2
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +4 -0
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +4 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +20 -0
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +1 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +1 -0
- 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
|
-
|
|
73
|
-
|
|
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(
|
|
442
|
+
function WaitingForYou() {
|
|
438
443
|
const [items, setItems] = useState(null);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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=${() =>
|
|
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
|
-
${(
|
|
553
|
-
|
|
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
|
|
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
|
-
${
|
|
653
|
-
? html
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
52
|
+
const agents = await orgService.getAgentsActivity(orgId);
|
|
50
53
|
const map = {};
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
})();
|