aimeat 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +22 -1
  2. package/dist/locales/en.json +28 -0
  3. package/dist/locales/fi.json +28 -0
  4. package/dist/public/css/views/agents-detail.css +145 -0
  5. package/dist/public/css/views/scheduler.css +16 -0
  6. package/dist/public/js/services/admin.js +3 -0
  7. package/dist/public/llms-template.txt +170 -2
  8. package/dist/public/oauth-consent.html +7 -1
  9. package/dist/public/views/admin/portal-tab.js +94 -9
  10. package/dist/public/views/profile/agents/agent-card.js +24 -4
  11. package/dist/public/views/profile/agents/tab-schedules.js +3 -32
  12. package/dist/public/views/profile/agents/tab-tasks.js +5 -2
  13. package/dist/public/views/profile/agents-tab.js +86 -3
  14. package/dist/public/views/profile/agents-tasks-subtab.js +71 -7
  15. package/dist/public/views/profile/schedule-item.js +177 -0
  16. package/dist/public/views/profile/scheduler-tab.js +5 -51
  17. package/dist/src/generated/api-types.d.ts +277 -2
  18. package/dist/src/generated/api-types.d.ts.map +1 -1
  19. package/dist/src/mcp/catalog/scopes.js +1 -1
  20. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  21. package/dist/src/mcp/index.d.ts.map +1 -1
  22. package/dist/src/mcp/index.js +12 -0
  23. package/dist/src/mcp/index.js.map +1 -1
  24. package/dist/src/routes/bootstrap.d.ts +2 -1
  25. package/dist/src/routes/bootstrap.d.ts.map +1 -1
  26. package/dist/src/routes/bootstrap.js +14 -3
  27. package/dist/src/routes/bootstrap.js.map +1 -1
  28. package/dist/src/routes/cortex.d.ts +13 -0
  29. package/dist/src/routes/cortex.d.ts.map +1 -1
  30. package/dist/src/routes/cortex.js +211 -0
  31. package/dist/src/routes/cortex.js.map +1 -1
  32. package/dist/src/routes/extensions.d.ts +14 -0
  33. package/dist/src/routes/extensions.d.ts.map +1 -1
  34. package/dist/src/routes/extensions.js +255 -109
  35. package/dist/src/routes/extensions.js.map +1 -1
  36. package/dist/src/routes/lib-header.d.ts +22 -0
  37. package/dist/src/routes/lib-header.d.ts.map +1 -0
  38. package/dist/src/routes/lib-header.js +244 -0
  39. package/dist/src/routes/lib-header.js.map +1 -0
  40. package/dist/src/routes/libs.d.ts.map +1 -1
  41. package/dist/src/routes/libs.js +13 -0
  42. package/dist/src/routes/libs.js.map +1 -1
  43. package/dist/src/routes/schedules.d.ts.map +1 -1
  44. package/dist/src/routes/schedules.js +19 -5
  45. package/dist/src/routes/schedules.js.map +1 -1
  46. package/dist/src/routes/site.d.ts.map +1 -1
  47. package/dist/src/routes/site.js +39 -1
  48. package/dist/src/routes/site.js.map +1 -1
  49. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  50. package/dist/src/server-bootstrap/routes-loader.js +5 -2
  51. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  52. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  53. package/dist/src/services/prompt-defaults.js +35 -0
  54. package/dist/src/services/prompt-defaults.js.map +1 -1
  55. package/dist/src/services/prompt-seeder.d.ts.map +1 -1
  56. package/dist/src/services/prompt-seeder.js +6 -2
  57. package/dist/src/services/prompt-seeder.js.map +1 -1
  58. package/dist/src/services/site.d.ts +4 -0
  59. package/dist/src/services/site.d.ts.map +1 -1
  60. package/dist/src/services/site.js +29 -0
  61. package/dist/src/services/site.js.map +1 -1
  62. package/dist/src/storage/interface.d.ts +1 -1
  63. package/dist/src/storage/interface.d.ts.map +1 -1
  64. package/dist/src/utils/csp-nonce.d.ts +19 -0
  65. package/dist/src/utils/csp-nonce.d.ts.map +1 -0
  66. package/dist/src/utils/csp-nonce.js +25 -0
  67. package/dist/src/utils/csp-nonce.js.map +1 -0
  68. package/package.json +1 -1
@@ -7,6 +7,12 @@
7
7
  * @version-history
8
8
  * v1.1.0 — 2026-06-02 — Admin design unification: raw template textarea →
9
9
  * adm-textarea adm-input-full (drop inline mono/border/background styles).
