@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,723 @@
1
+ /**
2
+ * SprintPanel - Display current story/sprint info
3
+ *
4
+ * Story MSSCI-12717 - React Migration
5
+ * Story MSSCI-14189 - Enhanced Sprint Panel with story management and epic actions
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
9
+ import { Check, Copy, Loader, Circle, AlertTriangle } from 'lucide-react';
10
+ import { Button } from '@/components/ui/button';
11
+ import { Badge } from '@/components/ui/badge';
12
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
13
+ import { Skeleton } from '@/components/ui/skeleton';
14
+ import { Separator } from '@/components/ui/separator';
15
+ import { useStory } from '../../hooks/useStory';
16
+ import { useSprint, type SprintStory, type SprintEpic, type FutureEpic, type FutureEpicChild } from '../../hooks/useSprint';
17
+
18
+ // =============================================================================
19
+ // Original SprintPanel (unchanged)
20
+ // =============================================================================
21
+
22
+ export function SprintPanel(): React.ReactElement {
23
+ const { story, isLoading, error } = useStory();
24
+
25
+ if (isLoading) {
26
+ return (
27
+ <div className="sprint-panel loading" data-testid="sprint-panel">
28
+ <div className="space-y-2 p-2">
29
+ <Skeleton className="h-4 w-24" />
30
+ <Skeleton className="h-5 w-48" />
31
+ <Skeleton className="h-4 w-32" />
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ if (error) {
38
+ return (
39
+ <div className="sprint-panel error" data-testid="sprint-panel">
40
+ <div className="error-message">{error.message}</div>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ if (!story) {
46
+ return (
47
+ <div className="sprint-panel empty" data-testid="sprint-panel">
48
+ <div className="placeholder">No active story</div>
49
+ <p className="hint">Use /sprint work to start a story</p>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ return (
55
+ <div className="sprint-panel" data-testid="sprint-panel">
56
+ <div className="story-header">
57
+ <span className="story-id">{story.id}</span>
58
+ {story.status && <span className="story-status">{story.status}</span>}
59
+ </div>
60
+ <h3 className="story-title">{story.title}</h3>
61
+ {story.phase && (
62
+ <div className="story-phase">
63
+ Phase: <strong>{story.phase}</strong>
64
+ </div>
65
+ )}
66
+ {story.workflow && (
67
+ <div className="story-workflow">
68
+ Workflow: {story.workflow}
69
+ </div>
70
+ )}
71
+ {story.points && (
72
+ <div className="story-points">
73
+ Points: {story.points}
74
+ </div>
75
+ )}
76
+ {story.epic && (
77
+ <div className="story-epic">
78
+ Epic: {story.epic}
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ // =============================================================================
86
+ // Enhanced Sprint Panel (MSSCI-14189)
87
+ // =============================================================================
88
+
89
+ /**
90
+ * Format email to short display name: "keith.avery@..." -> "K. Avery"
91
+ */
92
+ function formatAssignee(email: string | null | undefined): string | null {
93
+ if (!email) return null;
94
+ const local = email.split('@')[0];
95
+ const parts = local.split('.');
96
+ if (parts.length >= 2) {
97
+ const first = parts[0].charAt(0).toUpperCase();
98
+ const last = parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1);
99
+ return `${first}. ${last}`;
100
+ }
101
+ return local;
102
+ }
103
+
104
+ /**
105
+ * Calculate epic progress (done points / total points)
106
+ */
107
+ function calculateEpicProgress(epic: SprintEpic): { done: number; total: number } {
108
+ const total = epic.stories.reduce((sum, s) => sum + s.points, 0);
109
+ const done = epic.stories
110
+ .filter((s) => s.status === 'done')
111
+ .reduce((sum, s) => sum + s.points, 0);
112
+ return { done, total };
113
+ }
114
+
115
+ /**
116
+ * Check if epic is fully completed (all stories done)
117
+ */
118
+ function isEpicCompleted(epic: SprintEpic): boolean {
119
+ return epic.stories.length > 0 && epic.stories.every((s) => s.status === 'done' || s.status === 'cancelled');
120
+ }
121
+
122
+ /**
123
+ * Get status badge content and class for a story status
124
+ */
125
+ function getStatusBadgeInfo(status: SprintStory['status']): { icon: React.ReactElement; className: string } {
126
+ const size = 12;
127
+ switch (status) {
128
+ case 'done':
129
+ return { icon: <Check size={size} />, className: 'status-done' };
130
+ case 'in_progress':
131
+ return { icon: <Loader size={size} />, className: 'status-in-progress' };
132
+ case 'blocked':
133
+ return { icon: <AlertTriangle size={size} />, className: 'status-blocked' };
134
+ case 'backlog':
135
+ default:
136
+ return { icon: <Circle size={size} />, className: 'status-backlog' };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Build Jira ticket URL
142
+ */
143
+ function getJiraUrl(jiraKey: string): string {
144
+ return `https://1898andco.atlassian.net/browse/${jiraKey}`;
145
+ }
146
+
147
+ /**
148
+ * Context indicator component for epics and stories
149
+ */
150
+ function ContextIndicator({
151
+ hasContext,
152
+ testIdPrefix,
153
+ id,
154
+ }: {
155
+ hasContext: boolean;
156
+ testIdPrefix: 'epic' | 'story';
157
+ id: string;
158
+ }): React.ReactElement {
159
+ const tooltipText = hasContext ? 'Context file exists' : 'No context file';
160
+ return (
161
+ <Tooltip>
162
+ <TooltipTrigger asChild>
163
+ <span
164
+ className={`context-indicator ${hasContext ? 'has-context' : 'no-context'}`}
165
+ data-testid={`${testIdPrefix}-context-indicator-${id}`}
166
+ data-has-context={String(hasContext)}
167
+ title={tooltipText}
168
+ >
169
+ {hasContext ? '📄' : ''}
170
+ </span>
171
+ </TooltipTrigger>
172
+ <TooltipContent>{tooltipText}</TooltipContent>
173
+ </Tooltip>
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Priority label component - muted text abbreviation
179
+ */
180
+ function PriorityDot({ priority, storyId }: { priority?: string | null; storyId: string }): React.ReactElement | null {
181
+ if (!priority) return null;
182
+ return (
183
+ <span
184
+ className="priority-label"
185
+ data-testid={`story-priority-${storyId}`}
186
+ data-priority={priority}
187
+ title={priority}
188
+ >
189
+ {priority}
190
+ </span>
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Status badge component for stories
196
+ */
197
+ function StatusBadge({ status, storyId }: { status: SprintStory['status']; storyId: string }): React.ReactElement {
198
+ const { icon, className } = getStatusBadgeInfo(status);
199
+ return (
200
+ <Badge
201
+ variant={status === 'blocked' ? 'destructive' : status === 'done' ? 'default' : 'secondary'}
202
+ className={`story-status-badge ${className}`}
203
+ data-testid={`story-status-badge-${storyId}`}
204
+ data-status={status}
205
+ aria-label={`Status: ${status}`}
206
+ >
207
+ {icon}
208
+ </Badge>
209
+ );
210
+ }
211
+
212
+ /**
213
+ * Jira link component for stories
214
+ */
215
+ function JiraLink({ jiraKey, storyId }: { jiraKey: string; storyId: string }): React.ReactElement {
216
+ const handleClick = (e: React.MouseEvent) => {
217
+ e.preventDefault();
218
+ const url = getJiraUrl(jiraKey);
219
+ try {
220
+ const api = (window as any).electronAPI;
221
+ if (api?.shell?.openExternal) {
222
+ api.shell.openExternal(url);
223
+ return;
224
+ }
225
+ } catch {
226
+ // electronAPI not available or call failed
227
+ }
228
+ window.open(url, '_blank');
229
+ };
230
+
231
+ return (
232
+ <a
233
+ className="jira-link cyclist-link"
234
+ data-testid={`story-jira-link-${storyId}`}
235
+ href={getJiraUrl(jiraKey)}
236
+ onClick={handleClick}
237
+ >
238
+ {jiraKey}
239
+ </a>
240
+ );
241
+ }
242
+
243
+ /**
244
+ * CopyButton - Copy ID + title to clipboard on click
245
+ */
246
+ function CopyButton({ text }: { text: string }): React.ReactElement {
247
+ const [copied, setCopied] = useState(false);
248
+
249
+ const handleCopy = async (e: React.MouseEvent) => {
250
+ e.stopPropagation();
251
+ try {
252
+ await navigator.clipboard.writeText(text);
253
+ setCopied(true);
254
+ setTimeout(() => setCopied(false), 2000);
255
+ } catch {
256
+ // clipboard API not available
257
+ }
258
+ };
259
+
260
+ return (
261
+ <button
262
+ className={`copy-id-button ${copied ? 'copied' : ''}`}
263
+ onClick={handleCopy}
264
+ aria-label={`Copy ${text}`}
265
+ title="Copy ID + title"
266
+ >
267
+ {copied ? <Check size={12} /> : <Copy size={12} />}
268
+ </button>
269
+ );
270
+ }
271
+
272
+ /**
273
+ * EpicGroup - Renders a single epic with its stories
274
+ */
275
+ function EpicGroup({
276
+ epic,
277
+ isExpanded,
278
+ isArchiving,
279
+ onToggle,
280
+ onKeyDown,
281
+ onArchive,
282
+ }: {
283
+ epic: SprintEpic;
284
+ isExpanded: boolean;
285
+ isArchiving: boolean;
286
+ onToggle: (id: string) => void;
287
+ onKeyDown: (id: string, e: React.KeyboardEvent) => void;
288
+ onArchive: (id: string) => void;
289
+ }): React.ReactElement {
290
+ const { done, total } = calculateEpicProgress(epic);
291
+ const completed = isEpicCompleted(epic);
292
+
293
+ return (
294
+ <div
295
+ className={`epic-group ${completed ? 'epic-completed' : ''}`}
296
+ data-testid={`epic-group-${epic.id}`}
297
+ >
298
+ {/* Epic Header */}
299
+ <div className="epic-header">
300
+ <Button
301
+ variant="ghost"
302
+ size="icon"
303
+ className="epic-toggle"
304
+ data-testid={`epic-toggle-${epic.id}`}
305
+ onClick={() => onToggle(epic.id)}
306
+ onKeyDown={(e) => onKeyDown(epic.id, e)}
307
+ aria-expanded={isExpanded}
308
+ >
309
+ {isExpanded ? '▼' : '▶'}
310
+ </Button>
311
+ <span className="epic-title">{epic.title}</span>
312
+ {epic.jiraKey && <span className="epic-jira">{epic.jiraKey}</span>}
313
+ <CopyButton text={`${epic.id} ${epic.title}`} />
314
+ <ContextIndicator hasContext={epic.hasContext ?? false} testIdPrefix="epic" id={epic.id} />
315
+ {completed && epic.hasContext && (
316
+ <Badge variant="default" className="epic-ready-badge" data-testid={`epic-ready-badge-${epic.id}`}>
317
+ Ready
318
+ </Badge>
319
+ )}
320
+
321
+ {/* Progress bar */}
322
+ <div
323
+ className="epic-progress"
324
+ data-testid={`epic-progress-${epic.id}`}
325
+ data-done={String(done)}
326
+ data-total={String(total)}
327
+ >
328
+ <div
329
+ className="progress-bar"
330
+ style={{ width: `${total > 0 ? (done / total) * 100 : 0}%` }}
331
+ />
332
+ </div>
333
+ <span
334
+ className="epic-progress-label"
335
+ data-testid={`epic-progress-label-${epic.id}`}
336
+ >
337
+ {done}/{total} pts
338
+ </span>
339
+
340
+ {/* Archive button for completed epics */}
341
+ {completed && (
342
+ <>
343
+ {isArchiving && (
344
+ <span data-testid={`archive-loading-${epic.id}`}>...</span>
345
+ )}
346
+ <Button
347
+ variant="outline"
348
+ size="sm"
349
+ className="archive-button"
350
+ data-testid={`archive-button-${epic.id}`}
351
+ aria-label={`Archive ${epic.id}`}
352
+ disabled={isArchiving}
353
+ onClick={() => onArchive(epic.id)}
354
+ >
355
+ Archive
356
+ </Button>
357
+ </>
358
+ )}
359
+ </div>
360
+
361
+ {/* Stories list (collapsible) */}
362
+ {isExpanded && (
363
+ <div className="epic-stories">
364
+ {epic.stories.map((story) => {
365
+ const hasContext = story.hasContext ?? false;
366
+ const isBlocked = story.status === 'blocked';
367
+ const assigneeDisplay = formatAssignee(story.assignedTo);
368
+ return (
369
+ <div
370
+ key={story.id}
371
+ className={`story-item ${!hasContext ? 'missing-context' : ''} ${isBlocked ? 'story-blocked' : ''}`}
372
+ data-testid={`story-item-${story.id}`}
373
+ data-status={story.status}
374
+ data-story-id={story.id}
375
+ aria-label={`${story.id}: ${story.title}`}
376
+ >
377
+ <PriorityDot priority={story.priority} storyId={story.id} />
378
+ <StatusBadge status={story.status} storyId={story.id} />
379
+ {story.jiraKey && <JiraLink jiraKey={story.jiraKey} storyId={story.id} />}
380
+ <CopyButton text={`${story.id} ${story.title}`} />
381
+ <div className="story-info">
382
+ <span className="story-title">{story.title}</span>
383
+ <span className="story-meta">
384
+ {assigneeDisplay && (
385
+ <span
386
+ className="story-assignee"
387
+ data-testid={`story-assignee-${story.id}`}
388
+ >
389
+ {assigneeDisplay}
390
+ </span>
391
+ )}
392
+ {story.workflow && (
393
+ <span
394
+ className="story-workflow-badge"
395
+ data-testid={`story-workflow-${story.id}`}
396
+ >
397
+ {story.workflow}
398
+ </span>
399
+ )}
400
+ {story.status === 'done' && story.completed && (
401
+ <span
402
+ className="story-completed-date"
403
+ data-testid={`story-completed-${story.id}`}
404
+ >
405
+ {story.completed}
406
+ </span>
407
+ )}
408
+ </span>
409
+ </div>
410
+ <ContextIndicator hasContext={hasContext} testIdPrefix="story" id={story.id} />
411
+ <span
412
+ className="story-points"
413
+ data-testid={`story-points-${story.id}`}
414
+ >
415
+ {story.points}
416
+ </span>
417
+ </div>
418
+ );
419
+ })}
420
+ </div>
421
+ )}
422
+ </div>
423
+ );
424
+ }
425
+
426
+ /**
427
+ * EnhancedSprintPanel - Full sprint management with epic actions
428
+ */
429
+ export function EnhancedSprintPanel(): React.ReactElement {
430
+ // Use the sprint hook for data fetching via WebSocket
431
+ const { data, isLoading, error } = useSprint();
432
+ const [expandedEpics, setExpandedEpics] = useState<Set<string>>(new Set());
433
+ const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
434
+ const [confirmArchive, setConfirmArchive] = useState<string | null>(null);
435
+ const [actionError, setActionError] = useState<Error | null>(null);
436
+
437
+ // Split epics into active (has non-done stories) vs completed (all stories done)
438
+ const activeEpics = data?.epics.filter((e) => !isEpicCompleted(e)) ?? [];
439
+ const completedEpics = data?.epics.filter((e) => isEpicCompleted(e)) ?? [];
440
+
441
+ // Expand only active epics by default when data first loads (once only)
442
+ // Completed epics start collapsed
443
+ const hasInitializedExpansion = useRef(false);
444
+ useEffect(() => {
445
+ if (data?.epics && !hasInitializedExpansion.current) {
446
+ hasInitializedExpansion.current = true;
447
+ setExpandedEpics(new Set(activeEpics.map((e) => e.id)));
448
+ }
449
+ }, [data?.epics]);
450
+
451
+ // Toggle epic expansion
452
+ const toggleEpic = useCallback((epicId: string) => {
453
+ setExpandedEpics((prev) => {
454
+ const next = new Set(prev);
455
+ if (next.has(epicId)) {
456
+ next.delete(epicId);
457
+ } else {
458
+ next.add(epicId);
459
+ }
460
+ return next;
461
+ });
462
+ }, []);
463
+
464
+ // Handle epic toggle via keyboard
465
+ const handleEpicKeyDown = useCallback(
466
+ (epicId: string, event: React.KeyboardEvent) => {
467
+ if (event.key === 'Enter' || event.key === ' ') {
468
+ event.preventDefault();
469
+ toggleEpic(epicId);
470
+ }
471
+ },
472
+ [toggleEpic]
473
+ );
474
+
475
+ // Archive epic action
476
+ const handleArchive = useCallback(
477
+ async (epicId: string) => {
478
+ setLoadingActions((prev) => new Set(prev).add(`archive-${epicId}`));
479
+ setConfirmArchive(null);
480
+ setActionError(null); // Clear any previous errors
481
+
482
+ try {
483
+ // Use electronAPI if available (Electron mode), otherwise REST endpoint (web mode)
484
+ if (typeof window !== 'undefined' && (window as any).electronAPI?.sprint?.archiveEpic) {
485
+ await (window as any).electronAPI.sprint.archiveEpic(epicId);
486
+ } else {
487
+ const response = await fetch(`/api/sprint/archive-epic/${epicId}`, { method: 'POST' });
488
+ if (!response.ok) throw new Error('Archive failed');
489
+ }
490
+ // Success - error already cleared at start
491
+ } catch (err) {
492
+ setActionError(err instanceof Error ? err : new Error('Archive failed'));
493
+ } finally {
494
+ setLoadingActions((prev) => {
495
+ const next = new Set(prev);
496
+ next.delete(`archive-${epicId}`);
497
+ return next;
498
+ });
499
+ }
500
+ },
501
+ []
502
+ );
503
+
504
+ // Promote epic action
505
+ const handlePromote = useCallback(
506
+ async (epicId: string) => {
507
+ setLoadingActions((prev) => new Set(prev).add(`promote-${epicId}`));
508
+ setActionError(null); // Clear any previous errors
509
+
510
+ try {
511
+ // Use electronAPI if available (Electron mode), otherwise REST endpoint (web mode)
512
+ if (typeof window !== 'undefined' && (window as any).electronAPI?.sprint?.promoteEpic) {
513
+ await (window as any).electronAPI.sprint.promoteEpic(epicId);
514
+ } else {
515
+ const response = await fetch(`/api/sprint/promote-epic/${epicId}`, { method: 'POST' });
516
+ if (!response.ok) throw new Error('Promote failed');
517
+ }
518
+ // Success - error already cleared at start
519
+ } catch (err) {
520
+ setActionError(err instanceof Error ? err : new Error('Promote failed'));
521
+ } finally {
522
+ setLoadingActions((prev) => {
523
+ const next = new Set(prev);
524
+ next.delete(`promote-${epicId}`);
525
+ return next;
526
+ });
527
+ }
528
+ },
529
+ []
530
+ );
531
+
532
+ // Loading state
533
+ if (isLoading) {
534
+ return (
535
+ <div className="enhanced-sprint-panel" data-testid="enhanced-sprint-panel">
536
+ <div className="loading-state space-y-3 p-2" data-testid="sprint-panel-loading">
537
+ <Skeleton className="h-5 w-32" />
538
+ <Skeleton className="h-4 w-full" />
539
+ <Separator className="my-2" />
540
+ <Skeleton className="h-5 w-28" />
541
+ <Skeleton className="h-8 w-full" />
542
+ <Skeleton className="h-8 w-full" />
543
+ <Separator className="my-2" />
544
+ <Skeleton className="h-5 w-36" />
545
+ <Skeleton className="h-6 w-3/4" />
546
+ </div>
547
+ </div>
548
+ );
549
+ }
550
+
551
+ // Error toast (show either WebSocket error or action error)
552
+ const displayError = error || actionError;
553
+ const errorToast = displayError ? (
554
+ <div className="error-toast" data-testid="error-toast">
555
+ {displayError.message}
556
+ </div>
557
+ ) : null;
558
+
559
+ // Confirmation dialog
560
+ const confirmDialog = confirmArchive ? (
561
+ <div className="confirm-dialog" data-testid="confirm-archive-dialog">
562
+ <p>Are you sure you want to archive this epic?</p>
563
+ <Button variant="destructive" size="sm" data-testid="confirm-archive-yes" onClick={() => handleArchive(confirmArchive)}>
564
+ Yes
565
+ </Button>
566
+ <Button variant="outline" size="sm" data-testid="confirm-archive-no" onClick={() => setConfirmArchive(null)}>
567
+ No
568
+ </Button>
569
+ </div>
570
+ ) : null;
571
+
572
+ return (
573
+ <TooltipProvider delayDuration={300}>
574
+ <div className="enhanced-sprint-panel" data-testid="enhanced-sprint-panel">
575
+ {errorToast}
576
+ {confirmDialog}
577
+
578
+ {/* Section 1: Current Story */}
579
+ <section data-section="current-story">
580
+ <h2>Current Story</h2>
581
+ {data?.currentStory ? (
582
+ <div data-testid="current-story-section">
583
+ <span className="story-id">{data.currentStory.id}</span>
584
+ <span className="story-title">{data.currentStory.title}</span>
585
+ <span className="story-status">{data.currentStory.status}</span>
586
+ <span className="story-points" data-testid="current-story-points">
587
+ {data.currentStory.points} pts
588
+ </span>
589
+ </div>
590
+ ) : data?.nextStory ? (
591
+ <div data-testid="next-up-section">
592
+ <span className="next-up-label">Next up:</span>
593
+ <span className="story-id">{data.nextStory.id}</span>
594
+ <span className="story-title">{data.nextStory.title}</span>
595
+ </div>
596
+ ) : (
597
+ <div data-testid="no-stories-section">
598
+ <span>No active story</span>
599
+ </div>
600
+ )}
601
+ </section>
602
+
603
+ <Separator className="my-2" />
604
+
605
+ {/* Section 2: Active Epics */}
606
+ <section data-section="epics">
607
+ <h2>Current Epics</h2>
608
+ <div data-testid="epic-tree-view">
609
+ {(!data?.epics || data.epics.length === 0) && (
610
+ <div className="empty-state" data-testid="no-epics-section">
611
+ <span>No epics in current sprint</span>
612
+ <p className="hint">Promote an epic from Future Initiatives to get started</p>
613
+ </div>
614
+ )}
615
+ {activeEpics.map((epic) => (
616
+ <EpicGroup
617
+ key={epic.id}
618
+ epic={epic}
619
+ isExpanded={expandedEpics.has(epic.id)}
620
+ isArchiving={loadingActions.has(`archive-${epic.id}`)}
621
+ onToggle={toggleEpic}
622
+ onKeyDown={handleEpicKeyDown}
623
+ onArchive={setConfirmArchive}
624
+ />
625
+ ))}
626
+ </div>
627
+ </section>
628
+
629
+ {/* Section 2b: Completed Epics */}
630
+ {completedEpics.length > 0 && (
631
+ <>
632
+ <Separator className="my-2" />
633
+ <section data-section="completed-epics">
634
+ <h2>Completed Epics</h2>
635
+ <div data-testid="completed-epics-section">
636
+ {completedEpics.map((epic) => (
637
+ <EpicGroup
638
+ key={epic.id}
639
+ epic={epic}
640
+ isExpanded={expandedEpics.has(epic.id)}
641
+ isArchiving={loadingActions.has(`archive-${epic.id}`)}
642
+ onToggle={toggleEpic}
643
+ onKeyDown={handleEpicKeyDown}
644
+ onArchive={setConfirmArchive}
645
+ />
646
+ ))}
647
+ </div>
648
+ </section>
649
+ </>
650
+ )}
651
+
652
+ <Separator className="my-2" />
653
+
654
+ {/* Section 3: Future Initiatives */}
655
+ <section data-section="future">
656
+ <h2>Future Initiatives</h2>
657
+ <div data-testid="future-initiatives-section">
658
+ {data?.futureEpics.map((epic) => {
659
+ const isPromoting = loadingActions.has(`promote-${epic.id}`);
660
+ const canPromote = epic.status === 'ready';
661
+
662
+ return (
663
+ <div
664
+ key={epic.id}
665
+ className="future-epic"
666
+ data-testid={`future-epic-${epic.id}`}
667
+ >
668
+ <span className="future-epic-title">{epic.title}</span>
669
+ <span className="future-epic-points">{epic.estimatedPoints} pts</span>
670
+ <Badge
671
+ variant={epic.status === 'ready' ? 'default' : 'secondary'}
672
+ className="future-epic-status"
673
+ data-testid={`future-epic-status-${epic.id}`}
674
+ data-status={epic.status}
675
+ >
676
+ {epic.status}
677
+ </Badge>
678
+ {canPromote && (
679
+ <Button
680
+ variant="default"
681
+ size="sm"
682
+ className="promote-button"
683
+ data-testid={`promote-button-${epic.id}`}
684
+ aria-label={`Promote ${epic.id} to current sprint`}
685
+ disabled={isPromoting}
686
+ onClick={() => handlePromote(epic.id)}
687
+ >
688
+ Promote
689
+ </Button>
690
+ )}
691
+ {epic.children && epic.children.length > 0 && (
692
+ <div className="future-epic-children" data-testid={`future-children-${epic.id}`}>
693
+ {epic.children.map((child: FutureEpicChild) => (
694
+ <div
695
+ key={child.id}
696
+ className="future-epic-child"
697
+ data-testid={`future-child-${child.id}`}
698
+ >
699
+ <span className="future-child-title">{child.title}</span>
700
+ <span className="future-child-points">{child.estimatedPoints} pts</span>
701
+ <span className="future-child-stories">{child.storyCount} stories</span>
702
+ <Badge
703
+ variant={child.status === 'blocked' ? 'destructive' : 'secondary'}
704
+ className="future-child-status"
705
+ data-status={child.status}
706
+ >
707
+ {child.status}
708
+ </Badge>
709
+ </div>
710
+ ))}
711
+ </div>
712
+ )}
713
+ </div>
714
+ );
715
+ })}
716
+ </div>
717
+ </section>
718
+ </div>
719
+ </TooltipProvider>
720
+ );
721
+ }
722
+
723
+ export default SprintPanel;