aimeat 1.14.6 → 1.15.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 (39) hide show
  1. package/dist/locales/en.json +18 -1
  2. package/dist/locales/fi.json +18 -1
  3. package/dist/public/css/views/admin.css +12 -0
  4. package/dist/public/css/views/agents-detail.css +164 -2
  5. package/dist/public/spa.html +4 -0
  6. package/dist/public/views/admin/config-tab.js +34 -6
  7. package/dist/public/views/agent-solo.js +123 -0
  8. package/dist/public/views/profile/agents/agent-card.js +37 -12
  9. package/dist/public/views/profile/agents/tab-data-access.js +34 -3
  10. package/dist/public/views/profile/agents/tab-directives.js +25 -15
  11. package/dist/public/views/profile/agents-tab.js +105 -16
  12. package/dist/public/views/profile/agents-tasks-subtab.js +43 -0
  13. package/dist/src/routes/agent-directives.d.ts +5 -0
  14. package/dist/src/routes/agent-directives.d.ts.map +1 -1
  15. package/dist/src/routes/agent-directives.js +35 -16
  16. package/dist/src/routes/agent-directives.js.map +1 -1
  17. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  18. package/dist/src/routes/agent-tasks.js +7 -0
  19. package/dist/src/routes/agent-tasks.js.map +1 -1
  20. package/dist/src/routes/sse.d.ts +13 -0
  21. package/dist/src/routes/sse.d.ts.map +1 -1
  22. package/dist/src/routes/sse.js +20 -0
  23. package/dist/src/routes/sse.js.map +1 -1
  24. package/dist/src/server.d.ts.map +1 -1
  25. package/dist/src/server.js +14 -2
  26. package/dist/src/server.js.map +1 -1
  27. package/dist/src/storage/interface.d.ts +1 -0
  28. package/dist/src/storage/interface.d.ts.map +1 -1
  29. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  30. package/dist/src/storage/providers/mongodb/index.js +3 -0
  31. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  32. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  33. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  34. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  35. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  36. package/dist/src/storage/providers/sqlite/schema.js +4 -1
  37. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  38. package/package.json +1 -1
  39. package/prisma/schema.prisma +1 -0
@@ -394,6 +394,11 @@
394
394
  "untagged": "Untagged",
395
395
  "noMatches": "No agents match the current filter."
396
396
  },
397
+ "reorderHint": "Drag to reorder",
398
+ "solo": {
399
+ "notFound": "Agent not found.",
400
+ "loginRequired": "Please log in to view this agent."
401
+ },
397
402
  "approval": {
398
403
  "credentials_ready_title": "Agent approved",
399
404
  "credentials_ready_desc": "Copy the credentials below and paste them to your agent.",
@@ -450,6 +455,9 @@
450
455
  "starting": "Starting...",
451
456
  "startThisTask": "Start this task",
452
457
  "noEventsRecorded": "No events recorded",
458
+ "deliverable": "Deliverable",
459
+ "viewDeliverable": "View",
460
+ "deliverableGone": "The deliverable entry no longer exists.",
453
461
  "revision_requested": "Revision requested",
454
462
  "requestChanges": "Request changes",
455
463
  "requestChangesTitle": "Ask the agent to revise the plan",
@@ -655,6 +663,7 @@
655
663
  },
656
664
  "sharedTags": "Shared tags",
657
665
  "lastSeen": "Last seen",
666
+ "popOut": "Open in its own window",
658
667
  "deliveryMcp": "MCP notifications",
659
668
  "deliveryWebhook": "Webhook",
660
669
  "deliveryPolling": "Polling",
@@ -798,7 +807,14 @@
798
807
  "keyValuePlaceholder": "Value — plain text or JSON",
799
808
  "noKeys": "No memory keys stored yet",
800
809
  "keyCreated": "Memory key created",
801
- "createKeyError": "Failed to create memory key"
810
+ "createKeyError": "Failed to create memory key",
811
+ "created": "Created",
812
+ "updated": "Updated",
813
+ "sortBy": "Sort",
814
+ "sortUpdated": "Last updated",
815
+ "sortCreated": "Created",
816
+ "sortNewestFirst": "Newest first",
817
+ "sortOldestFirst": "Oldest first"
802
818
  },
