@markusylisiurunen/tau 0.3.60 → 0.3.62

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 (142) hide show
  1. package/dist/core/config/schema.js.map +1 -1
  2. package/dist/core/diff_review/bridge.js +1 -0
  3. package/dist/core/diff_review/bridge.js.map +1 -1
  4. package/dist/core/diff_review/protocol.js +4 -2
  5. package/dist/core/diff_review/protocol.js.map +1 -1
  6. package/dist/core/diff_review/review_instructions.js +2 -1
  7. package/dist/core/diff_review/review_instructions.js.map +1 -1
  8. package/dist/core/diff_review/tool.js +1 -1
  9. package/dist/core/diff_review/tool.js.map +1 -1
  10. package/dist/core/session/compaction.js +8 -7
  11. package/dist/core/session/compaction.js.map +1 -1
  12. package/dist/core/static/tau_docs/configuration.md +1 -1
  13. package/dist/core/static/tau_docs/index.md +1 -0
  14. package/dist/core/static/tau_docs/manifest.json +1 -0
  15. package/dist/core/static/tau_docs/node-sdk.md +8 -0
  16. package/dist/core/static/tau_docs/sdk-diff-review.md +79 -0
  17. package/dist/core/static/tau_docs/session-protocol-methods.md +4 -2
  18. package/dist/core/static/tau_docs/telegram.md +21 -22
  19. package/dist/core/static/tau_docs/troubleshooting.md +1 -1
  20. package/dist/core/static/tau_docs/tui.md +2 -0
  21. package/dist/core/telegram/config.js +20 -2
  22. package/dist/core/telegram/config.js.map +1 -1
  23. package/dist/core/telegram/workspace.js +3 -39
  24. package/dist/core/telegram/workspace.js.map +1 -1
  25. package/dist/core/version.js +1 -1
  26. package/dist/diff_tool/app/dist/assets/{angular-html-DoxI_och.js → angular-html-CNdMT1sj.js} +1 -1
  27. package/dist/diff_tool/app/dist/assets/{angular-ts-C7CUd-D2.js → angular-ts-ByZ1I9zp.js} +1 -1
  28. package/dist/diff_tool/app/dist/assets/{apl-7dieYM-l.js → apl-DTw4IZcL.js} +1 -1
  29. package/dist/diff_tool/app/dist/assets/{astro-Czj3O4jM.js → astro-Cuihy-h6.js} +1 -1
  30. package/dist/diff_tool/app/dist/assets/{blade-ClS0E2cC.js → blade-BRcWKIxm.js} +1 -1
  31. package/dist/diff_tool/app/dist/assets/{c-BKvTh-Jt.js → c-CZrnSPrO.js} +1 -1
  32. package/dist/diff_tool/app/dist/assets/{chapel-qvFrfwEr.js → chapel-BJWGSECM.js} +1 -1
  33. package/dist/diff_tool/app/dist/assets/{cobol-B0vsaB-J.js → cobol-BtqNzZtm.js} +1 -1
  34. package/dist/diff_tool/app/dist/assets/{coffee-BUlcrU75.js → coffee-DV53y0PJ.js} +1 -1
  35. package/dist/diff_tool/app/dist/assets/{cpp-CKqEgX1l.js → cpp-Bn3c6Y8W.js} +1 -1
  36. package/dist/diff_tool/app/dist/assets/{crystal-FCqSmIJ_.js → crystal-4DKOWvUP.js} +1 -1
  37. package/dist/diff_tool/app/dist/assets/{css-zZvYFJFc.js → css-DRAuUHHc.js} +1 -1
  38. package/dist/diff_tool/app/dist/assets/{edge-BKlbCo1f.js → edge-CvBIkt32.js} +1 -1
  39. package/dist/diff_tool/app/dist/assets/{elixir-B2gSxtgZ.js → elixir-WxLR57iz.js} +1 -1
  40. package/dist/diff_tool/app/dist/assets/{elm-DFEdzPJz.js → elm-C8UfKkLs.js} +1 -1
  41. package/dist/diff_tool/app/dist/assets/{erb-DSgn8fHS.js → erb-DDaRKv4Z.js} +1 -1
  42. package/dist/diff_tool/app/dist/assets/{git-rebase-CcgDECQr.js → git-rebase-BfBjC640.js} +1 -1
  43. package/dist/diff_tool/app/dist/assets/{glimmer-js-Dwxi4cX2.js → glimmer-js-5QdPyQJP.js} +1 -1
  44. package/dist/diff_tool/app/dist/assets/{glimmer-ts-BnJVER8W.js → glimmer-ts-Te0TMSCC.js} +1 -1
  45. package/dist/diff_tool/app/dist/assets/{glsl-z7_gU-HN.js → glsl-D4tyKssQ.js} +1 -1
  46. package/dist/diff_tool/app/dist/assets/{graphql-Dt7V-d3L.js → graphql-J0IcMfkN.js} +1 -1
  47. package/dist/diff_tool/app/dist/assets/{hack-jVkr6EC8.js → hack-BcEFuXu_.js} +1 -1
  48. package/dist/diff_tool/app/dist/assets/{haml-BF7cRiOF.js → haml-SbspNdjx.js} +1 -1
  49. package/dist/diff_tool/app/dist/assets/{handlebars-DAhNxR3E.js → handlebars-DH3a4U2h.js} +1 -1
  50. package/dist/diff_tool/app/dist/assets/{html-B7QW5YvU.js → html-CBno8dbY.js} +1 -1
  51. package/dist/diff_tool/app/dist/assets/{html-derivative-B-vyaE-N.js → html-derivative-COFLhWU9.js} +1 -1
  52. package/dist/diff_tool/app/dist/assets/{http-Drkl04RP.js → http-2GnhhV3-.js} +1 -1
  53. package/dist/diff_tool/app/dist/assets/{hurl-7gTFrvcN.js → hurl-BIPzVQmD.js} +1 -1
  54. package/dist/diff_tool/app/dist/assets/index-Cb0TCxCI.js +1550 -0
  55. package/dist/diff_tool/app/dist/assets/index-esno8y5b.css +1 -0
  56. package/dist/diff_tool/app/dist/assets/{java-ZmvXuvs3.js → java-bZo_LLEv.js} +1 -1
  57. package/dist/diff_tool/app/dist/assets/{javascript-C-OECu_Z.js → javascript-7XNFOfdJ.js} +1 -1
  58. package/dist/diff_tool/app/dist/assets/{jinja-hvkV87CG.js → jinja-Yuf1yufJ.js} +1 -1
  59. package/dist/diff_tool/app/dist/assets/{jison-eJBDxKI8.js → jison-DD2vLLcq.js} +1 -1
  60. package/dist/diff_tool/app/dist/assets/{json-BPQU2w0u.js → json-CVOsPviK.js} +1 -1
  61. package/dist/diff_tool/app/dist/assets/{jsx-C19s5MOj.js → jsx-BYrWYIX0.js} +1 -1
  62. package/dist/diff_tool/app/dist/assets/{julia-CYISvCwi.js → julia-BZ_A4I5C.js} +1 -1
  63. package/dist/diff_tool/app/dist/assets/{just-5omBg5pB.js → just-D8W5QT1p.js} +1 -1
  64. package/dist/diff_tool/app/dist/assets/{latex-B_L85F2z.js → latex-BSsqyYVM.js} +1 -1
  65. package/dist/diff_tool/app/dist/assets/{liquid-BxbaQPPS.js → liquid-CD5oivgr.js} +1 -1
  66. package/dist/diff_tool/app/dist/assets/{lua-DQA0z-44.js → lua-BnXOnOdP.js} +1 -1
  67. package/dist/diff_tool/app/dist/assets/{marko-Dj5RV8IH.js → marko-CjeqsSvM.js} +1 -1
  68. package/dist/diff_tool/app/dist/assets/{mdc-CoHI6VJN.js → mdc-Bk-o_FzD.js} +1 -1
  69. package/dist/diff_tool/app/dist/assets/{nginx-COnrY-gb.js → nginx-CU9exOaI.js} +1 -1
  70. package/dist/diff_tool/app/dist/assets/{nim-Ccfp0p1j.js → nim-C4eS-_Qo.js} +1 -1
  71. package/dist/diff_tool/app/dist/assets/{org-RForNsWm.js → org-gZKEwUlv.js} +1 -1
  72. package/dist/diff_tool/app/dist/assets/{perl-DhHRsiUu.js → perl-d_jthyH-.js} +1 -1
  73. package/dist/diff_tool/app/dist/assets/{php-BlxKhJek.js → php-Cldhvias.js} +1 -1
  74. package/dist/diff_tool/app/dist/assets/{pug-o9YxnGzH.js → pug-1Im3v9J7.js} +1 -1
  75. package/dist/diff_tool/app/dist/assets/{qml-Dzf494vL.js → qml-Bugdf4FS.js} +1 -1
  76. package/dist/diff_tool/app/dist/assets/{r-Dp0OUrF6.js → r-DNz8aXhA.js} +1 -1
  77. package/dist/diff_tool/app/dist/assets/{razor-gylahu-m.js → razor-CyO91A7-.js} +1 -1
  78. package/dist/diff_tool/app/dist/assets/{regexp-C0rsDjCm.js → regexp-7IVcp_0b.js} +1 -1
  79. package/dist/diff_tool/app/dist/assets/{rst-D-DRAFtq.js → rst-CPC6Rcsf.js} +1 -1
  80. package/dist/diff_tool/app/dist/assets/{ruby-BnY40vXq.js → ruby-CIw26EG-.js} +1 -1
  81. package/dist/diff_tool/app/dist/assets/{sas-BaSo_s7i.js → sas-CXXipNue.js} +1 -1
  82. package/dist/diff_tool/app/dist/assets/{scss-BX5R-Yyw.js → scss-Cb8mUz_m.js} +1 -1
  83. package/dist/diff_tool/app/dist/assets/{shellscript-CpRFVBRj.js → shellscript-8Dfk6ni5.js} +1 -1
  84. package/dist/diff_tool/app/dist/assets/{shellsession-BGRUDtms.js → shellsession-BBb6xY52.js} +1 -1
  85. package/dist/diff_tool/app/dist/assets/{soy-C2WL9FSb.js → soy-_rneSfGo.js} +1 -1
  86. package/dist/diff_tool/app/dist/assets/{sql-CGmNnOfz.js → sql-CmdLYztO.js} +1 -1
  87. package/dist/diff_tool/app/dist/assets/{stata-CDamV3Uz.js → stata-DmJpPQHj.js} +1 -1
  88. package/dist/diff_tool/app/dist/assets/{surrealql-BqI4o6tP.js → surrealql-D1R_-CBd.js} +1 -1
  89. package/dist/diff_tool/app/dist/assets/{svelte-DPaOUioa.js → svelte-CKPrzNdA.js} +1 -1
  90. package/dist/diff_tool/app/dist/assets/{templ-aqrZIEoc.js → templ-C0Eqag1M.js} +1 -1
  91. package/dist/diff_tool/app/dist/assets/{tex-Bjjtfacp.js → tex-IrZiFLxJ.js} +1 -1
  92. package/dist/diff_tool/app/dist/assets/{ts-tags-BGFnsvdQ.js → ts-tags-09-sH-my.js} +1 -1
  93. package/dist/diff_tool/app/dist/assets/{tsx-C_rOxPqZ.js → tsx-CSPybM8Z.js} +1 -1
  94. package/dist/diff_tool/app/dist/assets/{twig-DYqVwCOf.js → twig-C17vnjiX.js} +1 -1
  95. package/dist/diff_tool/app/dist/assets/{typescript-CppQYc7K.js → typescript-CvHNDrui.js} +1 -1
  96. package/dist/diff_tool/app/dist/assets/{typst-KIbA0FZG.js → typst-Ct_9rHAw.js} +1 -1
  97. package/dist/diff_tool/app/dist/assets/{vue-iOYoJEJi.js → vue-A0j7pYML.js} +1 -1
  98. package/dist/diff_tool/app/dist/assets/{vue-html-DcZpMui9.js → vue-html-DiVQf4vL.js} +1 -1
  99. package/dist/diff_tool/app/dist/assets/{vue-vine-C6v6c0Lr.js → vue-vine-CBf6VIg0.js} +1 -1
  100. package/dist/diff_tool/app/dist/assets/{xml-zR0RltJO.js → xml-BvQnnFTL.js} +1 -1
  101. package/dist/diff_tool/app/dist/assets/{xsl-B9pBLsIV.js → xsl-Dj-x_YCW.js} +1 -1
  102. package/dist/diff_tool/app/dist/assets/{yaml-sicIihvu.js → yaml-D6eQJHya.js} +1 -1
  103. package/dist/diff_tool/app/dist/index.html +2 -2
  104. package/dist/diff_tool/http_server.js +335 -56
  105. package/dist/diff_tool/http_server.js.map +1 -1
  106. package/dist/diff_tool/index.js.map +1 -1
  107. package/dist/diff_tool/review_prompts.js +168 -28
  108. package/dist/diff_tool/review_prompts.js.map +1 -1
  109. package/dist/diff_tool/review_state.js +213 -69
  110. package/dist/diff_tool/review_state.js.map +1 -1
  111. package/dist/diff_tool/review_state_persistence.js +282 -0
  112. package/dist/diff_tool/review_state_persistence.js.map +1 -0
  113. package/dist/diff_tool/shared_types.js +12 -0
  114. package/dist/diff_tool/shared_types.js.map +1 -1
  115. package/dist/host/hosted_ephemeral_agent_session.js +14 -2
  116. package/dist/host/hosted_ephemeral_agent_session.js.map +1 -1
  117. package/dist/host/local_session_host.js +67 -59
  118. package/dist/host/local_session_host.js.map +1 -1
  119. package/dist/host/session_protocol_handler.js +2 -0
  120. package/dist/host/session_protocol_handler.js.map +1 -1
  121. package/dist/protocol/session_protocol.d.ts +2 -0
  122. package/dist/protocol/session_protocol.js +9 -2
  123. package/dist/protocol/session_protocol.js.map +1 -1
  124. package/dist/sdk/diff_review.d.ts +40 -0
  125. package/dist/sdk/diff_review.js +112 -0
  126. package/dist/sdk/diff_review.js.map +1 -0
  127. package/dist/sdk/index.d.ts +2 -1
  128. package/dist/sdk/index.js +1 -0
  129. package/dist/sdk/index.js.map +1 -1
  130. package/dist/sdk/session.js +2 -0
  131. package/dist/sdk/session.js.map +1 -1
  132. package/dist/sdk/session_tool_execution_backend.d.ts +16 -0
  133. package/dist/sdk/session_tool_execution_backend.js +157 -0
  134. package/dist/sdk/session_tool_execution_backend.js.map +1 -0
  135. package/dist/sdk/types.d.ts +5 -2
  136. package/dist/tui/session_chat_controller.js +1 -0
  137. package/dist/tui/session_chat_controller.js.map +1 -1
  138. package/dist/tui/session_tool_execution_backend.js +1 -156
  139. package/dist/tui/session_tool_execution_backend.js.map +1 -1
  140. package/package.json +6 -6
  141. package/dist/diff_tool/app/dist/assets/index-Az_6lupE.css +0 -1
  142. package/dist/diff_tool/app/dist/assets/index-C9CiIimI.js +0 -1550
