aimeat 1.7.0 → 1.8.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 (95) hide show
  1. package/dist/bin/aimeat.js +0 -0
  2. package/dist/locales/en.json +46 -3
  3. package/dist/locales/fi.json +46 -3
  4. package/dist/public/css/views/agents-detail.css +150 -0
  5. package/dist/public/css/views/profile.css +22 -2
  6. package/dist/public/js/services/generator-spec-validate.js +23 -0
  7. package/dist/public/js/services/memory.js +39 -0
  8. package/dist/public/views/admin/federation-tab.js +5 -5
  9. package/dist/public/views/admin/prompts-tab.js +9 -10
  10. package/dist/public/views/profile/access-tab.js +18 -0
  11. package/dist/public/views/profile/agents-messages-subtab.js +3 -2
  12. package/dist/public/views/profile/agents-tab.js +10 -6
  13. package/dist/public/views/profile/agents-tasks-subtab.js +123 -64
  14. package/dist/public/views/profile/generator-detail.js +8 -1
  15. package/dist/public/views/profile/memory-tab.js +371 -7
  16. package/dist/public/views/profile/wallet-tab.js +14 -0
  17. package/dist/src/models/agent-capabilities-schemas.d.ts +3 -0
  18. package/dist/src/models/agent-capabilities-schemas.d.ts.map +1 -1
  19. package/dist/src/models/agent-capabilities-schemas.js +3 -0
  20. package/dist/src/models/agent-capabilities-schemas.js.map +1 -1
  21. package/dist/src/routes/admin-monitoring.js +12 -7
  22. package/dist/src/routes/admin-monitoring.js.map +1 -1
  23. package/dist/src/routes/agent-capabilities.d.ts +1 -0
  24. package/dist/src/routes/agent-capabilities.d.ts.map +1 -1
  25. package/dist/src/routes/agent-capabilities.js +9 -0
  26. package/dist/src/routes/agent-capabilities.js.map +1 -1
  27. package/dist/src/routes/agent-tasks.d.ts +1 -0
  28. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  29. package/dist/src/routes/agent-tasks.js +9 -7
  30. package/dist/src/routes/agent-tasks.js.map +1 -1
  31. package/dist/src/routes/federation-auth.d.ts.map +1 -1
  32. package/dist/src/routes/federation-auth.js +3 -1
  33. package/dist/src/routes/federation-auth.js.map +1 -1
  34. package/dist/src/routes/federation-peer.d.ts.map +1 -1
  35. package/dist/src/routes/federation-peer.js +11 -2
  36. package/dist/src/routes/federation-peer.js.map +1 -1
  37. package/dist/src/routes/federation-sync.d.ts +1 -0
  38. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  39. package/dist/src/routes/federation-sync.js +29 -0
  40. package/dist/src/routes/federation-sync.js.map +1 -1
  41. package/dist/src/routes/memory.d.ts +9 -0
  42. package/dist/src/routes/memory.d.ts.map +1 -1
  43. package/dist/src/routes/memory.js +304 -9
  44. package/dist/src/routes/memory.js.map +1 -1
  45. package/dist/src/routes/prompts.d.ts +1 -0
  46. package/dist/src/routes/prompts.d.ts.map +1 -1
  47. package/dist/src/routes/prompts.js +40 -1
  48. package/dist/src/routes/prompts.js.map +1 -1
  49. package/dist/src/routes/sharing-groups.d.ts.map +1 -1
  50. package/dist/src/routes/sharing-groups.js +8 -2
  51. package/dist/src/routes/sharing-groups.js.map +1 -1
  52. package/dist/src/routes/wallet.d.ts.map +1 -1
  53. package/dist/src/routes/wallet.js +2 -0
  54. package/dist/src/routes/wallet.js.map +1 -1
  55. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  56. package/dist/src/server-bootstrap/routes-loader.js +31 -0
  57. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  58. package/dist/src/services/federation-helpers.d.ts +1 -0
  59. package/dist/src/services/federation-helpers.d.ts.map +1 -1
  60. package/dist/src/services/federation-helpers.js +1 -1
  61. package/dist/src/services/federation-helpers.js.map +1 -1
  62. package/dist/src/services/generator-autopilot.d.ts.map +1 -1
  63. package/dist/src/services/generator-autopilot.js +125 -26
  64. package/dist/src/services/generator-autopilot.js.map +1 -1
  65. package/dist/src/services/generator-prompts/index.d.ts +1 -1
  66. package/dist/src/services/generator-prompts/index.d.ts.map +1 -1
  67. package/dist/src/services/generator-prompts/index.js +1 -1
  68. package/dist/src/services/generator-prompts/index.js.map +1 -1
  69. package/dist/src/services/generator-prompts/spec-validate.d.ts +1 -0
  70. package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
  71. package/dist/src/services/generator-prompts/spec-validate.js +20 -0
  72. package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
  73. package/dist/src/services/generator-prompts/validate.d.ts +7 -1
  74. package/dist/src/services/generator-prompts/validate.d.ts.map +1 -1
  75. package/dist/src/services/generator-prompts/validate.js +70 -0
  76. package/dist/src/services/generator-prompts/validate.js.map +1 -1
  77. package/dist/src/services/prompt-defaults.d.ts +1 -0
  78. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  79. package/dist/src/services/prompt-defaults.js +830 -8
  80. package/dist/src/services/prompt-defaults.js.map +1 -1
  81. package/dist/src/services/prompt-seeder.js +1 -1
  82. package/dist/src/services/prompt-seeder.js.map +1 -1
  83. package/dist/src/storage/interface.d.ts +2 -0
  84. package/dist/src/storage/interface.d.ts.map +1 -1
  85. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  86. package/dist/src/storage/providers/mongodb/index.js +5 -2
  87. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  88. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  89. package/dist/src/storage/providers/sqlite/index.js +7 -2
  90. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  91. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  92. package/dist/src/storage/providers/sqlite/schema.js +2 -0
  93. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  94. package/package.json +77 -97
  95. package/prisma/schema.prisma +2 -0
