@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,585 @@
1
+ /**
2
+ * DiffViewer - Display file diffs with syntax highlighting
3
+ *
4
+ * Story MSSCI-12711 - DiffViewer Component (Epic 71: Codebase Awareness)
5
+ *
6
+ * Features:
7
+ * - Side-by-side or unified diff view with toggle
8
+ * - Syntax highlighting per file type
9
+ * - Green additions, red deletions
10
+ * - Keyboard navigation between hunks
11
+ * - Toggle partial/full file view
12
+ */
13
+
14
+ import React, { useState, useCallback, useMemo, useRef, useEffect, KeyboardEvent } from 'react';
15
+ import { Button } from '@/components/ui/button';
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ export type DiffLineType = 'added' | 'removed' | 'unchanged';
22
+
23
+ export type ViewMode = 'unified' | 'side-by-side';
24
+
25
+ export type FileViewMode = 'partial' | 'full';
26
+
27
+ export interface DiffLine {
28
+ type: DiffLineType;
29
+ content: string;
30
+ oldLineNumber?: number;
31
+ newLineNumber?: number;
32
+ }
33
+
34
+ export interface DiffHunk {
35
+ oldStart: number;
36
+ oldCount: number;
37
+ newStart: number;
38
+ newCount: number;
39
+ lines: DiffLine[];
40
+ }
41
+
42
+ export interface DiffData {
43
+ filePath: string;
44
+ oldContent: string;
45
+ newContent: string;
46
+ hunks: DiffHunk[];
47
+ }
48
+
49
+ interface DiffViewerPropsBase {
50
+ viewMode?: ViewMode;
51
+ fileViewMode?: FileViewMode;
52
+ onViewModeChange?: (mode: ViewMode) => void;
53
+ onFileViewModeChange?: (mode: FileViewMode) => void;
54
+ onHunkNavigate?: (hunkIndex: number) => void;
55
+ }
56
+
57
+ interface DiffViewerPropsWithData extends DiffViewerPropsBase {
58
+ data: DiffData;
59
+ filePath?: never;
60
+ oldContent?: never;
61
+ newContent?: never;
62
+ }
63
+
64
+ interface DiffViewerPropsWithContent extends DiffViewerPropsBase {
65
+ data?: never;
66
+ filePath: string;
67
+ oldContent: string;
68
+ newContent: string;
69
+ }
70
+
71
+ export type DiffViewerProps = DiffViewerPropsWithData | DiffViewerPropsWithContent;
72
+
73
+ /**
74
+ * Generate hunks from content differences for simple add/remove cases
75
+ */
76
+ function generateSimpleHunks(oldContent: string, newContent: string): DiffHunk[] {
77
+ const isNew = oldContent === '' && newContent !== '';
78
+ const isDeleted = oldContent !== '' && newContent === '';
79
+
80
+ if (isNew) {
81
+ const lines = newContent.split('\n');
82
+ return [{
83
+ oldStart: 0,
84
+ oldCount: 0,
85
+ newStart: 1,
86
+ newCount: lines.length,
87
+ lines: lines.map((content, i) => ({
88
+ type: 'added' as DiffLineType,
89
+ content,
90
+ newLineNumber: i + 1,
91
+ })),
92
+ }];
93
+ }
94
+
95
+ if (isDeleted) {
96
+ const lines = oldContent.split('\n');
97
+ return [{
98
+ oldStart: 1,
99
+ oldCount: lines.length,
100
+ newStart: 0,
101
+ newCount: 0,
102
+ lines: lines.map((content, i) => ({
103
+ type: 'removed' as DiffLineType,
104
+ content,
105
+ oldLineNumber: i + 1,
106
+ })),
107
+ }];
108
+ }
109
+
110
+ // If both have content, treat as full modification
111
+ return [];
112
+ }
113
+
114
+ // =============================================================================
115
+ // Utility Functions
116
+ // =============================================================================
117
+
118
+ /**
119
+ * Get file extension from path
120
+ */
121
+ function getFileExtension(filePath: string): string {
122
+ const filename = filePath.split('/').pop() || '';
123
+ if (filename.startsWith('.')) {
124
+ return filename.slice(1); // .gitignore -> gitignore
125
+ }
126
+ const parts = filename.split('.');
127
+ if (parts.length < 2) return '';
128
+ return parts.pop() || '';
129
+ }
130
+
131
+ /**
132
+ * Get CSS language class for syntax highlighting
133
+ */
134
+ function getLanguageClass(ext: string): string {
135
+ const langMap: Record<string, string> = {
136
+ ts: 'lang-typescript',
137
+ tsx: 'lang-typescript',
138
+ js: 'lang-javascript',
139
+ jsx: 'lang-javascript',
140
+ css: 'lang-css',
141
+ html: 'lang-html',
142
+ json: 'lang-json',
143
+ md: 'lang-markdown',
144
+ py: 'lang-python',
145
+ gitignore: 'lang-gitignore',
146
+ };
147
+ return langMap[ext] || 'lang-plain';
148
+ }
149
+
150
+ /**
151
+ * Generate hidden lines between two hunks
152
+ */
153
+ function generateHiddenLines(
154
+ oldContent: string,
155
+ startLine: number,
156
+ endLine: number
157
+ ): DiffLine[] {
158
+ const lines = oldContent.split('\n');
159
+ const result: DiffLine[] = [];
160
+ for (let i = startLine; i < endLine && i < lines.length; i++) {
161
+ result.push({
162
+ type: 'unchanged',
163
+ content: lines[i],
164
+ oldLineNumber: i + 1,
165
+ newLineNumber: i + 1,
166
+ });
167
+ }
168
+ return result;
169
+ }
170
+
171
+ // =============================================================================
172
+ // DiffLine Component
173
+ // =============================================================================
174
+
175
+ interface DiffLineProps {
176
+ line: DiffLine;
177
+ }
178
+
179
+ function DiffLineComponent({ line }: DiffLineProps): React.ReactElement {
180
+ // Use non-breaking space for unchanged lines to preserve whitespace in HTML
181
+ const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : '\u00A0';
182
+ const ariaLabel = `Line ${line.oldLineNumber || line.newLineNumber}, ${line.type}: ${line.content}`;
183
+
184
+ return (
185
+ <div
186
+ data-testid={`diff-line-${line.type}`}
187
+ className={`diff-line diff-line-${line.type}`}
188
+ aria-label={ariaLabel}
189
+ >
190
+ <span
191
+ data-testid="diff-line-number-old"
192
+ className="diff-line-number diff-line-number-old"
193
+ >
194
+ {line.oldLineNumber ?? ''}
195
+ </span>
196
+ <span
197
+ data-testid="diff-line-number-new"
198
+ className="diff-line-number diff-line-number-new"
199
+ >
200
+ {line.newLineNumber ?? ''}
201
+ </span>
202
+ <span
203
+ data-testid={`diff-line-prefix-${line.type}`}
204
+ className="diff-line-prefix"
205
+ >
206
+ {prefix}
207
+ </span>
208
+ <span className="diff-line-content">{line.content}</span>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ // =============================================================================
214
+ // DiffHunk Component
215
+ // =============================================================================
216
+
217
+ interface DiffHunkProps {
218
+ hunk: DiffHunk;
219
+ index: number;
220
+ isFocused: boolean;
221
+ tabIndex: number;
222
+ }
223
+
224
+ function DiffHunkComponent({ hunk, index, isFocused, tabIndex }: DiffHunkProps): React.ReactElement {
225
+ const hunkRef = useRef<HTMLDivElement>(null);
226
+ const header = `@@ -${hunk.oldStart},${hunk.oldCount} +${hunk.newStart},${hunk.newCount} @@`;
227
+
228
+ useEffect(() => {
229
+ if (isFocused && hunkRef.current) {
230
+ hunkRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
231
+ }
232
+ }, [isFocused]);
233
+
234
+ return (
235
+ <div
236
+ ref={hunkRef}
237
+ data-testid="diff-hunk"
238
+ className={`diff-hunk${isFocused ? ' diff-hunk-focused' : ''}`}
239
+ tabIndex={tabIndex}
240
+ >
241
+ <div data-testid="diff-hunk-header" className="diff-hunk-header">
242
+ {header}
243
+ </div>
244
+ {hunk.lines.map((line, lineIndex) => (
245
+ <DiffLineComponent key={`${index}-${lineIndex}`} line={line} />
246
+ ))}
247
+ </div>
248
+ );
249
+ }
250
+
251
+ // =============================================================================
252
+ // Unified View Component
253
+ // =============================================================================
254
+
255
+ interface UnifiedViewProps {
256
+ hunks: DiffHunk[];
257
+ focusedHunk: number;
258
+ fileViewMode: FileViewMode;
259
+ oldContent: string;
260
+ expandedSections: Set<number>;
261
+ onExpandSection: (index: number) => void;
262
+ }
263
+
264
+ function UnifiedView({
265
+ hunks,
266
+ focusedHunk,
267
+ fileViewMode,
268
+ oldContent,
269
+ expandedSections,
270
+ onExpandSection,
271
+ }: UnifiedViewProps): React.ReactElement {
272
+ if (fileViewMode === 'full') {
273
+ // Show all lines, not just hunks
274
+ const allLines = oldContent.split('\n').map((content, i) => ({
275
+ type: 'unchanged' as DiffLineType,
276
+ content,
277
+ oldLineNumber: i + 1,
278
+ newLineNumber: i + 1,
279
+ }));
280
+
281
+ // Merge hunks with unchanged lines
282
+ const mergedLines: DiffLine[] = [];
283
+ let lineIndex = 0;
284
+
285
+ for (const hunk of hunks) {
286
+ // Add unchanged lines before this hunk
287
+ while (lineIndex < hunk.oldStart - 1) {
288
+ mergedLines.push(allLines[lineIndex]);
289
+ lineIndex++;
290
+ }
291
+ // Add hunk lines
292
+ for (const line of hunk.lines) {
293
+ mergedLines.push(line);
294
+ if (line.type !== 'added') {
295
+ lineIndex++;
296
+ }
297
+ }
298
+ }
299
+ // Add remaining unchanged lines
300
+ while (lineIndex < allLines.length) {
301
+ mergedLines.push(allLines[lineIndex]);
302
+ lineIndex++;
303
+ }
304
+
305
+ return (
306
+ <div data-testid="diff-full-view" className="diff-full-view">
307
+ {mergedLines.map((line, i) => (
308
+ <DiffLineComponent key={i} line={line} />
309
+ ))}
310
+ </div>
311
+ );
312
+ }
313
+
314
+ // Partial view - show only hunks with expand buttons between
315
+ const elements: React.ReactElement[] = [];
316
+
317
+ for (let i = 0; i < hunks.length; i++) {
318
+ const hunk = hunks[i];
319
+
320
+ // Add expand button between hunks
321
+ if (i > 0) {
322
+ const prevHunk = hunks[i - 1];
323
+ const prevEnd = prevHunk.oldStart + prevHunk.oldCount - 1;
324
+ const currentStart = hunk.oldStart - 1;
325
+ const hiddenCount = currentStart - prevEnd;
326
+
327
+ if (hiddenCount > 0) {
328
+ if (expandedSections.has(i)) {
329
+ // Show the expanded lines
330
+ const hiddenLines = generateHiddenLines(oldContent, prevEnd, currentStart);
331
+ elements.push(
332
+ <div key={`expanded-${i}`} className="diff-expanded-section">
333
+ {hiddenLines.map((line, j) => (
334
+ <DiffLineComponent key={`exp-${i}-${j}`} line={line} />
335
+ ))}
336
+ </div>
337
+ );
338
+ } else {
339
+ elements.push(
340
+ <Button
341
+ variant="ghost"
342
+ size="sm"
343
+ key={`expand-${i}`}
344
+ type="button"
345
+ className="diff-expand-button"
346
+ aria-label="Expand hidden lines"
347
+ onClick={() => onExpandSection(i)}
348
+ >
349
+ {hiddenCount} lines hidden ···
350
+ </Button>
351
+ );
352
+ }
353
+ }
354
+ }
355
+
356
+ elements.push(
357
+ <DiffHunkComponent
358
+ key={`hunk-${i}`}
359
+ hunk={hunk}
360
+ index={i}
361
+ isFocused={focusedHunk === i}
362
+ tabIndex={i === 0 ? 0 : -1}
363
+ />
364
+ );
365
+ }
366
+
367
+ return (
368
+ <div data-testid="diff-partial-view" className="diff-partial-view">
369
+ {elements}
370
+ </div>
371
+ );
372
+ }
373
+
374
+ // =============================================================================
375
+ // Side-by-Side View Component
376
+ // =============================================================================
377
+
378
+ interface SideBySideViewProps {
379
+ hunks: DiffHunk[];
380
+ focusedHunk: number;
381
+ }
382
+
383
+ function SideBySideView({ hunks, focusedHunk }: SideBySideViewProps): React.ReactElement {
384
+ // Separate lines into old (removed/unchanged) and new (added/unchanged)
385
+ const oldLines: DiffLine[] = [];
386
+ const newLines: DiffLine[] = [];
387
+
388
+ for (const hunk of hunks) {
389
+ for (const line of hunk.lines) {
390
+ if (line.type === 'removed') {
391
+ oldLines.push(line);
392
+ } else if (line.type === 'added') {
393
+ newLines.push(line);
394
+ } else {
395
+ oldLines.push(line);
396
+ newLines.push(line);
397
+ }
398
+ }
399
+ }
400
+
401
+ return (
402
+ <div data-testid="diff-side-by-side" className="diff-side-by-side">
403
+ <div data-testid="diff-panel-old" className="diff-panel diff-panel-old">
404
+ {oldLines.map((line, i) => (
405
+ <DiffLineComponent key={`old-${i}`} line={line} />
406
+ ))}
407
+ </div>
408
+ <div data-testid="diff-panel-new" className="diff-panel diff-panel-new">
409
+ {newLines.map((line, i) => (
410
+ <DiffLineComponent key={`new-${i}`} line={line} />
411
+ ))}
412
+ </div>
413
+ </div>
414
+ );
415
+ }
416
+
417
+ // =============================================================================
418
+ // DiffViewer Component
419
+ // =============================================================================
420
+
421
+ export function DiffViewer(props: DiffViewerProps): React.ReactElement {
422
+ const {
423
+ viewMode = 'unified',
424
+ fileViewMode = 'partial',
425
+ onViewModeChange,
426
+ onFileViewModeChange,
427
+ onHunkNavigate,
428
+ } = props;
429
+
430
+ // Support both data prop and direct props
431
+ const data: DiffData = 'data' in props && props.data
432
+ ? props.data
433
+ : {
434
+ filePath: (props as DiffViewerPropsWithContent).filePath,
435
+ oldContent: (props as DiffViewerPropsWithContent).oldContent,
436
+ newContent: (props as DiffViewerPropsWithContent).newContent,
437
+ hunks: generateSimpleHunks(
438
+ (props as DiffViewerPropsWithContent).oldContent,
439
+ (props as DiffViewerPropsWithContent).newContent
440
+ ),
441
+ };
442
+
443
+ const [focusedHunk, setFocusedHunk] = useState(0);
444
+ const [expandedSections, setExpandedSections] = useState<Set<number>>(new Set());
445
+ const viewerRef = useRef<HTMLDivElement>(null);
446
+
447
+ const ext = getFileExtension(data.filePath);
448
+ const langClass = getLanguageClass(ext);
449
+
450
+ const isNewFile = data.oldContent === '' && data.newContent !== '';
451
+ const isDeletedFile = data.oldContent !== '' && data.newContent === '';
452
+ const hasNoChanges = data.hunks.length === 0;
453
+
454
+ const handleKeyDown = useCallback(
455
+ (e: KeyboardEvent<HTMLDivElement>) => {
456
+ const nextKeys = ['j', 'ArrowDown', 'n'];
457
+ const prevKeys = ['k', 'ArrowUp'];
458
+
459
+ if (nextKeys.includes(e.key) || (e.key === 'n' && !e.shiftKey)) {
460
+ const nextIndex = focusedHunk + 1;
461
+ if (nextIndex < data.hunks.length) {
462
+ setFocusedHunk(nextIndex);
463
+ onHunkNavigate?.(nextIndex);
464
+ }
465
+ } else if (prevKeys.includes(e.key) || (e.key === 'N' && e.shiftKey)) {
466
+ const prevIndex = focusedHunk - 1;
467
+ if (prevIndex >= 0) {
468
+ setFocusedHunk(prevIndex);
469
+ onHunkNavigate?.(prevIndex);
470
+ }
471
+ }
472
+ },
473
+ [focusedHunk, data.hunks.length, onHunkNavigate]
474
+ );
475
+
476
+ const handleViewModeToggle = useCallback(() => {
477
+ const newMode = viewMode === 'unified' ? 'side-by-side' : 'unified';
478
+ onViewModeChange?.(newMode);
479
+ }, [viewMode, onViewModeChange]);
480
+
481
+ const handleFileViewModeToggle = useCallback(() => {
482
+ const newMode = fileViewMode === 'partial' ? 'full' : 'partial';
483
+ onFileViewModeChange?.(newMode);
484
+ }, [fileViewMode, onFileViewModeChange]);
485
+
486
+ const handleExpandSection = useCallback((index: number) => {
487
+ setExpandedSections((prev) => {
488
+ const next = new Set(prev);
489
+ next.add(index);
490
+ return next;
491
+ });
492
+ }, []);
493
+
494
+ // Empty/special states
495
+ if (hasNoChanges && !isNewFile && !isDeletedFile) {
496
+ return (
497
+ <div
498
+ ref={viewerRef}
499
+ role="region"
500
+ aria-label="Diff viewer"
501
+ data-testid="diff-viewer"
502
+ className={`diff-viewer ${langClass}`}
503
+ >
504
+ <div className="diff-header">
505
+ <span className="diff-file-path">{data.filePath}</span>
506
+ </div>
507
+ <div className="diff-empty">No changes</div>
508
+ </div>
509
+ );
510
+ }
511
+
512
+ return (
513
+ <div
514
+ ref={viewerRef}
515
+ role="region"
516
+ aria-label="Diff viewer"
517
+ data-testid="diff-viewer"
518
+ className={`diff-viewer ${langClass}`}
519
+ onKeyDown={handleKeyDown}
520
+ tabIndex={-1}
521
+ >
522
+ {/* Header */}
523
+ <div className="diff-header">
524
+ <span className="diff-file-path">{data.filePath}</span>
525
+ <div className="diff-controls">
526
+ <Button
527
+ variant="outline"
528
+ size="sm"
529
+ type="button"
530
+ aria-label="Toggle view mode"
531
+ onClick={handleViewModeToggle}
532
+ className="diff-view-toggle"
533
+ >
534
+ {viewMode === 'unified' ? 'Side-by-side' : 'Unified'}
535
+ </Button>
536
+ <Button
537
+ variant="outline"
538
+ size="sm"
539
+ type="button"
540
+ aria-label={fileViewMode === 'partial' ? 'Show full file' : 'Show changes only'}
541
+ onClick={handleFileViewModeToggle}
542
+ className="diff-file-view-toggle"
543
+ >
544
+ {fileViewMode === 'partial' ? 'Show full file' : 'Show changes only'}
545
+ </Button>
546
+ </div>
547
+ </div>
548
+
549
+ {/* New/Deleted file indicators */}
550
+ {isNewFile && <div data-testid="diff-new-file-indicator" className="diff-new-file">New file</div>}
551
+ {isDeletedFile && <div data-testid="diff-deleted-file-indicator" className="diff-deleted-file">File deleted</div>}
552
+
553
+ {/* Hunk position indicator (for accessibility) */}
554
+ {data.hunks.length > 1 && (
555
+ <div className="diff-hunk-position" aria-live="polite">
556
+ Hunk {focusedHunk + 1} of {data.hunks.length}
557
+ </div>
558
+ )}
559
+
560
+ {/* Content */}
561
+ {viewMode === 'unified' ? (
562
+ <UnifiedView
563
+ hunks={data.hunks}
564
+ focusedHunk={focusedHunk}
565
+ fileViewMode={fileViewMode}
566
+ oldContent={data.oldContent}
567
+ expandedSections={expandedSections}
568
+ onExpandSection={handleExpandSection}
569
+ />
570
+ ) : (
571
+ <SideBySideView hunks={data.hunks} focusedHunk={focusedHunk} />
572
+ )}
573
+
574
+ {/* Unified view marker */}
575
+ {viewMode === 'unified' && fileViewMode === 'partial' && (
576
+ <div data-testid="diff-unified" style={{ display: 'none' }} />
577
+ )}
578
+ {viewMode === 'unified' && fileViewMode === 'full' && (
579
+ <div data-testid="diff-unified" style={{ display: 'none' }} />
580
+ )}
581
+ </div>
582
+ );
583
+ }
584
+
585
+ export default DiffViewer;