@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,430 @@
1
+ /**
2
+ * FontPicker Component
3
+ *
4
+ * Font selection with system font browser via queryLocalFonts() API.
5
+ * Story MSSCI-12769 - Font Customization
6
+ *
7
+ * Features:
8
+ * - shadcn Select-based dropdown with system font discovery
9
+ * - Live font preview in each option
10
+ * - Monospace detection for code font filtering
11
+ * - Graceful fallback when queryLocalFonts() unavailable
12
+ * - Font size picker (segmented control)
13
+ * - ARIA accessibility via Radix Select
14
+ */
15
+
16
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectGroup,
23
+ SelectItem,
24
+ SelectLabel,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@/components/ui/select';
28
+ import {
29
+ UI_FONT_PRESETS,
30
+ CODE_FONT_PRESETS,
31
+ FONT_SIZE_SCALE,
32
+ FontSize,
33
+ } from '../../utils/font-presets';
34
+ import './FontPicker.css';
35
+
36
+ // =============================================================================
37
+ // Types
38
+ // =============================================================================
39
+
40
+ export interface FontPickerProps {
41
+ type: 'ui' | 'code';
42
+ currentFont: string;
43
+ customFont?: string;
44
+ onSelect: (presetId: string, customFamily?: string) => void;
45
+ onCustomFontChange?: (family: string) => void;
46
+ className?: string;
47
+ }
48
+
49
+ export interface FontSizePickerProps {
50
+ currentSize: FontSize;
51
+ onSelect: (size: FontSize) => void;
52
+ className?: string;
53
+ }
54
+
55
+ interface SystemFont {
56
+ family: string;
57
+ isMonospace: boolean;
58
+ }
59
+
60
+ // =============================================================================
61
+ // Monospace Detection via OpenType `post` table
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Parse SFNT table directory to find a table's offset and length.
66
+ * SFNT header: version(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2) = 12
67
+ * Each table record: tag(4) + checksum(4) + offset(4) + length(4) = 16
68
+ */
69
+ function findSfntTable(view: DataView, tag: string): { offset: number; length: number } | null {
70
+ const numTables = view.getUint16(4);
71
+ for (let i = 0; i < numTables; i++) {
72
+ const recordOffset = 12 + i * 16;
73
+ const tableTag = String.fromCharCode(
74
+ view.getUint8(recordOffset),
75
+ view.getUint8(recordOffset + 1),
76
+ view.getUint8(recordOffset + 2),
77
+ view.getUint8(recordOffset + 3),
78
+ );
79
+ if (tableTag === tag) {
80
+ return {
81
+ offset: view.getUint32(recordOffset + 8),
82
+ length: view.getUint32(recordOffset + 12),
83
+ };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Read `isFixedPitch` from the `post` table.
91
+ * post layout: version(4) + italicAngle(4) + underlinePosition(2) + underlineThickness(2) = offset 12
92
+ * isFixedPitch is uint32 at offset 12: 0 = proportional, non-zero = monospace.
93
+ */
94
+ async function detectMonospaceFromBlob(fontData: { blob: () => Promise<Blob> }): Promise<boolean> {
95
+ try {
96
+ const blob = await fontData.blob();
97
+ const buffer = await blob.arrayBuffer();
98
+ const view = new DataView(buffer);
99
+
100
+ const post = findSfntTable(view, 'post');
101
+ if (post) {
102
+ const isFixedPitch = view.getUint32(post.offset + 12);
103
+ return isFixedPitch !== 0;
104
+ }
105
+ return false;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // =============================================================================
112
+ // System Font Discovery
113
+ // =============================================================================
114
+
115
+ interface FontDataEntry {
116
+ family: string;
117
+ fullName: string;
118
+ postscriptName: string;
119
+ style: string;
120
+ blob: () => Promise<Blob>;
121
+ }
122
+
123
+ const FONT_CACHE_KEY = 'cyclist-system-fonts';
124
+
125
+ interface FontCacheData {
126
+ fonts: SystemFont[];
127
+ count: number; // number of font families — if it changes, fonts were installed/removed
128
+ }
129
+
130
+ function loadCachedFonts(): SystemFont[] | null {
131
+ try {
132
+ const raw = localStorage.getItem(FONT_CACHE_KEY);
133
+ if (!raw) return null;
134
+ const data: FontCacheData = JSON.parse(raw);
135
+ if (data.fonts?.length > 0) return data.fonts;
136
+ return null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function saveCachedFonts(fonts: SystemFont[], count: number): void {
143
+ try {
144
+ const data: FontCacheData = { fonts, count };
145
+ localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(data));
146
+ } catch {
147
+ // localStorage full or unavailable — not critical
148
+ }
149
+ }
150
+
151
+ let systemFontsPromise: Promise<SystemFont[]> | null = null;
152
+
153
+ async function getSystemFonts(): Promise<SystemFont[]> {
154
+ if (systemFontsPromise) return systemFontsPromise;
155
+
156
+ systemFontsPromise = (async () => {
157
+ if (!('queryLocalFonts' in window)) {
158
+ return [];
159
+ }
160
+
161
+ try {
162
+ const fonts: FontDataEntry[] = await (window as unknown as { queryLocalFonts: () => Promise<FontDataEntry[]> }).queryLocalFonts();
163
+
164
+ // Deduplicate by family name, keep one FontData per family for mono detection
165
+ const familyMap = new Map<string, FontDataEntry>();
166
+ for (const font of fonts) {
167
+ if (!familyMap.has(font.family)) {
168
+ familyMap.set(font.family, font);
169
+ }
170
+ }
171
+
172
+ const familyCount = familyMap.size;
173
+
174
+ // Check localStorage cache — reuse if font count hasn't changed
175
+ const cached = loadCachedFonts();
176
+ if (cached && cached.length > 0) {
177
+ // Load raw cached data to check count
178
+ try {
179
+ const raw = localStorage.getItem(FONT_CACHE_KEY);
180
+ if (raw) {
181
+ const data: FontCacheData = JSON.parse(raw);
182
+ if (data.count === familyCount) {
183
+ return cached;
184
+ }
185
+ }
186
+ } catch {
187
+ // Fall through to re-detect
188
+ }
189
+ }
190
+
191
+ // Detect monospace via post table in parallel
192
+ const entries = Array.from(familyMap.entries());
193
+ const monoResults = await Promise.all(
194
+ entries.map(([, fontData]) => detectMonospaceFromBlob(fontData))
195
+ );
196
+
197
+ const result: SystemFont[] = entries.map(([family], i) => ({
198
+ family,
199
+ isMonospace: monoResults[i],
200
+ }));
201
+
202
+ result.sort((a, b) => a.family.localeCompare(b.family));
203
+ saveCachedFonts(result, familyCount);
204
+ return result;
205
+ } catch {
206
+ // Permission denied or API error
207
+ return [];
208
+ }
209
+ })();
210
+
211
+ return systemFontsPromise;
212
+ }
213
+
214
+ // Prefix for system font values to distinguish from preset IDs
215
+ const SYSTEM_FONT_PREFIX = 'system-font:';
216
+
217
+ // =============================================================================
218
+ // FontPicker Component
219
+ // =============================================================================
220
+
221
+ export function FontPicker({
222
+ type,
223
+ currentFont,
224
+ customFont,
225
+ onSelect,
226
+ onCustomFontChange,
227
+ className = '',
228
+ }: FontPickerProps): React.ReactElement {
229
+ const [customValue, setCustomValue] = useState(customFont || '');
230
+ const [systemFonts, setSystemFonts] = useState<SystemFont[]>([]);
231
+ const [fontsLoaded, setFontsLoaded] = useState(false);
232
+
233
+ const presets = type === 'ui' ? UI_FONT_PRESETS : CODE_FONT_PRESETS;
234
+ const currentPreset = presets.find(p => p.id === currentFont);
235
+
236
+ // Load system fonts eagerly
237
+ useEffect(() => {
238
+ if (!fontsLoaded) {
239
+ getSystemFonts().then(fonts => {
240
+ setSystemFonts(fonts);
241
+ setFontsLoaded(true);
242
+ });
243
+ }
244
+ }, [fontsLoaded]);
245
+
246
+ // Filter system fonts: English-only (Latin names), monospace-only for code
247
+ const filteredSystemFonts = useMemo(() => {
248
+ let fonts = systemFonts;
249
+
250
+ // Filter to fonts with Latin-script names (excludes CJK, Arabic, Devanagari, etc.)
251
+ fonts = fonts.filter(f => /^[\x20-\x7E\u00C0-\u024F]+$/.test(f.family));
252
+
253
+ // For code fonts, only show monospace
254
+ if (type === 'code') {
255
+ fonts = fonts.filter(f => f.isMonospace);
256
+ }
257
+
258
+ return fonts;
259
+ }, [systemFonts, type]);
260
+
261
+ // Non-custom presets for display
262
+ const displayPresets = useMemo(() => {
263
+ return presets.filter(p => !p.isCustom);
264
+ }, [presets]);
265
+
266
+ // Update custom value when prop changes
267
+ useEffect(() => {
268
+ if (customFont !== undefined) {
269
+ setCustomValue(customFont);
270
+ }
271
+ }, [customFont]);
272
+
273
+ // Compute the Select value: for system fonts we encode as "system-font:FamilyName"
274
+ const selectValue = useMemo(() => {
275
+ if (currentFont === 'custom' && customValue) {
276
+ // Check if this matches a system font
277
+ const isSystemFont = systemFonts.some(f => f.family === customValue);
278
+ if (isSystemFont) {
279
+ return `${SYSTEM_FONT_PREFIX}${customValue}`;
280
+ }
281
+ return 'custom';
282
+ }
283
+ return currentFont;
284
+ }, [currentFont, customValue, systemFonts]);
285
+
286
+ const handleValueChange = useCallback(
287
+ (value: string) => {
288
+ if (value.startsWith(SYSTEM_FONT_PREFIX)) {
289
+ const family = value.slice(SYSTEM_FONT_PREFIX.length);
290
+ onSelect('custom', family);
291
+ } else if (value === 'custom') {
292
+ onSelect('custom');
293
+ } else {
294
+ onSelect(value);
295
+ }
296
+ },
297
+ [onSelect]
298
+ );
299
+
300
+ const handleCustomChange = useCallback(
301
+ (e: React.ChangeEvent<HTMLInputElement>) => {
302
+ const value = e.target.value;
303
+ setCustomValue(value);
304
+ onCustomFontChange?.(value);
305
+ if (currentFont === 'custom') {
306
+ onSelect('custom', value);
307
+ }
308
+ },
309
+ [currentFont, onSelect, onCustomFontChange]
310
+ );
311
+
312
+ const hasSystemFonts = systemFonts.length > 0;
313
+ const showCustomInput = currentFont === 'custom' && !hasSystemFonts;
314
+
315
+ return (
316
+ <div className={`font-picker ${className}`}>
317
+ <Select value={selectValue} onValueChange={handleValueChange}>
318
+ <SelectTrigger
319
+ className="font-picker-trigger"
320
+ aria-label={`Select ${type} font`}
321
+ style={{ fontFamily: currentPreset?.fontFamily || (customValue ? `"${customValue}"` : 'inherit') }}
322
+ >
323
+ <SelectValue placeholder="Select font..." />
324
+ </SelectTrigger>
325
+ <SelectContent className="max-h-[300px]">
326
+ {/* Presets section */}
327
+ {displayPresets.length > 0 && (
328
+ <SelectGroup>
329
+ <SelectLabel>Presets</SelectLabel>
330
+ {displayPresets.map((preset) => (
331
+ <SelectItem
332
+ key={preset.id}
333
+ value={preset.id}
334
+ style={{ fontFamily: preset.fontFamily || 'inherit' }}
335
+ >
336
+ <span className="font-picker-item-content">
337
+ <span className="font-name">{preset.name}</span>
338
+ <span className="font-preview" style={{ fontFamily: preset.fontFamily }}>
339
+ Aa
340
+ </span>
341
+ </span>
342
+ </SelectItem>
343
+ ))}
344
+ </SelectGroup>
345
+ )}
346
+
347
+ {/* System fonts section */}
348
+ {hasSystemFonts && filteredSystemFonts.length > 0 && (
349
+ <SelectGroup>
350
+ <SelectLabel>System Fonts ({filteredSystemFonts.length})</SelectLabel>
351
+ {filteredSystemFonts.map((font) => (
352
+ <SelectItem
353
+ key={font.family}
354
+ value={`${SYSTEM_FONT_PREFIX}${font.family}`}
355
+ style={{ fontFamily: `"${font.family}", inherit` }}
356
+ >
357
+ <span className="font-picker-item-content">
358
+ <span className="font-name">{font.family}</span>
359
+ <span className="font-preview" style={{ fontFamily: `"${font.family}"` }}>
360
+ Aa
361
+ </span>
362
+ </span>
363
+ </SelectItem>
364
+ ))}
365
+ </SelectGroup>
366
+ )}
367
+
368
+ {/* Custom option (fallback when no system fonts) */}
369
+ {!hasSystemFonts && fontsLoaded && (
370
+ <SelectGroup>
371
+ <SelectLabel>Custom</SelectLabel>
372
+ <SelectItem value="custom">
373
+ Custom...
374
+ </SelectItem>
375
+ </SelectGroup>
376
+ )}
377
+ </SelectContent>
378
+ </Select>
379
+
380
+ {showCustomInput && (
381
+ <input
382
+ type="text"
383
+ className="font-picker-custom-input"
384
+ value={customValue}
385
+ onChange={handleCustomChange}
386
+ placeholder="Enter font family..."
387
+ aria-label="Custom font family"
388
+ />
389
+ )}
390
+ </div>
391
+ );
392
+ }
393
+
394
+ // =============================================================================
395
+ // FontSizePicker Component
396
+ // =============================================================================
397
+
398
+ const SIZES: FontSize[] = ['xs', 'sm', 'base', 'lg', 'xl'];
399
+
400
+ export function FontSizePicker({
401
+ currentSize,
402
+ onSelect,
403
+ className = '',
404
+ }: FontSizePickerProps): React.ReactElement {
405
+ return (
406
+ <TooltipProvider delayDuration={300}>
407
+ <div className={`font-size-picker ${className}`} role="group" aria-label="Font size">
408
+ {SIZES.map((size) => (
409
+ <Tooltip key={size}>
410
+ <TooltipTrigger asChild>
411
+ <Button
412
+ variant={size === currentSize ? 'secondary' : 'ghost'}
413
+ size="sm"
414
+ className={`font-size-option ${size === currentSize ? 'active' : ''}`}
415
+ data-size={size}
416
+ onClick={() => onSelect(size)}
417
+ aria-pressed={size === currentSize}
418
+ >
419
+ {size.toUpperCase()}
420
+ </Button>
421
+ </TooltipTrigger>
422
+ <TooltipContent>{`${FONT_SIZE_SCALE[size]} (${size})`}</TooltipContent>
423
+ </Tooltip>
424
+ ))}
425
+ </div>
426
+ </TooltipProvider>
427
+ );
428
+ }
429
+
430
+ export default FontPicker;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * FullFileTree - Complete directory tree with changed file highlighting
3
+ *
4
+ * Displays the full project file tree with lazy-loaded directories.
5
+ * Changed files are highlighted with git status colors (created/modified/deleted).
6
+ * Uses /api/files for directory listing and /ws/git for change status.
7
+ */
8
+
9
+ import React, { useState, useCallback, useEffect } from 'react';
10
+ import { Badge } from '@/components/ui/badge';
11
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
12
+ import { ScrollArea } from '@/components/ui/scroll-area';
13
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
14
+ import { useFileBrowser, DirectoryEntry } from '../hooks/useFileBrowser';
15
+ import type { FileStatus } from './FileTree';
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ export interface FullFileTreeProps {
22
+ /** Map of file path → git status for highlighting */
23
+ changedFiles: Map<string, FileStatus>;
24
+ /** Callback when a file is clicked */
25
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
26
+ }
27
+
28
+ // =============================================================================
29
+ // File Item Component
30
+ // =============================================================================
31
+
32
+ function TreeFileItem({
33
+ entry,
34
+ status,
35
+ depth,
36
+ onFileClick,
37
+ }: {
38
+ entry: DirectoryEntry;
39
+ status?: FileStatus;
40
+ depth: number;
41
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
42
+ }): React.ReactElement {
43
+ const statusIcon = status === 'created' ? '+' : status === 'modified' ? '~' : status === 'deleted' ? '-' : null;
44
+
45
+ return (
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <div
49
+ role="treeitem"
50
+ className={`file-item${status ? ` file-${status}` : ''}`}
51
+ style={{ paddingLeft: `${12 + depth * 16}px` }}
52
+ tabIndex={0}
53
+ aria-label={`${entry.name}${status ? `, ${status}` : ''}`}
54
+ onClick={() => onFileClick?.(entry, status)}
55
+ onKeyDown={(e) => {
56
+ if (e.key === 'Enter' || e.key === ' ') {
57
+ e.preventDefault();
58
+ onFileClick?.(entry, status);
59
+ }
60
+ }}
61
+ >
62
+ {statusIcon && (
63
+ <span className={`status-icon status-${status}`} aria-hidden="true">
64
+ {statusIcon}
65
+ </span>
66
+ )}
67
+ <span className={`file-name${status === 'deleted' ? '' : ''}`}>{entry.name}</span>
68
+ </div>
69
+ </TooltipTrigger>
70
+ <TooltipContent>{entry.path}</TooltipContent>
71
+ </Tooltip>
72
+ );
73
+ }
74
+
75
+ // =============================================================================
76
+ // Directory Node Component (recursive)
77
+ // =============================================================================
78
+
79
+ function TreeDirectoryNode({
80
+ entry,
81
+ depth,
82
+ changedFiles,
83
+ onFileClick,
84
+ fetchDirectory,
85
+ cache,
86
+ loading,
87
+ }: {
88
+ entry: DirectoryEntry;
89
+ depth: number;
90
+ changedFiles: Map<string, FileStatus>;
91
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
92
+ fetchDirectory: (path: string) => Promise<void>;
93
+ cache: Record<string, DirectoryEntry[]>;
94
+ loading: Set<string>;
95
+ }): React.ReactElement {
96
+ // Check if this directory contains any changed files
97
+ const hasChanges = Array.from(changedFiles.keys()).some(
98
+ (filePath) => filePath.startsWith(entry.path + '/')
99
+ );
100
+
101
+ const [isOpen, setIsOpen] = useState(hasChanges);
102
+ const children = cache[entry.path];
103
+ const isLoading = loading.has(entry.path);
104
+
105
+ // Auto-fetch children when directory has changes and is opened by default
106
+ useEffect(() => {
107
+ if (hasChanges && !children && !loading.has(entry.path)) {
108
+ fetchDirectory(entry.path);
109
+ }
110
+ }, [hasChanges, children, entry.path, fetchDirectory, loading]);
111
+
112
+ // Auto-open when changes appear in this directory
113
+ useEffect(() => {
114
+ if (hasChanges) {
115
+ setIsOpen(true);
116
+ }
117
+ }, [hasChanges]);
118
+
119
+ const handleToggle = useCallback(() => {
120
+ const willOpen = !isOpen;
121
+ setIsOpen(willOpen);
122
+ if (willOpen && !children) {
123
+ fetchDirectory(entry.path);
124
+ }
125
+ }, [isOpen, children, entry.path, fetchDirectory]);
126
+
127
+ return (
128
+ <Collapsible open={isOpen} onOpenChange={handleToggle}>
129
+ <CollapsibleTrigger asChild>
130
+ <div
131
+ className={`directory-header${hasChanges ? ' has-changes' : ''}`}
132
+ style={{ paddingLeft: `${4 + depth * 16}px` }}
133
+ >
134
+ <span className="directory-toggle">
135
+ <span className="toggle-icon">{isOpen ? '▼' : '▶'}</span>
136
+ </span>
137
+ <span className="directory-name">{entry.name}</span>
138
+ </div>
139
+ </CollapsibleTrigger>
140
+ <CollapsibleContent>
141
+ {isLoading && (
142
+ <div
143
+ className="tree-loading"
144
+ style={{ paddingLeft: `${12 + (depth + 1) * 16}px` }}
145
+ >
146
+ Loading...
147
+ </div>
148
+ )}
149
+ {children?.map((child) =>
150
+ child.type === 'directory' ? (
151
+ <TreeDirectoryNode
152
+ key={child.path}
153
+ entry={child}
154
+ depth={depth + 1}
155
+ changedFiles={changedFiles}
156
+ onFileClick={onFileClick}
157
+ fetchDirectory={fetchDirectory}
158
+ cache={cache}
159
+ loading={loading}
160
+ />
161
+ ) : (
162
+ <TreeFileItem
163
+ key={child.path}
164
+ entry={child}
165
+ status={changedFiles.get(child.path)}
166
+ depth={depth + 1}
167
+ onFileClick={onFileClick}
168
+ />
169
+ )
170
+ )}
171
+ </CollapsibleContent>
172
+ </Collapsible>
173
+ );
174
+ }
175
+
176
+ // =============================================================================
177
+ // FullFileTree Component
178
+ // =============================================================================
179
+
180
+ export function FullFileTree({ changedFiles, onFileClick }: FullFileTreeProps): React.ReactElement {
181
+ const { cache, loading, error, fetchDirectory } = useFileBrowser();
182
+
183
+ // Load root directory on mount
184
+ useEffect(() => {
185
+ fetchDirectory('');
186
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
187
+
188
+ const rootEntries = cache['__root__'];
189
+ const changedCount = changedFiles.size;
190
+
191
+ return (
192
+ <TooltipProvider delayDuration={300}>
193
+ <div role="tree" aria-label="Project files" className="filetree full-filetree">
194
+ {changedCount > 0 && (
195
+ <Badge
196
+ variant="secondary"
197
+ data-testid="file-count-badge"
198
+ className="file-count-badge"
199
+ aria-label={`${changedCount} files changed`}
200
+ >
201
+ {changedCount}
202
+ </Badge>
203
+ )}
204
+ <ScrollArea className="filetree-scroll">
205
+ {error && <div className="tree-error">{error}</div>}
206
+ {!rootEntries && !error && (
207
+ <div className="tree-loading">Loading project files...</div>
208
+ )}
209
+ {rootEntries?.map((entry) =>
210
+ entry.type === 'directory' ? (
211
+ <TreeDirectoryNode
212
+ key={entry.path}
213
+ entry={entry}
214
+ depth={0}
215
+ changedFiles={changedFiles}
216
+ onFileClick={onFileClick}
217
+ fetchDirectory={fetchDirectory}
218
+ cache={cache}
219
+ loading={loading}
220
+ />
221
+ ) : (
222
+ <TreeFileItem
223
+ key={entry.path}
224
+ entry={entry}
225
+ status={changedFiles.get(entry.path)}
226
+ depth={0}
227
+ onFileClick={onFileClick}
228
+ />
229
+ )
230
+ )}
231
+ </ScrollArea>
232
+ </div>
233
+ </TooltipProvider>
234
+ );
235
+ }
236
+
237
+ export default FullFileTree;