aimeat 1.23.2 → 1.23.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/locales/en.json +536 -21
  2. package/dist/locales/fi.json +548 -33
  3. package/dist/public/components/ContactCard.js +28 -0
  4. package/dist/public/css/views/admin.css +12 -0
  5. package/dist/public/css/views/agents-detail.css +62 -10
  6. package/dist/public/css/views/landing.css +87 -0
  7. package/dist/public/css/views/offers.css +96 -0
  8. package/dist/public/css/views/portfolio.css +18 -0
  9. package/dist/public/css/views/profile.css +153 -8
  10. package/dist/public/css/views/start.css +96 -0
  11. package/dist/public/img/business-hero.png +0 -0
  12. package/dist/public/img/toimitus.png +0 -0
  13. package/dist/public/js/components/tag-cloud.js +46 -31
  14. package/dist/public/js/components/tag-editor.js +2 -2
  15. package/dist/public/js/recents.js +40 -0
  16. package/dist/public/js/services/admin.js +6 -0
  17. package/dist/public/js/services/agents.js +34 -0
  18. package/dist/public/js/services/offers.js +152 -0
  19. package/dist/public/js/services/packages.js +1 -0
  20. package/dist/public/spa.html +32 -2
  21. package/dist/public/views/admin/subdomains-tab.js +182 -0
  22. package/dist/public/views/admin.js +2 -0
  23. package/dist/public/views/business.js +119 -0
  24. package/dist/public/views/how-it-works.js +159 -0
  25. package/dist/public/views/landing.js +235 -0
  26. package/dist/public/views/portfolio.js +120 -23
  27. package/dist/public/views/pricing.js +75 -0
  28. package/dist/public/views/profile/access-tab.js +124 -51
  29. package/dist/public/views/profile/agents/agent-card.js +68 -37
  30. package/dist/public/views/profile/agents/shared-board.js +56 -22
  31. package/dist/public/views/profile/agents/tab-activity.js +24 -12
  32. package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
  33. package/dist/public/views/profile/agents/tab-data-access.js +9 -13
  34. package/dist/public/views/profile/agents/tab-integration.js +27 -1
  35. package/dist/public/views/profile/agents/tab-messages.js +6 -11
  36. package/dist/public/views/profile/agents/tab-quality.js +45 -5
  37. package/dist/public/views/profile/agents-tab.js +41 -6
  38. package/dist/public/views/profile/apps-tab.js +7 -1
  39. package/dist/public/views/profile/knowledge-tab.js +4 -1
  40. package/dist/public/views/profile/landing-page.js +427 -123
  41. package/dist/public/views/profile/memory-tab.js +409 -211
  42. package/dist/public/views/profile/nodes-tab.js +18 -3
  43. package/dist/public/views/profile/offers-tab.js +310 -0
  44. package/dist/public/views/profile/organisms-tab.js +289 -28
  45. package/dist/public/views/profile/packages-tab.js +152 -16
  46. package/dist/public/views/profile/portfolio-tab.js +98 -31
  47. package/dist/public/views/profile.js +2 -0
  48. package/dist/public/views/start-flows.js +281 -0
  49. package/dist/public/views/start.js +258 -0
  50. package/dist/src/generated/api-types.d.ts +856 -17
  51. package/dist/src/generated/api-types.d.ts.map +1 -1
  52. package/dist/src/generated/prisma-postgres/edge.js +15 -3
  53. package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
  54. package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
  55. package/dist/src/generated/prisma-postgres/index.js +15 -3
  56. package/dist/src/generated/prisma-postgres/package.json +1 -1
  57. package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
  58. package/dist/src/generated/prisma-postgres/wasm.js +15 -3
  59. package/dist/src/middleware/subdomain.d.ts +28 -0
  60. package/dist/src/middleware/subdomain.d.ts.map +1 -0
  61. package/dist/src/middleware/subdomain.js +32 -0
  62. package/dist/src/middleware/subdomain.js.map +1 -0
  63. package/dist/src/models/offer-schemas.d.ts +229 -0
  64. package/dist/src/models/offer-schemas.d.ts.map +1 -0
  65. package/dist/src/models/offer-schemas.js +100 -0
  66. package/dist/src/models/offer-schemas.js.map +1 -0
  67. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  68. package/dist/src/routes/agent-tasks.js +33 -0
  69. package/dist/src/routes/agent-tasks.js.map +1 -1
  70. package/dist/src/routes/agents.d.ts.map +1 -1
  71. package/dist/src/routes/agents.js +146 -1
  72. package/dist/src/routes/agents.js.map +1 -1
  73. package/dist/src/routes/apps-backup.d.ts +20 -0
  74. package/dist/src/routes/apps-backup.d.ts.map +1 -0
  75. package/dist/src/routes/apps-backup.js +165 -0
  76. package/dist/src/routes/apps-backup.js.map +1 -0
  77. package/dist/src/routes/ghii.d.ts.map +1 -1
  78. package/dist/src/routes/ghii.js +4 -0
  79. package/dist/src/routes/ghii.js.map +1 -1
  80. package/dist/src/routes/instances.d.ts +2 -1
  81. package/dist/src/routes/instances.d.ts.map +1 -1
  82. package/dist/src/routes/instances.js +59 -1
  83. package/dist/src/routes/instances.js.map +1 -1
  84. package/dist/src/routes/packages.d.ts.map +1 -1
  85. package/dist/src/routes/packages.js +32 -0
  86. package/dist/src/routes/packages.js.map +1 -1
  87. package/dist/src/routes/permissions.d.ts +12 -0
  88. package/dist/src/routes/permissions.d.ts.map +1 -1
  89. package/dist/src/routes/permissions.js +35 -3
  90. package/dist/src/routes/permissions.js.map +1 -1
  91. package/dist/src/routes/portal.d.ts +3 -0
  92. package/dist/src/routes/portal.d.ts.map +1 -1
  93. package/dist/src/routes/portal.js +12 -0
  94. package/dist/src/routes/portal.js.map +1 -1
  95. package/dist/src/routes/public-stats.d.ts +15 -0
  96. package/dist/src/routes/public-stats.d.ts.map +1 -0
  97. package/dist/src/routes/public-stats.js +90 -0
  98. package/dist/src/routes/public-stats.js.map +1 -0
  99. package/dist/src/routes/sse.d.ts +7 -2
  100. package/dist/src/routes/sse.d.ts.map +1 -1
  101. package/dist/src/routes/sse.js +35 -5
  102. package/dist/src/routes/sse.js.map +1 -1
  103. package/dist/src/routes/subdomains.d.ts +33 -0
  104. package/dist/src/routes/subdomains.d.ts.map +1 -0
  105. package/dist/src/routes/subdomains.js +208 -0
  106. package/dist/src/routes/subdomains.js.map +1 -0
  107. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  108. package/dist/src/server-bootstrap/routes-loader.js +12 -1
  109. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  110. package/dist/src/server.d.ts.map +1 -1
  111. package/dist/src/server.js +4 -0
  112. package/dist/src/server.js.map +1 -1
  113. package/dist/src/services/apps-backup-export.d.ts +22 -0
  114. package/dist/src/services/apps-backup-export.d.ts.map +1 -0
  115. package/dist/src/services/apps-backup-export.js +136 -0
  116. package/dist/src/services/apps-backup-export.js.map +1 -0
  117. package/dist/src/services/apps-backup-import.d.ts +120 -0
  118. package/dist/src/services/apps-backup-import.d.ts.map +1 -0
  119. package/dist/src/services/apps-backup-import.js +259 -0
  120. package/dist/src/services/apps-backup-import.js.map +1 -0
  121. package/dist/src/services/component-registrar.d.ts +17 -0
  122. package/dist/src/services/component-registrar.d.ts.map +1 -1
  123. package/dist/src/services/component-registrar.js +63 -5
  124. package/dist/src/services/component-registrar.js.map +1 -1
  125. package/dist/src/services/handbooks/appdev.d.ts +1 -1
  126. package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
  127. package/dist/src/services/handbooks/appdev.js +5 -2
  128. package/dist/src/services/handbooks/appdev.js.map +1 -1
  129. package/dist/src/storage/interface.d.ts +11 -1
  130. package/dist/src/storage/interface.d.ts.map +1 -1
  131. package/dist/src/storage/interface.js.map +1 -1
  132. package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
  133. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  134. package/dist/src/storage/providers/mongodb/index.js +76 -0
  135. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  136. package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
  137. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  138. package/dist/src/storage/providers/sqlite/index.js +61 -0
  139. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  140. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  141. package/dist/src/storage/providers/sqlite/schema.js +11 -0
  142. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  143. package/dist/src/storage/repositories/index.d.ts +1 -0
  144. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  145. package/dist/src/storage/repositories/package.repository.d.ts +2 -0
  146. package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
  147. package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
  148. package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
  149. package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
  150. package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
  151. package/dist/static/app-catalog.html +1139 -28
  152. package/package.json +1 -1
  153. package/prisma/schema.postgres.prisma +11 -0
  154. package/prisma/schema.prisma +11 -0
