@pennyfarthing/core 11.1.0 → 11.2.0
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 +16 -14
- package/packages/core/dist/cli/utils/constants.d.ts +1 -1
- package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/constants.js +2 -1
- package/packages/core/dist/cli/utils/constants.js.map +1 -1
- package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
- package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.js +334 -0
- package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
- package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
- package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
- package/packages/core/dist/server/api/git.d.ts +13 -1
- package/packages/core/dist/server/api/git.d.ts.map +1 -1
- package/packages/core/dist/server/api/git.js +53 -34
- package/packages/core/dist/server/api/git.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/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/skill-search.test.js +2 -2
- 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/pennyfarthing-dist/agents/dev.md +1 -1
- package/pennyfarthing-dist/agents/reviewer.md +1 -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-standalone.md +7 -2
- package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
- package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
- package/pennyfarthing-dist/guides/bikerack.md +3 -3
- package/pennyfarthing-dist/guides/hooks.md +1 -1
- 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/agent-session.sh +0 -0
- package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
- package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
- package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
- package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
- package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
- 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/release.sh +0 -0
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
- package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
- package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
- package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
- package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
- package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
- package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
- package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
- package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
- package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
- package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
- package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
- package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
- package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
- package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
- package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
- package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
- package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
- 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-story/scripts/create-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
- package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
- 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/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_scripts/CLAUDE.md +26 -4
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bc/cli.py +3 -5
- 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__/debug_panel.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.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.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__/tui.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
- package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
- package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
- package/pennyfarthing_scripts/bikerack/cli.py +10 -11
- package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
- package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
- package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
- package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
- package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
- package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
- package/pennyfarthing_scripts/bikerack/tui.py +336 -30
- package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
- package/pennyfarthing_scripts/cli.py +37 -65
- 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/cli.py +149 -0
- package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
- package/pennyfarthing_scripts/context.py +3 -3
- 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__/hooks_installer.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/git/__pycache__/worktree.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__/resolve_gate.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
- package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
- package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
- package/pennyfarthing_scripts/hooks.py +3 -17
- package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
- package/pennyfarthing_scripts/prime/__pycache__/heatmap.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/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 +1 -1
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/loader.py +15 -1
- package/pennyfarthing_scripts/sprint/story_finish.py +14 -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_workflow_check.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_workflow_check.py +2 -3
- package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
- package/pennyfarthing_scripts/validate/cli.py +17 -5
- 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 +1099 -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/scripts/README.md +41 -0
- 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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -34,8 +34,8 @@ from pennyfarthing_scripts.bc.focus import (
|
|
|
34
34
|
def _get_current_layout() -> dict | None:
|
|
35
35
|
"""Fetch the current layout from a running Cyclist or BikeRack server.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
Reads .wheelhub-port (shared by both Cyclist and BikeRack) and fetches
|
|
38
|
+
the layout endpoint.
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
41
|
Layout dict, or None if no server is running or fetch fails.
|
|
@@ -44,10 +44,8 @@ def _get_current_layout() -> dict | None:
|
|
|
44
44
|
|
|
45
45
|
root = _get_root()
|
|
46
46
|
|
|
47
|
-
# Try BikeRack first (bikerack-layout), then Cyclist (layout)
|
|
48
47
|
candidates = [
|
|
49
|
-
(root / ".
|
|
50
|
-
(root / ".cyclist-port", "/api/settings/bikerack-layout"),
|
|
48
|
+
(root / ".wheelhub-port", "/api/settings/bikerack-layout"),
|
|
51
49
|
]
|
|
52
50
|
|
|
53
51
|
for port_file, endpoint in candidates:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -6,12 +6,13 @@ list with status indicators (running, completed, failed).
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import time
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from rich.console import Group
|
|
12
13
|
from rich.text import Text
|
|
13
14
|
|
|
14
|
-
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
15
|
+
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel, format_duration
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class BackgroundPanel(BasePanel):
|
|
@@ -25,12 +26,62 @@ class BackgroundPanel(BasePanel):
|
|
|
25
26
|
panel_name: str = "Background"
|
|
26
27
|
icon: str = PANEL_ICONS["background"][0]
|
|
27
28
|
|
|
29
|
+
def __init__(self, client=None, **kwargs):
|
|
30
|
+
super().__init__(client=client, **kwargs)
|
|
31
|
+
self._timer = None
|
|
32
|
+
|
|
33
|
+
def on_mount(self) -> None:
|
|
34
|
+
"""Subscribe to channel and start elapsed timer."""
|
|
35
|
+
super().on_mount()
|
|
36
|
+
self._timer = self.set_interval(1, self._tick)
|
|
37
|
+
|
|
38
|
+
def on_unmount(self) -> None:
|
|
39
|
+
"""Stop timer and cleanup."""
|
|
40
|
+
if self._timer is not None:
|
|
41
|
+
self._timer.stop()
|
|
42
|
+
super().on_unmount()
|
|
43
|
+
|
|
44
|
+
def _tick(self) -> None:
|
|
45
|
+
"""Re-render every second to update elapsed times."""
|
|
46
|
+
if self._last_payload is not None:
|
|
47
|
+
rendered = self.render_panel(self._last_payload)
|
|
48
|
+
try:
|
|
49
|
+
self.update(rendered)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
28
53
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
29
54
|
"""Render background task data from WebSocket payload."""
|
|
30
55
|
tasks = payload.get("tasks", [])
|
|
31
56
|
if not isinstance(tasks, list) or not tasks:
|
|
32
57
|
return Text("No background tasks", style="dim italic")
|
|
33
58
|
|
|
59
|
+
running = 0
|
|
60
|
+
done = 0
|
|
61
|
+
failed = 0
|
|
62
|
+
for task in tasks:
|
|
63
|
+
if not isinstance(task, dict):
|
|
64
|
+
continue
|
|
65
|
+
if task.get("completedAt") is not None:
|
|
66
|
+
if task.get("success"):
|
|
67
|
+
done += 1
|
|
68
|
+
else:
|
|
69
|
+
failed += 1
|
|
70
|
+
else:
|
|
71
|
+
running += 1
|
|
72
|
+
|
|
73
|
+
# Summary header
|
|
74
|
+
summary = Text()
|
|
75
|
+
summary.append(f"{running} running", style="yellow")
|
|
76
|
+
summary.append(" ")
|
|
77
|
+
summary.append(f"{done} done", style="green")
|
|
78
|
+
if failed > 0:
|
|
79
|
+
summary.append(" ")
|
|
80
|
+
summary.append(f"{failed} failed", style="red")
|
|
81
|
+
|
|
82
|
+
separator = Text("─" * 30, style="dim")
|
|
83
|
+
|
|
84
|
+
# Task list
|
|
34
85
|
parts: list[Any] = []
|
|
35
86
|
for task in tasks:
|
|
36
87
|
if not isinstance(task, dict):
|
|
@@ -40,7 +91,7 @@ class BackgroundPanel(BasePanel):
|
|
|
40
91
|
if not parts:
|
|
41
92
|
return Text("No background tasks", style="dim italic")
|
|
42
93
|
|
|
43
|
-
return Group(*parts)
|
|
94
|
+
return Group(summary, separator, *parts)
|
|
44
95
|
|
|
45
96
|
|
|
46
97
|
def _render_task(task: dict[str, Any]) -> Text:
|
|
@@ -48,6 +99,7 @@ def _render_task(task: dict[str, Any]) -> Text:
|
|
|
48
99
|
description = task.get("description", "Unknown task")
|
|
49
100
|
subagent_type = task.get("subagentType", "")
|
|
50
101
|
completed_at = task.get("completedAt")
|
|
102
|
+
started_at = task.get("startedAt")
|
|
51
103
|
success = task.get("success")
|
|
52
104
|
result = task.get("result", "")
|
|
53
105
|
error = task.get("error", "")
|
|
@@ -60,7 +112,12 @@ def _render_task(task: dict[str, Any]) -> Text:
|
|
|
60
112
|
line.append(description)
|
|
61
113
|
if subagent_type:
|
|
62
114
|
line.append(f" [{subagent_type}]", style="dim")
|
|
63
|
-
|
|
115
|
+
# Show duration if timestamps available
|
|
116
|
+
duration = _calc_duration(started_at, completed_at)
|
|
117
|
+
if duration:
|
|
118
|
+
line.append(f" — {duration}", style="green")
|
|
119
|
+
else:
|
|
120
|
+
line.append(" — done", style="green")
|
|
64
121
|
if result:
|
|
65
122
|
line.append(f" ({result})", style="dim green")
|
|
66
123
|
else:
|
|
@@ -68,7 +125,11 @@ def _render_task(task: dict[str, Any]) -> Text:
|
|
|
68
125
|
line.append(description)
|
|
69
126
|
if subagent_type:
|
|
70
127
|
line.append(f" [{subagent_type}]", style="dim")
|
|
71
|
-
|
|
128
|
+
duration = _calc_duration(started_at, completed_at)
|
|
129
|
+
if duration:
|
|
130
|
+
line.append(f" — {duration}", style="red")
|
|
131
|
+
else:
|
|
132
|
+
line.append(" — failed", style="red")
|
|
72
133
|
if error:
|
|
73
134
|
line.append(f"\n {error}", style="red")
|
|
74
135
|
else:
|
|
@@ -76,6 +137,26 @@ def _render_task(task: dict[str, Any]) -> Text:
|
|
|
76
137
|
line.append(description)
|
|
77
138
|
if subagent_type:
|
|
78
139
|
line.append(f" [{subagent_type}]", style="dim")
|
|
79
|
-
|
|
140
|
+
# Live elapsed time for running tasks
|
|
141
|
+
if started_at:
|
|
142
|
+
try:
|
|
143
|
+
elapsed = time.time() - float(started_at) / 1000 # startedAt is typically ms
|
|
144
|
+
line.append(f" — {format_duration(elapsed)}", style="yellow")
|
|
145
|
+
except (ValueError, TypeError):
|
|
146
|
+
line.append(" — running", style="yellow")
|
|
147
|
+
else:
|
|
148
|
+
line.append(" — running", style="yellow")
|
|
80
149
|
|
|
81
150
|
return line
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _calc_duration(started_at, completed_at) -> str:
|
|
154
|
+
"""Calculate duration string from timestamps."""
|
|
155
|
+
if started_at is None or completed_at is None:
|
|
156
|
+
return ""
|
|
157
|
+
try:
|
|
158
|
+
start = float(started_at) / 1000 # ms to seconds
|
|
159
|
+
end = float(completed_at) / 1000
|
|
160
|
+
return format_duration(end - start)
|
|
161
|
+
except (ValueError, TypeError):
|
|
162
|
+
return ""
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from rich.text import Text
|
|
12
13
|
from textual.widgets import Static
|
|
13
14
|
|
|
14
15
|
# Nerd Font icon registry: panel_name → (nerd_font_icon, ascii_fallback)
|
|
@@ -25,6 +26,7 @@ PANEL_ICONS: dict[str, tuple[str, str]] = {
|
|
|
25
26
|
"debug": ("\uf188", "d"), # nf-fa-bug
|
|
26
27
|
"settings": ("\uf013", "S"), # nf-fa-gear
|
|
27
28
|
"tty": ("\uf120", ">"), # nf-fa-terminal
|
|
29
|
+
"progress": ("\uf200", "P"), # nf-fa-pie_chart
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
|
|
@@ -44,6 +46,66 @@ def get_panel_icon(panel_name: str, use_nerd_font: bool = True) -> str:
|
|
|
44
46
|
return entry[0] if use_nerd_font else entry[1]
|
|
45
47
|
|
|
46
48
|
|
|
49
|
+
def render_progress_bar(percent: int | float, width: int = 20, warn_high: bool = False) -> Text:
|
|
50
|
+
"""Render a Unicode progress bar with color based on percentage.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
percent: Value 0-100.
|
|
54
|
+
width: Number of bar characters (default 20).
|
|
55
|
+
warn_high: If True, use red at high values (for resource usage).
|
|
56
|
+
If False (default), use blue at 100% (for completion).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Rich Text like ``[████████░░░░░░░░░░░░] 22%``
|
|
60
|
+
"""
|
|
61
|
+
percent = max(0, min(100, int(percent)))
|
|
62
|
+
filled = round(width * percent / 100)
|
|
63
|
+
empty = width - filled
|
|
64
|
+
|
|
65
|
+
if warn_high:
|
|
66
|
+
if percent < 50:
|
|
67
|
+
style = "green"
|
|
68
|
+
elif percent <= 80:
|
|
69
|
+
style = "yellow"
|
|
70
|
+
else:
|
|
71
|
+
style = "red"
|
|
72
|
+
else:
|
|
73
|
+
style = "blue"
|
|
74
|
+
|
|
75
|
+
bar = Text()
|
|
76
|
+
bar.append("[")
|
|
77
|
+
bar.append("█" * filled, style=style)
|
|
78
|
+
bar.append("░" * empty, style="dim")
|
|
79
|
+
bar.append(f"] {percent}%")
|
|
80
|
+
return bar
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_duration(seconds: int | float) -> str:
|
|
84
|
+
"""Format seconds into human-friendly duration string.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
``47s``, ``2m 14s``, ``1h 5m``.
|
|
88
|
+
"""
|
|
89
|
+
seconds = max(0, int(seconds))
|
|
90
|
+
if seconds < 60:
|
|
91
|
+
return f"{seconds}s"
|
|
92
|
+
minutes, secs = divmod(seconds, 60)
|
|
93
|
+
if minutes < 60:
|
|
94
|
+
return f"{minutes}m {secs}s"
|
|
95
|
+
hours, mins = divmod(minutes, 60)
|
|
96
|
+
return f"{hours}h {mins}m"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def humanize_theme(slug: str) -> str:
|
|
100
|
+
"""Convert a theme slug to a display name.
|
|
101
|
+
|
|
102
|
+
``princess-bride`` → ``Princess Bride``
|
|
103
|
+
"""
|
|
104
|
+
if not slug:
|
|
105
|
+
return ""
|
|
106
|
+
return slug.replace("-", " ").replace("_", " ").title()
|
|
107
|
+
|
|
108
|
+
|
|
47
109
|
class BasePanel(Static):
|
|
48
110
|
"""Base class for BikeRack TUI panels.
|
|
49
111
|
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from rich.table import Table
|
|
12
11
|
from rich.text import Text
|
|
13
12
|
|
|
14
13
|
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
@@ -64,42 +63,47 @@ class ChangedPanel(BasePanel):
|
|
|
64
63
|
icon: str = PANEL_ICONS["changed"][0]
|
|
65
64
|
|
|
66
65
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
67
|
-
"""Render changed
|
|
66
|
+
"""Render changed files grouped by repository."""
|
|
68
67
|
repos = payload.get("repos", [])
|
|
69
68
|
if not isinstance(repos, list):
|
|
70
69
|
return Text("No changed files", style="dim italic")
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
# Group files by repo
|
|
72
|
+
repo_files: dict[str, list[dict[str, Any]]] = {}
|
|
73
73
|
for repo in repos:
|
|
74
74
|
if not isinstance(repo, dict):
|
|
75
75
|
continue
|
|
76
|
-
repo_name = repo.get("name", "")
|
|
76
|
+
repo_name = repo.get("name", "unknown")
|
|
77
77
|
dirty_files = repo.get("dirtyFiles", [])
|
|
78
|
-
if not isinstance(dirty_files, list):
|
|
78
|
+
if not isinstance(dirty_files, list) or not dirty_files:
|
|
79
79
|
continue
|
|
80
|
-
for f in dirty_files
|
|
81
|
-
if not isinstance(f, dict):
|
|
82
|
-
continue
|
|
83
|
-
files.append((repo_name, f))
|
|
80
|
+
repo_files[repo_name] = [f for f in dirty_files if isinstance(f, dict)]
|
|
84
81
|
|
|
85
|
-
if not
|
|
82
|
+
if not repo_files:
|
|
86
83
|
return Text("No changed files", style="dim italic")
|
|
87
84
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
85
|
+
from rich.console import Group as RichGroup
|
|
86
|
+
|
|
87
|
+
parts: list[Any] = []
|
|
88
|
+
for repo_name, files in repo_files.items():
|
|
89
|
+
count = len(files)
|
|
90
|
+
label = "file" if count == 1 else "files"
|
|
91
|
+
header = Text()
|
|
92
|
+
header.append(repo_name, style="bold cyan")
|
|
93
|
+
header.append(f" ({count} {label})", style="dim")
|
|
94
|
+
parts.append(header)
|
|
95
|
+
|
|
96
|
+
for f in files:
|
|
97
|
+
status_code = f.get("status", " ")
|
|
98
|
+
path = f.get("path", "")
|
|
99
|
+
icon, label_text, style = _parse_status(status_code)
|
|
100
|
+
line = Text()
|
|
101
|
+
line.append(" ")
|
|
102
|
+
line.append(icon, style=f"bold {style}")
|
|
103
|
+
line.append(f" {path}", style="cyan")
|
|
104
|
+
line.append(f" {label_text}", style=style)
|
|
105
|
+
parts.append(line)
|
|
106
|
+
|
|
107
|
+
parts.append(Text("")) # spacer between repos
|
|
108
|
+
|
|
109
|
+
return RichGroup(*parts)
|
|
@@ -64,12 +64,12 @@ def start(project_dir, dry_run):
|
|
|
64
64
|
|
|
65
65
|
running, pid, port = is_already_running(project_dir)
|
|
66
66
|
if running:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
click.echo("
|
|
72
|
-
|
|
67
|
+
# Idempotent: WheelHub already up, just exec Claude with OTEL env
|
|
68
|
+
click.echo(f"BikeRack already running (PID {pid}, port {port})")
|
|
69
|
+
otel_env = build_otel_env(port)
|
|
70
|
+
click.echo(f"Dashboard: http://localhost:{port}/bikerack")
|
|
71
|
+
click.echo("Starting Claude CLI...")
|
|
72
|
+
exec_claude(otel_env, project_dir)
|
|
73
73
|
|
|
74
74
|
click.echo("Starting BikeRack mode...")
|
|
75
75
|
try:
|
|
@@ -116,11 +116,10 @@ def stop(project_dir, dry_run):
|
|
|
116
116
|
|
|
117
117
|
result = stop_bikerack(project_dir)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sys.exit(1)
|
|
119
|
+
click.echo(result["message"])
|
|
120
|
+
if not result["success"]:
|
|
121
|
+
# "Not running" is not an error for stop — idempotent
|
|
122
|
+
sys.exit(0)
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
@bikerack.command()
|
|
@@ -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()
|
|
@@ -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)
|
|
@@ -52,6 +52,8 @@ class DiffsPanel(BasePanel):
|
|
|
52
52
|
self._current_page: int = 0
|
|
53
53
|
self._max_page: int = 0
|
|
54
54
|
self._temp_files: list[str] = []
|
|
55
|
+
self._current_file_index: int = 0
|
|
56
|
+
self._total_files: int = 0
|
|
55
57
|
|
|
56
58
|
def next_page(self) -> None:
|
|
57
59
|
"""Advance to the next page of truncated diff content."""
|
|
@@ -63,11 +65,34 @@ class DiffsPanel(BasePanel):
|
|
|
63
65
|
if self._current_page > 0:
|
|
64
66
|
self._current_page -= 1
|
|
65
67
|
|
|
68
|
+
def next_file(self) -> None:
|
|
69
|
+
"""Advance to the next file."""
|
|
70
|
+
if self._current_file_index < self._total_files - 1:
|
|
71
|
+
self._current_file_index += 1
|
|
72
|
+
if self._last_payload:
|
|
73
|
+
rendered = self.render_panel(self._last_payload)
|
|
74
|
+
try:
|
|
75
|
+
self.update(rendered)
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def prev_file(self) -> None:
|
|
80
|
+
"""Go back to the previous file."""
|
|
81
|
+
if self._current_file_index > 0:
|
|
82
|
+
self._current_file_index -= 1
|
|
83
|
+
if self._last_payload:
|
|
84
|
+
rendered = self.render_panel(self._last_payload)
|
|
85
|
+
try:
|
|
86
|
+
self.update(rendered)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
66
90
|
def handle_message(self, message: dict[str, Any] | None) -> None:
|
|
67
91
|
"""Handle incoming WebSocket message with pagination reset and temp management."""
|
|
68
92
|
if not self._mounted or message is None:
|
|
69
93
|
return
|
|
70
94
|
self._current_page = 0
|
|
95
|
+
self._current_file_index = 0
|
|
71
96
|
self._cleanup_temp_files()
|
|
72
97
|
self._store_large_diffs(message)
|
|
73
98
|
super().handle_message(message)
|
|
@@ -78,31 +103,63 @@ class DiffsPanel(BasePanel):
|
|
|
78
103
|
super().on_unmount()
|
|
79
104
|
|
|
80
105
|
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
81
|
-
"""Render diff data
|
|
106
|
+
"""Render diff data showing one file at a time with file selector header."""
|
|
82
107
|
diffs = payload.get("diffs", [])
|
|
83
108
|
if not diffs:
|
|
84
109
|
return Text("No diffs yet", style="dim italic")
|
|
85
110
|
|
|
111
|
+
self._total_files = len(diffs)
|
|
112
|
+
|
|
113
|
+
# Clamp file index
|
|
114
|
+
if self._current_file_index >= len(diffs):
|
|
115
|
+
self._current_file_index = len(diffs) - 1
|
|
116
|
+
|
|
86
117
|
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
118
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
# File selector header
|
|
120
|
+
selector = Text()
|
|
121
|
+
selector.append("Files: ", style="dim")
|
|
122
|
+
for i, d in enumerate(diffs):
|
|
123
|
+
path = d.get("path", "unknown")
|
|
124
|
+
additions = d.get("additions")
|
|
125
|
+
deletions = d.get("deletions")
|
|
126
|
+
stats = ""
|
|
127
|
+
if additions is not None and deletions is not None:
|
|
128
|
+
stats = f" +{additions} -{deletions}"
|
|
129
|
+
|
|
130
|
+
if i == self._current_file_index:
|
|
131
|
+
selector.append(f"[{i+1}/{len(diffs)}] ", style="bold")
|
|
132
|
+
selector.append(path, style="bold cyan")
|
|
133
|
+
if stats:
|
|
134
|
+
selector.append(stats, style="bold dim")
|
|
135
|
+
else:
|
|
136
|
+
selector.append(path, style="dim")
|
|
137
|
+
if stats:
|
|
138
|
+
selector.append(stats, style="dim")
|
|
139
|
+
|
|
140
|
+
if i < len(diffs) - 1:
|
|
141
|
+
selector.append(" | ", style="dim")
|
|
142
|
+
|
|
143
|
+
parts.append(selector)
|
|
144
|
+
parts.append(Text("n:next p:prev", style="dim"))
|
|
145
|
+
parts.append(Text(""))
|
|
146
|
+
|
|
147
|
+
# Render only current file's diff
|
|
148
|
+
diff_entry = diffs[self._current_file_index]
|
|
149
|
+
raw_diff = diff_entry.get("diff", "")
|
|
150
|
+
skip_highlight = raw_diff.count("\n") > HIGHLIGHT_THRESHOLD
|
|
151
|
+
|
|
152
|
+
file_parts, total_lines = _render_file_diff(
|
|
153
|
+
diff_entry,
|
|
154
|
+
page=self._current_page,
|
|
155
|
+
page_size=DEFAULT_LINE_LIMIT,
|
|
156
|
+
skip_highlight=skip_highlight,
|
|
157
|
+
)
|
|
158
|
+
parts.extend(file_parts)
|
|
102
159
|
|
|
103
160
|
# Track max page for pagination bounds
|
|
104
|
-
if
|
|
105
|
-
self._max_page = -(-
|
|
161
|
+
if total_lines > DEFAULT_LINE_LIMIT:
|
|
162
|
+
self._max_page = -(-total_lines // DEFAULT_LINE_LIMIT) - 1
|
|
106
163
|
else:
|
|
107
164
|
self._max_page = 0
|
|
108
165
|
|