@jpkme/diff-review 0.1.15 → 0.1.17

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 (164) hide show
  1. package/README.md +22 -14
  2. package/bin/diff-review.mjs +192 -21
  3. package/lib/acp-policy.mjs +10 -2
  4. package/lib/acp-policy.test.mjs +27 -0
  5. package/lib/acp.mjs +86 -10
  6. package/lib/agent-login.mjs +848 -36
  7. package/lib/agent-login.test.mjs +538 -0
  8. package/lib/agents.mjs +0 -39
  9. package/lib/agents.test.mjs +17 -24
  10. package/lib/extract-cache.test.mjs +162 -162
  11. package/lib/git.mjs +25 -9
  12. package/lib/model-label.mjs +20 -4
  13. package/lib/model-label.test.mjs +15 -2
  14. package/lib/output-name.mjs +7 -0
  15. package/lib/prompt.mjs +14 -11
  16. package/lib/prompt.test.mjs +49 -0
  17. package/lib/review-dir.mjs +14 -1
  18. package/lib/review-meta.mjs +11 -3
  19. package/lib/review-meta.test.mjs +19 -2
  20. package/lib/review-prep.mjs +63 -63
  21. package/lib/review.mjs +36 -4
  22. package/lib/review.test.mjs +35 -0
  23. package/lib/validate.mjs +11 -1
  24. package/lib/validate.test.mjs +47 -0
  25. package/lib/wait-output.test.mjs +1 -1
  26. package/package.json +2 -2
  27. package/schema.json +16 -1
  28. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css +1 -0
  29. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.br +0 -0
  30. package/web/build/client/_app/immutable/assets/0.B04dzd8P.css.gz +0 -0
  31. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css +1 -0
  32. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.br +0 -0
  33. package/web/build/client/_app/immutable/assets/2.CQmxVXnC.css.gz +0 -0
  34. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css +1 -0
  35. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.br +0 -0
  36. package/web/build/client/_app/immutable/assets/3.BVJgADrM.css.gz +0 -0
  37. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css +1 -0
  38. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.br +0 -0
  39. package/web/build/client/_app/immutable/assets/storage.Ztmr3s84.css.gz +0 -0
  40. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js +1 -0
  41. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.br +0 -0
  42. package/web/build/client/_app/immutable/chunks/B-2kfLeA.js.gz +0 -0
  43. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js +2 -0
  44. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.br +0 -0
  45. package/web/build/client/_app/immutable/chunks/B4SiKXUO.js.gz +0 -0
  46. package/web/build/client/_app/immutable/chunks/{D8WOD_eS.js → DA0YnpGR.js} +3 -3
  47. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.br +0 -0
  48. package/web/build/client/_app/immutable/chunks/DA0YnpGR.js.gz +0 -0
  49. package/web/build/client/_app/immutable/chunks/{BpVtXA-T.js → Dv85Hz5p.js} +1 -1
  50. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.br +2 -0
  51. package/web/build/client/_app/immutable/chunks/Dv85Hz5p.js.gz +0 -0
  52. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js +215 -0
  53. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.br +0 -0
  54. package/web/build/client/_app/immutable/chunks/b-BaDYIc.js.gz +0 -0
  55. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js +2 -0
  56. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.br +0 -0
  57. package/web/build/client/_app/immutable/entry/app.DPUME9Z_.js.gz +0 -0
  58. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js +1 -0
  59. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.br +0 -0
  60. package/web/build/client/_app/immutable/entry/start.hH87UZsD.js.gz +0 -0
  61. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js +1 -0
  62. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.br +0 -0
  63. package/web/build/client/_app/immutable/nodes/0.BhHA-Ohy.js.gz +0 -0
  64. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js +1 -0
  65. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.br +2 -0
  66. package/web/build/client/_app/immutable/nodes/1.jjvGFUxu.js.gz +0 -0
  67. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js +12 -0
  68. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.br +0 -0
  69. package/web/build/client/_app/immutable/nodes/2.CJTh6CGA.js.gz +0 -0
  70. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js +340 -0
  71. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.br +0 -0
  72. package/web/build/client/_app/immutable/nodes/3.DYAPRc_2.js.gz +0 -0
  73. package/web/build/client/_app/version.json +1 -1
  74. package/web/build/client/_app/version.json.br +0 -0
  75. package/web/build/client/_app/version.json.gz +0 -0
  76. package/web/build/handler.js +3 -3
  77. package/web/build/index.js +3 -3
  78. package/web/build/server/chunks/0-8NUeB9cB.js +11 -0
  79. package/web/build/server/chunks/{0-DUStDOsr.js.map → 0-8NUeB9cB.js.map} +1 -1
  80. package/web/build/server/chunks/1-BApN-ILJ.js +7 -0
  81. package/web/build/server/chunks/{1-DUj63QTT.js.map → 1-BApN-ILJ.js.map} +1 -1
  82. package/web/build/server/chunks/2-Bq7iMeQx.js +7 -0
  83. package/web/build/server/chunks/2-Bq7iMeQx.js.map +1 -0
  84. package/web/build/server/chunks/3-W0cqofyE.js +7 -0
  85. package/web/build/server/chunks/{3-C7UD6rei.js.map → 3-W0cqofyE.js.map} +1 -1
  86. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js +123 -0
  87. package/web/build/server/chunks/_server.ts-Bu1fdGSB.js.map +1 -0
  88. package/web/build/server/chunks/{_server.ts-4RcoUd0l.js → _server.ts-BuUNechu.js} +5 -4
  89. package/web/build/server/chunks/_server.ts-BuUNechu.js.map +1 -0
  90. package/web/build/server/chunks/{agent-sessions-C_hlZJKN.js → agent-sessions-BArYLTW6.js} +1103 -146
  91. package/web/build/server/chunks/agent-sessions-BArYLTW6.js.map +1 -0
  92. package/web/build/server/chunks/{git-review-Bh1w0gak.js → git-review-BJ_JrFNP.js} +87 -26
  93. package/web/build/server/chunks/git-review-BJ_JrFNP.js.map +1 -0
  94. package/web/build/server/chunks/{handler-e2lEvjVT.js → handler-Df_bJiCq.js} +3 -3
  95. package/web/build/server/chunks/{handler-e2lEvjVT.js.map → handler-Df_bJiCq.js.map} +1 -1
  96. package/web/build/server/chunks/{hooks.server-Um1Sknmz.js → hooks.server-BEl7rVad.js} +4 -3
  97. package/web/build/server/chunks/{hooks.server-Um1Sknmz.js.map → hooks.server-BEl7rVad.js.map} +1 -1
  98. package/web/build/server/chunks/{index.js-C3-YX8SA.js → index.js-Bb0CZLZ_.js} +884 -881
  99. package/web/build/server/chunks/index.js-Bb0CZLZ_.js.map +1 -0
  100. package/web/build/server/chunks/manifest.js-j442duM7.js +65 -0
  101. package/web/build/server/chunks/manifest.js-j442duM7.js.map +1 -0
  102. package/web/build/server/chunks/{remote-1g4ayag-DMiPy1mT.js → remote-1g4ayag-BxKnZQPn.js} +6 -6
  103. package/web/build/server/chunks/remote-1g4ayag-BxKnZQPn.js.map +1 -0
  104. package/web/build/server/chunks/{remote-wh6t6j-RlIVOUe-.js → remote-wh6t6j-B-AHrGaN.js} +58 -12
  105. package/web/build/server/chunks/remote-wh6t6j-B-AHrGaN.js.map +1 -0
  106. package/web/build/server/chunks/{validate-CAQ31C7k.js → validate-CXjTRGnR.js} +67 -21
  107. package/web/build/server/chunks/validate-CXjTRGnR.js.map +1 -0
  108. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css +0 -1
  109. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  110. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.gz +0 -0
  111. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css +0 -1
  112. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.br +0 -0
  113. package/web/build/client/_app/immutable/assets/2.C4whE4xz.css.gz +0 -0
  114. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css +0 -1
  115. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.br +0 -0
  116. package/web/build/client/_app/immutable/assets/3.Cw1J6YTM.css.gz +0 -0
  117. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css +0 -1
  118. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.br +0 -0
  119. package/web/build/client/_app/immutable/assets/storage.TlsiS-AP.css.gz +0 -0
  120. package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.br +0 -2
  121. package/web/build/client/_app/immutable/chunks/BpVtXA-T.js.gz +0 -0
  122. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js +0 -210
  123. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.br +0 -0
  124. package/web/build/client/_app/immutable/chunks/CUhrLa7d.js.gz +0 -0
  125. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.br +0 -0
  126. package/web/build/client/_app/immutable/chunks/D8WOD_eS.js.gz +0 -0
  127. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js +0 -2
  128. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.br +0 -0
  129. package/web/build/client/_app/immutable/chunks/QvO6K-HK.js.gz +0 -0
  130. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js +0 -1
  131. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.br +0 -0
  132. package/web/build/client/_app/immutable/chunks/lUYXRDtZ.js.gz +0 -0
  133. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js +0 -2
  134. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.br +0 -0
  135. package/web/build/client/_app/immutable/entry/app.BjJsHui4.js.gz +0 -0
  136. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js +0 -1
  137. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.br +0 -0
  138. package/web/build/client/_app/immutable/entry/start.D4unRHC4.js.gz +0 -0
  139. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js +0 -1
  140. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.br +0 -0
  141. package/web/build/client/_app/immutable/nodes/0.Dj97JW46.js.gz +0 -0
  142. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js +0 -1
  143. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.br +0 -1
  144. package/web/build/client/_app/immutable/nodes/1.C8JJYlWG.js.gz +0 -0
  145. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js +0 -10
  146. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.br +0 -0
  147. package/web/build/client/_app/immutable/nodes/2.CDI0Ipi4.js.gz +0 -0
  148. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js +0 -171
  149. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.br +0 -0
  150. package/web/build/client/_app/immutable/nodes/3.DVC22CDg.js.gz +0 -0
  151. package/web/build/server/chunks/0-DUStDOsr.js +0 -11
  152. package/web/build/server/chunks/1-DUj63QTT.js +0 -7
  153. package/web/build/server/chunks/2-B4PSKe5P.js +0 -7
  154. package/web/build/server/chunks/2-B4PSKe5P.js.map +0 -1
  155. package/web/build/server/chunks/3-C7UD6rei.js +0 -7
  156. package/web/build/server/chunks/_server.ts-4RcoUd0l.js.map +0 -1
  157. package/web/build/server/chunks/agent-sessions-C_hlZJKN.js.map +0 -1
  158. package/web/build/server/chunks/git-review-Bh1w0gak.js.map +0 -1
  159. package/web/build/server/chunks/index.js-C3-YX8SA.js.map +0 -1
  160. package/web/build/server/chunks/manifest.js-CmbRPv0j.js +0 -58
  161. package/web/build/server/chunks/manifest.js-CmbRPv0j.js.map +0 -1
  162. package/web/build/server/chunks/remote-1g4ayag-DMiPy1mT.js.map +0 -1
  163. package/web/build/server/chunks/remote-wh6t6j-RlIVOUe-.js.map +0 -1
  164. package/web/build/server/chunks/validate-CAQ31C7k.js.map +0 -1
