@monoes/monomindcli 1.10.47 → 1.10.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (573) hide show
  1. package/.claude/agents/optimization/benchmark-suite.md +2 -0
  2. package/.claude/agents/optimization/load-balancer.md +2 -0
  3. package/.claude/agents/optimization/performance-monitor.md +2 -0
  4. package/.claude/agents/optimization/resource-allocator.md +3 -1
  5. package/.claude/agents/optimization/topology-optimizer.md +2 -0
  6. package/.claude/commands/mastermind/_repeat.md +21 -0
  7. package/.claude/commands/mastermind/_taskfile.md +235 -0
  8. package/.claude/commands/mastermind/adr.md +11 -0
  9. package/.claude/commands/mastermind/approve.md +94 -0
  10. package/.claude/commands/mastermind/autodev.md +32 -0
  11. package/.claude/commands/mastermind/budget.md +7 -0
  12. package/.claude/commands/mastermind/code-review.md +317 -0
  13. package/.claude/commands/mastermind/createorg.md +40 -1
  14. package/.claude/commands/mastermind/createtask.md +383 -0
  15. package/.claude/commands/mastermind/debug.md +22 -0
  16. package/.claude/commands/mastermind/design.md +20 -0
  17. package/.claude/commands/mastermind/do.md +526 -0
  18. package/.claude/commands/mastermind/execute.md +20 -0
  19. package/.claude/commands/mastermind/finish.md +20 -0
  20. package/.claude/commands/mastermind/graph-status.md +7 -0
  21. package/.claude/commands/mastermind/help.md +118 -0
  22. package/.claude/commands/mastermind/ideate.md +261 -0
  23. package/.claude/commands/mastermind/improve.md +345 -0
  24. package/.claude/commands/mastermind/loops.md +7 -0
  25. package/.claude/commands/mastermind/master.md +186 -6
  26. package/.claude/commands/mastermind/memory.md +230 -0
  27. package/.claude/commands/mastermind/plan.md +26 -0
  28. package/.claude/commands/mastermind/receive-review.md +20 -0
  29. package/.claude/commands/mastermind/repeat.md +257 -0
  30. package/.claude/commands/mastermind/runorg.md +3 -0
  31. package/.claude/commands/mastermind/skill-builder.md +20 -0
  32. package/.claude/commands/mastermind/specialagents.md +125 -0
  33. package/.claude/commands/mastermind/swarm.md +161 -0
  34. package/.claude/commands/mastermind/taskdev.md +26 -0
  35. package/.claude/commands/mastermind/tdd.md +22 -0
  36. package/.claude/commands/mastermind/techport.md +4 -0
  37. package/.claude/commands/mastermind/understand.md +139 -0
  38. package/.claude/commands/mastermind/verify.md +22 -0
  39. package/.claude/commands/mastermind/worktree.md +20 -0
  40. package/.claude/commands/monomind/do.md +52 -0
  41. package/.claude/commands/monomind/improve.md +2 -0
  42. package/.claude/helpers/handlers/graph-status-handler.cjs +2 -1
  43. package/.claude/helpers/handlers/route-handler.cjs +61 -11
  44. package/.claude/helpers/hook-handler.cjs +19 -0
  45. package/.claude/helpers/skill-registry.json +122 -51
  46. package/.claude/helpers/statusline.cjs +1 -1
  47. package/.claude/skills/agent-browser-testing/SKILL.md +522 -152
  48. package/.claude/skills/github-issue-triage/SKILL.md +354 -0
  49. package/.claude/skills/github-repo-recap/SKILL.md +207 -0
  50. package/.claude/skills/mastermind/_delegation.md +83 -0
  51. package/.claude/skills/mastermind/_protocol.md +14 -0
  52. package/.claude/skills/mastermind/approve.md +15 -7
  53. package/.claude/skills/mastermind/autodev.md +534 -0
  54. package/.claude/skills/mastermind/createorg.md +21 -5
  55. package/.claude/skills/mastermind/debug.md +232 -0
  56. package/.claude/skills/mastermind/design.md +187 -0
  57. package/.claude/skills/mastermind/execute.md +104 -0
  58. package/.claude/skills/mastermind/finish.md +251 -0
  59. package/.claude/skills/mastermind/plan.md +180 -0
  60. package/.claude/skills/mastermind/receive-review.md +213 -0
  61. package/.claude/skills/mastermind/runorg.md +23 -8
  62. package/.claude/skills/mastermind/skill-builder.md +274 -0
  63. package/.claude/skills/mastermind/taskdev.md +307 -0
  64. package/.claude/skills/mastermind/tdd.md +394 -0
  65. package/.claude/skills/mastermind/verify.md +196 -0
  66. package/.claude/skills/mastermind/worktree.md +160 -132
  67. package/README.md +320 -253
  68. package/dist/src/commands/analyze.d.ts.map +1 -1
  69. package/dist/src/commands/analyze.js +9 -2
  70. package/dist/src/commands/analyze.js.map +1 -1
  71. package/dist/src/commands/benchmark.js.map +1 -1
  72. package/dist/src/commands/completions.js +1 -1
  73. package/dist/src/commands/guidance.js +7 -7
  74. package/dist/src/commands/hooks.d.ts.map +1 -1
  75. package/dist/src/commands/hooks.js +16 -3
  76. package/dist/src/commands/hooks.js.map +1 -1
  77. package/dist/src/commands/index.d.ts +3 -2
  78. package/dist/src/commands/index.d.ts.map +1 -1
  79. package/dist/src/commands/index.js +7 -0
  80. package/dist/src/commands/index.js.map +1 -1
  81. package/dist/src/commands/init.d.ts.map +1 -1
  82. package/dist/src/commands/init.js +47 -13
  83. package/dist/src/commands/init.js.map +1 -1
  84. package/dist/src/commands/neural.d.ts.map +1 -1
  85. package/dist/src/commands/neural.js +100 -14
  86. package/dist/src/commands/neural.js.map +1 -1
  87. package/dist/src/commands/platforms.d.ts +11 -0
  88. package/dist/src/commands/platforms.d.ts.map +1 -0
  89. package/dist/src/commands/platforms.js +195 -0
  90. package/dist/src/commands/platforms.js.map +1 -0
  91. package/dist/src/commands/ruvector/backup.js.map +1 -1
  92. package/dist/src/commands/ruvector/benchmark.js.map +1 -1
  93. package/dist/src/commands/ruvector/init.js.map +1 -1
  94. package/dist/src/commands/ruvector/migrate.js.map +1 -1
  95. package/dist/src/commands/ruvector/optimize.js.map +1 -1
  96. package/dist/src/commands/ruvector/status.js.map +1 -1
  97. package/dist/src/commands/update.js +6 -6
  98. package/dist/src/init/executor.d.ts.map +1 -1
  99. package/dist/src/init/executor.js +28 -0
  100. package/dist/src/init/executor.js.map +1 -1
  101. package/dist/src/init/statusline-generator.js +1 -1
  102. package/dist/src/init/types.d.ts +1 -0
  103. package/dist/src/init/types.d.ts.map +1 -1
  104. package/dist/src/mcp-server.d.ts.map +1 -1
  105. package/dist/src/mcp-server.js +92 -0
  106. package/dist/src/mcp-server.js.map +1 -1
  107. package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
  108. package/dist/src/mcp-tools/hive-mind-tools.js +52 -0
  109. package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
  110. package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
  111. package/dist/src/mcp-tools/hooks-tools.js +106 -5
  112. package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
  113. package/dist/src/mcp-tools/index.d.ts +0 -5
  114. package/dist/src/mcp-tools/index.d.ts.map +1 -1
  115. package/dist/src/mcp-tools/index.js +0 -5
  116. package/dist/src/mcp-tools/index.js.map +1 -1
  117. package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
  118. package/dist/src/mcp-tools/monograph-tools.js +507 -5587
  119. package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
  120. package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
  121. package/dist/src/mcp-tools/neural-tools.js +64 -4
  122. package/dist/src/mcp-tools/neural-tools.js.map +1 -1
  123. package/dist/src/mcp-tools/security-tools.js +4 -4
  124. package/dist/src/memory/intelligence.d.ts +2 -2
  125. package/dist/src/memory/intelligence.d.ts.map +1 -1
  126. package/dist/src/memory/intelligence.js +108 -3
  127. package/dist/src/memory/intelligence.js.map +1 -1
  128. package/dist/src/memory/memory-bridge.js +1 -1
  129. package/dist/src/memory/memory-bridge.js.map +1 -1
  130. package/dist/src/memory/sona-optimizer.d.ts +1 -10
  131. package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
  132. package/dist/src/memory/sona-optimizer.js +0 -46
  133. package/dist/src/memory/sona-optimizer.js.map +1 -1
  134. package/dist/src/runtime/headless.js +3 -3
  135. package/dist/src/ruvector/diff-classifier.d.ts +0 -2
  136. package/dist/src/ruvector/diff-classifier.d.ts.map +1 -1
  137. package/dist/src/ruvector/diff-classifier.js +2 -14
  138. package/dist/src/ruvector/diff-classifier.js.map +1 -1
  139. package/dist/src/ruvector/index.d.ts +26 -9
  140. package/dist/src/ruvector/index.d.ts.map +1 -1
  141. package/dist/src/ruvector/index.js +3 -21
  142. package/dist/src/ruvector/index.js.map +1 -1
  143. package/dist/src/ruvector/ruvllm-wasm.js +2 -2
  144. package/dist/src/ruvector/ruvllm-wasm.js.map +1 -1
  145. package/dist/src/types.d.ts +0 -15
  146. package/dist/src/types.d.ts.map +1 -1
  147. package/dist/src/types.js +0 -18
  148. package/dist/src/types.js.map +1 -1
  149. package/dist/src/ui/dashboard.html +8763 -9765
  150. package/dist/src/ui/data/agent-avatars.html +763 -0
  151. package/dist/src/ui/data/agent-avatars.json +966 -0
  152. package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
  153. package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
  154. package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
  155. package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
  156. package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
  157. package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
  158. package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
  159. package/dist/src/ui/data/avatars/api-tester.svg +53 -0
  160. package/dist/src/ui/data/avatars/architecture.svg +54 -0
  161. package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
  162. package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
  163. package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
  164. package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
  165. package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
  166. package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
  167. package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
  168. package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
  169. package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
  170. package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
  171. package/dist/src/ui/data/avatars/coder.svg +58 -0
  172. package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
  173. package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
  174. package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
  175. package/dist/src/ui/data/avatars/content-creator.svg +54 -0
  176. package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
  177. package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
  178. package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
  179. package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
  180. package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
  181. package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
  182. package/dist/src/ui/data/avatars/defender.svg +53 -0
  183. package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
  184. package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
  185. package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
  186. package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
  187. package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
  188. package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
  189. package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
  190. package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
  191. package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
  192. package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
  193. package/dist/src/ui/data/avatars/game-designer.svg +54 -0
  194. package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
  195. package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
  196. package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
  197. package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
  198. package/dist/src/ui/data/avatars/input-validator.svg +53 -0
  199. package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
  200. package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
  201. package/dist/src/ui/data/avatars/judge.svg +55 -0
  202. package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
  203. package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
  204. package/dist/src/ui/data/avatars/level-designer.svg +53 -0
  205. package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
  206. package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
  207. package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
  208. package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
  209. package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
  210. package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
  211. package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
  212. package/dist/src/ui/data/avatars/model-qa.svg +58 -0
  213. package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
  214. package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
  215. package/dist/src/ui/data/avatars/path-validator.svg +54 -0
  216. package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
  217. package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
  218. package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
  219. package/dist/src/ui/data/avatars/planner.svg +55 -0
  220. package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
  221. package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
  222. package/dist/src/ui/data/avatars/product-manager.svg +54 -0
  223. package/dist/src/ui/data/avatars/production-validator.svg +54 -0
  224. package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
  225. package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
  226. package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
  227. package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
  228. package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
  229. package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
  230. package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
  231. package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
  232. package/dist/src/ui/data/avatars/recruitment.svg +58 -0
  233. package/dist/src/ui/data/avatars/refinement.svg +53 -0
  234. package/dist/src/ui/data/avatars/release-manager.svg +54 -0
  235. package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
  236. package/dist/src/ui/data/avatars/researcher.svg +58 -0
  237. package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
  238. package/dist/src/ui/data/avatars/reviewer.svg +53 -0
  239. package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
  240. package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
  241. package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
  242. package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
  243. package/dist/src/ui/data/avatars/security-architect.svg +54 -0
  244. package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
  245. package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
  246. package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
  247. package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
  248. package/dist/src/ui/data/avatars/social-media.svg +54 -0
  249. package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
  250. package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
  251. package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
  252. package/dist/src/ui/data/avatars/specification.svg +57 -0
  253. package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
  254. package/dist/src/ui/data/avatars/sre.svg +54 -0
  255. package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
  256. package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
  257. package/dist/src/ui/data/avatars/support-responder.svg +56 -0
  258. package/dist/src/ui/data/avatars/system-architect.svg +54 -0
  259. package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
  260. package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
  261. package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
  262. package/dist/src/ui/data/avatars/tester.svg +53 -0
  263. package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
  264. package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
  265. package/dist/src/ui/data/avatars/trial-director.svg +55 -0
  266. package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
  267. package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
  268. package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
  269. package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
  270. package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
  271. package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
  272. package/dist/src/ui/data/known-projects.json +1 -1
  273. package/dist/src/ui/data/mastermind-events.jsonl +28 -0
  274. package/dist/src/ui/orgs.html +1171 -0
  275. package/dist/src/ui/server.mjs +529 -43
  276. package/dist/src/update/index.js +1 -1
  277. package/dist/src/update/validator.js +8 -8
  278. package/dist/tsconfig.tsbuildinfo +1 -1
  279. package/package.json +2 -2
  280. package/.claude/agents/academic/academic-anthropologist.md +0 -126
  281. package/.claude/agents/academic/academic-geographer.md +0 -128
  282. package/.claude/agents/academic/academic-historian.md +0 -124
  283. package/.claude/agents/academic/academic-narratologist.md +0 -119
  284. package/.claude/agents/academic/academic-psychologist.md +0 -119
  285. package/.claude/agents/analysis/analyze-code-quality.md +0 -58
  286. package/.claude/agents/analysis/code-analyzer.md +0 -189
  287. package/.claude/agents/analysis/code-review/analyze-code-quality.md +0 -58
  288. package/.claude/agents/consensus/performance-benchmarker.md +0 -831
  289. package/.claude/agents/data/ml/data-ml-model.md +0 -76
  290. package/.claude/agents/development/dev-backend-api.md +0 -178
  291. package/.claude/agents/devops/ci-cd/ops-cicd-github.md +0 -52
  292. package/.claude/agents/documentation/api-docs/docs-api-openapi.md +0 -63
  293. package/.claude/agents/game-development/blender/blender-addon-engineer.md +0 -235
  294. package/.claude/agents/game-development/game-audio-engineer.md +0 -265
  295. package/.claude/agents/game-development/game-designer.md +0 -168
  296. package/.claude/agents/game-development/godot/godot-gameplay-scripter.md +0 -335
  297. package/.claude/agents/game-development/godot/godot-multiplayer-engineer.md +0 -298
  298. package/.claude/agents/game-development/godot/godot-shader-developer.md +0 -267
  299. package/.claude/agents/game-development/level-designer.md +0 -209
  300. package/.claude/agents/game-development/narrative-designer.md +0 -244
  301. package/.claude/agents/game-development/roblox-studio/roblox-avatar-creator.md +0 -298
  302. package/.claude/agents/game-development/roblox-studio/roblox-experience-designer.md +0 -306
  303. package/.claude/agents/game-development/roblox-studio/roblox-systems-scripter.md +0 -326
  304. package/.claude/agents/game-development/technical-artist.md +0 -230
  305. package/.claude/agents/game-development/unity/unity-architect.md +0 -272
  306. package/.claude/agents/game-development/unity/unity-editor-tool-developer.md +0 -311
  307. package/.claude/agents/game-development/unity/unity-multiplayer-engineer.md +0 -322
  308. package/.claude/agents/game-development/unity/unity-shader-graph-artist.md +0 -270
  309. package/.claude/agents/game-development/unreal-engine/unreal-multiplayer-architect.md +0 -314
  310. package/.claude/agents/game-development/unreal-engine/unreal-systems-engineer.md +0 -311
  311. package/.claude/agents/game-development/unreal-engine/unreal-technical-artist.md +0 -257
  312. package/.claude/agents/game-development/unreal-engine/unreal-world-builder.md +0 -274
  313. package/.claude/agents/github/release-swarm.md +0 -597
  314. package/.claude/agents/goal/agent.md +0 -804
  315. package/.claude/agents/goal/code-goal-planner.md +0 -445
  316. package/.claude/agents/marketing/marketing-ai-citation-strategist.md +0 -171
  317. package/.claude/agents/marketing/marketing-app-store-optimizer.md +0 -322
  318. package/.claude/agents/marketing/marketing-baidu-seo-specialist.md +0 -227
  319. package/.claude/agents/marketing/marketing-bilibili-content-strategist.md +0 -200
  320. package/.claude/agents/marketing/marketing-book-co-author.md +0 -111
  321. package/.claude/agents/marketing/marketing-carousel-growth-engine.md +0 -200
  322. package/.claude/agents/marketing/marketing-china-ecommerce-operator.md +0 -284
  323. package/.claude/agents/marketing/marketing-content-creator.md +0 -67
  324. package/.claude/agents/marketing/marketing-cross-border-ecommerce.md +0 -260
  325. package/.claude/agents/marketing/marketing-douyin-strategist.md +0 -150
  326. package/.claude/agents/marketing/marketing-growth-hacker.md +0 -54
  327. package/.claude/agents/marketing/marketing-instagram-curator.md +0 -114
  328. package/.claude/agents/marketing/marketing-kuaishou-strategist.md +0 -224
  329. package/.claude/agents/marketing/marketing-linkedin-content-creator.md +0 -215
  330. package/.claude/agents/marketing/marketing-livestream-commerce-coach.md +0 -306
  331. package/.claude/agents/marketing/marketing-podcast-strategist.md +0 -278
  332. package/.claude/agents/marketing/marketing-private-domain-operator.md +0 -309
  333. package/.claude/agents/marketing/marketing-reddit-community-builder.md +0 -124
  334. package/.claude/agents/marketing/marketing-seo-specialist.md +0 -279
  335. package/.claude/agents/marketing/marketing-short-video-editing-coach.md +0 -413
  336. package/.claude/agents/marketing/marketing-social-media-strategist.md +0 -125
  337. package/.claude/agents/marketing/marketing-tiktok-strategist.md +0 -126
  338. package/.claude/agents/marketing/marketing-twitter-engager.md +0 -127
  339. package/.claude/agents/marketing/marketing-wechat-official-account.md +0 -146
  340. package/.claude/agents/marketing/marketing-weibo-strategist.md +0 -241
  341. package/.claude/agents/marketing/marketing-xiaohongshu-specialist.md +0 -139
  342. package/.claude/agents/marketing/marketing-zhihu-strategist.md +0 -163
  343. package/.claude/agents/neural/safla-neural.md +0 -74
  344. package/.claude/agents/paid-media/paid-media-auditor.md +0 -71
  345. package/.claude/agents/paid-media/paid-media-creative-strategist.md +0 -71
  346. package/.claude/agents/paid-media/paid-media-paid-social-strategist.md +0 -71
  347. package/.claude/agents/paid-media/paid-media-ppc-strategist.md +0 -71
  348. package/.claude/agents/paid-media/paid-media-programmatic-buyer.md +0 -71
  349. package/.claude/agents/paid-media/paid-media-search-query-analyst.md +0 -71
  350. package/.claude/agents/paid-media/paid-media-tracking-specialist.md +0 -71
  351. package/.claude/agents/payments/agentic-payments.md +0 -126
  352. package/.claude/agents/product/product-behavioral-nudge-engine.md +0 -81
  353. package/.claude/agents/product/product-feedback-synthesizer.md +0 -119
  354. package/.claude/agents/product/product-manager.md +0 -469
  355. package/.claude/agents/product/product-sprint-prioritizer.md +0 -154
  356. package/.claude/agents/product/product-trend-researcher.md +0 -159
  357. package/.claude/agents/project-management/project-management-experiment-tracker.md +0 -199
  358. package/.claude/agents/project-management/project-management-jira-workflow-steward.md +0 -231
  359. package/.claude/agents/project-management/project-management-project-shepherd.md +0 -195
  360. package/.claude/agents/project-management/project-management-studio-operations.md +0 -201
  361. package/.claude/agents/project-management/project-management-studio-producer.md +0 -204
  362. package/.claude/agents/project-management/project-manager-senior.md +0 -136
  363. package/.claude/agents/reasoning/agent.md +0 -804
  364. package/.claude/agents/reasoning/goal-planner.md +0 -73
  365. package/.claude/agents/sales/sales-account-strategist.md +0 -228
  366. package/.claude/agents/sales/sales-coach.md +0 -272
  367. package/.claude/agents/sales/sales-deal-strategist.md +0 -181
  368. package/.claude/agents/sales/sales-discovery-coach.md +0 -226
  369. package/.claude/agents/sales/sales-engineer.md +0 -183
  370. package/.claude/agents/sales/sales-outbound-strategist.md +0 -202
  371. package/.claude/agents/sales/sales-pipeline-analyst.md +0 -268
  372. package/.claude/agents/sales/sales-proposal-strategist.md +0 -218
  373. package/.claude/agents/sona/sona-learning-optimizer.md +0 -65
  374. package/.claude/agents/spatial-computing/macos-spatial-metal-engineer.md +0 -338
  375. package/.claude/agents/spatial-computing/terminal-integration-specialist.md +0 -71
  376. package/.claude/agents/spatial-computing/visionos-spatial-engineer.md +0 -55
  377. package/.claude/agents/specialists/memory-specialist.md +0 -298
  378. package/.claude/agents/specialists/performance-engineer.md +0 -387
  379. package/.claude/agents/specialists/queen-coordinator.md +0 -67
  380. package/.claude/agents/specialists/security-architect.md +0 -154
  381. package/.claude/agents/specialized/accounts-payable-agent.md +0 -186
  382. package/.claude/agents/specialized/corporate-training-designer.md +0 -193
  383. package/.claude/agents/specialized/data-consolidation-agent.md +0 -61
  384. package/.claude/agents/specialized/government-digital-presales-consultant.md +0 -364
  385. package/.claude/agents/specialized/healthcare-marketing-compliance.md +0 -396
  386. package/.claude/agents/specialized/recruitment-specialist.md +0 -510
  387. package/.claude/agents/specialized/report-distribution-agent.md +0 -66
  388. package/.claude/agents/specialized/sales-data-extraction-agent.md +0 -68
  389. package/.claude/agents/specialized/specialized-french-consulting-market.md +0 -193
  390. package/.claude/agents/specialized/specialized-korean-business-navigator.md +0 -217
  391. package/.claude/agents/specialized/specialized-salesforce-architect.md +0 -181
  392. package/.claude/agents/specialized/study-abroad-advisor.md +0 -283
  393. package/.claude/agents/specialized/supply-chain-strategist.md +0 -583
  394. package/.claude/agents/sublinear/consensus-coordinator.md +0 -333
  395. package/.claude/agents/sublinear/matrix-optimizer.md +0 -180
  396. package/.claude/agents/sublinear/pagerank-analyzer.md +0 -295
  397. package/.claude/agents/sublinear/performance-optimizer.md +0 -363
  398. package/.claude/agents/sublinear/trading-predictor.md +0 -242
  399. package/.claude/agents/support/support-analytics-reporter.md +0 -366
  400. package/.claude/agents/support/support-executive-summary-generator.md +0 -213
  401. package/.claude/agents/support/support-finance-tracker.md +0 -443
  402. package/.claude/agents/support/support-infrastructure-maintainer.md +0 -619
  403. package/.claude/agents/support/support-legal-compliance-checker.md +0 -589
  404. package/.claude/agents/support/support-support-responder.md +0 -586
  405. package/.claude/agents/swarm/adaptive-coordinator.md +0 -364
  406. package/.claude/agents/swarm/hierarchical-coordinator.md +0 -318
  407. package/.claude/agents/templates/github-pr-manager.md +0 -155
  408. package/.claude/agents/templates/memory-coordinator.md +0 -163
  409. package/.claude/agents/templates/migration-plan.md +0 -724
  410. package/.claude/agents/templates/orchestrator-task.md +0 -120
  411. package/.claude/agents/templates/performance-analyzer.md +0 -179
  412. package/.claude/agents/templates/sparc-coordinator.md +0 -163
  413. package/.claude/agents/testing/testing-reality-checker.md +0 -237
  414. package/.claude/commands/analysis/token-efficiency.md +0 -42
  415. package/.claude/commands/optimization/README.md +0 -73
  416. package/.claude/commands/optimization/parallel-execution.md +0 -76
  417. package/.claude/commands/swarm/swarm-analysis.md +0 -62
  418. package/.claude/commands/swarm/swarm-background.md +0 -65
  419. package/.claude/commands/swarm/swarm-modes.md +0 -67
  420. package/.claude/commands/swarm/swarm-monitor.md +0 -54
  421. package/.claude/commands/swarm/swarm-status.md +0 -44
  422. package/.claude/commands/swarm/swarm-strategies.md +0 -76
  423. package/.claude/commands/training/model-update.md +0 -78
  424. package/.claude/commands/training/pattern-learn.md +0 -69
  425. package/.claude/commands/training/specialization.md +0 -92
  426. package/.claude/commands/verify/check.md +0 -106
  427. package/.claude/commands/verify/start.md +0 -105
  428. package/.claude/helpers/README.md +0 -105
  429. package/.claude/helpers/context-persistence-hook.mjs +0 -1988
  430. package/.claude/helpers/intelligence.cjs +0 -247
  431. package/.claude/helpers/learning-service.mjs +0 -1302
  432. package/.claude/helpers/memory-palace.cjs +0 -461
  433. package/.claude/helpers/memory.cjs +0 -84
  434. package/.claude/helpers/metrics-db.mjs +0 -488
  435. package/.claude/helpers/router.cjs +0 -559
  436. package/.claude/helpers/session.cjs +0 -126
  437. package/.claude/helpers/swarm-hooks.sh +0 -761
  438. package/.claude/helpers/toggle-statusline.cjs +0 -58
  439. package/.claude/helpers/token-tracker.cjs +0 -934
  440. package/.claude/skills/agentdb-advanced/SKILL.md +0 -549
  441. package/.claude/skills/agentdb-learning/SKILL.md +0 -544
  442. package/.claude/skills/agentdb-memory-patterns/SKILL.md +0 -337
  443. package/.claude/skills/agentdb-optimization/SKILL.md +0 -508
  444. package/.claude/skills/agentdb-vector-search/SKILL.md +0 -335
  445. package/.claude/skills/agentic-integration/SKILL.md +0 -265
  446. package/.claude/skills/cli-modernization/SKILL.md +0 -950
  447. package/.claude/skills/core-implementation/SKILL.md +0 -892
  448. package/.claude/skills/ddd-architecture/SKILL.md +0 -444
  449. package/.claude/skills/github-code-review/SKILL.md +0 -1147
  450. package/.claude/skills/github-multi-repo/SKILL.md +0 -912
  451. package/.claude/skills/github-project-management/SKILL.md +0 -1245
  452. package/.claude/skills/github-release-management/SKILL.md +0 -1118
  453. package/.claude/skills/github-workflow-automation/SKILL.md +0 -1107
  454. package/.claude/skills/mcp-optimization/SKILL.md +0 -837
  455. package/.claude/skills/memory-unification/SKILL.md +0 -196
  456. package/.claude/skills/performance-optimization/SKILL.md +0 -416
  457. package/.claude/skills/reasoningbank-agentdb/SKILL.md +0 -444
  458. package/.claude/skills/reasoningbank-intelligence/SKILL.md +0 -199
  459. package/.claude/skills/security-hardening/SKILL.md +0 -101
  460. package/.claude/skills/stream-chain/SKILL.md +0 -560
  461. package/.claude/skills/swarm-coordination/SKILL.md +0 -451
  462. package/bundled-graph/dist/src/analyze.d.ts +0 -32
  463. package/bundled-graph/dist/src/analyze.d.ts.map +0 -1
  464. package/bundled-graph/dist/src/analyze.js +0 -297
  465. package/bundled-graph/dist/src/analyze.js.map +0 -1
  466. package/bundled-graph/dist/src/build.d.ts +0 -8
  467. package/bundled-graph/dist/src/build.d.ts.map +0 -1
  468. package/bundled-graph/dist/src/build.js.map +0 -1
  469. package/bundled-graph/dist/src/cache.d.ts +0 -12
  470. package/bundled-graph/dist/src/cache.d.ts.map +0 -1
  471. package/bundled-graph/dist/src/cache.js +0 -43
  472. package/bundled-graph/dist/src/cache.js.map +0 -1
  473. package/bundled-graph/dist/src/cluster.d.ts +0 -5
  474. package/bundled-graph/dist/src/cluster.d.ts.map +0 -1
  475. package/bundled-graph/dist/src/cluster.js.map +0 -1
  476. package/bundled-graph/dist/src/detect.d.ts +0 -21
  477. package/bundled-graph/dist/src/detect.d.ts.map +0 -1
  478. package/bundled-graph/dist/src/detect.js +0 -195
  479. package/bundled-graph/dist/src/detect.js.map +0 -1
  480. package/bundled-graph/dist/src/export.d.ts +0 -21
  481. package/bundled-graph/dist/src/export.d.ts.map +0 -1
  482. package/bundled-graph/dist/src/export.js +0 -68
  483. package/bundled-graph/dist/src/export.js.map +0 -1
  484. package/bundled-graph/dist/src/extract/index.d.ts +0 -20
  485. package/bundled-graph/dist/src/extract/index.d.ts.map +0 -1
  486. package/bundled-graph/dist/src/extract/index.js +0 -158
  487. package/bundled-graph/dist/src/extract/index.js.map +0 -1
  488. package/bundled-graph/dist/src/extract/languages/c.d.ts +0 -3
  489. package/bundled-graph/dist/src/extract/languages/c.d.ts.map +0 -1
  490. package/bundled-graph/dist/src/extract/languages/c.js +0 -88
  491. package/bundled-graph/dist/src/extract/languages/c.js.map +0 -1
  492. package/bundled-graph/dist/src/extract/languages/cpp.d.ts +0 -3
  493. package/bundled-graph/dist/src/extract/languages/cpp.d.ts.map +0 -1
  494. package/bundled-graph/dist/src/extract/languages/cpp.js +0 -121
  495. package/bundled-graph/dist/src/extract/languages/cpp.js.map +0 -1
  496. package/bundled-graph/dist/src/extract/languages/csharp.d.ts +0 -3
  497. package/bundled-graph/dist/src/extract/languages/csharp.d.ts.map +0 -1
  498. package/bundled-graph/dist/src/extract/languages/csharp.js +0 -121
  499. package/bundled-graph/dist/src/extract/languages/csharp.js.map +0 -1
  500. package/bundled-graph/dist/src/extract/languages/go.d.ts +0 -3
  501. package/bundled-graph/dist/src/extract/languages/go.d.ts.map +0 -1
  502. package/bundled-graph/dist/src/extract/languages/go.js +0 -181
  503. package/bundled-graph/dist/src/extract/languages/go.js.map +0 -1
  504. package/bundled-graph/dist/src/extract/languages/java.d.ts +0 -3
  505. package/bundled-graph/dist/src/extract/languages/java.d.ts.map +0 -1
  506. package/bundled-graph/dist/src/extract/languages/java.js +0 -117
  507. package/bundled-graph/dist/src/extract/languages/java.js.map +0 -1
  508. package/bundled-graph/dist/src/extract/languages/kotlin.d.ts +0 -3
  509. package/bundled-graph/dist/src/extract/languages/kotlin.d.ts.map +0 -1
  510. package/bundled-graph/dist/src/extract/languages/kotlin.js +0 -112
  511. package/bundled-graph/dist/src/extract/languages/kotlin.js.map +0 -1
  512. package/bundled-graph/dist/src/extract/languages/php.d.ts +0 -3
  513. package/bundled-graph/dist/src/extract/languages/php.d.ts.map +0 -1
  514. package/bundled-graph/dist/src/extract/languages/php.js +0 -130
  515. package/bundled-graph/dist/src/extract/languages/php.js.map +0 -1
  516. package/bundled-graph/dist/src/extract/languages/python.d.ts +0 -3
  517. package/bundled-graph/dist/src/extract/languages/python.d.ts.map +0 -1
  518. package/bundled-graph/dist/src/extract/languages/python.js +0 -230
  519. package/bundled-graph/dist/src/extract/languages/python.js.map +0 -1
  520. package/bundled-graph/dist/src/extract/languages/ruby.d.ts +0 -3
  521. package/bundled-graph/dist/src/extract/languages/ruby.d.ts.map +0 -1
  522. package/bundled-graph/dist/src/extract/languages/ruby.js +0 -120
  523. package/bundled-graph/dist/src/extract/languages/ruby.js.map +0 -1
  524. package/bundled-graph/dist/src/extract/languages/rust.d.ts +0 -3
  525. package/bundled-graph/dist/src/extract/languages/rust.d.ts.map +0 -1
  526. package/bundled-graph/dist/src/extract/languages/rust.js +0 -195
  527. package/bundled-graph/dist/src/extract/languages/rust.js.map +0 -1
  528. package/bundled-graph/dist/src/extract/languages/scala.d.ts +0 -3
  529. package/bundled-graph/dist/src/extract/languages/scala.d.ts.map +0 -1
  530. package/bundled-graph/dist/src/extract/languages/scala.js +0 -110
  531. package/bundled-graph/dist/src/extract/languages/scala.js.map +0 -1
  532. package/bundled-graph/dist/src/extract/languages/swift.d.ts +0 -3
  533. package/bundled-graph/dist/src/extract/languages/swift.d.ts.map +0 -1
  534. package/bundled-graph/dist/src/extract/languages/swift.js +0 -122
  535. package/bundled-graph/dist/src/extract/languages/swift.js.map +0 -1
  536. package/bundled-graph/dist/src/extract/languages/typescript.d.ts +0 -3
  537. package/bundled-graph/dist/src/extract/languages/typescript.d.ts.map +0 -1
  538. package/bundled-graph/dist/src/extract/languages/typescript.js +0 -295
  539. package/bundled-graph/dist/src/extract/languages/typescript.js.map +0 -1
  540. package/bundled-graph/dist/src/extract/semantic.d.ts +0 -38
  541. package/bundled-graph/dist/src/extract/semantic.d.ts.map +0 -1
  542. package/bundled-graph/dist/src/extract/semantic.js +0 -242
  543. package/bundled-graph/dist/src/extract/semantic.js.map +0 -1
  544. package/bundled-graph/dist/src/extract/tree-sitter-runner.d.ts +0 -48
  545. package/bundled-graph/dist/src/extract/tree-sitter-runner.d.ts.map +0 -1
  546. package/bundled-graph/dist/src/extract/tree-sitter-runner.js +0 -137
  547. package/bundled-graph/dist/src/extract/tree-sitter-runner.js.map +0 -1
  548. package/bundled-graph/dist/src/extract/types.d.ts +0 -7
  549. package/bundled-graph/dist/src/extract/types.d.ts.map +0 -1
  550. package/bundled-graph/dist/src/extract/types.js +0 -2
  551. package/bundled-graph/dist/src/extract/types.js.map +0 -1
  552. package/bundled-graph/dist/src/index.d.ts +0 -28
  553. package/bundled-graph/dist/src/index.d.ts.map +0 -1
  554. package/bundled-graph/dist/src/index.js +0 -26
  555. package/bundled-graph/dist/src/index.js.map +0 -1
  556. package/bundled-graph/dist/src/pipeline.d.ts +0 -27
  557. package/bundled-graph/dist/src/pipeline.d.ts.map +0 -1
  558. package/bundled-graph/dist/src/pipeline.js +0 -269
  559. package/bundled-graph/dist/src/pipeline.js.map +0 -1
  560. package/bundled-graph/dist/src/report.d.ts +0 -26
  561. package/bundled-graph/dist/src/report.d.ts.map +0 -1
  562. package/bundled-graph/dist/src/report.js +0 -214
  563. package/bundled-graph/dist/src/report.js.map +0 -1
  564. package/bundled-graph/dist/src/types.d.ts +0 -124
  565. package/bundled-graph/dist/src/types.d.ts.map +0 -1
  566. package/bundled-graph/dist/src/types.js +0 -2
  567. package/bundled-graph/dist/src/types.js.map +0 -1
  568. package/bundled-graph/dist/src/visualize.d.ts +0 -4
  569. package/bundled-graph/dist/src/visualize.d.ts.map +0 -1
  570. package/bundled-graph/dist/src/visualize.js +0 -574
  571. package/bundled-graph/dist/src/visualize.js.map +0 -1
  572. package/bundled-graph/dist/tsconfig.tsbuildinfo +0 -1
  573. package/dist/src/ui/dashboard-v2.html +0 -4576
