argus-code 0.3.0__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.
- {argus_code-0.3.0 → argus_code-0.4.0}/.gitignore +3 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/PKG-INFO +2 -2
- {argus_code-0.3.0 → argus_code-0.4.0}/README.md +1 -1
- argus_code-0.4.0/dashboard/src/pages/session.astro +479 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/scripts/api.ts +27 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/scripts/charts.ts +52 -7
- argus_code-0.4.0/dashboard-dist/_astro/charts.CQF9QNgP.js +1 -0
- argus_code-0.4.0/dashboard-dist/_astro/format.CgLh3UFa.js +1 -0
- argus_code-0.3.0/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 +1 -1
- argus_code-0.3.0/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
- argus_code-0.3.0/dashboard-dist/_astro/installCanvasRenderer.D_tC6TXz.js → argus_code-0.4.0/dashboard-dist/_astro/installCanvasRenderer.XK5yP7fW.js +18 -18
- argus_code-0.3.0/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
- argus_code-0.3.0/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
- argus_code-0.4.0/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.IkwnKMSS.js +114 -0
- argus_code-0.3.0/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 +1 -1
- argus_code-0.3.0/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
- argus_code-0.3.0/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 +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/index.html +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/models/index.html +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/prompts/index.html +1 -1
- argus_code-0.4.0/dashboard-dist/session/index.html +2 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/sessions/index.html +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/settings/index.html +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/tools/index.html +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/trends/index.html +1 -1
- argus_code-0.4.0/pricing/2026-06-12.json +27 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/pyproject.toml +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/__init__.py +1 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/base.py +1 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/discover.py +36 -2
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_transcript.py +2 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/first_run.py +15 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/pipeline.py +85 -27
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/pricing/load.py +13 -5
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/schema/types.py +1 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/server/api.py +18 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/store/db.py +5 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/store/migrations/inline.py +5 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/store/repository.py +150 -9
- argus_code-0.4.0/tests/adapters/claude_code/test_discover.py +68 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_transcript.py +23 -0
- argus_code-0.4.0/tests/collector/test_first_run.py +207 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/test_pipeline.py +129 -0
- argus_code-0.4.0/tests/pricing/test_load.py +43 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/server/test_api.py +55 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/store/test_db.py +17 -3
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/store/test_repository.py +224 -1
- {argus_code-0.3.0 → argus_code-0.4.0}/uv.lock +1 -1
- argus_code-0.3.0/dashboard/src/pages/session.astro +0 -237
- argus_code-0.3.0/dashboard-dist/_astro/charts.CAJCDcsn.js +0 -1
- argus_code-0.3.0/dashboard-dist/_astro/format.DxC1NGYT.js +0 -1
- argus_code-0.3.0/dashboard-dist/_astro/session.astro_astro_type_script_index_0_lang.Dj_bfrIa.js +0 -86
- argus_code-0.3.0/dashboard-dist/session/index.html +0 -2
- argus_code-0.3.0/tests/adapters/claude_code/test_discover.py +0 -31
- argus_code-0.3.0/tests/collector/test_first_run.py +0 -77
- argus_code-0.3.0/tests/pricing/test_load.py +0 -16
- {argus_code-0.3.0 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/.github/pull_request_template.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/.npmrc +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/ARCHITECTURE.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/CONTRIBUTING.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/LICENSE +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/NOTICE +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/PRD.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/SECURITY.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/TESTING.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/.astro/content-assets.mjs +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/.astro/content-modules.mjs +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/.astro/content.d.ts +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/.astro/types.d.ts +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/astro.config.mjs +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/package-lock.json +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/package.json +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/public/styles/global.css +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/layouts/Default.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/index.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/models.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/prompts.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/sessions/index.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/settings.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/tools.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/pages/trends.astro +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/scripts/alerts.ts +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/scripts/format.ts +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/src/styles/global.css +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard/tsconfig.json +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/dashboard-dist/styles/global.css +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/pricing/2026-05-02.json +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/adapter.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_tool_calls.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/extract_turns.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/history_jsonl.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/ingest_file.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/model.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/claude_code/schemas.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/adapters/registry.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/cli.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/aggregate.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/rollup_subagents.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/scheduler.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/search_backfill.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/collector/watcher.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/core/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/core/runtime.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/daemon/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/daemon/logging.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/daemon/pidfile.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/daemon/process.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/daemon/service.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/detectors/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/detectors/base.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/detectors/registry.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/detectors/tool_error_rate_spike.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/pricing/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/pricing/compute.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/pricing/refresh.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/pricing/types.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/scaffold/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/scaffold/scaffolder.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/scaffold/snapshot.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/scaffold/storage.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/schema/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/server/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/server/app.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/store/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/python/argus/store/migrations/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/agents/code-reviewer.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/agents/security-auditor.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/commands/commit.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/commands/deploy.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/commands/fix-issue.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/commands/pr.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/commands/review.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/rules/api-conventions.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/rules/code-style.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/rules/testing.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/settings.json +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/.claude/skills/example/SKILL.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/templates/default/CLAUDE.md +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_adapter.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_tool_calls.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_extract_turns.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_history_jsonl.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_ingest_file.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_integration.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_model.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/claude_code/test_schemas.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/adapters/test_registry.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/test_aggregate.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/test_rollup_subagents.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/test_scheduler.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/collector/test_watcher.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/conftest.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/core/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/core/test_runtime.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/test_logs.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/test_pidfile.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/test_process.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/test_service.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/daemon/test_yield.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/detectors/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/detectors/test_registry.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/detectors/test_tool_error_rate_spike.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/pricing/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/pricing/test_compute.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/pricing/test_refresh.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/test_bundled_template.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/test_cli_claude.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/test_scaffolder.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/test_snapshot.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/scaffold/test_storage.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/schema/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/schema/test_types.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/server/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/server/test_api_search.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/server/test_server.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/server/test_week_of.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/store/__init__.py +0 -0
- {argus_code-0.3.0 → argus_code-0.4.0}/tests/test_e2e.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: argus-code
|
|
3
|
-
Version: 0.
|
|
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
|
|
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) =>
|
|
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:
|
|
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:
|
|
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 =>
|
|
242
|
+
data: points.map(p => p.groups[k]?.tokens ?? 0),
|
|
198
243
|
})),
|
|
199
244
|
});
|
|
200
245
|
return c;
|