@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.
Files changed (263) hide show
  1. package/README.md +8 -8
  2. package/package.json +16 -14
  3. package/packages/core/dist/cli/utils/constants.d.ts +1 -1
  4. package/packages/core/dist/cli/utils/constants.d.ts.map +1 -1
  5. package/packages/core/dist/cli/utils/constants.js +2 -1
  6. package/packages/core/dist/cli/utils/constants.js.map +1 -1
  7. package/packages/core/dist/consultation/dialogue-manager.d.ts +75 -0
  8. package/packages/core/dist/consultation/dialogue-manager.d.ts.map +1 -0
  9. package/packages/core/dist/consultation/dialogue-manager.js +334 -0
  10. package/packages/core/dist/consultation/dialogue-manager.js.map +1 -0
  11. package/packages/core/dist/consultation/dialogue-manager.test.d.ts +19 -0
  12. package/packages/core/dist/consultation/dialogue-manager.test.d.ts.map +1 -0
  13. package/packages/core/dist/consultation/dialogue-manager.test.js +444 -0
  14. package/packages/core/dist/consultation/dialogue-manager.test.js.map +1 -0
  15. package/packages/core/dist/server/api/git.d.ts +13 -1
  16. package/packages/core/dist/server/api/git.d.ts.map +1 -1
  17. package/packages/core/dist/server/api/git.js +53 -34
  18. package/packages/core/dist/server/api/git.js.map +1 -1
  19. package/packages/core/dist/server/otlp-receiver.d.ts +16 -11
  20. package/packages/core/dist/server/otlp-receiver.d.ts.map +1 -1
  21. package/packages/core/dist/server/otlp-receiver.js +185 -24
  22. package/packages/core/dist/server/otlp-receiver.js.map +1 -1
  23. package/packages/core/dist/server/otlp-receiver.test.d.ts +21 -0
  24. package/packages/core/dist/server/otlp-receiver.test.d.ts.map +1 -0
  25. package/packages/core/dist/server/otlp-receiver.test.js +446 -0
  26. package/packages/core/dist/server/otlp-receiver.test.js.map +1 -0
  27. package/packages/core/dist/shared/portrait-resolver.d.ts +9 -0
  28. package/packages/core/dist/shared/portrait-resolver.d.ts.map +1 -1
  29. package/packages/core/dist/shared/portrait-resolver.js +27 -0
  30. package/packages/core/dist/shared/portrait-resolver.js.map +1 -1
  31. package/packages/core/dist/shared/portrait-resolver.test.js +47 -1
  32. package/packages/core/dist/shared/portrait-resolver.test.js.map +1 -1
  33. package/packages/core/dist/shared/skill-search.test.js +2 -2
  34. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts +13 -0
  35. package/packages/core/dist/shared/tandem-portrait-inventory.test.d.ts.map +1 -0
  36. package/packages/core/dist/shared/tandem-portrait-inventory.test.js +126 -0
  37. package/packages/core/dist/shared/tandem-portrait-inventory.test.js.map +1 -0
  38. package/pennyfarthing-dist/agents/dev.md +1 -1
  39. package/pennyfarthing-dist/agents/reviewer.md +1 -1
  40. package/pennyfarthing-dist/agents/sm-setup.md +1 -1
  41. package/pennyfarthing-dist/agents/sm.md +2 -2
  42. package/pennyfarthing-dist/agents/tea.md +1 -1
  43. package/pennyfarthing-dist/agents/testing-runner.md +2 -1
  44. package/pennyfarthing-dist/commands/pf-chore.md +2 -2
  45. package/pennyfarthing-dist/commands/pf-standalone.md +7 -2
  46. package/pennyfarthing-dist/guides/agent-behavior.md +1 -1
  47. package/pennyfarthing-dist/guides/agent-tag-taxonomy.md +1 -1
  48. package/pennyfarthing-dist/guides/bikerack.md +3 -3
  49. package/pennyfarthing-dist/guides/hooks.md +1 -1
  50. package/pennyfarthing-dist/guides/worktree-mode.md +3 -3
  51. package/pennyfarthing-dist/guides/xml-tags.md +2 -2
  52. package/pennyfarthing-dist/scripts/README.md +1 -1
  53. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  54. package/pennyfarthing-dist/scripts/core/check-context.sh +1 -1
  55. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +322 -0
  56. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  57. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  58. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  59. package/pennyfarthing-dist/scripts/git/README.md +24 -14
  60. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +5 -266
  61. package/pennyfarthing-dist/scripts/git/git-status-all.sh +5 -151
  62. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +6 -144
  63. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  64. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +5 -496
  65. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  66. package/pennyfarthing-dist/scripts/hooks/README.md +1 -1
  67. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +1 -1
  68. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  69. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  70. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  71. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  72. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +9 -11
  73. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  74. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  75. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  76. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  77. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  78. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  79. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  80. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  81. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  82. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  83. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +1 -1
  84. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  85. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  86. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  87. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  88. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  89. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  90. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  91. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  92. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  93. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  94. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  95. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  96. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  97. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  98. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  99. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  100. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  101. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  102. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  103. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  104. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  105. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  106. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -0
  107. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  108. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  109. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  110. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  111. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  112. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  113. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  114. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  115. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  116. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  117. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +76 -0
  118. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  119. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  120. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  121. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  122. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  123. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  124. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  125. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  126. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  127. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  128. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  129. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  130. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  131. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  132. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  133. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  134. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  135. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  136. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  137. package/pennyfarthing-dist/scripts/workflow/check.py +0 -0
  138. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  139. package/pennyfarthing-dist/scripts/workflow/complete-step.py +0 -0
  140. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  141. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +4 -221
  142. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  143. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +5 -13
  144. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +4 -123
  145. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +4 -33
  146. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +4 -156
  147. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +4 -131
  148. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +4 -249
  149. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +4 -160
  150. package/pennyfarthing-dist/skills/pf-bc/usage.md +1 -1
  151. package/pennyfarthing-dist/skills/pf-jira/examples.md +5 -2
  152. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  153. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  154. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  155. package/pennyfarthing-dist/skills/pf-workflow/examples.md +27 -16
  156. package/pennyfarthing-dist/skills/pf-workflow/skill.md +9 -12
  157. package/pennyfarthing-dist/skills/pf-workflow/usage.md +33 -8
  158. package/pennyfarthing-dist/workflows/bdd-tandem.yaml +18 -6
  159. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-01-analyze.md +1 -1
  160. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-04-verify.md +1 -1
  161. package/pennyfarthing-dist/workflows/git-cleanup/steps/step-05-complete.md +1 -1
  162. package/pennyfarthing-dist/workflows/review-tandem.yaml +65 -0
  163. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +16 -8
  164. package/pennyfarthing_scripts/CLAUDE.md +26 -4
  165. package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
  166. package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
  167. package/pennyfarthing_scripts/__pycache__/pretooluse_hook.cpython-314.pyc +0 -0
  168. package/pennyfarthing_scripts/__pycache__/session_start_hook.cpython-314.pyc +0 -0
  169. package/pennyfarthing_scripts/bc/__pycache__/cli.cpython-314.pyc +0 -0
  170. package/pennyfarthing_scripts/bc/cli.py +3 -5
  171. package/pennyfarthing_scripts/bikerack/__pycache__/background_panel.cpython-314.pyc +0 -0
  172. package/pennyfarthing_scripts/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  173. package/pennyfarthing_scripts/bikerack/__pycache__/changed_panel.cpython-314.pyc +0 -0
  174. package/pennyfarthing_scripts/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  175. package/pennyfarthing_scripts/bikerack/__pycache__/debug_panel.cpython-314.pyc +0 -0
  176. package/pennyfarthing_scripts/bikerack/__pycache__/diffs_panel.cpython-314.pyc +0 -0
  177. package/pennyfarthing_scripts/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  178. package/pennyfarthing_scripts/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  179. package/pennyfarthing_scripts/bikerack/__pycache__/portrait.cpython-314.pyc +0 -0
  180. package/pennyfarthing_scripts/bikerack/__pycache__/progress_panel.cpython-314.pyc +0 -0
  181. package/pennyfarthing_scripts/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  182. package/pennyfarthing_scripts/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  183. package/pennyfarthing_scripts/bikerack/__pycache__/ws_client.cpython-314.pyc +0 -0
  184. package/pennyfarthing_scripts/bikerack/background_panel.py +86 -5
  185. package/pennyfarthing_scripts/bikerack/base_panel.py +62 -0
  186. package/pennyfarthing_scripts/bikerack/changed_panel.py +32 -28
  187. package/pennyfarthing_scripts/bikerack/cli.py +10 -11
  188. package/pennyfarthing_scripts/bikerack/debug_panel.py +31 -1
  189. package/pennyfarthing_scripts/bikerack/diffs_panel.py +74 -17
  190. package/pennyfarthing_scripts/bikerack/git_panel.py +103 -33
  191. package/pennyfarthing_scripts/bikerack/launcher.py +15 -15
  192. package/pennyfarthing_scripts/bikerack/progress_panel.py +315 -0
  193. package/pennyfarthing_scripts/bikerack/sprint_panel.py +158 -26
  194. package/pennyfarthing_scripts/bikerack/tui.py +336 -30
  195. package/pennyfarthing_scripts/bikerack/ws_client.py +2 -2
  196. package/pennyfarthing_scripts/cli.py +37 -65
  197. package/pennyfarthing_scripts/consultation/__init__.py +1 -0
  198. package/pennyfarthing_scripts/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  199. package/pennyfarthing_scripts/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  200. package/pennyfarthing_scripts/consultation/cli.py +149 -0
  201. package/pennyfarthing_scripts/consultation/dialogue_manager.py +417 -0
  202. package/pennyfarthing_scripts/context.py +3 -3
  203. package/pennyfarthing_scripts/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  204. package/pennyfarthing_scripts/epic/__pycache__/cli.cpython-314.pyc +0 -0
  205. package/pennyfarthing_scripts/git/__init__.py +12 -1
  206. package/pennyfarthing_scripts/git/__pycache__/__init__.cpython-314.pyc +0 -0
  207. package/pennyfarthing_scripts/git/__pycache__/create_branches.cpython-314.pyc +0 -0
  208. package/pennyfarthing_scripts/git/__pycache__/hooks_installer.cpython-314.pyc +0 -0
  209. package/pennyfarthing_scripts/git/__pycache__/repos.cpython-314.pyc +0 -0
  210. package/pennyfarthing_scripts/git/__pycache__/status_all.cpython-314.pyc +0 -0
  211. package/pennyfarthing_scripts/git/__pycache__/worktree.cpython-314.pyc +0 -0
  212. package/pennyfarthing_scripts/git/create_branches.py +3 -4
  213. package/pennyfarthing_scripts/git/hooks_installer.py +152 -0
  214. package/pennyfarthing_scripts/git/repos.py +196 -0
  215. package/pennyfarthing_scripts/git/status_all.py +27 -11
  216. package/pennyfarthing_scripts/git/worktree.py +302 -0
  217. package/pennyfarthing_scripts/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  218. package/pennyfarthing_scripts/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing_scripts/git_group/cli.py +143 -40
  220. package/pennyfarthing_scripts/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  221. package/pennyfarthing_scripts/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  222. package/pennyfarthing_scripts/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  223. package/pennyfarthing_scripts/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  224. package/pennyfarthing_scripts/handoff/complete_phase.py +12 -0
  225. package/pennyfarthing_scripts/handoff/resolve_gate.py +5 -14
  226. package/pennyfarthing_scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  227. package/pennyfarthing_scripts/hooks.py +3 -17
  228. package/pennyfarthing_scripts/pretooluse_hook.py +1 -1
  229. package/pennyfarthing_scripts/prime/__pycache__/heatmap.cpython-314.pyc +0 -0
  230. package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  231. package/pennyfarthing_scripts/prime/heatmap.py +655 -0
  232. package/pennyfarthing_scripts/session/__pycache__/__init__.cpython-314.pyc +0 -0
  233. package/pennyfarthing_scripts/session/__pycache__/cli.cpython-314.pyc +0 -0
  234. package/pennyfarthing_scripts/session_start_hook.py +1 -1
  235. package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  236. package/pennyfarthing_scripts/sprint/loader.py +15 -1
  237. package/pennyfarthing_scripts/sprint/story_finish.py +14 -0
  238. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_cli.cpython-314-pytest-9.0.2.pyc +0 -0
  239. package/pennyfarthing_scripts/tests/__pycache__/test_handoff_e2e.cpython-314-pytest-9.0.2.pyc +0 -0
  240. package/pennyfarthing_scripts/tests/__pycache__/test_workflow_check.cpython-314-pytest-9.0.2.pyc +0 -0
  241. package/pennyfarthing_scripts/tests/test_bikerack.py +51 -51
  242. package/pennyfarthing_scripts/tests/test_dialogue_manager.py +811 -0
  243. package/pennyfarthing_scripts/tests/test_handoff_cli.py +16 -11
  244. package/pennyfarthing_scripts/tests/test_workflow_check.py +2 -3
  245. package/pennyfarthing_scripts/validate/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing_scripts/validate/adapters/tandem_awareness.py +254 -0
  247. package/pennyfarthing_scripts/validate/cli.py +17 -5
  248. package/pennyfarthing_scripts/workflow/__init__.py +40 -0
  249. package/pennyfarthing_scripts/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  250. package/pennyfarthing_scripts/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  251. package/pennyfarthing_scripts/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  252. package/pennyfarthing_scripts/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  253. package/pennyfarthing_scripts/workflow/__pycache__/state.cpython-314.pyc +0 -0
  254. package/pennyfarthing_scripts/workflow/cli.py +1099 -0
  255. package/pennyfarthing_scripts/workflow/helpers.py +241 -0
  256. package/pennyfarthing_scripts/{workflow.py → workflow/scale.py} +0 -104
  257. package/pennyfarthing_scripts/workflow/state.py +112 -0
  258. package/scripts/README.md +41 -0
  259. package/pennyfarthing-dist/skills/pf-workflow/scripts/list-workflows.sh +0 -91
  260. package/pennyfarthing-dist/skills/pf-workflow/scripts/resume-workflow.sh +0 -163
  261. package/pennyfarthing-dist/skills/pf-workflow/scripts/show-workflow.sh +0 -138
  262. package/pennyfarthing-dist/skills/pf-workflow/scripts/start-workflow.sh +0 -273
  263. 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.table import Table
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 render_panel(self, payload: dict[str, Any]) -> Any:
30
- """Render sprint data as Rich renderable.
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
- Returns a Group containing a sprint metrics header and a
33
- story table with columns: ID, Title, Status, Pts, Jira.
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
- # Story table
55
- table = Table(title=sprint.get("name", "Sprint"))
56
- table.add_column("ID", style="cyan")
57
- table.add_column("Title")
58
- table.add_column("Status")
59
- table.add_column("Pts", justify="right")
60
- table.add_column("Jira", style="dim")
61
-
62
- for epic in epics:
63
- for story in epic.get("stories", []):
64
- table.add_row(
65
- story.get("id", ""),
66
- story.get("title", ""),
67
- story.get("status", ""),
68
- str(story.get("points", "")),
69
- story.get("jiraKey") or "",
70
- )
71
-
72
- return Group(header, table)
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
- # Human-readable display names for panels
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
- panel_key: reactive[str] = reactive("sprint")
104
+ class PanelTabBar(Static):
105
+ """Horizontal tab bar showing all available panels with active highlight."""
54
106
 
55
- def watch_panel_key(self, key: str) -> None:
56
- """Update display when the active panel changes."""
57
- icon = get_panel_icon(key)
58
- name = PANEL_DISPLAY_NAMES.get(key, key.title())
59
- if icon:
60
- self.update(f"[bold]{icon} {name}[/bold]")
61
- else:
62
- self.update(f"[bold]{name}[/bold]")
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 | None = None
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 PanelIndicator(id="panel-indicator")
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-panel")
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
- self._focused_panel = result["last_panel"]
297
+ last = result["last_panel"]
298
+ if last in _PANEL_KEYS:
299
+ initial = last
300
+
301
+ self._focused_panel = initial
107
302
 
108
- # Set initial panel indicator
109
- self._update_panel_indicator(self._focused_panel or "sprint")
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 _update_panel_indicator(self, panel_key: str) -> None:
117
- """Update the panel indicator widget with the given panel key."""
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
- indicator = self.query_one("#panel-indicator", PanelIndicator)
120
- indicator.panel_key = panel_key
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
- self._update_panel_indicator(focus)
143
- else:
144
- self._focused_panel = None
145
- self._previous_panel = None
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(port: int | None = None, project_dir: Path | None = None) -> None:
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 .bikerack-port file.
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 / ".bikerack-port"
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())