@jpkme/diff-review 0.1.12 → 0.1.13

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 (120) hide show
  1. package/README.md +1 -1
  2. package/lib/acp-policy.mjs +99 -12
  3. package/lib/acp-policy.test.mjs +56 -3
  4. package/lib/acp-shell.mjs +38 -8
  5. package/lib/acp-shell.test.mjs +32 -1
  6. package/lib/agent-login.mjs +105 -105
  7. package/lib/check-report.mjs +1 -0
  8. package/lib/check-report.test.mjs +1 -0
  9. package/lib/extract-cache.test.mjs +162 -162
  10. package/lib/model-label.mjs +263 -263
  11. package/lib/model-label.test.mjs +77 -77
  12. package/lib/models-cache.mjs +75 -75
  13. package/lib/models-cache.test.mjs +50 -50
  14. package/lib/review-prep.mjs +63 -63
  15. package/package.json +1 -1
  16. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css → 0.BKRG1f36.css} +1 -1
  17. package/web/build/client/_app/immutable/assets/0.BKRG1f36.css.br +0 -0
  18. package/web/build/client/_app/immutable/assets/{0.CLQiq3Ed.css.gz → 0.BKRG1f36.css.gz} +0 -0
  19. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css +1 -0
  20. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.br +0 -0
  21. package/web/build/client/_app/immutable/assets/2.tqrMRTM0.css.gz +0 -0
  22. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css +1 -0
  23. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.br +0 -0
  24. package/web/build/client/_app/immutable/assets/3.BT54aVKb.css.gz +0 -0
  25. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css +1 -0
  26. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.br +0 -0
  27. package/web/build/client/_app/immutable/assets/storage.DqYH0w6R.css.gz +0 -0
  28. package/web/build/client/_app/immutable/chunks/BXUWMim4.js +213 -0
  29. package/web/build/client/_app/immutable/chunks/BXUWMim4.js.br +0 -0
  30. package/web/build/client/_app/immutable/chunks/BXUWMim4.js.gz +0 -0
  31. package/web/build/client/_app/immutable/chunks/{CPyWH418.js → Bu1mm6Oz.js} +1 -1
  32. package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.br +0 -0
  33. package/web/build/client/_app/immutable/chunks/Bu1mm6Oz.js.gz +0 -0
  34. package/web/build/client/_app/immutable/chunks/{B5ByWcPE.js → DPfXzQnm.js} +2 -2
  35. package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.br +0 -0
  36. package/web/build/client/_app/immutable/chunks/DPfXzQnm.js.gz +0 -0
  37. package/web/build/client/_app/immutable/entry/{app.BiBohpyO.js → app.CTgiuiU1.js} +2 -2
  38. package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.br +0 -0
  39. package/web/build/client/_app/immutable/entry/app.CTgiuiU1.js.gz +0 -0
  40. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js +1 -0
  41. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.br +0 -0
  42. package/web/build/client/_app/immutable/entry/start.CFSl0kht.js.gz +0 -0
  43. package/web/build/client/_app/immutable/nodes/{1.BzPXaJJj.js → 1.0PQZpSbJ.js} +1 -1
  44. package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.br +2 -0
  45. package/web/build/client/_app/immutable/nodes/1.0PQZpSbJ.js.gz +0 -0
  46. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js +10 -0
  47. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.br +0 -0
  48. package/web/build/client/_app/immutable/nodes/2.DQaoQoy4.js.gz +0 -0
  49. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js +171 -0
  50. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.br +0 -0
  51. package/web/build/client/_app/immutable/nodes/3.AciugOtL.js.gz +0 -0
  52. package/web/build/client/_app/version.json +1 -1
  53. package/web/build/client/_app/version.json.br +0 -0
  54. package/web/build/client/_app/version.json.gz +0 -0
  55. package/web/build/handler.js +3 -3
  56. package/web/build/index.js +3 -3
  57. package/web/build/server/chunks/{0-BYok4D0n.js → 0-DUStDOsr.js} +3 -3
  58. package/web/build/server/chunks/{0-BYok4D0n.js.map → 0-DUStDOsr.js.map} +1 -1
  59. package/web/build/server/chunks/1-C2qlInYk.js +7 -0
  60. package/web/build/server/chunks/{1-C7zcB0q0.js.map → 1-C2qlInYk.js.map} +1 -1
  61. package/web/build/server/chunks/2-DyE177MK.js +7 -0
  62. package/web/build/server/chunks/2-DyE177MK.js.map +1 -0
  63. package/web/build/server/chunks/3-SO3atuWO.js +7 -0
  64. package/web/build/server/chunks/{3-DAAmG6VX.js.map → 3-SO3atuWO.js.map} +1 -1
  65. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js → _server.ts-CRNhzL2a.js} +3 -3
  66. package/web/build/server/chunks/{_server.ts-qYVrMYPB.js.map → _server.ts-CRNhzL2a.js.map} +1 -1
  67. package/web/build/server/chunks/{agent-sessions-DSqGavSn.js → agent-sessions-kcUaLhK5.js} +171 -40
  68. package/web/build/server/chunks/agent-sessions-kcUaLhK5.js.map +1 -0
  69. package/web/build/server/chunks/{git-review-CpO_G8z8.js → git-review-CXKcjYgZ.js} +2 -2
  70. package/web/build/server/chunks/{git-review-CpO_G8z8.js.map → git-review-CXKcjYgZ.js.map} +1 -1
  71. package/web/build/server/chunks/{handler-Di1wJQuU.js → handler-_E0NucKN.js} +3 -3
  72. package/web/build/server/chunks/{handler-Di1wJQuU.js.map → handler-_E0NucKN.js.map} +1 -1
  73. package/web/build/server/chunks/{hooks.server-BLOHobEK.js → hooks.server-D7yPODoc.js} +2 -2
  74. package/web/build/server/chunks/{hooks.server-BLOHobEK.js.map → hooks.server-D7yPODoc.js.map} +1 -1
  75. package/web/build/server/chunks/{index.js-yLYmrAWy.js → index.js-lMtDM8ZQ.js} +3 -3
  76. package/web/build/server/chunks/{index.js-yLYmrAWy.js.map → index.js-lMtDM8ZQ.js.map} +1 -1
  77. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js → manifest.js-BIm6jot5.js} +9 -9
  78. package/web/build/server/chunks/{manifest.js-CXj8IIo3.js.map → manifest.js-BIm6jot5.js.map} +1 -1
  79. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js → remote-1g4ayag-DmqxwUIz.js} +3 -3
  80. package/web/build/server/chunks/{remote-1g4ayag-BiJeU0Pk.js.map → remote-1g4ayag-DmqxwUIz.js.map} +1 -1
  81. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js → remote-wh6t6j-DZk0W9Pp.js} +4 -4
  82. package/web/build/server/chunks/{remote-wh6t6j-CHanXurm.js.map → remote-wh6t6j-DZk0W9Pp.js.map} +1 -1
  83. package/web/build/client/_app/immutable/assets/0.CLQiq3Ed.css.br +0 -0
  84. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css +0 -1
  85. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.br +0 -0
  86. package/web/build/client/_app/immutable/assets/2.CKE12GwZ.css.gz +0 -0
  87. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css +0 -1
  88. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.br +0 -0
  89. package/web/build/client/_app/immutable/assets/3.Dx3c_e7d.css.gz +0 -0
  90. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css +0 -1
  91. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.br +0 -0
  92. package/web/build/client/_app/immutable/assets/storage.gDV-BhtT.css.gz +0 -0
  93. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.br +0 -0
  94. package/web/build/client/_app/immutable/chunks/B5ByWcPE.js.gz +0 -0
  95. package/web/build/client/_app/immutable/chunks/CPyWH418.js.br +0 -0
  96. package/web/build/client/_app/immutable/chunks/CPyWH418.js.gz +0 -0
  97. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js +0 -210
  98. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.br +0 -0
  99. package/web/build/client/_app/immutable/chunks/NwkMsx0h.js.gz +0 -0
  100. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.br +0 -0
  101. package/web/build/client/_app/immutable/entry/app.BiBohpyO.js.gz +0 -0
  102. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js +0 -1
  103. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.br +0 -0
  104. package/web/build/client/_app/immutable/entry/start.CjWbsiSO.js.gz +0 -0
  105. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.br +0 -2
  106. package/web/build/client/_app/immutable/nodes/1.BzPXaJJj.js.gz +0 -0
  107. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js +0 -10
  108. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.br +0 -0
  109. package/web/build/client/_app/immutable/nodes/2.Cp6Ttd_9.js.gz +0 -0
  110. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js +0 -171
  111. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.br +0 -0
  112. package/web/build/client/_app/immutable/nodes/3.DLiDQm__.js.gz +0 -0
  113. package/web/build/server/chunks/1-C7zcB0q0.js +0 -7
  114. package/web/build/server/chunks/2-v6ejI_0-.js +0 -7
  115. package/web/build/server/chunks/2-v6ejI_0-.js.map +0 -1
  116. package/web/build/server/chunks/3-DAAmG6VX.js +0 -7
  117. package/web/build/server/chunks/agent-sessions-DSqGavSn.js.map +0 -1
  118. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js → 0.Dj97JW46.js} +0 -0
  119. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.br → 0.Dj97JW46.js.br} +0 -0
  120. /package/web/build/client/_app/immutable/nodes/{0.Cak4ehiE.js.gz → 0.Dj97JW46.js.gz} +0 -0