@@ -5,6 +5,8 @@
5
5
  * @version-history
6
6
  * v1.0.0 — 2026-03-17 — Refactor: replace inline styles with CSS utility classes
7
7
  * v1.1.0 — 2026-03-18 — Fix: use AuthImage for thumbnails and authenticated download to avoid 401
8
+ * v1.2.0 — 2026-05-22 — Add Browse Home / Browse Remote panels for federation memory sync
9
+ * v1.3.0 — 2026-05-22 — Add timestamps, sort controls, fix group picker rotation bug
8
10
  */
9
11
  import { h } from 'preact';
10
12
  import { useState, useEffect, useRef } from 'preact/hooks';
@@ -16,6 +18,7 @@ import { Spinner, recipientBadge, VisibilityPill } from './shared.js';
16
18
  import * as memoryService from '/js/services/memory.js';
17
19
  import AuthImage from '/js/components/auth-image.js';
18
20
  import { listAgents } from '/js/services/agents.js';
21
+ import { listPeers } from '/js/services/federation.js';
19
22
  import { getKeyPermissions, listConsents, grantConsent, revokeConsent } from '/js/services/consent.js';
20
23
  import { getNodeUrl } from '/js/services/auth.js';
21
24
  import { listGroups } from '/js/services/sharing-groups.js';