@@ -1,10 +1,11 @@
1
- import { o as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, p as readDiffCache, q as extractBranchDiff, u as loadReviewConfig, v as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, x as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, y as validateAgainst, z as reviewIssues, R as REVIEW_SCHEMA, A as globMatch, C as writeDiffText } from './validate-CAQ31C7k.js';
2
- import { existsSync, readFileSync, statSync, accessSync, constants, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync } from 'node:fs';
3
- import { spawn } from 'node:child_process';
1
+ import { p as getRemoteUrl, e as ensureReviewDir, w as writeDiffCache, q as readDiffCache, u as extractBranchDiff, v as loadReviewConfig, x as outputRelPath, m as makeOutputFilename, O as OUTPUT_DIR, y as writeSessionRef, c as companionPaths, d as decodePayload, t as tryResolveGitRoot, f as loadBranchDiff, z as validateAgainst, A as reviewIssues, R as REVIEW_SCHEMA, C as globMatch, D as writeDiffText, E as writeHandoffChat } from './validate-CXjTRGnR.js';
2
+ import { existsSync, statSync, accessSync, constants, readFileSync, readdirSync, mkdirSync, writeFileSync, copyFileSync, rmSync, realpathSync, unlinkSync } from 'node:fs';
3
+ import { spawnSync, spawn } from 'node:child_process';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { join, delimiter, isAbsolute, resolve, basename, dirname, normalize } from 'node:path';
6
6
  import { Writable, Readable } from 'node:stream';
7
7
  import * as acp from '@agentclientprotocol/sdk';
8
+ import { randomBytes } from 'node:crypto';
8
9
 
9
10
  //#region ../lib/agents.mjs
10
11
  /**
@@ -40,15 +41,6 @@ function readJson(file) {
40
41
  return null;
41
42
  }
42
43
  }
43
- /** Primer `model = "..."` de nivel raíz de un TOML (antes de cualquier `[sección]`). */
44
- function topLevelTomlModel(text) {
45
- for (const line of String(text).split(/\r?\n/)) {
46
- if (/^\s*\[/.test(line)) break;
47
- const m = line.match(/^\s*model\s*=\s*["']([^"']+)["']/);
48
- if (m) return m[1];
49
- }
50
- return null;
51
- }
52
44
  /** Versiones tipo `2.1.286` de mayor a menor (las que no parsean, al final). */
53
45
  function byVersionDesc(a, b) {
54
46
  const pa = a.split(".").map(Number);
@@ -182,30 +174,6 @@ var AGENTS = [
182
174
  };
183
175
  }
184
176
  },
185
- {
186
- id: "codex",
187
- label: "Codex",
188
- bins: ["codex"],
189
- models: [],
190
- configuredModel: ({ home }) => {
191
- try {
192
- const model = topLevelTomlModel(readFileSync(join(home, ".codex", "config.toml"), "utf8"));
193
- return model ? {
194
- model,
195
- source: "~/.codex/config.toml"
196
- } : null;
197
- } catch {
198
- return null;
199
- }
200
- },
201
- args: ({ model }) => [
202
- "exec",
203
- "--sandbox",
204
- "workspace-write",
205
- ...model ? ["-m", model] : [],
206
- "-"
207
- ]
208
- },
209
177
  {
210
178
  id: "cursor",
211
179
  label: "Cursor CLI",
@@ -266,21 +234,6 @@ var AGENTS = [
266
234
  file: path,
267
235
  args: ["--acp"]
268
236
  })
269
- },
270
- {
271
- id: "gemini",
272
- label: "Gemini CLI",
273
- bins: ["gemini"],
274
- models: [],
275
- configuredModel: ({ home }) => {
276
- const model = readJson(join(home, ".gemini", "settings.json"))?.model;
277
- const name = typeof model === "string" ? model : model?.name;
278
- return typeof name === "string" && name ? {
279
- model: name,
280
- source: "~/.gemini/settings.json"
281
- } : null;
282
- },
283
- args: ({ model }) => [...model ? ["-m", model] : [], "--yolo"]
284
237
  }
285
238
  ];
286
239
  /** @param {string} file @param {NodeJS.Platform} platform */
