@jungjaehoon/mama-os 0.32.3 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +15 -17
  2. package/dist/agent/os-agent-capabilities.md +2 -2
  3. package/dist/api/graph-api.d.ts +4 -4
  4. package/dist/api/graph-api.d.ts.map +1 -1
  5. package/dist/api/graph-api.js +47 -78
  6. package/dist/api/graph-api.js.map +1 -1
  7. package/dist/api/index.d.ts +10 -0
  8. package/dist/api/index.d.ts.map +1 -1
  9. package/dist/api/index.js +9 -2
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/api/operator-handler.d.ts +1 -1
  12. package/dist/api/operator-handler.js +1 -1
  13. package/dist/api/runtime-status-handler.d.ts +41 -0
  14. package/dist/api/runtime-status-handler.d.ts.map +1 -0
  15. package/dist/api/runtime-status-handler.js +55 -0
  16. package/dist/api/runtime-status-handler.js.map +1 -0
  17. package/dist/cli/commands/start.d.ts.map +1 -1
  18. package/dist/cli/commands/start.js +58 -23
  19. package/dist/cli/commands/start.js.map +1 -1
  20. package/dist/cli/runtime/api-routes-init.d.ts +2 -0
  21. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  22. package/dist/cli/runtime/api-routes-init.js +62 -1
  23. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  24. package/dist/cli/runtime/api-server-init.d.ts +12 -1
  25. package/dist/cli/runtime/api-server-init.d.ts.map +1 -1
  26. package/dist/cli/runtime/api-server-init.js +54 -0
  27. package/dist/cli/runtime/api-server-init.js.map +1 -1
  28. package/dist/cli/runtime/utilities.d.ts +1 -1
  29. package/dist/cli/runtime/utilities.d.ts.map +1 -1
  30. package/dist/cli/runtime/utilities.js +3 -1
  31. package/dist/cli/runtime/utilities.js.map +1 -1
  32. package/dist/gateways/message-router.d.ts +4 -0
  33. package/dist/gateways/message-router.d.ts.map +1 -1
  34. package/dist/gateways/message-router.js +62 -13
  35. package/dist/gateways/message-router.js.map +1 -1
  36. package/dist/gateways/owner-report-inbox.d.ts +40 -0
  37. package/dist/gateways/owner-report-inbox.d.ts.map +1 -0
  38. package/dist/gateways/owner-report-inbox.js +128 -0
  39. package/dist/gateways/owner-report-inbox.js.map +1 -0
  40. package/dist/gateways/report-projection-v1.d.ts +41 -0
  41. package/dist/gateways/report-projection-v1.d.ts.map +1 -0
  42. package/dist/gateways/report-projection-v1.js +160 -0
  43. package/dist/gateways/report-projection-v1.js.map +1 -0
  44. package/dist/gateways/session-store.d.ts +22 -0
  45. package/dist/gateways/session-store.d.ts.map +1 -1
  46. package/dist/gateways/session-store.js +76 -3
  47. package/dist/gateways/session-store.js.map +1 -1
  48. package/dist/gateways/telegram-message-ledger.d.ts +10 -0
  49. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  50. package/dist/gateways/telegram-message-ledger.js +23 -2
  51. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  52. package/dist/gateways/telegram-report-context-store.d.ts +193 -0
  53. package/dist/gateways/telegram-report-context-store.d.ts.map +1 -0
  54. package/dist/gateways/telegram-report-context-store.js +612 -0
  55. package/dist/gateways/telegram-report-context-store.js.map +1 -0
  56. package/dist/gateways/telegram.d.ts +9 -0
  57. package/dist/gateways/telegram.d.ts.map +1 -1
  58. package/dist/gateways/telegram.js +100 -0
  59. package/dist/gateways/telegram.js.map +1 -1
  60. package/dist/gateways/types.d.ts +12 -0
  61. package/dist/gateways/types.d.ts.map +1 -1
  62. package/dist/multi-agent/dashboard-agent-persona.js +2 -2
  63. package/dist/operator/board-slot-instructions.d.ts +1 -1
  64. package/dist/operator/board-slot-instructions.js +1 -1
  65. package/dist/operator/operator-trigger-loop.d.ts +17 -1
  66. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  67. package/dist/operator/operator-trigger-loop.js +87 -23
  68. package/dist/operator/operator-trigger-loop.js.map +1 -1
  69. package/dist/operator/report-delivery-coordinator.d.ts +82 -0
  70. package/dist/operator/report-delivery-coordinator.d.ts.map +1 -0
  71. package/dist/operator/report-delivery-coordinator.js +123 -0
  72. package/dist/operator/report-delivery-coordinator.js.map +1 -0
  73. package/dist/operator/situation-report.d.ts +7 -0
  74. package/dist/operator/situation-report.d.ts.map +1 -1
  75. package/dist/operator/situation-report.js +12 -0
  76. package/dist/operator/situation-report.js.map +1 -1
  77. package/package.json +1 -2
  78. package/public/viewer/js/modules/system.js +183 -0
  79. package/public/viewer/js/utils/ui-commands.js +4 -9
  80. package/public/viewer/log-viewer.html +2 -192
  81. package/public/viewer/operator/operator.css +1 -0
  82. package/public/viewer/operator/operator.js +14830 -0
  83. package/public/viewer/src/modules/system.ts +242 -0
  84. package/public/viewer/src/utils/ui-commands.ts +4 -8
  85. package/public/viewer/sw.js +44 -10
  86. package/public/viewer/viewer.css +338 -331
  87. package/public/viewer/viewer.html +388 -1472
  88. package/public/ui/assets/index-BDXDGEV6.js +0 -62
  89. package/public/ui/assets/index-po2tN0xq.css +0 -1
  90. package/public/ui/index.html +0 -14
  91. package/public/ui/theme-init.js +0 -20
  92. package/public/viewer/js/modules/agents.js +0 -1153
  93. package/public/viewer/js/modules/chat.js +0 -1993
  94. package/public/viewer/js/modules/connector-feed.js +0 -352
  95. package/public/viewer/js/modules/dashboard.js +0 -456
  96. package/public/viewer/js/modules/entity-audit.js +0 -106
  97. package/public/viewer/js/modules/entity-review.js +0 -66
  98. package/public/viewer/js/modules/settings.js +0 -1257
  99. package/public/viewer/src/modules/agents.ts +0 -1302
  100. package/public/viewer/src/modules/chat.ts +0 -2426
  101. package/public/viewer/src/modules/connector-feed.ts +0 -391
  102. package/public/viewer/src/modules/dashboard.ts +0 -498
  103. package/public/viewer/src/modules/entity-audit.ts +0 -154
  104. package/public/viewer/src/modules/entity-review.ts +0 -175
  105. package/public/viewer/src/modules/settings.ts +0 -1547
@@ -255,56 +255,28 @@
255
255
  background: #ffce00;
256
256
  }
257
257
 
258
- /* ── Mobile bottom tab bar ── */
259
- #mama-mobile-tabs {
260
- display: none;
261
- position: fixed;
262
- bottom: 0;
263
- left: 0;
264
- right: 0;
265
- height: 64px;
266
- background: #ffffff;
267
- border-top: 1px solid #ede9e1;
268
- z-index: 100;
269
- padding: 0 8px;
270
- align-items: center;
271
- justify-content: space-around;
272
- }
273
- .mama-mobile-tab {
258
+ /* ── Group headings inside the rail ── */
259
+ .mama-nav-group {
274
260
  display: flex;
275
261
  flex-direction: column;
276
- align-items: center;
277
262
  gap: 2px;
278
- padding: 8px 12px;
279
- border: none;
280
- background: transparent;
281
- color: #6b6560;
282
- font-size: 10px;
283
- font-weight: 600;
284
- text-decoration: none;
285
- font-family: 'Inter', 'Noto Sans KR', sans-serif;
286
- cursor: pointer;
287
- transition: color 0.15s ease;
288
- }
289
- .mama-mobile-tab i,
290
- .mama-mobile-tab svg {
291
- width: 20px;
292
- height: 20px;
263
+ width: 100%;
264
+ margin-bottom: 10px;
293
265
  }
294
- .mama-mobile-tab.mama-nav-active {
295
- color: #ffce00;
296
- position: relative;
266
+ .mama-nav-group-label {
267
+ padding: 0 12px;
268
+ font-family: 'Inter', 'Noto Sans KR', sans-serif;
269
+ font-size: 10px;
270
+ font-weight: 700;
271
+ letter-spacing: 0.06em;
272
+ text-transform: uppercase;
273
+ color: #9b9389;
274
+ white-space: nowrap;
275
+ opacity: 0;
276
+ transition: opacity 0.15s ease;
297
277
  }
298
- .mama-mobile-tab.mama-nav-active::after {
299
- content: '';
300
- position: absolute;
301
- top: 0;
302
- left: 50%;
303
- transform: translateX(-50%);
304
- width: 24px;
305
- height: 2px;
306
- border-radius: 1px;
307
- background: #ffce00;
278
+ #mama-sidebar:hover .mama-nav-group-label {
279
+ opacity: 1;
308
280
  }
309
281
 
310
282
  /* ── Slot container ── */
@@ -315,101 +287,14 @@
315
287
  min-height: 200px;
316
288
  }
317
289
 
318
- /* ── Settings Sections ── */
319
- .mama-settings-section {
320
- border-bottom: 1px solid #ede9e1;
321
- padding: 16px 0;
322
- border-radius: 4px;
323
- }
324
- .mama-settings-section:last-child {
325
- border-bottom: none;
326
- }
327
- .mama-settings-heading {
328
- font-family: 'Inter', 'Noto Sans KR', sans-serif;
329
- font-size: 16px;
330
- font-weight: 600;
331
- color: #1a1a1a;
332
- cursor: pointer;
333
- display: flex;
334
- align-items: center;
335
- justify-content: space-between;
336
- user-select: none;
337
- padding: 0 0 8px 0;
338
- margin: 0;
339
- border: none;
340
- background: none;
341
- width: 100%;
342
- text-align: left;
343
- }
344
- .mama-settings-heading:hover {
345
- color: #3d3630;
346
- }
347
- .mama-collapse-icon {
348
- font-size: 14px;
349
- color: #9b9389;
350
- transition: transform 0.2s ease;
351
- display: inline-block;
352
- }
353
- .mama-settings-section.collapsed .mama-settings-body {
354
- display: none;
355
- }
356
- .mama-settings-section.collapsed .mama-collapse-icon {
357
- transform: rotate(-90deg);
358
- }
359
-
360
290
  /* ── Responsive ── */
361
291
  @media (max-width: 767px) {
362
292
  #mama-sidebar {
363
293
  display: none;
364
294
  }
365
- #mama-mobile-tabs {
366
- display: flex;
367
- }
368
295
  main.flex-1 {
369
296
  margin-left: 0 !important;
370
- padding-bottom: 72px;
371
- }
372
- /* Hide right panel chat on mobile — use full-screen chat tab instead */
373
- #chat-resize-handle-bar {
374
- display: none !important;
375
- }
376
- #chat-panel-wrapper {
377
- display: none !important;
378
- }
379
- #chat-collapsed-bar {
380
- display: none !important;
381
- }
382
- /* Full-screen chat overlay when chat tab is active */
383
- #chat-panel-wrapper.mobile-chat-active {
384
- display: flex !important;
385
- position: fixed;
386
- top: 0;
387
- left: 0;
388
- right: 0;
389
- bottom: 64px;
390
- width: 100% !important;
391
- height: auto !important;
392
- z-index: 90;
393
- border-left: none;
394
- flex-direction: column;
395
- }
396
- #chat-panel-wrapper.mobile-chat-active #chat-panel {
397
- flex: 1;
398
- display: flex;
399
- flex-direction: column;
400
- height: 100% !important;
401
- min-height: 0;
402
- overflow: visible;
403
- }
404
- #chat-panel-wrapper.mobile-chat-active #chat-messages {
405
- flex: 1;
406
- min-height: 0;
407
- overflow-y: auto;
408
- }
409
- #chat-panel-wrapper.mobile-chat-active .chat-input-area,
410
- #chat-panel-wrapper.mobile-chat-active > div:last-child {
411
- flex-shrink: 0;
412
- padding-bottom: max(4px, env(safe-area-inset-bottom));
297
+ padding-bottom: 64px;
413
298
  }
