@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,636 @@
1
+ /**
2
+ * ControlBar Component
3
+ *
4
+ * Provides Stop, Reset, Bell Mode, Relay Mode, and Agent Quick Picker controls.
5
+ * Story MSSCI-12729 - Stop/Reset Controls and Escape Key
6
+ * Story MSSCI-12275 - Bell Mode toggle
7
+ * Story MSSCI-12395 - Relay Mode toggle
8
+ * Story MSSCI-14762 - Quick agent picker in control bar
9
+ *
10
+ * Features:
11
+ * - Stop button visible only when Claude is running
12
+ * - Reset button always visible
13
+ * - Agent quick picker (lightweight dropdown for rapid agent switching)
14
+ * - Bell mode toggle (inject queued messages via PostToolUse hook)
15
+ * - Relay mode toggle (auto-handoff to next agent)
16
+ * - Escape key handler for stopping (single press = interrupt, double = force kill)
17
+ * - Visual feedback for "Stopping..." state
18
+ */
19
+
20
+ import React, { useEffect, useRef, useCallback, useState, FocusEvent } from 'react';
21
+ import { BellRing, Zap, RotateCcw, UserCog } from 'lucide-react';
22
+ import { Button } from '@/components/ui/button';
23
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
24
+ import { useClaudeContext } from '../contexts/ClaudeContext';
25
+
26
+ // =============================================================================
27
+ // Focus Tracking Hook
28
+ // =============================================================================
29
+
30
+ function useFocusTracking() {
31
+ const [focusedId, setFocusedId] = useState<string | null>(null);
32
+
33
+ const handleFocus = useCallback((id: string) => (e: FocusEvent<HTMLButtonElement>) => {
34
+ setFocusedId(id);
35
+ }, []);
36
+
37
+ const handleBlur = useCallback(() => {
38
+ setFocusedId(null);
39
+ }, []);
40
+
41
+ const isFocused = useCallback((id: string) => focusedId === id, [focusedId]);
42
+
43
+ return { handleFocus, handleBlur, isFocused };
44
+ }
45
+
46
+ // =============================================================================
47
+ // Agent Quick Picker
48
+ // =============================================================================
49
+
50
+ interface ThemeAgent {
51
+ role: string;
52
+ character: string;
53
+ slug: string;
54
+ }
55
+
56
+ interface ThemeData {
57
+ agents: ThemeAgent[];
58
+ }
59
+
60
+ function AgentQuickPicker({ currentAgent, onAgentSwitch }: { currentAgent: string | null; onAgentSwitch?: (role: string) => void }): React.ReactElement {
61
+ const [isOpen, setIsOpen] = useState(false);
62
+ const [agents, setAgents] = useState<ThemeAgent[]>([]);
63
+ const pickerRef = useRef<HTMLDivElement>(null);
64
+
65
+ // Fetch agent list on mount
66
+ useEffect(() => {
67
+ fetch('/api/theme-agents/full')
68
+ .then(res => res.ok ? res.json() : null)
69
+ .then((data: ThemeData | null) => {
70
+ if (data?.agents) {
71
+ setAgents(data.agents);
72
+ }
73
+ })
74
+ .catch(() => {});
75
+ }, []);
76
+
77
+ // Close on outside click
78
+ useEffect(() => {
79
+ if (!isOpen) return;
80
+
81
+ const handleMouseDown = (e: MouseEvent) => {
82
+ if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) {
83
+ setIsOpen(false);
84
+ }
85
+ };
86
+
87
+ document.addEventListener('mousedown', handleMouseDown);
88
+ return () => document.removeEventListener('mousedown', handleMouseDown);
89
+ }, [isOpen]);
90
+
91
+ // Close on Escape
92
+ useEffect(() => {
93
+ if (!isOpen) return;
94
+
95
+ const handleKeyDown = (e: KeyboardEvent) => {
96
+ if (e.key === 'Escape') {
97
+ e.stopPropagation();
98
+ setIsOpen(false);
99
+ }
100
+ };
101
+
102
+ document.addEventListener('keydown', handleKeyDown, true);
103
+ return () => document.removeEventListener('keydown', handleKeyDown, true);
104
+ }, [isOpen]);
105
+
106
+ const handleAgentClick = useCallback((agent: ThemeAgent) => {
107
+ if (agent.role === currentAgent) return;
108
+ onAgentSwitch?.(agent.role);
109
+ setIsOpen(false);
110
+ }, [currentAgent, onAgentSwitch]);
111
+
112
+ return (
113
+ <div className="agent-quick-picker-wrapper" ref={pickerRef}>
114
+ <Tooltip>
115
+ <TooltipTrigger asChild>
116
+ <Button
117
+ variant="ghost"
118
+ size="icon"
119
+ type="button"
120
+ className={`btn-toggle agent-picker-toggle ${isOpen ? 'active' : ''}`}
121
+ data-testid="agent-quick-picker"
122
+ onClick={() => setIsOpen(prev => !prev)}
123
+ aria-label="Switch agent"
124
+ aria-expanded={isOpen}
125
+ aria-haspopup="listbox"
126
+ >
127
+ <UserCog className="h-4 w-4" />
128
+ </Button>
129
+ </TooltipTrigger>
130
+ <TooltipContent>Switch Agent</TooltipContent>
131
+ </Tooltip>
132
+
133
+ {isOpen && (
134
+ <div
135
+ className="agent-quick-picker-dropdown"
136
+ data-testid="agent-quick-picker-dropdown"
137
+ role="listbox"
138
+ aria-label="Available agents"
139
+ >
140
+ {agents.map(agent => {
141
+ const isCurrent = agent.role === currentAgent;
142
+ return (
143
+ <div
144
+ key={agent.role}
145
+ className={`agent-quick-picker-option ${isCurrent ? 'current' : ''}`}
146
+ data-testid={`agent-option-${agent.role}`}
147
+ role="option"
148
+ aria-selected={isCurrent}
149
+ aria-label={`${agent.role} (${agent.character})`}
150
+ title={agent.character}
151
+ onClick={() => handleAgentClick(agent)}
152
+ >
153
+ <span className="agent-option-role">{agent.role}</span>
154
+ </div>
155
+ );
156
+ })}
157
+ </div>
158
+ )}
159
+ </div>
160
+ );
161
+ }
162
+
163
+ // =============================================================================
164
+ // Types
165
+ // =============================================================================
166
+
167
+ export interface ControlBarProps {
168
+ /** Whether Claude is currently running */
169
+ isRunning: boolean;
170
+ /** Whether stop is in progress */
171
+ isStopping?: boolean;
172
+ /** Called when stop button clicked or Escape pressed */
173
+ onStop: () => void;
174
+ /** Called on double Escape for force kill */
175
+ onForceStop?: () => void;
176
+ /** Called when reset button clicked */
177
+ onReset: () => void;
178
+ /** Bell mode state (inject queued messages via hook) */
179
+ bellMode?: boolean;
180
+ /** Relay mode state (auto-handoff to next agent) */
181
+ relayMode?: boolean;
182
+ /** Called when bell mode toggle clicked */
183
+ onBellModeChange?: (enabled: boolean) => void;
184
+ /** Called when relay mode toggle clicked */
185
+ onRelayModeChange?: (enabled: boolean) => void;
186
+ /** Context percentage for TirePump visibility */
187
+ contextPercent?: number;
188
+ /** Current agent slug for TirePump reload */
189
+ currentAgent?: string | null;
190
+ /** Called when TirePump button clicked */
191
+ onTirePump?: () => void;
192
+ /** Called when agent is selected from quick picker */
193
+ onAgentSwitch?: (role: string) => void;
194
+ }
195
+
196
+ // =============================================================================
197
+ // ControlBar Component
198
+ // =============================================================================
199
+
200
+ export function ControlBar({
201
+ isRunning,
202
+ isStopping = false,
203
+ onStop,
204
+ onForceStop,
205
+ onReset,
206
+ bellMode = false,
207
+ relayMode = false,
208
+ onBellModeChange,
209
+ onRelayModeChange,
210
+ contextPercent = 0,
211
+ currentAgent = null,
212
+ onTirePump,
213
+ onAgentSwitch,
214
+ }: ControlBarProps): React.ReactElement {
215
+ const lastEscapeTime = useRef<number>(0);
216
+ const DOUBLE_PRESS_THRESHOLD = 500; // ms
217
+ const { handleFocus, handleBlur, isFocused } = useFocusTracking();
218
+
219
+ // Global Escape key handler
220
+ const handleKeyDown = useCallback(
221
+ (event: KeyboardEvent) => {
222
+ // Only handle Escape
223
+ if (event.key !== 'Escape' && event.keyCode !== 27) {
224
+ return;
225
+ }
226
+
227
+ // Don't handle if not running
228
+ if (!isRunning) {
229
+ return;
230
+ }
231
+
232
+ // Check for double press
233
+ const now = Date.now();
234
+ const timeSinceLastEscape = now - lastEscapeTime.current;
235
+
236
+ if (timeSinceLastEscape < DOUBLE_PRESS_THRESHOLD && onForceStop) {
237
+ // Double Escape - force kill
238
+ onForceStop();
239
+ } else {
240
+ // Single Escape - normal stop
241
+ onStop();
242
+ }
243
+
244
+ lastEscapeTime.current = now;
245
+ },
246
+ [isRunning, onStop, onForceStop]
247
+ );
248
+
249
+ // Register global keydown listener
250
+ useEffect(() => {
251
+ document.addEventListener('keydown', handleKeyDown);
252
+ return () => {
253
+ document.removeEventListener('keydown', handleKeyDown);
254
+ };
255
+ }, [handleKeyDown]);
256
+
257
+ return (
258
+ <TooltipProvider delayDuration={300}>
259
+ <div className="control-bar" data-testid="control-bar">
260
+ {/* Mode toggles - Agent Picker, Bell, and Relay */}
261
+ <div className="control-bar-toggles">
262
+ {/* Agent Quick Picker */}
263
+ <AgentQuickPicker currentAgent={currentAgent} onAgentSwitch={onAgentSwitch} />
264
+
265
+ {/* Bell Mode Toggle */}
266
+ <Tooltip>
267
+ <TooltipTrigger asChild>
268
+ <Button
269
+ variant="ghost"
270
+ size="icon"
271
+ type="button"
272
+ className={`btn-toggle bell-toggle ${bellMode ? 'active' : ''}`}
273
+ data-testid="bell-mode-toggle"
274
+ onClick={() => onBellModeChange?.(!bellMode)}
275
+ aria-pressed={bellMode}
276
+ aria-label="Bell mode - inject queued messages via hook"
277
+ >
278
+ <BellRing className="h-4 w-4" />
279
+ </Button>
280
+ </TooltipTrigger>
281
+ <TooltipContent>Bell Mode: Inject queued messages during tool use (Cmd+B)</TooltipContent>
282
+ </Tooltip>
283
+
284
+ {/* Relay Mode Toggle */}
285
+ <Tooltip>
286
+ <TooltipTrigger asChild>
287
+ <Button
288
+ variant="ghost"
289
+ size="icon"
290
+ type="button"
291
+ className={`btn-toggle relay-toggle ${relayMode ? 'active' : ''}`}
292
+ data-testid="relay-toggle"
293
+ onClick={() => onRelayModeChange?.(!relayMode)}
294
+ aria-pressed={relayMode}
295
+ aria-label="Relay mode - auto-handoff to next agent"
296
+ >
297
+ <Zap className="h-4 w-4" />
298
+ </Button>
299
+ </TooltipTrigger>
300
+ <TooltipContent>Relay Mode: Auto-handoff to next agent (Cmd+4)</TooltipContent>
301
+ </Tooltip>
302
+
303
+ {/* TirePump Button - always visible, warning style at 70%+ */}
304
+ <Tooltip>
305
+ <TooltipTrigger asChild>
306
+ <Button
307
+ variant="ghost"
308
+ size="icon"
309
+ type="button"
310
+ className={`btn-toggle pump-toggle ${contextPercent >= 70 ? 'warning' : ''}`}
311
+ data-testid="pump-toggle"
312
+ onClick={onTirePump}
313
+ disabled={!currentAgent}
314
+ aria-label="TirePump: Clear context and reload agent"
315
+ >
316
+ <RotateCcw className="h-4 w-4" />
317
+ </Button>
318
+ </TooltipTrigger>
319
+ <TooltipContent>{currentAgent ? `TirePump: Clear context (${contextPercent}%) and reload ${currentAgent}` : 'TirePump: No agent loaded'}</TooltipContent>
320
+ </Tooltip>
321
+ </div>
322
+
323
+ {/* Stop button - always visible, disabled when not running */}
324
+ <Button
325
+ variant="destructive"
326
+ type="button"
327
+ className={`btn-stop danger ${isStopping ? 'stopping' : ''} ${isRunning && !isStopping ? 'throbbing' : ''} ${isFocused('stop') ? 'focused focus-visible' : ''}`}
328
+ data-testid="stop-button"
329
+ onClick={onStop}
330
+ disabled={!isRunning || isStopping}
331
+ aria-busy={isStopping}
332
+ aria-label="Stop Claude"
333
+ onFocus={handleFocus('stop')}
334
+ onBlur={handleBlur}
335
+ >
336
+ <span data-icon="stop" className="icon" />
337
+ {isStopping ? (
338
+ <>
339
+ <span className="spinner" data-loading />
340
+ Stopping...
341
+ </>
342
+ ) : (
343
+ 'Stop'
344
+ )}
345
+ </Button>
346
+
347
+ {/* Reset button - always visible */}
348
+ <Button
349
+ variant="outline"
350
+ type="button"
351
+ className={`btn-reset ${isFocused('reset') ? 'focused focus-visible' : ''}`}
352
+ data-testid="reset-button"
353
+ onClick={onReset}
354
+ aria-label="Reset session"
355
+ onFocus={handleFocus('reset')}
356
+ onBlur={handleBlur}
357
+ >
358
+ Reset
359
+ </Button>
360
+ </div>
361
+ </TooltipProvider>
362
+ );
363
+ }
364
+
365
+ // =============================================================================
366
+ // useControlBar Hook
367
+ // =============================================================================
368
+
369
+ interface UseControlBarResult {
370
+ /** Whether Claude is currently running */
371
+ isRunning: boolean;
372
+ /** Whether stop is in progress */
373
+ isStopping: boolean;
374
+ /** Bell mode state */
375
+ bellMode: boolean;
376
+ /** Relay mode state */
377
+ relayMode: boolean;
378
+ /** Context percentage for TirePump */
379
+ contextPercent: number;
380
+ /** Current agent slug for TirePump */
381
+ currentAgent: string | null;
382
+ /** Handle stop action */
383
+ handleStop: () => void;
384
+ /** Handle force stop action (SIGKILL) */
385
+ handleForceStop: () => void;
386
+ /** Handle reset action */
387
+ handleReset: () => void;
388
+ /** Handle bell mode toggle */
389
+ handleBellModeChange: (enabled: boolean) => void;
390
+ /** Handle relay mode toggle */
391
+ handleRelayModeChange: (enabled: boolean) => void;
392
+ /** Handle TirePump action */
393
+ handleTirePump: () => void;
394
+ /** Handle agent switch from quick picker */
395
+ handleAgentSwitch: (role: string) => void;
396
+ }
397
+
398
+ export function useControlBar(): UseControlBarResult {
399
+ const [isRunning, setIsRunning] = useState(false);
400
+ const [isStopping, setIsStopping] = useState(false);
401
+ const [bellMode, setBellMode] = useState(false);
402
+ const [relayMode, setRelayMode] = useState(false);
403
+ const [contextPercent, setContextPercent] = useState(0);
404
+ const [currentAgent, setCurrentAgent] = useState<string | null>(null);
405
+
406
+ // Claude context for WebSocket communication
407
+ const { abort, clear, clearAndReload, send, onMessage, onComplete, onError, isConnected } = useClaudeContext();
408
+
409
+ // Load initial settings and listen for changes (using REST/WebSocket, not IPC)
410
+ useEffect(() => {
411
+ // Handle settings update from any source
412
+ function handleSettingsUpdate(settings: Record<string, unknown>) {
413
+ const workflow = settings?.workflow as Record<string, unknown> | undefined;
414
+ const newBellMode = !!workflow?.bell_mode;
415
+ const newRelayMode = !!workflow?.relay_mode;
416
+ console.log('[ControlBar] Settings updated:', { bellMode: newBellMode, relayMode: newRelayMode });
417
+ setBellMode(newBellMode);
418
+ setRelayMode(newRelayMode);
419
+ }
420
+
421
+ // Load initial settings via REST
422
+ async function loadSettings() {
423
+ try {
424
+ console.log('[ControlBar] Loading settings via REST');
425
+ const response = await fetch('/api/settings');
426
+ if (response.ok) {
427
+ const settings = await response.json();
428
+ handleSettingsUpdate(settings);
429
+ }
430
+ } catch (err) {
431
+ console.error('[ControlBar] Failed to load settings:', err);
432
+ }
433
+ }
434
+
435
+ console.log('[ControlBar] useEffect mount - loading settings');
436
+ loadSettings();
437
+
438
+ // WebSocket subscription for real-time sync
439
+ console.log('[ControlBar] Connecting to /ws/settings for real-time sync');
440
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
441
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/settings`);
442
+
443
+ ws.onmessage = (event) => {
444
+ try {
445
+ const data = JSON.parse(event.data);
446
+ if (data.type === 'init' || data.type === 'update') {
447
+ handleSettingsUpdate(data.settings);
448
+ }
449
+ } catch (err) {
450
+ console.error('[ControlBar] Failed to parse WebSocket message:', err);
451
+ }
452
+ };
453
+
454
+ ws.onerror = (err) => {
455
+ console.error('[ControlBar] WebSocket error:', err);
456
+ };
457
+
458
+ return () => {
459
+ ws.close();
460
+ };
461
+ }, []);
462
+
463
+ // Subscribe to context WebSocket for TirePump visibility
464
+ useEffect(() => {
465
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
466
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/context`);
467
+
468
+ ws.onmessage = (event) => {
469
+ try {
470
+ const data = JSON.parse(event.data);
471
+ if (data.type === 'init' || data.type === 'update') {
472
+ const percent = data.context?.percent ?? 0;
473
+ setContextPercent(percent);
474
+ }
475
+ } catch (err) {
476
+ console.error('[ControlBar] Failed to parse context message:', err);
477
+ }
478
+ };
479
+
480
+ return () => {
481
+ ws.close();
482
+ };
483
+ }, []);
484
+
485
+ // Subscribe to persona WebSocket for current agent
486
+ useEffect(() => {
487
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
488
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/persona`);
489
+
490
+ ws.onmessage = (event) => {
491
+ try {
492
+ const persona = JSON.parse(event.data);
493
+ // slug is the agent role (dev, sm, tea, reviewer, etc.)
494
+ setCurrentAgent(persona?.slug ?? null);
495
+ } catch (err) {
496
+ console.error('[ControlBar] Failed to parse persona message:', err);
497
+ }
498
+ };
499
+
500
+ return () => {
501
+ ws.close();
502
+ };
503
+ }, []);
504
+
505
+ // Listen for Claude running state changes via WebSocket context
506
+ useEffect(() => {
507
+ if (!isConnected) return;
508
+
509
+ // Track when Claude starts/completes
510
+ const handleMessage = () => {
511
+ setIsRunning(true);
512
+ setIsStopping(false);
513
+ };
514
+
515
+ const handleComplete = () => {
516
+ setIsRunning(false);
517
+ setIsStopping(false);
518
+ };
519
+
520
+ const handleError = () => {
521
+ setIsRunning(false);
522
+ setIsStopping(false);
523
+ };
524
+
525
+ // Subscribe to events via context
526
+ const cleanupMessage = onMessage(handleMessage);
527
+ const cleanupComplete = onComplete(handleComplete);
528
+ const cleanupError = onError(handleError);
529
+
530
+ return () => {
531
+ cleanupMessage();
532
+ cleanupComplete();
533
+ cleanupError();
534
+ };
535
+ }, [isConnected, onMessage, onComplete, onError]);
536
+
537
+ const handleStop = useCallback(() => {
538
+ setIsStopping(true);
539
+ try {
540
+ // Use abort() via WebSocket
541
+ abort();
542
+ } catch (err) {
543
+ console.error('[ControlBar] Stop failed:', err);
544
+ }
545
+ }, [abort]);
546
+
547
+ const handleForceStop = useCallback(() => {
548
+ setIsStopping(true);
549
+ try {
550
+ abort();
551
+ } catch (err) {
552
+ console.error('[ControlBar] Force stop failed:', err);
553
+ }
554
+ }, [abort]);
555
+
556
+ const handleReset = useCallback(() => {
557
+ try {
558
+ clear();
559
+ setIsRunning(false);
560
+ setIsStopping(false);
561
+ } catch (err) {
562
+ console.error('[ControlBar] Reset failed:', err);
563
+ }
564
+ }, [clear]);
565
+
566
+ const handleBellModeChange = useCallback(async (enabled: boolean) => {
567
+ try {
568
+ // Use REST API for settings
569
+ await fetch('/api/settings', {
570
+ method: 'PATCH',
571
+ headers: { 'Content-Type': 'application/json' },
572
+ body: JSON.stringify({ workflow: { bell_mode: enabled } }),
573
+ });
574
+ setBellMode(enabled);
575
+ console.log('[ControlBar] Bell mode set to:', enabled);
576
+ } catch (err) {
577
+ console.error('[ControlBar] Failed to toggle bell mode:', err);
578
+ }
579
+ }, []);
580
+
581
+ const handleRelayModeChange = useCallback(async (enabled: boolean) => {
582
+ try {
583
+ // Use REST API for settings
584
+ await fetch('/api/settings', {
585
+ method: 'PATCH',
586
+ headers: { 'Content-Type': 'application/json' },
587
+ body: JSON.stringify({ workflow: { relay_mode: enabled } }),
588
+ });
589
+ setRelayMode(enabled);
590
+ console.log('[ControlBar] Relay mode set to:', enabled);
591
+ } catch (err) {
592
+ console.error('[ControlBar] Failed to toggle relay mode:', err);
593
+ }
594
+ }, []);
595
+
596
+ // TirePump: Clear context and reload current agent
597
+ const handleTirePump = useCallback(() => {
598
+ if (!currentAgent) {
599
+ console.warn('[ControlBar] Cannot TirePump: no current agent');
600
+ return;
601
+ }
602
+ try {
603
+ console.log('[ControlBar] TirePump: clearing context and reloading agent:', currentAgent);
604
+ clearAndReload(currentAgent);
605
+ // Reset local state since session is being cleared
606
+ setIsRunning(false);
607
+ setIsStopping(false);
608
+ setContextPercent(0);
609
+ } catch (err) {
610
+ console.error('[ControlBar] TirePump failed:', err);
611
+ }
612
+ }, [currentAgent, clearAndReload]);
613
+
614
+ // Agent quick picker: send /{role} command
615
+ const handleAgentSwitch = useCallback((role: string) => {
616
+ send(`/${role}`);
617
+ }, [send]);
618
+
619
+ return {
620
+ isRunning,
621
+ isStopping,
622
+ bellMode,
623
+ relayMode,
624
+ contextPercent,
625
+ currentAgent,
626
+ handleStop,
627
+ handleForceStop,
628
+ handleReset,
629
+ handleBellModeChange,
630
+ handleRelayModeChange,
631
+ handleTirePump,
632
+ handleAgentSwitch,
633
+ };
634
+ }
635
+
636
+ export default ControlBar;