@pennyfarthing/core 11.3.7 → 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 (405) hide show
  1. package/README.md +1 -1
  2. package/package.json +17 -16
  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/hooks/cyclist-pretooluse-hook.sh +0 -0
  230. package/pennyfarthing-dist/pf/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  231. package/pennyfarthing-dist/pf/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  232. package/pennyfarthing-dist/pf/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  233. package/pennyfarthing-dist/pf/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  234. package/pennyfarthing-dist/pf/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  235. package/pennyfarthing-dist/pf/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  236. package/pennyfarthing-dist/pf/jira/__pycache__/claim.cpython-314.pyc +0 -0
  237. package/pennyfarthing-dist/pf/jira/__pycache__/cli.cpython-314.pyc +0 -0
  238. package/pennyfarthing-dist/pf/jira/__pycache__/client.cpython-314.pyc +0 -0
  239. package/pennyfarthing-dist/pf/jira/__pycache__/create.cpython-314.pyc +0 -0
  240. package/pennyfarthing-dist/pf/jira/__pycache__/epic.cpython-314.pyc +0 -0
  241. package/pennyfarthing-dist/pf/jira/__pycache__/operations.cpython-314.pyc +0 -0
  242. package/pennyfarthing-dist/pf/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  243. package/pennyfarthing-dist/pf/jira/__pycache__/story.cpython-314.pyc +0 -0
  244. package/pennyfarthing-dist/pf/jira/__pycache__/sync.cpython-314.pyc +0 -0
  245. package/pennyfarthing-dist/pf/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing-dist/pf/launch/__pycache__/cli.cpython-314.pyc +0 -0
  247. package/pennyfarthing-dist/pf/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  248. package/pennyfarthing-dist/pf/prime/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing-dist/pf/prime/__pycache__/loader.cpython-314.pyc +0 -0
  250. package/pennyfarthing-dist/pf/prime/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing-dist/pf/prime/__pycache__/persona.cpython-314.pyc +0 -0
  252. package/pennyfarthing-dist/pf/prime/__pycache__/session.cpython-314.pyc +0 -0
  253. package/pennyfarthing-dist/pf/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  254. package/pennyfarthing-dist/pf/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  255. package/pennyfarthing-dist/pf/session/__pycache__/__init__.cpython-314.pyc +0 -0
  256. package/pennyfarthing-dist/pf/session/__pycache__/cli.cpython-314.pyc +0 -0
  257. package/pennyfarthing-dist/pf/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/pennyfarthing-dist/pf/settings/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing-dist/pf/settings/__pycache__/settings.cpython-314.pyc +0 -0
  260. package/pennyfarthing-dist/pf/settings/settings.py +44 -8
  261. package/pennyfarthing-dist/pf/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  262. package/pennyfarthing-dist/pf/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  263. package/pennyfarthing-dist/pf/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  264. package/pennyfarthing-dist/pf/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  265. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  266. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  267. package/pennyfarthing-dist/pf/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  268. package/pennyfarthing-dist/pf/sprint/__pycache__/status.cpython-314.pyc +0 -0
  269. package/pennyfarthing-dist/pf/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  270. package/pennyfarthing-dist/pf/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  271. package/pennyfarthing-dist/pf/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  272. package/pennyfarthing-dist/pf/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  273. package/pennyfarthing-dist/pf/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  274. package/pennyfarthing-dist/pf/sprint/__pycache__/work.cpython-314.pyc +0 -0
  275. package/pennyfarthing-dist/pf/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  276. package/pennyfarthing-dist/pf/sprint/story_finish.py +14 -2
  277. package/pennyfarthing-dist/pf/sprint/validator.py +7 -7
  278. package/pennyfarthing-dist/pf/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  279. package/pennyfarthing-dist/pf/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  280. package/pennyfarthing-dist/pf/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  281. package/pennyfarthing-dist/pf/tests/test_sprint_validator.py +44 -0
  282. package/pennyfarthing-dist/pf/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  283. package/pennyfarthing-dist/pf/theme/__pycache__/cli.cpython-314.pyc +0 -0
  284. package/pennyfarthing-dist/pf/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  285. package/pennyfarthing-dist/pf/validate/__pycache__/cli.cpython-314.pyc +0 -0
  286. package/pennyfarthing-dist/pf/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  287. package/pennyfarthing-dist/pf/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  288. package/pennyfarthing-dist/pf/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  289. package/pennyfarthing-dist/pf/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  290. package/pennyfarthing-dist/pf/workflow/__pycache__/state.cpython-314.pyc +0 -0
  291. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  292. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  293. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +0 -0
  294. package/pennyfarthing-dist/scripts/core/pf.sh +0 -0
  295. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  296. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  297. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  298. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  299. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  300. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
  301. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  302. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  303. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  304. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  305. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  306. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  307. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  308. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  309. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
  310. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  311. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  312. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  313. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  314. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  315. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  316. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  317. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  318. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  319. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  320. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  321. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  322. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  323. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  324. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  325. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  326. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  327. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  328. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  329. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  330. package/pennyfarthing-dist/scripts/lib/env.sh +0 -0
  331. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  332. package/pennyfarthing-dist/scripts/lib/find-root.sh +1 -1
  333. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  334. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  335. package/pennyfarthing-dist/scripts/lib/run-pf.sh +0 -0
  336. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  337. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  338. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  339. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  340. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  341. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  342. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  343. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  344. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  345. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  346. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -1
  347. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  348. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  349. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  350. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  351. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  352. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  353. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  354. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  355. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  356. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +13 -13
  357. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  358. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +0 -0
  359. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  360. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  361. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  362. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  363. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  364. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  365. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  366. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  367. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  368. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  369. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  370. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  371. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  372. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  373. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  374. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  375. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  376. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  377. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  378. package/pennyfarthing-dist/scripts/workflow/check.py +4 -6
  379. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  380. package/pennyfarthing-dist/scripts/workflow/complete-step.py +2 -2
  381. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  382. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  383. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  384. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  385. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  386. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  387. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  388. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  389. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  390. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  391. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  392. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  393. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  394. package/pennyfarthing-dist/skills/skill-registry.yaml +19 -0
  395. package/pennyfarthing-dist/workflows/release/steps/step-10-publish.md +41 -9
  396. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +15 -2
  397. package/packages/core/dist/workflow/__test_context_watch__/.session/.tandem-turn-counter +0 -1
  398. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-session.md +0 -3
  399. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-tandem-architect.md +0 -6
  400. package/packages/core/dist/workflow/__test_file_watch__/.session/95-4-tandem-architect.md +0 -6
  401. package/packages/core/dist/workflow/__test_file_watch__/workdir/trigger.ts +0 -1
  402. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-architect.md +0 -6
  403. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-toolcalls.jsonl +0 -1
  404. package/pennyfarthing-dist/pf/bikerack/changed_panel.py +0 -201
  405. package/scripts/README.md +0 -41
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Font Presets - MSSCI-12769
3
+ *
4
+ * Font customization system for Cyclist with UI and code font presets,
5
+ * Tailwind-like size scale, and global persistence.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Types
10
+ // =============================================================================
11
+
12
+ export interface FontPreset {
13
+ id: string;
14
+ name: string;
15
+ fontFamily: string;
16
+ isCustom?: boolean;
17
+ }
18
+
19
+ export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl';
20
+
21
+ export interface FontSettings {
22
+ uiFont: string;
23
+ codeFont: string;
24
+ uiFontSize: FontSize;
25
+ codeFontSize: FontSize;
26
+ customUiFont?: string;
27
+ customCodeFont?: string;
28
+ }
29
+
30
+ export interface FontSizeScale {
31
+ xs: string;
32
+ sm: string;
33
+ base: string;
34
+ lg: string;
35
+ xl: string;
36
+ }
37
+
38
+ // =============================================================================
39
+ // Font Presets
40
+ // =============================================================================
41
+
42
+ export const UI_FONT_PRESETS: FontPreset[] = [
43
+ {
44
+ id: 'system',
45
+ name: 'System',
46
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
47
+ },
48
+ {
49
+ id: 'inter',
50
+ name: 'Inter',
51
+ fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
52
+ },
53
+ {
54
+ id: 'custom',
55
+ name: 'Custom',
56
+ fontFamily: '',
57
+ isCustom: true,
58
+ },
59
+ ];
60
+
61
+ export const CODE_FONT_PRESETS: FontPreset[] = [
62
+ {
63
+ id: 'system-mono',
64
+ name: 'System Mono',
65
+ fontFamily: "ui-monospace, 'SF Mono', Monaco, Consolas, 'Liberation Mono', monospace",
66
+ },
67
+ {
68
+ id: 'jetbrains-mono',
69
+ name: 'JetBrains Mono',
70
+ fontFamily: "'JetBrains Mono', ui-monospace, 'SF Mono', Monaco, monospace",
71
+ },
72
+ {
73
+ id: 'fira-code',
74
+ name: 'Fira Code',
75
+ fontFamily: "'Fira Code', ui-monospace, 'SF Mono', Monaco, monospace",
76
+ },
77
+ {
78
+ id: 'custom',
79
+ name: 'Custom',
80
+ fontFamily: '',
81
+ isCustom: true,
82
+ },
83
+ ];
84
+
85
+ // =============================================================================
86
+ // Font Size Scale (Tailwind-like)
87
+ // =============================================================================
88
+
89
+ export const FONT_SIZE_SCALE: FontSizeScale = {
90
+ xs: '0.75rem',
91
+ sm: '0.875rem',
92
+ base: '1rem',
93
+ lg: '1.125rem',
94
+ xl: '1.25rem',
95
+ };
96
+
97
+ const VALID_SIZES: FontSize[] = ['xs', 'sm', 'base', 'lg', 'xl'];
98
+
99
+ // =============================================================================
100
+ // Sanitization
101
+ // =============================================================================
102
+
103
+ /**
104
+ * Sanitize custom font family input to prevent CSS injection.
105
+ * Removes characters that could break out of font-family context.
106
+ */
107
+ export function sanitizeFontFamily(input: string): string {
108
+ if (!input || typeof input !== 'string') {
109
+ return '';
110
+ }
111
+ // Remove dangerous characters that could break CSS context
112
+ // Allow: letters, numbers, spaces, quotes, commas, hyphens, underscores
113
+ return input
114
+ .replace(/[;{}()<>\\]/g, '') // Remove CSS-breaking chars
115
+ .replace(/javascript:/gi, '') // Remove script protocol
116
+ .replace(/expression\s*\(/gi, '') // Remove IE expression()
117
+ .trim()
118
+ .slice(0, 500); // Limit length
119
+ }
120
+
121
+ // =============================================================================
122
+ // Default Settings
123
+ // =============================================================================
124
+
125
+ export const DEFAULT_FONT_SETTINGS: FontSettings = {
126
+ uiFont: 'system',
127
+ codeFont: 'system-mono',
128
+ uiFontSize: 'base',
129
+ codeFontSize: 'base',
130
+ };
131
+
132
+ // =============================================================================
133
+ // In-Memory State
134
+ // =============================================================================
135
+
136
+ let currentSettings: FontSettings = { ...DEFAULT_FONT_SETTINGS };
137
+
138
+ // =============================================================================
139
+ // UI Font Functions
140
+ // =============================================================================
141
+
142
+ export function getUIFont(): string {
143
+ return currentSettings.uiFont;
144
+ }
145
+
146
+ export function setUIFont(presetId: string, customFamily?: string): void {
147
+ currentSettings.uiFont = presetId;
148
+ if (presetId === 'custom' && customFamily) {
149
+ currentSettings.customUiFont = customFamily;
150
+ }
151
+ applyUIFont(presetId, customFamily);
152
+ persistSettings();
153
+ }
154
+
155
+ export function getCustomUIFont(): string | undefined {
156
+ return currentSettings.customUiFont;
157
+ }
158
+
159
+ // =============================================================================
160
+ // Code Font Functions
161
+ // =============================================================================
162
+
163
+ export function getCodeFont(): string {
164
+ return currentSettings.codeFont;
165
+ }
166
+
167
+ export function setCodeFont(presetId: string, customFamily?: string): void {
168
+ currentSettings.codeFont = presetId;
169
+ if (presetId === 'custom' && customFamily) {
170
+ currentSettings.customCodeFont = customFamily;
171
+ }
172
+ applyCodeFont(presetId, customFamily);
173
+ persistSettings();
174
+ }
175
+
176
+ export function getCustomCodeFont(): string | undefined {
177
+ return currentSettings.customCodeFont;
178
+ }
179
+
180
+ // =============================================================================
181
+ // Font Size Functions
182
+ // =============================================================================
183
+
184
+ export function getUIFontSize(): FontSize {
185
+ return currentSettings.uiFontSize;
186
+ }
187
+
188
+ export function setUIFontSize(size: FontSize): void {
189
+ if (!VALID_SIZES.includes(size)) {
190
+ throw new Error(`Invalid font size: ${size}. Must be one of: ${VALID_SIZES.join(', ')}`);
191
+ }
192
+ currentSettings.uiFontSize = size;
193
+ applyFontSizes(currentSettings.uiFontSize, currentSettings.codeFontSize);
194
+ persistSettings();
195
+ }
196
+
197
+ export function getCodeFontSize(): FontSize {
198
+ return currentSettings.codeFontSize;
199
+ }
200
+
201
+ export function setCodeFontSize(size: FontSize): void {
202
+ if (!VALID_SIZES.includes(size)) {
203
+ throw new Error(`Invalid font size: ${size}. Must be one of: ${VALID_SIZES.join(', ')}`);
204
+ }
205
+ currentSettings.codeFontSize = size;
206
+ applyFontSizes(currentSettings.uiFontSize, currentSettings.codeFontSize);
207
+ persistSettings();
208
+ }
209
+
210
+ // =============================================================================
211
+ // Apply Functions (CSS Variables)
212
+ // =============================================================================
213
+
214
+ export function applyUIFont(presetId: string, customFamily?: string): void {
215
+ const preset = UI_FONT_PRESETS.find(p => p.id === presetId);
216
+ if (!preset) {
217
+ console.error(`[font-presets] Unknown UI font preset: ${presetId}`);
218
+ return;
219
+ }
220
+
221
+ const fontFamily = preset.isCustom && customFamily
222
+ ? sanitizeFontFamily(customFamily)
223
+ : preset.fontFamily;
224
+ document.documentElement.style.setProperty('--font-ui', fontFamily);
225
+ }
226
+
227
+ export function applyCodeFont(presetId: string, customFamily?: string): void {
228
+ const preset = CODE_FONT_PRESETS.find(p => p.id === presetId);
229
+ if (!preset) {
230
+ console.error(`[font-presets] Unknown code font preset: ${presetId}`);
231
+ return;
232
+ }
233
+
234
+ const fontFamily = preset.isCustom && customFamily
235
+ ? sanitizeFontFamily(customFamily)
236
+ : preset.fontFamily;
237
+ document.documentElement.style.setProperty('--font-mono', fontFamily);
238
+ }
239
+
240
+ export function applyFontSizes(uiSize: FontSize, codeSize: FontSize): void {
241
+ document.documentElement.style.setProperty('--font-size-ui', FONT_SIZE_SCALE[uiSize]);
242
+ document.documentElement.style.setProperty('--font-size-code', FONT_SIZE_SCALE[codeSize]);
243
+ }
244
+
245
+ export function applyFontSettings(settings: FontSettings): void {
246
+ // Apply UI font
247
+ const uiPreset = UI_FONT_PRESETS.find(p => p.id === settings.uiFont);
248
+ if (uiPreset) {
249
+ const uiFontFamily = uiPreset.isCustom && settings.customUiFont
250
+ ? sanitizeFontFamily(settings.customUiFont)
251
+ : uiPreset.fontFamily;
252
+ document.documentElement.style.setProperty('--font-ui', uiFontFamily);
253
+ }
254
+
255
+ // Apply code font
256
+ const codePreset = CODE_FONT_PRESETS.find(p => p.id === settings.codeFont);
257
+ if (codePreset) {
258
+ const codeFontFamily = codePreset.isCustom && settings.customCodeFont
259
+ ? sanitizeFontFamily(settings.customCodeFont)
260
+ : codePreset.fontFamily;
261
+ document.documentElement.style.setProperty('--font-mono', codeFontFamily);
262
+ }
263
+
264
+ // Apply sizes
265
+ applyFontSizes(settings.uiFontSize, settings.codeFontSize);
266
+
267
+ // Update in-memory state
268
+ currentSettings = { ...settings };
269
+ }
270
+
271
+ // =============================================================================
272
+ // IPC Integration
273
+ // =============================================================================
274
+
275
+ declare global {
276
+ interface Window {
277
+ electronAPI?: {
278
+ send?: (channel: string, data: unknown) => void;
279
+ invoke?: (channel: string, data: unknown) => Promise<unknown>;
280
+ font?: {
281
+ save: (settings: FontSettings) => Promise<boolean>;
282
+ load: () => Promise<FontSettings | null>;
283
+ getSettingsPath: () => Promise<string>;
284
+ };
285
+ };
286
+ }
287
+ }
288
+
289
+ export function notifyFontChange(type: 'ui' | 'code', presetId: string): void {
290
+ // Font change is broadcast via settings WebSocket
291
+ console.log('[font-presets] Font changed:', type, presetId);
292
+ }
293
+
294
+ // =============================================================================
295
+ // Persistence Functions
296
+ // =============================================================================
297
+
298
+ export async function saveFontSettings(settings: FontSettings): Promise<void> {
299
+ currentSettings = { ...settings };
300
+ try {
301
+ // Use REST API to save font settings
302
+ await fetch('/api/settings', {
303
+ method: 'PATCH',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ display: { fonts: settings } }),
306
+ });
307
+ } catch (err) {
308
+ console.error('[font-presets] Failed to save settings:', err);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Helper to persist current settings (async but not awaited for better UX)
314
+ */
315
+ function persistSettings(): void {
316
+ saveFontSettings(currentSettings).catch(() => {
317
+ // Error already logged in saveFontSettings
318
+ });
319
+ }
320
+
321
+ export async function loadFontSettings(): Promise<FontSettings> {
322
+ try {
323
+ // Use REST API to load font settings
324
+ const response = await fetch('/api/settings');
325
+ if (response.ok) {
326
+ const settings = await response.json();
327
+ if (settings?.display?.fonts) {
328
+ currentSettings = { ...settings.display.fonts };
329
+ return currentSettings;
330
+ }
331
+ }
332
+ } catch (err) {
333
+ console.error('[font-presets] Failed to load settings:', err);
334
+ }
335
+ return { ...DEFAULT_FONT_SETTINGS };
336
+ }
337
+
338
+ export function getFontSettingsPath(): string {
339
+ // Global settings path (in user's home directory)
340
+ const home = typeof process !== 'undefined' ? process.env.HOME : '~';
341
+ return `${home}/.config/cyclist/font-settings.yaml`;
342
+ }
343
+
344
+ // =============================================================================
345
+ // Preset Lookup Helpers
346
+ // =============================================================================
347
+
348
+ export function getUIFontPreset(id: string): FontPreset | undefined {
349
+ return UI_FONT_PRESETS.find(p => p.id === id);
350
+ }
351
+
352
+ export function getCodeFontPreset(id: string): FontPreset | undefined {
353
+ return CODE_FONT_PRESETS.find(p => p.id === id);
354
+ }
355
+
356
+ export function getUIFontPresetIds(): string[] {
357
+ return UI_FONT_PRESETS.map(p => p.id);
358
+ }
359
+
360
+ export function getCodeFontPresetIds(): string[] {
361
+ return CODE_FONT_PRESETS.map(p => p.id);
362
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Format duration in milliseconds to human-readable string
3
+ *
4
+ * Story MSSCI-13402 - Tool use visual design polish
5
+ *
6
+ * @param ms - Duration in milliseconds
7
+ * @returns Formatted string like "245ms" or "2.3s"
8
+ */
9
+ export function formatDuration(ms: number | undefined): string {
10
+ if (ms === undefined || ms === null) return '—';
11
+ if (ms < 0) return '0ms';
12
+ if (ms < 1000) return `${Math.round(ms)}ms`;
13
+ return `${(ms / 1000).toFixed(1)}s`;
14
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Markdown Utilities
3
+ *
4
+ * Story MSSCI-13969: TypeScript markdown parsing utilities extracted from
5
+ * js/components/message-view/markdown-parser.js
6
+ *
7
+ * Security: XSS prevention via HTML escaping BEFORE markdown processing.
8
+ */
9
+
10
+ import { highlightCode } from './syntax';
11
+
12
+ /**
13
+ * Strip CYCLIST structured markers from text before rendering.
14
+ * These markers are used for machine parsing (quick actions) but should
15
+ * be invisible to users.
16
+ * @param text - Text that may contain CYCLIST markers
17
+ * @returns Text with markers removed
18
+ */
19
+ export function stripMarkers(text: string | null | undefined): string {
20
+ if (!text) return text as string;
21
+ // Remove CYCLIST markers: <!-- CYCLIST:TYPE --> or <!-- CYCLIST:TYPE:value -->
22
+ return text.replace(/<!--\s*CYCLIST:[^>]+?\s*-->/gi, '').trim();
23
+ }
24
+
25
+ /**
26
+ * Escape HTML special characters to prevent XSS.
27
+ * @param text - Raw text that may contain HTML
28
+ * @returns Escaped text safe for HTML rendering
29
+ */
30
+ export function escapeHtml(text: string): string {
31
+ const map: Record<string, string> = {
32
+ '&': '&amp;',
33
+ '<': '&lt;',
34
+ '>': '&gt;',
35
+ '"': '&quot;',
36
+ "'": '&#039;',
37
+ };
38
+ return text.replace(/[&<>"']/g, (c) => map[c]);
39
+ }
40
+
41
+ /**
42
+ * Parse cells from a table line: | cell1 | cell2 | cell3 |
43
+ * @param line - Table row line
44
+ * @returns Array of cell contents
45
+ */
46
+ function parseCells(line: string): string[] {
47
+ return line
48
+ .split('|')
49
+ .map((cell) => cell.trim())
50
+ .filter((cell, index, arr) => {
51
+ // Filter out empty first/last cells from leading/trailing |
52
+ if (index === 0 && cell === '') return false;
53
+ if (index === arr.length - 1 && cell === '') return false;
54
+ return true;
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Check if a line is a separator row (|---|---|)
60
+ * @param line - Table row line
61
+ * @returns boolean
62
+ */
63
+ function isSeparator(line: string): boolean {
64
+ const cells = parseCells(line);
65
+ return cells.every((cell) => /^[-:]+$/.test(cell));
66
+ }
67
+
68
+ /**
69
+ * Convert array of table lines to HTML table
70
+ * @param lines - Array of | delimited lines
71
+ * @returns HTML table string
72
+ */
73
+ function convertTableLinesToHtml(lines: string[]): string {
74
+ if (lines.length === 0) return '';
75
+
76
+ // Filter out separator rows and identify header
77
+ const dataLines: string[] = [];
78
+ let headerLine: string | null = null;
79
+ let foundSeparator = false;
80
+
81
+ for (const line of lines) {
82
+ if (isSeparator(line)) {
83
+ foundSeparator = true;
84
+ continue; // Skip separator rows
85
+ }
86
+ if (!foundSeparator && headerLine === null) {
87
+ headerLine = line; // First non-separator row is header
88
+ } else {
89
+ dataLines.push(line);
90
+ }
91
+ }
92
+
93
+ // Build HTML table
94
+ let html = '<div class="table-wrapper"><table>';
95
+
96
+ // Header row
97
+ if (headerLine) {
98
+ const headerCells = parseCells(headerLine);
99
+ html += '<thead><tr>';
100
+ for (let i = 0; i < headerCells.length; i++) {
101
+ html += `<th data-col="${i}" class="sortable-th">${headerCells[i]} <span class="sort-indicator"></span></th>`;
102
+ }
103
+ html += '</tr></thead>';
104
+ }
105
+
106
+ // Body rows
107
+ if (dataLines.length > 0) {
108
+ html += '<tbody>';
109
+ for (const line of dataLines) {
110
+ const cells = parseCells(line);
111
+ html += '<tr>';
112
+ for (const cell of cells) {
113
+ html += `<td>${cell}</td>`;
114
+ }
115
+ html += '</tr>';
116
+ }
117
+ html += '</tbody>';
118
+ }
119
+
120
+ html += '</table></div>';
121
+ return html;
122
+ }
123
+
124
+ /**
125
+ * Parse markdown tables to HTML
126
+ * Detects consecutive lines starting with | and converts to <table>
127
+ * @param text - Text with escaped HTML
128
+ * @returns Text with tables converted to HTML
129
+ */
130
+ function parseMarkdownTables(text: string): string {
131
+ const lines = text.split('\n');
132
+ const result: string[] = [];
133
+ let tableLines: string[] = [];
134
+ let inTable = false;
135
+
136
+ for (let i = 0; i < lines.length; i++) {
137
+ const line = lines[i].trim();
138
+
139
+ // Check if line is a table row (starts and ends with |, or starts with |)
140
+ const isTableRow = line.startsWith('|') && line.includes('|', 1);
141
+
142
+ if (isTableRow) {
143
+ if (!inTable) {
144
+ inTable = true;
145
+ tableLines = [];
146
+ }
147
+ tableLines.push(line);
148
+ } else {
149
+ // End of table or not a table line
150
+ if (inTable && tableLines.length > 0) {
151
+ result.push(convertTableLinesToHtml(tableLines));
152
+ tableLines = [];
153
+ inTable = false;
154
+ }
155
+ result.push(lines[i]); // Preserve original line (not trimmed)
156
+ }
157
+ }
158
+
159
+ // Handle table at end of text
160
+ if (inTable && tableLines.length > 0) {
161
+ result.push(convertTableLinesToHtml(tableLines));
162
+ }
163
+
164
+ return result.join('\n');
165
+ }
166
+
167
+ /**
168
+ * Parse markdown text to HTML with XSS protection.
169
+ * @param markdown - Raw markdown text
170
+ * @returns HTML string
171
+ */
172
+ export function parseMarkdown(markdown: string | null | undefined): string {
173
+ if (!markdown) return '';
174
+
175
+ // Strip CYCLIST structured markers BEFORE escaping HTML
176
+ // These are for quick-actions parsing, not display
177
+ const cleaned = stripMarkers(markdown);
178
+
179
+ // SECURITY: Escape HTML special characters FIRST to prevent XSS
180
+ // This ensures any <script>, <img onerror>, etc. are neutralized before processing
181
+ // Markdown syntax chars (*, #, `, -) are NOT escaped, so regexes still work
182
+ let html = escapeHtml(cleaned);
183
+
184
+ // Code blocks with syntax highlighting (must be first to avoid conflicts)
185
+ // Content is already escaped, apply highlighting then wrap
186
+ html = html.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_, lang, code) => {
187
+ const langClass = lang ? ` class="language-${lang}"` : '';
188
+ const highlighted = lang ? highlightCode(code.trim(), lang) : code.trim();
189
+ return `<pre><code${langClass}>${highlighted}</code></pre>`;
190
+ });
191
+
192
+ // B-15: Join multi-line list items BEFORE other processing
193
+ // Indented continuation lines (2+ spaces) are joined to previous list item
194
+ // Stop at blank lines or new list items
195
+ html = html.replace(/^(\d+\.\s+.+)\n((?: +[^\n]+\n)+)/gm, (_, firstLine, continuation) => {
196
+ // Join continuation lines with spaces, removing leading whitespace
197
+ // Preserve trailing newline for next list item
198
+ const joined = continuation.trim().replace(/\n\s*/g, ' ');
199
+ return `${firstLine} ${joined}\n`;
200
+ });
201
+
202
+ // Inline code - content already escaped
203
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
204
+
205
+ // Tables - convert | delimited rows to HTML tables
206
+ // Must be after code blocks to avoid parsing tables in code
207
+ html = parseMarkdownTables(html);
208
+
209
+ // Headers (h1-h6) - content already escaped
210
+ html = html.replace(/^######\s+(.+)$/gm, '<h6>$1</h6>');
211
+ html = html.replace(/^#####\s+(.+)$/gm, '<h5>$1</h5>');
212
+ html = html.replace(/^####\s+(.+)$/gm, '<h4>$1</h4>');
213
+ html = html.replace(/^###\s+(.+)$/gm, '<h3>$1</h3>');
214
+ html = html.replace(/^##\s+(.+)$/gm, '<h2>$1</h2>');
215
+ html = html.replace(/^#\s+(.+)$/gm, '<h1>$1</h1>');
216
+
217
+ // Bold and italic - content already escaped
218
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
219
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
220
+
221
+ // Unordered lists - mark with data attribute to distinguish from ordered
222
+ html = html.replace(/^- (.+)$/gm, '<li data-ul>$1</li>');
223
+
224
+ // Ordered lists - mark with data attribute
225
+ // B-15: Match ordered list items that may contain inline formatting
226
+ html = html.replace(/^\d+\.\s+(.+)$/gm, '<li data-ol>$1</li>');
227
+
228
+ // Wrap consecutive unordered list items
229
+ html = html.replace(/(<li data-ul>.*?<\/li>\n?)+/g, (match) => {
230
+ return `<ul>${match.replace(/ data-ul/g, '')}</ul>`;
231
+ });
232
+
233
+ // Wrap consecutive ordered list items
234
+ html = html.replace(/(<li data-ol>.*?<\/li>\n?)+/g, (match) => {
235
+ return `<ol>${match.replace(/ data-ol/g, '')}</ol>`;
236
+ });
237
+
238
+ // Paragraphs (lines not already wrapped) - content already escaped
239
+ // Excludes: h1-6, ul, ol, li, pre, div (table-wrapper), table elements
240
+ html = html.replace(/^(?!<[hulodtp]|<pre|<li)(.+)$/gm, '<p>$1</p>');
241
+
242
+ return html;
243
+ }
244
+
245
+ export default {
246
+ parseMarkdown,
247
+ escapeHtml,
248
+ stripMarkers,
249
+ };