@@ -856,8 +809,9 @@ function checkShell(command, { repo, platform = process.platform } = {}) {
856
809
  * habilitó en su agente.
857
810
  */
858
811
  /**
859
- * @typedef {"review" | "ask" | "edit"} PolicyMode
860
- * review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON.
812
+ * @typedef {"review" | "ask" | "edit" | "fix"} PolicyMode
813
+ * review = generar el JSON; ask = preguntas (no puede escribir nada); edit = modificar el JSON;
814
+ * fix = editar código dentro del repo (no solo el JSON).
861
815
  * @typedef {{ repo: string, outputFilename: string, mode: PolicyMode }} PolicyContext
862
816
  * @typedef {{
863
817
  * kind?: string | null,
@@ -1021,6 +975,16 @@ function decidePermission(toolCall, { repo, outputFilename, mode }) {
1021
975
  allow: false,
1022
976
  reason: `${kind === "delete" ? "borrado" : "edición"} sin path`
1023
977
  };
978
+ if (mode === "fix") {
979
+ const outside = paths.find((p) => !isInside(p, repo));
980
+ return outside ? {
981
+ allow: false,
982
+ reason: `path fuera del repo: ${outside}`
983
+ } : {
984
+ allow: true,
985
+ reason: "escritura en el repo"
986
+ };
987
+ }
1024
988
  const target = resolve(repo, outputRelPath(outputFilename));
1025
989
  const outside = paths.find((p) => !samePath(canonicalPath(p), canonicalPath(target)));
1026
990
  return outside ? {
@@ -1055,48 +1019,139 @@ function permissionOutcome(options, allow, opts = {}) {
1055
1019
  //#endregion
1056
1020
  //#region ../lib/agent-login.mjs
1057
1021
  /**
1058
- * Login de agentes ACP: mensajes claros + abrir una terminal interactiva cuando se pueda.
1022
+ * Login de agentes ACP: clasificación de authMethods, mensajes claros y lanzamiento del CLI de auth.
1023
+ *
1024
+ * Preferencia de launch:
1025
+ * 1. Misma TTY (stdio inherit) si stdin/stdout son TTY — CLI interactiva, exit code real.
1026
+ * 2. Proceso hijo oculto (sin ventana de terminal) si no hay TTY — p. ej. web; OAuth abre el browser.
1027
+ * 3. Terminal externa solo con `external: true` (raro: login que exige TTY interactiva).
1028
+ *
1029
+ * En Windows, `cmd /c start /wait` **no** propaga el exit code del proceso iniciado (siempre 0
1030
+ * hacia Node). El fallback externo usa un archivo sentinel, igual que macOS/Linux.
1059
1031
  */
1060
1032
  /**
1061
- * @typedef {{ bin: string, args: string[], display: string }} LoginCommand
1033
+ * @typedef {{ bin: string, args: string[], display: string, env?: Record<string, string> }} LoginCommand
1034
+ * @typedef {'agent' | 'terminal' | 'terminal-meta' | 'unknown'} AuthMethodKind
1035
+ * @typedef {'inherit' | 'hidden' | 'external'} LoginLaunchMode
1062
1036
  */
1063
1037
  /**
1064
- * Comando de login interactivo por agente (null = no hay CLI de login, p. ej. Cursor app).
1038
+ * Hostname de Copilot (Enterprise). Vacío / `github.com` = cuenta en github.com.
1039
+ * @param {string | null | undefined} host
1040
+ * @returns {string}
1041
+ */
1042
+ function normalizeCopilotLoginHost(host) {
1043
+ const h = String(host || "").trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
1044
+ if (!h || /^github\.com$/i.test(h)) return "";
1045
+ return h;
1046
+ }
1047
+ /**
1048
+ * Agrega `--host` a un comando `copilot login` cuando el usuario eligió Enterprise.
1049
+ * No fuerza nada por env: el host lo elige quien llama (CLI/UI).
1050
+ * @param {LoginCommand} cmd
1051
+ * @param {string | null | undefined} host
1052
+ * @returns {LoginCommand}
1053
+ */
1054
+ function withCopilotLoginHost(cmd, host) {
1055
+ if (!cmd || cmd.bin == null) return cmd;
1056
+ const h = normalizeCopilotLoginHost(host);
1057
+ if (!h) return cmd;
1058
+ const args = Array.isArray(cmd.args) ? [...cmd.args] : [];
1059
+ const hostIdx = args.findIndex((a) => a === "--host");
1060
+ if (hostIdx >= 0) args[hostIdx + 1] = h;
1061
+ else args.push("--host", h);
1062
+ const withoutHost = String(cmd.display || "").replace(/\s+--host\s+\S+/g, "").trim();
1063
+ return {
1064
+ ...cmd,
1065
+ args,
1066
+ display: `${withoutHost} --host ${h}`.trim()
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Comando de login interactivo por agente (fallback si no hay authMethod usable).
1065
1071
  * @param {string} agentId
1072
+ * @param {{ host?: string | null }} [opts] solo Copilot: host Enterprise opcional
1066
1073
  * @returns {LoginCommand | null}
1067
1074
  */
1068
- function loginCommand(agentId) {
1075
+ function loginCommand(agentId, opts = {}) {
1069
1076
  switch (agentId) {
1070
1077
  case "claude": return {
1071
1078
  bin: "claude",
1072
- args: ["/login"],
1073
- display: "claude /login"
1074
- };
1075
- case "copilot": return {
1076
- bin: "gh",
1077
1079
  args: ["auth", "login"],
1078
- display: "gh auth login"
1080
+ display: "claude auth login"
1079
1081
  };
1080
- case "codex": return {
1081
- bin: "codex",
1082
+ case "copilot": return withCopilotLoginHost({
1083
+ bin: "copilot",
1082
1084
  args: ["login"],
1083
- display: "codex login"
1084
- };
1085
- case "gemini": return {
1086
- bin: "gemini",
1087
- args: [],
1088
- display: "gemini"
1085
+ display: "copilot login"
1086
+ }, opts.host);
1087
+ case "cursor": return {
1088
+ bin: "cursor-agent",
1089
+ args: ["login"],
1090
+ display: "cursor-agent login"
1089
1091
  };
1090
1092
  default: return null;
1091
1093
  }
1092
1094
  }
1095
+ /**
1096
+ * Args de `claude auth login …` a partir de un authMethod ACP.
1097
+ * El adaptador anuncia `["--cli", "auth", "login", "--claudeai"]` (o lo mismo tras el path del
1098
+ * adapter en `_meta.terminal-auth.args`); `--cli` solo entiende el wrapper ACP, no `claude`.
1099
+ * @param {any} [method]
1100
+ * @returns {string[]}
1101
+ */
1102
+ function claudeCliLoginArgs(method) {
1103
+ const meta = method?._meta?.["terminal-auth"];
1104
+ const raw = Array.isArray(meta?.args) ? meta.args.map(String) : Array.isArray(method?.args) ? method.args.map(String) : [];
1105
+ const cliIdx = raw.indexOf("--cli");
1106
+ const after = cliIdx >= 0 ? raw.slice(cliIdx + 1) : raw;
1107
+ if (after[0] === "auth") return after;
1108
+ return [
1109
+ "auth",
1110
+ "login",
1111
+ ...after.filter((a) => a.startsWith("--"))
1112
+ ];
1113
+ }
1114
+ /**
1115
+ * Clasifica un authMethod de `initialize` según ACP v1 + extensión Copilot `_meta.terminal-auth`.
1116
+ * @param {any} method
1117
+ * @returns {AuthMethodKind}
1118
+ */
1119
+ function classifyAuthMethod(method) {
1120
+ if (!method || typeof method.id !== "string" || !method.id) return "unknown";
1121
+ if (method._meta != null && method._meta["terminal-auth"] != null) return "terminal-meta";
1122
+ if (method.type === "terminal") return "terminal";
1123
+ if (!method.type || method.type === "agent") return "agent";
1124
+ return "unknown";
1125
+ }
1126
+ /**
1127
+ * @param {any[]} authMethods
1128
+ * @returns {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }}
1129
+ */
1130
+ function groupAuthMethods(authMethods) {
1131
+ /** @type {{ agent: any[], terminal: any[], terminalMeta: any[], unknown: any[] }} */
1132
+ const groups = {
1133
+ agent: [],
1134
+ terminal: [],
1135
+ terminalMeta: [],
1136
+ unknown: []
1137
+ };
1138
+ for (const method of authMethods || []) {
1139
+ const kind = classifyAuthMethod(method);
1140
+ if (kind === "agent") groups.agent.push(method);
1141
+ else if (kind === "terminal") groups.terminal.push(method);
1142
+ else if (kind === "terminal-meta") groups.terminalMeta.push(method);
1143
+ else groups.unknown.push(method);
1144
+ }
1145
+ return groups;
1146
+ }
1093
1147
  /** @param {string} text */
1094
1148
  function looksLikeAuthFailure(text) {
1095
1149
  const s = String(text || "").toLowerCase();
1096
- return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run/.test(s);
1150
+ return /(not\s+)?auth|login|unauthor|not logged|sign[\s-]?in|credentials|token.*(invalid|expired|missing)|please\s+run|auth_required/.test(s);
1097
1151
  }
1098
1152
  /**
1099
1153
  * Mensaje en español si el agente murió / falló por falta de login.
1154
+ * Sin comandos CLI: el CTA abre el login por el usuario.
1100
1155
  * @param {string} agentId
1101
1156
  * @param {string} agentLabel
1102
1157
  * @param {string} detail
@@ -1104,38 +1159,280 @@ function looksLikeAuthFailure(text) {
1104
1159
  */
1105
1160
  function authFailureMessage$1(agentId, agentLabel, detail) {
1106
1161
  if (!looksLikeAuthFailure(detail)) return null;
1107
- const cmd = loginCommand(agentId);
1108
- if (agentId === "cursor") return `${agentLabel} no está autenticado. Iniciá sesión en la app de Cursor y reintentá.`;
1109
- if (cmd) return `${agentLabel} no está autenticado. Corré \`${cmd.display}\` en una terminal y reintentá.`;
1110
- return `${agentLabel} no está autenticado. Revisá el login del agente y reintentá.`;
1162
+ return `Necesitás iniciar sesión en ${String(agentLabel || "").trim() || "el agente"}.`;
1163
+ }
1164
+ /** Fallo del proceso de login (exit ≠ 0 o cancelado a medias). */
1165
+ function loginIncompleteMessage() {
1166
+ return "No se pudo completar el inicio de sesión. Probá de nuevo.";
1167
+ }
1168
+ /** Estado mientras corre el login (TTY, proceso oculto o terminal externa). */
1169
+ function loginWaitingMessage() {
1170
+ return "Esperando que completes el inicio de sesión…";
1111
1171
  }
1112
1172
  /**
1113
- * Abre una terminal nueva con el login del agente (Windows / macOS / Linux).
1114
- * @param {string} agentId
1173
+ * Método interactivo a abrir: `_meta.terminal-auth` (Copilot) o `type: terminal` (Claude).
1174
+ * Si solo hay métodos `agent` (Cursor), devolvemos null → `resolveLoginCommand` usa el fallback CLI.
1175
+ * @param {any[]} [methods]
1176
+ * @returns {any | null}
1177
+ */
1178
+ function pickLoginAuthMethod(methods) {
1179
+ const grouped = groupAuthMethods(methods || []);
1180
+ return grouped.terminalMeta[0] ?? grouped.terminal[0] ?? null;
1181
+ }
1182
+ /**
1183
+ * ¿Podemos correr el login en la misma terminal (stdio inherit)?
1184
+ * La web / procesos sin TTY usan proceso oculto (o `external: true` → ventana).
1185
+ * @param {{ external?: boolean }} [opts]
1186
+ */
1187
+ function canInheritLoginTty(opts = {}) {
1188
+ if (opts.external) return false;
1189
+ return Boolean(process.stdin?.isTTY && process.stdout?.isTTY);
1190
+ }
1191
+ /**
1192
+ * Modo de launch: inherit (CLI con TTY) → hidden (web / OAuth en browser) → external (forzado).
1193
+ * @param {{ wait?: boolean, external?: boolean }} [opts]
1194
+ * @returns {LoginLaunchMode}
1195
+ */
1196
+ function resolveLoginLaunchMode(opts = {}) {
1197
+ if (Boolean(opts.wait) && canInheritLoginTty(opts)) return "inherit";
1198
+ if (opts.external) return "external";
1199
+ return "hidden";
1200
+ }
1201
+ /**
1202
+ * Arma el comando de login a partir de un authMethod (o fallback por agentId).
1203
+ * @param {{ def: { id: string, acp?: Function }, path: string }} agent
1204
+ * @param {any} [method]
1205
+ * @returns {LoginCommand}
1206
+ */
1207
+ function resolveLoginCommand(agent, method) {
1208
+ const kind = method ? classifyAuthMethod(method) : "unknown";
1209
+ const meta = method?._meta?.["terminal-auth"];
1210
+ if (agent?.def?.id === "claude" && method && (kind === "terminal" || kind === "terminal-meta")) {
1211
+ const args = claudeCliLoginArgs(method);
1212
+ return {
1213
+ bin: typeof agent.path === "string" && agent.path.trim() ? agent.path : "claude",
1214
+ args,
1215
+ display: `claude ${args.join(" ")}`.trim(),
1216
+ ...method?.env && typeof method.env === "object" ? { env: method.env } : {}
1217
+ };
1218
+ }
1219
+ if ((kind === "terminal-meta" || kind === "terminal") && meta && typeof meta === "object") {
1220
+ const command = typeof meta.command === "string" ? meta.command.trim() : "";
1221
+ if (command) {
1222
+ const args = Array.isArray(meta.args) ? meta.args.map(String) : [];
1223
+ return {
1224
+ bin: command,
1225
+ args,
1226
+ display: (typeof meta.label === "string" && meta.label.trim() ? meta.label.trim() : "") || `${command}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
1227
+ ...method?.env && typeof method.env === "object" ? { env: method.env } : {}
1228
+ };
1229
+ }
1230
+ }
1231
+ if (kind === "terminal" && method && typeof agent?.def?.acp === "function") {
1232
+ const launch = agent.def.acp({ path: agent.path });
1233
+ const extra = Array.isArray(method.args) ? method.args.map(String) : [];
1234
+ const env = {
1235
+ ...launch.env && typeof launch.env === "object" ? launch.env : {},
1236
+ ...method.env && typeof method.env === "object" ? method.env : {}
1237
+ };
1238
+ const args = [...launch.args || [], ...extra];
1239
+ return {
1240
+ bin: launch.file,
1241
+ args,
1242
+ display: `${launch.file}${args.length ? ` ${args.join(" ")}` : ""}`.trim(),
1243
+ ...Object.keys(env).length ? { env } : {}
1244
+ };
1245
+ }
1246
+ const fallback = loginCommand(agent?.def?.id);
1247
+ if (fallback) {
1248
+ if (agent?.def?.id === "claude" && typeof agent.path === "string" && agent.path.trim()) return {
1249
+ ...fallback,
1250
+ bin: agent.path
1251
+ };
1252
+ return fallback;
1253
+ }
1254
+ throw new Error("Este agente no tiene un inicio de sesión conocido.");
1255
+ }
1256
+ /** @param {import('node:child_process').ChildProcess} child */
1257
+ function waitExit(child) {
1258
+ return new Promise((resolve, reject) => {
1259
+ child.once("error", reject);
1260
+ child.once("exit", (code, signal) => resolve(signal ? 1 : code ?? 1));
1261
+ });
1262
+ }
1263
+ /**
1264
+ * Lanza el login en background (proceso oculto; no espera). Compat con UI/CTA simples (web sin sesión).
1265
+ * @param {string | LoginCommand} agentIdOrCmd
1115
1266
  * @returns {{ display: string }}
1116
1267
  */
1117
- function openLoginTerminal(agentId) {
1118
- const cmd = loginCommand(agentId);
1119
- if (!cmd) throw new Error(agentId === "cursor" ? "Cursor se autentica desde la app de escritorio, no desde una terminal." : "Este agente no tiene un comando de login conocido.");
1120
- const platform = process.platform;
1121
- if (platform === "win32") spawn("cmd.exe", [
1122
- "/c",
1123
- "start",
1124
- `${cmd.bin} login`,
1125
- "cmd.exe",
1126
- "/k",
1127
- cmd.bin,
1128
- ...cmd.args
1129
- ], {
1130
- detached: true,
1268
+ function openLoginTerminal(agentIdOrCmd) {
1269
+ const cmd = typeof agentIdOrCmd === "string" ? loginCommand(agentIdOrCmd) : agentIdOrCmd;
1270
+ if (!cmd) throw new Error((typeof agentIdOrCmd === "string" ? agentIdOrCmd : "") === "cursor" ? "Necesitás iniciar sesión en Cursor." : "Este agente no tiene un inicio de sesión conocido.");
1271
+ launchLoginProcess(cmd, {
1272
+ wait: false,
1273
+ mode: "hidden"
1274
+ });
1275
+ return { display: cmd.display };
1276
+ }
1277
+ /**
1278
+ * Lanza el login. Con TTY y `wait: true`, corre en esta terminal (inherit); si no, proceso oculto
1279
+ * (OAuth en browser). Solo con `external: true` abre una ventana de terminal del OS.
1280
+ * @param {LoginCommand} cmd
1281
+ * @param {{ wait?: boolean, cwd?: string, external?: boolean }} [opts]
1282
+ * @returns {Promise<{ display: string, code: number | null, mode: LoginLaunchMode }>}
1283
+ */
1284
+ async function runLoginTerminal(cmd, opts = {}) {
1285
+ const wait = Boolean(opts.wait);
1286
+ const mode = resolveLoginLaunchMode(opts);
1287
+ const code = await launchLoginProcess(cmd, {
1288
+ wait,
1289
+ cwd: opts.cwd,
1290
+ mode
1291
+ });
1292
+ return {
1293
+ display: cmd.display,
1294
+ code: wait ? code : null,
1295
+ mode
1296
+ };
1297
+ }
1298
+ /** @param {string} arg */
1299
+ function quoteBatArg(arg) {
1300
+ const s = String(arg);
1301
+ if (/^[\w./:\\@=-]+$/.test(s)) return s;
1302
+ return `"${s.replace(/"/g, "\"\"")}"`;
1303
+ }
1304
+ /**
1305
+ * Espera a que el sentinel tenga un exit code (macOS/Linux/Windows externo).
1306
+ * @param {string} sentinel
1307
+ * @param {string} display
1308
+ * @returns {Promise<number>}
1309
+ */
1310
+ async function waitForExitSentinel(sentinel, display) {
1311
+ const deadline = Date.now() + 1800 * 1e3;
1312
+ while (Date.now() < deadline) {
1313
+ try {
1314
+ const raw = readFileSync(sentinel, "utf8").trim();
1315
+ if (raw !== "") {
1316
+ const code = Number.parseInt(raw, 10);
1317
+ try {
1318
+ unlinkSync(sentinel);
1319
+ } catch {}
1320
+ return Number.isFinite(code) ? code : 1;
1321
+ }
1322
+ } catch {}
1323
+ await new Promise((r) => setTimeout(r, 400));
1324
+ }
1325
+ try {
1326
+ unlinkSync(sentinel);
1327
+ } catch {}
1328
+ throw new Error(`Timeout esperando el login (${display}).`);
1329
+ }
1330
+ /**
1331
+ * Proceso hijo sin ventana de terminal (web / OAuth). Exit code real vía waitExit.
1332
+ * @param {LoginCommand} cmd
1333
+ * @param {{ wait: boolean, cwd?: string, env: NodeJS.ProcessEnv }} opts
1334
+ * @returns {Promise<number>}
1335
+ */
1336
+ async function launchHiddenLogin(cmd, { wait, cwd, env }) {
1337
+ const child = spawnCommand(cmd.bin, cmd.args, {
1338
+ cwd,
1339
+ env,
1131
1340
  stdio: "ignore",
1132
- windowsHide: false
1133
- }).unref();
1134
- else if (platform === "darwin") spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`], {
1135
- detached: true,
1136
- stdio: "ignore"
1137
- }).unref();
1138
- else {
1341
+ windowsHide: true,
1342
+ ...wait ? {} : { detached: true }
1343
+ });
1344
+ if (!wait) {
1345
+ child.unref();
1346
+ return 0;
1347
+ }
1348
+ return waitExit(child);
1349
+ }
1350
+ /**
1351
+ * @param {LoginCommand} cmd
1352
+ * @param {{ wait: boolean, cwd?: string, mode: LoginLaunchMode }} opts
1353
+ * @returns {Promise<number>}
1354
+ */
1355
+ async function launchLoginProcess(cmd, { wait, cwd, mode }) {
1356
+ const env = {
1357
+ ...process.env,
1358
+ ...cmd.env || {}
1359
+ };
1360
+ if (mode === "inherit") return waitExit(spawnCommand(cmd.bin, cmd.args, {
1361
+ cwd,
1362
+ env,
1363
+ stdio: "inherit"
1364
+ }));
1365
+ if (mode === "hidden") return launchHiddenLogin(cmd, {
1366
+ wait,
1367
+ cwd,
1368
+ env
1369
+ });
1370
+ const platform = process.platform;
1371
+ if (platform === "win32") {
1372
+ if (!wait) {
1373
+ spawn("cmd.exe", [
1374
+ "/c",
1375
+ "start",
1376
+ "diff-review login",
1377
+ "cmd.exe",
1378
+ "/k",
1379
+ cmd.bin,
1380
+ ...cmd.args
1381
+ ], {
1382
+ cwd,
1383
+ env,
1384
+ detached: true,
1385
+ stdio: "ignore",
1386
+ windowsHide: false
1387
+ }).unref();
1388
+ return 0;
1389
+ }
1390
+ const id = randomBytes(8).toString("hex");
1391
+ const sentinel = join(tmpdir(), `diff-review-login-${id}.exit`);
1392
+ const bat = join(tmpdir(), `diff-review-login-${id}.bat`);
1393
+ const line = [quoteBatArg(cmd.bin), ...cmd.args.map(quoteBatArg)].join(" ");
1394
+ const sentPath = sentinel.replace(/"/g, "\"\"");
1395
+ writeFileSync(bat, [
1396
+ `@echo off`,
1397
+ `call ${line}`,
1398
+ `> "${sentPath}" echo %ERRORLEVEL%`,
1399
+ ``
1400
+ ].join("\r\n"));
1401
+ try {
1402
+ writeFileSync(sentinel, "");
1403
+ } catch {}
1404
+ const child = spawn("cmd.exe", [
1405
+ "/c",
1406
+ "start",
1407
+ "/wait",
1408
+ "diff-review login",
1409
+ "cmd.exe",
1410
+ "/c",
1411
+ bat
1412
+ ], {
1413
+ cwd,
1414
+ env,
1415
+ stdio: "ignore",
1416
+ windowsHide: false
1417
+ });
1418
+ try {
1419
+ await waitExit(child);
1420
+ return await waitForExitSentinel(sentinel, cmd.display);
1421
+ } finally {
1422
+ try {
1423
+ unlinkSync(bat);
1424
+ } catch {}
1425
+ }
1426
+ }
1427
+ if (!wait) {
1428
+ if (platform === "darwin") {
1429
+ spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(`${cmd.bin} ${cmd.args.join(" ")}`.trim())}`], {
1430
+ detached: true,
1431
+ stdio: "ignore",
1432
+ env
1433
+ }).unref();
1434
+ return 0;
1435
+ }
1139
1436
  const line = `${cmd.bin} ${cmd.args.join(" ")}`.trim();
1140
1437
  const tried = [
1141
1438
  ["x-terminal-emulator", [
@@ -1163,18 +1460,375 @@ function openLoginTerminal(agentId) {
1163
1460
  `${line}; exec bash`
1164
1461
  ]]
1165
1462
  ];
1166
- let ok = false;
1167
1463
  for (const [bin, args] of tried) try {
1168
1464
  spawn(bin, args, {
1169
1465
  detached: true,
1170
- stdio: "ignore"
1466
+ stdio: "ignore",
1467
+ env,
1468
+ cwd
1171
1469
  }).unref();
1470
+ return 0;
1471
+ } catch {}
1472
+ throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
1473
+ }
1474
+ const sentinel = join(tmpdir(), `diff-review-login-${randomBytes(8).toString("hex")}.exit`);
1475
+ try {
1476
+ writeFileSync(sentinel, "");
1477
+ } catch {}
1478
+ const line = `${[cmd.bin, ...cmd.args].map((a) => JSON.stringify(a)).join(" ")}; echo $? > ${JSON.stringify(sentinel)}`;
1479
+ if (platform === "darwin") spawn("osascript", ["-e", `tell application "Terminal" to do script ${JSON.stringify(line)}`], {
1480
+ stdio: "ignore",
1481
+ env,
1482
+ cwd
1483
+ });
1484
+ else {
1485
+ const tried = [
1486
+ ["gnome-terminal", [
1487
+ "--wait",
1488
+ "--",
1489
+ "bash",
1490
+ "-lc",
1491
+ line
1492
+ ]],
1493
+ ["x-terminal-emulator", [
1494
+ "-e",
1495
+ "bash",
1496
+ "-lc",
1497
+ line
1498
+ ]],
1499
+ ["konsole", [
1500
+ "-e",
1501
+ "bash",
1502
+ "-lc",
1503
+ line
1504
+ ]],
1505
+ ["xterm", [
1506
+ "-e",
1507
+ "bash",
1508
+ "-lc",
1509
+ line
1510
+ ]]
1511
+ ];
1512
+ let ok = false;
1513
+ for (const [bin, args] of tried) try {
1514
+ await waitExit(spawn(bin, args, {
1515
+ stdio: "ignore",
1516
+ env,
1517
+ cwd
1518
+ }));
1172
1519
  ok = true;
1173
1520
  break;
1174
1521
  } catch {}
1175
1522
  if (!ok) throw new Error(`No pude abrir una terminal. Corré a mano: ${cmd.display}`);
1176
1523
  }
1177
- return { display: cmd.display };
1524
+ return waitForExitSentinel(sentinel, cmd.display);
1525
+ }
1526
+ /**
1527
+ * Cuenta activa del agente (para el header de la web).
1528
+ * @param {string} agentId
1529
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
1530
+ * @returns {{
1531
+ * agentId: string,
1532
+ * loggedIn: boolean,
1533
+ * accountLabel: string | null,
1534
+ * detail: string | null,
1535
+ * source: 'gh' | 'claude' | 'env' | 'unknown' | null,
1536
+ * warnings: string[],
1537
+ * }}
1538
+ */
1539
+ function agentAccountStatus(agentId, opts = {}) {
1540
+ const id = String(agentId || "").trim();
1541
+ const env = opts.env || process.env;
1542
+ const run = opts.run || spawnSync;
1543
+ /** @type {string[]} */
1544
+ const warnings = [];
1545
+ if (id === "copilot") {
1546
+ const envHits = copilotTokenEnvOverrides(env);
1547
+ if (envHits.length) {
1548
+ warnings.push(`${envHits.join(", ")} en el entorno (pisa otras cuentas).`);
1549
+ return {
1550
+ agentId: id,
1551
+ loggedIn: true,
1552
+ accountLabel: "Token de entorno",
1553
+ detail: envHits.join(", "),
1554
+ source: "env",
1555
+ warnings
1556
+ };
1557
+ }
1558
+ const accounts = listGhAuthAccounts({
1559
+ env,
1560
+ run
1561
+ });
1562
+ const active = accounts.find((a) => a.active) || accounts[0] || null;
1563
+ if (active) return {
1564
+ agentId: id,
1565
+ loggedIn: true,
1566
+ accountLabel: active.user,
1567
+ detail: active.host,
1568
+ source: "gh",
1569
+ warnings
1570
+ };
1571
+ return {
1572
+ agentId: id,
1573
+ loggedIn: false,
1574
+ accountLabel: null,
1575
+ detail: null,
1576
+ source: null,
1577
+ warnings
1578
+ };
1579
+ }
1580
+ if (id === "claude") {
1581
+ const r = run("claude", [
1582
+ "auth",
1583
+ "status",
1584
+ "--json"
1585
+ ], {
1586
+ encoding: "utf8",
1587
+ env,
1588
+ windowsHide: true
1589
+ });
1590
+ if (r.error) return {
1591
+ agentId: id,
1592
+ loggedIn: false,
1593
+ accountLabel: null,
1594
+ detail: null,
1595
+ source: null,
1596
+ warnings: [r.error.message]
1597
+ };
1598
+ try {
1599
+ const parsed = JSON.parse(String(r.stdout || "{}"));
1600
+ const loggedIn = Boolean(parsed?.loggedIn);
1601
+ const email = String(parsed?.email || "").trim() || null;
1602
+ const org = String(parsed?.orgName || "").trim() || null;
1603
+ return {
1604
+ agentId: id,
1605
+ loggedIn,
1606
+ accountLabel: loggedIn ? email || "Claude" : null,
1607
+ detail: loggedIn ? org || String(parsed?.authMethod || "") || null : null,
1608
+ source: loggedIn ? "claude" : null,
1609
+ warnings
1610
+ };
1611
+ } catch {
1612
+ return {
1613
+ agentId: id,
1614
+ loggedIn: false,
1615
+ accountLabel: null,
1616
+ detail: null,
1617
+ source: null,
1618
+ warnings: ["No pude leer el estado de Claude."]
1619
+ };
1620
+ }
1621
+ }
1622
+ if (id === "cursor") return {
1623
+ agentId: id,
1624
+ loggedIn: true,
1625
+ accountLabel: "Cursor",
1626
+ detail: "Sesión del CLI / app",
1627
+ source: "unknown",
1628
+ warnings
1629
+ };
1630
+ return {
1631
+ agentId: id,
1632
+ loggedIn: false,
1633
+ accountLabel: null,
1634
+ detail: null,
1635
+ source: null,
1636
+ warnings
1637
+ };
1638
+ }
1639
+ /**
1640
+ * Env vars que Copilot usa antes que OAuth / gh (impiden un cambio de cuenta real).
1641
+ * @param {NodeJS.ProcessEnv} [env]
1642
+ * @returns {string[]}
1643
+ */
1644
+ function copilotTokenEnvOverrides(env = process.env) {
1645
+ /** @type {string[]} */
1646
+ const hit = [];
1647
+ for (const key of [
1648
+ "COPILOT_GITHUB_TOKEN",
1649
+ "GH_TOKEN",
1650
+ "GITHUB_TOKEN"
1651
+ ]) if (String(env[key] || "").trim()) hit.push(key);
1652
+ return hit;
1653
+ }
1654
+ /**
1655
+ * Cuentas conocidas por `gh auth status --json hosts`.
1656
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync }} [opts]
1657
+ * @returns {{ host: string, user: string, active: boolean }[]}
1658
+ */
1659
+ function listGhAuthAccounts(opts = {}) {
1660
+ const env = opts.env || process.env;
1661
+ const r = (opts.run || spawnSync)("gh", [
1662
+ "auth",
1663
+ "status",
1664
+ "--json",
1665
+ "hosts"
1666
+ ], {
1667
+ encoding: "utf8",
1668
+ env,
1669
+ windowsHide: true
1670
+ });
1671
+ if (r.error) return [];
1672
+ try {
1673
+ const parsed = JSON.parse(String(r.stdout || "{}"));
1674
+ const hosts = parsed?.hosts && typeof parsed.hosts === "object" ? parsed.hosts : {};
1675
+ /** @type {{ host: string, user: string, active: boolean }[]} */
1676
+ const out = [];
1677
+ for (const [host, accounts] of Object.entries(hosts)) {
1678
+ const list = Array.isArray(accounts) ? accounts : [];
1679
+ for (const acc of list) {
1680
+ const user = String(acc?.login || acc?.user || "").trim();
1681
+ if (!user) continue;
1682
+ out.push({
1683
+ host: String(host),
1684
+ user,
1685
+ active: Boolean(acc?.active)
1686
+ });
1687
+ }
1688
+ }
1689
+ return out;
1690
+ } catch {
1691
+ return [];
1692
+ }
1693
+ }
1694
+ /**
1695
+ * Cierra sesiones de `gh` (Copilot las reusa si no hay OAuth propio).
1696
+ * @param {{ env?: NodeJS.ProcessEnv, run?: typeof spawnSync, host?: string | null }} [opts]
1697
+ * @returns {{ ok: boolean, loggedOut: { host: string, user: string }[], warnings: string[] }}
1698
+ */
1699
+ function logoutGhAuthAccounts(opts = {}) {
1700
+ const env = opts.env || process.env;
1701
+ const run = opts.run || spawnSync;
1702
+ const onlyHost = normalizeCopilotLoginHost(opts.host) || null;
1703
+ const accounts = listGhAuthAccounts({
1704
+ env,
1705
+ run
1706
+ }).filter((a) => onlyHost ? a.host === onlyHost || a.host === `https://${onlyHost}` : true);
1707
+ /** @type {{ host: string, user: string }[]} */
1708
+ const loggedOut = [];
1709
+ /** @type {string[]} */
1710
+ const warnings = [];
1711
+ for (const acc of accounts) {
1712
+ const r = run("gh", [
1713
+ "auth",
1714
+ "logout",
1715
+ "--hostname",
1716
+ acc.host,
1717
+ "--user",
1718
+ acc.user
1719
+ ], {
1720
+ encoding: "utf8",
1721
+ env,
1722
+ windowsHide: true
1723
+ });
1724
+ if (r.error) {
1725
+ warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${r.error.message}`);
1726
+ continue;
1727
+ }
1728
+ if (r.status !== 0) {
1729
+ const err = String(r.stderr || r.stdout || "").trim() || `exit ${r.status}`;
1730
+ warnings.push(`No pude cerrar ${acc.user}@${acc.host}: ${err}`);
1731
+ continue;
1732
+ }
1733
+ loggedOut.push({
1734
+ host: acc.host,
1735
+ user: acc.user
1736
+ });
1737
+ }
1738
+ const envHits = copilotTokenEnvOverrides(env);
1739
+ if (envHits.length) warnings.push(`Hay ${envHits.join(", ")} en el entorno: Copilot seguirá usando ese token hasta que lo saques.`);
1740
+ return {
1741
+ ok: warnings.length === 0 || loggedOut.length > 0,
1742
+ loggedOut,
1743
+ warnings
1744
+ };
1745
+ }
1746
+ /**
1747
+ * Comando de logout CLI por agente (sin depender de gh).
1748
+ * @param {string} agentId
1749
+ * @returns {LoginCommand | null}
1750
+ */
1751
+ function logoutCommand(agentId) {
1752
+ switch (agentId) {
1753
+ case "claude": return {
1754
+ bin: "claude",
1755
+ args: ["auth", "logout"],
1756
+ display: "claude auth logout"
1757
+ };
1758
+ default: return null;
1759
+ }
1760
+ }
1761
+ /**
1762
+ * Cierra la sesión del agente para poder elegir otra cuenta.
1763
+ * Copilot: también desloguea cuentas de `gh` (si solo hacés logout OAuth, gh sigue ganando).
1764
+ *
1765
+ * @param {string} agentId
1766
+ * @param {{
1767
+ * host?: string | null,
1768
+ * env?: NodeJS.ProcessEnv,
1769
+ * run?: typeof spawnSync,
1770
+ * runTerminal?: typeof runLoginTerminal,
1771
+ * cwd?: string,
1772
+ * }} [opts]
1773
+ * @returns {Promise<{ ok: boolean, steps: string[], warnings: string[] }>}
1774
+ */
1775
+ async function runAgentLogout(agentId, opts = {}) {
1776
+ const id = String(agentId || "").trim();
1777
+ /** @type {string[]} */
1778
+ const steps = [];
1779
+ /** @type {string[]} */
1780
+ const warnings = [];
1781
+ const env = opts.env || process.env;
1782
+ const run = opts.run || spawnSync;
1783
+ const runTerm = opts.runTerminal || runLoginTerminal;
1784
+ if (id === "copilot") {
1785
+ const gh = logoutGhAuthAccounts({
1786
+ env,
1787
+ run,
1788
+ host: opts.host
1789
+ });
1790
+ for (const a of gh.loggedOut) steps.push(`Cerré ${a.user} en ${a.host} (GitHub CLI)`);
1791
+ warnings.push(...gh.warnings);
1792
+ if (!gh.loggedOut.length && !gh.warnings.length) steps.push("No había cuentas de GitHub CLI activas.");
1793
+ return {
1794
+ ok: true,
1795
+ steps,
1796
+ warnings
1797
+ };
1798
+ }
1799
+ const cmd = logoutCommand(id);
1800
+ if (!cmd) return {
1801
+ ok: false,
1802
+ steps,
1803
+ warnings: ["Este agente no tiene cierre de sesión desde la app."]
1804
+ };
1805
+ try {
1806
+ const result = await runTerm(cmd, {
1807
+ wait: true,
1808
+ cwd: opts.cwd
1809
+ });
1810
+ if (result.code !== 0) {
1811
+ warnings.push(`El logout terminó con código ${result.code ?? "?"}.`);
1812
+ return {
1813
+ ok: false,
1814
+ steps,
1815
+ warnings
1816
+ };
1817
+ }
1818
+ steps.push(`Cerré la sesión (${cmd.display}).`);
1819
+ return {
1820
+ ok: true,
1821
+ steps,
1822
+ warnings
1823
+ };
1824
+ } catch (err) {
1825
+ warnings.push(err instanceof Error ? err.message : String(err));
1826
+ return {
1827
+ ok: false,
1828
+ steps,
1829
+ warnings
1830
+ };
1831
+ }
1178
1832
  }
1179
1833
  /** @returns {string} */
1180
1834
  function modelsCachePath(home = homedir()) {
@@ -1250,6 +1904,23 @@ function writeModelsCache(agentId, data, opts = {}) {
1250
1904
  * review y después para las preguntas y los cambios que se le pidan desde la web. Cada pedido de permiso pasa por
1251
1905
  * `decide` (ver `acp-policy.mjs`): no hay nadie mirando una terminal para aprobar.
1252
1906
  */
1907
+ /**
1908
+ * session/new (u otra op) requiere login interactivo (terminal / terminal-meta).
1909
+ * No es un fallo genérico: la UI debe abrir el login y reintentar.
1910
+ */
1911
+ var AcpAuthRequiredError = class extends Error {
1912
+ /**
1913
+ * @param {string} message
1914
+ * @param {{ methods?: any[], agentId?: string, agentLabel?: string }} [info]
1915
+ */
1916
+ constructor(message, info = {}) {
1917
+ super(message);
1918
+ this.name = "AcpAuthRequiredError";
1919
+ this.methods = Array.isArray(info.methods) ? info.methods : [];
1920
+ this.agentId = info.agentId || "";
1921
+ this.agentLabel = info.agentLabel || "";
1922
+ }
1923
+ };
1253
1924
  /** El agente tiene un modo ACP declarado (no todos los agentes lo traen). */
1254
1925
  function supportsAcp(agent) {
1255
1926
  return typeof agent?.def?.acp === "function";
@@ -1280,7 +1951,7 @@ var ACP_START_TIMEOUT_MS = 9e4;
1280
1951
  * @returns {Promise<AcpSession>}
1281
1952
  */
1282
1953
  async function startAcpSession(agent, opts) {
1283
- if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no tiene modo ACP.`);
1954
+ if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
1284
1955
  const { file, args, env, sessionMeta } = agent.def.acp({ path: agent.path });
1285
1956
  const child = spawnCommand(file, args, {
1286
1957
  cwd: opts.cwd,
@@ -1358,16 +2029,21 @@ async function startAcpSession(agent, opts) {
1358
2029
  readTextFile: false,
1359
2030
  writeTextFile: false
1360
2031
  },
1361
- terminal: false
2032
+ terminal: false,
2033
+ auth: { terminal: true },
2034
+ _meta: { "terminal-auth": true }
1362
2035
  },
1363
2036
  clientInfo: {
1364
2037
  name: "diff-review",
1365
2038
  version: "0"
1366
2039
  }
1367
2040
  });
1368
- /** Métodos donde el agente maneja el login por `authenticate` (no terminal interactiva). */
1369
- const agentAuthMethods = (Array.isArray(init.authMethods) ? init.authMethods : []).filter((m) => m && (!m.type || m.type === "agent") && m.id);
1370
- /** Si session/new falla por auth, pedimos `authenticate` una vez y reintentamos. */
2041
+ const authMethods = Array.isArray(init.authMethods) ? init.authMethods : [];
2042
+ const grouped = groupAuthMethods(authMethods);
2043
+ /** Solo type agent (p. ej. Cursor `cursor_login`). Nunca terminal ni Copilot `_meta.terminal-auth`. */
2044
+ const agentAuthMethods = grouped.agent;
2045
+ const interactiveAuthMethods = [...grouped.terminal, ...grouped.terminalMeta];
2046
+ /** Si session/new falla por auth, pedimos `authenticate` una vez (métodos agent) y reintentamos. */
1371
2047
  const tryAuthenticate = async () => {
1372
2048
  for (const method of agentAuthMethods) try {
1373
2049
  await conn.agent.request(acp.methods.agent.authenticate, { methodId: method.id });
@@ -1375,6 +2051,14 @@ async function startAcpSession(agent, opts) {
1375
2051
  } catch {}
1376
2052
  return false;
1377
2053
  };
2054
+ const authRequired = (msg) => {
2055
+ const label = agent.def.label;
2056
+ return new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, label, msg) || `Necesitás iniciar sesión en ${label}.`, {
2057
+ methods: interactiveAuthMethods.length ? interactiveAuthMethods : authMethods,
2058
+ agentId: agent.def.id,
2059
+ agentLabel: label
2060
+ });
2061
+ };
1378
2062
  const openNewSession = async () => {
1379
2063
  try {
1380
2064
  return await conn.agent.request(acp.methods.agent.session.new, {
@@ -1384,11 +2068,13 @@ async function startAcpSession(agent, opts) {
1384
2068
  });
1385
2069
  } catch (err) {
1386
2070
  const msg = err instanceof Error ? err.message : String(err);
1387
- if (agentAuthMethods.length && looksLikeAuthFailure(msg) && await tryAuthenticate()) return await conn.agent.request(acp.methods.agent.session.new, {
2071
+ if (!looksLikeAuthFailure(msg)) throw err;
2072
+ if (agentAuthMethods.length && await tryAuthenticate()) return await conn.agent.request(acp.methods.agent.session.new, {
1388
2073
  cwd: opts.cwd,
1389
2074
  mcpServers: [],
1390
2075
  ...sessionMeta ? { _meta: sessionMeta } : {}
1391
2076
  });
2077
+ if (interactiveAuthMethods.length || agentAuthMethods.length || looksLikeAuthFailure(msg)) throw authRequired(msg);
1392
2078
  throw err;
1393
2079
  }
1394
2080
  };
@@ -1420,7 +2106,7 @@ async function startAcpSession(agent, opts) {
1420
2106
  if (!wanted || wanted === currentModel) return { currentModel };
1421
2107
  if (!models) return {
1422
2108
  currentModel,
1423
- warning: `El agente no deja elegir el modelo por ACP; usa el suyo.`
2109
+ warning: `El agente no deja elegir el modelo; usa el suyo.`
1424
2110
  };
1425
2111
  if (!models.options.some((o) => o.value === wanted)) return {
1426
2112
  currentModel,
@@ -1440,6 +2126,7 @@ async function startAcpSession(agent, opts) {
1440
2126
  const applied = await setModel(initialWanted);
1441
2127
  if (applied.warning) modelWarning = applied.warning;
1442
2128
  }
2129
+ const supportsLogout = Boolean(init.agentCapabilities?.auth?.logout);
1443
2130
  clearTimeout(timer);
1444
2131
  return {
1445
2132
  sessionId,
@@ -1448,6 +2135,7 @@ async function startAcpSession(agent, opts) {
1448
2135
  get currentModel() {
1449
2136
  return currentModel;
1450
2137
  },
2138
+ supportsLogout,
1451
2139
  ...modelWarning ? { modelWarning } : {},
1452
2140
  setModel,
1453
2141
  prompt: async (text) => {
@@ -1461,6 +2149,11 @@ async function startAcpSession(agent, opts) {
1461
2149
  return { stopReason: String(res.stopReason) };
1462
2150
  },
1463
2151
  cancel: () => conn.agent.notify(acp.methods.agent.session.cancel, { sessionId }),
2152
+ /** ACP `logout` si el agente lo anunció; no garantiza nada sobre credenciales de `gh`. */
2153
+ logout: async () => {
2154
+ if (!supportsLogout) throw new Error(`${agent.def.label} no admite cerrar sesión desde acá.`);
2155
+ await conn.agent.request(acp.methods.agent.logout, {});
2156
+ },
1464
2157
  close,
1465
2158
  closed: conn.closed,
1466
2159
  stderrTail: () => stderr
@@ -1468,13 +2161,24 @@ async function startAcpSession(agent, opts) {
1468
2161
  } catch (err) {
1469
2162
  clearTimeout(timer);
1470
2163
  close();
2164
+ if (err instanceof AcpAuthRequiredError) throw err;
1471
2165
  const tail = stderr.trim().split(/\r?\n/).slice(-8).join("\n");
1472
2166
  if (timedOut) {
1473
2167
  const msg = `Timeout (${Math.round(timeoutMs / 1e3)}s) al lanzar ${agent.def.label}. ¿Está autenticado el CLI?`;
1474
- throw new Error(tail ? `${msg}\n${tail}` : msg);
2168
+ const full = tail ? `${msg}\n${tail}` : msg;
2169
+ if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
2170
+ agentId: agent.def.id,
2171
+ agentLabel: agent.def.label
2172
+ });
2173
+ throw new Error(full);
1475
2174
  }
1476
2175
  const reason = err instanceof Error ? err.message : String(err);
1477
- throw new Error(tail ? `${reason}\n${tail}` : reason);
2176
+ const full = tail ? `${reason}\n${tail}` : reason;
2177
+ if (looksLikeAuthFailure(full)) throw new AcpAuthRequiredError(authFailureMessage$1(agent.def.id, agent.def.label, full) || full, {
2178
+ agentId: agent.def.id,
2179
+ agentLabel: agent.def.label
2180
+ });
2181
+ throw new Error(full);
1478
2182
  }
1479
2183
  }
1480
2184
  /**
@@ -1690,12 +2394,15 @@ function formatCheckReport(file, command, result) {
1690
2394
  /**
1691
2395
  * `agentSession` = la sesión ACP que generó la review, para seguir conversando con el agente desde la web.
1692
2396
  * @param {string} filePath
1693
- * @param {{ agent?: string, model?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
2397
+ * `modelLabel` = nombre legible que el agente da al modelo ("Haiku 5.5"): `model` puede ser un alias
2398
+ * (`haiku`) que más adelante apunte a otra versión.
2399
+ * @param {{ agent?: string, model?: string, modelLabel?: string, agentSession?: { agentId: string, sessionId: string, outputFilename: string } }} provenance
1694
2400
  * @returns {boolean}
1695
2401
  */
1696
- function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
2402
+ function stampReviewProvenance(filePath, { agent, model, modelLabel, agentSession } = {}) {
1697
2403
  const agentName = String(agent || "").trim();
1698
2404
  const modelId = String(model || "").trim();
2405
+ const label = String(modelLabel || "").trim();
1699
2406
  if (!agentName && !modelId && !agentSession) return false;
1700
2407
  let payload;
1701
2408
  try {
@@ -1707,7 +2414,11 @@ function stampReviewProvenance(filePath, { agent, model, agentSession } = {}) {
1707
2414
  const current = payload.meta;
1708
2415
  const meta = current && typeof current === "object" && !Array.isArray(current) ? { ...current } : {};
1709
2416
  if (agentName) meta.agent = agentName;
1710
- if (modelId) meta.model = modelId;
2417
+ if (modelId) {
2418
+ meta.model = modelId;
2419
+ if (label && label !== modelId) meta.modelLabel = label;
2420
+ else delete meta.modelLabel;
2421
+ }
1711
2422
  if (agentSession) meta.agentSession = { ...agentSession };
1712
2423
  payload.meta = meta;
1713
2424
  writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
@@ -1736,7 +2447,7 @@ var OUTPUT_SCHEMA_BLOCK = `{
1736
2447
  "id": "t1",
1737
2448
  "kind": "feat|fix|refactor|perf|test|chore|docs|infra",
1738
2449
  "title": "string, aim for ≤72 chars, imperative, no kind prefix",
1739
- "intent": "string"
2450
+ "intent": "string: why this theme exists + what it changes for the reader (not a restatement of the title)"
1740
2451
  }
1741
2452
  ],
1742
2453
  "blocks": [
@@ -1749,8 +2460,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
1749
2460
  "start": 40,
1750
2461
  "end": 58,
1751
2462
  "op": "add|mod|del|rename",
1752
- "what": "string, aim for ≤120 chars: concrete change + effect",
1753
- "why": "string, aim for ≤120 chars",
2463
+ "what": "string: concrete change + effect (aim ≤120 chars; longer OK if needed for clarity)",
2464
+ "why": "string: motivation / why this change belongs in the branch (aim ≤120 chars)",
1754
2465
  "source": "code|commit|pr|issue|inferred"
1755
2466
  }
1756
2467
  ],
@@ -1764,8 +2475,8 @@ var OUTPUT_SCHEMA_BLOCK = `{
1764
2475
  "file": "relative/path.ext",
1765
2476
  "line": 87,
1766
2477
  "block": "b1",
1767
- "what": "string, aim for ≤120 chars: concrete cause + observable consequence (what fails / risk / cost)",
1768
- "fix": "string, aim for ≤120 chars: suggested change + what the reader gains or avoids if applied"
2478
+ "what": "string: cause + why it is a problem + observable consequence (aim ≤120 chars; prefer clarity over brevity)",
2479
+ "fix": "string: suggested change + why that change resolves the problem / what it avoids (aim ≤120 chars)"
1769
2480
  }
1770
2481
  ],
1771
2482
  "skipped": [
@@ -1779,13 +2490,16 @@ var RULES = `- Ids: themes use \`t1\`, \`t2\`, …; findings use \`f1\`, \`f2\`,
1779
2490
  - Line anchors: "lines"/"start"/"end"/"line" MUST match the chosen "side" ("new" = post-image / right side of @@; "old" = pre-image, typically deletions). One block per coherent change span, not one per line. Point findings at that block; leave "block" empty only if cross-cutting.
1780
2491
  - Do NOT report: pre-existing code outside the diff; linter/formatter noise; naming/style prefs with no real consequence; intentional branch behavior; silenced lint rules; "missing docs/coverage" without a concrete broken scenario.
1781
2492
  - Quality findings must name a concrete cost, not a vague feeling.
1782
- - Finding "what": state the concrete cause and the observable consequence (what fails, the risk, or the cost) — not only a symptom.
1783
- - Finding "fix": state the suggested change and what the reader gains or avoids if they apply it.
2493
+ - Theme "intent": explain why the theme exists and what it changes for the reader — not a paraphrase of "title". One short sentence is too little; aim for 1–3 sentences with cause and scope.
2494
+ - Top-level "intent": 2–4 sentences on what the branch solves and why it matters.
2495
+ - Finding "what": cover (1) the concrete cause in the diff, (2) why it is a problem (failure mode, risk, or cost), and (3) the observable consequence. Do not stop at a symptom label.
2496
+ - Finding "fix": cover (1) the suggested change and (2) why that change resolves the problem / what the reader gains or avoids. Vague advice ("mejorar el manejo") is not enough.
1784
2497
  - Signal over volume: prefer fewer high-signal findings. Order findings by severity (high, then nit). Use "nit" sparingly.
1785
2498
  - "blocking": true ONLY if the branch should not merge as-is. Use only with class "risk".
1786
- - When several read-only lookups are independent (files, searches), batch them in the same turn if your tools allow parallel calls.
2499
+ - Parallelize: when several read-only lookups are independent (Read, Grep/search, Glob, etc.), issue them in the same turn if your tools allow parallel calls. Do not serialize independent work — finish faster by batching.
1787
2500
  - Write human-readable strings (intent, title, what, why, fix, notes) in Spanish. Keep enums/ids/paths/JSON keys exactly as in the schema.
1788
- - Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters. Prefer clarity over shortening. Longer text is still valid. Do not rewrite the file only to shorten it.`;
2501
+ - Length is a soft target, not a check: aim for title ≤72 characters and what/why/fix ≤120 characters when possible. Prefer clarity over shortening — thin one-liners that omit cause, impact, or why-the-fix-works are worse than a slightly longer explanation. Longer text is still valid. Do not rewrite the file only to shorten it.
2502
+ - If the output JSON already exists and findings have \`resolution\` ({ kind: ignored|fix|corrected, detail?: string }), preserve those fields on rewrite (coverage / edit / regenerate). Keep existing finding ids when possible; do not drop or reshuffle resolutions.`;
1789
2503
  /** @param {{ hasDiff?: boolean }} [ctx] */
1790
2504
  function workflowLocal(base, branch, ctx = {}) {
1791
2505
  if (ctx.hasDiff) return `Workflow:
@@ -1934,9 +2648,9 @@ Write the result to \`${outputPath}\` (relative to the repo root; use forward sl
1934
2648
 
1935
2649
  ${outputSchemaBlock(meta)}
1936
2650
 
1937
- Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Codex, Copilot, Gemini, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
2651
+ Include "meta" with the source/repo/branch/base above (and origin "remoteUrl" when local). "agent" and "model" are required: set "agent" to the product you are (Claude Code, Cursor, Copilot, …) and "model" to the exact model id you are running. Do not leave placeholders or omit them; validation fails without both. "generatedAt" is optional (ISO-8601 now).
1938
2652
 
1939
- Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON.
2653
+ Do NOT include diffs or a "files" array. The tool loads the companion \`.diff\` (or computes with git if missing) when opening the report. Do not invent hunks or paste patches into the JSON. If you are rewriting an existing review JSON, copy through any per-finding \`resolution\` already present.
1940
2654
 
1941
2655
  ${validateSection}`;
1942
2656
  }
@@ -2102,7 +2816,7 @@ function agentModels(agentId, cwd, opts) {
2102
2816
  function findAgent(agentId) {
2103
2817
  const agent = detectAgents().find((a) => a.def.id === agentId);
2104
2818
  if (!agent) throw new Error(`No encontré el agente "${agentId}" instalado.`);
2105
- if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no tiene modo ACP.`);
2819
+ if (!supportsAcp(agent)) throw new Error(`${agent.def.label} no se puede usar desde la web.`);
2106
2820
  return agent;
2107
2821
  }
2108
2822
  function info(entry) {
@@ -2126,9 +2840,137 @@ function info(entry) {
2126
2840
  function authFailureMessage(agentId, agentLabel, detail) {
2127
2841
  return authFailureMessage$1(agentId, agentLabel, detail);
2128
2842
  }
2129
- /** Abre una terminal con el login del agente (Claude `/login`, `gh auth login`, …). */
2130
- function openAgentLogin(agentId) {
2131
- return openLoginTerminal(String(agentId || ""));
2843
+ /**
2844
+ * Abre el login del agente.
2845
+ * - Con `sessionId`: comando según authMethods ACP (proceso oculto sin TTY), espera exit 0, reconecta y retoma.
2846
+ * - Solo `agentId`: lanza el comando fallback en background (p. ej. fallo al listar modelos).
2847
+ */
2848
+ async function openAgentLogin(input) {
2849
+ const sessionId = String(input?.sessionId || "").trim();
2850
+ const agentId = String(input?.agentId || "").trim();
2851
+ const host = input?.host != null ? String(input.host) : void 0;
2852
+ if (sessionId) {
2853
+ const entry = sessions.get(sessionId);
2854
+ if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2855
+ const method = pickLoginAuthMethod(entry.pendingAuthMethods || []);
2856
+ let cmd;
2857
+ try {
2858
+ cmd = resolveLoginCommand(entry.agent, method);
2859
+ } catch {
2860
+ cmd = loginCommand(entry.agentId, { host });
2861
+ if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
2862
+ }
2863
+ if (entry.agentId === "copilot" || agentId === "copilot") cmd = withCopilotLoginHost(cmd, host);
2864
+ setStatus(entry, "starting", loginWaitingMessage());
2865
+ entry.busy = true;
2866
+ try {
2867
+ const result = await runLoginTerminal(cmd, {
2868
+ wait: true,
2869
+ cwd: entry.repo
2870
+ });
2871
+ if (result.code !== 0) {
2872
+ entry.busy = false;
2873
+ const msg = loginIncompleteMessage();
2874
+ setStatus(entry, "needs_auth", msg);
2875
+ throw new Error(msg);
2876
+ }
2877
+ try {
2878
+ entry.acp?.close();
2879
+ } catch {}
2880
+ entry.acp = null;
2881
+ entry.pendingAuthMethods = null;
2882
+ const resume = entry.authResume || "chat";
2883
+ entry.authResume = null;
2884
+ entry.busy = false;
2885
+ if (resume === "review") {
2886
+ setStatus(entry, "starting", "Login ok. Reintentando la review…");
2887
+ continueReviewAfterDiff(entry, {
2888
+ repo: entry.repo,
2889
+ branch: entry.branch,
2890
+ base: entry.base,
2891
+ outputFilename: entry.outputFilename,
2892
+ model: entry.model || void 0
2893
+ });
2894
+ } else {
2895
+ setStatus(entry, "starting", "Login ok. Reconectando con el agente…");
2896
+ try {
2897
+ await ensureAcp(entry, entry.model || void 0);
2898
+ setStatus(entry, "idle");
2899
+ } catch (err) {
2900
+ entry.busy = false;
2901
+ if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
2902
+ else {
2903
+ const raw = err instanceof Error ? err.message : String(err);
2904
+ setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
2905
+ }
2906
+ throw err;
2907
+ }
2908
+ }
2909
+ return {
2910
+ display: result.display,
2911
+ resumed: true,
2912
+ code: result.code
2913
+ };
2914
+ } catch (err) {
2915
+ entry.busy = false;
2916
+ if (entry.status !== "needs_auth" && entry.status !== "error" && entry.status !== "starting") setStatus(entry, "needs_auth", err instanceof Error ? err.message : String(err));
2917
+ throw err;
2918
+ }
2919
+ }
2920
+ if (!agentId) throw new Error("Falta agentId o sessionId.");
2921
+ const cmd = loginCommand(agentId, { host });
2922
+ if (!cmd) throw new Error("Este agente no tiene un inicio de sesión conocido.");
2923
+ return openLoginTerminal(cmd);
2924
+ }
2925
+ /**
2926
+ * Cierra la sesión del agente (y cuentas `gh` si es Copilot) para poder elegir otra cuenta.
2927
+ * Deja la entry en `needs_auth` lista para un nuevo login.
2928
+ */
2929
+ async function openAgentLogout(input) {
2930
+ const sessionId = String(input?.sessionId || "").trim();
2931
+ const host = input?.host != null ? String(input.host) : void 0;
2932
+ let agentId = String(input?.agentId || "").trim();
2933
+ /** @type {Entry | undefined} */
2934
+ let entry;
2935
+ if (sessionId) {
2936
+ entry = sessions.get(sessionId);
2937
+ if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2938
+ agentId = entry.agentId || agentId;
2939
+ }
2940
+ if (!agentId) throw new Error("Falta agentId o sessionId.");
2941
+ /** @type {string[]} */
2942
+ const steps = [];
2943
+ /** @type {string[]} */
2944
+ const warnings = [];
2945
+ if (entry?.acp?.supportsLogout) try {
2946
+ await entry.acp.logout();
2947
+ steps.push("Cerré la sesión del agente.");
2948
+ } catch (err) {
2949
+ warnings.push(err instanceof Error ? err.message : String(err));
2950
+ }
2951
+ try {
2952
+ entry?.acp?.close();
2953
+ } catch {}
2954
+ if (entry) {
2955
+ entry.acp = null;
2956
+ entry.starting = null;
2957
+ entry.busy = false;
2958
+ entry.pendingAuthMethods = null;
2959
+ entry.authResume = entry.authResume || (entry.seed || entry.diffText ? "review" : "chat");
2960
+ }
2961
+ const cli = await runAgentLogout(agentId, {
2962
+ host,
2963
+ cwd: entry?.repo
2964
+ });
2965
+ steps.push(...cli.steps);
2966
+ warnings.push(...cli.warnings);
2967
+ const msg = warnings.length && !cli.ok ? warnings[0] : "Sesión cerrada. Elegí con qué cuenta iniciar de nuevo.";
2968
+ if (entry) setStatus(entry, "needs_auth", msg);
2969
+ return {
2970
+ ok: cli.ok || steps.length > 0,
2971
+ steps,
2972
+ warnings
2973
+ };
2132
2974
  }
2133
2975
  /** Escribe seed (JSON + .diff) en `.diff-review/` para que el agente los lea. */
2134
2976
  function materializeSeed(entry) {
@@ -2145,15 +2987,25 @@ function materializeSeed(entry) {
2145
2987
  });
2146
2988
  } catch {}
2147
2989
  }
2990
+ /** Escribe (pisa) el companion `.chat.md` de handoff si hay markdown pendiente. */
2991
+ function materializeHandoffChat(entry) {
2992
+ const text = entry.handoffChatMarkdown;
2993
+ if (text == null) return;
2994
+ try {
2995
+ writeHandoffChat(entry.repo, entry.outputFilename, text);
2996
+ } catch {}
2997
+ entry.handoffChatMarkdown = null;
2998
+ }
2148
2999
  /** Prefijo del primer mensaje cuando la sesión ACP es nueva. */
2149
- function contextPreamble(entry) {
3000
+ function contextPreamble(entry, handoffNote) {
2150
3001
  materializeSeed(entry);
3002
+ materializeHandoffChat(entry);
2151
3003
  const paths = companionPaths(entry.repo, entry.outputFilename);
2152
3004
  const jsonRel = outputRelPath(entry.outputFilename);
2153
3005
  const diffRel = `${outputRelPath(entry.outputFilename).replace(/\.json$/i, "")}.diff`;
2154
3006
  const hasJson = existsSync(paths.json);
2155
3007
  const hasDiff = existsSync(paths.diff);
2156
- return [
3008
+ const lines = [
2157
3009
  "Context for this chat (new agent session — read these files before answering):",
2158
3010
  `- Repo: ${entry.repo}`,
2159
3011
  `- Branch under review: ${entry.branch}`,
@@ -2161,8 +3013,14 @@ function contextPreamble(entry) {
2161
3013
  hasJson ? `- Review JSON (relative to repo root): \`${jsonRel}\` — read this file.` : `- Review JSON: not available on disk (no seed/payload was provided). Say so if you need it.`,
2162
3014
  hasDiff ? `- Diff (relative to repo root): \`${diffRel}\` — read this file.` : `- Diff file: not available on disk (no seed/diff and git cache failed). Say so if you need it.`,
2163
3015
  "",
2164
- "User message:"
2165
- ].join("\n");
3016
+ "The user question or fix request is the priority. The review JSON is reference context — do not re-litigate or re-review findings unless the user asks.",
3017
+ "",
3018
+ "Findings may include optional `resolution` ({ kind: ignored|fix|corrected, detail?: string }) written by the UI. Preserve those fields when editing the JSON. Corrected findings and their detail are prior work — use them as context; do not undo them unless the user asks."
3019
+ ];
3020
+ const note = String(handoffNote || "").trim();
3021
+ if (note) lines.push("", note);
3022
+ lines.push("", "User message:");
3023
+ return lines.join("\n");
2166
3024
  }
2167
3025
  function permissionReasonLabel(allow, scope, deniedReason) {
2168
3026
  if (!allow) return deniedReason;
@@ -2263,14 +3121,36 @@ function emit(entry, event) {
2263
3121
  }
2264
3122
  function setStatus(entry, status, message) {
2265
3123
  entry.status = status;
2266
- if (status === "error") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
3124
+ if (status === "error" || status === "needs_auth") entry.statusMessage = message || entry.statusMessage || "Error del agente.";
2267
3125
  else if (message !== void 0) delete entry.statusMessage;
2268
3126
  emit(entry, {
2269
3127
  type: "status",
2270
3128
  status,
2271
- ...message || status === "error" ? { message: message || entry.statusMessage } : {}
3129
+ ...message || status === "error" || status === "needs_auth" ? { message: message || entry.statusMessage } : {}
2272
3130
  });
2273
3131
  }
3132
+ function markNeedsAuth(entry, err, resume) {
3133
+ const authErr = err instanceof AcpAuthRequiredError ? err : null;
3134
+ const raw = err instanceof Error ? err.message : String(err);
3135
+ const msg = authErr?.message || authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw;
3136
+ entry.pendingAuthMethods = authErr?.methods?.length ? authErr.methods : entry.pendingAuthMethods;
3137
+ entry.authResume = resume;
3138
+ entry.acp = null;
3139
+ setStatus(entry, "needs_auth", msg);
3140
+ }
3141
+ function isAuthError(err) {
3142
+ if (err instanceof AcpAuthRequiredError) return true;
3143
+ return looksLikeAuthFailure(err instanceof Error ? err.message : String(err));
3144
+ }
3145
+ /** Sin sesión CLI y con login desde la app (Claude / Copilot / Cursor). */
3146
+ function needsInteractiveLogin(agentId) {
3147
+ if (!loginCommand(agentId)) return false;
3148
+ try {
3149
+ return !agentAccountStatus(agentId).loggedIn;
3150
+ } catch {
3151
+ return false;
3152
+ }
3153
+ }
2274
3154
  /** SessionUpdate de ACP → evento de la web (lo que no se muestra se descarta). */
2275
3155
  function toEvent(update) {
2276
3156
  switch (update.sessionUpdate) {
@@ -2354,13 +3234,13 @@ async function ensureAcp(entry, model) {
2354
3234
  entry.busy = false;
2355
3235
  const auth = authFailureMessage(entry.agentId, entry.agentLabel, stderr);
2356
3236
  if (auth) {
2357
- setStatus(entry, "error", auth);
3237
+ markNeedsAuth(entry, new Error(auth), entry.mode === "review" ? "review" : "chat");
2358
3238
  return;
2359
3239
  }
2360
3240
  const raw = stderr.trim() || `código ${code ?? "?"}`;
2361
3241
  const authFromCode = authFailureMessage(entry.agentId, entry.agentLabel, raw);
2362
3242
  if (authFromCode) {
2363
- setStatus(entry, "error", authFromCode);
3243
+ markNeedsAuth(entry, new Error(authFromCode), entry.mode === "review" ? "review" : "chat");
2364
3244
  return;
2365
3245
  }
2366
3246
  const tail = stderr.trim().split(/\r?\n/).slice(-3).join(" · ");
@@ -2428,7 +3308,7 @@ function checkEntryReport(entry) {
2428
3308
  function invalidJsonFixPrompt(entry, path, result) {
2429
3309
  return `${formatCheckReport(path, validateCliCommand(entry.outputFilename), result)}
2430
3310
 
2431
- Critical: do NOT only bump meta.generatedAt or rewrite notes. Edit \`${outputRelPath(entry.outputFilename).replace(/\\/g, "/")}\` so every problem above is fixed (add blocks or skipped for each missing path). Do not claim success until coverage is complete.
3311
+ Critical: fix ONLY the listed schema/validation errors above. Do NOT regenerate or reshuffle findings unnecessarily. Preserve existing finding ids and any \`resolution\` fields when possible. Do NOT only bump meta.generatedAt or rewrite notes. Edit \`${outputRelPath(entry.outputFilename).replace(/\\/g, "/")}\` so every problem above is fixed (add blocks or skipped for each missing path). Do not claim success until coverage is complete.
2432
3312
 
2433
3313
  ${TOOL_RULES}
2434
3314
 
@@ -2478,6 +3358,7 @@ async function checkOutput(entry, since, retriesLeft) {
2478
3358
  stampReviewProvenance(path, {
2479
3359
  agent: entry.agentLabel,
2480
3360
  model: entry.model,
3361
+ modelLabel: entry.acp?.models.find((o) => o.value === entry.model)?.name,
2481
3362
  agentSession: {
2482
3363
  agentId: entry.agentId,
2483
3364
  sessionId: entry.acpSessionId,
@@ -2545,6 +3426,11 @@ async function runTurn(entry, text, mode, opts) {
2545
3426
  mode: mode === "review" ? "review" : mode
2546
3427
  });
2547
3428
  try {
3429
+ if (!entry.acp && needsInteractiveLogin(entry.agentId)) {
3430
+ entry.busy = false;
3431
+ markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), mode === "review" ? "review" : "chat");
3432
+ return;
3433
+ }
2548
3434
  if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
2549
3435
  const acp = await ensureAcp(entry, opts.model);
2550
3436
  if (entry.cancelled) {
@@ -2566,7 +3452,7 @@ async function runTurn(entry, text, mode, opts) {
2566
3452
  setStatus(entry, "idle", "Cancelado.");
2567
3453
  return;
2568
3454
  }
2569
- if (mode !== "ask") await checkOutput(entry, since, OUTPUT_AUTO_RETRIES);
3455
+ if (mode !== "ask" && mode !== "fix") await checkOutput(entry, since, OUTPUT_AUTO_RETRIES);
2570
3456
  entry.busy = false;
2571
3457
  if (entry.cancelled) setStatus(entry, "idle", "Cancelado.");
2572
3458
  else setStatus(entry, "idle");
@@ -2576,6 +3462,10 @@ async function runTurn(entry, text, mode, opts) {
2576
3462
  setStatus(entry, "idle", "Cancelado.");
2577
3463
  return;
2578
3464
  }
3465
+ if (isAuthError(err)) {
3466
+ markNeedsAuth(entry, err, mode === "review" ? "review" : "chat");
3467
+ return;
3468
+ }
2579
3469
  const raw = err instanceof Error ? err.message : String(err);
2580
3470
  setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
2581
3471
  } finally {
@@ -2608,10 +3498,14 @@ function newEntry(agent, fields) {
2608
3498
  rejections: [],
2609
3499
  needsContext: false,
2610
3500
  seed: null,
3501
+ handoffNote: null,
3502
+ handoffChatMarkdown: null,
2611
3503
  diffPaths: null,
2612
3504
  pendingPermissions: /* @__PURE__ */ new Map(),
2613
3505
  sessionAllowDenied: false,
2614
- cancelled: false
3506
+ cancelled: false,
3507
+ pendingAuthMethods: null,
3508
+ authResume: null
2615
3509
  };
2616
3510
  sessions.set(entry.id, entry);
2617
3511
  return entry;
@@ -2697,9 +3591,13 @@ async function continueReviewAfterDiff(entry, input) {
2697
3591
  */
2698
3592
  async function openChat(input) {
2699
3593
  const sessionId = String(input.sessionId || "").trim();
3594
+ const handoffNote = String(input.handoffNote || "").trim() || null;
3595
+ const handoffChat = input.handoffChatMarkdown != null ? String(input.handoffChatMarkdown) : null;
2700
3596
  if (sessionId) {
2701
3597
  for (const entry of sessions.values()) if (entry.acpSessionId === sessionId && entry.repo === input.repo) {
2702
3598
  if (input.seed) entry.seed = input.seed;
3599
+ if (handoffNote) entry.handoffNote = handoffNote;
3600
+ if (handoffChat != null) entry.handoffChatMarkdown = handoffChat;
2703
3601
  return info(entry);
2704
3602
  }
2705
3603
  }
@@ -2709,14 +3607,23 @@ async function openChat(input) {
2709
3607
  acpSessionId: sessionId
2710
3608
  });
2711
3609
  if (input.seed) entry.seed = input.seed;
3610
+ if (handoffNote) entry.handoffNote = handoffNote;
3611
+ if (handoffChat != null) entry.handoffChatMarkdown = handoffChat;
2712
3612
  if (!sessionId) entry.needsContext = true;
3613
+ if (needsInteractiveLogin(entry.agentId)) {
3614
+ markNeedsAuth(entry, /* @__PURE__ */ new Error("not logged in"), "chat");
3615
+ return info(entry);
3616
+ }
2713
3617
  try {
2714
3618
  if (!entry.acp) setStatus(entry, "starting", `Lanzando ${entry.agentLabel}…`);
2715
3619
  await ensureAcp(entry, input.model);
2716
3620
  if (entry.status === "starting") setStatus(entry, "idle");
2717
3621
  } catch (err) {
2718
- const raw = err instanceof Error ? err.message : String(err);
2719
- setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
3622
+ if (isAuthError(err)) markNeedsAuth(entry, err, "chat");
3623
+ else {
3624
+ const raw = err instanceof Error ? err.message : String(err);
3625
+ setStatus(entry, "error", authFailureMessage(entry.agentId, entry.agentLabel, raw) || raw);
3626
+ }
2720
3627
  }
2721
3628
  return info(entry);
2722
3629
  }
@@ -2726,13 +3633,17 @@ function sendMessage(id, text, mode, opts) {
2726
3633
  const body = text.trim();
2727
3634
  if (!body) throw new Error("El mensaje está vacío.");
2728
3635
  if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
3636
+ const sendHandoff = String(opts?.handoffNote || "").trim();
3637
+ if (sendHandoff) entry.handoffNote = sendHandoff;
3638
+ if (opts?.handoffChatMarkdown != null) entry.handoffChatMarkdown = String(opts.handoffChatMarkdown);
2729
3639
  let agentText = body;
2730
3640
  if (entry.needsContext) {
2731
- agentText = `${contextPreamble(entry)}\n${body}`;
3641
+ agentText = `${contextPreamble(entry, entry.handoffNote)}\n${body}`;
2732
3642
  entry.needsContext = false;
3643
+ entry.handoffNote = null;
2733
3644
  entry.seed = null;
2734
3645
  }
2735
- runTurn(entry, mode === "edit" ? `${agentText}\n\nApply this by editing ${entry.outputFilename} in place (keep everything else as is). Reply briefly in Spanish with what you changed.\n\n${SERVER_VALIDATES}` : `${agentText}\n\nAnswer in Spanish. This is a question about the review: do not modify any file.`, mode, {
3646
+ runTurn(entry, mode === "edit" ? `${agentText}\n\nApply this by editing ${entry.outputFilename} in place (keep everything else as is; preserve finding ids and any \`resolution\` fields). Reply briefly in Spanish with what you changed.\n\n${SERVER_VALIDATES}` : mode === "fix" ? `${agentText}\n\nApply ONLY the listed code fixes by editing files inside this repo. Do not rewrite the review JSON. Touch nothing else. Reply briefly in Spanish with a short per-finding detail suitable for resolution.detail.` : `${agentText}\n\nAnswer in Spanish. This is a question about the review: do not modify any file.`, mode, {
2736
3647
  shown: body,
2737
3648
  model: String(opts?.model || "").trim() || void 0
2738
3649
  });
@@ -2747,6 +3658,23 @@ function retryReview(id) {
2747
3658
  const entry = sessions.get(String(id || ""));
2748
3659
  if (!entry) throw new Error("La sesión del agente ya no existe. Recargá la página.");
2749
3660
  if (entry.busy) throw new Error("El agente todavía está trabajando en el pedido anterior.");
3661
+ if (entry.status === "needs_auth" || entry.status === "error" && looksLikeAuthFailure(entry.statusMessage || "")) {
3662
+ entry.pendingAuthMethods = null;
3663
+ entry.authResume = null;
3664
+ try {
3665
+ entry.acp?.close();
3666
+ } catch {}
3667
+ entry.acp = null;
3668
+ setStatus(entry, "starting", "Reintentando la review…");
3669
+ continueReviewAfterDiff(entry, {
3670
+ repo: entry.repo,
3671
+ branch: entry.branch,
3672
+ base: entry.base,
3673
+ outputFilename: entry.outputFilename,
3674
+ model: entry.model || void 0
3675
+ });
3676
+ return info(entry);
3677
+ }
2750
3678
  if (entry.status === "error") throw new Error(entry.statusMessage || "La sesión está en error; iniciá una nueva review.");
2751
3679
  const posixRel = outputRelPath(entry.outputFilename).replace(/\\/g, "/");
2752
3680
  const existing = checkEntryReport(entry);
@@ -2853,6 +3781,35 @@ async function cancelTurn(id) {
2853
3781
  entry.busy = false;
2854
3782
  setStatus(entry, "idle", "Cancelado.");
2855
3783
  }
3784
+ /**
3785
+ * Cierra la sesión de chat: ACP, Map y permisos pendientes.
3786
+ * La UI lo usa al cambiar de agente (el reporte / triage se mantienen).
3787
+ */
3788
+ async function closeChat(id) {
3789
+ const entry = sessions.get(String(id || ""));
3790
+ if (!entry) return;
3791
+ entry.cancelled = true;
3792
+ for (const [requestId, pending] of [...entry.pendingPermissions]) {
3793
+ clearTimeout(pending.timer);
3794
+ entry.pendingPermissions.delete(requestId);
3795
+ try {
3796
+ pending.resolve(false, { quiet: true });
3797
+ } catch {}
3798
+ }
3799
+ if (entry.idleTimer) {
3800
+ clearTimeout(entry.idleTimer);
3801
+ entry.idleTimer = null;
3802
+ }
3803
+ try {
3804
+ if (entry.acp && entry.busy) try {
3805
+ await entry.acp.cancel();
3806
+ } catch {}
3807
+ entry.acp?.close();
3808
+ } catch {}
3809
+ entry.acp = null;
3810
+ entry.listeners.clear();
3811
+ sessions.delete(entry.id);
3812
+ }
2856
3813
  function subscribe(id, listener) {
2857
3814
  const entry = sessions.get(id);
2858
3815
  if (!entry) return null;
@@ -2876,5 +3833,5 @@ function startPendingAgentRun() {
2876
3833
  }
2877
3834
  }
2878
3835
 
2879
- export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, retryReview as f, openAgentLogin as g, loginCommand as h, subscribe as i, startPendingAgentRun as j, listAcpAgents as l, openChat as o, respondPermission as r, sessionInfo as s };
2880
- //# sourceMappingURL=agent-sessions-C_hlZJKN.js.map
3836
+ export { agentModels as a, startReview as b, sendMessage as c, setPermissionTrust as d, cancelTurn as e, closeChat as f, retryReview as g, agentAccountStatus as h, openAgentLogin as i, openAgentLogout as j, subscribe as k, listAcpAgents as l, startPendingAgentRun as m, openChat as o, respondPermission as r, sessionInfo as s };
3837
+ //# sourceMappingURL=agent-sessions-BArYLTW6.js.map