@monoes/monomindcli 1.10.29 → 1.10.31

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 (111) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +39 -4
  2. package/.claude/helpers/handlers/adr-draft-handler.cjs +64 -0
  3. package/.claude/helpers/handlers/agent-start-handler.cjs +99 -0
  4. package/.claude/helpers/handlers/edit-handler.cjs +145 -0
  5. package/.claude/helpers/handlers/graph-status-handler.cjs +38 -0
  6. package/.claude/helpers/handlers/route-handler.cjs +393 -0
  7. package/.claude/helpers/handlers/session-handler.cjs +167 -0
  8. package/.claude/helpers/handlers/session-restore-handler.cjs +348 -0
  9. package/.claude/helpers/handlers/task-handler.cjs +329 -0
  10. package/.claude/helpers/hook-handler.cjs +120 -2431
  11. package/.claude/helpers/intelligence.cjs +21 -2
  12. package/.claude/helpers/learning-service.mjs +166 -8
  13. package/.claude/helpers/memory-palace.cjs +72 -12
  14. package/.claude/helpers/router.cjs +79 -5
  15. package/.claude/helpers/statusline.cjs +193 -399
  16. package/.claude/helpers/utils/micro-agents.cjs +338 -0
  17. package/.claude/helpers/utils/monograph.cjs +349 -0
  18. package/.claude/helpers/utils/telemetry.cjs +144 -0
  19. package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
  20. package/.claude/skills/monomind/browse-agentcore.md +116 -0
  21. package/.claude/skills/monomind/browse-electron.md +189 -0
  22. package/.claude/skills/monomind/browse-qa.md +229 -0
  23. package/.claude/skills/monomind/browse-references/authentication.md +162 -0
  24. package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
  25. package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
  26. package/.claude/skills/monomind/browse-slack.md +189 -0
  27. package/.claude/skills/monomind/browse-vercel.md +240 -0
  28. package/.claude/skills/monomind/browse.md +724 -0
  29. package/dist/src/browser/actions.d.ts +28 -0
  30. package/dist/src/browser/actions.d.ts.map +1 -0
  31. package/dist/src/browser/actions.js +292 -0
  32. package/dist/src/browser/actions.js.map +1 -0
  33. package/dist/src/browser/batch.d.ts +13 -0
  34. package/dist/src/browser/batch.d.ts.map +1 -0
  35. package/dist/src/browser/batch.js +11 -0
  36. package/dist/src/browser/batch.js.map +1 -0
  37. package/dist/src/browser/browser.d.ts +14 -0
  38. package/dist/src/browser/browser.d.ts.map +1 -0
  39. package/dist/src/browser/browser.js +198 -0
  40. package/dist/src/browser/browser.js.map +1 -0
  41. package/dist/src/browser/cdp.d.ts +17 -0
  42. package/dist/src/browser/cdp.d.ts.map +1 -0
  43. package/dist/src/browser/cdp.js +106 -0
  44. package/dist/src/browser/cdp.js.map +1 -0
  45. package/dist/src/browser/console-log.d.ts +22 -0
  46. package/dist/src/browser/console-log.d.ts.map +1 -0
  47. package/dist/src/browser/console-log.js +55 -0
  48. package/dist/src/browser/console-log.js.map +1 -0
  49. package/dist/src/browser/dialog.d.ts +11 -0
  50. package/dist/src/browser/dialog.d.ts.map +1 -0
  51. package/dist/src/browser/dialog.js +36 -0
  52. package/dist/src/browser/dialog.js.map +1 -0
  53. package/dist/src/browser/emulation.d.ts +15 -0
  54. package/dist/src/browser/emulation.d.ts.map +1 -0
  55. package/dist/src/browser/emulation.js +62 -0
  56. package/dist/src/browser/emulation.js.map +1 -0
  57. package/dist/src/browser/find.d.ts +21 -0
  58. package/dist/src/browser/find.d.ts.map +1 -0
  59. package/dist/src/browser/find.js +118 -0
  60. package/dist/src/browser/find.js.map +1 -0
  61. package/dist/src/browser/index.d.ts +18 -0
  62. package/dist/src/browser/index.d.ts.map +1 -0
  63. package/dist/src/browser/index.js +18 -0
  64. package/dist/src/browser/index.js.map +1 -0
  65. package/dist/src/browser/network.d.ts +11 -0
  66. package/dist/src/browser/network.d.ts.map +1 -0
  67. package/dist/src/browser/network.js +81 -0
  68. package/dist/src/browser/network.js.map +1 -0
  69. package/dist/src/browser/pdf.d.ts +15 -0
  70. package/dist/src/browser/pdf.d.ts.map +1 -0
  71. package/dist/src/browser/pdf.js +27 -0
  72. package/dist/src/browser/pdf.js.map +1 -0
  73. package/dist/src/browser/screenshot.d.ts +15 -0
  74. package/dist/src/browser/screenshot.d.ts.map +1 -0
  75. package/dist/src/browser/screenshot.js +36 -0
  76. package/dist/src/browser/screenshot.js.map +1 -0
  77. package/dist/src/browser/session.d.ts +8 -0
  78. package/dist/src/browser/session.d.ts.map +1 -0
  79. package/dist/src/browser/session.js +50 -0
  80. package/dist/src/browser/session.js.map +1 -0
  81. package/dist/src/browser/snapshot.d.ts +12 -0
  82. package/dist/src/browser/snapshot.d.ts.map +1 -0
  83. package/dist/src/browser/snapshot.js +147 -0
  84. package/dist/src/browser/snapshot.js.map +1 -0
  85. package/dist/src/browser/storage.d.ts +11 -0
  86. package/dist/src/browser/storage.d.ts.map +1 -0
  87. package/dist/src/browser/storage.js +43 -0
  88. package/dist/src/browser/storage.js.map +1 -0
  89. package/dist/src/browser/tabs.d.ts +8 -0
  90. package/dist/src/browser/tabs.d.ts.map +1 -0
  91. package/dist/src/browser/tabs.js +25 -0
  92. package/dist/src/browser/tabs.js.map +1 -0
  93. package/dist/src/browser/types.d.ts +109 -0
  94. package/dist/src/browser/types.d.ts.map +1 -0
  95. package/dist/src/browser/types.js +16 -0
  96. package/dist/src/browser/types.js.map +1 -0
  97. package/dist/src/browser/wait.d.ts +4 -0
  98. package/dist/src/browser/wait.d.ts.map +1 -0
  99. package/dist/src/browser/wait.js +122 -0
  100. package/dist/src/browser/wait.js.map +1 -0
  101. package/dist/src/commands/browse.d.ts +8 -0
  102. package/dist/src/commands/browse.d.ts.map +1 -0
  103. package/dist/src/commands/browse.js +1494 -0
  104. package/dist/src/commands/browse.js.map +1 -0
  105. package/dist/src/commands/index.d.ts.map +1 -1
  106. package/dist/src/commands/index.js +2 -0
  107. package/dist/src/commands/index.js.map +1 -1
  108. package/dist/src/ui/dashboard-v2.html +1857 -0
  109. package/dist/src/ui/server.mjs +71 -1
  110. package/dist/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +2 -1
