@pennyfarthing/core 11.3.8 → 11.4.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 (304) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -1
  3. package/packages/core/dist/public/css/react.css +1 -1
  4. package/packages/core/dist/public/js/react/react.js +24 -24
  5. package/packages/core/src/public/App.tsx +356 -0
  6. package/packages/core/src/public/components/AgentLoadDialog.tsx +202 -0
  7. package/packages/core/src/public/components/AgentPopup.tsx +308 -0
  8. package/packages/core/src/public/components/ApprovalModal/ApprovalModal.css +35 -0
  9. package/packages/core/src/public/components/ApprovalModal/index.tsx +632 -0
  10. package/packages/core/src/public/components/BikeRackIndex.tsx +53 -0
  11. package/packages/core/src/public/components/BikeRackWorkspace.tsx +217 -0
  12. package/packages/core/src/public/components/CommandPalette.tsx +554 -0
  13. package/packages/core/src/public/components/ConfirmDialog.tsx +168 -0
  14. package/packages/core/src/public/components/ContextIndicator/ContextIndicator.css +85 -0
  15. package/packages/core/src/public/components/ContextIndicator/index.tsx +330 -0
  16. package/packages/core/src/public/components/ContextSparkline.tsx +56 -0
  17. package/packages/core/src/public/components/ControlBar.tsx +636 -0
  18. package/packages/core/src/public/components/DeadCodeDialog.tsx +169 -0
  19. package/packages/core/src/public/components/DiffViewer.tsx +585 -0
  20. package/packages/core/src/public/components/DockviewWorkspace.tsx +749 -0
  21. package/packages/core/src/public/components/Editor.tsx +630 -0
  22. package/packages/core/src/public/components/ErrorBoundary.tsx +67 -0
  23. package/packages/core/src/public/components/FileTree.tsx +379 -0
  24. package/packages/core/src/public/components/FontPicker/FontPicker.css +276 -0
  25. package/packages/core/src/public/components/FontPicker/index.tsx +430 -0
  26. package/packages/core/src/public/components/FullFileTree.tsx +237 -0
  27. package/packages/core/src/public/components/HealthGauge.tsx +181 -0
  28. package/packages/core/src/public/components/Message.tsx +225 -0
  29. package/packages/core/src/public/components/MessageList.tsx +98 -0
  30. package/packages/core/src/public/components/MessageView.tsx +400 -0
  31. package/packages/core/src/public/components/ModeSwitch/ModeSwitch.css +165 -0
  32. package/packages/core/src/public/components/ModeSwitch/index.tsx +372 -0
  33. package/packages/core/src/public/components/PersonaHeader.tsx +242 -0
  34. package/packages/core/src/public/components/ProjectInfoBar.tsx +45 -0
  35. package/packages/core/src/public/components/QuickActions.tsx +267 -0
  36. package/packages/core/src/public/components/SpanTimeline.tsx +352 -0
  37. package/packages/core/src/public/components/StandalonePanel.tsx +82 -0
  38. package/packages/core/src/public/components/StatsStrip.tsx +162 -0
  39. package/packages/core/src/public/components/StreamingContent.tsx +77 -0
  40. package/packages/core/src/public/components/SubagentSpan.tsx +180 -0
  41. package/packages/core/src/public/components/TandemPortrait.tsx +72 -0
  42. package/packages/core/src/public/components/ThemePalette/ThemePalette.css +179 -0
  43. package/packages/core/src/public/components/ThemePalette/index.tsx +326 -0
  44. package/packages/core/src/public/components/ToolCallBlock.tsx +252 -0
  45. package/packages/core/src/public/components/ToolStack.tsx +209 -0
  46. package/packages/core/src/public/components/ToolStatus.tsx +57 -0
  47. package/packages/core/src/public/components/dialogs/CodeMarkersDialog.tsx +169 -0
  48. package/packages/core/src/public/components/dialogs/ComplexityDialog.tsx +163 -0
  49. package/packages/core/src/public/components/dialogs/DependenciesDialog.tsx +120 -0
  50. package/packages/core/src/public/components/dialogs/HotspotsDialog.tsx +451 -0
  51. package/packages/core/src/public/components/dialogs/ToolDialog.tsx +43 -0
  52. package/packages/core/src/public/components/panel-registry.ts +13 -0
  53. package/packages/core/src/public/components/panels/ACPanel.tsx +93 -0
  54. package/packages/core/src/public/components/panels/AcceptanceCriteriaPanel.tsx +104 -0
  55. package/packages/core/src/public/components/panels/AuditLogPanel.tsx +489 -0
  56. package/packages/core/src/public/components/panels/BackgroundPanel.tsx +115 -0
  57. package/packages/core/src/public/components/panels/BikeLanePanel.tsx +214 -0
  58. package/packages/core/src/public/components/panels/DebugPanel.tsx +344 -0
  59. package/packages/core/src/public/components/panels/DiffView.tsx +109 -0
  60. package/packages/core/src/public/components/panels/DiffsPanel.tsx +56 -0
  61. package/packages/core/src/public/components/panels/GitPanel.tsx +260 -0
  62. package/packages/core/src/public/components/panels/HotspotsPanel.tsx +365 -0
  63. package/packages/core/src/public/components/panels/MessageFeed.tsx +39 -0
  64. package/packages/core/src/public/components/panels/MessagePanel.tsx +497 -0
  65. package/packages/core/src/public/components/panels/ProgressPanel.tsx +189 -0
  66. package/packages/core/src/public/components/panels/SettingsPanel.tsx +361 -0
  67. package/packages/core/src/public/components/panels/SprintPanel.tsx +723 -0
  68. package/packages/core/src/public/components/panels/TandemPanel.tsx +104 -0
  69. package/packages/core/src/public/components/panels/TaskTracker.tsx +48 -0
  70. package/packages/core/src/public/components/panels/TeamPanel.tsx +64 -0
  71. package/packages/core/src/public/components/panels/TeamRoster.tsx +67 -0
  72. package/packages/core/src/public/components/panels/TodoPanel.tsx +142 -0
  73. package/packages/core/src/public/components/panels/WorkflowPanel.tsx +224 -0
  74. package/packages/core/src/public/components/panels/index.ts +24 -0
  75. package/packages/core/src/public/components/ui/alert-dialog.tsx +139 -0
  76. package/packages/core/src/public/components/ui/badge.tsx +36 -0
  77. package/packages/core/src/public/components/ui/button.tsx +57 -0
  78. package/packages/core/src/public/components/ui/checkbox.tsx +28 -0
  79. package/packages/core/src/public/components/ui/collapsible.tsx +9 -0
  80. package/packages/core/src/public/components/ui/command.tsx +151 -0
  81. package/packages/core/src/public/components/ui/dialog.tsx +120 -0
  82. package/packages/core/src/public/components/ui/popover.tsx +31 -0
  83. package/packages/core/src/public/components/ui/progress.tsx +28 -0
  84. package/packages/core/src/public/components/ui/scroll-area.tsx +46 -0
  85. package/packages/core/src/public/components/ui/select.tsx +157 -0
  86. package/packages/core/src/public/components/ui/separator.tsx +29 -0
  87. package/packages/core/src/public/components/ui/skeleton.tsx +15 -0
  88. package/packages/core/src/public/components/ui/switch.tsx +27 -0
  89. package/packages/core/src/public/components/ui/toggle-group.tsx +59 -0
  90. package/packages/core/src/public/components/ui/toggle.tsx +43 -0
  91. package/packages/core/src/public/components/ui/tooltip.tsx +30 -0
  92. package/packages/core/src/public/contexts/ClaudeContext.tsx +311 -0
  93. package/packages/core/src/public/contexts/MessageQueueContext.tsx +143 -0
  94. package/packages/core/src/public/css/theme-browser.css +550 -0
  95. package/packages/core/src/public/css/theme-system.css +630 -0
  96. package/packages/core/src/public/hooks/index.ts +49 -0
  97. package/packages/core/src/public/hooks/useAgentLoad.ts +105 -0
  98. package/packages/core/src/public/hooks/useBackgroundTasks.ts +131 -0
  99. package/packages/core/src/public/hooks/useClaude.ts +234 -0
  100. package/packages/core/src/public/hooks/useCodeMarkers.ts +101 -0
  101. package/packages/core/src/public/hooks/useColorScheme.ts +42 -0
  102. package/packages/core/src/public/hooks/useCommandHistory.ts +99 -0
  103. package/packages/core/src/public/hooks/useComplexity.ts +80 -0
  104. package/packages/core/src/public/hooks/useDeadCode.ts +99 -0
  105. package/packages/core/src/public/hooks/useDependencies.ts +82 -0
  106. package/packages/core/src/public/hooks/useDiffs.ts +143 -0
  107. package/packages/core/src/public/hooks/useFileBrowser.ts +73 -0
  108. package/packages/core/src/public/hooks/useFocusPanel.ts +137 -0
  109. package/packages/core/src/public/hooks/useGitStatus.ts +233 -0
  110. package/packages/core/src/public/hooks/useHealthScore.ts +71 -0
  111. package/packages/core/src/public/hooks/useHotspots.ts +123 -0
  112. package/packages/core/src/public/hooks/useLayoutPersistence.ts +141 -0
  113. package/packages/core/src/public/hooks/useMarkdownParser.ts +36 -0
  114. package/packages/core/src/public/hooks/useMarkerActions.ts +234 -0
  115. package/packages/core/src/public/hooks/useMessageQueue.ts +380 -0
  116. package/packages/core/src/public/hooks/useMessageStream.ts +131 -0
  117. package/packages/core/src/public/hooks/usePersona.ts +112 -0
  118. package/packages/core/src/public/hooks/usePlanModeExit.ts +105 -0
  119. package/packages/core/src/public/hooks/useResponsiveLayout.ts +173 -0
  120. package/packages/core/src/public/hooks/useSprint.ts +157 -0
  121. package/packages/core/src/public/hooks/useStatsStrip.ts +204 -0
  122. package/packages/core/src/public/hooks/useStory.ts +135 -0
  123. package/packages/core/src/public/hooks/useSubagentHelper.ts +64 -0
  124. package/packages/core/src/public/hooks/useSyntaxHighlighter.ts +52 -0
  125. package/packages/core/src/public/hooks/useTabCompletion.ts +124 -0
  126. package/packages/core/src/public/hooks/useTandemObservations.ts +165 -0
  127. package/packages/core/src/public/hooks/useTeamMembers.ts +273 -0
  128. package/packages/core/src/public/hooks/useTodos.ts +93 -0
  129. package/packages/core/src/public/hooks/useUserAvatar.ts +54 -0
  130. package/packages/core/src/public/images/cyclist-dark.png +0 -0
  131. package/packages/core/src/public/images/cyclist-light.png +0 -0
  132. package/packages/core/src/public/index.html +14 -0
  133. package/packages/core/src/public/index.tsx +10 -0
  134. package/packages/core/src/public/lib/utils.ts +6 -0
  135. package/packages/core/src/public/styles/dockview-theme.css +376 -0
  136. package/packages/core/src/public/styles/tailwind.css +4454 -0
  137. package/packages/core/src/public/types/message.ts +51 -0
  138. package/packages/core/src/public/utils/avatar-service.ts +73 -0
  139. package/packages/core/src/public/utils/color-presets.ts +940 -0
  140. package/packages/core/src/public/utils/font-presets.ts +362 -0
  141. package/packages/core/src/public/utils/formatDuration.ts +14 -0
  142. package/packages/core/src/public/utils/markdown.ts +249 -0
  143. package/packages/core/src/public/utils/messageFilters.ts +128 -0
  144. package/packages/core/src/public/utils/slash-commands.ts +341 -0
  145. package/packages/core/src/public/utils/subagent-display.ts +146 -0
  146. package/packages/core/src/public/utils/syntax.ts +219 -0
  147. package/packages/core/src/public/utils/toolIntentSummarizer.ts +199 -0
  148. package/packages/core/src/public/utils/toolStackGrouper.ts +106 -0
  149. package/packages/core/src/public/utils/toolTypeColors.ts +45 -0
  150. package/pennyfarthing-dist/pf/__pycache__/__init__.cpython-314.pyc +0 -0
  151. package/pennyfarthing-dist/pf/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing-dist/pf/__pycache__/context.cpython-314.pyc +0 -0
  153. package/pennyfarthing-dist/pf/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/pennyfarthing-dist/pf/bc/__pycache__/cli.cpython-314.pyc +0 -0
  155. package/pennyfarthing-dist/pf/bc/__pycache__/focus.cpython-314.pyc +0 -0
  156. package/pennyfarthing-dist/pf/bc/__pycache__/split.cpython-314.pyc +0 -0
  157. package/pennyfarthing-dist/pf/bc/cli.py +0 -1
  158. package/pennyfarthing-dist/pf/bc/focus.py +0 -1
  159. package/pennyfarthing-dist/pf/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  160. package/pennyfarthing-dist/pf/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  161. package/pennyfarthing-dist/pf/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  162. package/pennyfarthing-dist/pf/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  163. package/pennyfarthing-dist/pf/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  164. package/pennyfarthing-dist/pf/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  165. package/pennyfarthing-dist/pf/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  166. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  167. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  168. package/pennyfarthing-dist/pf/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  169. package/pennyfarthing-dist/pf/bikerack/base_panel.py +0 -1
  170. package/pennyfarthing-dist/pf/bikerack/events.py +1 -7
  171. package/pennyfarthing-dist/pf/bikerack/git_panel.py +273 -10
  172. package/pennyfarthing-dist/pf/bikerack/portrait_resolver.py +21 -0
  173. package/pennyfarthing-dist/pf/bikerack/sprint_panel.py +58 -1
  174. package/pennyfarthing-dist/pf/bikerack/tui.py +5 -20
  175. package/pennyfarthing-dist/pf/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
  176. package/pennyfarthing-dist/pf/bmad/__pycache__/cli.cpython-314.pyc +0 -0
  177. package/pennyfarthing-dist/pf/bmad/__pycache__/parser.cpython-314.pyc +0 -0
  178. package/pennyfarthing-dist/pf/bmad/parser.py +15 -9
  179. package/pennyfarthing-dist/pf/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  180. package/pennyfarthing-dist/pf/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  181. package/pennyfarthing-dist/pf/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  182. package/pennyfarthing-dist/pf/common/__pycache__/__init__.cpython-314.pyc +0 -0
  183. package/pennyfarthing-dist/pf/common/__pycache__/config.cpython-314.pyc +0 -0
  184. package/pennyfarthing-dist/pf/common/__pycache__/output.cpython-314.pyc +0 -0
  185. package/pennyfarthing-dist/pf/common/__pycache__/pr_config.cpython-314.pyc +0 -0
  186. package/pennyfarthing-dist/pf/common/__pycache__/themes.cpython-314.pyc +0 -0
  187. package/pennyfarthing-dist/pf/common/pr_config.py +27 -2
  188. package/pennyfarthing-dist/pf/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  189. package/pennyfarthing-dist/pf/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  190. package/pennyfarthing-dist/pf/complexity/__pycache__/models.cpython-314.pyc +0 -0
  191. package/pennyfarthing-dist/pf/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing-dist/pf/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  193. package/pennyfarthing-dist/pf/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  194. package/pennyfarthing-dist/pf/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  195. package/pennyfarthing-dist/pf/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  196. package/pennyfarthing-dist/pf/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing-dist/pf/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing-dist/pf/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  199. package/pennyfarthing-dist/pf/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  200. package/pennyfarthing-dist/pf/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  201. package/pennyfarthing-dist/pf/epic/__pycache__/cli.cpython-314.pyc +0 -0
  202. package/pennyfarthing-dist/pf/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing-dist/pf/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  204. package/pennyfarthing-dist/pf/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  205. package/pennyfarthing-dist/pf/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing-dist/pf/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  207. package/pennyfarthing-dist/pf/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  208. package/pennyfarthing-dist/pf/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
  209. package/pennyfarthing-dist/pf/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  210. package/pennyfarthing-dist/pf/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/pennyfarthing-dist/pf/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  212. package/pennyfarthing-dist/pf/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  213. package/pennyfarthing-dist/pf/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  214. package/pennyfarthing-dist/pf/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  215. package/pennyfarthing-dist/pf/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  216. package/pennyfarthing-dist/pf/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  217. package/pennyfarthing-dist/pf/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  218. package/pennyfarthing-dist/pf/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing-dist/pf/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  220. package/pennyfarthing-dist/pf/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  221. package/pennyfarthing-dist/pf/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  222. package/pennyfarthing-dist/pf/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  223. package/pennyfarthing-dist/pf/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  224. package/pennyfarthing-dist/pf/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  225. package/pennyfarthing-dist/pf/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  226. package/pennyfarthing-dist/pf/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  227. package/pennyfarthing-dist/pf/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  228. package/pennyfarthing-dist/pf/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  229. package/pennyfarthing-dist/pf/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  230. package/pennyfarthing-dist/pf/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  231. package/pennyfarthing-dist/pf/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  232. package/pennyfarthing-dist/pf/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  233. package/pennyfarthing-dist/pf/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  234. package/pennyfarthing-dist/pf/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  235. package/pennyfarthing-dist/pf/jira/__pycache__/claim.cpython-314.pyc +0 -0
  236. package/pennyfarthing-dist/pf/jira/__pycache__/cli.cpython-314.pyc +0 -0
  237. package/pennyfarthing-dist/pf/jira/__pycache__/client.cpython-314.pyc +0 -0
  238. package/pennyfarthing-dist/pf/jira/__pycache__/create.cpython-314.pyc +0 -0
  239. package/pennyfarthing-dist/pf/jira/__pycache__/epic.cpython-314.pyc +0 -0
  240. package/pennyfarthing-dist/pf/jira/__pycache__/operations.cpython-314.pyc +0 -0
  241. package/pennyfarthing-dist/pf/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  242. package/pennyfarthing-dist/pf/jira/__pycache__/story.cpython-314.pyc +0 -0
  243. package/pennyfarthing-dist/pf/jira/__pycache__/sync.cpython-314.pyc +0 -0
  244. package/pennyfarthing-dist/pf/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  245. package/pennyfarthing-dist/pf/launch/__pycache__/cli.cpython-314.pyc +0 -0
  246. package/pennyfarthing-dist/pf/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  247. package/pennyfarthing-dist/pf/prime/__pycache__/cli.cpython-314.pyc +0 -0
  248. package/pennyfarthing-dist/pf/prime/__pycache__/loader.cpython-314.pyc +0 -0
  249. package/pennyfarthing-dist/pf/prime/__pycache__/models.cpython-314.pyc +0 -0
  250. package/pennyfarthing-dist/pf/prime/__pycache__/persona.cpython-314.pyc +0 -0
  251. package/pennyfarthing-dist/pf/prime/__pycache__/session.cpython-314.pyc +0 -0
  252. package/pennyfarthing-dist/pf/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  253. package/pennyfarthing-dist/pf/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  254. package/pennyfarthing-dist/pf/session/__pycache__/__init__.cpython-314.pyc +0 -0
  255. package/pennyfarthing-dist/pf/session/__pycache__/cli.cpython-314.pyc +0 -0
  256. package/pennyfarthing-dist/pf/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  257. package/pennyfarthing-dist/pf/settings/__pycache__/cli.cpython-314.pyc +0 -0
  258. package/pennyfarthing-dist/pf/settings/__pycache__/settings.cpython-314.pyc +0 -0
  259. package/pennyfarthing-dist/pf/settings/settings.py +44 -8
  260. package/pennyfarthing-dist/pf/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  261. package/pennyfarthing-dist/pf/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  262. package/pennyfarthing-dist/pf/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  263. package/pennyfarthing-dist/pf/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  264. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  265. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  266. package/pennyfarthing-dist/pf/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  267. package/pennyfarthing-dist/pf/sprint/__pycache__/status.cpython-314.pyc +0 -0
  268. package/pennyfarthing-dist/pf/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  269. package/pennyfarthing-dist/pf/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  270. package/pennyfarthing-dist/pf/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  271. package/pennyfarthing-dist/pf/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  272. package/pennyfarthing-dist/pf/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  273. package/pennyfarthing-dist/pf/sprint/__pycache__/work.cpython-314.pyc +0 -0
  274. package/pennyfarthing-dist/pf/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  275. package/pennyfarthing-dist/pf/sprint/story_finish.py +14 -2
  276. package/pennyfarthing-dist/pf/sprint/validator.py +7 -7
  277. package/pennyfarthing-dist/pf/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  278. package/pennyfarthing-dist/pf/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  279. package/pennyfarthing-dist/pf/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  280. package/pennyfarthing-dist/pf/tests/test_sprint_validator.py +44 -0
  281. package/pennyfarthing-dist/pf/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  282. package/pennyfarthing-dist/pf/theme/__pycache__/cli.cpython-314.pyc +0 -0
  283. package/pennyfarthing-dist/pf/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  284. package/pennyfarthing-dist/pf/validate/__pycache__/cli.cpython-314.pyc +0 -0
  285. package/pennyfarthing-dist/pf/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  286. package/pennyfarthing-dist/pf/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  287. package/pennyfarthing-dist/pf/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  288. package/pennyfarthing-dist/pf/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  289. package/pennyfarthing-dist/pf/workflow/__pycache__/state.cpython-314.pyc +0 -0
  290. package/pennyfarthing-dist/scripts/lib/find-root.sh +1 -1
  291. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -1
  292. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +13 -13
  293. package/pennyfarthing-dist/scripts/workflow/check.py +4 -6
  294. package/pennyfarthing-dist/scripts/workflow/complete-step.py +2 -2
  295. package/pennyfarthing-dist/skills/skill-registry.yaml +19 -0
  296. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +15 -2
  297. package/packages/core/dist/workflow/__test_context_watch__/.session/.tandem-turn-counter +0 -1
  298. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-session.md +0 -3
  299. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-tandem-architect.md +0 -6
  300. package/packages/core/dist/workflow/__test_file_watch__/.session/95-4-tandem-architect.md +0 -6
  301. package/packages/core/dist/workflow/__test_file_watch__/workdir/trigger.ts +0 -1
  302. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-architect.md +0 -6
  303. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-toolcalls.jsonl +0 -1
  304. package/pennyfarthing-dist/pf/bikerack/changed_panel.py +0 -201