@@ -138,6 +138,25 @@
138
138
  * v1.25.2 — 2026-06-10 — Settings Cancel works again: it discards changes AND closes the panel
139
139
  * (both organism + workspace settings) — the dirty-only reset variant did nothing visible on a
140
140
  * clean form, which read as a dead button.
141
+ * v1.26.0 — 2026-06-10 — Contract-agent discovery: agents tagged 'workspace-contract' (+ optional
142
+ * 'contract.<id>' names) surface to their OWNER — a 📜 block in the workspace People panel and a
143
+ * 📜 badge/marker in the organism Agents tab picker + rows. Tag convention: docs/agent-workspace-
144
+ * contracts.md §7b; helpers offersWorkspaceContract/contractNamesOf in /js/services/agents.js.
145
+ * v1.27.0 — 2026-06-10 — One-click contract adoption: each contract chip gets an "Adopt <id>"
146
+ * button that queues the agreed adopt-contract TASK for the agent (scope[kind]=adopt-contract +
147
+ * organism_id/ws/contract, status:'queued' — the create default is draft and a draft never
148
+ * auto-activates; §7c) — the agent joins/provisions its own spaces and the task completion is
149
+ * the ack. An agent already working in THIS workspace (per the participants data) shows a
150
+ * "✓ active here" badge instead of Adopt. Convention agreed with the crewaimeat side 2026-06-10.
151
+ * v1.28.0 — 2026-06-12 — Overview landing tab: the whole workspace on one vertical scroll — a
152
+ * "what happened here" strip (last 8 activity events, humanized + clickable) above every
153
+ * manifest space stacked in order (records: compact rows max 5 + "Show all N →"; documents:
154
+ * title + first-line preview; empty space: one row with an Add button; non-memory backing:
155
+ * one notice row). Overview is ALWAYS the landing view (tab persistence dropped — it hid
156
+ * content). Mobile: sections collapse as accordions and documents expand INLINE (view+edit,
157
+ * no popout window). Per-tab unseen badges (gray, never red) from localStorage seen marks vs
158
+ * activity events — cleared when the tab opens, surviving across sessions; first visit seeds
159
+ * a baseline so history is never "unseen"; own non-agent edits don't count.
141
160
  */
142
161
  import { h } from 'preact';