414
299
  }
415
300
  @media (min-width: 768px) {
@@ -420,6 +305,9 @@
420
305
  </style>
421
306
 
422
307
  <link rel="stylesheet" href="/viewer/viewer.css" />
308
+ <!-- Operator content bundle styles. Its reset is scoped under #operator-root,
309
+ so it cannot fight the viewer's own global reset. -->
310
+ <link rel="stylesheet" href="/viewer/operator/operator.css" />
423
311
  </head>
424
312
  <body class="bg-background text-mama-black font-body">
425
313
  <div id="app" class="h-screen flex flex-col">
@@ -429,89 +317,85 @@
429
317
  <img src="/viewer/icons/mama-icon.svg" alt="MAMA" width="28" height="28" />
430
318
  <span>MAMA</span>
431
319
  </a>
320
+ <!-- Grouped rail. Every entry is a real hash link: the address bar is
321
+ the single source of truth for which route is showing. -->
432
322
  <div class="mama-nav-items">
433
- <a class="mama-nav-item" href="/ui" title="Operator Board (beta)">
434
- <i data-lucide="zap"></i>
435
- <span>Operator (beta)</span>
436
- </a>
437
- <button
438
- class="mama-nav-item mama-nav-active"
439
- data-tab="feed"
440
- onclick="window.switchTab && window.switchTab('feed')"
441
- >
442
- <i data-lucide="rss"></i>
443
- <span>Feed</span>
444
- </button>
445
- <button
446
- class="mama-nav-item"
447
- data-tab="wiki"
448
- onclick="window.switchTab && window.switchTab('wiki')"
449
- >
450
- <i data-lucide="book-open"></i>
451
- <span>Wiki</span>
452
- </button>
453
- <button
454
- class="mama-nav-item"
455
- data-tab="memory"
456
- onclick="window.switchTab && window.switchTab('memory')"
457
- >
458
- <i data-lucide="brain"></i>
459
- <span>Memory</span>
460
- </button>
461
- <button
462
- class="mama-nav-item"
463
- data-tab="logs"
464
- onclick="window.switchTab && window.switchTab('logs')"
465
- >
466
- <i data-lucide="terminal"></i>
467
- <span>Logs</span>
468
- </button>
469
- <button
470
- class="mama-nav-item"
471
- data-tab="agents"
472
- onclick="window.switchTab && window.switchTab('agents')"
473
- >
474
- <i data-lucide="bot"></i>
475
- <span>Agents</span>
476
- </button>
477
- <button
478
- class="mama-nav-item"
479
- data-tab="settings"
480
- onclick="window.switchTab && window.switchTab('settings')"
481
- >
482
- <i data-lucide="settings"></i>
483
- <span>Settings</span>
484
- </button>
323
+ <div class="mama-nav-group">
324
+ <span class="mama-nav-group-label">Operator</span>
325
+ <a class="mama-nav-item" data-route="operator/board" href="#operator/board">
326
+ <i data-lucide="layout-dashboard"></i>
327
+ <span>Board</span>
328
+ </a>
329
+ <a class="mama-nav-item" data-route="operator/tasks" href="#operator/tasks">
330
+ <i data-lucide="list-checks"></i>
331
+ <span>Tasks</span>
332
+ </a>
333
+ <a class="mama-nav-item" data-route="operator/triggers" href="#operator/triggers">
334
+ <i data-lucide="zap"></i>
335
+ <span>Triggers</span>
336
+ </a>
337
+ </div>
338
+ <div class="mama-nav-group">
339
+ <span class="mama-nav-group-label">Knowledge</span>
340
+ <a class="mama-nav-item" data-route="knowledge/memory" href="#knowledge/memory">
341
+ <i data-lucide="brain"></i>
342
+ <span>Memory</span>
343
+ </a>
344
+ <a class="mama-nav-item" data-route="knowledge/wiki" href="#knowledge/wiki">
345
+ <i data-lucide="book-open"></i>
346
+ <span>Wiki</span>
347
+ </a>
348
+ </div>
349
+ <div class="mama-nav-group">
350
+ <span class="mama-nav-group-label">System</span>
351
+ <a class="mama-nav-item" data-route="system/runtime" href="#system/runtime">
352
+ <i data-lucide="activity"></i>
353
+ <span>Runtime</span>
354
+ </a>
355
+ <a class="mama-nav-item" data-route="system/connectors" href="#system/connectors">
356
+ <i data-lucide="plug"></i>
357
+ <span>Connectors</span>
358
+ </a>
359
+ <a class="mama-nav-item" data-route="system/logs" href="#system/logs">
360
+ <i data-lucide="terminal"></i>
361
+ <span>Logs</span>
362
+ </a>
363
+ </div>
485
364
  </div>
486
365
  </nav>
487
366
 
488
- <!-- Content Area: Main + Chat Panel (SmartStore layout) -->
367
+ <!-- Content Area -->
489
368
  <div id="content-area" class="flex-1 flex min-h-0 overflow-hidden">
490
369
  <!-- Main Content -->
491
370
  <main class="flex-1 overflow-hidden flex flex-col min-w-0">
492
- <!-- Dashboard Tab -->
493
- <div class="tab-content" id="tab-dashboard">
494
- <div class="flex-1 flex flex-col min-h-0 overflow-y-auto p-4 md:p-6">
495
- <div id="dashboard-slots" class="mama-slots-container">
496
- <div
497
- id="slots-empty"
498
- style="padding: 40px; text-align: center; color: #9e9891; font-size: 14px"
499
- >
500
- No briefing yet. Run the orchestrator to generate the first report.
501
- </div>
502
- </div>
503
- </div>
504
- </div>
371
+ <!-- Mobile sub-views: the same hash routes the desktop rail uses. -->
372
+ <nav id="mama-subviews" class="mama-subviews" aria-label="Sub views">
373
+ <a class="mama-subview-chip" data-route="operator/board" href="#operator/board"
374
+ >Board</a
375
+ >
376
+ <a class="mama-subview-chip" data-route="operator/tasks" href="#operator/tasks"
377
+ >Tasks</a
378
+ >
379
+ <a class="mama-subview-chip" data-route="operator/triggers" href="#operator/triggers"
380
+ >Triggers</a
381
+ >
382
+ <a class="mama-subview-chip" data-route="knowledge/memory" href="#knowledge/memory"
383
+ >Memory</a
384
+ >
385
+ <a class="mama-subview-chip" data-route="knowledge/wiki" href="#knowledge/wiki">Wiki</a>
386
+ <a class="mama-subview-chip" data-route="system/runtime" href="#system/runtime"
387
+ >Runtime</a
388
+ >
389
+ <a class="mama-subview-chip" data-route="system/connectors" href="#system/connectors"
390
+ >Connectors</a
391
+ >
392
+ <a class="mama-subview-chip" data-route="system/logs" href="#system/logs">Logs</a>
393
+ </nav>
505
394
 
506
- <!-- Feed Tab -->
507
- <div class="tab-content" id="tab-feed">
508
- <div class="flex-1 flex flex-col min-h-0 overflow-y-auto p-4 md:p-6">
509
- <div id="feed-content">
510
- <div style="padding: 40px; text-align: center; color: #9e9891; font-size: 14px">
511
- Loading connectors...
512
- </div>
513
- </div>
514
- </div>
395
+ <!-- Operator: the React content bundle mounts here once and stays
396
+ mounted; other routes only hide it. -->
397
+ <div class="tab-content" id="tab-operator">
398
+ <div id="operator-mount" class="flex-1 min-h-0"></div>
515
399
  </div>
516
400
 
517
401
  <!-- Wiki Tab -->
@@ -525,6 +409,20 @@
525
409
  </div>
526
410
  </div>
527
411
 
412
+ <!-- Runtime Tab: rendered from /api/runtime/status only. -->
413
+ <div class="tab-content" id="tab-runtime">
414
+ <div class="flex-1 flex flex-col min-h-0 overflow-y-auto p-4 md:p-6">
415
+ <div id="runtime-content"></div>
416
+ </div>
417
+ </div>
418
+
419
+ <!-- Connectors Tab: read-only state from the same runtime snapshot. -->
420
+ <div class="tab-content" id="tab-connectors">
421
+ <div class="flex-1 flex flex-col min-h-0 overflow-y-auto p-4 md:p-6">
422
+ <div id="connectors-list"></div>
423
+ </div>
424
+ </div>
425
+
528
426
  <!-- Logs Tab -->
529
427
  <div class="tab-content" id="tab-logs">
530
428
  <iframe
@@ -534,236 +432,12 @@
534
432
  ></iframe>
535
433
  </div>
536
434
 
537
- <!-- Agents Tab -->
538
- <div class="tab-content" id="tab-agents">
539
- <div class="flex-1 overflow-y-auto p-4" id="agents-content"></div>
540
- </div>
541
-
542
- <!-- Settings Tab -->
543
- <div class="tab-content" id="tab-settings">
544
- <div class="flex-1 flex flex-col min-h-0 overflow-y-auto p-2 md:p-3">
545
- <!-- Compact Header -->
546
- <div class="flex items-center justify-between mb-3">
547
- <h1 class="text-sm font-bold text-gray-900">Settings</h1>
548
- <span class="text-[10px] text-gray-400">MAMA OS Configuration</span>
549
- </div>
550
-
551
- <!-- Section 1: Connectors -->
552
- <div class="mama-settings-section">
553
- <h3
554
- class="mama-settings-heading"
555
- onclick="this.parentElement.classList.toggle('collapsed')"
556
- >
557
- Connectors <span class="mama-collapse-icon">&#9662;</span>
558
- </h3>
559
- <div class="mama-settings-body">
560
- <p class="text-[10px] text-gray-500 mb-2">
561
- Data source connectors for polling external channels. Configure in
562
- <code>~/.mama/connectors.json</code>.
563
- </p>
564
- <div id="settings-connectors-list" class="space-y-1.5">
565
- <p class="text-[10px] text-gray-400">Loading connectors...</p>
566
- </div>
567
- </div>
568
- </div>
569
-
570
- <!-- Section 3: System -->
571
- <div class="mama-settings-section">
572
- <h3
573
- class="mama-settings-heading"
574
- onclick="this.parentElement.classList.toggle('collapsed')"
575
- >
576
- System <span class="mama-collapse-icon">&#9662;</span>
577
- </h3>
578
- <div class="mama-settings-body">
579
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
580
- <!-- Left: Health + DB Info -->
581
- <div class="space-y-3">
582
- <!-- Health Check -->
583
- <section>
584
- <h2 class="text-xs font-semibold text-gray-700 mb-1.5">Health</h2>
585
- <div
586
- id="settings-system-health"
587
- class="bg-white border border-gray-200 rounded-lg p-2 shadow-sm"
588
- >
589
- <p class="text-[10px] text-gray-400">Health status will appear here.</p>
590
- </div>
591
- </section>
592
-
593
- <!-- Database Info -->
594
- <section>
595
- <h2 class="text-xs font-semibold text-gray-700 mb-1.5">Database</h2>
596
- <div
597
- id="settings-system-db"
598
- class="bg-white border border-gray-200 rounded-lg p-2 shadow-sm"
599
- >
600
- <p class="text-[10px] text-gray-400">Database info will appear here.</p>
601
- </div>
602
- </section>
603
- </div>
604
-
605
- <!-- Right: Token Usage + Metrics + Cron -->
606
- <div class="space-y-3">
607
- <!-- Token Usage -->
608
- <section>
609
- <h2 class="text-xs font-semibold text-gray-700 mb-1.5">Token Budget</h2>
610
- <div
611
- id="settings-system-tokens"
612
- class="bg-white border border-gray-200 rounded-lg p-2 shadow-sm"
613
- >
614
- <div class="grid grid-cols-2 gap-1.5">
615
- <div>
616
- <label class="block text-[9px] text-gray-500 mb-0.5"
617
- >Daily Limit</label
618
- >
619
- <input
620
- type="number"
621
- id="settings-token-daily-limit"
622
- placeholder="1000000"
623
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50"
624
- />
625
- </div>
626
- <div>
627
- <label class="block text-[9px] text-gray-500 mb-0.5">Alert ($)</label>
628
- <input
629
- type="number"
630
- id="settings-token-alert-threshold"
631
- step="0.01"
632
- placeholder="5.00"
633
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50"
634
- />
635
- </div>
636
- </div>
637
- </div>
638
- </section>
639
-
640
- <!-- Metrics -->
641
- <section>
642
- <h2 class="text-xs font-semibold text-gray-700 mb-1.5">Metrics</h2>
643
- <div class="bg-white border border-gray-200 rounded-lg p-2 shadow-sm">
644
- <div class="grid grid-cols-2 gap-1.5">
645
- <div>
646
- <label class="block text-[9px] text-gray-500 mb-0.5"
647
- >Enable metrics</label
648
- >
649
- <input
650
- type="checkbox"
651
- id="settings-metrics-enabled"
652
- checked
653
- class="w-4 h-4 rounded border-gray-300 text-yellow-500 focus:ring-yellow-400"
654
- />
655
- </div>
656
- <div>
657
- <label class="block text-[9px] text-gray-500 mb-0.5"
658
- >Retention (days)</label
659
- >
660
- <input
661
- type="number"
662
- id="settings-metrics-retention"
663
- min="1"
664
- max="90"
665
- value="7"
666
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50"
667
- />
668
- </div>
669
- </div>
670
- </div>
671
- </section>
672
-
673
- <!-- Scheduled Jobs -->
674
- <section>
675
- <h2 class="text-xs font-semibold text-gray-700 mb-1.5">Scheduled Jobs</h2>
676
- <div
677
- id="settings-cron-container"
678
- class="bg-white border border-gray-200 rounded-lg p-2 shadow-sm mb-1.5"
679
- >
680
- <p class="text-[10px] text-gray-500">Loading...</p>
681
- </div>
682
- <details class="bg-white border border-gray-200 rounded-lg shadow-sm">
683
- <summary
684
- class="px-2 py-1.5 text-[10px] font-medium text-gray-600 cursor-pointer hover:bg-gray-50"
685
- >
686
- + Add New Job
687
- </summary>
688
- <div
689
- class="p-2 border-t border-gray-100 space-y-1.5"
690
- id="settings-cron-form"
691
- >
692
- <div class="grid grid-cols-2 gap-1.5">
693
- <input
694
- type="text"
695
- id="settings-cron-name"
696
- placeholder="Job Name"
697
- aria-label="Cron job name"
698
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50"
699
- />
700
- <input
701
- type="text"
702
- id="settings-cron-expr"
703
- placeholder="0 * * * *"
704
- aria-label="Cron expression"
705
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50"
706
- />
707
- </div>
708
- <textarea
709
- id="settings-cron-prompt"
710
- rows="2"
711
- placeholder="Task prompt..."
712
- aria-label="Cron job task prompt"
713
- class="w-full px-1.5 py-0.5 text-[10px] border border-gray-200 rounded bg-gray-50 resize-none"
714
- ></textarea>
715
- <button
716
- onclick="settingsModule.addCronJob()"
717
- class="w-full px-2 py-1 text-[10px] font-medium bg-yellow-400 hover:bg-yellow-300 rounded"
718
- >
719
- Add
720
- </button>
721
- </div>
722
- </details>
723
- </section>
724
- </div>
725
- </div>
726
- </div>
727
- </div>
728
-
729
- <!-- Action Buttons -->
730
- <div class="flex items-center justify-between mt-3 pt-3 border-t border-gray-200">
731
- <button
732
- onclick="settingsModule.resetForm()"
733
- class="px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded transition-colors"
734
- >
735
- Reset
736
- </button>
737
- <div class="flex items-center gap-2">
738
- <span id="settings-status" class="text-[10px] text-gray-500"></span>
739
- <button
740
- onclick="settingsModule.saveAndRestart()"
741
- class="px-4 py-1.5 text-xs font-medium text-black bg-yellow-400 hover:bg-yellow-300 rounded transition-colors flex items-center gap-1.5"
742
- >
743
- <svg
744
- xmlns="http://www.w3.org/2000/svg"
745
- width="12"
746
- height="12"
747
- viewBox="0 0 24 24"
748
- fill="none"
749
- stroke="currentColor"
750
- stroke-width="2"
751
- >
752
- <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
753
- <polyline points="17 21 17 13 7 13 7 21" />
754
- <polyline points="7 3 7 8 15 8" />
755
- </svg>
756
- Save &amp; Restart
757
- </button>
758
- </div>
759
- </div>
760
- </div>
761
- </div>
762
-
763
435
  <div class="tab-content" id="tab-memory">
764
436
  <div class="flex-1 flex flex-col min-h-0">
765
437
  <!-- Compact Filter Bar (full width, top) -->
766
- <div class="flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white">
438
+ <div
439
+ class="memory-controls flex items-center gap-2 px-3 py-2 border-b border-gray-200 bg-white"
440
+ >
767
441
  <!-- Toggle sidebar -->
768
442
  <button
769
443
  id="checkpoint-sidebar-toggle"
@@ -814,7 +488,6 @@
814
488
  id="topic-filter"
815
489
  placeholder="Filter topics..."
816
490
  oninput="filterByTopic(this.value)"
817
- style="width: 160px"
818
491
  />
819
492
  <datalist id="topic-filter-list"></datalist>
820
493
  <select
@@ -836,7 +509,6 @@
836
509
  id="search-input"
837
510
  placeholder="Search content..."
838
511
  onkeyup="handleGraphSearch(event)"
839
- style="width: 160px"
840
512
  />
841
513
  <button
842
514
  onclick="clearFilters()"
@@ -1199,355 +871,27 @@
1199
871
  ></div>
1200
872
  </div>
1201
873
  </main>
1202
-
1203
- <!-- Chat Resize Handle (SmartStore pattern) -->
1204
- <div
1205
- id="chat-resize-handle-bar"
1206
- class="w-1 flex-shrink-0 cursor-col-resize bg-transparent hover:bg-mama-yellow/30 active:bg-mama-yellow/50 transition-colors chat-resize-visible"
1207
- ></div>
1208
-
1209
- <!-- Right Chat Panel (always visible, SmartStore Layout pattern) -->
1210
- <div
1211
- id="chat-panel-wrapper"
1212
- class="flex-shrink-0 border-l border-gray-200"
1213
- style="width: 380px; background: #f5f3ef"
1214
- >
1215
- <div id="chat-panel" class="flex flex-col">
1216
- <!-- Header (SmartStore ChatPanel pattern) -->
1217
- <div
1218
- id="chat-header"
1219
- class="px-4 py-3 border-b border-gray-200 flex items-center gap-2.5 bg-white flex-shrink-0"
1220
- >
1221
- <div
1222
- class="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0"
1223
- style="background: #ffce00"
1224
- >
1225
- <svg
1226
- class="w-4 h-4 text-white"
1227
- fill="none"
1228
- stroke="currentColor"
1229
- stroke-width="2"
1230
- viewBox="0 0 24 24"
1231
- >
1232
- <path
1233
- stroke-linecap="round"
1234
- stroke-linejoin="round"
1235
- d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
1236
- />
1237
- </svg>
1238
- </div>
1239
- <div class="flex-1 min-w-0">
1240
- <div class="flex items-center gap-1.5">
1241
- <span class="text-[13px] font-semibold text-mama-black">MAMA Chat</span>
1242
- <span
1243
- id="chat-tab-indicator"
1244
- class="text-[10px] bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded-md font-medium"
1245
- >Feed</span
1246
- >
1247
- <span
1248
- class="status-indicator w-1.5 h-1.5 rounded-full bg-gray-300 flex-shrink-0"
1249
- id="chat-status"
1250
- ></span>
1251
- </div>
1252
- </div>
1253
- <div class="flex items-center gap-1 flex-shrink-0">
1254
- <button
1255
- class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-400 hover:text-red-500 hover:bg-red-50 transition-colors"
1256
- id="chat-clear-btn"
1257
- onclick="clearChatHistory()"
1258
- title="Clear chat"
1259
- style="display: none"
1260
- >
1261
- <svg
1262
- class="w-3.5 h-3.5"
1263
- fill="none"
1264
- stroke="currentColor"
1265
- stroke-width="1.5"
1266
- viewBox="0 0 24 24"
1267
- >
1268
- <path
1269
- stroke-linecap="round"
1270
- stroke-linejoin="round"
1271
- d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
1272
- />
1273
- </svg>
1274
- </button>
1275
- <button
1276
- id="chat-collapse-btn"
1277
- class="w-7 h-7 rounded-lg flex items-center justify-center text-gray-400 hover:text-mama-black hover:bg-gray-100 transition-colors"
1278
- title="Collapse chat"
1279
- onclick="toggleChatPanel()"
1280
- >
1281
- <svg
1282
- class="w-4 h-4"
1283
- fill="none"
1284
- stroke="currentColor"
1285
- stroke-width="2"
1286
- viewBox="0 0 24 24"
1287
- >
1288
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
1289
- </svg>
1290
- </button>
1291
- </div>
1292
- </div>
1293
-
1294
- <!-- Messages (SmartStore pattern: flex-1 overflow-auto) -->
1295
- <div
1296
- class="flex-1 overflow-auto p-4 space-y-3"
1297
- id="chat-messages"
1298
- style="background: #f5f3ef"
1299
- >
1300
- <div class="flex flex-col items-center justify-center h-full text-center px-6">
1301
- <div
1302
- class="w-12 h-12 rounded-lg bg-mama-yellow/10 flex items-center justify-center mb-4"
1303
- >
1304
- <svg
1305
- class="w-6 h-6 text-mama-yellow"
1306
- fill="none"
1307
- stroke="currentColor"
1308
- stroke-width="1.5"
1309
- viewBox="0 0 24 24"
1310
- >
1311
- <path
1312
- stroke-linecap="round"
1313
- stroke-linejoin="round"
1314
- d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"
1315
- />
1316
- </svg>
1317
- </div>
1318
- <p class="text-[13px] text-gray-500 font-medium">Ask MAMA anything</p>
1319
- <p class="text-[11px] text-gray-400 mt-1">
1320
- Current page context is sent automatically
1321
- </p>
1322
- </div>
1323
- </div>
1324
-
1325
- <!-- Attachment preview -->
1326
- <div
1327
- id="chat-attachment-preview"
1328
- class="hidden px-3 pt-2 border-t border-gray-200 flex items-center gap-2 flex-wrap flex-shrink-0"
1329
- >
1330
- <img
1331
- id="chat-attachment-thumb"
1332
- class="w-12 h-12 object-cover rounded-lg border border-gray-200"
1333
- src="data:,"
1334
- alt="preview"
1335
- />
1336
- <span
1337
- id="chat-attachment-name"
1338
- class="text-[11px] text-gray-400 truncate max-w-[100px]"
1339
- ></span>
1340
- <button
1341
- class="text-gray-400 hover:text-red-500"
1342
- onclick="clearAttachment()"
1343
- title="Remove"
1344
- >
1345
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1346
- <line x1="18" y1="6" x2="6" y2="18" stroke-width="2" stroke-linecap="round" />
1347
- <line x1="6" y1="6" x2="18" y2="18" stroke-width="2" stroke-linecap="round" />
1348
- </svg>
1349
- </button>
1350
- </div>
1351
-
1352
- <!-- Input (SmartStore ChatPanel pattern: inline row) -->
1353
- <div class="p-3 border-t border-gray-200 bg-white flex-shrink-0">
1354
- <input
1355
- type="file"
1356
- id="chat-file-input"
1357
- class="hidden"
1358
- accept="image/*,.pdf,.doc,.docx,.txt,.csv,.xls,.xlsx,.md,.json,.html,.htm,.xml,.zip,.gz,.ppt,.pptx"
1359
- onchange="handleFileSelect(event)"
1360
- />
1361
- <div class="flex gap-2">
1362
- <button
1363
- class="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-100 text-gray-400 hover:text-mama-black hover:bg-gray-200 transition-all"
1364
- id="chat-attach"
1365
- onclick="document.getElementById('chat-file-input').click()"
1366
- title="Attach file"
1367
- >
1368
- <svg
1369
- class="w-4 h-4"
1370
- fill="none"
1371
- stroke="currentColor"
1372
- stroke-width="1.5"
1373
- viewBox="0 0 24 24"
1374
- >
1375
- <path
1376
- stroke-linecap="round"
1377
- stroke-linejoin="round"
1378
- d="M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13"
1379
- />
1380
- </svg>
1381
- </button>
1382
- <button
1383
- class="w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 bg-gray-100 text-gray-400 hover:text-mama-black hover:bg-gray-200 transition-all relative"
1384
- id="chat-mic"
1385
- onclick="toggleVoiceInput()"
1386
- title="Voice input"
1387
- >
1388
- <svg
1389
- class="w-4 h-4"
1390
- fill="none"
1391
- stroke="currentColor"
1392
- stroke-width="1.5"
1393
- viewBox="0 0 24 24"
1394
- >
1395
- <path
1396
- stroke-linecap="round"
1397
- stroke-linejoin="round"
1398
- d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z"
1399
- />
1400
- </svg>
1401
- <span
1402
- class="recording-dot absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full hidden"
1403
- ></span>
1404
- </button>
1405
- <input
1406
- type="text"
1407
- id="chat-input"
1408
- placeholder="Type your message..."
1409
- class="flex-1 bg-gray-100 border-0 rounded-lg px-3.5 py-2 text-[13px] text-mama-black placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-mama-yellow/20 transition-all disabled:opacity-50"
1410
- disabled
1411
- />
1412
- <button
1413
- id="chat-send"
1414
- onclick="sendChatMessage()"
1415
- disabled
1416
- class="w-9 h-9 rounded-lg bg-mama-yellow text-mama-black flex items-center justify-center flex-shrink-0 hover:bg-mama-yellow-hover disabled:opacity-40 transition-all"
1417
- >
1418
- <svg
1419
- class="w-4 h-4"
1420
- fill="none"
1421
- stroke="currentColor"
1422
- stroke-width="2"
1423
- viewBox="0 0 24 24"
1424
- >
1425
- <path
1426
- stroke-linecap="round"
1427
- stroke-linejoin="round"
1428
- d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"
1429
- />
1430
- </svg>
1431
- </button>
1432
- </div>
1433
- </div>
1434
- </div>
1435
- </div>
1436
-
1437
- <!-- Chat Collapsed Bar (visible when chat is collapsed) -->
1438
- <div
1439
- id="chat-collapsed-bar"
1440
- class="hidden flex-shrink-0 border-l border-gray-200 bg-white flex flex-col items-center py-3 w-10"
1441
- >
1442
- <button
1443
- onclick="toggleChatPanel()"
1444
- class="w-8 h-8 rounded-lg flex items-center justify-center text-gray-400 hover:text-mama-yellow hover:bg-mama-yellow/10 transition-colors"
1445
- title="Open chat"
1446
- >
1447
- <svg
1448
- class="w-4 h-4"
1449
- fill="none"
1450
- stroke="currentColor"
1451
- strokeWidth="1.5"
1452
- viewBox="0 0 24 24"
1453
- >
1454
- <path
1455
- d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
1456
- stroke-width="2"
1457
- stroke-linecap="round"
1458
- stroke-linejoin="round"
1459
- />
1460
- </svg>
1461
- </button>
1462
- <span
1463
- id="chat-badge-collapsed"
1464
- class="hidden w-2 h-2 bg-red-500 rounded-full mt-1 animate-pulse"
1465
- ></span>
1466
- </div>
1467
874
  </div>
1468
875
  <!-- /content-area -->
1469
876
 
1470
- <!-- Mobile Bottom Tab Bar -->
1471
- <div id="mama-mobile-tabs">
1472
- <button
1473
- class="mama-mobile-tab"
1474
- data-tab="chat"
1475
- onclick="window.switchTab && window.switchTab('chat')"
1476
- >
1477
- <i data-lucide="message-circle"></i>
1478
- <span>Chat</span>
1479
- </button>
1480
- <button
1481
- class="mama-mobile-tab"
1482
- data-tab="agents"
1483
- onclick="window.switchTab && window.switchTab('agents')"
1484
- >
1485
- <i data-lucide="bot"></i>
1486
- <span>Agents</span>
1487
- </button>
1488
- <button
1489
- class="mama-mobile-tab"
1490
- data-tab="memory"
1491
- onclick="window.switchTab && window.switchTab('memory')"
1492
- >
877
+ <!-- Mobile group bar. The rail is hidden under 768px, so this is the only
878
+ way to cross groups on a phone; the chip row above the content stays
879
+ as sub-navigation WITHIN the active group. Three groups, no overflow
880
+ menu: there is no fourth item to hide behind one. -->
881
+ <nav id="mama-mobile-tabs" aria-label="Groups">
882
+ <a class="mama-mobile-tab" data-group="operator" href="#operator/board">
883
+ <i data-lucide="zap"></i>
884
+ <span>Operator</span>
885
+ </a>
886
+ <a class="mama-mobile-tab" data-group="knowledge" href="#knowledge/memory">
1493
887
  <i data-lucide="brain"></i>
1494
- <span>Memory</span>
1495
- </button>
1496
- <button
1497
- class="mama-mobile-tab mama-nav-active"
1498
- data-tab="more"
1499
- onclick="window.toggleMobileMore && window.toggleMobileMore()"
1500
- >
1501
- <i data-lucide="menu"></i>
1502
- <span>More</span>
1503
- </button>
1504
- </div>
1505
- <!-- Mobile "More" overflow menu -->
1506
- <div
1507
- id="mama-mobile-more"
1508
- style="
1509
- display: none;
1510
- position: fixed;
1511
- bottom: 64px;
1512
- left: 0;
1513
- right: 0;
1514
- background: #fff;
1515
- border-top: 1px solid #ede9e1;
1516
- z-index: 99;
1517
- padding: 8px;
1518
- "
1519
- >
1520
- <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px">
1521
- <button
1522
- class="mama-mobile-tab"
1523
- data-tab="feed"
1524
- onclick="window.switchTab && window.switchTab('feed')"
1525
- >
1526
- <i data-lucide="rss"></i><span>Feed</span>
1527
- </button>
1528
- <button
1529
- class="mama-mobile-tab"
1530
- data-tab="wiki"
1531
- onclick="window.switchTab && window.switchTab('wiki')"
1532
- >
1533
- <i data-lucide="book-open"></i><span>Wiki</span>
1534
- </button>
1535
- <button
1536
- class="mama-mobile-tab"
1537
- data-tab="logs"
1538
- onclick="window.switchTab && window.switchTab('logs')"
1539
- >
1540
- <i data-lucide="scroll-text"></i><span>Logs</span>
1541
- </button>
1542
- <button
1543
- class="mama-mobile-tab"
1544
- data-tab="settings"
1545
- onclick="window.switchTab && window.switchTab('settings')"
1546
- >
1547
- <i data-lucide="settings"></i><span>Settings</span>
1548
- </button>
1549
- </div>
1550
- </div>
888
+ <span>Knowledge</span>
889
+ </a>
890
+ <a class="mama-mobile-tab" data-group="system" href="#system/runtime">
891
+ <i data-lucide="settings"></i>
892
+ <span>System</span>
893
+ </a>
894
+ </nav>
1551
895
 
1552
896
  <div
1553
897
  class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center"
@@ -1914,201 +1258,235 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
1914
1258
  </div>
1915
1259
 
1916
1260
  <script type="module">
1917
- import { ChatModule } from '/viewer/js/modules/chat.js';
1918
1261
  import { GraphModule } from '/viewer/js/modules/graph.js';
1919
1262
  import { MemoryModule } from '/viewer/js/modules/memory.js';
1920
- import { DashboardModule } from '/viewer/js/modules/dashboard.js';
1921
- import { SettingsModule } from '/viewer/js/modules/settings.js';
1922
- import { ConnectorFeedModule } from '/viewer/js/modules/connector-feed.js';
1923
1263
  import { WikiModule } from '/viewer/js/modules/wiki.js';
1924
- import { AgentsModule } from '/viewer/js/modules/agents.js';
1264
+ import { renderRuntimeView, renderConnectorsView } from '/viewer/js/modules/system.js';
1925
1265
  import { API } from '/viewer/js/utils/api.js';
1926
1266
  import { startUICommandPolling, reportPageContext } from '/viewer/js/utils/ui-commands.js';
1927
1267
 
1928
1268
  const memory = new MemoryModule();
1929
- const chat = new ChatModule(memory);
1930
1269
  const graph = new GraphModule();
1931
- const dashboard = new DashboardModule();
1932
- const settings = new SettingsModule();
1933
- const connectorFeed = new ConnectorFeedModule();
1934
1270
  const wiki = new WikiModule();
1935
- const agentsModule = new AgentsModule();
1936
-
1937
- // ── Chat Panel: toggle, resize, tab indicator (SmartStore pattern) ──
1938
- const CHAT_WIDTH_KEY = 'mama-chat-width';
1939
- const CHAT_COLLAPSED_KEY = 'mama-chat-collapsed';
1940
- const MIN_CHAT = 280;
1941
- const MAX_CHAT = 560;
1942
-
1943
- function toggleChatPanel() {
1944
- const wrapper = document.getElementById('chat-panel-wrapper');
1945
- const bar = document.getElementById('chat-collapsed-bar');
1946
- const handle = document.getElementById('chat-resize-handle-bar');
1947
- if (!wrapper || !bar) return;
1948
- const isCollapsed = wrapper.style.display === 'none';
1949
- if (isCollapsed) {
1950
- wrapper.style.display = '';
1951
- bar.classList.add('hidden');
1952
- if (handle) handle.classList.add('chat-resize-visible');
1953
- localStorage.setItem(CHAT_COLLAPSED_KEY, 'false');
1954
- } else {
1955
- wrapper.style.display = 'none';
1956
- bar.classList.remove('hidden');
1957
- if (handle) handle.classList.remove('chat-resize-visible');
1958
- localStorage.setItem(CHAT_COLLAPSED_KEY, 'true');
1959
- }
1271
+
1272
+ // Exposed for inline handlers and debugging.
1273
+ window.graphModule = graph;
1274
+ window.memoryModule = memory;
1275
+
1276
+ // ── Routing ─────────────────────────────────────────────────────────────
1277
+ // The hash is the single source of truth: every route is `<group>/<view>`,
1278
+ // with no per-tab implicit defaults. Nav entries are plain hash links, so
1279
+ // back/forward and deep links work without any pushState of our own.
1280
+ const ROUTES = {
1281
+ 'operator/board': { group: 'operator', view: 'board', region: 'tab-operator' },
1282
+ 'operator/tasks': { group: 'operator', view: 'tasks', region: 'tab-operator' },
1283
+ 'operator/triggers': { group: 'operator', view: 'triggers', region: 'tab-operator' },
1284
+ 'knowledge/memory': { group: 'knowledge', view: 'memory', region: 'tab-memory' },
1285
+ 'knowledge/wiki': { group: 'knowledge', view: 'wiki', region: 'tab-wiki' },
1286
+ 'system/runtime': { group: 'system', view: 'runtime', region: 'tab-runtime' },
1287
+ 'system/connectors': { group: 'system', view: 'connectors', region: 'tab-connectors' },
1288
+ 'system/logs': { group: 'system', view: 'logs', region: 'tab-logs' },
1289
+ };
1290
+ const DEFAULT_ROUTE = 'operator/board';
1291
+
1292
+ // Bare view names an agent navigate command or an old bookmark may carry.
1293
+ // Anything else falls back to the default rather than showing nothing.
1294
+ const ROUTE_ALIASES = {
1295
+ board: 'operator/board',
1296
+ tasks: 'operator/tasks',
1297
+ triggers: 'operator/triggers',
1298
+ memory: 'knowledge/memory',
1299
+ wiki: 'knowledge/wiki',
1300
+ runtime: 'system/runtime',
1301
+ connectors: 'system/connectors',
1302
+ logs: 'system/logs',
1303
+ };
1304
+
1305
+ function parseViewerHash() {
1306
+ const raw = String(window.location.hash || '').replace(/^#/, '');
1307
+ const [rawPath, query = ''] = raw.split('?');
1308
+ // Aliases must resolve here too, or a bookmarked '#logs' would land on
1309
+ // the default route while a navigate command for it works.
1310
+ const path = normalizeRoutePath(rawPath);
1311
+ const route = ROUTES[path];
1312
+ return {
1313
+ path,
1314
+ group: route.group,
1315
+ view: route.view,
1316
+ params: new URLSearchParams(query),
1317
+ };
1960
1318
  }
1961
- window.toggleChatPanel = toggleChatPanel;
1962
1319
 
1963
- // Restore collapsed state
1964
- if (localStorage.getItem(CHAT_COLLAPSED_KEY) === 'true') {
1965
- toggleChatPanel();
1320
+ function writeViewerHash(path, params) {
1321
+ const query = params ? params.toString() : '';
1322
+ const next = '#' + path + (query ? '?' + query : '');
1323
+ if (window.location.hash !== next) {
1324
+ window.location.hash = next;
1325
+ }
1966
1326
  }
1967
1327
 
1968
- // Restore width
1969
- {
1970
- const saved = localStorage.getItem(CHAT_WIDTH_KEY);
1971
- if (saved) {
1972
- const w = Math.max(MIN_CHAT, Math.min(MAX_CHAT, Number(saved)));
1973
- const wrapper = document.getElementById('chat-panel-wrapper');
1974
- if (wrapper) wrapper.style.width = w + 'px';
1328
+ function normalizeRoutePath(target) {
1329
+ const raw = String(target || '')
1330
+ .replace(/^#/, '')
1331
+ .trim();
1332
+ if (ROUTES[raw]) return raw;
1333
+ const alias = ROUTE_ALIASES[raw];
1334
+ return alias && ROUTES[alias] ? alias : DEFAULT_ROUTE;
1335
+ }
1336
+
1337
+ function routeKey(path, params) {
1338
+ const query = params ? params.toString() : '';
1339
+ return query ? path + '?' + query : path;
1340
+ }
1341
+
1342
+ // Hash selection params are `task` and `trigger` (the cross-surface
1343
+ // contract); the mount options keep the bundle's own taskId/triggerId
1344
+ // field names.
1345
+ function paramsToSelection(params) {
1346
+ const selection = {};
1347
+ const rawTask = params.get('task');
1348
+ const trigger = params.get('trigger');
1349
+ if (rawTask !== null && rawTask.trim() !== '') {
1350
+ // A task id is a positive integer. '-1', '0', '3.5', '1e3' and 'abc'
1351
+ // are not ids, and silently coercing them would open a drawer on
1352
+ // whatever row happened to answer.
1353
+ const taskId = Number(rawTask);
1354
+ if (Number.isInteger(taskId) && taskId > 0) {
1355
+ selection.taskId = taskId;
1356
+ }
1975
1357
  }
1358
+ if (trigger) {
1359
+ selection.triggerId = trigger;
1360
+ }
1361
+ return Object.keys(selection).length > 0 ? selection : undefined;
1976
1362
  }
1977
1363
 
1978
- // Chat resize handle (SmartStore Layout.tsx:158-176 pattern)
1979
- (function initChatResize() {
1980
- const handle = document.getElementById('chat-resize-handle-bar');
1981
- const wrapper = document.getElementById('chat-panel-wrapper');
1982
- if (!handle || !wrapper) return;
1364
+ function selectionToParams(selection) {
1365
+ const params = new URLSearchParams();
1366
+ if (selection && selection.taskId !== undefined && selection.taskId !== null) {
1367
+ params.set('task', String(selection.taskId));
1368
+ }
1369
+ if (selection && selection.triggerId) {
1370
+ params.set('trigger', selection.triggerId);
1371
+ }
1372
+ return params;
1373
+ }
1374
+
1375
+ function selectedItemFor(selection) {
1376
+ if (!selection) return undefined;
1377
+ if (selection.taskId !== undefined) return { type: 'task', id: String(selection.taskId) };
1378
+ if (selection.triggerId) return { type: 'trigger', id: selection.triggerId };
1379
+ return undefined;
1380
+ }
1381
+
1382
+ // ── Operator content bundle ─────────────────────────────────────────────
1383
+ // Mounted lazily on the first Operator route and then kept for the page
1384
+ // lifetime: unmounting would drop the report SSE stream and the query
1385
+ // cache and force a cold refetch every time the user comes back.
1386
+ let operatorHandle = null;
1387
+ let operatorMountPromise = null;
1388
+
1389
+ function renderOperatorLoadError(root, error) {
1390
+ root.innerHTML =
1391
+ '<div id="operator-load-error" class="m-6 max-w-md rounded-lg border border-gray-200 bg-white p-4 shadow-sm">' +
1392
+ '<p class="text-sm font-semibold text-mama-black">Operator failed to load</p>' +
1393
+ '<p class="mt-1 text-xs text-gray-500" data-detail></p>' +
1394
+ '<button type="button" class="mt-3 rounded bg-mama-yellow px-3 py-1.5 text-xs font-medium text-mama-black" data-reload>Reload</button>' +
1395
+ '</div>';
1396
+ const detail = root.querySelector('[data-detail]');
1397
+ if (detail) {
1398
+ detail.textContent = String((error && error.message) || error || 'Unknown error');
1399
+ }
1400
+ const reload = root.querySelector('[data-reload]');
1401
+ if (reload) {
1402
+ reload.addEventListener('click', () => window.location.reload());
1403
+ }
1404
+ }
1983
1405
 
1984
- handle.addEventListener('mousedown', (e) => {
1985
- e.preventDefault();
1986
- const startX = e.clientX;
1987
- const startWidth = wrapper.offsetWidth;
1988
- const onMove = (ev) => {
1989
- const delta = startX - ev.clientX;
1990
- const w = Math.max(MIN_CHAT, Math.min(MAX_CHAT, startWidth + delta));
1991
- wrapper.style.width = w + 'px';
1992
- };
1993
- const onUp = () => {
1994
- document.removeEventListener('mousemove', onMove);
1995
- document.removeEventListener('mouseup', onUp);
1996
- document.body.style.cursor = '';
1997
- document.body.style.userSelect = '';
1998
- localStorage.setItem(CHAT_WIDTH_KEY, String(wrapper.offsetWidth));
1999
- };
2000
- document.body.style.cursor = 'col-resize';
2001
- document.body.style.userSelect = 'none';
2002
- document.addEventListener('mousemove', onMove);
2003
- document.addEventListener('mouseup', onUp);
2004
- });
2005
- })();
1406
+ async function ensureOperator(view, selection) {
1407
+ const operatorRoot = document.getElementById('operator-mount');
1408
+ if (!operatorRoot) return;
2006
1409
 
2007
- // Start UI command polling (SmartStore Layout pattern)
2008
- startUICommandPolling(switchTab);
1410
+ if (operatorHandle) {
1411
+ operatorHandle.update(view, selection);
1412
+ return;
1413
+ }
2009
1414
 
2010
- // Expose modules globally for debugging and onclick handlers
2011
- window.chatModule = chat;
2012
- window.graphModule = graph;
2013
- window.memoryModule = memory;
2014
- window.dashboardModule = dashboard;
2015
- window.settingsModule = settings;
1415
+ if (!operatorMountPromise) {
1416
+ operatorMountPromise = import('/viewer/operator/operator.js')
1417
+ .then(({ mountOperator }) => {
1418
+ operatorHandle = mountOperator(operatorRoot, {
1419
+ initialView: view,
1420
+ initialSelection: selection,
1421
+ // In-content navigation is reported back so the address bar
1422
+ // stays truthful; host-driven update() deliberately does not
1423
+ // echo, so there is no loop here.
1424
+ onViewChange: (nextView, nextSelection) => {
1425
+ switchTab('operator/' + nextView, selectionToParams(nextSelection));
1426
+ },
1427
+ });
1428
+ })
1429
+ .catch((error) => {
1430
+ operatorMountPromise = null;
1431
+ console.error('[MAMA] Operator bundle failed to load:', error);
1432
+ renderOperatorLoadError(operatorRoot, error);
1433
+ });
1434
+ }
2016
1435
 
2017
- // Quiz choice button handler
2018
- window.sendQuizChoice = (choice) => {
2019
- chat.sendQuizChoice(choice);
2020
- };
1436
+ await operatorMountPromise;
1437
+ if (operatorHandle) {
1438
+ operatorHandle.update(view, selection);
1439
+ }
1440
+ }
2021
1441
 
2022
1442
  const STATE = {
2023
- currentTab: 'feed',
2024
- theme: 'dark',
1443
+ route: DEFAULT_ROUTE,
1444
+ routeKey: null,
1445
+ theme: 'light',
2025
1446
  graphLoaded: false,
2026
1447
  };
2027
1448
 
2028
- async function switchTab(tabName, params) {
2029
- STATE.currentTab = tabName;
1449
+ async function applyRoute(route) {
1450
+ STATE.route = route.path;
1451
+ STATE.routeKey = routeKey(route.path, route.params);
2030
1452
 
2031
- // Close mobile More menu if open
2032
- const moreMenu = document.getElementById('mama-mobile-more');
2033
- if (moreMenu) moreMenu.style.display = 'none';
2034
-
2035
- // Also highlight the source tab in More menu if it was selected from there
2036
- document.querySelectorAll('#mama-mobile-more .mama-mobile-tab').forEach((btn) => {
2037
- btn.classList.toggle('mama-nav-active', btn.dataset.tab === tabName);
1453
+ document.querySelectorAll('[data-route]').forEach((el) => {
1454
+ el.classList.toggle('mama-nav-active', el.dataset.route === route.path);
2038
1455
  });
2039
-
2040
- // Update chat panel tab indicator
2041
- const tabIndicator = document.getElementById('chat-tab-indicator');
2042
- if (tabIndicator) {
2043
- tabIndicator.textContent = tabName.charAt(0).toUpperCase() + tabName.slice(1);
2044
- }
2045
-
2046
- // Update sidebar nav items
2047
- document.querySelectorAll('#mama-sidebar .mama-nav-item').forEach((btn) => {
2048
- btn.classList.toggle('mama-nav-active', btn.dataset.tab === tabName);
2049
- });
2050
-
2051
- // Update mobile tab bar
2052
- document.querySelectorAll('#mama-mobile-tabs .mama-mobile-tab').forEach((btn) => {
2053
- btn.classList.toggle('mama-nav-active', btn.dataset.tab === tabName);
1456
+ // Mobile chips show only the active group's sub-views; the bottom bar
1457
+ // is what crosses groups.
1458
+ document.querySelectorAll('.mama-subview-chip').forEach((chip) => {
1459
+ chip.hidden = String(chip.dataset.route).split('/')[0] !== route.group;
2054
1460
  });
2055
- const moreBtn = document.querySelector('#mama-mobile-tabs [data-tab="more"]');
2056
- const overflowTabs = ['feed', 'wiki', 'logs', 'settings'];
2057
- if (moreBtn) {
2058
- moreBtn.classList.toggle('mama-nav-active', overflowTabs.includes(tabName));
2059
- }
2060
-
2061
- // Legacy: update any remaining [data-tab] buttons outside sidebar/mobile
2062
- document.querySelectorAll('[data-tab]').forEach((btn) => {
2063
- if (btn.closest('#mama-sidebar') || btn.closest('#mama-mobile-tabs')) return;
2064
- const isActive = btn.dataset.tab === tabName;
2065
- btn.classList.toggle('bg-indigo-100', isActive);
2066
- btn.classList.toggle('bg-mama-lavender/30', isActive);
2067
- btn.classList.toggle('text-mama-black', isActive);
1461
+ document.querySelectorAll('#mama-mobile-tabs .mama-mobile-tab').forEach((tab) => {
1462
+ tab.classList.toggle('mama-nav-active', tab.dataset.group === route.group);
2068
1463
  });
2069
1464
 
1465
+ const regionId = ROUTES[route.path].region;
2070
1466
  document.querySelectorAll('.tab-content').forEach((content) => {
2071
- content.classList.toggle('active', content.id === `tab-${tabName}`);
1467
+ content.classList.toggle('active', content.id === regionId);
2072
1468
  });
2073
1469
 
2074
- // Mobile chat: toggle full-screen chat overlay
2075
- const chatWrapper = document.getElementById('chat-panel-wrapper');
2076
- if (chatWrapper) {
2077
- if (tabName === 'chat') {
2078
- chatWrapper.classList.add('mobile-chat-active');
2079
- } else {
2080
- chatWrapper.classList.remove('mobile-chat-active');
2081
- }
2082
- }
1470
+ const selection = paramsToSelection(route.params);
1471
+ reportPageContext(
1472
+ route.path,
1473
+ {
1474
+ pageType: 'route',
1475
+ group: route.group,
1476
+ view: route.view,
1477
+ params: Object.fromEntries(route.params.entries()),
1478
+ },
1479
+ selectedItemFor(selection)
1480
+ );
2083
1481
 
2084
- const publishBasicTabContext = () => {
2085
- const selectedItem =
2086
- tabName === 'agents' && params?.id ? { type: 'agent', id: params.id } : undefined;
2087
- reportPageContext(
2088
- tabName,
2089
- { pageType: 'tab-switch', tab: tabName, ...(params || {}) },
2090
- selectedItem
2091
- );
2092
- };
1482
+ if (route.group === 'operator') {
1483
+ await ensureOperator(route.view, selection);
1484
+ return;
1485
+ }
2093
1486
 
2094
- if (tabName === 'dashboard') {
2095
- dashboard.init();
2096
- publishBasicTabContext();
2097
- } else if (tabName === 'settings') {
2098
- settings.init();
2099
- loadConnectorsList();
2100
- publishBasicTabContext();
2101
- } else if (tabName === 'memory') {
2102
- // Initialize sidebar state for current screen size
1487
+ if (route.view === 'memory') {
2103
1488
  updateCheckpointSidebarForScreenSize();
2104
-
2105
- // Load checkpoints
2106
- loadCheckpoints();
2107
-
2108
- // Load memory stats for analytics bar
2109
1489
  updateMemoryStats();
2110
-
2111
- // Load graph
2112
1490
  if (!STATE.graphLoaded) {
2113
1491
  try {
2114
1492
  const data = await graph.fetchData();
@@ -2119,38 +1497,68 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
2119
1497
  } catch (error) {
2120
1498
  console.error('[MAMA] Failed to load graph:', error);
2121
1499
  const loadingEl = document.getElementById('graph-loading');
2122
- if (loadingEl)
1500
+ if (loadingEl) {
2123
1501
  loadingEl.innerHTML = '<span class="text-red-500">Failed to load graph data</span>';
1502
+ }
2124
1503
  }
2125
1504
  }
2126
- publishBasicTabContext();
2127
- } else if (tabName === 'feed') {
2128
- connectorFeed.init();
2129
- publishBasicTabContext();
2130
- } else if (tabName === 'agents') {
2131
- agentsModule.init();
2132
- // Handle deep navigation: params.id opens agent detail, params.tab selects tab
2133
- if (params?.id) {
2134
- await agentsModule.navigateTo(params.id, params.tab);
2135
- } else {
2136
- agentsModule.showList();
2137
- }
2138
- } else if (tabName === 'wiki') {
1505
+ return;
1506
+ }
1507
+
1508
+ if (route.view === 'wiki') {
2139
1509
  wiki.init();
2140
- await wiki.navigateTo(params?.path);
2141
- } else if (tabName === 'logs') {
1510
+ await wiki.navigateTo(route.params.get('path') || undefined);
1511
+ return;
1512
+ }
1513
+
1514
+ // System is read-only truth: both views render the one authoritative
1515
+ // runtime snapshot, so the screen can never name a backend, model or
1516
+ // connector state the daemon is not actually running.
1517
+ if (route.view === 'runtime') {
1518
+ await renderRuntimeView(document.getElementById('runtime-content'));
1519
+ return;
1520
+ }
1521
+
1522
+ if (route.view === 'connectors') {
1523
+ await renderConnectorsView(document.getElementById('connectors-list'));
1524
+ return;
1525
+ }
1526
+
1527
+ if (route.view === 'logs') {
2142
1528
  const iframe = document.getElementById('logs-iframe');
2143
1529
  if (iframe && !iframe.getAttribute('src')) {
2144
1530
  iframe.src = '/viewer/log-viewer.html';
2145
1531
  }
2146
- publishBasicTabContext();
2147
- } else if (tabName === 'chat') {
2148
- publishBasicTabContext();
2149
- } else {
2150
- publishBasicTabContext();
2151
1532
  }
2152
1533
  }
2153
1534
 
1535
+ /**
1536
+ * Navigate by route path (`operator/tasks`) or bare view name (`tasks`).
1537
+ * Writes the hash first, then applies: the hashchange listener sees the
1538
+ * key it already holds and stays out of the way.
1539
+ */
1540
+ async function switchTab(target, params) {
1541
+ const path = normalizeRoutePath(target);
1542
+ const search =
1543
+ params instanceof URLSearchParams ? params : new URLSearchParams(params || {});
1544
+ writeViewerHash(path, search);
1545
+ await applyRoute({
1546
+ path,
1547
+ group: ROUTES[path].group,
1548
+ view: ROUTES[path].view,
1549
+ params: search,
1550
+ });
1551
+ }
1552
+
1553
+ window.addEventListener('hashchange', () => {
1554
+ const route = parseViewerHash();
1555
+ if (routeKey(route.path, route.params) === STATE.routeKey) return;
1556
+ applyRoute(route);
1557
+ });
1558
+
1559
+ // Agent → viewer navigation commands ride the same route resolver.
1560
+ startUICommandPolling(switchTab);
1561
+
2154
1562
  function toggleTheme() {
2155
1563
  // Light-only theme (brand unification) - no toggle needed
2156
1564
  }
@@ -2527,104 +1935,6 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
2527
1935
  .catch(() => (result.innerHTML = '<span class="text-red-500">Connection failed</span>'));
2528
1936
  }
2529
1937
 
2530
- async function sendChatMessage() {
2531
- if (pendingAttachment) {
2532
- const file = pendingAttachment;
2533
- // Don't clear attachment yet - wait for successful send
2534
-
2535
- const formData = new FormData();
2536
- formData.append('file', file);
2537
-
2538
- try {
2539
- const resp = await fetch('/api/upload', { method: 'POST', body: formData });
2540
- if (!resp.ok) throw new Error('Upload failed');
2541
- const data = await resp.json();
2542
-
2543
- // Check WS connection before sending
2544
- if (!chat.ws || chat.ws.readyState !== WebSocket.OPEN) {
2545
- chat.addSystemMessage('Not connected. Please wait for reconnection.', 'error');
2546
- return;
2547
- }
2548
-
2549
- const input = document.getElementById('chat-input');
2550
- const isImage = file.type.startsWith('image/');
2551
- const defaultMsg = isImage ? 'What is in this image?' : 'Analyze this document';
2552
- const message = input.value.trim() || defaultMsg;
2553
-
2554
- const mediaUrl = '/api/media/' + encodeURIComponent(data.filename);
2555
- const attachMeta = {
2556
- mediaUrl,
2557
- filename: data.filename,
2558
- originalName: file.name,
2559
- contentType: file.type,
2560
- isImage,
2561
- };
2562
-
2563
- // Save with attachment metadata for history persistence
2564
- chat.addUserMessageWithAttachment(message, attachMeta);
2565
-
2566
- chat.enableSend(false);
2567
- chat.ws.send(
2568
- JSON.stringify({
2569
- type: 'send',
2570
- sessionId: chat.sessionId,
2571
- content: message,
2572
- attachments: [
2573
- {
2574
- filename: data.filename,
2575
- contentType: data.contentType,
2576
- },
2577
- ],
2578
- })
2579
- );
2580
-
2581
- // Clear attachment only after successful send
2582
- clearAttachment();
2583
- input.value = '';
2584
- } catch (err) {
2585
- chat.addSystemMessage('Upload failed: ' + err.message, 'error');
2586
- // Keep pendingAttachment so user can retry
2587
- }
2588
- return;
2589
- }
2590
- chat.send();
2591
- }
2592
-
2593
- // Send quiz choice as message
2594
- window.sendQuizChoice = function (choice) {
2595
- if (chat && chat.ws && chat.ws.readyState === WebSocket.OPEN) {
2596
- // Set the input to the choice and send
2597
- const input = document.getElementById('chat-input');
2598
- if (input) {
2599
- input.value = choice;
2600
- chat.send();
2601
- }
2602
- }
2603
- };
2604
-
2605
- function toggleTTS() {
2606
- chat.toggleTTS && chat.toggleTTS();
2607
- }
2608
-
2609
- function toggleTTSPopup() {
2610
- const popup = document.getElementById('tts-speed-popup');
2611
- popup.classList.toggle('hidden');
2612
- }
2613
-
2614
- function updateTTSSpeed(value) {
2615
- document.getElementById('tts-speed-label').textContent = value + 'x';
2616
- document.getElementById('tts-speed-value').textContent = value + 'x';
2617
- chat.setTTSRate && chat.setTTSRate(parseFloat(value));
2618
- }
2619
-
2620
- function toggleHandsFree() {
2621
- chat.toggleHandsFree && chat.toggleHandsFree();
2622
- }
2623
-
2624
- function toggleVoiceInput() {
2625
- chat.toggleVoice && chat.toggleVoice();
2626
- }
2627
-
2628
1938
  function filterByTopic(topic) {
2629
1939
  graph.filterByTopic && graph.filterByTopic(topic);
2630
1940
  }
@@ -2633,11 +1943,17 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
2633
1943
  graph.filterByOutcome && graph.filterByOutcome(outcome);
2634
1944
  }
2635
1945
 
1946
+ // Clear is scoped to the Graph module instance that owns the filters. Each
1947
+ // control is looked up defensively: the bar is markup that can be edited,
1948
+ // and one missing input must not take the whole Clear action down.
2636
1949
  function clearFilters() {
2637
- document.getElementById('topic-filter').value = '';
2638
- document.getElementById('outcome-filter').value = '';
2639
- document.getElementById('search-input').value = '';
2640
- graph.clearFilters && graph.clearFilters();
1950
+ const topicFilter = document.getElementById('topic-filter');
1951
+ const outcomeFilter = document.getElementById('outcome-filter');
1952
+ const searchInput = document.getElementById('search-input');
1953
+ if (topicFilter) topicFilter.value = '';
1954
+ if (outcomeFilter) outcomeFilter.value = '';
1955
+ if (searchInput) searchInput.value = '';
1956
+ graph?.clearFilters?.();
2641
1957
  }
2642
1958
 
2643
1959
  async function exportDecisions(format) {
@@ -2692,174 +2008,8 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
2692
2008
  }
2693
2009
  }
2694
2010
 
2695
- let checkpointsData = [];
2696
-
2697
- function renderMarkdown(text) {
2698
- if (!text) return '';
2699
- try {
2700
- if (typeof marked !== 'undefined' && marked.parse) {
2701
- return marked.parse(text);
2702
- }
2703
- } catch (e) {
2704
- console.error('[MAMA] Markdown parse error:', e);
2705
- }
2706
- return text.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
2707
- }
2708
-
2709
- async function loadCheckpoints() {
2710
- const container = document.getElementById('checkpoint-list');
2711
- if (!container) {
2712
- console.error('[MAMA] checkpoint-list container not found');
2713
- return;
2714
- }
2715
-
2716
- console.log('[MAMA] Loading checkpoints...');
2717
-
2718
- try {
2719
- const data = await API.get('/api/checkpoints');
2720
- checkpointsData = data.checkpoints || [];
2721
-
2722
- console.log('[MAMA] Loaded', checkpointsData.length, 'checkpoints');
2723
-
2724
- if (checkpointsData.length > 0) {
2725
- container.innerHTML = checkpointsData
2726
- .map((cp, idx) => {
2727
- const fullSummary = renderMarkdown(cp.summary || '');
2728
- return `
2729
- <div class="checkpoint-item p-3 bg-white bg-white border border-gray-200 border-gray-300 rounded-lg cursor-pointer hover:border-indigo-500 transition-colors" onclick="toggleCheckpoint(${idx})">
2730
- <div class="text-xs font-semibold text-mama-black text-mama-black mb-1">${new Date(cp.timestamp).toLocaleString()}</div>
2731
- <div class="checkpoint-summary text-sm text-gray-700 text-gray-700 markdown-content line-clamp-3 overflow-hidden">${fullSummary}</div>
2732
- <div class="checkpoint-details hidden mt-3 pt-3 border-t border-gray-200 border-gray-300">
2733
- <div class="text-sm text-gray-700 text-gray-700 markdown-content max-h-64 overflow-y-auto">${fullSummary}</div>
2734
- </div>
2735
- </div>
2736
- `;
2737
- })
2738
- .join('');
2739
- } else {
2740
- container.innerHTML =
2741
- '<div class="text-center text-sm text-gray-500 py-4">No checkpoints found</div>';
2742
- }
2743
- } catch (error) {
2744
- console.error('[MAMA] Failed to load checkpoints:', error);
2745
- container.innerHTML =
2746
- '<div class="text-center text-sm text-red-500 py-4">Failed to load checkpoints</div>';
2747
- }
2748
- }
2749
-
2750
- function toggleCheckpoint(idx) {
2751
- const items = document.querySelectorAll('.checkpoint-item');
2752
- items.forEach((item, i) => {
2753
- const details = item.querySelector('.checkpoint-details');
2754
- const summary = item.querySelector('.checkpoint-summary');
2755
- if (i === idx) {
2756
- const isExpanded = !details.classList.contains('hidden');
2757
- details.classList.toggle('hidden');
2758
- summary.classList.toggle('hidden', !isExpanded);
2759
- item.classList.toggle('border-indigo-500', !isExpanded);
2760
- } else {
2761
- details.classList.add('hidden');
2762
- summary.classList.remove('hidden');
2763
- item.classList.remove('border-indigo-500');
2764
- }
2765
- });
2766
- }
2767
-
2768
- // Connector list loader for Settings
2769
- const CONNECTOR_NAMES = [
2770
- 'slack',
2771
- 'telegram',
2772
- 'discord',
2773
- 'chatwork',
2774
- 'gmail',
2775
- 'calendar',
2776
- 'notion',
2777
- 'obsidian',
2778
- 'kagemusha',
2779
- 'sheets',
2780
- 'trello',
2781
- 'drive',
2782
- 'imessage',
2783
- ];
2784
- const CONNECTOR_CATEGORIES = {
2785
- Messaging: ['slack', 'telegram', 'discord', 'chatwork'],
2786
- Google: ['gmail', 'calendar', 'drive', 'sheets'],
2787
- Knowledge: ['notion', 'obsidian', 'kagemusha'],
2788
- 'Dev Tools': ['claude-code'],
2789
- Other: ['trello', 'imessage'],
2790
- };
2791
- async function loadConnectorsList() {
2792
- const container = document.getElementById('settings-connectors-list');
2793
- if (!container) return;
2794
- let connectorStatus = {};
2795
- try {
2796
- const res = await fetch('/api/connectors/status');
2797
- if (res.ok) {
2798
- const data = await res.json();
2799
- for (const c of data.connectors || []) connectorStatus[c.name] = c;
2800
- }
2801
- } catch {}
2802
- // Load config
2803
- let config = {};
2804
- try {
2805
- const res = await fetch('/api/config');
2806
- if (res.ok) {
2807
- const data = await res.json();
2808
- config = data?.connectors || {};
2809
- }
2810
- } catch {}
2811
- let html = '';
2812
- for (const [category, names] of Object.entries(CONNECTOR_CATEGORIES)) {
2813
- html += `<div style="margin-bottom:12px"><div style="font-family:Inter,'Noto Sans KR',sans-serif;font-size:12px;font-weight:600;color:#6B6560;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px">${category}</div><div class="grid grid-cols-2 lg:grid-cols-3 gap-1.5">`;
2814
- for (const name of names) {
2815
- const status = connectorStatus[name];
2816
- const cfg = config[name];
2817
- const enabled = cfg?.enabled || status?.enabled || false;
2818
- const healthy = status?.healthy || false;
2819
- const lastPoll = status?.lastPollTime
2820
- ? new Date(status.lastPollTime).toLocaleTimeString('ko-KR', {
2821
- hour: '2-digit',
2822
- minute: '2-digit',
2823
- })
2824
- : null;
2825
- const channels = status?.channelCount || Object.keys(cfg?.channels || {}).length || 0;
2826
- const statusDot = enabled ? (healthy ? '#3A9E7E' : '#E8A040') : '#9E9891';
2827
- html += `<div style="background:#fff;border:1px solid #EDE9E1;border-radius:4px;padding:8px 10px;font-size:12px;display:flex;align-items:center;gap:8px">`;
2828
- html += `<span style="display:inline-block;width:6px;height:6px;background:${statusDot};border-radius:1px;flex-shrink:0"></span>`;
2829
- html += `<span style="font-weight:600;color:#1A1A1A;flex:1">${name}</span>`;
2830
- if (channels > 0)
2831
- html += `<span style="font-size:10px;color:#9E9891">${channels}ch</span>`;
2832
- if (lastPoll) html += `<span style="font-size:10px;color:#9E9891">${lastPoll}</span>`;
2833
- if (!enabled)
2834
- html += `<span style="font-size:9px;color:#9E9891;background:#F5F2ED;padding:1px 4px;border-radius:2px">off</span>`;
2835
- html += `</div>`;
2836
- }
2837
- html += `</div></div>`;
2838
- }
2839
- container.innerHTML = html;
2840
- }
2841
-
2842
- // Mobile "More" menu
2843
- function toggleMobileMore() {
2844
- const more = document.getElementById('mama-mobile-more');
2845
- if (!more) return;
2846
- const isVisible = more.style.display !== 'none';
2847
- more.style.display = isVisible ? 'none' : 'block';
2848
- // Update More button active state
2849
- const moreBtn = document.querySelector('#mama-mobile-tabs [data-tab="more"]');
2850
- if (moreBtn) moreBtn.classList.toggle('mama-nav-active', !isVisible);
2851
- }
2852
- function closeMobileMore() {
2853
- const more = document.getElementById('mama-mobile-more');
2854
- if (more) more.style.display = 'none';
2855
- const moreBtn = document.querySelector('#mama-mobile-tabs [data-tab="more"]');
2856
- if (moreBtn) moreBtn.classList.remove('mama-nav-active');
2857
- }
2858
-
2859
2011
  // Expose to window for onclick handlers
2860
2012
  window.switchTab = switchTab;
2861
- window.toggleMobileMore = toggleMobileMore;
2862
- window.closeMobileMore = closeMobileMore;
2863
2013
  window.toggleTheme = toggleTheme;
2864
2014
  window.toggleLegend = toggleLegend;
2865
2015
  window.closeDetailModal = closeDetailModal;
@@ -2869,266 +2019,32 @@ export MAMA_AUTH_TOKEN="your-secure-token-here"</pre
2869
2019
  window.showSaveDecisionForm = showSaveDecisionForm;
2870
2020
  window.hideSaveDecisionForm = hideSaveDecisionForm;
2871
2021
  window.submitSaveDecision = submitSaveDecision;
2872
- window.copyNgrokCommand = copyNgrokCommand;
2873
2022
  window.testConnection = testConnection;
2874
- window.sendChatMessage = sendChatMessage;
2875
- window.toggleTTS = toggleTTS;
2876
- window.toggleTTSPopup = toggleTTSPopup;
2877
- window.updateTTSSpeed = updateTTSSpeed;
2878
- window.toggleHandsFree = toggleHandsFree;
2879
- window.toggleVoiceInput = toggleVoiceInput;
2880
-
2881
- // File attachment state
2882
- let pendingAttachment = null;
2883
-
2884
- const FILE_ICONS = {
2885
- 'application/pdf': '📄',
2886
- 'text/plain': '📝',
2887
- 'text/csv': '📊',
2888
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '📃',
2889
- 'application/msword': '📃',
2890
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '📊',
2891
- 'application/vnd.ms-excel': '📊',
2892
- };
2893
-
2894
- function getFileIcon(mimeType) {
2895
- return FILE_ICONS[mimeType] || '📎';
2896
- }
2897
-
2898
- function handleFileSelect(event) {
2899
- const file = event.target.files[0];
2900
- if (!file) return;
2901
-
2902
- const preview = document.getElementById('chat-attachment-preview');
2903
- const thumb = document.getElementById('chat-attachment-thumb');
2904
- const name = document.getElementById('chat-attachment-name');
2905
-
2906
- if (file.type.startsWith('image/')) {
2907
- const reader = new FileReader();
2908
- reader.onload = (e) => {
2909
- thumb.src = e.target.result;
2910
- };
2911
- reader.readAsDataURL(file);
2912
- thumb.style.display = '';
2913
- } else {
2914
- // Show emoji icon for documents
2915
- thumb.style.display = 'none';
2916
- name.textContent = getFileIcon(file.type) + ' ' + file.name;
2917
- }
2918
-
2919
- if (file.type.startsWith('image/')) {
2920
- name.textContent = file.name;
2921
- }
2922
- preview.classList.remove('hidden');
2923
- pendingAttachment = file;
2924
- // cleared
2925
- }
2926
-
2927
- function clearAttachment() {
2928
- pendingAttachment = null;
2929
- // cleared
2930
- document.getElementById('chat-attachment-preview').classList.add('hidden');
2931
- document.getElementById('chat-file-input').value = '';
2932
- const thumb = document.getElementById('chat-attachment-thumb');
2933
- thumb.src = 'data:,';
2934
- thumb.style.display = '';
2935
- }
2936
-
2937
- window.handleFileSelect = handleFileSelect;
2938
- window.clearAttachment = clearAttachment;
2939
-
2940
- // Drag & drop support
2941
- const chatMessages = document.getElementById('chat-messages');
2942
- if (chatMessages) {
2943
- const panel = chatMessages.closest('#chat-panel') || chatMessages.parentElement;
2944
- ['dragenter', 'dragover'].forEach((evt) => {
2945
- panel.addEventListener(evt, (e) => {
2946
- e.preventDefault();
2947
- e.stopPropagation();
2948
- panel.classList.add('ring-2', 'ring-mama-yellow');
2949
- });
2950
- });
2951
- ['dragleave', 'drop'].forEach((evt) => {
2952
- panel.addEventListener(evt, (e) => {
2953
- e.preventDefault();
2954
- e.stopPropagation();
2955
- panel.classList.remove('ring-2', 'ring-mama-yellow');
2956
- });
2957
- });
2958
- panel.addEventListener('drop', (e) => {
2959
- const file = e.dataTransfer.files[0];
2960
- if (!file) return;
2961
-
2962
- // Allowed MIME types and extensions (match file input accept attribute)
2963
- const allowedTypes = [
2964
- 'image/',
2965
- 'application/pdf',
2966
- 'application/msword',
2967
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
2968
- 'text/plain',
2969
- 'text/csv',
2970
- 'text/markdown',
2971
- 'text/html',
2972
- 'text/xml',
2973
- 'application/json',
2974
- 'application/xml',
2975
- 'application/vnd.ms-excel',
2976
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
2977
- 'application/vnd.ms-powerpoint',
2978
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
2979
- 'application/zip',
2980
- 'application/gzip',
2981
- ];
2982
- const allowedExts = [
2983
- '.pdf',
2984
- '.doc',
2985
- '.docx',
2986
- '.txt',
2987
- '.csv',
2988
- '.xls',
2989
- '.xlsx',
2990
- '.md',
2991
- '.json',
2992
- '.html',
2993
- '.htm',
2994
- '.xml',
2995
- '.zip',
2996
- '.gz',
2997
- '.ppt',
2998
- '.pptx',
2999
- ];
3000
- const ext = '.' + (file.name.split('.').pop() || '').toLowerCase();
3001
-
3002
- const isAllowed =
3003
- allowedTypes.some((t) => file.type.startsWith(t)) || allowedExts.includes(ext);
3004
- if (isAllowed) {
3005
- const dt = new DataTransfer();
3006
- dt.items.add(file);
3007
- document.getElementById('chat-file-input').files = dt.files;
3008
- handleFileSelect({ target: { files: [file] } });
3009
- } else {
3010
- chat.addSystemMessage(
3011
- 'Unsupported file type. Allowed: images, PDF, DOC, TXT, CSV, XLS, PPT, MD, JSON, HTML, ZIP',
3012
- 'error'
3013
- );
3014
- }
3015
- });
3016
- }
3017
-
2023
+ window.filterByOutcome = filterByOutcome;
2024
+ window.clearFilters = clearFilters;
2025
+ window.exportDecisions = exportDecisions;
3018
2026
  window.filterByTopic = filterByTopic;
3019
2027
  window.handleGraphSearch = handleGraphSearch;
3020
2028
  window.saveOutcome = saveOutcome;
3021
- window.loadCheckpoints = loadCheckpoints;
3022
- window.toggleCheckpoint = toggleCheckpoint;
3023
2029
 
3024
2030
  document.addEventListener('DOMContentLoaded', () => {
3025
2031
  const userLang = navigator.language || navigator.userLanguage;
3026
2032
  document.documentElement.lang = userLang.split('-')[0];
3027
2033
 
3028
- // Initialize Lucide icons (sidebar + mobile tab bar)
2034
+ // Initialize Lucide icons (rail + chip row)
3029
2035
  if (typeof lucide !== 'undefined' && lucide.createIcons) {
3030
2036
  lucide.createIcons();
3031
2037
  }
3032
2038
 
3033
2039
  initTheme();
3034
- // Initialize floating chat panel bindings (session init is lazy, on first open)
3035
- chat.initFloating();
3036
- // Default to feed tab
3037
- switchTab('feed');
3038
- });
3039
2040
 
3040
- // Listen for postMessage from skill deployment
3041
- window.addEventListener('message', (event) => {
3042
- if (!event.data) return;
3043
-
3044
- // viewer:sendToChat — generic child iframe → chat handoff
3045
- if (event.data.type === 'viewer:sendToChat') {
3046
- const message = typeof event.data.message === 'string' ? event.data.message : '';
3047
- if (!message) {
3048
- return;
3049
- }
3050
- const input = document.getElementById('chat-input');
3051
- if (input && chat) {
3052
- input.value = message;
3053
- setTimeout(() => chat.send(), 50);
3054
- }
3055
- }
2041
+ // Route from the address bar; an empty or unknown hash is normalised to
2042
+ // the default so the URL always names what is on screen.
2043
+ const initialRoute = parseViewerHash();
2044
+ writeViewerHash(initialRoute.path, initialRoute.params);
2045
+ applyRoute(initialRoute);
3056
2046
  });
3057
2047
 
3058
- // Cleanup resources on page unload
3059
- window.addEventListener('beforeunload', () => {
3060
- // Cleanup dashboard intervals
3061
- if (window.dashboardModule?.cleanup) {
3062
- window.dashboardModule.cleanup();
3063
- }
3064
- // Cleanup chat resources
3065
- if (window.chatModule?.cleanup) {
3066
- window.chatModule.cleanup();
3067
- }
3068
- console.log('[MAMA] Resources cleaned up');
3069
- });
3070
-
3071
- // Context-aware help: ? button sends tab-specific prompt to chat
3072
- const TAB_HELP_PROMPTS = {
3073
- dashboard: `The user clicked the help button on the MAMA OS Dashboard tab.
3074
- Please explain what this tab shows:
3075
- - Gateway connection status (Discord, Slack, Telegram, Chatwork)
3076
- - MCP server status
3077
- - Multi-Agent swarm overview
3078
- - Token usage and cost tracking
3079
- - Scheduled jobs (Cron) status
3080
- Explain what each section means and what actions the user can take. Be friendly and concise.`,
3081
-
3082
- memory: `The user clicked the help button on the Memory tab.
3083
- Please explain this tab's features:
3084
- - Decision Graph: Visual graph showing relationships between decisions (click nodes for details)
3085
- - Checkpoints: Session checkpoint list (resume previous work)
3086
- - Stats bar: Total decisions, weekly activity, outcome distribution
3087
- - Search: Find past decisions by keyword
3088
- - Save new decisions: Record insights with the + button
3089
- Be friendly and concise.`,
3090
-
3091
- settings: `The user clicked the help button on the Settings tab.
3092
- Please explain what can be configured here:
3093
- - Gateways: Discord, Slack, Telegram, Chatwork bot connections
3094
- - Agent: Backend model, max turns, timeout, and other agent settings
3095
- - Multi-Agent: Per-agent model, backend, tier, and delegation settings
3096
- - Cron: Create, edit, and run scheduled jobs
3097
- - Token Budget: Set daily token limits and alerts
3098
- - Metrics: Enable/disable metrics collection and set retention period
3099
- Be friendly and concise.`,
3100
-
3101
- logs: `The user clicked the help button on the Logs tab.
3102
- Please explain this tab's features:
3103
- - Shows real-time daemon log output with auto-polling
3104
- - Filter by log level (ERROR, WARN, INFO, DEBUG)
3105
- - Search logs by keyword
3106
- - Pause/Resume auto-refresh
3107
- Be friendly and concise.`,
3108
- };
3109
-
3110
- window.askContextHelp = function () {
3111
- const currentTab = STATE.currentTab || 'feed';
3112
- const prompt = TAB_HELP_PROMPTS[currentTab];
3113
- if (!prompt) return;
3114
-
3115
- if (window.innerWidth < 768) {
3116
- switchTab('chat');
3117
- } else {
3118
- const wrapper = document.getElementById('chat-panel-wrapper');
3119
- if (wrapper && wrapper.style.display === 'none') {
3120
- toggleChatPanel();
3121
- }
3122
- }
3123
-
3124
- // Set input and send
3125
- const input = document.getElementById('chat-input');
3126
- if (input && chat) {
3127
- input.value = prompt;
3128
- setTimeout(() => chat.send(), 150);
3129
- }
3130
- };
3131
-
3132
2048
  // Register Service Worker for PWA
3133
2049
  if ('serviceWorker' in navigator) {
3134
2050
  window.addEventListener('load', () => {