@@ -0,0 +1,80 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ export interface FileComplexity {
4
+ path: string;
5
+ total_lines: number;
6
+ longest_function: number;
7
+ avg_cyclomatic_complexity: number;
8
+ max_nesting_depth: number;
9
+ function_count: number;
10
+ }
11
+
12
+ export interface ComplexityData {
13
+ success: boolean;
14
+ target_path: string;
15
+ file_count: number;
16
+ files: FileComplexity[];
17
+ error?: string | null;
18
+ }
19
+
20
+ export interface UseComplexityOptions {
21
+ path?: string;
22
+ top?: number;
23
+ }
24
+
25
+ export interface UseComplexityReturn {
26
+ data: ComplexityData | null;
27
+ isLoading: boolean;
28
+ error: Error | null;
29
+ refresh: () => void;
30
+ }
31
+
32
+ export function useComplexity(options: UseComplexityOptions): UseComplexityReturn {
33
+ const [data, setData] = useState<ComplexityData | null>(null);
34
+ const [isLoading, setIsLoading] = useState(false);
35
+ const [error, setError] = useState<Error | null>(null);
36
+ const abortRef = useRef<AbortController | null>(null);
37
+
38
+ const fetchComplexity = useCallback(() => {
39
+ if (abortRef.current) {
40
+ abortRef.current.abort();
41
+ }
42
+
43
+ const controller = new AbortController();
44
+ abortRef.current = controller;
45
+
46
+ setIsLoading(true);
47
+ setError(null);
48
+
49
+ const params = new URLSearchParams();
50
+ if (options.path) params.set('path', options.path);
51
+ if (options.top) params.set('top', String(options.top));
52
+
53
+ fetch(`/api/complexity?${params}`, { signal: controller.signal })
54
+ .then((res) => {
55
+ if (!res.ok) {
56
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
57
+ }
58
+ return res.json();
59
+ })
60
+ .then((json: ComplexityData) => {
61
+ setData(json);
62
+ setIsLoading(false);
63
+ })
64
+ .catch((err) => {
65
+ if (err.name === 'AbortError') return;
66
+ setError(err instanceof Error ? err : new Error(String(err)));
67
+ setIsLoading(false);
68
+ });
69
+ }, [options.path, options.top]);
70
+
71
+ useEffect(() => {
72
+ return () => {
73
+ if (abortRef.current) {
74
+ abortRef.current.abort();
75
+ }
76
+ };
77
+ }, []);
78
+
79
+ return { data, isLoading, error, refresh: fetchComplexity };
80
+ }
@@ -0,0 +1,99 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ export interface StaleFile {
4
+ path: string;
5
+ last_commit_date: string;
6
+ days_since_last_commit: number;
7
+ size_bytes: number;
8
+ }
9
+
10
+ export interface UnusedExport {
11
+ symbol: string;
12
+ file: string;
13
+ line: number;
14
+ export_type: string;
15
+ }
16
+
17
+ export interface DeadCodeData {
18
+ success: boolean;
19
+ repo_name?: string;
20
+ repo_path?: string;
21
+ time_window_days?: number;
22
+ stale_files?: StaleFile[];
23
+ unused_exports?: UnusedExport[];
24
+ stale_file_count?: number;
25
+ unused_export_count?: number;
26
+ total_files?: number;
27
+ total_exports_scanned?: number;
28
+ repo_results?: DeadCodeData[];
29
+ error?: string | null;
30
+ }
31
+
32
+ export interface UseDeadCodeOptions {
33
+ days: number;
34
+ repo?: string;
35
+ layer?: 'stale' | 'exports' | 'all';
36
+ }
37
+
38
+ export interface UseDeadCodeReturn {
39
+ data: DeadCodeData | null;
40
+ isLoading: boolean;
41
+ error: Error | null;
42
+ refresh: () => void;
43
+ }
44
+
45
+ export function useDeadCode(options: UseDeadCodeOptions): UseDeadCodeReturn {
46
+ const [data, setData] = useState<DeadCodeData | null>(null);
47
+ const [isLoading, setIsLoading] = useState(false);
48
+ const [error, setError] = useState<Error | null>(null);
49
+ const abortRef = useRef<AbortController | null>(null);
50
+
51
+ const fetchDeadCode = useCallback(() => {
52
+ // Cancel any in-flight request
53
+ if (abortRef.current) {
54
+ abortRef.current.abort();
55
+ }
56
+
57
+ const controller = new AbortController();
58
+ abortRef.current = controller;
59
+
60
+ setIsLoading(true);
61
+ setError(null);
62
+
63
+ const params = new URLSearchParams({
64
+ days: String(options.days),
65
+ layer: options.layer || 'all',
66
+ });
67
+ if (options.repo) {
68
+ params.set('repo', options.repo);
69
+ }
70
+
71
+ fetch(`/api/dead-code?${params}`, { signal: controller.signal })
72
+ .then((res) => {
73
+ if (!res.ok) {
74
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
75
+ }
76
+ return res.json();
77
+ })
78
+ .then((json: DeadCodeData) => {
79
+ setData(json);
80
+ setIsLoading(false);
81
+ })
82
+ .catch((err) => {
83
+ if (err.name === 'AbortError') return;
84
+ setError(err instanceof Error ? err : new Error(String(err)));
85
+ setIsLoading(false);
86
+ });
87
+ }, [options.days, options.repo, options.layer]);
88
+
89
+ // Cleanup abort controller on unmount
90
+ useEffect(() => {
91
+ return () => {
92
+ if (abortRef.current) {
93
+ abortRef.current.abort();
94
+ }
95
+ };
96
+ }, []);
97
+
98
+ return { data, isLoading, error, refresh: fetchDeadCode };
99
+ }
@@ -0,0 +1,82 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ export interface OutdatedPackage {
4
+ name: string;
5
+ current: string;
6
+ wanted: string;
7
+ latest: string;
8
+ type: string;
9
+ }
10
+
11
+ export interface SecurityAdvisory {
12
+ severity: string;
13
+ count: number;
14
+ }
15
+
16
+ export interface DependenciesData {
17
+ success: boolean;
18
+ target_path: string;
19
+ outdated: OutdatedPackage[];
20
+ advisories: SecurityAdvisory[];
21
+ error?: string | null;
22
+ }
23
+
24
+ export interface UseDependenciesOptions {
25
+ path?: string;
26
+ }
27
+
28
+ export interface UseDependenciesReturn {
29
+ data: DependenciesData | null;
30
+ isLoading: boolean;
31
+ error: Error | null;
32
+ refresh: () => void;
33
+ }
34
+
35
+ export function useDependencies(options: UseDependenciesOptions): UseDependenciesReturn {
36
+ const [data, setData] = useState<DependenciesData | null>(null);
37
+ const [isLoading, setIsLoading] = useState(false);
38
+ const [error, setError] = useState<Error | null>(null);
39
+ const abortRef = useRef<AbortController | null>(null);
40
+
41
+ const fetchDependencies = useCallback(() => {
42
+ if (abortRef.current) {
43
+ abortRef.current.abort();
44
+ }
45
+
46
+ const controller = new AbortController();
47
+ abortRef.current = controller;
48
+
49
+ setIsLoading(true);
50
+ setError(null);
51
+
52
+ const params = new URLSearchParams();
53
+ if (options.path) params.set('path', options.path);
54
+
55
+ fetch(`/api/dependencies?${params}`, { signal: controller.signal })
56
+ .then((res) => {
57
+ if (!res.ok) {
58
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
59
+ }
60
+ return res.json();
61
+ })
62
+ .then((json: DependenciesData) => {
63
+ setData(json);
64
+ setIsLoading(false);
65
+ })
66
+ .catch((err) => {
67
+ if (err.name === 'AbortError') return;
68
+ setError(err instanceof Error ? err : new Error(String(err)));
69
+ setIsLoading(false);
70
+ });
71
+ }, [options.path]);
72
+
73
+ useEffect(() => {
74
+ return () => {
75
+ if (abortRef.current) {
76
+ abortRef.current.abort();
77
+ }
78
+ };
79
+ }, []);
80
+
81
+ return { data, isLoading, error, refresh: fetchDependencies };
82
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * useDiffs Hook
3
+ *
4
+ * React hook for subscribing to diff data via WebSocket.
5
+ * Story MSSCI-12717 - React Migration
6
+ * Story MSSCI-14238 - Git-based diffs (replaces OTEL-based extraction)
7
+ *
8
+ * IPC DEPRECATED - Now uses WebSocket /ws/diffs
9
+ */
10
+
11
+ import { useState, useEffect, useCallback, useRef } from 'react';
12
+
13
+ export interface DiffData {
14
+ id?: string;
15
+ path: string;
16
+ /** @deprecated Use `diff` field for raw git diff content */
17
+ original: string;
18
+ /** @deprecated Use `diff` field for raw git diff content */
19
+ modified: string;
20
+ /** Raw git diff output (MSSCI-14238) */
21
+ diff?: string;
22
+ toolName: string;
23
+ timestamp: number;
24
+ /** File status from git (MSSCI-14238) */
25
+ status?: 'modified' | 'added' | 'deleted' | 'renamed';
26
+ /** Line additions count (MSSCI-14238) */
27
+ additions?: number;
28
+ /** Line deletions count (MSSCI-14238) */
29
+ deletions?: number;
30
+ }
31
+
32
+ interface UseDiffsResult {
33
+ diffs: DiffData[];
34
+ selectedDiff: DiffData | null;
35
+ selectDiff: (path: string) => void;
36
+ clearDiffs: () => void;
37
+ }
38
+
39
+ const WS_RECONNECT_DELAY = 2000;
40
+
41
+ export function useDiffs(): UseDiffsResult {
42
+ const [diffs, setDiffs] = useState<DiffData[]>([]);
43
+ const [selectedDiff, setSelectedDiff] = useState<DiffData | null>(null);
44
+ const wsRef = useRef<WebSocket | null>(null);
45
+
46
+ useEffect(() => {
47
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
48
+ let mounted = true;
49
+
50
+ const connect = () => {
51
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
52
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/diffs`);
53
+ wsRef.current = ws;
54
+
55
+ ws.onopen = () => {
56
+ console.log('[useDiffs] WebSocket connected');
57
+ };
58
+
59
+ ws.onmessage = (event) => {
60
+ try {
61
+ const data = JSON.parse(event.data);
62
+
63
+ if (data.type === 'init') {
64
+ // Initial load of existing diffs
65
+ const initialDiffs = (data.diffs || []) as DiffData[];
66
+ setDiffs(initialDiffs);
67
+ if (initialDiffs.length > 0) {
68
+ setSelectedDiff(initialDiffs[initialDiffs.length - 1]);
69
+ }
70
+ } else if (data.type === 'diff') {
71
+ // New diff arrived
72
+ const diffData = data.diff as DiffData;
73
+ setDiffs(prev => {
74
+ // Update or add diff
75
+ const existing = prev.findIndex(d => d.path === diffData.path);
76
+ if (existing >= 0) {
77
+ const updated = [...prev];
78
+ updated[existing] = diffData;
79
+ return updated;
80
+ }
81
+ return [...prev, diffData];
82
+ });
83
+ // Auto-select new diff
84
+ setSelectedDiff(diffData);
85
+ } else if (data.type === 'refresh') {
86
+ // Full refresh of diffs (MSSCI-14238: git cache refresh)
87
+ const refreshedDiffs = (data.diffs || []) as DiffData[];
88
+ setDiffs(refreshedDiffs);
89
+ // Keep current selection if it still exists
90
+ if (refreshedDiffs.length > 0) {
91
+ setSelectedDiff(prev => {
92
+ if (prev) {
93
+ const stillExists = refreshedDiffs.find(d => d.path === prev.path);
94
+ if (stillExists) return stillExists;
95
+ }
96
+ return refreshedDiffs[refreshedDiffs.length - 1];
97
+ });
98
+ } else {
99
+ setSelectedDiff(null);
100
+ }
101
+ }
102
+ } catch (err) {
103
+ console.error('[useDiffs] Failed to parse message:', err);
104
+ }
105
+ };
106
+
107
+ ws.onclose = () => {
108
+ if (mounted) {
109
+ reconnectTimer = setTimeout(connect, WS_RECONNECT_DELAY);
110
+ }
111
+ };
112
+
113
+ ws.onerror = (err) => {
114
+ console.error('[useDiffs] WebSocket error:', err);
115
+ ws.close();
116
+ };
117
+ };
118
+
119
+ connect();
120
+
121
+ return () => {
122
+ mounted = false;
123
+ if (reconnectTimer) clearTimeout(reconnectTimer);
124
+ wsRef.current?.close();
125
+ };
126
+ }, []);
127
+
128
+ const selectDiff = useCallback((path: string) => {
129
+ const diff = diffs.find(d => d.path === path);
130
+ setSelectedDiff(diff || null);
131
+ }, [diffs]);
132
+
133
+ const clearDiffs = useCallback(() => {
134
+ setDiffs([]);
135
+ setSelectedDiff(null);
136
+ // Send clear message to server
137
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
138
+ wsRef.current.send(JSON.stringify({ type: 'clear' }));
139
+ }
140
+ }, []);
141
+
142
+ return { diffs, selectedDiff, selectDiff, clearDiffs };
143
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * useFileBrowser Hook
3
+ *
4
+ * Fetches directory listings from /api/files for the full file tree.
5
+ * Lazy-loads subdirectories on demand.
6
+ */
7
+
8
+ import { useState, useCallback } from 'react';
9
+
10
+ export interface DirectoryEntry {
11
+ name: string;
12
+ path: string;
13
+ type: 'file' | 'directory';
14
+ isModified?: boolean;
15
+ size?: number;
16
+ }
17
+
18
+ export interface DirectoryListing {
19
+ path: string;
20
+ entries: DirectoryEntry[];
21
+ }
22
+
23
+ interface DirectoryCache {
24
+ [path: string]: DirectoryEntry[];
25
+ }
26
+
27
+ interface UseFileBrowserResult {
28
+ cache: DirectoryCache;
29
+ loading: Set<string>;
30
+ error: string | null;
31
+ fetchDirectory: (dirPath: string) => Promise<void>;
32
+ }
33
+
34
+ export function useFileBrowser(): UseFileBrowserResult {
35
+ const [cache, setCache] = useState<DirectoryCache>({});
36
+ const [loading, setLoading] = useState<Set<string>>(new Set());
37
+ const [error, setError] = useState<string | null>(null);
38
+
39
+ const fetchDirectory = useCallback(async (dirPath: string) => {
40
+ // Already cached or loading
41
+ if (cache[dirPath] || loading.has(dirPath)) return;
42
+
43
+ setLoading(prev => new Set(prev).add(dirPath));
44
+ setError(null);
45
+
46
+ try {
47
+ const params = dirPath ? `?path=${encodeURIComponent(dirPath)}` : '';
48
+ const res = await fetch(`/api/files${params}`);
49
+ if (!res.ok) throw new Error(`Failed to list directory: ${res.statusText}`);
50
+ const json = await res.json();
51
+ // API may return { entries: [...] } or a raw array
52
+ const entries: DirectoryEntry[] = Array.isArray(json) ? json : (json.entries ?? []);
53
+
54
+ // Sort: directories first, then files, alphabetical within each
55
+ const sorted = entries.sort((a, b) => {
56
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
57
+ return a.name.localeCompare(b.name);
58
+ });
59
+
60
+ setCache(prev => ({ ...prev, [dirPath || '__root__']: sorted }));
61
+ } catch (err) {
62
+ setError(err instanceof Error ? err.message : 'Failed to load directory');
63
+ } finally {
64
+ setLoading(prev => {
65
+ const next = new Set(prev);
66
+ next.delete(dirPath);
67
+ return next;
68
+ });
69
+ }
70
+ }, [cache, loading]);
71
+
72
+ return { cache, loading, error, fetchDirectory };
73
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * useFocusPanel Hook
3
+ *
4
+ * React hook for handling panel focus events via /ws/focus WebSocket.
5
+ * Story MSSCI-14977 - BikeShow client layout stash/restore on panel focus
6
+ * Epic 104: /bc CLI Panel Focus
7
+ *
8
+ * Listens to /ws/focus WebSocket for focus events.
9
+ *
10
+ * Multi-group layouts (Cyclist): maximizeGroup/exitMaximizedGroup
11
+ * Single-group layouts (BikeRack): activate target tab, stash previous
12
+ */
13
+
14
+ import { useState, useEffect, useRef } from 'react';
15
+ import type { DockviewApi } from 'dockview-react';
16
+
17
+ export interface UseFocusPanelResult {
18
+ /** Currently focused panel ID, or null if not in focus mode */
19
+ focusedPanel: string | null;
20
+ /** Whether the workspace is currently in single-panel focus mode */
21
+ isInFocusMode: boolean;
22
+ }
23
+
24
+ /** WebSocket message format from /ws/focus */
25
+ interface FocusMessage {
26
+ type: 'init' | 'update';
27
+ focus: string | null;
28
+ }
29
+
30
+ /**
31
+ * Hook for managing panel focus mode.
32
+ *
33
+ * @param api - Dockview API instance for layout manipulation, or null if not ready
34
+ * @returns Focus state including current focused panel and mode
35
+ */
36
+ export function useFocusPanel(api: DockviewApi | null): UseFocusPanelResult {
37
+ const [focusedPanel, setFocusedPanel] = useState<string | null>(null);
38
+ const [isInFocusMode, setIsInFocusMode] = useState(false);
39
+
40
+ const wsRef = useRef<WebSocket | null>(null);
41
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
42
+ const isMountedRef = useRef(true);
43
+
44
+ // Refs to access latest values inside WebSocket callbacks without re-creating the effect
45
+ const apiRef = useRef(api);
46
+ apiRef.current = api;
47
+
48
+ // Stash the previously active panel ID for single-group reset
49
+ const previousActivePanelRef = useRef<string | null>(null);
50
+
51
+ useEffect(() => {
52
+ isMountedRef.current = true;
53
+
54
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
55
+ const wsUrl = `${protocol}//${window.location.host}/ws/focus`;
56
+
57
+ const handleFocusChange = (focus: string | null) => {
58
+ const currentApi = apiRef.current;
59
+ if (!currentApi) return;
60
+
61
+ if (focus !== null) {
62
+ const panel = currentApi.getPanel(focus);
63
+ if (!panel) return;
64
+
65
+ const isMultiGroup = currentApi.groups.length > 1;
66
+
67
+ if (isMultiGroup) {
68
+ // Multi-group (Cyclist): maximize the target panel's group
69
+ currentApi.maximizeGroup(panel);
70
+ } else {
71
+ // Single-group (BikeRack): stash current active, switch tab
72
+ if (!previousActivePanelRef.current) {
73
+ previousActivePanelRef.current = currentApi.activePanel?.id ?? null;
74
+ }
75
+ panel.api.setActive();
76
+ }
77
+
78
+ setIsInFocusMode(true);
79
+ setFocusedPanel(focus);
80
+ } else {
81
+ // Reset
82
+ const isMaximized = currentApi.hasMaximizedGroup();
83
+
84
+ if (isMaximized) {
85
+ // Multi-group: exit maximize
86
+ currentApi.exitMaximizedGroup();
87
+ } else if (previousActivePanelRef.current) {
88
+ // Single-group: restore previous active tab
89
+ const prev = currentApi.getPanel(previousActivePanelRef.current);
90
+ if (prev) prev.api.setActive();
91
+ }
92
+
93
+ previousActivePanelRef.current = null;
94
+ setIsInFocusMode(false);
95
+ setFocusedPanel(null);
96
+ }
97
+ };
98
+
99
+ const connect = () => {
100
+ if (!isMountedRef.current) return;
101
+
102
+ wsRef.current = new WebSocket(wsUrl);
103
+
104
+ wsRef.current.onmessage = (event: MessageEvent) => {
105
+ try {
106
+ const msg = JSON.parse(event.data) as FocusMessage;
107
+ if (msg.type === 'update') {
108
+ // Live /bc commands — apply focus change immediately
109
+ handleFocusChange(msg.focus);
110
+ }
111
+ // 'init' messages are ignored — focus is ephemeral, not persistent.
112
+ // Stale focus values in config would destroy the layout on page load.
113
+ } catch {
114
+ // Ignore malformed messages
115
+ }
116
+ };
117
+
118
+ wsRef.current.onclose = () => {
119
+ reconnectTimeoutRef.current = setTimeout(connect, 2000);
120
+ };
121
+ };
122
+
123
+ connect();
124
+
125
+ return () => {
126
+ isMountedRef.current = false;
127
+ if (reconnectTimeoutRef.current) {
128
+ clearTimeout(reconnectTimeoutRef.current);
129
+ }
130
+ if (wsRef.current) {
131
+ wsRef.current.close();
132
+ }
133
+ };
134
+ }, []);
135
+
136
+ return { focusedPanel, isInFocusMode };
137
+ }