argus-code 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. {argus_code-0.3.1 → argus_code-0.4.0}/.gitignore +3 -0
  2. {argus_code-0.3.1 → argus_code-0.4.0}/PKG-INFO +2 -2
  3. {argus_code-0.3.1 → argus_code-0.4.0}/README.md +1 -1
  4. argus_code-0.4.0/dashboard/src/pages/session.astro +479 -0
  5. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/scripts/api.ts +27 -0
  6. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/scripts/charts.ts +52 -7
  7. argus_code-0.4.0/dashboard-dist/_astro/charts.CQF9QNgP.js +1 -0
  8. argus_code-0.4.0/dashboard-dist/_astro/format.CgLh3UFa.js +1 -0
  9. argus_code-0.3.1/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.Dtzf0Pdc.js → argus_code-0.4.0/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.UFVBMlig.js +24 -24
  10. argus_code-0.3.1/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.W18SJsr7.js → argus_code-0.4.0/dashboard-dist/_astro/index.astro_astro_type_script_index_0_lang.cnW_pvh0.js +11 -11
  11. argus_code-0.3.1/dashboard-dist/_astro/installCanvasRenderer.D_tC6TXz.js → argus_code-0.4.0/dashboard-dist/_astro/installCanvasRenderer.XK5yP7fW.js +18 -18
  12. argus_code-0.3.1/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.BHTHXYHC.js → argus_code-0.4.0/dashboard-dist/_astro/models.astro_astro_type_script_index_0_lang.DsSR6D_a.js +13 -13
  13. argus_code-0.3.1/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.DfNgiDv9.js → argus_code-0.4.0/dashboard-dist/_astro/prompts.astro_astro_type_script_index_0_lang.C0Mv7sTw.js +17 -17
  14. argus_code-0.4.0/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.IkwnKMSS.js +114 -0
  15. argus_code-0.3.1/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.C--f3VLy.js → argus_code-0.4.0/dashboard-dist/_astro/settings.astro_astro_type_script_index_0_lang.Cs7bzN_0.js +24 -24
  16. argus_code-0.3.1/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.Dzzau3Yt.js → argus_code-0.4.0/dashboard-dist/_astro/tools.astro_astro_type_script_index_0_lang.DTzvpY-G.js +12 -12
  17. argus_code-0.3.1/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.BZ0GmC-o.js → argus_code-0.4.0/dashboard-dist/_astro/trends.astro_astro_type_script_index_0_lang.5olZlQfw.js +5 -5
  18. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/index.html +2 -2
  19. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/models/index.html +1 -1
  20. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/prompts/index.html +18 -18
  21. argus_code-0.4.0/dashboard-dist/session/index.html +2 -0
  22. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/sessions/index.html +1 -1
  23. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/settings/index.html +8 -8
  24. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/tools/index.html +1 -1
  25. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/trends/index.html +1 -1
  26. argus_code-0.4.0/pricing/2026-06-12.json +27 -0
  27. {argus_code-0.3.1 → argus_code-0.4.0}/pyproject.toml +1 -1
  28. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/__init__.py +1 -1
  29. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/base.py +1 -0
  30. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/discover.py +36 -2
  31. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_transcript.py +2 -0
  32. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/first_run.py +15 -0
  33. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/pipeline.py +85 -27
  34. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/pricing/load.py +13 -5
  35. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/schema/types.py +1 -0
  36. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/server/api.py +18 -0
  37. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/store/db.py +5 -1
  38. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/store/migrations/inline.py +5 -0
  39. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/store/repository.py +130 -3
  40. argus_code-0.4.0/tests/adapters/claude_code/test_discover.py +68 -0
  41. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_transcript.py +23 -0
  42. argus_code-0.4.0/tests/collector/test_first_run.py +207 -0
  43. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/test_pipeline.py +129 -0
  44. argus_code-0.4.0/tests/pricing/test_load.py +43 -0
  45. {argus_code-0.3.1 → argus_code-0.4.0}/tests/server/test_api.py +55 -0
  46. {argus_code-0.3.1 → argus_code-0.4.0}/tests/store/test_db.py +17 -3
  47. {argus_code-0.3.1 → argus_code-0.4.0}/tests/store/test_repository.py +175 -1
  48. {argus_code-0.3.1 → argus_code-0.4.0}/uv.lock +983 -983
  49. argus_code-0.3.1/dashboard/src/pages/session.astro +0 -237
  50. argus_code-0.3.1/dashboard-dist/_astro/charts.CAJCDcsn.js +0 -1
  51. argus_code-0.3.1/dashboard-dist/_astro/format.DxC1NGYT.js +0 -1
  52. argus_code-0.3.1/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.C2_GW8Bb.js +0 -86
  53. argus_code-0.3.1/dashboard-dist/session/index.html +0 -2
  54. argus_code-0.3.1/tests/adapters/claude_code/test_discover.py +0 -31
  55. argus_code-0.3.1/tests/collector/test_first_run.py +0 -77
  56. argus_code-0.3.1/tests/pricing/test_load.py +0 -16
  57. {argus_code-0.3.1 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  58. {argus_code-0.3.1 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  59. {argus_code-0.3.1 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  60. {argus_code-0.3.1 → argus_code-0.4.0}/.github/pull_request_template.md +0 -0
  61. {argus_code-0.3.1 → argus_code-0.4.0}/.npmrc +0 -0
  62. {argus_code-0.3.1 → argus_code-0.4.0}/ARCHITECTURE.md +0 -0
  63. {argus_code-0.3.1 → argus_code-0.4.0}/CONTRIBUTING.md +0 -0
  64. {argus_code-0.3.1 → argus_code-0.4.0}/LICENSE +0 -0
  65. {argus_code-0.3.1 → argus_code-0.4.0}/NOTICE +0 -0
  66. {argus_code-0.3.1 → argus_code-0.4.0}/PRD.md +0 -0
  67. {argus_code-0.3.1 → argus_code-0.4.0}/SECURITY.md +0 -0
  68. {argus_code-0.3.1 → argus_code-0.4.0}/TESTING.md +0 -0
  69. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/.astro/content-assets.mjs +0 -0
  70. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/.astro/content-modules.mjs +0 -0
  71. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/.astro/content.d.ts +0 -0
  72. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/.astro/types.d.ts +0 -0
  73. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/astro.config.mjs +0 -0
  74. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/package-lock.json +0 -0
  75. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/package.json +0 -0
  76. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/public/styles/global.css +0 -0
  77. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/layouts/Default.astro +0 -0
  78. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/index.astro +0 -0
  79. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/models.astro +0 -0
  80. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/prompts.astro +0 -0
  81. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/sessions/index.astro +0 -0
  82. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/settings.astro +0 -0
  83. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/tools.astro +0 -0
  84. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/pages/trends.astro +0 -0
  85. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/scripts/alerts.ts +0 -0
  86. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/scripts/format.ts +0 -0
  87. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/src/styles/global.css +0 -0
  88. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard/tsconfig.json +0 -0
  89. {argus_code-0.3.1 → argus_code-0.4.0}/dashboard-dist/styles/global.css +0 -0
  90. {argus_code-0.3.1 → argus_code-0.4.0}/pricing/2026-05-02.json +0 -0
  91. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/__init__.py +0 -0
  92. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/__init__.py +0 -0
  93. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/adapter.py +0 -0
  94. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_tool_calls.py +0 -0
  95. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_turns.py +0 -0
  96. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/history_jsonl.py +0 -0
  97. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/ingest_file.py +0 -0
  98. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/model.py +0 -0
  99. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/claude_code/schemas.py +0 -0
  100. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/adapters/registry.py +0 -0
  101. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/cli.py +0 -0
  102. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/__init__.py +0 -0
  103. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/aggregate.py +0 -0
  104. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/rollup_subagents.py +0 -0
  105. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/scheduler.py +0 -0
  106. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/search_backfill.py +0 -0
  107. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/collector/watcher.py +0 -0
  108. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/core/__init__.py +0 -0
  109. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/core/runtime.py +0 -0
  110. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/daemon/__init__.py +0 -0
  111. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/daemon/logging.py +0 -0
  112. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/daemon/pidfile.py +0 -0
  113. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/daemon/process.py +0 -0
  114. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/daemon/service.py +0 -0
  115. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/detectors/__init__.py +0 -0
  116. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/detectors/base.py +0 -0
  117. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/detectors/registry.py +0 -0
  118. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/detectors/tool_error_rate_spike.py +0 -0
  119. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/pricing/__init__.py +0 -0
  120. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/pricing/compute.py +0 -0
  121. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/pricing/refresh.py +0 -0
  122. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/pricing/types.py +0 -0
  123. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/scaffold/__init__.py +0 -0
  124. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/scaffold/scaffolder.py +0 -0
  125. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/scaffold/snapshot.py +0 -0
  126. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/scaffold/storage.py +0 -0
  127. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/schema/__init__.py +0 -0
  128. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/server/__init__.py +0 -0
  129. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/server/app.py +0 -0
  130. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/store/__init__.py +0 -0
  131. {argus_code-0.3.1 → argus_code-0.4.0}/python/argus/store/migrations/__init__.py +0 -0
  132. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/agents/code-reviewer.md +0 -0
  133. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/agents/security-auditor.md +0 -0
  134. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/commands/commit.md +0 -0
  135. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/commands/deploy.md +0 -0
  136. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/commands/fix-issue.md +0 -0
  137. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/commands/pr.md +0 -0
  138. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/commands/review.md +0 -0
  139. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/rules/api-conventions.md +0 -0
  140. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/rules/code-style.md +0 -0
  141. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/rules/testing.md +0 -0
  142. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/settings.json +0 -0
  143. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/.claude/skills/example/SKILL.md +0 -0
  144. {argus_code-0.3.1 → argus_code-0.4.0}/templates/default/CLAUDE.md +0 -0
  145. {argus_code-0.3.1 → argus_code-0.4.0}/tests/__init__.py +0 -0
  146. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/__init__.py +0 -0
  147. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/__init__.py +0 -0
  148. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_adapter.py +0 -0
  149. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_tool_calls.py +0 -0
  150. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_turns.py +0 -0
  151. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_history_jsonl.py +0 -0
  152. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_ingest_file.py +0 -0
  153. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_integration.py +0 -0
  154. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_model.py +0 -0
  155. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/claude_code/test_schemas.py +0 -0
  156. {argus_code-0.3.1 → argus_code-0.4.0}/tests/adapters/test_registry.py +0 -0
  157. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/__init__.py +0 -0
  158. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/test_aggregate.py +0 -0
  159. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/test_rollup_subagents.py +0 -0
  160. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/test_scheduler.py +0 -0
  161. {argus_code-0.3.1 → argus_code-0.4.0}/tests/collector/test_watcher.py +0 -0
  162. {argus_code-0.3.1 → argus_code-0.4.0}/tests/conftest.py +0 -0
  163. {argus_code-0.3.1 → argus_code-0.4.0}/tests/core/__init__.py +0 -0
  164. {argus_code-0.3.1 → argus_code-0.4.0}/tests/core/test_runtime.py +0 -0
  165. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/__init__.py +0 -0
  166. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/test_logs.py +0 -0
  167. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/test_pidfile.py +0 -0
  168. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/test_process.py +0 -0
  169. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/test_service.py +0 -0
  170. {argus_code-0.3.1 → argus_code-0.4.0}/tests/daemon/test_yield.py +0 -0
  171. {argus_code-0.3.1 → argus_code-0.4.0}/tests/detectors/__init__.py +0 -0
  172. {argus_code-0.3.1 → argus_code-0.4.0}/tests/detectors/test_registry.py +0 -0
  173. {argus_code-0.3.1 → argus_code-0.4.0}/tests/detectors/test_tool_error_rate_spike.py +0 -0
  174. {argus_code-0.3.1 → argus_code-0.4.0}/tests/pricing/__init__.py +0 -0
  175. {argus_code-0.3.1 → argus_code-0.4.0}/tests/pricing/test_compute.py +0 -0
  176. {argus_code-0.3.1 → argus_code-0.4.0}/tests/pricing/test_refresh.py +0 -0
  177. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/__init__.py +0 -0
  178. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/test_bundled_template.py +0 -0
  179. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/test_cli_claude.py +0 -0
  180. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/test_scaffolder.py +0 -0
  181. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/test_snapshot.py +0 -0
  182. {argus_code-0.3.1 → argus_code-0.4.0}/tests/scaffold/test_storage.py +0 -0
  183. {argus_code-0.3.1 → argus_code-0.4.0}/tests/schema/__init__.py +0 -0
  184. {argus_code-0.3.1 → argus_code-0.4.0}/tests/schema/test_types.py +0 -0
  185. {argus_code-0.3.1 → argus_code-0.4.0}/tests/server/__init__.py +0 -0
  186. {argus_code-0.3.1 → argus_code-0.4.0}/tests/server/test_api_search.py +0 -0
  187. {argus_code-0.3.1 → argus_code-0.4.0}/tests/server/test_server.py +0 -0
  188. {argus_code-0.3.1 → argus_code-0.4.0}/tests/server/test_week_of.py +0 -0
  189. {argus_code-0.3.1 → argus_code-0.4.0}/tests/store/__init__.py +0 -0
  190. {argus_code-0.3.1 → argus_code-0.4.0}/tests/test_e2e.py +0 -0
@@ -11,6 +11,9 @@ coverage/
11
11
  # Personal planning notes — kept local, not published
12
12
  docs/
13
13
 
14
+ # Brainstorming visual-companion mockups
15
+ .superpowers/
16
+
14
17
  # Python build / virtualenv / cache artifacts
15
18
  __pycache__/
16
19
  *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-code
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Local-first dashboard for Claude Code cost, token, tool-usage, and full-text search analytics
5
5
  Project-URL: Homepage, https://github.com/KrishBhimani/argus-code
6
6
  Project-URL: Repository, https://github.com/KrishBhimani/argus-code.git
@@ -79,7 +79,7 @@ ingest finishes (~5–10s for a typical install).
79
79
  | Page | What it answers |
80
80
  |---|---|
81
81
  | **Overview** | How many tokens have I burned? How much would I have paid on the API? Where does my spend land each day? Plus a **"What needs attention"** card that surfaces detector findings (e.g. a tool whose error rate spiked this week). |
82
- | **Sessions** | Sortable table of every session — project, model, tokens, cost, duration. Click any row to drill in. |
82
+ | **Sessions** | Sortable table of every session — project, model, tokens, cost, duration. Click any row to drill in: an **Overview** tab with totals and the turns table, and a **Timeline** tab showing per-turn token burn (chart + trace feed), every tool call, and failures — with the actual error output inline when search indexing is on. |
83
83
  | **Tools** | Tool-call leaderboard (Bash vs Edit vs Read vs WebFetch…), error rates per tool, MCP server breakdown, sub-agent invocations. |
84
84
  | **Search** *(opt-in)* | Full-text search over every prompt you've typed AND every assistant response, your replies, and tool output. SQLite FTS5, sub-millisecond, no embeddings. |
85
85
  | **Trends** | Tokens and cost bucketed by day / week / month, grouped by model. |
@@ -27,7 +27,7 @@ ingest finishes (~5–10s for a typical install).
27
27
  | Page | What it answers |
28
28
  |---|---|
29
29
  | **Overview** | How many tokens have I burned? How much would I have paid on the API? Where does my spend land each day? Plus a **"What needs attention"** card that surfaces detector findings (e.g. a tool whose error rate spiked this week). |
30
- | **Sessions** | Sortable table of every session — project, model, tokens, cost, duration. Click any row to drill in. |
30
+ | **Sessions** | Sortable table of every session — project, model, tokens, cost, duration. Click any row to drill in: an **Overview** tab with totals and the turns table, and a **Timeline** tab showing per-turn token burn (chart + trace feed), every tool call, and failures — with the actual error output inline when search indexing is on. |
31
31
  | **Tools** | Tool-call leaderboard (Bash vs Edit vs Read vs WebFetch…), error rates per tool, MCP server breakdown, sub-agent invocations. |
32
32
  | **Search** *(opt-in)* | Full-text search over every prompt you've typed AND every assistant response, your replies, and tool output. SQLite FTS5, sub-millisecond, no embeddings. |
33
33
  | **Trends** | Tokens and cost bucketed by day / week / month, grouped by model. |
@@ -0,0 +1,479 @@
1
+ ---
2
+ import Default from '../layouts/Default.astro';
3
+ ---
4
+ <Default title="Argus — Session">
5
+ <p style="margin:0 0 1rem;"><a href="/sessions">← All sessions</a></p>
6
+ <div class="sd-tabs">
7
+ <button id="tab-btn-overview" class="sd-tab active" type="button">Overview</button>
8
+ <button id="tab-btn-timeline" class="sd-tab" type="button">Timeline</button>
9
+ </div>
10
+ <div id="content">
11
+ <div class="card"><div class="skel" style="width:60%;height:1.5em;"></div></div>
12
+ </div>
13
+ <div id="timeline-content" style="display:none;">
14
+ <div class="card"><div class="skel" style="width:60%;height:1.5em;"></div></div>
15
+ </div>
16
+ <button id="to-top" type="button" title="Back to top">↑</button>
17
+
18
+ <!--
19
+ is:global so the styles apply to the result list we render via innerHTML
20
+ (Astro's component-scoped styles wouldn't reach those nodes). Class
21
+ names are namespaced (sd-…) to keep them out of the global pool.
22
+ -->
23
+ <style is:global>
24
+ .sd-seg-card {
25
+ background: var(--bg-1);
26
+ border: 1px solid var(--border);
27
+ border-radius: 8px;
28
+ padding: 0.75rem 1rem;
29
+ margin-bottom: 0.55rem;
30
+ }
31
+ .sd-seg-meta {
32
+ font-size: 0.74rem;
33
+ color: var(--text-2);
34
+ margin-bottom: 0.3rem;
35
+ }
36
+ .sd-seg-snippet {
37
+ font-size: 0.88rem;
38
+ color: var(--text-0);
39
+ white-space: pre-wrap;
40
+ word-break: break-word;
41
+ line-height: 1.4;
42
+ overflow-wrap: anywhere;
43
+ }
44
+ .sd-seg-snippet mark {
45
+ background: rgba(240, 136, 62, 0.28);
46
+ color: var(--text-0);
47
+ padding: 0 1px;
48
+ border-radius: 2px;
49
+ }
50
+ .sd-role-pill {
51
+ display: inline-block;
52
+ padding: 0.05em 0.55em;
53
+ border-radius: 4px;
54
+ font-size: 0.7rem;
55
+ font-weight: 500;
56
+ }
57
+ .sd-role-pill.role-user { background: rgba(126,231,135,0.16); color: var(--good); }
58
+ .sd-role-pill.role-assistant { background: rgba(88,166,255,0.16); color: var(--codex); }
59
+ .sd-role-pill.role-thinking { background: rgba(188,140,255,0.16); color: #bc8cff; }
60
+ .sd-role-pill.role-tool_result { background: var(--bg-2); color: var(--text-2); }
61
+ .sd-tabs {
62
+ display: flex; gap: 0.4rem; margin-bottom: 1rem;
63
+ border-bottom: 1px solid var(--border);
64
+ }
65
+ .sd-tab {
66
+ background: none; border: none; border-bottom: 2px solid transparent;
67
+ color: var(--text-2); font: inherit; font-size: 0.9rem;
68
+ padding: 0.4rem 0.9rem; cursor: pointer;
69
+ }
70
+ .sd-tab.active { color: var(--text-0); border-bottom-color: var(--accent, #f0883e); }
71
+ .sd-turn-hdr {
72
+ display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.4rem;
73
+ font-size: 0.85rem; margin-bottom: 0.4rem;
74
+ }
75
+ .sd-tool-row {
76
+ padding: 0.15rem 0 0.15rem 1.2rem; font-size: 0.84rem; color: var(--text-1);
77
+ }
78
+ .sd-tool-row .ok { color: var(--good); }
79
+ .sd-tool-row .fail { color: #f85149; font-weight: 600; }
80
+ .sd-tool-size { color: var(--text-2); font-size: 0.76rem; margin-left: 0.4rem; }
81
+ .sd-chip {
82
+ display: inline-block; background: rgba(188,140,255,0.16); color: #bc8cff;
83
+ border-radius: 4px; font-size: 0.7rem; padding: 0.05em 0.5em; margin-left: 0.4rem;
84
+ }
85
+ .sd-err-box {
86
+ margin: 0.25rem 0 0.5rem 1.2rem; background: rgba(248,81,73,0.08);
87
+ border: 1px solid rgba(248,81,73,0.4); border-radius: 4px;
88
+ padding: 0.35rem 0.6rem; font-size: 0.8rem; color: #f85149;
89
+ }
90
+ .sd-err-box pre {
91
+ margin: 0.4rem 0 0; white-space: pre-wrap; word-break: break-word;
92
+ color: var(--text-1); font-size: 0.78rem;
93
+ }
94
+ .sd-err-box summary { cursor: pointer; }
95
+ .sd-err-hint {
96
+ margin: 0.25rem 0 0.5rem 1.2rem; font-size: 0.78rem;
97
+ color: var(--text-2); font-style: italic;
98
+ }
99
+ .sd-no-tools { padding-left: 1.2rem; font-size: 0.8rem; color: var(--text-2); font-style: italic; }
100
+ .tl-expandable { cursor: pointer; }
101
+ .tl-expandable:hover b { color: var(--accent, #f0883e); }
102
+ .sd-out-box {
103
+ margin: 0.25rem 0 0.5rem 1.2rem; background: var(--bg-2);
104
+ border: 1px solid var(--border); border-radius: 4px;
105
+ padding: 0.35rem 0.6rem; font-size: 0.78rem; color: var(--text-2);
106
+ }
107
+ .sd-out-box pre {
108
+ margin: 0; white-space: pre-wrap; word-break: break-word;
109
+ color: var(--text-1); max-height: 280px; overflow: auto;
110
+ }
111
+ #to-top {
112
+ position: fixed; right: 1.4rem; bottom: 1.4rem; z-index: 50;
113
+ width: 40px; height: 40px; border-radius: 50%;
114
+ background: var(--bg-1); color: var(--text-1);
115
+ border: 1px solid var(--border); font-size: 1.1rem; cursor: pointer;
116
+ display: none; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
117
+ }
118
+ #to-top:hover { color: var(--text-0); border-color: var(--accent, #f0883e); }
119
+ </style>
120
+
121
+ <script>
122
+ import { api, type Turn, type TimelineTurn } from '../scripts/api';
123
+ import { turnTokensBar } from '../scripts/charts';
124
+ import { usd, tok, num, dur, escapeHtml } from '../scripts/format';
125
+
126
+ const TZ = Intl.DateTimeFormat().resolvedOptions().timeZone;
127
+ const TZ_ABBREV = (() => {
128
+ const d = new Date();
129
+ const m = d.toLocaleTimeString('en-US', { timeZoneName: 'short' }).match(/[A-Z]{2,5}$/);
130
+ return m ? m[0] : '';
131
+ })();
132
+
133
+ const fmtLocalDateTime = (iso: string) => {
134
+ if (!iso) return '—';
135
+ return new Date(iso).toLocaleString([], {
136
+ year: 'numeric', month: '2-digit', day: '2-digit',
137
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
138
+ });
139
+ };
140
+ const fmtLocalTime = (iso: string) => {
141
+ if (!iso) return '—';
142
+ return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
143
+ };
144
+
145
+ function safeSnippet(s: string): string {
146
+ const open = '\x00MK_OPEN\x00';
147
+ const close = '\x00MK_CLOSE\x00';
148
+ const stripped = s.replace(/<mark>/g, open).replace(/<\/mark>/g, close);
149
+ return escapeHtml(stripped).replace(new RegExp(open, 'g'), '<mark>').replace(new RegExp(close, 'g'), '</mark>');
150
+ }
151
+ function roleLabel(role: string): string {
152
+ switch (role) {
153
+ case 'user': return 'you said';
154
+ case 'assistant': return 'claude said';
155
+ case 'thinking': return 'claude thinking';
156
+ case 'tool_result': return 'tool output';
157
+ default: return role;
158
+ }
159
+ }
160
+
161
+ const params = new URLSearchParams(location.search);
162
+ const id = params.get('id');
163
+ const initialQ = params.get('q') ?? '';
164
+ const root = document.getElementById('content')!;
165
+ if (!id) { root.innerHTML = '<p class="empty">No session id.</p>'; }
166
+ else {
167
+ api.session(id).then(d => {
168
+ if (!d) { root.innerHTML = '<p class="empty">Session not found.</p>'; return; }
169
+ const { session: s, turns } = d;
170
+ const totalTokens = s.total_fresh_input_tokens + s.total_output_tokens + s.total_cache_read_tokens + s.total_cache_write_tokens;
171
+ const subAgents = (s.metadata as any)?.sub_agent_session_ids as string[] | undefined;
172
+ const reportedDelta = s.agent_reported_cost_usd != null
173
+ ? `<span style="color:var(--text-2);font-size:0.8rem;"> (agent reported: ${usd(s.agent_reported_cost_usd)})</span>` : '';
174
+
175
+ root.innerHTML = `
176
+ <div class="grid-cards" style="margin-bottom:1.2rem;">
177
+ <div class="card kpi"><span class="kpi-label">Tokens</span><span class="kpi-value tokens">${tok(totalTokens)}</span><span class="kpi-sub">${num(totalTokens)} total${subAgents?.length ? ` · incl. ${subAgents.length} sub-agent${subAgents.length === 1 ? '' : 's'}` : ''}</span></div>
178
+ <div class="card kpi"><span class="kpi-label">Cost <span style="color:var(--text-2);font-weight:400;">(est.)</span></span><span class="kpi-value cost">~${usd(s.total_cost_usd)}</span><span class="kpi-sub">${reportedDelta}</span></div>
179
+ <div class="card kpi"><span class="kpi-label">Turns</span><span class="kpi-value">${num(s.turn_count)}</span><span class="kpi-sub">${turns.length} loaded</span></div>
180
+ <div class="card kpi"><span class="kpi-label">Duration</span><span class="kpi-value">${dur(s.duration_sec)}</span><span class="kpi-sub">started ${fmtLocalDateTime(s.started_at)}</span></div>
181
+ </div>
182
+
183
+ <div class="card" style="margin-bottom:1rem;">
184
+ <h3>Session</h3>
185
+ <table style="border:none;">
186
+ <tbody>
187
+ <tr><td style="width:25%;color:var(--text-2);">Claude Code</td><td><code>${escapeHtml(s.agent_version ?? '—')}</code></td></tr>
188
+ <tr><td style="color:var(--text-2);">Project</td><td class="mono">${escapeHtml(s.project_path || '—')}</td></tr>
189
+ <tr><td style="color:var(--text-2);">Primary model</td><td><code>${escapeHtml(s.primary_model)}</code></td></tr>
190
+ <tr><td style="color:var(--text-2);">Started</td><td>${fmtLocalDateTime(s.started_at)} <span style="color:var(--text-2);font-size:0.8rem;">${escapeHtml(TZ_ABBREV)} (${escapeHtml(TZ)})</span></td></tr>
191
+ <tr><td style="color:var(--text-2);">Ended</td><td>${s.ended_at ? fmtLocalDateTime(s.ended_at) : '<span style="color:var(--text-2);">— still active</span>'}</td></tr>
192
+ <tr><td style="color:var(--text-2);">Fresh input</td><td>${num(s.total_fresh_input_tokens)} tokens</td></tr>
193
+ <tr><td style="color:var(--text-2);">Output</td><td>${num(s.total_output_tokens)} tokens</td></tr>
194
+ <tr><td style="color:var(--text-2);">Cache writes</td><td>${num(s.total_cache_write_tokens)} tokens</td></tr>
195
+ <tr><td style="color:var(--text-2);">Cache reads</td><td>${num(s.total_cache_read_tokens)} tokens</td></tr>
196
+ <tr><td style="color:var(--text-2);">Pricing version</td><td><code>${escapeHtml(s.pricing_table_version)}</code></td></tr>
197
+ <tr><td style="color:var(--text-2);">Session id</td><td class="mono" style="word-break:break-all;">${escapeHtml(s.id)}</td></tr>
198
+ </tbody>
199
+ </table>
200
+ </div>
201
+
202
+ ${subAgents?.length ? `
203
+ <div class="card" style="margin-bottom:1rem;">
204
+ <h3>Sub-agents (${subAgents.length})</h3>
205
+ <ul style="margin:0;padding-left:1.2rem;color:var(--text-1);font-size:0.88rem;">
206
+ ${subAgents.map(id => `<li><a href="/session?id=${encodeURIComponent(id)}" class="mono">${escapeHtml(id)}</a></li>`).join('')}
207
+ </ul>
208
+ </div>` : ''}
209
+
210
+ <div class="card" style="margin-bottom:1rem;">
211
+ <div style="display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap;">
212
+ <h3 style="margin:0;flex-shrink:0;">Search this session</h3>
213
+ <input id="seg-q" type="text" placeholder="Find text in this session…"
214
+ style="flex:1;min-width:200px;background:var(--bg-2);color:var(--text-0);
215
+ border:1px solid var(--border);border-radius:6px;padding:0.4rem 0.7rem;
216
+ font-size:0.9rem;font-family:inherit;outline:none;" />
217
+ <span id="seg-count" style="color:var(--text-2);font-size:0.8rem;"></span>
218
+ </div>
219
+ <div id="seg-results" style="margin-top:0.8rem;"></div>
220
+ </div>
221
+
222
+ <div class="card">
223
+ <div style="display:flex;justify-content:space-between;align-items:baseline;">
224
+ <h3 style="margin:0;">Turns timeline</h3>
225
+ <span style="color:var(--text-2);font-size:0.75rem;">times in ${TZ_ABBREV} (${TZ})</span>
226
+ </div>
227
+ ${turns.length ? `
228
+ <table style="margin-top:0.8rem;">
229
+ <thead>
230
+ <tr>
231
+ <th class="num">#</th><th>Time</th><th>Model</th>
232
+ <th class="num">Fresh in</th><th class="num">Cache read</th><th class="num">Cache write</th>
233
+ <th class="num">Output</th><th class="num">Tools</th><th class="num">Cost <span class="est">(est.)</span></th>
234
+ </tr>
235
+ </thead>
236
+ <tbody>
237
+ ${turns.map((t: Turn) => `
238
+ <tr>
239
+ <td class="num">${t.sequence}</td>
240
+ <td>${fmtLocalTime(t.timestamp)}</td>
241
+ <td><code>${escapeHtml(t.model)}</code></td>
242
+ <td class="num tok">${num(t.fresh_input_tokens)}</td>
243
+ <td class="num tok">${num(t.cache_read_tokens)}</td>
244
+ <td class="num tok">${num(t.cache_write_tokens)}</td>
245
+ <td class="num tok">${num(t.output_tokens)}</td>
246
+ <td class="num">${t.tool_calls_count}</td>
247
+ <td class="num cost">~${usd(t.cost_usd)}</td>
248
+ </tr>`).join('')}
249
+ </tbody>
250
+ </table>` : '<p class="empty">No turns recorded for this session.</p>'}
251
+ </div>
252
+ `;
253
+
254
+ // Wire the per-session search box
255
+ const segQ = document.getElementById('seg-q') as HTMLInputElement;
256
+ const segResults = document.getElementById('seg-results')!;
257
+ const segCount = document.getElementById('seg-count')!;
258
+ let debTimer: any;
259
+
260
+ async function runSegSearch() {
261
+ const q = segQ.value.trim();
262
+ if (!q) {
263
+ segResults.innerHTML = '';
264
+ segCount.textContent = '';
265
+ return;
266
+ }
267
+ try {
268
+ const r = await api.sessionTranscriptSearch(id!, q, 200);
269
+ segCount.textContent = `${r.total} match${r.total === 1 ? '' : 'es'}`;
270
+ if (r.segments.length === 0) {
271
+ segResults.innerHTML = '<p class="empty" style="margin:0.5rem 0;">No matches in this session.</p>';
272
+ return;
273
+ }
274
+ // Sort chronologically so the user can read in conversation order
275
+ const sorted = r.segments.slice().sort((a, b) =>
276
+ Date.parse(a.timestamp) - Date.parse(b.timestamp));
277
+ segResults.innerHTML = sorted.map(s => `
278
+ <div class="sd-seg-card">
279
+ <div class="sd-seg-meta">
280
+ <span class="sd-role-pill role-${s.role}">${roleLabel(s.role)}</span>
281
+ <span style="margin-left:0.5rem;">${fmtLocalTime(s.timestamp)}</span>
282
+ </div>
283
+ <div class="sd-seg-snippet">${safeSnippet(s.snippet)}</div>
284
+ </div>
285
+ `).join('');
286
+ } catch {
287
+ segResults.innerHTML = '<p class="empty">Search failed.</p>';
288
+ }
289
+ }
290
+
291
+ segQ.addEventListener('input', () => {
292
+ clearTimeout(debTimer);
293
+ debTimer = setTimeout(runSegSearch, 150);
294
+ });
295
+
296
+ // Auto-run if a query came from the URL (?q=…)
297
+ if (initialQ) {
298
+ segQ.value = initialQ;
299
+ runSegSearch();
300
+ }
301
+ });
302
+
303
+ // ─── Tabs ───────────────────────────────────────────────────────
304
+ const overviewPane = document.getElementById('content')!;
305
+ const timelinePane = document.getElementById('timeline-content')!;
306
+ const btnOverview = document.getElementById('tab-btn-overview')!;
307
+ const btnTimeline = document.getElementById('tab-btn-timeline')!;
308
+ let timelineLoaded = false;
309
+
310
+ function showTab(tab: 'overview' | 'timeline') {
311
+ const onOverview = tab === 'overview';
312
+ overviewPane.style.display = onOverview ? '' : 'none';
313
+ timelinePane.style.display = onOverview ? 'none' : '';
314
+ btnOverview.classList.toggle('active', onOverview);
315
+ btnTimeline.classList.toggle('active', !onOverview);
316
+ if (!onOverview && !timelineLoaded) {
317
+ timelineLoaded = true;
318
+ loadTimeline();
319
+ }
320
+ }
321
+ btnOverview.addEventListener('click', () => showTab('overview'));
322
+ btnTimeline.addEventListener('click', () => showTab('timeline'));
323
+
324
+ // ─── Timeline tab ───────────────────────────────────────────────
325
+ // "Burn" = tokens this turn actually generated/consumed at full rate.
326
+ // Cache reads are the rolling context window — monotonically growing
327
+ // and 20-50x larger, they'd drown the signal if charted directly.
328
+ const turnBurn = (t: TimelineTurn) =>
329
+ t.fresh_input_tokens + t.output_tokens;
330
+
331
+ function toolRow(c: TimelineTurn['tool_calls'][number], searchEnabled: boolean): string {
332
+ const status = c.is_error
333
+ ? '<span class="fail">✗ failed</span>'
334
+ : '<span class="ok">✓</span>';
335
+ const chip = c.subagent_type
336
+ ? `<span class="sd-chip">${escapeHtml(c.subagent_type)}</span>` : '';
337
+ const size = `<span class="sd-tool-size">${(c.input_size / 1024).toFixed(1)} KB</span>`;
338
+ let errDetail = '';
339
+ if (c.is_error) {
340
+ if (c.error_text) {
341
+ const text = c.error_text;
342
+ const head = text.length > 200 ? text.slice(0, 200) + '…' : text;
343
+ errDetail = text.length > 200
344
+ ? `<details class="sd-err-box"><summary>${escapeHtml(head)}</summary><pre>${escapeHtml(text)}</pre></details>`
345
+ : `<div class="sd-err-box">${escapeHtml(text)}</div>`;
346
+ } else {
347
+ errDetail = `<div class="sd-err-hint">${searchEnabled
348
+ ? 'error output not indexed yet — restart argus (or argusd) and reload'
349
+ : 'enable search indexing in Settings to see error output'}</div>`;
350
+ }
351
+ }
352
+ // Rows without an inline error box expand on click to show the
353
+ // call's indexed output.
354
+ const expandable = !c.is_error;
355
+ const attrs = expandable
356
+ ? ` tl-expandable" data-tu="${escapeHtml(c.tool_use_id)}" title="click to show output`
357
+ : '';
358
+ return `<div class="sd-tool-row${attrs}">▸ <b>${escapeHtml(c.tool_name)}</b>${chip}${size} ${status}</div>${errDetail}`;
359
+ }
360
+
361
+ async function loadTimeline() {
362
+ const d = await api.sessionTimeline(id!);
363
+ if (!d) {
364
+ timelinePane.innerHTML = '<p class="empty">Timeline unavailable.</p>';
365
+ return;
366
+ }
367
+ if (d.turns.length === 0) {
368
+ timelinePane.innerHTML = '<p class="empty">No turns recorded for this session.</p>';
369
+ return;
370
+ }
371
+ const cardHtml = (t: TimelineTurn) => `
372
+ <div class="card tl-turn" data-fail="${t.tool_calls.some(c => c.is_error === 1) ? 1 : 0}" id="tl-turn-${t.sequence}" style="margin-bottom:0.6rem;">
373
+ <div class="sd-turn-hdr">
374
+ <span>#${t.sequence} · ${fmtLocalTime(t.timestamp)} · <code>${escapeHtml(t.model)}</code></span>
375
+ <span><span class="tok">${tok(turnBurn(t))} tok</span> <span style="color:var(--text-2);font-size:0.78rem;">· ${tok(t.cache_read_tokens)} cache read</span> · <span class="cost">~${usd(t.cost_usd)}</span></span>
376
+ </div>
377
+ ${t.tool_calls.length
378
+ ? t.tool_calls.map(c => toolRow(c, d.search_enabled)).join('')
379
+ : '<div class="sd-no-tools">no tools — text reply</div>'}
380
+ </div>`;
381
+ // Parsing one giant innerHTML for a 700-turn session visibly hangs
382
+ // the tab — render the first chunk, defer the rest behind a button.
383
+ const CHUNK = 150;
384
+ const cards = d.turns.map(cardHtml);
385
+ let renderedCount = Math.min(cards.length, CHUNK);
386
+ const totalCalls = d.turns.reduce((n, t) => n + t.tool_calls.length, 0);
387
+ const failedCalls = d.turns.reduce((n, t) => n + t.tool_calls.filter(c => c.is_error === 1).length, 0);
388
+ const failedTurns = d.turns.filter(t => t.tool_calls.some(c => c.is_error === 1)).length;
389
+ timelinePane.innerHTML = `
390
+ <div style="display:flex;gap:1.4rem;flex-wrap:wrap;margin-bottom:0.8rem;font-size:0.88rem;color:var(--text-1);">
391
+ <span><b>${num(d.turns.length)}</b> turns</span>
392
+ <span><b>${num(totalCalls)}</b> tool calls</span>
393
+ <span style="${failedCalls ? 'color:#f85149;' : ''}"><b>${num(failedCalls)}</b> failed${failedCalls ? ` <span style="color:var(--text-2);">across ${num(failedTurns)} turn${failedTurns === 1 ? '' : 's'}</span>` : ''}</span>
394
+ </div>
395
+ <div class="card" style="margin-bottom:1rem;">
396
+ <div style="display:flex;justify-content:space-between;align-items:baseline;">
397
+ <h3 style="margin:0;">Fresh + output tokens per turn</h3>
398
+ <span style="color:var(--text-2);font-size:0.75rem;">red = turn contains a failed tool call · click a bar to jump · drag the slider to zoom</span>
399
+ </div>
400
+ <div id="tl-chart" style="height:200px;margin-top:0.6rem;"></div>
401
+ </div>
402
+ <div style="display:flex;justify-content:flex-end;margin-bottom:0.6rem;">
403
+ <label style="color:var(--text-2);font-size:0.8rem;cursor:pointer;user-select:none;">
404
+ <input type="checkbox" id="tl-fail-only" style="vertical-align:-2px;" /> failures only
405
+ </label>
406
+ </div>
407
+ <div id="tl-feed">${cards.slice(0, renderedCount).join('')}</div>
408
+ ${cards.length > CHUNK ? `<button id="tl-more" type="button" style="width:100%;background:none;color:var(--text-2);border:1px dashed var(--border);border-radius:6px;padding:0.5rem;font:inherit;font-size:0.85rem;cursor:pointer;">Show remaining ${cards.length - CHUNK} turns</button>` : ''}
409
+ <p id="tl-fail-empty" class="empty" style="display:none;">No failed tool calls in this session.</p>
410
+ `;
411
+ const failOnly = document.getElementById('tl-fail-only') as HTMLInputElement;
412
+ function applyFailFilter() {
413
+ let visible = 0;
414
+ timelinePane.querySelectorAll<HTMLElement>('.tl-turn').forEach(el => {
415
+ const show = !failOnly.checked || el.dataset.fail === '1';
416
+ el.style.display = show ? '' : 'none';
417
+ if (show) visible++;
418
+ });
419
+ document.getElementById('tl-fail-empty')!.style.display = visible ? 'none' : '';
420
+ }
421
+ function renderRest() {
422
+ if (renderedCount >= cards.length) return;
423
+ document.getElementById('tl-feed')!.insertAdjacentHTML('beforeend', cards.slice(renderedCount).join(''));
424
+ renderedCount = cards.length;
425
+ document.getElementById('tl-more')?.remove();
426
+ }
427
+ document.getElementById('tl-more')?.addEventListener('click', renderRest);
428
+ failOnly.addEventListener('change', () => {
429
+ if (failOnly.checked) renderRest(); // a filter over a partial feed would lie
430
+ applyFailFilter();
431
+ });
432
+ turnTokensBar(
433
+ document.getElementById('tl-chart')!,
434
+ d.turns.map(t => ({
435
+ sequence: t.sequence,
436
+ tokens: turnBurn(t),
437
+ cacheRead: t.cache_read_tokens,
438
+ hasError: t.tool_calls.some(c => c.is_error === 1),
439
+ })),
440
+ seq => {
441
+ if (!document.getElementById(`tl-turn-${seq}`)) renderRest();
442
+ document.getElementById(`tl-turn-${seq}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
443
+ },
444
+ );
445
+ // Click a successful tool call → lazily fetch + toggle its output.
446
+ timelinePane.addEventListener('click', async ev => {
447
+ const row = (ev.target as HTMLElement).closest<HTMLElement>('.tl-expandable');
448
+ if (!row) return;
449
+ const existing = row.nextElementSibling;
450
+ if (existing && existing.classList.contains('sd-out-box')) {
451
+ existing.remove();
452
+ return;
453
+ }
454
+ const box = document.createElement('div');
455
+ box.className = 'sd-out-box';
456
+ box.textContent = 'loading…';
457
+ row.after(box);
458
+ const r = await api.sessionToolOutput(id!, row.dataset.tu!);
459
+ if (!r) box.textContent = 'failed to load output';
460
+ else if (!r.search_enabled) box.textContent = 'enable search indexing in Settings to see tool output';
461
+ else if (!r.found) box.textContent = 'output not indexed — it may have been empty, or restart argus to backfill';
462
+ else box.innerHTML = `<pre>${escapeHtml(r.text!)}</pre>`;
463
+ });
464
+ }
465
+
466
+ // ─── Back-to-top ──────────────────────────────────────────────────
467
+ const toTop = document.getElementById('to-top')!;
468
+ let toTopVisible = false;
469
+ window.addEventListener('scroll', () => {
470
+ const v = window.scrollY > 600;
471
+ if (v !== toTopVisible) { // style write per scroll event janks big DOMs
472
+ toTopVisible = v;
473
+ toTop.style.display = v ? 'block' : 'none';
474
+ }
475
+ }, { passive: true });
476
+ toTop.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
477
+ }
478
+ </script>
479
+ </Default>
@@ -34,6 +34,27 @@ export interface Turn {
34
34
  metadata: Record<string, unknown>;
35
35
  }
36
36
 
37
+ export interface TimelineToolCall {
38
+ tool_name: string;
39
+ tool_use_id: string;
40
+ is_error: 0 | 1;
41
+ input_size: number;
42
+ subagent_type: string | null;
43
+ error_text: string | null;
44
+ }
45
+
46
+ export interface TimelineTurn {
47
+ sequence: number;
48
+ timestamp: string;
49
+ model: string;
50
+ fresh_input_tokens: number;
51
+ cache_read_tokens: number;
52
+ cache_write_tokens: number;
53
+ output_tokens: number;
54
+ cost_usd: number;
55
+ tool_calls: TimelineToolCall[];
56
+ }
57
+
37
58
  export interface Overview {
38
59
  window: string;
39
60
  total_cost_usd: number;
@@ -78,6 +99,12 @@ export interface Alert {
78
99
  export const api = {
79
100
  sessions: (q = '') => fetch('/api/sessions' + q).then(r => r.json() as Promise<{ sessions: Session[] }>),
80
101
  session: (id: string) => fetch('/api/sessions/' + encodeURIComponent(id)).then(r => r.ok ? r.json() as Promise<{ session: Session; turns: Turn[] }> : null),
102
+ sessionTimeline: (id: string) =>
103
+ fetch(`/api/sessions/${encodeURIComponent(id)}/timeline`)
104
+ .then(r => r.ok ? r.json() as Promise<{ search_enabled: boolean; turns: TimelineTurn[] }> : null),
105
+ sessionToolOutput: (id: string, toolUseId: string) =>
106
+ fetch(`/api/sessions/${encodeURIComponent(id)}/tool-output/${encodeURIComponent(toolUseId)}`)
107
+ .then(r => r.ok ? r.json() as Promise<{ search_enabled: boolean; found: boolean; text: string | null }> : null),
81
108
  overview: (window: string) => fetch('/api/overview?window=' + window).then(r => r.json() as Promise<Overview>),
82
109
  trends: (granularity: string, groupBy: string) => fetch(`/api/trends?granularity=${granularity}&groupBy=${groupBy}`).then(r => r.json() as Promise<{ points: { bucket: string; groups: Record<string, { cost: number; tokens: number; sessions: number }> }[] }>),
83
110
  pricing: () => fetch('/api/pricing').then(r => r.json() as Promise<{ version: string }>),
@@ -1,10 +1,10 @@
1
1
  import * as echarts from 'echarts/core';
2
2
  import { LineChart, BarChart, HeatmapChart, PieChart } from 'echarts/charts';
3
- import { GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
3
+ import { GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent, DataZoomComponent } from 'echarts/components';
4
4
  import { CanvasRenderer } from 'echarts/renderers';
5
5
  import { tok } from './format';
6
6
 
7
- echarts.use([LineChart, BarChart, HeatmapChart, PieChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent, CanvasRenderer]);
7
+ echarts.use([LineChart, BarChart, HeatmapChart, PieChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, VisualMapComponent, MarkLineComponent, DataZoomComponent, CanvasRenderer]);
8
8
 
9
9
  const THEME = {
10
10
  textStyle: { color: '#9ba6b3', fontFamily: '-apple-system, system-ui, sans-serif' },
@@ -178,23 +178,68 @@ export function calendarHeatmap(el: HTMLElement, days: { day: string; value: num
178
178
  return c;
179
179
  }
180
180
 
181
- export function trendsLine(el: HTMLElement, points: { bucket: string; groups: Record<string, { cost: number }> }[]) {
181
+ export function turnTokensBar(
182
+ el: HTMLElement,
183
+ turns: { sequence: number; tokens: number; cacheRead: number; hasError: boolean }[],
184
+ onBarClick: (sequence: number) => void,
185
+ ) {
186
+ const c = makeChart(el);
187
+ c.setOption({
188
+ ...THEME,
189
+ // Animating hundreds of bars on load and on every zoom drag visibly
190
+ // janks large sessions.
191
+ animation: turns.length <= 200,
192
+ tooltip: {
193
+ ...THEME.tooltip,
194
+ trigger: 'item',
195
+ formatter: (p: any) => {
196
+ const t = turns[p.dataIndex];
197
+ return `Turn #${t.sequence}<br>${tok(t.tokens)} fresh + output<br><span style="color:#6b7585;">${tok(t.cacheRead)} cache read (context)</span>${t.hasError ? '<br><span style="color:#f85149;">contains a failed tool call</span>' : ''}`;
198
+ },
199
+ },
200
+ grid: { left: 50, right: 18, top: 12, bottom: 52 },
201
+ xAxis: { type: 'category', data: turns.map(t => '#' + t.sequence), ...AXIS },
202
+ yAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: (v: number) => tok(v) }, splitLine: AXIS.splitLine, axisLine: { show: false } },
203
+ dataZoom: [
204
+ { type: 'inside' },
205
+ {
206
+ type: 'slider', height: 16, bottom: 8,
207
+ borderColor: '#262d3a', backgroundColor: '#11151c',
208
+ fillerColor: 'rgba(88,166,255,0.15)',
209
+ handleStyle: { color: '#58a6ff' },
210
+ textStyle: { color: '#6b7585', fontSize: 9 },
211
+ },
212
+ ],
213
+ series: [{
214
+ type: 'bar',
215
+ barMaxWidth: 26,
216
+ data: turns.map(t => ({
217
+ value: t.tokens,
218
+ itemStyle: { color: t.hasError ? '#f85149' : '#58a6ff', borderRadius: [3, 3, 0, 0] },
219
+ })),
220
+ }],
221
+ });
222
+ c.on('click', (p: any) => onBarClick(turns[p.dataIndex].sequence));
223
+ return c;
224
+ }
225
+
226
+ export function trendsLine(el: HTMLElement, points: { bucket: string; groups: Record<string, { tokens: number }> }[]) {
182
227
  const c = makeChart(el);
183
228
  const allKeys = [...new Set(points.flatMap(p => Object.keys(p.groups)))];
184
229
  const palette = ['#f0883e', '#58a6ff', '#7ee787', '#d29922', '#bc8cff', '#f85149'];
185
230
  c.setOption({
186
231
  ...THEME,
187
- tooltip: { ...THEME.tooltip, trigger: 'axis', valueFormatter: (v: number) => '$' + v.toFixed(2) },
232
+ tooltip: { ...THEME.tooltip, trigger: 'axis', valueFormatter: (v: number) => tok(v) + ' tokens' },
188
233
  legend: { data: allKeys, textStyle: { color: '#9ba6b3', fontSize: 11 }, top: 0 },
189
- grid: { left: 50, right: 18, top: 38, bottom: 30 },
234
+ grid: { left: 56, right: 18, top: 38, bottom: 30 },
190
235
  xAxis: { type: 'category', data: points.map(p => p.bucket), ...AXIS },
191
- yAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: '${value}' }, splitLine: AXIS.splitLine, axisLine: { show: false } },
236
+ yAxis: { type: 'value', axisLabel: { ...AXIS.axisLabel, formatter: (v: number) => tok(v) }, splitLine: AXIS.splitLine, axisLine: { show: false } },
192
237
  series: allKeys.map((k, i) => ({
193
238
  type: 'line', name: k,
194
239
  smooth: true, symbol: 'circle', symbolSize: 5,
195
240
  lineStyle: { color: palette[i % palette.length], width: 2 },
196
241
  itemStyle: { color: palette[i % palette.length] },
197
- data: points.map(p => +(p.groups[k]?.cost ?? 0).toFixed(4)),
242
+ data: points.map(p => p.groups[k]?.tokens ?? 0),
198
243
  })),
199
244
  });
200
245
  return c;