@@ -1,4576 +0,0 @@
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; min-width: 0; }
78
- #feed-scroll::-webkit-scrollbar { width: 3px; }
79
- #feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
80
-
81
- /* feed entries */
82
- .feed-entry { display: flex; align-items: flex-start; gap: 10px; padding: 5px 18px; cursor: pointer; transition: background 0.08s; }
83
- .feed-entry:hover { background: var(--surface-hi); }
84
- .feed-entry.selected { background: var(--accent-dim); }
85
- .feed-ico { width: 20px; height: 20px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-size: 10px; flex-shrink: 0; margin-top: 1px; }
86
- .feed-body { flex: 1; min-width: 0; }
87
- .feed-lbl { font-size: 13px; color: var(--text-hi); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88
- .feed-detail { font-size: 11px; color: var(--text-lo); margin-top: 1px; font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
89
- .feed-ts { font-size: 10px; color: var(--text-xs); white-space: nowrap; flex-shrink: 0; margin-top: 3px; font-family: var(--mono); }
90
- .feed-entry.k-user .feed-lbl { color: var(--text-mid); font-style: italic; }
91
- .feed-entry.errored .feed-lbl { color: var(--red); }
92
- .feed-entry.errored .feed-ico { background: oklch(60% 0.18 25 / 0.14); color: oklch(60% 0.18 25); }
93
-
94
- /* group row */
95
- .feed-group { display: flex; align-items: center; gap: 8px; padding: 3px 18px 3px 48px; cursor: pointer; }
96
- .feed-group:hover .fg-label { color: var(--text-mid); }
97
- .fg-label { font-size: 11px; color: var(--text-lo); }
98
- .fg-expand { font-size: 10px; color: var(--text-xs); margin-left: 4px; }
99
-
100
- /* category icon colors */
101
- .cat-file { background: oklch(65% 0.15 150 / 0.14); color: oklch(65% 0.15 150); }
102
- .cat-bash { background: oklch(65% 0.12 240 / 0.14); color: oklch(65% 0.12 240); }
103
- .cat-agent { background: oklch(65% 0.13 290 / 0.14); color: oklch(65% 0.13 290); }
104
- .cat-mcp { background: oklch(65% 0.12 195 / 0.14); color: oklch(65% 0.12 195); }
105
- .cat-search { background: oklch(65% 0.14 35 / 0.14); color: oklch(65% 0.14 35); }
106
- .cat-skill { background: oklch(72% 0.18 75 / 0.14); color: oklch(72% 0.18 75); }
107
- .cat-task { background: oklch(62% 0.12 55 / 0.14); color: oklch(72% 0.12 55); }
108
- .cat-mem { background: oklch(62% 0.11 160 / 0.14); color: oklch(68% 0.11 160); }
109
- .cat-user { background: oklch(55% 0.08 75 / 0.12); color: oklch(62% 0.06 75); }
110
- .cat-other { background: var(--surface-hi); color: var(--text-lo); }
111
-
112
- .feed-divider { height: 1px; background: var(--border); margin: 2px 18px; opacity: 0.4; }
113
- .feed-empty { padding: 40px 18px; color: var(--text-lo); font-size: 13px; }
114
-
115
- /* ── detail panel ─────────────────────────────────────────── */
116
- #detail-panel {
117
- position: absolute; top: 0; right: 252px; bottom: 0;
118
- width: 0; overflow: hidden;
119
- background: oklch(13% 0.009 55);
120
- border-left: 1px solid var(--border);
121
- border-right: 1px solid var(--border);
122
- transition: width 0.22s cubic-bezier(0.16, 1, 0.3, 1);
123
- display: flex; flex-direction: column; z-index: 10;
124
- }
125
- #detail-panel.open { width: 280px; }
126
- #detail-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
127
- #detail-head h3 { font-size: 12px; font-weight: 600; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
128
- #detail-close { font-size: 14px; color: var(--text-lo); background: none; border: none; cursor: pointer; padding: 0 2px; line-height: 1; }
129
- #detail-close:hover { color: var(--text-hi); }
130
- #detail-body { flex: 1; overflow-y: auto; padding: 12px 14px; }
131
- #detail-body::-webkit-scrollbar { width: 3px; }
132
- #detail-body::-webkit-scrollbar-thumb { background: var(--border); }
133
- .d-cat-pill { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; border-radius: 10px; padding: 2px 8px; margin-bottom: 10px; }
134
- .d-row { margin-bottom: 10px; }
135
- .d-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 3px; }
136
- .d-val { font-size: 12px; color: var(--text-hi); word-break: break-word; }
137
- .d-val.mono { font-family: var(--mono); font-size: 11px; }
138
- .d-val.error { color: var(--red); }
139
-
140
- /* ── metrics pane ────────────────────────────────────────── */
141
- #metrics-pane { width: 252px; flex-shrink: 0; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 18px; }
142
- #metrics-pane::-webkit-scrollbar { width: 3px; }
143
- #metrics-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
144
- .m-group-title { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-lo); padding-bottom: 8px; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
145
- .m-row { display: flex; align-items: baseline; justify-content: space-between; padding: 3px 0; }
146
- .m-name { font-size: 12px; color: var(--text-mid); }
147
- .m-val { font-size: 13px; font-weight: 600; color: var(--text-hi); font-family: var(--mono); }
148
- .m-val.gold { color: var(--accent); }
149
- .mini-loop { padding: 6px 0; border-bottom: 1px solid var(--border); }
150
- .mini-loop:last-child { border-bottom: none; }
151
- .ml-name { font-size: 12px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
152
- .ml-meta { font-size: 11px; color: var(--text-lo); margin-top: 1px; display: flex; align-items: center; gap: 5px; }
153
- .ml-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; background: var(--green); }
154
- .mini-sess { padding: 5px 0; cursor: pointer; border-bottom: 1px solid var(--border); }
155
- .mini-sess:last-child { border-bottom: none; }
156
- .mini-sess:hover .ms-prompt { color: var(--accent); }
157
- .ms-prompt { font-size: 12px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; transition: color 0.1s; }
158
- .ms-meta { font-size: 11px; color: var(--text-lo); margin-top: 1px; }
159
-
160
- /* ── views with scroll ───────────────────────────────────── */
161
- .vscroll { flex: 1; overflow-y: auto; padding: 22px 24px; }
162
- .vscroll::-webkit-scrollbar { width: 4px; }
163
- .vscroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
164
- .pg-title { font-size: 18px; font-weight: 600; color: var(--text-hi); margin-bottom: 4px; }
165
- .pg-sub { font-size: 13px; color: var(--text-lo); margin-bottom: 22px; }
166
-
167
- /* filter bar */
168
- .filter-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
169
- .filter-input { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 6px 10px; font-size: 12px; color: var(--text-hi); font-family: var(--sans); outline: none; transition: border-color 0.1s; }
170
- .filter-input::placeholder { color: var(--text-lo); }
171
- .filter-input:focus { border-color: var(--accent); }
172
-
173
- /* projects */
174
- .proj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; }
175
- .proj-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; cursor: pointer; transition: border-color 0.12s, background 0.12s; position: relative; }
176
- .proj-card:hover { border-color: var(--accent); background: var(--surface-hi); }
177
- .proj-card.current { border-color: var(--accent); }
178
- .proj-card-badge { position: absolute; top: 10px; right: 10px; font-size: 10px; background: var(--accent-dim); color: var(--accent); border-radius: 8px; padding: 1px 7px; }
179
- .proj-card-name { font-size: 14px; font-weight: 500; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 4px; }
180
- .proj-card-path { font-size: 10px; color: var(--text-lo); font-family: var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 10px; }
181
- .proj-card-stats { display: flex; gap: 14px; }
182
- .ps-v { font-size: 16px; font-weight: 700; color: var(--text-hi); }
183
- .ps-l { font-size: 10px; color: var(--text-lo); margin-top: 1px; }
184
-
185
- /* sessions */
186
- .sess-list { display: flex; flex-direction: column; gap: 6px; }
187
- .sess-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; cursor: pointer; transition: border-color 0.12s, background 0.12s; }
188
- .sess-row:hover { border-color: var(--accent); background: var(--surface-hi); }
189
- .sr-top { display: flex; align-items: baseline; gap: 10px; }
190
- .sr-prompt { font-size: 13px; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
191
- .sr-time { font-size: 11px; color: var(--text-lo); white-space: nowrap; font-family: var(--mono); }
192
- .sr-meta { font-size: 11px; color: var(--text-lo); margin-top: 3px; }
193
-
194
- /* loops */
195
- .loop-list { display: flex; flex-direction: column; gap: 8px; }
196
- .loop-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; display: flex; align-items: center; gap: 12px; }
197
- .loop-ico { width: 30px; height: 30px; border-radius: 6px; background: var(--accent-dim); display: flex; align-items: center; justify-content: center; font-size: 13px; color: var(--accent); flex-shrink: 0; }
198
- .loop-body { flex: 1; min-width: 0; }
199
- .loop-name { font-size: 13px; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
200
- .loop-meta { font-size: 11px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
201
- .loop-status { font-size: 11px; padding: 2px 8px; border-radius: 10px; flex-shrink: 0; }
202
- .loop-status.active { background: oklch(65% 0.15 150 / 0.12); color: var(--green); }
203
- .loop-status.stopped { background: var(--surface-hi); color: var(--text-lo); }
204
-
205
- /* memory */
206
- .mem-section { margin-bottom: 22px; }
207
- .mem-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); margin-bottom: 10px; }
208
- .drawer-item { padding: 8px 12px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 4px; }
209
- .dr-key { font-size: 12px; color: var(--accent); font-family: var(--mono); }
210
- .dr-val { font-size: 12px; color: var(--text-hi); margin-top: 2px; word-break: break-word; }
211
- .dr-ts { font-size: 10px; color: var(--text-lo); margin-top: 2px; }
212
- .drawer-item.hidden { display: none; }
213
-
214
- /* orgs */
215
- .org-list { display: flex; flex-direction: column; gap: 6px; }
216
- .org-row { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 11px 14px; cursor: pointer; transition: border-color 0.12s; }
217
- .org-row:hover { border-color: var(--accent); }
218
- .org-name { font-size: 13px; color: var(--text-hi); font-weight: 500; }
219
- .org-meta { font-size: 11px; color: var(--text-lo); margin-top: 3px; }
220
-
221
- /* ── alerts rail ─────────────────────────────────────────── */
222
- #alerts-rail { display: none; flex-shrink: 0; border-bottom: 1px solid var(--border); padding: 0 16px; min-height: 36px; flex-direction: row; gap: 8px; align-items: center; overflow-x: auto; }
223
- #alerts-rail.has-alerts { display: flex; }
224
- .alert-item { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; padding: 4px 10px; border-radius: 10px; cursor: default; white-space: nowrap; user-select: none; }
225
- .alert-item:hover .al-x { opacity: 1; }
226
- .alert-warn { background: oklch(72% 0.18 75 / 0.1); color: oklch(78% 0.18 75); border: 1px solid oklch(72% 0.18 75 / 0.25); }
227
- .alert-crit { background: oklch(60% 0.18 25 / 0.12); color: oklch(70% 0.18 25); border: 1px solid oklch(60% 0.18 25 / 0.3); }
228
- .al-ico { font-size: 10px; }
229
- .al-x { opacity: 0; transition: opacity 0.1s; cursor: pointer; font-size: 10px; margin-left: 2px; color: inherit; }
230
-
231
- /* ── session context bar ─────────────────────────────────── */
232
- #sess-ctx { display: none; flex-shrink: 0; align-items: center; gap: 8px; padding: 5px 18px; border-bottom: 1px solid var(--border); background: oklch(72% 0.18 75 / 0.06); }
233
- #sess-ctx.show { display: flex; }
234
- .sctx-back { font-size: 11px; color: var(--accent); background: none; border: none; cursor: pointer; padding: 0; white-space: nowrap; }
235
- .sctx-back:hover { text-decoration: underline; }
236
- .sctx-sep { color: var(--text-xs); font-size: 11px; }
237
- .sctx-label { font-size: 12px; color: var(--text-hi); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
238
- .sctx-live { font-size: 11px; color: var(--text-lo); background: none; border: 1px solid var(--border); border-radius: var(--r); padding: 2px 9px; cursor: pointer; white-space: nowrap; transition: color 0.1s, border-color 0.1s; }
239
- .sctx-live:hover { color: var(--green); border-color: var(--green); }
240
-
241
- /* enhanced session row → view affordance */
242
- .sr-top { position: relative; }
243
- .sr-view { position: absolute; right: 0; top: 0; font-size: 11px; color: var(--accent); opacity: 0; transition: opacity 0.1s; }
244
- .sess-row:hover .sr-view { opacity: 1; }
245
- .sr-tags { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
246
- .sr-tag { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--surface-hi); color: var(--text-lo); }
247
- .sr-tag.err { background: oklch(60% 0.18 25 / 0.1); color: oklch(70% 0.18 25); }
248
-
249
- /* loops detail expand */
250
- .loop-row { cursor: pointer; }
251
- .loop-row:hover { background: var(--surface-hi); }
252
- .loop-row.open { border-color: var(--accent); }
253
- .loop-expand { display: none; padding: 10px 14px 12px 56px; background: oklch(14% 0.009 55); border-top: 1px solid var(--border); border-radius: 0 0 6px 6px; }
254
- .loop-row.open + .loop-expand { display: block; }
255
- .le-row { display: flex; gap: 10px; margin-bottom: 6px; }
256
- .le-lbl { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-lo); min-width: 70px; flex-shrink: 0; padding-top: 1px; }
257
- .le-val { font-size: 12px; color: var(--text-hi); word-break: break-all; }
258
- .le-val.mono { font-family: var(--mono); font-size: 11px; }
259
-
260
- /* 7-day sparkline */
261
- .spark-wrap { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 10px; }
262
- .spark-lbl { font-size: 10px; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-lo); margin-bottom: 6px; }
263
- .sparkline { display: flex; align-items: flex-end; gap: 3px; height: 24px; }
264
- .spark-bar { flex: 1; border-radius: 2px 2px 0 0; background: oklch(72% 0.18 75 / 0.25); min-height: 2px; transition: background 0.1s; }
265
- .spark-bar.spark-today { background: oklch(72% 0.18 75 / 0.75); }
266
- .spark-bar:hover { background: oklch(72% 0.18 75 / 0.6); }
267
-
268
- /* shared */
269
- .empty { padding: 44px 18px; color: var(--text-lo); text-align: center; }
270
- .empty-ico { font-size: 22px; margin-bottom: 10px; }
271
- .loading-txt { padding: 18px; color: var(--text-lo); font-size: 12px; }
272
- ::-webkit-scrollbar { width: 4px; height: 4px; }
273
- ::-webkit-scrollbar-track { background: transparent; }
274
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
275
-
276
- /* ── command palette ─────────────────────────────────────── */
277
- #cmd-backdrop { display:none; position:fixed; inset:0; z-index:100; background: oklch(5% 0 0 / 0.6); backdrop-filter:blur(2px); }
278
- #cmd-backdrop.open { display:block; }
279
- #cmd-palette { display:none; position:fixed; top:18%; left:50%; transform:translateX(-50%); width:540px; max-width:90vw; background: oklch(16% 0.009 55); border:1px solid oklch(32% 0.008 55); border-radius:10px; z-index:101; box-shadow:0 24px 60px oklch(5% 0 0 / 0.7); overflow:hidden; flex-direction:column; }
280
- #cmd-palette.open { display:flex; }
281
- #cmd-input-wrap { display:flex; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid var(--border); }
282
- #cmd-ico { font-size:13px; color:var(--text-lo); flex-shrink:0; }
283
- #cmd-input { flex:1; background:transparent; border:none; font-size:14px; color:var(--text-hi); font-family:var(--sans); outline:none; }
284
- #cmd-input::placeholder { color:var(--text-lo); }
285
- #cmd-results { max-height:340px; overflow-y:auto; padding:4px 0; }
286
- .cmd-group-lbl { font-size:10px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-xs); padding:8px 14px 3px; }
287
- .cmd-item { display:flex; align-items:center; gap:10px; padding:8px 14px; cursor:pointer; transition:background 0.07s; }
288
- .cmd-item:hover, .cmd-item.focused { background:var(--accent-dim); }
289
- .cmd-item .ci-ico { width:20px; text-align:center; color:var(--text-lo); flex-shrink:0; font-size:12px; }
290
- .cmd-item-body { flex:1; min-width:0; }
291
- .cmd-item .ci-title { font-size:13px; color:var(--text-hi); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
292
- .cmd-item .ci-sub { font-size:11px; color:var(--text-lo); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
293
- .cmd-item.focused .ci-title { color:var(--accent); }
294
- .cmd-empty { padding:28px 16px; text-align:center; color:var(--text-lo); font-size:13px; }
295
- .cmd-footer { padding:6px 14px; border-top:1px solid var(--border); display:flex; gap:16px; }
296
- .cmd-key { font-size:10px; color:var(--text-xs); display:flex; align-items:center; gap:4px; }
297
- .cmd-key kbd { background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 5px; font-family:var(--mono); font-size:10px; color:var(--text-lo); }
298
-
299
- /* ── feed time filter ────────────────────────────────────── */
300
- #feed-time-filter { display:flex; gap:4px; padding:5px 18px; border-bottom:1px solid var(--border); flex-shrink:0; align-items:center; }
301
- .tf-lbl { font-size:10px; color:var(--text-xs); letter-spacing:0.06em; margin-right:4px; }
302
- .tf-btn { font-size:11px; padding:2px 9px; background:transparent; border:1px solid transparent; border-radius:8px; color:var(--text-lo); cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
303
- .tf-btn:hover { color:var(--text-hi); }
304
- .tf-btn.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
305
-
306
- /* ── keyboard hint ───────────────────────────────────────── */
307
- .kb-hint { margin-left:auto; font-size:10px; color:var(--text-xs); display:flex; align-items:center; gap:6px; }
308
- .kb-hint kbd { background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 5px; font-family:var(--mono); font-size:10px; }
309
-
310
- /* ── topbar cost badge ───────────────────────────────────── */
311
- #topbar-cost { font-size:11px; color:var(--accent); font-family:var(--mono); font-weight:600; opacity:0; transition:opacity 0.3s; }
312
- #topbar-cost.loaded { opacity:1; }
313
-
314
- /* ── feed search bar ─────────────────────────────────────── */
315
- #feed-search { display:none; flex-shrink:0; padding:4px 18px; border-bottom:1px solid var(--border); align-items:center; gap:8px; }
316
- #feed-search.open { display:flex; }
317
- #feed-search-input { flex:1; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:4px 10px; font-size:12px; color:var(--text-hi); font-family:var(--sans); outline:none; transition:border-color 0.1s; }
318
- #feed-search-input::placeholder { color:var(--text-lo); }
319
- #feed-search-input:focus { border-color:var(--accent); }
320
- #feed-search-count { font-size:11px; color:var(--text-lo); white-space:nowrap; font-family:var(--mono); }
321
- #feed-search-close { font-size:12px; color:var(--text-lo); background:none; border:none; cursor:pointer; padding:0 2px; }
322
- #feed-search-close:hover { color:var(--text-hi); }
323
-
324
- /* ── memory namespace tabs ───────────────────────────────── */
325
- #mem-ns-tabs { display:flex; flex-wrap:wrap; gap:4px; margin-bottom:14px; }
326
- .ns-tab { font-size:11px; padding:3px 10px; border-radius:8px; border:1px solid var(--border); background:transparent; color:var(--text-lo); cursor:pointer; font-family:var(--sans); transition:color 0.1s, background 0.1s; }
327
- .ns-tab:hover { color:var(--text-hi); }
328
- .ns-tab.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
329
-
330
- /* ── copy session button ─────────────────────────────────── */
331
- .sess-copy-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.1s; line-height:1.4; white-space:nowrap; }
332
- .sess-copy-btn:hover { color:var(--text-hi); }
333
- .sess-copy-btn.copied { color:var(--green); border-color:var(--green); }
334
-
335
- /* ── session timeline bar ────────────────────────────────── */
336
- #feed-timeline { height:6px; flex-shrink:0; display:flex; overflow:hidden; border-bottom:1px solid var(--border); }
337
- .tl-seg { height:100%; flex-shrink:0; transition:opacity 0.1s; }
338
- .tl-seg:hover { opacity:0.7; }
339
-
340
- /* ── feed density toggle ─────────────────────────────────── */
341
- #feed-pane.compact .feed-entry { padding: 2px 18px; }
342
- #feed-pane.compact .feed-lbl { font-size:12px; }
343
- #feed-pane.compact .feed-detail { display:none; }
344
- #feed-pane.compact .feed-ico { width:16px; height:16px; font-size:9px; }
345
- .density-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.1s; line-height:1.4; white-space:nowrap; }
346
- .density-btn:hover { color:var(--text-hi); }
347
- .density-btn.compact-on { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.5); }
348
-
349
- /* ── tool usage breakdown ────────────────────────────────── */
350
- .m-breakdown { margin-top:4px; }
351
- .tb-row { display:flex; align-items:center; gap:6px; margin-bottom:4px; }
352
- .tb-lbl { font-size:10px; color:var(--text-lo); width:44px; flex-shrink:0; text-align:right; }
353
- .tb-bar-wrap { flex:1; height:5px; background:var(--surface-hi); border-radius:3px; overflow:hidden; }
354
- .tb-bar { height:100%; border-radius:3px; }
355
- .tb-count { font-size:10px; color:var(--text-xs); font-family:var(--mono); width:22px; text-align:right; }
356
-
357
- /* ── week-over-week delta ────────────────────────────────── */
358
- .wow-delta { font-size:11px; margin-left:6px; }
359
- .wow-up { color:oklch(60% 0.18 25); }
360
- .wow-down { color:oklch(65% 0.15 150); }
361
- .wow-flat { color:var(--text-lo); }
362
-
363
- /* ── live tail ───────────────────────────────────────────── */
364
- .live-tail-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.15s, border-color 0.15s; line-height:1.4; white-space:nowrap; }
365
- .live-tail-btn:hover { color:var(--text-hi); }
366
- .live-tail-btn.on { color:var(--green); border-color:oklch(65% 0.15 150 / 0.5); }
367
- @keyframes live-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
368
- #btn-live-tail.on { animation: live-pulse 2s ease-in-out infinite; }
369
- @media (prefers-reduced-motion: reduce) { #btn-live-tail.on { animation:none; } }
370
-
371
- /* ── calendar heatmap ────────────────────────────────────── */
372
- .cal-grid { display:grid; grid-template-rows:repeat(7,9px); grid-auto-flow:column; grid-auto-columns:9px; gap:2px; margin-top:6px; }
373
- .cal-cell { border-radius:2px; background:var(--surface-hi); }
374
- .cal-cell.cal-1 { background:oklch(72% 0.18 75 / 0.22); }
375
- .cal-cell.cal-2 { background:oklch(72% 0.18 75 / 0.42); }
376
- .cal-cell.cal-3 { background:oklch(72% 0.18 75 / 0.65); }
377
- .cal-cell.cal-4 { background:oklch(72% 0.18 75 / 0.85); }
378
- .cal-cell.cal-today { outline:1px solid var(--accent); outline-offset:-1px; }
379
-
380
- /* ── session bookmark ────────────────────────────────────── */
381
- .sess-star { font-size:13px; background:none; border:none; cursor:pointer; color:var(--text-xs); padding:0 2px; transition:color 0.1s; line-height:1; flex-shrink:0; }
382
- .sess-star:hover { color:var(--text-lo); }
383
- .sess-star.on { color:oklch(78% 0.18 75); }
384
- .sr-starred { font-size:10px; color:oklch(78% 0.18 75); margin-left:auto; }
385
- #sess-star-filter { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); margin-left:auto; }
386
- #sess-star-filter:hover { color:var(--text-hi); }
387
- #sess-star-filter.on { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border-color:oklch(72% 0.18 75 / 0.3); }
388
-
389
- /* ── forecast row ────────────────────────────────────────── */
390
- .m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
391
-
392
- /* ── session date groups ─────────────────────────────────── */
393
- .sg-section { margin-bottom:4px; }
394
- .sg-header { display:flex; align-items:center; gap:6px; padding:5px 2px 3px; cursor:pointer; user-select:none; }
395
- .sg-title { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); font-weight:600; }
396
- .sg-count { font-size:10px; color:var(--text-lo); background:var(--surface-hi); border-radius:8px; padding:1px 6px; }
397
- .sg-toggle { font-size:10px; color:var(--text-xs); margin-left:auto; }
398
- .sg-header:hover .sg-title { color:var(--text-lo); }
399
- .sg-body.collapsed { display:none; }
400
-
401
- /* ── session heatmap calendar ─────────────────────────────── */
402
- #sess-heatmap { margin-bottom:14px; }
403
- .shm-grid { display:grid; grid-template-rows:repeat(7,10px); grid-auto-flow:column; grid-auto-columns:10px; gap:2px; margin-top:6px; }
404
- .shm-cell { border-radius:2px; background:var(--surface-hi); cursor:pointer; transition:outline 0.1s; }
405
- .shm-cell:hover { outline:1px solid var(--accent); outline-offset:-1px; }
406
- .shm-cell.shm-1 { background:oklch(72% 0.18 75 / 0.22); }
407
- .shm-cell.shm-2 { background:oklch(72% 0.18 75 / 0.42); }
408
- .shm-cell.shm-3 { background:oklch(72% 0.18 75 / 0.65); }
409
- .shm-cell.shm-4 { background:oklch(72% 0.18 75 / 0.85); }
410
- .shm-cell.shm-active { outline:2px solid var(--accent); outline-offset:-1px; }
411
- .shm-label { font-size:10px; color:var(--text-xs); display:flex; align-items:center; justify-content:space-between; }
412
- #shm-clear { font-size:10px; color:var(--accent); background:none; border:none; cursor:pointer; padding:0; display:none; }
413
- #shm-clear.show { display:inline; }
414
-
415
- /* ── cost period toggle ───────────────────────────────────── */
416
- .period-toggles { display:flex; gap:4px; margin-bottom:10px; flex-wrap:wrap; }
417
- .period-btn { 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; }
418
- .period-btn:hover { color:var(--text-hi); }
419
- .period-btn.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
420
-
421
- /* ── bulk session actions ─────────────────────────────────── */
422
- .sess-row.bulk-sel { background:oklch(72% 0.18 75 / 0.08); outline:1px solid oklch(72% 0.18 75 / 0.3); outline-offset:-1px; }
423
- #bulk-toolbar { display:none; position:sticky; top:0; z-index:10; background:oklch(18% 0.009 55); border:1px solid oklch(72% 0.18 75 / 0.3); border-radius:var(--r); padding:8px 14px; margin-bottom:10px; gap:10px; align-items:center; }
424
- #bulk-toolbar.show { display:flex; }
425
- .bulk-count { font-size:12px; color:var(--text-mid); flex:1; }
426
- .bulk-btn { font-size:11px; padding:4px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface); color:var(--text-mid); cursor:pointer; font-family:var(--sans); transition:color 0.1s; }
427
- .bulk-btn:hover { color:var(--text-hi); }
428
- .bulk-btn.danger { color:oklch(65% 0.15 25); border-color:oklch(65% 0.15 25 / 0.3); }
429
-
430
- /* ── files touched ────────────────────────────────────────── */
431
- .sr-files { font-size:10px; color:var(--text-xs); margin-top:2px; display:flex; flex-wrap:wrap; gap:3px; }
432
- .sr-file-chip { font-family:var(--mono); background:var(--surface-hi); border-radius:3px; padding:1px 5px; color:var(--text-lo); }
433
-
434
- /* ── memory age bars ──────────────────────────────────────── */
435
- .dr-age-bar { height:2px; border-radius:1px; margin-top:4px; background:oklch(72% 0.18 75 / 0.25); }
436
- .dr-age-bar-fill { height:100%; border-radius:1px; background:var(--accent); }
437
-
438
- /* ── loop creation form ───────────────────────────────────── */
439
- #loop-create-form { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:14px 16px; margin-bottom:16px; }
440
- .lcf-title { font-size:12px; font-weight:600; color:var(--text-hi); margin-bottom:10px; }
441
- .lcf-row { display:flex; flex-direction:column; gap:3px; margin-bottom:10px; }
442
- .lcf-label { font-size:10px; color:var(--text-xs); text-transform:uppercase; letter-spacing:0.06em; }
443
- .lcf-input, .lcf-textarea { background:var(--bg); border:1px solid var(--border); border-radius:4px; color:var(--text-hi); font-family:var(--sans); font-size:12px; padding:6px 10px; transition:border-color 0.15s; }
444
- .lcf-textarea { resize:vertical; min-height:56px; }
445
- .lcf-input:focus, .lcf-textarea:focus { outline:none; border-color:oklch(72% 0.18 75 / 0.5); }
446
- .lcf-row-inline { display:flex; gap:8px; }
447
- .lcf-row-inline .lcf-row { flex:1; margin-bottom:0; }
448
- .lcf-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:4px; }
449
- .lcf-submit { background:var(--accent); color:oklch(11% 0.009 55); border:none; border-radius:6px; font-size:12px; font-weight:600; padding:5px 16px; cursor:pointer; font-family:var(--sans); }
450
- .lcf-submit:hover { background:oklch(78% 0.18 75); }
451
- .lcf-cancel { background:transparent; border:1px solid var(--border); border-radius:6px; font-size:12px; color:var(--text-lo); padding:5px 12px; cursor:pointer; font-family:var(--sans); }
452
- #btn-new-loop { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid var(--border); border-radius:8px; padding:3px 10px; cursor:pointer; transition:color 0.1s; font-family:var(--sans); margin-bottom:12px; }
453
- #btn-new-loop:hover { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.4); }
454
-
455
- /* ── auto-tags ───────────────────────────────────────────── */
456
- .sr-autotag { font-size:10px; padding:1px 6px; border-radius:8px; background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border:1px solid oklch(72% 0.18 75 / 0.2); }
457
- .tag-filter-bar { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
458
- .tag-chip { 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; }
459
- .tag-chip:hover { color:var(--text-hi); }
460
- .tag-chip.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
461
-
462
- /* ── session recap ───────────────────────────────────────── */
463
- #feed-recap { display:none; flex-shrink:0; padding:8px 18px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
464
- #feed-recap.show { display:block; }
465
- .recap-text { font-size:12px; color:var(--text-mid); line-height:1.6; }
466
- .recap-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:1px 7px; border-radius:8px; margin-right:5px; }
467
- .recap-stat.rs-tool { background:oklch(65% 0.15 150 / 0.1); color:oklch(65% 0.15 150); }
468
- .recap-stat.rs-cost { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); }
469
- .recap-stat.rs-err { background:oklch(60% 0.18 25 / 0.1); color:oklch(70% 0.18 25); }
470
- .recap-stat.rs-user { background:var(--surface-hi); color:var(--text-lo); }
471
-
472
- /* ── replay mode ─────────────────────────────────────────── */
473
- #replay-bar { display:none; flex-shrink:0; align-items:center; gap:8px; padding:5px 18px; border-bottom:1px solid var(--border); background:oklch(13% 0.009 55); }
474
- #replay-bar.show { display:flex; }
475
- .rp-btn { font-size:11px; background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 8px; cursor:pointer; color:var(--text-lo); transition:color 0.1s; }
476
- .rp-btn:hover { color:var(--text-hi); }
477
- .rp-btn.active { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.5); }
478
- #rp-progress { flex:1; height:3px; background:var(--surface-hi); border-radius:2px; overflow:hidden; }
479
- #rp-fill { height:100%; background:var(--accent); border-radius:2px; transition:width 0.15s; }
480
- #rp-counter { font-size:11px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; }
481
-
482
- /* ── global feed (multi-project) ─────────────────────────── */
483
- .gf-proj-tag { font-size:10px; padding:1px 6px; border-radius:6px; background:var(--surface-hi); color:var(--text-lo); white-space:nowrap; flex-shrink:0; margin-top:3px; }
484
-
485
- /* ── project health score ────────────────────────────────── */
486
- .proj-health { position:absolute; bottom:12px; right:12px; font-size:11px; font-weight:700; width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
487
- .ph-hi { background:oklch(65% 0.15 150 / 0.15); color:oklch(65% 0.15 150); }
488
- .ph-mid { background:oklch(72% 0.18 75 / 0.15); color:oklch(78% 0.18 75); }
489
- .ph-lo { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
490
-
491
- /* ── ambient mode ────────────────────────────────────────── */
492
- #app.ambient #sidebar,
493
- #app.ambient #topbar,
494
- #app.ambient #alerts-rail,
495
- #app.ambient #feed-head,
496
- #app.ambient #feed-time-filter,
497
- #app.ambient #metrics-pane,
498
- #app.ambient #replay-bar,
499
- #app.ambient #feed-recap,
500
- #app.ambient #feed-timeline,
501
- #app.ambient #digest-card { display:none !important; }
502
- #app.ambient #weekly-card { display:none !important; }
503
- #app.ambient #main { background:var(--bg); }
504
- #app.ambient #view-now { height:100vh; }
505
- #app.ambient #feed-pane { border:none; }
506
- #app.ambient .feed-entry { padding: 6px 22px; }
507
- #app.ambient .feed-lbl { font-size:14px; }
508
- #app-ambient-hint { display:none; position:fixed; bottom:16px; right:16px; font-size:11px; color:var(--text-xs); z-index:300; pointer-events:none; }
509
- #app.ambient #app-ambient-hint { display:block; }
510
-
511
- /* ── minimap scrubber ─────────────────────────────────────── */
512
- #feed-minimap { position:absolute; top:0; right:0; width:8px; height:100%; z-index:10; cursor:pointer; }
513
- #feed-minimap-track { position:absolute; inset:0; }
514
- .mm-pip { position:absolute; right:1px; width:6px; border-radius:3px; min-height:3px; opacity:0.55; transition:opacity 0.1s; }
515
- .mm-pip:hover { opacity:1; }
516
- .mm-pip.mp-file { background:oklch(60% 0.12 220); }
517
- .mm-pip.mp-bash { background:oklch(72% 0.18 75); }
518
- .mm-pip.mp-agent { background:oklch(70% 0.15 300); }
519
- .mm-pip.mp-mcp { background:oklch(65% 0.15 200); }
520
- .mm-pip.mp-err { background:var(--red); opacity:0.8; }
521
- .mm-pip.mp-user { background:var(--text-xs); }
522
- .mm-pip.mp-other { background:var(--surface-hi); }
523
- #mm-thumb { position:absolute; right:0; width:8px; background:oklch(100% 0 0 / 0.08); border-radius:4px; pointer-events:none; transition:top 0.05s; }
524
- #feed-scroll-wrap { position:relative; flex:1; overflow:hidden; display:flex; }
525
- #feed-scroll { flex:1; }
526
-
527
- /* ── daily digest ─────────────────────────────────────────── */
528
- #digest-card { display:none; flex-shrink:0; border-bottom:1px solid var(--border); background:oklch(13.5% 0.009 55); padding:10px 18px; }
529
- #digest-card.show { display:block; }
530
- .digest-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
531
- .digest-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); margin-bottom:6px; display:flex; align-items:center; gap:8px; }
532
- .digest-close { margin-left:auto; background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:13px; line-height:1; padding:0; }
533
- .digest-close:hover { color:var(--text-lo); }
534
- .digest-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:2px 8px; border-radius:8px; background:var(--surface-hi); color:var(--text-mid); white-space:nowrap; }
535
-
536
- /* ── cost leaderboard ────────────────────────────────────── */
537
- .lb-toggle { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
538
- .lb-toggle:hover { color:var(--text-hi); }
539
- .lb-toggle.on { background:oklch(72% 0.18 75 / 0.1); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
540
- .lb-table { width:100%; border-collapse:collapse; margin-top:4px; }
541
- .lb-table th { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); padding:4px 6px; text-align:left; border-bottom:1px solid var(--border); }
542
- .lb-table td { font-size:12px; padding:7px 6px; border-bottom:1px solid oklch(25% 0.008 55 / 0.5); color:var(--text-mid); vertical-align:top; cursor:pointer; }
543
- .lb-table tr:hover td { background:var(--surface-hi); color:var(--text-hi); }
544
- .lb-rank { font-family:var(--mono); color:var(--text-xs); width:22px; }
545
- .lb-cost { font-family:var(--mono); color:oklch(78% 0.18 75); white-space:nowrap; }
546
- .lb-dur { font-family:var(--mono); color:var(--text-lo); white-space:nowrap; }
547
- .lb-prompt { max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
548
-
549
- /* ── session diff ─────────────────────────────────────────── */
550
- .diff-toggle { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
551
- .diff-toggle:hover { color:var(--text-hi); }
552
- .diff-toggle.on { background:oklch(60% 0.12 220 / 0.1); color:oklch(65% 0.12 220); border-color:oklch(60% 0.12 220 / 0.3); }
553
- #diff-panel { display:none; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; overflow:hidden; }
554
- #diff-panel.show { display:block; }
555
- .diff-header { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
556
- .diff-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); flex:1; }
557
- .diff-clear { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:12px; }
558
- .diff-cols { display:grid; grid-template-columns:1fr 1fr; }
559
- .diff-col { padding:10px 14px; }
560
- .diff-col + .diff-col { border-left:1px solid var(--border); }
561
- .diff-col-title { font-size:11px; font-weight:600; color:var(--text-mid); margin-bottom:8px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
562
- .diff-row { display:flex; justify-content:space-between; gap:8px; padding:3px 0; border-bottom:1px solid oklch(25% 0.008 55 / 0.4); }
563
- .diff-row:last-child { border-bottom:none; }
564
- .diff-k { font-size:11px; color:var(--text-xs); }
565
- .diff-v { font-size:11px; font-family:var(--mono); color:var(--text-mid); }
566
- .diff-v.diff-hi { color:oklch(78% 0.18 75); }
567
- .diff-v.diff-lo { color:var(--red); }
568
- .diff-hint { font-size:11px; color:var(--text-xs); padding:8px 12px; text-align:center; }
569
- .sess-row.diff-sel-a { background:oklch(60% 0.12 220 / 0.08); outline:1px solid oklch(60% 0.12 220 / 0.3); }
570
- .sess-row.diff-sel-b { background:oklch(72% 0.18 75 / 0.06); outline:1px solid oklch(72% 0.18 75 / 0.3); }
571
-
572
- /* ── burn rate gauge ─────────────────────────────────────── */
573
- .burn-gauge-wrap { margin-top:6px; }
574
- .burn-gauge-row { display:flex; align-items:center; gap:8px; margin-bottom:4px; }
575
- .burn-gauge-label { font-size:10px; color:var(--text-lo); width:60px; flex-shrink:0; }
576
- .burn-gauge-track { flex:1; height:6px; background:var(--surface-hi); border-radius:3px; overflow:hidden; }
577
- .burn-gauge-fill { height:100%; border-radius:3px; transition:width 0.4s, background 0.4s; }
578
- .burn-val { font-size:10px; font-family:var(--mono); color:var(--text-xs); white-space:nowrap; }
579
- .burn-rate-ok { background:oklch(65% 0.15 150); }
580
- .burn-rate-warn { background:oklch(72% 0.18 75); }
581
- .burn-rate-hot { background:oklch(60% 0.18 25); }
582
-
583
- /* ── swimlane timeline ────────────────────────────────────── */
584
- #swimlane-wrap { padding:4px 0; }
585
- .sw-row { display:flex; align-items:center; gap:6px; margin-bottom:3px; height:14px; }
586
- .sw-lbl { font-size:10px; color:var(--text-xs); width:60px; flex-shrink:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:right; cursor:pointer; }
587
- .sw-lbl:hover { color:var(--text-lo); }
588
- .sw-track { flex:1; height:10px; background:var(--surface-hi); border-radius:3px; position:relative; overflow:hidden; }
589
- .sw-bar { position:absolute; top:0; height:100%; border-radius:2px; opacity:0.8; }
590
- .sw-gap { position:absolute; top:0; height:100%; background:var(--bg); width:2px; }
591
-
592
- /* ── focus mode ──────────────────────────────────────────── */
593
- #feed-pane.focus-mode .feed-entry.k-tool:not(.errored) { display:none; }
594
- #feed-pane.focus-mode .feed-group { display:none; }
595
- .focus-btn { font-size:10px; color:var(--text-lo); background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 7px; cursor:pointer; transition:color 0.15s, border-color 0.15s; line-height:1.4; white-space:nowrap; }
596
- .focus-btn:hover { color:var(--text-hi); }
597
- .focus-btn.on { color:oklch(60% 0.12 220); border-color:oklch(60% 0.12 220 / 0.5); }
598
-
599
- /* ── session notes ───────────────────────────────────────── */
600
- .sess-notes-wrap { margin-top:8px; }
601
- .sess-notes-toggle { font-size:11px; color:var(--text-xs); background:none; border:none; cursor:pointer; padding:0; display:flex; align-items:center; gap:5px; }
602
- .sess-notes-toggle:hover { color:var(--text-lo); }
603
- .sess-notes-toggle.has-note { color:oklch(72% 0.14 220); }
604
- .sess-notes-area { display:none; margin-top:6px; }
605
- .sess-notes-area.open { display:block; }
606
- textarea.sess-note-input { width:100%; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); color:var(--text-mid); font-size:12px; font-family:var(--sans); padding:6px 8px; resize:vertical; min-height:56px; outline:none; transition:border-color 0.1s; }
607
- textarea.sess-note-input:focus { border-color:var(--accent); }
608
- .sess-note-saved { font-size:10px; color:var(--text-xs); margin-top:3px; height:14px; }
609
-
610
- /* ── export button ───────────────────────────────────────── */
611
- .sess-copy-btn.dl { }
612
-
613
- /* ── loop run history sparkline ──────────────────────────── */
614
- .loop-sparkline { display:flex; gap:2px; align-items:flex-end; height:20px; margin-top:4px; }
615
- .lsp-bar { width:5px; border-radius:2px 2px 0 0; background:var(--accent); opacity:0.6; min-height:3px; }
616
- .lsp-bar.err { background:var(--red); opacity:0.8; }
617
- .le-spark { display:flex; align-items:center; gap:8px; }
618
-
619
- /* ── feature 19: cache efficiency ───────────────────────── */
620
- .eff-row { display:flex; align-items:center; justify-content:space-between; font-size:11px; margin-bottom:2px; gap:6px; }
621
- .eff-lbl { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text-lo); flex:1; min-width:0; font-size:10px; }
622
- .eff-pct { font-size:10px; font-variant-numeric:tabular-nums; flex-shrink:0; }
623
- .eff-good { color:oklch(65% 0.15 150); }
624
- .eff-warn { color:oklch(70% 0.18 80); }
625
- .eff-bad { color:oklch(65% 0.2 25); }
626
- .eff-bar-wrap { height:2px; background:var(--border); border-radius:1px; margin-bottom:5px; }
627
- .eff-bar-fill { height:2px; border-radius:1px; }
628
-
629
- /* ── feature 21: weekly recap ────────────────────────────── */
630
- #weekly-card { display:none; border:1px solid var(--border); border-radius:8px; margin:0 18px 10px; padding:10px 12px; background:oklch(14% 0.008 55 / 0.6); }
631
- #weekly-card.show { display:block; }
632
- .weekly-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
633
- .weekly-title { font-size:10px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-lo); }
634
- .weekly-dismiss { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:11px; padding:0; line-height:1; }
635
- .weekly-dismiss:hover { color:var(--text-lo); }
636
-
637
- /* ── feature 22: context saturation ─────────────────────── */
638
- .ctx-sat-wrap { margin-top:4px; }
639
- .ctx-sat-bar { height:2px; background:var(--border); border-radius:1px; overflow:hidden; }
640
- .ctx-sat-fill { height:2px; border-radius:1px; }
641
- .ctx-sat-lbl { font-size:9px; color:var(--text-xs); margin-top:1px; }
642
-
643
- /* ── feature 24: activity heatmap ───────────────────────── */
644
- .heatmap-grid { display:flex; flex-direction:column; gap:2px; }
645
- .heatmap-row { display:flex; align-items:center; gap:1px; }
646
- .heatmap-lbl { width:14px; font-size:8px; color:var(--text-xs); flex-shrink:0; text-align:right; padding-right:2px; }
647
- .heatmap-hdr-cell { width:9px; font-size:7px; color:var(--text-xs); text-align:center; flex-shrink:0; }
648
- .heatmap-cell { width:9px; height:9px; border-radius:1px; flex-shrink:0; cursor:default; }
649
-
650
- /* ── budget cap ──────────────────────────────────────────── */
651
- #budget-modal { display:none; position:fixed; inset:0; z-index:200; background:oklch(5% 0 0 / 0.6); align-items:center; justify-content:center; }
652
- #budget-modal.open { display:flex; }
653
- #budget-box { background:oklch(16% 0.009 55); border:1px solid oklch(32% 0.008 55); border-radius:10px; padding:22px 24px; width:320px; box-shadow:0 24px 60px oklch(5% 0 0 / 0.7); }
654
- .bm-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:14px; }
655
- .bm-row { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; }
656
- .bm-lbl { font-size:11px; color:var(--text-lo); }
657
- .bm-input { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:6px 10px; font-size:13px; color:var(--text-hi); font-family:var(--mono); outline:none; transition:border-color 0.1s; }
658
- .bm-input:focus { border-color:var(--accent); }
659
- .bm-btns { display:flex; gap:8px; margin-top:16px; }
660
- .bm-save { flex:1; background:var(--accent); border:none; border-radius:var(--r); padding:7px 0; font-size:13px; color:oklch(11% 0.009 55); font-weight:600; cursor:pointer; }
661
- .bm-save:hover { background:oklch(68% 0.18 75); }
662
- .bm-cancel { background:transparent; border:1px solid var(--border); border-radius:var(--r); padding:7px 12px; font-size:13px; color:var(--text-lo); cursor:pointer; }
663
- .bm-cancel:hover { color:var(--text-hi); }
664
-
665
- /* ── toast notifications ──────────────────────────────────── */
666
- #toast-rack { position:fixed; bottom:20px; right:20px; z-index:9999; display:flex; flex-direction:column; gap:8px; pointer-events:none; }
667
- .toast { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; border-radius:8px; font-size:12px; max-width:300px; pointer-events:all; background:oklch(18% 0.012 55); border:1px solid var(--border); color:var(--text-mid); box-shadow:0 4px 24px rgba(0,0,0,0.4); animation:toast-in 0.25s var(--ease-out); }
668
- .toast.t-warn { border-color:oklch(72% 0.18 75 / 0.4); background:oklch(18% 0.015 75); }
669
- .toast.t-err { border-color:oklch(60% 0.18 25 / 0.4); background:oklch(17% 0.015 25); }
670
- .toast.t-ok { border-color:oklch(65% 0.15 150 / 0.4); background:oklch(17% 0.012 150); }
671
- .toast-ico { flex-shrink:0; font-size:14px; }
672
- .toast-body { flex:1; min-width:0; }
673
- .toast-title { font-weight:600; color:var(--text-hi); margin-bottom:2px; }
674
- .toast-msg { color:var(--text-lo); line-height:1.4; }
675
- .toast-close { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:12px; line-height:1; padding:0; flex-shrink:0; }
676
- .toast-close:hover { color:var(--text-lo); }
677
- @keyframes toast-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
678
-
679
- /* ── token velocity sparkline ─────────────────────────────── */
680
- #vel-chart { display:flex; align-items:flex-end; gap:2px; height:28px; margin-top:6px; }
681
- .vel-bar { flex:1; border-radius:2px 2px 0 0; min-height:2px; background:oklch(65% 0.18 200 / 0.55); }
682
- .vel-bar.vel-hi { background:oklch(65% 0.18 150 / 0.8); }
683
- .vel-bar.vel-lo { background:oklch(65% 0.08 200 / 0.3); }
684
-
685
- /* ── shortcut help modal ──────────────────────────────────── */
686
- #shortcut-modal { display:none; position:fixed; inset:0; z-index:500; background:oklch(0% 0 0 / 0.6); align-items:center; justify-content:center; }
687
- #shortcut-modal.open { display:flex; }
688
- #shortcut-box { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:24px 28px; max-width:460px; width:90%; max-height:80vh; overflow-y:auto; }
689
- .sk-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:16px; display:flex; justify-content:space-between; align-items:center; }
690
- .sk-close { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:16px; padding:0; }
691
- .sk-close:hover { color:var(--text-lo); }
692
- .sk-section { font-size:10px; letter-spacing:0.08em; text-transform:uppercase; color:var(--text-xs); margin:14px 0 6px; }
693
- .sk-row { display:flex; justify-content:space-between; align-items:center; padding:5px 0; border-bottom:1px solid oklch(25% 0.008 55 / 0.4); }
694
- .sk-row:last-child { border-bottom:none; }
695
- .sk-keys { display:flex; gap:3px; }
696
- .sk-keys kbd { font-size:10px; background:var(--surface-hi); border:1px solid var(--border); border-radius:3px; padding:1px 6px; font-family:var(--mono); color:var(--text-lo); }
697
- .sk-desc { font-size:11px; color:var(--text-mid); }
698
-
699
- /* ── session filter ───────────────────────────────────────── */
700
- #sess-filter-wrap { display:flex; align-items:center; gap:8px; margin-bottom:10px; }
701
- #sess-filter-input { flex:1; background:var(--surface-hi); border:1px solid var(--border); border-radius:var(--r); padding:5px 10px; font-size:12px; color:var(--text-hi); outline:none; font-family:var(--sans); }
702
- #sess-filter-input:focus { border-color:oklch(72% 0.18 75 / 0.5); }
703
- #sess-filter-input::placeholder { color:var(--text-xs); }
704
- #sess-filter-count { font-size:11px; color:var(--text-xs); white-space:nowrap; min-width:60px; text-align:right; }
705
-
706
- /* ── topbar activity chip ─────────────────────────────────── */
707
- #topbar-activity { font-size:10px; color:var(--text-xs); font-family:var(--mono); max-width:180px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; opacity:0; transition:opacity 0.3s; }
708
- #topbar-activity.loaded { opacity:1; }
709
-
710
- /* ── anomaly badge on session rows ────────────────────────── */
711
- .sess-anomaly { display:inline-flex; align-items:center; font-size:10px; padding:1px 5px; border-radius:5px; font-weight:600; margin-left:4px; vertical-align:middle; }
712
- .sess-anomaly.anom-cost { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
713
- .sess-anomaly.anom-err { background:oklch(65% 0.15 300 / 0.12); color:oklch(72% 0.15 300); }
714
-
715
- /* ── streak badge ─────────────────────────────────────────── */
716
- .streak-chip { display:inline-flex; align-items:center; gap:3px; font-size:11px; padding:2px 7px; border-radius:8px; background:oklch(65% 0.18 75 / 0.12); color:oklch(78% 0.18 75); white-space:nowrap; }
717
-
718
- /* ── f47: 30-day daily cost trend ────────────────────────── */
719
- #daily-trend-chart { display:flex; align-items:flex-end; gap:2px; height:40px; margin-top:6px; }
720
- .dt-bar { flex:1; min-width:4px; border-radius:2px 2px 0 0; background:oklch(72% 0.18 75 / 0.5); cursor:pointer; transition:background 0.15s; }
721
- .dt-bar:hover,.dt-bar.active { background:oklch(72% 0.18 75); }
722
- .dt-bar.has-filter { background:oklch(65% 0.15 150 / 0.7); }
723
-
724
- /* ── f48: live cost ticker ────────────────────────────────── */
725
- #live-cost-ticker { font-size:11px; font-family:var(--mono); font-weight:600; color:oklch(78% 0.18 75); opacity:0; transition:opacity 0.3s; white-space:nowrap; }
726
- #live-cost-ticker.show { opacity:1; }
727
- #live-cost-ticker .lct-change { font-size:10px; color:var(--green); margin-left:3px; }
728
-
729
- /* ── f49: hourly productivity heatmap ────────────────────── */
730
- #hourly-heatmap-grid { display:grid; grid-template-columns:20px repeat(24,1fr); grid-template-rows:auto; gap:2px; margin-top:6px; font-size:9px; }
731
- .hh-hour-lbl { color:var(--text-xs); text-align:center; padding-bottom:1px; }
732
- .hh-day-lbl { color:var(--text-xs); line-height:12px; }
733
- .hh-cell { height:10px; border-radius:2px; background:var(--surface-hi); cursor:default; }
734
- .hh-cell.hh-1 { background:oklch(72% 0.18 75 / 0.25); }
735
- .hh-cell.hh-2 { background:oklch(72% 0.18 75 / 0.5); }
736
- .hh-cell.hh-3 { background:oklch(72% 0.18 75 / 0.75); }
737
- .hh-cell.hh-4 { background:oklch(72% 0.18 75); }
738
-
739
- /* ── f50: custom tag editor ──────────────────────────────── */
740
- .sr-custom-tags { display:flex; flex-wrap:wrap; gap:4px; align-items:center; margin-top:4px; }
741
- .sr-ctag { display:inline-flex; align-items:center; gap:3px; font-size:10px; padding:1px 6px; border-radius:8px; background:oklch(65% 0.15 200 / 0.15); color:oklch(72% 0.15 200); }
742
- .sr-ctag .ctag-del { cursor:pointer; opacity:0.6; font-size:9px; line-height:1; }
743
- .sr-ctag .ctag-del:hover { opacity:1; }
744
- .ctag-add-btn { font-size:10px; color:var(--text-xs); background:none; border:1px dashed var(--border); border-radius:8px; padding:1px 6px; cursor:pointer; }
745
- .ctag-add-btn:hover { color:var(--text-lo); border-color:var(--text-xs); }
746
- .ctag-input-wrap { display:flex; gap:4px; margin-top:4px; }
747
- .ctag-input { font-size:11px; background:var(--surface-hi); border:1px solid var(--border); border-radius:var(--r); padding:2px 6px; color:var(--text-hi); outline:none; width:100px; }
748
- .ctag-input:focus { border-color:oklch(72% 0.18 75 / 0.5); }
749
- .ctag-ok { font-size:10px; background:none; border:1px solid var(--border); border-radius:var(--r); padding:2px 6px; cursor:pointer; color:var(--text-lo); }
750
- .ctag-ok:hover { color:var(--text-hi); }
751
- .sr-autotag.ctag { background:oklch(65% 0.15 200 / 0.12); color:oklch(72% 0.15 200); }
752
-
753
- /* ── f51: tool error drawer ──────────────────────────────── */
754
- .sess-anomaly.anom-err.clickable { cursor:pointer; }
755
- .sess-anomaly.anom-err.clickable:hover { opacity:0.8; }
756
- .err-drawer { display:none; margin-top:6px; border:1px solid oklch(60% 0.18 25 / 0.3); border-radius:var(--r); overflow:hidden; }
757
- .err-drawer.open { display:block; }
758
- .err-drawer-head { display:flex; align-items:center; gap:8px; padding:6px 10px; background:oklch(60% 0.18 25 / 0.08); font-size:11px; font-weight:600; color:oklch(70% 0.18 25); }
759
- .err-drawer-head .err-close { margin-left:auto; cursor:pointer; font-size:12px; background:none; border:none; color:var(--text-xs); }
760
- .err-drawer-body { max-height:160px; overflow-y:auto; padding:8px 10px; }
761
- .err-item { font-size:11px; font-family:var(--mono); color:oklch(80% 0.1 25); border-bottom:1px solid var(--border); padding:4px 0; white-space:pre-wrap; word-break:break-word; }
762
- .err-item:last-child { border:none; }
763
-
764
- /* ── f52: prompt copy button ─────────────────────────────── */
765
- .sr-copy-btn { font-size:10px; background:none; border:none; cursor:pointer; color:var(--text-xs); padding:0 4px; opacity:0; transition:opacity 0.15s; line-height:1; }
766
- .sess-row:hover .sr-copy-btn { opacity:1; }
767
- .sr-copy-btn:hover { color:var(--text-hi); }
768
- .sr-copy-btn.copied { color:var(--green); opacity:1; }
769
-
770
- /* ── f53: session cost histogram ─────────────────────────── */
771
- #cost-histogram-panel { display:none; margin-bottom:16px; padding:12px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); }
772
- .ch-title { font-size:11px; font-weight:600; color:var(--text-lo); letter-spacing:0.06em; text-transform:uppercase; margin-bottom:8px; }
773
- .ch-bars { display:flex; align-items:flex-end; gap:4px; height:50px; }
774
- .ch-bar-wrap { flex:1; display:flex; flex-direction:column; align-items:center; gap:2px; }
775
- .ch-bar { width:100%; border-radius:2px 2px 0 0; background:oklch(72% 0.18 75 / 0.4); min-height:2px; }
776
- .ch-lbl { font-size:8px; color:var(--text-xs); white-space:nowrap; }
777
- .ch-cnt { font-size:9px; color:var(--text-lo); }
778
-
779
- /* ── f54: URL param indicator ────────────────────────────── */
780
- .url-param-active { background:oklch(72% 0.18 75 / 0.08); }
781
-
782
- /* ── f59: keyboard focus in sessions ────────────────────── */
783
- .sess-row.kb-focus { outline:1px solid oklch(72% 0.18 75 / 0.4); outline-offset:-1px; }
784
-
785
- /* ── f55: session timeline ───────────────────────────────── */
786
- #timeline-panel { display:none; margin-bottom:16px; background:var(--surface); border:1px solid var(--border); border-radius:var(--r); overflow:hidden; }
787
- #timeline-panel.open { display:block; }
788
- #timeline-head { padding:8px 12px; font-size:11px; font-weight:600; color:var(--text-lo); letter-spacing:0.06em; text-transform:uppercase; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:8px; }
789
- #timeline-scroll { overflow-x:auto; padding:10px 12px; }
790
- .tl-day-row { display:flex; align-items:center; gap:6px; margin-bottom:4px; min-height:18px; }
791
- .tl-day-lbl { font-size:10px; color:var(--text-xs); width:60px; flex-shrink:0; font-family:var(--mono); }
792
- .tl-track { flex:1; position:relative; height:14px; min-width:400px; }
793
- .tl-bar { position:absolute; height:10px; top:2px; border-radius:3px; cursor:pointer; opacity:0.7; transition:opacity 0.15s; }
794
- .tl-bar:hover { opacity:1; z-index:2; }
795
-
796
- /* ── f56: daily report card ──────────────────────────────── */
797
- #report-modal { display:none; position:fixed; inset:0; z-index:200; background:rgba(0,0,0,0.6); backdrop-filter:blur(4px); align-items:center; justify-content:center; }
798
- #report-modal.open { display:flex; }
799
- #report-box { background:var(--surface); border:1px solid var(--border); border-radius:10px; width:520px; max-width:90vw; max-height:80vh; display:flex; flex-direction:column; }
800
- .rp-head { display:flex; align-items:center; gap:8px; padding:14px 16px; border-bottom:1px solid var(--border); }
801
- .rp-title { font-size:14px; font-weight:600; color:var(--text-hi); flex:1; }
802
- .rp-copy-btn { font-size:11px; background:none; border:1px solid var(--border); border-radius:var(--r); padding:4px 10px; cursor:pointer; color:var(--text-lo); transition:color 0.1s; }
803
- .rp-copy-btn:hover { color:var(--text-hi); }
804
- .rp-close-btn { background:none; border:none; cursor:pointer; color:var(--text-xs); font-size:16px; padding:0 2px; }
805
- #report-content { flex:1; overflow-y:auto; padding:16px; }
806
- #report-content pre { font-family:var(--mono); font-size:11px; color:var(--text-mid); white-space:pre-wrap; line-height:1.6; }
807
-
808
- /* ── f57: file-pivot filter ──────────────────────────────── */
809
- .sr-file-chip { cursor:pointer; }
810
- .sr-file-chip:hover { background:oklch(72% 0.18 75 / 0.25); color:oklch(85% 0.18 75); }
811
- .sr-file-chip.pivot-active { background:oklch(72% 0.18 75 / 0.3); color:oklch(90% 0.18 75); border-color:oklch(72% 0.18 75 / 0.5); }
812
- #file-pivot-bar { display:none; align-items:center; gap:8px; padding:6px 12px; background:oklch(72% 0.18 75 / 0.08); border:1px solid oklch(72% 0.18 75 / 0.2); border-radius:var(--r); margin-bottom:10px; font-size:11px; }
813
- #file-pivot-bar.show { display:flex; }
814
- .fpb-label { color:var(--accent); font-weight:600; }
815
- .fpb-clear { background:none; border:none; cursor:pointer; color:var(--text-lo); font-size:12px; margin-left:auto; }
816
-
817
- /* ── f58: model cost donut ───────────────────────────────── */
818
- #model-donut-panel { display:none; margin-bottom:16px; }
819
- #model-donut-panel.open { display:block; }
820
- .donut-wrap { display:flex; align-items:center; gap:16px; padding:10px 0; }
821
- .donut-svg { flex-shrink:0; }
822
- .donut-legend { display:flex; flex-direction:column; gap:4px; }
823
- .donut-item { display:flex; align-items:center; gap:6px; font-size:11px; }
824
- .donut-swatch { width:8px; height:8px; border-radius:2px; flex-shrink:0; }
825
- .donut-name { color:var(--text-mid); min-width:80px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
826
- .donut-cost { color:var(--accent); font-family:var(--mono); }
827
-
828
- /* ── f62: context window pressure gauge ─────────────────── */
829
- .ctx-pressure-wrap { margin-top:3px; display:flex; align-items:center; gap:5px; }
830
- .ctx-pressure-bar { flex:1; height:3px; background:var(--surface-hi); border-radius:2px; overflow:hidden; }
831
- .ctx-pressure-fill { height:100%; border-radius:2px; background:oklch(65% 0.15 150); }
832
- .ctx-pressure-fill.warn { background:oklch(70% 0.18 80); }
833
- .ctx-pressure-fill.crit { background:oklch(65% 0.2 25); }
834
- .ctx-pressure-lbl { font-size:9px; color:var(--text-xs); white-space:nowrap; }
835
- </style>
836
- </head>
837
- <body>
838
- <div id="app">
839
-
840
- <!-- ── Sidebar ─────────────────────────────────────────── -->
841
- <nav id="sidebar">
842
- <div id="sb-logo">
843
- <div class="mark">monomind</div>
844
- <div class="proj" id="sb-proj">—</div>
845
- </div>
846
- <div id="sb-nav">
847
- <div class="nav-sect">
848
- <div class="nav-item active" data-view="now">
849
- <span class="ico">◉</span><span class="lbl">Now</span>
850
- </div>
851
- </div>
852
- <div class="nav-sect">
853
- <div class="nav-lbl">Workspace</div>
854
- <div class="nav-item" data-view="projects">
855
- <span class="ico">⊞</span><span class="lbl">Projects</span>
856
- <span class="bdg" id="bdg-projects">—</span>
857
- </div>
858
- <div class="nav-item" data-view="sessions">
859
- <span class="ico">◫</span><span class="lbl">Sessions</span>
860
- <span class="bdg" id="bdg-sessions">—</span>
861
- </div>
862
- <div class="nav-item" data-view="loops">
863
- <span class="ico">↺</span><span class="lbl">Loops</span>
864
- <span class="bdg" id="bdg-loops">—</span>
865
- </div>
866
- </div>
867
- <div class="nav-sect">
868
- <div class="nav-lbl">Intelligence</div>
869
- <div class="nav-item" data-view="memory">
870
- <span class="ico">◈</span><span class="lbl">Memory</span>
871
- </div>
872
- <div class="nav-item" data-view="orgs">
873
- <span class="ico">⬡</span><span class="lbl">Orgs</span>
874
- </div>
875
- <div class="nav-item" data-view="global">
876
- <span class="ico">⊕</span><span class="lbl">Global Feed</span>
877
- </div>
878
- </div>
879
- </div>
880
- <div id="sb-footer">
881
- <div id="sb-user">—</div>
882
- <div id="sb-path">—</div>
883
- </div>
884
- </nav>
885
-
886
- <!-- ── Main ────────────────────────────────────────────── -->
887
- <div id="main">
888
- <div id="topbar">
889
- <span id="view-title">Now</span>
890
- <span class="pill"><span class="live-dot"></span> live</span>
891
- <span id="topbar-cost"></span>
892
- <span id="live-cost-ticker" title="Current session accumulated cost"></span>
893
- <span id="topbar-activity"></span>
894
- <div id="tb-right">
895
- <button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
896
- <button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
897
- <button class="btn" onclick="openShortcutHelp()" title="Keyboard shortcuts (?)">? Help</button>
898
- <button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
899
- </div>
900
- </div>
901
-
902
- <div id="alerts-rail"></div>
903
-
904
- <!-- command palette -->
905
- <div id="cmd-backdrop" onclick="closeCmdPalette()"></div>
906
- <div id="cmd-palette" role="dialog" aria-label="Command palette">
907
- <div id="cmd-input-wrap">
908
- <span id="cmd-ico">⌕</span>
909
- <input id="cmd-input" type="text" placeholder="Search sessions, memory, projects…" oninput="cmdSearch(this.value)" onkeydown="cmdKey(event)" autocomplete="off" spellcheck="false">
910
- </div>
911
- <div id="cmd-results"></div>
912
- <div class="cmd-footer">
913
- <span class="cmd-key"><kbd>↑↓</kbd> navigate</span>
914
- <span class="cmd-key"><kbd>↵</kbd> select</span>
915
- <span class="cmd-key"><kbd>></kbd> search all sessions</span>
916
- <span class="cmd-key"><kbd>esc</kbd> close</span>
917
- </div>
918
- </div>
919
-
920
- <div id="view-wrap">
921
-
922
- <!-- NOW -->
923
- <div class="view active" id="view-now">
924
- <div id="feed-pane">
925
- <div id="feed-head">
926
- <h2>Live Feed</h2>
927
- <span id="feed-sess">—</span>
928
- <div id="feed-sess-nav">
929
- <button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
930
- <button class="sess-copy-btn" id="btn-replay" onclick="replayToggle()" title="Replay session event-by-event">⏵ Replay</button>
931
- <button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
932
- <button class="sess-copy-btn" id="btn-export-sess" onclick="exportSession()" title="Download session as .md file">⬇ Export</button>
933
- <button class="focus-btn" id="btn-focus" onclick="toggleFocusMode()" title="Focus mode: show only user messages + errors">⊡ Focus</button>
934
- <button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
935
- <button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
936
- <button class="sess-btn" id="btn-prev-sess" onclick="prevSession()" title="Older session">‹</button>
937
- <button class="sess-btn" id="btn-next-sess" onclick="nextSession()" title="Newer session">›</button>
938
- </div>
939
- </div>
940
- <div id="feed-search">
941
- <input id="feed-search-input" type="text" placeholder="Search feed…" oninput="filterFeed(this.value)" onkeydown="if(event.key==='Escape')closeFeedSearch()">
942
- <span id="feed-search-count"></span>
943
- <button id="feed-search-close" onclick="closeFeedSearch()">✕</button>
944
- </div>
945
- <div id="sess-ctx">
946
- <button class="sctx-back" onclick="switchView('sessions')">← Sessions</button>
947
- <span class="sctx-sep">/</span>
948
- <span class="sctx-label" id="sctx-label"></span>
949
- <button class="sctx-live" onclick="goLive()">⬤ Go live</button>
950
- </div>
951
- <div id="feed-recap"></div>
952
- <div id="replay-bar">
953
- <button class="rp-btn" id="rp-play" onclick="replayToggle()" title="Play/pause replay">▶</button>
954
- <button class="rp-btn" onclick="replayStep(-1)" title="Step back">‹</button>
955
- <button class="rp-btn" onclick="replayStep(1)" title="Step forward">›</button>
956
- <div id="rp-progress"><div id="rp-fill" style="width:0%"></div></div>
957
- <span id="rp-counter">0 / 0</span>
958
- <button class="rp-btn" onclick="stopReplay()" title="Exit replay">✕</button>
959
- </div>
960
- <div id="feed-timeline" title="Session tool activity timeline"></div>
961
- <div id="feed-time-filter">
962
- <span class="tf-lbl">Range</span>
963
- <button class="tf-btn active" data-tf="all" onclick="setFeedTimeFilter('all')">All</button>
964
- <button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
965
- <button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
966
- <button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
967
- <span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate &nbsp;<kbd>↵</kbd> detail &nbsp;<kbd>/</kbd> find &nbsp;<kbd>G</kbd> live &nbsp;<kbd>A</kbd> ambient &nbsp;<kbd>⌘K</kbd> search</span>
968
- </div>
969
- <div id="weekly-card">
970
- <div class="weekly-header">
971
- <span class="weekly-title">This week</span>
972
- <button class="weekly-dismiss" onclick="dismissWeekly()" title="Dismiss">✕</button>
973
- </div>
974
- <div class="digest-row" id="weekly-stats"></div>
975
- </div>
976
- <div id="digest-card">
977
- <div class="digest-title">Today's Digest <button class="digest-close" onclick="dismissDigest()" title="Dismiss">✕</button></div>
978
- <div class="digest-row" id="digest-stats"></div>
979
- </div>
980
- <div id="feed-scroll-wrap">
981
- <div id="feed-scroll">
982
- <div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
983
- </div>
984
- <div id="feed-minimap" title="Click to jump · events map">
985
- <div id="feed-minimap-track"></div>
986
- <div id="mm-thumb"></div>
987
- </div>
988
- </div>
989
- </div>
990
-
991
- <!-- detail panel (slides in over feed) -->
992
- <div id="detail-panel">
993
- <div id="detail-head">
994
- <h3 id="detail-title">Detail</h3>
995
- <button id="detail-close" onclick="closeDetail()">✕</button>
996
- </div>
997
- <div id="detail-body"></div>
998
- </div>
999
-
1000
- <div id="metrics-pane">
1001
- <div id="m-today">
1002
- <div class="m-group-title">Today</div>
1003
- <div class="loading-txt">Loading…</div>
1004
- </div>
1005
- <div id="m-loops">
1006
- <div class="m-group-title">Active Loops</div>
1007
- <div class="loading-txt">Loading…</div>
1008
- </div>
1009
- <div id="m-sessions">
1010
- <div class="m-group-title">Recent Sessions</div>
1011
- <div class="loading-txt">Loading…</div>
1012
- </div>
1013
- <div id="m-breakdown">
1014
- <div class="m-group-title">Tool Usage</div>
1015
- <div class="loading-txt">—</div>
1016
- </div>
1017
- <div id="m-burn">
1018
- <div class="m-group-title">Burn Rate</div>
1019
- <div class="loading-txt">—</div>
1020
- </div>
1021
- <div id="m-swimlane">
1022
- <div class="m-group-title">Session Lanes</div>
1023
- <div class="loading-txt">—</div>
1024
- </div>
1025
- <div id="m-efficiency">
1026
- <div class="m-group-title">Cache Efficiency</div>
1027
- <div class="loading-txt">—</div>
1028
- </div>
1029
- <div id="m-heatmap">
1030
- <div class="m-group-title">Activity Heatmap</div>
1031
- <div class="loading-txt">—</div>
1032
- </div>
1033
- <div id="m-velocity">
1034
- <div class="m-group-title">Token Velocity</div>
1035
- <div class="loading-txt">—</div>
1036
- </div>
1037
- <div id="m-daily-trend">
1038
- <div class="m-group-title">30-Day Cost Trend</div>
1039
- <div class="loading-txt">—</div>
1040
- </div>
1041
- <div id="m-hourly-heatmap">
1042
- <div class="m-group-title">Peak Work Hours</div>
1043
- <div class="loading-txt">—</div>
1044
- </div>
1045
- </div>
1046
- </div>
1047
-
1048
- <!-- PROJECTS -->
1049
- <div class="view" id="view-projects">
1050
- <div class="vscroll">
1051
- <div class="pg-title">Projects</div>
1052
- <div class="pg-sub" id="proj-pg-sub">All monomind-enabled Claude Code projects</div>
1053
- <div class="filter-bar">
1054
- <input class="filter-input" id="proj-filter" type="text" placeholder="Filter projects…" oninput="filterProjects(this.value)">
1055
- </div>
1056
- <div id="proj-content" class="proj-grid"><div class="loading-txt">Loading…</div></div>
1057
- </div>
1058
- </div>
1059
-
1060
- <!-- SESSIONS -->
1061
- <div class="view" id="view-sessions">
1062
- <div class="vscroll">
1063
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap">
1064
- <div class="pg-title" style="margin-bottom:0">Sessions</div>
1065
- <button id="sess-star-filter" onclick="toggleSessStarFilter()" title="Show only bookmarked sessions">☆ Starred</button>
1066
- <button class="lb-toggle" id="btn-leaderboard" onclick="toggleLeaderboard()" title="Cost leaderboard">⬆ Leaderboard</button>
1067
- <button class="lb-toggle" id="btn-model-mix" onclick="toggleModelMix()" title="Model usage breakdown">⬡ Models</button>
1068
- <button class="lb-toggle" id="btn-tool-errors" onclick="toggleToolErrors()" title="Tool error rate">⚠ Errors</button>
1069
- <button class="lb-toggle" id="btn-tool-rank" onclick="toggleToolRank()" title="Most-used tools across sessions">⟳ Tools</button>
1070
- <button class="lb-toggle" id="btn-proj-costs" onclick="toggleProjCosts()" title="Cost breakdown by project">$ Projects</button>
1071
- <button class="lb-toggle" id="btn-export-csv" onclick="exportSessionsCSV()" title="Export sessions as CSV">⬇ CSV</button>
1072
- <button class="lb-toggle" id="btn-patterns" onclick="togglePatterns()" title="Prompt word frequency across sessions">⊞ Patterns</button>
1073
- <button class="diff-toggle" id="btn-diff" onclick="toggleDiffMode()" title="Compare two sessions">⇄ Compare</button>
1074
- <button class="lb-toggle" id="btn-timeline" onclick="toggleTimeline()" title="Session timeline (Gantt)">⊟ Timeline</button>
1075
- <button class="lb-toggle" id="btn-donut" onclick="toggleModelDonut()" title="Model cost donut">◕ Donut</button>
1076
- <button class="lb-toggle" id="btn-report" onclick="showReportCard()" title="Generate daily/weekly report card">✦ Report</button>
1077
- </div>
1078
- <div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
1079
- <div id="diff-panel">
1080
- <div class="diff-header"><span class="diff-title">Session Comparison</span><button class="diff-clear" onclick="clearDiff()" title="Clear">✕</button></div>
1081
- <div class="diff-hint" id="diff-hint">Click two sessions below to compare them</div>
1082
- <div class="diff-cols" id="diff-cols" style="display:none"></div>
1083
- </div>
1084
- <div id="lb-panel" style="display:none;margin-bottom:16px">
1085
- <table class="lb-table"><thead><tr>
1086
- <th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
1087
- </tr></thead><tbody id="lb-body"></tbody></table>
1088
- </div>
1089
- <div id="cost-histogram-panel"></div>
1090
- <div id="timeline-panel"><div id="timeline-head">Session Timeline <span style="font-weight:400;font-size:10px;color:var(--text-xs)">— each bar = one session, width = duration, color = cost</span></div><div id="timeline-scroll"></div></div>
1091
- <div id="model-donut-panel"></div>
1092
- <div id="file-pivot-bar"><span class="fpb-label" id="fpb-label"></span><button class="fpb-clear" onclick="clearFilePivot()">✕ Clear filter</button></div>
1093
- <div id="model-mix-panel" style="display:none;margin-bottom:16px">
1094
- <div id="model-mix-body"></div>
1095
- </div>
1096
- <div id="tool-errors-panel" style="display:none;margin-bottom:16px">
1097
- <div id="tool-errors-body"></div>
1098
- </div>
1099
- <div id="tool-rank-panel" style="display:none;margin-bottom:16px">
1100
- <div id="tool-rank-body"></div>
1101
- </div>
1102
- <div id="proj-costs-panel" style="display:none;margin-bottom:16px">
1103
- <div id="proj-costs-body"></div>
1104
- </div>
1105
- <div id="patterns-panel" style="display:none;margin-bottom:16px">
1106
- <div id="patterns-body"></div>
1107
- </div>
1108
- <div id="sess-heatmap" style="margin-bottom:14px;display:none">
1109
- <div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
1110
- <div class="shm-grid" id="shm-grid"></div>
1111
- </div>
1112
- <div class="period-toggles" id="period-toggles">
1113
- <span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
1114
- <button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
1115
- <button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
1116
- <button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
1117
- <button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
1118
- </div>
1119
- <div id="bulk-toolbar">
1120
- <span class="bulk-count" id="bulk-count">0 selected</span>
1121
- <button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
1122
- <button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
1123
- <button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
1124
- </div>
1125
- <div id="sess-filter-wrap">
1126
- <input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
1127
- <span id="sess-filter-count"></span>
1128
- </div>
1129
- <div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
1130
- </div>
1131
- </div>
1132
-
1133
- <!-- LOOPS -->
1134
- <div class="view" id="view-loops">
1135
- <div class="vscroll">
1136
- <div class="pg-title">Loops</div>
1137
- <div class="pg-sub">Scheduled automation loops</div>
1138
- <button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
1139
- <div id="loop-create-form" style="display:none">
1140
- <div class="lcf-title">Create Loop</div>
1141
- <div class="lcf-row">
1142
- <label class="lcf-label">Prompt</label>
1143
- <textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
1144
- </div>
1145
- <div class="lcf-row">
1146
- <label class="lcf-label">Name (optional)</label>
1147
- <input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
1148
- </div>
1149
- <div class="lcf-row-inline">
1150
- <div class="lcf-row">
1151
- <label class="lcf-label">Interval</label>
1152
- <input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
1153
- </div>
1154
- <div class="lcf-row">
1155
- <label class="lcf-label">Max reps (blank = ∞)</label>
1156
- <input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
1157
- </div>
1158
- </div>
1159
- <div class="lcf-actions">
1160
- <button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
1161
- <button class="lcf-submit" onclick="createLoop()">Create Loop</button>
1162
- </div>
1163
- </div>
1164
- <div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
1165
- </div>
1166
- </div>
1167
-
1168
- <!-- MEMORY -->
1169
- <div class="view" id="view-memory">
1170
- <div class="vscroll">
1171
- <div class="pg-title">Memory</div>
1172
- <div class="pg-sub">Knowledge palace — stored facts, graph, identity</div>
1173
- <div class="filter-bar">
1174
- <input class="filter-input" id="mem-filter" type="text" placeholder="Search memory…" oninput="filterMemory(this.value)">
1175
- </div>
1176
- <div id="mem-ns-tabs"></div>
1177
- <div id="mem-content"><div class="loading-txt">Loading…</div></div>
1178
- </div>
1179
- </div>
1180
-
1181
- <!-- ORGS -->
1182
- <div class="view" id="view-orgs">
1183
- <div class="vscroll">
1184
- <div class="pg-title">Orgs</div>
1185
- <div class="pg-sub">MASTERMIND organizations and swarms</div>
1186
- <div id="orgs-content"><div class="loading-txt">Loading…</div></div>
1187
- </div>
1188
- </div>
1189
-
1190
- <!-- GLOBAL FEED -->
1191
- <div class="view" id="view-global">
1192
- <div class="vscroll">
1193
- <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
1194
- <div class="pg-title" style="margin-bottom:0">Global Feed</div>
1195
- <span class="pg-sub" id="gf-sub" style="margin-bottom:0">Activity across all projects</span>
1196
- </div>
1197
- <div id="gf-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
1198
- </div>
1199
- </div>
1200
-
1201
- </div><!-- /view-wrap -->
1202
- </div><!-- /main -->
1203
- <div id="app-ambient-hint">Press A to exit ambient mode</div>
1204
- </div><!-- /app -->
1205
- <div id="toast-rack"></div>
1206
-
1207
- <!-- f56: report card modal -->
1208
- <div id="report-modal" onclick="if(event.target===this)closeReportCard()">
1209
- <div id="report-box">
1210
- <div class="rp-head">
1211
- <span class="rp-title">Report Card</span>
1212
- <button class="rp-copy-btn" onclick="copyReportCard()">⎘ Copy</button>
1213
- <button class="rp-close-btn" onclick="closeReportCard()">✕</button>
1214
- </div>
1215
- <div id="report-content"><pre id="report-pre"></pre></div>
1216
- </div>
1217
- </div>
1218
-
1219
- <!-- shortcut help modal -->
1220
- <div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
1221
- <div id="shortcut-box">
1222
- <div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
1223
- <div class="sk-section">Sessions view</div>
1224
- <div class="sk-row"><span class="sk-desc">Navigate rows</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
1225
- <div class="sk-row"><span class="sk-desc">Open focused session</span><span class="sk-keys"><kbd>↵</kbd></span></div>
1226
- <div class="sk-section">Feed (Now view)</div>
1227
- <div class="sk-row"><span class="sk-desc">Navigate entries</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
1228
- <div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
1229
- <div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
1230
- <div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
1231
- <div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
1232
- <div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
1233
- <div class="sk-section">Global</div>
1234
- <div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
1235
- <div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
1236
- <div class="sk-row"><span class="sk-desc">This help</span><span class="sk-keys"><kbd>?</kbd></span></div>
1237
- </div>
1238
- </div>
1239
-
1240
- <!-- budget modal (fixed overlay, outside app) -->
1241
- <div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
1242
- <div id="budget-box">
1243
- <div class="bm-title">Set Cost Budget</div>
1244
- <div class="bm-row">
1245
- <div class="bm-lbl">Daily limit ($)</div>
1246
- <input class="bm-input" id="bm-daily" type="number" min="0" step="1" placeholder="e.g. 20">
1247
- </div>
1248
- <div class="bm-row">
1249
- <div class="bm-lbl">Monthly limit ($)</div>
1250
- <input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
1251
- </div>
1252
- <div class="bm-btns">
1253
- <button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
1254
- <button class="bm-save" onclick="saveBudget()">Save</button>
1255
- </div>
1256
- </div>
1257
- </div>
1258
-
1259
- <script>
1260
- // ── state ──────────────────────────────────────────────────
1261
- let DIR = '';
1262
- let ORIGINAL_DIR = '';
1263
- let gitUser = {};
1264
- let currentView = 'now';
1265
- let allSessions = [];
1266
- let allProjects = [];
1267
- let sessionIdx = 0;
1268
- let pollTimer = null;
1269
- let viewRendered = {};
1270
- let userScrolled = false;
1271
- let selectedEntryId = null;
1272
- let allDrawers = [];
1273
- let dismissedAlerts = new Set();
1274
- let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
1275
- let feedTimeFilter = 'all';
1276
- let cmdFocusIdx = 0;
1277
- let cmdItems = [];
1278
- let liveTailMode = false;
1279
- let liveTailTimer = null;
1280
- let bookmarks = new Set(JSON.parse(localStorage.getItem('mm-bookmarks') || '[]'));
1281
- let showStarredOnly = false;
1282
-
1283
- // ── nav ────────────────────────────────────────────────────
1284
- document.querySelectorAll('.nav-item[data-view]').forEach(el => {
1285
- el.addEventListener('click', () => switchView(el.dataset.view));
1286
- });
1287
-
1288
- document.getElementById('feed-scroll').addEventListener('scroll', () => {
1289
- userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
1290
- });
1291
-
1292
- function switchView(v) {
1293
- currentView = v;
1294
- document.querySelectorAll('.nav-item[data-view]').forEach(el =>
1295
- el.classList.toggle('active', el.dataset.view === v));
1296
- document.querySelectorAll('.view').forEach(el =>
1297
- el.classList.toggle('active', el.id === 'view-' + v));
1298
- const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs', global:'Global Feed' };
1299
- document.getElementById('view-title').textContent = titles[v] || v;
1300
- if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
1301
- }
1302
-
1303
- function renderView(v) {
1304
- if (v === 'now') { refreshNow(); return; }
1305
- if (v === 'projects') renderProjects();
1306
- if (v === 'sessions') renderSessions();
1307
- if (v === 'loops') renderLoops();
1308
- if (v === 'memory') renderMemory();
1309
- if (v === 'orgs') renderOrgs();
1310
- if (v === 'global') renderGlobalFeed();
1311
- }
1312
-
1313
- function refreshCurrent() {
1314
- viewRendered[currentView] = false;
1315
- renderView(currentView);
1316
- }
1317
-
1318
- // ── init ───────────────────────────────────────────────────
1319
- async function init() {
1320
- try {
1321
- const gu = await apiFetch('/api/git-user');
1322
- DIR = gu.cwd || '';
1323
- ORIGINAL_DIR = DIR;
1324
- gitUser = gu;
1325
- document.getElementById('sb-user').textContent = gu.name || gu.email || '—';
1326
- document.getElementById('sb-path').textContent = DIR;
1327
- document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1328
- } catch (_) {}
1329
- // deep-link: ?sess=<id>&proj=<path>
1330
- const params = new URLSearchParams(window.location.search);
1331
- const projParam = params.get('proj');
1332
- const sessParam = params.get('sess');
1333
- if (projParam) {
1334
- DIR = projParam;
1335
- document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1336
- document.getElementById('sb-path').textContent = DIR;
1337
- }
1338
- restoreURLParams();
1339
- viewRendered['now'] = true;
1340
- updateBudgetBtnStyle();
1341
- await refreshNow();
1342
- if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
1343
- initSSE();
1344
- }
1345
-
1346
- function startPolling() {
1347
- clearInterval(pollTimer);
1348
- pollTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 30000);
1349
- }
1350
-
1351
- let _sseSource = null;
1352
- function initSSE() {
1353
- if (_sseSource) { try { _sseSource.close(); } catch {} _sseSource = null; }
1354
- if (!DIR || !window.EventSource) { startPolling(); return; }
1355
- try {
1356
- const src = new EventSource('/api/events-stream?dir=' + enc(DIR));
1357
- src.addEventListener('update', () => { if (currentView === 'now') refreshNowSilent(); });
1358
- src.addEventListener('connected', () => {});
1359
- src.onerror = () => { src.close(); _sseSource = null; startPolling(); };
1360
- _sseSource = src;
1361
- clearInterval(pollTimer); // SSE replaces polling
1362
- } catch { startPolling(); }
1363
- }
1364
-
1365
- async function apiFetch(url) {
1366
- const r = await fetch(url);
1367
- if (!r.ok) throw new Error(r.status);
1368
- return r.json();
1369
- }
1370
-
1371
- // ── project switching ──────────────────────────────────────
1372
- function switchProject(path) {
1373
- if (DIR === path) { switchView('sessions'); return; }
1374
- DIR = path;
1375
- document.getElementById('sb-proj').textContent = path.split('/').filter(Boolean).pop() || '—';
1376
- document.getElementById('sb-path').textContent = path;
1377
- viewRendered = {};
1378
- allSessions = [];
1379
- closeDetail();
1380
- switchView('sessions');
1381
- }
1382
-
1383
- // ── NOW view ───────────────────────────────────────────────
1384
- async function refreshNow() {
1385
- userScrolled = false;
1386
- await Promise.allSettled([loadFeed(), loadMetrics()]);
1387
- }
1388
-
1389
- async function refreshNowSilent() {
1390
- if (liveTailMode) { userScrolled = false; sessionIdx = 0; }
1391
- await Promise.allSettled([loadFeedSilent(), loadMetrics()]);
1392
- }
1393
-
1394
- // ── live tail ──────────────────────────────────────────────
1395
- function toggleLiveTail() {
1396
- liveTailMode = !liveTailMode;
1397
- const btn = document.getElementById('btn-live-tail');
1398
- btn.classList.toggle('on', liveTailMode);
1399
- btn.title = liveTailMode ? 'Live tail ON — click to disable' : 'Toggle live tail (5s refresh + auto-scroll)';
1400
- clearInterval(liveTailTimer);
1401
- if (liveTailMode) {
1402
- // jump to newest session and start fast polling
1403
- if (sessionIdx !== 0 && allSessions.length) { sessionIdx = 0; userScrolled = false; loadFeedForSession(allSessions[0]); }
1404
- liveTailTimer = setInterval(() => { if (currentView === 'now') refreshNowSilent(); }, 5000);
1405
- } else {
1406
- initSSE();
1407
- }
1408
- }
1409
-
1410
- // session nav
1411
- function prevSession() {
1412
- if (sessionIdx < allSessions.length - 1) { sessionIdx++; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
1413
- }
1414
- function nextSession() {
1415
- if (sessionIdx > 0) { sessionIdx--; userScrolled = false; loadFeedForSession(allSessions[sessionIdx]); }
1416
- }
1417
-
1418
- async function loadFeed() {
1419
- if (!DIR) return;
1420
- try {
1421
- const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
1422
- allSessions = sessions;
1423
- document.getElementById('bdg-sessions').textContent = sessions.length || '—';
1424
- if (!sessions.length) {
1425
- setFeedContent('<div class="feed-empty">No sessions yet in this project.</div>');
1426
- return;
1427
- }
1428
- sessionIdx = 0;
1429
- await loadFeedForSession(sessions[0]);
1430
- renderMiniSessions(sessions.slice(0, 6));
1431
- // patch sparkline now that allSessions is populated
1432
- const todayEl = document.getElementById('m-today');
1433
- const sparkWrap = todayEl?.querySelector('.spark-wrap');
1434
- if (sparkWrap) sparkWrap.outerHTML = buildSparkline();
1435
- detectAnomalies();
1436
- } catch (err) {
1437
- setFeedContent('<div class="feed-empty">Could not load feed: ' + esc(err.message) + '</div>');
1438
- }
1439
- }
1440
-
1441
- async function loadFeedSilent() {
1442
- if (!DIR || !allSessions.length) return;
1443
- try {
1444
- const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
1445
- allSessions = sessions;
1446
- if (!sessions.length) return;
1447
- const currentSess = allSessions[sessionIdx] || sessions[0];
1448
- if (!currentSess?.file) return;
1449
- const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(currentSess.file) + '&limit=120');
1450
- renderFeedEvents(data.events || [], true);
1451
- } catch (_) {}
1452
- }
1453
-
1454
- async function loadFeedForSession(sess) {
1455
- if (!sess) return;
1456
- document.getElementById('feed-sess').textContent = sess.id.slice(0, 8) + '…';
1457
- document.getElementById('btn-prev-sess').style.opacity = sessionIdx < allSessions.length - 1 ? '1' : '0.3';
1458
- document.getElementById('btn-next-sess').style.opacity = sessionIdx > 0 ? '1' : '0.3';
1459
- showSessCtx(sess);
1460
- // f48: live cost ticker
1461
- const sessCost = typeof sess.totalCost === 'number' ? sess.totalCost : (typeof sess.cost === 'number' ? sess.cost : null);
1462
- updateLiveTicker(sessCost);
1463
- if (!sess.file) { setFeedContent('<div class="feed-empty">Session file path unavailable.</div>'); return; }
1464
- try {
1465
- const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=120');
1466
- renderFeedEvents(data.events || [], false);
1467
- } catch (err) {
1468
- setFeedContent('<div class="feed-empty">Could not load session: ' + esc(err.message) + '</div>');
1469
- }
1470
- }
1471
-
1472
- // ── feed rendering ─────────────────────────────────────────
1473
-
1474
- // pre-pass: mark tool events as errored based on their tool_result
1475
- function annotateErrors(events) {
1476
- const byId = new Map();
1477
- events.forEach((ev, i) => { if (ev.kind === 'tool' && ev.id) byId.set(ev.id, i); });
1478
- events.forEach(ev => {
1479
- if (ev.kind === 'tool_result' && ev.isError && ev.tool_use_id) {
1480
- const idx = byId.get(ev.tool_use_id);
1481
- if (idx != null) events[idx]._errored = true;
1482
- }
1483
- });
1484
- }
1485
-
1486
- // group consecutive same-cat tool events (threshold: 3+)
1487
- function groupEvents(events) {
1488
- const out = [];
1489
- let i = 0;
1490
- while (i < events.length) {
1491
- const ev = events[i];
1492
- if (ev.kind !== 'tool') { out.push(ev); i++; continue; }
1493
- // look ahead for same cat run
1494
- let j = i + 1;
1495
- while (j < events.length && events[j].kind === 'tool' && events[j].cat === ev.cat && !events[j]._errored && !ev._errored) j++;
1496
- const run = j - i;
1497
- if (run >= 3) {
1498
- out.push({ kind: '_group', cat: ev.cat, count: run, label: catLabel(ev.cat), ts: ev.ts, items: events.slice(i, j) });
1499
- i = j;
1500
- } else {
1501
- out.push(ev); i++;
1502
- }
1503
- }
1504
- return out;
1505
- }
1506
-
1507
- function catLabel(c) {
1508
- 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' };
1509
- return m[c] || 'tool calls';
1510
- }
1511
-
1512
- function renderFeedEvents(events, silent) {
1513
- if (!events.length) {
1514
- if (!silent) setFeedContent('<div class="feed-empty">No events in this session yet.</div>');
1515
- return;
1516
- }
1517
-
1518
- annotateErrors(events);
1519
-
1520
- // filter: only tool + user, skip text/thinking/tool_result and hook system messages
1521
- const HOOK_RE = /^<(local-command-|command-name>|command-message>|local-command-caveat>)/;
1522
- const filtered = events.filter(ev =>
1523
- ev.kind === 'tool' ||
1524
- (ev.kind === 'user' && ev.text?.trim() && !HOOK_RE.test(ev.text.trim())));
1525
-
1526
- // apply time-range filter
1527
- let visible = filtered;
1528
- if (feedTimeFilter !== 'all') {
1529
- const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000 }[feedTimeFilter] || 0;
1530
- const cutoff = Date.now() - ms;
1531
- visible = filtered.filter(ev => !ev.ts || new Date(ev.ts).getTime() >= cutoff);
1532
- }
1533
-
1534
- // update error alert state
1535
- alertState.errorCount = visible.filter(ev => ev._errored).length;
1536
- updateAlerts();
1537
-
1538
- // reverse (newest first), then group
1539
- const reversed = [...visible].reverse();
1540
- const grouped = groupEvents(reversed);
1541
-
1542
- const parts = [];
1543
- let prevKind = null;
1544
-
1545
- for (const item of grouped) {
1546
- if (item.kind === '_group') {
1547
- parts.push(renderGroupRow(item));
1548
- prevKind = 'tool';
1549
- continue;
1550
- }
1551
- if (prevKind === 'user' && item.kind === 'tool') {
1552
- parts.push('<div class="feed-divider"></div>');
1553
- } else if (prevKind === 'tool' && item.kind === 'user') {
1554
- parts.push('<div class="feed-divider"></div>');
1555
- }
1556
- parts.push(renderFeedEntry(item));
1557
- prevKind = item.kind;
1558
- }
1559
-
1560
- const newHtml = parts.join('');
1561
- if (silent) {
1562
- const el = document.getElementById('feed-content');
1563
- if (el.innerHTML === newHtml) return;
1564
- const wasAtTop = !userScrolled;
1565
- el.innerHTML = newHtml;
1566
- if (wasAtTop) document.getElementById('feed-scroll').scrollTop = 0;
1567
- } else {
1568
- setFeedContent(newHtml);
1569
- if (!userScrolled) document.getElementById('feed-scroll').scrollTop = 0;
1570
- }
1571
-
1572
- // update timeline + breakdown with all original events (before time-filter)
1573
- buildTimeline(filtered);
1574
- buildBreakdownByName(filtered);
1575
- buildMinimap(filtered);
1576
- updateBurnRate(filtered);
1577
- // session recap card
1578
- buildRecap(filtered, allSessions[sessionIdx]);
1579
- // infer current activity from most recent tool
1580
- updateCurrentActivity(visible);
1581
- }
1582
-
1583
- function renderGroupRow(g) {
1584
- const { ico, catCls } = toolStyle(g.cat, '');
1585
- const itemsData = JSON.stringify(g.items).replace(/'/g, '&#39;');
1586
- return `<div class="feed-group" data-items='${itemsData}' onclick="expandGroup(this)">
1587
- <div class="feed-ico ${catCls}" style="font-size:9px">${ico}</div>
1588
- <span class="fg-label">${g.count} ${esc(g.label)}</span>
1589
- <span class="fg-expand">▸ expand</span>
1590
- </div>`;
1591
- }
1592
-
1593
- function expandGroup(el) {
1594
- const items = JSON.parse(el.dataset.items);
1595
- const html = items.map(renderFeedEntry).join('');
1596
- el.outerHTML = html;
1597
- // re-apply active feed search to newly injected entries
1598
- const q = document.getElementById('feed-search-input')?.value || '';
1599
- if (feedSearchActive && q) filterFeed(q);
1600
- }
1601
-
1602
- function renderFeedEntry(ev) {
1603
- const ts = ev.ts ? relTime(ev.ts) : '';
1604
- let lbl = '', detail = '', id = ev.id || ev.uuid || '';
1605
- let catCls, ico;
1606
-
1607
- if (ev.kind === 'tool') {
1608
- ({ ico, catCls } = toolStyle(ev.cat, ev.name));
1609
- lbl = esc(ev.label || ev.name || 'tool');
1610
- if (ev.subagent) detail = esc(ev.subagent);
1611
- } else {
1612
- ico = '↵'; catCls = 'cat-user';
1613
- const t = (ev.text || '').trim();
1614
- lbl = esc(t.length > 90 ? t.slice(0, 90) + '…' : t);
1615
- }
1616
-
1617
- const errClass = ev._errored ? ' errored' : '';
1618
- const selClass = selectedEntryId && selectedEntryId === id ? ' selected' : '';
1619
-
1620
- const evData = JSON.stringify(ev).replace(/'/g, '&#39;');
1621
- return `<div class="feed-entry k-${ev.kind}${errClass}${selClass}" data-ev='${evData}' onclick="openDetail(this.dataset.ev)">
1622
- <div class="feed-ico ${catCls}">${ico}</div>
1623
- <div class="feed-body">
1624
- <div class="feed-lbl">${lbl}</div>
1625
- ${detail ? `<div class="feed-detail">${detail}</div>` : ''}
1626
- </div>
1627
- <div class="feed-ts">${ts}</div>
1628
- </div>`;
1629
- }
1630
-
1631
- function toolStyle(cat, name) {
1632
- const map = {
1633
- file: { ico: '◧', catCls: 'cat-file' },
1634
- bash: { ico: '$', catCls: 'cat-bash' },
1635
- agent: { ico: '→', catCls: 'cat-agent' },
1636
- mcp: { ico: '⬡', catCls: 'cat-mcp' },
1637
- search: { ico: '/', catCls: 'cat-search' },
1638
- skill: { ico: '◆', catCls: 'cat-skill' },
1639
- task: { ico: '☑', catCls: 'cat-task' },
1640
- mem: { ico: '◈', catCls: 'cat-mem' },
1641
- };
1642
- if (name === 'Skill') return { ico: '◆', catCls: 'cat-skill' };
1643
- return map[cat] || { ico: '·', catCls: 'cat-other' };
1644
- }
1645
-
1646
- function setFeedContent(html) {
1647
- document.getElementById('feed-content').innerHTML = html;
1648
- }
1649
-
1650
- // ── detail panel ───────────────────────────────────────────
1651
- function openDetail(evJson) {
1652
- const ev = JSON.parse(evJson);
1653
- selectedEntryId = ev.id || ev.uuid || '';
1654
-
1655
- const panel = document.getElementById('detail-panel');
1656
- panel.classList.add('open');
1657
-
1658
- let title = '';
1659
- let bodyHtml = '';
1660
-
1661
- if (ev.kind === 'tool') {
1662
- const { catCls } = toolStyle(ev.cat, ev.name);
1663
- title = ev.name || 'Tool';
1664
- bodyHtml = `
1665
- <div class="d-cat-pill ${catCls}" style="font-size:11px">${esc(ev.cat || 'other')}</div>
1666
- <div class="d-row"><div class="d-lbl">Label</div><div class="d-val">${esc(ev.label || ev.name)}</div></div>
1667
- ${ev.subagent ? `<div class="d-row"><div class="d-lbl">Subagent</div><div class="d-val">${esc(ev.subagent)}</div></div>` : ''}
1668
- ${ev._errored ? `<div class="d-row"><div class="d-lbl">Status</div><div class="d-val error">Error</div></div>` : ''}
1669
- <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
1670
- <div class="d-row"><div class="d-lbl">Tool ID</div><div class="d-val mono">${esc((ev.id || '').slice(0, 24))}</div></div>
1671
- `;
1672
- } else if (ev.kind === 'user') {
1673
- title = 'User message';
1674
- bodyHtml = `
1675
- <div class="d-row"><div class="d-lbl">Time</div><div class="d-val">${ev.ts ? new Date(ev.ts).toLocaleTimeString() : '—'}</div></div>
1676
- <div class="d-row"><div class="d-lbl">Message</div><div class="d-val" style="white-space:pre-wrap">${esc(ev.text || '')}</div></div>
1677
- `;
1678
- }
1679
-
1680
- document.getElementById('detail-title').textContent = title;
1681
- document.getElementById('detail-body').innerHTML = bodyHtml;
1682
- }
1683
-
1684
- function closeDetail() {
1685
- document.getElementById('detail-panel').classList.remove('open');
1686
- selectedEntryId = null;
1687
- }
1688
-
1689
- // ── 12-week calendar heatmap ───────────────────────────────
1690
- function buildSparkline() {
1691
- const DAY = 86400000;
1692
- const now = Date.now();
1693
- const WEEKS = 12;
1694
- const DAYS = WEEKS * 7; // 84 days
1695
- const buckets = new Array(DAYS).fill(0);
1696
- for (const s of allSessions) {
1697
- const ts = s.lastTs || s.mtime;
1698
- if (!ts) continue;
1699
- const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
1700
- const idx = DAYS - 1 - Math.floor(age / DAY);
1701
- if (idx >= 0 && idx < DAYS) buckets[idx]++;
1702
- }
1703
- const max = Math.max(...buckets, 1);
1704
- // offset so first cell starts on Monday of the week 12 weeks ago
1705
- const todayDow = new Date().getDay(); // 0=Sun
1706
- // pad start so column 0 begins on Monday
1707
- const startOffset = todayDow === 0 ? 6 : todayDow - 1;
1708
- const cells = buckets.map((v, i) => {
1709
- const isToday = i === DAYS - 1;
1710
- const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / max * 4));
1711
- const d = new Date(now - (DAYS - 1 - i) * DAY);
1712
- const label = d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
1713
- const title = `${label}: ${v} session${v !== 1 ? 's' : ''}`;
1714
- return `<div class="cal-cell cal-${level}${isToday ? ' cal-today' : ''}" title="${title}"></div>`;
1715
- });
1716
- return `<div class="spark-wrap"><div class="spark-lbl">12-week activity ${buildWowDelta()}</div><div class="cal-grid">${cells.join('')}</div></div>`;
1717
- }
1718
-
1719
- // ── alerts rail ────────────────────────────────────────────
1720
- function updateAlerts() {
1721
- const rail = document.getElementById('alerts-rail');
1722
- const all = [];
1723
-
1724
- if (alertState.todayCost > 50) {
1725
- all.push({ id: 'cost-crit', cls: 'alert-crit', ico: '⚑', msg: `Critical spend: $${alertState.todayCost.toFixed(2)} today` });
1726
- } else if (alertState.todayCost > 20) {
1727
- all.push({ id: 'cost-warn', cls: 'alert-warn', ico: '⚠', msg: `High spend: $${alertState.todayCost.toFixed(2)} today` });
1728
- }
1729
-
1730
- if (alertState.errorCount >= 3) {
1731
- all.push({ id: 'feed-errors', cls: 'alert-warn', ico: '⚠', msg: `${alertState.errorCount} errors in current session`, action: 'jumpToErrors()' });
1732
- }
1733
-
1734
- if (alertState.anomaly) {
1735
- all.push({ id: 'anomaly-sess', cls: 'alert-warn', ico: '◎', msg: alertState.anomaly });
1736
- }
1737
-
1738
- if (alertState.budgetAlert) {
1739
- all.push({ id: 'budget-alert', cls: alertState.budgetCls || 'alert-warn', ico: '⚑', msg: alertState.budgetAlert });
1740
- }
1741
-
1742
- for (const l of alertState.longLoops) {
1743
- all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
1744
- }
1745
-
1746
- const visible = all.filter(a => !dismissedAlerts.has(a.id));
1747
- if (!visible.length) {
1748
- rail.className = '';
1749
- rail.innerHTML = '';
1750
- return;
1751
- }
1752
- rail.className = 'has-alerts';
1753
- rail.innerHTML = visible.map(a =>
1754
- `<div class="alert-item ${a.cls}" data-alert-id="${a.id}"${a.action ? ` onclick="${a.action}" style="cursor:pointer"` : ''}>
1755
- <span class="al-ico">${a.ico}</span>${esc(a.msg)}<span class="al-x" onclick="event.stopPropagation();dismissAlert('${a.id}')">✕</span>
1756
- </div>`).join('');
1757
- }
1758
-
1759
- function dismissAlert(id) {
1760
- dismissedAlerts.add(id);
1761
- updateAlerts();
1762
- }
1763
-
1764
- // ── anomaly detection ──────────────────────────────────────
1765
- function detectAnomalies() {
1766
- const withCost = allSessions.filter(s => typeof (s.totalCost ?? s.cost) === 'number');
1767
- if (withCost.length < 3) { alertState.anomaly = null; updateAlerts(); return; }
1768
- const avg = withCost.reduce((sum, s) => sum + (s.totalCost ?? s.cost ?? 0), 0) / withCost.length;
1769
- const curr = allSessions[sessionIdx];
1770
- const currCost = typeof curr?.totalCost === 'number' ? curr.totalCost : (typeof curr?.cost === 'number' ? curr.cost : null);
1771
- if (currCost !== null && avg > 0.05 && currCost > avg * 2.5 && currCost > 0.5) {
1772
- alertState.anomaly = `Session unusually costly: $${currCost.toFixed(2)} vs $${avg.toFixed(2)} avg`;
1773
- } else {
1774
- alertState.anomaly = null;
1775
- }
1776
- updateAlerts();
1777
- }
1778
-
1779
- // ── session context bar ────────────────────────────────────
1780
- function showSessCtx(sess) {
1781
- const bar = document.getElementById('sess-ctx');
1782
- const isLive = !sess || !allSessions.length || allSessions[0]?.id === sess.id;
1783
- if (isLive) {
1784
- bar.classList.remove('show');
1785
- return;
1786
- }
1787
- document.getElementById('sctx-label').textContent = sess.lastPrompt || sess.id.slice(0, 16) + '…';
1788
- bar.classList.add('show');
1789
- }
1790
-
1791
- function goLive() {
1792
- if (!allSessions.length) return;
1793
- sessionIdx = 0;
1794
- userScrolled = false;
1795
- loadFeedForSession(allSessions[0]);
1796
- }
1797
-
1798
- // ── metrics ────────────────────────────────────────────────
1799
- async function loadMetrics() {
1800
- if (!DIR) return;
1801
- await Promise.allSettled([loadTodayMetrics(), loadLoopMetrics()]);
1802
- }
1803
-
1804
- async function loadTodayMetrics() {
1805
- try {
1806
- const data = await apiFetch('/api/section?name=tokens&dir=' + enc(DIR));
1807
- const s = data?.tokens?.summary || {};
1808
- alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
1809
- updateAlerts();
1810
- checkBudget();
1811
- // topbar cost badge
1812
- const badge = document.getElementById('topbar-cost');
1813
- if (badge && typeof s.todayCost === 'number') {
1814
- badge.textContent = '$' + s.todayCost.toFixed(2) + ' today';
1815
- badge.classList.add('loaded');
1816
- }
1817
- const cost = typeof s.todayCost === 'number' ? '$' + s.todayCost.toFixed(2) : '—';
1818
- const calls = s.todayCalls != null ? s.todayCalls : '—';
1819
- const moCost = typeof s.monthCost === 'number' ? '$' + s.monthCost.toFixed(2) : '—';
1820
- // cost forecast: project monthly spend from daily average
1821
- let forecast = '';
1822
- if (typeof s.monthCost === 'number' && s.monthCost > 0) {
1823
- const day = new Date().getDate();
1824
- const daysInMonth = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate();
1825
- const projected = (s.monthCost / day) * daysInMonth;
1826
- forecast = `<div class="m-row"><span class="m-name">Month forecast</span><span class="m-val forecast">~$${projected.toFixed(2)}</span></div>`;
1827
- }
1828
- document.getElementById('m-today').innerHTML = `
1829
- <div class="m-group-title">Today</div>
1830
- <div class="m-row"><span class="m-name">API cost</span><span class="m-val gold">${cost}</span></div>
1831
- <div class="m-row"><span class="m-name">API calls</span><span class="m-val">${calls}</span></div>
1832
- <div class="m-row"><span class="m-name">Month total</span><span class="m-val">${moCost}</span></div>
1833
- ${forecast}
1834
- ${buildSparkline()}
1835
- `;
1836
- } catch (_) {
1837
- document.getElementById('m-today').innerHTML = `<div class="m-group-title">Today</div><div class="loading-txt">—</div>`;
1838
- }
1839
- }
1840
-
1841
- async function loadLoopMetrics() {
1842
- try {
1843
- const data = await apiFetch('/api/loops?dir=' + enc(DIR));
1844
- const loops = Array.isArray(data) ? data : (data.loops || []);
1845
- document.getElementById('bdg-loops').textContent = loops.length || '—';
1846
-
1847
- // alert on loops running > 2h
1848
- const TWO_HOURS = 2 * 3600 * 1000;
1849
- const now = Date.now();
1850
- alertState.longLoops = loops
1851
- .filter(l => l.status !== 'stopped' && l.status !== 'paused' && l.startedAt && (now - new Date(l.startedAt).getTime()) > TWO_HOURS)
1852
- .map(l => (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 30));
1853
- updateAlerts();
1854
-
1855
- if (!loops.length) {
1856
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt" style="padding:8px 0">None</div>`;
1857
- return;
1858
- }
1859
- const items = loops.slice(0, 5).map(l => {
1860
- const name = (l.name || l.prompt || 'loop').split('--')[0].trim().slice(0, 36);
1861
- return `<div class="mini-loop">
1862
- <div class="ml-name">${esc(name)}</div>
1863
- <div class="ml-meta"><span class="ml-dot"></span>${esc(l.interval || l.schedule || 'running')}</div>
1864
- </div>`;
1865
- }).join('');
1866
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div>${items}`;
1867
- } catch (_) {
1868
- document.getElementById('m-loops').innerHTML = `<div class="m-group-title">Active Loops</div><div class="loading-txt">—</div>`;
1869
- }
1870
- }
1871
-
1872
- function renderMiniSessions(sessions) {
1873
- if (!sessions.length) return;
1874
- const items = sessions.map((s, i) => `
1875
- <div class="mini-sess" onclick="sessionIdx=${i};userScrolled=false;loadFeedForSession(allSessions[${i}])">
1876
- <div class="ms-prompt">${esc(s.lastPrompt || s.id.slice(0, 8))}</div>
1877
- <div class="ms-meta">${relTime(s.lastTs || s.mtime)}</div>
1878
- </div>`).join('');
1879
- document.getElementById('m-sessions').innerHTML = `<div class="m-group-title">Recent Sessions</div>${items}`;
1880
- buildSwimlane();
1881
- }
1882
-
1883
- // ── projects ───────────────────────────────────────────────
1884
- async function renderProjects() {
1885
- const el = document.getElementById('proj-content');
1886
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
1887
- try {
1888
- const data = await apiFetch('/api/projects');
1889
- allProjects = data?.projects || [];
1890
- document.getElementById('bdg-projects').textContent = allProjects.length || '—';
1891
- document.getElementById('proj-pg-sub').textContent =
1892
- allProjects.length + ' project' + (allProjects.length !== 1 ? 's' : '') + ' found';
1893
- renderProjectGrid(allProjects, '');
1894
- } catch (err) {
1895
- el.innerHTML = '<div class="empty">Could not load projects: ' + esc(err.message) + '</div>';
1896
- }
1897
- }
1898
-
1899
- function renderProjectGrid(projects, query) {
1900
- const el = document.getElementById('proj-content');
1901
- const filtered = query ? projects.filter(p =>
1902
- (p.name || p.slug || '').toLowerCase().includes(query.toLowerCase()) ||
1903
- (p.path || '').toLowerCase().includes(query.toLowerCase())) : projects;
1904
- if (!filtered.length) {
1905
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊞</div><div>No projects match</div></div>';
1906
- return;
1907
- }
1908
- el.className = 'proj-grid';
1909
- el.innerHTML = filtered.map(p => {
1910
- const isCurrent = p.path === DIR;
1911
- const score = computeHealthScore(p);
1912
- const hCls = healthClass(score);
1913
- return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
1914
- ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
1915
- <div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
1916
- <div class="proj-card-name">${esc(p.name || p.slug)}</div>
1917
- <div class="proj-card-path">${esc(p.path || '')}</div>
1918
- <div class="proj-card-stats">
1919
- <div class="proj-stat"><div class="ps-v">${p.sessionCount || 0}</div><div class="ps-l">sessions</div></div>
1920
- <div class="proj-stat"><div class="ps-v">${p.memoryCount || 0}</div><div class="ps-l">memories</div></div>
1921
- ${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>` : ''}
1922
- </div>
1923
- </div>`;
1924
- }).join('');
1925
- }
1926
-
1927
- function filterProjects(q) {
1928
- renderProjectGrid(allProjects, q);
1929
- }
1930
-
1931
- // ── sessions ───────────────────────────────────────────────
1932
- async function renderSessions() {
1933
- const el = document.getElementById('sess-content');
1934
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
1935
- try {
1936
- const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
1937
- allSessions = sessions; // always sync — stale ordering breaks jumpToSession
1938
- initTags();
1939
- document.getElementById('bdg-sessions').textContent = sessions.length || '—';
1940
- document.getElementById('sess-pg-sub').textContent =
1941
- sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
1942
- if (!sessions.length) {
1943
- el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
1944
- return;
1945
- }
1946
- let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
1947
- if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
1948
- if (heatmapDateFilter) toShow = toShow.filter(s => {
1949
- const t = s.lastTs || s.mtime; if (!t) return false;
1950
- return new Date(typeof t === 'number' ? t : t).toDateString() === heatmapDateFilter;
1951
- });
1952
- // f57: file pivot filter
1953
- if (filePivot) toShow = toShow.filter(s => (s.filesTouched || []).includes(filePivot));
1954
- if (!toShow.length) {
1955
- el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
1956
- buildSessionHeatmap(sessions);
1957
- return;
1958
- }
1959
- // compute median cost for anomaly detection
1960
- const costsForMedian = sessions.map(s => s.totalCost || 0).filter(c => c > 0).sort((a, b) => a - b);
1961
- const medianCost = costsForMedian.length ? costsForMedian[Math.floor(costsForMedian.length / 2)] : 0;
1962
-
1963
- // group by date
1964
- const now = Date.now(); const DAY = 86400000;
1965
- function sessDateGroup(s) {
1966
- const t = s.lastTs || s.mtime; if (!t) return 'Older';
1967
- const age = now - (typeof t === 'number' ? t : new Date(t).getTime());
1968
- if (age < DAY) return 'Today';
1969
- if (age < 2 * DAY) return 'Yesterday';
1970
- if (age < 8 * DAY) return 'This week';
1971
- return 'Older';
1972
- }
1973
- const GROUP_ORDER = ['Today', 'Yesterday', 'This week', 'Older'];
1974
- const groups = {};
1975
- for (const s of toShow) {
1976
- const g = sessDateGroup(s); if (!groups[g]) groups[g] = [];
1977
- groups[g].push(s);
1978
- }
1979
-
1980
- function renderSessRow(s, idx) {
1981
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
1982
- const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
1983
- const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
1984
- : typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
1985
- const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
1986
- const sCost = s.totalCost || 0;
1987
- let anomBadge = '';
1988
- if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
1989
- // f60: anomaly badge is clickable for explainer
1990
- anomBadge = `<span class="sess-anomaly anom-cost clickable" style="cursor:pointer" onclick="showCostExplainer('${esc(s.id)}',event)" title="Cost ${((sCost/medianCost).toFixed(1))}× above median — click for details">! costly</span>`;
1991
- } else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
1992
- anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
1993
- }
1994
- const satPct = Math.min(100, Math.round((s.totalMessages || 0) / 200 * 100));
1995
- const satColor = satPct > 80 ? 'oklch(65% 0.2 25)' : satPct > 50 ? 'oklch(70% 0.18 80)' : 'var(--accent)';
1996
- const satBar = satPct > 0 ? `<div class="ctx-sat-wrap" title="Context saturation ~${satPct}% (${s.totalMessages||0} turns / 200 est. max)">
1997
- <div class="ctx-sat-bar"><div class="ctx-sat-fill" style="width:${satPct}%;background:${satColor}"></div></div>
1998
- </div>` : '';
1999
- 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('');
2000
- const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
2001
- const isStarred = bookmarks.has(s.id);
2002
- const sData = JSON.stringify(s).replace(/'/g, '&#39;');
2003
- const note = getSessNote(s.id);
2004
- const hasNote = !!note;
2005
- const files = (s.filesTouched || []).slice(0, 5);
2006
- // f57: file chips are clickable for pivot filter — use data-attr to avoid JSON.stringify in onclick
2007
- const filesHtml = files.length ? `<div class="sr-files">${files.map(f => `<span class="sr-file-chip${filePivot===f?' pivot-active':''}" data-fname="${esc(f)}" onclick="setFilePivot(this.dataset.fname,event)" title="Filter by ${esc(f)}">${esc(f)}</span>`).join('')}${(s.filesTouched||[]).length > 5 ? `<span class="sr-file-chip">+${(s.filesTouched||[]).length-5}</span>` : ''}</div>` : '';
2008
- // f62: context window pressure gauge
2009
- const CTX_LIMIT = 200000;
2010
- const tokPct = s.totalInputTokens ? Math.min(100, Math.round(s.totalInputTokens / CTX_LIMIT * 100)) : 0;
2011
- const tokCls = tokPct > 80 ? 'crit' : tokPct > 50 ? 'warn' : '';
2012
- const ctxGauge = tokPct > 5 ? `<div class="ctx-pressure-wrap" title="${(s.totalInputTokens||0).toLocaleString()} input tokens — ${tokPct}% of 200k context">
2013
- <div class="ctx-pressure-bar"><div class="ctx-pressure-fill ${tokCls}" style="width:${tokPct}%"></div></div>
2014
- <span class="ctx-pressure-lbl">${tokPct}% ctx</span>
2015
- </div>` : '';
2016
- // f51: error badge — clickable if errors exist
2017
- const errBadge = (s.errorCount > 0 && s.toolCalls > 0 && (s.errorCount / s.toolCalls) > 0.3)
2018
- ? `<span class="sess-anomaly anom-err clickable" onclick="toggleErrDrawer('${esc(s.id)}',event)" title="Click to see ${s.errorCount} tool errors">${s.errorCount} err</span>`
2019
- : anomBadge;
2020
- // f50: custom tags
2021
- const ctags = getCustomTags(s.id);
2022
- const ctagsHtml = renderCustomTagsInline(s.id, ctags);
2023
- return `<div class="sess-row" data-sess-idx="${idx}" data-sess-id="${esc(s.id)}" onclick="handleSessRowClick(event,this,'${esc(s.id)}')" data-sess-data='${sData}'>
2024
- <div class="sr-top">
2025
- <div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
2026
- <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
2027
- <button class="sr-copy-btn" data-prompt="${esc(s.lastPrompt || s.id)}" onclick="copyPrompt(this.dataset.prompt,event)" title="Copy prompt to clipboard">⎘</button>
2028
- <button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
2029
- <span class="sr-view">→ view</span>
2030
- </div>
2031
- <div class="sr-meta">${esc(meta)}${errBadge}</div>
2032
- ${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
2033
- ${ctagsHtml}
2034
- ${filesHtml}
2035
- ${satBar}
2036
- ${ctxGauge}
2037
- <div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
2038
- <div class="sess-notes-wrap" onclick="event.stopPropagation()">
2039
- <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
2040
- <div class="sess-notes-area" id="snote-${esc(s.id)}">
2041
- <textarea class="sess-note-input" rows="2" placeholder="Session note…" oninput="saveSessNote('${esc(s.id)}',this.value,this.closest('.sess-notes-wrap').querySelector('.sess-notes-toggle'),this.closest('.sess-row').querySelector('.sess-note-saved'))">${esc(note)}</textarea>
2042
- <div class="sess-note-saved"></div>
2043
- </div>
2044
- </div>
2045
- </div>`;
2046
- }
2047
-
2048
- let html = '';
2049
- let flatIdx = 0;
2050
- for (const grp of GROUP_ORDER) {
2051
- const items = groups[grp];
2052
- if (!items || !items.length) continue;
2053
- const gid = 'sg-' + grp.replace(/\s+/g, '-').toLowerCase();
2054
- html += `<div class="sg-section"><div class="sg-header" onclick="toggleSessGroup('${gid}')">
2055
- <span class="sg-title">${grp}</span><span class="sg-count">${items.length}</span><span class="sg-toggle">▾</span>
2056
- </div><div class="sg-body" id="${gid}">`;
2057
- for (const s of items) { html += renderSessRow(s, flatIdx++); }
2058
- html += '</div></div>';
2059
- }
2060
- el.innerHTML = html;
2061
- // prepend tag filter bar if there are common tags
2062
- if (allTags.common.size > 1) {
2063
- el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
2064
- }
2065
- buildSessionHeatmap(sessions);
2066
- buildDigest();
2067
- buildWeeklyRecap();
2068
- buildEfficiencyPanel();
2069
- buildActivityHeatmap();
2070
- buildTokenVelocity();
2071
- buildDailyCostTrend();
2072
- buildHourlyHeatmap();
2073
- buildCostHistogram();
2074
- if (leaderboardOpen) renderLeaderboard();
2075
- syncURLParams();
2076
- // check budget thresholds and fire toasts
2077
- const todayCost = allSessions.filter(s => {
2078
- const t = s.firstTs || s.mtime;
2079
- if (!t) return false;
2080
- const d = new Date(typeof t === 'number' ? t : t);
2081
- const now = new Date();
2082
- return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
2083
- }).reduce((a, s) => a + (s.totalCost || 0), 0);
2084
- const monthCost = allSessions.filter(s => {
2085
- const t = s.firstTs || s.mtime;
2086
- if (!t) return false;
2087
- const d = new Date(typeof t === 'number' ? t : t);
2088
- const now = new Date();
2089
- return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();
2090
- }).reduce((a, s) => a + (s.totalCost || 0), 0);
2091
- checkBudgetToast(todayCost, monthCost);
2092
- } catch (err) {
2093
- el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
2094
- }
2095
- }
2096
-
2097
- function jumpToSession(id) {
2098
- switchView('now');
2099
- history.replaceState(null, '', '?sess=' + encodeURIComponent(id) + '&proj=' + encodeURIComponent(DIR));
2100
- setTimeout(() => {
2101
- const i = allSessions.findIndex(x => x.id === id);
2102
- if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
2103
- }, 80);
2104
- }
2105
-
2106
- // ── session bookmarks ──────────────────────────────────────
2107
- function toggleBookmark(id, e) {
2108
- e.stopPropagation();
2109
- if (bookmarks.has(id)) bookmarks.delete(id);
2110
- else bookmarks.add(id);
2111
- localStorage.setItem('mm-bookmarks', JSON.stringify([...bookmarks]));
2112
- document.querySelectorAll('.sess-star[data-sid="' + id + '"]').forEach(el => {
2113
- const on = bookmarks.has(id);
2114
- el.classList.toggle('on', on);
2115
- el.textContent = on ? '★' : '☆';
2116
- el.title = on ? 'Remove bookmark' : 'Bookmark session';
2117
- });
2118
- }
2119
-
2120
- function toggleSessStarFilter() {
2121
- showStarredOnly = !showStarredOnly;
2122
- const btn = document.getElementById('sess-star-filter');
2123
- btn.classList.toggle('on', showStarredOnly);
2124
- viewRendered['sessions'] = false;
2125
- renderSessions();
2126
- viewRendered['sessions'] = true;
2127
- syncURLParams();
2128
- }
2129
-
2130
- // ── feature 1: auto-tags ───────────────────────────────────
2131
- const STOP_WORDS = new Set('the a an and or but in on at to for of is are was were be been being have has had do does did will would could should may might shall can i you he she it we they this that these those with from by about as into through during before after above below up down out off over under again further then once here there when where why how all any both each few more most other some such no nor not only own same so than too very just because if although when while'.split(' '));
2132
-
2133
- function extractTags(sessions) {
2134
- // compute per-session tags from lastPrompt text
2135
- const sessionTags = new Map();
2136
- const globalFreq = {};
2137
- for (const s of sessions) {
2138
- const text = (s.lastPrompt || '').toLowerCase();
2139
- const words = text.match(/\b[a-z][a-z0-9_-]{2,}\b/g) || [];
2140
- const freq = {};
2141
- for (const w of words) {
2142
- if (!STOP_WORDS.has(w)) freq[w] = (freq[w] || 0) + 1;
2143
- }
2144
- // top 3 words for this session
2145
- const top = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 3).map(e => e[0]);
2146
- sessionTags.set(s.id, top);
2147
- for (const t of top) globalFreq[t] = (globalFreq[t] || 0) + 1;
2148
- }
2149
- // only keep tags that appear in 2+ sessions OR are in the current session
2150
- const common = new Set(Object.entries(globalFreq).filter(([, v]) => v >= 2).map(([k]) => k));
2151
- return { sessionTags, common };
2152
- }
2153
-
2154
- let allTags = { sessionTags: new Map(), common: new Set() };
2155
- let activeTagFilter = null;
2156
-
2157
- function initTags() {
2158
- allTags = extractTags(allSessions);
2159
- }
2160
-
2161
- function buildTagFilterBar(sessions) {
2162
- if (!allTags.common.size) return '';
2163
- const sorted = [...allTags.common].sort();
2164
- const chips = sorted.map(t =>
2165
- `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
2166
- ).join('');
2167
- return `<div class="tag-filter-bar">${chips}</div>`;
2168
- }
2169
-
2170
- function setTagFilter(tag) {
2171
- activeTagFilter = activeTagFilter === tag ? null : tag;
2172
- viewRendered['sessions'] = false;
2173
- renderSessions();
2174
- viewRendered['sessions'] = true;
2175
- syncURLParams();
2176
- }
2177
-
2178
- // ── feature 2: session recap ───────────────────────────────
2179
- function buildRecap(events, sess) {
2180
- const recap = document.getElementById('feed-recap');
2181
- if (!recap) return;
2182
- const tools = events.filter(e => e.kind === 'tool');
2183
- const users = events.filter(e => e.kind === 'user');
2184
- const errors = events.filter(e => e._errored);
2185
- if (!tools.length && !users.length) { recap.className = ''; return; }
2186
-
2187
- // dominant tool category
2188
- const cats = {};
2189
- for (const e of tools) cats[e.cat || 'other'] = (cats[e.cat || 'other'] || 0) + 1;
2190
- const topCat = Object.entries(cats).sort((a, b) => b[1] - a[1])[0];
2191
- const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
2192
-
2193
- const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
2194
-
2195
- const stats = [
2196
- tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
2197
- users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
2198
- costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
2199
- errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
2200
- ].filter(Boolean).join('');
2201
-
2202
- recap.innerHTML = `<div class="recap-text">${stats}</div>`;
2203
- recap.className = 'show';
2204
- }
2205
-
2206
- // ── feature 3: global feed ─────────────────────────────────
2207
- async function renderGlobalFeed() {
2208
- const el = document.getElementById('gf-content');
2209
- el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
2210
- try {
2211
- // fetch project list
2212
- const data = await apiFetch('/api/projects');
2213
- const projects = (data?.projects || []).slice(0, 8);
2214
- if (!projects.length) {
2215
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
2216
- return;
2217
- }
2218
- document.getElementById('gf-sub').textContent = `Last activity across ${projects.length} projects`;
2219
-
2220
- // fetch sessions for each project in parallel
2221
- const results = await Promise.allSettled(
2222
- projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
2223
- );
2224
-
2225
- // flatten + sort by recency
2226
- const entries = [];
2227
- for (const r of results) {
2228
- if (r.status !== 'fulfilled') continue;
2229
- const { project, sessions } = r.value;
2230
- for (const s of sessions.slice(0, 3)) {
2231
- entries.push({ project, session: s });
2232
- }
2233
- }
2234
- entries.sort((a, b) => {
2235
- const ta = a.session.lastTs || a.session.mtime || 0;
2236
- const tb = b.session.lastTs || b.session.mtime || 0;
2237
- return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
2238
- });
2239
-
2240
- if (!entries.length) {
2241
- el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
2242
- return;
2243
- }
2244
-
2245
- el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
2246
- const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
2247
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
2248
- const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
2249
- const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
2250
- return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
2251
- <div class="sr-top">
2252
- <div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
2253
- <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
2254
- <span class="gf-proj-tag">${esc(projName)}</span>
2255
- </div>
2256
- <div class="sr-meta">${esc(meta)}</div>
2257
- </div>`;
2258
- }).join('') + '</div>';
2259
- } catch (err) {
2260
- el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
2261
- }
2262
- }
2263
-
2264
- // ── feature 4: budget cap + desktop notification ───────────
2265
- let budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
2266
-
2267
- function openBudgetModal() {
2268
- const b = budget;
2269
- document.getElementById('bm-daily').value = b.daily || '';
2270
- document.getElementById('bm-monthly').value = b.monthly || '';
2271
- document.getElementById('budget-modal').classList.add('open');
2272
- document.getElementById('bm-daily').focus();
2273
- }
2274
-
2275
- function closeBudgetModal() {
2276
- document.getElementById('budget-modal').classList.remove('open');
2277
- }
2278
-
2279
- function saveBudget() {
2280
- budget = {
2281
- daily: parseFloat(document.getElementById('bm-daily').value) || null,
2282
- monthly: parseFloat(document.getElementById('bm-monthly').value) || null,
2283
- };
2284
- localStorage.setItem('mm-budget', JSON.stringify(budget));
2285
- closeBudgetModal();
2286
- checkBudget(); // check immediately
2287
- updateBudgetBtnStyle();
2288
- }
2289
-
2290
- function updateBudgetBtnStyle() {
2291
- const btn = document.getElementById('btn-budget');
2292
- if (!btn) return;
2293
- const hasBudget = budget.daily || budget.monthly;
2294
- btn.style.color = hasBudget ? 'var(--accent)' : '';
2295
- }
2296
-
2297
- function checkBudget() {
2298
- const cost = alertState.todayCost;
2299
- if (!cost) return;
2300
- if (budget.daily) {
2301
- const pct = cost / budget.daily;
2302
- if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
2303
- alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
2304
- alertState.budgetCls = 'alert-crit';
2305
- } else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
2306
- alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
2307
- alertState.budgetCls = 'alert-warn';
2308
- maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
2309
- } else {
2310
- alertState.budgetAlert = null;
2311
- }
2312
- updateAlerts();
2313
- }
2314
- }
2315
-
2316
- function maybeNotify(title, body) {
2317
- if (!('Notification' in window)) return;
2318
- if (Notification.permission === 'granted') {
2319
- new Notification(title, { body, icon: '' });
2320
- } else if (Notification.permission !== 'denied') {
2321
- Notification.requestPermission().then(p => { if (p === 'granted') new Notification(title, { body }); });
2322
- }
2323
- }
2324
-
2325
- // ── feature 5: session replay ──────────────────────────────
2326
- let replayEvents = [];
2327
- let replayIdx = 0;
2328
- let replayActive = false;
2329
- let replayTimer = null;
2330
-
2331
- function startReplay() {
2332
- // collect visible feed entries as ordered list
2333
- const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
2334
- if (!entries.length) return;
2335
- replayEvents = entries;
2336
- replayIdx = 0;
2337
- replayActive = false;
2338
- document.getElementById('replay-bar').classList.add('show');
2339
- // dim all entries
2340
- entries.forEach(el => { el.style.opacity = '0.2'; el.style.transition = 'opacity 0.15s'; });
2341
- highlightReplayEntry();
2342
- }
2343
-
2344
- function stopReplay() {
2345
- clearInterval(replayTimer);
2346
- replayActive = false;
2347
- replayEvents.forEach(el => { el.style.opacity = ''; el.style.transition = ''; });
2348
- replayEvents = [];
2349
- document.getElementById('replay-bar').classList.remove('show');
2350
- document.getElementById('rp-play').textContent = '▶';
2351
- document.getElementById('rp-play').classList.remove('active');
2352
- }
2353
-
2354
- function highlightReplayEntry() {
2355
- replayEvents.forEach((el, i) => {
2356
- el.style.opacity = i === replayIdx ? '1' : (i < replayIdx ? '0.5' : '0.2');
2357
- });
2358
- const total = replayEvents.length;
2359
- const pct = total > 1 ? Math.round(replayIdx / (total - 1) * 100) : 100;
2360
- document.getElementById('rp-fill').style.width = pct + '%';
2361
- document.getElementById('rp-counter').textContent = `${replayIdx + 1} / ${total}`;
2362
- // scroll into view
2363
- replayEvents[replayIdx]?.scrollIntoView({ block: 'nearest' });
2364
- }
2365
-
2366
- function replayStep(dir) {
2367
- replayIdx = Math.max(0, Math.min(replayEvents.length - 1, replayIdx + dir));
2368
- highlightReplayEntry();
2369
- }
2370
-
2371
- function replayToggle() {
2372
- if (!replayEvents.length) { startReplay(); return; }
2373
- replayActive = !replayActive;
2374
- const btn = document.getElementById('rp-play');
2375
- btn.textContent = replayActive ? '⏸' : '▶';
2376
- btn.classList.toggle('active', replayActive);
2377
- if (replayActive) {
2378
- replayTimer = setInterval(() => {
2379
- if (replayIdx >= replayEvents.length - 1) { replayToggle(); return; }
2380
- replayStep(1);
2381
- }, 600);
2382
- } else {
2383
- clearInterval(replayTimer);
2384
- }
2385
- }
2386
-
2387
- // ── feature 6: project health score ───────────────────────
2388
- function computeHealthScore(p) {
2389
- let score = 50; // base
2390
- const now = Date.now();
2391
- const DAY = 86400000;
2392
- // recency: up to +30 points for activity in last 7 days
2393
- if (p.lastActivity) {
2394
- const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : new Date(p.lastActivity).getTime());
2395
- if (age < DAY) score += 30;
2396
- else if (age < 3*DAY) score += 20;
2397
- else if (age < 7*DAY) score += 10;
2398
- else if (age > 30*DAY) score -= 15;
2399
- }
2400
- // session count: up to +15
2401
- const sc = p.sessionCount || 0;
2402
- score += Math.min(15, sc * 2);
2403
- // memory: up to +5
2404
- score += Math.min(5, (p.memoryCount || 0));
2405
- return Math.max(0, Math.min(99, Math.round(score)));
2406
- }
2407
-
2408
- function healthClass(score) {
2409
- if (score >= 70) return 'ph-hi';
2410
- if (score >= 40) return 'ph-mid';
2411
- return 'ph-lo';
2412
- }
2413
-
2414
- // ── feature 7: tool call frequency chart (by name) ─────────
2415
- function buildBreakdownByName(events) {
2416
- const counts = {};
2417
- for (const ev of events) {
2418
- if (ev.kind !== 'tool') continue;
2419
- const name = (ev.name || ev.cat || 'other').replace(/^mcp__.*$/, 'MCP').replace(/^m__.*$/, 'MCP');
2420
- counts[name] = (counts[name] || 0) + 1;
2421
- }
2422
- const total = Object.values(counts).reduce((a, b) => a + b, 0);
2423
- if (!total) {
2424
- document.getElementById('m-breakdown').innerHTML =
2425
- '<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
2426
- return;
2427
- }
2428
- const CAT_COLOR = { Bash:'oklch(72% 0.18 75)', Read:'oklch(60% 0.12 220)', Edit:'oklch(65% 0.15 160)', Write:'oklch(65% 0.15 160)', Agent:'oklch(70% 0.15 300)', Task:'oklch(65% 0.15 280)', MCP:'oklch(65% 0.15 200)', WebFetch:'oklch(60% 0.12 195)', WebSearch:'oklch(60% 0.12 195)' };
2429
- const getColor = n => CAT_COLOR[n] || 'var(--text-xs)';
2430
- const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
2431
- const rows = sorted.map(([name, cnt]) => {
2432
- const pct = Math.round(cnt / total * 100);
2433
- return `<div class="tb-row">
2434
- <div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
2435
- <div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
2436
- <div class="tb-count">${cnt}</div>
2437
- </div>`;
2438
- }).join('');
2439
- document.getElementById('m-breakdown').innerHTML =
2440
- `<div class="m-group-title">Tool Usage <span style="font-size:10px;color:var(--text-xs);font-weight:400">${total} calls</span></div><div class="m-breakdown">${rows}</div>`;
2441
- }
2442
-
2443
- // ── feature 8: ambient mode ────────────────────────────────
2444
- function toggleAmbient() {
2445
- document.getElementById('app').classList.toggle('ambient');
2446
- }
2447
-
2448
- // ── feature 9: daily digest ────────────────────────────────
2449
- const DIGEST_DISMISSED_KEY = 'mm-digest-dismissed';
2450
-
2451
- function buildDigest() {
2452
- const todayKey = new Date().toISOString().slice(0, 10);
2453
- if (localStorage.getItem(DIGEST_DISMISSED_KEY) === todayKey) return;
2454
- if (!allSessions.length) return;
2455
-
2456
- const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
2457
- const todaySessions = allSessions.filter(s => {
2458
- const t = s.lastTs || s.mtime;
2459
- return t && new Date(typeof t === 'number' ? t : t).getTime() >= todayStart.getTime();
2460
- });
2461
- if (!todaySessions.length) return;
2462
-
2463
- const totalCost = todaySessions.reduce((a, s) => a + (s.totalCost || 0), 0);
2464
- const totalTools = todaySessions.reduce((a, s) => a + (s.toolCalls || 0), 0);
2465
- const totalMsgs = todaySessions.reduce((a, s) => a + (s.userMessages || 0), 0);
2466
- const longestMs = Math.max(...todaySessions.map(s => s.totalDurationMs || 0));
2467
-
2468
- // top tags from today's sessions using the auto-tag system
2469
- const tagFreq = {};
2470
- for (const s of todaySessions) {
2471
- for (const t of (allTags.sessionTags.get(s.id) || [])) tagFreq[t] = (tagFreq[t] || 0) + 1;
2472
- }
2473
- const themes = Object.entries(tagFreq).sort((a,b) => b[1]-a[1]).slice(0, 3).map(e => e[0]);
2474
-
2475
- const stats = [
2476
- `${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
2477
- totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
2478
- totalTools > 0 ? `${totalTools} tool calls` : null,
2479
- totalMsgs > 0 ? `${totalMsgs} messages` : null,
2480
- longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
2481
- ...themes.map(t => `#${t}`),
2482
- ].filter(Boolean);
2483
-
2484
- // cost forecast: project monthly spend from daily average
2485
- const today2 = new Date();
2486
- const daysInMonth = new Date(today2.getFullYear(), today2.getMonth() + 1, 0).getDate();
2487
- const dayOfMonth = today2.getDate();
2488
- const monthCostSoFar = allSessions.filter(s => {
2489
- const t = s.firstTs || s.mtime;
2490
- if (!t) return false;
2491
- const d = new Date(typeof t === 'number' ? t : t);
2492
- return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
2493
- }).reduce((a, s) => a + (s.totalCost || 0), 0);
2494
- const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
2495
- const projected = dailyAvg * daysInMonth;
2496
- const forecastHtml = projected > 0.05 ? `<span class="digest-stat" title="Projected monthly spend at current pace" style="color:oklch(72% 0.14 200)">~$${projected.toFixed(2)}/mo</span>` : '';
2497
- document.getElementById('digest-stats').innerHTML =
2498
- stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + forecastHtml;
2499
- document.getElementById('digest-card').classList.add('show');
2500
- }
2501
-
2502
- function dismissDigest() {
2503
- const todayKey = new Date().toISOString().slice(0, 10);
2504
- localStorage.setItem(DIGEST_DISMISSED_KEY, todayKey);
2505
- document.getElementById('digest-card').classList.remove('show');
2506
- }
2507
-
2508
- // ── feature 10: minimap scrubber ──────────────────────────
2509
- function buildMinimap(events) {
2510
- const track = document.getElementById('feed-minimap-track');
2511
- const thumb = document.getElementById('mm-thumb');
2512
- const scroll = document.getElementById('feed-scroll');
2513
- if (!track || !thumb || !scroll) return;
2514
-
2515
- // feed renders newest-first (reversed), so reverse events to match visual order
2516
- const tools = [...events].reverse().filter(ev => ev.kind === 'tool' || ev.kind === 'user');
2517
- if (!tools.length) { track.innerHTML = ''; return; }
2518
-
2519
- const CAT_CLS = { file:'mp-file', bash:'mp-bash', agent:'mp-agent', mcp:'mp-mcp', user:'mp-user' };
2520
- const H = scroll.clientHeight || 400;
2521
- const N = tools.length;
2522
- track.innerHTML = tools.map((ev, i) => {
2523
- const top = Math.round((i / N) * H);
2524
- const h = Math.max(3, Math.round((1 / N) * H * 0.7));
2525
- const cls = ev._errored ? 'mp-err' : (ev.kind === 'user' ? 'mp-user' : (CAT_CLS[ev.cat] || 'mp-other'));
2526
- return `<div class="mm-pip ${cls}" style="top:${top}px;height:${h}px" onclick="minimapJump(${i},${N})"></div>`;
2527
- }).join('');
2528
-
2529
- // sync thumb
2530
- const updateThumb = () => {
2531
- const ratio = scroll.scrollHeight > H ? scroll.scrollTop / (scroll.scrollHeight - H) : 0;
2532
- const thH = Math.max(20, Math.round(H * (H / Math.max(scroll.scrollHeight, H + 1))));
2533
- thumb.style.height = thH + 'px';
2534
- thumb.style.top = Math.round(ratio * (H - thH)) + 'px';
2535
- };
2536
- scroll.removeEventListener('scroll', scroll._mmListener || (() => {}));
2537
- scroll._mmListener = updateThumb;
2538
- scroll.addEventListener('scroll', scroll._mmListener);
2539
- updateThumb();
2540
- }
2541
-
2542
- function minimapJump(idx, total) {
2543
- const scroll = document.getElementById('feed-scroll');
2544
- if (!scroll) return;
2545
- scroll.scrollTop = Math.round((idx / total) * scroll.scrollHeight);
2546
- }
2547
-
2548
- // ── feature 11: cost leaderboard ──────────────────────────
2549
- let leaderboardOpen = false;
2550
-
2551
- function toggleLeaderboard() {
2552
- leaderboardOpen = !leaderboardOpen;
2553
- document.getElementById('btn-leaderboard').classList.toggle('on', leaderboardOpen);
2554
- const panel = document.getElementById('lb-panel');
2555
- panel.style.display = leaderboardOpen ? 'block' : 'none';
2556
- if (leaderboardOpen) renderLeaderboard();
2557
- }
2558
-
2559
- function renderLeaderboard() {
2560
- const sorted = [...allSessions]
2561
- .filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
2562
- .sort((a, b) => b.totalCost - a.totalCost)
2563
- .slice(0, 15);
2564
- const body = document.getElementById('lb-body');
2565
- if (!sorted.length) { body.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-xs);padding:12px">No cost data yet</td></tr>'; return; }
2566
- body.innerHTML = sorted.map((s, i) => {
2567
- const cost = '$' + s.totalCost.toFixed(2);
2568
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
2569
- const prompt = s.lastPrompt || s.id;
2570
- return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
2571
- <td class="lb-rank">${i + 1}</td>
2572
- <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
2573
- <td class="lb-cost">${cost}</td>
2574
- <td class="lb-dur">${dur}</td>
2575
- </tr>`;
2576
- }).join('');
2577
- }
2578
-
2579
- // ── feature 12: session diff ──────────────────────────────
2580
- let diffMode = false;
2581
- let diffSelA = null; let diffSelB = null;
2582
-
2583
- function toggleDiffMode() {
2584
- diffMode = !diffMode;
2585
- document.getElementById('btn-diff').classList.toggle('on', diffMode);
2586
- const panel = document.getElementById('diff-panel');
2587
- panel.classList.toggle('show', diffMode);
2588
- if (!diffMode) clearDiff();
2589
- }
2590
-
2591
- function clearDiff() {
2592
- diffSelA = null; diffSelB = null;
2593
- document.getElementById('diff-hint').style.display = '';
2594
- document.getElementById('diff-cols').style.display = 'none';
2595
- document.getElementById('diff-cols').innerHTML = '';
2596
- document.querySelectorAll('.sess-row.diff-sel-a, .sess-row.diff-sel-b').forEach(el => {
2597
- el.classList.remove('diff-sel-a', 'diff-sel-b');
2598
- });
2599
- }
2600
-
2601
- function diffSelectSession(sess, el) {
2602
- if (!diffMode) return;
2603
- if (diffSelA && diffSelA.id === sess.id) {
2604
- diffSelA = null;
2605
- el.classList.remove('diff-sel-a');
2606
- document.getElementById('diff-hint').style.display = '';
2607
- document.getElementById('diff-cols').style.display = 'none';
2608
- return;
2609
- }
2610
- if (diffSelB && diffSelB.id === sess.id) {
2611
- diffSelB = null;
2612
- el.classList.remove('diff-sel-b');
2613
- renderDiff();
2614
- return;
2615
- }
2616
- if (!diffSelA) {
2617
- diffSelA = sess;
2618
- el.classList.add('diff-sel-a');
2619
- } else if (!diffSelB) {
2620
- diffSelB = sess;
2621
- el.classList.add('diff-sel-b');
2622
- } else {
2623
- // replace B with new selection
2624
- document.querySelectorAll('.sess-row.diff-sel-b').forEach(e => e.classList.remove('diff-sel-b'));
2625
- diffSelB = sess;
2626
- el.classList.add('diff-sel-b');
2627
- }
2628
- if (diffSelA && diffSelB) {
2629
- renderDiff();
2630
- } else {
2631
- document.getElementById('diff-hint').textContent = diffSelA ? 'Now click a second session to compare' : 'Click two sessions below to compare them';
2632
- document.getElementById('diff-hint').style.display = '';
2633
- document.getElementById('diff-cols').style.display = 'none';
2634
- }
2635
- }
2636
-
2637
- function renderDiff() {
2638
- if (!diffSelA || !diffSelB) return;
2639
- document.getElementById('diff-hint').style.display = 'none';
2640
- const cols = document.getElementById('diff-cols');
2641
- cols.style.display = '';
2642
- const fmt = (a, b, key, prefix = '') => {
2643
- const va = a[key], vb = b[key];
2644
- if (va == null && vb == null) return '';
2645
- const fa = prefix + (va != null ? va : '—');
2646
- const fb = prefix + (vb != null ? vb : '—');
2647
- const hiA = va != null && vb != null && va > vb ? ' diff-hi' : (va != null && vb != null && va < vb ? ' diff-lo' : '');
2648
- const hiB = va != null && vb != null && vb > va ? ' diff-hi' : (va != null && vb != null && vb < va ? ' diff-lo' : '');
2649
- return [fa, hiA, fb, hiB];
2650
- };
2651
-
2652
- const diffCol = (s) => {
2653
- const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '—';
2654
- const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
2655
- const tools = s.toolCalls != null ? s.toolCalls : '—';
2656
- const msgs = s.userMessages != null ? s.userMessages : '—';
2657
- const errs = s.errors != null ? s.errors : '—';
2658
- const prompt = (s.lastPrompt || s.id || '').slice(0, 50);
2659
- return { cost, dur, tools, msgs, errs, prompt, s };
2660
- };
2661
-
2662
- const a = diffCol(diffSelA), b = diffCol(diffSelB);
2663
- const rows = [
2664
- ['Cost', a.cost, b.cost, typeof diffSelA.totalCost === 'number' && typeof diffSelB.totalCost === 'number' ? (diffSelA.totalCost > diffSelB.totalCost ? ['diff-hi','diff-lo'] : diffSelA.totalCost < diffSelB.totalCost ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
2665
- ['Duration', a.dur, b.dur, ['','']],
2666
- ['Tool calls', a.tools, b.tools, typeof a.tools === 'number' && typeof b.tools === 'number' ? (a.tools > b.tools ? ['diff-hi','diff-lo'] : a.tools < b.tools ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
2667
- ['Messages', a.msgs, b.msgs, ['','']],
2668
- ['Errors', a.errs, b.errs, ['','']],
2669
- ];
2670
-
2671
- const colHtml = (idx) => `<div class="diff-col">
2672
- <div class="diff-col-title">${esc(idx === 0 ? a.prompt : b.prompt)}</div>
2673
- ${rows.map(([k, va, vb, cls]) => `<div class="diff-row">
2674
- <span class="diff-k">${k}</span>
2675
- <span class="diff-v ${cls[idx]}">${esc(idx === 0 ? va : vb)}</span>
2676
- </div>`).join('')}
2677
- </div>`;
2678
-
2679
- cols.innerHTML = colHtml(0) + colHtml(1);
2680
- }
2681
-
2682
- // ── feature 13: export session as .md file ────────────────
2683
- async function exportSession() {
2684
- const btn = document.getElementById('btn-export-sess');
2685
- const sess = allSessions[sessionIdx];
2686
- if (!sess?.file) return;
2687
- try {
2688
- btn.textContent = '…';
2689
- const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
2690
- const events = data.events || [];
2691
- const lines = [
2692
- `# Session: ${sess.lastPrompt || sess.id}`,
2693
- `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`,
2694
- sess.totalCost != null ? `> Cost: $${sess.totalCost.toFixed(2)}` : '',
2695
- sess.totalDurationMs ? `> Duration: ${fmtDur(sess.totalDurationMs)}` : '',
2696
- '',
2697
- ].filter(s => s !== null);
2698
- for (const ev of events) {
2699
- if (ev.kind === 'user' && ev.text?.trim()) {
2700
- lines.push(`\n## ${ev.text.trim().slice(0, 80)}`);
2701
- if (ev.ts) lines.push(`_${new Date(ev.ts).toLocaleTimeString()}_`);
2702
- } else if (ev.kind === 'tool') {
2703
- const label = ev.label || ev.name || ev.cat;
2704
- lines.push(`- \`${ev.name || ev.cat}\`${label ? ': ' + label : ''}${ev._errored ? ' ⚠ error' : ''}`);
2705
- }
2706
- }
2707
- lines.push('', `---`, `_Exported from monomind dashboard_`);
2708
- const blob = new Blob([lines.join('\n')], { type: 'text/markdown' });
2709
- const a = document.createElement('a');
2710
- a.href = URL.createObjectURL(blob);
2711
- a.download = `session-${sess.id.slice(0, 8)}.md`;
2712
- a.click();
2713
- URL.revokeObjectURL(a.href);
2714
- btn.textContent = '✓ Saved';
2715
- setTimeout(() => { btn.textContent = '⬇ Export'; }, 1800);
2716
- } catch (err) {
2717
- btn.textContent = '✕ Error';
2718
- setTimeout(() => { btn.textContent = '⬇ Export'; }, 1500);
2719
- }
2720
- }
2721
-
2722
- // ── feature 14: focus mode ────────────────────────────────
2723
- let focusMode = false;
2724
- function toggleFocusMode() {
2725
- focusMode = !focusMode;
2726
- document.getElementById('feed-pane').classList.toggle('focus-mode', focusMode);
2727
- document.getElementById('btn-focus').classList.toggle('on', focusMode);
2728
- document.getElementById('btn-focus').title = focusMode ? 'Exit focus mode' : 'Focus mode: user messages + errors only';
2729
- }
2730
-
2731
- // ── feature 15: session notes ─────────────────────────────
2732
- const NOTES_KEY = 'mm-sess-notes';
2733
- function getAllNotes() {
2734
- try { return JSON.parse(localStorage.getItem(NOTES_KEY) || '{}'); } catch { return {}; }
2735
- }
2736
- function getSessNote(id) { return getAllNotes()[id] || ''; }
2737
- function saveSessNote(id, text, toggleBtn, savedEl) {
2738
- const all = getAllNotes();
2739
- if (text.trim()) all[id] = text; else delete all[id];
2740
- localStorage.setItem(NOTES_KEY, JSON.stringify(all));
2741
- if (toggleBtn) {
2742
- toggleBtn.classList.toggle('has-note', !!text.trim());
2743
- toggleBtn.textContent = '✎ ' + (text.trim() ? 'Note' : 'Add note');
2744
- }
2745
- if (savedEl) {
2746
- savedEl.textContent = 'Saved';
2747
- clearTimeout(savedEl._t);
2748
- savedEl._t = setTimeout(() => { savedEl.textContent = ''; }, 1200);
2749
- }
2750
- }
2751
- function toggleSessNote(id, btn) {
2752
- const area = document.getElementById('snote-' + id);
2753
- if (!area) return;
2754
- const open = area.classList.toggle('open');
2755
- if (open) { const ta = area.querySelector('textarea'); if (ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); } }
2756
- }
2757
-
2758
- // ── feature 16: burn rate gauge ───────────────────────────
2759
- let burnRateEvents = [];
2760
- let burnRateTimer = null;
2761
-
2762
- function updateBurnRate(events) {
2763
- burnRateEvents = events;
2764
- renderBurnGauge();
2765
- }
2766
-
2767
- function renderBurnGauge() {
2768
- const el = document.getElementById('m-burn');
2769
- if (!el) return;
2770
- const tools = burnRateEvents.filter(ev => ev.kind === 'tool' && ev.ts);
2771
- if (tools.length < 2) {
2772
- el.innerHTML = '<div class="m-group-title">Burn Rate</div><div class="loading-txt" style="padding:4px 0">—</div>';
2773
- return;
2774
- }
2775
- const now = Date.now();
2776
- // calls in last 5 min, 15 min, 60 min
2777
- const t5 = tools.filter(e => now - new Date(e.ts).getTime() < 300000).length;
2778
- const t15 = tools.filter(e => now - new Date(e.ts).getTime() < 900000).length;
2779
- const t60 = tools.filter(e => now - new Date(e.ts).getTime() < 3600000).length;
2780
- const rate5 = (t5 / 5).toFixed(1); // calls/min
2781
- const rate15 = (t15 / 15).toFixed(1);
2782
- const rate60 = (t60 / 60).toFixed(1);
2783
- const maxRate = 20; // calls/min considered "hot"
2784
- const pct = v => Math.min(100, Math.round((parseFloat(v) / maxRate) * 100));
2785
- const cls = v => parseFloat(v) < 4 ? 'burn-rate-ok' : parseFloat(v) < 10 ? 'burn-rate-warn' : 'burn-rate-hot';
2786
- el.innerHTML = `<div class="m-group-title">Burn Rate <span style="font-size:10px;color:var(--text-xs);font-weight:400">calls/min</span></div>
2787
- <div class="burn-gauge-wrap">
2788
- <div class="burn-gauge-row"><div class="burn-gauge-label">5 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate5)}" style="width:${pct(rate5)}%"></div></div><div class="burn-val">${rate5}</div></div>
2789
- <div class="burn-gauge-row"><div class="burn-gauge-label">15 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate15)}" style="width:${pct(rate15)}%"></div></div><div class="burn-val">${rate15}</div></div>
2790
- <div class="burn-gauge-row"><div class="burn-gauge-label">60 min</div><div class="burn-gauge-track"><div class="burn-gauge-fill ${cls(rate60)}" style="width:${pct(rate60)}%"></div></div><div class="burn-val">${rate60}</div></div>
2791
- </div>`;
2792
- }
2793
-
2794
- // ── feature 17: session swimlane ──────────────────────────
2795
- function buildSwimlane() {
2796
- const el = document.getElementById('m-swimlane');
2797
- if (!el) return;
2798
- const recent = allSessions.slice(0, 8);
2799
- if (!recent.length) { el.innerHTML = '<div class="m-group-title">Session Lanes</div><div class="loading-txt" style="padding:4px 0">—</div>'; return; }
2800
- const now = Date.now();
2801
- const DAY = 86400000;
2802
- const windowMs = DAY; // 24-hour window
2803
- const windowStart = now - windowMs;
2804
- // hues spread across sessions for visual distinction
2805
- const LANE_HUES = [75, 200, 300, 150, 25, 220, 340, 120];
2806
- const rows = recent.map((s, si) => {
2807
- const start = s.firstTs || s.startTs || s.mtime || now;
2808
- const startMs = typeof start === 'number' ? start : new Date(start).getTime();
2809
- const dur = s.totalDurationMs || 60000;
2810
- const endMs = startMs + dur;
2811
- const leftPct = Math.max(0, Math.min(100, ((startMs - windowStart) / windowMs) * 100));
2812
- const rightPct = Math.max(0, Math.min(100, ((now - endMs) / windowMs) * 100));
2813
- const widthPct = Math.max(2, 100 - leftPct - rightPct);
2814
- const hue = LANE_HUES[si % LANE_HUES.length];
2815
- const color = `oklch(65% 0.14 ${hue})`;
2816
- const name = (s.lastPrompt || s.id).slice(0, 16);
2817
- return `<div class="sw-row">
2818
- <div class="sw-lbl" onclick="jumpToSession('${esc(s.id)}')" title="${esc(s.lastPrompt || s.id)}">${esc(name)}</div>
2819
- <div class="sw-track">
2820
- <div class="sw-bar" style="left:${leftPct}%;width:${widthPct}%;background:${color}" title="${esc(name)} · ${fmtDur(dur)}"></div>
2821
- </div>
2822
- </div>`;
2823
- }).join('');
2824
- // dead time: find largest gap between consecutive sessions
2825
- const sorted = recent.slice().sort((a, b) => {
2826
- const aTs = typeof (a.firstTs || a.mtime) === 'number' ? (a.firstTs || a.mtime) : new Date(a.firstTs || a.mtime).getTime();
2827
- const bTs = typeof (b.firstTs || b.mtime) === 'number' ? (b.firstTs || b.mtime) : new Date(b.firstTs || b.mtime).getTime();
2828
- return aTs - bTs;
2829
- });
2830
- let maxGapMs = 0; let gapStart = 0;
2831
- for (let i = 1; i < sorted.length; i++) {
2832
- const prev = sorted[i - 1];
2833
- const curr = sorted[i];
2834
- const prevTs = typeof (prev.firstTs || prev.mtime) === 'number' ? (prev.firstTs || prev.mtime) : new Date(prev.firstTs || prev.mtime).getTime();
2835
- const prevEnd = prevTs + (prev.totalDurationMs || 60000);
2836
- const currTs = typeof (curr.firstTs || curr.mtime) === 'number' ? (curr.firstTs || curr.mtime) : new Date(curr.firstTs || curr.mtime).getTime();
2837
- const gap = currTs - prevEnd;
2838
- if (gap > maxGapMs) { maxGapMs = gap; gapStart = prevEnd; }
2839
- }
2840
- const MIN15 = 15 * 60000;
2841
- let gapNote = '';
2842
- if (maxGapMs > MIN15) {
2843
- const gapMins = Math.round(maxGapMs / 60000);
2844
- const when = new Date(gapStart);
2845
- const hr = when.getHours().toString().padStart(2, '0');
2846
- const mn = when.getMinutes().toString().padStart(2, '0');
2847
- const label = maxGapMs > 3600000 ? `${Math.floor(maxGapMs/3600000)}h idle` : `${gapMins}m idle`;
2848
- gapNote = `<div style="font-size:10px;color:var(--text-xs);margin-top:5px;padding:3px 6px;border-radius:4px;background:oklch(40% 0.04 55 / 0.15)">Longest gap: ${label} starting ${hr}:${mn}</div>`;
2849
- }
2850
- el.innerHTML = `<div class="m-group-title">Session Lanes <span style="font-size:10px;color:var(--text-xs);font-weight:400">24h</span></div><div id="swimlane-wrap">${rows}</div>${gapNote}`;
2851
- }
2852
-
2853
- // ── feature 18: loop run history ──────────────────────────
2854
- function buildLoopSparkline(l) {
2855
- // Synthesize a run history from currentRep if actual history unavailable
2856
- const runHistory = l.runHistory || [];
2857
- if (!runHistory.length && l.currentRep) {
2858
- for (let i = 0; i < Math.min(l.currentRep, 10); i++) {
2859
- runHistory.push({ ok: true });
2860
- }
2861
- }
2862
- if (!runHistory.length) return '';
2863
- const bars = runHistory.slice(-10).map(r => {
2864
- const h = r.durationMs ? Math.max(4, Math.min(20, Math.round(r.durationMs / 2000))) : 10;
2865
- return `<div class="lsp-bar${r.error || r.ok === false ? ' err' : ''}" style="height:${h}px" title="${r.error ? 'error' : 'ok'}${r.durationMs ? ' · ' + fmtDur(r.durationMs) : ''}"></div>`;
2866
- }).join('');
2867
- return `<div class="le-spark"><span style="font-size:10px;color:var(--text-xs)">last ${runHistory.slice(-10).length} runs</span><div class="loop-sparkline">${bars}</div></div>`;
2868
- }
2869
-
2870
- // ── loops ──────────────────────────────────────────────────
2871
- async function renderLoops() {
2872
- const el = document.getElementById('loops-content');
2873
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
2874
- try {
2875
- const data = await apiFetch('/api/loops?dir=' + enc(DIR));
2876
- const loops = Array.isArray(data) ? data : (data.loops || []);
2877
- document.getElementById('bdg-loops').textContent = loops.length || '—';
2878
- if (!loops.length) {
2879
- el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No loops scheduled</div></div>';
2880
- return;
2881
- }
2882
- el.innerHTML = loops.map((l, idx) => {
2883
- const running = l.status !== 'stopped' && l.status !== 'paused';
2884
- const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
2885
- const interval = l.interval || l.schedule || '';
2886
- const fullPrompt = l.prompt || l.command || '';
2887
- const startedAt = l.startedAt ? new Date(l.startedAt).toLocaleString() : '—';
2888
- const lastRun = l.lastRunAt ? relTime(l.lastRunAt) : (l.startedAt ? relTime(l.startedAt) : '—');
2889
- const runs = l.currentRep != null ? l.currentRep : '—';
2890
- return `<div class="loop-row" onclick="toggleLoop(this)">
2891
- <div class="loop-ico">↺</div>
2892
- <div class="loop-body">
2893
- <div class="loop-name">${esc(name)}</div>
2894
- <div class="loop-meta">${esc([interval, l.description].filter(Boolean).join(' · ').slice(0, 80))}</div>
2895
- </div>
2896
- <div class="loop-status ${running ? 'active' : 'stopped'}">${running ? 'active' : 'stopped'}</div>
2897
- </div>
2898
- <div class="loop-expand">
2899
- ${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
2900
- <div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${esc(interval || '—')}</div></div>
2901
- <div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${running ? '● running' : '○ stopped'}</div></div>
2902
- <div class="le-row"><div class="le-lbl">Started</div><div class="le-val mono">${esc(startedAt)}</div></div>
2903
- <div class="le-row"><div class="le-lbl">Last run</div><div class="le-val">${esc(lastRun)}</div></div>
2904
- <div class="le-row"><div class="le-lbl">Run count</div><div class="le-val">${esc(String(runs))}</div></div>
2905
- ${buildLoopSparkline(l)}
2906
- </div>`;
2907
- }).join('');
2908
- } catch (err) {
2909
- el.innerHTML = '<div class="empty">Could not load loops: ' + esc(err.message) + '</div>';
2910
- }
2911
- }
2912
-
2913
- function toggleLoop(row) {
2914
- row.classList.toggle('open');
2915
- }
2916
-
2917
- function showLoopForm() {
2918
- document.getElementById('loop-create-form').style.display = 'block';
2919
- document.getElementById('btn-new-loop').style.display = 'none';
2920
- document.getElementById('lcf-prompt').focus();
2921
- }
2922
-
2923
- function hideLoopForm() {
2924
- document.getElementById('loop-create-form').style.display = 'none';
2925
- document.getElementById('btn-new-loop').style.display = '';
2926
- }
2927
-
2928
- async function createLoop() {
2929
- const prompt = document.getElementById('lcf-prompt').value.trim();
2930
- if (!prompt) { showToast('Required', 'Prompt is required', 'warn'); return; }
2931
- const name = document.getElementById('lcf-name').value.trim();
2932
- const interval = document.getElementById('lcf-interval').value.trim() || '1h';
2933
- const maxRepsVal = document.getElementById('lcf-maxreps').value;
2934
- const maxReps = maxRepsVal ? parseInt(maxRepsVal) : null;
2935
- try {
2936
- const r = await fetch('/api/loops/create?dir=' + enc(DIR), {
2937
- method: 'POST', headers: { 'Content-Type': 'application/json' },
2938
- body: JSON.stringify({ name, prompt, interval, maxReps }),
2939
- });
2940
- const d = await r.json();
2941
- if (!d.ok) { showToast('Error', d.error || 'Failed to create loop', 'err'); return; }
2942
- showToast('Created', `Loop created: ${name || prompt.slice(0, 30)}`, 'ok');
2943
- hideLoopForm();
2944
- document.getElementById('lcf-prompt').value = '';
2945
- document.getElementById('lcf-name').value = '';
2946
- document.getElementById('lcf-interval').value = '1h';
2947
- document.getElementById('lcf-maxreps').value = '';
2948
- viewRendered['loops'] = false;
2949
- renderLoops();
2950
- } catch (err) { showToast('Error', err.message, 'err'); }
2951
- }
2952
-
2953
- // ── feature 19: cache efficiency panel ────────────────────
2954
- function buildEfficiencyPanel() {
2955
- const el = document.getElementById('m-efficiency');
2956
- if (!el) return;
2957
- const sessions = allSessions.filter(s => s.totalInputTokens > 0);
2958
- if (!sessions.length) {
2959
- el.innerHTML = `<div class="m-group-title">Cache Efficiency</div><div class="loading-txt" style="padding:4px 0">No token data yet</div>`;
2960
- return;
2961
- }
2962
- const totalCR = sessions.reduce((a,s) => a + (s.cacheReadTokens||0), 0);
2963
- const totalIn = sessions.reduce((a,s) => a + (s.totalInputTokens||0), 0);
2964
- const avgPct = totalIn > 0 ? Math.round(totalCR/totalIn*100) : 0;
2965
- const avgCls = avgPct >= 60 ? 'eff-good' : avgPct >= 20 ? 'eff-warn' : 'eff-bad';
2966
- const rows = sessions.slice(0, 5).map(s => {
2967
- const pct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens||0)/s.totalInputTokens*100) : 0;
2968
- const cls = pct >= 60 ? 'eff-good' : pct >= 20 ? 'eff-warn' : 'eff-bad';
2969
- const fillColor = pct >= 60 ? 'oklch(65% 0.15 150)' : pct >= 20 ? 'oklch(70% 0.18 80)' : 'oklch(65% 0.2 25)';
2970
- const lbl = (s.lastPrompt || s.id).slice(0, 22);
2971
- return `<div class="eff-row">
2972
- <span class="eff-lbl" title="${esc(s.lastPrompt||s.id)}">${esc(lbl)}</span>
2973
- <span class="eff-pct ${cls}">${pct}%</span>
2974
- </div>
2975
- <div class="eff-bar-wrap"><div class="eff-bar-fill" style="width:${pct}%;background:${fillColor}"></div></div>`;
2976
- }).join('');
2977
- el.innerHTML = `<div class="m-group-title">Cache Efficiency <span class="${avgCls}" style="font-size:10px;font-weight:400">${avgPct}% avg</span></div>${rows}`;
2978
- }
2979
-
2980
- // ── feature 20: model mix ──────────────────────────────────
2981
- let modelMixOpen = false;
2982
- function toggleModelMix() {
2983
- modelMixOpen = !modelMixOpen;
2984
- const p = document.getElementById('model-mix-panel');
2985
- p.style.display = modelMixOpen ? 'block' : 'none';
2986
- document.getElementById('btn-model-mix').classList.toggle('active', modelMixOpen);
2987
- if (modelMixOpen) renderModelMix();
2988
- }
2989
- function renderModelMix() {
2990
- const body = document.getElementById('model-mix-body');
2991
- const breakdown = {};
2992
- for (const s of allSessions) {
2993
- for (const [model, d] of Object.entries(s.modelBreakdown || {})) {
2994
- if (!breakdown[model]) breakdown[model] = { calls: 0, cost: 0 };
2995
- breakdown[model].calls += d.calls || 0;
2996
- breakdown[model].cost += d.cost || 0;
2997
- }
2998
- }
2999
- const entries = Object.entries(breakdown).sort((a,b) => b[1].cost - a[1].cost);
3000
- if (!entries.length) {
3001
- body.innerHTML = '<div style="color:var(--text-lo);font-size:11px;padding:8px 0">No model data</div>';
3002
- return;
3003
- }
3004
- const totalCost = entries.reduce((a,[,d]) => a + d.cost, 0);
3005
- body.innerHTML = `<table class="lb-table"><thead><tr>
3006
- <th>Model</th><th class="lb-cost">Cost</th><th class="lb-dur">%</th><th class="lb-dur">Calls</th>
3007
- </tr></thead><tbody>
3008
- ${entries.map(([model, d]) => {
3009
- const short = model.replace(/^claude-/,'').replace(/-\d{8}$/,'');
3010
- const pct = totalCost > 0 ? Math.round(d.cost/totalCost*100) : 0;
3011
- return `<tr>
3012
- <td style="font-size:11px">${esc(short)}</td>
3013
- <td class="lb-cost">$${d.cost.toFixed(2)}</td>
3014
- <td class="lb-dur">${pct}%</td>
3015
- <td class="lb-dur">${d.calls}</td>
3016
- </tr>`;
3017
- }).join('')}
3018
- </tbody></table>`;
3019
- }
3020
-
3021
- // ── feature 21: weekly recap ───────────────────────────────
3022
- const WEEKLY_DISMISSED_KEY = 'mm-weekly-dismissed';
3023
- function getWeekKey() {
3024
- const d = new Date();
3025
- const sun = new Date(d); sun.setDate(d.getDate() - d.getDay()); sun.setHours(0,0,0,0);
3026
- return sun.toISOString().slice(0,10);
3027
- }
3028
- function buildWeeklyRecap() {
3029
- if (localStorage.getItem(WEEKLY_DISMISSED_KEY) === getWeekKey()) return;
3030
- if (!allSessions.length) return;
3031
- const weekStart = new Date(); weekStart.setDate(weekStart.getDate() - weekStart.getDay()); weekStart.setHours(0,0,0,0);
3032
- const weekSess = allSessions.filter(s => {
3033
- const t = s.lastTs || s.mtime;
3034
- return t && new Date(typeof t === 'number' ? t : t).getTime() >= weekStart.getTime();
3035
- });
3036
- if (!weekSess.length) return;
3037
- const totalCost = weekSess.reduce((a,s) => a + (s.totalCost||0), 0);
3038
- const totalTools = weekSess.reduce((a,s) => a + (s.toolCalls||0), 0);
3039
- const days = new Set(weekSess.map(s => {
3040
- const t = s.lastTs || s.mtime;
3041
- return new Date(typeof t === 'number' ? t : t).toDateString();
3042
- })).size;
3043
- const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
3044
- const streak = calcStreak();
3045
- const stats = [
3046
- `${weekSess.length} session${weekSess.length!==1?'s':''}`,
3047
- `${days} day${days!==1?'s':''}`,
3048
- totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
3049
- totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
3050
- longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
3051
- ].filter(Boolean);
3052
- const streakHtml = streak >= 2 ? `<span class="streak-chip" title="${streak} consecutive days with sessions">🔥 ${streak}d streak</span>` : '';
3053
- document.getElementById('weekly-stats').innerHTML = stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + streakHtml;
3054
- document.getElementById('weekly-card').classList.add('show');
3055
- }
3056
- function dismissWeekly() {
3057
- localStorage.setItem(WEEKLY_DISMISSED_KEY, getWeekKey());
3058
- document.getElementById('weekly-card').classList.remove('show');
3059
- }
3060
-
3061
- // ── feature 23: tool error rate ────────────────────────────
3062
- let toolErrorsOpen = false;
3063
- function toggleToolErrors() {
3064
- toolErrorsOpen = !toolErrorsOpen;
3065
- const p = document.getElementById('tool-errors-panel');
3066
- p.style.display = toolErrorsOpen ? 'block' : 'none';
3067
- document.getElementById('btn-tool-errors').classList.toggle('active', toolErrorsOpen);
3068
- if (toolErrorsOpen) loadToolErrors();
3069
- }
3070
- async function loadToolErrors() {
3071
- const body = document.getElementById('tool-errors-body');
3072
- body.innerHTML = '<div class="loading-txt">Loading…</div>';
3073
- try {
3074
- const data = await apiFetch('/api/tool-errors?dir=' + enc(DIR));
3075
- const errors = data.errors || [];
3076
- if (!errors.length) {
3077
- body.innerHTML = '<div style="color:var(--text-lo);font-size:11px;padding:8px 0">No tool errors found in recent sessions</div>';
3078
- return;
3079
- }
3080
- body.innerHTML = `<table class="lb-table"><thead><tr>
3081
- <th>Tool</th><th class="lb-cost">Errors</th><th class="lb-dur">Rate</th>
3082
- </tr></thead><tbody>
3083
- ${errors.map(e => {
3084
- const rate = e.total > 0 ? Math.round(e.count/e.total*100)+'%' : '—';
3085
- return `<tr>
3086
- <td style="font-size:11px">${esc(e.tool)}</td>
3087
- <td class="lb-cost" style="color:oklch(65% 0.2 25)">${e.count}</td>
3088
- <td class="lb-dur">${rate}</td>
3089
- </tr>`;
3090
- }).join('')}
3091
- </tbody></table>`;
3092
- } catch (err) {
3093
- body.innerHTML = `<div style="color:var(--text-lo);font-size:11px">Error: ${esc(err.message)}</div>`;
3094
- }
3095
- }
3096
-
3097
- // ── feature 24: activity heatmap ──────────────────────────
3098
- function buildActivityHeatmap() {
3099
- const el = document.getElementById('m-heatmap');
3100
- if (!el) return;
3101
- if (!allSessions.length) {
3102
- el.innerHTML = `<div class="m-group-title">Activity Heatmap</div><div class="loading-txt" style="padding:4px 0">No sessions</div>`;
3103
- return;
3104
- }
3105
- const grid = Array.from({length:7}, () => new Array(24).fill(0));
3106
- for (const s of allSessions) {
3107
- const t = s.firstTs || s.mtime;
3108
- if (!t) continue;
3109
- const d = new Date(typeof t === 'number' ? t : t);
3110
- grid[d.getDay()][d.getHours()]++;
3111
- }
3112
- const maxVal = Math.max(1, ...grid.flat());
3113
- const DAYS = ['Su','Mo','Tu','We','Th','Fr','Sa'];
3114
- let html = '<div class="heatmap-grid">';
3115
- html += '<div class="heatmap-row"><div class="heatmap-lbl"></div>';
3116
- for (let h = 0; h < 24; h++) {
3117
- html += `<div class="heatmap-hdr-cell">${h % 6 === 0 ? h : ''}</div>`;
3118
- }
3119
- html += '</div>';
3120
- for (let d = 0; d < 7; d++) {
3121
- html += `<div class="heatmap-row"><div class="heatmap-lbl">${DAYS[d]}</div>`;
3122
- for (let h = 0; h < 24; h++) {
3123
- const v = grid[d][h];
3124
- const alpha = v > 0 ? Math.max(0.18, v/maxVal).toFixed(2) : 0;
3125
- const bg = v > 0 ? `oklch(65% 0.18 200 / ${alpha})` : 'transparent';
3126
- html += `<div class="heatmap-cell" style="background:${bg};border:1px solid ${v>0?'transparent':'var(--border)'}" title="${DAYS[d]} ${h}:00 — ${v} session${v!==1?'s':''}"></div>`;
3127
- }
3128
- html += '</div>';
3129
- }
3130
- html += '</div>';
3131
- el.innerHTML = `<div class="m-group-title">Activity Heatmap <span style="font-size:10px;color:var(--text-xs);font-weight:400">by day/hr</span></div>${html}`;
3132
- }
3133
-
3134
- // ── memory ─────────────────────────────────────────────────
3135
- async function renderMemory() {
3136
- activeMemNs = 'All'; // reset on every render so stale filter doesn't persist
3137
- const el = document.getElementById('mem-content');
3138
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
3139
- try {
3140
- const data = await apiFetch('/api/palace?dir=' + enc(DIR));
3141
- allDrawers = data.drawers || [];
3142
- const identity = data.identity || '';
3143
- let html = '';
3144
- if (identity) {
3145
- html += `<div class="mem-section">
3146
- <div class="mem-title">Identity</div>
3147
- <div class="drawer-item"><div class="dr-val" style="white-space:pre-wrap">${esc(identity.slice(0, 1200))}</div></div>
3148
- </div>`;
3149
- }
3150
- if (allDrawers.length) {
3151
- // build namespace tabs
3152
- const namespaces = ['All', ...new Set(allDrawers.map(d => d.namespace || 'default').filter(Boolean))];
3153
- const tabsEl = document.getElementById('mem-ns-tabs');
3154
- if (tabsEl) {
3155
- tabsEl.innerHTML = namespaces.map((ns, i) =>
3156
- `<button class="ns-tab${i === 0 ? ' active' : ''}" data-ns="${esc(ns)}" onclick="filterMemoryNs('${esc(ns)}')">${esc(ns)}</button>`
3157
- ).join('');
3158
- }
3159
-
3160
- const tsValues = allDrawers.map(d => d.timestamp ? new Date(d.timestamp).getTime() : 0).filter(Boolean);
3161
- const oldestTs = tsValues.length ? Math.min(...tsValues) : 0;
3162
- const newestTs = tsValues.length ? Math.max(...tsValues) : 0;
3163
- const tsRange = newestTs - oldestTs || 1;
3164
- const items = allDrawers.map((d, i) => {
3165
- const ts = d.timestamp ? new Date(d.timestamp).getTime() : 0;
3166
- const agePct = ts && oldestTs ? Math.round(((ts - oldestTs) / tsRange) * 100) : 0;
3167
- const ageBar = ts ? `<div class="dr-age-bar"><div class="dr-age-bar-fill" style="width:${agePct}%"></div></div>` : '';
3168
- return `<div class="drawer-item" data-idx="${i}" data-ns="${esc(d.namespace || 'default')}">
3169
- <div class="dr-key">${esc(d.key || d.namespace || '—')}</div>
3170
- <div class="dr-val">${esc(String(d.value || d.text || '').slice(0, 300))}</div>
3171
- ${d.timestamp ? `<div class="dr-ts">${relTime(d.timestamp)}</div>` : ''}
3172
- ${ageBar}
3173
- </div>`;
3174
- }).join('');
3175
- html += `<div class="mem-section" id="drawers-section">
3176
- <div class="mem-title">Drawers (${allDrawers.length})</div>
3177
- <div id="drawers-list">${items}</div>
3178
- </div>`;
3179
- }
3180
- if (!html) html = '<div class="empty"><div class="empty-ico">◈</div><div>Memory palace is empty</div></div>';
3181
- el.innerHTML = html;
3182
- } catch (err) {
3183
- el.innerHTML = '<div class="empty">Could not load memory: ' + esc(err.message) + '</div>';
3184
- }
3185
- }
3186
-
3187
- let activeMemNs = 'All';
3188
-
3189
- function filterMemory(q) {
3190
- const items = document.querySelectorAll('#drawers-list .drawer-item');
3191
- const lq = q.toLowerCase();
3192
- items.forEach(item => {
3193
- const key = (item.querySelector('.dr-key')?.textContent || '').toLowerCase();
3194
- const val = (item.querySelector('.dr-val')?.textContent || '').toLowerCase();
3195
- const nsMatch = activeMemNs === 'All' || (item.dataset.ns === activeMemNs);
3196
- item.classList.toggle('hidden', !nsMatch || (!!lq && !key.includes(lq) && !val.includes(lq)));
3197
- });
3198
- }
3199
-
3200
- function filterMemoryNs(ns) {
3201
- activeMemNs = ns;
3202
- document.querySelectorAll('.ns-tab').forEach(t => t.classList.toggle('active', t.dataset.ns === ns));
3203
- filterMemory(document.getElementById('mem-filter')?.value || '');
3204
- }
3205
-
3206
- // ── orgs ───────────────────────────────────────────────────
3207
- async function renderOrgs() {
3208
- const el = document.getElementById('orgs-content');
3209
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
3210
- try {
3211
- const data = await apiFetch('/api/orgs');
3212
- const orgs = Array.isArray(data) ? data : (data.orgs || []);
3213
- if (!orgs.length) {
3214
- el.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No MASTERMIND orgs found</div></div>';
3215
- return;
3216
- }
3217
- el.innerHTML = '<div class="org-list">' + orgs.map(o =>
3218
- `<div class="org-row">
3219
- <div class="org-name">${esc(o.name || o.id || '—')}</div>
3220
- <div class="org-meta">${esc(o.description || (o.agents != null ? o.agents + ' agents' : ''))}</div>
3221
- </div>`).join('') + '</div>';
3222
- } catch (err) {
3223
- el.innerHTML = '<div class="empty">Could not load orgs: ' + esc(err.message) + '</div>';
3224
- }
3225
- }
3226
-
3227
- // ── density toggle ─────────────────────────────────────────
3228
- let compactMode = false;
3229
- function toggleDensity() {
3230
- compactMode = !compactMode;
3231
- const pane = document.getElementById('feed-pane');
3232
- const btn = document.getElementById('btn-density');
3233
- pane.classList.toggle('compact', compactMode);
3234
- btn.classList.toggle('compact-on', compactMode);
3235
- btn.title = compactMode ? 'Switch to comfortable view' : 'Toggle compact view';
3236
- }
3237
-
3238
- // ── session timeline ────────────────────────────────────────
3239
- const TL_COLORS = {
3240
- file: 'oklch(65% 0.15 150)',
3241
- bash: 'oklch(65% 0.12 240)',
3242
- agent: 'oklch(65% 0.13 290)',
3243
- mcp: 'oklch(65% 0.12 195)',
3244
- search: 'oklch(65% 0.14 35)',
3245
- skill: 'oklch(72% 0.18 75)',
3246
- task: 'oklch(62% 0.12 55)',
3247
- mem: 'oklch(62% 0.11 160)',
3248
- user: 'oklch(55% 0.08 75 / 0.5)',
3249
- other: 'oklch(32% 0.005 75)',
3250
- };
3251
-
3252
- function buildTimeline(events) {
3253
- const tl = document.getElementById('feed-timeline');
3254
- if (!tl) return;
3255
- // Only tool + user events with timestamps
3256
- const stamped = events.filter(ev => ev.ts && (ev.kind === 'tool' || ev.kind === 'user'));
3257
- if (stamped.length < 2) { tl.innerHTML = ''; return; }
3258
- const times = stamped.map(ev => new Date(ev.ts).getTime());
3259
- const tMin = Math.min(...times), tMax = Math.max(...times);
3260
- const span = tMax - tMin || 1;
3261
- const segs = stamped.map(ev => {
3262
- const pct = ((new Date(ev.ts).getTime() - tMin) / span * 100).toFixed(2);
3263
- const cat = ev.kind === 'user' ? 'user' : (ev.cat || 'other');
3264
- const color = TL_COLORS[cat] || TL_COLORS.other;
3265
- const label = ev.kind === 'user' ? 'user message' : (ev.label || ev.name || cat);
3266
- return `<div class="tl-seg" style="flex:${pct > 0 ? pct : 0.5};background:${color}" title="${esc(label)}"></div>`;
3267
- });
3268
- tl.innerHTML = segs.join('');
3269
- }
3270
-
3271
- // ── error jump ──────────────────────────────────────────────
3272
- function jumpToErrors() {
3273
- // find first errored feed entry and scroll to it
3274
- const first = document.querySelector('#feed-content .feed-entry.errored');
3275
- if (!first) return;
3276
- first.scrollIntoView({ behavior: 'smooth', block: 'center' });
3277
- first.style.outline = '1px solid var(--red)';
3278
- setTimeout(() => { first.style.outline = ''; }, 1800);
3279
- // highlight all errored entries briefly
3280
- document.querySelectorAll('#feed-content .feed-entry.errored').forEach(el => {
3281
- el.style.background = 'oklch(60% 0.18 25 / 0.08)';
3282
- setTimeout(() => { el.style.background = ''; }, 1800);
3283
- });
3284
- }
3285
-
3286
- // ── week-over-week delta ────────────────────────────────────
3287
- function buildWowDelta() {
3288
- // Compare this week (last 7 days) vs prior week (8–14 days ago)
3289
- const DAY = 86400000;
3290
- const now = Date.now();
3291
- let thisWeek = 0, lastWeek = 0;
3292
- for (const s of allSessions) {
3293
- const ts = s.lastTs || s.mtime;
3294
- if (!ts) continue;
3295
- const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
3296
- if (age < 7 * DAY) thisWeek++;
3297
- else if (age < 14 * DAY) lastWeek++;
3298
- }
3299
- if (!lastWeek) return '';
3300
- const delta = Math.round((thisWeek - lastWeek) / lastWeek * 100);
3301
- if (delta > 0) return `<span class="wow-delta wow-up" title="vs prior 7 days">↑${delta}%</span>`;
3302
- if (delta < 0) return `<span class="wow-delta wow-down" title="vs prior 7 days">↓${Math.abs(delta)}%</span>`;
3303
- return `<span class="wow-delta wow-flat" title="vs prior 7 days">→ flat</span>`;
3304
- }
3305
-
3306
- // ── feed search ────────────────────────────────────────────
3307
- let feedSearchActive = false;
3308
-
3309
- function toggleFeedSearch() {
3310
- const bar = document.getElementById('feed-search');
3311
- if (feedSearchActive) { closeFeedSearch(); return; }
3312
- feedSearchActive = true;
3313
- bar.classList.add('open');
3314
- requestAnimationFrame(() => document.getElementById('feed-search-input').focus());
3315
- }
3316
-
3317
- function closeFeedSearch() {
3318
- feedSearchActive = false;
3319
- document.getElementById('feed-search').classList.remove('open');
3320
- document.getElementById('feed-search-input').value = '';
3321
- filterFeed('');
3322
- }
3323
-
3324
- function filterFeed(q) {
3325
- const lq = q.toLowerCase().trim();
3326
- let visible = 0;
3327
-
3328
- // individual entries
3329
- document.querySelectorAll('#feed-content .feed-entry').forEach(el => {
3330
- const text = (el.querySelector('.feed-lbl')?.textContent || '') + ' ' +
3331
- (el.querySelector('.feed-detail')?.textContent || '');
3332
- const show = !lq || text.toLowerCase().includes(lq);
3333
- el.style.display = show ? '' : 'none';
3334
- if (show) visible++;
3335
- });
3336
-
3337
- // collapsed group rows
3338
- document.querySelectorAll('#feed-content .feed-group').forEach(el => {
3339
- const text = el.querySelector('.fg-label')?.textContent || '';
3340
- const show = !lq || text.toLowerCase().includes(lq);
3341
- el.style.display = show ? '' : 'none';
3342
- if (show) visible++;
3343
- });
3344
-
3345
- const countEl = document.getElementById('feed-search-count');
3346
- if (countEl) countEl.textContent = lq ? `${visible} match${visible !== 1 ? 'es' : ''}` : '';
3347
- }
3348
-
3349
- // ── copy session as markdown ───────────────────────────────
3350
- async function copySession() {
3351
- const btn = document.getElementById('btn-copy-sess');
3352
- const sess = allSessions[sessionIdx];
3353
- if (!sess?.file) return;
3354
- try {
3355
- const data = await apiFetch('/api/session?dir=' + enc(DIR) + '&file=' + enc(sess.file) + '&limit=300');
3356
- const events = data.events || [];
3357
- const lines = [`# Session: ${sess.lastPrompt || sess.id}`, `> ${new Date(sess.lastTs || sess.mtime).toLocaleString()}`, ''];
3358
- for (const ev of events) {
3359
- if (ev.kind === 'user' && ev.text?.trim()) {
3360
- lines.push('**User:** ' + ev.text.trim().replace(/\n/g, ' '));
3361
- } else if (ev.kind === 'tool') {
3362
- lines.push(`- \`${ev.name || ev.cat}\`: ${ev.label || ''}`);
3363
- }
3364
- }
3365
- await navigator.clipboard.writeText(lines.join('\n'));
3366
- btn.textContent = '✓ Copied';
3367
- btn.classList.add('copied');
3368
- setTimeout(() => { btn.textContent = '⎘ Copy'; btn.classList.remove('copied'); }, 2000);
3369
- } catch (err) {
3370
- btn.textContent = '✕ Error';
3371
- setTimeout(() => { btn.textContent = '⎘ Copy'; }, 1500);
3372
- }
3373
- }
3374
-
3375
- // ── feed time filter ───────────────────────────────────────
3376
- function setFeedTimeFilter(f) {
3377
- feedTimeFilter = f;
3378
- document.querySelectorAll('.tf-btn').forEach(b => b.classList.toggle('active', b.dataset.tf === f));
3379
- if (!allSessions.length) return;
3380
- const sess = allSessions[sessionIdx] || allSessions[0];
3381
- if (sess) loadFeedForSession(sess);
3382
- }
3383
-
3384
- // ── command palette ────────────────────────────────────────
3385
- function openCmdPalette() {
3386
- document.getElementById('cmd-backdrop').classList.add('open');
3387
- document.getElementById('cmd-palette').classList.add('open');
3388
- const inp = document.getElementById('cmd-input');
3389
- inp.value = '';
3390
- cmdSearch('');
3391
- requestAnimationFrame(() => inp.focus());
3392
- }
3393
-
3394
- function closeCmdPalette() {
3395
- document.getElementById('cmd-backdrop').classList.remove('open');
3396
- document.getElementById('cmd-palette').classList.remove('open');
3397
- }
3398
-
3399
- function cmdSearch(q) {
3400
- cmdItems = [];
3401
- const lq = q.toLowerCase().trim();
3402
- const results = document.getElementById('cmd-results');
3403
-
3404
- // ">" prefix = cross-session full-text search
3405
- if (q.startsWith('>')) {
3406
- const sq = q.slice(1).trim();
3407
- results.innerHTML = sq.length >= 2
3408
- ? '<div class="cmd-empty">Searching sessions…</div>'
3409
- : '<div class="cmd-empty">Type at least 2 chars after &gt; to search all sessions</div>';
3410
- if (sq.length >= 2) searchSessions(sq);
3411
- return;
3412
- }
3413
-
3414
- const sessMatches = allSessions
3415
- .filter(s => !lq || (s.lastPrompt || s.id).toLowerCase().includes(lq))
3416
- .slice(0, 5);
3417
-
3418
- const memMatches = allDrawers
3419
- .filter(d => !lq || (d.key || '').toLowerCase().includes(lq) ||
3420
- String(d.value || d.text || '').toLowerCase().includes(lq))
3421
- .slice(0, 3);
3422
-
3423
- const projMatches = allProjects
3424
- .filter(p => !lq || (p.name || p.slug || '').toLowerCase().includes(lq) ||
3425
- (p.path || '').toLowerCase().includes(lq))
3426
- .slice(0, 3);
3427
-
3428
- if (!sessMatches.length && !memMatches.length && !projMatches.length) {
3429
- results.innerHTML = '<div class="cmd-empty">No results</div>';
3430
- cmdItems = [];
3431
- return;
3432
- }
3433
-
3434
- let html = '';
3435
-
3436
- if (sessMatches.length) {
3437
- html += '<div class="cmd-group-lbl">Sessions</div>';
3438
- sessMatches.forEach(s => {
3439
- const idx = cmdItems.length;
3440
- cmdItems.push({ type: 'session', data: s });
3441
- html += `<div class="cmd-item" data-ci="${idx}">
3442
- <span class="ci-ico">◫</span>
3443
- <div class="cmd-item-body">
3444
- <div class="ci-title">${esc(s.lastPrompt || s.id.slice(0, 32))}</div>
3445
- <div class="ci-sub">${relTime(s.lastTs || s.mtime)}</div>
3446
- </div>
3447
- </div>`;
3448
- });
3449
- }
3450
-
3451
- if (memMatches.length) {
3452
- html += '<div class="cmd-group-lbl">Memory</div>';
3453
- memMatches.forEach(d => {
3454
- const idx = cmdItems.length;
3455
- cmdItems.push({ type: 'memory', data: d });
3456
- html += `<div class="cmd-item" data-ci="${idx}">
3457
- <span class="ci-ico">◈</span>
3458
- <div class="cmd-item-body">
3459
- <div class="ci-title">${esc(d.key || d.namespace || '—')}</div>
3460
- <div class="ci-sub">${esc(String(d.value || d.text || '').slice(0, 60))}</div>
3461
- </div>
3462
- </div>`;
3463
- });
3464
- }
3465
-
3466
- if (projMatches.length) {
3467
- html += '<div class="cmd-group-lbl">Projects</div>';
3468
- projMatches.forEach(p => {
3469
- const idx = cmdItems.length;
3470
- cmdItems.push({ type: 'project', data: p });
3471
- html += `<div class="cmd-item" data-ci="${idx}">
3472
- <span class="ci-ico">⊞</span>
3473
- <div class="cmd-item-body">
3474
- <div class="ci-title">${esc(p.name || p.slug)}</div>
3475
- <div class="ci-sub">${esc(p.path || '')}</div>
3476
- </div>
3477
- </div>`;
3478
- });
3479
- }
3480
-
3481
- results.innerHTML = html;
3482
- cmdFocusIdx = 0;
3483
- updateCmdFocus();
3484
-
3485
- results.querySelectorAll('.cmd-item').forEach(el => {
3486
- el.addEventListener('click', () => {
3487
- cmdFocusIdx = parseInt(el.dataset.ci);
3488
- executeCmdItem();
3489
- });
3490
- });
3491
- }
3492
-
3493
- function updateCmdFocus() {
3494
- const items = document.querySelectorAll('#cmd-results .cmd-item');
3495
- items.forEach((el, i) => el.classList.toggle('focused', i === cmdFocusIdx));
3496
- }
3497
-
3498
- function cmdKey(e) {
3499
- const items = document.querySelectorAll('#cmd-results .cmd-item');
3500
- if (e.key === 'ArrowDown') { e.preventDefault(); cmdFocusIdx = Math.min(cmdFocusIdx + 1, items.length - 1); updateCmdFocus(); }
3501
- else if (e.key === 'ArrowUp') { e.preventDefault(); cmdFocusIdx = Math.max(cmdFocusIdx - 1, 0); updateCmdFocus(); }
3502
- else if (e.key === 'Enter') { e.preventDefault(); executeCmdItem(); }
3503
- else if (e.key === 'Escape') { closeCmdPalette(); }
3504
- }
3505
-
3506
- function executeCmdItem() {
3507
- const item = cmdItems[cmdFocusIdx];
3508
- if (!item) return;
3509
- closeCmdPalette();
3510
- if (item.type === 'session') jumpToSession(item.data.id);
3511
- else if (item.type === 'memory') switchView('memory');
3512
- else if (item.type === 'project') switchProject(item.data.path);
3513
- }
3514
-
3515
- // ── cross-session search ───────────────────────────────────
3516
- async function searchSessions(q) {
3517
- const resultsEl = document.getElementById('cmd-results');
3518
- try {
3519
- const data = await apiFetch('/api/search-sessions?dir=' + enc(DIR) + '&q=' + enc(q));
3520
- if (!data.results?.length) {
3521
- resultsEl.innerHTML = '<div class="cmd-empty">No matches found across sessions</div>';
3522
- return;
3523
- }
3524
- cmdItems = [];
3525
- let html = '<div class="cmd-group-lbl">Matches across sessions</div>';
3526
- data.results.forEach(r => {
3527
- const idx = cmdItems.length;
3528
- cmdItems.push({ type: 'session', data: { id: r.id, lastPrompt: r.lastPrompt, lastTs: r.mtime } });
3529
- const snippet = r.matches[0]?.text?.replace(/\s+/g, ' ').trim() || '';
3530
- html += `<div class="cmd-item" data-ci="${idx}">
3531
- <span class="ci-ico">◫</span>
3532
- <div class="cmd-item-body">
3533
- <div class="ci-title">${esc(r.lastPrompt || r.id.slice(0, 32))}</div>
3534
- <div class="ci-sub">${esc(snippet.length > 70 ? snippet.slice(0, 70) + '…' : snippet)}</div>
3535
- </div>
3536
- </div>`;
3537
- });
3538
- resultsEl.innerHTML = html;
3539
- cmdFocusIdx = 0;
3540
- updateCmdFocus();
3541
- resultsEl.querySelectorAll('.cmd-item').forEach(el => {
3542
- el.addEventListener('click', () => { cmdFocusIdx = parseInt(el.dataset.ci); executeCmdItem(); });
3543
- });
3544
- } catch (_) {
3545
- resultsEl.innerHTML = '<div class="cmd-empty">Search error — is the server running?</div>';
3546
- }
3547
- }
3548
-
3549
- // ── keyboard shortcuts ─────────────────────────────────────
3550
- document.addEventListener('keydown', e => {
3551
- // ⌘K / Ctrl+K — command palette
3552
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
3553
- e.preventDefault();
3554
- const open = document.getElementById('cmd-palette').classList.contains('open');
3555
- if (open) closeCmdPalette(); else openCmdPalette();
3556
- return;
3557
- }
3558
-
3559
- // ignore when typing in inputs
3560
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
3561
- if (document.getElementById('cmd-palette').classList.contains('open')) return;
3562
-
3563
- if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
3564
- if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
3565
- if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
3566
-
3567
- if (currentView === 'now') {
3568
- if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
3569
- if (e.key === 'g' || e.key === 'G') { e.preventDefault(); goLive(); }
3570
- if (e.key === 'r' || e.key === 'R') { e.preventDefault(); refreshCurrent(); }
3571
-
3572
- if (e.key === 'j' || e.key === 'k') {
3573
- e.preventDefault();
3574
- const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
3575
- if (!entries.length) return;
3576
- let cur = entries.findIndex(el => el.classList.contains('selected'));
3577
- if (e.key === 'j') cur = cur < 0 ? 0 : Math.min(cur + 1, entries.length - 1);
3578
- else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
3579
- entries.forEach((el, i) => el.classList.toggle('selected', i === cur));
3580
- entries[cur].scrollIntoView({ block: 'nearest' });
3581
- selectedEntryId = entries[cur].dataset.ev
3582
- ? (JSON.parse(entries[cur].dataset.ev).id || '')
3583
- : '';
3584
- }
3585
-
3586
- if (e.key === 'Enter') {
3587
- const sel = document.querySelector('#feed-content .feed-entry.selected');
3588
- if (sel) openDetail(sel.dataset.ev);
3589
- }
3590
- }
3591
-
3592
- // ── f59: J/K navigation in sessions list ─────────────────
3593
- if (currentView === 'sessions') {
3594
- if (e.key === 'j' || e.key === 'k') {
3595
- e.preventDefault();
3596
- const rows = [...document.querySelectorAll('#sess-content .sess-row:not([style*="display: none"])')];
3597
- if (!rows.length) return;
3598
- let cur = rows.findIndex(r => r.classList.contains('kb-focus'));
3599
- if (e.key === 'j') cur = cur < 0 ? 0 : Math.min(cur + 1, rows.length - 1);
3600
- else cur = cur < 0 ? 0 : Math.max(cur - 1, 0);
3601
- rows.forEach((r, i) => r.classList.toggle('kb-focus', i === cur));
3602
- rows[cur].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3603
- }
3604
- if (e.key === 'Enter') {
3605
- const focused = document.querySelector('#sess-content .sess-row.kb-focus');
3606
- if (focused) {
3607
- const sid = focused.dataset.sessId;
3608
- if (sid) jumpToSession(sid);
3609
- }
3610
- }
3611
- }
3612
- });
3613
-
3614
- // ── feature 31: inline session filter ─────────────────────
3615
- // ── feature 61: extended session search ───────────────────
3616
- function filterSessions(q) {
3617
- const rows = document.querySelectorAll('#sess-content .sess-row');
3618
- const lq = q.toLowerCase().trim();
3619
- let visible = 0;
3620
- rows.forEach(row => {
3621
- if (!lq) { row.style.display = ''; visible++; return; }
3622
- const sessId = row.dataset.sessId || '';
3623
- // search prompt, meta, tags (rendered text)
3624
- const prompt = (row.querySelector('.sr-prompt')?.textContent || '').toLowerCase();
3625
- const meta = (row.querySelector('.sr-meta')?.textContent || '').toLowerCase();
3626
- const tags = (row.querySelector('.sr-tags')?.textContent || '').toLowerCase();
3627
- const files = (row.querySelector('.sr-files')?.textContent || '').toLowerCase();
3628
- const ctags = (row.querySelector('.sr-custom-tags')?.textContent || '').toLowerCase();
3629
- // search summaries from session data
3630
- const sess = allSessions.find(s => s.id === sessId);
3631
- const summText = sess ? (sess.summaries || []).map(sm => typeof sm === 'string' ? sm : (sm.summary || sm.text || '')).join(' ').toLowerCase() : '';
3632
- const match = prompt.includes(lq) || meta.includes(lq) || tags.includes(lq) || files.includes(lq) || ctags.includes(lq) || summText.includes(lq);
3633
- row.style.display = match ? '' : 'none';
3634
- if (match) visible++;
3635
- });
3636
- const countEl = document.getElementById('sess-filter-count');
3637
- if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
3638
- }
3639
-
3640
- // ── feature 32: keyboard shortcut help modal ──────────────
3641
- function openShortcutHelp() { document.getElementById('shortcut-modal').classList.add('open'); }
3642
- function closeShortcutHelp() { document.getElementById('shortcut-modal').classList.remove('open'); }
3643
-
3644
- // ── feature 33: "currently working on" inference ──────────
3645
- function updateCurrentActivity(events) {
3646
- const el = document.getElementById('topbar-activity');
3647
- if (!el) return;
3648
- if (!events?.length) { el.textContent = ''; el.classList.remove('loaded'); return; }
3649
- const recent = [...events].reverse().find(ev => ev.kind === 'tool');
3650
- if (!recent) { el.textContent = ''; el.classList.remove('loaded'); return; }
3651
- const name = recent.name || '';
3652
- let activity = '';
3653
- if (['Write', 'Edit', 'Read', 'MultiEdit'].includes(name)) {
3654
- const lbl = recent.label || '';
3655
- const match = lbl.match(/([^/\\]+\.[a-zA-Z0-9]+)/) ;
3656
- activity = match ? match[1] : (lbl.split('/').pop() || name);
3657
- } else if (name === 'Bash') {
3658
- activity = 'bash: ' + (recent.label || '').slice(0, 24);
3659
- } else if (name === 'WebSearch' || name === 'WebFetch') {
3660
- activity = 'web: ' + (recent.label || '').slice(0, 20);
3661
- } else if (name) {
3662
- activity = name;
3663
- }
3664
- if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
3665
- else { el.textContent = ''; el.classList.remove('loaded'); }
3666
- }
3667
-
3668
- // ── feature 34: prompt pattern analysis ───────────────────
3669
- let patternsOpen = false;
3670
- function togglePatterns() {
3671
- patternsOpen = !patternsOpen;
3672
- document.getElementById('btn-patterns').classList.toggle('on', patternsOpen);
3673
- const p = document.getElementById('patterns-panel');
3674
- p.style.display = patternsOpen ? '' : 'none';
3675
- if (patternsOpen) buildPatterns();
3676
- }
3677
- function buildPatterns() {
3678
- const el = document.getElementById('patterns-body');
3679
- if (!allSessions.length) { el.innerHTML = '<div class="loading-txt">No sessions loaded</div>'; return; }
3680
- const freq = {};
3681
- for (const s of allSessions) {
3682
- const words = (s.lastPrompt || '').toLowerCase().match(/\b[a-z]{4,}\b/g) || [];
3683
- const seen = new Set();
3684
- for (const w of words) {
3685
- if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
3686
- }
3687
- }
3688
- const sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
3689
- if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
3690
- const maxCount = sorted[0][1];
3691
- const rows = sorted.map(([word, count], i) => {
3692
- const barW = Math.round((count / maxCount) * 100);
3693
- return `<tr><td class="lb-rank">${i + 1}</td>
3694
- <td style="font-size:12px;color:var(--text-mid)">${esc(word)}</td>
3695
- <td style="width:100px;padding:4px 6px"><div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
3696
- <div style="height:100%;width:${barW}%;background:oklch(70% 0.15 300);border-radius:2px"></div></div></td>
3697
- <td class="lb-cost" style="color:var(--text-mid)">${count}</td></tr>`;
3698
- }).join('');
3699
- el.innerHTML = `<table class="lb-table"><thead><tr>
3700
- <th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
3701
- </tr></thead><tbody>${rows}</tbody></table>`;
3702
- }
3703
-
3704
- // ── feature 35: session streak tracker ────────────────────
3705
- function calcStreak() {
3706
- const dates = new Set(allSessions.map(s => {
3707
- const t = s.firstTs || s.mtime;
3708
- if (!t) return null;
3709
- return new Date(typeof t === 'number' ? t : t).toDateString();
3710
- }).filter(Boolean));
3711
- let streak = 0;
3712
- const today = new Date();
3713
- for (let i = 0; i <= 365; i++) {
3714
- const d = new Date(today);
3715
- d.setDate(d.getDate() - i);
3716
- if (dates.has(d.toDateString())) {
3717
- streak++;
3718
- } else if (i > 0) {
3719
- break;
3720
- }
3721
- }
3722
- return streak;
3723
- }
3724
-
3725
- // ── feature 25: notification toasts ──────────────────────
3726
- let _toastLastBudgetKey = '';
3727
- function showToast(title, msg, type = 'info', duration = 5000) {
3728
- const rack = document.getElementById('toast-rack');
3729
- if (!rack) return;
3730
- const icoMap = { warn: '⚑', err: '⚠', ok: '✓', info: '◉' };
3731
- const div = document.createElement('div');
3732
- div.className = 'toast t-' + type;
3733
- div.innerHTML = `<span class="toast-ico">${icoMap[type] || '◉'}</span>
3734
- <div class="toast-body">
3735
- <div class="toast-title">${esc(title)}</div>
3736
- <div class="toast-msg">${esc(msg)}</div>
3737
- </div>
3738
- <button class="toast-close" onclick="this.closest('.toast').remove()">✕</button>`;
3739
- rack.appendChild(div);
3740
- if (duration > 0) setTimeout(() => { try { div.remove(); } catch {} }, duration);
3741
- }
3742
-
3743
- function checkBudgetToast(todayCost, monthCost) {
3744
- const budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
3745
- const daily = parseFloat(budget.daily) || 0;
3746
- const monthly = parseFloat(budget.monthly) || 0;
3747
- if (daily > 0 && todayCost >= daily * 0.9) {
3748
- const pct = Math.round((todayCost / daily) * 100);
3749
- const key = 'daily-' + pct;
3750
- if (key !== _toastLastBudgetKey) {
3751
- _toastLastBudgetKey = key;
3752
- showToast('Budget alert', `Daily spend at ${pct}% ($${todayCost.toFixed(2)} of $${daily})`, todayCost >= daily ? 'err' : 'warn');
3753
- }
3754
- }
3755
- if (monthly > 0 && monthCost >= monthly * 0.9) {
3756
- const pct = Math.round((monthCost / monthly) * 100);
3757
- const key = 'monthly-' + pct;
3758
- if (key !== _toastLastBudgetKey) {
3759
- _toastLastBudgetKey = key;
3760
- showToast('Monthly budget', `Monthly spend at ${pct}% ($${monthCost.toFixed(2)} of $${monthly})`, monthCost >= monthly ? 'err' : 'warn');
3761
- }
3762
- }
3763
- }
3764
-
3765
- // ── feature 39: cost period toggle ────────────────────────
3766
- let activePeriod = 'day';
3767
- let heatmapDateFilter = null;
3768
-
3769
- function setPeriod(p) {
3770
- activePeriod = p;
3771
- document.querySelectorAll('.period-btn').forEach(b => b.classList.toggle('active', b.dataset.period === p));
3772
- buildTokenVelocity();
3773
- syncURLParams();
3774
- }
3775
-
3776
- function periodFilteredSessions() {
3777
- const now = Date.now();
3778
- const DAY = 86400000;
3779
- const windows = { day: DAY, week: 7 * DAY, month: 30 * DAY, all: Infinity };
3780
- const w = windows[activePeriod] || DAY;
3781
- if (w === Infinity) return allSessions;
3782
- return allSessions.filter(s => {
3783
- const t = s.firstTs || s.mtime; if (!t) return false;
3784
- const ts = typeof t === 'number' ? t : new Date(t).getTime();
3785
- return (now - ts) <= w;
3786
- });
3787
- }
3788
-
3789
- // ── feature 40: session heatmap ────────────────────────────
3790
- function buildSessionHeatmap(sessions) {
3791
- const el = document.getElementById('shm-grid');
3792
- const wrap = document.getElementById('sess-heatmap');
3793
- if (!el || !wrap || !sessions.length) return;
3794
- wrap.style.display = 'block';
3795
- const DAY = 86400000;
3796
- const now = Date.now();
3797
- const WEEKS = 12; const DAYS = WEEKS * 7;
3798
- const buckets = new Array(DAYS).fill(null).map(() => ({ count:0, date:null }));
3799
- for (let i = 0; i < DAYS; i++) {
3800
- buckets[i].date = new Date(now - (DAYS - 1 - i) * DAY).toDateString();
3801
- }
3802
- for (const s of sessions) {
3803
- const ts = s.lastTs || s.mtime; if (!ts) continue;
3804
- const age = now - (typeof ts === 'number' ? ts : new Date(ts).getTime());
3805
- const idx = DAYS - 1 - Math.floor(age / DAY);
3806
- if (idx >= 0 && idx < DAYS) buckets[idx].count++;
3807
- }
3808
- const max = Math.max(...buckets.map(b => b.count), 1);
3809
- el.innerHTML = buckets.map(b => {
3810
- const level = b.count === 0 ? 0 : Math.min(4, Math.ceil(b.count / max * 4));
3811
- const isActive = b.date === heatmapDateFilter;
3812
- return `<div class="shm-cell shm-${level}${isActive ? ' shm-active' : ''}" title="${b.date}: ${b.count} session${b.count !== 1 ? 's' : ''}" onclick="setHeatmapFilter('${b.date}',${b.count})"></div>`;
3813
- }).join('');
3814
- }
3815
-
3816
- function setHeatmapFilter(dateStr, count) {
3817
- if (!count) return;
3818
- heatmapDateFilter = heatmapDateFilter === dateStr ? null : dateStr;
3819
- const clearBtn = document.getElementById('shm-clear');
3820
- if (clearBtn) clearBtn.classList.toggle('show', !!heatmapDateFilter);
3821
- viewRendered['sessions'] = false;
3822
- renderSessions();
3823
- syncURLParams();
3824
- }
3825
-
3826
- function clearHeatmapFilter() {
3827
- heatmapDateFilter = null;
3828
- const clearBtn = document.getElementById('shm-clear');
3829
- if (clearBtn) clearBtn.classList.remove('show');
3830
- viewRendered['sessions'] = false;
3831
- renderSessions();
3832
- }
3833
-
3834
- function toggleSessGroup(id) {
3835
- const body = document.getElementById(id);
3836
- if (!body) return;
3837
- body.classList.toggle('collapsed');
3838
- const hdr = body.previousElementSibling;
3839
- if (hdr) { const tog = hdr.querySelector('.sg-toggle'); if (tog) tog.textContent = body.classList.contains('collapsed') ? '▸' : '▾'; }
3840
- }
3841
-
3842
- // ── feature 41: bulk session actions ───────────────────────
3843
- let bulkSelected = new Set();
3844
- let lastClickedSessIdx = null;
3845
-
3846
- function handleSessRowClick(evt, row, sessId) {
3847
- if (diffMode) { diffSelectSession(JSON.parse(row.dataset.sessData || '{}'), row); return; }
3848
- if (evt.shiftKey && lastClickedSessIdx !== null) {
3849
- // range-select
3850
- const rows = [...document.querySelectorAll('#sess-content .sess-row')];
3851
- const curIdx = rows.indexOf(row);
3852
- const lo = Math.min(lastClickedSessIdx, curIdx);
3853
- const hi = Math.max(lastClickedSessIdx, curIdx);
3854
- for (let i = lo; i <= hi; i++) {
3855
- const r = rows[i]; if (!r) continue;
3856
- const id = r.dataset.sessId;
3857
- if (id) { bulkSelected.add(id); r.classList.add('bulk-sel'); }
3858
- }
3859
- updateBulkToolbar();
3860
- return;
3861
- }
3862
- if (bulkSelected.size > 0) {
3863
- // toggle this row in bulk selection
3864
- if (bulkSelected.has(sessId)) { bulkSelected.delete(sessId); row.classList.remove('bulk-sel'); }
3865
- else { bulkSelected.add(sessId); row.classList.add('bulk-sel'); }
3866
- const rows = [...document.querySelectorAll('#sess-content .sess-row')];
3867
- lastClickedSessIdx = rows.indexOf(row);
3868
- updateBulkToolbar();
3869
- return;
3870
- }
3871
- lastClickedSessIdx = [...document.querySelectorAll('#sess-content .sess-row')].indexOf(row);
3872
- jumpToSession(sessId);
3873
- }
3874
-
3875
- function updateBulkToolbar() {
3876
- const tb = document.getElementById('bulk-toolbar');
3877
- const cnt = document.getElementById('bulk-count');
3878
- if (!tb) return;
3879
- tb.classList.toggle('show', bulkSelected.size > 0);
3880
- if (cnt) cnt.textContent = bulkSelected.size + ' selected';
3881
- }
3882
-
3883
- function clearBulkSelection() {
3884
- bulkSelected.clear();
3885
- document.querySelectorAll('#sess-content .sess-row.bulk-sel').forEach(r => r.classList.remove('bulk-sel'));
3886
- updateBulkToolbar();
3887
- }
3888
-
3889
- function bulkBookmark() {
3890
- for (const id of bulkSelected) bookmarks.add(id);
3891
- localStorage.setItem('mm-bookmarks', JSON.stringify([...bookmarks]));
3892
- clearBulkSelection();
3893
- showToast('Bookmarked', `${bulkSelected.size || 'Selected'} sessions bookmarked`, 'ok');
3894
- viewRendered['sessions'] = false;
3895
- renderSessions();
3896
- }
3897
-
3898
- function bulkExport() {
3899
- const toExport = allSessions.filter(s => bulkSelected.has(s.id));
3900
- if (!toExport.length) return;
3901
- const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'Files Touched'];
3902
- const rows = toExport.map(s => {
3903
- const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
3904
- const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
3905
- const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
3906
- const prompt = (s.lastPrompt || '').replace(/"/g, '""');
3907
- const files = (s.filesTouched || []).join(';');
3908
- return [dt, s.id, prompt, cost, dur, s.toolCalls || '', files];
3909
- });
3910
- const csv = [headers, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n');
3911
- const blob = new Blob([csv], { type: 'text/csv' });
3912
- const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
3913
- a.download = `sessions-bulk-${new Date().toISOString().slice(0,10)}.csv`; a.click();
3914
- URL.revokeObjectURL(a.href);
3915
- showToast('Exported', `${toExport.length} sessions saved`, 'ok');
3916
- clearBulkSelection();
3917
- }
3918
-
3919
- // ── feature 26: token velocity sparkline ──────────────────
3920
- function buildTokenVelocity() {
3921
- const el = document.getElementById('m-velocity');
3922
- if (!el || !allSessions.length) return;
3923
- const now = Date.now();
3924
- const HOUR = 3600000;
3925
- const filtered = periodFilteredSessions();
3926
- const buckets = new Array(24).fill(0);
3927
- for (const s of filtered) {
3928
- const t = s.firstTs || s.mtime;
3929
- if (!t) continue;
3930
- const ts = typeof t === 'number' ? t : new Date(t).getTime();
3931
- const hoursAgo = (now - ts) / HOUR;
3932
- if (hoursAgo < 0 || hoursAgo >= 24) continue;
3933
- const bucket = Math.min(23, Math.floor(23 - hoursAgo));
3934
- buckets[bucket] += s.totalInputTokens || 0;
3935
- }
3936
- const maxTok = Math.max(1, ...buckets);
3937
- const totalTok = buckets.reduce((a, b) => a + b, 0);
3938
- const fmt = n => n > 1e6 ? (n/1e6).toFixed(1)+'M' : n > 1e3 ? (n/1e3).toFixed(0)+'k' : String(n);
3939
- const bars = buckets.map((v, i) => {
3940
- const h = Math.max(2, Math.round((v / maxTok) * 28));
3941
- const cls = v > maxTok * 0.66 ? 'vel-hi' : v < maxTok * 0.15 ? 'vel-lo' : '';
3942
- const hrsAgo = 23 - i;
3943
- return `<div class="vel-bar ${cls}" style="height:${h}px" title="${fmt(v)} tokens — ${hrsAgo}h ago"></div>`;
3944
- }).join('');
3945
- const totalCost = filtered.reduce((a, s) => a + (s.totalCost || 0), 0);
3946
- const periodLabel = { day:'24h', week:'7d', month:'30d', all:'all time' }[activePeriod] || '24h';
3947
- el.innerHTML = `<div class="m-group-title">Token Velocity <span style="font-size:10px;color:var(--text-xs);font-weight:400">${periodLabel} · ${fmt(totalTok)}</span></div>
3948
- <div id="vel-chart">${bars}</div>
3949
- <div style="font-size:10px;color:var(--text-xs);margin-top:4px">Cost <span style="color:oklch(78% 0.18 75)">$${totalCost.toFixed(2)}</span> <span style="color:var(--text-xs)">· ${filtered.length} sessions</span></div>`;
3950
- }
3951
-
3952
- // ── feature 27: export sessions CSV ───────────────────────
3953
- function exportSessionsCSV() {
3954
- if (!allSessions.length) { showToast('No data', 'No sessions loaded yet', 'warn'); return; }
3955
- const headers = ['Date', 'Session ID', 'Prompt', 'Cost ($)', 'Duration (s)', 'Tool Calls', 'User Messages', 'Cache Hit %', 'Input Tokens'];
3956
- const rows = allSessions.map(s => {
3957
- const dt = new Date(s.firstTs || s.mtime || 0).toISOString().slice(0, 19).replace('T', ' ');
3958
- const cost = typeof s.totalCost === 'number' ? s.totalCost.toFixed(4) : '';
3959
- const dur = s.totalDurationMs ? Math.round(s.totalDurationMs / 1000) : '';
3960
- const cachePct = s.totalInputTokens > 0 ? Math.round((s.cacheReadTokens || 0) / s.totalInputTokens * 100) : '';
3961
- const prompt = (s.lastPrompt || '').replace(/"/g, '""');
3962
- return [dt, s.id, prompt, cost, dur, s.toolCalls || '', s.userMessages || '', cachePct, s.totalInputTokens || ''];
3963
- });
3964
- const csv = [headers, ...rows].map(r => r.map(c => `"${c}"`).join(',')).join('\n');
3965
- const blob = new Blob([csv], { type: 'text/csv' });
3966
- const a = document.createElement('a');
3967
- a.href = URL.createObjectURL(blob);
3968
- a.download = `sessions-${new Date().toISOString().slice(0, 10)}.csv`;
3969
- a.click();
3970
- URL.revokeObjectURL(a.href);
3971
- showToast('Exported', `${allSessions.length} sessions saved as CSV`, 'ok');
3972
- }
3973
-
3974
- // ── feature 28: tool usage ranking ────────────────────────
3975
- let toolRankOpen = false;
3976
- function toggleToolRank() {
3977
- toolRankOpen = !toolRankOpen;
3978
- document.getElementById('btn-tool-rank').classList.toggle('on', toolRankOpen);
3979
- const p = document.getElementById('tool-rank-panel');
3980
- p.style.display = toolRankOpen ? '' : 'none';
3981
- if (toolRankOpen) loadToolRank();
3982
- }
3983
- async function loadToolRank() {
3984
- const el = document.getElementById('tool-rank-body');
3985
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
3986
- try {
3987
- const data = await apiFetch('/api/tool-ranking?dir=' + enc(DIR));
3988
- if (!data.tools?.length) { el.innerHTML = '<div class="loading-txt">No tool usage data</div>'; return; }
3989
- const maxCount = data.tools[0].count;
3990
- const rows = data.tools.slice(0, 15).map((t, i) => {
3991
- const barW = Math.round((t.count / maxCount) * 100);
3992
- const errRate = t.errors > 0 ? ((t.errors / t.count) * 100).toFixed(0) + '%' : '—';
3993
- return `<tr><td class="lb-rank">${i + 1}</td>
3994
- <td style="font-size:12px;color:var(--text-mid);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t.tool)}</td>
3995
- <td style="width:80px;padding:4px 6px">
3996
- <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
3997
- <div style="height:100%;width:${barW}%;background:oklch(65% 0.15 200);border-radius:2px"></div>
3998
- </div>
3999
- </td>
4000
- <td class="lb-cost" style="color:var(--text-mid)">${t.count.toLocaleString()}</td>
4001
- <td class="lb-dur" style="color:${t.errors > 0 ? 'oklch(70% 0.18 25)' : 'var(--text-xs)'}">${errRate}</td>
4002
- </tr>`;
4003
- }).join('');
4004
- el.innerHTML = `<table class="lb-table"><thead><tr>
4005
- <th class="lb-rank">#</th><th>Tool</th><th></th><th class="lb-cost">Calls</th><th class="lb-dur">Error%</th>
4006
- </tr></thead><tbody>${rows}</tbody></table>`;
4007
- } catch (err) {
4008
- el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
4009
- }
4010
- }
4011
-
4012
- // ── feature 29: cost breakdown by project ─────────────────
4013
- let projCostsOpen = false;
4014
- function toggleProjCosts() {
4015
- projCostsOpen = !projCostsOpen;
4016
- document.getElementById('btn-proj-costs').classList.toggle('on', projCostsOpen);
4017
- const p = document.getElementById('proj-costs-panel');
4018
- p.style.display = projCostsOpen ? '' : 'none';
4019
- if (projCostsOpen) loadProjCosts();
4020
- }
4021
- async function loadProjCosts() {
4022
- const el = document.getElementById('proj-costs-body');
4023
- el.innerHTML = '<div class="loading-txt">Loading…</div>';
4024
- try {
4025
- const data = await apiFetch('/api/project-costs');
4026
- if (!data.projects?.length) { el.innerHTML = '<div class="loading-txt">No cost data across projects</div>'; return; }
4027
- const maxCost = data.projects[0].cost;
4028
- const rows = data.projects.slice(0, 10).map((p, i) => {
4029
- const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
4030
- const name = p.path.split('/').filter(Boolean).pop() || p.path;
4031
- return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
4032
- <td class="lb-rank">${i + 1}</td>
4033
- <td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
4034
- <td class="lb-cost">$${p.cost.toFixed(2)}</td>
4035
- <td style="width:80px;padding:4px 6px">
4036
- <div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
4037
- <div style="height:100%;width:${barW}%;background:oklch(72% 0.18 75 / 0.7);border-radius:2px"></div>
4038
- </div>
4039
- </td>
4040
- <td class="lb-dur">${p.sessions}</td>
4041
- </tr>`;
4042
- }).join('');
4043
- el.innerHTML = `<table class="lb-table"><thead><tr>
4044
- <th class="lb-rank">#</th><th>Project</th><th class="lb-cost">Cost</th><th></th><th class="lb-dur">Sessions</th>
4045
- </tr></thead><tbody>${rows}</tbody></table>`;
4046
- } catch (err) {
4047
- el.innerHTML = '<div class="loading-txt">Could not load: ' + esc(err.message) + '</div>';
4048
- }
4049
- }
4050
-
4051
- // ── feature 55: session timeline (Gantt) ──────────────────
4052
- let timelineOpen = false;
4053
- function toggleTimeline() {
4054
- timelineOpen = !timelineOpen;
4055
- document.getElementById('btn-timeline').classList.toggle('on', timelineOpen);
4056
- const panel = document.getElementById('timeline-panel');
4057
- panel.classList.toggle('open', timelineOpen);
4058
- if (timelineOpen) buildGanttTimeline();
4059
- }
4060
-
4061
- function buildGanttTimeline() {
4062
- const el = document.getElementById('timeline-scroll');
4063
- if (!el || !allSessions.length) return;
4064
- // Group by date, show last 14 days
4065
- const now = Date.now(); const DAY = 86400000;
4066
- const days = {};
4067
- for (let i = 13; i >= 0; i--) {
4068
- const d = new Date(now - i * DAY); d.setHours(0,0,0,0);
4069
- days[d.toDateString()] = [];
4070
- }
4071
- for (const s of allSessions) {
4072
- const t = s.firstTs || s.mtime; if (!t) continue;
4073
- const d = new Date(typeof t === 'number' ? t : t); d.setHours(0,0,0,0);
4074
- const key = d.toDateString();
4075
- if (key in days) days[key].push(s);
4076
- }
4077
- // Find day with most sessions to normalize bar widths by session start time within day
4078
- const allCosts = allSessions.map(s => s.totalCost || 0);
4079
- const maxCost = Math.max(0.01, ...allCosts);
4080
-
4081
- const DONUT_COLORS = ['oklch(72% 0.18 75)','oklch(65% 0.15 150)','oklch(65% 0.18 220)','oklch(65% 0.2 300)','oklch(65% 0.2 25)'];
4082
-
4083
- let html = '';
4084
- for (const [dateStr, sessions] of Object.entries(days)) {
4085
- const d = new Date(dateStr);
4086
- const lbl = d.toLocaleDateString(undefined, { month:'short', day:'numeric' });
4087
- let track = '';
4088
- // Sort sessions by start time
4089
- const sorted = [...sessions].sort((a, b) => {
4090
- const ta = a.firstTs || a.mtime || 0; const tb = b.firstTs || b.mtime || 0;
4091
- return (typeof ta === 'number' ? ta : new Date(ta).getTime()) - (typeof tb === 'number' ? tb : new Date(tb).getTime());
4092
- });
4093
- for (let i = 0; i < sorted.length; i++) {
4094
- const s = sorted[i];
4095
- const startTs = typeof (s.firstTs || s.mtime) === 'number' ? (s.firstTs || s.mtime) : new Date(s.firstTs || s.mtime).getTime();
4096
- const dayStart = new Date(dateStr).getTime();
4097
- const startPct = Math.min(95, ((startTs - dayStart) / DAY) * 100);
4098
- const durPct = s.totalDurationMs ? Math.max(0.5, Math.min(15, (s.totalDurationMs / DAY) * 100)) : 1;
4099
- const costRatio = Math.max(0.15, (s.totalCost || 0.001) / maxCost);
4100
- const opacity = 0.3 + costRatio * 0.7;
4101
- const color = DONUT_COLORS[i % DONUT_COLORS.length];
4102
- const tip = `${s.lastPrompt ? s.lastPrompt.slice(0,60) : s.id} | $${(s.totalCost||0).toFixed(3)} | ${fmtDur(s.totalDurationMs||0)}`;
4103
- track += `<div class="tl-bar" style="left:${startPct}%;width:${durPct}%;background:${color};opacity:${opacity}" title="${esc(tip)}" onclick="jumpToSession('${esc(s.id)}')"></div>`;
4104
- }
4105
- html += `<div class="tl-day-row"><div class="tl-day-lbl">${lbl}</div><div class="tl-track">${track}</div></div>`;
4106
- }
4107
- el.innerHTML = html || '<div class="loading-txt">No sessions in last 14 days</div>';
4108
- }
4109
-
4110
- // ── feature 56: daily report card ─────────────────────────
4111
- function showReportCard() {
4112
- const modal = document.getElementById('report-modal');
4113
- const pre = document.getElementById('report-pre');
4114
- if (!modal || !pre) return;
4115
- const now = new Date();
4116
- const todayStr = now.toDateString();
4117
- const weekMs = 7 * 86400000;
4118
- const weekAgo = Date.now() - weekMs;
4119
-
4120
- const todaySess = allSessions.filter(s => {
4121
- const t = s.firstTs || s.mtime; if (!t) return false;
4122
- return new Date(typeof t === 'number' ? t : t).toDateString() === todayStr;
4123
- });
4124
- const weekSess = allSessions.filter(s => {
4125
- const t = s.firstTs || s.mtime; if (!t) return false;
4126
- return (typeof t === 'number' ? t : new Date(t).getTime()) >= weekAgo;
4127
- });
4128
-
4129
- function summarize(sessions, label) {
4130
- if (!sessions.length) return `${label}: no sessions\n`;
4131
- const totalCost = sessions.reduce((a, s) => a + (s.totalCost||0), 0);
4132
- const totalDur = sessions.reduce((a, s) => a + (s.totalDurationMs||0), 0);
4133
- const totalTools = sessions.reduce((a, s) => a + (s.toolCalls||0), 0);
4134
- const totalErrs = sessions.reduce((a, s) => a + (s.errorCount||0), 0);
4135
- // top files
4136
- const fileFreq = {};
4137
- for (const s of sessions) for (const f of s.filesTouched||[]) fileFreq[f] = (fileFreq[f]||0) + 1;
4138
- const topFiles = Object.entries(fileFreq).sort((a,b)=>b[1]-a[1]).slice(0,5).map(([f,n])=>` - ${f} (×${n})`).join('\n');
4139
- // anomalies
4140
- const costsArr = sessions.map(s => s.totalCost||0).filter(c => c > 0).sort((a,b)=>a-b);
4141
- const median = costsArr.length ? costsArr[Math.floor(costsArr.length/2)] : 0;
4142
- const anomalies = sessions.filter(s => (s.totalCost||0) > median * 3 && (s.totalCost||0) > 0.5).map(s => ` - ${s.lastPrompt?.slice(0,50)||s.id}: $${(s.totalCost||0).toFixed(2)}`).join('\n');
4143
- return `### ${label}
4144
- - Sessions: ${sessions.length}
4145
- - Total cost: $${totalCost.toFixed(3)}
4146
- - Total duration: ${fmtDur(totalDur)}
4147
- - Tool calls: ${totalTools}${totalErrs ? ` (${totalErrs} errors)` : ''}
4148
- ${topFiles ? `\nTop files touched:\n${topFiles}` : ''}${anomalies ? `\n\nCost anomalies:\n${anomalies}` : ''}
4149
- `;
4150
- }
4151
-
4152
- const report = `# Monomind Report Card
4153
- Generated: ${now.toLocaleString()}
4154
- Project: ${DIR.split('/').filter(Boolean).pop() || DIR}
4155
-
4156
- ${summarize(todaySess, 'Today')}
4157
- ${summarize(weekSess, 'This Week')}
4158
- ---
4159
- Total all-time: ${allSessions.length} sessions | $${allSessions.reduce((a,s)=>a+(s.totalCost||0),0).toFixed(2)}
4160
- `;
4161
- pre.textContent = report;
4162
- modal.classList.add('open');
4163
- }
4164
-
4165
- function closeReportCard() {
4166
- document.getElementById('report-modal').classList.remove('open');
4167
- }
4168
-
4169
- function copyReportCard() {
4170
- const text = document.getElementById('report-pre')?.textContent || '';
4171
- navigator.clipboard.writeText(text).then(() => {
4172
- const btn = document.querySelector('.rp-copy-btn');
4173
- if (btn) { btn.textContent = '✓ Copied'; setTimeout(() => { btn.textContent = '⎘ Copy'; }, 1500); }
4174
- }).catch(() => {});
4175
- }
4176
-
4177
- // ── feature 57: file-pivot cross-filter ───────────────────
4178
- let filePivot = null;
4179
-
4180
- function setFilePivot(fname, event) {
4181
- if (event) event.stopPropagation();
4182
- filePivot = filePivot === fname ? null : fname;
4183
- const bar = document.getElementById('file-pivot-bar');
4184
- const lbl = document.getElementById('fpb-label');
4185
- if (bar) bar.classList.toggle('show', !!filePivot);
4186
- if (lbl) lbl.textContent = filePivot ? `Showing sessions that touched: ${filePivot}` : '';
4187
- viewRendered['sessions'] = false;
4188
- renderSessions();
4189
- }
4190
-
4191
- function clearFilePivot() {
4192
- filePivot = null;
4193
- const bar = document.getElementById('file-pivot-bar');
4194
- if (bar) bar.classList.remove('show');
4195
- viewRendered['sessions'] = false;
4196
- renderSessions();
4197
- }
4198
-
4199
- // ── feature 58: model cost donut ──────────────────────────
4200
- let modelDonutOpen = false;
4201
- const DONUT_PALETTE = ['oklch(72% 0.18 75)','oklch(65% 0.15 150)','oklch(65% 0.18 220)','oklch(65% 0.2 300)','oklch(62% 0.18 25)','oklch(70% 0.12 60)'];
4202
-
4203
- function toggleModelDonut() {
4204
- modelDonutOpen = !modelDonutOpen;
4205
- document.getElementById('btn-donut').classList.toggle('on', modelDonutOpen);
4206
- const panel = document.getElementById('model-donut-panel');
4207
- panel.classList.toggle('open', modelDonutOpen);
4208
- if (modelDonutOpen) buildModelDonut();
4209
- }
4210
-
4211
- function buildModelDonut() {
4212
- const el = document.getElementById('model-donut-panel');
4213
- if (!el) return;
4214
- const breakdown = {};
4215
- for (const s of allSessions) {
4216
- for (const [model, d] of Object.entries(s.modelBreakdown || {})) {
4217
- if (!breakdown[model]) breakdown[model] = { calls: 0, cost: 0 };
4218
- breakdown[model].calls += d.calls || 0;
4219
- breakdown[model].cost += d.cost || 0;
4220
- }
4221
- }
4222
- const entries = Object.entries(breakdown).sort((a, b) => b[1].cost - a[1].cost);
4223
- if (!entries.length) { el.innerHTML = '<div class="loading-txt" style="padding:8px">No model breakdown data</div>'; return; }
4224
- const totalCost = entries.reduce((a, [, d]) => a + d.cost, 0);
4225
-
4226
- // Build SVG conic-gradient-style donut using stroke-dasharray
4227
- const R = 36; const CX = 44; const CY = 44; const CIRCUMFERENCE = 2 * Math.PI * R;
4228
- let offset = 0;
4229
- const segments = entries.map(([model, d], i) => {
4230
- const pct = totalCost > 0 ? d.cost / totalCost : 0;
4231
- const dash = pct * CIRCUMFERENCE;
4232
- const seg = `<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${DONUT_PALETTE[i % DONUT_PALETTE.length]}" stroke-width="14" stroke-dasharray="${dash} ${CIRCUMFERENCE - dash}" stroke-dashoffset="${-offset}" transform="rotate(-90 ${CX} ${CY})"/>`;
4233
- offset += dash;
4234
- return seg;
4235
- }).join('');
4236
- const svg = `<svg class="donut-svg" width="88" height="88" viewBox="0 0 88 88">
4237
- <circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="var(--surface-hi)" stroke-width="14"/>
4238
- ${segments}
4239
- <text x="${CX}" y="${CY}" text-anchor="middle" dy="0.3em" font-size="9" fill="var(--text-xs)" font-family="var(--mono)">$${totalCost.toFixed(2)}</text>
4240
- </svg>`;
4241
-
4242
- const legend = entries.slice(0, 6).map(([model, d], i) => {
4243
- const short = model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
4244
- const pct = totalCost > 0 ? Math.round(d.cost / totalCost * 100) : 0;
4245
- return `<div class="donut-item"><div class="donut-swatch" style="background:${DONUT_PALETTE[i % DONUT_PALETTE.length]}"></div>
4246
- <span class="donut-name" title="${esc(model)}">${esc(short)}</span>
4247
- <span class="donut-cost">$${d.cost.toFixed(2)} <span style="color:var(--text-xs)">${pct}%</span></span>
4248
- </div>`;
4249
- }).join('');
4250
-
4251
- el.innerHTML = `<div class="donut-wrap">${svg}<div class="donut-legend">${legend}</div></div>`;
4252
- }
4253
-
4254
- // ── feature 60: cost anomaly explainer ────────────────────
4255
- // Enhance anom-cost badge with onclick that shows explainer panel
4256
- function showCostExplainer(sessId, event) {
4257
- if (event) event.stopPropagation();
4258
- const drawer = document.getElementById('err-drawer-' + sessId);
4259
- if (!drawer) return;
4260
- if (drawer.classList.contains('open')) { drawer.classList.remove('open'); drawer.innerHTML = ''; return; }
4261
- const sess = allSessions.find(s => s.id === sessId);
4262
- if (!sess) return;
4263
- drawer.classList.add('open');
4264
- const costsArr = allSessions.map(s => s.totalCost||0).filter(c=>c>0).sort((a,b)=>a-b);
4265
- const median = costsArr.length ? costsArr[Math.floor(costsArr.length/2)] : 0;
4266
- const pct = costsArr.length ? Math.round(costsArr.filter(c=>c<=(sess.totalCost||0)).length/costsArr.length*100) : 0;
4267
- const ratio = median > 0 ? ((sess.totalCost||0)/median).toFixed(1) : '—';
4268
- const modelRows = Object.entries(sess.modelBreakdown||{}).sort((a,b)=>b[1].cost-a[1].cost).slice(0,4)
4269
- .map(([m,d])=>`<div class="err-item">${esc(m.replace(/^claude-/,'').replace(/-\d{8}$/,''))}: $${(d.cost||0).toFixed(4)} · ${d.calls||0} calls</div>`).join('');
4270
- drawer.innerHTML = `<div class="err-drawer-head" style="color:oklch(70% 0.18 80)">
4271
- <span>Cost anomaly — $${(sess.totalCost||0).toFixed(3)} (${ratio}× median, top ${100-pct}%)</span>
4272
- <button class="err-close" onclick="this.closest('.err-drawer').classList.remove('open');this.closest('.err-drawer').innerHTML=''">✕</button>
4273
- </div>
4274
- <div class="err-drawer-body">
4275
- <div class="err-item" style="color:var(--text-lo)">Tool calls: ${sess.toolCalls||0} · Messages: ${sess.totalMessages||0} · Tokens in: ${(sess.totalInputTokens||0).toLocaleString()}</div>
4276
- ${modelRows || '<div class="err-item" style="color:var(--text-xs)">No model breakdown available</div>'}
4277
- </div>`;
4278
- }
4279
-
4280
- // ── feature 47: 30-day daily cost trend ───────────────────
4281
- function buildDailyCostTrend() {
4282
- const el = document.getElementById('m-daily-trend');
4283
- if (!el || !allSessions.length) return;
4284
- const now = Date.now(); const DAY = 86400000;
4285
- const buckets = new Array(30).fill(0);
4286
- const dates = new Array(30).fill(null).map((_, i) => new Date(now - (29 - i) * DAY).toDateString());
4287
- for (const s of allSessions) {
4288
- const t = s.firstTs || s.mtime; if (!t) continue;
4289
- const ts = typeof t === 'number' ? t : new Date(t).getTime();
4290
- const daysAgo = Math.floor((now - ts) / DAY);
4291
- const idx = 29 - daysAgo;
4292
- if (idx >= 0 && idx < 30) buckets[idx] += s.totalCost || 0;
4293
- }
4294
- const maxCost = Math.max(0.001, ...buckets);
4295
- const totalCost = buckets.reduce((a, b) => a + b, 0);
4296
- const bars = buckets.map((v, i) => {
4297
- const h = Math.max(2, Math.round((v / maxCost) * 38));
4298
- const isActive = dates[i] === heatmapDateFilter;
4299
- const label = i === 29 ? 'Today' : (i === 28 ? 'Yday' : '');
4300
- return `<div class="dt-bar${isActive ? ' active' : ''}" style="height:${h}px" title="${dates[i]}: $${v.toFixed(3)}" onclick="setDailyTrendFilter('${dates[i]}',${v})"></div>`;
4301
- }).join('');
4302
- el.innerHTML = `<div class="m-group-title">30-Day Cost <span style="font-size:10px;color:var(--text-xs);font-weight:400">$${totalCost.toFixed(2)} total</span></div>
4303
- <div id="daily-trend-chart">${bars}</div>
4304
- <div style="display:flex;justify-content:space-between;font-size:9px;color:var(--text-xs);margin-top:2px"><span>30d ago</span><span>Today</span></div>`;
4305
- }
4306
-
4307
- function setDailyTrendFilter(dateStr, cost) {
4308
- if (!cost) return;
4309
- heatmapDateFilter = heatmapDateFilter === dateStr ? null : dateStr;
4310
- const clearBtn = document.getElementById('shm-clear');
4311
- if (clearBtn) clearBtn.classList.toggle('show', !!heatmapDateFilter);
4312
- viewRendered['sessions'] = false;
4313
- renderSessions();
4314
- syncURLParams();
4315
- }
4316
-
4317
- // ── feature 48: live cost ticker ──────────────────────────
4318
- let _liveTickerCost = 0;
4319
- let _liveTickerPrev = 0;
4320
-
4321
- function updateLiveTicker(cost) {
4322
- const el = document.getElementById('live-cost-ticker');
4323
- if (!el) return;
4324
- if (cost == null || cost === 0) { el.classList.remove('show'); return; }
4325
- _liveTickerPrev = _liveTickerCost;
4326
- _liveTickerCost = cost;
4327
- const delta = _liveTickerCost - _liveTickerPrev;
4328
- const deltaHtml = delta > 0.0001 ? `<span class="lct-change">+$${delta.toFixed(4)}</span>` : '';
4329
- el.innerHTML = `$${cost.toFixed(3)}${deltaHtml}`;
4330
- el.classList.add('show');
4331
- }
4332
-
4333
- // ── feature 49: hourly productivity heatmap ───────────────
4334
- function buildHourlyHeatmap() {
4335
- const el = document.getElementById('m-hourly-heatmap');
4336
- if (!el || !allSessions.length) return;
4337
- // 24 hours × 7 days-of-week matrix
4338
- const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
4339
- for (const s of allSessions) {
4340
- const t = s.firstTs || s.mtime; if (!t) continue;
4341
- const d = new Date(typeof t === 'number' ? t : t);
4342
- const dow = d.getDay(); // 0=Sun
4343
- const hour = d.getHours();
4344
- grid[dow][hour]++;
4345
- }
4346
- const maxVal = Math.max(1, ...grid.flatMap(r => r));
4347
- const dayNames = ['Su','Mo','Tu','We','Th','Fr','Sa'];
4348
- let html = '<div id="hourly-heatmap-grid">';
4349
- // Header row: empty corner + hour labels (0,6,12,18,23)
4350
- html += '<div></div>';
4351
- for (let h = 0; h < 24; h++) {
4352
- const lbl = h % 6 === 0 ? String(h) : '';
4353
- html += `<div class="hh-hour-lbl">${lbl}</div>`;
4354
- }
4355
- // Data rows
4356
- for (let d = 0; d < 7; d++) {
4357
- html += `<div class="hh-day-lbl">${dayNames[d]}</div>`;
4358
- for (let h = 0; h < 24; h++) {
4359
- const v = grid[d][h];
4360
- const level = v === 0 ? 0 : Math.min(4, Math.ceil(v / maxVal * 4));
4361
- html += `<div class="hh-cell hh-${level}" title="${dayNames[d]} ${h}:00 — ${v} session${v !== 1 ? 's' : ''}"></div>`;
4362
- }
4363
- }
4364
- html += '</div>';
4365
- const peakHour = grid.flatMap((r, d) => r.map((v, h) => ({ d, h, v }))).sort((a, b) => b.v - a.v)[0];
4366
- const peakLabel = peakHour ? `${dayNames[peakHour.d]} ${peakHour.h}:00` : '';
4367
- el.innerHTML = `<div class="m-group-title">Peak Work Hours${peakLabel ? `<span style="font-size:10px;color:var(--text-xs);font-weight:400;margin-left:4px">peak: ${peakLabel}</span>` : ''}</div>${html}`;
4368
- }
4369
-
4370
- // ── feature 50: custom tag editor ─────────────────────────
4371
- const _customTagsKey = 'mm-custom-tags';
4372
- let _customTagsMap = new Map(Object.entries(JSON.parse(localStorage.getItem(_customTagsKey) || '{}')));
4373
-
4374
- function getCustomTags(sessId) {
4375
- return _customTagsMap.get(sessId) || [];
4376
- }
4377
-
4378
- function saveCustomTags(sessId, tags) {
4379
- if (tags.length === 0) _customTagsMap.delete(sessId);
4380
- else _customTagsMap.set(sessId, tags);
4381
- localStorage.setItem(_customTagsKey, JSON.stringify(Object.fromEntries(_customTagsMap)));
4382
- }
4383
-
4384
- function addCustomTag(sessId, tag, event) {
4385
- if (event) event.stopPropagation();
4386
- const t = tag.trim().toLowerCase().replace(/[^a-z0-9_-]/g, '');
4387
- if (!t) return;
4388
- const tags = getCustomTags(sessId);
4389
- if (!tags.includes(t)) { tags.push(t); saveCustomTags(sessId, tags); }
4390
- const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
4391
- if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
4392
- // rebuild tag filter bar in the DOM if it exists
4393
- initTags();
4394
- const tfBar = document.querySelector('.tag-filter-bar');
4395
- if (tfBar) tfBar.outerHTML = buildTagFilterBar(allSessions);
4396
- }
4397
-
4398
- function removeCustomTag(sessId, tag, event) {
4399
- if (event) event.stopPropagation();
4400
- const tags = getCustomTags(sessId).filter(t => t !== tag);
4401
- saveCustomTags(sessId, tags);
4402
- const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
4403
- if (wrap) wrap.outerHTML = renderCustomTagsInline(sessId, tags);
4404
- }
4405
-
4406
- function showCustomTagInput(sessId, event) {
4407
- if (event) event.stopPropagation();
4408
- const wrap = document.querySelector(`.sr-custom-tags[data-sess="${CSS.escape(sessId)}"]`);
4409
- if (!wrap) return;
4410
- const existing = wrap.querySelector('.ctag-input-wrap');
4411
- if (existing) { existing.remove(); return; }
4412
- const iw = document.createElement('div');
4413
- iw.className = 'ctag-input-wrap';
4414
- iw.onclick = e => e.stopPropagation();
4415
- iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
4416
- <button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
4417
- wrap.appendChild(iw);
4418
- const inp = iw.querySelector('input');
4419
- inp.focus();
4420
- inp.addEventListener('keydown', e => {
4421
- if (e.key === 'Enter') { addCustomTag(sessId, inp.value, e); inp.value = ''; }
4422
- if (e.key === 'Escape') iw.remove();
4423
- });
4424
- }
4425
-
4426
- function renderCustomTagsInline(sessId, tags) {
4427
- const tagHtml = tags.map(t =>
4428
- `<span class="sr-ctag">${esc(t)}<span class="ctag-del" onclick="removeCustomTag('${esc(sessId)}','${esc(t)}',event)" title="Remove tag">✕</span></span>`
4429
- ).join('');
4430
- return `<div class="sr-custom-tags" data-sess="${esc(sessId)}" onclick="event.stopPropagation()">
4431
- ${tagHtml}
4432
- <button class="ctag-add-btn" onclick="showCustomTagInput('${esc(sessId)}',event)" title="Add tag">+ tag</button>
4433
- </div>`;
4434
- }
4435
-
4436
- // ── feature 51: tool error drawer ─────────────────────────
4437
- async function toggleErrDrawer(sessId, event) {
4438
- if (event) event.stopPropagation();
4439
- const drawer = document.getElementById('err-drawer-' + sessId);
4440
- if (!drawer) return;
4441
- if (drawer.classList.contains('open')) { drawer.classList.remove('open'); drawer.innerHTML = ''; return; }
4442
- drawer.classList.add('open');
4443
- drawer.innerHTML = '<div class="err-drawer-head">Loading errors…</div>';
4444
- try {
4445
- const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
4446
- if (!data.errors?.length) {
4447
- drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
4448
- return;
4449
- }
4450
- const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
4451
- drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>
4452
- <div class="err-drawer-body">${items}</div>`;
4453
- } catch (err) {
4454
- drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
4455
- }
4456
- }
4457
-
4458
- function closeErrDrawer(sessId) {
4459
- const drawer = document.getElementById('err-drawer-' + sessId);
4460
- if (!drawer) return;
4461
- drawer.classList.remove('open');
4462
- drawer.innerHTML = '';
4463
- }
4464
-
4465
- // ── feature 52: prompt copy button ────────────────────────
4466
- function copyPrompt(text, event) {
4467
- if (event) event.stopPropagation();
4468
- const btn = event?.currentTarget || event?.target;
4469
- navigator.clipboard.writeText(text).then(() => {
4470
- if (btn) { btn.textContent = '✓'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = '⎘'; btn.classList.remove('copied'); }, 1500); }
4471
- }).catch(() => {});
4472
- }
4473
-
4474
- // ── feature 53: session cost histogram ────────────────────
4475
- function buildCostHistogram() {
4476
- const el = document.getElementById('cost-histogram-panel');
4477
- if (!el) return;
4478
- const costs = allSessions.map(s => s.totalCost || 0).filter(c => c > 0);
4479
- if (costs.length < 2) { el.style.display = 'none'; return; }
4480
- el.style.display = 'block';
4481
- const minC = Math.min(...costs); const maxC = Math.max(...costs);
4482
- const BUCKETS = 10;
4483
- const range = maxC - minC || 0.01;
4484
- const bucketSize = range / BUCKETS;
4485
- const counts = new Array(BUCKETS).fill(0);
4486
- for (const c of costs) { const i = Math.min(BUCKETS - 1, Math.floor((c - minC) / bucketSize)); counts[i]++; }
4487
- const maxCount = Math.max(1, ...counts);
4488
- const fmt = v => v < 0.01 ? v.toFixed(4) : v < 1 ? '$' + v.toFixed(2) : '$' + v.toFixed(1);
4489
- const bars = counts.map((n, i) => {
4490
- const h = Math.max(2, Math.round((n / maxCount) * 46));
4491
- const lo = minC + i * bucketSize; const hi = lo + bucketSize;
4492
- return `<div class="ch-bar-wrap" title="${fmt(lo)}–${fmt(hi)}: ${n} session${n !== 1 ? 's' : ''}">
4493
- <div class="ch-cnt">${n || ''}</div>
4494
- <div class="ch-bar" style="height:${h}px"></div>
4495
- <div class="ch-lbl">${i === 0 ? fmt(lo) : i === BUCKETS - 1 ? fmt(hi) : ''}</div>
4496
- </div>`;
4497
- }).join('');
4498
- el.innerHTML = `<div class="ch-title">Cost Distribution — ${costs.length} sessions</div>
4499
- <div class="ch-bars">${bars}</div>`;
4500
- }
4501
-
4502
- // ── feature 54: persistent filter URL params ──────────────
4503
- function syncURLParams() {
4504
- const p = new URLSearchParams(window.location.search);
4505
- if (DIR) p.set('proj', DIR); else p.delete('proj');
4506
- if (activePeriod && activePeriod !== 'day') p.set('period', activePeriod); else p.delete('period');
4507
- if (activeTagFilter) p.set('tag', activeTagFilter); else p.delete('tag');
4508
- if (heatmapDateFilter) p.set('date', heatmapDateFilter); else p.delete('date');
4509
- if (showStarredOnly) p.set('starred', '1'); else p.delete('starred');
4510
- if (filePivot) p.set('file', filePivot); else p.delete('file');
4511
- const newUrl = window.location.pathname + (p.toString() ? '?' + p.toString() : '');
4512
- history.replaceState(null, '', newUrl);
4513
- }
4514
-
4515
- function restoreURLParams() {
4516
- const p = new URLSearchParams(window.location.search);
4517
- const period = p.get('period');
4518
- if (period && ['day','week','month','all'].includes(period)) {
4519
- activePeriod = period;
4520
- document.querySelectorAll('.period-btn').forEach(b => b.classList.toggle('active', b.dataset.period === period));
4521
- }
4522
- const tag = p.get('tag');
4523
- if (tag) activeTagFilter = tag;
4524
- const date = p.get('date');
4525
- if (date) {
4526
- heatmapDateFilter = date;
4527
- const clearBtn = document.getElementById('shm-clear');
4528
- if (clearBtn) clearBtn.classList.add('show');
4529
- }
4530
- const starred = p.get('starred');
4531
- if (starred === '1') {
4532
- showStarredOnly = true;
4533
- const btn = document.getElementById('sess-star-filter');
4534
- if (btn) btn.classList.add('on');
4535
- }
4536
- const file = p.get('file');
4537
- if (file) {
4538
- filePivot = file;
4539
- const bar = document.getElementById('file-pivot-bar');
4540
- const lbl = document.getElementById('fpb-label');
4541
- if (bar) bar.classList.add('show');
4542
- if (lbl) lbl.textContent = `Showing sessions that touched: ${file}`;
4543
- }
4544
- }
4545
-
4546
- // ── helpers ────────────────────────────────────────────────
4547
- function enc(s) { return encodeURIComponent(s); }
4548
- function esc(s) {
4549
- return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
4550
- }
4551
-
4552
- function relTime(ts) {
4553
- if (!ts) return '';
4554
- const diff = Date.now() - (typeof ts === 'number' ? ts : new Date(ts).getTime());
4555
- const s = Math.floor(diff / 1000);
4556
- if (s < 5) return 'now';
4557
- if (s < 60) return s + 's';
4558
- const m = Math.floor(s / 60);
4559
- if (m < 60) return m + 'm';
4560
- const h = Math.floor(m / 60);
4561
- if (h < 24) return h + 'h';
4562
- return Math.floor(h / 24) + 'd';
4563
- }
4564
-
4565
- function fmtDur(ms) {
4566
- const s = Math.floor(ms / 1000);
4567
- if (s < 60) return s + 's';
4568
- const m = Math.floor(s / 60);
4569
- if (m < 60) return m + 'm';
4570
- return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
4571
- }
4572
-
4573
- init();
4574
- </script>
4575
- </body>
4576
- </html>