@pennyfarthing/core 11.1.1 → 11.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/package.json +2 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +381 -66
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.js +4 -4
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +4 -5
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts +3 -8
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +3 -4
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/cli/utils/settings.d.ts +11 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +65 -29
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/cli/utils/symlinks.js +16 -16
- package/packages/core/dist/cli/utils/symlinks.js.map +1 -1
- package/packages/core/dist/consultation/tandem-metrics.d.ts +91 -0
- package/packages/core/dist/consultation/tandem-metrics.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.js +131 -0
- package/packages/core/dist/consultation/tandem-metrics.js.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts +18 -0
- package/packages/core/dist/consultation/tandem-metrics.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js +457 -0
- package/packages/core/dist/consultation/tandem-metrics.test.js.map +1 -0
- package/packages/core/dist/public/js/react/react.js +14 -14
- package/packages/core/dist/scripts/benchmark-integration.d.ts +182 -0
- package/packages/core/dist/scripts/benchmark-integration.d.ts.map +1 -0
- package/packages/core/dist/scripts/benchmark-integration.js +691 -0
- package/packages/core/dist/scripts/benchmark-integration.js.map +1 -0
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts +150 -0
- package/packages/core/dist/scripts/job-fair-aggregator.d.ts.map +1 -0
- package/packages/core/dist/scripts/job-fair-aggregator.js +547 -0
- package/packages/core/dist/scripts/job-fair-aggregator.js.map +1 -0
- package/packages/core/dist/server/api/agent-load.js +1 -1
- package/packages/core/dist/server/api/agent-load.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
- package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
- package/packages/core/dist/server/otlp-receiver.js +185 -24
- package/packages/core/dist/server/otlp-receiver.js.map +1 -1
- package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
- package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
- package/packages/core/dist/server/otlp-receiver.test.js +446 -0
- package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
- package/packages/core/dist/server/server.d.ts +0 -3
- package/packages/core/dist/server/server.d.ts.map +1 -1
- package/packages/core/dist/server/server.js +3 -37
- package/packages/core/dist/server/server.js.map +1 -1
- package/packages/core/dist/server/server.test.d.ts +1 -1
- package/packages/core/dist/server/server.test.js +12 -23
- package/packages/core/dist/server/server.test.js.map +1 -1
- package/packages/core/dist/shared/capabilities.d.ts +88 -0
- package/packages/core/dist/shared/capabilities.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.js +133 -0
- package/packages/core/dist/shared/capabilities.js.map +1 -0
- package/packages/core/dist/shared/capabilities.test.d.ts +2 -0
- package/packages/core/dist/shared/capabilities.test.d.ts.map +1 -0
- package/packages/core/dist/shared/capabilities.test.js +217 -0
- package/packages/core/dist/shared/capabilities.test.js.map +1 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
- package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.js +27 -0
- package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
- package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
- package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
- package/packages/core/dist/shared/spawn-prompt.d.ts +47 -0
- package/packages/core/dist/shared/spawn-prompt.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.js +82 -0
- package/packages/core/dist/shared/spawn-prompt.js.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts +2 -0
- package/packages/core/dist/shared/spawn-prompt.test.d.ts.map +1 -0
- package/packages/core/dist/shared/spawn-prompt.test.js +251 -0
- package/packages/core/dist/shared/spawn-prompt.test.js.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
- package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts +18 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.d.ts.map +1 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js +434 -0
- package/packages/core/dist/workflow/tandem-workflow-templates.test.js.map +1 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts +32 -0
- package/packages/core/dist/workflow/workflow-schema.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.js +120 -0
- package/packages/core/dist/workflow/workflow-schema.js.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.d.ts.map +1 -1
- package/packages/core/dist/workflow/workflow-schema.test.js +570 -1
- package/packages/core/dist/workflow/workflow-schema.test.js.map +1 -1
- package/pennyfarthing-dist/agents/dev.md +7 -11
- package/pennyfarthing-dist/agents/reviewer.md +9 -3
- package/pennyfarthing-dist/agents/sm-finish.md +18 -1
- package/pennyfarthing-dist/agents/sm-setup.md +1 -1
- package/pennyfarthing-dist/agents/sm.md +2 -2
- package/pennyfarthing-dist/agents/tea.md +1 -1
- package/pennyfarthing-dist/agents/testing-runner.md +2 -1
- package/pennyfarthing-dist/commands/pf-chore.md +2 -2
- package/pennyfarthing-dist/commands/pf-git.md +4 -2
- package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
- package/pennyfarthing-dist/gates/approval.md +63 -0
- package/pennyfarthing-dist/gates/confidence-sm.md +71 -0
- package/pennyfarthing-dist/gates/context-ok.md +56 -0
- package/pennyfarthing-dist/gates/evaluations/confidence-sm.md +54 -0
- package/pennyfarthing-dist/gates/quality-pass.md +67 -0
- package/pennyfarthing-dist/gates/tests-fail.md +84 -0
- package/pennyfarthing-dist/gates/tests-pass.md +79 -0
- package/pennyfarthing-dist/guides/agent-behavior.md +23 -19
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bell-mode.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +29 -29
- package/pennyfarthing-dist/guides/reflector.md +1 -1
- package/pennyfarthing-dist/guides/tandem-protocol.md +3 -3
- package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +1 -1
- package/pennyfarthing-dist/scripts/core/check-context.sh +3 -1
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +5 -87
- package/pennyfarthing-dist/scripts/git/README.md +24 -14
- package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/hooks/README.md +6 -6
- package/pennyfarthing-dist/scripts/hooks/__pycache__/question_reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +4 -183
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +4 -95
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +3 -31
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +27 -33
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +4 -71
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +3 -19
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +4 -30
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +3 -32
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +4 -65
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +4 -78
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +4 -93
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/statusline.sh +4 -301
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
- package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
- package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
- package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
- package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
- package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
- package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
- package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
- package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
- package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
- package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
- package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
- package/pennyfarthing-dist/templates/settings.local.json.template +19 -10
- package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
- package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
- package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
- package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
- package/pennyfarthing-dist/workflows/tdd.yaml +11 -2
- package/pennyfarthing_scripts/CLAUDE.md +45 -14
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/bellmode_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/context.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_bidirectional_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_epic_creation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/jira_sync_story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/patch_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-311.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/focus.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- package/pennyfarthing_scripts/bellmode_hook.py +12 -296
- package/pennyfarthing_scripts/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/audit_log_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/context_meter_footer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/events.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/audit_log_panel.py +119 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +87 -2
- package/pennyfarthing_scripts/bikerack/changed_panel.py +125 -29
- package/pennyfarthing_scripts/bikerack/context_meter_footer.py +88 -0
- package/pennyfarthing_scripts/bikerack/debug_panel.py +32 -2
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +104 -17
- package/pennyfarthing_scripts/bikerack/events.py +28 -0
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/portrait_resolver.py +139 -0
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +395 -32
- package/pennyfarthing_scripts/bikerack/story_detail_data.py +244 -0
- package/pennyfarthing_scripts/bikerack/story_detail_screen.py +176 -0
- package/pennyfarthing_scripts/bikerack/tui.py +575 -37
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/brownfield/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/brownfield/__pycache__/discover.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +42 -65
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/output.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/pr_config.py +38 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__init__.py +1 -0
- package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/__pycache__/dialogue_manager.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/consultation/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__init__.py +12 -1
- package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/create_branches.py +3 -4
- package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
- package/pennyfarthing_scripts/git/repos.py +196 -0
- package/pennyfarthing_scripts/git/status_all.py +27 -11
- package/pennyfarthing_scripts/git/worktree.py +302 -0
- package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git_group/cli.py +143 -40
- package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_file.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/gate_runner.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/marker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/cli.py +33 -1
- package/pennyfarthing_scripts/handoff/complete_phase.py +40 -0
- package/pennyfarthing_scripts/handoff/marker.py +15 -15
- package/pennyfarthing_scripts/handoff/phase_check.py +96 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +18 -15
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__init__.py +437 -0
- package/pennyfarthing_scripts/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hooks/bell_mode.py +215 -0
- package/pennyfarthing_scripts/hooks/cli.py +96 -0
- package/pennyfarthing_scripts/hooks/context_breaker.py +104 -0
- package/pennyfarthing_scripts/hooks/context_warning.py +66 -0
- package/pennyfarthing_scripts/hooks/cyclist_pretooluse.py +129 -0
- package/pennyfarthing_scripts/hooks/pre_edit_check.py +78 -0
- package/pennyfarthing_scripts/hooks/reflector_check.py +271 -0
- package/pennyfarthing_scripts/hooks/schema_validation.py +203 -0
- package/pennyfarthing_scripts/hooks/session_start.py +296 -0
- package/pennyfarthing_scripts/hooks/session_stop.py +111 -0
- package/pennyfarthing_scripts/hooks/sprint_yaml_validation.py +97 -0
- package/pennyfarthing_scripts/hooks/statusline.py +420 -0
- package/pennyfarthing_scripts/hooks.py +27 -446
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/claim.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/compat.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/mappings.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/story.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/sync.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/launch/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/launch/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/skill.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/step.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/migration/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/preflight/__pycache__/finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/pretooluse_hook.py +3 -185
- package/pennyfarthing_scripts/prime/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/session.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/version_sentinel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/heatmap.py +655 -0
- package/pennyfarthing_scripts/prime/workflow.py +2 -1
- package/pennyfarthing_scripts/schema_validation_hook.py +3 -298
- package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/session_start_hook.py +4 -186
- package/pennyfarthing_scripts/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/import_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/status.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_update.py +19 -0
- package/pennyfarthing_scripts/story/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/size.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/story/__pycache__/template.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_108_1_gate_migration.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_archive_epic.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bc.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_bikerack.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_brownfield.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_modules.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_cli_normalization.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_common.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_evaluation.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_confidence_sm_gate.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_dialogue_manager.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_epic_shard_validation.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_git_utils.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_jira_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_package_structure.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_patch_mode.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_prime.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_panel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tiers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_token_counting.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_topology_loader.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_focus.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_tui_panel_persistence.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_version_sentinel.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_workflow_cli.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
- package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
- package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
- package/pennyfarthing_scripts/tests/test_sprint_panel.py +344 -265
- package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/agent.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/schema.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/skill_command.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/sprint.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/tandem_awareness.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/adapters/workflow.py +19 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- package/pennyfarthing_scripts/welcome_hook.py +3 -149
- package/pennyfarthing_scripts/workflow/__init__.py +40 -0
- package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/workflow/cli.py +1100 -0
- package/pennyfarthing_scripts/workflow/helpers.py +241 -0
- package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
- package/pennyfarthing_scripts/workflow/state.py +112 -0
- package/pennyfarthing_scripts/workflow/team_lifecycle.py +257 -0
- package/packages/core/dist/scripts/theme-detail.test.d.ts +0 -10
- package/packages/core/dist/scripts/theme-detail.test.js +0 -199
- package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
- package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
- package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
- package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
- package/pennyfarthing-dist/skills/pf-workflow/scripts/workflow-status.sh +0 -167
- package/pennyfarthing_scripts/gate/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/gate/__pycache__/validate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_108_2_remove_handoff_fallback.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_file_resolution.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_gate_runner.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_resolve_gate_file_field.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from textual.message import Message
|
|
12
14
|
from textual.widgets import Static
|
|
13
15
|
|
|
14
16
|
# Nerd Font icon registry: panel_name → (nerd_font_icon, ascii_fallback)
|
|
@@ -25,6 +27,7 @@ PANEL_ICONS: dict[str, tuple[str, str]] = {
|
|
|
25
27
|
"debug": ("\uf188", "d"), # nf-fa-bug
|
|
26
28
|
"settings": ("\uf013", "S"), # nf-fa-gear
|
|
27
29
|
"tty": ("\uf120", ">"), # nf-fa-terminal
|
|
30
|
+
"progress": ("\uf200", "P"), # nf-fa-pie_chart
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
|
|
@@ -44,6 +47,74 @@ def get_panel_icon(panel_name: str, use_nerd_font: bool = True) -> str:
|
|
|
44
47
|
return entry[0] if use_nerd_font else entry[1]
|
|
45
48
|
|
|
46
49
|
|
|
50
|
+
def render_progress_bar(
|
|
51
|
+
percent: int | float,
|
|
52
|
+
width: int = 20,
|
|
53
|
+
warn_high: bool = False,
|
|
54
|
+
fill_style: str | None = None,
|
|
55
|
+
) -> Text:
|
|
56
|
+
"""Render a Unicode progress bar with color based on percentage.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
percent: Value 0-100.
|
|
60
|
+
width: Number of bar characters (default 20).
|
|
61
|
+
warn_high: If True, use red at high values (for resource usage).
|
|
62
|
+
If False (default), use blue at 100% (for completion).
|
|
63
|
+
fill_style: Override the computed fill color (e.g. ``"dim green"``).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Rich Text like ``[████████░░░░░░░░░░░░] 22%``
|
|
67
|
+
"""
|
|
68
|
+
percent = max(0, min(100, int(percent)))
|
|
69
|
+
filled = round(width * percent / 100)
|
|
70
|
+
empty = width - filled
|
|
71
|
+
|
|
72
|
+
if fill_style is not None:
|
|
73
|
+
style = fill_style
|
|
74
|
+
elif warn_high:
|
|
75
|
+
if percent < 50:
|
|
76
|
+
style = "green"
|
|
77
|
+
elif percent <= 80:
|
|
78
|
+
style = "yellow"
|
|
79
|
+
else:
|
|
80
|
+
style = "red"
|
|
81
|
+
else:
|
|
82
|
+
style = "blue"
|
|
83
|
+
|
|
84
|
+
bar = Text()
|
|
85
|
+
bar.append("[")
|
|
86
|
+
bar.append("█" * filled, style=style)
|
|
87
|
+
bar.append("░" * empty, style="dim")
|
|
88
|
+
bar.append(f"] {percent}%")
|
|
89
|
+
return bar
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_duration(seconds: int | float) -> str:
|
|
93
|
+
"""Format seconds into human-friendly duration string.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
``47s``, ``2m 14s``, ``1h 5m``.
|
|
97
|
+
"""
|
|
98
|
+
seconds = max(0, int(seconds))
|
|
99
|
+
if seconds < 60:
|
|
100
|
+
return f"{seconds}s"
|
|
101
|
+
minutes, secs = divmod(seconds, 60)
|
|
102
|
+
if minutes < 60:
|
|
103
|
+
return f"{minutes}m {secs}s"
|
|
104
|
+
hours, mins = divmod(minutes, 60)
|
|
105
|
+
return f"{hours}h {mins}m"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def humanize_theme(slug: str) -> str:
|
|
109
|
+
"""Convert a theme slug to a display name.
|
|
110
|
+
|
|
111
|
+
``princess-bride`` → ``Princess Bride``
|
|
112
|
+
"""
|
|
113
|
+
if not slug:
|
|
114
|
+
return ""
|
|
115
|
+
return slug.replace("-", " ").replace("_", " ").title()
|
|
116
|
+
|
|
117
|
+
|
|
47
118
|
class BasePanel(Static):
|
|
48
119
|
"""Base class for BikeRack TUI panels.
|
|
49
120
|
|
|
@@ -51,6 +122,13 @@ class BasePanel(Static):
|
|
|
51
122
|
``render_panel(payload)`` to return a Rich renderable.
|
|
52
123
|
"""
|
|
53
124
|
|
|
125
|
+
class DataReceived(Message, bubble=False):
|
|
126
|
+
"""WebSocket data received — routed through Textual message system."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, content: Any) -> None:
|
|
129
|
+
super().__init__()
|
|
130
|
+
self.content = content
|
|
131
|
+
|
|
54
132
|
#: WebSocket channel this panel subscribes to (override in subclass)
|
|
55
133
|
channel: str = ""
|
|
56
134
|
|
|
@@ -76,10 +154,17 @@ class BasePanel(Static):
|
|
|
76
154
|
"""Mark panel as unmounted — messages ignored after this."""
|
|
77
155
|
self._mounted = False
|
|
78
156
|
|
|
157
|
+
def on_base_panel_data_received(self, event: DataReceived) -> None:
|
|
158
|
+
"""Process DataReceived in Textual message context — triggers repaint."""
|
|
159
|
+
self.update(event.content)
|
|
160
|
+
|
|
79
161
|
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
80
162
|
"""Handle incoming WebSocket message.
|
|
81
163
|
|
|
82
|
-
Stores payload, calls render_panel,
|
|
164
|
+
Stores payload, calls render_panel, posts DataReceived message.
|
|
165
|
+
Uses post_message to route through Textual's message system,
|
|
166
|
+
ensuring proper repaint cycle (call_from_thread / direct update
|
|
167
|
+
from async WS tasks does not reliably trigger redraws).
|
|
83
168
|
No-op after unmount or if message is None.
|
|
84
169
|
"""
|
|
85
170
|
if not self._mounted or message is None:
|
|
@@ -87,7 +172,7 @@ class BasePanel(Static):
|
|
|
87
172
|
self._last_payload = message
|
|
88
173
|
rendered = self.render_panel(message)
|
|
89
174
|
try:
|
|
90
|
-
self.
|
|
175
|
+
self.post_message(self.DataReceived(rendered))
|
|
91
176
|
except Exception:
|
|
92
177
|
pass
|
|
93
178
|
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Story 103-14: Subscribes to /ws/git, extracts dirtyFiles from all repos,
|
|
4
4
|
renders Rich table with file path, change type icon, and status.
|
|
5
|
+
|
|
6
|
+
Story 110-1: Selectable file list with arrow navigation and Enter to navigate.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from __future__ import annotations
|
|
8
10
|
|
|
9
11
|
from typing import Any
|
|
10
12
|
|
|
11
|
-
from rich.table import Table
|
|
12
13
|
from rich.text import Text
|
|
14
|
+
from textual.binding import Binding
|
|
13
15
|
|
|
14
16
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
15
17
|
|
|
@@ -56,50 +58,144 @@ class ChangedPanel(BasePanel):
|
|
|
56
58
|
|
|
57
59
|
Subscribes to the ``git`` WebSocket channel and renders
|
|
58
60
|
dirty files from all repos as a Rich table with file path,
|
|
59
|
-
change type icon, and status.
|
|
61
|
+
change type icon, and status. Supports arrow-key selection
|
|
62
|
+
and Enter to navigate to diffs.
|
|
60
63
|
"""
|
|
61
64
|
|
|
62
65
|
channel: str = "git"
|
|
63
66
|
panel_name: str = "Changed"
|
|
64
67
|
icon: str = PANEL_ICONS["changed"][0]
|
|
68
|
+
can_focus = True
|
|
69
|
+
|
|
70
|
+
BINDINGS = [
|
|
71
|
+
Binding("up", "select_prev_key", "Up"),
|
|
72
|
+
Binding("down", "select_next_key", "Down"),
|
|
73
|
+
Binding("enter", "select_file", "Select file"),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
def __init__(self, client=None, **kwargs):
|
|
77
|
+
super().__init__(client=client, **kwargs)
|
|
78
|
+
self._selected_index: int = 0
|
|
79
|
+
self._file_paths: list[str] = []
|
|
80
|
+
|
|
81
|
+
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
82
|
+
"""Handle incoming message — build file path index then render."""
|
|
83
|
+
if message is not None:
|
|
84
|
+
self._build_file_paths(message)
|
|
85
|
+
super().handle_message(message)
|
|
86
|
+
|
|
87
|
+
def _build_file_paths(self, payload: dict[str, Any]) -> None:
|
|
88
|
+
"""Extract flat list of file paths from repos payload."""
|
|
89
|
+
paths: list[str] = []
|
|
90
|
+
repos = payload.get("repos", [])
|
|
91
|
+
if isinstance(repos, list):
|
|
92
|
+
for repo in repos:
|
|
93
|
+
if not isinstance(repo, dict):
|
|
94
|
+
continue
|
|
95
|
+
dirty_files = repo.get("dirtyFiles", [])
|
|
96
|
+
if not isinstance(dirty_files, list):
|
|
97
|
+
continue
|
|
98
|
+
for f in dirty_files:
|
|
99
|
+
if isinstance(f, dict):
|
|
100
|
+
path = f.get("path", "")
|
|
101
|
+
if path:
|
|
102
|
+
paths.append(path)
|
|
103
|
+
self._file_paths = paths
|
|
104
|
+
if self._selected_index >= len(paths):
|
|
105
|
+
self._selected_index = max(0, len(paths) - 1)
|
|
106
|
+
|
|
107
|
+
def select_next(self) -> None:
|
|
108
|
+
"""Move selection to the next file."""
|
|
109
|
+
if self._file_paths and self._selected_index < len(self._file_paths) - 1:
|
|
110
|
+
self._selected_index += 1
|
|
111
|
+
self._rerender()
|
|
112
|
+
|
|
113
|
+
def select_prev(self) -> None:
|
|
114
|
+
"""Move selection to the previous file."""
|
|
115
|
+
if self._selected_index > 0:
|
|
116
|
+
self._selected_index -= 1
|
|
117
|
+
self._rerender()
|
|
118
|
+
|
|
119
|
+
def action_select_next_key(self) -> None:
|
|
120
|
+
"""Binding action: move selection down."""
|
|
121
|
+
self.select_next()
|
|
122
|
+
|
|
123
|
+
def action_select_prev_key(self) -> None:
|
|
124
|
+
"""Binding action: move selection up."""
|
|
125
|
+
self.select_prev()
|
|
126
|
+
|
|
127
|
+
def _rerender(self) -> None:
|
|
128
|
+
"""Re-render panel with current payload after selection change."""
|
|
129
|
+
if self._last_payload:
|
|
130
|
+
try:
|
|
131
|
+
self.update(self.render_panel(self._last_payload))
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def get_selected_path(self) -> str | None:
|
|
136
|
+
"""Return the currently selected file path, or None if empty."""
|
|
137
|
+
if not self._file_paths:
|
|
138
|
+
return None
|
|
139
|
+
if self._selected_index >= len(self._file_paths):
|
|
140
|
+
return None
|
|
141
|
+
return self._file_paths[self._selected_index]
|
|
142
|
+
|
|
143
|
+
def action_select_file(self) -> None:
|
|
144
|
+
"""Post NavigateToFile event for the selected file."""
|
|
145
|
+
path = self.get_selected_path()
|
|
146
|
+
if path is not None:
|
|
147
|
+
from pennyfarthing_scripts.bikerack.events import NavigateToFile
|
|
148
|
+
|
|
149
|
+
self.post_message(NavigateToFile(path=path))
|
|
65
150
|
|
|
66
151
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
67
|
-
"""Render changed
|
|
152
|
+
"""Render changed files grouped by repository with selection highlight."""
|
|
68
153
|
repos = payload.get("repos", [])
|
|
69
154
|
if not isinstance(repos, list):
|
|
70
155
|
return Text("No changed files", style="dim italic")
|
|
71
156
|
|
|
72
|
-
|
|
157
|
+
# Group files by repo
|
|
158
|
+
repo_files: dict[str, list[dict[str, Any]]] = {}
|
|
73
159
|
for repo in repos:
|
|
74
160
|
if not isinstance(repo, dict):
|
|
75
161
|
continue
|
|
76
|
-
repo_name = repo.get("name", "")
|
|
162
|
+
repo_name = repo.get("name", "unknown")
|
|
77
163
|
dirty_files = repo.get("dirtyFiles", [])
|
|
78
|
-
if not isinstance(dirty_files, list):
|
|
164
|
+
if not isinstance(dirty_files, list) or not dirty_files:
|
|
79
165
|
continue
|
|
80
|
-
for f in dirty_files
|
|
81
|
-
if not isinstance(f, dict):
|
|
82
|
-
continue
|
|
83
|
-
files.append((repo_name, f))
|
|
166
|
+
repo_files[repo_name] = [f for f in dirty_files if isinstance(f, dict)]
|
|
84
167
|
|
|
85
|
-
if not
|
|
168
|
+
if not repo_files:
|
|
86
169
|
return Text("No changed files", style="dim italic")
|
|
87
170
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
171
|
+
from rich.console import Group as RichGroup
|
|
172
|
+
|
|
173
|
+
parts: list[Any] = []
|
|
174
|
+
flat_idx = 0
|
|
175
|
+
for repo_name, files in repo_files.items():
|
|
176
|
+
count = len(files)
|
|
177
|
+
label = "file" if count == 1 else "files"
|
|
178
|
+
header = Text()
|
|
179
|
+
header.append(repo_name, style="bold cyan")
|
|
180
|
+
header.append(f" ({count} {label})", style="dim")
|
|
181
|
+
parts.append(header)
|
|
182
|
+
|
|
183
|
+
for f in files:
|
|
184
|
+
status_code = f.get("status", " ")
|
|
185
|
+
path = f.get("path", "")
|
|
186
|
+
icon, label_text, style = _parse_status(status_code)
|
|
187
|
+
is_selected = flat_idx == self._selected_index
|
|
188
|
+
line = Text()
|
|
189
|
+
if is_selected:
|
|
190
|
+
line.append("› ", style="bold reverse")
|
|
191
|
+
else:
|
|
192
|
+
line.append(" ")
|
|
193
|
+
line.append(icon, style=f"bold {style}")
|
|
194
|
+
line.append(f" {path}", style="bold cyan reverse" if is_selected else "cyan")
|
|
195
|
+
line.append(f" {label_text}", style=style)
|
|
196
|
+
parts.append(line)
|
|
197
|
+
flat_idx += 1
|
|
198
|
+
|
|
199
|
+
parts.append(Text("")) # spacer between repos
|
|
200
|
+
|
|
201
|
+
return RichGroup(*parts)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""ContextMeterFooter — Persistent context usage footer bar for BikeRack TUI.
|
|
2
|
+
|
|
3
|
+
Story 110-5: Context meter footer bar. Displays context window usage
|
|
4
|
+
percentage with color-coded tier thresholds, always visible at the
|
|
5
|
+
bottom of the layout.
|
|
6
|
+
|
|
7
|
+
Subscribes to /ws/context WebSocket channel.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from textual.message import Message
|
|
16
|
+
from textual.widgets import Static
|
|
17
|
+
|
|
18
|
+
from pennyfarthing_scripts.bikerack.base_panel import render_progress_bar
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContextMeterFooter(Static):
|
|
22
|
+
"""Persistent footer bar showing context window usage.
|
|
23
|
+
|
|
24
|
+
Not a Footer subclass — this is a Static widget mounted between
|
|
25
|
+
#main-content and BindingFooter in the app layout.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
class MeterUpdate(Message, bubble=False):
|
|
29
|
+
"""Context meter data received — routed through Textual message system."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, content: Any) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.content = content
|
|
34
|
+
|
|
35
|
+
#: WebSocket channel this footer subscribes to
|
|
36
|
+
channel: str = "context"
|
|
37
|
+
|
|
38
|
+
def __init__(self, client: Any = None, **kwargs: Any) -> None:
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
self._client = client
|
|
41
|
+
self._context_data: dict[str, Any] | None = None
|
|
42
|
+
self._mounted = False
|
|
43
|
+
|
|
44
|
+
def on_mount(self) -> None:
|
|
45
|
+
"""Subscribe to context channel on mount."""
|
|
46
|
+
self._mounted = True
|
|
47
|
+
if self._client is not None:
|
|
48
|
+
self._client.subscribe("context", self.handle_context_message)
|
|
49
|
+
|
|
50
|
+
def on_unmount(self) -> None:
|
|
51
|
+
"""Mark as unmounted so further messages are ignored."""
|
|
52
|
+
self._mounted = False
|
|
53
|
+
|
|
54
|
+
def on_context_meter_footer_meter_update(self, event: MeterUpdate) -> None:
|
|
55
|
+
"""Process MeterUpdate in Textual message context — triggers repaint."""
|
|
56
|
+
self.update(event.content)
|
|
57
|
+
|
|
58
|
+
def handle_context_message(self, msg: dict[str, Any] | None) -> None:
|
|
59
|
+
"""Process incoming /ws/context message."""
|
|
60
|
+
if not self._mounted or msg is None:
|
|
61
|
+
return
|
|
62
|
+
ctx = msg.get("context")
|
|
63
|
+
if ctx is None:
|
|
64
|
+
return
|
|
65
|
+
self._context_data = ctx
|
|
66
|
+
try:
|
|
67
|
+
rendered = self.render_meter(ctx)
|
|
68
|
+
self.post_message(self.MeterUpdate(rendered))
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def render_meter(self, ctx: dict[str, Any]) -> Text:
|
|
73
|
+
"""Render a compact context usage bar with percentage and tier badge."""
|
|
74
|
+
percent = ctx.get("percent", 0)
|
|
75
|
+
tier = ctx.get("tier", "")
|
|
76
|
+
|
|
77
|
+
bar = render_progress_bar(percent, warn_high=True)
|
|
78
|
+
|
|
79
|
+
if tier:
|
|
80
|
+
if percent < 50:
|
|
81
|
+
tier_style = "green"
|
|
82
|
+
elif percent <= 80:
|
|
83
|
+
tier_style = "yellow"
|
|
84
|
+
else:
|
|
85
|
+
tier_style = "red"
|
|
86
|
+
bar.append(f" {tier}", style=f"bold {tier_style}")
|
|
87
|
+
|
|
88
|
+
return bar
|
|
@@ -7,13 +7,14 @@ token consumption stats (input, output, cache, cost).
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from collections import deque
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from rich.console import Group
|
|
13
14
|
from rich.table import Table
|
|
14
15
|
from rich.text import Text
|
|
15
16
|
|
|
16
|
-
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
17
|
+
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel, render_progress_bar
|
|
17
18
|
|
|
18
19
|
# Tier → Rich style mapping
|
|
19
20
|
_TIER_STYLES: dict[str, str] = {
|
|
@@ -68,6 +69,7 @@ class DebugPanel(BasePanel):
|
|
|
68
69
|
super().__init__(client=client, **kwargs)
|
|
69
70
|
self._context_data: dict[str, Any] | None = None
|
|
70
71
|
self._token_stats: dict[str, Any] | None = None
|
|
72
|
+
self._sparkline_history: deque[int] = deque(maxlen=20)
|
|
71
73
|
|
|
72
74
|
def on_mount(self) -> None:
|
|
73
75
|
"""Subscribe to both context and token-stats channels."""
|
|
@@ -83,6 +85,9 @@ class DebugPanel(BasePanel):
|
|
|
83
85
|
ctx = message.get("context")
|
|
84
86
|
if isinstance(ctx, dict):
|
|
85
87
|
self._context_data = ctx
|
|
88
|
+
pct = _safe_int(ctx.get("percent"))
|
|
89
|
+
if pct is not None:
|
|
90
|
+
self._sparkline_history.append(pct)
|
|
86
91
|
else:
|
|
87
92
|
self._context_data = {}
|
|
88
93
|
self._rerender()
|
|
@@ -98,7 +103,7 @@ class DebugPanel(BasePanel):
|
|
|
98
103
|
"""Re-render with the latest data from both channels."""
|
|
99
104
|
rendered = self.render_panel(self._context_data or {})
|
|
100
105
|
try:
|
|
101
|
-
self.
|
|
106
|
+
self._thread_safe_update(rendered)
|
|
102
107
|
except Exception:
|
|
103
108
|
pass
|
|
104
109
|
|
|
@@ -110,6 +115,8 @@ class DebugPanel(BasePanel):
|
|
|
110
115
|
ctx = self._context_data
|
|
111
116
|
if ctx:
|
|
112
117
|
parts.append(_render_context(ctx))
|
|
118
|
+
if len(self._sparkline_history) >= 2:
|
|
119
|
+
parts.append(_render_sparkline(self._sparkline_history))
|
|
113
120
|
elif not self._token_stats:
|
|
114
121
|
return Text("No context data", style="dim italic")
|
|
115
122
|
|
|
@@ -157,6 +164,10 @@ def _render_context(ctx: dict[str, Any]) -> Any:
|
|
|
157
164
|
usage_text.append(f" ({percent}%)")
|
|
158
165
|
parts.append(usage_text)
|
|
159
166
|
|
|
167
|
+
# Context usage progress bar
|
|
168
|
+
if percent is not None:
|
|
169
|
+
parts.append(render_progress_bar(percent, warn_high=True))
|
|
170
|
+
|
|
160
171
|
# Breakdown: baseline / conversation / available
|
|
161
172
|
if baseline is not None:
|
|
162
173
|
breakdown = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
|
|
@@ -176,6 +187,25 @@ def _render_context(ctx: dict[str, Any]) -> Any:
|
|
|
176
187
|
return Group(*parts)
|
|
177
188
|
|
|
178
189
|
|
|
190
|
+
_SPARKLINE_CHARS = "▁▂▃▄▅▆▇█"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _render_sparkline(history: deque[int]) -> Text:
|
|
194
|
+
"""Render a Unicode sparkline from context usage history."""
|
|
195
|
+
text = Text()
|
|
196
|
+
text.append("Context trend: ", style="dim")
|
|
197
|
+
for pct in history:
|
|
198
|
+
level = min(7, max(0, int(pct / 100 * 7.99)))
|
|
199
|
+
if pct < 50:
|
|
200
|
+
style = "green"
|
|
201
|
+
elif pct <= 80:
|
|
202
|
+
style = "yellow"
|
|
203
|
+
else:
|
|
204
|
+
style = "red"
|
|
205
|
+
text.append(_SPARKLINE_CHARS[level], style=style)
|
|
206
|
+
return text
|
|
207
|
+
|
|
208
|
+
|
|
179
209
|
def _render_token_stats(stats: dict[str, Any]) -> Any:
|
|
180
210
|
"""Render token stats section."""
|
|
181
211
|
table = Table(show_header=False, show_edge=False, pad_edge=False, box=None)
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
from rich.console import Group
|
|
18
18
|
from rich.syntax import Syntax
|
|
19
19
|
from rich.text import Text
|
|
20
|
+
from textual.binding import Binding
|
|
20
21
|
|
|
21
22
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
22
23
|
|
|
@@ -46,12 +47,20 @@ class DiffsPanel(BasePanel):
|
|
|
46
47
|
channel: str = "diffs"
|
|
47
48
|
panel_name: str = "Diffs"
|
|
48
49
|
icon: str = PANEL_ICONS["diffs"][0]
|
|
50
|
+
can_focus = True
|
|
51
|
+
|
|
52
|
+
BINDINGS = [
|
|
53
|
+
Binding("n", "next_file_key", "Next file"),
|
|
54
|
+
Binding("p", "prev_file_key", "Prev file"),
|
|
55
|
+
]
|
|
49
56
|
|
|
50
57
|
def __init__(self, client=None, **kwargs):
|
|
51
58
|
super().__init__(client=client, **kwargs)
|
|
52
59
|
self._current_page: int = 0
|
|
53
60
|
self._max_page: int = 0
|
|
54
61
|
self._temp_files: list[str] = []
|
|
62
|
+
self._current_file_index: int = 0
|
|
63
|
+
self._total_files: int = 0
|
|
55
64
|
|
|
56
65
|
def next_page(self) -> None:
|
|
57
66
|
"""Advance to the next page of truncated diff content."""
|
|
@@ -63,11 +72,57 @@ class DiffsPanel(BasePanel):
|
|
|
63
72
|
if self._current_page > 0:
|
|
64
73
|
self._current_page -= 1
|
|
65
74
|
|
|
75
|
+
def next_file(self) -> None:
|
|
76
|
+
"""Advance to the next file."""
|
|
77
|
+
if self._current_file_index < self._total_files - 1:
|
|
78
|
+
self._current_file_index += 1
|
|
79
|
+
if self._last_payload:
|
|
80
|
+
rendered = self.render_panel(self._last_payload)
|
|
81
|
+
try:
|
|
82
|
+
self.update(rendered)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def prev_file(self) -> None:
|
|
87
|
+
"""Go back to the previous file."""
|
|
88
|
+
if self._current_file_index > 0:
|
|
89
|
+
self._current_file_index -= 1
|
|
90
|
+
if self._last_payload:
|
|
91
|
+
rendered = self.render_panel(self._last_payload)
|
|
92
|
+
try:
|
|
93
|
+
self.update(rendered)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
def navigate_to_file(self, path: str) -> None:
|
|
98
|
+
"""Jump to a specific file by path. No-op if not found."""
|
|
99
|
+
if self._last_payload is None:
|
|
100
|
+
return
|
|
101
|
+
diffs = self._last_payload.get("diffs", [])
|
|
102
|
+
for i, d in enumerate(diffs):
|
|
103
|
+
if d.get("path") == path:
|
|
104
|
+
self._current_file_index = i
|
|
105
|
+
rendered = self.render_panel(self._last_payload)
|
|
106
|
+
try:
|
|
107
|
+
self.update(rendered)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
def action_next_file_key(self) -> None:
|
|
113
|
+
"""Binding action: advance to next file."""
|
|
114
|
+
self.next_file()
|
|
115
|
+
|
|
116
|
+
def action_prev_file_key(self) -> None:
|
|
117
|
+
"""Binding action: go to previous file."""
|
|
118
|
+
self.prev_file()
|
|
119
|
+
|
|
66
120
|
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
67
121
|
"""Handle incoming WebSocket message with pagination reset and temp management."""
|
|
68
122
|
if not self._mounted or message is None:
|
|
69
123
|
return
|
|
70
124
|
self._current_page = 0
|
|
125
|
+
self._current_file_index = 0
|
|
71
126
|
self._cleanup_temp_files()
|
|
72
127
|
self._store_large_diffs(message)
|
|
73
128
|
super().handle_message(message)
|
|
@@ -78,31 +133,63 @@ class DiffsPanel(BasePanel):
|
|
|
78
133
|
super().on_unmount()
|
|
79
134
|
|
|
80
135
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
81
|
-
"""Render diff data
|
|
136
|
+
"""Render diff data showing one file at a time with file selector header."""
|
|
82
137
|
diffs = payload.get("diffs", [])
|
|
83
138
|
if not diffs:
|
|
84
139
|
return Text("No diffs yet", style="dim italic")
|
|
85
140
|
|
|
141
|
+
self._total_files = len(diffs)
|
|
142
|
+
|
|
143
|
+
# Clamp file index
|
|
144
|
+
if self._current_file_index >= len(diffs):
|
|
145
|
+
self._current_file_index = len(diffs) - 1
|
|
146
|
+
|
|
86
147
|
parts: list[Any] = []
|
|
87
|
-
max_total = 0
|
|
88
|
-
for diff_entry in diffs:
|
|
89
|
-
# Skip syntax highlighting for very large diffs (>2000 lines) for performance
|
|
90
|
-
raw_diff = diff_entry.get("diff", "")
|
|
91
|
-
skip_highlight = raw_diff.count("\n") > HIGHLIGHT_THRESHOLD
|
|
92
148
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
149
|
+
# File selector header
|
|
150
|
+
selector = Text()
|
|
151
|
+
selector.append("Files: ", style="dim")
|
|
152
|
+
for i, d in enumerate(diffs):
|
|
153
|
+
path = d.get("path", "unknown")
|
|
154
|
+
additions = d.get("additions")
|
|
155
|
+
deletions = d.get("deletions")
|
|
156
|
+
stats = ""
|
|
157
|
+
if additions is not None and deletions is not None:
|
|
158
|
+
stats = f" +{additions} -{deletions}"
|
|
159
|
+
|
|
160
|
+
if i == self._current_file_index:
|
|
161
|
+
selector.append(f"[{i+1}/{len(diffs)}] ", style="bold")
|
|
162
|
+
selector.append(path, style="bold cyan")
|
|
163
|
+
if stats:
|
|
164
|
+
selector.append(stats, style="bold dim")
|
|
165
|
+
else:
|
|
166
|
+
selector.append(path, style="dim")
|
|
167
|
+
if stats:
|
|
168
|
+
selector.append(stats, style="dim")
|
|
169
|
+
|
|
170
|
+
if i < len(diffs) - 1:
|
|
171
|
+
selector.append(" | ", style="dim")
|
|
172
|
+
|
|
173
|
+
parts.append(selector)
|
|
174
|
+
parts.append(Text("n:next p:prev", style="dim"))
|
|
175
|
+
parts.append(Text(""))
|
|
176
|
+
|
|
177
|
+
# Render only current file's diff
|
|
178
|
+
diff_entry = diffs[self._current_file_index]
|
|
179
|
+
raw_diff = diff_entry.get("diff", "")
|
|
180
|
+
skip_highlight = raw_diff.count("\n") > HIGHLIGHT_THRESHOLD
|
|
181
|
+
|
|
182
|
+
file_parts, total_lines = _render_file_diff(
|
|
183
|
+
diff_entry,
|
|
184
|
+
page=self._current_page,
|
|
185
|
+
page_size=DEFAULT_LINE_LIMIT,
|
|
186
|
+
skip_highlight=skip_highlight,
|
|
187
|
+
)
|
|
188
|
+
parts.extend(file_parts)
|
|
102
189
|
|
|
103
190
|
# Track max page for pagination bounds
|
|
104
|
-
if
|
|
105
|
-
self._max_page = -(-
|
|
191
|
+
if total_lines > DEFAULT_LINE_LIMIT:
|
|
192
|
+
self._max_page = -(-total_lines // DEFAULT_LINE_LIMIT) - 1
|
|
106
193
|
else:
|
|
107
194
|
self._max_page = 0
|
|
108
195
|
|