@@ -0,0 +1,1857 @@
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>monomind</title>
7
+ <style>
8
+ :root {
9
+ --bg: oklch(11% 0.009 55);
10
+ --surface: oklch(15% 0.009 55);
11
+ --surface-hi: oklch(18% 0.009 55);
12
+ --border: oklch(25% 0.008 55);
13
+ --accent: oklch(72% 0.18 75);
14
+ --accent-dim: oklch(72% 0.18 75 / 0.12);
15
+ --text-hi: oklch(93% 0.008 75);
16
+ --text-mid: oklch(65% 0.006 75);
17
+ --text-lo: oklch(42% 0.006 75);
18
+ --text-xs: oklch(32% 0.005 75);
19
+ --green: oklch(65% 0.15 150);
20
+ --red: oklch(60% 0.18 25);
21
+ --sans: 'Inter', system-ui, -apple-system, sans-serif;
22
+ --mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
23
+ --r: 6px;
24
+ --sidebar-w: 196px;
25
+ }
26
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
+ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-family: var(--sans); font-size: 13px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
28
+
29
+ /* ── layout ──────────────────────────────────────────────── */
30
+ #app { display: flex; height: 100vh; overflow: hidden; }
31
+
32
+ /* ── sidebar ─────────────────────────────────────────────── */
33
+ #sidebar { width: var(--sidebar-w); flex-shrink: 0; background: var(--bg); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; }
34
+ #sb-logo { padding: 18px 16px 14px; border-bottom: 1px solid var(--border); }
35
+ #sb-logo .mark { font-size: 13px; font-weight: 600; letter-spacing: 0.05em; color: var(--text-hi); }
36
+ #sb-logo .proj { font-size: 11px; color: var(--accent); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
37
+ #sb-nav { flex: 1; padding: 10px 8px; overflow-y: auto; }
38
+ .nav-sect { margin-bottom: 8px; }
39
+ .nav-lbl { font-size: 10px; letter-spacing: 0.08em; color: var(--text-xs); padding: 8px 8px 4px; text-transform: uppercase; }
40
+ .nav-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: var(--r); cursor: pointer; color: var(--text-mid); transition: background 0.1s, color 0.1s; user-select: none; }
41
+ .nav-item:hover { background: var(--surface-hi); color: var(--text-hi); }
42
+ .nav-item.active { background: var(--accent-dim); color: var(--accent); }
43
+ .nav-item .ico { width: 14px; text-align: center; flex-shrink: 0; font-size: 12px; }
44
+ .nav-item .lbl { font-size: 13px; }
45
+ .nav-item .bdg { margin-left: auto; font-size: 10px; background: var(--surface-hi); color: var(--text-lo); border-radius: 8px; padding: 1px 6px; min-width: 18px; text-align: center; }
46
+ .nav-item.active .bdg { background: var(--accent-dim); color: var(--accent); }
47
+ #sb-footer { padding: 10px 14px; border-top: 1px solid var(--border); }
48
+ #sb-user { font-size: 12px; font-weight: 500; color: var(--text-mid); }
49
+ #sb-path { font-size: 10px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
50
+
51
+ /* ── main ────────────────────────────────────────────────── */
52
+ #main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
53
+ #topbar { height: 46px; border-bottom: 1px solid var(--border); padding: 0 18px; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
54
+ #view-title { font-size: 14px; font-weight: 600; color: var(--text-hi); }
55
+ .pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text-lo); background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 2px 8px; }
56
+ .live-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: blink 2s ease-in-out infinite; }
57
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.35} }
58
+ @media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
59
+ #tb-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
60
+ .btn { font-size: 11px; color: var(--text-lo); background: transparent; border: 1px solid var(--border); border-radius: var(--r); padding: 4px 10px; cursor: pointer; transition: color 0.1s, border-color 0.1s; }
61
+ .btn:hover { color: var(--text-hi); border-color: var(--text-lo); }
62
+ #view-wrap { flex: 1; overflow: hidden; }
63
+ .view { display: none; height: 100%; overflow: hidden; }
64
+ .view.active { display: flex; }
65
+
66
+ /* ── NOW view ────────────────────────────────────────────── */
67
+ #view-now { flex-direction: row; position: relative; }
68
+
69
+ /* feed pane */
70
+ #feed-pane { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); overflow: hidden; min-width: 0; }
71
+ #feed-head { padding: 10px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
72
+ #feed-head h2 { font-size: 11px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-lo); }
73
+ #feed-sess { font-size: 11px; font-family: var(--mono); color: var(--text-xs); }
74
+ #feed-sess-nav { margin-left: auto; display: flex; gap: 6px; align-items: center; }
75
+ .sess-btn { font-size: 11px; color: var(--text-lo); background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; cursor: pointer; transition: color 0.1s; line-height: 1.4; }
76
+ .sess-btn:hover { color: var(--text-hi); }
77
+ #feed-scroll { flex: 1; overflow-y: auto; }
78
+ #feed-scroll::-webkit-scrollbar { width: 3px; }
79
+ #feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
80
+
81
+ /* feed entries */
82
+ .feed-entry { display: flex; align-items: flex-start; gap: 10px; padding: 5px 18px; cursor: pointer; transition: background 0.08s; }
83
+ .feed-entry:hover { background: var(--surface-hi); }
84
+ .feed-entry.selected { background: var(--accent-dim); }
85
+ .feed-ico { width: 20px; height: 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; flex-shrink: 0; margin-top: 1px; }
86
+ .feed-body { flex: 1; min-width: 0; }
87
+ .feed-lbl { font-size: 13px; color: var(--text-hi); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88
+ .feed-detail { font-size: 11px; color: var(--text-lo); margin-top: 1px; font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
89
+ .feed-ts { font-size: 10px; color: var(--text-xs); white-space: nowrap; flex-shrink: 0; margin-top: 3px; font-family: var(--mono); }
90
+ .feed-entry.k-user .feed-lbl { color: var(--text-mid); font-style: italic; }
91
+ .feed-entry.errored .feed-lbl { color: var(--red); }
92
+ .feed-entry.errored .feed-ico { background: oklch(60% 0.18 25 / 0.14); color: oklch(60% 0.18 25); }
93
+
94
+ /* group row */
95
+ .feed-group { display: flex; align-items: center; gap: 8px; padding: 3px 18px 3px 48px; cursor: pointer; }
96
+ .feed-group:hover .fg-label { color: var(--text-mid); }
97
+ .fg-label { font-size: 11px; color: var(--text-lo); }
98
+ .fg-expand { font-size: 10px; color: var(--text-xs); margin-left: 4px; }
99
+
100
+ /* category icon colors */
101
+ .cat-file { background: oklch(65% 0.15 150 / 0.14); color: oklch(65% 0.15 150); }
102
+ .cat-bash { background: oklch(65% 0.12 240 / 0.14); color: oklch(65% 0.12 240); }
103
+ .cat-agent { background: oklch(65% 0.13 290 / 0.14); color: oklch(65% 0.13 290); }
104
+ .cat-mcp { background: oklch(65% 0.12 195 / 0.14); color: oklch(65% 0.12 195); }
105
+ .cat-search { background: oklch(65% 0.14 35 / 0.14); color: oklch(65% 0.14 35); }
106
+ .cat-skill { background: oklch(72% 0.18 75 / 0.14); color: oklch(72% 0.18 75); }
107
+ .cat-task { background: oklch(62% 0.12 55 / 0.14); color: oklch(72% 0.12 55); }
108
+ .cat-mem { background: oklch(62% 0.11 160 / 0.14); color: oklch(68% 0.11 160); }
109
+ .cat-user { background: oklch(55% 0.08 75 / 0.12); color: oklch(62% 0.06 75); }
110
+ .cat-other { background: var(--surface-hi); color: var(--text-lo); }
111
+
112
+ .feed-divider { height: 1px; background: var(--border); margin: 2px 18px; opacity: 0.4; }
113
+ .feed-empty { padding: 40px 18px; color: var(--text-lo); font-size: 13px; }
114
+
115
+ /* ── detail panel ─────────────────────────────────────────── */
116
+ #detail-panel {
117
+ position: absolute; top: 0; right: 252px; bottom: 0;
118
+ width: 0; overflow: hidden;
119
+ background: oklch(13% 0.009 55);
120
+ border-left: 1px solid var(--border);
121
+ border-right: 1px solid var(--border);
122
+ transition: width 0.22s cubic-bezier(0.16, 1, 0.3, 1);
123
+ display: flex; flex-direction: column; z-index: 10;
124
+ }
125
+ #detail-panel.open { width: 280px; }
126
+ #detail-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
127
+ #detail-head h3 { font-size: 12px; font-weight: 600; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
128
+ #detail-close { font-size: 14px; color: var(--text-lo); background: none; border: none; cursor: pointer; padding: 0 2px; line-height: 1; }
129
+ #detail-close:hover { color: var(--text-hi); }
130
+ #detail-body { flex: 1; overflow-y: auto; padding: 12px 14px; }
131
+ #detail-body::-webkit-scrollbar { width: 3px; }
132
+ #detail-body::-webkit-scrollbar-thumb { background: var(--border); }
133
+ .d-cat-pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; border-radius: 10px; padding: 2px 8px; margin-bottom: 10px; }
134
+ .d-row { margin-bottom: 10px; }
135
+ .d-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 3px; }
136
+ .d-val { font-size: 12px; color: var(--text-hi); word-break: break-word; }
137
+ .d-val.mono { font-family: var(--mono); font-size: 11px; }
138
+ .d-val.error { color: var(--red); }
139
+
140
+ /* ── metrics pane ────────────────────────────────────────── */
141
+ #metrics-pane { width: 252px; flex-shrink: 0; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 18px; }
142
+ #metrics-pane::-webkit-scrollbar { width: 3px; }
143
+ #metrics-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
144
+ .m-group-title { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-lo); padding-bottom: 8px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
145
+ .m-row { display: flex; align-items: baseline; justify-content: space-between; padding: 3px 0; }
146
+ .m-name { font-size: 12px; color: var(--text-mid); }
147
+ .m-val { font-size: 13px; font-weight: 600; color: var(--text-hi); font-family: var(--mono); }
148
+ .m-val.gold { color: var(--accent); }
149
+ .mini-loop { padding: 6px 0; border-bottom: 1px solid var(--border); }
150
+ .mini-loop:last-child { border-bottom: none; }
151
+ .ml-name { font-size: 12px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
152
+ .ml-meta { font-size: 11px; color: var(--text-lo); margin-top: 1px; display: flex; align-items: center; gap: 5px; }
153
+ .ml-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; background: var(--green); }
154
+ .mini-sess { padding: 5px 0; cursor: pointer; border-bottom: 1px solid var(--border); }
155
+ .mini-sess:last-child { border-bottom: none; }
156
+ .mini-sess:hover .ms-prompt { color: var(--accent); }
157
+ .ms-prompt { font-size: 12px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color 0.1s; }
158
+ .ms-meta { font-size: 11px; color: var(--text-lo); margin-top: 1px; }
159
+
160
+ /* ── views with scroll ───────────────────────────────────── */
161
+ .vscroll { flex: 1; overflow-y: auto; padding: 22px 24px; }
162
+ .vscroll::-webkit-scrollbar { width: 4px; }
163
+ .vscroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
164
+ .pg-title { font-size: 18px; font-weight: 600; color: var(--text-hi); margin-bottom: 4px; }
165
+ .pg-sub { font-size: 13px; color: var(--text-lo); margin-bottom: 22px; }
166
+
167
+ /* filter bar */
168
+ .filter-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
169
+ .filter-input { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 6px 10px; font-size: 12px; color: var(--text-hi); font-family: var(--sans); outline: none; transition: border-color 0.1s; }
170
+ .filter-input::placeholder { color: var(--text-lo); }
171
+ .filter-input:focus { border-color: var(--accent); }
172
+
173
+ /* projects */
174
+ .proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; }
175
+ .proj-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; cursor: pointer; transition: border-color 0.12s, background 0.12s; position: relative; }
176
+ .proj-card:hover { border-color: var(--accent); background: var(--surface-hi); }
177
+ .proj-card.current { border-color: var(--accent); }
178
+ .proj-card-badge { position: absolute; top: 10px; right: 10px; font-size: 10px; background: var(--accent-dim); color: var(--accent); border-radius: 8px; padding: 1px 7px; }
179
+ .proj-card-name { font-size: 14px; font-weight: 500; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 4px; }
180
+ .proj-card-path { font-size: 10px; color: var(--text-lo); font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 10px; }
181
+ .proj-card-stats { display: flex; gap: 14px; }
182
+ .ps-v { font-size: 16px; font-weight: 700; color: var(--text-hi); }
183
+ .ps-l { font-size: 10px; color: var(--text-lo); margin-top: 1px; }
184
+
185
+ /* sessions */
186
+ .sess-list { display: flex; flex-direction: column; gap: 6px; }
187
+ .sess-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; cursor: pointer; transition: border-color 0.12s, background 0.12s; }
188
+ .sess-row:hover { border-color: var(--accent); background: var(--surface-hi); }
189
+ .sr-top { display: flex; align-items: baseline; gap: 10px; }
190
+ .sr-prompt { font-size: 13px; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
191
+ .sr-time { font-size: 11px; color: var(--text-lo); white-space: nowrap; font-family: var(--mono); }
192
+ .sr-meta { font-size: 11px; color: var(--text-lo); margin-top: 3px; }
193
+
194
+ /* loops */
195
+ .loop-list { display: flex; flex-direction: column; gap: 8px; }
196
+ .loop-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; display: flex; align-items: center; gap: 12px; }
197
+ .loop-ico { width: 30px; height: 30px; border-radius: 6px; background: var(--accent-dim); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--accent); flex-shrink: 0; }
198
+ .loop-body { flex: 1; min-width: 0; }
199
+ .loop-name { font-size: 13px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
200
+ .loop-meta { font-size: 11px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
201
+ .loop-status { font-size: 11px; padding: 2px 8px; border-radius: 10px; flex-shrink: 0; }
202
+ .loop-status.active { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
203
+ .loop-status.stopped { background: var(--surface-hi); color: var(--text-lo); }
204
+
205
+ /* memory */
206
+ .mem-section { margin-bottom: 22px; }
207
+ .mem-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 10px; }
208
+ .drawer-item { padding: 8px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 4px; }
209
+ .dr-key { font-size: 12px; color: var(--accent); font-family: var(--mono); }
210
+ .dr-val { font-size: 12px; color: var(--text-hi); margin-top: 2px; word-break: break-word; }
211
+ .dr-ts { font-size: 10px; color: var(--text-lo); margin-top: 2px; }
212
+ .drawer-item.hidden { display: none; }
213
+
214
+ /* orgs */
215
+ .org-list { display: flex; flex-direction: column; gap: 6px; }
216
+ .org-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; cursor: pointer; transition: border-color 0.12s; }
217
+ .org-row:hover { border-color: var(--accent); }
218
+ .org-name { font-size: 13px; color: var(--text-hi); font-weight: 500; }
219
+ .org-meta { font-size: 11px; color: var(--text-lo); margin-top: 3px; }
220
+
221
+ /* ── alerts rail ─────────────────────────────────────────── */
222
+ #alerts-rail { display: none; flex-shrink: 0; border-bottom: 1px solid var(--border); padding: 0 16px; min-height: 36px; flex-direction: row; gap: 8px; align-items: center; overflow-x: auto; }
223
+ #alerts-rail.has-alerts { display: flex; }
224
+ .alert-item { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 4px 10px; border-radius: 10px; cursor: default; white-space: nowrap; user-select: none; }
225
+ .alert-item:hover .al-x { opacity: 1; }
226
+ .alert-warn { background: oklch(72% 0.18 75 / 0.1); color: oklch(78% 0.18 75); border: 1px solid oklch(72% 0.18 75 / 0.25); }
227
+ .alert-crit { background: oklch(60% 0.18 25 / 0.12); color: oklch(70% 0.18 25); border: 1px solid oklch(60% 0.18 25 / 0.3); }
228
+ .al-ico { font-size: 10px; }
229
+ .al-x { opacity: 0; transition: opacity 0.1s; cursor: pointer; font-size: 10px; margin-left: 2px; color: inherit; }
230
+
231
+ /* ── session context bar ─────────────────────────────────── */
232
+ #sess-ctx { display: none; flex-shrink: 0; align-items: center; gap: 8px; padding: 5px 18px; border-bottom: 1px solid var(--border); background: oklch(72% 0.18 75 / 0.06); }
233
+ #sess-ctx.show { display: flex; }
234
+ .sctx-back { font-size: 11px; color: var(--accent); background: none; border: none; cursor: pointer; padding: 0; white-space: nowrap; }
235
+ .sctx-back:hover { text-decoration: underline; }
236
+ .sctx-sep { color: var(--text-xs); font-size: 11px; }
237
+ .sctx-label { font-size: 12px; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
238
+ .sctx-live { font-size: 11px; color: var(--text-lo); background: none; border: 1px solid var(--border); border-radius: var(--r); padding: 2px 9px; cursor: pointer; white-space: nowrap; transition: color 0.1s, border-color 0.1s; }
239
+ .sctx-live:hover { color: var(--green); border-color: var(--green); }
240
+
241
+ /* enhanced session row → view affordance */
242
+ .sr-top { position: relative; }
243
+ .sr-view { position: absolute; right: 0; top: 0; font-size: 11px; color: var(--accent); opacity: 0; transition: opacity 0.1s; }
244
+ .sess-row:hover .sr-view { opacity: 1; }
245
+ .sr-tags { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
246
+ .sr-tag { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--surface-hi); color: var(--text-lo); }
247
+ .sr-tag.err { background: oklch(60% 0.18 25 / 0.1); color: oklch(70% 0.18 25); }
248
+
249
+ /* loops detail expand */
250
+ .loop-row { cursor: pointer; }
251
+ .loop-row:hover { background: var(--surface-hi); }
252
+ .loop-row.open { border-color: var(--accent); }
253
+ .loop-expand { display: none; padding: 10px 14px 12px 56px; background: oklch(14% 0.009 55); border-top: 1px solid var(--border); border-radius: 0 0 6px 6px; }
254
+ .loop-row.open + .loop-expand { display: block; }
255
+ .le-row { display: flex; gap: 10px; margin-bottom: 6px; }
256
+ .le-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); min-width: 70px; flex-shrink: 0; padding-top: 1px; }
257
+ .le-val { font-size: 12px; color: var(--text-hi); word-break: break-all; }
258
+ .le-val.mono { font-family: var(--mono); font-size: 11px; }
259
+
260
+ /* 7-day sparkline */
261
+ .spark-wrap { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 10px; }
262
+ .spark-lbl { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-lo); margin-bottom: 6px; }
263
+ .sparkline { display: flex; align-items: flex-end; gap: 3px; height: 24px; }
264
+ .spark-bar { flex: 1; border-radius: 2px 2px 0 0; background: oklch(72% 0.18 75 / 0.25); min-height: 2px; transition: background 0.1s; }
265
+ .spark-bar.spark-today { background: oklch(72% 0.18 75 / 0.75); }
266
+ .spark-bar:hover { background: oklch(72% 0.18 75 / 0.6); }
267
+
268
+ /* shared */
269
+ .empty { padding: 44px 18px; color: var(--text-lo); text-align: center; }
270
+ .empty-ico { font-size: 22px; margin-bottom: 10px; }
271
+ .loading-txt { padding: 18px; color: var(--text-lo); font-size: 12px; }
272
+ ::-webkit-scrollbar { width: 4px; height: 4px; }
273
+ ::-webkit-scrollbar-track { background: transparent; }
274
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
275
+
276
+ /* ── command palette ─────────────────────────────────────── */
277
+ #cmd-backdrop { display:none; position:fixed; inset:0; z-index:100; background: oklch(5% 0 0 / 0.6); backdrop-filter:blur(2px); }
278
+ #cmd-backdrop.open { display:block; }
279
+ #cmd-palette { display:none; position:fixed; top:18%; left:50%; transform:translateX(-50%); width:540px; max-width:90vw; background: oklch(16% 0.009 55); border:1px solid oklch(32% 0.008 55); border-radius:10px; z-index:101; box-shadow:0 24px 60px oklch(5% 0 0 / 0.7); overflow:hidden; flex-direction:column; }
280
+ #cmd-palette.open { display:flex; }
281
+ #cmd-input-wrap { display:flex; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid var(--border); }
282
+ #cmd-ico { font-size:13px; color:var(--text-lo); flex-shrink:0; }
283
+ #cmd-input { flex:1; background:transparent; border:none; font-size:14px; color:var(--text-hi); font-family:var(--sans); outline:none; }
284
+ #cmd-input::placeholder { color:var(--text-lo); }
285
+ #cmd-results { max-height:340px; overflow-y:auto; padding:4px 0; }
286
+ .cmd-group-lbl { font-size:10px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-xs); padding:8px 14px 3px; }
287
+ .cmd-item { display:flex; align-items:center; gap:10px; padding:8px 14px; cursor:pointer; transition:background 0.07s; }
288
+ .cmd-item:hover, .cmd-item.focused { background:var(--accent-dim); }
289
+ .cmd-item .ci-ico { width:20px; text-align:center; color:var(--text-lo); flex-shrink:0; font-size:12px; }
290
+ .cmd-item-body { flex:1; min-width:0; }
291
+ .cmd-item .ci-title { font-size:13px; color:var(--text-hi); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
292
+ .cmd-item .ci-sub { font-size:11px; color:var(--text-lo); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
293
+ .cmd-item.focused .ci-title { color:var(--accent); }
294
+ .cmd-empty { padding:28px 16px; text-align:center; color:var(--text-lo); font-size:13px; }
295
+ .cmd-footer { padding:6px 14px; border-top:1px solid var(--border); display:flex; gap:16px; }
296
+ .cmd-key { font-size:10px; color:var(--text-xs); display:flex; align-items:center; gap:4px; }
297
+ .cmd-key kbd { background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 5px; font-family:var(--mono); font-size:10px; color:var(--text-lo); }
298
+
299
+ /* ── feed time filter ────────────────────────────────────── */
300
+ #feed-time-filter { display:flex; gap:4px; padding:5px 18px; border-bottom:1px solid var(--border); flex-shrink:0; align-items:center; }
301
+ .tf-lbl { font-size:10px; color:var(--text-xs); letter-spacing:0.06em; margin-right:4px; }
302
+ .tf-btn { font-size:11px; padding:2px 9px; background:transparent; border:1px solid transparent; border-radius:8px; color:var(--text-lo); cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
303
+ .tf-btn:hover { color:var(--text-hi); }
304
+ .tf-btn.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
305
+
306
+ /* ── keyboard hint ───────────────────────────────────────── */
307
+ .kb-hint { margin-left:auto; font-size:10px; color:var(--text-xs); display:flex; align-items:center; gap:6px; }
308
+ .kb-hint kbd { background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 5px; font-family:var(--mono); font-size:10px; }
309
+
310
+ /* ── topbar cost badge ───────────────────────────────────── */
311
+ #topbar-cost { font-size:11px; color:var(--accent); font-family:var(--mono); font-weight:600; opacity:0; transition:opacity 0.3s; }
312
+ #topbar-cost.loaded { opacity:1; }
313
+
314
+ /* ── feed search bar ─────────────────────────────────────── */
315
+ #feed-search { display:none; flex-shrink:0; padding:4px 18px; border-bottom:1px solid var(--border); align-items:center; gap:8px; }
316
+ #feed-search.open { display:flex; }
317
+ #feed-search-input { flex:1; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:4px 10px; font-size:12px; color:var(--text-hi); font-family:var(--sans); outline:none; transition:border-color 0.1s; }
318
+ #feed-search-input::placeholder { color:var(--text-lo); }
319
+ #feed-search-input:focus { border-color:var(--accent); }
320
+ #feed-search-count { font-size:11px; color:var(--text-lo); white-space:nowrap; font-family:var(--mono); }
321
+ #feed-search-close { font-size:12px; color:var(--text-lo); background:none; border:none; cursor:pointer; padding:0 2px; }
322
+ #feed-search-close:hover { color:var(--text-hi); }
323
+
324
+ /* ── memory namespace tabs ───────────────────────────────── */
325
+ #mem-ns-tabs { display:flex; flex-wrap:wrap; gap:4px; margin-bottom:14px; }
326
+ .ns-tab { font-size:11px; padding:3px 10px; border-radius:8px; border:1px solid var(--border); background:transparent; color:var(--text-lo); cursor:pointer; font-family:var(--sans); transition:color 0.1s, background 0.1s; }
327
+ .ns-tab:hover { color:var(--text-hi); }
328
+ .ns-tab.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
329
+
330
+ /* ── copy session button ─────────────────────────────────── */
331
+ .sess-copy-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.1s; line-height:1.4; white-space:nowrap; }
332
+ .sess-copy-btn:hover { color:var(--text-hi); }
333
+ .sess-copy-btn.copied { color:var(--green); border-color:var(--green); }
334
+
335
+ /* ── session timeline bar ────────────────────────────────── */
336
+ #feed-timeline { height:6px; flex-shrink:0; display:flex; overflow:hidden; border-bottom:1px solid var(--border); }
337
+ .tl-seg { height:100%; flex-shrink:0; transition:opacity 0.1s; }
338
+ .tl-seg:hover { opacity:0.7; }
339
+
340
+ /* ── feed density toggle ─────────────────────────────────── */
341
+ #feed-pane.compact .feed-entry { padding: 2px 18px; }
342
+ #feed-pane.compact .feed-lbl { font-size:12px; }
343
+ #feed-pane.compact .feed-detail { display:none; }
344
+ #feed-pane.compact .feed-ico { width:16px; height:16px; font-size:9px; }
345
+ .density-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.1s; line-height:1.4; white-space:nowrap; }
346
+ .density-btn:hover { color:var(--text-hi); }
347
+ .density-btn.compact-on { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.5); }
348
+
349
+ /* ── tool usage breakdown ────────────────────────────────── */
350
+ .m-breakdown { margin-top:4px; }
351
+ .tb-row { display:flex; align-items:center; gap:6px; margin-bottom:4px; }
352
+ .tb-lbl { font-size:10px; color:var(--text-lo); width:44px; flex-shrink:0; text-align:right; }
353
+ .tb-bar-wrap { flex:1; height:5px; background:var(--surface-hi); border-radius:3px; overflow:hidden; }
354
+ .tb-bar { height:100%; border-radius:3px; }
355
+ .tb-count { font-size:10px; color:var(--text-xs); font-family:var(--mono); width:22px; text-align:right; }
356
+
357
+ /* ── week-over-week delta ────────────────────────────────── */
358
+ .wow-delta { font-size:11px; margin-left:6px; }
359
+ .wow-up { color:oklch(60% 0.18 25); }
360
+ .wow-down { color:oklch(65% 0.15 150); }
361
+ .wow-flat { color:var(--text-lo); }
362
+
363
+ /* ── live tail ───────────────────────────────────────────── */
364
+ .live-tail-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.15s, border-color 0.15s; line-height:1.4; white-space:nowrap; }
365
+ .live-tail-btn:hover { color:var(--text-hi); }
366
+ .live-tail-btn.on { color:var(--green); border-color:oklch(65% 0.15 150 / 0.5); }
367
+ @keyframes live-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
368
+ #btn-live-tail.on { animation: live-pulse 2s ease-in-out infinite; }
369
+ @media (prefers-reduced-motion: reduce) { #btn-live-tail.on { animation:none; } }
370
+
371
+ /* ── calendar heatmap ────────────────────────────────────── */
372
+ .cal-grid { display:grid; grid-template-rows:repeat(7,9px); grid-auto-flow:column; grid-auto-columns:9px; gap:2px; margin-top:6px; }
373
+ .cal-cell { border-radius:2px; background:var(--surface-hi); }
374
+ .cal-cell.cal-1 { background:oklch(72% 0.18 75 / 0.22); }
375
+ .cal-cell.cal-2 { background:oklch(72% 0.18 75 / 0.42); }
376
+ .cal-cell.cal-3 { background:oklch(72% 0.18 75 / 0.65); }
377
+ .cal-cell.cal-4 { background:oklch(72% 0.18 75 / 0.85); }
378
+ .cal-cell.cal-today { outline:1px solid var(--accent); outline-offset:-1px; }
379
+
380
+ /* ── session bookmark ────────────────────────────────────── */
381
+ .sess-star { font-size:13px; background:none; border:none; cursor:pointer; color:var(--text-xs); padding:0 2px; transition:color 0.1s; line-height:1; flex-shrink:0; }
382
+ .sess-star:hover { color:var(--text-lo); }
383
+ .sess-star.on { color:oklch(78% 0.18 75); }
384
+ .sr-starred { font-size:10px; color:oklch(78% 0.18 75); margin-left:auto; }
385
+ #sess-star-filter { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); margin-left:auto; }
386
+ #sess-star-filter:hover { color:var(--text-hi); }
387
+ #sess-star-filter.on { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border-color:oklch(72% 0.18 75 / 0.3); }
388
+
389
+ /* ── forecast row ────────────────────────────────────────── */
390
+ .m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
391
+ </style>
392
+ </head>
393
+ <body>
394
+ <div id="app">
395
+
396
+ <!-- ── Sidebar ─────────────────────────────────────────── -->
397
+ <nav id="sidebar">
398
+ <div id="sb-logo">
399
+ <div class="mark">monomind</div>
400
+ <div class="proj" id="sb-proj">—</div>
401
+ </div>
402
+ <div id="sb-nav">
403
+ <div class="nav-sect">
404
+ <div class="nav-item active" data-view="now">
405
+ <span class="ico">◉</span><span class="lbl">Now</span>
406
+ </div>
407
+ </div>
408
+ <div class="nav-sect">
409
+ <div class="nav-lbl">Workspace</div>
410
+ <div class="nav-item" data-view="projects">
411
+ <span class="ico">⊞</span><span class="lbl">Projects</span>
412
+ <span class="bdg" id="bdg-projects">—</span>
413
+ </div>
414
+ <div class="nav-item" data-view="sessions">
415
+ <span class="ico">◫</span><span class="lbl">Sessions</span>
416
+ <span class="bdg" id="bdg-sessions">—</span>
417
+ </div>
418
+ <div class="nav-item" data-view="loops">
419
+ <span class="ico">↺</span><span class="lbl">Loops</span>
420
+ <span class="bdg" id="bdg-loops">—</span>
421
+ </div>
422
+ </div>
423
+ <div class="nav-sect">
424
+ <div class="nav-lbl">Intelligence</div>
425
+ <div class="nav-item" data-view="memory">
426
+ <span class="ico">◈</span><span class="lbl">Memory</span>
427
+ </div>
428
+ <div class="nav-item" data-view="orgs">
429
+ <span class="ico">⬡</span><span class="lbl">Orgs</span>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ <div id="sb-footer">
434
+ <div id="sb-user">—</div>
435
+ <div id="sb-path">—</div>
436
+ </div>
437
+ </nav>
438
+
439
+ <!-- ── Main ────────────────────────────────────────────── -->
440
+ <div id="main">
441
+ <div id="topbar">
442
+ <span id="view-title">Now</span>
443
+ <span class="pill"><span class="live-dot"></span> live</span>
444
+ <span id="topbar-cost"></span>
445
+ <div id="tb-right">
446
+ <button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
447
+ <button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
448
+ </div>
449
+ </div>
450
+
451
+ <div id="alerts-rail"></div>
452
+
453
+ <!-- command palette -->
454
+ <div id="cmd-backdrop" onclick="closeCmdPalette()"></div>
455
+ <div id="cmd-palette" role="dialog" aria-label="Command palette">
456
+ <div id="cmd-input-wrap">
457
+ <span id="cmd-ico">⌕</span>
458
+ <input id="cmd-input" type="text" placeholder="Search sessions, memory, projects…" oninput="cmdSearch(this.value)" onkeydown="cmdKey(event)" autocomplete="off" spellcheck="false">
459
+ </div>
460
+ <div id="cmd-results"></div>
461
+ <div class="cmd-footer">
462
+ <span class="cmd-key"><kbd>↑↓</kbd> navigate</span>
463
+ <span class="cmd-key"><kbd>↵</kbd> select</span>
464
+ <span class="cmd-key"><kbd>></kbd> search all sessions</span>
465
+ <span class="cmd-key"><kbd>esc</kbd> close</span>
466
+ </div>
467
+ </div>
468
+
469
+ <div id="view-wrap">
470
+
471
+ <!-- NOW -->
472
+ <div class="view active" id="view-now">
473
+ <div id="feed-pane">
474
+ <div id="feed-head">
475
+ <h2>Live Feed</h2>
476
+ <span id="feed-sess">—</span>
477
+ <div id="feed-sess-nav">
478
+ <button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
479
+ <button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
480
+ <button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
481
+ <button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
482
+ <button class="sess-btn" id="btn-prev-sess" onclick="prevSession()" title="Older session">‹</button>
483
+ <button class="sess-btn" id="btn-next-sess" onclick="nextSession()" title="Newer session">›</button>
484
+ </div>
485
+ </div>
486
+ <div id="feed-search">
487
+ <input id="feed-search-input" type="text" placeholder="Search feed…" oninput="filterFeed(this.value)" onkeydown="if(event.key==='Escape')closeFeedSearch()">
488
+ <span id="feed-search-count"></span>
489
+ <button id="feed-search-close" onclick="closeFeedSearch()">✕</button>
490
+ </div>
491
+ <div id="sess-ctx">
492
+ <button class="sctx-back" onclick="switchView('sessions')">← Sessions</button>
493
+ <span class="sctx-sep">/</span>
494
+ <span class="sctx-label" id="sctx-label"></span>
495
+ <button class="sctx-live" onclick="goLive()">⬤ Go live</button>
496
+ </div>
497
+ <div id="feed-timeline" title="Session tool activity timeline"></div>
498
+ <div id="feed-time-filter">
499
+ <span class="tf-lbl">Range</span>
500
+ <button class="tf-btn active" data-tf="all" onclick="setFeedTimeFilter('all')">All</button>
501
+ <button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
502
+ <button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
503
+ <button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
504
+ <span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate &nbsp;<kbd>↵</kbd> detail &nbsp;<kbd>/</kbd> find &nbsp;<kbd>G</kbd> live &nbsp;<kbd>⌘K</kbd> search</span>
505
+ </div>
506
+ <div id="feed-scroll">
507
+ <div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
508
+ </div>
509
+ </div>
510
+
511
+ <!-- detail panel (slides in over feed) -->
512
+ <div id="detail-panel">
513
+ <div id="detail-head">
514
+ <h3 id="detail-title">Detail</h3>
515
+ <button id="detail-close" onclick="closeDetail()">✕</button>
516
+ </div>
517
+ <div id="detail-body"></div>
518
+ </div>
519
+
520
+ <div id="metrics-pane">
521
+ <div id="m-today">
522
+ <div class="m-group-title">Today</div>
523
+ <div class="loading-txt">Loading…</div>
524
+ </div>
525
+ <div id="m-loops">
526
+ <div class="m-group-title">Active Loops</div>
527
+ <div class="loading-txt">Loading…</div>
528
+ </div>
529
+ <div id="m-sessions">
530
+ <div class="m-group-title">Recent Sessions</div>
531
+ <div class="loading-txt">Loading…</div>
532
+ </div>
533
+ <div id="m-breakdown">
534
+ <div class="m-group-title">Tool Usage</div>
535
+ <div class="loading-txt">—</div>
536
+ </div>
537
+ </div>
538
+ </div>
539
+
540
+ <!-- PROJECTS -->
541
+ <div class="view" id="view-projects">
542
+ <div class="vscroll">
543
+ <div class="pg-title">Projects</div>
544
+ <div class="pg-sub" id="proj-pg-sub">All monomind-enabled Claude Code projects</div>
545
+ <div class="filter-bar">
546
+ <input class="filter-input" id="proj-filter" type="text" placeholder="Filter projects…" oninput="filterProjects(this.value)">
547
+ </div>
548
+ <div id="proj-content" class="proj-grid"><div class="loading-txt">Loading…</div></div>
549
+ </div>
550
+ </div>
551
+
552
+ <!-- SESSIONS -->
553
+ <div class="view" id="view-sessions">
554
+ <div class="vscroll">
555
+ <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
556
+ <div class="pg-title" style="margin-bottom:0">Sessions</div>
557
+ <button id="sess-star-filter" onclick="toggleSessStarFilter()" title="Show only bookmarked sessions">☆ Starred</button>
558
+ </div>
559
+ <div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
560
+ <div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
561
+ </div>
562
+ </div>
563
+
564
+ <!-- LOOPS -->
565
+ <div class="view" id="view-loops">
566
+ <div class="vscroll">
567
+ <div class="pg-title">Loops</div>
568
+ <div class="pg-sub">Scheduled automation loops</div>
569
+ <div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
570
+ </div>
571
+ </div>
572
+
573
+ <!-- MEMORY -->
574
+ <div class="view" id="view-memory">
575
+ <div class="vscroll">
576
+ <div class="pg-title">Memory</div>
577
+ <div class="pg-sub">Knowledge palace — stored facts, graph, identity</div>
578
+ <div class="filter-bar">
579
+ <input class="filter-input" id="mem-filter" type="text" placeholder="Search memory…" oninput="filterMemory(this.value)">
580
+ </div>
581
+ <div id="mem-ns-tabs"></div>
582
+ <div id="mem-content"><div class="loading-txt">Loading…</div></div>
583
+ </div>
584
+ </div>
585
+
586
+ <!-- ORGS -->
587
+ <div class="view" id="view-orgs">
588
+ <div class="vscroll">
589
+ <div class="pg-title">Orgs</div>
590
+ <div class="pg-sub">MASTERMIND organizations and swarms</div>
591
+ <div id="orgs-content"><div class="loading-txt">Loading…</div></div>
592
+ </div>
593
+ </div>
594
+
595
+ </div><!-- /view-wrap -->
596
+ </div><!-- /main -->
597
+ </div><!-- /app -->
598
+
599
+ <script>
600
+ // ── state ──────────────────────────────────────────────────
601
+ let DIR = '';
602
+ let ORIGINAL_DIR = '';
603
+ let gitUser = {};
604
+ let currentView = 'now';
605
+ let allSessions = [];
606
+ let allProjects = [];
607
+ let sessionIdx = 0;
608
+ let pollTimer = null;
609
+ let viewRendered = {};
610
+ let userScrolled = false;
611
+ let selectedEntryId = null;
612
+ let allDrawers = [];
613
+ let dismissedAlerts = new Set();
614
+ let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null };
615
+ let feedTimeFilter = 'all';
616
+ let cmdFocusIdx = 0;
617
+ let cmdItems = [];
618
+ let liveTailMode = false;
619
+ let liveTailTimer = null;
620
+ let bookmarks = new Set(JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'));
621
+ let showStarredOnly = false;
622
+
623
+ // ── nav ────────────────────────────────────────────────────
624
+ document.querySelectorAll('.nav-item[data-view]').forEach(el => {
625
+ el.addEventListener('click', () => switchView(el.dataset.view));
626
+ });
627
+
628
+ document.getElementById('feed-scroll').addEventListener('scroll', () => {
629
+ userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
630
+ });
631
+
632
+ function switchView(v) {
633
+ currentView = v;
634
+ document.querySelectorAll('.nav-item[data-view]').forEach(el =>
635
+ el.classList.toggle('active', el.dataset.view === v));
636
+ document.querySelectorAll('.view').forEach(el =>
637
+ el.classList.toggle('active', el.id === 'view-' + v));
638
+ const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs' };
639
+ document.getElementById('view-title').textContent = titles[v] || v;
640
+ if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
641
+ }
642
+
643
+ function renderView(v) {
644
+ if (v === 'now') { refreshNow(); return; }
645
+ if (v === 'projects') renderProjects();
646
+ if (v === 'sessions') renderSessions();
647
+ if (v === 'loops') renderLoops();
648
+ if (v === 'memory') renderMemory();
649
+ if (v === 'orgs') renderOrgs();
650
+ }
651
+
652
+ function refreshCurrent() {
653
+ viewRendered[currentView] = false;
654
+ renderView(currentView);
655
+ }
656
+
657
+ // ── init ───────────────────────────────────────────────────
658
+ async function init() {
659
+ try {
660
+ const gu = await apiFetch('/api/git-user');
661
+ DIR = gu.cwd || '';
662
+ ORIGINAL_DIR = DIR;
663
+ gitUser = gu;
664
+ document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
665
+ document.getElementById('sb-path').textContent = DIR;
666
+ document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
667
+ } catch (_) {}
668
+ viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
669
+ await refreshNow();
670
+ startPolling();
671
+ }
672
+
673
+ function startPolling() {
674
+ clearInterval(pollTimer);
675
+ pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
676
+ }
677
+
678
+ async function apiFetch(url) {
679
+ const r = await fetch(url);
680
+ if (!r.ok) throw new Error(r.status);
681
+ return r.json();
682
+ }
683
+
684
+ // ── project switching ──────────────────────────────────────
685
+ function switchProject(path) {
686
+ if (DIR === path) { switchView('sessions'); return; }
687
+ DIR = path;
688
+ document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
689
+ document.getElementById('sb-path').textContent = path;
690
+ viewRendered = {};
691
+ allSessions = [];
692
+ closeDetail();
693
+ switchView('sessions');
694
+ }
695
+
696
+ // ── NOW view ───────────────────────────────────────────────
697
+ async function refreshNow() {
698
+ userScrolled = false;
699
+ await Promise.allSettled([loadFeed(), loadMetrics()]);
700
+ }
701
+
702
+ async function refreshNowSilent() {
703
+ if (liveTailMode) { userScrolled = false; sessionIdx = 0; }
704
+ await Promise.allSettled([loadFeedSilent(), loadMetrics()]);
705
+ }
706
+
707
+ // ── live tail ──────────────────────────────────────────────
708
+ function toggleLiveTail() {
709
+ liveTailMode = !liveTailMode;
710
+ const btn = document.getElementById('btn-live-tail');
711
+ btn.classList.toggle('on', liveTailMode);
712
+ btn.title = liveTailMode ? 'Live tail ON — click to disable' : 'Toggle live tail (5s refresh + auto-scroll)';
713
+ clearInterval(liveTailTimer);
714
+ if (liveTailMode) {
715
+ // jump to newest session and start fast polling
716
+ if (sessionIdx !== 0 && allSessions.length) { sessionIdx = 0; userScrolled = false; loadFeedForSession(allSessions[0]); }
717
+ liveTailTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 5000);
718
+ } else {
719
+ startPolling();
720
+ }
721
+ }
722
+
723
+ // session nav
724
+ function prevSession() {
725
+ if (sessionIdx < allSessions.length - 1) { sessionIdx++; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
726
+ }
727
+ function nextSession() {
728
+ if (sessionIdx > 0) { sessionIdx--; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
729
+ }
730
+
731
+ async function loadFeed() {
732
+ if (!DIR) return;
733
+ try {
734
+ const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
735
+ allSessions = sessions;
736
+ document.getElementById('bdg-sessions').textContent = sessions.length || '—';
737
+ if (!sessions.length) {
738
+ setFeedContent('<div class="feed-empty">No sessions yet in this project.</div>');
739
+ return;
740
+ }
741
+ sessionIdx = 0;
742
+ await loadFeedForSession(sessions[0]);
743
+ renderMiniSessions(sessions.slice(0, 6));
744
+ // patch sparkline now that allSessions is populated
745
+ const todayEl = document.getElementById('m-today');
746
+ const sparkWrap = todayEl?.querySelector('.spark-wrap');
747
+ if (sparkWrap) sparkWrap.outerHTML = buildSparkline();
748
+ detectAnomalies();
749
+ } catch (err) {
750
+ setFeedContent('<div class="feed-empty">Could not load feed: ' + esc(err.message) + '</div>');
751
+ }
752
+ }
753
+
754
+ async function loadFeedSilent() {
755
+ if (!DIR || !allSessions.length) return;
756
+ try {
757
+ const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
758
+ allSessions = sessions;
759
+ if (!sessions.length) return;
760
+ const currentSess = allSessions[sessionIdx] || sessions[0];
761
+ if (!currentSess?.file) return;
762
+ const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(currentSess.file) + '&limit=120');
763
+ renderFeedEvents(data.events || [], true);
764
+ } catch (_) {}
765
+ }
766
+
767
+ async function loadFeedForSession(sess) {
768
+ if (!sess) return;
769
+ document.getElementById('feed-sess').textContent = sess.id.slice(0, 8) + '…';
770
+ document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
771
+ document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
772
+ showSessCtx(sess);
773
+ if (!sess.file) { setFeedContent('<div class="feed-empty">Session file path unavailable.</div>'); return; }
774
+ try {
775
+ const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=120');
776
+ renderFeedEvents(data.events || [], false);
777
+ } catch (err) {
778
+ setFeedContent('<div class="feed-empty">Could not load session: ' + esc(err.message) + '</div>');
779
+ }
780
+ }
781
+
782
+ // ── feed rendering ─────────────────────────────────────────
783
+
784
+ // pre-pass: mark tool events as errored based on their tool_result
785
+ function annotateErrors(events) {
786
+ const byId = new Map();
787
+ events.forEach((ev, i) => { if (ev.kind === 'tool' && ev.id) byId.set(ev.id, i); });
788
+ events.forEach(ev => {
789
+ if (ev.kind === 'tool_result' && ev.isError && ev.tool_use_id) {
790
+ const idx = byId.get(ev.tool_use_id);
791
+ if (idx != null) events[idx]._errored = true;
792
+ }
793
+ });
794
+ }
795
+
796
+ // group consecutive same-cat tool events (threshold: 3+)
797
+ function groupEvents(events) {
798
+ const out = [];
799
+ let i = 0;
800
+ while (i < events.length) {
801
+ const ev = events[i];
802
+ if (ev.kind !== 'tool') { out.push(ev); i++; continue; }
803
+ // look ahead for same cat run
804
+ let j = i + 1;
805
+ while (j < events.length && events[j].kind === 'tool' && events[j].cat === ev.cat && !events[j]._errored && !ev._errored) j++;
806
+ const run = j - i;
807
+ if (run >= 3) {
808
+ out.push({ kind: '_group', cat: ev.cat, count: run, label: catLabel(ev.cat), ts: ev.ts, items: events.slice(i, j) });
809
+ i = j;
810
+ } else {
811
+ out.push(ev); i++;
812
+ }
813
+ }
814
+ return out;
815
+ }
816
+
817
+ function catLabel(c) {
818
+ const m = { file:'file operations', bash:'shell commands', agent:'agent spawns', mcp:'MCP calls', search:'searches', skill:'skills', task:'task writes', mem:'memory ops', other:'tool calls' };
819
+ return m[c] || 'tool calls';
820
+ }
821
+
822
+ function renderFeedEvents(events, silent) {
823
+ if (!events.length) {
824
+ if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.</div>');
825
+ return;
826
+ }
827
+
828
+ annotateErrors(events);
829
+
830
+ // filter: only tool + user, skip text/thinking/tool_result and hook system messages
831
+ const HOOK_RE = /^<(local-command-|command-name>|command-message>|local-command-caveat>)/;
832
+ const filtered = events.filter(ev =>
833
+ ev.kind === 'tool' ||
834
+ (ev.kind === 'user' && ev.text?.trim() && !HOOK_RE.test(ev.text.trim())));
835
+
836
+ // apply time-range filter
837
+ let visible = filtered;
838
+ if (feedTimeFilter !== 'all') {
839
+ const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000 }[feedTimeFilter] || 0;
840
+ const cutoff = Date.now() - ms;
841
+ visible = filtered.filter(ev => !ev.ts || new Date(ev.ts).getTime() >= cutoff);
842
+ }
843
+
844
+ // update error alert state
845
+ alertState.errorCount = visible.filter(ev => ev._errored).length;
846
+ updateAlerts();
847
+
848
+ // reverse (newest first), then group
849
+ const reversed = [...visible].reverse();
850
+ const grouped = groupEvents(reversed);
851
+
852
+ const parts = [];
853
+ let prevKind = null;
854
+
855
+ for (const item of grouped) {
856
+ if (item.kind === '_group') {
857
+ parts.push(renderGroupRow(item));
858
+ prevKind = 'tool';
859
+ continue;
860
+ }
861
+ if (prevKind === 'user' && item.kind === 'tool') {
862
+ parts.push('<div class="feed-divider"></div>');
863
+ } else if (prevKind === 'tool' && item.kind === 'user') {
864
+ parts.push('<div class="feed-divider"></div>');
865
+ }
866
+ parts.push(renderFeedEntry(item));
867
+ prevKind = item.kind;
868
+ }
869
+
870
+ const newHtml = parts.join('');
871
+ if (silent) {
872
+ const el = document.getElementById('feed-content');
873
+ if (el.innerHTML === newHtml) return;
874
+ const wasAtTop = !userScrolled;
875
+ el.innerHTML = newHtml;
876
+ if (wasAtTop) document.getElementById('feed-scroll').scrollTop = 0;
877
+ } else {
878
+ setFeedContent(newHtml);
879
+ if (!userScrolled) document.getElementById('feed-scroll').scrollTop = 0;
880
+ }
881
+
882
+ // update timeline + breakdown with all original events (before time-filter)
883
+ buildTimeline(filtered);
884
+ buildBreakdown(filtered);
885
+ }
886
+
887
+ function renderGroupRow(g) {
888
+ const { ico, catCls } = toolStyle(g.cat, '');
889
+ const itemsData = JSON.stringify(g.items).replace(/'/g, '&#39;');
890
+ return `<div class="feed-group" data-items='${itemsData}' onclick="expandGroup(this)">
891
+ <div class="feed-ico ${catCls}" style="font-size:9px">${ico}</div>
892
+ <span class="fg-label">${g.count} ${esc(g.label)}</span>
893
+ <span class="fg-expand">▸ expand</span>
894
+ </div>`;
895
+ }
896
+
897
+ function expandGroup(el) {
898
+ const items = JSON.parse(el.dataset.items);
899
+ const html = items.map(renderFeedEntry).join('');
900
+ el.outerHTML = html;
901
+ // re-apply active feed search to newly injected entries
902
+ const q = document.getElementById('feed-search-input')?.value || '';
903
+ if (feedSearchActive && q) filterFeed(q);
904
+ }
905
+
906
+ function renderFeedEntry(ev) {
907
+ const ts = ev.ts ? relTime(ev.ts) : '';
908
+ let lbl = '', detail = '', id = ev.id || ev.uuid || '';
909
+ let catCls, ico;
910
+
911
+ if (ev.kind === 'tool') {
912
+ ({ ico, catCls } = toolStyle(ev.cat, ev.name));
913
+ lbl = esc(ev.label || ev.name || 'tool');
914
+ if (ev.subagent) detail = esc(ev.subagent);
915
+ } else {
916
+ ico = '↵'; catCls = 'cat-user';
917
+ const t = (ev.text || '').trim();
918
+ lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
919
+ }
920
+
921
+ const errClass = ev._errored ? ' errored' : '';
922
+ const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
923
+
924
+ const evData = JSON.stringify(ev).replace(/'/g, '&#39;');
925
+ return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
926
+ <div class="feed-ico ${catCls}">${ico}</div>
927
+ <div class="feed-body">
928
+ <div class="feed-lbl">${lbl}</div>
929
+ ${detail ? `<div class="feed-detail">${detail}</div>` : ''}
930
+ </div>
931
+ <div class="feed-ts">${ts}</div>
932
+ </div>`;
933
+ }
934
+
935
+ function toolStyle(cat, name) {
936
+ const map = {
937
+ file: { ico: '◧', catCls: 'cat-file' },
938
+ bash: { ico: '$', catCls: 'cat-bash' },
939
+ agent: { ico: '→', catCls: 'cat-agent' },
940
+ mcp: { ico: '⬡', catCls: 'cat-mcp' },
941
+ search: { ico: '/', catCls: 'cat-search' },
942
+ skill: { ico: '◆', catCls: 'cat-skill' },
943
+ task: { ico: '☑', catCls: 'cat-task' },
944
+ mem: { ico: '◈', catCls: 'cat-mem' },
945
+ };
946
+ if (name === 'Skill') return { ico: '◆', catCls: 'cat-skill' };
947
+ return map[cat] || { ico: '·', catCls: 'cat-other' };
948
+ }
949
+
950
+ function setFeedContent(html) {
951
+ document.getElementById('feed-content').innerHTML = html;
952
+ }
953
+
954
+ // ── detail panel ───────────────────────────────────────────
955
+ function openDetail(evJson) {
956
+ const ev = JSON.parse(evJson);
957
+ selectedEntryId = ev.id || ev.uuid || '';
958
+
959
+ const panel = document.getElementById('detail-panel');
960
+ panel.classList.add('open');
961
+
962
+ let title = '';
963
+ let bodyHtml = '';
964
+
965
+ if (ev.kind === 'tool') {
966
+ const { catCls } = toolStyle(ev.cat, ev.name);
967
+ title = ev.name || 'Tool';
968
+ bodyHtml = `
969
+ <div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
970
+ <div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(ev.label || ev.name)}</div></div>
971
+ ${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
972
+ ${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
973
+ <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
974
+ <div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((ev.id || '').slice(0, 24))}</div></div>
975
+ `;
976
+ } else if (ev.kind === 'user') {
977
+ title = 'User message';
978
+ bodyHtml = `
979
+ <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
980
+ <div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(ev.text || '')}</div></div>
981
+ `;
982
+ }
983
+
984
+ document.getElementById('detail-title').textContent = title;
985
+ document.getElementById('detail-body').innerHTML = bodyHtml;
986
+ }
987
+
988
+ function closeDetail() {
989
+ document.getElementById('detail-panel').classList.remove('open');
990
+ selectedEntryId = null;
991
+ }
992
+
993
+ // ── 12-week calendar heatmap ───────────────────────────────
994
+ function buildSparkline() {
995
+ const DAY = 86400000;
996
+ const now = Date.now();
997
+ const WEEKS = 12;
998
+ const DAYS = WEEKS * 7; // 84 days
999
+ const buckets = new Array(DAYS).fill(0);
1000
+ for (const s of allSessions) {
1001
+ const ts = s.lastTs || s.mtime;
1002
+ if (!ts) continue;
1003
+ const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
1004
+ const idx = DAYS - 1 - Math.floor(age / DAY);
1005
+ if (idx >= 0 && idx < DAYS) buckets[idx]++;
1006
+ }
1007
+ const max = Math.max(...buckets, 1);
1008
+ // offset so first cell starts on Monday of the week 12 weeks ago
1009
+ const todayDow = new Date().getDay(); // 0=Sun
1010
+ // pad start so column 0 begins on Monday
1011
+ const startOffset = todayDow === 0 ? 6 : todayDow - 1;
1012
+ const cells = buckets.map((v, i) => {
1013
+ const isToday = i === DAYS - 1;
1014
+ const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
1015
+ const d = new Date(now - (DAYS - 1 - i) * DAY);
1016
+ const label = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
1017
+ const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
1018
+ return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
1019
+ });
1020
+ return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
1021
+ }
1022
+
1023
+ // ── alerts rail ────────────────────────────────────────────
1024
+ function updateAlerts() {
1025
+ const rail = document.getElementById('alerts-rail');
1026
+ const all = [];
1027
+
1028
+ if (alertState.todayCost > 50) {
1029
+ all.push({ id: 'cost-crit', cls: 'alert-crit', ico: '⚑', msg: `Critical spend: $${alertState.todayCost.toFixed(2)} today` });
1030
+ } else if (alertState.todayCost > 20) {
1031
+ all.push({ id: 'cost-warn', cls: 'alert-warn', ico: '⚠', msg: `High spend: $${alertState.todayCost.toFixed(2)} today` });
1032
+ }
1033
+
1034
+ if (alertState.errorCount >= 3) {
1035
+ all.push({ id: 'feed-errors', cls: 'alert-warn', ico: '⚠', msg: `${alertState.errorCount} errors in current session`, action: 'jumpToErrors()' });
1036
+ }
1037
+
1038
+ if (alertState.anomaly) {
1039
+ all.push({ id: 'anomaly-sess', cls: 'alert-warn', ico: '◎', msg: alertState.anomaly });
1040
+ }
1041
+
1042
+ for (const l of alertState.longLoops) {
1043
+ all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
1044
+ }
1045
+
1046
+ const visible = all.filter(a => !dismissedAlerts.has(a.id));
1047
+ if (!visible.length) {
1048
+ rail.className = '';
1049
+ rail.innerHTML = '';
1050
+ return;
1051
+ }
1052
+ rail.className = 'has-alerts';
1053
+ rail.innerHTML = visible.map(a =>
1054
+ `<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
1055
+ <span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
1056
+ </div>`).join('');
1057
+ }
1058
+
1059
+ function dismissAlert(id) {
1060
+ dismissedAlerts.add(id);
1061
+ updateAlerts();
1062
+ }
1063
+
1064
+ // ── anomaly detection ──────────────────────────────────────
1065
+ function detectAnomalies() {
1066
+ const withCost = allSessions.filter(s => typeof (s.totalCost ?? s.cost) === 'number');
1067
+ if (withCost.length < 3) { alertState.anomaly = null; updateAlerts(); return; }
1068
+ const avg = withCost.reduce((sum, s) => sum + (s.totalCost ?? s.cost ?? 0), 0) / withCost.length;
1069
+ const curr = allSessions[sessionIdx];
1070
+ const currCost = typeof curr?.totalCost === 'number' ? curr.totalCost : (typeof curr?.cost === 'number' ? curr.cost : null);
1071
+ if (currCost !== null && avg > 0.05 && currCost > avg * 2.5 && currCost > 0.5) {
1072
+ alertState.anomaly = `Session unusually costly: $${currCost.toFixed(2)} vs $${avg.toFixed(2)} avg`;
1073
+ } else {
1074
+ alertState.anomaly = null;
1075
+ }
1076
+ updateAlerts();
1077
+ }
1078
+
1079
+ // ── session context bar ────────────────────────────────────
1080
+ function showSessCtx(sess) {
1081
+ const bar = document.getElementById('sess-ctx');
1082
+ const isLive = !sess || !allSessions.length || allSessions[0]?.id === sess.id;
1083
+ if (isLive) {
1084
+ bar.classList.remove('show');
1085
+ return;
1086
+ }
1087
+ document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
1088
+ bar.classList.add('show');
1089
+ }
1090
+
1091
+ function goLive() {
1092
+ if (!allSessions.length) return;
1093
+ sessionIdx = 0;
1094
+ userScrolled = false;
1095
+ loadFeedForSession(allSessions[0]);
1096
+ }
1097
+
1098
+ // ── metrics ────────────────────────────────────────────────
1099
+ async function loadMetrics() {
1100
+ if (!DIR) return;
1101
+ await Promise.allSettled([loadTodayMetrics(), loadLoopMetrics()]);
1102
+ }
1103
+
1104
+ async function loadTodayMetrics() {
1105
+ try {
1106
+ const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
1107
+ const s = data?.tokens?.summary || {};
1108
+ alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
1109
+ updateAlerts();
1110
+ // topbar cost badge
1111
+ const badge = document.getElementById('topbar-cost');
1112
+ if (badge && typeof s.todayCost === 'number') {
1113
+ badge.textContent = '$' + s.todayCost.toFixed(2) + ' today';
1114
+ badge.classList.add('loaded');
1115
+ }
1116
+ const cost = typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—';
1117
+ const calls = s.todayCalls != null ? s.todayCalls : '—';
1118
+ const moCost = typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—';
1119
+ // cost forecast: project monthly spend from daily average
1120
+ let forecast = '';
1121
+ if (typeof s.monthCost === 'number' && s.monthCost > 0) {
1122
+ const day = new Date().getDate();
1123
+ const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
1124
+ const projected = (s.monthCost / day) * daysInMonth;
1125
+ forecast = `<div class="m-row"><span class="m-name">Month forecast</span><span class="m-val forecast">~$${projected.toFixed(2)}</span></div>`;
1126
+ }
1127
+ document.getElementById('m-today').innerHTML = `
1128
+ <div class="m-group-title">Today</div>
1129
+ <div class="m-row"><span class="m-name">API cost</span><span class="m-val gold">${cost}</span></div>
1130
+ <div class="m-row"><span class="m-name">API calls</span><span class="m-val">${calls}</span></div>
1131
+ <div class="m-row"><span class="m-name">Month total</span><span class="m-val">${moCost}</span></div>
1132
+ ${forecast}
1133
+ ${buildSparkline()}
1134
+ `;
1135
+ } catch (_) {
1136
+ document.getElementById('m-today').innerHTML = `<div class="m-group-title">Today</div><div class="loading-txt">—</div>`;
1137
+ }
1138
+ }
1139
+
1140
+ async function loadLoopMetrics() {
1141
+ try {
1142
+ const data = await apiFetch('/api/loops?dir=' + enc(DIR));
1143
+ const loops = Array.isArray(data) ? data : (data.loops || []);
1144
+ document.getElementById('bdg-loops').textContent = loops.length || '—';
1145
+
1146
+ // alert on loops running > 2h
1147
+ const TWO_HOURS = 2 * 3600 * 1000;
1148
+ const now = Date.now();
1149
+ alertState.longLoops = loops
1150
+ .filter(l => l.running !== false && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
1151
+ .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
1152
+ updateAlerts();
1153
+
1154
+ if (!loops.length) {
1155
+ document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt" style="padding:8px 0">None</div>`;
1156
+ return;
1157
+ }
1158
+ const items = loops.slice(0, 5).map(l => {
1159
+ const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
1160
+ return `<div class="mini-loop">
1161
+ <div class="ml-name">${esc(name)}</div>
1162
+ <div class="ml-meta"><span class="ml-dot"></span>${esc(l.interval || l.schedule || 'running')}</div>
1163
+ </div>`;
1164
+ }).join('');
1165
+ document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
1166
+ } catch (_) {
1167
+ document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
1168
+ }
1169
+ }
1170
+
1171
+ function renderMiniSessions(sessions) {
1172
+ if (!sessions.length) return;
1173
+ const items = sessions.map((s, i) => `
1174
+ <div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
1175
+ <div class="ms-prompt">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
1176
+ <div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
1177
+ </div>`).join('');
1178
+ document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
1179
+ }
1180
+
1181
+ // ── projects ───────────────────────────────────────────────
1182
+ async function renderProjects() {
1183
+ const el = document.getElementById('proj-content');
1184
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
1185
+ try {
1186
+ const data = await apiFetch('/api/data?dir=' + enc(ORIGINAL_DIR));
1187
+ allProjects = data?.allProjects || [];
1188
+ document.getElementById('bdg-projects').textContent = allProjects.length || '—';
1189
+ document.getElementById('proj-pg-sub').textContent =
1190
+ allProjects.length + ' project' + (allProjects.length !== 1 ? 's' : '') + ' found';
1191
+ renderProjectGrid(allProjects, '');
1192
+ } catch (err) {
1193
+ el.innerHTML = '<div class="empty">Could not load projects: ' + esc(err.message) + '</div>';
1194
+ }
1195
+ }
1196
+
1197
+ function renderProjectGrid(projects, query) {
1198
+ const el = document.getElementById('proj-content');
1199
+ const filtered = query ? projects.filter(p =>
1200
+ (p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
1201
+ (p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
1202
+ if (!filtered.length) {
1203
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
1204
+ return;
1205
+ }
1206
+ el.className = 'proj-grid';
1207
+ el.innerHTML = filtered.map(p => {
1208
+ const isCurrent = p.path === DIR;
1209
+ return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
1210
+ ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
1211
+ <div class="proj-card-name">${esc(p.name || p.slug)}</div>
1212
+ <div class="proj-card-path">${esc(p.path || '')}</div>
1213
+ <div class="proj-card-stats">
1214
+ <div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
1215
+ <div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
1216
+ ${p.lastActivity ? `<div class="proj-stat"><div class="ps-v" style="font-size:12px">${relTime(p.lastActivity)}</div><div class="ps-l">last active</div></div>` : ''}
1217
+ </div>
1218
+ </div>`;
1219
+ }).join('');
1220
+ }
1221
+
1222
+ function filterProjects(q) {
1223
+ renderProjectGrid(allProjects, q);
1224
+ }
1225
+
1226
+ // ── sessions ───────────────────────────────────────────────
1227
+ async function renderSessions() {
1228
+ const el = document.getElementById('sess-content');
1229
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
1230
+ try {
1231
+ const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
1232
+ allSessions = sessions; // always sync — stale ordering breaks jumpToSession
1233
+ document.getElementById('bdg-sessions').textContent = sessions.length || '—';
1234
+ document.getElementById('sess-pg-sub').textContent =
1235
+ sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
1236
+ if (!sessions.length) {
1237
+ el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
1238
+ return;
1239
+ }
1240
+ const toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
1241
+ if (!toShow.length) {
1242
+ el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
1243
+ return;
1244
+ }
1245
+ el.innerHTML = toShow.map(s => {
1246
+ const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
1247
+ const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
1248
+ const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
1249
+ : typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
1250
+ const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
1251
+ const summaries = (s.summaries || []).slice(0, 2).map(sm => { const t = typeof sm === 'string' ? sm : (sm.summary || sm.text || String(sm)); return `<span class="sr-tag">${esc(t.slice(0, 40))}</span>`; }).join('');
1252
+ const isStarred = bookmarks.has(s.id);
1253
+ return `<div class="sess-row" onclick="jumpToSession('${esc(s.id)}')">
1254
+ <div class="sr-top">
1255
+ <div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
1256
+ <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
1257
+ <button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
1258
+ <span class="sr-view">→ view</span>
1259
+ </div>
1260
+ <div class="sr-meta">${esc(meta)}</div>
1261
+ ${summaries ? `<div class="sr-tags">${summaries}</div>` : ''}
1262
+ </div>`;
1263
+ }).join('');
1264
+ } catch (err) {
1265
+ el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
1266
+ }
1267
+ }
1268
+
1269
+ function jumpToSession(id) {
1270
+ switchView('now');
1271
+ setTimeout(() => {
1272
+ const i = allSessions.findIndex(x => x.id === id);
1273
+ if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
1274
+ }, 80);
1275
+ }
1276
+
1277
+ // ── session bookmarks ──────────────────────────────────────
1278
+ function toggleBookmark(id, e) {
1279
+ e.stopPropagation();
1280
+ if (bookmarks.has(id)) bookmarks.delete(id);
1281
+ else bookmarks.add(id);
1282
+ localStorage.setItem('mm-bookmarks', JSON.stringify([...bookmarks]));
1283
+ document.querySelectorAll('.sess-star[data-sid="' + id + '"]').forEach(el => {
1284
+ const on = bookmarks.has(id);
1285
+ el.classList.toggle('on', on);
1286
+ el.textContent = on ? '★' : '☆';
1287
+ el.title = on ? 'Remove bookmark' : 'Bookmark session';
1288
+ });
1289
+ }
1290
+
1291
+ function toggleSessStarFilter() {
1292
+ showStarredOnly = !showStarredOnly;
1293
+ const btn = document.getElementById('sess-star-filter');
1294
+ btn.classList.toggle('on', showStarredOnly);
1295
+ // re-render with filter
1296
+ viewRendered['sessions'] = false;
1297
+ renderSessions();
1298
+ viewRendered['sessions'] = true;
1299
+ }
1300
+
1301
+ // ── loops ──────────────────────────────────────────────────
1302
+ async function renderLoops() {
1303
+ const el = document.getElementById('loops-content');
1304
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
1305
+ try {
1306
+ const data = await apiFetch('/api/loops?dir=' + enc(DIR));
1307
+ const loops = Array.isArray(data) ? data : (data.loops || []);
1308
+ document.getElementById('bdg-loops').textContent = loops.length || '—';
1309
+ if (!loops.length) {
1310
+ el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div></div>';
1311
+ return;
1312
+ }
1313
+ el.innerHTML = loops.map((l, idx) => {
1314
+ const running = l.running !== false;
1315
+ const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
1316
+ const interval = l.interval || l.schedule || '';
1317
+ const fullPrompt = l.prompt || l.command || '';
1318
+ const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
1319
+ const lastRun = l.lastRun ? relTime(l.lastRun) : (l.startedAt ? relTime(l.startedAt) : '—');
1320
+ const runs = l.runCount != null ? l.runCount : '—';
1321
+ return `<div class="loop-row" onclick="toggleLoop(this)">
1322
+ <div class="loop-ico">↺</div>
1323
+ <div class="loop-body">
1324
+ <div class="loop-name">${esc(name)}</div>
1325
+ <div class="loop-meta">${esc([interval, l.description].filter(Boolean).join(' · ').slice(0, 80))}</div>
1326
+ </div>
1327
+ <div class="loop-status ${running ? 'active' : 'stopped'}">${running ? 'active' : 'stopped'}</div>
1328
+ </div>
1329
+ <div class="loop-expand">
1330
+ ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
1331
+ <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(interval || '—')}</div></div>
1332
+ <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${running ? '● running' : '○ stopped'}</div></div>
1333
+ <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
1334
+ <div class="le-row"><div class="le-lbl">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
1335
+ <div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}</div></div>
1336
+ </div>`;
1337
+ }).join('');
1338
+ } catch (err) {
1339
+ el.innerHTML = '<div class="empty">Could not load loops: ' + esc(err.message) + '</div>';
1340
+ }
1341
+ }
1342
+
1343
+ function toggleLoop(row) {
1344
+ row.classList.toggle('open');
1345
+ }
1346
+
1347
+ // ── memory ─────────────────────────────────────────────────
1348
+ async function renderMemory() {
1349
+ activeMemNs = 'All'; // reset on every render so stale filter doesn't persist
1350
+ const el = document.getElementById('mem-content');
1351
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
1352
+ try {
1353
+ const data = await apiFetch('/api/palace?dir=' + enc(DIR));
1354
+ allDrawers = data.drawers || [];
1355
+ const identity = data.identity || '';
1356
+ let html = '';
1357
+ if (identity) {
1358
+ html += `<div class="mem-section">
1359
+ <div class="mem-title">Identity</div>
1360
+ <div class="drawer-item"><div class="dr-val" style="white-space:pre-wrap">${esc(identity.slice(0, 1200))}</div></div>
1361
+ </div>`;
1362
+ }
1363
+ if (allDrawers.length) {
1364
+ // build namespace tabs
1365
+ const namespaces = ['All', ...new Set(allDrawers.map(d => d.namespace || 'default').filter(Boolean))];
1366
+ const tabsEl = document.getElementById('mem-ns-tabs');
1367
+ if (tabsEl) {
1368
+ tabsEl.innerHTML = namespaces.map((ns, i) =>
1369
+ `<button class="ns-tab${i === 0 ? ' active' : ''}" data-ns="${esc(ns)}" onclick="filterMemoryNs('${esc(ns)}')">${esc(ns)}</button>`
1370
+ ).join('');
1371
+ }
1372
+
1373
+ const items = allDrawers.map((d, i) =>
1374
+ `<div class="drawer-item" data-idx="${i}" data-ns="${esc(d.namespace || 'default')}">
1375
+ <div class="dr-key">${esc(d.key || d.namespace || '—')}</div>
1376
+ <div class="dr-val">${esc(String(d.value || d.text || '').slice(0, 300))}</div>
1377
+ ${d.timestamp ? `<div class="dr-ts">${relTime(d.timestamp)}</div>` : ''}
1378
+ </div>`).join('');
1379
+ html += `<div class="mem-section" id="drawers-section">
1380
+ <div class="mem-title">Drawers (${allDrawers.length})</div>
1381
+ <div id="drawers-list">${items}</div>
1382
+ </div>`;
1383
+ }
1384
+ if (!html) html = '<div class="empty"><div class="empty-ico">◈</div><div>Memory palace is empty</div></div>';
1385
+ el.innerHTML = html;
1386
+ } catch (err) {
1387
+ el.innerHTML = '<div class="empty">Could not load memory: ' + esc(err.message) + '</div>';
1388
+ }
1389
+ }
1390
+
1391
+ let activeMemNs = 'All';
1392
+
1393
+ function filterMemory(q) {
1394
+ const items = document.querySelectorAll('#drawers-list .drawer-item');
1395
+ const lq = q.toLowerCase();
1396
+ items.forEach(item => {
1397
+ const key = (item.querySelector('.dr-key')?.textContent || '').toLowerCase();
1398
+ const val = (item.querySelector('.dr-val')?.textContent || '').toLowerCase();
1399
+ const nsMatch = activeMemNs === 'All' || (item.dataset.ns === activeMemNs);
1400
+ item.classList.toggle('hidden', !nsMatch || (!!lq && !key.includes(lq) && !val.includes(lq)));
1401
+ });
1402
+ }
1403
+
1404
+ function filterMemoryNs(ns) {
1405
+ activeMemNs = ns;
1406
+ document.querySelectorAll('.ns-tab').forEach(t => t.classList.toggle('active', t.dataset.ns === ns));
1407
+ filterMemory(document.getElementById('mem-filter')?.value || '');
1408
+ }
1409
+
1410
+ // ── orgs ───────────────────────────────────────────────────
1411
+ async function renderOrgs() {
1412
+ const el = document.getElementById('orgs-content');
1413
+ el.innerHTML = '<div class="loading-txt">Loading…</div>';
1414
+ try {
1415
+ const data = await apiFetch('/api/orgs');
1416
+ const orgs = Array.isArray(data) ? data : (data.orgs || []);
1417
+ if (!orgs.length) {
1418
+ el.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No MASTERMIND orgs found</div></div>';
1419
+ return;
1420
+ }
1421
+ el.innerHTML = '<div class="org-list">' + orgs.map(o =>
1422
+ `<div class="org-row">
1423
+ <div class="org-name">${esc(o.name || o.id || '—')}</div>
1424
+ <div class="org-meta">${esc(o.description || (o.agents != null ? o.agents + ' agents' : ''))}</div>
1425
+ </div>`).join('') + '</div>';
1426
+ } catch (err) {
1427
+ el.innerHTML = '<div class="empty">Could not load orgs: ' + esc(err.message) + '</div>';
1428
+ }
1429
+ }
1430
+
1431
+ // ── density toggle ─────────────────────────────────────────
1432
+ let compactMode = false;
1433
+ function toggleDensity() {
1434
+ compactMode = !compactMode;
1435
+ const pane = document.getElementById('feed-pane');
1436
+ const btn = document.getElementById('btn-density');
1437
+ pane.classList.toggle('compact', compactMode);
1438
+ btn.classList.toggle('compact-on', compactMode);
1439
+ btn.title = compactMode ? 'Switch to comfortable view' : 'Toggle compact view';
1440
+ }
1441
+
1442
+ // ── session timeline ────────────────────────────────────────
1443
+ const TL_COLORS = {
1444
+ file: 'oklch(65% 0.15 150)',
1445
+ bash: 'oklch(65% 0.12 240)',
1446
+ agent: 'oklch(65% 0.13 290)',
1447
+ mcp: 'oklch(65% 0.12 195)',
1448
+ search: 'oklch(65% 0.14 35)',
1449
+ skill: 'oklch(72% 0.18 75)',
1450
+ task: 'oklch(62% 0.12 55)',
1451
+ mem: 'oklch(62% 0.11 160)',
1452
+ user: 'oklch(55% 0.08 75 / 0.5)',
1453
+ other: 'oklch(32% 0.005 75)',
1454
+ };
1455
+
1456
+ function buildTimeline(events) {
1457
+ const tl = document.getElementById('feed-timeline');
1458
+ if (!tl) return;
1459
+ // Only tool + user events with timestamps
1460
+ const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
1461
+ if (stamped.length < 2) { tl.innerHTML = ''; return; }
1462
+ const times = stamped.map(ev => new Date(ev.ts).getTime());
1463
+ const tMin = Math.min(...times), tMax = Math.max(...times);
1464
+ const span = tMax - tMin || 1;
1465
+ const segs = stamped.map(ev => {
1466
+ const pct = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
1467
+ const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
1468
+ const color = TL_COLORS[cat] || TL_COLORS.other;
1469
+ const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
1470
+ return `<div class="tl-seg" style="flex:${pct > 0 ? pct : 0.5};background:${color}" title="${esc(label)}"></div>`;
1471
+ });
1472
+ tl.innerHTML = segs.join('');
1473
+ }
1474
+
1475
+ // ── tool breakdown ──────────────────────────────────────────
1476
+ const TB_COLORS = TL_COLORS;
1477
+ const TB_ORDER = ['file','bash','mcp','agent','search','skill','task','mem','other'];
1478
+
1479
+ function buildBreakdown(events) {
1480
+ const counts = {};
1481
+ for (const ev of events) {
1482
+ if (ev.kind !== 'tool') continue;
1483
+ const c = ev.cat || 'other';
1484
+ counts[c] = (counts[c] || 0) + 1;
1485
+ }
1486
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
1487
+ if (!total) {
1488
+ document.getElementById('m-breakdown').innerHTML =
1489
+ '<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
1490
+ return;
1491
+ }
1492
+ const rows = TB_ORDER.filter(c => counts[c])
1493
+ .sort((a, b) => counts[b] - counts[a])
1494
+ .map(c => {
1495
+ const pct = Math.round(counts[c] / total * 100);
1496
+ const color = TB_COLORS[c] || TB_COLORS.other;
1497
+ return `<div class="tb-row">
1498
+ <div class="tb-lbl">${c}</div>
1499
+ <div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${color}"></div></div>
1500
+ <div class="tb-count">${counts[c]}</div>
1501
+ </div>`;
1502
+ }).join('');
1503
+ document.getElementById('m-breakdown').innerHTML =
1504
+ `<div class="m-group-title">Tool Usage</div><div class="m-breakdown">${rows}</div>`;
1505
+ }
1506
+
1507
+ // ── error jump ──────────────────────────────────────────────
1508
+ function jumpToErrors() {
1509
+ // find first errored feed entry and scroll to it
1510
+ const first = document.querySelector('#feed-content .feed-entry.errored');
1511
+ if (!first) return;
1512
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' });
1513
+ first.style.outline = '1px solid var(--red)';
1514
+ setTimeout(() => { first.style.outline = ''; }, 1800);
1515
+ // highlight all errored entries briefly
1516
+ document.querySelectorAll('#feed-content .feed-entry.errored').forEach(el => {
1517
+ el.style.background = 'oklch(60% 0.18 25 / 0.08)';
1518
+ setTimeout(() => { el.style.background = ''; }, 1800);
1519
+ });
1520
+ }
1521
+
1522
+ // ── week-over-week delta ────────────────────────────────────
1523
+ function buildWowDelta() {
1524
+ // Compare this week (last 7 days) vs prior week (8–14 days ago)
1525
+ const DAY = 86400000;
1526
+ const now = Date.now();
1527
+ let thisWeek = 0, lastWeek = 0;
1528
+ for (const s of allSessions) {
1529
+ const ts = s.lastTs || s.mtime;
1530
+ if (!ts) continue;
1531
+ const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
1532
+ if (age < 7 * DAY) thisWeek++;
1533
+ else if (age < 14 * DAY) lastWeek++;
1534
+ }
1535
+ if (!lastWeek) return '';
1536
+ const delta = Math.round((thisWeek - lastWeek) / lastWeek * 100);
1537
+ if (delta > 0) return `<span class="wow-delta wow-up" title="vs prior 7 days">↑${delta}%</span>`;
1538
+ if (delta < 0) return `<span class="wow-delta wow-down" title="vs prior 7 days">↓${Math.abs(delta)}%</span>`;
1539
+ return `<span class="wow-delta wow-flat" title="vs prior 7 days">→ flat</span>`;
1540
+ }
1541
+
1542
+ // ── feed search ────────────────────────────────────────────
1543
+ let feedSearchActive = false;
1544
+
1545
+ function toggleFeedSearch() {
1546
+ const bar = document.getElementById('feed-search');
1547
+ if (feedSearchActive) { closeFeedSearch(); return; }
1548
+ feedSearchActive = true;
1549
+ bar.classList.add('open');
1550
+ requestAnimationFrame(() => document.getElementById('feed-search-input').focus());
1551
+ }
1552
+
1553
+ function closeFeedSearch() {
1554
+ feedSearchActive = false;
1555
+ document.getElementById('feed-search').classList.remove('open');
1556
+ document.getElementById('feed-search-input').value = '';
1557
+ filterFeed('');
1558
+ }
1559
+
1560
+ function filterFeed(q) {
1561
+ const lq = q.toLowerCase().trim();
1562
+ let visible = 0;
1563
+
1564
+ // individual entries
1565
+ document.querySelectorAll('#feed-content .feed-entry').forEach(el => {
1566
+ const text = (el.querySelector('.feed-lbl')?.textContent || '') + ' ' +
1567
+ (el.querySelector('.feed-detail')?.textContent || '');
1568
+ const show = !lq || text.toLowerCase().includes(lq);
1569
+ el.style.display = show ? '' : 'none';
1570
+ if (show) visible++;
1571
+ });
1572
+
1573
+ // collapsed group rows
1574
+ document.querySelectorAll('#feed-content .feed-group').forEach(el => {
1575
+ const text = el.querySelector('.fg-label')?.textContent || '';
1576
+ const show = !lq || text.toLowerCase().includes(lq);
1577
+ el.style.display = show ? '' : 'none';
1578
+ if (show) visible++;
1579
+ });
1580
+
1581
+ const countEl = document.getElementById('feed-search-count');
1582
+ if (countEl) countEl.textContent = lq ? `${visible} match${visible !== 1 ? 'es' : ''}` : '';
1583
+ }
1584
+
1585
+ // ── copy session as markdown ───────────────────────────────
1586
+ async function copySession() {
1587
+ const btn = document.getElementById('btn-copy-sess');
1588
+ const sess = allSessions[sessionIdx];
1589
+ if (!sess?.file) return;
1590
+ try {
1591
+ const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
1592
+ const events = data.events || [];
1593
+ const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
1594
+ for (const ev of events) {
1595
+ if (ev.kind === 'user' && ev.text?.trim()) {
1596
+ lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
1597
+ } else if (ev.kind === 'tool') {
1598
+ lines.push(`- \`${ev.name || ev.cat}\`: ${ev.label || ''}`);
1599
+ }
1600
+ }
1601
+ await navigator.clipboard.writeText(lines.join('\n'));
1602
+ btn.textContent = '✓ Copied';
1603
+ btn.classList.add('copied');
1604
+ setTimeout(() => { btn.textContent = '⎘ Copy'; btn.classList.remove('copied'); }, 2000);
1605
+ } catch (err) {
1606
+ btn.textContent = '✕ Error';
1607
+ setTimeout(() => { btn.textContent = '⎘ Copy'; }, 1500);
1608
+ }
1609
+ }
1610
+
1611
+ // ── feed time filter ───────────────────────────────────────
1612
+ function setFeedTimeFilter(f) {
1613
+ feedTimeFilter = f;
1614
+ document.querySelectorAll('.tf-btn').forEach(b => b.classList.toggle('active', b.dataset.tf === f));
1615
+ if (!allSessions.length) return;
1616
+ const sess = allSessions[sessionIdx] || allSessions[0];
1617
+ if (sess) loadFeedForSession(sess);
1618
+ }
1619
+
1620
+ // ── command palette ────────────────────────────────────────
1621
+ function openCmdPalette() {
1622
+ document.getElementById('cmd-backdrop').classList.add('open');
1623
+ document.getElementById('cmd-palette').classList.add('open');
1624
+ const inp = document.getElementById('cmd-input');
1625
+ inp.value = '';
1626
+ cmdSearch('');
1627
+ requestAnimationFrame(() => inp.focus());
1628
+ }
1629
+
1630
+ function closeCmdPalette() {
1631
+ document.getElementById('cmd-backdrop').classList.remove('open');
1632
+ document.getElementById('cmd-palette').classList.remove('open');
1633
+ }
1634
+
1635
+ function cmdSearch(q) {
1636
+ cmdItems = [];
1637
+ const lq = q.toLowerCase().trim();
1638
+ const results = document.getElementById('cmd-results');
1639
+
1640
+ // ">" prefix = cross-session full-text search
1641
+ if (q.startsWith('>')) {
1642
+ const sq = q.slice(1).trim();
1643
+ results.innerHTML = sq.length >= 2
1644
+ ? '<div class="cmd-empty">Searching sessions…</div>'
1645
+ : '<div class="cmd-empty">Type at least 2 chars after &gt; to search all sessions</div>';
1646
+ if (sq.length >= 2) searchSessions(sq);
1647
+ return;
1648
+ }
1649
+
1650
+ const sessMatches = allSessions
1651
+ .filter(s => !lq || (s.lastPrompt || s.id).toLowerCase().includes(lq))
1652
+ .slice(0, 5);
1653
+
1654
+ const memMatches = allDrawers
1655
+ .filter(d => !lq || (d.key || '').toLowerCase().includes(lq) ||
1656
+ String(d.value || d.text || '').toLowerCase().includes(lq))
1657
+ .slice(0, 3);
1658
+
1659
+ const projMatches = allProjects
1660
+ .filter(p => !lq || (p.name || p.slug || '').toLowerCase().includes(lq) ||
1661
+ (p.path || '').toLowerCase().includes(lq))
1662
+ .slice(0, 3);
1663
+
1664
+ if (!sessMatches.length && !memMatches.length && !projMatches.length) {
1665
+ results.innerHTML = '<div class="cmd-empty">No results</div>';
1666
+ cmdItems = [];
1667
+ return;
1668
+ }
1669
+
1670
+ let html = '';
1671
+
1672
+ if (sessMatches.length) {
1673
+ html += '<div class="cmd-group-lbl">Sessions</div>';
1674
+ sessMatches.forEach(s => {
1675
+ const idx = cmdItems.length;
1676
+ cmdItems.push({ type: 'session', data: s });
1677
+ html += `<div class="cmd-item" data-ci="${idx}">
1678
+ <span class="ci-ico">◫</span>
1679
+ <div class="cmd-item-body">
1680
+ <div class="ci-title">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
1681
+ <div class="ci-sub">${relTime(s.lastTs || s.mtime)}</div>
1682
+ </div>
1683
+ </div>`;
1684
+ });
1685
+ }
1686
+
1687
+ if (memMatches.length) {
1688
+ html += '<div class="cmd-group-lbl">Memory</div>';
1689
+ memMatches.forEach(d => {
1690
+ const idx = cmdItems.length;
1691
+ cmdItems.push({ type: 'memory', data: d });
1692
+ html += `<div class="cmd-item" data-ci="${idx}">
1693
+ <span class="ci-ico">◈</span>
1694
+ <div class="cmd-item-body">
1695
+ <div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
1696
+ <div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
1697
+ </div>
1698
+ </div>`;
1699
+ });
1700
+ }
1701
+
1702
+ if (projMatches.length) {
1703
+ html += '<div class="cmd-group-lbl">Projects</div>';
1704
+ projMatches.forEach(p => {
1705
+ const idx = cmdItems.length;
1706
+ cmdItems.push({ type: 'project', data: p });
1707
+ html += `<div class="cmd-item" data-ci="${idx}">
1708
+ <span class="ci-ico">⊞</span>
1709
+ <div class="cmd-item-body">
1710
+ <div class="ci-title">${esc(p.name || p.slug)}</div>
1711
+ <div class="ci-sub">${esc(p.path || '')}</div>
1712
+ </div>
1713
+ </div>`;
1714
+ });
1715
+ }
1716
+
1717
+ results.innerHTML = html;
1718
+ cmdFocusIdx = 0;
1719
+ updateCmdFocus();
1720
+
1721
+ results.querySelectorAll('.cmd-item').forEach(el => {
1722
+ el.addEventListener('click', () => {
1723
+ cmdFocusIdx = parseInt(el.dataset.ci);
1724
+ executeCmdItem();
1725
+ });
1726
+ });
1727
+ }
1728
+
1729
+ function updateCmdFocus() {
1730
+ const items = document.querySelectorAll('#cmd-results .cmd-item');
1731
+ items.forEach((el, i) => el.classList.toggle('focused', i === cmdFocusIdx));
1732
+ }
1733
+
1734
+ function cmdKey(e) {
1735
+ const items = document.querySelectorAll('#cmd-results .cmd-item');
1736
+ if (e.key === 'ArrowDown') { e.preventDefault(); cmdFocusIdx = Math.min(cmdFocusIdx + 1, items.length - 1); updateCmdFocus(); }
1737
+ else if (e.key === 'ArrowUp') { e.preventDefault(); cmdFocusIdx = Math.max(cmdFocusIdx - 1, 0); updateCmdFocus(); }
1738
+ else if (e.key === 'Enter') { e.preventDefault(); executeCmdItem(); }
1739
+ else if (e.key === 'Escape') { closeCmdPalette(); }
1740
+ }
1741
+
1742
+ function executeCmdItem() {
1743
+ const item = cmdItems[cmdFocusIdx];
1744
+ if (!item) return;
1745
+ closeCmdPalette();
1746
+ if (item.type === 'session') jumpToSession(item.data.id);
1747
+ else if (item.type === 'memory') switchView('memory');
1748
+ else if (item.type === 'project') switchProject(item.data.path);
1749
+ }
1750
+
1751
+ // ── cross-session search ───────────────────────────────────
1752
+ async function searchSessions(q) {
1753
+ const resultsEl = document.getElementById('cmd-results');
1754
+ try {
1755
+ const data = await apiFetch('/api/search-sessions?dir=' + enc(DIR) + '&q=' + enc(q));
1756
+ if (!data.results?.length) {
1757
+ resultsEl.innerHTML = '<div class="cmd-empty">No matches found across sessions</div>';
1758
+ return;
1759
+ }
1760
+ cmdItems = [];
1761
+ let html = '<div class="cmd-group-lbl">Matches across sessions</div>';
1762
+ data.results.forEach(r => {
1763
+ const idx = cmdItems.length;
1764
+ cmdItems.push({ type: 'session', data: { id: r.id, lastPrompt: r.lastPrompt, lastTs: r.mtime } });
1765
+ const snippet = r.matches[0]?.text?.replace(/\s+/g, ' ').trim() || '';
1766
+ html += `<div class="cmd-item" data-ci="${idx}">
1767
+ <span class="ci-ico">◫</span>
1768
+ <div class="cmd-item-body">
1769
+ <div class="ci-title">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
1770
+ <div class="ci-sub">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
1771
+ </div>
1772
+ </div>`;
1773
+ });
1774
+ resultsEl.innerHTML = html;
1775
+ cmdFocusIdx = 0;
1776
+ updateCmdFocus();
1777
+ resultsEl.querySelectorAll('.cmd-item').forEach(el => {
1778
+ el.addEventListener('click', () => { cmdFocusIdx = parseInt(el.dataset.ci); executeCmdItem(); });
1779
+ });
1780
+ } catch (_) {
1781
+ resultsEl.innerHTML = '<div class="cmd-empty">Search error — is the server running?</div>';
1782
+ }
1783
+ }
1784
+
1785
+ // ── keyboard shortcuts ─────────────────────────────────────
1786
+ document.addEventListener('keydown', e => {
1787
+ // ⌘K / Ctrl+K — command palette
1788
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
1789
+ e.preventDefault();
1790
+ const open = document.getElementById('cmd-palette').classList.contains('open');
1791
+ if (open) closeCmdPalette(); else openCmdPalette();
1792
+ return;
1793
+ }
1794
+
1795
+ // ignore when typing in inputs
1796
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1797
+ if (document.getElementById('cmd-palette').classList.contains('open')) return;
1798
+
1799
+ if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); }
1800
+
1801
+ if (currentView === 'now') {
1802
+ if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
1803
+ if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
1804
+ if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
1805
+
1806
+ if (e.key === 'j' || e.key === 'k') {
1807
+ e.preventDefault();
1808
+ const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
1809
+ if (!entries.length) return;
1810
+ let cur = entries.findIndex(el => el.classList.contains('selected'));
1811
+ if (e.key === 'j') cur = cur < 0 ? 0 : Math.min(cur + 1, entries.length - 1);
1812
+ else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
1813
+ entries.forEach((el, i) => el.classList.toggle('selected', i === cur));
1814
+ entries[cur].scrollIntoView({ block: 'nearest' });
1815
+ selectedEntryId = entries[cur].dataset.ev
1816
+ ? (JSON.parse(entries[cur].dataset.ev).id || '')
1817
+ : '';
1818
+ }
1819
+
1820
+ if (e.key === 'Enter') {
1821
+ const sel = document.querySelector('#feed-content .feed-entry.selected');
1822
+ if (sel) openDetail(sel.dataset.ev);
1823
+ }
1824
+ }
1825
+ });
1826
+
1827
+ // ── helpers ────────────────────────────────────────────────
1828
+ function enc(s) { return encodeURIComponent(s); }
1829
+ function esc(s) {
1830
+ return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
1831
+ }
1832
+
1833
+ function relTime(ts) {
1834
+ if (!ts) return '';
1835
+ const diff = Date.now() - (typeof ts === 'number' ? ts : new Date(ts).getTime());
1836
+ const s = Math.floor(diff / 1000);
1837
+ if (s < 5) return 'now';
1838
+ if (s < 60) return s + 's';
1839
+ const m = Math.floor(s / 60);
1840
+ if (m < 60) return m + 'm';
1841
+ const h = Math.floor(m / 60);
1842
+ if (h < 24) return h + 'h';
1843
+ return Math.floor(h / 24) + 'd';
1844
+ }
1845
+
1846
+ function fmtDur(ms) {
1847
+ const s = Math.floor(ms / 1000);
1848
+ if (s < 60) return s + 's';
1849
+ const m = Math.floor(s / 60);
1850
+ if (m < 60) return m + 'm';
1851
+ return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
1852
+ }
1853
+
1854
+ init();
1855
+ </script>
1856
+ </body>
1857
+ </html>