@@ -2,10 +2,12 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { createServer } from "node:http";
3
3
  import { extname, join, normalize, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { buildDiffReviewBootstrapPrompt, buildDiffReviewBriefPrompt, buildDiffReviewCommentThreadPrompt, } from "./review_prompts.js";
5
+ import { buildDiffReviewBootstrapPrompt, buildDiffReviewCommentThreadPrompt, buildDiffReviewGuideOperationsPrompt, buildDiffReviewGuidePrompt, parseDiffReviewGuideOperationsResponse, parseDiffReviewGuideResponse, } from "./review_prompts.js";
6
6
  import { DiffToolReviewStateStore } from "./review_state.js";
7
- import { DIFF_TOOL_CODE_THEMES } from "./shared_types.js";
7
+ import { createDiffReviewScopeFingerprint, createDiffToolPersistedReviewStateDocument, parseDiffToolPersistedReviewStateDocument, } from "./review_state_persistence.js";
8
+ import { DIFF_TOOL_CODE_THEMES, DIFF_TOOL_GUIDE_QUESTION_LIMIT, DIFF_TOOL_GUIDE_TOPIC_LIMIT, } from "./shared_types.js";
8
9
  const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
10
+ const DIFF_REVIEW_FORK_REASONING = "medium";
9
11
  const CONTENT_TYPE_BY_EXT = {
10
12
  ".html": "text/html; charset=utf-8",
11
13
  ".js": "text/javascript; charset=utf-8",
@@ -27,9 +29,17 @@ export class DiffToolHttpServer {
27
29
  removeSessionCloseListener;
28
30
  staticDir;
29
31
  reviewState;
32
+ storage;
33
+ onSubmit;
30
34
  context;
31
35
  files = [];
36
+ persistence;
37
+ stateMutationQueue = Promise.resolve();
38
+ submissionState = "active";
32
39
  bootstrapThreadPromise;
40
+ guideGenerationPromise;
41
+ guideOperationQueue = [];
42
+ guideOperationRunning = false;
33
43
  httpServerClosePromise;
34
44
  sessionClosing = false;
35
45
  server = createServer((request, response) => {
@@ -51,6 +61,8 @@ export class DiffToolHttpServer {
51
61
  });
52
62
  this.host = options.host ?? "127.0.0.1";
53
63
  this.port = options.port ?? 0;
64
+ this.storage = options.storage;
65
+ this.onSubmit = options.onSubmit;
54
66
  this.staticDir = resolveStaticDir();
55
67
  this.removeClientCloseListener = this.client.onClose(() => {
56
68
  if (this.closed) {
@@ -68,14 +80,27 @@ export class DiffToolHttpServer {
68
80
  await this.client.connect();
69
81
  this.context = await this.client.getContext();
70
82
  this.files = (await this.client.listFiles()).files;
83
+ if (this.storage) {
84
+ const diff = await this.client.getDiff();
85
+ const persistence = {
86
+ storage: this.storage,
87
+ scopeFingerprint: createDiffReviewScopeFingerprint(this.context, this.files, diff.patch),
88
+ };
89
+ await this.restoreReviewState(persistence);
90
+ this.persistence = persistence;
91
+ }
92
+ if (this.closed || this.sessionClosing) {
93
+ throw new Error("diff tool session closed during startup");
94
+ }
71
95
  await this.listen();
72
96
  const address = this.server.address();
73
97
  if (!address || typeof address === "string") {
74
98
  throw new Error("failed to start diff tool http server");
75
99
  }
76
- const url = `http://${this.host}:${String(address.port)}`;
100
+ const url = `http://${formatHttpUrlHost(this.host)}:${String(address.port)}/`;
77
101
  await this.client.setUiText({ text: url });
78
102
  void this.startBootstrapThread().catch(() => { });
103
+ void this.startGuideGeneration().catch(() => { });
79
104
  return {
80
105
  url,
81
106
  };
@@ -118,6 +143,35 @@ export class DiffToolHttpServer {
118
143
  await this.close();
119
144
  }
120
145
  }
146
+ async restoreReviewState(persistence) {
147
+ const document = await persistence.storage.load();
148
+ if (document !== undefined) {
149
+ this.reviewState.replaceState(parseDiffToolPersistedReviewStateDocument(document, persistence.scopeFingerprint));
150
+ return;
151
+ }
152
+ await this.persistReviewState(this.reviewState.getState(), persistence);
153
+ }
154
+ async persistReviewState(state, persistence = this.persistence) {
155
+ if (!persistence)
156
+ return;
157
+ await persistence.storage.save(createDiffToolPersistedReviewStateDocument(persistence.scopeFingerprint, state));
158
+ }
159
+ async mutateReviewState(mutation, isApplied = () => true) {
160
+ const operation = this.stateMutationQueue.then(async () => {
161
+ const previousState = this.reviewState.getState();
162
+ const draft = this.reviewState.clone();
163
+ const result = mutation(draft);
164
+ if (!isApplied(result)) {
165
+ return { result, state: previousState };
166
+ }
167
+ const nextState = draft.getState();
168
+ await this.persistReviewState(nextState);
169
+ this.reviewState.replaceStatePreservingConcurrentLoading(nextState, previousState);
170
+ return { result, state: this.reviewState.getState() };
171
+ });
172
+ this.stateMutationQueue = operation.then(() => undefined, () => undefined);
173
+ return await operation;
174
+ }
121
175
  async listen() {
122
176
  await new Promise((resolve, reject) => {
123
177
  const onError = (error) => {
@@ -126,6 +180,10 @@ export class DiffToolHttpServer {
126
180
  };
127
181
  const onListening = () => {
128
182
  this.server.off("error", onError);
183
+ if (this.closed || this.sessionClosing) {
184
+ void this.closeHttpServer().then(() => reject(new Error("diff tool session closed during startup")), reject);
185
+ return;
186
+ }
129
187
  resolve();
130
188
  };
131
189
  this.server.once("error", onError);
@@ -186,6 +244,107 @@ export class DiffToolHttpServer {
186
244
  async getBootstrapThreadId() {
187
245
  return await this.startBootstrapThread();
188
246
  }
247
+ startGuideGeneration() {
248
+ if (this.guideGenerationPromise) {
249
+ return this.guideGenerationPromise;
250
+ }
251
+ const currentState = this.reviewState.getState();
252
+ if (currentState.guide.orientation.trim()) {
253
+ return Promise.resolve(currentState);
254
+ }
255
+ if (this.closed) {
256
+ return Promise.reject(new Error("diff tool http server is closed"));
257
+ }
258
+ this.reviewState.setGuideLoading(true);
259
+ const promise = this.generateGuide()
260
+ .catch((error) => {
261
+ this.reviewState.setGuideLoading(false);
262
+ throw error;
263
+ })
264
+ .finally(() => {
265
+ if (this.guideGenerationPromise === promise) {
266
+ this.guideGenerationPromise = undefined;
267
+ }
268
+ });
269
+ this.guideGenerationPromise = promise;
270
+ void promise.catch(() => { });
271
+ return promise;
272
+ }
273
+ async generateGuide() {
274
+ const result = await this.client.submitThreadMessage({
275
+ forkFromThreadId: await this.getBootstrapThreadId(),
276
+ message: buildDiffReviewGuidePrompt(),
277
+ reasoning: DIFF_REVIEW_FORK_REASONING,
278
+ });
279
+ const guide = parseDiffReviewGuideResponse(result.response);
280
+ const { state } = await this.mutateReviewState((draft) => draft.applyGuideResult(result, guide));
281
+ return state;
282
+ }
283
+ enqueueGuideOperation(operation) {
284
+ this.reviewState.setGuideLoading(true);
285
+ const queued = new Promise((resolve, reject) => {
286
+ this.guideOperationQueue.push({ operation, resolve, reject });
287
+ });
288
+ void this.drainGuideOperationQueue();
289
+ return queued;
290
+ }
291
+ async drainGuideOperationQueue() {
292
+ if (this.guideOperationRunning) {
293
+ return;
294
+ }
295
+ this.guideOperationRunning = true;
296
+ try {
297
+ if (this.guideGenerationPromise) {
298
+ await this.guideGenerationPromise;
299
+ }
300
+ while (this.guideOperationQueue.length > 0) {
301
+ const queued = this.guideOperationQueue.splice(0);
302
+ try {
303
+ const result = await this.runGuideOperations(queued.map((entry) => entry.operation));
304
+ for (const entry of queued) {
305
+ entry.resolve(result);
306
+ }
307
+ }
308
+ catch (error) {
309
+ this.reviewState.setGuideLoading(false);
310
+ for (const entry of queued) {
311
+ entry.reject(error);
312
+ }
313
+ }
314
+ }
315
+ }
316
+ catch (error) {
317
+ this.reviewState.setGuideLoading(false);
318
+ for (const entry of this.guideOperationQueue.splice(0)) {
319
+ entry.reject(error);
320
+ }
321
+ }
322
+ finally {
323
+ this.guideOperationRunning = false;
324
+ if (this.guideOperationQueue.length > 0) {
325
+ void this.drainGuideOperationQueue();
326
+ }
327
+ }
328
+ }
329
+ async runGuideOperations(operations) {
330
+ const currentGuide = this.reviewState.getState().guide;
331
+ this.reviewState.setGuideLoading(true);
332
+ const message = buildDiffReviewGuideOperationsPrompt(operations, currentGuide);
333
+ const result = await this.client.submitThreadMessage(currentGuide.threadId
334
+ ? { threadId: currentGuide.threadId, message }
335
+ : {
336
+ forkFromThreadId: await this.getBootstrapThreadId(),
337
+ message,
338
+ reasoning: DIFF_REVIEW_FORK_REASONING,
339
+ });
340
+ const contents = parseDiffReviewGuideOperationsResponse(operations, result.response);
341
+ const { result: applied, state } = await this.mutateReviewState((draft) => draft.applyGuideOperationResults(result, operations, contents), (value) => value);
342
+ if (!applied) {
343
+ this.reviewState.setGuideLoading(false);
344
+ return { applied, state: this.reviewState.getState() };
345
+ }
346
+ return { applied, state };
347
+ }
189
348
  async handleRequest(request, response) {
190
349
  const method = request.method ?? "GET";
191
350
  const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? this.host}`);
@@ -213,8 +372,8 @@ export class DiffToolHttpServer {
213
372
  }
214
373
  if (method === "POST" && requestUrl.pathname === "/api/state") {
215
374
  const payload = parseStatePatch(await this.readJsonBody(request));
216
- this.reviewState.updateState(payload);
217
- this.sendJson(response, 200, { state: this.reviewState.getState() });
375
+ const { state } = await this.mutateReviewState((draft) => draft.updateState(payload));
376
+ this.sendJson(response, 200, { state });
218
377
  return;
219
378
  }
220
379
  if (method === "POST" && requestUrl.pathname === "/api/thread") {
@@ -223,9 +382,9 @@ export class DiffToolHttpServer {
223
382
  this.sendJson(response, 400, { error: "invalid thread payload" });
224
383
  return;
225
384
  }
226
- const threadId = this.reviewState.createThread(payload);
385
+ const { result: threadId, state } = await this.mutateReviewState((draft) => draft.createThread(payload));
227
386
  this.sendJson(response, 200, {
228
- state: this.reviewState.getState(),
387
+ state,
229
388
  threadId,
230
389
  });
231
390
  return;
@@ -234,27 +393,27 @@ export class DiffToolHttpServer {
234
393
  const payload = await this.readJsonBody(request);
235
394
  const id = typeof payload.id === "string" ? payload.id.trim() : "";
236
395
  const text = typeof payload.text === "string" ? payload.text.trim() : "";
237
- if (!this.reviewState.findThread(id)) {
238
- this.sendJson(response, 404, { error: "thread not found" });
239
- return;
240
- }
241
396
  if (!text) {
242
397
  this.sendJson(response, 400, { error: "reply text is required" });
243
398
  return;
244
399
  }
245
- this.reviewState.addReply(id, text);
246
- this.sendJson(response, 200, { state: this.reviewState.getState() });
400
+ const { result: added, state } = await this.mutateReviewState((draft) => draft.addReply(id, text), (result) => result);
401
+ if (!added) {
402
+ this.sendJson(response, 404, { error: "thread not found" });
403
+ return;
404
+ }
405
+ this.sendJson(response, 200, { state });
247
406
  return;
248
407
  }
249
408
  if (method === "POST" && requestUrl.pathname === "/api/thread/delete") {
250
409
  const payload = await this.readJsonBody(request);
251
410
  const id = typeof payload.id === "string" ? payload.id.trim() : "";
252
- const removed = this.reviewState.deleteThread(id);
411
+ const { result: removed, state } = await this.mutateReviewState((draft) => draft.deleteThread(id), (result) => result);
253
412
  if (!removed) {
254
413
  this.sendJson(response, 404, { error: "thread not found" });
255
414
  return;
256
415
  }
257
- this.sendJson(response, 200, { state: this.reviewState.getState() });
416
+ this.sendJson(response, 200, { state });
258
417
  return;
259
418
  }
260
419
  if (method === "POST" && requestUrl.pathname === "/api/thread-message/delete") {
@@ -263,46 +422,54 @@ export class DiffToolHttpServer {
263
422
  const messageIndex = typeof payload.messageIndex === "number" && Number.isInteger(payload.messageIndex)
264
423
  ? payload.messageIndex
265
424
  : -1;
266
- if (!this.reviewState.findThread(id)) {
425
+ const { result, state } = await this.mutateReviewState((draft) => {
426
+ if (!draft.findThread(id))
427
+ return "thread-not-found";
428
+ return draft.deleteThreadMessage(id, messageIndex)
429
+ ? "removed"
430
+ : "message-not-found";
431
+ }, (result) => result === "removed");
432
+ if (result === "thread-not-found") {
267
433
  this.sendJson(response, 404, { error: "thread not found" });
268
434
  return;
269
435
  }
270
- const removed = this.reviewState.deleteThreadMessage(id, messageIndex);
271
- if (!removed) {
436
+ if (result === "message-not-found") {
272
437
  this.sendJson(response, 400, { error: "message not found" });
273
438
  return;
274
439
  }
275
- this.sendJson(response, 200, { state: this.reviewState.getState() });
440
+ this.sendJson(response, 200, { state });
276
441
  return;
277
442
  }
278
443
  if (method === "POST" && requestUrl.pathname === "/api/thread/resolve") {
279
444
  const payload = await this.readJsonBody(request);
280
445
  const id = typeof payload.id === "string" ? payload.id.trim() : "";
281
- if (!this.reviewState.findThread(id)) {
282
- this.sendJson(response, 404, { error: "thread not found" });
283
- return;
284
- }
285
446
  if (typeof payload.resolved !== "boolean") {
286
447
  this.sendJson(response, 400, { error: "resolved flag is required" });
287
448
  return;
288
449
  }
289
- this.reviewState.setThreadResolved(id, payload.resolved);
290
- this.sendJson(response, 200, { state: this.reviewState.getState() });
450
+ const resolved = payload.resolved;
451
+ const { result: updated, state } = await this.mutateReviewState((draft) => draft.setThreadResolved(id, resolved), (result) => result);
452
+ if (!updated) {
453
+ this.sendJson(response, 404, { error: "thread not found" });
454
+ return;
455
+ }
456
+ this.sendJson(response, 200, { state });
291
457
  return;
292
458
  }
293
459
  if (method === "POST" && requestUrl.pathname === "/api/thread/collapse") {
294
460
  const payload = await this.readJsonBody(request);
295
461
  const id = typeof payload.id === "string" ? payload.id.trim() : "";
296
- if (!this.reviewState.findThread(id)) {
297
- this.sendJson(response, 404, { error: "thread not found" });
298
- return;
299
- }
300
462
  if (typeof payload.collapsed !== "boolean") {
301
463
  this.sendJson(response, 400, { error: "collapsed flag is required" });
302
464
  return;
303
465
  }
304
- this.reviewState.setThreadCollapsed(id, payload.collapsed);
305
- this.sendJson(response, 200, { state: this.reviewState.getState() });
466
+ const collapsed = payload.collapsed;
467
+ const { result: updated, state } = await this.mutateReviewState((draft) => draft.setThreadCollapsed(id, collapsed), (result) => result);
468
+ if (!updated) {
469
+ this.sendJson(response, 404, { error: "thread not found" });
470
+ return;
471
+ }
472
+ this.sendJson(response, 200, { state });
306
473
  return;
307
474
  }
308
475
  if (method === "POST" && requestUrl.pathname === "/api/thread-message") {
@@ -313,21 +480,35 @@ export class DiffToolHttpServer {
313
480
  this.sendJson(response, 404, { error: "thread not found" });
314
481
  return;
315
482
  }
483
+ if (thread.loading) {
484
+ this.sendJson(response, 409, { error: "thread message already in progress" });
485
+ return;
486
+ }
316
487
  const message = this.reviewState.buildThreadAgentMessage(id);
317
488
  if (!message) {
318
489
  this.sendJson(response, 400, { error: "thread has no pending user message" });
319
490
  return;
320
491
  }
492
+ const guideSnapshot = !thread.threadId &&
493
+ thread.anchor.kind === "detached" &&
494
+ !thread.messages.some((entry) => entry.role === "assistant")
495
+ ? this.reviewState.getState().guide
496
+ : undefined;
321
497
  this.reviewState.setThreadLoading(id, true);
322
498
  try {
323
499
  const result = await this.client.submitThreadMessage(thread.threadId
324
500
  ? { threadId: thread.threadId, message }
325
501
  : {
326
502
  forkFromThreadId: await this.getBootstrapThreadId(),
327
- message: buildDiffReviewCommentThreadPrompt(message),
503
+ message: buildDiffReviewCommentThreadPrompt(message, guideSnapshot),
504
+ reasoning: DIFF_REVIEW_FORK_REASONING,
328
505
  });
329
- this.reviewState.applyThreadResponse(id, result);
330
- this.sendJson(response, 200, { state: this.reviewState.getState() });
506
+ const { result: applied, state } = await this.mutateReviewState((draft) => draft.applyThreadResponse(id, result), (result) => result);
507
+ if (!applied) {
508
+ this.sendJson(response, 404, { error: "thread not found" });
509
+ return;
510
+ }
511
+ this.sendJson(response, 200, { state });
331
512
  }
332
513
  catch (error) {
333
514
  this.reviewState.setThreadLoading(id, false);
@@ -335,32 +516,83 @@ export class DiffToolHttpServer {
335
516
  }
336
517
  return;
337
518
  }
338
- if (method === "POST" && requestUrl.pathname === "/api/brief/generate") {
519
+ if (method === "POST" && requestUrl.pathname === "/api/guide/generate") {
520
+ const state = await this.startGuideGeneration();
521
+ this.sendJson(response, 200, { state });
522
+ return;
523
+ }
524
+ if (method === "POST" && requestUrl.pathname === "/api/guide/operate") {
525
+ const operation = parseGuideOperation(await this.readJsonBody(request));
526
+ if (!operation) {
527
+ this.sendJson(response, 400, { error: "invalid guide operation" });
528
+ return;
529
+ }
339
530
  const currentState = this.reviewState.getState();
340
- if (currentState.brief.loading) {
341
- this.sendJson(response, 409, { error: "brief generation already in progress" });
531
+ if (operation.kind === "topic.add" &&
532
+ currentState.guide.topics.length >= DIFF_TOOL_GUIDE_TOPIC_LIMIT) {
533
+ this.sendJson(response, 409, { error: "guide topic limit reached" });
534
+ return;
535
+ }
536
+ if (operation.kind === "question.ask" &&
537
+ currentState.guide.questions.length >= DIFF_TOOL_GUIDE_QUESTION_LIMIT) {
538
+ this.sendJson(response, 409, { error: "guide question limit reached" });
539
+ return;
540
+ }
541
+ if (operation.kind === "topic.revise" &&
542
+ !currentState.guide.topics.some((topic) => topic.id === operation.topicId)) {
543
+ this.sendJson(response, 404, { error: "guide topic not found" });
544
+ return;
545
+ }
546
+ const { applied, state } = await this.enqueueGuideOperation(operation);
547
+ if (!applied) {
548
+ this.sendJson(response, 409, { error: "guide changed while it was being updated" });
549
+ return;
550
+ }
551
+ this.sendJson(response, 200, { state });
552
+ return;
553
+ }
554
+ if (method === "POST" && requestUrl.pathname === "/api/guide/comment") {
555
+ const payload = parseGuideCommentPayload(await this.readJsonBody(request));
556
+ if (!payload) {
557
+ this.sendJson(response, 400, { error: "invalid guide comment" });
342
558
  return;
343
559
  }
344
- this.reviewState.startBriefGeneration();
560
+ const currentState = this.reviewState.getState();
561
+ if (!guideCommentTargetExists(currentState, payload.target)) {
562
+ this.sendJson(response, 404, { error: "guide comment target not found" });
563
+ return;
564
+ }
565
+ const { state } = await this.mutateReviewState((draft) => draft.saveGuideComment(payload.target, payload.body));
566
+ this.sendJson(response, 200, { state });
567
+ return;
568
+ }
569
+ if (method === "POST" && requestUrl.pathname === "/api/review") {
570
+ await this.readJsonBody(request);
571
+ await this.stateMutationQueue;
572
+ if (this.submissionState !== "active") {
573
+ this.sendJson(response, 409, { error: "diff review has already been submitted" });
574
+ return;
575
+ }
576
+ this.submissionState = "submitting";
577
+ const review = this.reviewState.buildReviewText();
578
+ const context = this.context;
579
+ if (!context) {
580
+ this.submissionState = "active";
581
+ throw new Error("diff tool session context is unavailable");
582
+ }
345
583
  try {
346
- const result = await this.client.submitThreadMessage({
347
- forkFromThreadId: await this.getBootstrapThreadId(),
348
- message: buildDiffReviewBriefPrompt(),
584
+ await this.onSubmit?.({
585
+ review,
586
+ context: { ...context, diffArgs: [...context.diffArgs] },
587
+ files: this.files.map((file) => ({ ...file })),
349
588
  });
350
- this.reviewState.applyBriefResult(result);
351
- this.sendJson(response, 200, { state: this.reviewState.getState() });
352
589
  }
353
590
  catch (error) {
354
- this.reviewState.setBriefLoading(false);
591
+ this.submissionState = "active";
355
592
  throw error;
356
593
  }
357
- return;
358
- }
359
- if (method === "POST" && requestUrl.pathname === "/api/review") {
360
- const payload = parseReviewPayload(await this.readJsonBody(request));
361
- const result = await this.client.returnReview({
362
- review: this.reviewState.buildReviewText(payload.message),
363
- });
594
+ this.submissionState = "submitted";
595
+ const result = await this.client.returnReview({ review });
364
596
  this.sendJson(response, 200, result);
365
597
  return;
366
598
  }
@@ -445,10 +677,58 @@ export class DiffToolHttpServer {
445
677
  response.end(JSON.stringify(payload));
446
678
  }
447
679
  }
448
- function parseReviewPayload(payload) {
449
- return typeof payload.message === "string" && payload.message.trim()
450
- ? { message: payload.message }
451
- : {};
680
+ function formatHttpUrlHost(host) {
681
+ return host.includes(":") ? `[${host}]` : host;
682
+ }
683
+ function parseGuideOperation(payload) {
684
+ switch (payload.kind) {
685
+ case "topic.add": {
686
+ const request = typeof payload.request === "string" ? payload.request.trim() : "";
687
+ return request ? { kind: payload.kind, request } : undefined;
688
+ }
689
+ case "topic.revise": {
690
+ const topicId = typeof payload.topicId === "string" ? payload.topicId.trim() : "";
691
+ const request = typeof payload.request === "string" ? payload.request.trim() : "";
692
+ return topicId && request ? { kind: payload.kind, topicId, request } : undefined;
693
+ }
694
+ case "question.ask": {
695
+ const question = typeof payload.question === "string" ? payload.question.trim() : "";
696
+ return question ? { kind: payload.kind, question } : undefined;
697
+ }
698
+ default:
699
+ return undefined;
700
+ }
701
+ }
702
+ function parseGuideCommentPayload(payload) {
703
+ const body = typeof payload.body === "string" ? payload.body.trim() : "";
704
+ const target = payload.target && typeof payload.target === "object" && !Array.isArray(payload.target)
705
+ ? payload.target
706
+ : undefined;
707
+ if (!body || !target) {
708
+ return undefined;
709
+ }
710
+ if (target.kind === "orientation") {
711
+ return { body, target: { kind: "orientation" } };
712
+ }
713
+ if (target.kind === "topic" && typeof target.topicId === "string" && target.topicId.trim()) {
714
+ return { body, target: { kind: "topic", topicId: target.topicId.trim() } };
715
+ }
716
+ if (target.kind === "question" &&
717
+ typeof target.questionId === "string" &&
718
+ target.questionId.trim()) {
719
+ return { body, target: { kind: "question", questionId: target.questionId.trim() } };
720
+ }
721
+ return undefined;
722
+ }
723
+ function guideCommentTargetExists(state, target) {
724
+ switch (target.kind) {
725
+ case "orientation":
726
+ return Boolean(state.guide.orientation);
727
+ case "topic":
728
+ return state.guide.topics.some((topic) => topic.id === target.topicId);
729
+ case "question":
730
+ return state.guide.questions.some((question) => question.id === target.questionId);
731
+ }
452
732
  }
453
733
  const codeThemes = new Set(DIFF_TOOL_CODE_THEMES);
454
734
  function parseStatePatch(payload) {
@@ -463,7 +743,6 @@ function parseStatePatch(payload) {
463
743
  codeThemes.has(payload.codeTheme)
464
744
  ? { codeTheme: payload.codeTheme }
465
745
  : {}),
466
- ...(typeof payload.sidebarOpen === "boolean" ? { sidebarOpen: payload.sidebarOpen } : {}),
467
746
  ...(Array.isArray(payload.collapsedFileIds)
468
747
  ? {
469
748
  collapsedFileIds: payload.collapsedFileIds.flatMap((value) => typeof value === "string" && value.trim() ? [value] : []),