@dsdenes/devboard 0.1.560 → 0.1.562

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 (323) hide show
  1. package/AGENTS.md +59 -0
  2. package/README.md +2497 -26
  3. package/bin/devboard.mjs +352 -118
  4. package/client/App.svelte +523 -0
  5. package/client/FeedbackOverlay.svelte +1125 -0
  6. package/client/FlowMode.svelte +1015 -0
  7. package/client/app.css +3922 -0
  8. package/client/env.d.ts +23 -0
  9. package/client/lib/commands.ts +836 -0
  10. package/client/lib/components/ActivityBar.svelte +295 -0
  11. package/client/lib/components/AdrTable.svelte +241 -0
  12. package/client/lib/components/CommandPalette.svelte +198 -0
  13. package/client/lib/components/ConceptKind.svelte +66 -0
  14. package/client/lib/components/ConfirmDialog.svelte +62 -0
  15. package/client/lib/components/DataTable.svelte +243 -0
  16. package/client/lib/components/DetailSplit.svelte +68 -0
  17. package/client/lib/components/DiscoverOverview.svelte +898 -0
  18. package/client/lib/components/DiscoverWorkspace.svelte +437 -0
  19. package/client/lib/components/DraftsPage.svelte +186 -0
  20. package/client/lib/components/GoalWorkspace.svelte +1042 -0
  21. package/client/lib/components/IssuesAgentFocus.svelte +117 -0
  22. package/client/lib/components/IssuesAgentLog.svelte +50 -0
  23. package/client/lib/components/IssuesQuestion.svelte +95 -0
  24. package/client/lib/components/IssuesWorkspace.svelte +577 -0
  25. package/client/lib/components/MermaidDiagram.svelte +89 -0
  26. package/client/lib/components/MonacoEditor.svelte +190 -0
  27. package/client/lib/components/NotificationRegion.svelte +195 -0
  28. package/client/lib/components/PaneHandle.svelte +25 -0
  29. package/client/lib/components/ScoutIssues.svelte +215 -0
  30. package/client/lib/components/ScoutWorkspace.svelte +180 -0
  31. package/client/lib/components/Sidebar.svelte +622 -0
  32. package/client/lib/components/StatusBar.svelte +118 -0
  33. package/client/lib/components/Terminal.svelte +193 -0
  34. package/client/lib/components/WatchdogWorkspace.svelte +537 -0
  35. package/client/lib/components/ui/button.svelte +56 -0
  36. package/client/lib/components/ui/icon-button.svelte +56 -0
  37. package/client/lib/components/ui/input.svelte +19 -0
  38. package/client/lib/components/ui/page-header.svelte +120 -0
  39. package/client/lib/components/ui/stage-track.svelte +141 -0
  40. package/client/lib/components/watchdog-indicator.svelte +189 -0
  41. package/client/lib/connection.svelte.ts +210 -0
  42. package/client/lib/dependency-diagram.test.ts +90 -0
  43. package/client/lib/dependency-diagram.ts +75 -0
  44. package/client/lib/discover-format.test.ts +265 -0
  45. package/client/lib/discover-format.ts +394 -0
  46. package/client/lib/discover.svelte.ts +412 -0
  47. package/client/lib/draft-format.test.ts +48 -0
  48. package/client/lib/draft-format.ts +13 -0
  49. package/client/lib/flow-save.test.ts +148 -0
  50. package/client/lib/flow.svelte.ts +445 -0
  51. package/client/lib/goal-reconnect.test.ts +282 -0
  52. package/client/lib/goal.svelte.ts +652 -0
  53. package/client/lib/issues.svelte.ts +439 -0
  54. package/client/lib/layout.svelte.ts +234 -0
  55. package/client/lib/notification-issue.test.ts +53 -0
  56. package/client/lib/notification-issue.ts +27 -0
  57. package/client/lib/notifications.svelte.ts +62 -0
  58. package/client/lib/poll-status.test.ts +141 -0
  59. package/client/lib/poll-status.ts +88 -0
  60. package/client/lib/record-status.test.ts +19 -0
  61. package/client/lib/record-status.ts +25 -0
  62. package/client/lib/scout.svelte.ts +164 -0
  63. package/client/lib/skill-document.test.ts +77 -0
  64. package/client/lib/skill-document.ts +57 -0
  65. package/client/lib/status-tone.test.ts +40 -0
  66. package/client/lib/status-tone.ts +33 -0
  67. package/client/lib/types.ts +578 -0
  68. package/client/lib/utils.ts +6 -0
  69. package/client/lib/viewport.svelte.ts +20 -0
  70. package/client/lib/watchdog.svelte.ts +69 -0
  71. package/client/main.ts +7 -0
  72. package/dist/apple-touch-icon.png +0 -0
  73. package/dist/assets/abnfDiagram-VCTEODGH-CmBSMArZ.js +1 -0
  74. package/dist/assets/arc-BZgCSX2H.js +1 -0
  75. package/dist/assets/architecture-7GRP2DOG-CF9MHlfo.js +1 -0
  76. package/dist/assets/architectureDiagram-5GKGNRK7-DzWB6xiQ.js +36 -0
  77. package/dist/assets/array-BifhSqXX.js +1 -0
  78. package/dist/assets/blockDiagram-I7D4REHJ-BtmGJ0hn.js +129 -0
  79. package/dist/assets/c4Diagram-7LVT6UL2-D5CHrrRX.js +38 -0
  80. package/dist/assets/channel-CgM4lcUW.js +1 -0
  81. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. package/dist/assets/chunk-4HAMMTFA-DeIufjFl.js +62 -0
  83. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  84. package/dist/assets/chunk-75Z2AOVW-DbtzyKUg.js +2 -0
  85. package/dist/assets/chunk-DU6HZSFF-C0fdbx3K.js +127 -0
  86. package/dist/assets/chunk-F27PBJKO-HObL2IvF.js +1 -0
  87. package/dist/assets/chunk-FOHPRMQF-DD4dKubD.js +161 -0
  88. package/dist/assets/chunk-GMAD6QVW-fg8wvtPY.js +72 -0
  89. package/dist/assets/chunk-GVQU2GXP-C0TaHOrD.js +1 -0
  90. package/dist/assets/chunk-IMKFNOWR-BVoIdx20.js +231 -0
  91. package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  92. package/dist/assets/chunk-L3NEJ4N5-B_kmqfk5.js +1 -0
  93. package/dist/assets/chunk-OSK3NFVY-DO0vhxHH.js +10 -0
  94. package/dist/assets/chunk-P2QGCYS3-CjljLvFj.js +1 -0
  95. package/dist/assets/chunk-POPQ4Y6H-BU0C7tw6.js +1 -0
  96. package/dist/assets/chunk-PWAF6VOD-Dn0OjBt0.js +1 -0
  97. package/dist/assets/chunk-SHT3W25Y-CMCZE2xx.js +168 -0
  98. package/dist/assets/chunk-SVP7TREG-BIRmRy_W.js +88 -0
  99. package/dist/assets/chunk-TICWLB2K-C1bc2CDE.js +206 -0
  100. package/dist/assets/chunk-XXDRQBXY-Cc38QUmR.js +1 -0
  101. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  102. package/dist/assets/classDiagram-ZZMXUADV-BDh5w6Nc.js +1 -0
  103. package/dist/assets/classDiagram-v2-VYDZK3BY-BDh5w6Nc.js +1 -0
  104. package/dist/assets/cose-bilkent-JH36ORCC-B7pU7qhM.js +1 -0
  105. package/dist/assets/cynefin-OW5HDTMX-76dswSqS.js +1 -0
  106. package/dist/assets/cynefinDiagram-5FMLGOSQ-2kS4q01d.js +62 -0
  107. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  108. package/dist/assets/dagre-D7JM2Yhv.js +1 -0
  109. package/dist/assets/dagre-GXQ25YYZ-B5Y21TQM.js +4 -0
  110. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  111. package/dist/assets/diagram-S7CK7UJ4-CY6td7Et.js +30 -0
  112. package/dist/assets/diagram-UQ7AKVKN-DmDqnGZf.js +41 -0
  113. package/dist/assets/diagram-VSXAHHWV-KGLWOOcE.js +3 -0
  114. package/dist/assets/diagram-VX7I27RA-D5syUraJ.js +24 -0
  115. package/dist/assets/diagram-Z3DM3KII-Dtrbs1Om.js +24 -0
  116. package/dist/assets/dist-Dce-2YbA.js +1 -0
  117. package/dist/assets/ebnfDiagram-PWID7BFC-CQpJLtU3.js +1 -0
  118. package/dist/assets/editor.worker-BmdWdrHZ.js +33 -0
  119. package/dist/assets/erDiagram-RLTQ6QDP-cMTzKLu_.js +99 -0
  120. package/dist/assets/eventmodeling-NTZA5JFV-CMzLdNaL.js +1 -0
  121. package/dist/assets/flowDiagram-HODETNUW-BMUajB-G.js +1 -0
  122. package/dist/assets/ganttDiagram-EL5Y4UJY-DseBS5Fs.js +292 -0
  123. package/dist/assets/gitGraph-4MIJSDKK-CUQZRpxJ.js +1 -0
  124. package/dist/assets/gitGraphDiagram-WWUBYQGX-CfMfsJ3b.js +106 -0
  125. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  126. package/dist/assets/index-ByQ-XX38.css +1 -0
  127. package/dist/assets/index-CIf5TpfY.js +214 -0
  128. package/dist/assets/info-A6RAGUB7-D3nAKZ8M.js +1 -0
  129. package/dist/assets/infoDiagram-27XIBGKW-zu1O0WOy.js +2 -0
  130. package/dist/assets/init-C-OQMol4.js +1 -0
  131. package/dist/assets/ishikawaDiagram-5VMMS53U-DKC7BO9p.js +70 -0
  132. package/dist/assets/journeyDiagram-3NMN7TZE-Ld5mXse7.js +139 -0
  133. package/dist/assets/kanban-definition-UXKFOSKX-CH7_QlHL.js +89 -0
  134. package/dist/assets/katex-DJhrJSbw.js +257 -0
  135. package/dist/assets/line-N9XiCREw.js +1 -0
  136. package/dist/assets/linear-C9hnqmJv.js +1 -0
  137. package/dist/assets/mermaid-parser.core-DgPwV5Bh.js +7 -0
  138. package/dist/assets/mermaid.core-BKqynXLR.js +44 -0
  139. package/dist/assets/mindmap-definition-YA3MSWOX-DkiL-wiW.js +96 -0
  140. package/dist/assets/monaco-DjpsE8to.css +1 -0
  141. package/dist/assets/monaco-DqzJaNIe.js +908 -0
  142. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  143. package/dist/assets/packet-AYTQ26CC-DuMTvpEJ.js +1 -0
  144. package/dist/assets/path-fybaL0A-.js +1 -0
  145. package/dist/assets/pegDiagram-XKGWAZYB-DvMl_xSe.js +1 -0
  146. package/dist/assets/pie-WAS4IAKB-Da0xPaD8.js +1 -0
  147. package/dist/assets/pieDiagram-E7YTZNPT-amANNRJY.js +39 -0
  148. package/dist/assets/quadrantDiagram-AXDQQJYC-BrgvDMnk.js +7 -0
  149. package/dist/assets/radar-RG4KPBEZ-Dz3Ih5-N.js +1 -0
  150. package/dist/assets/railroad-74A4TZTK-CKMW-IV2.js +1 -0
  151. package/dist/assets/railroad-abnf-HS5TGJTU-ChqmSWSF.js +1 -0
  152. package/dist/assets/railroad-ebnf-LZEXJU2U-V3R4fFdb.js +1 -0
  153. package/dist/assets/railroad-peg-WCYAUIDC-B-RuhUQW.js +1 -0
  154. package/dist/assets/railroadDiagram-O6MQD6OU-FjG-6OLu.js +1 -0
  155. package/dist/assets/requirementDiagram-BXWQKSXE-Cnj8RfE4.js +84 -0
  156. package/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  157. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  158. package/dist/assets/sankeyDiagram-P5KCCOFB-DMu1fnEh.js +40 -0
  159. package/dist/assets/sequenceDiagram-WJ2MYXX4-CtCtaRpW.js +162 -0
  160. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  161. package/dist/assets/src-CGp3bfio.js +1 -0
  162. package/dist/assets/stateDiagram-D77RDMKH-xcJbLNhz.js +1 -0
  163. package/dist/assets/stateDiagram-v2-MP3YSRHH-DYHrQmJJ.js +1 -0
  164. package/dist/assets/swimlanes-42K2YHIH-BwLln_ga.js +1 -0
  165. package/dist/assets/swimlanesDiagram-VR7AAH4N-BcDfUQ0A.js +8 -0
  166. package/dist/assets/timeline-definition-24CTP7MA-ucNGsUUz.js +120 -0
  167. package/dist/assets/treeView-Q6P3EWNA-3uOomRue.js +1 -0
  168. package/dist/assets/treemap-WGGIJYW6-iGK7vwID.js +1 -0
  169. package/dist/assets/vennDiagram-4TSXK5OY-B9LgGe16.js +34 -0
  170. package/dist/assets/wardley-WFR3VGLG-FupMOf6k.js +1 -0
  171. package/dist/assets/wardleyDiagram-VM6X3IG4-CGZdEXM2.js +78 -0
  172. package/dist/assets/xterm-BrP-ENHg.css +1 -0
  173. package/dist/assets/xterm-C8zLTNGk.js +16 -0
  174. package/dist/assets/xychartDiagram-S5SC5T6Z-DafZrSBJ.js +7 -0
  175. package/dist/favicon.ico +0 -0
  176. package/dist/favicon.svg +6 -0
  177. package/dist/index.html +34 -0
  178. package/extensions/implement-goal-issue.ts +368 -0
  179. package/extensions/implement-next-epic.ts +835 -0
  180. package/index.html +28 -0
  181. package/install.sh +111 -0
  182. package/issues/control.ts +91 -0
  183. package/issues/guards.ts +12 -0
  184. package/issues/ledger.ts +122 -0
  185. package/issues/legacy.ts +68 -0
  186. package/issues/lock.ts +305 -0
  187. package/issues/paths.ts +76 -0
  188. package/issues/preflight.ts +189 -0
  189. package/issues/repository.ts +203 -0
  190. package/issues/scout/actions.ts +255 -0
  191. package/issues/scout/control.ts +80 -0
  192. package/issues/scout/github.ts +358 -0
  193. package/issues/scout/paths.ts +129 -0
  194. package/issues/scout/preflight.ts +277 -0
  195. package/issues/scout/recommendation.ts +400 -0
  196. package/issues/scout/scan.ts +143 -0
  197. package/issues/scout/state.ts +448 -0
  198. package/issues/scout/worker.ts +1282 -0
  199. package/issues/state.ts +584 -0
  200. package/issues/tracker.ts +271 -0
  201. package/issues/worker.ts +1065 -0
  202. package/lib/dependencies.ts +61 -0
  203. package/lib/forge-auth.ts +953 -0
  204. package/lib/forge.ts +141 -0
  205. package/lib/gitea.ts +709 -0
  206. package/lib/github-release.ts +318 -0
  207. package/lib/inventory.test.ts +79 -0
  208. package/lib/inventory.ts +53 -0
  209. package/lib/release.test.ts +269 -0
  210. package/lib/release.ts +801 -0
  211. package/omp/commands/adr-resolve.md +8 -0
  212. package/omp/commands/discover.md +6 -0
  213. package/omp/commands/epic-next.md +8 -0
  214. package/omp/commands/flow-change.md +8 -0
  215. package/omp/commands/flow-goal.md +8 -0
  216. package/omp/commands/flow-implement.md +8 -0
  217. package/omp/commands/flow-widget.md +8 -0
  218. package/omp/commands/goal-draft.md +8 -0
  219. package/omp/commands/goal-outcome.md +8 -0
  220. package/omp/commands/implement-issue.md +8 -0
  221. package/omp/commands/prepare-skills.md +6 -0
  222. package/omp/commands/scout-recommendations.md +8 -0
  223. package/omp/commands/setup-devboard.md +24 -0
  224. package/omp/config.yml +12 -0
  225. package/omp/extensions/implement-goal-issue.ts +2 -0
  226. package/omp/extensions/implement-next-epic.ts +2 -0
  227. package/omp/mcp.json +9 -0
  228. package/package.json +86 -7
  229. package/prompts/adr-resolve.md +130 -0
  230. package/prompts/discover.md +541 -0
  231. package/prompts/epic-next.md +533 -0
  232. package/prompts/flow-change.md +76 -0
  233. package/prompts/flow-goal.md +126 -0
  234. package/prompts/flow-implement.md +103 -0
  235. package/prompts/flow-widget.md +54 -0
  236. package/prompts/goal-draft.md +147 -0
  237. package/prompts/goal-outcome.md +87 -0
  238. package/prompts/implement-issue.md +66 -0
  239. package/prompts/prepare-skills.md +142 -0
  240. package/prompts/scout-recommendations.md +143 -0
  241. package/public/apple-touch-icon.png +0 -0
  242. package/public/favicon.ico +0 -0
  243. package/public/favicon.svg +6 -0
  244. package/server/acp.ts +540 -0
  245. package/server/activity.test.ts +96 -0
  246. package/server/activity.ts +48 -0
  247. package/server/app.test.ts +882 -0
  248. package/server/app.ts +6179 -0
  249. package/server/ask.test.ts +543 -0
  250. package/server/ask.ts +589 -0
  251. package/server/dependencies.test.ts +118 -0
  252. package/server/discover-remote.test.ts +1602 -0
  253. package/server/discover-sheet.test.ts +176 -0
  254. package/server/discover.test.ts +1510 -0
  255. package/server/discover.ts +3198 -0
  256. package/server/feedback.test.ts +458 -0
  257. package/server/feedback.ts +369 -0
  258. package/server/fixtures/adr-source.ts +71 -0
  259. package/server/fixtures/hang-download.ts +14 -0
  260. package/server/fixtures/stub-agent.ts +1870 -0
  261. package/server/fixtures/stub-forge.ts +141 -0
  262. package/server/fixtures/stub-gh.ts +57 -0
  263. package/server/flow-pr.test.ts +976 -0
  264. package/server/flow.test.ts +1632 -0
  265. package/server/flow.ts +2655 -0
  266. package/server/forge-pr.ts +387 -0
  267. package/server/goal-creation.test.ts +957 -0
  268. package/server/goal-draft.test.ts +348 -0
  269. package/server/goal-drafts.ts +449 -0
  270. package/server/goal-proposal.test.ts +479 -0
  271. package/server/goal-proposal.ts +232 -0
  272. package/server/goal-revision.test.ts +613 -0
  273. package/server/goal-slug.test.ts +56 -0
  274. package/server/goal-slug.ts +96 -0
  275. package/server/goal.ts +20 -0
  276. package/server/index.ts +147 -0
  277. package/server/issue-close.test.ts +100 -0
  278. package/server/issue-close.ts +178 -0
  279. package/server/issues.test.ts +167 -0
  280. package/server/issues.ts +479 -0
  281. package/server/journal.test.ts +350 -0
  282. package/server/journal.ts +329 -0
  283. package/server/lifecycle-pause.test.ts +1795 -0
  284. package/server/lifecycle.test.ts +1879 -0
  285. package/server/parallel-lifecycle.test.ts +1029 -0
  286. package/server/persistence-restart.test.ts +723 -0
  287. package/server/persistence.test.ts +1935 -0
  288. package/server/records.test.ts +180 -0
  289. package/server/records.ts +99 -0
  290. package/server/reset-implementation.test.ts +997 -0
  291. package/server/run.test.ts +611 -0
  292. package/server/scout.test.ts +877 -0
  293. package/server/scout.ts +459 -0
  294. package/server/skills-preparation.test.ts +1073 -0
  295. package/server/state.ts +249 -0
  296. package/server/tracker.ts +369 -0
  297. package/server/updates.test.ts +444 -0
  298. package/server/updates.ts +198 -0
  299. package/server/version.test.ts +140 -0
  300. package/server/version.ts +151 -0
  301. package/server/watchdog.ts +389 -0
  302. package/svelte.config.js +5 -0
  303. package/templates/host-AGENTS.md +235 -0
  304. package/tsconfig.json +21 -0
  305. package/vite.config.ts +21 -0
  306. package/watchdog/cleanup-main.ts +98 -0
  307. package/watchdog/cleanup.ts +301 -0
  308. package/watchdog/collect.ts +311 -0
  309. package/watchdog/coordinator.ts +397 -0
  310. package/watchdog/delivery.ts +107 -0
  311. package/watchdog/discovery.ts +209 -0
  312. package/watchdog/findings.ts +211 -0
  313. package/watchdog/github.ts +189 -0
  314. package/watchdog/history.ts +138 -0
  315. package/watchdog/install.ts +103 -0
  316. package/watchdog/ledger.ts +188 -0
  317. package/watchdog/lock.ts +63 -0
  318. package/watchdog/main.ts +106 -0
  319. package/watchdog/status.ts +71 -0
  320. package/watchdog/systemd.ts +96 -0
  321. package/watchdog/uninstall.ts +30 -0
  322. package/watchdog/units/devboard-watchdog.service +9 -0
  323. package/watchdog/units/devboard-watchdog.timer +10 -0
