@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,630 @@
1
+ /**
2
+ * Editor Component
3
+ *
4
+ * React textarea editor with command history, tab completion, and message queue.
5
+ * Story MSSCI-12717 - React Migration
6
+ *
7
+ * Features:
8
+ * - Textarea with Enter=submit, Shift+Enter=newline
9
+ * - Command history (Up/Down arrows)
10
+ * - Tab completion popup for /commands
11
+ * - Message queue indicator
12
+ * - Image paste handling
13
+ * - Mode toolbar (Plan/Manual/Accept)
14
+ */
15
+
16
+ import React, {
17
+ useState,
18
+ useRef,
19
+ useCallback,
20
+ useEffect,
21
+ KeyboardEvent,
22
+ ClipboardEvent,
23
+ ChangeEvent,
24
+ } from 'react';
25
+ import { Button } from '@/components/ui/button';
26
+ import { Badge } from '@/components/ui/badge';
27
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
28
+ import { useCommandHistory } from '../hooks/useCommandHistory';
29
+ import { useTabCompletion } from '../hooks/useTabCompletion';
30
+ import { useMessageQueueContext, QueuedMessage } from '../contexts/MessageQueueContext';
31
+ import { ModeSwitch, Mode, useModeSync, useModeSwitchShortcuts } from './ModeSwitch';
32
+ import { trackCommandUsage } from '../utils/slash-commands';
33
+
34
+ // =============================================================================
35
+ // Types
36
+ // =============================================================================
37
+
38
+ export interface PastedImage {
39
+ dataUrl: string;
40
+ mimeType: string;
41
+ filename: string;
42
+ }
43
+
44
+ export interface EditorProps {
45
+ onSubmit: (text: string, images: PastedImage[]) => void;
46
+ isProcessing?: boolean;
47
+ placeholder?: string;
48
+ /** Callback to immediately inject a queued message (abort + send) */
49
+ onInject?: (index: number) => Promise<boolean>;
50
+ }
51
+
52
+ // PermissionMode type moved to ModeSwitch component
53
+
54
+ // Supported image types for paste
55
+ const SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
56
+ const IMAGE_MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20MB
57
+
58
+ // =============================================================================
59
+ // Completion Popup Component
60
+ // =============================================================================
61
+
62
+ interface CompletionPopupProps {
63
+ commands: Array<{ name: string; description: string }>;
64
+ selectedIndex: number;
65
+ visible: boolean;
66
+ onSelect: (index: number) => void;
67
+ }
68
+
69
+ function CompletionPopup({ commands, selectedIndex, visible, onSelect }: CompletionPopupProps) {
70
+ if (!visible || commands.length === 0) return null;
71
+
72
+ return (
73
+ <div className="completion-popup" data-testid="completion-popup">
74
+ {commands.map((cmd, index) => (
75
+ <div
76
+ key={cmd.name}
77
+ className={`completion-item ${index === selectedIndex ? 'selected' : ''}`}
78
+ onClick={() => onSelect(index)}
79
+ data-testid={`completion-item-${index}`}
80
+ >
81
+ <span className="completion-name">{cmd.name}</span>
82
+ <span className="completion-desc">{cmd.description}</span>
83
+ </div>
84
+ ))}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ // =============================================================================
90
+ // Queue Display Component (MSSCI-12275)
91
+ // =============================================================================
92
+
93
+ interface QueueDisplayProps {
94
+ queue: QueuedMessage[];
95
+ bellMode: boolean;
96
+ onRemove: (index: number) => void;
97
+ onClear: () => void;
98
+ /** Callback to immediately inject a queued message (abort + send) */
99
+ onInject?: (index: number) => Promise<boolean>;
100
+ }
101
+
102
+ /**
103
+ * Escape HTML for safe rendering
104
+ */
105
+ function escapeHtml(str: string): string {
106
+ if (!str) return '';
107
+ return str
108
+ .replace(/&/g, '&amp;')
109
+ .replace(/</g, '&lt;')
110
+ .replace(/>/g, '&gt;')
111
+ .replace(/"/g, '&quot;');
112
+ }
113
+
114
+ function QueueDisplay({ queue, bellMode, onRemove, onClear, onInject }: QueueDisplayProps) {
115
+ if (queue.length === 0) return null;
116
+
117
+ return (
118
+ <TooltipProvider delayDuration={300}>
119
+ <div className="queue-display" data-testid="queue-display">
120
+ <div className="queue-header">
121
+ <span className="queue-count">{queue.length} queued</span>
122
+ {bellMode && (
123
+ <Tooltip>
124
+ <TooltipTrigger asChild>
125
+ <Badge variant="secondary" className="queue-mode-badge bell-mode">🔔</Badge>
126
+ </TooltipTrigger>
127
+ <TooltipContent>Bell mode active - messages inject via hook</TooltipContent>
128
+ </Tooltip>
129
+ )}
130
+ <Tooltip>
131
+ <TooltipTrigger asChild>
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
135
+ type="button"
136
+ className="queue-clear-btn"
137
+ onClick={onClear}
138
+ >
139
+ Clear
140
+ </Button>
141
+ </TooltipTrigger>
142
+ <TooltipContent>Clear all queued messages</TooltipContent>
143
+ </Tooltip>
144
+ </div>
145
+ <ul className="queue-list">
146
+ {queue.map((msg, index) => {
147
+ const truncated = msg.text.length > 60 ? msg.text.substring(0, 60) + '...' : msg.text;
148
+ const hasImages = msg.images && msg.images.length > 0;
149
+
150
+ return (
151
+ <li key={index} className="queue-item" data-testid={`queue-item-${index}`}>
152
+ <span className="queue-item-text">{escapeHtml(truncated)}</span>
153
+ {hasImages && (
154
+ <Tooltip>
155
+ <TooltipTrigger asChild>
156
+ <Badge variant="outline" className="queue-image-indicator">
157
+ 📎{msg.images.length}
158
+ </Badge>
159
+ </TooltipTrigger>
160
+ <TooltipContent>{`${msg.images.length} image(s) attached`}</TooltipContent>
161
+ </Tooltip>
162
+ )}
163
+ <div className="queue-item-actions">
164
+ {onInject && (
165
+ <Tooltip>
166
+ <TooltipTrigger asChild>
167
+ <Button
168
+ variant="ghost"
169
+ size="icon"
170
+ type="button"
171
+ className="queue-item-inject"
172
+ onClick={() => onInject(index)}
173
+ >
174
+
175
+ </Button>
176
+ </TooltipTrigger>
177
+ <TooltipContent>Send now (abort current and send this message)</TooltipContent>
178
+ </Tooltip>
179
+ )}
180
+ <Tooltip>
181
+ <TooltipTrigger asChild>
182
+ <Button
183
+ variant="ghost"
184
+ size="icon"
185
+ type="button"
186
+ className="queue-item-remove"
187
+ onClick={() => onRemove(index)}
188
+ >
189
+ ×
190
+ </Button>
191
+ </TooltipTrigger>
192
+ <TooltipContent>Remove from queue</TooltipContent>
193
+ </Tooltip>
194
+ </div>
195
+ </li>
196
+ );
197
+ })}
198
+ </ul>
199
+ </div>
200
+ </TooltipProvider>
201
+ );
202
+ }
203
+
204
+ // =============================================================================
205
+ // Image Preview Component
206
+ // =============================================================================
207
+
208
+ interface ImagePreviewProps {
209
+ images: PastedImage[];
210
+ onRemove: (index: number) => void;
211
+ }
212
+
213
+ function ImagePreview({ images, onRemove }: ImagePreviewProps) {
214
+ if (images.length === 0) return null;
215
+
216
+ return (
217
+ <TooltipProvider delayDuration={300}>
218
+ <div className="image-preview" data-testid="image-preview">
219
+ {images.map((img, index) => (
220
+ <div key={index} className="image-preview-item">
221
+ <img src={img.dataUrl} alt={img.filename} />
222
+ <Tooltip>
223
+ <TooltipTrigger asChild>
224
+ <Button
225
+ variant="ghost"
226
+ size="icon"
227
+ type="button"
228
+ className="image-remove"
229
+ onClick={() => onRemove(index)}
230
+ >
231
+ X
232
+ </Button>
233
+ </TooltipTrigger>
234
+ <TooltipContent>Remove image</TooltipContent>
235
+ </Tooltip>
236
+ </div>
237
+ ))}
238
+ </div>
239
+ </TooltipProvider>
240
+ );
241
+ }
242
+
243
+ // =============================================================================
244
+ // Editor Component
245
+ // =============================================================================
246
+
247
+ export function Editor({ onSubmit, isProcessing = false, placeholder, onInject }: EditorProps): React.ReactElement {
248
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
249
+ const [value, setValue] = useState('');
250
+ const [pendingImages, setPendingImages] = useState<PastedImage[]>([]);
251
+
252
+ // Mode state synced with Claude backend
253
+ const { mode, setMode } = useModeSync();
254
+
255
+ // Register Cmd+1/2/3 shortcuts for mode switching
256
+ useModeSwitchShortcuts(setMode);
257
+
258
+ // Hooks
259
+ const { addToHistory, navigateUp, navigateDown, resetNavigation } = useCommandHistory();
260
+ const {
261
+ state: completionState,
262
+ showCompletion,
263
+ hideCompletion,
264
+ updateCompletion,
265
+ navigateUp: completionUp,
266
+ navigateDown: completionDown,
267
+ selectCurrent,
268
+ isVisible: isCompletionVisible,
269
+ } = useTabCompletion();
270
+ const {
271
+ queue,
272
+ queueCount,
273
+ bellMode,
274
+ queueMessage,
275
+ removeFromQueue,
276
+ clearQueue,
277
+ setProcessing,
278
+ resumeQueue,
279
+ } = useMessageQueueContext();
280
+
281
+ // Sync processing state
282
+ useEffect(() => {
283
+ setProcessing(isProcessing);
284
+ }, [isProcessing, setProcessing]);
285
+
286
+ // Mode initialization is now handled by useModeSync hook
287
+
288
+ // Listen for suggested prompts from QuickActions
289
+ useEffect(() => {
290
+ const handleSuggestPrompt = (e: CustomEvent<{ prompt: string }>) => {
291
+ setValue(e.detail.prompt);
292
+ textareaRef.current?.focus();
293
+ };
294
+ window.addEventListener('cyclist:suggest-prompt', handleSuggestPrompt as EventListener);
295
+ return () => {
296
+ window.removeEventListener('cyclist:suggest-prompt', handleSuggestPrompt as EventListener);
297
+ };
298
+ }, []);
299
+
300
+ // ==========================================================================
301
+ // Image Handling
302
+ // ==========================================================================
303
+
304
+ const handleImagePaste = useCallback(async (clipboardData: DataTransfer): Promise<boolean> => {
305
+ let imageFile: File | null = null;
306
+
307
+ // Check clipboard items
308
+ if (clipboardData.items) {
309
+ for (const item of clipboardData.items) {
310
+ if (item.type && SUPPORTED_IMAGE_TYPES.includes(item.type)) {
311
+ imageFile = item.getAsFile();
312
+ break;
313
+ }
314
+ }
315
+ }
316
+
317
+ // Fallback to files
318
+ if (!imageFile && clipboardData.files?.length > 0) {
319
+ for (const file of clipboardData.files) {
320
+ if (file.type && SUPPORTED_IMAGE_TYPES.includes(file.type)) {
321
+ imageFile = file;
322
+ break;
323
+ }
324
+ }
325
+ }
326
+
327
+ if (!imageFile) return false;
328
+
329
+ // Check size
330
+ if (imageFile.size > IMAGE_MAX_SIZE_BYTES) {
331
+ console.warn('Image too large:', imageFile.size);
332
+ return false;
333
+ }
334
+
335
+ // Convert to data URL
336
+ return new Promise((resolve) => {
337
+ const reader = new FileReader();
338
+ reader.onload = () => {
339
+ const dataUrl = reader.result as string;
340
+ const filename = imageFile!.name || `Pasted Image.${imageFile!.type.split('/')[1] || 'png'}`;
341
+ setPendingImages(prev => [...prev, {
342
+ dataUrl,
343
+ mimeType: imageFile!.type,
344
+ filename,
345
+ }]);
346
+ resolve(true);
347
+ };
348
+ reader.onerror = () => resolve(false);
349
+ reader.readAsDataURL(imageFile);
350
+ });
351
+ }, []);
352
+
353
+ const removeImage = useCallback((index: number) => {
354
+ setPendingImages(prev => prev.filter((_, i) => i !== index));
355
+ }, []);
356
+
357
+ // ==========================================================================
358
+ // Slash Prefix Detection
359
+ // ==========================================================================
360
+
361
+ const getSlashPrefix = useCallback((): { prefix: string; start: number; end: number } | null => {
362
+ if (!textareaRef.current) return null;
363
+ const cursorPos = textareaRef.current.selectionStart;
364
+ const text = value;
365
+
366
+ // Find word start
367
+ let wordStart = cursorPos;
368
+ while (wordStart > 0 && text[wordStart - 1] !== ' ' && text[wordStart - 1] !== '\n') {
369
+ wordStart--;
370
+ }
371
+
372
+ const word = text.substring(wordStart, cursorPos);
373
+ if (word.startsWith('/')) {
374
+ return { prefix: word, start: wordStart, end: cursorPos };
375
+ }
376
+ return null;
377
+ }, [value]);
378
+
379
+ const replaceSlashPrefix = useCallback((commandName: string) => {
380
+ const prefixInfo = getSlashPrefix();
381
+ if (!prefixInfo) {
382
+ setValue(prev => prev + commandName);
383
+ return;
384
+ }
385
+
386
+ const { start, end } = prefixInfo;
387
+ setValue(prev => prev.substring(0, start) + commandName + prev.substring(end));
388
+
389
+ // Move cursor after command
390
+ setTimeout(() => {
391
+ if (textareaRef.current) {
392
+ const newPos = start + commandName.length;
393
+ textareaRef.current.selectionStart = textareaRef.current.selectionEnd = newPos;
394
+ }
395
+ }, 0);
396
+ }, [getSlashPrefix]);
397
+
398
+ // ==========================================================================
399
+ // Submit Logic
400
+ // ==========================================================================
401
+
402
+ const handleSubmit = useCallback(() => {
403
+ const trimmed = value.trim();
404
+ if (!trimmed && pendingImages.length === 0) return;
405
+
406
+ // If processing, queue the message
407
+ if (isProcessing) {
408
+ const queued = queueMessage({ text: trimmed, images: [...pendingImages] });
409
+ if (queued) {
410
+ setValue('');
411
+ setPendingImages([]);
412
+ textareaRef.current?.focus();
413
+ }
414
+ return;
415
+ }
416
+
417
+ // Add to history and submit
418
+ addToHistory(trimmed);
419
+ resetNavigation();
420
+
421
+ // Track slash command usage for frequency sorting
422
+ if (trimmed.startsWith('/')) {
423
+ const command = trimmed.split(/\s/)[0]; // Extract "/command" from "/command args"
424
+ trackCommandUsage(command);
425
+ }
426
+
427
+ // Resume queue if it was paused (e.g., after abort)
428
+ resumeQueue();
429
+
430
+ // Dispatch event to clear QuickActions (Reflector questions)
431
+ window.dispatchEvent(new CustomEvent('cyclist:user-submit'));
432
+
433
+ onSubmit(trimmed, pendingImages);
434
+ setValue('');
435
+ setPendingImages([]);
436
+ textareaRef.current?.focus();
437
+ }, [value, pendingImages, isProcessing, queueMessage, addToHistory, resetNavigation, resumeQueue, onSubmit]);
438
+
439
+ // ==========================================================================
440
+ // Event Handlers
441
+ // ==========================================================================
442
+
443
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
444
+ // Mode shortcuts are now handled globally by useModeSwitchShortcuts
445
+
446
+ // Tab - trigger or select completion
447
+ if (e.key === 'Tab' && !e.shiftKey && !e.ctrlKey && !e.altKey) {
448
+ if (isCompletionVisible) {
449
+ e.preventDefault();
450
+ const selected = selectCurrent();
451
+ if (selected) replaceSlashPrefix(selected);
452
+ return;
453
+ }
454
+ const prefixInfo = getSlashPrefix();
455
+ if (prefixInfo) {
456
+ e.preventDefault();
457
+ showCompletion(prefixInfo.prefix);
458
+ return;
459
+ }
460
+ }
461
+
462
+ // Escape - close completion
463
+ if (e.key === 'Escape') {
464
+ if (isCompletionVisible) {
465
+ e.preventDefault();
466
+ hideCompletion();
467
+ return;
468
+ }
469
+ }
470
+
471
+ // Enter - submit or select completion
472
+ if (e.key === 'Enter' && !e.shiftKey) {
473
+ if (isCompletionVisible) {
474
+ e.preventDefault();
475
+ const selected = selectCurrent();
476
+ if (selected) replaceSlashPrefix(selected);
477
+ return;
478
+ }
479
+ e.preventDefault();
480
+ handleSubmit();
481
+ return;
482
+ }
483
+
484
+ // Up arrow - completion or history
485
+ if (e.key === 'ArrowUp' && !e.shiftKey && !e.ctrlKey && !e.altKey) {
486
+ if (isCompletionVisible) {
487
+ e.preventDefault();
488
+ completionUp();
489
+ return;
490
+ }
491
+ // Only navigate history if at start
492
+ if (textareaRef.current?.selectionStart === 0) {
493
+ const prev = navigateUp(value);
494
+ if (prev !== null) {
495
+ e.preventDefault();
496
+ setValue(prev);
497
+ return;
498
+ }
499
+ }
500
+ }
501
+
502
+ // Down arrow - completion or history
503
+ if (e.key === 'ArrowDown' && !e.shiftKey && !e.ctrlKey && !e.altKey) {
504
+ if (isCompletionVisible) {
505
+ e.preventDefault();
506
+ completionDown();
507
+ return;
508
+ }
509
+ // Only navigate history if at end
510
+ if (textareaRef.current?.selectionEnd === value.length) {
511
+ const next = navigateDown();
512
+ if (next !== null) {
513
+ e.preventDefault();
514
+ setValue(next);
515
+ return;
516
+ }
517
+ }
518
+ }
519
+ }, [
520
+ isCompletionVisible, selectCurrent, replaceSlashPrefix,
521
+ getSlashPrefix, showCompletion, hideCompletion, handleSubmit,
522
+ completionUp, completionDown, navigateUp, navigateDown, value
523
+ ]);
524
+
525
+ const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
526
+ const newValue = e.target.value;
527
+ setValue(newValue);
528
+
529
+ // Auto-show completion when "/" is typed at start
530
+ if (newValue === '/' && !isCompletionVisible) {
531
+ showCompletion('/');
532
+ return;
533
+ }
534
+
535
+ // Update completion if visible
536
+ if (isCompletionVisible) {
537
+ const prefixInfo = getSlashPrefix();
538
+ if (prefixInfo) {
539
+ updateCompletion(prefixInfo.prefix);
540
+ } else {
541
+ hideCompletion();
542
+ }
543
+ }
544
+ }, [isCompletionVisible, showCompletion, getSlashPrefix, updateCompletion, hideCompletion]);
545
+
546
+ const handlePaste = useCallback(async (e: ClipboardEvent<HTMLTextAreaElement>) => {
547
+ const clipboardData = e.clipboardData;
548
+ if (!clipboardData) return;
549
+
550
+ // Check for image
551
+ let hasImage = false;
552
+ if (clipboardData.items) {
553
+ for (const item of clipboardData.items) {
554
+ if (item.type && SUPPORTED_IMAGE_TYPES.includes(item.type)) {
555
+ hasImage = true;
556
+ break;
557
+ }
558
+ }
559
+ }
560
+ if (!hasImage && clipboardData.files?.length > 0) {
561
+ for (const file of clipboardData.files) {
562
+ if (file.type && SUPPORTED_IMAGE_TYPES.includes(file.type)) {
563
+ hasImage = true;
564
+ break;
565
+ }
566
+ }
567
+ }
568
+
569
+ if (hasImage) {
570
+ e.preventDefault();
571
+ await handleImagePaste(clipboardData);
572
+ }
573
+ }, [handleImagePaste]);
574
+
575
+ const handleCompletionSelect = useCallback((index: number) => {
576
+ const cmd = completionState.commands[index];
577
+ if (cmd) {
578
+ replaceSlashPrefix(cmd.name);
579
+ hideCompletion();
580
+ }
581
+ }, [completionState.commands, replaceSlashPrefix, hideCompletion]);
582
+
583
+ // ==========================================================================
584
+ // Render
585
+ // ==========================================================================
586
+
587
+ return (
588
+ <div className="editor-container" data-testid="editor-container">
589
+ <ModeSwitch
590
+ mode={mode}
591
+ onModeChange={setMode}
592
+ className="editor-mode-switch"
593
+ />
594
+ <div className="editor-wrapper" id="editor-wrapper">
595
+ <ImagePreview images={pendingImages} onRemove={removeImage} />
596
+
597
+ <textarea
598
+ ref={textareaRef}
599
+ id="editor-textarea"
600
+ className="editor-textarea"
601
+ value={value}
602
+ onChange={handleChange}
603
+ onKeyDown={handleKeyDown}
604
+ onPaste={handlePaste}
605
+ placeholder={placeholder}
606
+ spellCheck={false}
607
+ autoFocus
608
+ data-testid="editor-textarea"
609
+ />
610
+
611
+ <CompletionPopup
612
+ commands={completionState.commands}
613
+ selectedIndex={completionState.selectedIndex}
614
+ visible={isCompletionVisible}
615
+ onSelect={handleCompletionSelect}
616
+ />
617
+ </div>
618
+
619
+ <QueueDisplay
620
+ queue={queue}
621
+ bellMode={bellMode}
622
+ onRemove={removeFromQueue}
623
+ onClear={clearQueue}
624
+ onInject={onInject}
625
+ />
626
+ </div>
627
+ );
628
+ }
629
+
630
+ export default Editor;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ErrorBoundary - Catches React errors to prevent entire app from crashing
3
+ *
4
+ * When a panel component throws (e.g., undefined.split()), this boundary
5
+ * catches the error and displays a fallback UI instead of blanking the screen.
6
+ */
7
+
8
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
9
+
10
+ interface Props {
11
+ children: ReactNode;
12
+ fallback?: ReactNode;
13
+ panelName?: string;
14
+ }
15
+
16
+ interface State {
17
+ hasError: boolean;
18
+ error?: Error;
19
+ }
20
+
21
+ export class ErrorBoundary extends Component<Props, State> {
22
+ state: State = { hasError: false };
23
+
24
+ static getDerivedStateFromError(error: Error): State {
25
+ return { hasError: true, error };
26
+ }
27
+
28
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
29
+ const panelContext = this.props.panelName ? ` in ${this.props.panelName}` : '';
30
+ console.error(`[ErrorBoundary] Caught error${panelContext}:`, error, errorInfo);
31
+ }
32
+
33
+ render() {
34
+ if (this.state.hasError) {
35
+ if (this.props.fallback) {
36
+ return this.props.fallback;
37
+ }
38
+
39
+ return (
40
+ <div className="error-boundary-fallback" style={{
41
+ padding: '16px',
42
+ color: 'var(--status-error, #ef4444)',
43
+ backgroundColor: 'var(--bg-tertiary, #0f0f1a)',
44
+ border: '1px solid var(--status-error, #ef4444)',
45
+ borderRadius: '4px',
46
+ margin: '8px',
47
+ }}>
48
+ <h4 style={{ margin: '0 0 8px 0' }}>
49
+ {this.props.panelName ? `${this.props.panelName} Error` : 'Panel Error'}
50
+ </h4>
51
+ <pre style={{
52
+ margin: 0,
53
+ fontSize: '12px',
54
+ whiteSpace: 'pre-wrap',
55
+ wordBreak: 'break-word',
56
+ }}>
57
+ {this.state.error?.message}
58
+ </pre>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return this.props.children;
64
+ }
65
+ }
66
+
67
+ export default ErrorBoundary;