mercury-agent 0.5.0 → 0.5.2

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 (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -1,729 +1,729 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Mercury</title>
7
- <link rel="stylesheet" href="/dashboard/tokens.css">
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
11
- <script src="https://unpkg.com/htmx.org@2.0.4"></script>
12
- <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
13
- <style>
14
- * { margin: 0; padding: 0; box-sizing: border-box; }
15
-
16
- body {
17
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
18
- font-size: 14px;
19
- background: var(--bg);
20
- color: var(--text);
21
- min-height: 100vh;
22
- overflow-x: hidden;
23
- }
24
-
25
- /* ─── Layout ─────────────────────────────────────────────────────────── */
26
-
27
- .shell {
28
- max-width: 1100px;
29
- margin: 0 auto;
30
- padding: 0 16px;
31
- }
32
-
33
- /* ─── Header ─────────────────────────────────────────────────────────── */
34
-
35
- .header {
36
- border-bottom: 1px solid var(--border);
37
- padding: 12px 0;
38
- display: flex;
39
- justify-content: space-between;
40
- align-items: center;
41
- margin-bottom: 16px;
42
- }
43
-
44
- .header-left {
45
- display: flex;
46
- align-items: center;
47
- gap: 16px;
48
- }
49
-
50
- .logo {
51
- font-size: 16px;
52
- font-weight: 600;
53
- display: flex;
54
- align-items: center;
55
- gap: 8px;
56
- }
57
-
58
- .logo svg { color: var(--text); flex-shrink: 0; }
59
-
60
- .logo-version {
61
- font-size: 11px;
62
- font-weight: 400;
63
- color: var(--muted);
64
- margin-left: 2px;
65
- }
66
-
67
- /* ─── Health Banner ──────────────────────────────────────────────────── */
68
-
69
- .health-banner {
70
- display: flex;
71
- align-items: center;
72
- justify-content: space-between;
73
- padding: 8px 12px;
74
- border-radius: var(--radius);
75
- background: var(--surface);
76
- border: 1px solid var(--border);
77
- font-size: 13px;
78
- }
79
-
80
- .health-status {
81
- display: flex;
82
- align-items: center;
83
- gap: 8px;
84
- }
85
-
86
- .health-status.healthy { color: var(--color-success); }
87
- .health-status.degraded { color: var(--color-warning); }
88
- .health-status.critical { color: var(--color-error); }
89
-
90
- .health-icon { font-size: 14px; }
91
-
92
- .health-meta {
93
- display: flex;
94
- align-items: center;
95
- gap: 16px;
96
- color: var(--muted);
97
- }
98
-
99
- .connection {
100
- display: flex;
101
- align-items: center;
102
- gap: 6px;
103
- }
104
-
105
- .connection::before {
106
- content: '';
107
- width: 8px;
108
- height: 8px;
109
- border-radius: 50%;
110
- background: var(--color-success);
111
- }
112
-
113
- .connection.disconnected::before { background: var(--color-error); }
114
-
115
- /* ─── Navigation ─────────────────────────────────────────────────────── */
116
-
117
- .nav {
118
- display: flex;
119
- gap: 4px;
120
- border-bottom: 1px solid var(--border);
121
- margin-bottom: 16px;
122
- }
123
-
124
- .nav-item {
125
- padding: 10px 16px;
126
- color: var(--muted);
127
- text-decoration: none;
128
- border-bottom: 2px solid transparent;
129
- margin-bottom: -1px;
130
- cursor: pointer;
131
- transition: color 0.15s;
132
- }
133
-
134
- .nav-item:hover { color: var(--text); }
135
-
136
- .nav-item.active {
137
- color: var(--accent);
138
- border-bottom-color: var(--accent);
139
- }
140
-
141
- .nav-right {
142
- margin-left: auto;
143
- display: flex;
144
- align-items: center;
145
- gap: 8px;
146
- padding: 0 8px;
147
- }
148
-
149
- /* ─── Panels ─────────────────────────────────────────────────────────── */
150
-
151
- .panel {
152
- border: 1px solid var(--border);
153
- border-radius: var(--radius);
154
- margin-bottom: 16px;
155
- background: var(--surface);
156
- }
157
-
158
- .panel-header {
159
- padding: 10px 16px;
160
- border-bottom: 1px solid var(--border);
161
- color: var(--muted);
162
- font-size: 12px;
163
- font-weight: 500;
164
- text-transform: uppercase;
165
- letter-spacing: 0.5px;
166
- display: flex;
167
- justify-content: space-between;
168
- align-items: center;
169
- }
170
-
171
- .panel-body { padding: 12px 16px; min-width: 0; }
172
-
173
- /* ─── Grid ───────────────────────────────────────────────────────────── */
174
-
175
- .grid-2 {
176
- display: grid;
177
- grid-template-columns: 1fr 1fr;
178
- gap: 16px;
179
- }
180
-
181
- @media (max-width: 768px) {
182
- .grid-2 { grid-template-columns: 1fr; }
183
- }
184
-
185
- /* ─── Stats ──────────────────────────────────────────────────────────── */
186
-
187
- .stats {
188
- display: grid;
189
- grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
190
- gap: 16px;
191
- text-align: center;
192
- }
193
-
194
- .stat-value {
195
- font-size: 24px;
196
- font-weight: 600;
197
- color: var(--accent);
198
- }
199
-
200
- .stat-label {
201
- font-size: 11px;
202
- color: var(--muted);
203
- text-transform: uppercase;
204
- letter-spacing: 0.5px;
205
- margin-top: 4px;
206
- }
207
-
208
- /* ─── Tables ─────────────────────────────────────────────────────────── */
209
-
210
- .table {
211
- width: 100%;
212
- border-collapse: collapse;
213
- }
214
-
215
- .table th {
216
- text-align: left;
217
- color: var(--muted);
218
- font-weight: 500;
219
- font-size: 11px;
220
- text-transform: uppercase;
221
- letter-spacing: 0.5px;
222
- padding: 10px 8px;
223
- border-bottom: 1px solid var(--border);
224
- }
225
-
226
- .table td {
227
- padding: 10px 8px;
228
- border-bottom: 1px solid var(--border);
229
- }
230
-
231
- .table tr:last-child td { border-bottom: none; }
232
-
233
- .table tr.clickable { cursor: pointer; }
234
- .table tr.clickable:hover { background: var(--surface-elevated); }
235
-
236
- .table .actions { white-space: nowrap; }
237
-
238
- /* ─── Rows ───────────────────────────────────────────────────────────── */
239
-
240
- .adapter-row, .task-row, .role-row, .active-run {
241
- display: flex;
242
- justify-content: space-between;
243
- align-items: center;
244
- padding: 8px 0;
245
- border-bottom: 1px solid var(--border);
246
- gap: 12px;
247
- }
248
-
249
- .adapter-row:last-child, .task-row:last-child,
250
- .role-row:last-child, .active-run:last-child { border-bottom: none; }
251
-
252
- .activity-row {
253
- display: flex;
254
- align-items: center;
255
- gap: 10px;
256
- padding: 8px 0;
257
- border-bottom: 1px solid var(--border);
258
- cursor: pointer;
259
- }
260
-
261
- .activity-row:hover { background: var(--surface-elevated); margin: 0 -16px; padding: 8px 16px; }
262
- .activity-row:last-child { border-bottom: none; }
263
-
264
- .activity-row .time { color: var(--muted); min-width: 60px; font-size: 12px; }
265
- .activity-row .preview { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
-
267
- /* ─── Messages ───────────────────────────────────────────────────────── */
268
-
269
- .messages-list {
270
- max-height: 500px;
271
- overflow-y: auto;
272
- }
273
-
274
- .scroll-cap {
275
- max-height: 480px;
276
- overflow-y: auto;
277
- }
278
-
279
- .message {
280
- padding: 12px;
281
- border-bottom: 1px solid var(--border);
282
- }
283
-
284
- .message:last-child { border-bottom: none; }
285
-
286
- .message-meta {
287
- display: flex;
288
- gap: 12px;
289
- margin-bottom: 6px;
290
- font-size: 12px;
291
- }
292
-
293
- .message-content {
294
- white-space: pre-wrap;
295
- word-break: break-word;
296
- font-family: 'SF Mono', 'Fira Code', monospace;
297
- font-size: 13px;
298
- line-height: 1.5;
299
- }
300
-
301
- .message.assistant { background: var(--accent-tint); }
302
- .message.user { background: var(--success-tint); }
303
-
304
- /* ─── Logs ───────────────────────────────────────────────────────────── */
305
-
306
- .logs-list {
307
- max-height: 600px;
308
- overflow-y: auto;
309
- font-family: 'SF Mono', 'Fira Code', monospace;
310
- font-size: 12px;
311
- }
312
-
313
- .log-row {
314
- display: flex;
315
- gap: 12px;
316
- padding: 6px 0;
317
- border-bottom: 1px solid var(--border);
318
- }
319
-
320
- .log-row:last-child { border-bottom: none; }
321
- .log-row .time { color: var(--muted); min-width: 80px; }
322
- .log-row .level { min-width: 50px; font-weight: 500; }
323
- .log-row .level.info { color: var(--accent); }
324
- .log-row .level.error { color: var(--color-error); }
325
- .log-row .level.warn { color: var(--color-warning); }
326
- .log-row .source { color: var(--muted); min-width: 80px; }
327
- .log-row .message { flex: 1; }
328
-
329
- /* ─── Elements ───────────────────────────────────────────────────────── */
330
-
331
- .badge {
332
- display: inline-block;
333
- background: var(--bg);
334
- border: 1px solid var(--border);
335
- padding: 2px 8px;
336
- border-radius: 4px;
337
- font-size: 11px;
338
- font-weight: 500;
339
- }
340
-
341
- .badge.green { border-color: var(--color-success); color: var(--color-success); }
342
- .badge.muted { border-color: var(--muted); color: var(--muted); }
343
- .badge.pulse { animation: pulse 2s ease-in-out infinite; background: var(--color-warning); border-color: var(--color-warning); color: var(--bg); }
344
-
345
- .space-badge {
346
- display: inline-block;
347
- padding: 2px 8px;
348
- border-radius: 4px;
349
- font-size: 11px;
350
- font-weight: 500;
351
- border: 1px solid transparent;
352
- }
353
- .space-badge-0 { background: var(--space-palette-0-bg); color: var(--space-palette-0-fg); }
354
- .space-badge-1 { background: var(--space-palette-1-bg); color: var(--space-palette-1-fg); }
355
- .space-badge-2 { background: var(--space-palette-2-bg); color: var(--space-palette-2-fg); }
356
- .space-badge-3 { background: var(--space-palette-3-bg); color: var(--space-palette-3-fg); }
357
- .space-badge-4 { background: var(--space-palette-4-bg); color: var(--space-palette-4-fg); }
358
- .space-badge-5 { background: var(--space-palette-5-bg); color: var(--space-palette-5-fg); }
359
- .space-badge-6 { background: var(--space-palette-6-bg); color: var(--space-palette-6-fg); }
360
- .space-badge-7 { background: var(--space-palette-7-bg); color: var(--space-palette-7-fg); }
361
- .space-badge-unknown { background: transparent; border-color: var(--muted); color: var(--muted); }
362
-
363
- @keyframes pulse {
364
- 0%, 100% { opacity: 1; }
365
- 50% { opacity: 0.6; }
366
- }
367
-
368
- .btn {
369
- background: var(--bg);
370
- border: 1px solid var(--border);
371
- color: var(--text);
372
- padding: 6px 12px;
373
- border-radius: var(--radius);
374
- cursor: pointer;
375
- font-size: 13px;
376
- transition: background 0.15s, border-color 0.15s;
377
- }
378
-
379
- .btn:hover { background: var(--surface-elevated); border-color: var(--muted); }
380
-
381
- .btn-sm { padding: 4px 8px; font-size: 12px; }
382
- .btn-danger:hover { border-color: var(--color-error); color: var(--color-error); }
383
-
384
- .status {
385
- display: inline-flex;
386
- align-items: center;
387
- gap: 6px;
388
- }
389
-
390
- .status::before {
391
- content: '';
392
- width: 8px;
393
- height: 8px;
394
- border-radius: 50%;
395
- background: var(--muted);
396
- }
397
-
398
- .status.connected::before { background: var(--color-success); }
399
- .status.active::before { background: var(--color-warning); animation: pulse 1s ease-in-out infinite; }
400
-
401
- .role { font-weight: 500; font-size: 12px; }
402
- .role.user { color: var(--accent); }
403
- .role.assistant { color: var(--color-success); }
404
- .role.ambient { color: var(--muted); }
405
-
406
- .mono { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent); }
407
- .muted { color: var(--muted); }
408
- .truncate { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
409
-
410
- .empty, .empty-small {
411
- color: var(--muted);
412
- font-style: italic;
413
- text-align: center;
414
- }
415
- .empty { padding: 24px 0; }
416
- .empty-small { padding: 12px 0; }
417
-
418
- .link {
419
- color: var(--accent);
420
- text-decoration: none;
421
- font-size: 12px;
422
- }
423
-
424
- .link:hover { text-decoration: underline; }
425
-
426
- .back {
427
- color: var(--muted);
428
- text-decoration: none;
429
- font-size: 13px;
430
- }
431
-
432
- .back:hover { color: var(--text); }
433
-
434
- /* ─── Page Header ────────────────────────────────────────────────────── */
435
-
436
- .page-header {
437
- display: flex;
438
- justify-content: space-between;
439
- align-items: center;
440
- margin-bottom: 16px;
441
- }
442
-
443
- .page-header h2 {
444
- font-size: 18px;
445
- font-weight: 600;
446
- display: flex;
447
- align-items: center;
448
- gap: 10px;
449
- }
450
-
451
- .search-box input, .select {
452
- background: var(--bg);
453
- border: 1px solid var(--border);
454
- color: var(--text);
455
- padding: 8px 12px;
456
- border-radius: var(--radius);
457
- font-size: 13px;
458
- width: 200px;
459
- }
460
-
461
- .search-box input:focus, .select:focus {
462
- outline: none;
463
- border-color: var(--accent);
464
- }
465
-
466
- /* ─── Scroll container with edge-fade ───────────────────────────────── */
467
-
468
- .table-scroll {
469
- overflow-x: auto;
470
- -webkit-overflow-scrolling: touch;
471
- /* symmetric two-edge fade — RTL-safe, no directional offset */
472
- mask-image: linear-gradient(90deg, transparent 0, black 16px, black calc(100% - 16px), transparent 100%);
473
- }
474
-
475
- /* prevent column compression — table sizes to content, .table-scroll handles scroll */
476
- .table-scroll .table { width: max-content; min-width: 100%; }
477
-
478
- /* ─── Responsive ─────────────────────────────────────────────────────── */
479
-
480
- @media (max-width: 768px) {
481
- .shell { padding: 0 12px; }
482
- .header { flex-direction: column; gap: 12px; }
483
- .health-banner { flex-direction: column; gap: 8px; }
484
- .nav { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
485
- .nav::-webkit-scrollbar { display: none; }
486
- .nav-item { padding: 10px 12px; white-space: nowrap; }
487
- .page-header { flex-direction: column; gap: 12px; align-items: flex-start; }
488
- .search-box input { width: 100%; }
489
- }
490
-
491
- @media (max-width: 640px) {
492
- .panel-body { padding: 8px 12px; }
493
- .nav-item { min-height: 44px; display: flex; align-items: center; }
494
- .btn { min-height: 44px; }
495
- }
496
-
497
- :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
498
-
499
- @media (prefers-reduced-motion: reduce) {
500
- .badge.pulse,
501
- .status.active::before { animation: none; }
502
- }
503
- </style>
504
- </head>
505
- <body hx-ext="sse" sse-connect="/dashboard/events">
506
- <div class="shell">
507
- <!-- Header -->
508
- <div class="header">
509
- <div class="header-left">
510
- <div class="logo">
511
- <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
512
- <path d="M6.5 4.5C6.5 2.5 13.5 2.5 13.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
513
- <circle cx="10" cy="9" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/>
514
- <line x1="10" y1="13" x2="10" y2="18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
515
- <line x1="7" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
516
- </svg>
517
- Mercury <span class="logo-version">{{VERSION}}</span>
518
- </div>
519
- </div>
520
- <div id="active-count" sse-swap="active-count"></div>
521
- </div>
522
-
523
- <!-- Health Banner -->
524
- <div class="health-banner" id="health" sse-swap="health">
525
- <div class="health-status">
526
- <span class="health-icon">⏳</span>
527
- <span class="health-message">Connecting...</span>
528
- </div>
529
- <div class="health-meta">
530
- <span class="connection" id="connection">live</span>
531
- </div>
532
- </div>
533
-
534
- <!-- Navigation -->
535
- <nav class="nav" id="nav">
536
- <a class="nav-item active"
537
- hx-get="/dashboard/page/overview"
538
- hx-target="#main"
539
- hx-push-url="true"
540
- onclick="setActiveNav(this)"
541
- title="Summary of spaces, recent activity, and system health (shortcut: 1)">Overview</a>
542
- <a class="nav-item"
543
- hx-get="/dashboard/page/spaces"
544
- hx-target="#main"
545
- hx-push-url="true"
546
- onclick="setActiveNav(this)"
547
- title="Manage memory spaces and their linked conversations (shortcut: 2)">Spaces</a>
548
- <a class="nav-item"
549
- hx-get="/dashboard/page/conversations"
550
- hx-target="#main"
551
- hx-push-url="true"
552
- onclick="setActiveNav(this)"
553
- title="Browse all discovered platform conversations across adapters (shortcut: 3)">Conversations</a>
554
- <a class="nav-item"
555
- hx-get="/dashboard/page/tasks"
556
- hx-target="#main"
557
- hx-push-url="true"
558
- onclick="setActiveNav(this)"
559
- title="Scheduled tasks — cron jobs and one-shot timers (shortcut: 4)">Tasks</a>
560
- <a class="nav-item"
561
- hx-get="/dashboard/page/permissions"
562
- hx-target="#main"
563
- hx-push-url="true"
564
- onclick="setActiveNav(this)"
565
- title="Role assignments and permission sets per space (shortcut: 5)">Permissions</a>
566
- <a class="nav-item"
567
- hx-get="/dashboard/page/logs"
568
- hx-target="#main"
569
- hx-push-url="true"
570
- onclick="setActiveNav(this)"
571
- title="Live agent logs (shortcut: 6)">Logs</a>
572
- <a class="nav-item"
573
- hx-get="/dashboard/page/usage"
574
- hx-target="#main"
575
- hx-push-url="true"
576
- onclick="setActiveNav(this)"
577
- title="Token and cost usage statistics (shortcut: 7)">Usage</a>
578
- <a class="nav-item"
579
- hx-get="/dashboard/page/billing"
580
- hx-target="#main"
581
- hx-push-url="true"
582
- onclick="setActiveNav(this)"
583
- title="Subscription and billing details (shortcut: 8)">Billing</a>
584
- <a class="nav-item"
585
- hx-get="/dashboard/page/keys"
586
- hx-target="#main"
587
- hx-push-url="true"
588
- onclick="setActiveNav(this)"
589
- title="Manage AI provider API keys">API keys</a>
590
- <a class="nav-item"
591
- hx-get="/dashboard/page/features"
592
- hx-target="#main"
593
- hx-push-url="true"
594
- onclick="setActiveNav(this)"
595
- title="Experimental features and extension configuration">Features</a>
596
- </nav>
597
-
598
- <!-- Main Content -->
599
- <main id="main" hx-get="/dashboard/page/overview" hx-trigger="load">
600
- <div class="empty">Loading...</div>
601
- </main>
602
- </div>
603
-
604
- <script>
605
- // Navigation (global for hx-on/htmx; Biome cannot see HTML callers)
606
- globalThis.setActiveNav = function setActiveNav(el) {
607
- for (const item of document.querySelectorAll('.nav-item')) {
608
- item.classList.remove('active');
609
- }
610
- el.classList.add('active');
611
- const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
612
- el.scrollIntoView({ inline: 'center', block: 'nearest', behavior: reducedMotion ? 'auto' : 'smooth' });
613
- }
614
-
615
- // Scroll initially active tab into view on load
616
- document.addEventListener('DOMContentLoaded', () => {
617
- const active = document.querySelector('.nav-item.active');
618
- if (active) active.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'auto' });
619
- });
620
-
621
- // Handle browser back/forward
622
- window.addEventListener('popstate', () => {
623
- const path = window.location.pathname;
624
- const navItems = document.querySelectorAll('.nav-item');
625
- navItems.forEach(item => {
626
- const href = item.getAttribute('hx-get');
627
- if (path.includes(href.replace('/dashboard/page/', ''))) {
628
- item.classList.add('active');
629
- } else {
630
- item.classList.remove('active');
631
- }
632
- });
633
- });
634
-
635
- // SSE connection state
636
- const connectionEl = document.getElementById('connection');
637
-
638
- document.body.addEventListener('htmx:sseOpen', () => {
639
- connectionEl.textContent = 'live';
640
- connectionEl.classList.remove('disconnected');
641
- });
642
-
643
- document.body.addEventListener('htmx:sseError', () => {
644
- connectionEl.textContent = 'reconnecting...';
645
- connectionEl.classList.add('disconnected');
646
- });
647
-
648
- document.body.addEventListener('htmx:sseClose', () => {
649
- connectionEl.textContent = 'disconnected';
650
- connectionEl.classList.add('disconnected');
651
- });
652
-
653
- // Table search filter
654
- function _filterTable(input, tableId) {
655
- const filter = input.value.toLowerCase();
656
- const table = document.getElementById(tableId);
657
- const rows = table.querySelectorAll('tbody tr');
658
-
659
- rows.forEach(row => {
660
- const text = row.textContent.toLowerCase();
661
- row.style.display = text.includes(filter) ? '' : 'none';
662
- });
663
- }
664
-
665
- // Space filter for the tasks table
666
- function _filterTasksBySpace(select) {
667
- const wanted = select.value;
668
- document.querySelectorAll('#tasks-table tbody tr').forEach(row => {
669
- row.style.display = (!wanted || row.dataset.spaceId === wanted) ? '' : 'none';
670
- });
671
- }
672
-
673
- // Keyboard shortcuts
674
- document.addEventListener('keydown', (e) => {
675
- // Don't trigger when typing in inputs
676
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
677
-
678
- const navItems = document.querySelectorAll('.nav-item');
679
-
680
- if (e.key === '1') navItems[0]?.click();
681
- if (e.key === '2') navItems[1]?.click();
682
- if (e.key === '3') navItems[2]?.click();
683
- if (e.key === '4') navItems[3]?.click();
684
- if (e.key === '5') navItems[4]?.click();
685
- if (e.key === '6') navItems[5]?.click();
686
- if (e.key === '7') navItems[6]?.click();
687
- if (e.key === '8') navItems[7]?.click();
688
- if (e.key === '?') alert('Keyboard shortcuts:\n1-8: Navigate tabs\n/: Focus search');
689
- if (e.key === '/') {
690
- e.preventDefault();
691
- document.querySelector('input[type="text"]')?.focus();
692
- }
693
- });
694
-
695
- // Refresh current page after htmx actions (like delete, pause, etc.)
696
- document.body.addEventListener('htmx:afterRequest', (e) => {
697
- if (e.detail.requestConfig.verb !== 'get') {
698
- // Refresh the current page after mutations
699
- setTimeout(() => {
700
- const activeNav = document.querySelector('.nav-item.active');
701
- if (activeNav) activeNav.click();
702
- }, 100);
703
- }
704
- });
705
-
706
- // Handle hash-based navigation on page load (for refresh support)
707
- window.addEventListener('DOMContentLoaded', () => {
708
- const hash = window.location.hash.slice(1); // Remove #
709
- if (hash) {
710
- // Find the nav item that matches and click it
711
- const navItems = document.querySelectorAll('.nav-item');
712
- let found = false;
713
- for (const item of navItems) {
714
- const href = item.getAttribute('hx-get');
715
- if (href?.includes(hash.split('/')[0])) {
716
- item.click();
717
- found = true;
718
- break;
719
- }
720
- }
721
- // If it's a detail page (e.g., groups/id), load it directly
722
- if (!found && hash.includes('/')) {
723
- htmx.ajax('GET', `/dashboard/page/${hash}`, '#main');
724
- }
725
- }
726
- });
727
- </script>
728
- </body>
729
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Mercury</title>
7
+ <link rel="stylesheet" href="/dashboard/tokens.css">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
11
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
12
+ <script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+
16
+ body {
17
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
18
+ font-size: 14px;
19
+ background: var(--bg);
20
+ color: var(--text);
21
+ min-height: 100vh;
22
+ overflow-x: hidden;
23
+ }
24
+
25
+ /* ─── Layout ─────────────────────────────────────────────────────────── */
26
+
27
+ .shell {
28
+ max-width: 1100px;
29
+ margin: 0 auto;
30
+ padding: 0 16px;
31
+ }
32
+
33
+ /* ─── Header ─────────────────────────────────────────────────────────── */
34
+
35
+ .header {
36
+ border-bottom: 1px solid var(--border);
37
+ padding: 12px 0;
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ margin-bottom: 16px;
42
+ }
43
+
44
+ .header-left {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 16px;
48
+ }
49
+
50
+ .logo {
51
+ font-size: 16px;
52
+ font-weight: 600;
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 8px;
56
+ }
57
+
58
+ .logo svg { color: var(--text); flex-shrink: 0; }
59
+
60
+ .logo-version {
61
+ font-size: 11px;
62
+ font-weight: 400;
63
+ color: var(--muted);
64
+ margin-left: 2px;
65
+ }
66
+
67
+ /* ─── Health Banner ──────────────────────────────────────────────────── */
68
+
69
+ .health-banner {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ padding: 8px 12px;
74
+ border-radius: var(--radius);
75
+ background: var(--surface);
76
+ border: 1px solid var(--border);
77
+ font-size: 13px;
78
+ }
79
+
80
+ .health-status {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ }
85
+
86
+ .health-status.healthy { color: var(--color-success); }
87
+ .health-status.degraded { color: var(--color-warning); }
88
+ .health-status.critical { color: var(--color-error); }
89
+
90
+ .health-icon { font-size: 14px; }
91
+
92
+ .health-meta {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 16px;
96
+ color: var(--muted);
97
+ }
98
+
99
+ .connection {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 6px;
103
+ }
104
+
105
+ .connection::before {
106
+ content: '';
107
+ width: 8px;
108
+ height: 8px;
109
+ border-radius: 50%;
110
+ background: var(--color-success);
111
+ }
112
+
113
+ .connection.disconnected::before { background: var(--color-error); }
114
+
115
+ /* ─── Navigation ─────────────────────────────────────────────────────── */
116
+
117
+ .nav {
118
+ display: flex;
119
+ gap: 4px;
120
+ border-bottom: 1px solid var(--border);
121
+ margin-bottom: 16px;
122
+ }
123
+
124
+ .nav-item {
125
+ padding: 10px 16px;
126
+ color: var(--muted);
127
+ text-decoration: none;
128
+ border-bottom: 2px solid transparent;
129
+ margin-bottom: -1px;
130
+ cursor: pointer;
131
+ transition: color 0.15s;
132
+ }
133
+
134
+ .nav-item:hover { color: var(--text); }
135
+
136
+ .nav-item.active {
137
+ color: var(--accent);
138
+ border-bottom-color: var(--accent);
139
+ }
140
+
141
+ .nav-right {
142
+ margin-left: auto;
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 8px;
146
+ padding: 0 8px;
147
+ }
148
+
149
+ /* ─── Panels ─────────────────────────────────────────────────────────── */
150
+
151
+ .panel {
152
+ border: 1px solid var(--border);
153
+ border-radius: var(--radius);
154
+ margin-bottom: 16px;
155
+ background: var(--surface);
156
+ }
157
+
158
+ .panel-header {
159
+ padding: 10px 16px;
160
+ border-bottom: 1px solid var(--border);
161
+ color: var(--muted);
162
+ font-size: 12px;
163
+ font-weight: 500;
164
+ text-transform: uppercase;
165
+ letter-spacing: 0.5px;
166
+ display: flex;
167
+ justify-content: space-between;
168
+ align-items: center;
169
+ }
170
+
171
+ .panel-body { padding: 12px 16px; min-width: 0; }
172
+
173
+ /* ─── Grid ───────────────────────────────────────────────────────────── */
174
+
175
+ .grid-2 {
176
+ display: grid;
177
+ grid-template-columns: 1fr 1fr;
178
+ gap: 16px;
179
+ }
180
+
181
+ @media (max-width: 768px) {
182
+ .grid-2 { grid-template-columns: 1fr; }
183
+ }
184
+
185
+ /* ─── Stats ──────────────────────────────────────────────────────────── */
186
+
187
+ .stats {
188
+ display: grid;
189
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
190
+ gap: 16px;
191
+ text-align: center;
192
+ }
193
+
194
+ .stat-value {
195
+ font-size: 24px;
196
+ font-weight: 600;
197
+ color: var(--accent);
198
+ }
199
+
200
+ .stat-label {
201
+ font-size: 11px;
202
+ color: var(--muted);
203
+ text-transform: uppercase;
204
+ letter-spacing: 0.5px;
205
+ margin-top: 4px;
206
+ }
207
+
208
+ /* ─── Tables ─────────────────────────────────────────────────────────── */
209
+
210
+ .table {
211
+ width: 100%;
212
+ border-collapse: collapse;
213
+ }
214
+
215
+ .table th {
216
+ text-align: left;
217
+ color: var(--muted);
218
+ font-weight: 500;
219
+ font-size: 11px;
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.5px;
222
+ padding: 10px 8px;
223
+ border-bottom: 1px solid var(--border);
224
+ }
225
+
226
+ .table td {
227
+ padding: 10px 8px;
228
+ border-bottom: 1px solid var(--border);
229
+ }
230
+
231
+ .table tr:last-child td { border-bottom: none; }
232
+
233
+ .table tr.clickable { cursor: pointer; }
234
+ .table tr.clickable:hover { background: var(--surface-elevated); }
235
+
236
+ .table .actions { white-space: nowrap; }
237
+
238
+ /* ─── Rows ───────────────────────────────────────────────────────────── */
239
+
240
+ .adapter-row, .task-row, .role-row, .active-run {
241
+ display: flex;
242
+ justify-content: space-between;
243
+ align-items: center;
244
+ padding: 8px 0;
245
+ border-bottom: 1px solid var(--border);
246
+ gap: 12px;
247
+ }
248
+
249
+ .adapter-row:last-child, .task-row:last-child,
250
+ .role-row:last-child, .active-run:last-child { border-bottom: none; }
251
+
252
+ .activity-row {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 10px;
256
+ padding: 8px 0;
257
+ border-bottom: 1px solid var(--border);
258
+ cursor: pointer;
259
+ }
260
+
261
+ .activity-row:hover { background: var(--surface-elevated); margin: 0 -16px; padding: 8px 16px; }
262
+ .activity-row:last-child { border-bottom: none; }
263
+
264
+ .activity-row .time { color: var(--muted); min-width: 60px; font-size: 12px; }
265
+ .activity-row .preview { color: var(--text); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
266
+
267
+ /* ─── Messages ───────────────────────────────────────────────────────── */
268
+
269
+ .messages-list {
270
+ max-height: 500px;
271
+ overflow-y: auto;
272
+ }
273
+
274
+ .scroll-cap {
275
+ max-height: 480px;
276
+ overflow-y: auto;
277
+ }
278
+
279
+ .message {
280
+ padding: 12px;
281
+ border-bottom: 1px solid var(--border);
282
+ }
283
+
284
+ .message:last-child { border-bottom: none; }
285
+
286
+ .message-meta {
287
+ display: flex;
288
+ gap: 12px;
289
+ margin-bottom: 6px;
290
+ font-size: 12px;
291
+ }
292
+
293
+ .message-content {
294
+ white-space: pre-wrap;
295
+ word-break: break-word;
296
+ font-family: 'SF Mono', 'Fira Code', monospace;
297
+ font-size: 13px;
298
+ line-height: 1.5;
299
+ }
300
+
301
+ .message.assistant { background: var(--accent-tint); }
302
+ .message.user { background: var(--success-tint); }
303
+
304
+ /* ─── Logs ───────────────────────────────────────────────────────────── */
305
+
306
+ .logs-list {
307
+ max-height: 600px;
308
+ overflow-y: auto;
309
+ font-family: 'SF Mono', 'Fira Code', monospace;
310
+ font-size: 12px;
311
+ }
312
+
313
+ .log-row {
314
+ display: flex;
315
+ gap: 12px;
316
+ padding: 6px 0;
317
+ border-bottom: 1px solid var(--border);
318
+ }
319
+
320
+ .log-row:last-child { border-bottom: none; }
321
+ .log-row .time { color: var(--muted); min-width: 80px; }
322
+ .log-row .level { min-width: 50px; font-weight: 500; }
323
+ .log-row .level.info { color: var(--accent); }
324
+ .log-row .level.error { color: var(--color-error); }
325
+ .log-row .level.warn { color: var(--color-warning); }
326
+ .log-row .source { color: var(--muted); min-width: 80px; }
327
+ .log-row .message { flex: 1; }
328
+
329
+ /* ─── Elements ───────────────────────────────────────────────────────── */
330
+
331
+ .badge {
332
+ display: inline-block;
333
+ background: var(--bg);
334
+ border: 1px solid var(--border);
335
+ padding: 2px 8px;
336
+ border-radius: 4px;
337
+ font-size: 11px;
338
+ font-weight: 500;
339
+ }
340
+
341
+ .badge.green { border-color: var(--color-success); color: var(--color-success); }
342
+ .badge.muted { border-color: var(--muted); color: var(--muted); }
343
+ .badge.pulse { animation: pulse 2s ease-in-out infinite; background: var(--color-warning); border-color: var(--color-warning); color: var(--bg); }
344
+
345
+ .space-badge {
346
+ display: inline-block;
347
+ padding: 2px 8px;
348
+ border-radius: 4px;
349
+ font-size: 11px;
350
+ font-weight: 500;
351
+ border: 1px solid transparent;
352
+ }
353
+ .space-badge-0 { background: var(--space-palette-0-bg); color: var(--space-palette-0-fg); }
354
+ .space-badge-1 { background: var(--space-palette-1-bg); color: var(--space-palette-1-fg); }
355
+ .space-badge-2 { background: var(--space-palette-2-bg); color: var(--space-palette-2-fg); }
356
+ .space-badge-3 { background: var(--space-palette-3-bg); color: var(--space-palette-3-fg); }
357
+ .space-badge-4 { background: var(--space-palette-4-bg); color: var(--space-palette-4-fg); }
358
+ .space-badge-5 { background: var(--space-palette-5-bg); color: var(--space-palette-5-fg); }
359
+ .space-badge-6 { background: var(--space-palette-6-bg); color: var(--space-palette-6-fg); }
360
+ .space-badge-7 { background: var(--space-palette-7-bg); color: var(--space-palette-7-fg); }
361
+ .space-badge-unknown { background: transparent; border-color: var(--muted); color: var(--muted); }
362
+
363
+ @keyframes pulse {
364
+ 0%, 100% { opacity: 1; }
365
+ 50% { opacity: 0.6; }
366
+ }
367
+
368
+ .btn {
369
+ background: var(--bg);
370
+ border: 1px solid var(--border);
371
+ color: var(--text);
372
+ padding: 6px 12px;
373
+ border-radius: var(--radius);
374
+ cursor: pointer;
375
+ font-size: 13px;
376
+ transition: background 0.15s, border-color 0.15s;
377
+ }
378
+
379
+ .btn:hover { background: var(--surface-elevated); border-color: var(--muted); }
380
+
381
+ .btn-sm { padding: 4px 8px; font-size: 12px; }
382
+ .btn-danger:hover { border-color: var(--color-error); color: var(--color-error); }
383
+
384
+ .status {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ gap: 6px;
388
+ }
389
+
390
+ .status::before {
391
+ content: '';
392
+ width: 8px;
393
+ height: 8px;
394
+ border-radius: 50%;
395
+ background: var(--muted);
396
+ }
397
+
398
+ .status.connected::before { background: var(--color-success); }
399
+ .status.active::before { background: var(--color-warning); animation: pulse 1s ease-in-out infinite; }
400
+
401
+ .role { font-weight: 500; font-size: 12px; }
402
+ .role.user { color: var(--accent); }
403
+ .role.assistant { color: var(--color-success); }
404
+ .role.ambient { color: var(--muted); }
405
+
406
+ .mono { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent); }
407
+ .muted { color: var(--muted); }
408
+ .truncate { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
409
+
410
+ .empty, .empty-small {
411
+ color: var(--muted);
412
+ font-style: italic;
413
+ text-align: center;
414
+ }
415
+ .empty { padding: 24px 0; }
416
+ .empty-small { padding: 12px 0; }
417
+
418
+ .link {
419
+ color: var(--accent);
420
+ text-decoration: none;
421
+ font-size: 12px;
422
+ }
423
+
424
+ .link:hover { text-decoration: underline; }
425
+
426
+ .back {
427
+ color: var(--muted);
428
+ text-decoration: none;
429
+ font-size: 13px;
430
+ }
431
+
432
+ .back:hover { color: var(--text); }
433
+
434
+ /* ─── Page Header ────────────────────────────────────────────────────── */
435
+
436
+ .page-header {
437
+ display: flex;
438
+ justify-content: space-between;
439
+ align-items: center;
440
+ margin-bottom: 16px;
441
+ }
442
+
443
+ .page-header h2 {
444
+ font-size: 18px;
445
+ font-weight: 600;
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 10px;
449
+ }
450
+
451
+ .search-box input, .select {
452
+ background: var(--bg);
453
+ border: 1px solid var(--border);
454
+ color: var(--text);
455
+ padding: 8px 12px;
456
+ border-radius: var(--radius);
457
+ font-size: 13px;
458
+ width: 200px;
459
+ }
460
+
461
+ .search-box input:focus, .select:focus {
462
+ outline: none;
463
+ border-color: var(--accent);
464
+ }
465
+
466
+ /* ─── Scroll container with edge-fade ───────────────────────────────── */
467
+
468
+ .table-scroll {
469
+ overflow-x: auto;
470
+ -webkit-overflow-scrolling: touch;
471
+ /* symmetric two-edge fade — RTL-safe, no directional offset */
472
+ mask-image: linear-gradient(90deg, transparent 0, black 16px, black calc(100% - 16px), transparent 100%);
473
+ }
474
+
475
+ /* prevent column compression — table sizes to content, .table-scroll handles scroll */
476
+ .table-scroll .table { width: max-content; min-width: 100%; }
477
+
478
+ /* ─── Responsive ─────────────────────────────────────────────────────── */
479
+
480
+ @media (max-width: 768px) {
481
+ .shell { padding: 0 12px; }
482
+ .header { flex-direction: column; gap: 12px; }
483
+ .health-banner { flex-direction: column; gap: 8px; }
484
+ .nav { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
485
+ .nav::-webkit-scrollbar { display: none; }
486
+ .nav-item { padding: 10px 12px; white-space: nowrap; }
487
+ .page-header { flex-direction: column; gap: 12px; align-items: flex-start; }
488
+ .search-box input { width: 100%; }
489
+ }
490
+
491
+ @media (max-width: 640px) {
492
+ .panel-body { padding: 8px 12px; }
493
+ .nav-item { min-height: 44px; display: flex; align-items: center; }
494
+ .btn { min-height: 44px; }
495
+ }
496
+
497
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
498
+
499
+ @media (prefers-reduced-motion: reduce) {
500
+ .badge.pulse,
501
+ .status.active::before { animation: none; }
502
+ }
503
+ </style>
504
+ </head>
505
+ <body hx-ext="sse" sse-connect="/dashboard/events">
506
+ <div class="shell">
507
+ <!-- Header -->
508
+ <div class="header">
509
+ <div class="header-left">
510
+ <div class="logo">
511
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
512
+ <path d="M6.5 4.5C6.5 2.5 13.5 2.5 13.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>
513
+ <circle cx="10" cy="9" r="4" stroke="currentColor" stroke-width="1.5" fill="none"/>
514
+ <line x1="10" y1="13" x2="10" y2="18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
515
+ <line x1="7" y1="16" x2="13" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
516
+ </svg>
517
+ Mercury <span class="logo-version">{{VERSION}}</span>
518
+ </div>
519
+ </div>
520
+ <div id="active-count" sse-swap="active-count"></div>
521
+ </div>
522
+
523
+ <!-- Health Banner -->
524
+ <div class="health-banner" id="health" sse-swap="health">
525
+ <div class="health-status">
526
+ <span class="health-icon">⏳</span>
527
+ <span class="health-message">Connecting...</span>
528
+ </div>
529
+ <div class="health-meta">
530
+ <span class="connection" id="connection">live</span>
531
+ </div>
532
+ </div>
533
+
534
+ <!-- Navigation -->
535
+ <nav class="nav" id="nav">
536
+ <a class="nav-item active"
537
+ hx-get="/dashboard/page/overview"
538
+ hx-target="#main"
539
+ hx-push-url="true"
540
+ onclick="setActiveNav(this)"
541
+ title="Summary of spaces, recent activity, and system health (shortcut: 1)">Overview</a>
542
+ <a class="nav-item"
543
+ hx-get="/dashboard/page/spaces"
544
+ hx-target="#main"
545
+ hx-push-url="true"
546
+ onclick="setActiveNav(this)"
547
+ title="Manage memory spaces and their linked conversations (shortcut: 2)">Spaces</a>
548
+ <a class="nav-item"
549
+ hx-get="/dashboard/page/conversations"
550
+ hx-target="#main"
551
+ hx-push-url="true"
552
+ onclick="setActiveNav(this)"
553
+ title="Browse all discovered platform conversations across adapters (shortcut: 3)">Conversations</a>
554
+ <a class="nav-item"
555
+ hx-get="/dashboard/page/tasks"
556
+ hx-target="#main"
557
+ hx-push-url="true"
558
+ onclick="setActiveNav(this)"
559
+ title="Scheduled tasks — cron jobs and one-shot timers (shortcut: 4)">Tasks</a>
560
+ <a class="nav-item"
561
+ hx-get="/dashboard/page/permissions"
562
+ hx-target="#main"
563
+ hx-push-url="true"
564
+ onclick="setActiveNav(this)"
565
+ title="Role assignments and permission sets per space (shortcut: 5)">Permissions</a>
566
+ <a class="nav-item"
567
+ hx-get="/dashboard/page/logs"
568
+ hx-target="#main"
569
+ hx-push-url="true"
570
+ onclick="setActiveNav(this)"
571
+ title="Live agent logs (shortcut: 6)">Logs</a>
572
+ <a class="nav-item"
573
+ hx-get="/dashboard/page/usage"
574
+ hx-target="#main"
575
+ hx-push-url="true"
576
+ onclick="setActiveNav(this)"
577
+ title="Token and cost usage statistics (shortcut: 7)">Usage</a>
578
+ <a class="nav-item"
579
+ hx-get="/dashboard/page/billing"
580
+ hx-target="#main"
581
+ hx-push-url="true"
582
+ onclick="setActiveNav(this)"
583
+ title="Subscription and billing details (shortcut: 8)">Billing</a>
584
+ <a class="nav-item"
585
+ hx-get="/dashboard/page/keys"
586
+ hx-target="#main"
587
+ hx-push-url="true"
588
+ onclick="setActiveNav(this)"
589
+ title="Manage AI provider API keys">API keys</a>
590
+ <a class="nav-item"
591
+ hx-get="/dashboard/page/features"
592
+ hx-target="#main"
593
+ hx-push-url="true"
594
+ onclick="setActiveNav(this)"
595
+ title="Experimental features and extension configuration">Features</a>
596
+ </nav>
597
+
598
+ <!-- Main Content -->
599
+ <main id="main" hx-get="/dashboard/page/overview" hx-trigger="load">
600
+ <div class="empty">Loading...</div>
601
+ </main>
602
+ </div>
603
+
604
+ <script>
605
+ // Navigation (global for hx-on/htmx; Biome cannot see HTML callers)
606
+ globalThis.setActiveNav = function setActiveNav(el) {
607
+ for (const item of document.querySelectorAll('.nav-item')) {
608
+ item.classList.remove('active');
609
+ }
610
+ el.classList.add('active');
611
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
612
+ el.scrollIntoView({ inline: 'center', block: 'nearest', behavior: reducedMotion ? 'auto' : 'smooth' });
613
+ }
614
+
615
+ // Scroll initially active tab into view on load
616
+ document.addEventListener('DOMContentLoaded', () => {
617
+ const active = document.querySelector('.nav-item.active');
618
+ if (active) active.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'auto' });
619
+ });
620
+
621
+ // Handle browser back/forward
622
+ window.addEventListener('popstate', () => {
623
+ const path = window.location.pathname;
624
+ const navItems = document.querySelectorAll('.nav-item');
625
+ navItems.forEach(item => {
626
+ const href = item.getAttribute('hx-get');
627
+ if (path.includes(href.replace('/dashboard/page/', ''))) {
628
+ item.classList.add('active');
629
+ } else {
630
+ item.classList.remove('active');
631
+ }
632
+ });
633
+ });
634
+
635
+ // SSE connection state
636
+ const connectionEl = document.getElementById('connection');
637
+
638
+ document.body.addEventListener('htmx:sseOpen', () => {
639
+ connectionEl.textContent = 'live';
640
+ connectionEl.classList.remove('disconnected');
641
+ });
642
+
643
+ document.body.addEventListener('htmx:sseError', () => {
644
+ connectionEl.textContent = 'reconnecting...';
645
+ connectionEl.classList.add('disconnected');
646
+ });
647
+
648
+ document.body.addEventListener('htmx:sseClose', () => {
649
+ connectionEl.textContent = 'disconnected';
650
+ connectionEl.classList.add('disconnected');
651
+ });
652
+
653
+ // Table search filter
654
+ function _filterTable(input, tableId) {
655
+ const filter = input.value.toLowerCase();
656
+ const table = document.getElementById(tableId);
657
+ const rows = table.querySelectorAll('tbody tr');
658
+
659
+ rows.forEach(row => {
660
+ const text = row.textContent.toLowerCase();
661
+ row.style.display = text.includes(filter) ? '' : 'none';
662
+ });
663
+ }
664
+
665
+ // Space filter for the tasks table
666
+ function _filterTasksBySpace(select) {
667
+ const wanted = select.value;
668
+ document.querySelectorAll('#tasks-table tbody tr').forEach(row => {
669
+ row.style.display = (!wanted || row.dataset.spaceId === wanted) ? '' : 'none';
670
+ });
671
+ }
672
+
673
+ // Keyboard shortcuts
674
+ document.addEventListener('keydown', (e) => {
675
+ // Don't trigger when typing in inputs
676
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
677
+
678
+ const navItems = document.querySelectorAll('.nav-item');
679
+
680
+ if (e.key === '1') navItems[0]?.click();
681
+ if (e.key === '2') navItems[1]?.click();
682
+ if (e.key === '3') navItems[2]?.click();
683
+ if (e.key === '4') navItems[3]?.click();
684
+ if (e.key === '5') navItems[4]?.click();
685
+ if (e.key === '6') navItems[5]?.click();
686
+ if (e.key === '7') navItems[6]?.click();
687
+ if (e.key === '8') navItems[7]?.click();
688
+ if (e.key === '?') alert('Keyboard shortcuts:\n1-8: Navigate tabs\n/: Focus search');
689
+ if (e.key === '/') {
690
+ e.preventDefault();
691
+ document.querySelector('input[type="text"]')?.focus();
692
+ }
693
+ });
694
+
695
+ // Refresh current page after htmx actions (like delete, pause, etc.)
696
+ document.body.addEventListener('htmx:afterRequest', (e) => {
697
+ if (e.detail.requestConfig.verb !== 'get') {
698
+ // Refresh the current page after mutations
699
+ setTimeout(() => {
700
+ const activeNav = document.querySelector('.nav-item.active');
701
+ if (activeNav) activeNav.click();
702
+ }, 100);
703
+ }
704
+ });
705
+
706
+ // Handle hash-based navigation on page load (for refresh support)
707
+ window.addEventListener('DOMContentLoaded', () => {
708
+ const hash = window.location.hash.slice(1); // Remove #
709
+ if (hash) {
710
+ // Find the nav item that matches and click it
711
+ const navItems = document.querySelectorAll('.nav-item');
712
+ let found = false;
713
+ for (const item of navItems) {
714
+ const href = item.getAttribute('hx-get');
715
+ if (href?.includes(hash.split('/')[0])) {
716
+ item.click();
717
+ found = true;
718
+ break;
719
+ }
720
+ }
721
+ // If it's a detail page (e.g., groups/id), load it directly
722
+ if (!found && hash.includes('/')) {
723
+ htmx.ajax('GET', `/dashboard/page/${hash}`, '#main');
724
+ }
725
+ }
726
+ });
727
+ </script>
728
+ </body>
729
+ </html>