@monoes/monomindcli 1.10.28 → 1.10.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/helpers/auto-memory-hook.mjs +39 -4
- package/.claude/helpers/handlers/edit-handler.cjs +145 -0
- package/.claude/helpers/handlers/route-handler.cjs +393 -0
- package/.claude/helpers/handlers/session-handler.cjs +167 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +343 -0
- package/.claude/helpers/handlers/task-handler.cjs +329 -0
- package/.claude/helpers/hook-handler.cjs +114 -2247
- package/.claude/helpers/intelligence.cjs +21 -2
- package/.claude/helpers/learning-service.mjs +166 -8
- package/.claude/helpers/memory-palace.cjs +72 -12
- package/.claude/helpers/router.cjs +79 -5
- package/.claude/helpers/statusline.cjs +193 -399
- package/.claude/helpers/utils/micro-agents.cjs +338 -0
- package/.claude/helpers/utils/monograph.cjs +349 -0
- package/.claude/helpers/utils/telemetry.cjs +144 -0
- package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
- package/.claude/skills/monomind/browse-agentcore.md +116 -0
- package/.claude/skills/monomind/browse-electron.md +189 -0
- package/.claude/skills/monomind/browse-qa.md +229 -0
- package/.claude/skills/monomind/browse-references/authentication.md +162 -0
- package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
- package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
- package/.claude/skills/monomind/browse-slack.md +189 -0
- package/.claude/skills/monomind/browse-vercel.md +240 -0
- package/.claude/skills/monomind/browse.md +724 -0
- package/dist/src/browser/actions.d.ts +13 -0
- package/dist/src/browser/actions.d.ts.map +1 -0
- package/dist/src/browser/actions.js +201 -0
- package/dist/src/browser/actions.js.map +1 -0
- package/dist/src/browser/browser.d.ts +14 -0
- package/dist/src/browser/browser.d.ts.map +1 -0
- package/dist/src/browser/browser.js +198 -0
- package/dist/src/browser/browser.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +17 -0
- package/dist/src/browser/cdp.d.ts.map +1 -0
- package/dist/src/browser/cdp.js +106 -0
- package/dist/src/browser/cdp.js.map +1 -0
- package/dist/src/browser/index.d.ts +11 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +11 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/network.d.ts +11 -0
- package/dist/src/browser/network.d.ts.map +1 -0
- package/dist/src/browser/network.js +81 -0
- package/dist/src/browser/network.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +15 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -0
- package/dist/src/browser/screenshot.js +36 -0
- package/dist/src/browser/screenshot.js.map +1 -0
- package/dist/src/browser/session.d.ts +8 -0
- package/dist/src/browser/session.d.ts.map +1 -0
- package/dist/src/browser/session.js +50 -0
- package/dist/src/browser/session.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts +12 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -0
- package/dist/src/browser/snapshot.js +147 -0
- package/dist/src/browser/snapshot.js.map +1 -0
- package/dist/src/browser/tabs.d.ts +8 -0
- package/dist/src/browser/tabs.d.ts.map +1 -0
- package/dist/src/browser/tabs.js +25 -0
- package/dist/src/browser/tabs.js.map +1 -0
- package/dist/src/browser/types.d.ts +109 -0
- package/dist/src/browser/types.d.ts.map +1 -0
- package/dist/src/browser/types.js +16 -0
- package/dist/src/browser/types.js.map +1 -0
- package/dist/src/browser/wait.d.ts +4 -0
- package/dist/src/browser/wait.d.ts.map +1 -0
- package/dist/src/browser/wait.js +122 -0
- package/dist/src/browser/wait.js.map +1 -0
- package/dist/src/commands/browse.d.ts +8 -0
- package/dist/src/commands/browse.d.ts.map +1 -0
- package/dist/src/commands/browse.js +573 -0
- package/dist/src/commands/browse.js.map +1 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +25 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +27 -0
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/ui/dashboard-v2.html +1692 -0
- package/dist/src/ui/server.mjs +15 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/scripts/understand-analyze.mjs +14 -1
|
@@ -0,0 +1,1692 @@
|
|
|
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
|
+
</style>
|
|
363
|
+
</head>
|
|
364
|
+
<body>
|
|
365
|
+
<div id="app">
|
|
366
|
+
|
|
367
|
+
<!-- ── Sidebar ─────────────────────────────────────────── -->
|
|
368
|
+
<nav id="sidebar">
|
|
369
|
+
<div id="sb-logo">
|
|
370
|
+
<div class="mark">monomind</div>
|
|
371
|
+
<div class="proj" id="sb-proj">—</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div id="sb-nav">
|
|
374
|
+
<div class="nav-sect">
|
|
375
|
+
<div class="nav-item active" data-view="now">
|
|
376
|
+
<span class="ico">◉</span><span class="lbl">Now</span>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="nav-sect">
|
|
380
|
+
<div class="nav-lbl">Workspace</div>
|
|
381
|
+
<div class="nav-item" data-view="projects">
|
|
382
|
+
<span class="ico">⊞</span><span class="lbl">Projects</span>
|
|
383
|
+
<span class="bdg" id="bdg-projects">—</span>
|
|
384
|
+
</div>
|
|
385
|
+
<div class="nav-item" data-view="sessions">
|
|
386
|
+
<span class="ico">◫</span><span class="lbl">Sessions</span>
|
|
387
|
+
<span class="bdg" id="bdg-sessions">—</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div class="nav-item" data-view="loops">
|
|
390
|
+
<span class="ico">↺</span><span class="lbl">Loops</span>
|
|
391
|
+
<span class="bdg" id="bdg-loops">—</span>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="nav-sect">
|
|
395
|
+
<div class="nav-lbl">Intelligence</div>
|
|
396
|
+
<div class="nav-item" data-view="memory">
|
|
397
|
+
<span class="ico">◈</span><span class="lbl">Memory</span>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="nav-item" data-view="orgs">
|
|
400
|
+
<span class="ico">⬡</span><span class="lbl">Orgs</span>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
<div id="sb-footer">
|
|
405
|
+
<div id="sb-user">—</div>
|
|
406
|
+
<div id="sb-path">—</div>
|
|
407
|
+
</div>
|
|
408
|
+
</nav>
|
|
409
|
+
|
|
410
|
+
<!-- ── Main ────────────────────────────────────────────── -->
|
|
411
|
+
<div id="main">
|
|
412
|
+
<div id="topbar">
|
|
413
|
+
<span id="view-title">Now</span>
|
|
414
|
+
<span class="pill"><span class="live-dot"></span> live</span>
|
|
415
|
+
<span id="topbar-cost"></span>
|
|
416
|
+
<div id="tb-right">
|
|
417
|
+
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
418
|
+
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div id="alerts-rail"></div>
|
|
423
|
+
|
|
424
|
+
<!-- command palette -->
|
|
425
|
+
<div id="cmd-backdrop" onclick="closeCmdPalette()"></div>
|
|
426
|
+
<div id="cmd-palette" role="dialog" aria-label="Command palette">
|
|
427
|
+
<div id="cmd-input-wrap">
|
|
428
|
+
<span id="cmd-ico">⌕</span>
|
|
429
|
+
<input id="cmd-input" type="text" placeholder="Search sessions, memory, projects…" oninput="cmdSearch(this.value)" onkeydown="cmdKey(event)" autocomplete="off" spellcheck="false">
|
|
430
|
+
</div>
|
|
431
|
+
<div id="cmd-results"></div>
|
|
432
|
+
<div class="cmd-footer">
|
|
433
|
+
<span class="cmd-key"><kbd>↑↓</kbd> navigate</span>
|
|
434
|
+
<span class="cmd-key"><kbd>↵</kbd> select</span>
|
|
435
|
+
<span class="cmd-key"><kbd>esc</kbd> close</span>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div id="view-wrap">
|
|
440
|
+
|
|
441
|
+
<!-- NOW -->
|
|
442
|
+
<div class="view active" id="view-now">
|
|
443
|
+
<div id="feed-pane">
|
|
444
|
+
<div id="feed-head">
|
|
445
|
+
<h2>Live Feed</h2>
|
|
446
|
+
<span id="feed-sess">—</span>
|
|
447
|
+
<div id="feed-sess-nav">
|
|
448
|
+
<button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
|
|
449
|
+
<button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
|
|
450
|
+
<button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
|
|
451
|
+
<button class="sess-btn" id="btn-prev-sess" onclick="prevSession()" title="Older session">‹</button>
|
|
452
|
+
<button class="sess-btn" id="btn-next-sess" onclick="nextSession()" title="Newer session">›</button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div id="feed-search">
|
|
456
|
+
<input id="feed-search-input" type="text" placeholder="Search feed…" oninput="filterFeed(this.value)" onkeydown="if(event.key==='Escape')closeFeedSearch()">
|
|
457
|
+
<span id="feed-search-count"></span>
|
|
458
|
+
<button id="feed-search-close" onclick="closeFeedSearch()">✕</button>
|
|
459
|
+
</div>
|
|
460
|
+
<div id="sess-ctx">
|
|
461
|
+
<button class="sctx-back" onclick="switchView('sessions')">← Sessions</button>
|
|
462
|
+
<span class="sctx-sep">/</span>
|
|
463
|
+
<span class="sctx-label" id="sctx-label"></span>
|
|
464
|
+
<button class="sctx-live" onclick="goLive()">⬤ Go live</button>
|
|
465
|
+
</div>
|
|
466
|
+
<div id="feed-timeline" title="Session tool activity timeline"></div>
|
|
467
|
+
<div id="feed-time-filter">
|
|
468
|
+
<span class="tf-lbl">Range</span>
|
|
469
|
+
<button class="tf-btn active" data-tf="all" onclick="setFeedTimeFilter('all')">All</button>
|
|
470
|
+
<button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
|
|
471
|
+
<button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
|
|
472
|
+
<button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
|
|
473
|
+
<span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate <kbd>↵</kbd> detail <kbd>/</kbd> find <kbd>G</kbd> live <kbd>⌘K</kbd> search</span>
|
|
474
|
+
</div>
|
|
475
|
+
<div id="feed-scroll">
|
|
476
|
+
<div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<!-- detail panel (slides in over feed) -->
|
|
481
|
+
<div id="detail-panel">
|
|
482
|
+
<div id="detail-head">
|
|
483
|
+
<h3 id="detail-title">Detail</h3>
|
|
484
|
+
<button id="detail-close" onclick="closeDetail()">✕</button>
|
|
485
|
+
</div>
|
|
486
|
+
<div id="detail-body"></div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div id="metrics-pane">
|
|
490
|
+
<div id="m-today">
|
|
491
|
+
<div class="m-group-title">Today</div>
|
|
492
|
+
<div class="loading-txt">Loading…</div>
|
|
493
|
+
</div>
|
|
494
|
+
<div id="m-loops">
|
|
495
|
+
<div class="m-group-title">Active Loops</div>
|
|
496
|
+
<div class="loading-txt">Loading…</div>
|
|
497
|
+
</div>
|
|
498
|
+
<div id="m-sessions">
|
|
499
|
+
<div class="m-group-title">Recent Sessions</div>
|
|
500
|
+
<div class="loading-txt">Loading…</div>
|
|
501
|
+
</div>
|
|
502
|
+
<div id="m-breakdown">
|
|
503
|
+
<div class="m-group-title">Tool Usage</div>
|
|
504
|
+
<div class="loading-txt">—</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
<!-- PROJECTS -->
|
|
510
|
+
<div class="view" id="view-projects">
|
|
511
|
+
<div class="vscroll">
|
|
512
|
+
<div class="pg-title">Projects</div>
|
|
513
|
+
<div class="pg-sub" id="proj-pg-sub">All monomind-enabled Claude Code projects</div>
|
|
514
|
+
<div class="filter-bar">
|
|
515
|
+
<input class="filter-input" id="proj-filter" type="text" placeholder="Filter projects…" oninput="filterProjects(this.value)">
|
|
516
|
+
</div>
|
|
517
|
+
<div id="proj-content" class="proj-grid"><div class="loading-txt">Loading…</div></div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<!-- SESSIONS -->
|
|
522
|
+
<div class="view" id="view-sessions">
|
|
523
|
+
<div class="vscroll">
|
|
524
|
+
<div class="pg-title">Sessions</div>
|
|
525
|
+
<div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
|
|
526
|
+
<div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<!-- LOOPS -->
|
|
531
|
+
<div class="view" id="view-loops">
|
|
532
|
+
<div class="vscroll">
|
|
533
|
+
<div class="pg-title">Loops</div>
|
|
534
|
+
<div class="pg-sub">Scheduled automation loops</div>
|
|
535
|
+
<div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- MEMORY -->
|
|
540
|
+
<div class="view" id="view-memory">
|
|
541
|
+
<div class="vscroll">
|
|
542
|
+
<div class="pg-title">Memory</div>
|
|
543
|
+
<div class="pg-sub">Knowledge palace — stored facts, graph, identity</div>
|
|
544
|
+
<div class="filter-bar">
|
|
545
|
+
<input class="filter-input" id="mem-filter" type="text" placeholder="Search memory…" oninput="filterMemory(this.value)">
|
|
546
|
+
</div>
|
|
547
|
+
<div id="mem-ns-tabs"></div>
|
|
548
|
+
<div id="mem-content"><div class="loading-txt">Loading…</div></div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<!-- ORGS -->
|
|
553
|
+
<div class="view" id="view-orgs">
|
|
554
|
+
<div class="vscroll">
|
|
555
|
+
<div class="pg-title">Orgs</div>
|
|
556
|
+
<div class="pg-sub">MASTERMIND organizations and swarms</div>
|
|
557
|
+
<div id="orgs-content"><div class="loading-txt">Loading…</div></div>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
</div><!-- /view-wrap -->
|
|
562
|
+
</div><!-- /main -->
|
|
563
|
+
</div><!-- /app -->
|
|
564
|
+
|
|
565
|
+
<script>
|
|
566
|
+
// ── state ──────────────────────────────────────────────────
|
|
567
|
+
let DIR = '';
|
|
568
|
+
let ORIGINAL_DIR = '';
|
|
569
|
+
let gitUser = {};
|
|
570
|
+
let currentView = 'now';
|
|
571
|
+
let allSessions = [];
|
|
572
|
+
let allProjects = [];
|
|
573
|
+
let sessionIdx = 0;
|
|
574
|
+
let pollTimer = null;
|
|
575
|
+
let viewRendered = {};
|
|
576
|
+
let userScrolled = false;
|
|
577
|
+
let selectedEntryId = null;
|
|
578
|
+
let allDrawers = [];
|
|
579
|
+
let dismissedAlerts = new Set();
|
|
580
|
+
let alertState = { todayCost: 0, errorCount: 0, longLoops: [] };
|
|
581
|
+
let feedTimeFilter = 'all';
|
|
582
|
+
let cmdFocusIdx = 0;
|
|
583
|
+
let cmdItems = [];
|
|
584
|
+
|
|
585
|
+
// ── nav ────────────────────────────────────────────────────
|
|
586
|
+
document.querySelectorAll('.nav-item[data-view]').forEach(el => {
|
|
587
|
+
el.addEventListener('click', () => switchView(el.dataset.view));
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
document.getElementById('feed-scroll').addEventListener('scroll', () => {
|
|
591
|
+
userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
function switchView(v) {
|
|
595
|
+
currentView = v;
|
|
596
|
+
document.querySelectorAll('.nav-item[data-view]').forEach(el =>
|
|
597
|
+
el.classList.toggle('active', el.dataset.view === v));
|
|
598
|
+
document.querySelectorAll('.view').forEach(el =>
|
|
599
|
+
el.classList.toggle('active', el.id === 'view-' + v));
|
|
600
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs' };
|
|
601
|
+
document.getElementById('view-title').textContent = titles[v] || v;
|
|
602
|
+
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function renderView(v) {
|
|
606
|
+
if (v === 'now') { refreshNow(); return; }
|
|
607
|
+
if (v === 'projects') renderProjects();
|
|
608
|
+
if (v === 'sessions') renderSessions();
|
|
609
|
+
if (v === 'loops') renderLoops();
|
|
610
|
+
if (v === 'memory') renderMemory();
|
|
611
|
+
if (v === 'orgs') renderOrgs();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function refreshCurrent() {
|
|
615
|
+
viewRendered[currentView] = false;
|
|
616
|
+
renderView(currentView);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ── init ───────────────────────────────────────────────────
|
|
620
|
+
async function init() {
|
|
621
|
+
try {
|
|
622
|
+
const gu = await apiFetch('/api/git-user');
|
|
623
|
+
DIR = gu.cwd || '';
|
|
624
|
+
ORIGINAL_DIR = DIR;
|
|
625
|
+
gitUser = gu;
|
|
626
|
+
document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
|
|
627
|
+
document.getElementById('sb-path').textContent = DIR;
|
|
628
|
+
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
629
|
+
} catch (_) {}
|
|
630
|
+
viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
|
|
631
|
+
await refreshNow();
|
|
632
|
+
startPolling();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function startPolling() {
|
|
636
|
+
clearInterval(pollTimer);
|
|
637
|
+
pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function apiFetch(url) {
|
|
641
|
+
const r = await fetch(url);
|
|
642
|
+
if (!r.ok) throw new Error(r.status);
|
|
643
|
+
return r.json();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// ── project switching ──────────────────────────────────────
|
|
647
|
+
function switchProject(path) {
|
|
648
|
+
if (DIR === path) { switchView('sessions'); return; }
|
|
649
|
+
DIR = path;
|
|
650
|
+
document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
|
|
651
|
+
document.getElementById('sb-path').textContent = path;
|
|
652
|
+
viewRendered = {};
|
|
653
|
+
allSessions = [];
|
|
654
|
+
closeDetail();
|
|
655
|
+
switchView('sessions');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── NOW view ───────────────────────────────────────────────
|
|
659
|
+
async function refreshNow() {
|
|
660
|
+
userScrolled = false;
|
|
661
|
+
await Promise.allSettled([loadFeed(), loadMetrics()]);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function refreshNowSilent() {
|
|
665
|
+
await Promise.allSettled([loadFeedSilent(), loadMetrics()]);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// session nav
|
|
669
|
+
function prevSession() {
|
|
670
|
+
if (sessionIdx < allSessions.length - 1) { sessionIdx++; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
|
|
671
|
+
}
|
|
672
|
+
function nextSession() {
|
|
673
|
+
if (sessionIdx > 0) { sessionIdx--; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function loadFeed() {
|
|
677
|
+
if (!DIR) return;
|
|
678
|
+
try {
|
|
679
|
+
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
680
|
+
allSessions = sessions;
|
|
681
|
+
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
682
|
+
if (!sessions.length) {
|
|
683
|
+
setFeedContent('<div class="feed-empty">No sessions yet in this project.</div>');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
sessionIdx = 0;
|
|
687
|
+
await loadFeedForSession(sessions[0]);
|
|
688
|
+
renderMiniSessions(sessions.slice(0, 6));
|
|
689
|
+
// patch sparkline now that allSessions is populated
|
|
690
|
+
const todayEl = document.getElementById('m-today');
|
|
691
|
+
const sparkWrap = todayEl?.querySelector('.spark-wrap');
|
|
692
|
+
if (sparkWrap) sparkWrap.outerHTML = buildSparkline();
|
|
693
|
+
} catch (err) {
|
|
694
|
+
setFeedContent('<div class="feed-empty">Could not load feed: ' + esc(err.message) + '</div>');
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function loadFeedSilent() {
|
|
699
|
+
if (!DIR || !allSessions.length) return;
|
|
700
|
+
try {
|
|
701
|
+
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
702
|
+
allSessions = sessions;
|
|
703
|
+
if (!sessions.length) return;
|
|
704
|
+
const currentSess = allSessions[sessionIdx] || sessions[0];
|
|
705
|
+
if (!currentSess?.file) return;
|
|
706
|
+
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(currentSess.file) + '&limit=120');
|
|
707
|
+
renderFeedEvents(data.events || [], true);
|
|
708
|
+
} catch (_) {}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function loadFeedForSession(sess) {
|
|
712
|
+
if (!sess) return;
|
|
713
|
+
document.getElementById('feed-sess').textContent = sess.id.slice(0, 8) + '…';
|
|
714
|
+
document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
|
|
715
|
+
document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
|
|
716
|
+
showSessCtx(sess);
|
|
717
|
+
if (!sess.file) { setFeedContent('<div class="feed-empty">Session file path unavailable.</div>'); return; }
|
|
718
|
+
try {
|
|
719
|
+
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=120');
|
|
720
|
+
renderFeedEvents(data.events || [], false);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
setFeedContent('<div class="feed-empty">Could not load session: ' + esc(err.message) + '</div>');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── feed rendering ─────────────────────────────────────────
|
|
727
|
+
|
|
728
|
+
// pre-pass: mark tool events as errored based on their tool_result
|
|
729
|
+
function annotateErrors(events) {
|
|
730
|
+
const byId = new Map();
|
|
731
|
+
events.forEach((ev, i) => { if (ev.kind === 'tool' && ev.id) byId.set(ev.id, i); });
|
|
732
|
+
events.forEach(ev => {
|
|
733
|
+
if (ev.kind === 'tool_result' && ev.isError && ev.tool_use_id) {
|
|
734
|
+
const idx = byId.get(ev.tool_use_id);
|
|
735
|
+
if (idx != null) events[idx]._errored = true;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// group consecutive same-cat tool events (threshold: 3+)
|
|
741
|
+
function groupEvents(events) {
|
|
742
|
+
const out = [];
|
|
743
|
+
let i = 0;
|
|
744
|
+
while (i < events.length) {
|
|
745
|
+
const ev = events[i];
|
|
746
|
+
if (ev.kind !== 'tool') { out.push(ev); i++; continue; }
|
|
747
|
+
// look ahead for same cat run
|
|
748
|
+
let j = i + 1;
|
|
749
|
+
while (j < events.length && events[j].kind === 'tool' && events[j].cat === ev.cat && !events[j]._errored && !ev._errored) j++;
|
|
750
|
+
const run = j - i;
|
|
751
|
+
if (run >= 3) {
|
|
752
|
+
out.push({ kind: '_group', cat: ev.cat, count: run, label: catLabel(ev.cat), ts: ev.ts, items: events.slice(i, j) });
|
|
753
|
+
i = j;
|
|
754
|
+
} else {
|
|
755
|
+
out.push(ev); i++;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function catLabel(c) {
|
|
762
|
+
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' };
|
|
763
|
+
return m[c] || 'tool calls';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function renderFeedEvents(events, silent) {
|
|
767
|
+
if (!events.length) {
|
|
768
|
+
if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.</div>');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
annotateErrors(events);
|
|
773
|
+
|
|
774
|
+
// filter: only tool + user, skip text/thinking/tool_result and hook system messages
|
|
775
|
+
const HOOK_RE = /^<(local-command-|command-name>|command-message>|local-command-caveat>)/;
|
|
776
|
+
const filtered = events.filter(ev =>
|
|
777
|
+
ev.kind === 'tool' ||
|
|
778
|
+
(ev.kind === 'user' && ev.text?.trim() && !HOOK_RE.test(ev.text.trim())));
|
|
779
|
+
|
|
780
|
+
// apply time-range filter
|
|
781
|
+
let visible = filtered;
|
|
782
|
+
if (feedTimeFilter !== 'all') {
|
|
783
|
+
const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000 }[feedTimeFilter] || 0;
|
|
784
|
+
const cutoff = Date.now() - ms;
|
|
785
|
+
visible = filtered.filter(ev => !ev.ts || new Date(ev.ts).getTime() >= cutoff);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// update error alert state
|
|
789
|
+
alertState.errorCount = visible.filter(ev => ev._errored).length;
|
|
790
|
+
updateAlerts();
|
|
791
|
+
|
|
792
|
+
// reverse (newest first), then group
|
|
793
|
+
const reversed = [...visible].reverse();
|
|
794
|
+
const grouped = groupEvents(reversed);
|
|
795
|
+
|
|
796
|
+
const parts = [];
|
|
797
|
+
let prevKind = null;
|
|
798
|
+
|
|
799
|
+
for (const item of grouped) {
|
|
800
|
+
if (item.kind === '_group') {
|
|
801
|
+
parts.push(renderGroupRow(item));
|
|
802
|
+
prevKind = 'tool';
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (prevKind === 'user' && item.kind === 'tool') {
|
|
806
|
+
parts.push('<div class="feed-divider"></div>');
|
|
807
|
+
} else if (prevKind === 'tool' && item.kind === 'user') {
|
|
808
|
+
parts.push('<div class="feed-divider"></div>');
|
|
809
|
+
}
|
|
810
|
+
parts.push(renderFeedEntry(item));
|
|
811
|
+
prevKind = item.kind;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const newHtml = parts.join('');
|
|
815
|
+
if (silent) {
|
|
816
|
+
const el = document.getElementById('feed-content');
|
|
817
|
+
if (el.innerHTML === newHtml) return;
|
|
818
|
+
const wasAtTop = !userScrolled;
|
|
819
|
+
el.innerHTML = newHtml;
|
|
820
|
+
if (wasAtTop) document.getElementById('feed-scroll').scrollTop = 0;
|
|
821
|
+
} else {
|
|
822
|
+
setFeedContent(newHtml);
|
|
823
|
+
if (!userScrolled) document.getElementById('feed-scroll').scrollTop = 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// update timeline + breakdown with all original events (before time-filter)
|
|
827
|
+
buildTimeline(filtered);
|
|
828
|
+
buildBreakdown(filtered);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function renderGroupRow(g) {
|
|
832
|
+
const { ico, catCls } = toolStyle(g.cat, '');
|
|
833
|
+
const itemsData = JSON.stringify(g.items).replace(/'/g, ''');
|
|
834
|
+
return `<div class="feed-group" data-items='${itemsData}' onclick="expandGroup(this)">
|
|
835
|
+
<div class="feed-ico ${catCls}" style="font-size:9px">${ico}</div>
|
|
836
|
+
<span class="fg-label">${g.count} ${esc(g.label)}</span>
|
|
837
|
+
<span class="fg-expand">▸ expand</span>
|
|
838
|
+
</div>`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function expandGroup(el) {
|
|
842
|
+
const items = JSON.parse(el.dataset.items);
|
|
843
|
+
const html = items.map(renderFeedEntry).join('');
|
|
844
|
+
el.outerHTML = html;
|
|
845
|
+
// re-apply active feed search to newly injected entries
|
|
846
|
+
const q = document.getElementById('feed-search-input')?.value || '';
|
|
847
|
+
if (feedSearchActive && q) filterFeed(q);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function renderFeedEntry(ev) {
|
|
851
|
+
const ts = ev.ts ? relTime(ev.ts) : '';
|
|
852
|
+
let lbl = '', detail = '', id = ev.id || ev.uuid || '';
|
|
853
|
+
let catCls, ico;
|
|
854
|
+
|
|
855
|
+
if (ev.kind === 'tool') {
|
|
856
|
+
({ ico, catCls } = toolStyle(ev.cat, ev.name));
|
|
857
|
+
lbl = esc(ev.label || ev.name || 'tool');
|
|
858
|
+
if (ev.subagent) detail = esc(ev.subagent);
|
|
859
|
+
} else {
|
|
860
|
+
ico = '↵'; catCls = 'cat-user';
|
|
861
|
+
const t = (ev.text || '').trim();
|
|
862
|
+
lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const errClass = ev._errored ? ' errored' : '';
|
|
866
|
+
const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
|
|
867
|
+
|
|
868
|
+
const evData = JSON.stringify(ev).replace(/'/g, ''');
|
|
869
|
+
return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
|
|
870
|
+
<div class="feed-ico ${catCls}">${ico}</div>
|
|
871
|
+
<div class="feed-body">
|
|
872
|
+
<div class="feed-lbl">${lbl}</div>
|
|
873
|
+
${detail ? `<div class="feed-detail">${detail}</div>` : ''}
|
|
874
|
+
</div>
|
|
875
|
+
<div class="feed-ts">${ts}</div>
|
|
876
|
+
</div>`;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function toolStyle(cat, name) {
|
|
880
|
+
const map = {
|
|
881
|
+
file: { ico: '◧', catCls: 'cat-file' },
|
|
882
|
+
bash: { ico: '$', catCls: 'cat-bash' },
|
|
883
|
+
agent: { ico: '→', catCls: 'cat-agent' },
|
|
884
|
+
mcp: { ico: '⬡', catCls: 'cat-mcp' },
|
|
885
|
+
search: { ico: '/', catCls: 'cat-search' },
|
|
886
|
+
skill: { ico: '◆', catCls: 'cat-skill' },
|
|
887
|
+
task: { ico: '☑', catCls: 'cat-task' },
|
|
888
|
+
mem: { ico: '◈', catCls: 'cat-mem' },
|
|
889
|
+
};
|
|
890
|
+
if (name === 'Skill') return { ico: '◆', catCls: 'cat-skill' };
|
|
891
|
+
return map[cat] || { ico: '·', catCls: 'cat-other' };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function setFeedContent(html) {
|
|
895
|
+
document.getElementById('feed-content').innerHTML = html;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ── detail panel ───────────────────────────────────────────
|
|
899
|
+
function openDetail(evJson) {
|
|
900
|
+
const ev = JSON.parse(evJson);
|
|
901
|
+
selectedEntryId = ev.id || ev.uuid || '';
|
|
902
|
+
|
|
903
|
+
const panel = document.getElementById('detail-panel');
|
|
904
|
+
panel.classList.add('open');
|
|
905
|
+
|
|
906
|
+
let title = '';
|
|
907
|
+
let bodyHtml = '';
|
|
908
|
+
|
|
909
|
+
if (ev.kind === 'tool') {
|
|
910
|
+
const { catCls } = toolStyle(ev.cat, ev.name);
|
|
911
|
+
title = ev.name || 'Tool';
|
|
912
|
+
bodyHtml = `
|
|
913
|
+
<div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
|
|
914
|
+
<div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(ev.label || ev.name)}</div></div>
|
|
915
|
+
${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
|
|
916
|
+
${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
|
|
917
|
+
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
|
|
918
|
+
<div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((ev.id || '').slice(0, 24))}</div></div>
|
|
919
|
+
`;
|
|
920
|
+
} else if (ev.kind === 'user') {
|
|
921
|
+
title = 'User message';
|
|
922
|
+
bodyHtml = `
|
|
923
|
+
<div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
|
|
924
|
+
<div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(ev.text || '')}</div></div>
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
document.getElementById('detail-title').textContent = title;
|
|
929
|
+
document.getElementById('detail-body').innerHTML = bodyHtml;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function closeDetail() {
|
|
933
|
+
document.getElementById('detail-panel').classList.remove('open');
|
|
934
|
+
selectedEntryId = null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ── sparkline ──────────────────────────────────────────────
|
|
938
|
+
function buildSparkline() {
|
|
939
|
+
// bucket allSessions into 7 days (0 = 6 days ago, 6 = today)
|
|
940
|
+
const DAY = 86400000;
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
const buckets = [0, 0, 0, 0, 0, 0, 0];
|
|
943
|
+
for (const s of allSessions) {
|
|
944
|
+
const ts = s.lastTs || s.mtime;
|
|
945
|
+
if (!ts) continue;
|
|
946
|
+
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
947
|
+
const dayIdx = 6 - Math.floor(age / DAY);
|
|
948
|
+
if (dayIdx >= 0 && dayIdx < 7) buckets[dayIdx]++;
|
|
949
|
+
}
|
|
950
|
+
const max = Math.max(...buckets, 1);
|
|
951
|
+
const days = ['6d','5d','4d','3d','2d','1d','now'];
|
|
952
|
+
const bars = buckets.map((v, i) => {
|
|
953
|
+
const pct = Math.round((v / max) * 100);
|
|
954
|
+
const isToday = i === 6;
|
|
955
|
+
const title = `${days[i]}: ${v} session${v !== 1 ? 's' : ''}`;
|
|
956
|
+
return `<div class="spark-bar${isToday ? ' spark-today' : ''}" style="height:${Math.max(pct, 8)}%" title="${title}"></div>`;
|
|
957
|
+
}).join('');
|
|
958
|
+
return `<div class="spark-wrap"><div class="spark-lbl">7-day activity ${buildWowDelta()}</div><div class="sparkline">${bars}</div></div>`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// ── alerts rail ────────────────────────────────────────────
|
|
962
|
+
function updateAlerts() {
|
|
963
|
+
const rail = document.getElementById('alerts-rail');
|
|
964
|
+
const all = [];
|
|
965
|
+
|
|
966
|
+
if (alertState.todayCost > 50) {
|
|
967
|
+
all.push({ id: 'cost-crit', cls: 'alert-crit', ico: '⚑', msg: `Critical spend: $${alertState.todayCost.toFixed(2)} today` });
|
|
968
|
+
} else if (alertState.todayCost > 20) {
|
|
969
|
+
all.push({ id: 'cost-warn', cls: 'alert-warn', ico: '⚠', msg: `High spend: $${alertState.todayCost.toFixed(2)} today` });
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (alertState.errorCount >= 3) {
|
|
973
|
+
all.push({ id: 'feed-errors', cls: 'alert-warn', ico: '⚠', msg: `${alertState.errorCount} errors in current session`, action: 'jumpToErrors()' });
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
for (const l of alertState.longLoops) {
|
|
977
|
+
all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const visible = all.filter(a => !dismissedAlerts.has(a.id));
|
|
981
|
+
if (!visible.length) {
|
|
982
|
+
rail.className = '';
|
|
983
|
+
rail.innerHTML = '';
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
rail.className = 'has-alerts';
|
|
987
|
+
rail.innerHTML = visible.map(a =>
|
|
988
|
+
`<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
|
|
989
|
+
<span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
|
|
990
|
+
</div>`).join('');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function dismissAlert(id) {
|
|
994
|
+
dismissedAlerts.add(id);
|
|
995
|
+
updateAlerts();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ── session context bar ────────────────────────────────────
|
|
999
|
+
function showSessCtx(sess) {
|
|
1000
|
+
const bar = document.getElementById('sess-ctx');
|
|
1001
|
+
const isLive = !sess || !allSessions.length || allSessions[0]?.id === sess.id;
|
|
1002
|
+
if (isLive) {
|
|
1003
|
+
bar.classList.remove('show');
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
|
|
1007
|
+
bar.classList.add('show');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function goLive() {
|
|
1011
|
+
if (!allSessions.length) return;
|
|
1012
|
+
sessionIdx = 0;
|
|
1013
|
+
userScrolled = false;
|
|
1014
|
+
loadFeedForSession(allSessions[0]);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// ── metrics ────────────────────────────────────────────────
|
|
1018
|
+
async function loadMetrics() {
|
|
1019
|
+
if (!DIR) return;
|
|
1020
|
+
await Promise.allSettled([loadTodayMetrics(), loadLoopMetrics()]);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function loadTodayMetrics() {
|
|
1024
|
+
try {
|
|
1025
|
+
const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
|
|
1026
|
+
const s = data?.tokens?.summary || {};
|
|
1027
|
+
alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
1028
|
+
updateAlerts();
|
|
1029
|
+
// topbar cost badge
|
|
1030
|
+
const badge = document.getElementById('topbar-cost');
|
|
1031
|
+
if (badge && typeof s.todayCost === 'number') {
|
|
1032
|
+
badge.textContent = '$' + s.todayCost.toFixed(2) + ' today';
|
|
1033
|
+
badge.classList.add('loaded');
|
|
1034
|
+
}
|
|
1035
|
+
const cost = typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—';
|
|
1036
|
+
const calls = s.todayCalls != null ? s.todayCalls : '—';
|
|
1037
|
+
const moCost = typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—';
|
|
1038
|
+
document.getElementById('m-today').innerHTML = `
|
|
1039
|
+
<div class="m-group-title">Today</div>
|
|
1040
|
+
<div class="m-row"><span class="m-name">API cost</span><span class="m-val gold">${cost}</span></div>
|
|
1041
|
+
<div class="m-row"><span class="m-name">API calls</span><span class="m-val">${calls}</span></div>
|
|
1042
|
+
<div class="m-row"><span class="m-name">Month total</span><span class="m-val">${moCost}</span></div>
|
|
1043
|
+
${buildSparkline()}
|
|
1044
|
+
`;
|
|
1045
|
+
} catch (_) {
|
|
1046
|
+
document.getElementById('m-today').innerHTML = `<div class="m-group-title">Today</div><div class="loading-txt">—</div>`;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async function loadLoopMetrics() {
|
|
1051
|
+
try {
|
|
1052
|
+
const data = await apiFetch('/api/loops?dir=' + enc(DIR));
|
|
1053
|
+
const loops = Array.isArray(data) ? data : (data.loops || []);
|
|
1054
|
+
document.getElementById('bdg-loops').textContent = loops.length || '—';
|
|
1055
|
+
|
|
1056
|
+
// alert on loops running > 2h
|
|
1057
|
+
const TWO_HOURS = 2 * 3600 * 1000;
|
|
1058
|
+
const now = Date.now();
|
|
1059
|
+
alertState.longLoops = loops
|
|
1060
|
+
.filter(l => l.running !== false && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
|
|
1061
|
+
.map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
|
|
1062
|
+
updateAlerts();
|
|
1063
|
+
|
|
1064
|
+
if (!loops.length) {
|
|
1065
|
+
document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt" style="padding:8px 0">None</div>`;
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const items = loops.slice(0, 5).map(l => {
|
|
1069
|
+
const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
|
|
1070
|
+
return `<div class="mini-loop">
|
|
1071
|
+
<div class="ml-name">${esc(name)}</div>
|
|
1072
|
+
<div class="ml-meta"><span class="ml-dot"></span>${esc(l.interval || l.schedule || 'running')}</div>
|
|
1073
|
+
</div>`;
|
|
1074
|
+
}).join('');
|
|
1075
|
+
document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
|
|
1076
|
+
} catch (_) {
|
|
1077
|
+
document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function renderMiniSessions(sessions) {
|
|
1082
|
+
if (!sessions.length) return;
|
|
1083
|
+
const items = sessions.map((s, i) => `
|
|
1084
|
+
<div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
|
|
1085
|
+
<div class="ms-prompt">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
|
|
1086
|
+
<div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
|
|
1087
|
+
</div>`).join('');
|
|
1088
|
+
document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// ── projects ───────────────────────────────────────────────
|
|
1092
|
+
async function renderProjects() {
|
|
1093
|
+
const el = document.getElementById('proj-content');
|
|
1094
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1095
|
+
try {
|
|
1096
|
+
const data = await apiFetch('/api/data?dir=' + enc(ORIGINAL_DIR));
|
|
1097
|
+
allProjects = data?.allProjects || [];
|
|
1098
|
+
document.getElementById('bdg-projects').textContent = allProjects.length || '—';
|
|
1099
|
+
document.getElementById('proj-pg-sub').textContent =
|
|
1100
|
+
allProjects.length + ' project' + (allProjects.length !== 1 ? 's' : '') + ' found';
|
|
1101
|
+
renderProjectGrid(allProjects, '');
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
el.innerHTML = '<div class="empty">Could not load projects: ' + esc(err.message) + '</div>';
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function renderProjectGrid(projects, query) {
|
|
1108
|
+
const el = document.getElementById('proj-content');
|
|
1109
|
+
const filtered = query ? projects.filter(p =>
|
|
1110
|
+
(p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
|
|
1111
|
+
(p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
|
|
1112
|
+
if (!filtered.length) {
|
|
1113
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
el.className = 'proj-grid';
|
|
1117
|
+
el.innerHTML = filtered.map(p => {
|
|
1118
|
+
const isCurrent = p.path === DIR;
|
|
1119
|
+
return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
|
|
1120
|
+
${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
|
|
1121
|
+
<div class="proj-card-name">${esc(p.name || p.slug)}</div>
|
|
1122
|
+
<div class="proj-card-path">${esc(p.path || '')}</div>
|
|
1123
|
+
<div class="proj-card-stats">
|
|
1124
|
+
<div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
|
|
1125
|
+
<div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
|
|
1126
|
+
${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>` : ''}
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>`;
|
|
1129
|
+
}).join('');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function filterProjects(q) {
|
|
1133
|
+
renderProjectGrid(allProjects, q);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ── sessions ───────────────────────────────────────────────
|
|
1137
|
+
async function renderSessions() {
|
|
1138
|
+
const el = document.getElementById('sess-content');
|
|
1139
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1140
|
+
try {
|
|
1141
|
+
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
1142
|
+
allSessions = sessions; // always sync — stale ordering breaks jumpToSession
|
|
1143
|
+
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
1144
|
+
document.getElementById('sess-pg-sub').textContent =
|
|
1145
|
+
sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
|
|
1146
|
+
if (!sessions.length) {
|
|
1147
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
el.innerHTML = sessions.map(s => {
|
|
1151
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1152
|
+
const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
|
|
1153
|
+
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
|
|
1154
|
+
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1155
|
+
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1156
|
+
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('');
|
|
1157
|
+
return `<div class="sess-row" onclick="jumpToSession('${s.id}')">
|
|
1158
|
+
<div class="sr-top">
|
|
1159
|
+
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1160
|
+
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1161
|
+
<span class="sr-view">→ view</span>
|
|
1162
|
+
</div>
|
|
1163
|
+
<div class="sr-meta">${esc(meta)}</div>
|
|
1164
|
+
${summaries ? `<div class="sr-tags">${summaries}</div>` : ''}
|
|
1165
|
+
</div>`;
|
|
1166
|
+
}).join('');
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function jumpToSession(id) {
|
|
1173
|
+
switchView('now');
|
|
1174
|
+
setTimeout(() => {
|
|
1175
|
+
const i = allSessions.findIndex(x => x.id === id);
|
|
1176
|
+
if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
|
|
1177
|
+
}, 80);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// ── loops ──────────────────────────────────────────────────
|
|
1181
|
+
async function renderLoops() {
|
|
1182
|
+
const el = document.getElementById('loops-content');
|
|
1183
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1184
|
+
try {
|
|
1185
|
+
const data = await apiFetch('/api/loops?dir=' + enc(DIR));
|
|
1186
|
+
const loops = Array.isArray(data) ? data : (data.loops || []);
|
|
1187
|
+
document.getElementById('bdg-loops').textContent = loops.length || '—';
|
|
1188
|
+
if (!loops.length) {
|
|
1189
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div></div>';
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
el.innerHTML = loops.map((l, idx) => {
|
|
1193
|
+
const running = l.running !== false;
|
|
1194
|
+
const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
|
|
1195
|
+
const interval = l.interval || l.schedule || '';
|
|
1196
|
+
const fullPrompt = l.prompt || l.command || '';
|
|
1197
|
+
const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
|
|
1198
|
+
const lastRun = l.lastRun ? relTime(l.lastRun) : (l.startedAt ? relTime(l.startedAt) : '—');
|
|
1199
|
+
const runs = l.runCount != null ? l.runCount : '—';
|
|
1200
|
+
return `<div class="loop-row" onclick="toggleLoop(this)">
|
|
1201
|
+
<div class="loop-ico">↺</div>
|
|
1202
|
+
<div class="loop-body">
|
|
1203
|
+
<div class="loop-name">${esc(name)}</div>
|
|
1204
|
+
<div class="loop-meta">${esc([interval, l.description].filter(Boolean).join(' · ').slice(0, 80))}</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
<div class="loop-status ${running ? 'active' : 'stopped'}">${running ? 'active' : 'stopped'}</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
<div class="loop-expand">
|
|
1209
|
+
${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
|
|
1210
|
+
<div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(interval || '—')}</div></div>
|
|
1211
|
+
<div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${running ? '● running' : '○ stopped'}</div></div>
|
|
1212
|
+
<div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
|
|
1213
|
+
<div class="le-row"><div class="le-lbl">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
|
|
1214
|
+
<div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}</div></div>
|
|
1215
|
+
</div>`;
|
|
1216
|
+
}).join('');
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
el.innerHTML = '<div class="empty">Could not load loops: ' + esc(err.message) + '</div>';
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function toggleLoop(row) {
|
|
1223
|
+
row.classList.toggle('open');
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ── memory ─────────────────────────────────────────────────
|
|
1227
|
+
async function renderMemory() {
|
|
1228
|
+
activeMemNs = 'All'; // reset on every render so stale filter doesn't persist
|
|
1229
|
+
const el = document.getElementById('mem-content');
|
|
1230
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1231
|
+
try {
|
|
1232
|
+
const data = await apiFetch('/api/palace?dir=' + enc(DIR));
|
|
1233
|
+
allDrawers = data.drawers || [];
|
|
1234
|
+
const identity = data.identity || '';
|
|
1235
|
+
let html = '';
|
|
1236
|
+
if (identity) {
|
|
1237
|
+
html += `<div class="mem-section">
|
|
1238
|
+
<div class="mem-title">Identity</div>
|
|
1239
|
+
<div class="drawer-item"><div class="dr-val" style="white-space:pre-wrap">${esc(identity.slice(0, 1200))}</div></div>
|
|
1240
|
+
</div>`;
|
|
1241
|
+
}
|
|
1242
|
+
if (allDrawers.length) {
|
|
1243
|
+
// build namespace tabs
|
|
1244
|
+
const namespaces = ['All', ...new Set(allDrawers.map(d => d.namespace || 'default').filter(Boolean))];
|
|
1245
|
+
const tabsEl = document.getElementById('mem-ns-tabs');
|
|
1246
|
+
if (tabsEl) {
|
|
1247
|
+
tabsEl.innerHTML = namespaces.map((ns, i) =>
|
|
1248
|
+
`<button class="ns-tab${i === 0 ? ' active' : ''}" data-ns="${esc(ns)}" onclick="filterMemoryNs('${esc(ns)}')">${esc(ns)}</button>`
|
|
1249
|
+
).join('');
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const items = allDrawers.map((d, i) =>
|
|
1253
|
+
`<div class="drawer-item" data-idx="${i}" data-ns="${esc(d.namespace || 'default')}">
|
|
1254
|
+
<div class="dr-key">${esc(d.key || d.namespace || '—')}</div>
|
|
1255
|
+
<div class="dr-val">${esc(String(d.value || d.text || '').slice(0, 300))}</div>
|
|
1256
|
+
${d.timestamp ? `<div class="dr-ts">${relTime(d.timestamp)}</div>` : ''}
|
|
1257
|
+
</div>`).join('');
|
|
1258
|
+
html += `<div class="mem-section" id="drawers-section">
|
|
1259
|
+
<div class="mem-title">Drawers (${allDrawers.length})</div>
|
|
1260
|
+
<div id="drawers-list">${items}</div>
|
|
1261
|
+
</div>`;
|
|
1262
|
+
}
|
|
1263
|
+
if (!html) html = '<div class="empty"><div class="empty-ico">◈</div><div>Memory palace is empty</div></div>';
|
|
1264
|
+
el.innerHTML = html;
|
|
1265
|
+
} catch (err) {
|
|
1266
|
+
el.innerHTML = '<div class="empty">Could not load memory: ' + esc(err.message) + '</div>';
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
let activeMemNs = 'All';
|
|
1271
|
+
|
|
1272
|
+
function filterMemory(q) {
|
|
1273
|
+
const items = document.querySelectorAll('#drawers-list .drawer-item');
|
|
1274
|
+
const lq = q.toLowerCase();
|
|
1275
|
+
items.forEach(item => {
|
|
1276
|
+
const key = (item.querySelector('.dr-key')?.textContent || '').toLowerCase();
|
|
1277
|
+
const val = (item.querySelector('.dr-val')?.textContent || '').toLowerCase();
|
|
1278
|
+
const nsMatch = activeMemNs === 'All' || (item.dataset.ns === activeMemNs);
|
|
1279
|
+
item.classList.toggle('hidden', !nsMatch || (!!lq && !key.includes(lq) && !val.includes(lq)));
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function filterMemoryNs(ns) {
|
|
1284
|
+
activeMemNs = ns;
|
|
1285
|
+
document.querySelectorAll('.ns-tab').forEach(t => t.classList.toggle('active', t.dataset.ns === ns));
|
|
1286
|
+
filterMemory(document.getElementById('mem-filter')?.value || '');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// ── orgs ───────────────────────────────────────────────────
|
|
1290
|
+
async function renderOrgs() {
|
|
1291
|
+
const el = document.getElementById('orgs-content');
|
|
1292
|
+
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
1293
|
+
try {
|
|
1294
|
+
const data = await apiFetch('/api/orgs');
|
|
1295
|
+
const orgs = Array.isArray(data) ? data : (data.orgs || []);
|
|
1296
|
+
if (!orgs.length) {
|
|
1297
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No MASTERMIND orgs found</div></div>';
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
el.innerHTML = '<div class="org-list">' + orgs.map(o =>
|
|
1301
|
+
`<div class="org-row">
|
|
1302
|
+
<div class="org-name">${esc(o.name || o.id || '—')}</div>
|
|
1303
|
+
<div class="org-meta">${esc(o.description || (o.agents != null ? o.agents + ' agents' : ''))}</div>
|
|
1304
|
+
</div>`).join('') + '</div>';
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
el.innerHTML = '<div class="empty">Could not load orgs: ' + esc(err.message) + '</div>';
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// ── density toggle ─────────────────────────────────────────
|
|
1311
|
+
let compactMode = false;
|
|
1312
|
+
function toggleDensity() {
|
|
1313
|
+
compactMode = !compactMode;
|
|
1314
|
+
const pane = document.getElementById('feed-pane');
|
|
1315
|
+
const btn = document.getElementById('btn-density');
|
|
1316
|
+
pane.classList.toggle('compact', compactMode);
|
|
1317
|
+
btn.classList.toggle('compact-on', compactMode);
|
|
1318
|
+
btn.title = compactMode ? 'Switch to comfortable view' : 'Toggle compact view';
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ── session timeline ────────────────────────────────────────
|
|
1322
|
+
const TL_COLORS = {
|
|
1323
|
+
file: 'oklch(65% 0.15 150)',
|
|
1324
|
+
bash: 'oklch(65% 0.12 240)',
|
|
1325
|
+
agent: 'oklch(65% 0.13 290)',
|
|
1326
|
+
mcp: 'oklch(65% 0.12 195)',
|
|
1327
|
+
search: 'oklch(65% 0.14 35)',
|
|
1328
|
+
skill: 'oklch(72% 0.18 75)',
|
|
1329
|
+
task: 'oklch(62% 0.12 55)',
|
|
1330
|
+
mem: 'oklch(62% 0.11 160)',
|
|
1331
|
+
user: 'oklch(55% 0.08 75 / 0.5)',
|
|
1332
|
+
other: 'oklch(32% 0.005 75)',
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
function buildTimeline(events) {
|
|
1336
|
+
const tl = document.getElementById('feed-timeline');
|
|
1337
|
+
if (!tl) return;
|
|
1338
|
+
// Only tool + user events with timestamps
|
|
1339
|
+
const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
|
|
1340
|
+
if (stamped.length < 2) { tl.innerHTML = ''; return; }
|
|
1341
|
+
const times = stamped.map(ev => new Date(ev.ts).getTime());
|
|
1342
|
+
const tMin = Math.min(...times), tMax = Math.max(...times);
|
|
1343
|
+
const span = tMax - tMin || 1;
|
|
1344
|
+
const segs = stamped.map(ev => {
|
|
1345
|
+
const pct = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
|
|
1346
|
+
const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
|
|
1347
|
+
const color = TL_COLORS[cat] || TL_COLORS.other;
|
|
1348
|
+
const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
|
|
1349
|
+
return `<div class="tl-seg" style="flex:${pct > 0 ? pct : 0.5};background:${color}" title="${esc(label)}"></div>`;
|
|
1350
|
+
});
|
|
1351
|
+
tl.innerHTML = segs.join('');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ── tool breakdown ──────────────────────────────────────────
|
|
1355
|
+
const TB_COLORS = TL_COLORS;
|
|
1356
|
+
const TB_ORDER = ['file','bash','mcp','agent','search','skill','task','mem','other'];
|
|
1357
|
+
|
|
1358
|
+
function buildBreakdown(events) {
|
|
1359
|
+
const counts = {};
|
|
1360
|
+
for (const ev of events) {
|
|
1361
|
+
if (ev.kind !== 'tool') continue;
|
|
1362
|
+
const c = ev.cat || 'other';
|
|
1363
|
+
counts[c] = (counts[c] || 0) + 1;
|
|
1364
|
+
}
|
|
1365
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
1366
|
+
if (!total) {
|
|
1367
|
+
document.getElementById('m-breakdown').innerHTML =
|
|
1368
|
+
'<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const rows = TB_ORDER.filter(c => counts[c])
|
|
1372
|
+
.sort((a, b) => counts[b] - counts[a])
|
|
1373
|
+
.map(c => {
|
|
1374
|
+
const pct = Math.round(counts[c] / total * 100);
|
|
1375
|
+
const color = TB_COLORS[c] || TB_COLORS.other;
|
|
1376
|
+
return `<div class="tb-row">
|
|
1377
|
+
<div class="tb-lbl">${c}</div>
|
|
1378
|
+
<div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${color}"></div></div>
|
|
1379
|
+
<div class="tb-count">${counts[c]}</div>
|
|
1380
|
+
</div>`;
|
|
1381
|
+
}).join('');
|
|
1382
|
+
document.getElementById('m-breakdown').innerHTML =
|
|
1383
|
+
`<div class="m-group-title">Tool Usage</div><div class="m-breakdown">${rows}</div>`;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// ── error jump ──────────────────────────────────────────────
|
|
1387
|
+
function jumpToErrors() {
|
|
1388
|
+
// find first errored feed entry and scroll to it
|
|
1389
|
+
const first = document.querySelector('#feed-content .feed-entry.errored');
|
|
1390
|
+
if (!first) return;
|
|
1391
|
+
first.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1392
|
+
first.style.outline = '1px solid var(--red)';
|
|
1393
|
+
setTimeout(() => { first.style.outline = ''; }, 1800);
|
|
1394
|
+
// highlight all errored entries briefly
|
|
1395
|
+
document.querySelectorAll('#feed-content .feed-entry.errored').forEach(el => {
|
|
1396
|
+
el.style.background = 'oklch(60% 0.18 25 / 0.08)';
|
|
1397
|
+
setTimeout(() => { el.style.background = ''; }, 1800);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// ── week-over-week delta ────────────────────────────────────
|
|
1402
|
+
function buildWowDelta() {
|
|
1403
|
+
// Compare this week (last 7 days) vs prior week (8–14 days ago)
|
|
1404
|
+
const DAY = 86400000;
|
|
1405
|
+
const now = Date.now();
|
|
1406
|
+
let thisWeek = 0, lastWeek = 0;
|
|
1407
|
+
for (const s of allSessions) {
|
|
1408
|
+
const ts = s.lastTs || s.mtime;
|
|
1409
|
+
if (!ts) continue;
|
|
1410
|
+
const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
1411
|
+
if (age < 7 * DAY) thisWeek++;
|
|
1412
|
+
else if (age < 14 * DAY) lastWeek++;
|
|
1413
|
+
}
|
|
1414
|
+
if (!lastWeek) return '';
|
|
1415
|
+
const delta = Math.round((thisWeek - lastWeek) / lastWeek * 100);
|
|
1416
|
+
if (delta > 0) return `<span class="wow-delta wow-up" title="vs prior 7 days">↑${delta}%</span>`;
|
|
1417
|
+
if (delta < 0) return `<span class="wow-delta wow-down" title="vs prior 7 days">↓${Math.abs(delta)}%</span>`;
|
|
1418
|
+
return `<span class="wow-delta wow-flat" title="vs prior 7 days">→ flat</span>`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ── feed search ────────────────────────────────────────────
|
|
1422
|
+
let feedSearchActive = false;
|
|
1423
|
+
|
|
1424
|
+
function toggleFeedSearch() {
|
|
1425
|
+
const bar = document.getElementById('feed-search');
|
|
1426
|
+
if (feedSearchActive) { closeFeedSearch(); return; }
|
|
1427
|
+
feedSearchActive = true;
|
|
1428
|
+
bar.classList.add('open');
|
|
1429
|
+
requestAnimationFrame(() => document.getElementById('feed-search-input').focus());
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function closeFeedSearch() {
|
|
1433
|
+
feedSearchActive = false;
|
|
1434
|
+
document.getElementById('feed-search').classList.remove('open');
|
|
1435
|
+
document.getElementById('feed-search-input').value = '';
|
|
1436
|
+
filterFeed('');
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function filterFeed(q) {
|
|
1440
|
+
const lq = q.toLowerCase().trim();
|
|
1441
|
+
let visible = 0;
|
|
1442
|
+
|
|
1443
|
+
// individual entries
|
|
1444
|
+
document.querySelectorAll('#feed-content .feed-entry').forEach(el => {
|
|
1445
|
+
const text = (el.querySelector('.feed-lbl')?.textContent || '') + ' ' +
|
|
1446
|
+
(el.querySelector('.feed-detail')?.textContent || '');
|
|
1447
|
+
const show = !lq || text.toLowerCase().includes(lq);
|
|
1448
|
+
el.style.display = show ? '' : 'none';
|
|
1449
|
+
if (show) visible++;
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
// collapsed group rows
|
|
1453
|
+
document.querySelectorAll('#feed-content .feed-group').forEach(el => {
|
|
1454
|
+
const text = el.querySelector('.fg-label')?.textContent || '';
|
|
1455
|
+
const show = !lq || text.toLowerCase().includes(lq);
|
|
1456
|
+
el.style.display = show ? '' : 'none';
|
|
1457
|
+
if (show) visible++;
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
const countEl = document.getElementById('feed-search-count');
|
|
1461
|
+
if (countEl) countEl.textContent = lq ? `${visible} match${visible !== 1 ? 'es' : ''}` : '';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// ── copy session as markdown ───────────────────────────────
|
|
1465
|
+
async function copySession() {
|
|
1466
|
+
const btn = document.getElementById('btn-copy-sess');
|
|
1467
|
+
const sess = allSessions[sessionIdx];
|
|
1468
|
+
if (!sess?.file) return;
|
|
1469
|
+
try {
|
|
1470
|
+
const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
|
|
1471
|
+
const events = data.events || [];
|
|
1472
|
+
const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
|
|
1473
|
+
for (const ev of events) {
|
|
1474
|
+
if (ev.kind === 'user' && ev.text?.trim()) {
|
|
1475
|
+
lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
|
|
1476
|
+
} else if (ev.kind === 'tool') {
|
|
1477
|
+
lines.push(`- \`${ev.name || ev.cat}\`: ${ev.label || ''}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
await navigator.clipboard.writeText(lines.join('\n'));
|
|
1481
|
+
btn.textContent = '✓ Copied';
|
|
1482
|
+
btn.classList.add('copied');
|
|
1483
|
+
setTimeout(() => { btn.textContent = '⎘ Copy'; btn.classList.remove('copied'); }, 2000);
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
btn.textContent = '✕ Error';
|
|
1486
|
+
setTimeout(() => { btn.textContent = '⎘ Copy'; }, 1500);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// ── feed time filter ───────────────────────────────────────
|
|
1491
|
+
function setFeedTimeFilter(f) {
|
|
1492
|
+
feedTimeFilter = f;
|
|
1493
|
+
document.querySelectorAll('.tf-btn').forEach(b => b.classList.toggle('active', b.dataset.tf === f));
|
|
1494
|
+
if (!allSessions.length) return;
|
|
1495
|
+
const sess = allSessions[sessionIdx] || allSessions[0];
|
|
1496
|
+
if (sess) loadFeedForSession(sess);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// ── command palette ────────────────────────────────────────
|
|
1500
|
+
function openCmdPalette() {
|
|
1501
|
+
document.getElementById('cmd-backdrop').classList.add('open');
|
|
1502
|
+
document.getElementById('cmd-palette').classList.add('open');
|
|
1503
|
+
const inp = document.getElementById('cmd-input');
|
|
1504
|
+
inp.value = '';
|
|
1505
|
+
cmdSearch('');
|
|
1506
|
+
requestAnimationFrame(() => inp.focus());
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function closeCmdPalette() {
|
|
1510
|
+
document.getElementById('cmd-backdrop').classList.remove('open');
|
|
1511
|
+
document.getElementById('cmd-palette').classList.remove('open');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function cmdSearch(q) {
|
|
1515
|
+
cmdItems = [];
|
|
1516
|
+
const lq = q.toLowerCase().trim();
|
|
1517
|
+
const results = document.getElementById('cmd-results');
|
|
1518
|
+
|
|
1519
|
+
const sessMatches = allSessions
|
|
1520
|
+
.filter(s => !lq || (s.lastPrompt || s.id).toLowerCase().includes(lq))
|
|
1521
|
+
.slice(0, 5);
|
|
1522
|
+
|
|
1523
|
+
const memMatches = allDrawers
|
|
1524
|
+
.filter(d => !lq || (d.key || '').toLowerCase().includes(lq) ||
|
|
1525
|
+
String(d.value || d.text || '').toLowerCase().includes(lq))
|
|
1526
|
+
.slice(0, 3);
|
|
1527
|
+
|
|
1528
|
+
const projMatches = allProjects
|
|
1529
|
+
.filter(p => !lq || (p.name || p.slug || '').toLowerCase().includes(lq) ||
|
|
1530
|
+
(p.path || '').toLowerCase().includes(lq))
|
|
1531
|
+
.slice(0, 3);
|
|
1532
|
+
|
|
1533
|
+
if (!sessMatches.length && !memMatches.length && !projMatches.length) {
|
|
1534
|
+
results.innerHTML = '<div class="cmd-empty">No results</div>';
|
|
1535
|
+
cmdItems = [];
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
let html = '';
|
|
1540
|
+
|
|
1541
|
+
if (sessMatches.length) {
|
|
1542
|
+
html += '<div class="cmd-group-lbl">Sessions</div>';
|
|
1543
|
+
sessMatches.forEach(s => {
|
|
1544
|
+
const idx = cmdItems.length;
|
|
1545
|
+
cmdItems.push({ type: 'session', data: s });
|
|
1546
|
+
html += `<div class="cmd-item" data-ci="${idx}">
|
|
1547
|
+
<span class="ci-ico">◫</span>
|
|
1548
|
+
<div class="cmd-item-body">
|
|
1549
|
+
<div class="ci-title">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
|
|
1550
|
+
<div class="ci-sub">${relTime(s.lastTs || s.mtime)}</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
</div>`;
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (memMatches.length) {
|
|
1557
|
+
html += '<div class="cmd-group-lbl">Memory</div>';
|
|
1558
|
+
memMatches.forEach(d => {
|
|
1559
|
+
const idx = cmdItems.length;
|
|
1560
|
+
cmdItems.push({ type: 'memory', data: d });
|
|
1561
|
+
html += `<div class="cmd-item" data-ci="${idx}">
|
|
1562
|
+
<span class="ci-ico">◈</span>
|
|
1563
|
+
<div class="cmd-item-body">
|
|
1564
|
+
<div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
|
|
1565
|
+
<div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
|
|
1566
|
+
</div>
|
|
1567
|
+
</div>`;
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (projMatches.length) {
|
|
1572
|
+
html += '<div class="cmd-group-lbl">Projects</div>';
|
|
1573
|
+
projMatches.forEach(p => {
|
|
1574
|
+
const idx = cmdItems.length;
|
|
1575
|
+
cmdItems.push({ type: 'project', data: p });
|
|
1576
|
+
html += `<div class="cmd-item" data-ci="${idx}">
|
|
1577
|
+
<span class="ci-ico">⊞</span>
|
|
1578
|
+
<div class="cmd-item-body">
|
|
1579
|
+
<div class="ci-title">${esc(p.name || p.slug)}</div>
|
|
1580
|
+
<div class="ci-sub">${esc(p.path || '')}</div>
|
|
1581
|
+
</div>
|
|
1582
|
+
</div>`;
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
results.innerHTML = html;
|
|
1587
|
+
cmdFocusIdx = 0;
|
|
1588
|
+
updateCmdFocus();
|
|
1589
|
+
|
|
1590
|
+
results.querySelectorAll('.cmd-item').forEach(el => {
|
|
1591
|
+
el.addEventListener('click', () => {
|
|
1592
|
+
cmdFocusIdx = parseInt(el.dataset.ci);
|
|
1593
|
+
executeCmdItem();
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function updateCmdFocus() {
|
|
1599
|
+
const items = document.querySelectorAll('#cmd-results .cmd-item');
|
|
1600
|
+
items.forEach((el, i) => el.classList.toggle('focused', i === cmdFocusIdx));
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function cmdKey(e) {
|
|
1604
|
+
const items = document.querySelectorAll('#cmd-results .cmd-item');
|
|
1605
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); cmdFocusIdx = Math.min(cmdFocusIdx + 1, items.length - 1); updateCmdFocus(); }
|
|
1606
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); cmdFocusIdx = Math.max(cmdFocusIdx - 1, 0); updateCmdFocus(); }
|
|
1607
|
+
else if (e.key === 'Enter') { e.preventDefault(); executeCmdItem(); }
|
|
1608
|
+
else if (e.key === 'Escape') { closeCmdPalette(); }
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function executeCmdItem() {
|
|
1612
|
+
const item = cmdItems[cmdFocusIdx];
|
|
1613
|
+
if (!item) return;
|
|
1614
|
+
closeCmdPalette();
|
|
1615
|
+
if (item.type === 'session') jumpToSession(item.data.id);
|
|
1616
|
+
else if (item.type === 'memory') switchView('memory');
|
|
1617
|
+
else if (item.type === 'project') switchProject(item.data.path);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// ── keyboard shortcuts ─────────────────────────────────────
|
|
1621
|
+
document.addEventListener('keydown', e => {
|
|
1622
|
+
// ⌘K / Ctrl+K — command palette
|
|
1623
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
1624
|
+
e.preventDefault();
|
|
1625
|
+
const open = document.getElementById('cmd-palette').classList.contains('open');
|
|
1626
|
+
if (open) closeCmdPalette(); else openCmdPalette();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// ignore when typing in inputs
|
|
1631
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
1632
|
+
if (document.getElementById('cmd-palette').classList.contains('open')) return;
|
|
1633
|
+
|
|
1634
|
+
if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); }
|
|
1635
|
+
|
|
1636
|
+
if (currentView === 'now') {
|
|
1637
|
+
if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
|
|
1638
|
+
if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
|
|
1639
|
+
if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
|
|
1640
|
+
|
|
1641
|
+
if (e.key === 'j' || e.key === 'k') {
|
|
1642
|
+
e.preventDefault();
|
|
1643
|
+
const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
|
|
1644
|
+
if (!entries.length) return;
|
|
1645
|
+
let cur = entries.findIndex(el => el.classList.contains('selected'));
|
|
1646
|
+
if (e.key === 'j') cur = cur < 0 ? 0 : Math.min(cur + 1, entries.length - 1);
|
|
1647
|
+
else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
|
|
1648
|
+
entries.forEach((el, i) => el.classList.toggle('selected', i === cur));
|
|
1649
|
+
entries[cur].scrollIntoView({ block: 'nearest' });
|
|
1650
|
+
selectedEntryId = entries[cur].dataset.ev
|
|
1651
|
+
? (JSON.parse(entries[cur].dataset.ev).id || '')
|
|
1652
|
+
: '';
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (e.key === 'Enter') {
|
|
1656
|
+
const sel = document.querySelector('#feed-content .feed-entry.selected');
|
|
1657
|
+
if (sel) openDetail(sel.dataset.ev);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// ── helpers ────────────────────────────────────────────────
|
|
1663
|
+
function enc(s) { return encodeURIComponent(s); }
|
|
1664
|
+
function esc(s) {
|
|
1665
|
+
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function relTime(ts) {
|
|
1669
|
+
if (!ts) return '';
|
|
1670
|
+
const diff = Date.now() - (typeof ts === 'number' ? ts : new Date(ts).getTime());
|
|
1671
|
+
const s = Math.floor(diff / 1000);
|
|
1672
|
+
if (s < 5) return 'now';
|
|
1673
|
+
if (s < 60) return s + 's';
|
|
1674
|
+
const m = Math.floor(s / 60);
|
|
1675
|
+
if (m < 60) return m + 'm';
|
|
1676
|
+
const h = Math.floor(m / 60);
|
|
1677
|
+
if (h < 24) return h + 'h';
|
|
1678
|
+
return Math.floor(h / 24) + 'd';
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function fmtDur(ms) {
|
|
1682
|
+
const s = Math.floor(ms / 1000);
|
|
1683
|
+
if (s < 60) return s + 's';
|
|
1684
|
+
const m = Math.floor(s / 60);
|
|
1685
|
+
if (m < 60) return m + 'm';
|
|
1686
|
+
return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
init();
|
|
1690
|
+
</script>
|
|
1691
|
+
</body>
|
|
1692
|
+
</html>
|