10
+ * v1.2.0 — 2026-06-03 — Active-source status (custom template vs built-in SPA)
11
+ * + "Load current page" button that seeds the editor with the live / HTML.
12
+ * v1.3.0 — 2026-06-03 — Fix portal memory keys writing to the wrong namespace
13
+ * (now /v1/site/memory → __site__ so {{memory:portal/*}} resolves); add AI
14
+ * bundle import (/v1/site/import) so the AI-Assisted result no longer 422s in
15
+ * the template box; reload the preview iframe after every change + Clear Cache.
10
16
  */
11
17
  import { h } from 'preact';
12
18
  import { useState, useEffect } from 'preact/hooks';
@@ -17,7 +23,8 @@ import { escHtml, copyToClipboard } from '/js/utils.js';
17
23
  import { dt, Badge, ExpandableHelp, useToast, Toast } from './shared.js';
18
24
  import {
19
25
  saveSiteTemplate, deleteSiteTemplate, clearSiteCache,
20
- getSiteMemoryKeys, getSitePrompt, addMemory, deleteMemory, triggerLbSync,
26
+ getSiteMemoryKeys, getSitePrompt, setSiteMemory, deleteSiteMemory,
27
+ importSiteBundle, triggerLbSync,
21
28
  } from '/js/services/admin.js';
22
29
  import { useConfirm } from '/components/Modal.js';
23
30
 
@@ -28,12 +35,17 @@ export default function PortalTab({ data, reload }) {
28
35
  const tmpl = p.template || {};
29
36
  const changes = (p.changelog?.entries) || [];
30
37
  const isLb = meta.lb_mode?.enabled;
38
+ const hasCustom = !!meta.has_custom_template;
31
39
 
32
40
  const { confirm, ConfirmUI } = useConfirm();
33
41
  const [template, setTemplate] = useState(tmpl.template || '');
34
42
  const [memKeys, setMemKeys] = useState(null);
35
43
  const [newKey, setNewKey] = useState('');
36
44
  const [newVal, setNewVal] = useState('');
45
+ const [aiBundle, setAiBundle] = useState('');
46
+ // Bumped after any change so the preview iframe re-fetches `/` (busts browser cache).
47
+ const [previewNonce, setPreviewNonce] = useState(0);
48
+ const bumpPreview = () => setPreviewNonce(n => n + 1);
37
49
 
38
50
  useEffect(() => { loadMemKeys(); }, []);
39
51
 
@@ -45,10 +57,56 @@ export default function PortalTab({ data, reload }) {
45
57
  }
46
58
 
47
59
  async function saveTemplate() {
48
- try { await saveSiteTemplate(template); reload(); }
60
+ // The AI-Assisted prompt produces a JSON import bundle, not raw HTML. If the
61
+ // user pasted that here, the template route would 422 — point them to Import.
62
+ if (template.trimStart().startsWith('{')) {
63
+ showErr(t('dashboard.portalSaveLooksLikeJson'));
64
+ return;
65
+ }
66
+ try { await saveSiteTemplate(template); bumpPreview(); reload(); }
49
67
  catch (e) { showErr(e.message); }
50
68
  }
51
69
 
70
+ async function importAiBundle() {
71
+ let bundle;
72
+ try { bundle = JSON.parse(aiBundle); }
73
+ catch { showErr(t('dashboard.portalImportInvalid')); return; }
74
+ if (!bundle || typeof bundle !== 'object' || (!bundle.template && !bundle.memory && !bundle.kv)) {
75
+ showErr(t('dashboard.portalImportInvalid'));
76
+ return;
77
+ }
78
+ try {
79
+ const res = await importSiteBundle(bundle);
80
+ const d = res.data || {};
81
+ const parts = [];
82
+ if (d.template_stored) parts.push('template');
83
+ if (d.memory_keys_written) parts.push(d.memory_keys_written + ' memory');
84
+ if (d.kv_pairs_updated) parts.push(d.kv_pairs_updated + ' KV');
85
+ showOk(t('dashboard.portalImportDone').replace('{summary}', parts.join(', ') || '—'));
86
+ if (typeof bundle.template === 'string') setTemplate(bundle.template);
87
+ setAiBundle('');
88
+ loadMemKeys();
89
+ bumpPreview();
90
+ reload();
91
+ } catch (e) { showErr(e.message); }
92
+ }
93
+
94
+ async function loadCurrentAsTemplate() {
95
+ const run = async () => {
96
+ try {
97
+ const resp = await fetch('/', { headers: { Accept: 'text/html' }, cache: 'no-store' });
98
+ if (!resp.ok) throw new Error('HTTP ' + resp.status);
99
+ setTemplate(await resp.text());
100
+ showOk(t('dashboard.portalLoadedCurrent'));
101
+ } catch (e) { showErr(e.message); }
102
+ };
103
+ if (template && template.trim()) {
104
+ confirm(t('dashboard.portalLoadCurrentConfirm'), run);
105
+ } else {
106
+ run();
107
+ }
108
+ }
109
+
52
110
  function downloadTemplate() {
53
111
  if (!template) return;
54
112
  const blob = new Blob([template], { type: 'text/html' });
@@ -58,24 +116,28 @@ export default function PortalTab({ data, reload }) {
58
116
 
59
117
  function resetTemplate() {
60
118
  confirm(t('dashboard.portalResetDefault') + '?', async () => {
61
- try { await deleteSiteTemplate(); reload(); }
119
+ try { await deleteSiteTemplate(); setTemplate(''); bumpPreview(); reload(); }
62
120
  catch (e) { showErr(e.message); }
63
121
  }, { danger: true });
64
122
  }
65
123
 
66
124
  async function doClearCache() {
67
- try { await clearSiteCache(); } catch (e) { showErr(e.message); }
125
+ try { await clearSiteCache(); bumpPreview(); } catch (e) { showErr(e.message); }
68
126
  }
69
127
 
70
128
  async function addMem() {
71
129
  if (!newKey.trim()) return;
72
- try { await addMemory(newKey, newVal); setNewKey(''); setNewVal(''); loadMemKeys(); }
73
- catch (e) { showErr(e.message); }
130
+ try {
131
+ await setSiteMemory(newKey.trim(), newVal);
132
+ setNewKey(''); setNewVal('');
133
+ showOk(t('dashboard.portalMemorySaved'));
134
+ loadMemKeys(); bumpPreview();
135
+ } catch (e) { showErr(e.message); }
74
136
  }
75
137
 
76
138
  function delMem(key) {
77
139
  confirm(t('dashboard.portalDeleteKeyConfirm').replace('{key}', key), async () => {
78
- try { await deleteMemory(key); loadMemKeys(); }
140
+ try { await deleteSiteMemory(key); loadMemKeys(); bumpPreview(); }
79
141
  catch (e) { showErr(e.message); }
80
142
  }, { danger: true });
81
143
  }
@@ -120,7 +182,7 @@ export default function PortalTab({ data, reload }) {
120
182
  <!-- Preview -->
121
183
  <div class="adm-card">
122
184
  <h3>${t('dashboard.portalPreview')}</h3>
123
- <iframe src="/" style="width:100%;height:400px;border:1px solid var(--glass-border);border-radius:8px;background:#fff" sandbox="allow-same-origin allow-scripts"></iframe>
185
+ <iframe src=${'/?_preview=' + previewNonce} style="width:100%;height:400px;border:1px solid var(--glass-border);border-radius:8px;background:#fff" sandbox="allow-same-origin allow-scripts"></iframe>
124
186
  <div class="adm-flex adm-mt-sm">
125
187
  <button class="adm-btn-action" onClick=${doClearCache}>\u{1F6AB} ${t('dashboard.portalClearCache')}</button>
126
188
  </div>
@@ -129,6 +191,22 @@ export default function PortalTab({ data, reload }) {
129
191
  <!-- Template editor -->
130
192
  <div class="adm-card">
131
193
  <h3>${t('dashboard.portalTemplate')}</h3>
194
+ <div class="adm-erow">
195
+ <span class="adm-elabel">${t('dashboard.portalActiveSource')}</span>
196
+ <span class="adm-eval">
197
+ ${hasCustom
198
+ ? html`<span class="adm-badge adm-badge-active">${t('dashboard.portalCustomActive')}</span>${tmpl.updated_at ? html` · ${dt(tmpl.updated_at)}` : ''}`
199
+ : html`<span class="adm-badge adm-badge-info">${t('dashboard.portalDefaultActive')}</span>`}
200
+ </span>
201
+ </div>
202
+ <p class="adm-text-dim adm-text-base adm-mb-sm">
203
+ ${hasCustom
204
+ ? t('dashboard.portalCustomExplain')
205
+ : t('dashboard.portalDefaultExplain').replace('{action}', t('dashboard.portalLoadCurrent'))}
206
+ </p>
207
+ <div class="adm-mb-sm">
208
+ <button class="adm-btn-action" onClick=${loadCurrentAsTemplate}>\u{1F4C4} ${t('dashboard.portalLoadCurrent')}</button>
209
+ </div>
132
210
  <${ExpandableHelp} title=${t('dashboard.portalTagHelpTitle')}>
133
211
  <p>${t('dashboard.portalTagHelpDetail')}</p>
134
212
  <table>
@@ -194,9 +272,16 @@ export default function PortalTab({ data, reload }) {
194
272
  <div class="adm-card">
195
273
  <h3>${t('dashboard.portalAiChat')}</h3>
196
274
  <p class="adm-text-dim adm-text-base">${t('dashboard.portalAiExplain')}</p>
197
- <div class="adm-mt-sm">
275
+ <div class="adm-mt-sm adm-mb-sm">
198
276
  <button class="adm-btn-action" onClick=${copyPrompt}>\u{1F4CB} ${t('dashboard.portalAiLoadPrompt')}</button>
199
277
  </div>
278
+ <p class="adm-text-dim adm-text-base adm-mb-sm">${t('dashboard.portalAiBundleExplain')}</p>
279
+ <textarea class="adm-textarea adm-input-full" rows="6" value=${aiBundle}
280
+ onInput=${e => setAiBundle(e.target.value)}
281
+ placeholder=${t('dashboard.portalAiBundlePlaceholder')} style="font-size:13px"></textarea>
282
+ <div class="adm-flex adm-mt-sm">
283
+ <button class="adm-btn-action" onClick=${importAiBundle}>\u{1F4E5} ${t('dashboard.portalAiImport')}</button>
284
+ </div>
200
285
  </div>
201
286
 
202
287
  <!-- Changelog -->
@@ -3,6 +3,10 @@
3
3
  * @description Agent card component with collapsed/expanded states,
4
4
  * Two-Zone Header (identity + state-dependent status), and tab bar.
5
5
  * @version-history
6
+ * v1.13.0 -- 2026-06-03 -- Deep-link props: preSelectedTab forces a tab (and
7
+ * locks it past the async README default), openTaskId + openTaskNonce are
8
+ * threaded to the Tasks sub-tab so the fleet "running now" panel can open a
9
+ * specific task directly.
6
10
  * v1.12.0 -- 2026-06-02 -- Active tracked tab never shows its own badge and is
7
11
  * kept marked-seen as new items arrive (so a message landing while you're on
8
12
  * the Messages tab doesn't raise a (1) you're already looking at).
@@ -111,7 +115,7 @@ function totalChanges(changes) {
111
115
  return (changes.tasks || 0) + (changes.messages || 0) + (changes.memory || 0);
112
116
  }
113
117
 
114
- export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, changes, onTabSeen, onScopesClick, onDeleteClick, onFederateToggle, onPopOut, soloMode = false }) {
118
+ export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, changes, onTabSeen, onScopesClick, onDeleteClick, onFederateToggle, onPopOut, soloMode = false, preSelectedTab = null, openTaskId = null, openTaskNonce = 0 }) {
115
119
  const state = detectAgentState(agent, onboarding);
116
120
  const [activeTab, setActiveTab] = useState(null);
117
121
  // README markdown: undefined = not loaded, '' = none published, string = show tab.
@@ -119,6 +123,9 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
119
123
  // True once the user clicks a tab, so the async README default never yanks
120
124
  // them off a tab they chose.
121
125
  const userPickedTab = useRef(false);
126
+ // Last deep-link nonce we acted on, so a fresh panel click re-forces the tab
127
+ // but a plain re-expand (same nonce) does not override the README default.
128
+ const lastDeepLinkNonce = useRef(0);
122
129
 
123
130
  // Load the README once the card is expanded (lazy — collapsed cards skip it).
124
131
  useEffect(() => {
@@ -141,6 +148,19 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
141
148
  setActiveTab(hasReadme ? 'readme' : getDefaultTab(state));
142
149
  }, [expanded, readme]);
143
150
 
151
+ // Deep-link: when the fleet "running now" panel requests this agent's Tasks
152
+ // tab (preSelectedTab), force that tab and lock it (userPickedTab) so the
153
+ // async README default can't yank it away. Gated on a NEW openTaskNonce so a
154
+ // fresh panel click re-forces it, but a plain manual re-expand (same nonce)
155
+ // leaves the README default / the user's own tab choice alone.
156
+ useEffect(() => {
157
+ if (!expanded || !preSelectedTab || !openTaskNonce) return;
158
+ if (openTaskNonce === lastDeepLinkNonce.current) return;
159
+ lastDeepLinkNonce.current = openTaskNonce;
160
+ userPickedTab.current = true;
161
+ setActiveTab(preSelectedTab);
162
+ }, [expanded, preSelectedTab, openTaskNonce]);
163
+
144
164
  // While the owner is actively viewing a tracked tab (Tasks/Messages/Memory),
145
165
  // keep it marked-seen as new items arrive. Without this, a message landing
146
166
  // while you're on the Messages tab would raise its (1) badge even though you
@@ -256,7 +276,7 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
256
276
 
257
277
  <!-- Tab Content -->
258
278
  <div class="pf-agd-tab-content" onClick=${(e) => e.stopPropagation()}>
259
- ${renderTabContent(activeTab, agent, onboarding, session, showToast, allAgents, readme)}
279
+ ${renderTabContent(activeTab, agent, onboarding, session, showToast, allAgents, readme, openTaskId, openTaskNonce)}
260
280
  </div>
261
281
 
262
282
  <!-- Card Footer: Scopes + Delete -->
@@ -550,12 +570,12 @@ function ProblemZone2({ agent, setActiveTab, showToast }) {
550
570
  `;
551
571
  }
552
572
 
553
- function renderTabContent(activeTab, agent, onboarding, session, showToast, allAgents, readme) {
573
+ function renderTabContent(activeTab, agent, onboarding, session, showToast, allAgents, readme, openTaskId, openTaskNonce) {
554
574
  const props = { agent, onboarding, session, showToast, agentName: agent.name, allAgents };
555
575
  switch (activeTab) {
556
576
  case 'readme': return html`<${TabReadme} readme=${readme} />`;
557
577
  case 'integration': return html`<${TabIntegration} ...${props} />`;
558
- case 'tasks': return html`<${TabTasks} ...${props} />`;
578
+ case 'tasks': return html`<${TabTasks} ...${props} openTaskId=${openTaskId} openTaskNonce=${openTaskNonce} />`;
559
579
  case 'messages': return html`<${TabMessages} ...${props} />`;
560
580
  case 'data-access': return html`<${TabDataAccess} ...${props} />`;
561
581
  case 'directives': return html`<${TabDirectives} ...${props} />`;
@@ -11,14 +11,12 @@ import { h } from 'preact';
11
11
  import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
12
12
  import htm from 'htm';
13
13
  import { t } from '/js/i18n.js';
14
- import { timeAgo } from '/js/utils.js';
15
- import { listAgentSchedules, setScheduleEnabled, triggerSchedule, deleteSchedule } from '/js/services/schedules.js';
14
+ import { listAgentSchedules } from '/js/services/schedules.js';
16
15
  import { CreateForm } from '../scheduler-tab.js';
16
+ import ScheduleItem from '../schedule-item.js';
17
17
 
18
18
  const html = htm.bind(h);
19
19
 
20
- const KIND_LABEL = { ai: 'profile.scheduler.kind.ai', agent_task: 'profile.scheduler.kind.agent_task', extension: 'profile.scheduler.kind.extension', core: 'profile.scheduler.kind.core' };
21
-
22
20
  export default function TabSchedules({ agentName, allAgents = [], showToast }) {
23
21
  const [managed, setManaged] = useState([]);
24
22
  const [internal, setInternal] = useState([]);
@@ -45,13 +43,6 @@ export default function TabSchedules({ agentName, allAgents = [], showToast }) {
45
43
  return () => { window.removeEventListener('aimeat-live-update', handler); clearInterval(poller); };
46
44
  }, [loadData]);
47
45
 
48
- const onToggle = async (j) => { try { await setScheduleEnabled(j.id, !j.enabled); await loadData(); } catch (e) { showToast?.(e.message, true); } };
49
- const onTrigger = async (j) => { try { await triggerSchedule(j.id); showToast?.(t('profile.scheduler.triggered')); await loadData(); } catch (e) { showToast?.(e.message, true); } };
50
- const onCancel = async (j) => {
51
- if (!window.confirm(t('profile.scheduler.confirmCancel'))) return;
52
- try { await deleteSchedule(j.id); showToast?.(t('profile.scheduler.cancelled')); await loadData(); } catch (e) { showToast?.(e.message, true); }
53
- };
54
-
55
46
  if (loading) return html`<div class="sch-loading">${t('profile.scheduler.loading')}</div>`;
56
47
 
57
48
  return html`
@@ -70,27 +61,7 @@ export default function TabSchedules({ agentName, allAgents = [], showToast }) {
70
61
  <h3 class="sch-section-title">${t('profile.scheduler.dispatchedTitle')}</h3>
71
62
  ${managed.length === 0
72
63
  ? html`<div class="sch-empty">${t('profile.scheduler.noDispatched')}</div>`
73
- : html`<table class="sch-table"><thead><tr>
74
- <th>${t('profile.scheduler.col.name')}</th>
75
- <th>${t('profile.scheduler.col.kind')}</th>
76
- <th>${t('profile.scheduler.col.cron')}</th>
77
- <th>${t('profile.scheduler.col.lastRun')}</th>
78
- <th>${t('profile.scheduler.col.nextRun')}</th>
79
- <th></th>
80
- </tr></thead><tbody>
81
- ${managed.map(j => html`<tr key=${j.id}>
82
- <td><div class="sch-name">${j.displayName || j.name}</div>${j.createdByAgent && html`<span class="sch-tag">${t('profile.scheduler.byAgent')}</span>`}</td>
83
- <td><span class="sch-badge sch-badge--${j.type === 'ai' ? 'ai' : j.type === 'extension' ? 'ext' : 'agent'}">${t(KIND_LABEL[j.type] || KIND_LABEL.core)}</span></td>
84
- <td><code class="sch-cron">${j.cron}</code>${j.timezone && html`<div class="sch-muted">${j.timezone}</div>`}</td>
85
- <td>${j.lastRunAt ? timeAgo(j.lastRunAt) : html`<span class="sch-muted">${t('profile.scheduler.never')}</span>`} ${j.lastRunResult ? html`<span class="sch-badge ${j.lastRunResult === 'error' ? 'sch-badge--err' : 'sch-badge--ok'}">${j.lastRunResult}</span>` : ''}</td>
86
- <td>${j.nextRunAt ? timeAgo(j.nextRunAt) : html`<span class="sch-muted">—</span>`}</td>
87
- <td class="sch-actions">
88
- <button class="btn-ghost sch-btn" onClick=${() => onToggle(j)}>${j.enabled ? t('profile.scheduler.pause') : t('profile.scheduler.resume')}</button>
89
- <button class="btn-ghost sch-btn" onClick=${() => onTrigger(j)}>${t('profile.scheduler.runNow')}</button>
90
- <button class="btn-danger sch-btn" onClick=${() => onCancel(j)}>${t('profile.scheduler.cancel')}</button>
91
- </td>
92
- </tr>`)}
93
- </tbody></table>`}
64
+ : html`<div class="sch-card-list">${managed.map(j => html`<${ScheduleItem} key=${j.id} schedule=${j} onChanged=${loadData} showToast=${showToast} />`)}</div>`}
94
65
  </div>
95
66
 
96
67
  <div class="sch-section">
@@ -3,6 +3,8 @@
3
3
  * @description Tasks tab wrapper for agent detail tab-view.
4
4
  * Delegates to the existing AgentTasksSubtab component.
5
5
  * @version-history
6
+ * v1.2.0 -- 2026-06-03 -- Forward openTaskId + openTaskNonce so the fleet
7
+ * "running now" panel can deep-link straight to a specific task.
6
8
  * v1.1.0 -- 2026-06-01 -- Forward the `agent` object so the subtab can show the
7
9
  * max_concurrent_tasks runner config.
8
10
  * v1.0.0 -- 2026-05-24 -- Initial creation for Agent Detail Tab-View
@@ -14,6 +16,7 @@ import AgentTasksSubtab from '../agents-tasks-subtab.js';
14
16
 
15
17
  const html = htm.bind(h);
16
18
 
17
- export default function TabTasks({ agent, agentName, session, showToast }) {
18
- return html`<${AgentTasksSubtab} agent=${agent} agentName=${agentName} session=${session} showToast=${showToast} />`;
19
+ export default function TabTasks({ agent, agentName, session, showToast, openTaskId, openTaskNonce }) {
20
+ return html`<${AgentTasksSubtab} agent=${agent} agentName=${agentName} session=${session} showToast=${showToast}
21
+ openTaskId=${openTaskId} openTaskNonce=${openTaskNonce} />`;
19
22
  }
@@ -35,13 +35,18 @@
35
35
  * (connect command, MCP onboarding, manual agent prompt, task-runner prompt) with the
36
36
  * canonical <CopyButton> (className="copy-prompt-btn" preserves appearance); removed the
37
37
  * now-dead copiedAction state + markCopied helper.
38
+ * v3.6.0 -- 2026-06-03 -- Fleet "running now" panel between the agent board and
39
+ * the list: every agent's currently-active tasks in one list (reuses the
40
+ * active tasks already fetched in loadData, so no extra requests); clicking a
41
+ * row deep-links into that agent's Tasks tab and auto-opens the task
42
+ * (expandedAgent + deepLink → AgentCard preSelectedTab/openTaskId).
38
43
  */
39
44
  import { h } from 'preact';
40
45
  import { useState, useEffect, useRef } from 'preact/hooks';
41
46
  import htm from 'htm';
42
47
  const html = htm.bind(h);
43
48
  import { t } from '/js/i18n.js';
44
- import { escHtml } from '/js/utils.js';
49
+ import { escHtml, timeAgo } from '/js/utils.js';
45
50
  import { CopyButton } from '/components/CopyButton.js';
46
51
  import { Spinner } from './shared.js';
47
52
  import { apiGet, apiPost, apiPatch } from '/js/api.js';
@@ -449,6 +454,13 @@ export default function AgentsTab({ session, showToast, onStats }) {
449
454
  const [taskRunnerExpanded, setTaskRunnerExpanded] = useState(false);
450
455
  const [taskRunnerName, setTaskRunnerName] = useState('');
451
456
  const [taskStatsMap, setTaskStatsMap] = useState({});
457
+ // Currently-active tasks per agent { name: Task[] } — powers the fleet
458
+ // "running now" panel. Reuses the active list already fetched in loadData().
459
+ const [activeTasksMap, setActiveTasksMap] = useState({});
460
+ // Deep-link request from the running-now panel: { agent, taskId, nonce }.
461
+ // The nonce makes a repeat click re-fire the open even for the same task.
462
+ const [deepLink, setDeepLink] = useState(null);
463
+ const deepLinkNonce = useRef(0);
452
464
  // Per-agent unseen-change counts { name: { tasks, messages, memory } } driving
453
465
  // the collapsed mini-badge + per-tab number badges. Computed in loadData()
454
466
  // from the localStorage "last seen per tab" baseline (see loadSeen/countNewer).
@@ -534,6 +546,7 @@ export default function AgentsTab({ session, showToast, onStats }) {
534
546
  }));
535
547
  setOnboardings(obMap);
536
548
  const tsMap = {};
549
+ const atsMap = {};
537
550
  const seen = loadSeen(session.owner);
538
551
  let seenSeeded = false;
539
552
  const chMap = {};
@@ -550,6 +563,7 @@ export default function AgentsTab({ session, showToast, onStats }) {
550
563
  const activeTasks = activeResp?.data?.tasks || [];
551
564
  const doneToday = doneTasks.filter(tk => tk.completedAt?.startsWith(today)).length;
552
565
  tsMap[a.name] = { done: doneToday, active: activeTasks.length };
566
+ atsMap[a.name] = activeTasks;
553
567
 
554
568
  // Per-tab unseen-change counts. First observation of a tab seeds its
555
569
  // baseline to "now" (count 0) so an agent's whole history is never
@@ -568,9 +582,10 @@ export default function AgentsTab({ session, showToast, onStats }) {
568
582
  else ch[tab] = countNewer(items, agentSeen[tab], fields);
569
583
  }
570
584
  chMap[a.name] = ch;
571
- } catch { tsMap[a.name] = null; chMap[a.name] = null; }
585
+ } catch { tsMap[a.name] = null; chMap[a.name] = null; atsMap[a.name] = []; }
572
586
  }));
573
587
  setTaskStatsMap(tsMap);
588
+ setActiveTasksMap(atsMap);
574
589
  // Persist any freshly-seeded baselines. Merge against the latest storage
575
590
  // so a tab the user opened mid-fetch (markTabSeen) is not overwritten.
576
591
  if (seenSeeded) {
@@ -604,6 +619,19 @@ export default function AgentsTab({ session, showToast, onStats }) {
604
619
  setExpandedAgent(prev => prev === name ? null : name);
605
620
  }
606
621
 
622
+ // Running-now panel → open a specific agent's Tasks tab on a specific task.
623
+ // Expand the agent, record the deep-link (nonce bump re-fires repeat clicks),
624
+ // and scroll the agent's card into view.
625
+ function openAgentTask(name, taskId) {
626
+ setExpandedAgent(name);
627
+ deepLinkNonce.current += 1;
628
+ setDeepLink({ agent: name, taskId, nonce: deepLinkNonce.current });
629
+ requestAnimationFrame(() => {
630
+ const el = document.querySelector(`[data-agent-name="${name}"]`);
631
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
632
+ });
633
+ }
634
+
607
635
  async function handleSaveScopes(agentName, newScopes) {
608
636
  try {
609
637
  const resp = await updateAgentScopes(agentName, newScopes);
@@ -846,6 +874,11 @@ export default function AgentsTab({ session, showToast, onStats }) {
846
874
  });
847
875
  }}
848
876
  />
877
+ <${ActiveTasksPanel}
878
+ activeTasksMap=${activeTasksMap}
879
+ agents=${agents}
880
+ onOpen=${openAgentTask}
881
+ />
849
882
  ${renderFilterBar(agents, tagFilter, setTagFilter, groupBy, setGroupBy)}
850
883
  ${renderAgentGroups({
851
884
  agents,
@@ -865,6 +898,7 @@ export default function AgentsTab({ session, showToast, onStats }) {
865
898
  onPopOut: popOutAgent,
866
899
  dnd,
867
900
  agentOrder,
901
+ deepLink,
868
902
  })}
869
903
  `
870
904
  }
@@ -930,7 +964,53 @@ function renderFilterBar(agents, tagFilter, setTagFilter, groupBy, setGroupBy) {
930
964
  `;
931
965
  }
932
966
 
933
- function renderAgentGroups({ agents, tagFilter, groupBy, onboardings, taskStatsMap, changesMap, onTabSeen, expandedAgent, toggleAgent, session, showToast, setScopesModal, handleDeleteAgent, toggleFederate, onPopOut, dnd, agentOrder }) {
967
+ // Fleet-wide "running now" panel shown between the agent board and the list.
968
+ // Flattens every agent's currently-active tasks into one newest-first list.
969
+ // Clicking a row asks the parent (onOpen) to expand that agent and open the
970
+ // task. Reuses the active tasks already fetched in loadData() — no extra calls.
971
+ function ActiveTasksPanel({ activeTasksMap, agents, onOpen }) {
972
+ const nameToDisplay = new Map((agents || []).map(a => [a.name, a.display_name || a.name]));
973
+ const rows = [];
974
+ for (const [name, tasks] of Object.entries(activeTasksMap || {})) {
975
+ for (const task of (tasks || [])) {
976
+ rows.push({ agentName: name, agentDisplay: nameToDisplay.get(name) || name, task });
977
+ }
978
+ }
979
+ rows.sort((a, b) =>
980
+ String(b.task.updatedAt || b.task.createdAt || '').localeCompare(
981
+ String(a.task.updatedAt || a.task.createdAt || '')));
982
+
983
+ // Cap the rendered rows so a very busy fleet can't produce an enormous list;
984
+ // the overflow is surfaced explicitly (never silently dropped).
985
+ const CAP = 50;
986
+ const shown = rows.slice(0, CAP);
987
+ const overflow = rows.length - shown.length;
988
+
989
+ return html`
990
+ <div class="pf-agd-active">
991
+ <div class="pf-agd-active-head">
992
+ <span class="section-title">${t('profile.agents.active.title')}${rows.length > 0 ? html` <span class="pf-agd-count-badge">(${rows.length})</span>` : ''}</span>
993
+ </div>
994
+ ${rows.length === 0
995
+ ? html`<div class="pf-agd-active-empty">${t('profile.agents.active.empty')}</div>`
996
+ : html`<div class="pf-agd-active-list">
997
+ ${shown.map(r => html`
998
+ <button class="pf-agd-active-row" key=${r.task.id}
999
+ onClick=${() => onOpen(r.agentName, r.task.id)}
1000
+ title=${t('profile.agents.active.openHint')}>
1001
+ <span class="pf-agd-active-dot" aria-hidden="true"></span>
1002
+ <span class="pf-agd-active-agent">${r.agentDisplay}</span>
1003
+ <span class="pf-agd-active-title">${r.task.title || t('profile.agents.active.untitled')}</span>
1004
+ <span class="pf-agd-active-time">${timeAgo(r.task.updatedAt || r.task.createdAt)}</span>
1005
+ </button>
1006
+ `)}
1007
+ ${overflow > 0 && html`<div class="pf-agd-active-empty">+ ${overflow} ${t('profile.agents.active.more')}</div>`}
1008
+ </div>`}
1009
+ </div>
1010
+ `;
1011
+ }
1012
+
1013
+ function renderAgentGroups({ agents, tagFilter, groupBy, onboardings, taskStatsMap, changesMap, onTabSeen, expandedAgent, toggleAgent, session, showToast, setScopesModal, handleDeleteAgent, toggleFederate, onPopOut, dnd, agentOrder, deepLink }) {
934
1014
  // Tag filter: agent must have ALL selected tags (intersection)
935
1015
  const filtered = tagFilter.size === 0
936
1016
  ? agents
@@ -959,6 +1039,9 @@ function renderAgentGroups({ agents, tagFilter, groupBy, onboardings, taskStatsM
959
1039
  onDeleteClick=${handleDeleteAgent}
960
1040
  onFederateToggle=${toggleFederate}
961
1041
  onPopOut=${onPopOut}
1042
+ preSelectedTab=${deepLink?.agent === a.name ? 'tasks' : null}
1043
+ openTaskId=${deepLink?.agent === a.name ? deepLink.taskId : null}
1044
+ openTaskNonce=${deepLink?.agent === a.name ? deepLink.nonce : 0}
962
1045
  />
963
1046
  `;
964
1047
 
@@ -9,6 +9,18 @@
9
9
  * - TaskItem -- task row with expand/collapse, todo list, start button
10
10
  * - RequestChangesModal -- inline modal for owner to send a free-text change request
11
11
  * @version-history
12
+ * v4.10.0 -- 2026-06-03 -- Deep-link support: AgentTasksSubtab accepts
13
+ * openTaskId + openTaskNonce; a nonce bump resets the bucket to 'recent'
14
+ * (clears search/time) so the target task is visible, and TaskItem auto-opens
15
+ * (expand + fetch events + scroll into view). Lets the fleet "running now"
16
+ * panel jump straight to a specific task.
17
+ * v4.9.1 -- 2026-06-03 -- Fix: task Scope row rendered structured provenance
18
+ * entries (e.g. the scheduler's { name, value, type, description }) as
19
+ * "[object Object]" via a naive join(). Add formatScopeEntry() and render
20
+ * each entry on its own line as "name: value — description".
21
+ * v4.9.0 -- 2026-06-03 -- Render a task's description as Markdown via the shared
22
+ * safe Markdown component (headings, lists, code, line breaks) instead of a
23
+ * single collapsed text blob, so long agent-authored prompts are readable.
12
24
  * v4.8.3 -- 2026-06-02 -- Render non-JSON memory values (e.g. an agent's
13
25
  * latest_output) as Markdown via the shared safe Markdown component, instead
14
26
  * of raw monospace text.
@@ -199,6 +211,23 @@ function todoStatusIcon(status) {
199
211
  return '⬜';
200
212
  }
201
213
 
214
+ // Render one task scope entry as readable text. Scope is an array whose entries
215
+ // may be plain strings (legacy free-text scopes) or structured provenance objects
216
+ // stamped by the scheduler, e.g.
217
+ // { name:'schedule', value:'0 9 * * *', type:'cron', description:'Uutisputki – aamukirjoitus' }
218
+ // A naive join()/String() prints "[object Object]" for the structured form, so
219
+ // format the parts we know into e.g. "schedule: 0 9 * * * — Uutisputki – aamukirjoitus".
220
+ function formatScopeEntry(s) {
221
+ if (s == null) return '';
222
+ if (typeof s !== 'object') return String(s);
223
+ const head = s.name || s.type || '';
224
+ const val = s.value != null && s.value !== '' ? String(s.value) : '';
225
+ const lead = [head, val].filter(Boolean).join(': ');
226
+ const desc = s.description ? ` — ${s.description}` : '';
227
+ const out = `${lead}${desc}`.trim();
228
+ return out || JSON.stringify(s);
229
+ }
230
+
202
231
  function todoProgress(todos) {
203
232
  if (!todos || todos.length === 0) return null;
204
233
  const active = todos.filter(td => td.status !== 'outdated');
@@ -305,9 +334,13 @@ function TaskMemoryEntry({ entry }) {
305
334
  `;
306
335
  }
307
336
 
308
- function TaskItem({ task, agentName, showToast, onRefresh }) {
337
+ function TaskItem({ task, agentName, showToast, onRefresh, autoOpen = 0 }) {
309
338
  const [expanded, setExpanded] = useState(false);
310
339
  const [events, setEvents] = useState(null);
340
+ // Deep-link target: the parent bumps `autoOpen` (a nonce) when this exact task
341
+ // should be opened. taskRef scrolls it into view.
342
+ const taskRef = useRef(null);
343
+ const autoOpenedNonce = useRef(0);
311
344
  const [loadingEvents, setLoadingEvents] = useState(false);
312
345
  const [starting, setStarting] = useState(false);
313
346
  const [showRevisionModal, setShowRevisionModal] = useState(false);
@@ -418,6 +451,21 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
418
451
  return () => window.removeEventListener('aimeat-live-update', handler);
419
452
  }, [expanded, task.id, agentName]);
420
453
 
454
+ // Deep-link from the fleet "running now" panel: when autoOpen bumps to a new
455
+ // nonce for this task, expand it, load its events, and scroll it into view.
456
+ useEffect(() => {
457
+ if (!autoOpen || autoOpen === autoOpenedNonce.current) return;
458
+ autoOpenedNonce.current = autoOpen;
459
+ setExpanded(true);
460
+ // Always refresh the event log on a fresh deep-link open (don't gate on the
461
+ // possibly-stale `events` closure — the task's log may have moved on since a
462
+ // previous open).
463
+ fetchEvents();
464
+ requestAnimationFrame(() => {
465
+ taskRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
466
+ });
467
+ }, [autoOpen]);
468
+
421
469
  async function handleStart(e) {
422
470
  e.stopPropagation();
423
471
  setStarting(true);
@@ -548,7 +596,7 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
548
596
  }
549
597
 
550
598
  return html`
551
- <div>
599
+ <div ref=${taskRef}>
552
600
  <div class="pf-agd-task-item" onClick=${handleExpand}>
553
601
  <div class="pf-agd-task-title-row">
554
602
  <button
@@ -567,7 +615,7 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
567
615
  </div>
568
616
  ${expanded && html`
569
617
  <div class="pf-agd-task-expanded">
570
- ${task.description && html`<div class="pf-agd-task-desc">${task.description}</div>`}
618
+ ${task.description && html`<div class="pf-agd-task-desc"><${Markdown} text=${task.description} /></div>`}
571
619
 
572
620
  ${task.deliverableKey && html`
573
621
  <div class="pf-agd-deliverable">
@@ -616,10 +664,14 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
616
664
  </div>
617
665
  `}
618
666
 
619
- ${task.scope && html`
667
+ ${task.scope && (!Array.isArray(task.scope) || task.scope.length > 0) && html`
620
668
  <div class="pf-agd-info-row">
621
669
  <span class="pf-agd-info-label">${t('profile.agents.detail.tasks.scope')}</span>
622
- <span class="pf-agd-info-value">${Array.isArray(task.scope) ? task.scope.join(', ') : task.scope}</span>
670
+ <span class="pf-agd-info-value">
671
+ ${Array.isArray(task.scope)
672
+ ? task.scope.map((s, i) => html`<div key=${i}>${formatScopeEntry(s)}</div>`)
673
+ : formatScopeEntry(task.scope)}
674
+ </span>
623
675
  </div>
624
676
  `}
625
677
  ${task.rules && task.rules.length > 0 && html`
@@ -769,7 +821,7 @@ function TaskItem({ task, agentName, showToast, onRefresh }) {
769
821
  `;
770
822
  }
771
823
 
772
- export default function AgentTasksSubtab({ agent, agentName, showToast }) {
824
+ export default function AgentTasksSubtab({ agent, agentName, showToast, openTaskId = null, openTaskNonce = 0 }) {
773
825
  const [tasks, setTasks] = useState(null);
774
826
  const [counts, setCounts] = useState({ recent: 0, keep: 0, archive: 0 });
775
827
  const [bucket, setBucket] = useState('recent');
@@ -828,6 +880,17 @@ export default function AgentTasksSubtab({ agent, agentName, showToast }) {
828
880
  return () => window.removeEventListener('aimeat-live-update', handler);
829
881
  }, []);
830
882
 
883
+ // Deep-link (from the fleet "running now" panel): a target task may live in a
884
+ // bucket/filter that isn't currently shown. Reset to the default Recent view
885
+ // (where active tasks always appear) so the target TaskItem renders and can
886
+ // auto-open. Keyed on the nonce so repeat clicks re-trigger.
887
+ useEffect(() => {
888
+ if (!openTaskId || !openTaskNonce) return;
889
+ setBucket('recent');
890
+ setQ('');
891
+ setTimeChip('all');
892
+ }, [openTaskId, openTaskNonce]);
893
+
831
894
  function handleCreated() {
832
895
  setShowCreate(false);
833
896
  loadTasks();
@@ -906,7 +969,8 @@ export default function AgentTasksSubtab({ agent, agentName, showToast }) {
906
969
  `}
907
970
 
908
971
  ${tasks.length > 0 ? tasks.map(task => html`
909
- <${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} onRefresh=${loadTasks} />
972
+ <${TaskItem} key=${task.id} task=${task} agentName=${agentName} showToast=${showToast} onRefresh=${loadTasks}
973
+ autoOpen=${task.id === openTaskId ? openTaskNonce : 0} />
910
974
  `) : html`
911
975
  <div class="pf-agd-empty">${q ? t('profile.agents.tasks.search.noResults') : t(`profile.agents.tasks.bucket.empty.${bucket}`)}</div>
912
976
  `}