@diffohq/diffo 0.4.1 → 0.6.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 (99) hide show
  1. package/dist/cli.mjs +244 -36
  2. package/dist/client/assets/{abnfDiagram-O67JEVCF-b6l_FGHi.js → abnfDiagram-O67JEVCF-vILxBZcB.js} +1 -1
  3. package/dist/client/assets/{arc-Cz0jZwLH.js → arc-KM37zfEu.js} +1 -1
  4. package/dist/client/assets/architecture-7GRP2DOG-DzlmD0Wj.js +1 -0
  5. package/dist/client/assets/{architectureDiagram-NJMV4G6O-CJdtqKce.js → architectureDiagram-NJMV4G6O-CCe24XcA.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-BEXU5L5S-Ci-lnLGN.js → blockDiagram-BEXU5L5S-GL2esJ0w.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-YGBWAQC7-DDO2mTMB.js → c4Diagram-YGBWAQC7-BcRujrvs.js} +1 -1
  8. package/dist/client/assets/channel-BffjfVqs.js +1 -0
  9. package/dist/client/assets/{chunk-3FUC2YCW-VyswqFt3.js → chunk-3FUC2YCW-Gj4X9KK9.js} +1 -1
  10. package/dist/client/assets/{chunk-5DYCD2WN-B4MBZW5P.js → chunk-5DYCD2WN-DZs-erj_.js} +1 -1
  11. package/dist/client/assets/{chunk-742MDFTN-Cxvmvrid.js → chunk-742MDFTN-Db1JJZ9G.js} +1 -1
  12. package/dist/client/assets/{chunk-7INBJB4K-DkBnRFbH.js → chunk-7INBJB4K-B_wqJ7Xn.js} +1 -1
  13. package/dist/client/assets/{chunk-7M6MHVWA-BvJgBwEi.js → chunk-7M6MHVWA-DuALBizw.js} +1 -1
  14. package/dist/client/assets/{chunk-7PRAP22T-OfiMNi_y.js → chunk-7PRAP22T-D9QvZUm3.js} +1 -1
  15. package/dist/client/assets/{chunk-GTNCS2PH-BodN5cRQ.js → chunk-GTNCS2PH-JFHsSpPU.js} +1 -1
  16. package/dist/client/assets/{chunk-GWA4HPMP-KifxxZcy.js → chunk-GWA4HPMP-CX_60U9Y.js} +1 -1
  17. package/dist/client/assets/{chunk-MBY4JIJT-rw02bHlj.js → chunk-MBY4JIJT-BVqUStmy.js} +1 -1
  18. package/dist/client/assets/{chunk-NETBCI7D-CWjhZoou.js → chunk-NETBCI7D-BoSm0O5f.js} +1 -1
  19. package/dist/client/assets/{chunk-O7XYJQB3-CyxnSfZ3.js → chunk-O7XYJQB3-DG64mbS_.js} +1 -1
  20. package/dist/client/assets/{chunk-UA2S7LBM-BReIFgsu.js → chunk-UA2S7LBM-BL039fcM.js} +1 -1
  21. package/dist/client/assets/{chunk-WEXAMYUT-DHwMy8v7.js → chunk-WEXAMYUT-BPFyuzbi.js} +1 -1
  22. package/dist/client/assets/{chunk-XXDRQBXY-C398QZbS.js → chunk-XXDRQBXY-BzCs4oPB.js} +1 -1
  23. package/dist/client/assets/{chunk-Z7XXMR3K-hEV1GU64.js → chunk-Z7XXMR3K-D9UN9ldM.js} +1 -1
  24. package/dist/client/assets/{chunk-ZIGJFQKS-DJJL7llO.js → chunk-ZIGJFQKS-D47UlTiy.js} +1 -1
  25. package/dist/client/assets/{classDiagram-v2-NBCMYWYE-pSGZI880.js → classDiagram-v2-NBCMYWYE-CXeBds6Z.js} +1 -1
  26. package/dist/client/assets/{core-C05I4wHV.js → core-De6Bel6T.js} +1 -1
  27. package/dist/client/assets/{cose-bilkent-JH36ORCC-DNV8Dqd8.js → cose-bilkent-JH36ORCC-C5C9BMn5.js} +1 -1
  28. package/dist/client/assets/{cynefin-OW5HDTMX-DF-6FOgS.js → cynefin-OW5HDTMX-TQm3krMs.js} +1 -1
  29. package/dist/client/assets/{cynefinDiagram-VND7K2PF-D3-iJq2J.js → cynefinDiagram-VND7K2PF-D9L1g2S1.js} +1 -1
  30. package/dist/client/assets/{dagre-6A5THRUB-C9FDZkaa.js → dagre-6A5THRUB-D9iVd0tg.js} +1 -1
  31. package/dist/client/assets/{diagram-22UHCM2B-CfSJBDyN.js → diagram-22UHCM2B-CD_1yZgj.js} +1 -1
  32. package/dist/client/assets/{diagram-3UASUU5V-D946baYo.js → diagram-3UASUU5V-BLiXkub3.js} +1 -1
  33. package/dist/client/assets/{diagram-ATOU4E4O-CGrjukoA.js → diagram-ATOU4E4O-CnXY0tjf.js} +1 -1
  34. package/dist/client/assets/{diagram-CDSNMT55-B47UPtWq.js → diagram-CDSNMT55-BBnDsrf_.js} +1 -1
  35. package/dist/client/assets/{diagram-MLGK6HIB-D06yEd5R.js → diagram-MLGK6HIB-BUiu4OKD.js} +1 -1
  36. package/dist/client/assets/{diagram-MPIPVDR6-BRUBT2B9.js → diagram-MPIPVDR6-DiI9FWeO.js} +1 -1
  37. package/dist/client/assets/{dist-BfTRyLGm.js → dist-BWYGCgdo.js} +1 -1
  38. package/dist/client/assets/{dist-DVw88SBM.js → dist-Co85aeoI.js} +1 -1
  39. package/dist/client/assets/{ebnfDiagram-ZINNZB2B-BnuExfrd.js → ebnfDiagram-ZINNZB2B-Bg_BLedv.js} +1 -1
  40. package/dist/client/assets/{elk-276RUBZZ-BmdcLjR9.js → elk-276RUBZZ-BO7Zoa7U.js} +1 -1
  41. package/dist/client/assets/{engine-oniguruma-Bw1dOuOO.js → engine-oniguruma-DCFwTJLE.js} +1 -1
  42. package/dist/client/assets/{erDiagram-OPXOYQCR-CKCR3jQT.js → erDiagram-OPXOYQCR-DjBak3Ld.js} +1 -1
  43. package/dist/client/assets/eventmodeling-NTZA5JFV-BA4dpxhq.js +1 -0
  44. package/dist/client/assets/flowDiagram-KWPJA3E3-BXSGu0Q5.js +1 -0
  45. package/dist/client/assets/{ganttDiagram-FUAMR5RP-BwkoweG7.js → ganttDiagram-FUAMR5RP-CEn0Np5L.js} +1 -1
  46. package/dist/client/assets/{gitGraph-4MIJSDKK-M7_hHVyH.js → gitGraph-4MIJSDKK-DVMdaaG2.js} +1 -1
  47. package/dist/client/assets/{gitGraphDiagram-X574FWY7-BVm_sLT_.js → gitGraphDiagram-X574FWY7-B6ZkiY7u.js} +1 -1
  48. package/dist/client/assets/index-6w3rS9cw.js +91 -0
  49. package/dist/client/assets/index-D934d4yh.css +1 -0
  50. package/dist/client/assets/{info-A6RAGUB7-DPBs3m_X.js → info-A6RAGUB7-Idt2RahI.js} +1 -1
  51. package/dist/client/assets/{infoDiagram-VRGFBTTK-DDb3KyOP.js → infoDiagram-VRGFBTTK-CA8T3csq.js} +1 -1
  52. package/dist/client/assets/{ishikawaDiagram-OU5B5YK6-CPt4RDlr.js → ishikawaDiagram-OU5B5YK6-DcgLwVJX.js} +1 -1
  53. package/dist/client/assets/{journeyDiagram-ZHPQQLJL-ECQSHJ0N.js → journeyDiagram-ZHPQQLJL-_AVEW5aE.js} +1 -1
  54. package/dist/client/assets/{kanban-definition-PNTS6WVX-tAmonxA8.js → kanban-definition-PNTS6WVX-CZ2gNo-B.js} +1 -1
  55. package/dist/client/assets/{line-x6vAh41a.js → line-CHew3ngr.js} +1 -1
  56. package/dist/client/assets/{linear-BqczUNss.js → linear-Baz1Hb3u.js} +1 -1
  57. package/dist/client/assets/{mermaid-parser.core-siQPZsry.js → mermaid-parser.core-D2BrkwRE.js} +3 -3
  58. package/dist/client/assets/{mermaid.core-CLHTQVF-.js → mermaid.core-CCFlYVgY.js} +4 -4
  59. package/dist/client/assets/{mindmap-definition-NLK3R4M7-C9fnEdyl.js → mindmap-definition-NLK3R4M7-BXG9w5sC.js} +1 -1
  60. package/dist/client/assets/{packet-AYTQ26CC-DAeK0Q4n.js → packet-AYTQ26CC-BOvg32Rh.js} +1 -1
  61. package/dist/client/assets/{pegDiagram-GJSIUBJH-BrWBFYPO.js → pegDiagram-GJSIUBJH-COukdq7o.js} +1 -1
  62. package/dist/client/assets/{pie-WAS4IAKB-CnieihkO.js → pie-WAS4IAKB-ChHsdIQV.js} +1 -1
  63. package/dist/client/assets/{pieDiagram-5QR66LMP-Bp5f6Eal.js → pieDiagram-5QR66LMP-Deng5oun.js} +1 -1
  64. package/dist/client/assets/{quadrantDiagram-O4NWA36T-ECuLA3RS.js → quadrantDiagram-O4NWA36T-DmEJ83mR.js} +1 -1
  65. package/dist/client/assets/{radar-RG4KPBEZ-BJXkERy0.js → radar-RG4KPBEZ-YxcXBiXc.js} +1 -1
  66. package/dist/client/assets/{railroad-74A4TZTK-Di61fiV5.js → railroad-74A4TZTK-RkYX7A2D.js} +1 -1
  67. package/dist/client/assets/railroad-abnf-HS5TGJTU-CYF7Z2Wu.js +1 -0
  68. package/dist/client/assets/railroad-ebnf-LZEXJU2U-CqV11tet.js +1 -0
  69. package/dist/client/assets/railroad-peg-WCYAUIDC-CPusJj6I.js +1 -0
  70. package/dist/client/assets/{railroadDiagram-XR7U4H2S-Dn2WPOQe.js → railroadDiagram-XR7U4H2S-uj5HQbYy.js} +1 -1
  71. package/dist/client/assets/{requirementDiagram-PLB6GJNP-uuvtvHmq.js → requirementDiagram-PLB6GJNP-C_QtkIlI.js} +1 -1
  72. package/dist/client/assets/{sankeyDiagram-IPEJSGJF-BXk6X0af.js → sankeyDiagram-IPEJSGJF-BfxM5TKv.js} +1 -1
  73. package/dist/client/assets/{sequenceDiagram-PO4LG4MO-BsPoSXhg.js → sequenceDiagram-PO4LG4MO-DXflg1u_.js} +1 -1
  74. package/dist/client/assets/{src-B4djUhOi.js → src-YTrwJCk8.js} +1 -1
  75. package/dist/client/assets/{stateDiagram-v2-GCMORJYK-cCml5k6n.js → stateDiagram-v2-GCMORJYK-BjDmED1Q.js} +1 -1
  76. package/dist/client/assets/{swimlanes-2SLR337P-Dh7v55ZA.js → swimlanes-2SLR337P-BAvnQ6KT.js} +1 -1
  77. package/dist/client/assets/swimlanesDiagram-TC7HE7FX-CbFPwcec.js +8 -0
  78. package/dist/client/assets/{timeline-definition-EJHVYXUP-LFjkFV14.js → timeline-definition-EJHVYXUP-BYemcgr1.js} +1 -1
  79. package/dist/client/assets/{treeView-Q6P3EWNA-RYZHSXeB.js → treeView-Q6P3EWNA-CsuBtqLq.js} +1 -1
  80. package/dist/client/assets/{treemap-WGGIJYW6-UVw_-EPz.js → treemap-WGGIJYW6-DUk1Z6g4.js} +1 -1
  81. package/dist/client/assets/{usecaseDiagram-POWQR4AR-DZfPogyb.js → usecaseDiagram-POWQR4AR-j1nHXvyL.js} +1 -1
  82. package/dist/client/assets/{vennDiagram-UO4OBE2U-CN7Ldg-2.js → vennDiagram-UO4OBE2U-B5eUL-dD.js} +1 -1
  83. package/dist/client/assets/{wardley-WFR3VGLG-CXPbbIy_.js → wardley-WFR3VGLG-1YjpwPJM.js} +1 -1
  84. package/dist/client/assets/{wardleyDiagram-VNRHLVJA-5p0A7_g6.js → wardleyDiagram-VNRHLVJA-C7laBq5A.js} +1 -1
  85. package/dist/client/assets/{xychartDiagram-PMCCYNJV-gJ7xwZXO.js → xychartDiagram-PMCCYNJV-BkYPx-ts.js} +1 -1
  86. package/dist/client/index.html +2 -2
  87. package/package.json +1 -1
  88. package/plugin.json +1 -1
  89. package/skills/diffo/SKILL.md +3 -3
  90. package/dist/client/assets/architecture-7GRP2DOG-BXYREMFQ.js +0 -1
  91. package/dist/client/assets/channel-CtvNwuQX.js +0 -1
  92. package/dist/client/assets/eventmodeling-NTZA5JFV-C7ouopD6.js +0 -1
  93. package/dist/client/assets/flowDiagram-KWPJA3E3-C2o4wCpi.js +0 -1
  94. package/dist/client/assets/index-BEG1ZxrX.css +0 -1
  95. package/dist/client/assets/index-BwDjlzOZ.js +0 -91
  96. package/dist/client/assets/railroad-abnf-HS5TGJTU-DCBYLLLl.js +0 -1
  97. package/dist/client/assets/railroad-ebnf-LZEXJU2U-VWOalqH1.js +0 -1
  98. package/dist/client/assets/railroad-peg-WCYAUIDC-BTTWaHra.js +0 -1
  99. package/dist/client/assets/swimlanesDiagram-TC7HE7FX-q8VF72XO.js +0 -8
