aimeat 1.29.0 → 1.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/en.json +25 -1
- package/dist/locales/fi.json +25 -1
- package/dist/public/css/views/inbox.css +62 -1
- package/dist/public/css/views/profile.css +18 -2
- package/dist/public/js/services/messages.js +4 -3
- package/dist/public/llms-template.txt +38 -1
- package/dist/public/views/profile/agents-tab.js +1 -0
- package/dist/public/views/profile/inbox-tab.js +175 -28
- package/dist/public/views/profile/landing-page.js +67 -0
- package/dist/public/views/profile/organisms-tab.js +61 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +13 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +62 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/index.js +2 -0
- package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +54 -0
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +194 -2
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +4 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +1 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +30 -1
- package/dist/src/generated/prisma-postgres/index.js +4 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +1 -0
- package/dist/src/generated/prisma-postgres/wasm.js +4 -3
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +3 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +33 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +6 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/dm-messages.d.ts +20 -0
- package/dist/src/mcp/dm-messages.d.ts.map +1 -0
- package/dist/src/mcp/dm-messages.js +120 -0
- package/dist/src/mcp/dm-messages.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +3 -1
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/models/message-schemas.d.ts +3 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +4 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +2 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/bootstrap.js +1 -1
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/catalogue.d.ts.map +1 -1
- package/dist/src/routes/catalogue.js +17 -8
- package/dist/src/routes/catalogue.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +40 -16
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +10 -3
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +22 -19
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +2 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +93 -104
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/stats.d.ts.map +1 -1
- package/dist/src/routes/stats.js +6 -0
- package/dist/src/routes/stats.js.map +1 -1
- package/dist/src/routes/usage.d.ts +17 -0
- package/dist/src/routes/usage.d.ts.map +1 -0
- package/dist/src/routes/usage.js +35 -0
- package/dist/src/routes/usage.js.map +1 -0
- package/dist/src/routes/wallet.d.ts +2 -0
- package/dist/src/routes/wallet.d.ts.map +1 -1
- package/dist/src/routes/wallet.js +13 -6
- package/dist/src/routes/wallet.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +18 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/cache.d.ts +61 -0
- package/dist/src/services/cache.d.ts.map +1 -0
- package/dist/src/services/cache.js +143 -0
- package/dist/src/services/cache.js.map +1 -0
- package/dist/src/services/message-delivery.d.ts.map +1 -1
- package/dist/src/services/message-delivery.js +8 -3
- package/dist/src/services/message-delivery.js.map +1 -1
- package/dist/src/services/message-send.d.ts +13 -0
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +43 -6
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/usage-summary.d.ts +77 -0
- package/dist/src/services/usage-summary.d.ts.map +1 -0
- package/dist/src/services/usage-summary.js +85 -0
- package/dist/src/services/usage-summary.js.map +1 -0
- package/dist/src/storage/interface.d.ts +5 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +15 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +49 -1
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +15 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +16 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts +15 -0
- package/dist/src/storage/providers/sqlite/repos/direct-message.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/direct-message.js +28 -2
- package/dist/src/storage/providers/sqlite/repos/direct-message.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +4 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/direct-message.repository.d.ts +20 -0
- package/dist/src/storage/repositories/direct-message.repository.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +1 -0
- package/prisma/schema.prisma +1 -0
package/dist/locales/en.json
CHANGED
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
"inbox.empty": "Nothing here yet.",
|
|
19
19
|
"inbox.attachmentPending": "attachment pending",
|
|
20
20
|
"inbox.attachmentExpired": "attachment expired",
|
|
21
|
+
"inbox.attachmentOpen": "Open in a new tab",
|
|
22
|
+
"inbox.attachmentView": "View",
|
|
23
|
+
"inbox.attachmentOpenRaw": "Open raw",
|
|
24
|
+
"inbox.attachmentDownload": "Download",
|
|
25
|
+
"inbox.attachmentLoadError": "Could not load this attachment.",
|
|
26
|
+
"inbox.close": "Close",
|
|
27
|
+
"inbox.directThread": "Direct",
|
|
28
|
+
"inbox.subjectPlaceholder": "Subject (optional — starts a new thread)",
|
|
21
29
|
"admin.messages.title": "Direct messages",
|
|
22
30
|
"admin.messages.desc": "Delivery telemetry for user-to-user messages. No message content or participant identities are shown.",
|
|
23
31
|
"admin.messages.total": "Total sent",
|
|
@@ -1108,6 +1116,7 @@
|
|
|
1108
1116
|
"domainStorage": "Storage (files)",
|
|
1109
1117
|
"domainWork": "Work",
|
|
1110
1118
|
"domainSocial": "Social",
|
|
1119
|
+
"domainMessages": "Direct messages (inbox)",
|
|
1111
1120
|
"domainWallet": "Wallet",
|
|
1112
1121
|
"domainConsent": "Consent",
|
|
1113
1122
|
"domainTunnel": "Tunnel",
|
|
@@ -1122,6 +1131,7 @@
|
|
|
1122
1131
|
"permWrite": "Write",
|
|
1123
1132
|
"permDelete": "Delete",
|
|
1124
1133
|
"permRequest": "Request",
|
|
1134
|
+
"permSend": "Send",
|
|
1125
1135
|
"permAccept": "Accept",
|
|
1126
1136
|
"permPublish": "Publish",
|
|
1127
1137
|
"permManage": "Manage",
|
|
@@ -1506,7 +1516,7 @@
|
|
|
1506
1516
|
"readme": "README",
|
|
1507
1517
|
"integration": "Integration",
|
|
1508
1518
|
"tasks": "Tasks",
|
|
1509
|
-
"messages": "
|
|
1519
|
+
"messages": "Chat",
|
|
1510
1520
|
"data_access": "Memory",
|
|
1511
1521
|
"directives": "Directives",
|
|
1512
1522
|
"agent_config": "Agent Config",
|
|
@@ -3481,6 +3491,19 @@
|
|
|
3481
3491
|
"viewBtn": "View",
|
|
3482
3492
|
"continueTitle": "Continue",
|
|
3483
3493
|
"agentsTitle": "Agents",
|
|
3494
|
+
"usageTitle": "Usage & quotas",
|
|
3495
|
+
"usageMemory": "Memory",
|
|
3496
|
+
"usageStorage": "Files",
|
|
3497
|
+
"usageMicro": "Micro-memory",
|
|
3498
|
+
"usageFilesWord": "files",
|
|
3499
|
+
"usageSetsWord": "sets",
|
|
3500
|
+
"usageAgents": "Agents",
|
|
3501
|
+
"usageOrganisms": "Organisms",
|
|
3502
|
+
"usageApps": "Apps",
|
|
3503
|
+
"usageEcoApps": "Connected apps",
|
|
3504
|
+
"usageExtensions": "Extensions",
|
|
3505
|
+
"usageCortexes": "Cortexes",
|
|
3506
|
+
"usageServices": "Services",
|
|
3484
3507
|
"activeTodayCount": "{n} active today",
|
|
3485
3508
|
"agentActiveToday": "active today",
|
|
3486
3509
|
"nextRunAt": "next run {time}",
|
|
@@ -6359,6 +6382,7 @@
|
|
|
6359
6382
|
"search": {
|
|
6360
6383
|
"wsPlaceholder": "Search this workspace…",
|
|
6361
6384
|
"orgPlaceholder": "Search this organism…",
|
|
6385
|
+
"allOrgsPlaceholder": "Search across all organisms…",
|
|
6362
6386
|
"clear": "Clear",
|
|
6363
6387
|
"noMatches": "No matches",
|
|
6364
6388
|
"paletteTitle": "Search everything",
|
package/dist/locales/fi.json
CHANGED
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
"inbox.empty": "Ei mitään vielä.",
|
|
19
19
|
"inbox.attachmentPending": "liite odottaa",
|
|
20
20
|
"inbox.attachmentExpired": "liite vanhentui",
|
|
21
|
+
"inbox.attachmentOpen": "Avaa uudessa välilehdessä",
|
|
22
|
+
"inbox.attachmentView": "Näytä",
|
|
23
|
+
"inbox.attachmentOpenRaw": "Avaa raakana",
|
|
24
|
+
"inbox.attachmentDownload": "Lataa",
|
|
25
|
+
"inbox.attachmentLoadError": "Liitettä ei voitu ladata.",
|
|
26
|
+
"inbox.close": "Sulje",
|
|
27
|
+
"inbox.directThread": "Suora",
|
|
28
|
+
"inbox.subjectPlaceholder": "Aihe (valinnainen — aloittaa uuden ketjun)",
|
|
21
29
|
"admin.messages.title": "Suorat viestit",
|
|
22
30
|
"admin.messages.desc": "Toimitustelemetria käyttäjien välisille viesteille. Viestien sisältöä tai osapuolten henkilöllisyyttä ei näytetä.",
|
|
23
31
|
"admin.messages.total": "Lähetetty yhteensä",
|
|
@@ -1108,6 +1116,7 @@
|
|
|
1108
1116
|
"domainStorage": "Tallennustila (tiedostot)",
|
|
1109
1117
|
"domainWork": "Työ",
|
|
1110
1118
|
"domainSocial": "Sosiaalinen",
|
|
1119
|
+
"domainMessages": "Suorat viestit (postilaatikko)",
|
|
1111
1120
|
"domainWallet": "Lompakko",
|
|
1112
1121
|
"domainConsent": "Suostumus",
|
|
1113
1122
|
"domainTunnel": "Tunneli",
|
|
@@ -1122,6 +1131,7 @@
|
|
|
1122
1131
|
"permWrite": "Kirjoitus",
|
|
1123
1132
|
"permDelete": "Poisto",
|
|
1124
1133
|
"permRequest": "Pyyntö",
|
|
1134
|
+
"permSend": "Lähetys",
|
|
1125
1135
|
"permAccept": "Hyväksyntä",
|
|
1126
1136
|
"permPublish": "Julkaisu",
|
|
1127
1137
|
"permManage": "Hallinta",
|
|
@@ -1506,7 +1516,7 @@
|
|
|
1506
1516
|
"readme": "README",
|
|
1507
1517
|
"integration": "Integraatio",
|
|
1508
1518
|
"tasks": "Tehtävät",
|
|
1509
|
-
"messages": "
|
|
1519
|
+
"messages": "Keskustelu",
|
|
1510
1520
|
"data_access": "Muisti",
|
|
1511
1521
|
"directives": "Ohjeistukset",
|
|
1512
1522
|
"agent_config": "Agenttiasetukset",
|
|
@@ -3481,6 +3491,19 @@
|
|
|
3481
3491
|
"viewBtn": "Avaa",
|
|
3482
3492
|
"continueTitle": "Jatka",
|
|
3483
3493
|
"agentsTitle": "Agentit",
|
|
3494
|
+
"usageTitle": "Käyttö & kiintiöt",
|
|
3495
|
+
"usageMemory": "Muisti",
|
|
3496
|
+
"usageStorage": "Tiedostot",
|
|
3497
|
+
"usageMicro": "Mikromuisti",
|
|
3498
|
+
"usageFilesWord": "tiedostoa",
|
|
3499
|
+
"usageSetsWord": "settiä",
|
|
3500
|
+
"usageAgents": "Agentit",
|
|
3501
|
+
"usageOrganisms": "Organismit",
|
|
3502
|
+
"usageApps": "Sovellukset",
|
|
3503
|
+
"usageEcoApps": "Liitetyt sovellukset",
|
|
3504
|
+
"usageExtensions": "Laajennukset",
|
|
3505
|
+
"usageCortexes": "Korteksit",
|
|
3506
|
+
"usageServices": "Palvelut",
|
|
3484
3507
|
"activeTodayCount": "{n} aktiivista tänään",
|
|
3485
3508
|
"agentActiveToday": "aktiivinen tänään",
|
|
3486
3509
|
"nextRunAt": "seuraava ajo {time}",
|
|
@@ -6359,6 +6382,7 @@
|
|
|
6359
6382
|
"search": {
|
|
6360
6383
|
"wsPlaceholder": "Hae tästä työtilasta…",
|
|
6361
6384
|
"orgPlaceholder": "Hae tästä organismista…",
|
|
6385
|
+
"allOrgsPlaceholder": "Hae kaikista organismeista…",
|
|
6362
6386
|
"clear": "Tyhjennä",
|
|
6363
6387
|
"noMatches": "Ei osumia",
|
|
6364
6388
|
"paletteTitle": "Hae kaikkea",
|
|
@@ -116,6 +116,25 @@
|
|
|
116
116
|
}
|
|
117
117
|
.inbox-conv:hover { background: var(--card-bg-alt); }
|
|
118
118
|
.inbox-conv--active { background: var(--accent-subtle); }
|
|
119
|
+
|
|
120
|
+
/* A person group: header (the human) + their threads (human + agents) nested + indented beneath. */
|
|
121
|
+
.inbox-conv-group { margin-bottom: 2px; }
|
|
122
|
+
.inbox-conv-group-head {
|
|
123
|
+
display: flex; align-items: center; gap: 8px;
|
|
124
|
+
padding: 6px 8px 2px; font-weight: 600;
|
|
125
|
+
}
|
|
126
|
+
.inbox-conv-group-head .inbox-name { flex: 1; min-width: 0; }
|
|
127
|
+
.inbox-conv--nested { padding-left: 18px; }
|
|
128
|
+
.inbox-conv--nested .inbox-name { font-weight: 400; }
|
|
129
|
+
.inbox-conv-subico {
|
|
130
|
+
width: 28px; flex: none; text-align: center; font-size: 15px; opacity: 0.8;
|
|
131
|
+
}
|
|
132
|
+
/* Subject thread chips (conversation list + thread header). */
|
|
133
|
+
.inbox-conv-subject {
|
|
134
|
+
flex: none; max-width: 60%; font-size: 11px; color: var(--accent);
|
|
135
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
136
|
+
}
|
|
137
|
+
.inbox-thread-subject { font-size: 12px; color: var(--accent); margin-top: 1px; }
|
|
119
138
|
.inbox-conv-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
120
139
|
.inbox-conv-line1, .inbox-conv-line2 { display: flex; align-items: center; gap: 6px; }
|
|
121
140
|
.inbox-conv-line1 .inbox-name { flex: 1; }
|
|
@@ -228,10 +247,52 @@
|
|
|
228
247
|
text-decoration: none;
|
|
229
248
|
color: inherit;
|
|
230
249
|
}
|
|
231
|
-
.inbox-attach-chip--pending { opacity: 0.55;
|
|
250
|
+
.inbox-attach-chip--pending { opacity: 0.55; }
|
|
232
251
|
.inbox-attach-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
233
252
|
.inbox-attach-pending { color: var(--text-dim); font-style: italic; }
|
|
234
253
|
|
|
254
|
+
/* The chip's main affordance (link or button) opens/views the file; the download button sits beside it. */
|
|
255
|
+
.inbox-attach-open {
|
|
256
|
+
display: inline-flex; align-items: center; gap: 5px; min-width: 0;
|
|
257
|
+
background: none; border: none; padding: 0; margin: 0; cursor: pointer;
|
|
258
|
+
font: inherit; color: inherit; text-decoration: none;
|
|
259
|
+
}
|
|
260
|
+
.inbox-attach-open:hover .inbox-attach-name { text-decoration: underline; }
|
|
261
|
+
.inbox-attach-dl {
|
|
262
|
+
flex-shrink: 0; text-decoration: none; color: var(--text-dim);
|
|
263
|
+
font-size: 13px; line-height: 1; padding: 2px 4px; border-radius: var(--radius-xs);
|
|
264
|
+
}
|
|
265
|
+
.inbox-attach-dl:hover { color: var(--text); background: var(--bg-dim); }
|
|
266
|
+
|
|
267
|
+
/* Image attachment: thumbnail + caption row (name + download). */
|
|
268
|
+
.inbox-attach-item {
|
|
269
|
+
display: inline-flex; flex-direction: column; gap: 4px; max-width: 200px;
|
|
270
|
+
}
|
|
271
|
+
.inbox-attach-thumb-link { display: block; line-height: 0; }
|
|
272
|
+
.inbox-attach-thumb {
|
|
273
|
+
max-width: 200px; max-height: 160px; border-radius: var(--radius-sm);
|
|
274
|
+
border: 1px solid var(--border); object-fit: cover; cursor: zoom-in;
|
|
275
|
+
}
|
|
276
|
+
.inbox-attach-cap { display: flex; align-items: center; gap: 6px; font-size: 12px; max-width: 200px; }
|
|
277
|
+
.inbox-attach-cap .inbox-attach-name { flex: 1; min-width: 0; color: var(--text-dim); }
|
|
278
|
+
|
|
279
|
+
/* In-app markdown viewer overlay. */
|
|
280
|
+
.inbox-mdviewer-overlay {
|
|
281
|
+
position: fixed; inset: 0; z-index: 1200; background: rgba(0, 0, 0, 0.6);
|
|
282
|
+
display: flex; align-items: center; justify-content: center; padding: 24px;
|
|
283
|
+
}
|
|
284
|
+
.inbox-mdviewer {
|
|
285
|
+
display: flex; flex-direction: column; width: min(860px, 100%); max-height: 88vh;
|
|
286
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden;
|
|
287
|
+
}
|
|
288
|
+
.inbox-mdviewer-head {
|
|
289
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
290
|
+
padding: 10px 14px; border-bottom: 1px solid var(--border); background: var(--surface);
|
|
291
|
+
}
|
|
292
|
+
.inbox-mdviewer-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
293
|
+
.inbox-mdviewer-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
294
|
+
.inbox-mdviewer-body { padding: 18px 22px; overflow: auto; }
|
|
295
|
+
|
|
235
296
|
/* ── Composer ── */
|
|
236
297
|
.inbox-compose-fields { padding: 12px 12px 0; }
|
|
237
298
|
.inbox-composer {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* wallet, memory, agents, extensions, packages, knowledge, generator, and
|
|
5
5
|
* edit panels. Consumes theme.css tokens so it flips with dark/light mode.
|
|
6
6
|
* @version-history
|
|
7
|
+
* v2.7.0 — 2026-06-22 — Home "Usage & quotas" card: .pf-usage-card/.pf-usage-row/.pf-usage-bar/
|
|
8
|
+
* .pf-usage-fill (quota bars) + .pf-usage-chips/.pf-usage-chip (count chips).
|
|
7
9
|
* v2.6.0 — 2026-06-22 — Scalable Memory tab: .pf-mem-quota* (storage-usage bar), .pf-mem-size
|
|
8
10
|
* (value-size column), .mem-tools-bar/.mem-import-group (server search + export/import row),
|
|
9
11
|
* .mem-group-actions (hover search/delete on group headers), .mem-search-summary.
|
|
@@ -2207,8 +2209,8 @@
|
|
|
2207
2209
|
.pj-empty { color: var(--muted); font-size: 0.85rem; padding: 0.2rem 0; }
|
|
2208
2210
|
|
|
2209
2211
|
/* ── In-context content search (Kerros 1): filter tabs to matches + grouped result list ── */
|
|
2210
|
-
.pj-ws-searchbar { display: flex; align-items: center; gap: 0.5rem; margin: 0.4rem 0 0.6rem; }
|
|
2211
|
-
.pj-ws-searchbar .search-bar { flex: 1; }
|
|
2212
|
+
.pj-ws-searchbar, .pj-org-globalsearch { display: flex; align-items: center; gap: 0.5rem; margin: 0.4rem 0 0.6rem; }
|
|
2213
|
+
.pj-ws-searchbar .search-bar, .pj-org-globalsearch .search-bar { flex: 1; }
|
|
2212
2214
|
.pj-org-tab-match { background: var(--accent); color: #fff; }
|
|
2213
2215
|
.pj-search-results { margin-top: 0.4rem; }
|
|
2214
2216
|
.pj-search-group { margin: 0 0 0.9rem; }
|
|
@@ -2723,6 +2725,20 @@
|
|
|
2723
2725
|
.pf-home-row-meta.pf-ok { color: var(--success-fg, var(--text)); }
|
|
2724
2726
|
.pf-lp-stat-link { background: none; border: none; padding: 0.15rem 0.35rem; border-radius: 8px; cursor: pointer; font: inherit; color: inherit; }
|
|
2725
2727
|
.pf-lp-stat-link:hover { background: var(--bg-surface); }
|
|
2728
|
+
/* Home "Usage & quotas" card — quota bars + count chips */
|
|
2729
|
+
.pf-usage-card { grid-column: 1 / -1; }
|
|
2730
|
+
.pf-usage-row { margin-bottom: 0.55rem; }
|
|
2731
|
+
.pf-usage-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; margin-bottom: 0.25rem; }
|
|
2732
|
+
.pf-usage-label { font-size: 0.82rem; font-weight: 600; color: var(--text); }
|
|
2733
|
+
.pf-usage-bar { height: 6px; border-radius: 999px; background: var(--bg-dim); overflow: hidden; }
|
|
2734
|
+
.pf-usage-fill { height: 100%; background: var(--accent); border-radius: 999px; transition: width 0.3s; }
|
|
2735
|
+
.pf-usage-fill--danger { background: var(--danger); }
|
|
2736
|
+
.pf-usage-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.7rem; }
|
|
2737
|
+
.pf-usage-chip { display: inline-flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; padding: 0.35rem 0.6rem; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-surface); cursor: pointer; text-align: left; }
|
|
2738
|
+
.pf-usage-chip[disabled] { cursor: default; }
|
|
2739
|
+
.pf-usage-chip:not([disabled]):hover { border-color: var(--accent); }
|
|
2740
|
+
.pf-usage-chip-val { font-size: 0.95rem; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums; }
|
|
2741
|
+
.pf-usage-chip-label { font-size: 0.7rem; color: var(--muted); }
|
|
2726
2742
|
.pf-promo-dismiss { float: right; background: none; border: none; cursor: pointer; color: var(--muted); font-size: 0.85rem; padding: 0 0.2rem; }
|
|
2727
2743
|
.pf-promo-dismiss:hover { color: var(--text); }
|
|
2728
2744
|
/* Edit profile / password dialogs */
|
|
@@ -15,11 +15,12 @@ import { api, apiGet } from '/js/api.js';
|
|
|
15
15
|
|
|
16
16
|
const enc = encodeURIComponent;
|
|
17
17
|
|
|
18
|
-
/** Send a direct message. `attachments` is an array of descriptors (storage_key, mime, size, kind, inline, id).
|
|
19
|
-
|
|
18
|
+
/** Send a direct message. `attachments` is an array of descriptors (storage_key, mime, size, kind, inline, id).
|
|
19
|
+
* `subject` opens a new topic thread; `conversationId` continues a specific existing thread. */
|
|
20
|
+
export async function send({ to, body, attachments, replyTo, subject, conversationId } = /** @type {{ to?: any, body?: any, attachments?: any, replyTo?: any, subject?: any, conversationId?: any }} */ ({})) {
|
|
20
21
|
return api('/v1/messages', {
|
|
21
22
|
method: 'POST',
|
|
22
|
-
body: JSON.stringify({ to, body, attachments, reply_to: replyTo }),
|
|
23
|
+
body: JSON.stringify({ to, body, attachments, reply_to: replyTo, subject, conversation_id: conversationId }),
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -1059,7 +1059,11 @@ POST /v1/agents/me/telemetry
|
|
|
1059
1059
|
```
|
|
1060
1060
|
Types: `llm_call`, `tool_call`, `agent_report`
|
|
1061
1061
|
|
|
1062
|
-
### Messages — Send
|
|
1062
|
+
### Messages — Send (agent ↔ your owner)
|
|
1063
|
+
|
|
1064
|
+
This is the private dashboard channel between you and YOUR OWNER (task coordination, prompts). It is NOT
|
|
1065
|
+
federated and does not reach anyone else. To message other people/agents across the network, use the
|
|
1066
|
+
Federated Direct Messages below.
|
|
1063
1067
|
|
|
1064
1068
|
```
|
|
1065
1069
|
POST /v1/agents/me/messages
|
|
@@ -1070,6 +1074,39 @@ POST /v1/agents/me/messages
|
|
|
1070
1074
|
}
|
|
1071
1075
|
```
|
|
1072
1076
|
|
|
1077
|
+
### Federated Direct Messages (Inbox) — message anyone on the network
|
|
1078
|
+
|
|
1079
|
+
A separate, federation-wide messenger (the human "Postilaatikko"). You send FROM your own agent identity
|
|
1080
|
+
TO any person (`owner@node`), agent (`agent#owner@node`) or app (`eco:app#owner@node`), across nodes. The
|
|
1081
|
+
recipient sees the message is from you. First contact lands in their requests until they accept.
|
|
1082
|
+
|
|
1083
|
+
Requires scopes: `messages:send` (send), `messages:read` (read replies). MCP tools: `aimeat_dm_send`,
|
|
1084
|
+
`aimeat_dm_inbox`, `aimeat_dm_thread` — distinct from the `aimeat_message_*` owner-dashboard tools above.
|
|
1085
|
+
|
|
1086
|
+
Send (REST equivalent of `aimeat_dm_send`):
|
|
1087
|
+
```
|
|
1088
|
+
POST {{BASE_URL}}/v1/messages
|
|
1089
|
+
{
|
|
1090
|
+
"to": "alice@aimeat-fi-001", // or "claude#alice@aimeat-fi-001", or "eco:app#alice@aimeat-fi-001"
|
|
1091
|
+
"body": "Markdown supported.",
|
|
1092
|
+
"reply_to": "<message-id>", // optional — keep the same thread
|
|
1093
|
+
"subject": "Project Falcon", // optional — open a NEW topic thread (avoids one endless chat)
|
|
1094
|
+
"conversation_id": "<thread-id>", // optional — continue a specific existing thread
|
|
1095
|
+
"attachments": [ // optional — up to 20
|
|
1096
|
+
{ "storage_key": "<key>", "mime": "image/png", "kind": "image", "size": 2048, "name": "shot.png" }
|
|
1097
|
+
]
|
|
1098
|
+
}
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
Attachments travel via storage, NOT through MCP/the body: upload each file first (`aimeat_storage_upload`
|
|
1102
|
+
presigned, or `POST /v1/storage`), then pass the returned storage key(s) in `attachments`.
|
|
1103
|
+
|
|
1104
|
+
Read replies addressed to you:
|
|
1105
|
+
```
|
|
1106
|
+
GET {{BASE_URL}}/v1/messages/agent-inbox — recent DMs addressed to you (newest first)
|
|
1107
|
+
GET {{BASE_URL}}/v1/messages/agent-thread/{conversationId} — a full thread (your sent + received)
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1073
1110
|
### Onboarding — Confirm a step
|
|
1074
1111
|
|
|
1075
1112
|
```
|
|
@@ -77,6 +77,7 @@ const SCOPE_DOMAINS = [
|
|
|
77
77
|
{ key: 'storage', permissions: ['read', 'write'] },
|
|
78
78
|
{ key: 'work', permissions: ['request', 'read', 'accept', 'publish'] },
|
|
79
79
|
{ key: 'social', permissions: ['read', 'write'] },
|
|
80
|
+
{ key: 'messages', permissions: ['send', 'read'] },
|
|
80
81
|
{ key: 'wallet', permissions: ['read'] },
|
|
81
82
|
{ key: 'consent', permissions: ['manage'] },
|
|
82
83
|
{ key: 'tunnel', permissions: ['connect'] },
|
|
@@ -85,6 +85,33 @@ function peerName(id) {
|
|
|
85
85
|
return beforeAt.includes('#') ? beforeAt.split('#').pop() : beforeAt;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// ── Grouping a person + their agents ──
|
|
89
|
+
// A peer is a human GHII (owner@node), an agent GAII (agent#owner@node) or an app GEAI
|
|
90
|
+
// (eco:app#owner@node). They all belong to ONE person — the owner — so we group conversations under
|
|
91
|
+
// the owner key and keep each thread separate beneath it.
|
|
92
|
+
function ownerKeyOf(ghii) {
|
|
93
|
+
const s = String(ghii || '');
|
|
94
|
+
const hash = s.indexOf('#'); // 'agent#owner@node' / 'eco:app#owner@node' → 'owner@node'
|
|
95
|
+
return hash >= 0 ? s.slice(hash + 1) : s; // human 'owner@node' → itself
|
|
96
|
+
}
|
|
97
|
+
function isAgentPeer(ghii) { return String(ghii).includes('#'); }
|
|
98
|
+
function ownerDisplayName(ownerKey) { return String(ownerKey).split('@')[0]; }
|
|
99
|
+
/** Label for one thread INSIDE a person group: the agent/app id before '#', or null for the human thread. */
|
|
100
|
+
function subThreadLabel(ghii) {
|
|
101
|
+
const at = String(ghii || '').split('@')[0];
|
|
102
|
+
return at.includes('#') ? at.split('#')[0] : null;
|
|
103
|
+
}
|
|
104
|
+
/** Group conversations by the owner behind each peer, preserving input (recency) order. */
|
|
105
|
+
function groupConversations(conversations) {
|
|
106
|
+
const map = new Map();
|
|
107
|
+
for (const c of conversations) {
|
|
108
|
+
const key = ownerKeyOf(c.peerGhii);
|
|
109
|
+
if (!map.has(key)) map.set(key, { ownerKey: key, convs: [] });
|
|
110
|
+
map.get(key).convs.push(c);
|
|
111
|
+
}
|
|
112
|
+
return [...map.values()];
|
|
113
|
+
}
|
|
114
|
+
|
|
88
115
|
function Avatar({ seed, size = 36 }) {
|
|
89
116
|
const svg = minidenticon(seed || 'user');
|
|
90
117
|
return html`<span class="inbox-avatar" style=${`width:${size}px;height:${size}px`}
|
|
@@ -134,7 +161,92 @@ function trackStateLabel(state) {
|
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
|
|
137
|
-
|
|
164
|
+
/** Classify an attachment for rendering: how to view it (thumbnail / native tab / markdown viewer). */
|
|
165
|
+
const ATTACH_ICO = { image: '🖼', pdf: '📄', markdown: '📄', audio: '🎵', video: '🎬', file: '📎' };
|
|
166
|
+
function attachKind(a) {
|
|
167
|
+
const mime = a.mime || '';
|
|
168
|
+
const name = a.name || a.storageKey || '';
|
|
169
|
+
if (a.kind === 'image' || /^image\//.test(mime)) return 'image';
|
|
170
|
+
if (mime === 'application/pdf' || /\.pdf$/i.test(name)) return 'pdf';
|
|
171
|
+
if (/markdown/.test(mime) || /\.(md|markdown|mdx)$/i.test(name)) return 'markdown';
|
|
172
|
+
if (a.kind === 'audio' || /^audio\//.test(mime)) return 'audio';
|
|
173
|
+
if (a.kind === 'video' || /^video\//.test(mime)) return 'video';
|
|
174
|
+
return 'file';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** One received/sent attachment. Images render as a thumbnail (click → full-size in a new tab);
|
|
178
|
+
* PDF/audio/video/file open natively in a new tab; markdown opens the in-app rendered viewer. Every
|
|
179
|
+
* ready attachment gets a download button. Not-yet-duplicated / expired attachments show their state. */
|
|
180
|
+
function AttachmentItem({ a, url, onOpenMarkdown }) {
|
|
181
|
+
const kind = attachKind(a);
|
|
182
|
+
const name = a.name || a.storageKey;
|
|
183
|
+
const ready = !!url && !a.expired;
|
|
184
|
+
|
|
185
|
+
if (!ready) {
|
|
186
|
+
const status = a.expired ? t('inbox.attachmentExpired') : (a.mode !== 'duplicate' ? t('inbox.attachmentPending') : null);
|
|
187
|
+
return html`<div class="inbox-attach-chip inbox-attach-chip--pending">
|
|
188
|
+
<span class="inbox-attach-ico">${ATTACH_ICO[kind]}</span>
|
|
189
|
+
<span class="inbox-attach-name">${escHtml(name)}</span>
|
|
190
|
+
${status ? html`<span class="inbox-attach-pending">${status}</span>` : null}
|
|
191
|
+
</div>`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const download = html`<a class="inbox-attach-dl" href=${url} download=${name} title=${t('inbox.attachmentDownload')}>⬇</a>`;
|
|
195
|
+
|
|
196
|
+
if (kind === 'image') {
|
|
197
|
+
return html`<div class="inbox-attach-item">
|
|
198
|
+
<a class="inbox-attach-thumb-link" href=${url} target="_blank" rel="noopener" title=${t('inbox.attachmentOpen')}>
|
|
199
|
+
<img class="inbox-attach-thumb" src=${url} alt=${escHtml(name)} loading="lazy" />
|
|
200
|
+
</a>
|
|
201
|
+
<div class="inbox-attach-cap"><span class="inbox-attach-name">${escHtml(name)}</span>${download}</div>
|
|
202
|
+
</div>`;
|
|
203
|
+
}
|
|
204
|
+
if (kind === 'markdown') {
|
|
205
|
+
return html`<div class="inbox-attach-chip">
|
|
206
|
+
<button class="inbox-attach-open" onClick=${() => onOpenMarkdown?.(url, name)} title=${t('inbox.attachmentView')}>
|
|
207
|
+
<span class="inbox-attach-ico">📄</span><span class="inbox-attach-name">${escHtml(name)}</span>
|
|
208
|
+
</button>${download}
|
|
209
|
+
</div>`;
|
|
210
|
+
}
|
|
211
|
+
// pdf / audio / video / file — let the browser open it in a new tab.
|
|
212
|
+
return html`<div class="inbox-attach-chip">
|
|
213
|
+
<a class="inbox-attach-open" href=${url} target="_blank" rel="noopener" title=${t('inbox.attachmentOpen')}>
|
|
214
|
+
<span class="inbox-attach-ico">${ATTACH_ICO[kind]}</span><span class="inbox-attach-name">${escHtml(name)}</span>
|
|
215
|
+
</a>${download}
|
|
216
|
+
</div>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** In-app viewer for a markdown attachment — browsers don't render .md, so we fetch the (same-origin,
|
|
220
|
+
* presigned) file and render it with the shared safe Markdown component. Offers open-raw + download. */
|
|
221
|
+
function MarkdownViewer({ url, name, onClose }) {
|
|
222
|
+
const [text, setText] = useState(null);
|
|
223
|
+
const [failed, setFailed] = useState(false);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
let alive = true;
|
|
226
|
+
fetch(url).then(r => r.ok ? r.text() : Promise.reject(new Error('http'))).then(tx => { if (alive) setText(tx); }).catch(() => { if (alive) setFailed(true); });
|
|
227
|
+
return () => { alive = false; };
|
|
228
|
+
}, [url]);
|
|
229
|
+
return html`
|
|
230
|
+
<div class="inbox-mdviewer-overlay" onClick=${onClose}>
|
|
231
|
+
<div class="inbox-mdviewer" onClick=${e => e.stopPropagation()}>
|
|
232
|
+
<div class="inbox-mdviewer-head">
|
|
233
|
+
<span class="inbox-mdviewer-title">📄 ${escHtml(name)}</span>
|
|
234
|
+
<div class="inbox-mdviewer-actions">
|
|
235
|
+
<a class="btn-ghost btn-sm" href=${url} target="_blank" rel="noopener">${t('inbox.attachmentOpenRaw')}</a>
|
|
236
|
+
<a class="btn-ghost btn-sm" href=${url} download=${name}>${t('inbox.attachmentDownload')}</a>
|
|
237
|
+
<button class="btn-ghost btn-sm" onClick=${onClose} title=${t('inbox.close')}>✕</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<div class="inbox-mdviewer-body">
|
|
241
|
+
${failed ? html`<div class="inbox-empty-sm">${t('inbox.attachmentLoadError')}</div>`
|
|
242
|
+
: text === null ? html`<div class="inbox-empty-sm">…</div>`
|
|
243
|
+
: html`<${Markdown} text=${text} />`}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, tracked, onOpenMarkdown }) {
|
|
138
250
|
const nonInline = (msg.attachments || []).filter(a => !a.inline);
|
|
139
251
|
const expiredIds = new Set((msg.attachments || []).filter(a => a.expired).map(a => a.id));
|
|
140
252
|
const trk = tracked ? trackStateLabel(tracked.state) : null;
|
|
@@ -152,14 +264,7 @@ function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, tracked })
|
|
|
152
264
|
<div class="inbox-bubble-body"><${Markdown} text=${prepareBody(msg.body, urlMap, expiredIds)} /></div>
|
|
153
265
|
${nonInline.length > 0 && html`
|
|
154
266
|
<div class="inbox-attach-row">
|
|
155
|
-
${nonInline.map(a => html
|
|
156
|
-
<a key=${a.id} class=${`inbox-attach-chip${(urlMap[a.id] && !a.expired) ? '' : ' inbox-attach-chip--pending'}`}
|
|
157
|
-
href=${(urlMap[a.id] && !a.expired) ? urlMap[a.id] : '#'} target="_blank" rel="noopener">
|
|
158
|
-
<span class="inbox-attach-ico">${a.kind === 'image' ? '🖼' : a.kind === 'audio' ? '🎵' : a.kind === 'video' ? '🎬' : '📎'}</span>
|
|
159
|
-
<span class="inbox-attach-name">${escHtml(a.name || a.storageKey)}</span>
|
|
160
|
-
${a.expired ? html`<span class="inbox-attach-pending">${t('inbox.attachmentExpired')}</span>`
|
|
161
|
-
: a.mode !== 'duplicate' ? html`<span class="inbox-attach-pending">${t('inbox.attachmentPending')}</span>` : null}
|
|
162
|
-
</a>`)}
|
|
267
|
+
${nonInline.map(a => html`<${AttachmentItem} key=${a.id} a=${a} url=${urlMap[a.id]} onOpenMarkdown=${onOpenMarkdown} />`)}
|
|
163
268
|
</div>`}
|
|
164
269
|
<div class="inbox-bubble-meta">
|
|
165
270
|
${trk ? html`<span class=${`inbox-track-badge inbox-track-badge--${trk.tone}`} title=${t('inbox.trackResponse')}>🔗 ${trk.text}</span>` : null}
|
|
@@ -256,6 +361,8 @@ export default function InboxTab({ showToast }) {
|
|
|
256
361
|
const [activeConv, setActiveConv] = useState(null); // { conversationId, peerGhii }
|
|
257
362
|
const [thread, setThread] = useState([]);
|
|
258
363
|
const [urlMap, setUrlMap] = useState({});
|
|
364
|
+
const [mdViewer, setMdViewer] = useState(null); // { url, name } — open markdown attachment viewer
|
|
365
|
+
const [composeSubject, setComposeSubject] = useState(''); // optional subject → opens a new topic thread
|
|
259
366
|
const [mode, setMode] = useState('idle'); // 'idle' | 'compose' | 'thread'
|
|
260
367
|
const [to, setTo] = useState('');
|
|
261
368
|
const [sending, setSending] = useState(false);
|
|
@@ -385,8 +492,16 @@ export default function InboxTab({ showToast }) {
|
|
|
385
492
|
setThread(msgs);
|
|
386
493
|
const map = {};
|
|
387
494
|
await Promise.all(msgs.flatMap(m => (m.attachments || [])
|
|
388
|
-
.filter(a => a.
|
|
389
|
-
.map(async a => {
|
|
495
|
+
.filter(a => !a.inline)
|
|
496
|
+
.map(async a => {
|
|
497
|
+
// Inbound: only the recipient's duplicated local copy is fetchable (after accept). Outbound:
|
|
498
|
+
// the sender owns the original storage key, so resolve that — you can view/download what you sent.
|
|
499
|
+
const key = (a.mode === 'duplicate' && a.localKey) ? a.localKey
|
|
500
|
+
: (m.direction === 'outbound' && a.storageKey) ? a.storageKey : null;
|
|
501
|
+
if (!key) return;
|
|
502
|
+
const u = await messages.attachmentUrl(key).catch(() => null);
|
|
503
|
+
if (u) map[a.id] = u;
|
|
504
|
+
})));
|
|
390
505
|
setUrlMap(map);
|
|
391
506
|
if (markRead) await messages.markConversationRead(conv.conversationId).catch(() => {});
|
|
392
507
|
}, []);
|
|
@@ -434,7 +549,7 @@ export default function InboxTab({ showToast }) {
|
|
|
434
549
|
await loadThread(conv, true); // mark read only on explicit open (avoids a refresh loop)
|
|
435
550
|
loadLists();
|
|
436
551
|
};
|
|
437
|
-
const startCompose = () => { setMode('compose'); setActiveConv(null); setTo(''); };
|
|
552
|
+
const startCompose = () => { setMode('compose'); setActiveConv(null); setTo(''); setComposeSubject(''); };
|
|
438
553
|
|
|
439
554
|
// Open the conversation for a tracked response, then (for awaiting-approval) seed the suggested reply.
|
|
440
555
|
const openTracked = async (tr) => {
|
|
@@ -485,10 +600,13 @@ export default function InboxTab({ showToast }) {
|
|
|
485
600
|
const desc = await messages.uploadAttachment(files[i]);
|
|
486
601
|
attachments.push({ ...desc, inline: false, id: `at${i}` });
|
|
487
602
|
}
|
|
488
|
-
|
|
603
|
+
// A subject (compose mode only) opens a new topic thread; replies stay in the active thread.
|
|
604
|
+
const subject = (mode === 'compose' && composeSubject.trim()) ? composeSubject.trim() : undefined;
|
|
605
|
+
const resp = await messages.send({ to: recipient, body, attachments, subject });
|
|
489
606
|
if (resp?.ok === false) { showToast?.(resp?.error?.message || t('inbox.failed'), true); }
|
|
490
607
|
else {
|
|
491
608
|
reset?.();
|
|
609
|
+
setComposeSubject('');
|
|
492
610
|
// If this send fulfils a Tracked Response awaiting approval, mark it replied.
|
|
493
611
|
if (replyingTrId) {
|
|
494
612
|
await tracked.markTrackedResponseReplied(replyingTrId, resp?.data?.message?.id).catch(() => {});
|
|
@@ -528,23 +646,47 @@ export default function InboxTab({ showToast }) {
|
|
|
528
646
|
|
|
529
647
|
<div class="inbox-list-section">${t('inbox.conversations')}</div>
|
|
530
648
|
${conversations.length === 0 ? html`<div class="inbox-empty-sm">${t('inbox.noConversations')}</div>` : null}
|
|
531
|
-
${conversations.map(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
649
|
+
${groupConversations(conversations).map(g => {
|
|
650
|
+
// A person with a single human-only thread (no agents) renders flat, as before.
|
|
651
|
+
if (g.convs.length === 1 && !isAgentPeer(g.convs[0].peerGhii)) return convRow(g.convs[0], false);
|
|
652
|
+
// Otherwise: a group header for the person + a nested row per thread (human + each agent).
|
|
653
|
+
const unread = g.convs.reduce((n, c) => n + (c.unread || 0), 0);
|
|
654
|
+
return html`
|
|
655
|
+
<div class="inbox-conv-group" key=${g.ownerKey}>
|
|
656
|
+
<div class="inbox-conv-group-head">
|
|
657
|
+
<${Avatar} seed=${g.ownerKey} size=${28} />
|
|
658
|
+
<span class="inbox-name">${escHtml(ownerDisplayName(g.ownerKey))} <${PresenceDot} ghii=${g.ownerKey} /></span>
|
|
659
|
+
${unread > 0 ? html`<span class="inbox-conv-badge">${unread}</span>` : null}
|
|
539
660
|
</div>
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
</div>
|
|
544
|
-
</div>
|
|
545
|
-
</button>`)}
|
|
661
|
+
${g.convs.map(c => convRow(c, true))}
|
|
662
|
+
</div>`;
|
|
663
|
+
})}
|
|
546
664
|
</div>`;
|
|
547
665
|
|
|
666
|
+
/** One conversation row. `nested` = inside a person group (labelled by the agent/thread, indented). */
|
|
667
|
+
const convRow = (c, nested) => {
|
|
668
|
+
const active = activeConv?.conversationId === c.conversationId ? ' inbox-conv--active' : '';
|
|
669
|
+
const sub = subThreadLabel(c.peerGhii);
|
|
670
|
+
// Nested: a subject thread shows its topic; otherwise the agent name / "Direct". Flat: the person.
|
|
671
|
+
const label = nested ? (c.subject || sub || t('inbox.directThread')) : peerName(c.peerGhii);
|
|
672
|
+
const icon = sub ? '🤖' : (c.subject ? '🏷' : '💬');
|
|
673
|
+
return html`
|
|
674
|
+
<button class=${`inbox-conv${active}${nested ? ' inbox-conv--nested' : ''}`} key=${c.conversationId} onClick=${() => openConversation(c)}>
|
|
675
|
+
${nested ? html`<span class="inbox-conv-subico">${icon}</span>` : html`<${Avatar} seed=${c.peerGhii} size=${40} />`}
|
|
676
|
+
<div class="inbox-conv-main">
|
|
677
|
+
<div class="inbox-conv-line1">
|
|
678
|
+
<span class="inbox-name">${escHtml(label)} ${nested ? '' : html`<${PresenceDot} ghii=${c.peerGhii} />`}</span>
|
|
679
|
+
<span class="inbox-conv-time">${c.updatedAt ? timeShort(c.updatedAt) : ''}</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="inbox-conv-line2">
|
|
682
|
+
${(!nested && c.subject) ? html`<span class="inbox-conv-subject">🏷 ${escHtml(c.subject)}</span>` : ''}
|
|
683
|
+
<span class="inbox-conv-preview">${c.lastDirection === 'outbound' ? `${t('inbox.youPrefix')} ` : ''}${escHtml(c.lastMessage || '')}</span>
|
|
684
|
+
${c.unread > 0 ? html`<span class="inbox-conv-badge">${c.unread}</span>` : null}
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</button>`;
|
|
688
|
+
};
|
|
689
|
+
|
|
548
690
|
const renderThread = () => {
|
|
549
691
|
let lastDay = '';
|
|
550
692
|
return html`
|
|
@@ -553,6 +695,7 @@ export default function InboxTab({ showToast }) {
|
|
|
553
695
|
<${Avatar} seed=${activeConv.peerGhii} size=${36} />
|
|
554
696
|
<div class="inbox-thread-id">
|
|
555
697
|
<div class="inbox-name">${escHtml(peerName(activeConv.peerGhii))} <${PresenceDot} ghii=${activeConv.peerGhii} label=${true} /></div>
|
|
698
|
+
${activeConv.subject ? html`<div class="inbox-thread-subject">🏷 ${escHtml(activeConv.subject)}</div>` : null}
|
|
556
699
|
<div class="inbox-sub">${escHtml(activeConv.peerGhii)}</div>
|
|
557
700
|
</div>
|
|
558
701
|
</div>
|
|
@@ -564,7 +707,8 @@ export default function InboxTab({ showToast }) {
|
|
|
564
707
|
return html`
|
|
565
708
|
${showDay ? html`<div class="inbox-day" key=${'d' + m.id}><span>${dayLabel(m.createdAt)}</span></div>` : null}
|
|
566
709
|
<${MessageBubble} key=${m.id + m.direction} msg=${m} mine=${m.direction === 'outbound'} urlMap=${urlMap}
|
|
567
|
-
starred=${important.has(m.id)} onStar=${toggleImportant} onTrack=${onTrackMsg} tracked=${trackedByMsg[m.id]}
|
|
710
|
+
starred=${important.has(m.id)} onStar=${toggleImportant} onTrack=${onTrackMsg} tracked=${trackedByMsg[m.id]}
|
|
711
|
+
onOpenMarkdown=${(url, name) => setMdViewer({ url, name })} />`;
|
|
568
712
|
})}
|
|
569
713
|
</div>
|
|
570
714
|
${awaitingForConv.map(tr => html`
|
|
@@ -653,6 +797,8 @@ export default function InboxTab({ showToast }) {
|
|
|
653
797
|
<div class="inbox-compose-fields">
|
|
654
798
|
<input class="inbox-input" type="text" placeholder=${t('inbox.toPlaceholder')}
|
|
655
799
|
value=${to} onInput=${(e) => setTo(e.target.value)} />
|
|
800
|
+
<input class="inbox-input" type="text" placeholder=${t('inbox.subjectPlaceholder')}
|
|
801
|
+
value=${composeSubject} onInput=${(e) => setComposeSubject(e.target.value)} />
|
|
656
802
|
</div>
|
|
657
803
|
<${Composer} key="c-new" recipient=${to.trim()} sendLabel=${t('inbox.send')}
|
|
658
804
|
sending=${sending} onSend=${doSend} />
|
|
@@ -673,5 +819,6 @@ export default function InboxTab({ showToast }) {
|
|
|
673
819
|
|
|
674
820
|
<${TrackResponseModal} open=${!!trackMsg} msg=${trackMsg}
|
|
675
821
|
onClose=${() => setTrackMsg(null)} onDone=${loadLists} showToast=${showToast} />
|
|
822
|
+
${mdViewer && html`<${MarkdownViewer} url=${mdViewer.url} name=${mdViewer.name} onClose=${() => setMdViewer(null)} />`}
|
|
676
823
|
</div>`;
|
|
677
824
|
}
|