@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,233 @@
1
+ /**
2
+ * useGitStatus Hook
3
+ *
4
+ * React hook for subscribing to git status data.
5
+ * Story MSSCI-12717 - React Migration
6
+ * Story MSSCI-12781 - Fixed to handle multi-repo response format
7
+ * Story MSSCI-12798 - Expose full repo array for stacked display
8
+ * Story MSSCI-12860 - IPC to WebSocket Migration (Phase 1)
9
+ *
10
+ * Uses WebSocket /ws/git for real-time updates (no polling).
11
+ */
12
+
13
+ import { useState, useEffect, useRef } from 'react';
14
+
15
+ export interface GitStatusData {
16
+ branch: string;
17
+ ahead?: number;
18
+ behind?: number;
19
+ staged?: number;
20
+ modified?: number;
21
+ untracked?: number;
22
+ isDirty?: boolean;
23
+ }
24
+
25
+ /** Per-repo status data for stacked display */
26
+ export interface RepoStatusData {
27
+ name: string;
28
+ path: string;
29
+ branch: string;
30
+ ahead?: number;
31
+ behind?: number;
32
+ /** Commits origin/develop has that this branch doesn't (needs pull/rebase) */
33
+ developBehind?: number;
34
+ staged: number;
35
+ modified: number;
36
+ untracked: number;
37
+ isDirty: boolean;
38
+ files: DirtyFile[];
39
+ }
40
+
41
+ /** Dirty file from git status --porcelain */
42
+ export interface DirtyFile {
43
+ status: string; // M, A, D, ?, etc.
44
+ path: string;
45
+ }
46
+
47
+ /** Raw repo git info from server */
48
+ interface RepoGitInfo {
49
+ name: string;
50
+ path: string;
51
+ branch: string;
52
+ clean: boolean;
53
+ ahead: number | null;
54
+ behind: number | null;
55
+ developBehind: number | null;
56
+ dirtyFiles: DirtyFile[];
57
+ }
58
+
59
+ /** WebSocket message format from /ws/git */
60
+ interface GitMessage {
61
+ type: 'init' | 'update';
62
+ repos: RepoGitInfo[];
63
+ }
64
+
65
+ /**
66
+ * Count files by type from dirty files array
67
+ */
68
+ function countFilesByType(dirtyFiles: DirtyFile[]): { staged: number; modified: number; untracked: number } {
69
+ let staged = 0;
70
+ let modified = 0;
71
+ let untracked = 0;
72
+
73
+ for (const file of dirtyFiles) {
74
+ // Git status codes:
75
+ // First char = staging area, Second char = working tree
76
+ // M = modified, A = added (staged), D = deleted, ? = untracked
77
+ const indexStatus = file.status[0] || ' ';
78
+ const workTreeStatus = file.status[1] || ' ';
79
+
80
+ // Staged files (anything in the index that's not space or ?)
81
+ if (indexStatus !== ' ' && indexStatus !== '?') {
82
+ staged++;
83
+ }
84
+
85
+ // Modified in working tree (not staged)
86
+ if (workTreeStatus === 'M' || workTreeStatus === 'D') {
87
+ modified++;
88
+ }
89
+
90
+ // Untracked files
91
+ if (indexStatus === '?' && workTreeStatus === '?') {
92
+ untracked++;
93
+ }
94
+ }
95
+
96
+ return { staged, modified, untracked };
97
+ }
98
+
99
+ /**
100
+ * Transform raw git response to per-repo status array for stacked display
101
+ */
102
+ function transformToRepoArray(repos: RepoGitInfo[]): RepoStatusData[] {
103
+ if (!repos?.length) {
104
+ return [];
105
+ }
106
+
107
+ return repos.map(repo => {
108
+ const counts = countFilesByType(repo.dirtyFiles);
109
+ return {
110
+ name: repo.name,
111
+ path: repo.path,
112
+ branch: repo.branch,
113
+ ahead: repo.ahead ?? undefined,
114
+ behind: repo.behind ?? undefined,
115
+ developBehind: repo.developBehind ?? undefined,
116
+ staged: counts.staged,
117
+ modified: counts.modified,
118
+ untracked: counts.untracked,
119
+ isDirty: !repo.clean,
120
+ files: repo.dirtyFiles,
121
+ };
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Transform raw git response to GitStatusData for display (legacy aggregated view)
127
+ * Uses first repo for branch info, aggregates file counts across all repos
128
+ */
129
+ function transformGitResponse(repos: RepoGitInfo[]): GitStatusData | null {
130
+ if (!repos?.length) {
131
+ return null;
132
+ }
133
+
134
+ // Use first repo as primary (usually the orchestrator or main repo)
135
+ const primaryRepo = repos[0];
136
+
137
+ // Count file types across all repos
138
+ let staged = 0;
139
+ let modified = 0;
140
+ let untracked = 0;
141
+
142
+ for (const repo of repos) {
143
+ const counts = countFilesByType(repo.dirtyFiles);
144
+ staged += counts.staged;
145
+ modified += counts.modified;
146
+ untracked += counts.untracked;
147
+ }
148
+
149
+ // Check if any repo is dirty
150
+ const isDirty = repos.some(repo => !repo.clean);
151
+
152
+ return {
153
+ branch: primaryRepo.branch,
154
+ ahead: primaryRepo.ahead ?? undefined,
155
+ behind: primaryRepo.behind ?? undefined,
156
+ staged: staged > 0 ? staged : undefined,
157
+ modified: modified > 0 ? modified : undefined,
158
+ untracked: untracked > 0 ? untracked : undefined,
159
+ isDirty,
160
+ };
161
+ }
162
+
163
+ interface UseGitStatusResult {
164
+ gitStatus: GitStatusData | null;
165
+ repos: RepoStatusData[];
166
+ isLoading: boolean;
167
+ error: Error | null;
168
+ }
169
+
170
+ export function useGitStatus(): UseGitStatusResult {
171
+ const [gitStatus, setGitStatus] = useState<GitStatusData | null>(null);
172
+ const [repos, setRepos] = useState<RepoStatusData[]>([]);
173
+ const [isLoading, setIsLoading] = useState(true);
174
+ const [error, setError] = useState<Error | null>(null);
175
+ const wsRef = useRef<WebSocket | null>(null);
176
+ const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
177
+
178
+ useEffect(() => {
179
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
180
+ const wsUrl = `${protocol}//${window.location.host}/ws/git`;
181
+
182
+ const connect = () => {
183
+ try {
184
+ wsRef.current = new WebSocket(wsUrl);
185
+
186
+ wsRef.current.onopen = () => {
187
+ console.debug('[useGitStatus] WebSocket connected');
188
+ };
189
+
190
+ wsRef.current.onmessage = (event) => {
191
+ try {
192
+ const msg = JSON.parse(event.data) as GitMessage;
193
+ if (msg.type === 'init' || msg.type === 'update') {
194
+ setGitStatus(transformGitResponse(msg.repos));
195
+ setRepos(transformToRepoArray(msg.repos));
196
+ setIsLoading(false);
197
+ setError(null);
198
+ }
199
+ } catch (err) {
200
+ console.error('[useGitStatus] Failed to parse message:', err);
201
+ }
202
+ };
203
+
204
+ wsRef.current.onclose = () => {
205
+ console.debug('[useGitStatus] WebSocket closed, reconnecting...');
206
+ reconnectTimeoutRef.current = setTimeout(connect, 2000);
207
+ };
208
+
209
+ wsRef.current.onerror = (err) => {
210
+ console.error('[useGitStatus] WebSocket error:', err);
211
+ setError(new Error('WebSocket connection failed'));
212
+ };
213
+ } catch (err) {
214
+ console.error('[useGitStatus] WebSocket init failed:', err);
215
+ setError(err instanceof Error ? err : new Error('Failed to connect'));
216
+ setIsLoading(false);
217
+ }
218
+ };
219
+
220
+ connect();
221
+
222
+ return () => {
223
+ if (reconnectTimeoutRef.current) {
224
+ clearTimeout(reconnectTimeoutRef.current);
225
+ }
226
+ if (wsRef.current) {
227
+ wsRef.current.close();
228
+ }
229
+ };
230
+ }, []);
231
+
232
+ return { gitStatus, repos, isLoading, error };
233
+ }
@@ -0,0 +1,71 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ export interface HealthScoreDimension {
4
+ name: string;
5
+ score: number | null;
6
+ weight: number;
7
+ }
8
+
9
+ export interface HealthScoreData {
10
+ success: boolean;
11
+ composite_score: number;
12
+ dimensions: HealthScoreDimension[];
13
+ cached: boolean;
14
+ error?: string;
15
+ }
16
+
17
+ export interface UseHealthScoreReturn {
18
+ data: HealthScoreData | null;
19
+ isLoading: boolean;
20
+ error: Error | null;
21
+ lastFetchedAt: number | null;
22
+ refresh: () => void;
23
+ }
24
+
25
+ export function useHealthScore(): UseHealthScoreReturn {
26
+ const [data, setData] = useState<HealthScoreData | null>(null);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState<Error | null>(null);
29
+ const [lastFetchedAt, setLastFetchedAt] = useState<number | null>(null);
30
+ const abortRef = useRef<AbortController | null>(null);
31
+
32
+ const refresh = useCallback(() => {
33
+ if (abortRef.current) {
34
+ abortRef.current.abort();
35
+ }
36
+
37
+ const controller = new AbortController();
38
+ abortRef.current = controller;
39
+
40
+ setIsLoading(true);
41
+ setError(null);
42
+
43
+ fetch('/api/health-score', { signal: controller.signal })
44
+ .then((res) => {
45
+ if (!res.ok) {
46
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
47
+ }
48
+ return res.json();
49
+ })
50
+ .then((json: HealthScoreData) => {
51
+ setData(json);
52
+ setLastFetchedAt(Date.now());
53
+ setIsLoading(false);
54
+ })
55
+ .catch((err) => {
56
+ if (err.name === 'AbortError') return;
57
+ setError(err instanceof Error ? err : new Error(String(err)));
58
+ setIsLoading(false);
59
+ });
60
+ }, []);
61
+
62
+ // Auto-fetch on mount
63
+ useEffect(() => {
64
+ refresh();
65
+ return () => {
66
+ abortRef.current?.abort();
67
+ };
68
+ }, [refresh]);
69
+
70
+ return { data, isLoading, error, lastFetchedAt, refresh };
71
+ }
@@ -0,0 +1,123 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+
3
+ // Types matching Python HotspotResult / MultiRepoHotspotResult
4
+ export interface FileHotspot {
5
+ path: string;
6
+ change_count: number;
7
+ bug_fix_count: number;
8
+ author_count: number;
9
+ lines_added: number;
10
+ lines_deleted: number;
11
+ churn: number;
12
+ last_changed: string;
13
+ hotspot_score: number;
14
+ }
15
+
16
+ export interface DirectoryHotspot {
17
+ path: string;
18
+ file_count: number;
19
+ total_changes: number;
20
+ total_bug_fixes: number;
21
+ avg_author_count: number;
22
+ hotspot_score: number;
23
+ }
24
+
25
+ export interface HotspotRepoResult {
26
+ success: boolean;
27
+ repo_name: string;
28
+ repo_path: string;
29
+ time_window_days: number;
30
+ commit_count: number;
31
+ file_hotspots: FileHotspot[];
32
+ directory_hotspots: DirectoryHotspot[];
33
+ error?: string;
34
+ }
35
+
36
+ export interface HotspotData {
37
+ success: boolean;
38
+ // Single-repo result fields (when --path is used)
39
+ repo_name?: string;
40
+ repo_path?: string;
41
+ time_window_days?: number;
42
+ commit_count?: number;
43
+ file_hotspots?: FileHotspot[];
44
+ directory_hotspots?: DirectoryHotspot[];
45
+ // Multi-repo result fields
46
+ repo_results?: HotspotRepoResult[];
47
+ error?: string;
48
+ }
49
+
50
+ export interface UseHotspotsOptions {
51
+ days: number;
52
+ repo?: string;
53
+ skipTypes?: string[];
54
+ includeOrchestrator?: boolean;
55
+ }
56
+
57
+ export interface UseHotspotsReturn {
58
+ data: HotspotData | null;
59
+ isLoading: boolean;
60
+ error: Error | null;
61
+ refresh: () => void;
62
+ }
63
+
64
+ export function useHotspots(options: UseHotspotsOptions): UseHotspotsReturn {
65
+ const [data, setData] = useState<HotspotData | null>(null);
66
+ const [isLoading, setIsLoading] = useState(false);
67
+ const [error, setError] = useState<Error | null>(null);
68
+ const abortRef = useRef<AbortController | null>(null);
69
+
70
+ const fetchHotspots = useCallback(() => {
71
+ // Cancel any in-flight request
72
+ if (abortRef.current) {
73
+ abortRef.current.abort();
74
+ }
75
+
76
+ const controller = new AbortController();
77
+ abortRef.current = controller;
78
+
79
+ setIsLoading(true);
80
+ setError(null);
81
+
82
+ const params = new URLSearchParams({ days: String(options.days) });
83
+ if (options.repo) {
84
+ params.set('repo', options.repo);
85
+ }
86
+
87
+ // Determine skip_type values: use explicit skipTypes, or default to ['orchestrator']
88
+ // unless includeOrchestrator is true
89
+ const skipTypes = options.skipTypes ??
90
+ (options.includeOrchestrator ? [] : ['orchestrator']);
91
+ for (const st of skipTypes) {
92
+ params.append('skip_type', st);
93
+ }
94
+
95
+ fetch(`/api/hotspots?${params}`, { signal: controller.signal })
96
+ .then((res) => {
97
+ if (!res.ok) {
98
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
99
+ }
100
+ return res.json();
101
+ })
102
+ .then((json: HotspotData) => {
103
+ setData(json);
104
+ setIsLoading(false);
105
+ })
106
+ .catch((err) => {
107
+ if (err.name === 'AbortError') return;
108
+ setError(err instanceof Error ? err : new Error(String(err)));
109
+ setIsLoading(false);
110
+ });
111
+ }, [options.days, options.repo, options.skipTypes, options.includeOrchestrator]);
112
+
113
+ // Cleanup abort controller on unmount
114
+ useEffect(() => {
115
+ return () => {
116
+ if (abortRef.current) {
117
+ abortRef.current.abort();
118
+ }
119
+ };
120
+ }, []);
121
+
122
+ return { data, isLoading, error, refresh: fetchHotspots };
123
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * useLayoutPersistence Hook
3
+ *
4
+ * React hook for saving and restoring layout state to config.local.yaml.
5
+ * Story MSSCI-12706 - Layout Persistence
6
+ *
7
+ * Uses native Dockview SerializedDockview format for complete layout fidelity.
8
+ *
9
+ * REST API:
10
+ * - GET /api/settings/layout - Load layout
11
+ * - PATCH /api/settings/layout - Save layout
12
+ *
13
+ * Features:
14
+ * - Load layout from config on mount
15
+ * - Save layout on changes (debounced)
16
+ * - Per-project independent layouts
17
+ * - Graceful handling of corrupted/missing config
18
+ * - Native Dockview serialization for perfect restore
19
+ */
20
+
21
+ import { useState, useEffect, useCallback, useRef } from 'react';
22
+ import type { SerializedDockview } from 'dockview-react';
23
+
24
+ const DEBOUNCE_DELAY = 300;
25
+
26
+ interface UseLayoutPersistenceResult {
27
+ layout: SerializedDockview | null;
28
+ isLoading: boolean;
29
+ isSaving: boolean;
30
+ error: Error | null;
31
+ saveLayout: (layout: SerializedDockview) => void;
32
+ }
33
+
34
+ /**
35
+ * Validate that the layout has the native Dockview structure
36
+ */
37
+ function isValidDockviewLayout(layout: unknown): layout is SerializedDockview {
38
+ if (!layout || typeof layout !== 'object') return false;
39
+ const layoutObj = layout as Record<string, unknown>;
40
+
41
+ // Check for native Dockview structure: grid and panels are required
42
+ if (!layoutObj.grid || typeof layoutObj.grid !== 'object') return false;
43
+ if (!layoutObj.panels || typeof layoutObj.panels !== 'object') return false;
44
+
45
+ // A layout with zero panels is empty — treat as invalid so default panels get created
46
+ if (Object.keys(layoutObj.panels as Record<string, unknown>).length === 0) return false;
47
+
48
+ const grid = layoutObj.grid as Record<string, unknown>;
49
+ // Grid should have root, width, height, orientation
50
+ if (!grid.root || typeof grid.width !== 'number' || typeof grid.height !== 'number') return false;
51
+
52
+ return true;
53
+ }
54
+
55
+ export function useLayoutPersistence(endpoint: string = '/api/settings/layout'): UseLayoutPersistenceResult {
56
+ const [layout, setLayout] = useState<SerializedDockview | null>(null);
57
+ const [isLoading, setIsLoading] = useState(true);
58
+ const [isSaving, setIsSaving] = useState(false);
59
+ const [error, setError] = useState<Error | null>(null);
60
+
61
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
62
+ const pendingLayoutRef = useRef<SerializedDockview | null>(null);
63
+
64
+ // Load layout on mount via REST API
65
+ useEffect(() => {
66
+ const loadLayout = async () => {
67
+ try {
68
+ const response = await fetch(endpoint);
69
+ if (response.ok) {
70
+ const data = await response.json();
71
+ if (data.layout && isValidDockviewLayout(data.layout)) {
72
+ // Use native Dockview layout directly
73
+ setLayout(data.layout as SerializedDockview);
74
+ } else {
75
+ // No saved layout or invalid format - let DockviewWorkspace build default
76
+ setLayout(null);
77
+ }
78
+ } else {
79
+ // API error - let DockviewWorkspace build default
80
+ setLayout(null);
81
+ }
82
+ } catch (err) {
83
+ // On error, let DockviewWorkspace build default
84
+ console.error('[useLayoutPersistence] Failed to load layout:', err);
85
+ setLayout(null);
86
+ setError(err instanceof Error ? err : new Error('Failed to load layout'));
87
+ } finally {
88
+ setIsLoading(false);
89
+ }
90
+ };
91
+
92
+ loadLayout();
93
+ }, [endpoint]);
94
+
95
+ // Debounced save function via REST API - saves native Dockview format directly
96
+ const saveLayout = useCallback((newLayout: SerializedDockview) => {
97
+ pendingLayoutRef.current = newLayout;
98
+
99
+ // Clear existing debounce timer
100
+ if (debounceRef.current) {
101
+ clearTimeout(debounceRef.current);
102
+ }
103
+
104
+ // Set new debounce timer
105
+ debounceRef.current = setTimeout(async () => {
106
+ const layoutToSave = pendingLayoutRef.current;
107
+ if (!layoutToSave) return;
108
+
109
+ setIsSaving(true);
110
+ try {
111
+ // Save native Dockview format directly - no conversion needed
112
+ const response = await fetch(endpoint, {
113
+ method: 'PATCH',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify(layoutToSave),
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new Error('Failed to save layout');
120
+ }
121
+ setError(null);
122
+ } catch (err) {
123
+ console.error('[useLayoutPersistence] Failed to save layout:', err);
124
+ setError(err instanceof Error ? err : new Error('Failed to save layout'));
125
+ } finally {
126
+ setIsSaving(false);
127
+ }
128
+ }, DEBOUNCE_DELAY);
129
+ }, [endpoint]);
130
+
131
+ // Cleanup debounce timer on unmount
132
+ useEffect(() => {
133
+ return () => {
134
+ if (debounceRef.current) {
135
+ clearTimeout(debounceRef.current);
136
+ }
137
+ };
138
+ }, []);
139
+
140
+ return { layout, isLoading, isSaving, error, saveLayout };
141
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * useMarkdownParser Hook
3
+ *
4
+ * Story MSSCI-13969: React hook for parsing markdown to HTML with security.
5
+ * Extracted from js/components/message-view/markdown-parser.js
6
+ *
7
+ * Features:
8
+ * - XSS prevention via HTML escaping
9
+ * - CYCLIST marker stripping
10
+ * - Memoization for performance
11
+ */
12
+
13
+ import { useMemo } from 'react';
14
+ import { parseMarkdown } from '../utils/markdown.js';
15
+
16
+ export interface UseMarkdownParserResult {
17
+ html: string;
18
+ isLoading: boolean;
19
+ error: Error | null;
20
+ }
21
+
22
+ /**
23
+ * React hook for parsing markdown to HTML with security and memoization.
24
+ * @param markdown - Raw markdown text (or null)
25
+ * @returns Object with html string, loading state, and error
26
+ */
27
+ export function useMarkdownParser(
28
+ markdown: string | null
29
+ ): UseMarkdownParserResult {
30
+ const html = useMemo(() => {
31
+ if (!markdown) return '';
32
+ return parseMarkdown(markdown);
33
+ }, [markdown]);
34
+
35
+ return { html, isLoading: false, error: null };
36
+ }