803
819
  "agent_config": {
804
820
  "copy": "Copy",
@@ -3138,6 +3154,7 @@
3138
3154
  "cfgReadOnlyHelpDetail": "Config persistence requires a database (MongoDB or SQLite). In-memory nodes lose all changes on restart. Configure this node via .env file, aimeat.ini, aimeat.json, or CLI arguments (aimeat start --db sqlite).",
3139
3155
  "cfgHelpTitle": "Help",
3140
3156
  "cfgReset": "Reset",
3157
+ "cfgOnePerLine": "One entry per line",
3141
3158
  "cfgResetDone": "Reset {path} — DB override removed.",
3142
3159
  "cfgSource": "Source",
3143
3160
  "consul": "Consul",
@@ -394,6 +394,11 @@
394
394
  "untagged": "Ei tageja",
395
395
  "noMatches": "Yksikään agentti ei vastaa nykyistä suodatinta."
396
396
  },
397
+ "reorderHint": "Raahaa järjestääksesi",
398
+ "solo": {
399
+ "notFound": "Agenttia ei löytynyt.",
400
+ "loginRequired": "Kirjaudu sisään nähdäksesi tämän agentin."
401
+ },
397
402
  "approval": {
398
403
  "credentials_ready_title": "Agentti hyväksytty",
399
404
  "credentials_ready_desc": "Kopioi alla olevat tunnistetiedot ja liitä ne agentillesi.",
@@ -450,6 +455,9 @@
450
455
  "starting": "Käynnistetään...",
451
456
  "startThisTask": "Käynnistä tehtävä",
452
457
  "noEventsRecorded": "Ei tallennettuja tapahtumia",
458
+ "deliverable": "Tuotos",
459
+ "viewDeliverable": "Näytä",
460
+ "deliverableGone": "Tuotosmerkintää ei ole enää olemassa.",
453
461
  "revision_requested": "Korjauspyyntö lähetetty",
454
462
  "requestChanges": "Pyydä muutoksia",
455
463
  "requestChangesTitle": "Pyydä agenttia muokkaamaan suunnitelmaa",
@@ -655,6 +663,7 @@
655
663
  },
656
664
  "sharedTags": "Jaetut tunnisteet",
657
665
  "lastSeen": "Viimeksi nähty",
666
+ "popOut": "Avaa omaan ikkunaan",
658
667
  "deliveryMcp": "MCP-ilmoitukset",
659
668
  "deliveryWebhook": "Webhook",
660
669
  "deliveryPolling": "Pollaus",
@@ -798,7 +807,14 @@
798
807
  "keyValuePlaceholder": "Arvo — pelkkä teksti tai JSON",
799
808
  "noKeys": "Ei tallennettuja muistiavaimia vielä",
800
809
  "keyCreated": "Muistiavain luotu",
801
- "createKeyError": "Muistiavaimen luonti epäonnistui"
810
+ "createKeyError": "Muistiavaimen luonti epäonnistui",
811
+ "created": "Luotu",
812
+ "updated": "Päivitetty",
813
+ "sortBy": "Järjestä",
814
+ "sortUpdated": "Viimeksi päivitetty",
815
+ "sortCreated": "Luotu",
816
+ "sortNewestFirst": "Uusin ensin",
817
+ "sortOldestFirst": "Vanhin ensin"
802
818
  },
803
819
  "agent_config": {
804
820
  "copy": "Kopioi",
@@ -3138,6 +3154,7 @@
3138
3154
  "cfgReadOnlyHelpDetail": "Asetusten tallennus vaatii tietokannan (MongoDB tai SQLite). Muistinsisäiset solmut menettävät kaikki muutokset uudelleenkäynnistyksessä. Määritä tämä solmu .env-tiedoston, aimeat.ini:n, aimeat.json:n tai CLI-argumenttien kautta.",
3139
3155
  "cfgHelpTitle": "Ohje",
3140
3156
  "cfgReset": "Palauta",
3157
+ "cfgOnePerLine": "Yksi kohta per rivi",
3141
3158
  "cfgResetDone": "Palautettu {path} — tietokantaohitus poistettu.",
3142
3159
  "cfgSource": "Lähde",
3143
3160
  "consul": "Consul",
@@ -333,6 +333,18 @@
333
333
  border: 1px solid #FDE68A;
334
334
  border-radius: 8px;
335
335
  }
