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