@@ -42,7 +45,21 @@ export default function MemoryTab({ session, showToast, onStats }) {
42
45
  const [fedConsents, setFedConsents] = useState({}); // { memoryKey: consentId }
43
46
  const [togglingFed, setTogglingFed] = useState(null);
44
47
  const [groups, setGroups] = useState([]);
45
- const [groupPickerFor, setGroupPickerFor] = useState(null); // key of the memory entry showing group picker
48
+ const [groupPickerFor, setGroupPickerFor] = useState(null);
49
+ const [browseMode, setBrowseMode] = useState(null); // 'home' | 'remote' | null
50
+ const [remoteEntries, setRemoteEntries] = useState(null);
51
+ const [remotePeers, setRemotePeers] = useState([]);
52
+ const [selectedPeer, setSelectedPeer] = useState('');
53
+ const [browseLoading, setBrowseLoading] = useState(false);
54
+ const [browseError, setBrowseError] = useState(null);
55
+ const [pullingKeys, setPullingKeys] = useState(new Set());
56
+ const [sortBy, setSortBy] = useState('updated');
57
+ const [discoverEntries, setDiscoverEntries] = useState(null);
58
+ const [discoverLoading, setDiscoverLoading] = useState(false);
59
+ const [discoverError, setDiscoverError] = useState(null);
60
+ const [discoverSearch, setDiscoverSearch] = useState('');
61
+ const [copyingKeys, setCopyingKeys] = useState(new Set());
62
+ const [expandedDiscover, setExpandedDiscover] = useState(null);
46
63
 
47
64
  useEffect(() => {
48
65
  if (session) { loadAgents(); loadMemories(); loadFiles(); loadFedConsents(); loadGroups(); }
@@ -279,6 +296,35 @@ export default function MemoryTab({ session, showToast, onStats }) {
279
296
  }
280
297
  }
281
298
 
299
+ function formatRelativeTime(isoStr) {
300
+ if (!isoStr) return '';
301
+ const d = new Date(isoStr);
302
+ const now = Date.now();
303
+ const diff = now - d.getTime();
304
+ const mins = Math.floor(diff / 60000);
305
+ if (mins < 1) return t('profile.memory.timeJustNow');
306
+ if (mins < 60) return t('profile.memory.timeMinsAgo').replace('{n}', mins);
307
+ const hrs = Math.floor(mins / 60);
308
+ if (hrs < 24) return t('profile.memory.timeHoursAgo').replace('{n}', hrs);
309
+ const days = Math.floor(hrs / 24);
310
+ if (days < 30) return t('profile.memory.timeDaysAgo').replace('{n}', days);
311
+ return d.toLocaleDateString();
312
+ }
313
+
314
+ function sortEntries(entries) {
315
+ const sorted = [...entries];
316
+ switch (sortBy) {
317
+ case 'updated':
318
+ return sorted.sort((a, b) => new Date(b.updated_at || 0) - new Date(a.updated_at || 0));
319
+ case 'created':
320
+ return sorted.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
321
+ case 'alpha':
322
+ return sorted.sort((a, b) => a.key.localeCompare(b.key));
323
+ default:
324
+ return sorted;
325
+ }
326
+ }
327
+
282
328
  const renderEntries = () => {
283
329
  const searchRef = useRef(null);
284
330
  if (!memories) return html`<${Spinner} text=${t('profile.memory.loading')} />`;
@@ -289,9 +335,11 @@ export default function MemoryTab({ session, showToast, onStats }) {
289
335
  if (m.tags) for (const tag of m.tags) allMemTags.add(tag);
290
336
  }
291
337
 
292
- // Filter by selected tags
293
- const filtered = memTagFilter.size === 0 ? memories : memories.filter(m =>
294
- m.tags && [...memTagFilter].every(tag => m.tags.includes(tag))
338
+ // Filter by selected tags, then sort
339
+ const filtered = sortEntries(
340
+ memTagFilter.size === 0 ? memories : memories.filter(m =>
341
+ m.tags && [...memTagFilter].every(tag => m.tags.includes(tag))
342
+ )
295
343
  );
296
344
 
297
345
  const toggleMemTag = (tag) => {
@@ -309,6 +357,14 @@ export default function MemoryTab({ session, showToast, onStats }) {
309
357
  <button class="btn-sm" onClick=${() => handleSearch(searchRef.current?.value)}>${t('profile.memory.searchBtn')}</button>
310
358
  <button class="btn-outline btn-sm" onClick=${() => { if (searchRef.current) searchRef.current.value = ''; loadMemories(); }}>${t('profile.memory.clearBtn')}</button>
311
359
  </div>
360
+ <div class="mem-sort-bar">
361
+ <label class="text-meta-sm">${t('profile.memory.sortLabel')}</label>
362
+ <select class="input-field mem-sort-select" value=${sortBy} onChange=${e => setSortBy(e.target.value)}>
363
+ <option value="updated">${t('profile.memory.sortUpdated')}</option>
364
+ <option value="created">${t('profile.memory.sortCreated')}</option>
365
+ <option value="alpha">${t('profile.memory.sortAlpha')}</option>
366
+ </select>
367
+ </div>
312
368
  <button class="btn-primary" onClick=${() => setShowMemForm(!showMemForm)}>${t('profile.memory.newBtn')}</button>
313
369
  </div>
314
370
  <${TagCloud} tags=${[...allMemTags]} selected=${memTagFilter} onToggle=${toggleMemTag} onClear=${() => setMemTagFilter(new Set())} />
@@ -319,6 +375,9 @@ export default function MemoryTab({ session, showToast, onStats }) {
319
375
  <div>
320
376
  <div class="mem-item" onClick=${() => setExpandedMem(expandedMem === m.key ? null : m.key)}>
321
377
  <span class="mem-key">${escHtml(m.key)}</span>
378
+ <span class="mem-time" title="${m.created_at ? new Date(m.created_at).toLocaleString() : ''} / ${m.updated_at ? new Date(m.updated_at).toLocaleString() : ''}">
379
+ ${formatRelativeTime(m.updated_at || m.created_at)}
380
+ </span>
322
381
  ${(() => {
323
382
  const vis = m.visibility || 'private';
324
383
  const nextVis = cycleVis[(cycleVis.indexOf(vis) + 1) % cycleVis.length];
@@ -343,7 +402,7 @@ export default function MemoryTab({ session, showToast, onStats }) {
343
402
  }}>${g.name} (${g.members?.length || 0} ${t('profile.access.sharingGroups.members')})</button>
344
403
  `)
345
404
  }
346
- <button class="btn-outline btn-sm mt-xs" onClick=${() => { setGroupPickerFor(null); }}>${t('profile.memory.cancelBtn')}</button>
405
+ <button class="btn-outline btn-sm mt-xs" onClick=${() => { setGroupPickerFor(null); handleUpdateVisibility(m.key, 'public', m.version); }}>${t('profile.memory.skipGroup')}</button>
347
406
  </div>
348
407
  `}
349
408
  ${expandedMem === m.key && html`
@@ -502,16 +561,301 @@ export default function MemoryTab({ session, showToast, onStats }) {
502
561
  } catch (e) { showToast(e.message, true); }
503
562
  }
504
563
 
564
+ function browseErrorMessage(e) {
565
+ const code = e.code || '';
566
+ const msg = e.message || '';
567
+ if (code === 'FEDERATION_PROXY_ERROR' || msg.includes('Localhost not allowed') || msg.includes('ocalhost'))
568
+ return t('profile.memory.errorLocalhost');
569
+ if (code === 'PEER_NOT_FOUND' || msg.includes('not found'))
570
+ return t('profile.memory.errorPeerNotFound');
571
+ if (code === 'ROUTE_NOT_FOUND' || e.status === 404)
572
+ return t('profile.memory.errorPeerUnsupported');
573
+ return msg;
574
+ }
575
+
576
+ async function loadBrowseHome() {
577
+ setBrowseMode('home');
578
+ setBrowseLoading(true);
579
+ setBrowseError(null);
580
+ setRemoteEntries(null);
581
+ try {
582
+ const entries = await memoryService.listHomeMemories();
583
+ setRemoteEntries(entries);
584
+ } catch (e) {
585
+ setBrowseError(browseErrorMessage(e));
586
+ setRemoteEntries([]);
587
+ } finally { setBrowseLoading(false); }
588
+ }
589
+
590
+ async function loadBrowseRemote(peerNodeId) {
591
+ if (!peerNodeId) return;
592
+ setSelectedPeer(peerNodeId);
593
+ setBrowseMode('remote');
594
+ setBrowseLoading(true);
595
+ setBrowseError(null);
596
+ setRemoteEntries(null);
597
+ try {
598
+ const entries = await memoryService.listRemoteMemories(peerNodeId);
599
+ setRemoteEntries(entries);
600
+ } catch (e) {
601
+ setBrowseError(browseErrorMessage(e));
602
+ setRemoteEntries([]);
603
+ } finally { setBrowseLoading(false); }
604
+ }
605
+
606
+ async function initBrowseRemote() {
607
+ setBrowseMode('remote');
608
+ setRemoteEntries(null);
609
+ setSelectedPeer('');
610
+ try {
611
+ const peers = await listPeers();
612
+ setRemotePeers(Array.isArray(peers) ? peers.filter(p => p.status === 'active' || p.status === 'healthy') : []);
613
+ } catch { setRemotePeers([]); }
614
+ }
615
+
616
+ async function handlePullRemoteEntry(key, peerNodeId) {
617
+ const nodeId = peerNodeId || selectedPeer;
618
+ setPullingKeys(prev => new Set([...prev, key]));
619
+ try {
620
+ if (session?.federated) {
621
+ await memoryService.pullFromHome(key);
622
+ } else {
623
+ await memoryService.pullFromRemote(nodeId, key);
624
+ }
625
+ showToast(t('profile.memory.pullSuccess'));
626
+ loadMemories();
627
+ } catch (e) {
628
+ showToast(e.message, true);
629
+ } finally {
630
+ setPullingKeys(prev => { const n = new Set(prev); n.delete(key); return n; });
631
+ }
632
+ }
633
+
634
+ async function handlePullAll() {
635
+ if (!remoteEntries?.length) return;
636
+ const node = session?.federated ? (session.homeNode || 'home') : selectedPeer;
637
+ const msg = t('profile.memory.pullAllConfirm').replace('{count}', remoteEntries.length).replace('{node}', node);
638
+ confirm(msg, async () => {
639
+ let pulled = 0;
640
+ for (const entry of remoteEntries) {
641
+ try {
642
+ if (session?.federated) {
643
+ await memoryService.pullFromHome(entry.key);
644
+ } else {
645
+ await memoryService.pullFromRemote(selectedPeer, entry.key);
646
+ }
647
+ pulled++;
648
+ } catch { /* skip failed entries */ }
649
+ }
650
+ showToast(t('profile.memory.pullAllSuccess').replace('{count}', pulled));
651
+ loadMemories();
652
+ });
653
+ }
654
+
655
+ async function loadDiscoverEntries(query) {
656
+ setDiscoverLoading(true);
657
+ setDiscoverError(null);
658
+ setDiscoverEntries(null);
659
+ setExpandedDiscover(null);
660
+ try {
661
+ const result = await memoryService.discoverPublicMemories({ q: query || undefined, limit: 100 });
662
+ setDiscoverEntries(result.items || []);
663
+ } catch (e) {
664
+ setDiscoverError(e.message || t('profile.error'));
665
+ setDiscoverEntries([]);
666
+ } finally { setDiscoverLoading(false); }
667
+ }
668
+
669
+ function initDiscover() {
670
+ setBrowseMode('discover');
671
+ loadDiscoverEntries('');
672
+ }
673
+
674
+ async function handleCopyEntry(ownerGaii, key) {
675
+ setCopyingKeys(prev => new Set([...prev, key]));
676
+ try {
677
+ const resp = await memoryService.copyPublicMemory(ownerGaii, key, 'private');
678
+ if (resp.ok === false) { showToast(resp.error?.message || t('profile.error'), true); return; }
679
+ showToast(t('profile.memory.discoverCopied'));
680
+ loadMemories();
681
+ } catch (e) {
682
+ showToast(e.message || t('profile.error'), true);
683
+ } finally {
684
+ setCopyingKeys(prev => { const n = new Set(prev); n.delete(key); return n; });
685
+ }
686
+ }
687
+
688
+ function closeBrowse() {
689
+ setBrowseMode(null);
690
+ setRemoteEntries(null);
691
+ setSelectedPeer('');
692
+ setDiscoverEntries(null);
693
+ setDiscoverSearch('');
694
+ setExpandedDiscover(null);
695
+ }
696
+
697
+ const renderBrowsePanel = () => {
698
+ if (!browseMode) return null;
699
+
700
+ if (browseMode === 'discover') {
701
+ return html`
702
+ <div class="mem-browse-panel">
703
+ <div class="mem-browse-header">
704
+ <div>
705
+ <div class="section-title">${t('profile.memory.discoverTitle')}</div>
706
+ <div class="section-desc">${t('profile.memory.discoverDesc')}</div>
707
+ </div>
708
+ <button class="btn-outline btn-sm" onClick=${closeBrowse}>${t('profile.memory.cancelBtn')}</button>
709
+ </div>
710
+ <div class="mem-discover-search mb-half">
711
+ <input type="text" class="input-field" placeholder=${t('profile.memory.discoverSearchPlaceholder')}
712
+ value=${discoverSearch}
713
+ onInput=${e => setDiscoverSearch(e.target.value)}
714
+ onKeyDown=${e => e.key === 'Enter' && loadDiscoverEntries(discoverSearch)} />
715
+ <button class="btn-sm" onClick=${() => loadDiscoverEntries(discoverSearch)}>${t('profile.memory.searchBtn')}</button>
716
+ </div>
717
+
718
+ ${discoverLoading && html`<${Spinner} text=${t('profile.memory.discoverLoading')} />`}
719
+
720
+ ${discoverError && !discoverLoading && html`
721
+ <div class="alert alert-warning"><span class="alert-msg">${discoverError}</span></div>
722
+ `}
723
+
724
+ ${discoverEntries && !discoverLoading && !discoverError && html`
725
+ <div class="mb-half text-meta">
726
+ ${t('profile.memory.discoverCount').replace('{count}', discoverEntries.length)}
727
+ </div>
728
+ ${discoverEntries.length === 0
729
+ ? html`<div class="empty">${t('profile.memory.discoverEmpty')}</div>`
730
+ : html`<div class="mem-browse-list">
731
+ ${discoverEntries.map(entry => {
732
+ const ownerShort = entry.owner_gaii?.split('@')[0] || entry.owner_gaii;
733
+ const isExpanded = expandedDiscover === entry.owner_gaii + '/' + entry.key;
734
+ return html`
735
+ <div key=${entry.owner_gaii + '/' + entry.key} class="mem-discover-item">
736
+ <div class="mem-discover-row" onClick=${() => setExpandedDiscover(isExpanded ? null : entry.owner_gaii + '/' + entry.key)}>
737
+ <div class="mem-discover-info">
738
+ <div class="mem-browse-key" title=${entry.key}>${escHtml(entry.key)}</div>
739
+ <div class="mem-discover-owner">${escHtml(ownerShort)}</div>
740
+ </div>
741
+ <div class="mem-browse-meta">
742
+ ${entry.tags?.length > 0 && html`<span class="text-meta-sm mem-browse-tags" title=${entry.tags.join(', ')}>${entry.tags.join(', ')}</span>`}
743
+ <span class="mem-time">${formatRelativeTime(entry.updated_at || entry.created_at)}</span>
744
+ </div>
745
+ <button class="btn-primary btn-sm"
746
+ disabled=${copyingKeys.has(entry.key)}
747
+ onClick=${(e) => { e.stopPropagation(); handleCopyEntry(entry.owner_gaii, entry.key); }}>
748
+ ${copyingKeys.has(entry.key) ? '...' : t('profile.memory.discoverCopy')}
749
+ </button>
750
+ </div>
751
+ ${isExpanded && html`
752
+ <${DiscoverPreview} ownerGaii=${entry.owner_gaii} memKey=${entry.key} />
753
+ `}
754
+ </div>
755
+ `;
756
+ })}
757
+ </div>`
758
+ }
759
+ `}
760
+ </div>
761
+ `;
762
+ }
763
+
764
+ const isHome = browseMode === 'home';
765
+ const title = isHome ? t('profile.memory.browseHome') : t('profile.memory.browseRemote');
766
+ const desc = isHome ? t('profile.memory.browseHomeDesc') : t('profile.memory.browseRemoteDesc');
767
+
768
+ return html`
769
+ <div class="mem-browse-panel">
770
+ <div class="mem-browse-header">
771
+ <div>
772
+ <div class="section-title">${title}</div>
773
+ <div class="section-desc">${desc}</div>
774
+ </div>
775
+ <button class="btn-outline btn-sm" onClick=${closeBrowse}>${t('profile.memory.cancelBtn')}</button>
776
+ </div>
777
+
778
+ ${!isHome && !selectedPeer && html`
779
+ <div class="mb-1">
780
+ ${remotePeers.length === 0
781
+ ? html`<div class="empty">${t('profile.memory.noPeers')}</div>`
782
+ : html`
783
+ <select class="input-field" onChange=${e => loadBrowseRemote(e.target.value)}>
784
+ <option value="">${t('profile.memory.browseRemoteSelect')}</option>
785
+ ${remotePeers.map(p => html`<option key=${p.node_id} value=${p.node_id}>${escHtml(p.node_id)} (${escHtml(p.url || '')})</option>`)}
786
+ </select>
787
+ `}
788
+ </div>
789
+ `}
790
+
791
+ ${browseLoading && html`<${Spinner} text=${isHome ? t('profile.memory.loadingHome') : t('profile.memory.loadingRemote')} />`}
792
+
793
+ ${browseError && !browseLoading && html`
794
+ <div class="alert alert-warning">
795
+ <span class="alert-msg">${browseError}</span>
796
+ </div>
797
+ `}
798
+
799
+ ${remoteEntries && !browseLoading && !browseError && html`
800
+ <div class="mb-half text-meta">
801
+ ${isHome
802
+ ? t('profile.memory.homeEntries').replace('{count}', remoteEntries.length)
803
+ : t('profile.memory.remoteEntries').replace('{count}', remoteEntries.length).replace('{node}', selectedPeer)}
804
+ ${remoteEntries.length > 0 && html`
805
+ <button class="btn-ghost btn-sm pf-ml-half" onClick=${handlePullAll}>${t('profile.memory.pullAllBtn')}</button>
806
+ `}
807
+ </div>
808
+ ${remoteEntries.length === 0
809
+ ? html`<div class="empty">${isHome ? t('profile.memory.noHomeEntries') : t('profile.memory.noRemoteEntries')}</div>`
810
+ : html`<div class="mem-browse-list">
811
+ ${remoteEntries.map(entry => html`
812
+ <div key=${entry.key} class="mem-browse-item">
813
+ <div class="mem-browse-key" title=${entry.key}>${escHtml(entry.key)}</div>
814
+ <div class="mem-browse-meta">
815
+ <${VisibilityPill} visibility=${entry.visibility} />
816
+ ${entry.tags?.length > 0 && html`<span class="text-meta-sm mem-browse-tags" title=${entry.tags.join(', ')}>${entry.tags.join(', ')}</span>`}
817
+ </div>
818
+ <button class="btn-primary btn-sm"
819
+ disabled=${pullingKeys.has(entry.key)}
820
+ onClick=${() => handlePullRemoteEntry(entry.key)}>
821
+ ${pullingKeys.has(entry.key) ? '...' : t('profile.memory.pullEntry')}
822
+ </button>
823
+ </div>
824
+ `)}
825
+ </div>`
826
+ }
827
+ `}
828
+ </div>
829
+ `;
830
+ };
831
+
505
832
  return html`
506
833
  <div class="section-title">${t('profile.memory.title')}</div>
507
834
  <div class="section-desc">${t('profile.memory.desc')}</div>
508
835
 
836
+ <div class="mem-browse-buttons mb-1">
837
+ <button class="btn-outline btn-sm" onClick=${initDiscover}>
838
+ ${t('profile.memory.discoverTitle')}
839
+ </button>
840
+ ${session.federated && html`
841
+ <button class="btn-outline btn-sm" onClick=${loadBrowseHome}>
842
+ ${t('profile.memory.browseHome')}
843
+ </button>
844
+ `}
845
+ ${!session.federated && html`
846
+ <button class="btn-outline btn-sm" onClick=${initBrowseRemote}>
847
+ ${t('profile.memory.browseRemote')}
848
+ </button>
849
+ `}
850
+ </div>
509
851
  ${session.federated && html`
510
- <div class="alert alert-info">
852
+ <div class="alert alert-info mb-half">
511
853
  <span class="alert-msg">${t('profile.memory.federatedSession')}</span>
512
854
  </div>
513
855
  `}
514
856
 
857
+ ${renderBrowsePanel()}
858
+
515
859
  ${agents.length > 1 && html`
516
860
  <div class="agent-selector mb-half">
517
861
  <label class="text-meta pf-mr-half">${t('profile.memory.agent') || 'Agent'}:</label>
@@ -541,6 +885,26 @@ export default function MemoryTab({ session, showToast, onStats }) {
541
885
  `;
542
886
  }
543
887
 
888
+ function DiscoverPreview({ ownerGaii, memKey }) {
889
+ const [value, setValue] = useState(null);
890
+ const [loading, setLoading] = useState(true);
891
+ const [err, setErr] = useState(null);
892
+ useEffect(() => {
893
+ setLoading(true);
894
+ const url = getNodeUrl() + '/v1/memory/' + encodeURIComponent(ownerGaii) + '/' + encodeURIComponent(memKey);
895
+ fetch(url).then(r => r.json()).then(res => {
896
+ if (res.ok) setValue(res.data.value);
897
+ else setErr(res.error?.message || 'Not found');
898
+ }).catch(e => setErr(e.message)).finally(() => setLoading(false));
899
+ }, [ownerGaii, memKey]);
900
+
901
+ if (loading) return html`<div class="mem-discover-preview"><span class="text-meta-sm">...</span></div>`;
902
+ if (err) return html`<div class="mem-discover-preview"><span class="text-meta-sm" style="color:var(--danger)">${err}</span></div>`;
903
+ const text = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value || '');
904
+ const truncated = text.length > 2000 ? text.slice(0, 2000) + '\n...' : text;
905
+ return html`<div class="mem-discover-preview"><pre>${truncated}</pre></div>`;
906
+ }
907
+
544
908
  function MemoryForm({ onSave, onCancel, groups }) {
545
909
  const [key, setKey] = useState('');
546
910
  const [value, setValue] = useState('');
@@ -739,7 +1103,7 @@ function EditMemoryModal({ memKey, initialValue, initialVisibility, initialVersi
739
1103
  ${(groups || []).map(g => html`<option value=${g.id}>${g.name}</option>`)}
740
1104
  </select>
741
1105
  ${(groups || []).length === 0 && html`<div class="text-meta-sm">${t('profile.memory.noGroups')}</div>`}
742
- <button class="btn-outline btn-sm mt-xs" onClick=${() => setShowGroupSelect(false)}>${t('profile.memory.cancelBtn')}</button>
1106
+ <button class="btn-outline btn-sm mt-xs" onClick=${() => { setVis('public'); setShowGroupSelect(false); setGroupId(''); }}>${t('profile.memory.skipGroup')}</button>
743
1107
  </div>
744
1108
  `}
745
1109
  <textarea class="input-field" rows="6" value=${value} onInput=${e => setValue(e.target.value)}></textarea>
@@ -5,6 +5,7 @@
5
5
  * @version-history
6
6
  * v1.0.0 — 2026-03-16 — Initial wallet tab
7
7
  * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
8
+ * v1.2.0 — 2026-05-22 — Show info message for federated users instead of infinite spinner
8
9
  */
9
10
  import { h } from 'preact';
10
11
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -131,6 +132,19 @@ export default function WalletTab({ session, showToast, onStats }) {
131
132
  }
132
133
  }
133
134
 
135
+ if (session?.federated) {
136
+ return html`
137
+ <div class="section-title">${t('profile.wallet.title')}</div>
138
+ <div class="section-desc">${t('profile.wallet.desc')}</div>
139
+ <div class="alert alert-info">
140
+ <span class="alert-msg">${t('profile.wallet.federatedInfo')}</span>
141
+ </div>
142
+ <div class="text-meta mt-1">
143
+ ${t('profile.wallet.federatedHomeNode')}: <strong>${session.homeNode || '?'}</strong>
144
+ </div>
145
+ `;
146
+ }
147
+
134
148
  if (!walletData) return html`<${Spinner} text=${t('profile.wallet.loading')} />`;
135
149
  const w = walletData;
136
150
  const escrow = w.in_escrow ?? w.escrow ?? 0;
@@ -2,6 +2,7 @@
2
2
  * @file agent-capabilities-schemas.ts
3
3
  * @description Zod validation schemas for agent capability reporting endpoints
4
4
  * @version-history
5
+ * v1.1.0 -- 2026-05-22 -- Add modules_loaded and limitations fields
5
6
  * v1.0.0 -- 2026-05-22 -- Initial schemas for capabilities PUT
6
7
  */
7
8
  import { z } from 'zod';
@@ -16,5 +17,7 @@ export declare const AgentCapabilitiesUpdateSchema: z.ZodObject<{
16
17
  }, z.core.$strip>>>>;
17
18
  domain: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
18
19
  languages: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
+ modules_loaded: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ limitations: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
22
  }, z.core.$strip>;
20
23
  //# sourceMappingURL=agent-capabilities-schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-capabilities-schemas.d.ts","sourceRoot":"","sources":["../../../src/models/agent-capabilities-schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,6BAA6B;;;;;;;;;;;iBAOxC,CAAC"}
1
+ {"version":3,"file":"agent-capabilities-schemas.d.ts","sourceRoot":"","sources":["../../../src/models/agent-capabilities-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;iBASxC,CAAC"}
@@ -2,6 +2,7 @@
2
2
  * @file agent-capabilities-schemas.ts
3
3
  * @description Zod validation schemas for agent capability reporting endpoints
4
4
  * @version-history
5
+ * v1.1.0 -- 2026-05-22 -- Add modules_loaded and limitations fields
5
6
  * v1.0.0 -- 2026-05-22 -- Initial schemas for capabilities PUT
6
7
  */
7
8
  import { z } from 'zod';
@@ -12,5 +13,7 @@ export const AgentCapabilitiesUpdateSchema = z.object({
12
13
  })).max(100).optional().default([]),
13
14
  domain: z.array(z.string().min(1).max(256)).max(50).optional().default([]),
14
15
  languages: z.array(z.string().min(1).max(10)).max(20).optional(),
16
+ modules_loaded: z.array(z.string().min(1).max(64)).max(20).optional(),
17
+ limitations: z.array(z.string().min(1).max(500)).max(50).optional(),
15
18
  });
16
19
  //# sourceMappingURL=agent-capabilities-schemas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-capabilities-schemas.js","sourceRoot":"","sources":["../../../src/models/agent-capabilities-schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;QAChC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;KACvC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;CACjE,CAAC,CAAC"}
1
+ {"version":3,"file":"agent-capabilities-schemas.js","sourceRoot":"","sources":["../../../src/models/agent-capabilities-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;QAChC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;KACvC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1E,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAChE,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACrE,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;CACpE,CAAC,CAAC"}
@@ -136,10 +136,10 @@ export function adminMonitoringRouter(config, storage, peers) {
136
136
  emitChange('config');
137
137
  const targetNodeId = targetInfo.node_id ?? targetUrl;
138
138
  if (remoteStatus === 'auto_approved') {
139
- completeJoin(targetUrl, targetNodeId, publicKey, config, storage, peers);
139
+ completeJoin(targetUrl, targetNodeId, config, storage, peers);
140
140
  }
141
141
  else {
142
- pollForApprovalAndComplete(targetUrl, targetNodeId, requestId, publicKey, config, storage, peers);
142
+ pollForApprovalAndComplete(targetUrl, targetNodeId, requestId, config, storage, peers);
143
143
  }
144
144
  }
145
145
  catch (e) {
@@ -371,12 +371,12 @@ export function adminMonitoringRouter(config, storage, peers) {
371
371
  return router;
372
372
  }
373
373
  /** Save the remote node as a local peer and perform key exchange. */
374
- async function completeJoin(targetUrl, targetNodeId, targetPublicKey, config, storage, peers) {
374
+ async function completeJoin(targetUrl, targetNodeId, config, storage, peers) {
375
375
  const now = new Date().toISOString();
376
376
  const newPeer = {
377
377
  nodeId: targetNodeId,
378
378
  url: targetUrl,
379
- publicKey: targetPublicKey,
379
+ publicKey: '',
380
380
  status: 'active',
381
381
  addedAt: now,
382
382
  lastSeen: now,
@@ -391,16 +391,21 @@ async function completeJoin(targetUrl, targetNodeId, targetPublicKey, config, st
391
391
  peers.set(targetNodeId, newPeer);
392
392
  await storage.saveFederationPeer(newPeer);
393
393
  const result = await performKeyExchange(targetUrl, config, storage);
394
- if (result.success) {
394
+ if (result.success && result.peerPublicKey) {
395
+ newPeer.publicKey = result.peerPublicKey;
396
+ await storage.saveFederationPeer(newPeer);
395
397
  logger.info(`Join complete: peer ${targetNodeId} added and keys exchanged`);
396
398
  }
399
+ else if (result.success) {
400
+ logger.info(`Join complete: peer ${targetNodeId} added (no public key received)`);
401
+ }
397
402
  else {
398
403
  logger.warn(`Join: peer ${targetNodeId} saved but key exchange failed: ${result.error}`);
399
404
  }
400
405
  emitChange('federation');
401
406
  }
402
407
  /** Background poll for remote approval, then complete the join. */
403
- function pollForApprovalAndComplete(targetUrl, targetNodeId, requestId, targetPublicKey, config, storage, peers) {
408
+ function pollForApprovalAndComplete(targetUrl, targetNodeId, requestId, config, storage, peers) {
404
409
  const startedAt = Date.now();
405
410
  const timer = setInterval(async () => {
406
411
  if (Date.now() - startedAt > MAX_POLL_DURATION_MS) {
@@ -417,7 +422,7 @@ function pollForApprovalAndComplete(targetUrl, targetNodeId, requestId, targetPu
417
422
  if (status === 'approved' || status === 'auto_approved') {
418
423
  clearInterval(timer);
419
424
  logger.info(`Join request ${requestId} approved by ${targetNodeId}`);
420
- await completeJoin(targetUrl, targetNodeId, targetPublicKey, config, storage, peers);
425
+ await completeJoin(targetUrl, targetNodeId, config, storage, peers);
421
426
  }
422
427
  else if (status === 'rejected') {
423
428
  clearInterval(timer);