@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.
- package/.claude/agents/optimization/benchmark-suite.md +2 -0
- package/.claude/agents/optimization/load-balancer.md +2 -0
- package/.claude/agents/optimization/performance-monitor.md +2 -0
- package/.claude/agents/optimization/resource-allocator.md +3 -1
- package/.claude/agents/optimization/topology-optimizer.md +2 -0
- package/.claude/commands/mastermind/_repeat.md +21 -0
- package/.claude/commands/mastermind/_taskfile.md +235 -0
- package/.claude/commands/mastermind/adr.md +11 -0
- package/.claude/commands/mastermind/approve.md +94 -0
- package/.claude/commands/mastermind/autodev.md +32 -0
- package/.claude/commands/mastermind/budget.md +7 -0
- package/.claude/commands/mastermind/code-review.md +317 -0
- package/.claude/commands/mastermind/createorg.md +40 -1
- package/.claude/commands/mastermind/createtask.md +383 -0
- package/.claude/commands/mastermind/debug.md +22 -0
- package/.claude/commands/mastermind/design.md +20 -0
- package/.claude/commands/mastermind/do.md +526 -0
- package/.claude/commands/mastermind/execute.md +20 -0
- package/.claude/commands/mastermind/finish.md +20 -0
- package/.claude/commands/mastermind/graph-status.md +7 -0
- package/.claude/commands/mastermind/help.md +118 -0
- package/.claude/commands/mastermind/ideate.md +261 -0
- package/.claude/commands/mastermind/improve.md +345 -0
- package/.claude/commands/mastermind/loops.md +7 -0
- package/.claude/commands/mastermind/master.md +186 -6
- package/.claude/commands/mastermind/memory.md +230 -0
- package/.claude/commands/mastermind/plan.md +26 -0
- package/.claude/commands/mastermind/receive-review.md +20 -0
- package/.claude/commands/mastermind/repeat.md +257 -0
- package/.claude/commands/mastermind/runorg.md +3 -0
- package/.claude/commands/mastermind/skill-builder.md +20 -0
- package/.claude/commands/mastermind/specialagents.md +125 -0
- package/.claude/commands/mastermind/swarm.md +161 -0
- package/.claude/commands/mastermind/taskdev.md +26 -0
- package/.claude/commands/mastermind/tdd.md +22 -0
- package/.claude/commands/mastermind/techport.md +4 -0
- package/.claude/commands/mastermind/understand.md +139 -0
- package/.claude/commands/mastermind/verify.md +22 -0
- package/.claude/commands/mastermind/worktree.md +20 -0
- package/.claude/commands/monomind/do.md +52 -0
- package/.claude/commands/monomind/improve.md +2 -0
- package/.claude/helpers/handlers/graph-status-handler.cjs +2 -1
- package/.claude/helpers/handlers/route-handler.cjs +61 -11
- package/.claude/helpers/hook-handler.cjs +19 -0
- package/.claude/helpers/skill-registry.json +122 -51
- package/.claude/helpers/statusline.cjs +1 -1
- package/.claude/skills/agent-browser-testing/SKILL.md +522 -152
- package/.claude/skills/github-issue-triage/SKILL.md +354 -0
- package/.claude/skills/github-repo-recap/SKILL.md +207 -0
- package/.claude/skills/mastermind/_delegation.md +83 -0
- package/.claude/skills/mastermind/_protocol.md +14 -0
- package/.claude/skills/mastermind/approve.md +15 -7
- package/.claude/skills/mastermind/autodev.md +534 -0
- package/.claude/skills/mastermind/createorg.md +21 -5
- package/.claude/skills/mastermind/debug.md +232 -0
- package/.claude/skills/mastermind/design.md +187 -0
- package/.claude/skills/mastermind/execute.md +104 -0
- package/.claude/skills/mastermind/finish.md +251 -0
- package/.claude/skills/mastermind/plan.md +180 -0
- package/.claude/skills/mastermind/receive-review.md +213 -0
- package/.claude/skills/mastermind/runorg.md +23 -8
- package/.claude/skills/mastermind/skill-builder.md +274 -0
- package/.claude/skills/mastermind/taskdev.md +307 -0
- package/.claude/skills/mastermind/tdd.md +394 -0
- package/.claude/skills/mastermind/verify.md +196 -0
- package/.claude/skills/mastermind/worktree.md +160 -132
- package/README.md +320 -253
- package/dist/src/commands/analyze.d.ts.map +1 -1
- package/dist/src/commands/analyze.js +9 -2
- package/dist/src/commands/analyze.js.map +1 -1
- package/dist/src/commands/benchmark.js.map +1 -1
- package/dist/src/commands/completions.js +1 -1
- package/dist/src/commands/guidance.js +7 -7
- package/dist/src/commands/hooks.d.ts.map +1 -1
- package/dist/src/commands/hooks.js +16 -3
- package/dist/src/commands/hooks.js.map +1 -1
- package/dist/src/commands/index.d.ts +3 -2
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +7 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +47 -13
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/neural.d.ts.map +1 -1
- package/dist/src/commands/neural.js +100 -14
- package/dist/src/commands/neural.js.map +1 -1
- package/dist/src/commands/platforms.d.ts +11 -0
- package/dist/src/commands/platforms.d.ts.map +1 -0
- package/dist/src/commands/platforms.js +195 -0
- package/dist/src/commands/platforms.js.map +1 -0
- package/dist/src/commands/ruvector/backup.js.map +1 -1
- package/dist/src/commands/ruvector/benchmark.js.map +1 -1
- package/dist/src/commands/ruvector/init.js.map +1 -1
- package/dist/src/commands/ruvector/migrate.js.map +1 -1
- package/dist/src/commands/ruvector/optimize.js.map +1 -1
- package/dist/src/commands/ruvector/status.js.map +1 -1
- package/dist/src/commands/update.js +6 -6
- package/dist/src/init/executor.d.ts.map +1 -1
- package/dist/src/init/executor.js +28 -0
- package/dist/src/init/executor.js.map +1 -1
- package/dist/src/init/statusline-generator.js +1 -1
- package/dist/src/init/types.d.ts +1 -0
- package/dist/src/init/types.d.ts.map +1 -1
- package/dist/src/mcp-server.d.ts.map +1 -1
- package/dist/src/mcp-server.js +92 -0
- package/dist/src/mcp-server.js.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hive-mind-tools.js +52 -0
- package/dist/src/mcp-tools/hive-mind-tools.js.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +106 -5
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/index.d.ts +0 -5
- package/dist/src/mcp-tools/index.d.ts.map +1 -1
- package/dist/src/mcp-tools/index.js +0 -5
- package/dist/src/mcp-tools/index.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +507 -5587
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/neural-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/neural-tools.js +64 -4
- package/dist/src/mcp-tools/neural-tools.js.map +1 -1
- package/dist/src/mcp-tools/security-tools.js +4 -4
- package/dist/src/memory/intelligence.d.ts +2 -2
- package/dist/src/memory/intelligence.d.ts.map +1 -1
- package/dist/src/memory/intelligence.js +108 -3
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.js +1 -1
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/sona-optimizer.d.ts +1 -10
- package/dist/src/memory/sona-optimizer.d.ts.map +1 -1
- package/dist/src/memory/sona-optimizer.js +0 -46
- package/dist/src/memory/sona-optimizer.js.map +1 -1
- package/dist/src/runtime/headless.js +3 -3
- package/dist/src/ruvector/diff-classifier.d.ts +0 -2
- package/dist/src/ruvector/diff-classifier.d.ts.map +1 -1
- package/dist/src/ruvector/diff-classifier.js +2 -14
- package/dist/src/ruvector/diff-classifier.js.map +1 -1
- package/dist/src/ruvector/index.d.ts +26 -9
- package/dist/src/ruvector/index.d.ts.map +1 -1
- package/dist/src/ruvector/index.js +3 -21
- package/dist/src/ruvector/index.js.map +1 -1
- package/dist/src/ruvector/ruvllm-wasm.js +2 -2
- package/dist/src/ruvector/ruvllm-wasm.js.map +1 -1
- package/dist/src/types.d.ts +0 -15
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +0 -18
- package/dist/src/types.js.map +1 -1
- package/dist/src/ui/dashboard.html +8763 -9765
- package/dist/src/ui/data/agent-avatars.html +763 -0
- package/dist/src/ui/data/agent-avatars.json +966 -0
- package/dist/src/ui/data/avatars/account-strategist.svg +58 -0
- package/dist/src/ui/data/avatars/accounts-payable.svg +54 -0
- package/dist/src/ui/data/avatars/adaptive-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/adaptive-coordinator2.svg +54 -0
- package/dist/src/ui/data/avatars/ai-citation.svg +57 -0
- package/dist/src/ui/data/avatars/ai-engineer.svg +61 -0
- package/dist/src/ui/data/avatars/analytics-reporter.svg +53 -0
- package/dist/src/ui/data/avatars/api-tester.svg +53 -0
- package/dist/src/ui/data/avatars/architecture.svg +54 -0
- package/dist/src/ui/data/avatars/automation-governance.svg +55 -0
- package/dist/src/ui/data/avatars/backend-dev.svg +53 -0
- package/dist/src/ui/data/avatars/benchmarker.svg +54 -0
- package/dist/src/ui/data/avatars/blockchain-auditor.svg +53 -0
- package/dist/src/ui/data/avatars/byzantine-coord.svg +57 -0
- package/dist/src/ui/data/avatars/case-analyst.svg +57 -0
- package/dist/src/ui/data/avatars/cicd-engineer.svg +55 -0
- package/dist/src/ui/data/avatars/cloud-architect.svg +54 -0
- package/dist/src/ui/data/avatars/code-review-swarm.svg +57 -0
- package/dist/src/ui/data/avatars/coder-v119.svg +57 -0
- package/dist/src/ui/data/avatars/coder.svg +58 -0
- package/dist/src/ui/data/avatars/collective-coord.svg +54 -0
- package/dist/src/ui/data/avatars/compliance-auditor.svg +58 -0
- package/dist/src/ui/data/avatars/consensus-coordinator.svg +54 -0
- package/dist/src/ui/data/avatars/content-creator.svg +54 -0
- package/dist/src/ui/data/avatars/crdt-synchronizer.svg +53 -0
- package/dist/src/ui/data/avatars/cro-specialist.svg +58 -0
- package/dist/src/ui/data/avatars/data-consolidator.svg +54 -0
- package/dist/src/ui/data/avatars/data-engineer.svg +53 -0
- package/dist/src/ui/data/avatars/database-optimizer.svg +61 -0
- package/dist/src/ui/data/avatars/deal-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/defender.svg +53 -0
- package/dist/src/ui/data/avatars/devops-automator.svg +56 -0
- package/dist/src/ui/data/avatars/discovery-coach.svg +54 -0
- package/dist/src/ui/data/avatars/email-marketing.svg +57 -0
- package/dist/src/ui/data/avatars/embedded-firmware.svg +61 -0
- package/dist/src/ui/data/avatars/evidence-collector.svg +57 -0
- package/dist/src/ui/data/avatars/experiment-tracker.svg +53 -0
- package/dist/src/ui/data/avatars/feedback-synthesizer.svg +54 -0
- package/dist/src/ui/data/avatars/finance-tracker.svg +54 -0
- package/dist/src/ui/data/avatars/frontend-developer.svg +54 -0
- package/dist/src/ui/data/avatars/game-audio-engineer.svg +59 -0
- package/dist/src/ui/data/avatars/game-designer.svg +54 -0
- package/dist/src/ui/data/avatars/gossip-coordinator.svg +54 -0
- package/dist/src/ui/data/avatars/hierarchical-coord.svg +54 -0
- package/dist/src/ui/data/avatars/incident-commander.svg +57 -0
- package/dist/src/ui/data/avatars/infrastructure.svg +54 -0
- package/dist/src/ui/data/avatars/input-validator.svg +53 -0
- package/dist/src/ui/data/avatars/ios-developer.svg +54 -0
- package/dist/src/ui/data/avatars/issue-tracker.svg +53 -0
- package/dist/src/ui/data/avatars/judge.svg +55 -0
- package/dist/src/ui/data/avatars/launch-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/legal-compliance.svg +53 -0
- package/dist/src/ui/data/avatars/level-designer.svg +53 -0
- package/dist/src/ui/data/avatars/load-balancer.svg +57 -0
- package/dist/src/ui/data/avatars/mcp-builder.svg +53 -0
- package/dist/src/ui/data/avatars/memory-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/mesh-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/ml-developer.svg +58 -0
- package/dist/src/ui/data/avatars/mobile-app-builder.svg +53 -0
- package/dist/src/ui/data/avatars/mobile-dev.svg +54 -0
- package/dist/src/ui/data/avatars/model-qa.svg +58 -0
- package/dist/src/ui/data/avatars/narrative-designer.svg +58 -0
- package/dist/src/ui/data/avatars/outbound-strategist.svg +55 -0
- package/dist/src/ui/data/avatars/path-validator.svg +54 -0
- package/dist/src/ui/data/avatars/payment-agent.svg +53 -0
- package/dist/src/ui/data/avatars/perf-analyzer.svg +58 -0
- package/dist/src/ui/data/avatars/pipeline-analyst.svg +54 -0
- package/dist/src/ui/data/avatars/planner.svg +55 -0
- package/dist/src/ui/data/avatars/pr-manager.svg +54 -0
- package/dist/src/ui/data/avatars/pricing-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/product-manager.svg +54 -0
- package/dist/src/ui/data/avatars/production-validator.svg +54 -0
- package/dist/src/ui/data/avatars/project-shepherd.svg +54 -0
- package/dist/src/ui/data/avatars/proposal-strategist.svg +54 -0
- package/dist/src/ui/data/avatars/prosecutor.svg +57 -0
- package/dist/src/ui/data/avatars/pseudocode.svg +53 -0
- package/dist/src/ui/data/avatars/queen-coordinator.svg +55 -0
- package/dist/src/ui/data/avatars/quorum-manager.svg +53 -0
- package/dist/src/ui/data/avatars/raft-manager.svg +53 -0
- package/dist/src/ui/data/avatars/reality-checker.svg +58 -0
- package/dist/src/ui/data/avatars/recruitment.svg +58 -0
- package/dist/src/ui/data/avatars/refinement.svg +53 -0
- package/dist/src/ui/data/avatars/release-manager.svg +54 -0
- package/dist/src/ui/data/avatars/repo-architect.svg +54 -0
- package/dist/src/ui/data/avatars/researcher.svg +58 -0
- package/dist/src/ui/data/avatars/resource-allocator.svg +53 -0
- package/dist/src/ui/data/avatars/reviewer.svg +53 -0
- package/dist/src/ui/data/avatars/safe-executor.svg +53 -0
- package/dist/src/ui/data/avatars/sales-coach.svg +53 -0
- package/dist/src/ui/data/avatars/sales-engineer.svg +58 -0
- package/dist/src/ui/data/avatars/scout-explorer.svg +58 -0
- package/dist/src/ui/data/avatars/security-architect.svg +54 -0
- package/dist/src/ui/data/avatars/security-auditor.svg +55 -0
- package/dist/src/ui/data/avatars/senior-developer.svg +58 -0
- package/dist/src/ui/data/avatars/senior-pm.svg +58 -0
- package/dist/src/ui/data/avatars/seo-specialist.svg +57 -0
- package/dist/src/ui/data/avatars/social-media.svg +54 -0
- package/dist/src/ui/data/avatars/solidity-engineer.svg +58 -0
- package/dist/src/ui/data/avatars/sparc-coder.svg +58 -0
- package/dist/src/ui/data/avatars/sparc-coord.svg +56 -0
- package/dist/src/ui/data/avatars/specification.svg +57 -0
- package/dist/src/ui/data/avatars/sprint-prioritizer.svg +53 -0
- package/dist/src/ui/data/avatars/sre.svg +54 -0
- package/dist/src/ui/data/avatars/studio-operations.svg +53 -0
- package/dist/src/ui/data/avatars/studio-producer.svg +55 -0
- package/dist/src/ui/data/avatars/support-responder.svg +56 -0
- package/dist/src/ui/data/avatars/system-architect.svg +54 -0
- package/dist/src/ui/data/avatars/task-orchestrator.svg +56 -0
- package/dist/src/ui/data/avatars/technical-artist.svg +53 -0
- package/dist/src/ui/data/avatars/technical-writer.svg +59 -0
- package/dist/src/ui/data/avatars/tester.svg +53 -0
- package/dist/src/ui/data/avatars/threat-detection.svg +61 -0
- package/dist/src/ui/data/avatars/trend-researcher.svg +54 -0
- package/dist/src/ui/data/avatars/trial-director.svg +55 -0
- package/dist/src/ui/data/avatars/unity-architect.svg +54 -0
- package/dist/src/ui/data/avatars/visionos-engineer.svg +57 -0
- package/dist/src/ui/data/avatars/worker-specialist.svg +55 -0
- package/dist/src/ui/data/avatars/workflow-architect.svg +57 -0
- package/dist/src/ui/data/avatars/workflow-automation.svg +54 -0
- package/dist/src/ui/data/avatars/zk-steward.svg +54 -0
- package/dist/src/ui/data/known-projects.json +1 -1
- package/dist/src/ui/data/mastermind-events.jsonl +28 -0
- package/dist/src/ui/orgs.html +1171 -0
- package/dist/src/ui/server.mjs +529 -43
- package/dist/src/update/index.js +1 -1
- package/dist/src/update/validator.js +8 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/.claude/agents/academic/academic-anthropologist.md +0 -126
- package/.claude/agents/academic/academic-geographer.md +0 -128
- package/.claude/agents/academic/academic-historian.md +0 -124
- package/.claude/agents/academic/academic-narratologist.md +0 -119
- package/.claude/agents/academic/academic-psychologist.md +0 -119
- package/.claude/agents/analysis/analyze-code-quality.md +0 -58
- package/.claude/agents/analysis/code-analyzer.md +0 -189
- package/.claude/agents/analysis/code-review/analyze-code-quality.md +0 -58
- package/.claude/agents/consensus/performance-benchmarker.md +0 -831
- package/.claude/agents/data/ml/data-ml-model.md +0 -76
- package/.claude/agents/development/dev-backend-api.md +0 -178
- package/.claude/agents/devops/ci-cd/ops-cicd-github.md +0 -52
- package/.claude/agents/documentation/api-docs/docs-api-openapi.md +0 -63
- package/.claude/agents/game-development/blender/blender-addon-engineer.md +0 -235
- package/.claude/agents/game-development/game-audio-engineer.md +0 -265
- package/.claude/agents/game-development/game-designer.md +0 -168
- package/.claude/agents/game-development/godot/godot-gameplay-scripter.md +0 -335
- package/.claude/agents/game-development/godot/godot-multiplayer-engineer.md +0 -298
- package/.claude/agents/game-development/godot/godot-shader-developer.md +0 -267
- package/.claude/agents/game-development/level-designer.md +0 -209
- package/.claude/agents/game-development/narrative-designer.md +0 -244
- package/.claude/agents/game-development/roblox-studio/roblox-avatar-creator.md +0 -298
- package/.claude/agents/game-development/roblox-studio/roblox-experience-designer.md +0 -306
- package/.claude/agents/game-development/roblox-studio/roblox-systems-scripter.md +0 -326
- package/.claude/agents/game-development/technical-artist.md +0 -230
- package/.claude/agents/game-development/unity/unity-architect.md +0 -272
- package/.claude/agents/game-development/unity/unity-editor-tool-developer.md +0 -311
- package/.claude/agents/game-development/unity/unity-multiplayer-engineer.md +0 -322
- package/.claude/agents/game-development/unity/unity-shader-graph-artist.md +0 -270
- package/.claude/agents/game-development/unreal-engine/unreal-multiplayer-architect.md +0 -314
- package/.claude/agents/game-development/unreal-engine/unreal-systems-engineer.md +0 -311
- package/.claude/agents/game-development/unreal-engine/unreal-technical-artist.md +0 -257
- package/.claude/agents/game-development/unreal-engine/unreal-world-builder.md +0 -274
- package/.claude/agents/github/release-swarm.md +0 -597
- package/.claude/agents/goal/agent.md +0 -804
- package/.claude/agents/goal/code-goal-planner.md +0 -445
- package/.claude/agents/marketing/marketing-ai-citation-strategist.md +0 -171
- package/.claude/agents/marketing/marketing-app-store-optimizer.md +0 -322
- package/.claude/agents/marketing/marketing-baidu-seo-specialist.md +0 -227
- package/.claude/agents/marketing/marketing-bilibili-content-strategist.md +0 -200
- package/.claude/agents/marketing/marketing-book-co-author.md +0 -111
- package/.claude/agents/marketing/marketing-carousel-growth-engine.md +0 -200
- package/.claude/agents/marketing/marketing-china-ecommerce-operator.md +0 -284
- package/.claude/agents/marketing/marketing-content-creator.md +0 -67
- package/.claude/agents/marketing/marketing-cross-border-ecommerce.md +0 -260
- package/.claude/agents/marketing/marketing-douyin-strategist.md +0 -150
- package/.claude/agents/marketing/marketing-growth-hacker.md +0 -54
- package/.claude/agents/marketing/marketing-instagram-curator.md +0 -114
- package/.claude/agents/marketing/marketing-kuaishou-strategist.md +0 -224
- package/.claude/agents/marketing/marketing-linkedin-content-creator.md +0 -215
- package/.claude/agents/marketing/marketing-livestream-commerce-coach.md +0 -306
- package/.claude/agents/marketing/marketing-podcast-strategist.md +0 -278
- package/.claude/agents/marketing/marketing-private-domain-operator.md +0 -309
- package/.claude/agents/marketing/marketing-reddit-community-builder.md +0 -124
- package/.claude/agents/marketing/marketing-seo-specialist.md +0 -279
- package/.claude/agents/marketing/marketing-short-video-editing-coach.md +0 -413
- package/.claude/agents/marketing/marketing-social-media-strategist.md +0 -125
- package/.claude/agents/marketing/marketing-tiktok-strategist.md +0 -126
- package/.claude/agents/marketing/marketing-twitter-engager.md +0 -127
- package/.claude/agents/marketing/marketing-wechat-official-account.md +0 -146
- package/.claude/agents/marketing/marketing-weibo-strategist.md +0 -241
- package/.claude/agents/marketing/marketing-xiaohongshu-specialist.md +0 -139
- package/.claude/agents/marketing/marketing-zhihu-strategist.md +0 -163
- package/.claude/agents/neural/safla-neural.md +0 -74
- package/.claude/agents/paid-media/paid-media-auditor.md +0 -71
- package/.claude/agents/paid-media/paid-media-creative-strategist.md +0 -71
- package/.claude/agents/paid-media/paid-media-paid-social-strategist.md +0 -71
- package/.claude/agents/paid-media/paid-media-ppc-strategist.md +0 -71
- package/.claude/agents/paid-media/paid-media-programmatic-buyer.md +0 -71
- package/.claude/agents/paid-media/paid-media-search-query-analyst.md +0 -71
- package/.claude/agents/paid-media/paid-media-tracking-specialist.md +0 -71
- package/.claude/agents/payments/agentic-payments.md +0 -126
- package/.claude/agents/product/product-behavioral-nudge-engine.md +0 -81
- package/.claude/agents/product/product-feedback-synthesizer.md +0 -119
- package/.claude/agents/product/product-manager.md +0 -469
- package/.claude/agents/product/product-sprint-prioritizer.md +0 -154
- package/.claude/agents/product/product-trend-researcher.md +0 -159
- package/.claude/agents/project-management/project-management-experiment-tracker.md +0 -199
- package/.claude/agents/project-management/project-management-jira-workflow-steward.md +0 -231
- package/.claude/agents/project-management/project-management-project-shepherd.md +0 -195
- package/.claude/agents/project-management/project-management-studio-operations.md +0 -201
- package/.claude/agents/project-management/project-management-studio-producer.md +0 -204
- package/.claude/agents/project-management/project-manager-senior.md +0 -136
- package/.claude/agents/reasoning/agent.md +0 -804
- package/.claude/agents/reasoning/goal-planner.md +0 -73
- package/.claude/agents/sales/sales-account-strategist.md +0 -228
- package/.claude/agents/sales/sales-coach.md +0 -272
- package/.claude/agents/sales/sales-deal-strategist.md +0 -181
- package/.claude/agents/sales/sales-discovery-coach.md +0 -226
- package/.claude/agents/sales/sales-engineer.md +0 -183
- package/.claude/agents/sales/sales-outbound-strategist.md +0 -202
- package/.claude/agents/sales/sales-pipeline-analyst.md +0 -268
- package/.claude/agents/sales/sales-proposal-strategist.md +0 -218
- package/.claude/agents/sona/sona-learning-optimizer.md +0 -65
- package/.claude/agents/spatial-computing/macos-spatial-metal-engineer.md +0 -338
- package/.claude/agents/spatial-computing/terminal-integration-specialist.md +0 -71
- package/.claude/agents/spatial-computing/visionos-spatial-engineer.md +0 -55
- package/.claude/agents/specialists/memory-specialist.md +0 -298
- package/.claude/agents/specialists/performance-engineer.md +0 -387
- package/.claude/agents/specialists/queen-coordinator.md +0 -67
- package/.claude/agents/specialists/security-architect.md +0 -154
- package/.claude/agents/specialized/accounts-payable-agent.md +0 -186
- package/.claude/agents/specialized/corporate-training-designer.md +0 -193
- package/.claude/agents/specialized/data-consolidation-agent.md +0 -61
- package/.claude/agents/specialized/government-digital-presales-consultant.md +0 -364
- package/.claude/agents/specialized/healthcare-marketing-compliance.md +0 -396
- package/.claude/agents/specialized/recruitment-specialist.md +0 -510
- package/.claude/agents/specialized/report-distribution-agent.md +0 -66
- package/.claude/agents/specialized/sales-data-extraction-agent.md +0 -68
- package/.claude/agents/specialized/specialized-french-consulting-market.md +0 -193
- package/.claude/agents/specialized/specialized-korean-business-navigator.md +0 -217
- package/.claude/agents/specialized/specialized-salesforce-architect.md +0 -181
- package/.claude/agents/specialized/study-abroad-advisor.md +0 -283
- package/.claude/agents/specialized/supply-chain-strategist.md +0 -583
- package/.claude/agents/sublinear/consensus-coordinator.md +0 -333
- package/.claude/agents/sublinear/matrix-optimizer.md +0 -180
- package/.claude/agents/sublinear/pagerank-analyzer.md +0 -295
- package/.claude/agents/sublinear/performance-optimizer.md +0 -363
- package/.claude/agents/sublinear/trading-predictor.md +0 -242
- package/.claude/agents/support/support-analytics-reporter.md +0 -366
- package/.claude/agents/support/support-executive-summary-generator.md +0 -213
- package/.claude/agents/support/support-finance-tracker.md +0 -443
- package/.claude/agents/support/support-infrastructure-maintainer.md +0 -619
- package/.claude/agents/support/support-legal-compliance-checker.md +0 -589
- package/.claude/agents/support/support-support-responder.md +0 -586
- package/.claude/agents/swarm/adaptive-coordinator.md +0 -364
- package/.claude/agents/swarm/hierarchical-coordinator.md +0 -318
- package/.claude/agents/templates/github-pr-manager.md +0 -155
- package/.claude/agents/templates/memory-coordinator.md +0 -163
- package/.claude/agents/templates/migration-plan.md +0 -724
- package/.claude/agents/templates/orchestrator-task.md +0 -120
- package/.claude/agents/templates/performance-analyzer.md +0 -179
- package/.claude/agents/templates/sparc-coordinator.md +0 -163
- package/.claude/agents/testing/testing-reality-checker.md +0 -237
- package/.claude/commands/analysis/token-efficiency.md +0 -42
- package/.claude/commands/optimization/README.md +0 -73
- package/.claude/commands/optimization/parallel-execution.md +0 -76
- package/.claude/commands/swarm/swarm-analysis.md +0 -62
- package/.claude/commands/swarm/swarm-background.md +0 -65
- package/.claude/commands/swarm/swarm-modes.md +0 -67
- package/.claude/commands/swarm/swarm-monitor.md +0 -54
- package/.claude/commands/swarm/swarm-status.md +0 -44
- package/.claude/commands/swarm/swarm-strategies.md +0 -76
- package/.claude/commands/training/model-update.md +0 -78
- package/.claude/commands/training/pattern-learn.md +0 -69
- package/.claude/commands/training/specialization.md +0 -92
- package/.claude/commands/verify/check.md +0 -106
- package/.claude/commands/verify/start.md +0 -105
- package/.claude/helpers/README.md +0 -105
- package/.claude/helpers/context-persistence-hook.mjs +0 -1988
- package/.claude/helpers/intelligence.cjs +0 -247
- package/.claude/helpers/learning-service.mjs +0 -1302
- package/.claude/helpers/memory-palace.cjs +0 -461
- package/.claude/helpers/memory.cjs +0 -84
- package/.claude/helpers/metrics-db.mjs +0 -488
- package/.claude/helpers/router.cjs +0 -559
- package/.claude/helpers/session.cjs +0 -126
- package/.claude/helpers/swarm-hooks.sh +0 -761
- package/.claude/helpers/toggle-statusline.cjs +0 -58
- package/.claude/helpers/token-tracker.cjs +0 -934
- package/.claude/skills/agentdb-advanced/SKILL.md +0 -549
- package/.claude/skills/agentdb-learning/SKILL.md +0 -544
- package/.claude/skills/agentdb-memory-patterns/SKILL.md +0 -337
- package/.claude/skills/agentdb-optimization/SKILL.md +0 -508
- package/.claude/skills/agentdb-vector-search/SKILL.md +0 -335
- package/.claude/skills/agentic-integration/SKILL.md +0 -265
- package/.claude/skills/cli-modernization/SKILL.md +0 -950
- package/.claude/skills/core-implementation/SKILL.md +0 -892
- package/.claude/skills/ddd-architecture/SKILL.md +0 -444
- package/.claude/skills/github-code-review/SKILL.md +0 -1147
- package/.claude/skills/github-multi-repo/SKILL.md +0 -912
- package/.claude/skills/github-project-management/SKILL.md +0 -1245
- package/.claude/skills/github-release-management/SKILL.md +0 -1118
- package/.claude/skills/github-workflow-automation/SKILL.md +0 -1107
- package/.claude/skills/mcp-optimization/SKILL.md +0 -837
- package/.claude/skills/memory-unification/SKILL.md +0 -196
- package/.claude/skills/performance-optimization/SKILL.md +0 -416
- package/.claude/skills/reasoningbank-agentdb/SKILL.md +0 -444
- package/.claude/skills/reasoningbank-intelligence/SKILL.md +0 -199
- package/.claude/skills/security-hardening/SKILL.md +0 -101
- package/.claude/skills/stream-chain/SKILL.md +0 -560
- package/.claude/skills/swarm-coordination/SKILL.md +0 -451
- package/bundled-graph/dist/src/analyze.d.ts +0 -32
- package/bundled-graph/dist/src/analyze.d.ts.map +0 -1
- package/bundled-graph/dist/src/analyze.js +0 -297
- package/bundled-graph/dist/src/analyze.js.map +0 -1
- package/bundled-graph/dist/src/build.d.ts +0 -8
- package/bundled-graph/dist/src/build.d.ts.map +0 -1
- package/bundled-graph/dist/src/build.js.map +0 -1
- package/bundled-graph/dist/src/cache.d.ts +0 -12
- package/bundled-graph/dist/src/cache.d.ts.map +0 -1
- package/bundled-graph/dist/src/cache.js +0 -43
- package/bundled-graph/dist/src/cache.js.map +0 -1
- package/bundled-graph/dist/src/cluster.d.ts +0 -5
- package/bundled-graph/dist/src/cluster.d.ts.map +0 -1
- package/bundled-graph/dist/src/cluster.js.map +0 -1
- package/bundled-graph/dist/src/detect.d.ts +0 -21
- package/bundled-graph/dist/src/detect.d.ts.map +0 -1
- package/bundled-graph/dist/src/detect.js +0 -195
- package/bundled-graph/dist/src/detect.js.map +0 -1
- package/bundled-graph/dist/src/export.d.ts +0 -21
- package/bundled-graph/dist/src/export.d.ts.map +0 -1
- package/bundled-graph/dist/src/export.js +0 -68
- package/bundled-graph/dist/src/export.js.map +0 -1
- package/bundled-graph/dist/src/extract/index.d.ts +0 -20
- package/bundled-graph/dist/src/extract/index.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/index.js +0 -158
- package/bundled-graph/dist/src/extract/index.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/c.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/c.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/c.js +0 -88
- package/bundled-graph/dist/src/extract/languages/c.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/cpp.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/cpp.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/cpp.js +0 -121
- package/bundled-graph/dist/src/extract/languages/cpp.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/csharp.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/csharp.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/csharp.js +0 -121
- package/bundled-graph/dist/src/extract/languages/csharp.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/go.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/go.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/go.js +0 -181
- package/bundled-graph/dist/src/extract/languages/go.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/java.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/java.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/java.js +0 -117
- package/bundled-graph/dist/src/extract/languages/java.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/kotlin.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/kotlin.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/kotlin.js +0 -112
- package/bundled-graph/dist/src/extract/languages/kotlin.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/php.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/php.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/php.js +0 -130
- package/bundled-graph/dist/src/extract/languages/php.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/python.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/python.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/python.js +0 -230
- package/bundled-graph/dist/src/extract/languages/python.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/ruby.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/ruby.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/ruby.js +0 -120
- package/bundled-graph/dist/src/extract/languages/ruby.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/rust.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/rust.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/rust.js +0 -195
- package/bundled-graph/dist/src/extract/languages/rust.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/scala.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/scala.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/scala.js +0 -110
- package/bundled-graph/dist/src/extract/languages/scala.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/swift.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/swift.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/swift.js +0 -122
- package/bundled-graph/dist/src/extract/languages/swift.js.map +0 -1
- package/bundled-graph/dist/src/extract/languages/typescript.d.ts +0 -3
- package/bundled-graph/dist/src/extract/languages/typescript.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/languages/typescript.js +0 -295
- package/bundled-graph/dist/src/extract/languages/typescript.js.map +0 -1
- package/bundled-graph/dist/src/extract/semantic.d.ts +0 -38
- package/bundled-graph/dist/src/extract/semantic.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/semantic.js +0 -242
- package/bundled-graph/dist/src/extract/semantic.js.map +0 -1
- package/bundled-graph/dist/src/extract/tree-sitter-runner.d.ts +0 -48
- package/bundled-graph/dist/src/extract/tree-sitter-runner.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/tree-sitter-runner.js +0 -137
- package/bundled-graph/dist/src/extract/tree-sitter-runner.js.map +0 -1
- package/bundled-graph/dist/src/extract/types.d.ts +0 -7
- package/bundled-graph/dist/src/extract/types.d.ts.map +0 -1
- package/bundled-graph/dist/src/extract/types.js +0 -2
- package/bundled-graph/dist/src/extract/types.js.map +0 -1
- package/bundled-graph/dist/src/index.d.ts +0 -28
- package/bundled-graph/dist/src/index.d.ts.map +0 -1
- package/bundled-graph/dist/src/index.js +0 -26
- package/bundled-graph/dist/src/index.js.map +0 -1
- package/bundled-graph/dist/src/pipeline.d.ts +0 -27
- package/bundled-graph/dist/src/pipeline.d.ts.map +0 -1
- package/bundled-graph/dist/src/pipeline.js +0 -269
- package/bundled-graph/dist/src/pipeline.js.map +0 -1
- package/bundled-graph/dist/src/report.d.ts +0 -26
- package/bundled-graph/dist/src/report.d.ts.map +0 -1
- package/bundled-graph/dist/src/report.js +0 -214
- package/bundled-graph/dist/src/report.js.map +0 -1
- package/bundled-graph/dist/src/types.d.ts +0 -124
- package/bundled-graph/dist/src/types.d.ts.map +0 -1
- package/bundled-graph/dist/src/types.js +0 -2
- package/bundled-graph/dist/src/types.js.map +0 -1
- package/bundled-graph/dist/src/visualize.d.ts +0 -4
- package/bundled-graph/dist/src/visualize.d.ts.map +0 -1
- package/bundled-graph/dist/src/visualize.js +0 -574
- package/bundled-graph/dist/src/visualize.js.map +0 -1
- package/bundled-graph/dist/tsconfig.tsbuildinfo +0 -1
- 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 <kbd>↵</kbd> detail <kbd>/</kbd> find <kbd>G</kbd> live <kbd>A</kbd> ambient <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, ''');
|
|
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, ''');
|
|
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, ''');
|
|
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 > 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
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>
|