@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
|
@@ -9,10 +9,26 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from rich.console import Group
|
|
12
|
-
from rich.
|
|
12
|
+
from rich.padding import Padding
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
-
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel
|
|
15
|
+
from pennyfarthing_scripts.bikerack.base_panel import PANEL_ICONS, BasePanel, render_progress_bar
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _status_badge(status: str) -> Text:
|
|
19
|
+
"""Convert status string to styled Rich Text badge."""
|
|
20
|
+
s = status.lower().strip() if status else ""
|
|
21
|
+
if s == "done":
|
|
22
|
+
return Text("\u2713 done", style="green")
|
|
23
|
+
if s == "in-progress":
|
|
24
|
+
return Text("\u27f3 in-progress", style="yellow")
|
|
25
|
+
if s == "backlog":
|
|
26
|
+
return Text("\u25ef backlog", style="dim")
|
|
27
|
+
if s == "blocked":
|
|
28
|
+
return Text("! blocked", style="bold red")
|
|
29
|
+
if s == "review":
|
|
30
|
+
return Text("\u25ce review", style="cyan")
|
|
31
|
+
return Text(status or "\u2014", style="dim")
|
|
16
32
|
|
|
17
33
|
|
|
18
34
|
class SprintPanel(BasePanel):
|
|
@@ -26,15 +42,83 @@ class SprintPanel(BasePanel):
|
|
|
26
42
|
panel_name: str = "Sprint"
|
|
27
43
|
icon: str = PANEL_ICONS["sprint"][0]
|
|
28
44
|
|
|
29
|
-
def
|
|
30
|
-
|
|
45
|
+
def __init__(self, client: Any = None, **kwargs: Any) -> None:
|
|
46
|
+
super().__init__(client=client, **kwargs)
|
|
47
|
+
self._selected_epic: int = 0
|
|
48
|
+
self._toggled: dict[str, bool] = {} # epic_id -> user override
|
|
49
|
+
|
|
50
|
+
def next_epic(self) -> None:
|
|
51
|
+
"""Move selection to the next epic."""
|
|
52
|
+
epic_count = self._epic_count()
|
|
53
|
+
if epic_count == 0:
|
|
54
|
+
return
|
|
55
|
+
self._selected_epic = (self._selected_epic + 1) % epic_count
|
|
56
|
+
self._rerender()
|
|
57
|
+
|
|
58
|
+
def prev_epic(self) -> None:
|
|
59
|
+
"""Move selection to the previous epic."""
|
|
60
|
+
epic_count = self._epic_count()
|
|
61
|
+
if epic_count == 0:
|
|
62
|
+
return
|
|
63
|
+
self._selected_epic = (self._selected_epic - 1) % epic_count
|
|
64
|
+
self._rerender()
|
|
65
|
+
|
|
66
|
+
def toggle_epic(self) -> None:
|
|
67
|
+
"""Toggle expand/collapse on the selected epic."""
|
|
68
|
+
if self._last_payload is None:
|
|
69
|
+
return
|
|
70
|
+
epics = self._last_payload.get("epics", [])
|
|
71
|
+
if not epics or self._selected_epic >= len(epics):
|
|
72
|
+
return
|
|
73
|
+
epic_id = epics[self._selected_epic].get("id", "")
|
|
74
|
+
if epic_id:
|
|
75
|
+
self._toggled[epic_id] = not self._is_expanded(epics[self._selected_epic])
|
|
76
|
+
self._rerender()
|
|
77
|
+
|
|
78
|
+
def _rerender(self) -> None:
|
|
79
|
+
if self._last_payload is not None:
|
|
80
|
+
rendered = self.render_panel(self._last_payload)
|
|
81
|
+
try:
|
|
82
|
+
self.update(rendered)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def _epic_count(self) -> int:
|
|
87
|
+
if self._last_payload is None:
|
|
88
|
+
return 0
|
|
89
|
+
return len(self._last_payload.get("epics", []))
|
|
90
|
+
|
|
91
|
+
def _is_expanded(self, epic: dict[str, Any]) -> bool:
|
|
92
|
+
"""Check if an epic should be expanded."""
|
|
93
|
+
epic_id = epic.get("id", "")
|
|
94
|
+
if epic_id in self._toggled:
|
|
95
|
+
return self._toggled[epic_id]
|
|
96
|
+
# Default: expand if has incomplete work
|
|
97
|
+
stories = epic.get("stories", [])
|
|
98
|
+
total_pts = 0
|
|
99
|
+
done_pts = 0
|
|
100
|
+
has_in_progress = False
|
|
101
|
+
for story in stories:
|
|
102
|
+
pts = story.get("points", 0)
|
|
103
|
+
if isinstance(pts, (int, float)):
|
|
104
|
+
total_pts += pts
|
|
105
|
+
status = (story.get("status") or "").lower().strip()
|
|
106
|
+
if status == "done":
|
|
107
|
+
done_pts += pts
|
|
108
|
+
if status == "in-progress":
|
|
109
|
+
has_in_progress = True
|
|
110
|
+
return has_in_progress or done_pts < total_pts
|
|
31
111
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"""
|
|
112
|
+
def render_panel(self, payload: dict[str, Any]) -> Any:
|
|
113
|
+
"""Render sprint data with epic grouping and progress bars."""
|
|
35
114
|
sprint = payload.get("sprint", {})
|
|
36
115
|
metrics = payload.get("metrics", {})
|
|
37
116
|
epics = payload.get("epics", [])
|
|
117
|
+
current_story_id = sprint.get("currentStory", "")
|
|
118
|
+
|
|
119
|
+
# Clamp selection
|
|
120
|
+
if epics and self._selected_epic >= len(epics):
|
|
121
|
+
self._selected_epic = len(epics) - 1
|
|
38
122
|
|
|
39
123
|
# Sprint metrics header
|
|
40
124
|
sprint_num = sprint.get("number", "")
|
|
@@ -51,22 +135,70 @@ class SprintPanel(BasePanel):
|
|
|
51
135
|
f"Velocity: {velocity}"
|
|
52
136
|
)
|
|
53
137
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
story.get("
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
138
|
+
hint = Text.from_markup("[dim]j/k:navigate e:expand/collapse[/dim]")
|
|
139
|
+
parts: list[Any] = [header, hint, Text("")]
|
|
140
|
+
|
|
141
|
+
for i, epic in enumerate(epics):
|
|
142
|
+
epic_id = epic.get("id", "")
|
|
143
|
+
epic_title = epic.get("title", "")
|
|
144
|
+
stories = epic.get("stories", [])
|
|
145
|
+
|
|
146
|
+
# Calculate epic progress
|
|
147
|
+
total_pts = 0
|
|
148
|
+
done_pts = 0
|
|
149
|
+
for story in stories:
|
|
150
|
+
pts = story.get("points", 0)
|
|
151
|
+
if isinstance(pts, (int, float)):
|
|
152
|
+
total_pts += pts
|
|
153
|
+
status = (story.get("status") or "").lower().strip()
|
|
154
|
+
if status == "done":
|
|
155
|
+
done_pts += pts
|
|
156
|
+
|
|
157
|
+
expanded = self._is_expanded(epic)
|
|
158
|
+
selected = i == self._selected_epic
|
|
159
|
+
|
|
160
|
+
# Epic header: selector arrow epic-id progress-bar pts title
|
|
161
|
+
arrow = "▼" if expanded else "▶"
|
|
162
|
+
epic_line = Text(no_wrap=True, overflow="ellipsis")
|
|
163
|
+
if selected:
|
|
164
|
+
epic_line.append("› ", style="bold yellow")
|
|
165
|
+
epic_line.append(f"{arrow} ", style="bold")
|
|
166
|
+
epic_line.append(f"{epic_id}", style="bold cyan")
|
|
167
|
+
epic_line.append(" ")
|
|
168
|
+
|
|
169
|
+
if total_pts > 0:
|
|
170
|
+
pct = int(done_pts / total_pts * 100)
|
|
171
|
+
epic_line.append_text(render_progress_bar(pct, width=10))
|
|
172
|
+
epic_line.append(f" {done_pts}/{total_pts} pts", style="dim")
|
|
173
|
+
else:
|
|
174
|
+
epic_line.append("0 pts", style="dim")
|
|
175
|
+
|
|
176
|
+
epic_line.append(f" {epic_title}", style="bold")
|
|
177
|
+
|
|
178
|
+
parts.append(epic_line)
|
|
179
|
+
|
|
180
|
+
# Show stories if expanded
|
|
181
|
+
if expanded:
|
|
182
|
+
for story in stories:
|
|
183
|
+
story_id = story.get("id", "")
|
|
184
|
+
title = story.get("title", "")
|
|
185
|
+
pts = story.get("points", "")
|
|
186
|
+
jira = story.get("jiraKey") or "—"
|
|
187
|
+
badge = _status_badge(story.get("status", ""))
|
|
188
|
+
|
|
189
|
+
# Fixed-width fields first, title last (truncates)
|
|
190
|
+
story_line = Text(no_wrap=True, overflow="ellipsis")
|
|
191
|
+
story_line.append_text(badge)
|
|
192
|
+
story_line.append(f" {story_id}", style="cyan" if story_id != current_story_id else "bold cyan")
|
|
193
|
+
story_line.append(f" {jira}", style="dim")
|
|
194
|
+
story_line.append(f" {pts}", style="dim")
|
|
195
|
+
story_line.append(f" {title}")
|
|
196
|
+
|
|
197
|
+
if story_id == current_story_id:
|
|
198
|
+
story_line.stylize("bold")
|
|
199
|
+
|
|
200
|
+
parts.append(Padding(story_line, (0, 0, 0, 4)))
|
|
201
|
+
|
|
202
|
+
parts.append(Text("")) # spacer between epics
|
|
203
|
+
|
|
204
|
+
return Group(*parts)
|
|
@@ -5,21 +5,30 @@ Story 103-4: Connection status indicator in TUI header.
|
|
|
5
5
|
Story 103-6: SprintPanel as default panel on launch.
|
|
6
6
|
Story 103-7: /bc TUI panel focus — subscribe to /ws/focus, switch panels.
|
|
7
7
|
Story 103-9: Panel header chrome — icon + name indicator for active panel.
|
|
8
|
+
Panel navigation: Mount all panels, tab bar, keyboard switching, command palette.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
13
|
+
from functools import partial
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Any
|
|
14
16
|
|
|
15
17
|
from textual.app import App, ComposeResult
|
|
16
18
|
from textual.binding import Binding
|
|
19
|
+
from textual.command import Hit, Hits, Provider
|
|
17
20
|
from textual.containers import VerticalScroll
|
|
18
21
|
from textual.reactive import reactive
|
|
19
22
|
from textual.widgets import Footer, Header, Static
|
|
20
23
|
|
|
21
24
|
from pennyfarthing_scripts.bc.focus import get_last_panel, save_last_panel
|
|
25
|
+
from pennyfarthing_scripts.bikerack.background_panel import BackgroundPanel
|
|
22
26
|
from pennyfarthing_scripts.bikerack.base_panel import get_panel_icon
|
|
27
|
+
from pennyfarthing_scripts.bikerack.changed_panel import ChangedPanel
|
|
28
|
+
from pennyfarthing_scripts.bikerack.debug_panel import DebugPanel
|
|
29
|
+
from pennyfarthing_scripts.bikerack.diffs_panel import DiffsPanel
|
|
30
|
+
from pennyfarthing_scripts.bikerack.git_panel import GitPanel
|
|
31
|
+
from pennyfarthing_scripts.bikerack.progress_panel import ProgressPanel
|
|
23
32
|
from pennyfarthing_scripts.bikerack.sprint_panel import SprintPanel
|
|
24
33
|
from pennyfarthing_scripts.bikerack.ws_client import ConnectionState, WheelHubClient
|
|
25
34
|
|
|
@@ -30,7 +39,48 @@ STATE_DISPLAY: dict[ConnectionState, str] = {
|
|
|
30
39
|
ConnectionState.CONNECTING: "[yellow]● Connecting…[/yellow]",
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
#
|
|
42
|
+
# Agent role colors for Rich markup (mapped from React AGENT_COLORS)
|
|
43
|
+
AGENT_ROLE_COLORS: dict[str, str] = {
|
|
44
|
+
"pm": "purple",
|
|
45
|
+
"sm": "blue",
|
|
46
|
+
"dev": "green",
|
|
47
|
+
"tea": "cyan",
|
|
48
|
+
"reviewer": "red",
|
|
49
|
+
"architect": "dark_orange",
|
|
50
|
+
"devops": "bright_cyan",
|
|
51
|
+
"ux-designer": "magenta",
|
|
52
|
+
"tech-writer": "white",
|
|
53
|
+
"orchestrator": "bright_magenta",
|
|
54
|
+
"ba": "bright_green",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
AGENT_ABBREV: dict[str, str] = {
|
|
58
|
+
"pm": "PM",
|
|
59
|
+
"sm": "SM",
|
|
60
|
+
"dev": "DEV",
|
|
61
|
+
"tea": "TEA",
|
|
62
|
+
"reviewer": "REV",
|
|
63
|
+
"architect": "ARC",
|
|
64
|
+
"devops": "OPS",
|
|
65
|
+
"ux-designer": "UX",
|
|
66
|
+
"tech-writer": "TW",
|
|
67
|
+
"orchestrator": "ORC",
|
|
68
|
+
"ba": "BA",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Ordered panel registry: (key, display_name, widget_class)
|
|
72
|
+
# Only panels with implemented widget classes are included.
|
|
73
|
+
PANEL_REGISTRY: list[tuple[str, str]] = [
|
|
74
|
+
("sprint", "Sprint"),
|
|
75
|
+
("git", "Git"),
|
|
76
|
+
("diffs", "Diffs"),
|
|
77
|
+
("changed", "Changed"),
|
|
78
|
+
("background", "Background"),
|
|
79
|
+
("debug", "Debug"),
|
|
80
|
+
("progress", "Progress"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# Human-readable display names for panels (full set for external focus messages)
|
|
34
84
|
PANEL_DISPLAY_NAMES: dict[str, str] = {
|
|
35
85
|
"sprint": "Sprint",
|
|
36
86
|
"git": "Git",
|
|
@@ -42,24 +92,106 @@ PANEL_DISPLAY_NAMES: dict[str, str] = {
|
|
|
42
92
|
"changed": "Changed",
|
|
43
93
|
"ac": "Acceptance Criteria",
|
|
44
94
|
"debug": "Debug",
|
|
95
|
+
"progress": "Progress",
|
|
45
96
|
"settings": "Settings",
|
|
46
97
|
"tty": "TTY",
|
|
47
98
|
}
|
|
48
99
|
|
|
100
|
+
# Keys from PANEL_REGISTRY for fast lookup
|
|
101
|
+
_PANEL_KEYS = [key for key, _ in PANEL_REGISTRY]
|
|
49
102
|
|
|
50
|
-
class PanelIndicator(Static):
|
|
51
|
-
"""Displays the active panel's Nerd Font icon and name."""
|
|
52
103
|
|
|
53
|
-
|
|
104
|
+
class PanelTabBar(Static):
|
|
105
|
+
"""Horizontal tab bar showing all available panels with active highlight."""
|
|
54
106
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
107
|
+
active: reactive[str] = reactive("sprint")
|
|
108
|
+
|
|
109
|
+
def watch_active(self, key: str) -> None:
|
|
110
|
+
"""Re-render tab bar when active panel changes."""
|
|
111
|
+
parts: list[str] = []
|
|
112
|
+
for panel_key, display_name in PANEL_REGISTRY:
|
|
113
|
+
icon = get_panel_icon(panel_key)
|
|
114
|
+
idx = _PANEL_KEYS.index(panel_key) + 1
|
|
115
|
+
prefix = f"{idx}:"
|
|
116
|
+
if panel_key == key:
|
|
117
|
+
if icon:
|
|
118
|
+
parts.append(f"[bold reverse] {prefix}{icon} {display_name} [/]")
|
|
119
|
+
else:
|
|
120
|
+
parts.append(f"[bold reverse] {prefix}{display_name} [/]")
|
|
121
|
+
else:
|
|
122
|
+
if icon:
|
|
123
|
+
parts.append(f"[dim]{prefix}{icon} {display_name}[/]")
|
|
124
|
+
else:
|
|
125
|
+
parts.append(f"[dim]{prefix}{display_name}[/]")
|
|
126
|
+
self.update(" ".join(parts))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class AgentHeader(Static):
|
|
130
|
+
"""Displays current agent persona from WheelHub /ws/persona channel."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
133
|
+
super().__init__(**kwargs)
|
|
134
|
+
self._is_streaming: bool = False
|
|
135
|
+
self._persona_data: dict[str, Any] = {}
|
|
136
|
+
|
|
137
|
+
def _apply_persona(self, data: dict[str, Any]) -> None:
|
|
138
|
+
"""Render persona data into the header."""
|
|
139
|
+
if data.get("type") == "streaming":
|
|
140
|
+
self._is_streaming = bool(data.get("isStreaming", False))
|
|
141
|
+
self._render_header()
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
self._persona_data = data
|
|
145
|
+
self._is_streaming = bool(data.get("isStreaming", False))
|
|
146
|
+
self._render_header()
|
|
147
|
+
|
|
148
|
+
def _render_header(self) -> None:
|
|
149
|
+
"""Re-render the header from stored state."""
|
|
150
|
+
data = self._persona_data
|
|
151
|
+
char = data.get("character", "")
|
|
152
|
+
role = data.get("role", "")
|
|
153
|
+
role_desc = data.get("roleDescription", "")
|
|
154
|
+
quote = data.get("quote", "")
|
|
155
|
+
style = data.get("style", "")
|
|
156
|
+
theme = data.get("theme", "")
|
|
157
|
+
|
|
158
|
+
if not char:
|
|
159
|
+
self.update("[dim]Waiting for agent...[/dim]")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
parts: list[str] = []
|
|
163
|
+
|
|
164
|
+
# Role badge
|
|
165
|
+
if role:
|
|
166
|
+
abbrev = AGENT_ABBREV.get(role, role.upper()[:3])
|
|
167
|
+
color = AGENT_ROLE_COLORS.get(role, "bright_magenta")
|
|
168
|
+
parts.append(f"[bold {color}][{abbrev}][/bold {color}]")
|
|
169
|
+
|
|
170
|
+
# Character name
|
|
171
|
+
parts.append(f"[bold]{char}[/bold]")
|
|
172
|
+
|
|
173
|
+
# Theme name
|
|
174
|
+
if theme:
|
|
175
|
+
from pennyfarthing_scripts.bikerack.base_panel import humanize_theme
|
|
176
|
+
parts.append(f"[dim]{humanize_theme(theme)}[/dim]")
|
|
177
|
+
|
|
178
|
+
# Streaming indicator
|
|
179
|
+
if self._is_streaming:
|
|
180
|
+
parts.append("[bold yellow]⚡[/bold yellow]")
|
|
181
|
+
|
|
182
|
+
line = " ".join(parts)
|
|
183
|
+
|
|
184
|
+
# Role description / style subtitle
|
|
185
|
+
if role_desc:
|
|
186
|
+
line += f"\n[dim]{role_desc}[/dim]"
|
|
187
|
+
elif style:
|
|
188
|
+
line += f"\n[dim]{style}[/dim]"
|
|
189
|
+
|
|
190
|
+
# Quote
|
|
191
|
+
if quote:
|
|
192
|
+
line += f"\n[italic dim]\"{quote}\"[/italic dim]"
|
|
193
|
+
|
|
194
|
+
self.update(line)
|
|
63
195
|
|
|
64
196
|
|
|
65
197
|
class ConnectionStatus(Static):
|
|
@@ -74,50 +206,212 @@ class ConnectionStatus(Static):
|
|
|
74
206
|
self.update(STATE_DISPLAY.get(state, "● Unknown"))
|
|
75
207
|
|
|
76
208
|
|
|
209
|
+
class PanelCommands(Provider):
|
|
210
|
+
"""Command palette provider for panel switching."""
|
|
211
|
+
|
|
212
|
+
async def search(self, query: str) -> Hits:
|
|
213
|
+
matcher = self.matcher(query)
|
|
214
|
+
for panel_key, display_name in PANEL_REGISTRY:
|
|
215
|
+
icon = get_panel_icon(panel_key)
|
|
216
|
+
label = f"{icon} {display_name}" if icon else display_name
|
|
217
|
+
score = matcher.match(display_name)
|
|
218
|
+
if score > 0:
|
|
219
|
+
yield Hit(
|
|
220
|
+
score,
|
|
221
|
+
matcher.highlight(label),
|
|
222
|
+
partial(self.app.action_switch_panel, panel_key),
|
|
223
|
+
help=f"Switch to {display_name} panel",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
77
227
|
class BikeRackApp(App):
|
|
78
228
|
"""BikeRack TUI application shell."""
|
|
79
229
|
|
|
80
230
|
TITLE = "BikeRack"
|
|
81
231
|
|
|
232
|
+
CSS = """
|
|
233
|
+
#agent-header {
|
|
234
|
+
height: auto;
|
|
235
|
+
max-height: 3;
|
|
236
|
+
padding: 0 1;
|
|
237
|
+
}
|
|
238
|
+
#tab-bar {
|
|
239
|
+
height: 1;
|
|
240
|
+
}
|
|
241
|
+
#connection-status {
|
|
242
|
+
height: 1;
|
|
243
|
+
}
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
COMMANDS = App.COMMANDS | {PanelCommands}
|
|
247
|
+
|
|
82
248
|
BINDINGS = [
|
|
83
249
|
Binding("q", "quit", "Quit"),
|
|
250
|
+
Binding("1", "switch_panel('sprint')", "Sprint", show=False),
|
|
251
|
+
Binding("2", "switch_panel('git')", "Git", show=False),
|
|
252
|
+
Binding("3", "switch_panel('diffs')", "Diffs", show=False),
|
|
253
|
+
Binding("4", "switch_panel('changed')", "Changed", show=False),
|
|
254
|
+
Binding("5", "switch_panel('background')", "Background", show=False),
|
|
255
|
+
Binding("6", "switch_panel('debug')", "Debug", show=False),
|
|
256
|
+
Binding("7", "switch_panel('progress')", "Progress", show=False),
|
|
257
|
+
Binding("bracketright", "next_panel", "]Next"),
|
|
258
|
+
Binding("bracketleft", "prev_panel", "[Prev"),
|
|
259
|
+
Binding("tab", "next_panel", show=False),
|
|
260
|
+
Binding("shift+tab", "prev_panel", show=False),
|
|
261
|
+
Binding("n", "next_diff_file", "Next file", show=False),
|
|
262
|
+
Binding("p", "prev_diff_file", "Prev file", show=False),
|
|
263
|
+
Binding("j", "next_epic", show=False),
|
|
264
|
+
Binding("k", "prev_epic", show=False),
|
|
265
|
+
Binding("e", "toggle_epic", show=False),
|
|
84
266
|
]
|
|
85
267
|
|
|
86
268
|
def __init__(self, client=None, **kwargs):
|
|
87
269
|
super().__init__(**kwargs)
|
|
88
270
|
self._client = client
|
|
89
|
-
self._focused_panel: str
|
|
271
|
+
self._focused_panel: str = "sprint"
|
|
90
272
|
self._previous_panel: str | None = None
|
|
91
273
|
|
|
92
274
|
def compose(self) -> ComposeResult:
|
|
93
275
|
yield Header()
|
|
94
|
-
yield
|
|
276
|
+
yield AgentHeader(id="agent-header")
|
|
277
|
+
yield PanelTabBar(id="tab-bar")
|
|
95
278
|
yield ConnectionStatus(
|
|
96
279
|
STATE_DISPLAY[ConnectionState.DISCONNECTED],
|
|
97
280
|
id="connection-status",
|
|
98
281
|
)
|
|
99
282
|
with VerticalScroll(id="main-content"):
|
|
100
|
-
yield SprintPanel(client=self._client, id="sprint
|
|
283
|
+
yield SprintPanel(client=self._client, id="panel-sprint")
|
|
284
|
+
yield GitPanel(client=self._client, id="panel-git")
|
|
285
|
+
yield DiffsPanel(client=self._client, id="panel-diffs")
|
|
286
|
+
yield ChangedPanel(client=self._client, id="panel-changed")
|
|
287
|
+
yield BackgroundPanel(client=self._client, id="panel-background")
|
|
288
|
+
yield DebugPanel(client=self._client, id="panel-debug")
|
|
289
|
+
yield ProgressPanel(client=self._client, id="panel-progress")
|
|
101
290
|
yield Footer()
|
|
102
291
|
|
|
103
292
|
async def on_mount(self) -> None:
|
|
293
|
+
# Restore last panel or default to sprint
|
|
104
294
|
result = get_last_panel()
|
|
295
|
+
initial = "sprint"
|
|
105
296
|
if result.get("success") and result.get("last_panel"):
|
|
106
|
-
|
|
297
|
+
last = result["last_panel"]
|
|
298
|
+
if last in _PANEL_KEYS:
|
|
299
|
+
initial = last
|
|
300
|
+
|
|
301
|
+
self._focused_panel = initial
|
|
107
302
|
|
|
108
|
-
#
|
|
109
|
-
|
|
303
|
+
# Hide all panels except the active one
|
|
304
|
+
for panel_key in _PANEL_KEYS:
|
|
305
|
+
widget_id = f"panel-{panel_key}"
|
|
306
|
+
try:
|
|
307
|
+
widget = self.query_one(f"#{widget_id}")
|
|
308
|
+
widget.display = (panel_key == initial)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# Set tab bar active state
|
|
313
|
+
self._update_tab_bar(initial)
|
|
110
314
|
|
|
111
315
|
if self._client is not None:
|
|
112
316
|
self._client.on_state_change(self._on_ws_state_change)
|
|
113
317
|
self._client.subscribe("focus", self._handle_focus_message)
|
|
318
|
+
self._client.subscribe("persona", self._handle_persona_message)
|
|
114
319
|
self.run_worker(self._client.connect(), exclusive=True, name="ws-client")
|
|
115
320
|
|
|
116
|
-
def
|
|
117
|
-
"""
|
|
321
|
+
def action_switch_panel(self, key: str) -> None:
|
|
322
|
+
"""Switch to a panel by key."""
|
|
323
|
+
if key not in _PANEL_KEYS:
|
|
324
|
+
return
|
|
325
|
+
if key == self._focused_panel:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Hide current panel
|
|
329
|
+
try:
|
|
330
|
+
current = self.query_one(f"#panel-{self._focused_panel}")
|
|
331
|
+
current.display = False
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
# Show target panel
|
|
336
|
+
try:
|
|
337
|
+
target = self.query_one(f"#panel-{key}")
|
|
338
|
+
target.display = True
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
self._previous_panel = self._focused_panel
|
|
343
|
+
self._focused_panel = key
|
|
344
|
+
save_last_panel(key, project_dir=None)
|
|
345
|
+
self._update_tab_bar(key)
|
|
346
|
+
|
|
347
|
+
def action_next_panel(self) -> None:
|
|
348
|
+
"""Cycle to the next panel."""
|
|
118
349
|
try:
|
|
119
|
-
|
|
120
|
-
|
|
350
|
+
idx = _PANEL_KEYS.index(self._focused_panel)
|
|
351
|
+
except ValueError:
|
|
352
|
+
idx = 0
|
|
353
|
+
next_idx = (idx + 1) % len(_PANEL_KEYS)
|
|
354
|
+
self.action_switch_panel(_PANEL_KEYS[next_idx])
|
|
355
|
+
|
|
356
|
+
def action_prev_panel(self) -> None:
|
|
357
|
+
"""Cycle to the previous panel."""
|
|
358
|
+
try:
|
|
359
|
+
idx = _PANEL_KEYS.index(self._focused_panel)
|
|
360
|
+
except ValueError:
|
|
361
|
+
idx = 0
|
|
362
|
+
prev_idx = (idx - 1) % len(_PANEL_KEYS)
|
|
363
|
+
self.action_switch_panel(_PANEL_KEYS[prev_idx])
|
|
364
|
+
|
|
365
|
+
def action_next_diff_file(self) -> None:
|
|
366
|
+
"""Advance to next file in diffs panel."""
|
|
367
|
+
if self._focused_panel == "diffs":
|
|
368
|
+
try:
|
|
369
|
+
panel = self.query_one("#panel-diffs", DiffsPanel)
|
|
370
|
+
panel.next_file()
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
def action_prev_diff_file(self) -> None:
|
|
375
|
+
"""Go to previous file in diffs panel."""
|
|
376
|
+
if self._focused_panel == "diffs":
|
|
377
|
+
try:
|
|
378
|
+
panel = self.query_one("#panel-diffs", DiffsPanel)
|
|
379
|
+
panel.prev_file()
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
def action_next_epic(self) -> None:
|
|
384
|
+
"""Move to next epic in sprint panel."""
|
|
385
|
+
if self._focused_panel == "sprint":
|
|
386
|
+
try:
|
|
387
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
388
|
+
panel.next_epic()
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
def action_prev_epic(self) -> None:
|
|
393
|
+
"""Move to previous epic in sprint panel."""
|
|
394
|
+
if self._focused_panel == "sprint":
|
|
395
|
+
try:
|
|
396
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
397
|
+
panel.prev_epic()
|
|
398
|
+
except Exception:
|
|
399
|
+
pass
|
|
400
|
+
|
|
401
|
+
def action_toggle_epic(self) -> None:
|
|
402
|
+
"""Toggle expand/collapse on selected epic in sprint panel."""
|
|
403
|
+
if self._focused_panel == "sprint":
|
|
404
|
+
try:
|
|
405
|
+
panel = self.query_one("#panel-sprint", SprintPanel)
|
|
406
|
+
panel.toggle_epic()
|
|
407
|
+
except Exception:
|
|
408
|
+
pass
|
|
409
|
+
|
|
410
|
+
def _update_tab_bar(self, panel_key: str) -> None:
|
|
411
|
+
"""Update the tab bar widget with the given panel key."""
|
|
412
|
+
try:
|
|
413
|
+
tab_bar = self.query_one("#tab-bar", PanelTabBar)
|
|
414
|
+
tab_bar.active = panel_key
|
|
121
415
|
except Exception:
|
|
122
416
|
pass
|
|
123
417
|
|
|
@@ -135,14 +429,23 @@ class BikeRackApp(App):
|
|
|
135
429
|
return
|
|
136
430
|
|
|
137
431
|
focus = message["focus"]
|
|
138
|
-
if focus is not None:
|
|
432
|
+
if focus is not None and focus in _PANEL_KEYS:
|
|
433
|
+
self.action_switch_panel(focus)
|
|
434
|
+
elif focus is not None:
|
|
435
|
+
# Panel exists in display names but not implemented — just update state
|
|
139
436
|
self._previous_panel = self._focused_panel
|
|
140
437
|
self._focused_panel = focus
|
|
141
438
|
save_last_panel(focus, project_dir=None)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
439
|
+
|
|
440
|
+
def _handle_persona_message(self, message: dict[str, Any] | None) -> None:
|
|
441
|
+
"""Handle incoming persona channel messages."""
|
|
442
|
+
if message is None or not isinstance(message, dict):
|
|
443
|
+
return
|
|
444
|
+
try:
|
|
445
|
+
header = self.query_one("#agent-header", AgentHeader)
|
|
446
|
+
header._apply_persona(message)
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
146
449
|
|
|
147
450
|
def _on_ws_state_change(self, state: ConnectionState) -> None:
|
|
148
451
|
"""Handle WheelHub connection state changes."""
|
|
@@ -156,16 +459,19 @@ class BikeRackApp(App):
|
|
|
156
459
|
DEFAULT_PORT = 2898
|
|
157
460
|
|
|
158
461
|
|
|
159
|
-
def main(
|
|
462
|
+
def main(
|
|
463
|
+
port: int | None = None,
|
|
464
|
+
project_dir: Path | None = None,
|
|
465
|
+
) -> None:
|
|
160
466
|
"""Launch BikeRack TUI as a standalone application.
|
|
161
467
|
|
|
162
468
|
Args:
|
|
163
|
-
port: Explicit WheelHub port. If None, reads from .
|
|
469
|
+
port: Explicit WheelHub port. If None, reads from .wheelhub-port file.
|
|
164
470
|
project_dir: Project directory for port file discovery. Defaults to cwd.
|
|
165
471
|
"""
|
|
166
472
|
if port is None:
|
|
167
473
|
if project_dir is not None:
|
|
168
|
-
port_file = project_dir / ".
|
|
474
|
+
port_file = project_dir / ".wheelhub-port"
|
|
169
475
|
if port_file.exists():
|
|
170
476
|
try:
|
|
171
477
|
port = int(port_file.read_text().strip())
|