@@ -0,0 +1,1015 @@
1
+ <script lang="ts">
2
+ import { html as diffHtml } from "diff2html";
3
+ import "diff2html/bundles/css/diff2html.min.css";
4
+ import { flow } from "./lib/flow.svelte";
5
+ import { layout } from "./lib/layout.svelte";
6
+ import type { WorkspaceTab } from "./lib/layout.svelte";
7
+ import { statusTone } from "./lib/status-tone";
8
+ import type { FlowFile, FlowWidget, RunQuestion } from "./lib/types";
9
+ import DetailSplit from "./lib/components/DetailSplit.svelte";
10
+ import MonacoEditor from "./lib/components/MonacoEditor.svelte";
11
+ import Terminal from "./lib/components/Terminal.svelte";
12
+ import Button from "./lib/components/ui/button.svelte";
13
+
14
+ interface Props {
15
+ tab: WorkspaceTab | null;
16
+ openTab: (tab: WorkspaceTab) => void;
17
+ closeTab: (tab: WorkspaceTab) => void;
18
+ }
19
+
20
+ let { tab, openTab, closeTab }: Props = $props();
21
+
22
+ const DIFF_OPTIONS = {
23
+ outputFormat: "side-by-side" as const,
24
+ drawFileList: false,
25
+ matching: "lines" as const,
26
+ };
27
+
28
+ const activeWidget = $derived(tab?.kind === "widget" ? flow.widget(tab.resourceId) : null);
29
+ const pendingSteering = $derived(flow.steering.filter((note) => note.status === "new").length);
30
+ const focusedFile = $derived(tab?.kind === "file" ? flow.file(tab.resourceId) : null);
31
+ const checkpoint = $derived(
32
+ tab?.kind === "checkpoint" ? flow.checkpoint(tab.resourceId) : null,
33
+ );
34
+ const conversationWidget = $derived(
35
+ flow.conversation === null ? null : flow.widget(flow.conversation.widgetId),
36
+ );
37
+
38
+ function openWidget(entry: FlowWidget): void {
39
+ openTab({ id: `widget:${entry.id}`, kind: "widget", resourceId: entry.id, title: entry.concept });
40
+ }
41
+
42
+ function openFile(file: FlowFile): void {
43
+ openTab({
44
+ id: `file:${file.path}`,
45
+ kind: "file",
46
+ resourceId: file.path,
47
+ title: file.path.split("/").pop() ?? file.path,
48
+ });
49
+ }
50
+
51
+ function submitAnswer(pending: RunQuestion, target: "goal" | "implementation" | "conversation"): void {
52
+ flow.answerQuestion(pending, flow.answers[target], target);
53
+ }
54
+
55
+ const hasGoal = $derived(flow.goal.trim().length > 0);
56
+ const goalHeaderState = $derived.by(() => {
57
+ if (flow.goalCreation !== null) {
58
+ return { state: "running", tone: "accent", label: "Interview running" };
59
+ }
60
+ if (flow.goalDirty) {
61
+ return { state: "dirty", tone: "warning", label: "Unsaved changes" };
62
+ }
63
+ if (hasGoal) {
64
+ return { state: "saved", tone: "success", label: "Saved" };
65
+ }
66
+ return { state: "empty", tone: null, label: "No goal yet" };
67
+ });
68
+ </script>
69
+
70
+ {#snippet questionPanel(
71
+ pending: RunQuestion,
72
+ target: "goal" | "implementation" | "conversation",
73
+ testid: string,
74
+ )}
75
+ <div class="flow-question" data-testid={testid} data-target={target}>
76
+ <p class="muted" style="margin: 0 0 6px;">{pending.message}</p>
77
+ {#if pending.options.length > 0}
78
+ <div class="choice-list">
79
+ {#each pending.options as option (option.label)}
80
+ <button
81
+ type="button"
82
+ class="choice"
83
+ data-testid="flow-choice"
84
+ data-command={`flow.answer.submit.${target}`}
85
+ onclick={() => flow.answerQuestion(pending, option.label, target)}
86
+ >
87
+ <span class="choice__label">
88
+ {option.label}
89
+ {#if option.recommended}
90
+ <span class="choice__recommended">Recommended</span>
91
+ {/if}
92
+ </span>
93
+ {#if option.description}
94
+ <span class="choice__description">{option.description}</span>
95
+ {/if}
96
+ </button>
97
+ {/each}
98
+ </div>
99
+ {/if}
100
+ <form
101
+ class="answer-form"
102
+ style="margin-top: 6px;"
103
+ onsubmit={(event) => {
104
+ event.preventDefault();
105
+ submitAnswer(pending, target);
106
+ }}
107
+ >
108
+ <input
109
+ id={`flow-answer-${target}`}
110
+ class="textarea"
111
+ style="min-height: 28px; padding: 4px 6px;"
112
+ type="text"
113
+ value={flow.answers[target]}
114
+ oninput={(event) => (flow.answers[target] = event.currentTarget.value)}
115
+ placeholder="Your answer"
116
+ aria-label="Your answer"
117
+ data-testid="flow-answer-input"
118
+ />
119
+ <Button
120
+ variant="default"
121
+ type="submit"
122
+ data-command={`flow.answer.submit.${target}`}
123
+ data-testid="flow-submit-answer"
124
+ disabled={flow.answers[target].trim().length === 0}
125
+ >
126
+ Submit answer
127
+ </Button>
128
+ </form>
129
+ </div>
130
+ {/snippet}
131
+
132
+ {#snippet diffStack(files: FlowFile[])}
133
+ <div
134
+ class="diff-html"
135
+ class:d2h-dark-color-scheme={layout.theme === "dark"}
136
+ data-testid="flow-diff"
137
+ style="height: 100%; overflow: auto;"
138
+ >
139
+ {#each files as file (file.path)}
140
+ <div class="flow-diff-file">
141
+ <p class="row-sm" style="margin: 4px 6px;">
142
+ <code>{file.path}</code>
143
+ <span class="badge" data-tone={file.status === "deleted" ? "error" : "accent"}>
144
+ {file.status}
145
+ </span>
146
+ <span class="mono muted">+{file.insertions}/-{file.deletions}</span>
147
+ </p>
148
+ <div>{@html diffHtml(file.diff, DIFF_OPTIONS)}</div>
149
+ </div>
150
+ {:else}
151
+ <p class="empty">No diff is available for this selection.</p>
152
+ {/each}
153
+ </div>
154
+ {/snippet}
155
+
156
+ {#snippet widgetDetail(entry: FlowWidget)}
157
+ <section
158
+ class="panel"
159
+ data-testid="flow-widget-detail"
160
+ data-widget-id={entry.id}
161
+ data-category={entry.category}
162
+ >
163
+ <div class="panel__header">
164
+ <h2 class="panel__title">{entry.concept}</h2>
165
+ <div class="row-sm">
166
+ <span class="badge" data-tone="accent">{entry.category}</span>
167
+ <Button
168
+ variant="outline"
169
+ data-command={`tab.close.flow.${tab?.id ?? ""}`}
170
+ data-testid="close-flow-widget-detail"
171
+ onclick={() => {
172
+ if (tab !== null) closeTab(tab);
173
+ }}
174
+ >
175
+ Close
176
+ </Button>
177
+ </div>
178
+ </div>
179
+ <div class="panel__body stack-sm">
180
+ <p style="margin: 0;">{entry.summary}</p>
181
+ <p class="muted" style="margin: 0;">{entry.rationale}</p>
182
+ <div>
183
+ <div class="field__label">Files</div>
184
+ <ul class="history-list">
185
+ {#each flow.detailFiles(entry) as file (file.path)}
186
+ <li>
187
+ <button
188
+ type="button"
189
+ class="sidebar-item"
190
+ data-testid="flow-widget-file"
191
+ data-command={`flow.file.open.${file.path}`}
192
+ onclick={() => openFile(file)}
193
+ >
194
+ <code class="sidebar-item__label">{file.path}</code>
195
+ <span class="badge" data-tone={file.status === "deleted" ? "error" : "accent"}>
196
+ {file.status}
197
+ </span>
198
+ <span class="mono muted">+{file.insertions}/-{file.deletions}</span>
199
+ </button>
200
+ </li>
201
+ {/each}
202
+ </ul>
203
+ </div>
204
+ <div class="row-sm">
205
+ <Button
206
+ variant="secondary"
207
+ data-command={`flow.widget.accept.${entry.id}`}
208
+ data-testid="flow-widget-accept"
209
+ onclick={() => flow.toggleWidget(entry)}
210
+ >
211
+ {entry.accepted ? "Unaccept change" : "Accept change"}
212
+ </Button>
213
+ <Button
214
+ variant="secondary"
215
+ data-command={`flow.widget.discuss.${entry.id}`}
216
+ data-testid="flow-widget-discuss"
217
+ onclick={() => flow.discuss(entry)}
218
+ >
219
+ Discuss
220
+ </Button>
221
+ <Button
222
+ variant="outline"
223
+ data-command={`flow.widget.refuse.${entry.id}`}
224
+ data-testid="flow-widget-refuse"
225
+ onclick={() => flow.startRefuse(entry)}
226
+ >
227
+ Refuse
228
+ </Button>
229
+ </div>
230
+ {#if flow.refusingId === entry.id}
231
+ <div class="flow-refuse" data-testid="flow-refuse" style="margin-top: 6px;">
232
+ <textarea
233
+ class="textarea"
234
+ data-testid="refuse-widget-reason"
235
+ placeholder="Optional reason"
236
+ aria-label="Reason for refusing this change"
237
+ bind:value={flow.refuseReason}
238
+ ></textarea>
239
+ <div class="row-sm" style="justify-content: flex-end; margin-top: 4px;">
240
+ <Button
241
+ variant="destructive"
242
+ data-command={`flow.widget.refuse.confirm.${entry.id}`}
243
+ data-testid="confirm-refuse-widget"
244
+ onclick={() => flow.confirmRefuse(entry)}
245
+ >
246
+ Confirm refuse
247
+ </Button>
248
+ <Button
249
+ variant="outline"
250
+ data-command={`flow.widget.refuse.cancel.${entry.id}`}
251
+ data-testid="cancel-refuse-widget"
252
+ onclick={() => flow.cancelRefuse()}
253
+ >
254
+ Cancel
255
+ </Button>
256
+ </div>
257
+ </div>
258
+ {/if}
259
+ </div>
260
+ </section>
261
+ {/snippet}
262
+
263
+ {#snippet widgetsSection()}
264
+ <section class="panel" data-testid="flow-widgets-section">
265
+ <div class="panel__header">
266
+ <h2 class="panel__title">Change widgets</h2>
267
+ <span class="muted">{flow.widgets.length}</span>
268
+ </div>
269
+ <div class="panel__body panel__body--flush">
270
+ {#each flow.widgets as widget (widget.id)}
271
+ <div
272
+ class="flow-widget"
273
+ data-testid="flow-widget"
274
+ data-widget-id={widget.id}
275
+ data-accepted={widget.accepted ? "true" : "false"}
276
+ data-category={widget.category}
277
+ data-command={`flow.widget.open.${widget.id}`}
278
+ >
279
+ <span class="badge" data-tone="accent" data-category={widget.category}>
280
+ {widget.category}
281
+ </span>
282
+ <button
283
+ type="button"
284
+ class="sidebar-item grow"
285
+ data-testid="flow-widget-open"
286
+ data-command={`flow.widget.open.${widget.id}`}
287
+ onclick={() => openWidget(widget)}
288
+ >
289
+ <span class="sidebar-item__label">{widget.concept}</span>
290
+ <span class="muted" style="max-width: 40%; overflow: hidden; text-overflow: ellipsis;">
291
+ {widget.summary}
292
+ </span>
293
+ <span class="mono muted">{widget.files.length} files</span>
294
+ </button>
295
+ <label class="row-sm" style="gap: 4px;">
296
+ <input
297
+ type="checkbox"
298
+ checked={widget.accepted}
299
+ data-testid="flow-widget-accept"
300
+ data-command={`flow.widget.accept.${widget.id}`}
301
+ onchange={() => flow.toggleWidget(widget)}
302
+ />
303
+ <span class="muted">accepted</span>
304
+ </label>
305
+ <Button
306
+ variant="secondary"
307
+ size="sm"
308
+ data-command={`flow.widget.discuss.${widget.id}`}
309
+ data-testid="flow-widget-discuss"
310
+ onclick={() => flow.discuss(widget)}
311
+ >
312
+ Discuss
313
+ </Button>
314
+ <Button
315
+ variant="outline"
316
+ size="sm"
317
+ data-command={`flow.widget.refuse.${widget.id}`}
318
+ data-testid="flow-widget-refuse"
319
+ onclick={() => flow.startRefuse(widget)}
320
+ >
321
+ Refuse
322
+ </Button>
323
+ </div>
324
+ {#if flow.refusingId === widget.id}
325
+ <div class="flow-refuse" data-testid="flow-refuse" style="padding: 6px;">
326
+ <textarea
327
+ class="textarea"
328
+ data-testid="refuse-widget-reason"
329
+ placeholder="Optional reason"
330
+ aria-label="Reason for refusing this change"
331
+ bind:value={flow.refuseReason}
332
+ ></textarea>
333
+ <div class="row-sm" style="justify-content: flex-end; margin-top: 4px;">
334
+ <Button
335
+ variant="destructive"
336
+ data-command={`flow.widget.refuse.confirm.${widget.id}`}
337
+ data-testid="confirm-refuse-widget"
338
+ onclick={() => flow.confirmRefuse(widget)}
339
+ >
340
+ Confirm refuse
341
+ </Button>
342
+ <Button
343
+ variant="outline"
344
+ data-command={`flow.widget.refuse.cancel.${widget.id}`}
345
+ data-testid="cancel-refuse-widget"
346
+ onclick={() => flow.cancelRefuse()}
347
+ >
348
+ Cancel
349
+ </Button>
350
+ </div>
351
+ </div>
352
+ {/if}
353
+ {:else}
354
+ <p class="empty">No change widgets yet.</p>
355
+ {/each}
356
+ </div>
357
+ </section>
358
+ {/snippet}
359
+
360
+ {#snippet goalPanel()}
361
+ <section
362
+ class="panel flow-goal-panel"
363
+ class:flow-goal-panel--collapsed={flow.goalCreation !== null}
364
+ data-testid="flow-goal"
365
+ data-collapsed={flow.goalCreation === null ? "false" : "true"}
366
+ >
367
+ <div class="panel__header flow-goal__header">
368
+ <div class="flow-goal__identity">
369
+ <h2 class="panel__title flow-goal__title">Flow goal</h2>
370
+ <span
371
+ class="badge flow-goal__state"
372
+ data-state={goalHeaderState.state}
373
+ data-tone={goalHeaderState.tone}
374
+ data-testid="flow-goal-state"
375
+ >
376
+ {goalHeaderState.label}
377
+ </span>
378
+ </div>
379
+ <div class="row-sm flow-goal__actions">
380
+ <Button
381
+ variant={hasGoal ? "outline" : "default"}
382
+ data-command="flow.goal.create"
383
+ data-testid="create-flow-goal"
384
+ onclick={() => flow.createGoal()}
385
+ >
386
+ {hasGoal ? "Recreate flow goal" : "Create flow goal"}
387
+ </Button>
388
+ <Button
389
+ variant={flow.goalDirty ? "default" : "secondary"}
390
+ data-command="flow.goal.save"
391
+ data-testid="save-flow-goal"
392
+ disabled={!flow.goalDirty}
393
+ onclick={() => flow.saveGoal()}
394
+ >
395
+ Save
396
+ </Button>
397
+ </div>
398
+ </div>
399
+ {#if flow.goalCreation === null}
400
+ <div class="panel__body flow-goal-editor">
401
+ <MonacoEditor
402
+ value={flow.goalDraft}
403
+ label="Flow goal"
404
+ data-testid="flow-goal-text"
405
+ dirty={flow.goalDirty}
406
+ onchange={(text) => flow.setGoalDraft(text)}
407
+ onsave={() => flow.saveGoal()}
408
+ />
409
+ </div>
410
+ {/if}
411
+ </section>
412
+ {/snippet}
413
+
414
+ {#snippet overview()}
415
+ <div class="scroll-column flow-overview">
416
+ {@render goalPanel()}
417
+
418
+ {#if flow.goalCreation !== null}
419
+ {@const creation = flow.goalCreation}
420
+ <section class="panel" data-testid="flow-goal-active">
421
+ <div class="panel__header"><h2 class="panel__title">Goal interview</h2></div>
422
+ <div class="panel__body stack-sm">
423
+ <div class="flow-goal-activity">
424
+ <Terminal
425
+ lines={creation.activity}
426
+ data-testid="flow-goal-activity"
427
+ label="Flow goal creation activity"
428
+ />
429
+ </div>
430
+ {#if creation.question !== null}
431
+ {@render questionPanel(creation.question, "goal", "flow-goal-question")}
432
+ {/if}
433
+ </div>
434
+ </section>
435
+ {/if}
436
+
437
+ <section class="panel" data-testid="flow-session">
438
+ <div class="panel__header">
439
+ <h2 class="panel__title">Session</h2>
440
+ <div class="row-sm">
441
+ {#if flow.status !== "none"}
442
+ <span class="badge" data-testid="flow-status" data-status={flow.status}>
443
+ {flow.status}
444
+ </span>
445
+ {/if}
446
+ <Button
447
+ variant="default"
448
+ data-command="flow.start"
449
+ data-testid="start-flow-implementation"
450
+ title={flow.startDisabled ? "A flow goal and an idle session are required." : undefined}
451
+ disabled={flow.startDisabled}
452
+ onclick={() => flow.startImplementation()}
453
+ >
454
+ Start implementation
455
+ </Button>
456
+ <Button
457
+ variant="destructive"
458
+ data-command="flow.stop"
459
+ data-testid="stop-flow-implementation"
460
+ disabled={flow.stopDisabled}
461
+ onclick={() => flow.stopImplementation()}
462
+ >
463
+ Stop implementation
464
+ </Button>
465
+ </div>
466
+ </div>
467
+ <div class="panel__body">
468
+ <dl class="kv-grid">
469
+ {#if flow.sessionId !== null}
470
+ <dt>Session</dt>
471
+ <dd><code>{flow.sessionId}</code></dd>
472
+ {/if}
473
+ {#if flow.worktree !== null}
474
+ <dt>Worktree</dt>
475
+ <dd><code data-testid="flow-worktree">{flow.worktree}</code></dd>
476
+ {/if}
477
+ {#if flow.branch !== null}
478
+ <dt>Branch</dt>
479
+ <dd><code data-testid="flow-branch">{flow.branch}</code></dd>
480
+ {/if}
481
+ {#if flow.baseRevision !== null}
482
+ <dt>Base revision</dt>
483
+ <dd><code data-testid="flow-base-revision">{flow.baseRevision}</code></dd>
484
+ {/if}
485
+ </dl>
486
+ {#if flow.question !== null}
487
+ {@render questionPanel(flow.question, "implementation", "flow-question")}
488
+ {/if}
489
+ </div>
490
+ </section>
491
+
492
+ <section class="panel" data-testid="flow-pr-section">
493
+ <div class="panel__header">
494
+ <h2 class="panel__title">Pull request</h2>
495
+ <div class="row-sm">
496
+ <Button
497
+ variant="secondary"
498
+ data-command="flow.pr.retry"
499
+ data-testid="retry-flow-pr"
500
+ disabled={flow.prError === null}
501
+ onclick={() => flow.retryPr()}
502
+ >
503
+ Retry PR
504
+ </Button>
505
+ <Button
506
+ variant="secondary"
507
+ data-command="flow.pr.ready"
508
+ data-testid="mark-flow-pr-ready"
509
+ title={flow.readyDisabled ? "Only a stopped session with a draft PR can be marked ready." : undefined}
510
+ disabled={flow.readyDisabled}
511
+ onclick={() => flow.markPrReady()}
512
+ >
513
+ Mark ready
514
+ </Button>
515
+ <Button
516
+ variant="default"
517
+ data-command="flow.pr.merge"
518
+ data-testid="merge-flow-pr"
519
+ title={flow.mergeDisabled ? "Only a stopped session with a live PR can be merged." : undefined}
520
+ disabled={flow.mergeDisabled}
521
+ onclick={() => flow.mergePr()}
522
+ >
523
+ Merge
524
+ </Button>
525
+ </div>
526
+ </div>
527
+ <div class="panel__body">
528
+ {#if flow.pr === null}
529
+ <p class="empty">No pull request yet.</p>
530
+ {:else}
531
+ <p class="row-sm" data-testid="flow-pr" data-state={flow.pr.state}>
532
+ <span class="muted">Forge</span><span>{flow.pr.forge}</span>
533
+ <span class="muted">Number</span><span class="mono">#{flow.pr.number}</span>
534
+ <span class="muted">State</span><span>{flow.pr.state}</span>
535
+ {#if flow.isHttpUrl(flow.pr.url)}
536
+ <a href={flow.pr.url} target="_blank" rel="noopener noreferrer" data-testid="flow-pr-link">
537
+ {flow.pr.url}
538
+ </a>
539
+ {/if}
540
+ </p>
541
+ {/if}
542
+ {#if flow.prError !== null}
543
+ <p class="message" data-severity="error" data-testid="flow-pr-error" role="alert">
544
+ {flow.prError}
545
+ </p>
546
+ {/if}
547
+ {#if flow.status === "merged" && flow.leftover !== null}
548
+ <p class="muted" data-testid="flow-leftover">{flow.leftover}</p>
549
+ {/if}
550
+ </div>
551
+ </section>
552
+
553
+ {#if flow.verification !== null}
554
+ <section class="panel" data-testid="flow-verification" data-status={flow.verification.status}>
555
+ <div class="panel__header">
556
+ <h2 class="panel__title">Verification</h2>
557
+ <span class="badge" data-tone={flow.verification.status === "failed" ? "error" : "success"}>
558
+ {flow.verification.status}
559
+ </span>
560
+ </div>
561
+ <div class="panel__body stack-xs">
562
+ <p class="mono" style="margin: 0;">{flow.verification.command}</p>
563
+ <p class="muted" style="margin: 0;">{flow.verification.timestamp}</p>
564
+ <pre class="verification-output">{flow.outputTail(flow.verification.output)}</pre>
565
+ </div>
566
+ </section>
567
+ {/if}
568
+
569
+ {@render widgetsSection()}
570
+
571
+ <section class="panel" data-testid="flow-files-section">
572
+ <div class="panel__header">
573
+ <h2 class="panel__title">Changed files</h2>
574
+ <span class="muted">{flow.files.length}</span>
575
+ </div>
576
+ <div class="panel__body panel__body--flush">
577
+ {#each flow.files as file (file.path)}
578
+ <button
579
+ type="button"
580
+ class="sidebar-item"
581
+ data-testid="flow-file"
582
+ data-path={file.path}
583
+ data-status={file.status}
584
+ data-command={`flow.file.open.${file.path}`}
585
+ onclick={() => openFile(file)}
586
+ >
587
+ <code class="sidebar-item__label">{file.path}</code>
588
+ <span class="badge" data-tone={file.status === "deleted" ? "error" : "accent"}>
589
+ {file.status}
590
+ </span>
591
+ <span class="mono muted">+{file.insertions}/-{file.deletions}</span>
592
+ </button>
593
+ {:else}
594
+ <p class="empty">No changed files.</p>
595
+ {/each}
596
+ </div>
597
+ </section>
598
+
599
+ {#if flow.blockers.length > 0}
600
+ <section class="panel" data-testid="flow-blockers">
601
+ <div class="panel__header"><h2 class="panel__title">Blockers</h2></div>
602
+ <div class="panel__body panel__body--flush">
603
+ {#each flow.blockers as blocker (blocker.id)}
604
+ <div
605
+ class="flow-blocker sidebar-item"
606
+ id={`flow-blocker-${blocker.id}`}
607
+ tabindex="-1"
608
+ data-testid="flow-blocker"
609
+ data-dismissed={blocker.dismissed ? "true" : "false"}
610
+ >
611
+ <span class="mono muted">{blocker.timestamp}</span>
612
+ <span class="sidebar-item__label">{blocker.text}</span>
613
+ {#if blocker.dismissed}
614
+ <span class="badge" data-tone="success">Dismissed</span>
615
+ {:else}
616
+ <Button
617
+ variant="outline"
618
+ size="sm"
619
+ data-command={`flow.blocker.dismiss.${blocker.id}`}
620
+ data-testid="dismiss-flow-blocker"
621
+ onclick={() => flow.dismissBlocker(blocker)}
622
+ >
623
+ Dismiss
624
+ </Button>
625
+ {/if}
626
+ </div>
627
+ {/each}
628
+ </div>
629
+ </section>
630
+ {/if}
631
+
632
+ <section class="panel flow-activity-panel" data-testid="flow-activity-section">
633
+ <div class="panel__header">
634
+ <h2 class="panel__title">Implementation activity</h2>
635
+ {#if flow.status !== "none"}
636
+ <span class="badge" data-tone={statusTone(flow.status)}>
637
+ {flow.status}
638
+ </span>
639
+ {/if}
640
+ </div>
641
+ <div class="panel__body flow-activity-body">
642
+ <Terminal
643
+ lines={flow.activity}
644
+ data-testid="flow-activity"
645
+ label="Implementation activity"
646
+ />
647
+ </div>
648
+ </section>
649
+ </div>
650
+ {/snippet}
651
+
652
+ <div class="flow-mode mode-body" data-testid="flow-view" data-status={flow.status}>
653
+ {#if flow.errorMessage !== null}
654
+ <p class="message" data-testid="flow-error" data-severity="error" role="alert">
655
+ {flow.errorMessage}
656
+ </p>
657
+ {/if}
658
+ {#if flow.noticeMessage !== null || flow.notice !== null}
659
+ <p class="message" data-testid="flow-notice" data-severity="notice" role="status">
660
+ {flow.noticeMessage ?? flow.notice}
661
+ </p>
662
+ {/if}
663
+
664
+ {#if tab === null}
665
+ <p class="empty">No tab is open. Choose a surface from the sidebar.</p>
666
+ {:else if tab.kind === "overview"}
667
+ {@render overview()}
668
+ {:else if tab.kind === "decisions"}
669
+ <div class="scroll-column">
670
+ <section class="panel" data-testid="flow-decisions-section">
671
+ <div class="panel__header flow-decisions__header">
672
+ <div class="flow-decisions__identity">
673
+ <h2 class="panel__title flow-decisions__title">Decision board</h2>
674
+ <span
675
+ class="badge flow-decisions__state"
676
+ data-state={flow.decisionsDirty
677
+ ? "dirty"
678
+ : flow.decisions.trim().length > 0
679
+ ? "saved"
680
+ : "empty"}
681
+ data-tone={flow.decisionsDirty
682
+ ? "warning"
683
+ : flow.decisions.trim().length > 0
684
+ ? "success"
685
+ : undefined}
686
+ data-testid="flow-decisions-state"
687
+ >
688
+ {flow.decisionsDirty
689
+ ? "Unsaved changes"
690
+ : flow.decisions.trim().length > 0
691
+ ? "Saved"
692
+ : "No decisions yet"}
693
+ </span>
694
+ </div>
695
+ <div class="row-sm flow-decisions__actions">
696
+ <Button
697
+ variant={flow.decisionsDirty ? "default" : "secondary"}
698
+ data-command="flow.decisions.save"
699
+ data-testid="save-flow-decisions"
700
+ disabled={!flow.decisionsDirty}
701
+ title={flow.decisionsDirty ? "Save the decision board." : "No unsaved changes."}
702
+ onclick={() => flow.saveDecisions()}
703
+ >
704
+ Save
705
+ </Button>
706
+ </div>
707
+ </div>
708
+ <div class="panel__body" style="height: 420px;">
709
+ <MonacoEditor
710
+ value={flow.decisionsDraft}
711
+ label="Decision board"
712
+ data-testid="flow-decisions"
713
+ dirty={flow.decisionsDirty}
714
+ onchange={(text) => flow.setDecisionsDraft(text)}
715
+ onsave={() => flow.saveDecisions()}
716
+ />
717
+ </div>
718
+ </section>
719
+ </div>
720
+ {:else if tab.kind === "steering"}
721
+ <div class="scroll-column">
722
+ <section class="panel" data-testid="flow-steering">
723
+ <div class="panel__header">
724
+ <h2 class="panel__title">Steering</h2>
725
+ <span
726
+ class="panel__subtitle steering-state"
727
+ data-testid="flow-steering-state"
728
+ data-state={pendingSteering > 0
729
+ ? "waiting"
730
+ : flow.steering.length > 0
731
+ ? "read"
732
+ : "empty"}
733
+ >
734
+ {#if pendingSteering > 0}
735
+ {pendingSteering} note{pendingSteering === 1 ? "" : "s"} waiting for the next checkpoint
736
+ {:else if flow.steering.length > 0}
737
+ All notes read
738
+ {:else}
739
+ Read at the next checkpoint
740
+ {/if}
741
+ </span>
742
+ </div>
743
+ <div class="panel__body stack-sm">
744
+ <div class="steering-composer">
745
+ <textarea
746
+ id="flow-steering-text"
747
+ class="textarea"
748
+ data-testid="flow-steering-text"
749
+ placeholder="Steer the implementation"
750
+ aria-label="Steering note"
751
+ bind:value={flow.steeringDraft}
752
+ ></textarea>
753
+ <div class="steering-composer__actions">
754
+ <Button
755
+ variant="default"
756
+ data-command="flow.steering.send"
757
+ data-testid="send-flow-steering"
758
+ disabled={flow.steeringDraft.trim().length === 0}
759
+ onclick={() => flow.sendSteering()}
760
+ >
761
+ Send
762
+ </Button>
763
+ </div>
764
+ </div>
765
+ {#each flow.steering as note (note.id)}
766
+ <div
767
+ class="steering-note"
768
+ data-testid="flow-steering-note"
769
+ data-status={note.status}
770
+ >
771
+ <div class="steering-meta">
772
+ <span>{note.source}</span>
773
+ <span>{note.timestamp}</span>
774
+ <span>{note.status}</span>
775
+ </div>
776
+ <p style="margin: 2px 0 0;">{note.text}</p>
777
+ </div>
778
+ {:else}
779
+ <p class="empty">No steering notes yet.</p>
780
+ {/each}
781
+ </div>
782
+ </section>
783
+ </div>
784
+ {:else if tab.kind === "history"}
785
+ <div class="scroll-column">
786
+ <section class="panel" data-testid="flow-history">
787
+ <div class="panel__header"><h2 class="panel__title">Past Flow sessions</h2></div>
788
+ <div class="panel__body panel__body--flush">
789
+ {#each flow.history as entry (entry.sessionId)}
790
+ <div class="history-entry" data-testid="flow-history-entry">
791
+ <code>{entry.sessionId}</code>
792
+ <span>{entry.goal}</span>
793
+ {#if entry.pr !== null && flow.isHttpUrl(entry.pr.url)}
794
+ <a href={entry.pr.url} target="_blank" rel="noopener noreferrer">
795
+ PR #{entry.pr.number}
796
+ </a>
797
+ {:else}
798
+ <span class="muted">no PR</span>
799
+ {/if}
800
+ </div>
801
+ {:else}
802
+ <p class="empty">No past Flow sessions.</p>
803
+ {/each}
804
+ </div>
805
+ </section>
806
+ </div>
807
+ {:else if tab.kind === "widget"}
808
+ {#if activeWidget === null}
809
+ <p class="empty">This change widget is no longer current.</p>
810
+ {:else}
811
+ {@const detailFiles = flow.detailFiles(activeWidget)}
812
+ <DetailSplit id="flow-widget" label="Widget diff">
813
+ {#snippet main()}
814
+ {@render widgetDetail(activeWidget)}
815
+ {/snippet}
816
+ {#snippet detail()}
817
+ <section class="panel" style="height: 100%;">
818
+ <div class="panel__header">
819
+ <h2 class="panel__title">Diff</h2>
820
+ <Button
821
+ variant="outline"
822
+ data-command={`tab.close.flow.${tab.id}`}
823
+ data-testid="close-flow-diff"
824
+ onclick={() => closeTab(tab)}
825
+ >
826
+ Close diff
827
+ </Button>
828
+ </div>
829
+ {@render diffStack(detailFiles)}
830
+ </section>
831
+ {/snippet}
832
+ </DetailSplit>
833
+ {/if}
834
+ {:else if tab.kind === "file"}
835
+ {#if focusedFile === null}
836
+ <p class="empty">This file is no longer part of the change.</p>
837
+ {:else}
838
+ <DetailSplit id="flow-file" label="File diff">
839
+ {#snippet main()}
840
+ <section class="panel" style="height: 100%;">
841
+ <div class="panel__header">
842
+ <h2 class="panel__title mono">{focusedFile.path}</h2>
843
+ <Button
844
+ variant="outline"
845
+ data-command={`tab.close.flow.${tab.id}`}
846
+ data-testid="close-flow-diff"
847
+ onclick={() => closeTab(tab)}
848
+ >
849
+ Close diff
850
+ </Button>
851
+ </div>
852
+ {@render diffStack([focusedFile])}
853
+ </section>
854
+ {/snippet}
855
+ {#snippet detail()}
856
+ <section class="panel" style="height: 100%;">
857
+ <div class="panel__header"><h2 class="panel__title">File</h2></div>
858
+ <div class="panel__body">
859
+ <dl class="kv-grid">
860
+ <dt>Path</dt><dd><code>{focusedFile.path}</code></dd>
861
+ <dt>Status</dt><dd>{focusedFile.status}</dd>
862
+ <dt>Insertions</dt><dd class="mono">+{focusedFile.insertions}</dd>
863
+ <dt>Deletions</dt><dd class="mono">-{focusedFile.deletions}</dd>
864
+ </dl>
865
+ {#if flow.verification !== null}
866
+ <p class="muted" style="margin-top: 8px;">
867
+ Last verification
868
+ <span class="badge" data-tone={flow.verification.status === "failed" ? "error" : "success"}>
869
+ {flow.verification.status}
870
+ </span>
871
+ </p>
872
+ {/if}
873
+ </div>
874
+ </section>
875
+ {/snippet}
876
+ </DetailSplit>
877
+ {/if}
878
+ {:else if tab.kind === "checkpoint"}
879
+ {#if checkpoint === null}
880
+ <p class="empty">This checkpoint is no longer listed.</p>
881
+ {:else}
882
+ <div class="scroll-column">
883
+ <section class="panel" data-testid="flow-checkpoints">
884
+ <div class="panel__header">
885
+ <h2 class="panel__title">Checkpoint</h2>
886
+ <span class="badge" data-tone={checkpoint.verification === "failed" ? "error" : "success"}>
887
+ {checkpoint.verification}
888
+ </span>
889
+ </div>
890
+ <div class="panel__body" data-testid="flow-checkpoint">
891
+ <dl class="kv-grid">
892
+ <dt>Summary</dt><dd>{checkpoint.summary}</dd>
893
+ <dt>Time</dt><dd class="mono">{checkpoint.timestamp}</dd>
894
+ <dt>Files</dt><dd>{checkpoint.files}</dd>
895
+ <dt>Commit</dt><dd><code>{checkpoint.commit}</code></dd>
896
+ <dt>Verification</dt><dd>{checkpoint.verification}</dd>
897
+ </dl>
898
+ </div>
899
+ </section>
900
+ </div>
901
+ {/if}
902
+ {:else if tab.kind === "conversation"}
903
+ {#if flow.conversation === null}
904
+ <p class="empty">No widget conversation is open.</p>
905
+ {:else}
906
+ {@const convo = flow.conversation}
907
+ <DetailSplit id="flow-conversation" label="Conversation diff">
908
+ {#snippet main()}
909
+ <section class="panel" data-testid="flow-conversation" style="height: 100%;">
910
+ <div class="panel__header">
911
+ <h2 class="panel__title">Widget conversation</h2>
912
+ <div class="row-sm">
913
+ <span class="muted" data-testid="flow-conversation-widget">
914
+ Widget <code>{convo.widgetId}</code>
915
+ </span>
916
+ <Button
917
+ variant="outline"
918
+ data-command="flow.conversation.close"
919
+ data-testid="close-flow-conversation"
920
+ onclick={() => flow.closeConversation()}
921
+ >
922
+ Close
923
+ </Button>
924
+ </div>
925
+ </div>
926
+ <div class="panel__body stack-sm">
927
+ {#if flow.conversationStale}
928
+ <p class="message" data-severity="warning" data-testid="flow-conversation-stale">
929
+ This change is no longer current. The conversation is read-only, but the
930
+ conclusion can still be sent.
931
+ </p>
932
+ {/if}
933
+ <div class="stack-xs" data-testid="flow-conversation-messages">
934
+ {#each convo.messages as entry, index (index)}
935
+ <div class="message" data-role={entry.role}>
936
+ <div class="notification__meta">{entry.role}</div>
937
+ <p style="margin: 0;">{entry.text}</p>
938
+ </div>
939
+ {:else}
940
+ <p class="empty">No messages yet.</p>
941
+ {/each}
942
+ </div>
943
+ <div style="height: 140px;">
944
+ <Terminal
945
+ lines={convo.activity}
946
+ data-testid="flow-conversation-activity"
947
+ label="Conversation activity"
948
+ />
949
+ </div>
950
+ {#if convo.question !== null}
951
+ {@render questionPanel(convo.question, "conversation", "flow-conversation-question")}
952
+ {/if}
953
+ <form
954
+ class="answer-form"
955
+ onsubmit={(event) => {
956
+ event.preventDefault();
957
+ flow.sendConversation();
958
+ }}
959
+ >
960
+ <input
961
+ class="textarea"
962
+ style="min-height: 28px; padding: 4px 6px;"
963
+ type="text"
964
+ bind:value={flow.conversationDraft}
965
+ placeholder="Message the conversation"
966
+ aria-label="Conversation message"
967
+ data-testid="flow-conversation-input"
968
+ />
969
+ <Button
970
+ variant="default"
971
+ type="submit"
972
+ data-command="flow.conversation.send"
973
+ data-testid="send-flow-conversation"
974
+ disabled={flow.conversationDraft.trim().length === 0}
975
+ >
976
+ Send
977
+ </Button>
978
+ </form>
979
+ <div class="field">
980
+ <label class="field__label" for="flow-conclusion">Conclusion</label>
981
+ <textarea
982
+ id="flow-conclusion"
983
+ class="textarea"
984
+ data-testid="flow-conclusion"
985
+ bind:value={flow.conclusionDraft}
986
+ ></textarea>
987
+ </div>
988
+ <div class="row-sm" style="justify-content: flex-end;">
989
+ <Button
990
+ variant="secondary"
991
+ data-command="flow.conclusion.send"
992
+ data-testid="send-flow-conclusion"
993
+ disabled={flow.conclusionDraft.trim().length === 0}
994
+ onclick={() => flow.sendConclusion()}
995
+ >
996
+ Send conclusion to implementation
997
+ </Button>
998
+ </div>
999
+ </div>
1000
+ </section>
1001
+ {/snippet}
1002
+ {#snippet detail()}
1003
+ <section class="panel" style="height: 100%;">
1004
+ <div class="panel__header"><h2 class="panel__title">Widget diff</h2></div>
1005
+ {#if conversationWidget === null}
1006
+ <p class="empty">The widget is no longer current.</p>
1007
+ {:else}
1008
+ {@render diffStack(flow.detailFiles(conversationWidget))}
1009
+ {/if}
1010
+ </section>
1011
+ {/snippet}
1012
+ </DetailSplit>
1013
+ {/if}
1014
+ {/if}
1015
+ </div>