package/dist/cli.mjs CHANGED
@@ -73,11 +73,25 @@ function detectSessionPid() {
73
73
  //#endregion
74
74
  //#region src/shared/review.ts
75
75
  const THREAD_INTENTS = ["fix", "question"];
76
+ function parseSuggestedReply(value) {
77
+ if (typeof value !== "string") return void 0;
78
+ const line = value.replace(/\s+/g, " ").trim();
79
+ return line ? line.slice(0, 120) : void 0;
80
+ }
76
81
  /** The thread was started by the agent (its opening message is the agent's).
77
82
  * Drives rendering — avatar, head label — and never changes. */
78
83
  function startedByAgent(thread) {
79
84
  return thread.messages[0]?.author === "agent";
80
85
  }
86
+ /** The agent has this message: it replied after it, or a delivery carried it.
87
+ * Decides what an edit is — a draft fixed in place, or a change of mind the
88
+ * agent has to be told about. */
89
+ function seenByAgent(thread, index) {
90
+ const message = thread.messages[index];
91
+ if (!message) return false;
92
+ if (thread.messages.slice(index + 1).some((m) => m.author === "agent")) return true;
93
+ return thread.deliveredThrough !== void 0 && Date.parse(message.at) <= Date.parse(thread.deliveredThrough);
94
+ }
81
95
  /** Agent-started with nothing of the reviewer's in it. Only these are excluded
82
96
  * from the reviewer's counts, Send, and the finish flush — the moment the
83
97
  * reviewer writes a reply into an agent thread, it carries their feedback and
@@ -274,20 +288,38 @@ const ACK_NEXT_STEP = {
274
288
  /**
275
289
  * The guide doctrine — the one agent comment that orients a cold reader.
276
290
  * Stated once and interpolated into every surface that teaches it (the skill,
277
- * `help agent`, the open-time nudge below), the same way POLL_STANCE keeps the
278
- * poll rules aligned: the surfaces phrase it at different lengths, but these
279
- * invariants cannot drift apart.
291
+ * `help agent`, `help guide`, the open-time nudge below), the same way
292
+ * POLL_STANCE keeps the poll rules aligned: the surfaces phrase it at different
293
+ * lengths, but these invariants cannot drift apart.
294
+ *
295
+ * It is a map, not a tour, written for a reader who never saw the session:
296
+ * the problem comes before the mechanism, and the diagram draws what happens,
297
+ * not which code names which — a change to text alone has no call graph worth
298
+ * drawing. Every other tool infers a summary by reading the diff back; the session that wrote the change still holds what no diff shows
299
+ * — the invariant it had to respect, the seam it chose, the alternative it
300
+ * rejected, what it left alone on purpose — and that is what the reviewer
301
+ * cannot get anywhere else. Reading order is not on the list: layers own it.
280
302
  */
281
303
  const GUIDE = {
282
304
  /** When one is warranted — and that silence is a valid outcome. */
283
305
  when: "multi-file, structural, or subtle — skip when the diff explains itself",
284
- /** What it contains. */
285
- what: "one sentence on what the change does, plus a small ```mermaid diagram if a picture explains the shape better than words",
306
+ /** What it contains: ingredients, not sections — use the ones this change needs. */
307
+ what: "a map of the change, not a tour of it: the problem it solves and what it changes, in a sentence or two a reader who never saw your session follows; the flow it changes, from what sets it off to what someone sees, as a small ```mermaid diagram when a picture beats words — functions where code carries it, people and screens when they are the flow, never a file list or which code references which; what is worth checking as they read — an invariant the change must keep, a judgment call it made, a shortcoming it knows about — said in plain words, never behind a label of your own; and what is safe to skip",
286
308
  /** The line it must not cross. */
287
309
  stance: "Orient reading, never pre-review: no verdicts, nothing is \"fine\"",
310
+ /** What layers took over: the guide never orders the read. */
311
+ order: "no reading order and no file list — layers and the Files tab own those; a change that reads better in order gets a layers suggestion, not a numbered guide",
312
+ /** How much: one screen, and only the parts that apply. */
313
+ budget: "about a hundred words of prose plus the diagram — one screen; use only the parts this change needs, in whatever form fits",
314
+ /** The diagram convention: two tags, and a plain node means anything existing. */
315
+ legend: "in the diagram tag new code `:::new` and changed code `:::changed`, declared by the two classDef lines below; a plain node is anything existing the change now leans on",
288
316
  /** Staleness: the guide is a thread, so updates land under it. */
289
317
  update: "reply to your own guide thread with a short update"
290
318
  };
319
+ /** The two classDef lines the legend refers to, verbatim — pasted at the foot
320
+ * of the diagram. Strokes only, in hexes both renderers honour and both themes
321
+ * read; fills stay the renderer's own, so the diagram still follows the theme. */
322
+ const GUIDE_CLASSDEFS = ["classDef new stroke:#2e9e4f,stroke-width:2px", "classDef changed stroke:#d99a00,stroke-width:2px"];
291
323
  /**
292
324
  * The layers doctrine — the agent's reading plan for a changeset with an order
293
325
  * worth explaining. Same single-source rule as GUIDE: the skill, `help agent`,
@@ -302,7 +334,13 @@ const GUIDE = {
302
334
  */
303
335
  const LAYERS = {
304
336
  /** What one is. */
305
- what: "one step of the change — a coherent unit you would explain in one breath — with the files that belong to it and a one- or two-sentence summary (markdown; a ```mermaid fence renders)",
337
+ what: "one step of the change — a coherent unit you would explain in one breath — with the files that belong to it and a short summary",
338
+ /** What the summary is for — a goal, not a template: the model writes it
339
+ * however this step reads best. The files sit right under it, so retelling
340
+ * them spends the reviewer's attention twice. */
341
+ summary: "say what this layer is about and give the reviewer enough context to review it — the part the files below cannot show on their own. Write it however fits the step, keep it short, and don't retell the diff",
342
+ /** Diagrams when they help: a picture of the step usually beats a paragraph. */
343
+ diagram: "when the step has a shape — a flow, a decision, a before/after — a small ```mermaid diagram usually says it better than prose; tag nodes as the guide does (`:::new`, `:::changed`, plain for anything existing) with the same two classDef lines",
306
344
  /** How to order them. */
307
345
  order: "the order you would explain it, not the order you wrote it — the file that explains the rest first, mechanical consequences last; for a feature, follow the request from entry point to effect; for a refactor, contract first, then consumers",
308
346
  /** When to raise the flag at open — and that not raising it is the common case. */
@@ -329,7 +367,7 @@ const LAYERS = {
329
367
  */
330
368
  function guideNudge(hasGuide) {
331
369
  if (hasGuide) return null;
332
- return `share the URL above with the user right now, as a message line, before anything else. Then, while they open it, orient them if this changeset needs it (${GUIDE.when}): post a guide — one comment on the whole changeset: ${GUIDE.what}: \`${CLI_COMMANDS.guide}\` (no file, so it anchors to the changeset; it appears live at the top of their review). ${GUIDE.stance}.`;
370
+ return `share the URL above with the user right now, as a message line, before anything else. Then, while they open it, orient them if this changeset needs it (${GUIDE.when}): post a guide — one comment on the whole changeset: \`${CLI_COMMANDS.guide}\` (no file, so it anchors to the changeset; it appears live at the top of their review). It is ${GUIDE.what}. Not in it: ${GUIDE.order}. How much: ${GUIDE.budget}. ${GUIDE.stance}. \`diffo help guide\` has the diagram legend and one example.`;
333
371
  }
334
372
  /**
335
373
  * Printed by `diffo` (open) next to the guide nudge while the review has neither
@@ -340,7 +378,7 @@ function guideNudge(hasGuide) {
340
378
  */
341
379
  function layersNudge(review) {
342
380
  if (review.layers || review.layersSuggested) return null;
343
- return `if this changeset reads better in order — ${LAYERS.suggest} — flag it: \`${CLI_COMMANDS.layersSuggest}\`, and offer it in your handoff ("say layers and I'll outline it"). Post the outline only when asked: \`${CLI_COMMANDS.layers}\` — each layer ${LAYERS.what}; ${LAYERS.order}. ${LAYERS.stance}. \`diffo help layers\` has the shape.`;
381
+ return `if this changeset reads better in order — ${LAYERS.suggest} — flag it: \`${CLI_COMMANDS.layersSuggest}\`, and offer it in your handoff ("say layers and I'll outline it"). Post the outline only when asked: \`${CLI_COMMANDS.layers}\` — each layer ${LAYERS.what}; ${LAYERS.summary}; ${LAYERS.diagram}; ${LAYERS.order}. ${LAYERS.stance}. \`diffo help layers\` has the shape.`;
344
382
  }
345
383
  /**
346
384
  * Printed to stderr when a poll takes the review over and the review already
@@ -567,8 +605,9 @@ function threadBlock(thread, index) {
567
605
  `id: ${thread.id}`,
568
606
  ...anchorNote(thread),
569
607
  ...snapshotBlock(thread),
608
+ ...thread.rewound ? ["EDITED: the reviewer rewrote the message marked (edited) and withdrew everything after it, your earlier replies included. Answer the edited version. Code you changed for the withdrawn replies is still in the tree: keep it or revert it as the new message implies, and say which."] : [],
570
609
  "Messages:",
571
- ...thread.messages.map((m) => `- ${m.author}: ${m.text.replace(/\n/g, "\n ")}`)
610
+ ...thread.messages.map((m) => `- ${m.author}${m.editedAt ? " (edited)" : ""}: ${m.text.replace(/\n/g, "\n ")}`)
572
611
  ].join("\n");
573
612
  }
574
613
  function buildThreadPrompt(thread, ctx) {
@@ -611,11 +650,15 @@ function buildLayersRequestPrompt(ctx, existing) {
611
650
  const refresh = existing !== null && existing.length > 0;
612
651
  const titles = refresh ? existing.map((l) => `"${l.title}"`).join(", ") : "";
613
652
  return [
614
- refresh ? `The reviewer asked you to refresh the layers in \`${ctx.repo.name}\` (branch \`${ctx.repo.branch}\`): the code moved since you outlined it, and files outside the outline have been gathering under "Since your review". Re-post the whole list as the change stands now.` : `The reviewer asked for layers in \`${ctx.repo.name}\` (branch \`${ctx.repo.branch}\`): outline the changeset as steps to read in order. The Layers tab reads "The agent is outlining…" until you post.`,
653
+ refresh ? `The reviewer asked you to refresh the layers in \`${ctx.repo.name}\` (branch \`${ctx.repo.branch}\`): the code moved since you outlined it, and files outside the outline have been gathering under "Since your review". Re-post the whole list as the change stands now.` : `The reviewer asked for layers in \`${ctx.repo.name}\` (branch \`${ctx.repo.branch}\`): outline the changeset as steps to read in order. The Layers tab reads "Outlining…" until you post.`,
615
654
  "",
616
655
  ...ctx.changeset ? [specLine(ctx.changeset), ""] : [],
617
656
  ...refresh ? [`The outline they have: ${titles}. Keep a title that still fits its step, rename or drop the ones that don't — ${LAYERS.replace}.`, ""] : [],
618
- `Each layer is ${LAYERS.what}. Order: ${LAYERS.order}. ${LAYERS.mechanical}. ${LAYERS.stance}. Files are whole files, by path relative to the repo root; list every file of the changeset somewhere, or the leftovers land in "Since your review".`,
657
+ `Each layer is ${LAYERS.what}. Summary: ${LAYERS.summary}. Diagram: ${LAYERS.diagram}:`,
658
+ "",
659
+ ...GUIDE_CLASSDEFS.map((line) => ` ${line}`),
660
+ "",
661
+ `Order: ${LAYERS.order}. ${LAYERS.mechanical}. ${LAYERS.stance}. Files are whole files, by path relative to the repo root; list every file of the changeset somewhere, or the leftovers land in "Since your review".`,
619
662
  "",
620
663
  `Shape: ${LAYERS.shape}`,
621
664
  "",
@@ -630,7 +673,7 @@ function buildClearedPrompt(ctx) {
630
673
  `The reviewer cleared the review in \`${ctx.repo.name}\` (branch \`${ctx.repo.branch}\`): the previous round landed, and its threads and guide are gone. What the reviewer sees now is a fresh round.`,
631
674
  "",
632
675
  ...ctx.changeset ? [specLine(ctx.changeset), ""] : [],
633
- `There is no feedback to act on. But the fresh round has no guide — if this changeset needs one (${GUIDE.when}): post it — one comment on the whole changeset: ${GUIDE.what}: \`${CLI_COMMANDS.guide}\` (no file, so it anchors to the changeset). ${GUIDE.stance}.`,
676
+ `There is no feedback to act on. But the fresh round has no guide — if this changeset needs one (${GUIDE.when}): post it — one comment on the whole changeset: \`${CLI_COMMANDS.guide}\` (no file, so it anchors to the changeset). It is ${GUIDE.what}. Not in it: ${GUIDE.order}. How much: ${GUIDE.budget}. ${GUIDE.stance}. \`diffo help guide\` has the diagram legend and one example.`,
634
677
  "",
635
678
  `Then run \`${CLI_COMMANDS.poll}\` again to keep listening (${POLL_STANCE}).`,
636
679
  ""
@@ -681,8 +724,8 @@ function buildFinishPrompt(threads, ctx, coverage) {
681
724
  }
682
725
  //#endregion
683
726
  //#region src/shared/layers.ts
684
- /** A summary is one or two sentences by doctrine; this is the backstop against
685
- * an essay, the same cap a closing note gets. */
727
+ /** A summary is short by doctrine; this is the backstop against an essay, the
728
+ * same cap a closing note gets. */
686
729
  const SUMMARY_CAP = 4e3;
687
730
  /** A note is one line by contract — anything past the first newline is dropped,
688
731
  * and the line itself is capped so the file header stays a header. */
@@ -849,6 +892,7 @@ For the agent (the AI that wrote the change):
849
892
  or --stdin posts the list, replacing the last one)
850
893
  end Detach from the review politely
851
894
  help agent The agent's whole protocol on one page
895
+ help guide The guide comment — the map of the change — with an example
852
896
 
853
897
  Options (for opening the review):
854
898
  --base <branch> Review everything since forking from <branch>
@@ -890,9 +934,12 @@ The loop:
890
934
  2. Guide — post one only when the changeset needs orientation:
891
935
  ${GUIDE.when}.
892
936
  Right after sharing the URL, while the reviewer opens the page, post ONE
893
- comment on the whole changeset (\`diffo comment -m "…"\`, no file),
894
- containing: ${GUIDE.what}.
937
+ comment on the whole changeset (\`diffo comment -m "…"\`, no file). It is
938
+ ${GUIDE.what}.
939
+ Not in it: ${GUIDE.order}.
940
+ How much: ${GUIDE.budget}.
895
941
  ${GUIDE.stance}.
942
+ Legend: ${GUIDE.legend} — \`diffo help guide\` has them, and one example.
896
943
  It lands live at the top of their review — never hold the URL back for it.
897
944
  If the changeset later shifts under the guide, ${GUIDE.update}.
898
945
  3. Layers — offer them when the changeset reads better in order:
@@ -902,6 +949,7 @@ The loop:
902
949
  outline only when the reviewer asks — in chat, or through the poll as a
903
950
  \`"kind": "layers"\` payload: \`diffo layers --json '<Layer[]>'\`
904
951
  (or pipe it to \`diffo layers --stdin\`). Each layer is ${LAYERS.what}.
952
+ Summary: ${LAYERS.summary}. Diagram: ${LAYERS.diagram}.
905
953
  Order: ${LAYERS.order}. ${LAYERS.mechanical}. ${LAYERS.stance}.
906
954
  ${LAYERS.replace}. \`diffo help layers\` has the shape.
907
955
  4. Listen: run \`${CLI_COMMANDS.firstPoll}\` — it blocks until the reviewer
@@ -922,6 +970,11 @@ The loop:
922
970
  plain reply before the next poll.
923
971
  7. Comment (sparingly): \`diffo comment [<file>] [--line <n>] -m "<text>"\`
924
972
  starts a thread in your voice — a concern, or context that helps the read.
973
+ When a reply or comment of yours ends in a decision that is the
974
+ reviewer's ("want me to extract this?"), add \`--suggest-reply "<one
975
+ line>"\` — it shows as ghost text in their reply box, taken with Tab.
976
+ Offer the answer you expect, not a menu; skip it on messages that only
977
+ report.
925
978
  8. Poll again only when the whole batch is handled — a new poll tells the
926
979
  reviewer you are done with the previous one.
927
980
  9. Detach: run \`diffo end\` when the review is over or the user moves on.
@@ -934,6 +987,54 @@ Rules:
934
987
  a poll says it took the review over, or returns status "superseded", say so
935
988
  to the user: their feedback moved with it.
936
989
  - Resolving a thread is the reviewer's call, never yours.`,
990
+ guide: `diffo help guide — the map an agent posts on the whole changeset
991
+
992
+ Posted with \`diffo comment -m "…"\` (no file, so it anchors to the changeset)
993
+ right after sharing the URL, while the reviewer opens the page — and only
994
+ when the changeset needs orientation: ${GUIDE.when}.
995
+
996
+ It is ${GUIDE.what}.
997
+ Not in it: ${GUIDE.order}.
998
+ How much: ${GUIDE.budget}.
999
+ ${GUIDE.stance}.
1000
+ Legend: ${GUIDE.legend}:
1001
+
1002
+ ${GUIDE_CLASSDEFS[0]}
1003
+ ${GUIDE_CLASSDEFS[1]}
1004
+
1005
+ A file named as \`path\` or \`path:line\` — in backticks, or in a diagram
1006
+ node — becomes a jump into the review, so the map is navigation too.
1007
+ If the changeset later shifts under the guide, ${GUIDE.update}.
1008
+
1009
+ Say each part in plain words a reader meets cold — "worth checking as you
1010
+ read", "safe to skip" — never a label of your own like "Hold:" that they
1011
+ would have to decode.
1012
+
1013
+ One shape that works — not THE shape: a rename needs no diagram, a one-file
1014
+ change needs no checks, and headings are optional.
1015
+
1016
+ A huge tool result could overflow the model's context. This caps it at the
1017
+ model boundary and adds \`read_tool_result\` so the model can page the
1018
+ rest — nothing is stored, the text is recomputed on read.
1019
+
1020
+ \`\`\`mermaid
1021
+ flowchart LR
1022
+ E[tool.execute] --> W[withToolResultCap]:::new
1023
+ W -->|over budget: slice + notice| P[provider]
1024
+ M[read_tool_result]:::new --> R[resolveFullToolResult]:::new
1025
+ R --> S[SDK steps · raw output]
1026
+ T[island-chat-transport.ts · prepareStep]:::changed --> W
1027
+ ${GUIDE_CLASSDEFS[0]}
1028
+ ${GUIDE_CLASSDEFS[1]}
1029
+ \`\`\`
1030
+
1031
+ Worth checking as you read: the capper and the reader must agree, byte for
1032
+ byte, on one string — \`canonicalToolText\` is the only definition both use.
1033
+ And the reader re-runs each tool's own \`toModelOutput\`, which assumes every
1034
+ converter is pure.
1035
+
1036
+ Safe to skip: four \`tool-*\` display components moved only because a prop
1037
+ went dead.`,
937
1038
  poll: `diffo poll — wait for the reviewer's feedback
938
1039
 
939
1040
  Usage: diffo poll [--title "<what the change is>"]
@@ -957,7 +1058,7 @@ Examples:
957
1058
  diffo poll`,
958
1059
  reply: `diffo reply — post a reply to a review thread
959
1060
 
960
- Usage: diffo reply <threadId> --message "<text>"
1061
+ Usage: diffo reply <threadId> --message "<text>" [--suggest-reply "<one line>"]
961
1062
  … | diffo reply <threadId> (long replies: pipe on stdin)
962
1063
 
963
1064
  Thread ids arrive in poll payloads. Each run posts one message, so don't
@@ -966,6 +1067,11 @@ re-run a reply that succeeded.
966
1067
  reply on this thread is promised. The reviewer keeps seeing the thread as
967
1068
  "with the agent" until your next plain reply; going quiet instead marks it
968
1069
  unanswered.
1070
+ --suggest-reply offers the reviewer their answer: one line that appears as
1071
+ ghost text in their reply box, taken with Tab, edited or ignored at will.
1072
+ Use it when your message ends in a decision that is theirs to make
1073
+ ("want me to extract this?" → --suggest-reply "yes, extract it"); never
1074
+ on a message that only reports.
969
1075
  Messages render GitHub-flavored markdown; a \`\`\`mermaid fence renders as a
970
1076
  diagram in the review.
971
1077
 
@@ -975,7 +1081,7 @@ Example:
975
1081
  diffo reply t-3 --message "fixed — the guard now covers the empty case"`,
976
1082
  comment: `diffo comment — start a comment thread as the agent
977
1083
 
978
- Usage: diffo comment [<file>] [--line <n>] --message "<text>"
1084
+ Usage: diffo comment [<file>] [--line <n>] --message "<text>" [--suggest-reply "<one line>"]
979
1085
  … | diffo comment [<file>] [--line <n>] (long comments: pipe on stdin)
980
1086
 
981
1087
  Anchors to a line (--line), a file, or — with no file — the whole changeset.
@@ -983,13 +1089,18 @@ A potential issue, or context that helps the reviewer read: either way it is
983
1089
  one thread, labeled as yours, and it never counts as the reviewer's feedback
984
1090
  until they reply into it — then it is theirs to send — or they resolve it.
985
1091
  Spend these sparingly — an agent that annotates everything gets skimmed.
1092
+ --suggest-reply offers the reviewer their answer: one line that appears as
1093
+ ghost text in their reply box, taken with Tab, edited or ignored at will.
1094
+ Use it when the comment proposes something and the call is theirs; the
1095
+ reply they take hands the thread to you like any other.
986
1096
  Messages render GitHub-flavored markdown; a \`\`\`mermaid fence renders as a
987
1097
  diagram in the review.
988
1098
 
989
1099
  Output: {"ok":true,"threadId":"t-1","next_step":"…"}
990
1100
 
991
- Example:
992
- diffo comment src/auth.ts --line 42 --message "this branch is unreachable"`,
1101
+ Examples:
1102
+ diffo comment src/auth.ts --line 42 --message "this branch is unreachable"
1103
+ diffo comment src/auth.ts --line 42 -m "I can fold these two guards into one. Want that?" --suggest-reply "yes, fold them"`,
993
1104
  layers: `diffo layers — outline the changeset as steps to read in order
994
1105
 
995
1106
  Usage: diffo layers --suggest ["<why, in one line>"] at open: this read benefits from layers
@@ -997,6 +1108,12 @@ Usage: diffo layers --suggest ["<why, in one line>"] at open: this read benefi
997
1108
  … | diffo layers --stdin the same payload, piped
998
1109
 
999
1110
  Each layer is ${LAYERS.what}.
1111
+ Summary: ${LAYERS.summary}.
1112
+ Diagram: ${LAYERS.diagram}:
1113
+
1114
+ ${GUIDE_CLASSDEFS[0]}
1115
+ ${GUIDE_CLASSDEFS[1]}
1116
+
1000
1117
  Order: ${LAYERS.order}.
1001
1118
  ${LAYERS.mechanical}.
1002
1119
  ${LAYERS.stance}.
@@ -1012,7 +1129,7 @@ Output: {"ok":true,"layers":4,"next_step":"…"}
1012
1129
 
1013
1130
  Examples:
1014
1131
  diffo layers --suggest "the parser change explains the rest"
1015
- diffo layers --json '[{"title":"Parser contract","summary":"parse() now returns null instead of throwing.","files":["src/parse.ts"]},{"title":"Callers adapted","kind":"mechanical","files":["src/cli.ts","src/api.ts"]}]'`,
1132
+ diffo layers --json '[{"title":"Parser contract","summary":"The contract the rest of the change leans on: bad input now comes back as null, not an exception.","files":["src/parse.ts"]},{"title":"Callers adapted","kind":"mechanical","summary":"Call sites following the new return type.","files":["src/cli.ts","src/api.ts"]}]'`,
1016
1133
  end: `diffo end — detach from the review politely
1017
1134
 
1018
1135
  Usage: diffo end
@@ -1114,7 +1231,7 @@ function parseHelp(rest) {
1114
1231
  message: "help takes at most one command"
1115
1232
  };
1116
1233
  if (topic === void 0 || topic === "help") return { kind: "help" };
1117
- if (topic !== "agent" && !VERBS.has(topic)) return unknownCommand(topic);
1234
+ if (topic !== "agent" && topic !== "guide" && !VERBS.has(topic)) return unknownCommand(topic);
1118
1235
  return {
1119
1236
  kind: "help",
1120
1237
  topic
@@ -1301,6 +1418,7 @@ function parseVerb(verb, rest) {
1301
1418
  line: { type: "string" },
1302
1419
  title: { type: "string" },
1303
1420
  more: { type: "boolean" },
1421
+ "suggest-reply": { type: "string" },
1304
1422
  json: { type: "boolean" },
1305
1423
  help: {
1306
1424
  type: "boolean",
@@ -1313,6 +1431,7 @@ function parseVerb(verb, rest) {
1313
1431
  message: parsed.message
1314
1432
  };
1315
1433
  const { values, positionals } = parsed.value;
1434
+ const suggestReply = values["suggest-reply"];
1316
1435
  if (values.help) return {
1317
1436
  kind: "help",
1318
1437
  topic: verb
@@ -1325,6 +1444,20 @@ function parseVerb(verb, rest) {
1325
1444
  kind: "error",
1326
1445
  message: `'${verb}' takes no --more`
1327
1446
  };
1447
+ if (suggestReply !== void 0) {
1448
+ if (verb !== "reply" && verb !== "comment") return {
1449
+ kind: "error",
1450
+ message: `'${verb}' takes no --suggest-reply`
1451
+ };
1452
+ if (!suggestReply.trim()) return {
1453
+ kind: "error",
1454
+ message: "--suggest-reply needs the reply you are offering"
1455
+ };
1456
+ if (suggestReply.includes("\n")) return {
1457
+ kind: "error",
1458
+ message: "--suggest-reply is one line — the reviewer completes it"
1459
+ };
1460
+ }
1328
1461
  if (values.title !== void 0 && verb !== "poll") return {
1329
1462
  kind: "error",
1330
1463
  message: `'${verb}' takes no --title`
@@ -1373,7 +1506,8 @@ function parseVerb(verb, rest) {
1373
1506
  kind: "reply",
1374
1507
  threadId,
1375
1508
  message: values.message ?? null,
1376
- more: values.more === true
1509
+ more: values.more === true,
1510
+ suggestReply: suggestReply?.trim() ?? null
1377
1511
  };
1378
1512
  }
1379
1513
  const file = positionals[0] ?? null;
@@ -1397,7 +1531,8 @@ function parseVerb(verb, rest) {
1397
1531
  kind: "comment",
1398
1532
  file,
1399
1533
  line,
1400
- message: values.message ?? null
1534
+ message: values.message ?? null,
1535
+ suggestReply: suggestReply?.trim() ?? null
1401
1536
  };
1402
1537
  }
1403
1538
  //#endregion
@@ -2799,7 +2934,7 @@ var ReviewStore = class {
2799
2934
  this.listeners.add(listener);
2800
2935
  return () => this.listeners.delete(listener);
2801
2936
  }
2802
- createThread(anchor, text, capture, intent, author = "reviewer") {
2937
+ createThread(anchor, text, capture, intent, author = "reviewer", suggestedReply) {
2803
2938
  const now = (/* @__PURE__ */ new Date()).toISOString();
2804
2939
  return this.insert({
2805
2940
  id: randomUUID(),
@@ -2813,7 +2948,8 @@ var ReviewStore = class {
2813
2948
  id: randomUUID(),
2814
2949
  author,
2815
2950
  text,
2816
- at: now
2951
+ at: now,
2952
+ ...author === "agent" && suggestedReply ? { suggestedReply } : {}
2817
2953
  }],
2818
2954
  createdAt: now,
2819
2955
  updatedAt: now
@@ -2861,13 +2997,14 @@ var ReviewStore = class {
2861
2997
  * the reviewer raced in since — appended at the end, it would render as an
2862
2998
  * answer to a comment it never saw, and the raced comment would stop reading
2863
2999
  * as "waiting on the agent" even though it still is. */
2864
- addMessage(threadId, author, text, withheld = false, seenThroughMs, followUp = false) {
3000
+ addMessage(threadId, author, text, withheld = false, seenThroughMs, followUp = false, suggestedReply) {
2865
3001
  return this.update(threadId, ({ unanswered: _answered, ...thread }) => {
2866
3002
  const message = {
2867
3003
  id: randomUUID(),
2868
3004
  author,
2869
3005
  text,
2870
- at: (/* @__PURE__ */ new Date()).toISOString()
3006
+ at: (/* @__PURE__ */ new Date()).toISOString(),
3007
+ ...author === "agent" && suggestedReply ? { suggestedReply } : {}
2871
3008
  };
2872
3009
  const raced = author === "agent" && seenThroughMs !== void 0 ? thread.messages.findIndex((m) => m.author === "reviewer" && Date.parse(m.at) > seenThroughMs) : -1;
2873
3010
  const { awaitingFollowUp: _promised, ...settled } = thread;
@@ -2883,18 +3020,56 @@ var ReviewStore = class {
2883
3020
  };
2884
3021
  });
2885
3022
  }
3023
+ /**
3024
+ * The reviewer rewrites one of their own messages. Words the agent never saw
3025
+ * are a draft fixed in place. Words it did see are a change of mind, like
3026
+ * editing a chat message: the message is stamped edited and everything after
3027
+ * it is cut — the agent's answers to the old words included — so the thread
3028
+ * is the agent's to answer again. Cut, not kept: the old branch is not worth
3029
+ * a second history on the card. `withheld` holds a seen rewrite back from the
3030
+ * agent, the same as a withheld reply.
3031
+ */
3032
+ editMessage(threadId, messageId, text, withheld = false) {
3033
+ const current = this.state.threads.find((t) => t.id === threadId);
3034
+ const index = current?.messages.findIndex((m) => m.id === messageId) ?? -1;
3035
+ if (!current || index === -1) return null;
3036
+ const seen = seenByAgent(current, index);
3037
+ const cut = current.messages.slice(index + 1);
3038
+ const cutAnswer = cut.some((m) => m.author === "agent");
3039
+ return this.update(threadId, ({ unanswered, awaitingFollowUp, ...thread }) => {
3040
+ const before = thread.messages[index];
3041
+ const edited = {
3042
+ ...before,
3043
+ text,
3044
+ ...seen || before.editedAt ? { editedAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
3045
+ };
3046
+ return {
3047
+ ...thread,
3048
+ ...unanswered && !seen ? { unanswered } : {},
3049
+ ...awaitingFollowUp && !cutAnswer ? { awaitingFollowUp } : {},
3050
+ ...cutAnswer && thread.state === "addressed" ? { state: "sent" } : {},
3051
+ ...seen && cut.length > 0 ? { rewound: true } : {},
3052
+ ...seen && withheld ? { withheld: true } : {},
3053
+ messages: [...thread.messages.slice(0, index), edited]
3054
+ };
3055
+ });
3056
+ }
2886
3057
  /** A delivery just handed these threads to the agent — remember through when,
2887
- * so replies and the typing indicator can tell delivered words from raced ones. */
3058
+ * so replies and the typing indicator can tell delivered words from raced ones.
3059
+ * The delivery also carried the word that a rewind withdrew the old replies. */
2888
3060
  markDelivered(threadIds) {
2889
3061
  const ids = new Set(threadIds);
2890
3062
  if (ids.size === 0) return;
2891
3063
  const at = (/* @__PURE__ */ new Date()).toISOString();
2892
3064
  this.state = {
2893
3065
  ...this.state,
2894
- threads: this.state.threads.map((t) => ids.has(t.id) ? {
3066
+ threads: this.state.threads.map(({ rewound, ...t }) => ids.has(t.id) ? {
2895
3067
  ...t,
2896
3068
  deliveredThrough: at
2897
- } : t)
3069
+ } : {
3070
+ ...t,
3071
+ ...rewound ? { rewound } : {}
3072
+ })
2898
3073
  };
2899
3074
  this.commit();
2900
3075
  }
@@ -3348,7 +3523,9 @@ function normalizeThread(value, now) {
3348
3523
  author: msg.author,
3349
3524
  text: msg.text,
3350
3525
  at: typeof msg.at === "string" ? msg.at : now,
3351
- ...typeof msg.durationMs === "number" ? { durationMs: msg.durationMs } : {}
3526
+ ...typeof msg.durationMs === "number" ? { durationMs: msg.durationMs } : {},
3527
+ ...msg.author === "agent" && typeof msg.suggestedReply === "string" && msg.suggestedReply ? { suggestedReply: msg.suggestedReply } : {},
3528
+ ...typeof msg.editedAt === "string" ? { editedAt: msg.editedAt } : {}
3352
3529
  });
3353
3530
  }
3354
3531
  return {
@@ -3363,6 +3540,7 @@ function normalizeThread(value, now) {
3363
3540
  ...t.awaitingFollowUp === true && t.unanswered !== true ? { awaitingFollowUp: true } : {},
3364
3541
  ...t.closingNote === true ? { closingNote: true } : {},
3365
3542
  ...typeof t.sentAt === "string" ? { sentAt: t.sentAt } : {},
3543
+ ...t.rewound === true ? { rewound: true } : {},
3366
3544
  messages,
3367
3545
  createdAt: typeof t.createdAt === "string" ? t.createdAt : now,
3368
3546
  updatedAt: typeof t.updatedAt === "string" ? t.updatedAt : now
@@ -3912,7 +4090,7 @@ function createApp(ctx, store, review, queue) {
3912
4090
  const line = typeof body?.line === "number" && Number.isInteger(body.line) && body.line >= 1 ? body.line : null;
3913
4091
  const anchor = agentAnchor(file, line);
3914
4092
  const capture = store ? captureAnchor(store.get(), anchor) : null;
3915
- return c.json(review.createThread(anchor, text, capture, void 0, "agent"));
4093
+ return c.json(review.createThread(anchor, text, capture, void 0, "agent", parseSuggestedReply(body?.suggestedReply)));
3916
4094
  }
3917
4095
  const anchor = parseAnchor(body?.anchor);
3918
4096
  if (!anchor) return c.json({ error: "need {anchor, text}" }, 400);
@@ -3957,7 +4135,7 @@ function createApp(ctx, store, review, queue) {
3957
4135
  const more = body?.more === true;
3958
4136
  const waitedMs = queue?.agentReplied(id, more) ?? null;
3959
4137
  const seenThroughMs = waitedMs === null ? void 0 : Date.now() - waitedMs;
3960
- const thread = review.addMessage(id, "agent", text, false, seenThroughMs, more);
4138
+ const thread = review.addMessage(id, "agent", text, false, seenThroughMs, more, parseSuggestedReply(body?.suggestedReply));
3961
4139
  if (!thread) return c.json({ error: "no such thread" }, 404);
3962
4140
  if (waitedMs !== null) review.annotateAgentReplies([thread.id], waitedMs);
3963
4141
  return c.json({
@@ -3981,6 +4159,34 @@ function createApp(ctx, store, review, queue) {
3981
4159
  presence: queue?.presence() ?? "waiting"
3982
4160
  });
3983
4161
  });
4162
+ app.patch("/api/review/threads/:id/messages/:messageId", async (c) => {
4163
+ if (!review) return c.json({ error: "review unavailable" }, 503);
4164
+ const body = await c.req.json().catch(() => null);
4165
+ const text = typeof body?.text === "string" ? body.text.trim() : "";
4166
+ if (!text) return c.json({ error: "need {text}" }, 400);
4167
+ const id = c.req.param("id");
4168
+ const messageId = c.req.param("messageId");
4169
+ const current = review.get().threads.find((t) => t.id === id);
4170
+ const index = current?.messages.findIndex((m) => m.id === messageId) ?? -1;
4171
+ if (!current || index === -1) return c.json({ error: "no such message" }, 404);
4172
+ if (current.messages[index].author !== "reviewer") return c.json({ error: "only your own messages can be edited" }, 400);
4173
+ if (current.state === "resolved") return c.json({ error: "reopen the thread to edit it" }, 409);
4174
+ if (queue?.deliveredThreadIds().includes(id)) return c.json({ error: "the agent is answering this thread — edit after it replies" }, 409);
4175
+ const seen = seenByAgent(current, index);
4176
+ const deliver = body?.deliver !== false;
4177
+ let thread = review.editMessage(id, messageId, text, !deliver);
4178
+ let delivered = false;
4179
+ if (seen && deliver && (thread.state === "sent" || thread.state === "addressed")) {
4180
+ delivered = deliverThreads([id]);
4181
+ review.clearWithheld([id]);
4182
+ thread = review.get().threads.find((t) => t.id === id) ?? thread;
4183
+ }
4184
+ return c.json({
4185
+ thread,
4186
+ delivered,
4187
+ presence: queue?.presence() ?? "waiting"
4188
+ });
4189
+ });
3984
4190
  app.post("/api/review/threads/:id/state", async (c) => {
3985
4191
  if (!review) return c.json({ error: "review unavailable" }, 503);
3986
4192
  const state = (await c.req.json().catch(() => null))?.state;
@@ -5344,7 +5550,8 @@ if (command.kind === "reply") {
5344
5550
  const { status, body } = await postJson(await requireServer(), `/api/review/threads/${encodeURIComponent(command.threadId)}/messages`, {
5345
5551
  author: "agent",
5346
5552
  text: message,
5347
- ...command.more ? { more: true } : {}
5553
+ ...command.more ? { more: true } : {},
5554
+ ...command.suggestReply ? { suggestedReply: command.suggestReply } : {}
5348
5555
  });
5349
5556
  if (status === 404) fail(`no thread with id '${command.threadId}'`);
5350
5557
  if (status !== 200) fail(`reply failed (${status})`);
@@ -5365,7 +5572,8 @@ if (command.kind === "comment") {
5365
5572
  author: "agent",
5366
5573
  file: command.file,
5367
5574
  line: command.line,
5368
- text: message
5575
+ text: message,
5576
+ ...command.suggestReply ? { suggestedReply: command.suggestReply } : {}
5369
5577
  });
5370
5578
  if (status !== 200) fail(`comment failed (${status})`);
5371
5579
  console.log(JSON.stringify({
@@ -1 +1 @@
1
- import{m as e,t}from"./mermaid-parser.core-siQPZsry.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-B4djUhOi.js";import"./chunk-O7XYJQB3-CyxnSfZ3.js";import{n as i,r as a,t as o}from"./chunk-GTNCS2PH-BodN5cRQ.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CLHTQVF-.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
1
+ import{m as e,t}from"./mermaid-parser.core-D2BrkwRE.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-YTrwJCk8.js";import"./chunk-O7XYJQB3-DG64mbS_.js";import{n as i,r as a,t as o}from"./chunk-GTNCS2PH-JFHsSpPU.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-CCFlYVgY.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
@@ -1 +1 @@
1
- import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-BfTRyLGm.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
1
+ import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-BWYGCgdo.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
@@ -0,0 +1 @@
1
+ import"./chunk-FOHPRMQF-DzxwRta7.js";import{M as e}from"./mermaid-parser.core-D2BrkwRE.js";export{e as createArchitectureServices};