@@ -1,263 +1,263 @@
1
- /** Compara nombres de modelo ignorando mayúsculas, espacios y puntuación. */
2
- export function modelSlug(s) {
3
- return String(s || "")
4
- .toLowerCase()
5
- .replace(/[^a-z0-9]+/g, "-")
6
- .replace(/^-+|-+$/g, "");
7
- }
8
-
9
- /** Parece un id técnico (claude-opus-5-5) más que un nombre legible. */
10
- function isSlugLike(s) {
11
- const t = String(s || "").trim();
12
- if (!t) return true;
13
- if (/\s/.test(t)) return false;
14
- if (/[A-Z]/.test(t) && /[a-z]/.test(t) && !t.includes("-")) return false; // "Sonnet", "Auto"
15
- return /^[a-z0-9][a-z0-9._|-]*$/i.test(t) && (t.includes("-") || t.includes(".") || t.includes("["));
16
- }
17
-
18
- /**
19
- * claude-opus-5-5 → Claude Opus 5.5; gpt-5.6-sol → GPT-5.6 Sol; gemini-3.8-flash → Gemini 3.8 Flash
20
- * @param {string} id
21
- */
22
- export function humanizeModelId(id) {
23
- const base = String(id || "")
24
- .trim()
25
- .replace(/\[.*\]$/, "");
26
- if (!base) return "";
27
- const parts = base.split(/[-_]/).filter(Boolean);
28
- const out = [];
29
- for (let i = 0; i < parts.length; i++) {
30
- const p = parts[i];
31
- const lower = p.toLowerCase();
32
- if (lower === "gpt" || lower === "gpt5") {
33
- out.push(lower === "gpt5" ? "GPT-5" : "GPT");
34
- continue;
35
- }
36
- if (lower === "claude" || lower === "gemini" || lower === "grok" || lower === "composer" || lower === "muse") {
37
- out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
38
- continue;
39
- }
40
- if (lower === "sonnet" || lower === "opus" || lower === "haiku" || lower === "flash" || lower === "codex") {
41
- out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
42
- continue;
43
- }
44
- // 5-5 → 5.5 (versión mayor-menor)
45
- if (/^\d+$/.test(p) && i + 1 < parts.length && /^\d+$/.test(parts[i + 1])) {
46
- out.push(`${p}.${parts[i + 1]}`);
47
- i++;
48
- continue;
49
- }
50
- if (/^\d+(\.\d+)?$/.test(p)) {
51
- out.push(p);
52
- continue;
53
- }
54
- out.push(p.charAt(0).toUpperCase() + p.slice(1).toLowerCase());
55
- }
56
- return out.join(" ");
57
- }
58
-
59
- /**
60
- * context=300k,effort=medium,fast=false → ["300k", "medium"]
61
- * @param {string} raw
62
- * @returns {string[]}
63
- */
64
- export function compactModelMeta(raw) {
65
- const s = String(raw || "").trim();
66
- if (!s) return [];
67
- /** @type {string[]} */
68
- const badges = [];
69
- for (const piece of s.split(",")) {
70
- const part = piece.trim();
71
- if (!part) continue;
72
- const eq = part.indexOf("=");
73
- if (eq < 0) {
74
- // bare token: "1m", "1M"
75
- badges.push(part);
76
- continue;
77
- }
78
- const key = part.slice(0, eq).trim().toLowerCase();
79
- const val = part.slice(eq + 1).trim();
80
- if (!val || val === "false" || val === "0") continue;
81
- if (key === "fast" || key === "thinking") {
82
- if (val === "true" || val === "1") badges.push(key);
83
- continue;
84
- }
85
- if (key === "context" || key === "ctx") {
86
- badges.push(val);
87
- continue;
88
- }
89
- if (key === "effort" || key === "reasoning_effort" || key === "reasoning") {
90
- badges.push(val);
91
- continue;
92
- }
93
- badges.push(val);
94
- }
95
- return badges;
96
- }
97
-
98
- /**
99
- * @param {string} name
100
- * @param {string} value
101
- * @returns {{ title: string, badges: string[], label: string, hint?: string }}
102
- */
103
- export function formatModelOption(name, value) {
104
- const n = String(name ?? "").trim();
105
- const v = String(value ?? "").trim();
106
- if (!v && !n) return { title: "", badges: [], label: "" };
107
- if (/^default(\[\])?$/i.test(v) && n) {
108
- return { title: n, badges: [], label: n };
109
- }
110
-
111
- const bracket = /^(.+?)\[(.*)\]$/.exec(v);
112
- const base = bracket ? bracket[1].trim() : v;
113
- const metaRaw = bracket ? bracket[2].trim() : "";
114
- let badges = compactModelMeta(metaRaw);
115
-
116
- let title;
117
- if (n && !isSlugLike(n)) {
118
- title = n;
119
- } else if (n && modelSlug(n) === modelSlug(base)) {
120
- title = isSlugLike(n) ? humanizeModelId(base || n) : n;
121
- } else if (n && isSlugLike(n)) {
122
- title = humanizeModelId(base || n);
123
- } else if (n) {
124
- title = n;
125
- } else {
126
- title = humanizeModelId(base) || base || v;
127
- }
128
-
129
- // Si el título ya menciona el contexto (p. ej. "Sonnet (1M context)"), no repetir el badge "1m".
130
- if (badges.length && /\(\s*\d+\s*m\b/i.test(title)) {
131
- badges = badges.filter((b) => !/^\d+m$/i.test(b));
132
- }
133
- const titleSlug = modelSlug(title);
134
- if (badges.length && titleSlug.includes(modelSlug(badges.join("")))) {
135
- /* keep badges anyway for effort/fast */
136
- }
137
-
138
- const hint = badges.length ? `[ ${badges.join(", ")} ]` : undefined;
139
- const label = hint ? `${title} · ${hint}` : title;
140
- return { title, badges, label, ...(hint ? { hint } : {}) };
141
- }
142
-
143
- /**
144
- * Etiqueta compacta (compat / un solo string).
145
- * @param {string} name
146
- * @param {string} value
147
- */
148
- export function modelOptionLabel(name, value) {
149
- return formatModelOption(name, value).label || String(name || value || "").trim();
150
- }
151
-
152
- /**
153
- * Etiqueta del ítem "usar el default del agente".
154
- * @param {{ value: string, name: string } | undefined} current
155
- * @param {string} [configuredOptionLabel]
156
- */
157
- export function defaultModelOptionLabel(current, configuredOptionLabel) {
158
- const name = String(current?.name ?? "").trim();
159
- const value = String(current?.value ?? "").trim();
160
- const generic =
161
- !name ||
162
- value === "default" ||
163
- value === "auto" ||
164
- /^default(\b|\s|\()/i.test(name) ||
165
- /^auto(\b|\s|\()/i.test(name) ||
166
- /\(recommended\)/i.test(name);
167
-
168
- if (generic) {
169
- const configured = String(configuredOptionLabel ?? "").trim();
170
- if (configured && configured !== "Por defecto del agente") return configured;
171
- return "Por defecto del agente";
172
- }
173
-
174
- const { title, hint } = formatModelOption(name, value);
175
- return hint ? `Por defecto · ${title} · ${hint}` : `Por defecto · ${title}`;
176
- }
177
-
178
- /**
179
- * Orden de la lista: default (`""`) y `auto` primero, resto A→Z por label.
180
- * @template {{ value: string, label?: string, title?: string }} T
181
- * @param {T[]} options
182
- * @returns {T[]}
183
- */
184
- export function sortModelSelectOptions(options) {
185
- const list = Array.isArray(options) ? [...options] : [];
186
-
187
- /** @param {{ value: string, label?: string, title?: string }} o */
188
- const isAuto = (o) => {
189
- const v = String(o.value ?? "").toLowerCase();
190
- if (v === "auto" || v.startsWith("auto[")) return true;
191
- const t = String(o.title || o.label || "").trim();
192
- return /^auto\b/i.test(t);
193
- };
194
-
195
- const defaults = list.filter((o) => o.value === "");
196
- const autos = list.filter((o) => o.value !== "" && isAuto(o));
197
- const middle = list.filter((o) => o.value !== "" && !isAuto(o));
198
-
199
- /** @param {{ label?: string, title?: string, value: string }} a */
200
- /** @param {{ label?: string, title?: string, value: string }} b */
201
- const byLabel = (a, b) =>
202
- String(a.label || a.title || a.value).localeCompare(String(b.label || b.title || b.value), "es", {
203
- sensitivity: "base",
204
- numeric: true,
205
- });
206
-
207
- autos.sort(byLabel);
208
- middle.sort(byLabel);
209
- return [...defaults, ...autos, ...middle];
210
- }
211
-
212
- /**
213
- * Misma lista que la UI: default + aliases fijos + modelos ACP.
214
- * Clack: `label` = título amigable, `hint` = `[ 300k, medium ]`.
215
- * UI: también `title` + `badges` para chips.
216
- *
217
- * @param {{ models: { value: string, name: string }[], currentModel: string }} live
218
- * @param {{ options: { value: string, label: string, hint?: string }[], initialValue?: string }} fixed
219
- * @returns {{ options: { value: string, label: string, hint?: string, title?: string, badges?: string[] }[], initialValue: string }}
220
- */
221
- export function buildAcpModelSelectOptions(live, fixed) {
222
- const fixedOptions = fixed?.options ?? [];
223
- const fallback = fixedOptions.filter((m) => m.value !== "");
224
- const configuredLabel = fixedOptions.find((m) => m.value === "")?.label;
225
- const current = live.models.find((m) => m.value === live.currentModel);
226
- const seen = new Set(["", "default"]);
227
- /** @type {{ value: string, label: string, hint?: string, title?: string, badges?: string[] }[]} */
228
- const options = [
229
- {
230
- value: "",
231
- label: defaultModelOptionLabel(current, configuredLabel),
232
- title: defaultModelOptionLabel(current, configuredLabel),
233
- badges: [],
234
- },
235
- ];
236
-
237
- for (const m of fallback) {
238
- if (seen.has(m.value)) continue;
239
- seen.add(m.value);
240
- options.push({
241
- value: m.value,
242
- label: m.label,
243
- title: m.label,
244
- badges: [],
245
- ...(m.hint ? { hint: m.hint } : {}),
246
- });
247
- }
248
-
249
- for (const m of live.models) {
250
- if (seen.has(m.value)) continue;
251
- seen.add(m.value);
252
- const formatted = formatModelOption(m.name, m.value);
253
- options.push({
254
- value: m.value,
255
- label: formatted.title,
256
- title: formatted.title,
257
- badges: formatted.badges,
258
- ...(formatted.hint ? { hint: formatted.hint } : {}),
259
- });
260
- }
261
-
262
- return { options: sortModelSelectOptions(options), initialValue: "" };
263
- }
1
+ /** Compara nombres de modelo ignorando mayúsculas, espacios y puntuación. */
2
+ export function modelSlug(s) {
3
+ return String(s || "")
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, "-")
6
+ .replace(/^-+|-+$/g, "");
7
+ }
8
+
9
+ /** Parece un id técnico (claude-opus-5-5) más que un nombre legible. */
10
+ function isSlugLike(s) {
11
+ const t = String(s || "").trim();
12
+ if (!t) return true;
13
+ if (/\s/.test(t)) return false;
14
+ if (/[A-Z]/.test(t) && /[a-z]/.test(t) && !t.includes("-")) return false; // "Sonnet", "Auto"
15
+ return /^[a-z0-9][a-z0-9._|-]*$/i.test(t) && (t.includes("-") || t.includes(".") || t.includes("["));
16
+ }
17
+
18
+ /**
19
+ * claude-opus-5-5 → Claude Opus 5.5; gpt-5.6-sol → GPT-5.6 Sol; gemini-3.8-flash → Gemini 3.8 Flash
20
+ * @param {string} id
21
+ */
22
+ export function humanizeModelId(id) {
23
+ const base = String(id || "")
24
+ .trim()
25
+ .replace(/\[.*\]$/, "");
26
+ if (!base) return "";
27
+ const parts = base.split(/[-_]/).filter(Boolean);
28
+ const out = [];
29
+ for (let i = 0; i < parts.length; i++) {
30
+ const p = parts[i];
31
+ const lower = p.toLowerCase();
32
+ if (lower === "gpt" || lower === "gpt5") {
33
+ out.push(lower === "gpt5" ? "GPT-5" : "GPT");
34
+ continue;
35
+ }
36
+ if (lower === "claude" || lower === "gemini" || lower === "grok" || lower === "composer" || lower === "muse") {
37
+ out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
38
+ continue;
39
+ }
40
+ if (lower === "sonnet" || lower === "opus" || lower === "haiku" || lower === "flash" || lower === "codex") {
41
+ out.push(lower.charAt(0).toUpperCase() + lower.slice(1));
42
+ continue;
43
+ }
44
+ // 5-5 → 5.5 (versión mayor-menor)
45
+ if (/^\d+$/.test(p) && i + 1 < parts.length && /^\d+$/.test(parts[i + 1])) {
46
+ out.push(`${p}.${parts[i + 1]}`);
47
+ i++;
48
+ continue;
49
+ }
50
+ if (/^\d+(\.\d+)?$/.test(p)) {
51
+ out.push(p);
52
+ continue;
53
+ }
54
+ out.push(p.charAt(0).toUpperCase() + p.slice(1).toLowerCase());
55
+ }
56
+ return out.join(" ");
57
+ }
58
+
59
+ /**
60
+ * context=300k,effort=medium,fast=false → ["300k", "medium"]
61
+ * @param {string} raw
62
+ * @returns {string[]}
63
+ */
64
+ export function compactModelMeta(raw) {
65
+ const s = String(raw || "").trim();
66
+ if (!s) return [];
67
+ /** @type {string[]} */
68
+ const badges = [];
69
+ for (const piece of s.split(",")) {
70
+ const part = piece.trim();
71
+ if (!part) continue;
72
+ const eq = part.indexOf("=");
73
+ if (eq < 0) {
74
+ // bare token: "1m", "1M"
75
+ badges.push(part);
76
+ continue;
77
+ }
78
+ const key = part.slice(0, eq).trim().toLowerCase();
79
+ const val = part.slice(eq + 1).trim();
80
+ if (!val || val === "false" || val === "0") continue;
81
+ if (key === "fast" || key === "thinking") {
82
+ if (val === "true" || val === "1") badges.push(key);
83
+ continue;
84
+ }
85
+ if (key === "context" || key === "ctx") {
86
+ badges.push(val);
87
+ continue;
88
+ }
89
+ if (key === "effort" || key === "reasoning_effort" || key === "reasoning") {
90
+ badges.push(val);
91
+ continue;
92
+ }
93
+ badges.push(val);
94
+ }
95
+ return badges;
96
+ }
97
+
98
+ /**
99
+ * @param {string} name
100
+ * @param {string} value
101
+ * @returns {{ title: string, badges: string[], label: string, hint?: string }}
102
+ */
103
+ export function formatModelOption(name, value) {
104
+ const n = String(name ?? "").trim();
105
+ const v = String(value ?? "").trim();
106
+ if (!v && !n) return { title: "", badges: [], label: "" };
107
+ if (/^default(\[\])?$/i.test(v) && n) {
108
+ return { title: n, badges: [], label: n };
109
+ }
110
+
111
+ const bracket = /^(.+?)\[(.*)\]$/.exec(v);
112
+ const base = bracket ? bracket[1].trim() : v;
113
+ const metaRaw = bracket ? bracket[2].trim() : "";
114
+ let badges = compactModelMeta(metaRaw);
115
+
116
+ let title;
117
+ if (n && !isSlugLike(n)) {
118
+ title = n;
119
+ } else if (n && modelSlug(n) === modelSlug(base)) {
120
+ title = isSlugLike(n) ? humanizeModelId(base || n) : n;
121
+ } else if (n && isSlugLike(n)) {
122
+ title = humanizeModelId(base || n);
123
+ } else if (n) {
124
+ title = n;
125
+ } else {
126
+ title = humanizeModelId(base) || base || v;
127
+ }
128
+
129
+ // Si el título ya menciona el contexto (p. ej. "Sonnet (1M context)"), no repetir el badge "1m".
130
+ if (badges.length && /\(\s*\d+\s*m\b/i.test(title)) {
131
+ badges = badges.filter((b) => !/^\d+m$/i.test(b));
132
+ }
133
+ const titleSlug = modelSlug(title);
134
+ if (badges.length && titleSlug.includes(modelSlug(badges.join("")))) {
135
+ /* keep badges anyway for effort/fast */
136
+ }
137
+
138
+ const hint = badges.length ? `[ ${badges.join(", ")} ]` : undefined;
139
+ const label = hint ? `${title} · ${hint}` : title;
140
+ return { title, badges, label, ...(hint ? { hint } : {}) };
141
+ }
142
+
143
+ /**
144
+ * Etiqueta compacta (compat / un solo string).
145
+ * @param {string} name
146
+ * @param {string} value
147
+ */
148
+ export function modelOptionLabel(name, value) {
149
+ return formatModelOption(name, value).label || String(name || value || "").trim();
150
+ }
151
+
152
+ /**
153
+ * Etiqueta del ítem "usar el default del agente".
154
+ * @param {{ value: string, name: string } | undefined} current
155
+ * @param {string} [configuredOptionLabel]
156
+ */
157
+ export function defaultModelOptionLabel(current, configuredOptionLabel) {
158
+ const name = String(current?.name ?? "").trim();
159
+ const value = String(current?.value ?? "").trim();
160
+ const generic =
161
+ !name ||
162
+ value === "default" ||
163
+ value === "auto" ||
164
+ /^default(\b|\s|\()/i.test(name) ||
165
+ /^auto(\b|\s|\()/i.test(name) ||
166
+ /\(recommended\)/i.test(name);
167
+
168
+ if (generic) {
169
+ const configured = String(configuredOptionLabel ?? "").trim();
170
+ if (configured && configured !== "Por defecto del agente") return configured;
171
+ return "Por defecto del agente";
172
+ }
173
+
174
+ const { title, hint } = formatModelOption(name, value);
175
+ return hint ? `Por defecto · ${title} · ${hint}` : `Por defecto · ${title}`;
176
+ }
177
+
178
+ /**
179
+ * Orden de la lista: default (`""`) y `auto` primero, resto A→Z por label.
180
+ * @template {{ value: string, label?: string, title?: string }} T
181
+ * @param {T[]} options
182
+ * @returns {T[]}
183
+ */
184
+ export function sortModelSelectOptions(options) {
185
+ const list = Array.isArray(options) ? [...options] : [];
186
+
187
+ /** @param {{ value: string, label?: string, title?: string }} o */
188
+ const isAuto = (o) => {
189
+ const v = String(o.value ?? "").toLowerCase();
190
+ if (v === "auto" || v.startsWith("auto[")) return true;
191
+ const t = String(o.title || o.label || "").trim();
192
+ return /^auto\b/i.test(t);
193
+ };
194
+
195
+ const defaults = list.filter((o) => o.value === "");
196
+ const autos = list.filter((o) => o.value !== "" && isAuto(o));
197
+ const middle = list.filter((o) => o.value !== "" && !isAuto(o));
198
+
199
+ /** @param {{ label?: string, title?: string, value: string }} a */
200
+ /** @param {{ label?: string, title?: string, value: string }} b */
201
+ const byLabel = (a, b) =>
202
+ String(a.label || a.title || a.value).localeCompare(String(b.label || b.title || b.value), "es", {
203
+ sensitivity: "base",
204
+ numeric: true,
205
+ });
206
+
207
+ autos.sort(byLabel);
208
+ middle.sort(byLabel);
209
+ return [...defaults, ...autos, ...middle];
210
+ }
211
+
212
+ /**
213
+ * Misma lista que la UI: default + aliases fijos + modelos ACP.
214
+ * Clack: `label` = título amigable, `hint` = `[ 300k, medium ]`.
215
+ * UI: también `title` + `badges` para chips.
216
+ *
217
+ * @param {{ models: { value: string, name: string }[], currentModel: string }} live
218
+ * @param {{ options: { value: string, label: string, hint?: string }[], initialValue?: string }} fixed
219
+ * @returns {{ options: { value: string, label: string, hint?: string, title?: string, badges?: string[] }[], initialValue: string }}
220
+ */
221
+ export function buildAcpModelSelectOptions(live, fixed) {
222
+ const fixedOptions = fixed?.options ?? [];
223
+ const fallback = fixedOptions.filter((m) => m.value !== "");
224
+ const configuredLabel = fixedOptions.find((m) => m.value === "")?.label;
225
+ const current = live.models.find((m) => m.value === live.currentModel);
226
+ const seen = new Set(["", "default"]);
227
+ /** @type {{ value: string, label: string, hint?: string, title?: string, badges?: string[] }[]} */
228
+ const options = [
229
+ {
230
+ value: "",
231
+ label: defaultModelOptionLabel(current, configuredLabel),
232
+ title: defaultModelOptionLabel(current, configuredLabel),
233
+ badges: [],
234
+ },
235
+ ];
236
+
237
+ for (const m of fallback) {
238
+ if (seen.has(m.value)) continue;
239
+ seen.add(m.value);
240
+ options.push({
241
+ value: m.value,
242
+ label: m.label,
243
+ title: m.label,
244
+ badges: [],
245
+ ...(m.hint ? { hint: m.hint } : {}),
246
+ });
247
+ }
248
+
249
+ for (const m of live.models) {
250
+ if (seen.has(m.value)) continue;
251
+ seen.add(m.value);
252
+ const formatted = formatModelOption(m.name, m.value);
253
+ options.push({
254
+ value: m.value,
255
+ label: formatted.title,
256
+ title: formatted.title,
257
+ badges: formatted.badges,
258
+ ...(formatted.hint ? { hint: formatted.hint } : {}),
259
+ });
260
+ }
261
+
262
+ return { options: sortModelSelectOptions(options), initialValue: "" };
263
+ }