@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,749 @@
1
+ /**
2
+ * DockviewWorkspace - Dockview-based panel management for Cyclist
3
+ *
4
+ * Story MSSCI-14001: Replace DockingWorkspace with Dockview
5
+ * Epic: epic-76 (Dockview Panel Migration)
6
+ *
7
+ * Features:
8
+ * - Three-region layout (left sidebar, center sacred, right sidebar)
9
+ * - Message view is sacred (fixed center, cannot be closed or moved)
10
+ * - Tabbed panels in sidebars with drag-and-drop
11
+ * - Responsive breakpoints (auto-collapse at <1024px)
12
+ * - Layout persistence via native Dockview toJSON/fromJSON
13
+ * - Theme integration via CSS custom properties
14
+ */
15
+
16
+ import React, { useEffect, useRef, useCallback, useState } from 'react';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
19
+ import {
20
+ DockviewReact,
21
+ DockviewReadyEvent,
22
+ IDockviewPanelProps,
23
+ DockviewApi,
24
+ IDockviewPanel,
25
+ SerializedDockview,
26
+ DockviewDefaultTab,
27
+ } from 'dockview-react';
28
+ import 'dockview-react/dist/styles/dockview.css';
29
+ import { ErrorBoundary } from './ErrorBoundary';
30
+ import { panelRegistry, type PanelComponent } from './panel-registry';
31
+ import { useResponsiveLayout, MIN_DIMENSIONS, SIDEBAR_WIDTHS } from '../hooks/useResponsiveLayout';
32
+ import { useFocusPanel } from '../hooks/useFocusPanel.js';
33
+ import '../styles/dockview-theme.css';
34
+
35
+ // =============================================================================
36
+ // Panel Inventory - All available panels in Cyclist
37
+ // =============================================================================
38
+
39
+ export const PANEL_INVENTORY = {
40
+ // Left sidebar panels
41
+ GIT: 'git',
42
+ DIFFS: 'diffs',
43
+ DEBUG: 'debug',
44
+ AUDIT_LOG: 'audit-log',
45
+ // Center panel (sacred)
46
+ MESSAGE: 'message',
47
+ // Right sidebar panels
48
+ SPRINT: 'sprint',
49
+ WORKFLOW: 'workflow',
50
+ AC: 'ac',
51
+ TODO: 'todo',
52
+ BACKGROUND: 'background',
53
+ SETTINGS: 'settings',
54
+ PROGRESS: 'progress',
55
+ TANDEM: 'tandem',
56
+ } as const;
57
+
58
+ export type PanelId = typeof PANEL_INVENTORY[keyof typeof PANEL_INVENTORY];
59
+
60
+ // =============================================================================
61
+ // Panel Component Registry
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Register a panel component by ID
66
+ */
67
+ export function registerPanelComponent(id: string, component: PanelComponent): void {
68
+ panelRegistry.set(id, component);
69
+ }
70
+
71
+ // =============================================================================
72
+ // Global API Reference
73
+ // =============================================================================
74
+
75
+ let dockviewApiRef: DockviewApi | null = null;
76
+
77
+ /**
78
+ * Get the Dockview API instance for external access
79
+ */
80
+ export function getDockviewApi(): DockviewApi | null {
81
+ return dockviewApiRef;
82
+ }
83
+
84
+ // Panel group definitions (needed for restore logic)
85
+ // Exported so layout persistence can merge missing panels
86
+ export const LEFT_SIDEBAR_PANELS = [PANEL_INVENTORY.GIT, PANEL_INVENTORY.DIFFS, PANEL_INVENTORY.DEBUG, PANEL_INVENTORY.AUDIT_LOG] as const;
87
+ export const RIGHT_SIDEBAR_PANELS = [
88
+ PANEL_INVENTORY.SPRINT,
89
+ PANEL_INVENTORY.WORKFLOW,
90
+ PANEL_INVENTORY.AC,
91
+ PANEL_INVENTORY.TODO,
92
+ PANEL_INVENTORY.BACKGROUND,
93
+ PANEL_INVENTORY.SETTINGS,
94
+ PANEL_INVENTORY.TANDEM,
95
+ ] as const;
96
+
97
+ // Title Case display names for tab headers (AC4: Story 75-5)
98
+ const PANEL_TITLES: Record<string, string> = {
99
+ git: 'Git',
100
+ diffs: 'Diffs',
101
+ debug: 'Debug',
102
+ 'audit-log': 'Audit Log',
103
+ message: 'Message',
104
+ sprint: 'Sprint',
105
+ workflow: 'Workflow',
106
+ ac: 'AC',
107
+ todo: 'Todo',
108
+ background: 'Subagents',
109
+ settings: 'Settings',
110
+ progress: 'Progress',
111
+ tandem: 'Tandem',
112
+ };
113
+
114
+ // Track closed panels for restoration
115
+ const closedPanels: Set<string> = new Set();
116
+
117
+ /**
118
+ * Get list of closed panels that can be restored
119
+ */
120
+ export function getClosedPanels(): string[] {
121
+ return Array.from(closedPanels);
122
+ }
123
+
124
+ /**
125
+ * Restore a previously closed panel
126
+ */
127
+ export function restorePanel(panelId: string): boolean {
128
+ const api = dockviewApiRef;
129
+ if (!api) return false;
130
+
131
+ // If panel already exists in Dockview, nothing to restore
132
+ if (api.getPanel(panelId)) return false;
133
+
134
+ // Determine which group to add it to
135
+ const isLeftPanel = LEFT_SIDEBAR_PANELS.includes(panelId as any);
136
+ const isRightPanel = RIGHT_SIDEBAR_PANELS.includes(panelId as any);
137
+
138
+ // Find a reference panel in the appropriate group
139
+ let referencePanel: IDockviewPanel | undefined;
140
+
141
+ if (isLeftPanel) {
142
+ for (const id of LEFT_SIDEBAR_PANELS) {
143
+ const panel = api.getPanel(id);
144
+ if (panel) {
145
+ referencePanel = panel;
146
+ break;
147
+ }
148
+ }
149
+ } else if (isRightPanel) {
150
+ for (const id of RIGHT_SIDEBAR_PANELS) {
151
+ const panel = api.getPanel(id);
152
+ if (panel) {
153
+ referencePanel = panel;
154
+ break;
155
+ }
156
+ }
157
+ }
158
+
159
+ // Add the panel back
160
+ api.addPanel({
161
+ id: panelId,
162
+ component: 'PanelAdapter',
163
+ params: { panelId },
164
+ position: referencePanel ? { referencePanel: referencePanel.id } : undefined,
165
+ title: PANEL_TITLES[panelId] || panelId,
166
+ });
167
+
168
+ closedPanels.delete(panelId);
169
+ return true;
170
+ }
171
+
172
+ // =============================================================================
173
+ // Panel Adapter - Wraps existing panels for Dockview
174
+ // =============================================================================
175
+
176
+ interface PanelAdapterParams {
177
+ panelId: string;
178
+ }
179
+
180
+ export function PanelAdapter({ params }: IDockviewPanelProps<PanelAdapterParams>): React.ReactElement | null {
181
+ const Component = panelRegistry.get(params.panelId);
182
+
183
+ if (!Component) {
184
+ console.warn(`[DockviewWorkspace] No component registered for panel: ${params.panelId}`);
185
+ return null;
186
+ }
187
+
188
+ return (
189
+ <div data-testid={`panel-${params.panelId}`} className="dockview-panel-content">
190
+ <ErrorBoundary panelName={params.panelId}>
191
+ <div className="error-boundary-wrapper">
192
+ <Component />
193
+ </div>
194
+ </ErrorBoundary>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ // =============================================================================
200
+ // Types for Layout Persistence
201
+ // =============================================================================
202
+
203
+ // Re-export SerializedDockview for external use
204
+ export type { SerializedDockview };
205
+
206
+ /**
207
+ * @deprecated Use SerializedDockview from dockview-react instead
208
+ * Kept for backward compatibility during migration
209
+ */
210
+ export interface WorkspaceLayoutConfig {
211
+ leftSidebar: {
212
+ panels: string[];
213
+ width: number;
214
+ collapsed: boolean;
215
+ activePanel?: string;
216
+ };
217
+ center: {
218
+ panels: string[];
219
+ locked: boolean;
220
+ };
221
+ rightSidebar: {
222
+ panels: string[];
223
+ width: number;
224
+ collapsed: boolean;
225
+ activePanel?: string;
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Create default workspace layout using native Dockview format
231
+ * This builds the initial three-region layout programmatically
232
+ */
233
+ export function createDefaultDockviewLayout(): SerializedDockview {
234
+ // Build the default layout structure that matches what toJSON() produces
235
+ // This creates: [left sidebar] | [center message] | [right sidebar]
236
+ return {
237
+ grid: {
238
+ root: {
239
+ type: 'branch',
240
+ data: [
241
+ // Left sidebar group
242
+ {
243
+ type: 'leaf',
244
+ data: {
245
+ views: LEFT_SIDEBAR_PANELS.map(id => id),
246
+ activeView: LEFT_SIDEBAR_PANELS[0],
247
+ id: 'left-sidebar',
248
+ },
249
+ size: SIDEBAR_WIDTHS.medium,
250
+ },
251
+ // Center (message) group
252
+ {
253
+ type: 'leaf',
254
+ data: {
255
+ views: [PANEL_INVENTORY.MESSAGE],
256
+ activeView: PANEL_INVENTORY.MESSAGE,
257
+ id: 'center',
258
+ hideHeader: true,
259
+ },
260
+ size: 600, // Center takes remaining space
261
+ },
262
+ // Right sidebar group
263
+ {
264
+ type: 'leaf',
265
+ data: {
266
+ views: [...RIGHT_SIDEBAR_PANELS],
267
+ activeView: RIGHT_SIDEBAR_PANELS[0],
268
+ id: 'right-sidebar',
269
+ },
270
+ size: SIDEBAR_WIDTHS.medium,
271
+ },
272
+ ],
273
+ size: 800, // Will be overridden by actual container height
274
+ },
275
+ width: 1200,
276
+ height: 800,
277
+ orientation: 'HORIZONTAL',
278
+ },
279
+ panels: {
280
+ // Left sidebar panels
281
+ ...Object.fromEntries(LEFT_SIDEBAR_PANELS.map(id => [id, {
282
+ id,
283
+ contentComponent: 'PanelAdapter',
284
+ title: PANEL_TITLES[id] || id,
285
+ params: { panelId: id },
286
+ }])),
287
+ // Center panel
288
+ [PANEL_INVENTORY.MESSAGE]: {
289
+ id: PANEL_INVENTORY.MESSAGE,
290
+ contentComponent: 'PanelAdapter',
291
+ title: PANEL_TITLES[PANEL_INVENTORY.MESSAGE],
292
+ params: { panelId: PANEL_INVENTORY.MESSAGE },
293
+ },
294
+ // Right sidebar panels
295
+ ...Object.fromEntries(RIGHT_SIDEBAR_PANELS.map(id => [id, {
296
+ id,
297
+ contentComponent: 'PanelAdapter',
298
+ title: PANEL_TITLES[id] || id,
299
+ params: { panelId: id },
300
+ }])),
301
+ },
302
+ activeGroup: 'center',
303
+ };
304
+ }
305
+
306
+ /**
307
+ * @deprecated Use createDefaultDockviewLayout instead
308
+ * Kept for backward compatibility
309
+ */
310
+ export function createWorkspaceLayout(): WorkspaceLayoutConfig {
311
+ return {
312
+ leftSidebar: {
313
+ panels: [...LEFT_SIDEBAR_PANELS],
314
+ width: SIDEBAR_WIDTHS.medium,
315
+ collapsed: false,
316
+ },
317
+ center: {
318
+ panels: [PANEL_INVENTORY.MESSAGE],
319
+ locked: true,
320
+ },
321
+ rightSidebar: {
322
+ panels: [...RIGHT_SIDEBAR_PANELS],
323
+ width: SIDEBAR_WIDTHS.medium,
324
+ collapsed: false,
325
+ },
326
+ };
327
+ }
328
+
329
+ // =============================================================================
330
+ // Layout Migration (MSSCI-14188)
331
+ // =============================================================================
332
+
333
+ interface SimplifiedLayoutPanel {
334
+ id: string;
335
+ position?: string;
336
+ }
337
+
338
+ interface SimplifiedLayout {
339
+ panels: SimplifiedLayoutPanel[];
340
+ }
341
+
342
+ /**
343
+ * Migrate old layout format that had "progress" panel to new format with
344
+ * separate workflow, ac, and todo panels.
345
+ *
346
+ * Story: MSSCI-14188 - Split Progress panel into Workflow, AC, and Todo panels
347
+ */
348
+ export function migrateLayout(layout: SimplifiedLayout | null | undefined): SimplifiedLayout {
349
+ // Handle null/undefined gracefully
350
+ if (!layout || !layout.panels) {
351
+ return { panels: [] };
352
+ }
353
+
354
+ // Check if layout already has new panels (no migration needed)
355
+ const hasNewPanels = layout.panels.some(
356
+ (p) => p.id === 'workflow' || p.id === 'ac' || p.id === 'todo'
357
+ );
358
+ if (hasNewPanels) {
359
+ // Already migrated, just filter out any stale 'progress' panel
360
+ return {
361
+ panels: layout.panels.filter((p) => p.id !== 'progress'),
362
+ };
363
+ }
364
+
365
+ // Check if layout has old 'progress' panel that needs migration
366
+ const progressIndex = layout.panels.findIndex((p) => p.id === 'progress');
367
+ if (progressIndex === -1) {
368
+ // No progress panel to migrate
369
+ return layout;
370
+ }
371
+
372
+ // Get the position of the old progress panel
373
+ const progressPanel = layout.panels[progressIndex];
374
+ const position = progressPanel.position || 'right';
375
+
376
+ // Build new panels array with progress replaced by workflow, ac, todo
377
+ const newPanels: SimplifiedLayoutPanel[] = [];
378
+
379
+ for (let i = 0; i < layout.panels.length; i++) {
380
+ if (i === progressIndex) {
381
+ // Replace progress with three new panels at the same position
382
+ newPanels.push({ id: 'workflow', position });
383
+ newPanels.push({ id: 'ac', position });
384
+ newPanels.push({ id: 'todo', position });
385
+ } else {
386
+ newPanels.push(layout.panels[i]);
387
+ }
388
+ }
389
+
390
+ return { panels: newPanels };
391
+ }
392
+
393
+ // =============================================================================
394
+ // DockviewWorkspace Component
395
+ // =============================================================================
396
+
397
+ export interface DockviewWorkspaceProps {
398
+ /** Native Dockview serialized layout (preferred) */
399
+ initialLayout?: SerializedDockview;
400
+ /** Callback when layout changes - receives native Dockview format */
401
+ onLayoutChange?: (layout: SerializedDockview) => void;
402
+ }
403
+
404
+ export function DockviewWorkspace({
405
+ initialLayout,
406
+ onLayoutChange,
407
+ }: DockviewWorkspaceProps): React.ReactElement {
408
+ const apiRef = useRef<DockviewApi | null>(null);
409
+ const [dockviewApi, setDockviewApi] = useState<DockviewApi | null>(null);
410
+ const { isSmall, isBelowMinimum, sidebarWidth } = useResponsiveLayout();
411
+ const [isReady, setIsReady] = useState(false);
412
+ const [closedPanelsList, setClosedPanelsList] = useState<string[]>([]);
413
+ const [showRestoreMenu, setShowRestoreMenu] = useState(false);
414
+ const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
415
+ // Panel focus mode — stash/restore layout on /bc CLI events
416
+ useFocusPanel(dockviewApi);
417
+
418
+ // Track if responsive effect should apply - skip on initial load to respect saved collapsed state
419
+ const hasAppliedInitialLayout = useRef(false);
420
+ const previousIsSmall = useRef<boolean | null>(null);
421
+
422
+ // Update closed panels list when panels change
423
+ const updateClosedPanelsList = useCallback(() => {
424
+ setClosedPanelsList(Array.from(closedPanels));
425
+ }, []);
426
+
427
+ // Handle Dockview ready event
428
+ const onReady = useCallback((event: DockviewReadyEvent) => {
429
+ const api = event.api;
430
+ apiRef.current = api;
431
+ dockviewApiRef = api;
432
+ setDockviewApi(api);
433
+
434
+ // Use native fromJSON if we have a saved layout, otherwise build default
435
+ if (initialLayout && initialLayout.grid && initialLayout.panels) {
436
+ // Restore from native Dockview serialized format
437
+ try {
438
+ api.fromJSON(initialLayout);
439
+
440
+ // After restoring, lock the message panel's group and hide its tab bar
441
+ const messagePanel = api.getPanel(PANEL_INVENTORY.MESSAGE);
442
+ if (messagePanel?.group) {
443
+ messagePanel.group.locked = 'no-drop-target';
444
+ messagePanel.group.model.header.hidden = true;
445
+ }
446
+
447
+ setIsReady(true);
448
+ return;
449
+ } catch (err) {
450
+ console.warn('[DockviewWorkspace] Failed to restore layout from JSON, building default:', err);
451
+ // Fall through to build default layout
452
+ }
453
+ }
454
+
455
+ // Build default layout programmatically (for first-time users or failed restore)
456
+ // Add first panel to left sidebar (creates the first group)
457
+ const leftFirstPanel = api.addPanel({
458
+ id: LEFT_SIDEBAR_PANELS[0],
459
+ component: 'PanelAdapter',
460
+ params: { panelId: LEFT_SIDEBAR_PANELS[0] },
461
+ title: PANEL_TITLES[LEFT_SIDEBAR_PANELS[0]],
462
+ });
463
+
464
+ // Add remaining left sidebar panels to the same group
465
+ for (let i = 1; i < LEFT_SIDEBAR_PANELS.length; i++) {
466
+ const panelId = LEFT_SIDEBAR_PANELS[i];
467
+ if (panelRegistry.has(panelId)) {
468
+ api.addPanel({
469
+ id: panelId,
470
+ component: 'PanelAdapter',
471
+ params: { panelId },
472
+ position: { referencePanel: leftFirstPanel.id },
473
+ title: PANEL_TITLES[panelId],
474
+ });
475
+ }
476
+ }
477
+
478
+ // Add message panel to center (creates new group to the right)
479
+ const messagePanel = api.addPanel({
480
+ id: PANEL_INVENTORY.MESSAGE,
481
+ component: 'PanelAdapter',
482
+ params: { panelId: PANEL_INVENTORY.MESSAGE },
483
+ position: { referencePanel: leftFirstPanel.id, direction: 'right' },
484
+ title: PANEL_TITLES[PANEL_INVENTORY.MESSAGE],
485
+ });
486
+
487
+ // Add first right sidebar panel (creates new group to the right of center)
488
+ const rightFirstPanel = api.addPanel({
489
+ id: RIGHT_SIDEBAR_PANELS[0],
490
+ component: 'PanelAdapter',
491
+ params: { panelId: RIGHT_SIDEBAR_PANELS[0] },
492
+ position: { referencePanel: messagePanel.id, direction: 'right' },
493
+ title: PANEL_TITLES[RIGHT_SIDEBAR_PANELS[0]],
494
+ });
495
+
496
+ // Add remaining right sidebar panels to the same group
497
+ for (let i = 1; i < RIGHT_SIDEBAR_PANELS.length; i++) {
498
+ const panelId = RIGHT_SIDEBAR_PANELS[i];
499
+ if (panelRegistry.has(panelId)) {
500
+ api.addPanel({
501
+ id: panelId,
502
+ component: 'PanelAdapter',
503
+ params: { panelId },
504
+ position: { referencePanel: rightFirstPanel.id },
505
+ title: PANEL_TITLES[panelId],
506
+ });
507
+ }
508
+ }
509
+
510
+ // Lock the center group - MessagePanel cannot be closed or moved
511
+ // Hide the tab bar so users can't accidentally close the message tab
512
+ if (messagePanel?.group) {
513
+ messagePanel.group.locked = 'no-drop-target';
514
+ messagePanel.group.model.header.hidden = true;
515
+ }
516
+
517
+ // Set initial sidebar sizes
518
+ const leftGroup = leftFirstPanel.group;
519
+ const rightGroup = rightFirstPanel.group;
520
+
521
+ if (leftGroup) {
522
+ leftGroup.api.setSize({ width: SIDEBAR_WIDTHS.medium });
523
+ }
524
+ if (rightGroup) {
525
+ rightGroup.api.setSize({ width: SIDEBAR_WIDTHS.medium });
526
+ }
527
+
528
+ setIsReady(true);
529
+ }, [initialLayout]);
530
+
531
+ // Handle layout changes for persistence using native toJSON
532
+ const handleLayoutChange = useCallback(() => {
533
+ const api = apiRef.current;
534
+ if (!api || !onLayoutChange) return;
535
+
536
+ // Never save empty layouts — prevents corruption loop
537
+ if (api.panels.length === 0) return;
538
+
539
+ // Debounce saves
540
+ if (saveTimeoutRef.current) {
541
+ clearTimeout(saveTimeoutRef.current);
542
+ }
543
+
544
+ saveTimeoutRef.current = setTimeout(() => {
545
+ // Use native Dockview toJSON for complete layout serialization
546
+ const serializedLayout = api.toJSON();
547
+ // Double-check: don't persist if serialization produced empty panels
548
+ if (serializedLayout.panels && Object.keys(serializedLayout.panels).length > 0) {
549
+ onLayoutChange(serializedLayout);
550
+ }
551
+ }, 300);
552
+ }, [onLayoutChange]);
553
+
554
+ // Subscribe to layout changes and track closed panels
555
+ useEffect(() => {
556
+ const api = apiRef.current;
557
+ if (!api || !isReady) return;
558
+
559
+ const disposables = [
560
+ api.onDidLayoutChange(() => handleLayoutChange()),
561
+ api.onDidAddPanel((e) => {
562
+ // Panel restored, remove from closed set
563
+ const panelId = e?.panel?.id;
564
+ if (panelId) {
565
+ closedPanels.delete(panelId);
566
+ }
567
+ updateClosedPanelsList();
568
+ handleLayoutChange();
569
+ }),
570
+ api.onDidRemovePanel((e) => {
571
+ // Track closed panels for restoration
572
+ const panelId = e?.panel?.id;
573
+ if (panelId) {
574
+ closedPanels.add(panelId);
575
+ updateClosedPanelsList();
576
+ }
577
+ handleLayoutChange();
578
+ }),
579
+ ];
580
+
581
+ return () => {
582
+ disposables.forEach(d => d.dispose());
583
+ };
584
+ }, [isReady, handleLayoutChange]);
585
+
586
+ // Handle responsive breakpoints - only on viewport changes, not initial load
587
+ useEffect(() => {
588
+ const api = apiRef.current;
589
+ if (!api || !isReady) return;
590
+
591
+ // On first run after layout is ready, just record current state without changing anything
592
+ // This preserves the saved collapsed state from initialLayout
593
+ if (!hasAppliedInitialLayout.current) {
594
+ hasAppliedInitialLayout.current = true;
595
+ previousIsSmall.current = isSmall;
596
+ return;
597
+ }
598
+
599
+ // Only apply responsive changes when isSmall actually changes
600
+ if (previousIsSmall.current === isSmall) {
601
+ return;
602
+ }
603
+ previousIsSmall.current = isSmall;
604
+
605
+ // Find groups by their panels (groups don't have fixed names)
606
+ const gitPanel = api.getPanel(PANEL_INVENTORY.GIT);
607
+ const sprintPanel = api.getPanel(PANEL_INVENTORY.SPRINT);
608
+ const leftGroup = gitPanel?.group;
609
+ const rightGroup = sprintPanel?.group;
610
+
611
+ if (isSmall) {
612
+ // Collapse sidebars at small viewport
613
+ leftGroup?.api.setSize({ width: 0 });
614
+ rightGroup?.api.setSize({ width: 0 });
615
+ } else {
616
+ // Restore sidebars to configured width when viewport expands
617
+ leftGroup?.api.setSize({ width: sidebarWidth });
618
+ rightGroup?.api.setSize({ width: sidebarWidth });
619
+ }
620
+ }, [isSmall, sidebarWidth, isReady]);
621
+
622
+ // Handle restoring a closed panel
623
+ const handleRestorePanel = useCallback((panelId: string) => {
624
+ restorePanel(panelId);
625
+ setShowRestoreMenu(false);
626
+ }, []);
627
+
628
+ // Cleanup on unmount
629
+ useEffect(() => {
630
+ return () => {
631
+ if (saveTimeoutRef.current) {
632
+ clearTimeout(saveTimeoutRef.current);
633
+ }
634
+ dockviewApiRef = null;
635
+ };
636
+ }, []);
637
+
638
+ // Listen for panel toggle commands via WebSocket (View menu integration)
639
+ useEffect(() => {
640
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
641
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws/settings`);
642
+
643
+ ws.onmessage = (event) => {
644
+ try {
645
+ const data = JSON.parse(event.data);
646
+ if (data.type === 'panel:toggle' && data.panelId) {
647
+ const api = dockviewApiRef;
648
+ if (!api) return;
649
+ const existing = api.getPanel(data.panelId);
650
+ if (existing) {
651
+ existing.api.close();
652
+ } else {
653
+ restorePanel(data.panelId);
654
+ }
655
+ }
656
+ } catch {
657
+ // ignore parse errors
658
+ }
659
+ };
660
+
661
+ return () => ws.close();
662
+ }, []);
663
+
664
+ // Component map for Dockview
665
+ const components = {
666
+ PanelAdapter,
667
+ };
668
+
669
+ // Panel display names for the restore menu
670
+ const panelDisplayNames: Record<string, string> = {
671
+ git: 'Git',
672
+ diffs: 'Diffs',
673
+ debug: 'Debug',
674
+ 'audit-log': 'Audit Log',
675
+ sprint: 'Sprint',
676
+ workflow: 'Workflow',
677
+ ac: 'AC',
678
+ todo: 'Todo',
679
+ background: 'Subagents',
680
+ hotspots: 'Hotspots',
681
+ settings: 'Settings',
682
+ };
683
+
684
+ return (
685
+ <div className="cyclist-dockview" data-dockview-group="container">
686
+ {/* Minimum dimension warning */}
687
+ {isBelowMinimum && (
688
+ <div
689
+ data-testid="min-dimension-warning"
690
+ className="min-dimension-warning"
691
+ role="alert"
692
+ >
693
+ Window is below minimum size ({MIN_DIMENSIONS.width}x{MIN_DIMENSIONS.height})
694
+ </div>
695
+ )}
696
+
697
+ {/* Panel restore button - shown when panels are closed */}
698
+ {closedPanelsList.length > 0 && (
699
+ <div className="panel-restore-container">
700
+ <TooltipProvider delayDuration={300}>
701
+ <Tooltip>
702
+ <TooltipTrigger asChild>
703
+ <Button
704
+ variant="outline"
705
+ size="sm"
706
+ className="panel-restore-button"
707
+ onClick={() => setShowRestoreMenu(!showRestoreMenu)}
708
+ aria-expanded={showRestoreMenu}
709
+ aria-haspopup="menu"
710
+ >
711
+ <span className="panel-restore-icon">+</span>
712
+ <span className="panel-restore-count">{closedPanelsList.length}</span>
713
+ </Button>
714
+ </TooltipTrigger>
715
+ <TooltipContent>Restore closed panels</TooltipContent>
716
+ </Tooltip>
717
+ </TooltipProvider>
718
+
719
+ {showRestoreMenu && (
720
+ <div className="panel-restore-menu" role="menu">
721
+ <div className="panel-restore-header">Restore Panel</div>
722
+ {closedPanelsList.map((panelId) => (
723
+ <Button
724
+ variant="ghost"
725
+ key={panelId}
726
+ className="panel-restore-item"
727
+ onClick={() => handleRestorePanel(panelId)}
728
+ role="menuitem"
729
+ >
730
+ {panelDisplayNames[panelId] || panelId}
731
+ </Button>
732
+ ))}
733
+ </div>
734
+ )}
735
+ </div>
736
+ )}
737
+
738
+ <DockviewReact
739
+ className="dockview-container"
740
+ onReady={onReady}
741
+ components={components}
742
+ defaultTabComponent={(props) => <DockviewDefaultTab {...props} hideClose />}
743
+ watermarkComponent={() => null}
744
+ />
745
+ </div>
746
+ );
747
+ }
748
+
749
+ export default DockviewWorkspace;