143
162
  import { useState, useEffect, useCallback, useRef, useMemo } from 'preact/hooks';
@@ -152,12 +171,13 @@ import { SearchBar } from '/components/SearchBar.js';
152
171
  import * as orgService from '/js/services/organisms.js';
153
172
  import * as memoryService from '/js/services/memory.js';
154
173
  import * as knowledgeService from '/js/services/knowledge.js';
155
- import { listAgents } from '/js/services/agents.js';
174
+ import { listAgents, offersWorkspaceContract, contractNamesOf, adoptContractTask } from '/js/services/agents.js';
156
175
  import { getGhii } from '/js/services/auth.js';
157
176
  import { listPosts, createPost } from '/js/services/boards.js';
158
177
  import { OpenRouterSettings } from './generator-settings.js';
159
178
  import { copyToClipboard } from '/js/utils.js';
160
179
  import { dt, fmtBytes } from '/js/format.js';
180
+ import { recordRecent } from '/js/recents.js';
161
181
  import { Markdown, slugifyHeading } from '/components/Markdown.js';
162
182
  import { Mermaid } from '/components/Mermaid.js';
163
183
 
@@ -422,7 +442,7 @@ function OrgAgentsPanel({ org, ghii, canManage, showToast, onChanged }) {
422
442
  <div class="flex-row-wrap">
423
443
  <select class="input-field input-sm" value=${pick} onChange=${e => setPick(e.target.value)}>
424
444
  <option value="">${t('organisms.pickAgent') || 'Choose one of your agents…'}</option>
425
- ${pickable.map(a => html`<option value=${a.gaii} key=${a.gaii}>${a.display_name || a.name}</option>`)}
445
+ ${pickable.map(a => html`<option value=${a.gaii} key=${a.gaii}>${(a.display_name || a.name) + (offersWorkspaceContract(a) ? ` · 📜 ${t('organisms.contractTag') || 'contract'}` : '')}</option>`)}
426
446
  </select>
427
447
  <button class="btn-outline btn-sm" disabled=${busy || !pick} onClick=${() => attach(pick)}>${t('organisms.attach') || 'Attach'}</button>
428
448
  </div>` : html`
@@ -448,6 +468,7 @@ function OrgAgentsPanel({ org, ghii, canManage, showToast, onChanged }) {
448
468
  <div class="pj-org-titlerow">
449
469
  <span class="pj-org-name">${(own?.display_name || p.name)}</span>
450
470
  ${p.node ? html`<span class="badge badge-info">${(p.node)}</span>` : null}
471
+ ${own && offersWorkspaceContract(own) ? html`<span class="badge badge-success" title=${(t('organisms.contractAgentHint') || 'Advertises a workspace contract') + (contractNamesOf(own).length ? `: ${contractNamesOf(own).join(', ')}` : '')}>${'📜 '}${t('organisms.contractTag') || 'contract'}</span>` : null}
451
472
  </div>
452
473
  <div class="pj-org-desc" title=${g}>
453
474
  <span class="mono">${(p.owner ? `${p.name}#${p.owner}` : g)}</span>
@@ -1167,6 +1188,11 @@ function OrganismHome({ org, ghii, showToast, initialSettings, onOpenWs, onBack,
1167
1188
  return () => { cancelled = true; window.removeEventListener('aimeat-live-update', fetchIt); };
1168
1189
  }, [org.id, canEdit]);
1169
1190
 
1191
+ // Feed the home page's "Continue" list (only once the real name is known, not the {id} stub).
1192
+ useEffect(() => {
1193
+ if (org.name) recordRecent({ type: 'organism', id: org.id, label: org.name, data: { orgId: org.id } });
1194
+ }, [org.id, org.name]);
1195
+
1170
1196
  /* ── Settings: labelled + grouped form (Identity / Access) with live dirty-check, a one-line
1171
1197
  * read-only metadata row, per-choice access hints, and a clearly separated danger zone whose
1172
1198
  * delete needs the organism's name typed and states exactly what gets removed. ── */
@@ -1788,13 +1814,33 @@ function ActivityPanel({ orgId, wsId }) {
1788
1814
  /* Participants panel — who takes part in this workspace, as a node → owner → agents chart plus a
1789
1815
  * listing. Built from the records' identity traces (humans + their agents) + organism membership.
1790
1816
  * The viewer's own agents are named; everyone else's appear as anonymous ghost boxes. */
1791
- function ParticipantsPanel({ orgId, wsId }) {
1817
+ function ParticipantsPanel({ orgId, wsId, showToast }) {
1792
1818
  const [data, setData] = useState(null);
1793
1819
  const [show, setShow] = useState(true);
1794
1820
  const [access, setAccess] = useState(null); // { requests, members } — only for the workspace creator
1795
1821
  const [grantee, setGrantee] = useState('');
1796
1822
  const [role, setRole] = useState('contributor');
1797
1823
  const [busy, setBusy] = useState(false);
1824
+ // The viewer's own agents advertising a workspace contract (tag 'workspace-contract' +
1825
+ // optional 'contract.<name>' tags) — surfaced here so a user looking at a workspace sees
1826
+ // straight away which of their agents can serve it.
1827
+ const [contractAgents, setContractAgents] = useState([]);
1828
+ const [adoptBusy, setAdoptBusy] = useState(''); // `${gaii}:${contract}` of the in-flight adopt
1829
+ useEffect(() => {
1830
+ listAgents().then(a => setContractAgents((a || []).filter(offersWorkspaceContract))).catch(() => {});
1831
+ }, []);
1832
+ // One-click adoption: queue the agreed adopt-contract task (docs/agent-workspace-contracts.md
1833
+ // §7c) — the agent provisions its contract's spaces itself and the task completion is the ack.
1834
+ const adopt = async (a, contract) => {
1835
+ const key = `${a.gaii}:${contract || ''}`;
1836
+ setAdoptBusy(key);
1837
+ try {
1838
+ const r = await adoptContractTask(a.name, { organismId: orgId, ws: wsId, contract });
1839
+ if (r?.ok === false) showToast?.(r?.error?.message || (t('organisms.adoptFailed') || 'Could not queue the adoption task'));
1840
+ else showToast?.((t('organisms.adoptQueued') || 'Adoption task queued for {agent} — it provisions the contract and reports back').replace('{agent}', a.display_name || a.name));
1841
+ } catch (e) { showToast?.((e && e.message) || (t('organisms.adoptFailed') || 'Could not queue the adoption task')); }
1842
+ finally { setAdoptBusy(''); }
1843
+ };
1798
1844
  useEffect(() => {
1799
1845
  let cancelled = false;
1800
1846
  const fetchIt = () => orgService.getWorkspaceParticipants(orgId, wsId).then(d => { if (!cancelled) setData(d); }).catch(() => {});
@@ -1824,6 +1870,35 @@ function ParticipantsPanel({ orgId, wsId }) {
1824
1870
  </div>
1825
1871
  ${show ? html`
1826
1872
  <div class="pj-parts">
1873
+ ${contractAgents.length ? html`
1874
+ <div class="pj-contract-agents">
1875
+ <div class="pj-access-title">${'📜 '}${t('organisms.contractAgents') || 'Your contract agents'}</div>
1876
+ <div class="section-desc">${t('organisms.contractAgentsDesc') || 'These agents of yours advertise a workspace contract — they can process a workspace like this one. Grant access (below) or attach them in the organism Agents tab.'}</div>
1877
+ <div class="pj-part-agents">
1878
+ ${contractAgents.map(a => {
1879
+ const names = contractNamesOf(a);
1880
+ // An agent already working in THIS workspace (it appears among the viewer's own
1881
+ // agents in the participants data) gets a ✓ instead of Adopt buttons — offering
1882
+ // adoption for an agent that is already here is noise, and the task would be a
1883
+ // no-op re-provision anyway.
1884
+ const here = owners.some(o => o.isSelf && (o.agents || []).some(ag => ag.isOwn && ag.name === a.name));
1885
+ // One adopt action per advertised contract (a single unnamed one falls back to
1886
+ // the bare marker). The agent does the rest — join, provision, complete the task.
1887
+ const actions = names.length ? names : [''];
1888
+ return html`
1889
+ <span class="pj-part-agent own" key=${a.gaii} title=${a.gaii}>
1890
+ ${'📜 '}${a.display_name || a.name}
1891
+ ${here ? html`<span class="badge badge-success pj-mini" title=${t('organisms.contractActiveHint') || 'This agent already works in this workspace'}>${'✓ '}${t('organisms.contractActive') || 'active here'}</span>`
1892
+ : actions.map(c => html`
1893
+ <button class="btn-outline btn-sm pj-adopt-btn" key=${c} disabled=${adoptBusy === `${a.gaii}:${c}`}
1894
+ title=${t('organisms.adoptHint') || 'Queue a task for this agent to adopt its contract into THIS workspace (it provisions the spaces itself)'}
1895
+ onClick=${() => adopt(a, c)}>
1896
+ ${adoptBusy === `${a.gaii}:${c}` ? '…' : `${t('organisms.adoptContract') || 'Adopt'}${c ? ` ${c}` : ''}`}
1897
+ </button>`)}
1898
+ </span>`;
1899
+ })}
1900
+ </div>
1901
+ </div>` : null}
1827
1902
  <${Mermaid} chart=${orgService.buildParticipantsMermaid(data)} />
1828
1903
  <div class="pj-parts-list">
1829
1904
  ${owners.map((o, i) => html`<div class="pj-part-owner" key=${i}>
@@ -1967,9 +2042,15 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
1967
2042
  const [wsName, setWsName] = useState('');
1968
2043
  useEffect(() => {
1969
2044
  let cancelled = false;
1970
- orgService.listWorkspaces(orgId).then(list => { if (!cancelled) setWsName((list.find(w => w.id === wsId)?.name) || ''); }).catch(() => {});
2045
+ orgService.listWorkspaces(orgId).then(list => {
2046
+ if (cancelled) return;
2047
+ const name = (list.find(w => w.id === wsId)?.name) || '';
2048
+ setWsName(name);
2049
+ // Feed the home page's "Continue" list with a real display name.
2050
+ recordRecent({ type: 'workspace', id: `${orgId}/${wsId}`, label: name || wsId, sub: org.name || '', data: { orgId, wsId } });
2051
+ }).catch(() => {});
1971
2052
  return () => { cancelled = true; };
1972
- }, [orgId, wsId]);
2053
+ }, [orgId, wsId]); // eslint-disable-line react-hooks/exhaustive-deps
1973
2054
  // Pop a document out into its own window (served by doc-solo.js) so several can sit side by side,
1974
2055
  // each independent. The window name is unique per document, so re-clicking focuses the open one.
1975
2056
  const popOut = (typeName, docId) => {
@@ -2005,11 +2086,10 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2005
2086
  } catch (e) { /* noop */ }
2006
2087
  return null;
2007
2088
  });
2008
- // Active tab — one tab per manifest space + fixed Review/Activity/People. Persisted per workspace
2009
- // so an F5 (or back-and-forth) returns to the same tab. '' = not chosen yet → first space.
2010
- const tabStoreKey = 'aimeat.ws.' + orgId + '.' + wsId + '.tab';
2011
- const [tab, setTabState] = useState(() => { try { return sessionStorage.getItem(tabStoreKey) || ''; } catch (e) { return ''; } });
2012
- const setTab = (id) => { setTabState(id); try { sessionStorage.setItem(tabStoreKey, id); } catch (e) { /* noop */ } };
2089
+ // Active tab — Overview first (ALWAYS the landing view: the whole workspace on one scroll),
2090
+ // then one tab per manifest space + fixed Sources/Share/Review/Activity/People. Deliberately
2091
+ // NOT persisted: opening a workspace must show the overview, not whatever tab was last open.
2092
+ const [tab, setTab] = useState('overview');
2013
2093
  const [sectionsByType, setSectionsByType] = useState({}); // { typeName: [{id,name,parentId,documents:[docId]}] }
2014
2094
  const [editingSec, setEditingSec] = useState(null); // section id currently being renamed inline
2015
2095
  const draggedDoc = useRef(null); // { type, id } of the doc being dragged
@@ -2023,16 +2103,50 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2023
2103
  const [showFlow, setShowFlow] = useState(true); // the manifest-defined edit-flow chart (first item)
2024
2104
  const [share, setShare] = useState(null); // { public, spaces, docs } — lazy-loaded when Settings opens
2025
2105
  const [shareBusy, setShareBusy] = useState(false);
2106
+ const [wsEvents, setWsEvents] = useState([]); // activity events — Overview strip + per-tab unseen badges
2107
+ const [ovOpen, setOvOpen] = useState({}); // Overview accordion: { spaceName: bool } (mobile starts collapsed)
2108
+ const [ovDoc, setOvDoc] = useState(null); // Overview inline document (mobile): { type, id, mode }
2109
+
2110
+ // ── Unseen-change tracking: per-tab "seen" marks in localStorage (they survive sessions).
2111
+ // A tab's badge counts items changed since the tab was last opened; opening it (or changes
2112
+ // landing while it is open) marks it seen. First-ever visit seeds a baseline at "now", so
2113
+ // history never shows as unseen. Neutral gray badges only — red is reserved for errors.
2114
+ const seenKey = 'aimeat.ws.' + orgId + '.' + wsId + '.seen';
2115
+ const [seen, setSeen] = useState(() => {
2116
+ try { const raw = localStorage.getItem(seenKey); return raw ? JSON.parse(raw) : null; } catch (e) { return null; }
2117
+ });
2118
+ const markSeen = useCallback((tabId) => setSeen(prev => {
2119
+ const next = { ...(prev || {}), [tabId]: new Date().toISOString() };
2120
+ try { localStorage.setItem(seenKey, JSON.stringify(next)); } catch (e) { /* noop */ }
2121
+ return next;
2122
+ }), [seenKey]);
2123
+ // Items (not raw events) changed since the tab's seen mark. The user's own non-agent edits
2124
+ // are not news to them, so they never count. Event actors come as the BARE owner name from
2125
+ // the version history, so compare against both the bare name and the full GHII.
2126
+ const unseenOf = useCallback((tabId) => {
2127
+ if (!seen) return 0;
2128
+ const base = seen[tabId] || seen.__base;
2129
+ if (!base) return 0;
2130
+ const me = getGhii();
2131
+ const meBare = String(me || '').split('@')[0];
2132
+ const isOwn = (e) => !e.agent && me && (e.actor === me || e.actor === meBare);
2133
+ const match = tabId.startsWith('space:') ? (e) => e.type === tabId.slice(6)
2134
+ : (tabId === 'activity' ? () => true : () => false);
2135
+ const evs = wsEvents.filter(e => (e.at || '') > base && match(e) && !isOwn(e));
2136
+ return new Set(evs.map(e => (e.type || '') + ':' + (e.instance || ''))).size;
2137
+ }, [seen, wsEvents]);
2026
2138
 
2027
2139
  const load = useCallback(async () => {
2028
2140
  const w = await orgService.getWorkspace(orgId, wsId).catch(() => null);
2029
2141
  if (w && w.manifest) {
2030
- const [ap, cfg, secs] = await Promise.all([
2142
+ const [ap, cfg, secs, act] = await Promise.all([
2031
2143
  orgService.listApprovals(orgId, 'pending').catch(() => []),
2032
2144
  orgService.getConfig(orgId).catch(() => ({})),
2033
2145
  orgService.getAllSections(orgId, wsId).catch(() => ({})),
2146
+ orgService.getWorkspaceActivity(orgId, wsId).catch(() => ({ events: [] })),
2034
2147
  ]);
2035
2148
  setApprovals(ap); setGateOn(!!(cfg?.gates?.publish?.enabled)); setSectionsByType(secs);
2149
+ setWsEvents(act?.events || []);
2036
2150
  }
2037
2151
  setWs(w && w.manifest ? w : null);
2038
2152
  }, [orgId, wsId]);
@@ -2054,6 +2168,22 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2054
2168
  return () => window.removeEventListener('aimeat-live-update', handler);
2055
2169
  }, []);
2056
2170
 
2171
+ // First-ever visit: seed the seen baseline once the workspace has loaded — no badges for history.
2172
+ useEffect(() => {
2173
+ if (seen !== null || ws === undefined) return;
2174
+ const next = { __base: new Date().toISOString() };
2175
+ try { localStorage.setItem(seenKey, JSON.stringify(next)); } catch (e) { /* noop */ }
2176
+ setSeen(next);
2177
+ }, [seen, ws]); // eslint-disable-line react-hooks/exhaustive-deps
2178
+
2179
+ // Changes landing on the tab the user is LOOKING at are seen immediately — otherwise a draft
2180
+ // saved while you're on its space tab would raise that tab's own badge.
2181
+ useEffect(() => {
2182
+ if (!seen || showSettings) return;
2183
+ const cur = tab || 'overview';
2184
+ if (unseenOf(cur) > 0) markSeen(cur);
2185
+ }, [wsEvents, tab, showSettings, seen, unseenOf, markSeen]);
2186
+
2057
2187
  const setup = useCallback(async () => {
2058
2188
  setBusy(true);
2059
2189
  try {
@@ -2466,6 +2596,23 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2466
2596
  const isDocSpace = orgService.isDocSpace;
2467
2597
  const draftsFor = (name) => (ws.drafts && ws.drafts[name]) || [];
2468
2598
  const objectsFor = (name) => (ws.objects && ws.objects[name]) || [];
2599
+ // One entry per id, draft (working copy) taking precedence over its published version —
2600
+ // the index must show the draft badge even when a published `.latest` also exists.
2601
+ const mergedDocs = (ot) => {
2602
+ const byId = new Map();
2603
+ for (const d of objectsFor(ot.name)) byId.set(d.id, { ...d, _draft: false, _published: true });
2604
+ for (const d of draftsFor(ot.name)) {
2605
+ const pub = byId.get(d.id); // kept on `_pub` for the view's Draft/Published toggle
2606
+ byId.set(d.id, { ...d, _draft: true, _published: !!pub, _pub: pub || null });
2607
+ }
2608
+ return [...byId.values()];
2609
+ };
2610
+ const mergedRecords = (ot) => {
2611
+ const byId = new Map();
2612
+ for (const o of objectsFor(ot.name)) byId.set(o.id, { ...o, _draft: false });
2613
+ for (const d of draftsFor(ot.name)) byId.set(d.id, { ...d, _draft: true });
2614
+ return [...byId.values()];
2615
+ };
2469
2616
 
2470
2617
  // ── Public sharing (meta.share) — what published document-space pages anyone can read via the
2471
2618
  // no-login viewer. Independent of the access roles. Lazy-loaded the first time the panel opens. ──
@@ -2500,17 +2647,7 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2500
2647
  // tied to a section's documents[] (or unsorted). Edits to the tree persist immediately.
2501
2648
  const renderDocSpace = (ot) => {
2502
2649
  const secs = sectionsByType[ot.name] || [];
2503
- // One entry per document id. A draft (working copy) takes precedence over its published
2504
- // version, so the index shows the draft badge even when a published `.latest` also exists.
2505
- // (Without this, keying docById by id let the published entry overwrite the draft and the
2506
- // "draft" badge silently disappeared whenever a doc had both versions.)
2507
- const byId = new Map();
2508
- for (const d of objectsFor(ot.name)) byId.set(d.id, { ...d, _draft: false, _published: true });
2509
- for (const d of draftsFor(ot.name)) {
2510
- const pub = byId.get(d.id); // the published version, if one exists — kept on `_pub` for the view's Draft/Published toggle
2511
- byId.set(d.id, { ...d, _draft: true, _published: !!pub, _pub: pub || null });
2512
- }
2513
- const docs = [...byId.values()];
2650
+ const docs = mergedDocs(ot);
2514
2651
  const docById = {}; docs.forEach(d => { docById[d.id] = d; });
2515
2652
  const used = new Set(); secs.forEach(s => (s.documents || []).forEach(id => used.add(id)));
2516
2653
  const unsorted = docs.filter(d => !used.has(d.id));
@@ -2665,12 +2802,13 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2665
2802
  { id: 'activity', label: t('organisms.activity') || 'Activity', count: null },
2666
2803
  { id: 'people', label: t('organisms.tabPeople') || 'People', count: null },
2667
2804
  ];
2668
- const wsTabs = [...spaceTabs, ...fixedTabs];
2805
+ const wsTabs = [{ id: 'overview', label: t('organisms.tabOverview') || 'Overview', count: null }, ...spaceTabs, ...fixedTabs];
2669
2806
  // Settings REPLACES the tab content: while it is open no tab is active (the previous state — a
2670
2807
  // highlighted tab above settings content — lied about what the user was looking at).
2671
- const activeTab = showSettings ? '' : (wsTabs.some(tb => tb.id === tab) ? tab : (spaceTabs[0]?.id || 'review'));
2808
+ const activeTab = showSettings ? '' : (wsTabs.some(tb => tb.id === tab) ? tab : 'overview');
2672
2809
  const activeSpace = activeTab.startsWith('space:') ? allTypes.find(ot => ot.name === activeTab.slice(6)) : null;
2673
- const pickTab = (id) => guardWsDirty(() => { setShowSettings(false); setTab(id); });
2810
+ // Opening a tab clears its unseen badge (the seen mark persists across sessions).
2811
+ const pickTab = (id) => guardWsDirty(() => { setShowSettings(false); setTab(id); markSeen(id); });
2674
2812
 
2675
2813
  // Menu items are VERBS (a click copies to the clipboard; the toast confirms). The contract-agent
2676
2814
  // builder is a different category from the two copy actions, so a divider separates it.
@@ -2681,6 +2819,123 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2681
2819
  { label: t('organisms.contractPrompt') || 'Create contract agent', icon: '⚙️', onClick: copyContractPrompt },
2682
2820
  ];
2683
2821
 
2822
+ // ── Overview (landing tab): the whole workspace on one vertical scroll — a "what happened
2823
+ // here" strip from the activity feed, then every manifest space as its own stacked section.
2824
+ // Tabs stay for focused work; this answers "where is everything and what changed".
2825
+ const isMobileView = () => window.matchMedia('(max-width: 640px)').matches;
2826
+ const shortActor = (a) => String(a || '').split('@')[0];
2827
+ // First non-empty markdown line as a gray preview (heading/list markers stripped).
2828
+ const firstLine = (md) => {
2829
+ const line = String(md || '').split('\n').map(s => s.trim()).find(s => s && !s.startsWith('```'));
2830
+ return line ? line.replace(/^#{1,6}\s+/, '').replace(/^[-*>]\s+/, '').slice(0, 120) : '';
2831
+ };
2832
+ // Strip event → jump straight to the changed item in its space tab.
2833
+ const gotoEvent = (e) => {
2834
+ const target = allTypes.find(o => o.name === e.type);
2835
+ if (!target || !orgService.isMemorySpace(target)) { pickTab('activity'); return; }
2836
+ if (isDocSpace(target)) setActiveDoc({ type: target.name, mode: 'view', page: { id: e.instance } });
2837
+ else setExpandedRec(s => ({ ...s, [target.name + ':' + e.instance]: true }));
2838
+ pickTab('space:' + target.name);
2839
+ };
2840
+ const openOvRec = (ot, r) => { setExpandedRec(s => ({ ...s, [ot.name + ':' + r.id]: true })); pickTab('space:' + ot.name); };
2841
+ // A document opens in its space tab on desktop; on mobile it expands INLINE right here —
2842
+ // view and edit both — so no window juggling is ever needed on a phone.
2843
+ const openOvDoc = (ot, d) => {
2844
+ if (isMobileView()) setOvDoc(v => (v && v.type === ot.name && v.id === d.id) ? null : { type: ot.name, id: d.id, mode: 'view' });
2845
+ else { setActiveDoc({ type: ot.name, mode: 'view', page: { id: d.id } }); pickTab('space:' + ot.name); }
2846
+ };
2847
+ const ovAddNew = (ot, docMode) => {
2848
+ if (docMode) setActiveDoc({ type: ot.name, mode: 'edit', page: { id: '', title: '', markdown: '' } });
2849
+ else startAdd(ot);
2850
+ pickTab('space:' + ot.name);
2851
+ };
2852
+
2853
+ const renderOvDocInline = (ot, d) => ovDoc.mode === 'edit'
2854
+ ? html`<div class="pj-ov-doc-inline"><${DocumentEditor} key=${'oved-' + d.id} orgId=${orgId} page=${d} busy=${busy}
2855
+ onSave=${(p) => { savePage(ot, p); setOvDoc({ type: ot.name, id: d.id, mode: 'view' }); }}
2856
+ onCancel=${() => setOvDoc({ type: ot.name, id: d.id, mode: 'view' })} /></div>`
2857
+ : html`<div class="pj-ov-doc-inline"><${DocumentView} key=${'ovv-' + d.id} page=${d} busy=${busy}
2858
+ onEdit=${() => setOvDoc({ type: ot.name, id: d.id, mode: 'edit' })}
2859
+ onPublish=${() => publish(ot, d.id)}
2860
+ onWikiLink=${(content) => {
2861
+ const title = String(content).split('#')[0].trim();
2862
+ const targetDoc = title && mergedDocs(ot).find(x => (x.title || '').toLowerCase() === title.toLowerCase());
2863
+ if (targetDoc) setOvDoc({ type: ot.name, id: targetDoc.id, mode: 'view' });
2864
+ }} /></div>`;
2865
+
2866
+ const renderOvSection = (ot) => {
2867
+ const memory = orgService.isMemorySpace(ot);
2868
+ const docMode = memory && isDocSpace(ot);
2869
+ const items = memory ? (docMode ? mergedDocs(ot) : mergedRecords(ot)) : [];
2870
+ const label = cap(wsT('type.' + ot.name) || ot.name);
2871
+ // A space the manifest declares but memory doesn't back → one notice row, never hidden.
2872
+ if (!memory) return html`
2873
+ <div class="pj-ov-row" key=${ot.name}>
2874
+ <button class="pj-ov-name pj-ov-name-link" onClick=${() => pickTab('space:' + ot.name)}>${label}</button>
2875
+ <span class="badge badge-warn">${String(ot.backing)}</span>
2876
+ </div>`;
2877
+ // Empty space → a single compact row (name + none + add), not an empty box.
2878
+ if (items.length === 0) return html`
2879
+ <div class="pj-ov-row" key=${ot.name}>
2880
+ <span class="pj-ov-name">${label}</span>
2881
+ <span class="pj-muted">${t('organisms.noneYet') || 'none yet'}</span>
2882
+ <button class="btn-ghost btn-sm" onClick=${() => ovAddNew(ot, docMode)}>${'+ '}${docMode ? (t('organisms.newPage') || 'New document') : (t('organisms.addDraft') || 'Add draft')}</button>
2883
+ </div>`;
2884
+ const open = ovOpen[ot.name] ?? !isMobileView();
2885
+ const shown = items.slice(0, 5);
2886
+ return html`
2887
+ <div class="pj-ov-sec" key=${ot.name}>
2888
+ <div class="pj-ov-sec-head" onClick=${() => setOvOpen(s => ({ ...s, [ot.name]: !open }))}>
2889
+ <span class="pj-ov-chevron">${open ? '▾' : '▸'}</span>
2890
+ <span class="pj-ov-name">${label}</span>
2891
+ <span class="pj-org-tab-count">${items.length}</span>
2892
+ ${docMode ? html`<span class="pj-doc-tag">${t('organisms.docs') || 'docs'}</span>` : null}
2893
+ <span class="pj-ov-spacer"></span>
2894
+ <button class="btn-ghost btn-sm" onClick=${(ev) => { ev.stopPropagation(); ovAddNew(ot, docMode); }}>${'+ '}${docMode ? (t('organisms.newPage') || 'New document') : (t('organisms.addDraft') || 'Add draft')}</button>
2895
+ </div>
2896
+ ${open ? html`
2897
+ <div class="pj-ov-items">
2898
+ ${shown.map(d => docMode ? html`
2899
+ <div class="pj-ov-doc" key=${d.id}>
2900
+ <button class="pj-ov-item" onClick=${() => openOvDoc(ot, d)}>
2901
+ ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
2902
+ <span class="pj-ov-item-title">${d.title || d.id}</span>
2903
+ <span class="pj-ov-preview">${firstLine(d.markdown)}</span>
2904
+ </button>
2905
+ ${ovDoc && ovDoc.type === ot.name && ovDoc.id === d.id ? renderOvDocInline(ot, d) : null}
2906
+ </div>` : html`
2907
+ <button class="pj-ov-item" key=${d.id} onClick=${() => openOvRec(ot, d)}>
2908
+ ${d._draft ? html`<span class="badge badge-warn pj-mini">${t('organisms.draft') || 'draft'}</span>` : null}
2909
+ <span class="pj-ov-item-title">${String(d[PRIMARY_FIELD[ot.name] || 'title'] || d.summary || d.id || '')}</span>
2910
+ ${d.status ? html`<span class="badge badge-info">${String(d.status)}</span>` : null}
2911
+ </button>`)}
2912
+ ${items.length > 5 ? html`
2913
+ <button class="pj-ov-more" onClick=${() => pickTab('space:' + ot.name)}>${(t('organisms.ovShowAll') || 'Show all {n} →').replace('{n}', String(items.length))}</button>` : null}
2914
+ </div>` : null}
2915
+ </div>`;
2916
+ };
2917
+
2918
+ const renderOverview = () => {
2919
+ const recent = wsEvents.slice(0, 8);
2920
+ return html`
2921
+ <div class="pj-ov">
2922
+ ${recent.length > 0 ? html`
2923
+ <div class="pj-ov-strip">
2924
+ <div class="pj-ov-strip-title">${t('organisms.ovRecent') || 'What happened here'}</div>
2925
+ ${recent.map((e, i) => html`
2926
+ <button class="pj-ov-event" key=${i} onClick=${() => gotoEvent(e)}>
2927
+ <span class="pj-act-dot ${e.action}"></span>
2928
+ <span class="pj-ov-event-who">${shortActor(e.actor)}${e.agent ? html` <span class="pj-act-agent" title=${t('organisms.viaAgent') || 'via this agent'}>🤖 ${e.agent}</span>` : null}</span>
2929
+ <span class="pj-ov-event-act">${e.action === 'publish' ? (t('organisms.publishedVerb') || 'published') : (t('organisms.editedVerb') || 'edited')}</span>
2930
+ <span class="pj-ov-event-what">${e.mode === 'document' ? '📄' : '🗂'} ${(wsT('type.' + e.type) || e.type)}${' / '}${e.instance}</span>
2931
+ <span class="pj-ov-event-time">${relTime(e.at)}</span>
2932
+ </button>`)}
2933
+ </div>` : null}
2934
+ ${allTypes.map(renderOvSection)}
2935
+ ${allTypes.length === 0 ? html`<${EmptyState} text=${t('organisms.noneYet') || 'none yet'} />` : null}
2936
+ </div>`;
2937
+ };
2938
+
2684
2939
  return html`
2685
2940
  <div class="pj-ws">
2686
2941
  <${ConfirmUI} />
@@ -2706,11 +2961,15 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2706
2961
  </div>` : null}
2707
2962
 
2708
2963
  <div class="pj-org-tabs" role="tablist">
2709
- ${wsTabs.map(tb => html`
2964
+ ${wsTabs.map(tb => {
2965
+ const u = activeTab === tb.id ? 0 : unseenOf(tb.id);
2966
+ return html`
2710
2967
  <button class="pj-org-tab ${activeTab === tb.id ? 'active' : ''}" role="tab" aria-selected=${activeTab === tb.id} key=${tb.id}
2711
2968
  onClick=${() => pickTab(tb.id)}>
2712
2969
  ${(tb.label)}${tb.count !== null && tb.count !== undefined ? html`<span class="pj-org-tab-count">${tb.count}</span>` : null}
2713
- </button>`)}
2970
+ ${u > 0 ? html`<span class="pj-org-tab-unseen" title=${t('organisms.unseenHint') || 'Changed since your last visit'}>${u}</span>` : null}
2971
+ </button>`;
2972
+ })}
2714
2973
  <button class="pj-org-tab pj-ws-tab-add" title=${t('organisms.addDocSpaceTitle') || 'Add a document space'} onClick=${() => guardWsDirty(() => { setShowSettings(false); setShowSpaces(s => !s); })}>+</button>
2715
2974
  </div>
2716
2975
 
@@ -2793,6 +3052,8 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2793
3052
  </div>
2794
3053
  `}
2795
3054
 
3055
+ ${activeTab === 'overview' ? renderOverview() : null}
3056
+
2796
3057
  ${activeSpace
2797
3058
  ? (!orgService.isMemorySpace(activeSpace)
2798
3059
  ? renderSpaceNotice(activeSpace)
@@ -2870,7 +3131,7 @@ function Workspace({ org, wsId, showToast, onBack, onBackToList }) {
2870
3131
  `)}
2871
3132
  </div>` : null}` : null}
2872
3133
 
2873
- ${activeTab === 'people' ? html`<${ParticipantsPanel} orgId=${orgId} wsId=${wsId} />` : null}
3134
+ ${activeTab === 'people' ? html`<${ParticipantsPanel} orgId=${orgId} wsId=${wsId} showToast=${showToast} />` : null}
2874
3135
  </div>
2875
3136
  `;
2876
3137
  }