336
+ .adm-config-array-edit {
337
+ width: 360px;
338
+ max-width: 100%;
339
+ min-height: 4.5rem;
340
+ font-family: 'JetBrains Mono', monospace;
341
+ font-size: 0.8rem;
342
+ padding: 6px 8px;
343
+ border: 1px solid var(--border, #E5E7EB);
344
+ border-radius: 6px;
345
+ resize: vertical;
346
+ box-sizing: border-box;
347
+ }
336
348
  .adm-config-readonly { opacity: 0.85; pointer-events: none; }
337
349
  .adm-config-readonly input,
338
350
  .adm-config-readonly select { background: #F3F4F6; cursor: not-allowed; }
@@ -19,6 +19,12 @@
19
19
  * v2.4.0 -- 2026-05-24 -- Add header-row flex layout for title + connect button alignment
20
20
  * v2.5.0 -- 2026-05-31 -- Add stored-memory-key inline editor (textarea), memory-detail wrapper, in-row edit/delete action rows, and danger-tinted ghost action styles for the Data Access tab
21
21
  * v2.6.0 -- 2026-05-31 -- Add stored-key create form layout (pf-agd-keyadd-form, vertical stack)
22
+ * v2.7.0 -- 2026-05-31 -- Add stored-key sort controls (pf-agd-keys-controls/sort-*) and per-key created/updated meta (pf-agd-key-meta)
23
+ * v2.8.0 -- 2026-05-31 -- Add pop-out button (pf-agd-popout-btn), drag-to-reorder rows (pf-agd-dnd-*), and solo pop-out window layout (pf-agd-solo*)
24
+ * v2.9.0 -- 2026-05-31 -- Collapsed bar two-row layout (pf-agd-collapsed-main + pf-agd-collapsed-meta divider) to stop status overflow; move drag grip into the left margin so the card border stays flush
25
+ * v2.10.0 -- 2026-05-31 -- Extend the drag-row drop target left over the grip gutter (padding-left + negative margin-left) so dropping on/near the handle registers; card border stays aligned
26
+ * v2.11.0 -- 2026-05-31 -- Widen the solo pop-out window (and pf-agd-solo max-width) so the agent card matches the list-view width and the full tab bar fits
27
+ * v2.12.0 -- 2026-05-31 -- Add completed-task deliverable styles (pf-agd-deliverable*) linking a task to its agent-memory output key
22
28
  */
23
29
 
24
30
  /* Header row: title left, connect button right */
@@ -1262,8 +1268,9 @@
1262
1268
  }
1263
1269
  .pf .pf-agd-collapsed {
1264
1270
  display: flex;
1265
- align-items: center;
1266
- gap: 0.5rem;
1271
+ flex-direction: column;
1272
+ align-items: stretch;
1273
+ gap: 0;
1267
1274
  padding: 0.625rem 0.875rem;
1268
1275
  background: var(--card-bg, #FFFFFF);
1269
1276
  border: 1px solid var(--border, #E5E7EB);
@@ -1277,6 +1284,21 @@
1277
1284
  .pf .pf-agd-collapsed--problem {
1278
1285
  border-color: var(--danger, #ef4444);
1279
1286
  }
1287
+ /* Top row: expand arrow + icon + name + badges + pop-out */
1288
+ .pf .pf-agd-collapsed-main {
1289
+ display: flex;
1290
+ align-items: center;
1291
+ gap: 0.5rem;
1292
+ }
1293
+ /* Second row: delivery + status + last seen, smaller, faint divider above */
1294
+ .pf .pf-agd-collapsed-meta {
1295
+ margin-top: 0.4rem;
1296
+ padding-top: 0.35rem;
1297
+ border-top: 1px solid var(--border-subtle, #F3F4F6);
1298
+ font-size: 0.7rem;
1299
+ color: var(--text-muted, #9CA3AF);
1300
+ line-height: 1.4;
1301
+ }
1280
1302
  .pf .pf-agd-expand-icon {
1281
1303
  font-size: 0.7rem;
1282
1304
  color: var(--text-dim, #6B7280);
@@ -1313,6 +1335,74 @@
1313
1335
  text-align: right;
1314
1336
  }
1315
1337
 
1338
+ /* Pop-out button (open agent in its own window) */
1339
+ .pf .pf-agd-popout-btn {
1340
+ background: none;
1341
+ border: none;
1342
+ cursor: pointer;
1343
+ color: var(--text-dim, #6B7280);
1344
+ font-size: 0.95rem;
1345
+ line-height: 1;
1346
+ padding: 0.1rem 0.35rem;
1347
+ border-radius: 4px;
1348
+ flex-shrink: 0;
1349
+ }
1350
+ .pf .pf-agd-collapsed-main .pf-agd-popout-btn { margin-left: auto; }
1351
+ .pf .pf-agd-popout-btn:hover {
1352
+ color: var(--accent, #E8564A);
1353
+ background: var(--bg-dim, #F9FAFB);
1354
+ }
1355
+
1356
+ /* ── Drag-to-reorder rows (ungrouped, unfiltered agent list) ── */
1357
+ /* The row's box (= the drop target) is extended LEFT to cover the grip handle
1358
+ via padding-left, while a matching negative margin-left pulls the box back so
1359
+ the card border stays flush with the rest of the list. Net effect: the card
1360
+ stays aligned with the dividers above, the grip sits in the left gutter, and
1361
+ dropping anywhere over that gutter still drops onto the row (people grab the
1362
+ handle and drop near it). */
1363
+ .pf .pf-agd-dnd-row {
1364
+ position: relative;
1365
+ padding-left: 1.5rem;
1366
+ margin-left: -1.5rem;
1367
+ }
1368
+ .pf .pf-agd-dnd-grip {
1369
+ position: absolute;
1370
+ left: 0.2rem;
1371
+ top: 1.1rem;
1372
+ z-index: 2;
1373
+ cursor: grab;
1374
+ color: var(--text-muted, #9CA3AF);
1375
+ font-size: 0.85rem;
1376
+ line-height: 1;
1377
+ opacity: 0;
1378
+ transition: opacity 0.15s;
1379
+ user-select: none;
1380
+ }
1381
+ .pf .pf-agd-dnd-row:hover .pf-agd-dnd-grip { opacity: 0.7; }
1382
+ .pf .pf-agd-dnd-grip:active { cursor: grabbing; }
1383
+ .pf .pf-agd-dnd-dragging { opacity: 0.45; }
1384
+
1385
+ /* ── Solo (pop-out) window layout ── */
1386
+ .pf-agd-solo {
1387
+ padding: 0.5rem 1rem;
1388
+ max-width: 860px; /* card gets ≈828px — same usable width as the list view */
1389
+ margin: 0 auto;
1390
+ }
1391
+ .pf .pf-agd-solo-toast {
1392
+ position: fixed;
1393
+ bottom: 1rem;
1394
+ left: 50%;
1395
+ transform: translateX(-50%);
1396
+ background: var(--text, #1A1A2E);
1397
+ color: #FFFFFF;
1398
+ padding: 0.5rem 0.9rem;
1399
+ border-radius: 6px;
1400
+ font-size: 0.85rem;
1401
+ z-index: 1000;
1402
+ box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.18));
1403
+ }
1404
+ .pf .pf-agd-solo-toast--err { background: var(--danger, #EF4444); }
1405
+
1316
1406
  /* ── Badges (platform, readiness, federation) ── */
1317
1407
 
1318
1408
  .pf .pf-agd-badge {
@@ -2001,6 +2091,34 @@
2001
2091
  margin-top: 0.4rem;
2002
2092
  }
2003
2093
 
2094
+ /* Completed-task deliverable link (task → agent-memory key) */
2095
+ .pf .pf-agd-deliverable {
2096
+ display: flex;
2097
+ align-items: center;
2098
+ gap: 0.5rem;
2099
+ flex-wrap: wrap;
2100
+ margin: 0.4rem 0;
2101
+ }
2102
+ .pf .pf-agd-deliverable-label {
2103
+ font-size: 0.78rem;
2104
+ font-weight: 600;
2105
+ color: var(--text-dim, #6B7280);
2106
+ }
2107
+ .pf .pf-agd-deliverable-key {
2108
+ font-family: 'JetBrains Mono', monospace;
2109
+ font-size: 0.75rem;
2110
+ background: var(--code-bg, #F3F4F6);
2111
+ padding: 1px 6px;
2112
+ border-radius: 3px;
2113
+ word-break: break-all;
2114
+ }
2115
+ .pf .pf-agd-deliverable-gone {
2116
+ font-size: 0.78rem;
2117
+ color: var(--warn, #F59E0B);
2118
+ font-style: italic;
2119
+ margin: 0.25rem 0 0.5rem;
2120
+ }
2121
+
2004
2122
  /* Danger-tinted ghost button used for in-row delete actions */
2005
2123
  .pf .pf-agd-action-danger {
2006
2124
  color: var(--danger, #EF4444);
@@ -2287,6 +2405,50 @@
2287
2405
  .pf .pf-agd-keyadd-form select { width: 100%; box-sizing: border-box; }
2288
2406
  .pf .pf-agd-keyadd-form .pf-agd-memory-edit { margin: 0; }
2289
2407
 
2408
+ /* Data access: stored-key sort controls (in the section header) */
2409
+ .pf .pf-agd-keys-controls {
2410
+ display: flex;
2411
+ align-items: center;
2412
+ gap: 0.4rem;
2413
+ flex-wrap: wrap;
2414
+ }
2415
+ .pf .pf-agd-sort-label {
2416
+ font-size: 0.72rem;
2417
+ color: var(--text-dim, #6B7280);
2418
+ }
2419
+ .pf .pf-agd-sort-select {
2420
+ padding: 0.2rem 0.4rem;
2421
+ border: 1px solid var(--border, #E5E7EB);
2422
+ border-radius: 4px;
2423
+ background: var(--card-bg, #FFFFFF);
2424
+ color: var(--text, #1A1A2E);
2425
+ font-size: 0.78rem;
2426
+ }
2427
+ .pf .pf-agd-sort-dir {
2428
+ border: 1px solid var(--border, #E5E7EB);
2429
+ border-radius: 4px;
2430
+ background: var(--card-bg, #FFFFFF);
2431
+ color: var(--text-dim, #6B7280);
2432
+ cursor: pointer;
2433
+ font-size: 0.85rem;
2434
+ line-height: 1;
2435
+ padding: 0.2rem 0.45rem;
2436
+ }
2437
+ .pf .pf-agd-sort-dir:hover {
2438
+ border-color: var(--accent, #E8564A);
2439
+ color: var(--accent, #E8564A);
2440
+ }
2441
+
2442
+ /* Data access: per-key created/updated meta in the stored-key row */
2443
+ .pf .pf-agd-key-meta {
2444
+ margin-left: auto;
2445
+ display: flex;
2446
+ gap: 0.75rem;
2447
+ font-size: 0.7rem;
2448
+ color: var(--text-muted, #9CA3AF);
2449
+ white-space: nowrap;
2450
+ }
2451
+
2290
2452
  /* Data access: package picker */
2291
2453
  .pf .pf-agd-package-picker {
2292
2454
  margin-bottom: 0.5rem;
@@ -238,6 +238,10 @@
238
238
  if (pathname === '/v1/portal' && search.includes('view=dev')) {
239
239
  return () => import('/views/portal-dev.js' + B);
240
240
  }
241
+ // Single-agent pop-out window: /v1/profile?solo=<agentName>
242
+ if (pathname === '/v1/profile' && /[?&]solo=/.test(search)) {
243
+ return () => import('/views/agent-solo.js' + B);
244
+ }
241
245
  // Portfolio public view: /v1/portfolio/<username>
242
246
  if (pathname.startsWith('/v1/portfolio/') && pathname !== '/v1/portfolio/') {
243
247
  return () => import('/views/portfolio.js' + B);
@@ -1,3 +1,16 @@
1
+ /**
2
+ * @file config-tab.js
3
+ * @description Admin dashboard Config tab — renders the mutable node config
4
+ * schema (GET /v1/admin/config) with per-type editors and persists changes
5
+ * via PUT /v1/admin/config.
6
+ * @version-history
7
+ * v1.1.0 -- 2026-05-31 -- Add an editor for array-of-strings config fields
8
+ * (e.g. agent.system_principles): one item per line in a textarea, split
9
+ * back to an array on save. Previously object/array fields were read-only.
10
+ * v1.2.0 -- 2026-05-31 -- Fix: inputs were bound to the saved value, not the
11
+ * pending edit, so toggling a checkbox / typing snapped straight back on
12
+ * re-render (looked un-editable). All inputs now reflect pending[path].
13
+ */
1
14
  import { h } from 'preact';
2
15
  import { useState } from 'preact/hooks';
3
16
  import htm from 'htm';
@@ -51,7 +64,16 @@ export default function ConfigTab({ data, reload }) {
51
64
  }
52
65
 
53
66
  async function save() {
54
- const changes = Object.entries(pending).map(([path, value]) => ({ path, value }));
67
+ const changes = Object.entries(pending).map(([path, value]) => {
68
+ const entry = schema[path];
69
+ // Array-of-strings fields (e.g. agent.system_principles) are edited as
70
+ // one item per line; split the raw text back into an array on save so it
71
+ // matches what the API validator expects.
72
+ if (entry && entry.type === 'object' && Array.isArray(entry.value) && typeof value === 'string') {
73
+ return { path, value: value.split('\n').map(l => l.trim()).filter(Boolean) };
74
+ }
75
+ return { path, value };
76
+ });
55
77
  if (!changes.length) return;
56
78
  try {
57
79
  const r = await saveConfig(changes);
@@ -129,15 +151,21 @@ export default function ConfigTab({ data, reload }) {
129
151
  ? html`${e.value ? html`<${Badge} type="healthy" /> ${t('dashboard.yesLabel')}` : html`<${Badge} type="critical" /> ${t('dashboard.noLabel')}`}`
130
152
  : html`<code>${escHtml(String(e.value))}</code> <span class="adm-text-dim adm-text-xs">${t('dashboard.readOnly')}</span>`)
131
153
  : e.type === 'boolean'
132
- ? html`<label style="cursor:pointer"><input type="checkbox" checked=${e.value} onChange=${ev => onChange(p, ev.target.checked)} disabled=${!editable} /> ${e.value ? t('dashboard.enabled') : t('dashboard.disabled')}</label>`
154
+ ? html`<label style="cursor:pointer"><input type="checkbox" checked=${p in pending ? pending[p] : e.value} onChange=${ev => onChange(p, ev.target.checked)} disabled=${!editable} /> ${(p in pending ? pending[p] : e.value) ? t('dashboard.enabled') : t('dashboard.disabled')}</label>`
133
155
  : e.type === 'integer'
134
- ? html`<input type="number" value=${e.value} style="width:120px" onChange=${ev => onChange(p, parseInt(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
156
+ ? html`<input type="number" value=${p in pending ? pending[p] : e.value} style="width:120px" onInput=${ev => onChange(p, parseInt(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
135
157
  : e.type === 'float'
136
- ? html`<input type="number" step="0.01" value=${e.value} style="width:120px" onChange=${ev => onChange(p, parseFloat(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
158
+ ? html`<input type="number" step="0.01" value=${p in pending ? pending[p] : e.value} style="width:120px" onInput=${ev => onChange(p, parseFloat(ev.target.value))} disabled=${!editable} />${e.range ? html` <span class="adm-text-dim adm-text-xs">${escHtml(e.range)}</span>` : null}`
137
159
  : e.type === 'string'
138
- ? html`<input type="text" value=${e.value || ''} style="width:250px" onChange=${ev => onChange(p, ev.target.value)} disabled=${!editable} />`
160
+ ? html`<input type="text" value=${p in pending ? pending[p] : (e.value || '')} style="width:250px" onInput=${ev => onChange(p, ev.target.value)} disabled=${!editable} />`
139
161
  : e.type === 'object'
140
- ? html`<code style="font-size:.75rem">${escHtml(JSON.stringify(e.value)).substring(0, 100)}...</code>`
162
+ ? (Array.isArray(e.value)
163
+ ? html`<textarea class="adm-config-array-edit" rows="4" disabled=${!editable}
164
+ value=${pending[p] !== undefined ? pending[p] : e.value.join('\n')}
165
+ onInput=${ev => onChange(p, ev.target.value)}
166
+ placeholder=${t('dashboard.cfgOnePerLine')}></textarea>
167
+ <div class="adm-text-dim adm-text-xs">${t('dashboard.cfgOnePerLine')}</div>`
168
+ : html`<code style="font-size:.75rem">${escHtml(JSON.stringify(e.value)).substring(0, 100)}...</code>`)
141
169
  : html`<code>${escHtml(String(e.value))}</code>`
142
170
  }
143
171
  ${e.canReset && editable && html` <button class="adm-btn-sm" onClick=${() => resetConfig(p)}>${t('dashboard.cfgReset')}</button>`}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @file agent-solo.js
3
+ * @description Standalone single-agent view, served at /v1/profile?solo=<name>.
4
+ * Opened in its own window by the agent "pop out" button so the owner can
5
+ * place several agents side by side. Reuses the AgentCard component in
6
+ * soloMode (always expanded, no collapse, no pop-out button) and shares the
7
+ * logged-in session via localStorage (same origin). Opens its own SSE
8
+ * connection so the card live-updates independently.
9
+ * @structure AgentSolo (default export)
10
+ * @version-history
11
+ * v1.0.0 -- 2026-05-31 -- Initial creation for the agent pop-out window
12
+ */
13
+
14
+ import { h } from 'preact';
15
+ import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
16
+ import htm from 'htm';
17
+ import { t } from '/js/i18n.js';
18
+ import { getSession } from '/js/services/auth.js';
19
+ import { connect, disconnect, onUpdate, offUpdate } from '/lib/live-updates.js';
20
+ import { listAgents } from '/js/services/agents.js';
21
+ import { getOnboarding } from '/js/services/agent-integration.js';
22
+ import { listTasks } from '/js/services/agent-tasks.js';
23
+ import AgentCard from './profile/agents/agent-card.js';
24
+
25
+ const html = htm.bind(h);
26
+
27
+ function getSoloName() {
28
+ try { return new URLSearchParams(window.location.search).get('solo') || ''; }
29
+ catch { return ''; }
30
+ }
31
+
32
+ export default function AgentSolo() {
33
+ const session = getSession();
34
+ const name = getSoloName();
35
+ const [agent, setAgent] = useState(null);
36
+ const [onboarding, setOnboarding] = useState(null);
37
+ const [allAgents, setAllAgents] = useState([]);
38
+ const [error, setError] = useState(null);
39
+ const [loading, setLoading] = useState(true);
40
+ const [toast, setToast] = useState(null);
41
+
42
+ const showToast = useCallback((msg, isErr = false) => {
43
+ setToast({ msg, isErr });
44
+ setTimeout(() => setToast(cur => (cur && cur.msg === msg ? null : cur)), 3000);
45
+ }, []);
46
+
47
+ const load = useCallback(async () => {
48
+ if (!session || !name) return;
49
+ try {
50
+ const list = await listAgents(session.owner);
51
+ setAllAgents(list);
52
+ const a = list.find(x => x.name === name);
53
+ if (!a) { setError(t('profile.agents.solo.notFound')); setLoading(false); return; }
54
+ // Enrich with today's task stats, same as the list view does.
55
+ let taskStats = null;
56
+ try {
57
+ const [doneResp, activeResp] = await Promise.all([
58
+ listTasks(name, { status: 'done', per_page: 100 }),
59
+ listTasks(name, { status: 'active', per_page: 100 }),
60
+ ]);
61
+ const today = new Date().toISOString().slice(0, 10);
62
+ taskStats = {
63
+ done: (doneResp?.data?.tasks || []).filter(tk => tk.completedAt?.startsWith(today)).length,
64
+ active: (activeResp?.data?.tasks || []).length,
65
+ };
66
+ } catch { /* stats are best-effort */ }
67
+ setAgent({ ...a, taskStats });
68
+ try { const ob = await getOnboarding(name); setOnboarding(ob?.data?.onboarding || null); }
69
+ catch { setOnboarding(null); }
70
+ setError(null);
71
+ } catch (e) {
72
+ setError(e.message || 'Error');
73
+ }
74
+ setLoading(false);
75
+ }, [session, name]);
76
+
77
+ useEffect(() => { load(); }, [load]);
78
+
79
+ // Title the window after the agent so multiple pop-outs are distinguishable.
80
+ useEffect(() => { if (name) document.title = `${name} — AIMEAT`; }, [name]);
81
+
82
+ // This is a standalone window — profile.js (which normally bridges SSE to the
83
+ // `aimeat-live-update` event the tabs listen for) is not mounted here, so we
84
+ // open our own connection and dispatch the event ourselves.
85
+ const loadRef = useRef(load);
86
+ loadRef.current = load;
87
+ useEffect(() => {
88
+ if (!session) return;
89
+ const notify = () => {
90
+ window.dispatchEvent(new CustomEvent('aimeat-live-update'));
91
+ loadRef.current();
92
+ };
93
+ connect(() => getSession()?.jwt);
94
+ onUpdate(notify);
95
+ return () => { offUpdate(notify); disconnect(); };
96
+ }, [session?.owner]);
97
+
98
+ if (!session) {
99
+ return html`<div class="pf pf-agd-solo"><div class="pf-agd-empty">${t('profile.agents.solo.loginRequired')}</div></div>`;
100
+ }
101
+ if (loading && !agent) {
102
+ return html`<div class="pf pf-agd-solo"><div class="pf-agd-empty">${t('profile.loading')}</div></div>`;
103
+ }
104
+ if (error) {
105
+ return html`<div class="pf pf-agd-solo"><div class="pf-agd-empty">${error}</div></div>`;
106
+ }
107
+
108
+ return html`
109
+ <div class="pf pf-agd-solo">
110
+ <${AgentCard}
111
+ agent=${agent}
112
+ onboarding=${onboarding}
113
+ expanded=${true}
114
+ soloMode=${true}
115
+ onToggle=${() => {}}
116
+ session=${session}
117
+ showToast=${showToast}
118
+ allAgents=${allAgents}
119
+ />
120
+ ${toast && html`<div class="pf-agd-solo-toast ${toast.isErr ? 'pf-agd-solo-toast--err' : ''}">${toast.msg}</div>`}
121
+ </div>
122
+ `;
123
+ }
@@ -11,6 +11,13 @@
11
11
  * v1.5.0 -- 2026-05-31 -- Add README tab, rendered from the agents.<name>.readme
12
12
  * owner-namespaced memory entry. Shown first + selected by default when a
13
13
  * non-empty README exists; hidden otherwise.
14
+ * v1.6.0 -- 2026-05-31 -- Add `onPopOut` prop (pop-out ⤢ button on collapsed
15
+ * bar + expanded footer that opens the agent in its own window) and
16
+ * `soloMode` prop (used by the standalone /v1/profile?solo= view: forces
17
+ * expanded, disables collapse, hides the pop-out button).
18
+ * v1.7.0 -- 2026-05-31 -- Collapsed bar split into two rows (identity+badges on
19
+ * top, delivery/status/last-seen on a faint-divider-separated second line)
20
+ * so the status text stops overflowing the card.
14
21
  */
15
22
 
16
23
  import { h } from 'preact';
@@ -68,7 +75,7 @@ async function fetchReadme(agent) {
68
75
  }
69
76
  }
70
77
 
71
- export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, onScopesClick, onDeleteClick, onFederateToggle }) {
78
+ export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, onScopesClick, onDeleteClick, onFederateToggle, onPopOut, soloMode = false }) {
72
79
  const state = detectAgentState(agent, onboarding);
73
80
  const [activeTab, setActiveTab] = useState(null);
74
81
  // README markdown: undefined = not loaded, '' = none published, string = show tab.
@@ -99,28 +106,36 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
99
106
  }, [expanded, readme]);
100
107
 
101
108
  const handleCollapse = useCallback(() => {
109
+ // In solo mode the card is the whole window — clicking the header must not
110
+ // collapse it (there is nothing to collapse back into).
111
+ if (soloMode) return;
102
112
  onToggle(agent.name);
103
113
  if (expanded) { setActiveTab(null); userPickedTab.current = false; }
104
- }, [expanded, agent.name, onToggle]);
114
+ }, [expanded, agent.name, onToggle, soloMode]);
105
115
 
106
116
  if (!expanded) {
107
117
  return html`
108
118
  <div class="pf-agd-card">
109
119
  <div class="pf-agd-collapsed ${state === 'problem' ? 'pf-agd-collapsed--problem' : ''}"
110
120
  onClick=${() => onToggle(agent.name)}>
111
- <span class="pf-agd-expand-icon">▶</span>
112
- <span class="pf-agd-collapsed-icon">🤖</span>
113
- <span class="pf-agd-collapsed-name">${agent.display_name || agent.name}</span>
114
- <div class="pf-agd-collapsed-badges">
115
- ${renderModeBadge(agent)}
116
- ${renderPlatformBadge(onboarding)}
117
- ${renderReadinessBadge(state, onboarding)}
118
- ${agent.federate && html`<span class="pf-agd-badge pf-agd-badge--federation">${t('profile.federated')}</span>`}
121
+ <div class="pf-agd-collapsed-main">
122
+ <span class="pf-agd-expand-icon">▶</span>
123
+ <span class="pf-agd-collapsed-icon">🤖</span>
124
+ <span class="pf-agd-collapsed-name">${agent.display_name || agent.name}</span>
125
+ <div class="pf-agd-collapsed-badges">
126
+ ${renderModeBadge(agent)}
127
+ ${renderPlatformBadge(onboarding)}
128
+ ${renderReadinessBadge(state, onboarding)}
129
+ ${agent.federate && html`<span class="pf-agd-badge pf-agd-badge--federation">${t('profile.federated')}</span>`}
130
+ </div>
131
+ ${renderPopOut(onPopOut, agent)}
119
132
  </div>
120
- <span class="pf-agd-collapsed-stats">
133
+ <!-- Second line: faint-divider-separated meta (delivery + status + last seen)
134
+ so the long status text gets its own row instead of overflowing. -->
135
+ <div class="pf-agd-collapsed-meta">
121
136
  ${renderDeliveryIndicator(agent)}
122
137
  ${renderCollapsedStats(state, agent, onboarding)}
123
- </span>
138
+ </div>
124
139
  </div>
125
140
  </div>
126
141
  `;
@@ -189,6 +204,7 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
189
204
  <!-- Card Footer: Scopes + Delete -->
190
205
  <div class="pf-agd-card-footer">
191
206
  <div class="pf-agd-card-actions">
207
+ ${renderPopOut(onPopOut, agent)}
192
208
  ${onScopesClick && html`
193
209
  <button class="btn-outline btn-sm" onClick=${(e) => { e.stopPropagation(); onScopesClick(agent); }}>
194
210
  ${t('profile.agents.scopeUi.manage')}
@@ -211,6 +227,15 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
211
227
  `;
212
228
  }
213
229
 
230
+ // Pop-out button: opens this agent in its own window (one window per agent,
231
+ // so re-clicking focuses the existing one). Only rendered when the parent
232
+ // supplies onPopOut — the standalone solo view omits it.
233
+ function renderPopOut(onPopOut, agent) {
234
+ if (!onPopOut) return null;
235
+ return html`<button class="pf-agd-popout-btn" title=${t('profile.agents.detail.popOut')}
236
+ onClick=${(e) => { e.stopPropagation(); onPopOut(agent); }}>⤢</button>`;
237
+ }
238
+
214
239
  function renderDeliveryIndicator(agent) {
215
240
  const hasWebhook = agent.webhookUrl || agent.webhook_url;
216
241
  const hasMcp = agent.mcpEnabled || agent.mcp_enabled;