@ziamana/bruine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -0
  3. package/THIRD_PARTY_NOTICES.md +24 -0
  4. package/cordis.patch.yml +166 -0
  5. package/dist/bin.js +5358 -0
  6. package/dist/compat.js +40 -0
  7. package/dist/plugins/approval.js +147 -0
  8. package/dist/plugins/headless.js +236 -0
  9. package/dist/plugins/herdr.js +470 -0
  10. package/dist/plugins/mcp.js +275 -0
  11. package/dist/plugins/modes.js +850 -0
  12. package/dist/plugins/render.js +3723 -0
  13. package/dist/plugins/repl.js +9589 -0
  14. package/dist/plugins/silence.js +234 -0
  15. package/dist/plugins/startup.js +42 -0
  16. package/dist/plugins/web-search.js +138 -0
  17. package/package.json +93 -0
  18. package/skills/code-review/SKILL.md +30 -0
  19. package/skills/git-workflow/SKILL.md +31 -0
  20. package/skills/impeccable/LICENSE +191 -0
  21. package/skills/impeccable/NOTICE.md +11 -0
  22. package/skills/impeccable/SKILL.md +87 -0
  23. package/skills/impeccable/reference/adapt.md +318 -0
  24. package/skills/impeccable/reference/adapt.native.md +58 -0
  25. package/skills/impeccable/reference/android.md +46 -0
  26. package/skills/impeccable/reference/animate.md +89 -0
  27. package/skills/impeccable/reference/audit.md +137 -0
  28. package/skills/impeccable/reference/audit.native.md +139 -0
  29. package/skills/impeccable/reference/bolder.md +33 -0
  30. package/skills/impeccable/reference/clarify.md +94 -0
  31. package/skills/impeccable/reference/colorize.md +86 -0
  32. package/skills/impeccable/reference/component-review.md +63 -0
  33. package/skills/impeccable/reference/craft-floor.md +44 -0
  34. package/skills/impeccable/reference/craft.md +5 -0
  35. package/skills/impeccable/reference/critique.md +806 -0
  36. package/skills/impeccable/reference/degraded/asset-producer.md +42 -0
  37. package/skills/impeccable/reference/degraded/documenter.md +24 -0
  38. package/skills/impeccable/reference/degraded/finish-reviewer.md +38 -0
  39. package/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  40. package/skills/impeccable/reference/delight.md +70 -0
  41. package/skills/impeccable/reference/distill.md +111 -0
  42. package/skills/impeccable/reference/doctor.md +54 -0
  43. package/skills/impeccable/reference/document.md +416 -0
  44. package/skills/impeccable/reference/extract.md +69 -0
  45. package/skills/impeccable/reference/generate.md +101 -0
  46. package/skills/impeccable/reference/harden.md +345 -0
  47. package/skills/impeccable/reference/hooks.md +113 -0
  48. package/skills/impeccable/reference/init.md +131 -0
  49. package/skills/impeccable/reference/ios.md +51 -0
  50. package/skills/impeccable/reference/layout.md +84 -0
  51. package/skills/impeccable/reference/live-setup.md +104 -0
  52. package/skills/impeccable/reference/live.md +325 -0
  53. package/skills/impeccable/reference/mode-operate.md +21 -0
  54. package/skills/impeccable/reference/mode-persuade.md +19 -0
  55. package/skills/impeccable/reference/mode-read.md +21 -0
  56. package/skills/impeccable/reference/new-work.md +154 -0
  57. package/skills/impeccable/reference/onboard.md +234 -0
  58. package/skills/impeccable/reference/operate.md +61 -0
  59. package/skills/impeccable/reference/optimize.md +258 -0
  60. package/skills/impeccable/reference/overdrive.md +127 -0
  61. package/skills/impeccable/reference/polish.md +105 -0
  62. package/skills/impeccable/reference/quieter.md +99 -0
  63. package/skills/impeccable/reference/region-map.md +26 -0
  64. package/skills/impeccable/reference/routing.md +24 -0
  65. package/skills/impeccable/reference/shape.md +59 -0
  66. package/skills/impeccable/reference/typeset.md +80 -0
  67. package/skills/impeccable/reference/visualize.md +46 -0
  68. package/skills/impeccable/scripts/VERSION +1 -0
  69. package/skills/impeccable/scripts/command-metadata.json +98 -0
  70. package/skills/impeccable/scripts/data/font-index-failures.json +121 -0
  71. package/skills/impeccable/scripts/data/font-index.json +1 -0
  72. package/skills/impeccable/scripts/impeccable +206 -0
  73. package/skills/impeccable/scripts/impeccable.cmd +214 -0
  74. package/skills/impeccable/scripts/live-browser-dom.js +167 -0
  75. package/skills/impeccable/scripts/live-browser-ignores.js +242 -0
  76. package/skills/impeccable/scripts/live-browser-session.js +148 -0
  77. package/skills/impeccable/scripts/live-browser.js +13510 -0
  78. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  79. package/skills/make-interfaces-feel-better/LICENSE +21 -0
  80. package/skills/make-interfaces-feel-better/SKILL.md +187 -0
  81. package/skills/make-interfaces-feel-better/agents/openai.yaml +3 -0
  82. package/skills/make-interfaces-feel-better/animations.md +403 -0
  83. package/skills/make-interfaces-feel-better/icons.md +63 -0
  84. package/skills/make-interfaces-feel-better/performance.md +88 -0
  85. package/skills/make-interfaces-feel-better/surfaces.md +256 -0
  86. package/skills/make-interfaces-feel-better/typography.md +157 -0
  87. package/skills/playwright-cli/LICENSE +201 -0
  88. package/skills/playwright-cli/SKILL.md +489 -0
  89. package/skills/playwright-cli/references/element-attributes.md +23 -0
  90. package/skills/playwright-cli/references/playwright-tests.md +39 -0
  91. package/skills/playwright-cli/references/pr-attachments.md +60 -0
  92. package/skills/playwright-cli/references/request-mocking.md +87 -0
  93. package/skills/playwright-cli/references/running-code.md +245 -0
  94. package/skills/playwright-cli/references/session-management.md +227 -0
  95. package/skills/playwright-cli/references/storage-state.md +275 -0
  96. package/skills/playwright-cli/references/test-generation.md +433 -0
  97. package/skills/playwright-cli/references/tracing.md +139 -0
  98. package/skills/playwright-cli/references/video-recording.md +216 -0
  99. package/skills/remotion/SKILL.md +42 -0
  100. package/skills/systematic-debugging/SKILL.md +26 -0
  101. package/skills/thermo-nuclear-code-quality-review/LICENSE +21 -0
  102. package/skills/thermo-nuclear-code-quality-review/SKILL.md +192 -0
  103. package/skills/write-tests/SKILL.md +35 -0
  104. package/skills/youtube-transcript/LICENSE +21 -0
  105. package/skills/youtube-transcript/SKILL.md +41 -0
  106. package/skills/youtube-transcript/package.json +8 -0
  107. package/skills/youtube-transcript/transcript.js +44 -0
@@ -0,0 +1,3723 @@
1
+ // src/compat.ts
2
+ import { existsSync } from "fs";
3
+ import { homedir } from "os";
4
+ import path from "path";
5
+ function appEnv(suffix, env = process.env) {
6
+ return env[`BRUINE_${suffix}`] ?? env[`KUMO_${suffix}`];
7
+ }
8
+ function appHome(env = process.env, userHome = homedir(), pathMod = path, exists2 = existsSync) {
9
+ const explicit = appEnv("HOME", env);
10
+ if (explicit !== void 0) return explicit;
11
+ const current = pathMod.join(userHome, ".bruine");
12
+ const legacy = pathMod.join(userHome, ".kumo");
13
+ return exists2(current) ? current : exists2(legacy) ? legacy : current;
14
+ }
15
+ function runtimeHome(env = process.env, pathMod = path) {
16
+ return appEnv("HOME", env) ?? env.DSH_HOME ?? appHome(env, homedir(), pathMod);
17
+ }
18
+ function compatReadPath(currentPath, legacyPath) {
19
+ return existsSync(currentPath) || !existsSync(legacyPath) ? currentPath : legacyPath;
20
+ }
21
+ function configWritePath(home) {
22
+ return path.join(home, "bruine.json");
23
+ }
24
+ function configReadPath(home) {
25
+ return compatReadPath(configWritePath(home), path.join(home, "kumo.json"));
26
+ }
27
+
28
+ // src/plugins/render.ts
29
+ import { Text as Text4 } from "@earendil-works/pi-tui";
30
+ import { readFileSync as readFileSync14 } from "fs";
31
+
32
+ // src/plugins/suggest.ts
33
+ import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
34
+ var MainRequestMemory = class {
35
+ #last;
36
+ /**
37
+ * Look at a request on its way to the model. Only the main conversation's own loop request
38
+ * counts: it carries the session, it has no auxiliary purpose (titles, compaction), and it
39
+ * has tools. A request this module builds itself carries no session, so it is never mistaken
40
+ * for one.
41
+ */
42
+ see(options, sessionId) {
43
+ if (sessionId === void 0 || options.sessionId !== sessionId || options.purpose !== void 0) return;
44
+ if (typeof options.provider !== "string" || typeof options.model !== "string") return;
45
+ if (!Array.isArray(options.messages) || options.messages.length === 0) return;
46
+ this.#last = {
47
+ provider: options.provider,
48
+ model: options.model,
49
+ messages: [...options.messages],
50
+ ...Array.isArray(options.tools) && options.tools.length > 0 ? { tools: [...options.tools] } : {},
51
+ ...options.reasoningEffort !== void 0 ? { reasoningEffort: options.reasoningEffort } : {}
52
+ };
53
+ }
54
+ get last() {
55
+ return this.#last;
56
+ }
57
+ /** A new conversation (`/new`, `/resume`) starts from nothing. */
58
+ clear() {
59
+ this.#last = void 0;
60
+ }
61
+ };
62
+ var SUGGEST_INSTRUCTION = "Suggest the user's most likely next message, max 8 words, same language as the user. Reply with the message only.";
63
+ function standaloneInstruction(lastUser, answerExcerpt) {
64
+ return `${SUGGEST_INSTRUCTION}
65
+
66
+ Last user prompt: ${lastUser}
67
+ Last answer (excerpt): ${answerExcerpt}`;
68
+ }
69
+ var SOURCE = { kind: "plugin", plugin: "bruine-suggest" };
70
+ function sharedPrefixRequest(last, answer, opts = {}) {
71
+ const effort = opts.effort ?? last.reasoningEffort;
72
+ return {
73
+ provider: last.provider,
74
+ model: last.model,
75
+ messages: [
76
+ ...last.messages,
77
+ createAssistantMessage({ content: [{ type: "text", text: answer }], source: { provider: last.provider, model: last.model } }),
78
+ createUserMessage({ content: [{ type: "text", text: SUGGEST_INSTRUCTION }], source: SOURCE })
79
+ ],
80
+ ...last.tools !== void 0 ? { tools: last.tools } : {},
81
+ maxTokens: opts.maxTokens ?? 24,
82
+ ...effort !== void 0 ? { reasoningEffort: effort } : {}
83
+ };
84
+ }
85
+ function standaloneRequest(route, lastUser, answerExcerpt, opts = {}) {
86
+ return {
87
+ provider: route.provider,
88
+ model: route.model,
89
+ messages: [createUserMessage({ content: [{ type: "text", text: standaloneInstruction(lastUser, answerExcerpt) }], source: SOURCE })],
90
+ maxTokens: opts.maxTokens ?? 24,
91
+ ...opts.effort !== void 0 ? { reasoningEffort: opts.effort } : {}
92
+ };
93
+ }
94
+ function shouldSuggest(state2) {
95
+ return state2.enabled && !state2.plan && state2.lastUser.trim() !== "" && state2.answer.trim() !== "";
96
+ }
97
+ function sameRoute(a, b) {
98
+ return a.provider === b.provider && a.model === b.model;
99
+ }
100
+
101
+ // src/ui/palette.ts
102
+ import { visibleWidth } from "@earendil-works/pi-tui";
103
+ import { release as osRelease } from "os";
104
+
105
+ // src/shared-state.ts
106
+ function sharedState(key, init) {
107
+ const store = globalThis;
108
+ const id = /* @__PURE__ */ Symbol.for(`bruine.${key}`);
109
+ return store[id] ??= init();
110
+ }
111
+
112
+ // src/ui/palette.ts
113
+ function detectColorDepth(env = process.env, platform3 = process.platform, release = osRelease()) {
114
+ const forced = appEnv("COLOR", env);
115
+ if (forced === "truecolor" || forced === "256" || forced === "basic" || forced === "none") return forced;
116
+ if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return "none";
117
+ if (appEnv("ASCII", env) === "1") return "basic";
118
+ const ct = (env.COLORTERM ?? "").toLowerCase();
119
+ if (ct === "truecolor" || ct === "24bit") return "truecolor";
120
+ if (env.WT_SESSION !== void 0 || env.TERM_PROGRAM === "vscode") return "truecolor";
121
+ if ((env.TERM ?? "").includes("256color")) return "256";
122
+ if (platform3 === "win32" && env.TERM === void 0) {
123
+ const [major, , build] = release.split(".").map(Number);
124
+ if ((major ?? 0) >= 10 && (build ?? 0) >= 14931) return "truecolor";
125
+ if ((major ?? 0) >= 10 && (build ?? 0) >= 10586) return "256";
126
+ }
127
+ return "basic";
128
+ }
129
+ var NUAGE = {
130
+ sky: { hex: "#7dcfff", basic: 36 },
131
+ skyDeep: { hex: "#4aa8e0", basic: 34 },
132
+ lavender: { hex: "#b4a7ff", basic: 35 },
133
+ mint: { hex: "#8fe3a3", basic: 32 },
134
+ amber: { hex: "#f2cf73", basic: 33 },
135
+ rose: { hex: "#ff7a90", basic: 31 },
136
+ pink: { hex: "#ff9ed2", basic: 35 },
137
+ text: { hex: "#e6e9f2", basic: 37 },
138
+ // 4.8:1 on `chip`, the brightest dark surface it is read on (it was #8a90a6,
139
+ // 4.37:1 there and 4.41:1 on a One Dark background).
140
+ muted: { hex: "#9298ae", basic: 90 },
141
+ // 3.20:1 on `surface`, the AA floor for a border or control. It was #5b6178
142
+ // (2.64:1), under the floor, and it drew the editor border and the dock
143
+ // divider. Structure only: anything a user has to read is `muted` (5.10:1).
144
+ faint: { hex: "#676e87", basic: 90 },
145
+ // Painted surfaces (the console band and the prompt band). These are the
146
+ // authored values for a dark terminal; setTerminalBackdrop re-derives them
147
+ // from the background the terminal reports.
148
+ surface: { hex: "#1c2030", basic: 40 },
149
+ chip: { hex: "#262c3f", basic: 40 },
150
+ // Transcript surfaces: muted text remains above 4.5:1 on every block.
151
+ userBlock: { hex: "#152a36", basic: 40 },
152
+ toolOk: { hex: "#152b1e", basic: 40 },
153
+ toolPending: { hex: "#252830", basic: 40 },
154
+ toolErr: { hex: "#3a1820", basic: 40 },
155
+ railActive: { hex: "#afe3ff", basic: 36 },
156
+ railActiveEnd: { hex: "#8fd4ff", basic: 36 },
157
+ railErrorEnd: { hex: "#c04a5e", basic: 31 },
158
+ edge: { hex: "#4aa8e0", basic: 34 },
159
+ onSky: { hex: "#0c2b3d", basic: 30 },
160
+ // The two diff bands, on a NEUTRAL block: an added line and a removed line are
161
+ // the same kind of fact, so they get the same weight. The green is as strong as
162
+ // the red (it used to be so quiet it vanished into a green card).
163
+ addBg: { hex: "#17361f", basic: 40 },
164
+ delBg: { hex: "#3a1820", basic: 40 },
165
+ addFg: { hex: "#b8f0c6", basic: 32 },
166
+ delFg: { hex: "#ffb3c0", basic: 31 }
167
+ };
168
+ var INK_FLOOR = {
169
+ text: 7,
170
+ muted: 4.5,
171
+ // Structure only (borders, rules, dots): the 3:1 floor for a mark, not text.
172
+ faint: 3,
173
+ sky: 4.5,
174
+ skyDeep: 4.5,
175
+ lavender: 4.5,
176
+ mint: 4.5,
177
+ amber: 4.5,
178
+ rose: 4.5,
179
+ pink: 4.5,
180
+ railActive: 3,
181
+ railActiveEnd: 3,
182
+ railErrorEnd: 3,
183
+ addFg: 4.5,
184
+ delFg: 4.5
185
+ };
186
+ var INK_ROLES = Object.keys(INK_FLOOR);
187
+ function rgb(hex) {
188
+ const n = Number.parseInt(hex.slice(1), 16);
189
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
190
+ }
191
+ var state = sharedState("palette", () => ({
192
+ probed: {},
193
+ inked: {},
194
+ depth: void 0
195
+ }));
196
+ var probed = state.probed;
197
+ var inked = state.inked;
198
+ function inkHex(role) {
199
+ return inked[role] ?? NUAGE[role].hex;
200
+ }
201
+ var CUBE = [0, 95, 135, 175, 215, 255];
202
+ function distance(a, b) {
203
+ return 2 * (a[0] - b[0]) ** 2 + 4 * (a[1] - b[1]) ** 2 + 3 * (a[2] - b[2]) ** 2;
204
+ }
205
+ function to256(hex, hue = false) {
206
+ const c2 = rgb(hex);
207
+ const nearest = (v) => {
208
+ let best = 0;
209
+ for (let i = 1; i < CUBE.length; i += 1) if (Math.abs(CUBE[i] - v) < Math.abs(CUBE[best] - v)) best = i;
210
+ return best;
211
+ };
212
+ const [qr, qg, qb] = [nearest(c2[0]), nearest(c2[1]), nearest(c2[2])];
213
+ const cube = 16 + 36 * qr + 6 * qg + qb;
214
+ const cubeRgb = [CUBE[qr], CUBE[qg], CUBE[qb]];
215
+ const level = Math.max(0, Math.min(23, Math.round(((c2[0] + c2[1] + c2[2]) / 3 - 8) / 10)));
216
+ const grayRgb = [8 + 10 * level, 8 + 10 * level, 8 + 10 * level];
217
+ const isGray = Math.abs(c2[0] - c2[1]) < 12 && Math.abs(c2[1] - c2[2]) < 12;
218
+ if (hue && !isGray) {
219
+ if (cube === 16) return c2[0] >= c2[1] && c2[0] >= c2[2] ? 52 : c2[1] >= c2[2] ? 22 : 17;
220
+ return cube;
221
+ }
222
+ return distance(c2, grayRgb) <= distance(c2, cubeRgb) ? 232 + level : cube;
223
+ }
224
+ var HUE_ROLES = /* @__PURE__ */ new Set(["addBg", "delBg", "toolErr", "sky", "edge"]);
225
+ function fgCode(role, depth) {
226
+ const sw = typeof role === "string" && role.startsWith("#") ? { hex: role, basic: 37 } : { hex: inkHex(role), basic: NUAGE[role].basic };
227
+ if (depth === "none") return "";
228
+ if (depth === "truecolor") {
229
+ const [r, g, b] = rgb(sw.hex);
230
+ return `\x1B[38;2;${String(r)};${String(g)};${String(b)}m`;
231
+ }
232
+ if (depth === "256") return `\x1B[38;5;${String(to256(sw.hex))}m`;
233
+ return `\x1B[${String(sw.basic)}m`;
234
+ }
235
+ function bgCode(role, depth) {
236
+ if (typeof role === "string" && role.startsWith("#")) return bgEscape(role, 40, depth);
237
+ const sw = NUAGE[role];
238
+ return bgEscape(probed[role] ?? sw.hex, sw.basic, depth, HUE_ROLES.has(role));
239
+ }
240
+ function bgEscape(hex, basic, depth, hue = false) {
241
+ if (depth === "none") return "";
242
+ if (depth === "truecolor") {
243
+ const [r, g, b] = rgb(hex);
244
+ return `\x1B[48;2;${String(r)};${String(g)};${String(b)}m`;
245
+ }
246
+ if (depth === "256") return `\x1B[48;5;${String(to256(hex, hue))}m`;
247
+ return `\x1B[${String(basic + 10)}m`;
248
+ }
249
+ function bgOptOut(env = process.env) {
250
+ const v = (appEnv("BG", env) ?? "").toLowerCase();
251
+ return v === "0" || v === "none" || v === "off" || v === "false";
252
+ }
253
+ function bgEnabled(depth = colorDepth()) {
254
+ if (bgOptOut()) return false;
255
+ return depth === "truecolor" || depth === "256";
256
+ }
257
+ function colorDepth() {
258
+ state.depth ??= detectColorDepth();
259
+ return state.depth;
260
+ }
261
+ function paint(role, s) {
262
+ const d = colorDepth();
263
+ if (d === "none") return s;
264
+ return `${fgCode(role, d)}${s}\x1B[39m`;
265
+ }
266
+ function onBg(role, s) {
267
+ const d = colorDepth();
268
+ if (!bgEnabled(d)) return s;
269
+ return `${bgCode(role, d)}${s}\x1B[49m`;
270
+ }
271
+
272
+ // src/ui/rain.ts
273
+ var effortState = sharedState("effort", () => ({ level: 0.45, effort: void 0 }));
274
+ function rainLevel() {
275
+ return effortState.level;
276
+ }
277
+ var DROP_DOTS = ["\u2801", "\u2802", "\u2804", "\u2840"];
278
+ var DROP_ASCII = ["'", "`", ",", "."];
279
+ var DROP_REST = "\u2800";
280
+ var DROP_REST_ASCII = ".";
281
+ function dropSpinner(elapsed, level, ascii = false) {
282
+ const lv = Math.max(0, Math.min(1, level));
283
+ const fall = 4;
284
+ const cycle = fall + Math.round((1 - lv) * 7);
285
+ const step = Math.floor(Math.max(0, elapsed) / (140 - 60 * lv));
286
+ const frames = ascii ? DROP_ASCII : DROP_DOTS;
287
+ let out = "";
288
+ for (let cell = 0; cell < 3; cell += 1) {
289
+ const at = (step + cell * 3) % cycle;
290
+ out += at < fall ? frames[at] : ascii ? DROP_REST_ASCII : DROP_REST;
291
+ }
292
+ return out;
293
+ }
294
+
295
+ // src/render/reasoning.ts
296
+ import stringWidth from "string-width";
297
+
298
+ // src/render/chars.ts
299
+ var UNICODE_ICONS = {
300
+ think: "\u2234",
301
+ prompt: "\u203A",
302
+ ok: "\u2713",
303
+ fail: "\u2717",
304
+ bullet: "\u25CF",
305
+ spark: "",
306
+ folder: "\u25B8"
307
+ };
308
+ var ASCII_ICONS = {
309
+ think: "*",
310
+ prompt: ">",
311
+ ok: "v",
312
+ fail: "x",
313
+ bullet: ">",
314
+ spark: "",
315
+ folder: ">"
316
+ };
317
+ var UTF8_RE = /utf-?8/i;
318
+ var UNICODE_TERMINALS = /* @__PURE__ */ new Set(["vscode", "WezTerm", "ghostty", "iTerm.app", "Apple_Terminal", "Hyper", "Tabby"]);
319
+ function iconsFor(env = process.env, platform3 = process.platform) {
320
+ if (appEnv("ASCII", env) === "1") return ASCII_ICONS;
321
+ const locale = `${env.LC_ALL ?? ""}${env.LC_CTYPE ?? ""}${env.LANG ?? ""}`;
322
+ if (UTF8_RE.test(locale)) return UNICODE_ICONS;
323
+ if (env.WT_SESSION !== void 0 || UNICODE_TERMINALS.has(env.TERM_PROGRAM ?? "")) return UNICODE_ICONS;
324
+ if (platform3 === "darwin") return locale === "" ? UNICODE_ICONS : ASCII_ICONS;
325
+ return ASCII_ICONS;
326
+ }
327
+ function isAscii(icons = iconsFor()) {
328
+ return icons.think === "*";
329
+ }
330
+ var cached;
331
+ function bruineIcons() {
332
+ if (cached === void 0) cached = iconsFor();
333
+ return cached;
334
+ }
335
+ function withoutEmoji(text) {
336
+ return text.replace(
337
+ /[\p{Extended_Pictographic}\p{Emoji_Modifier}][\uFE0F\u200D]*(?:[\p{Extended_Pictographic}\p{Emoji_Modifier}][\uFE0F\u200D]*)* ?|[\uFE0F\u200D]/gu,
338
+ ""
339
+ );
340
+ }
341
+
342
+ // src/render/reasoning.ts
343
+ function dim(s) {
344
+ return `\x1B[2m${s}\x1B[22m`;
345
+ }
346
+ function sanitize(s) {
347
+ return withoutEmoji(s).replace(/[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]/g, " ");
348
+ }
349
+ function splitReasoningSegments(current, lastFinished, delta) {
350
+ const input = current + sanitize(delta);
351
+ let start = 0;
352
+ let finished = lastFinished;
353
+ let finishedAny = false;
354
+ for (let i = 0; i < input.length; i++) {
355
+ const ch = input[i];
356
+ const newline = ch === "\n";
357
+ const punctuation = /[.!?…]/.test(ch) && /\s/.test(input[i + 1] ?? "");
358
+ if (!newline && !punctuation) continue;
359
+ const candidate = input.slice(start, newline ? i : i + 1).trim();
360
+ if (!newline && /\b(?:e\.g|i\.e)\.$/i.test(candidate)) continue;
361
+ if (candidate !== "") {
362
+ finished = candidate;
363
+ finishedAny = true;
364
+ }
365
+ start = i + 1;
366
+ }
367
+ return { current: input.slice(start).trimStart(), lastFinished: finished, finishedAny };
368
+ }
369
+ var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
370
+ function clipCells(text, width, ellipsis = "\u2026") {
371
+ if (width <= 0) return "";
372
+ if (stringWidth(text) <= width) return text;
373
+ const suffix = width >= stringWidth(ellipsis) ? ellipsis : ".".repeat(width);
374
+ let out = "";
375
+ let used = 0;
376
+ for (const { segment } of segmenter.segment(text)) {
377
+ const cells = stringWidth(segment);
378
+ if (used + cells > width - stringWidth(suffix)) break;
379
+ out += segment;
380
+ used += cells;
381
+ }
382
+ return out + suffix;
383
+ }
384
+ var SPINNER_FRAMES = ["\xB7", "\u2722", "\u273A", "\u2736", "\u273B", "\u273D", "\u273B", "\u2736", "\u273A", "\u2722"];
385
+ var ASCII_SPINNER_FRAMES = ["-", "\\", "|", "/"];
386
+ function spinnerFrame(elapsed, icons) {
387
+ const frames = icons.think === "*" ? ASCII_SPINNER_FRAMES : SPINNER_FRAMES;
388
+ return frames[Math.floor(Math.max(0, elapsed) / 100) % frames.length];
389
+ }
390
+ function completeWords(current) {
391
+ if (current.trim() === "") return [];
392
+ const endsWithSpace = /\s$/.test(current);
393
+ const parts = current.split(/\s+/).filter((p) => p !== "");
394
+ if (endsWithSpace) return parts;
395
+ return parts.slice(0, -1);
396
+ }
397
+ function thinkingWords(words, columns, elapsed, icons) {
398
+ const prefix = `${dropSpinner(elapsed, rainLevel(), icons.think === "*")} Thinking`;
399
+ const ellipsis = icons.think === "*" ? "..." : "\u2026";
400
+ if (words.length === 0) return clipCells(prefix, columns, ellipsis);
401
+ for (let i = 0; i < words.length; i++) {
402
+ const candidate = words.slice(i).join(" ");
403
+ if (stringWidth(`${prefix} ${candidate}`) <= columns) {
404
+ return `${prefix} ${candidate}`;
405
+ }
406
+ }
407
+ const last = words.at(-1) ?? "";
408
+ const avail = Math.max(0, columns - stringWidth(`${prefix} `));
409
+ return `${prefix} ${clipCells(last, avail, ellipsis)}`;
410
+ }
411
+ var ReasoningLine = class {
412
+ constructor(screen, now = Date.now, icons = bruineIcons()) {
413
+ this.screen = screen;
414
+ this.now = now;
415
+ this.icons = icons;
416
+ }
417
+ screen;
418
+ now;
419
+ icons;
420
+ #active = false;
421
+ #current = "";
422
+ #lastFinished = "";
423
+ #startTime = 0;
424
+ get active() {
425
+ return this.#active;
426
+ }
427
+ push(delta) {
428
+ if (delta === "") return;
429
+ if (!this.#active) {
430
+ this.#active = true;
431
+ this.#startTime = this.now();
432
+ }
433
+ const split = splitReasoningSegments(this.#current, this.#lastFinished, delta);
434
+ this.#current = split.current;
435
+ this.#lastFinished = split.lastFinished;
436
+ }
437
+ end() {
438
+ if (!this.#active) return;
439
+ const seconds = (this.now() - this.#startTime) / 1e3;
440
+ this.screen.write(`Thought for ${seconds.toFixed(1)}s
441
+ `);
442
+ this.#active = false;
443
+ this.#current = "";
444
+ this.#lastFinished = "";
445
+ }
446
+ };
447
+
448
+ // src/render/text.ts
449
+ var TextStream = class {
450
+ #screen;
451
+ #first = true;
452
+ #lastChar = "";
453
+ constructor(screen) {
454
+ this.#screen = screen;
455
+ }
456
+ push(delta) {
457
+ if (delta === "") return;
458
+ this.#first = false;
459
+ this.#screen.write(delta);
460
+ this.#lastChar = delta[delta.length - 1] ?? "";
461
+ }
462
+ end() {
463
+ if (this.#first) return;
464
+ if (this.#lastChar !== "\n") {
465
+ this.#screen.write("\n");
466
+ }
467
+ this.#first = true;
468
+ this.#lastChar = "";
469
+ }
470
+ };
471
+
472
+ // src/render/tools.ts
473
+ var SUMMARY_KEYS = [
474
+ "command",
475
+ "cmd",
476
+ "path",
477
+ "file_path",
478
+ "url",
479
+ "query",
480
+ "pattern"
481
+ ];
482
+ var MAX_OUTPUT_LINES = 5;
483
+ function truncate(s, max) {
484
+ if (max <= 1) return "\u2026";
485
+ if (s.length <= max) return s;
486
+ return `${s.slice(0, max - 1)}\u2026`;
487
+ }
488
+ function toolSummary(rawArgs, tool, columns) {
489
+ const max = columns - tool.length - 6;
490
+ if (rawArgs === "") return "";
491
+ let parsed;
492
+ try {
493
+ parsed = JSON.parse(rawArgs);
494
+ } catch {
495
+ return "\u2026";
496
+ }
497
+ let text;
498
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
499
+ const obj = parsed;
500
+ const key = SUMMARY_KEYS.find((k) => k in obj);
501
+ text = key !== void 0 ? String(obj[key]) : JSON.stringify(parsed);
502
+ } else {
503
+ text = JSON.stringify(parsed);
504
+ }
505
+ return truncate(text, max);
506
+ }
507
+ var ToolCallView = class {
508
+ #screen;
509
+ #now;
510
+ #icons;
511
+ #calls = /* @__PURE__ */ new Map();
512
+ constructor(screen, now = Date.now, icons = bruineIcons()) {
513
+ this.#screen = screen;
514
+ this.#now = now;
515
+ this.#icons = icons;
516
+ }
517
+ start(id, toolName) {
518
+ this.#calls.set(id, { tool: toolName, rawArgs: "", startTime: this.#now() });
519
+ }
520
+ args(id, jsonDelta) {
521
+ const call = this.#calls.get(id);
522
+ if (!call) return;
523
+ call.rawArgs += jsonDelta;
524
+ }
525
+ result(id, ok, output) {
526
+ const call = this.#calls.get(id);
527
+ if (!call) return;
528
+ const seconds = (this.#now() - call.startTime) / 1e3;
529
+ const summary = this.#summary(call);
530
+ const mark = ok ? this.#icons.ok : this.#icons.fail;
531
+ const head = `${mark} ${call.tool}` + (summary !== "" ? ` ${summary}` : "") + ` ${seconds.toFixed(1)}s
532
+ `;
533
+ this.#screen.write(head);
534
+ const lines = output.split("\n");
535
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
536
+ for (const line of lines.slice(0, MAX_OUTPUT_LINES)) {
537
+ this.#screen.write(` ${line}
538
+ `);
539
+ }
540
+ const rest = lines.length - MAX_OUTPUT_LINES;
541
+ if (rest > 0) {
542
+ this.#screen.write(` \u2026 ${rest} more lines
543
+ `);
544
+ }
545
+ this.#calls.delete(id);
546
+ }
547
+ /** Current header info for a tracked call; used by the approval prompt. */
548
+ describe(id) {
549
+ const call = this.#calls.get(id);
550
+ if (!call) return void 0;
551
+ return { tool: call.tool, summary: this.#summary(call) };
552
+ }
553
+ #summary(call) {
554
+ return toolSummary(call.rawArgs, call.tool, this.#screen.columns);
555
+ }
556
+ #header(call, summary) {
557
+ return `\u25CF ${call.tool} ${summary}`;
558
+ }
559
+ };
560
+
561
+ // src/render/elapsed.ts
562
+ function formatElapsed(ms) {
563
+ const seconds = Math.floor(Math.max(0, Number.isFinite(ms) ? ms : 0) / 1e3);
564
+ if (seconds < 60) return `${seconds}s`;
565
+ const minutes = Math.floor(seconds / 60);
566
+ if (minutes < 60) return `${minutes}min ${String(seconds % 60).padStart(2, "0")}s`;
567
+ return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, "0")}min`;
568
+ }
569
+
570
+ // src/ui/reasoning-component.ts
571
+ import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
572
+
573
+ // src/ui/highlight.ts
574
+ import hljs from "highlight.js/lib/core";
575
+ import bash from "highlight.js/lib/languages/bash";
576
+ import c from "highlight.js/lib/languages/c";
577
+ import cpp from "highlight.js/lib/languages/cpp";
578
+ import css from "highlight.js/lib/languages/css";
579
+ import diff from "highlight.js/lib/languages/diff";
580
+ import go from "highlight.js/lib/languages/go";
581
+ import java from "highlight.js/lib/languages/java";
582
+ import javascript from "highlight.js/lib/languages/javascript";
583
+ import json from "highlight.js/lib/languages/json";
584
+ import markdown from "highlight.js/lib/languages/markdown";
585
+ import python from "highlight.js/lib/languages/python";
586
+ import rust from "highlight.js/lib/languages/rust";
587
+ import sql from "highlight.js/lib/languages/sql";
588
+ import typescript from "highlight.js/lib/languages/typescript";
589
+ import xml from "highlight.js/lib/languages/xml";
590
+ import yaml from "highlight.js/lib/languages/yaml";
591
+ var LANGS = {
592
+ bash,
593
+ c,
594
+ cpp,
595
+ css,
596
+ diff,
597
+ go,
598
+ java,
599
+ javascript,
600
+ json,
601
+ markdown,
602
+ python,
603
+ rust,
604
+ sql,
605
+ typescript,
606
+ xml,
607
+ yaml
608
+ };
609
+ for (const [name2, lang] of Object.entries(LANGS)) hljs.registerLanguage(name2, lang);
610
+ var ALIASES = {
611
+ sh: "bash",
612
+ shell: "bash",
613
+ zsh: "bash",
614
+ fish: "bash",
615
+ console: "bash",
616
+ js: "javascript",
617
+ jsx: "javascript",
618
+ mjs: "javascript",
619
+ cjs: "javascript",
620
+ ts: "typescript",
621
+ tsx: "typescript",
622
+ mts: "typescript",
623
+ py: "python",
624
+ rs: "rust",
625
+ yml: "yaml",
626
+ html: "xml",
627
+ htm: "xml",
628
+ svg: "xml",
629
+ md: "markdown",
630
+ "c++": "cpp",
631
+ h: "c",
632
+ hpp: "cpp"
633
+ };
634
+ var SCOPES = [
635
+ ["comment", "faint"],
636
+ ["quote", "faint"],
637
+ ["keyword", "pink"],
638
+ ["built_in", "pink"],
639
+ ["literal", "pink"],
640
+ ["selector-tag", "pink"],
641
+ ["string", "mint"],
642
+ ["regexp", "mint"],
643
+ ["addition", "mint"],
644
+ ["deletion", "rose"],
645
+ ["number", "amber"],
646
+ ["type", "amber"],
647
+ ["title.class", "amber"],
648
+ ["title", "sky"],
649
+ ["function", "sky"],
650
+ ["section", "sky"],
651
+ ["meta", "lavender"],
652
+ ["symbol", "lavender"],
653
+ ["attr", "lavender"],
654
+ ["attribute", "lavender"],
655
+ ["variable", "text"],
656
+ ["params", "text"],
657
+ ["property", "text"]
658
+ ];
659
+ function roleFor(classes) {
660
+ for (let i = classes.length - 1; i >= 0; i--) {
661
+ const scope = classes[i].replace(/^hljs-/, "").replace(/_+$/, "");
662
+ for (const [prefix, role] of SCOPES) if (scope === prefix || scope.startsWith(`${prefix}.`) || scope.startsWith(prefix)) return role;
663
+ }
664
+ return void 0;
665
+ }
666
+ function unescapeHtml(s) {
667
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&#39;/g, "'").replace(/&amp;/g, "&");
668
+ }
669
+ function resolveLanguage(lang) {
670
+ if (lang === void 0) return void 0;
671
+ const l = lang.trim().toLowerCase().split(/\s+/)[0] ?? "";
672
+ if (l === "") return void 0;
673
+ const name2 = ALIASES[l] ?? l;
674
+ return hljs.getLanguage(name2) !== void 0 ? name2 : void 0;
675
+ }
676
+ function highlightCode(code, lang) {
677
+ const language = resolveLanguage(lang);
678
+ const plainLines = code.split("\n");
679
+ if (language === void 0 || colorDepth() === "none") return plainLines;
680
+ let html;
681
+ try {
682
+ html = hljs.highlight(code, { language, ignoreIllegals: true }).value;
683
+ } catch {
684
+ return plainLines;
685
+ }
686
+ const out = [];
687
+ let line = "";
688
+ const stack = [];
689
+ const emit = (text) => {
690
+ const parts = unescapeHtml(text).split("\n");
691
+ parts.forEach((part, i) => {
692
+ if (i > 0) {
693
+ out.push(line);
694
+ line = "";
695
+ }
696
+ if (part === "") return;
697
+ const role = roleFor(stack.flat());
698
+ line += role === void 0 ? part : paint(role, part);
699
+ });
700
+ };
701
+ const re = /<span class="([^"]*)">|<\/span>|([^<]+)/g;
702
+ let m;
703
+ while ((m = re.exec(html)) !== null) {
704
+ if (m[1] !== void 0) stack.push(m[1].split(/\s+/));
705
+ else if (m[0] === "</span>") stack.pop();
706
+ else if (m[2] !== void 0) emit(m[2]);
707
+ }
708
+ out.push(line);
709
+ return out;
710
+ }
711
+
712
+ // src/ui/theme.ts
713
+ var ansi = {
714
+ dim: (s) => `\x1B[2m${s}\x1B[22m`,
715
+ gray: (s) => paint("muted", s),
716
+ faint: (s) => paint("faint", s),
717
+ cyan: (s) => paint("sky", s),
718
+ blue: (s) => paint("skyDeep", s),
719
+ violet: (s) => paint("lavender", s),
720
+ pink: (s) => paint("pink", s),
721
+ green: (s) => paint("mint", s),
722
+ red: (s) => paint("rose", s),
723
+ yellow: (s) => paint("amber", s),
724
+ text: (s) => paint("text", s),
725
+ bold: (s) => `\x1B[1m${s}\x1B[22m`,
726
+ italic: (s) => `\x1B[3m${s}\x1B[23m`,
727
+ chip: (s) => bgEnabled() ? onBg("sky", paint("onSky", s)) : paint("sky", s),
728
+ surface: (s) => onBg("surface", s),
729
+ chipBg: (s) => onBg("chip", s),
730
+ /** The 1-column accent that opens every painted surface. */
731
+ edge: (s) => onBg("edge", s)
732
+ };
733
+ var markdownThemeFor = (icons = bruineIcons()) => {
734
+ const rule = isAscii(icons) ? "|" : "\u258F";
735
+ const gutter = `${ansi.faint(rule)} `;
736
+ return {
737
+ // skyDeep, and not the sky the links wear: a title and a link are different
738
+ // jobs, and one accent at a time is what keeps a screen readable.
739
+ heading: (s) => ansi.bold(ansi.blue(s)),
740
+ link: (s) => ansi.cyan(s),
741
+ linkUrl: (s) => ansi.gray(s),
742
+ code: (s) => ansi.yellow(s),
743
+ codeBlock: (s) => s,
744
+ codeBlockBorder: (s) => {
745
+ const lang = s.replace(/^```/, "").trim();
746
+ return lang === "" ? gutter : `${gutter}${ansi.gray(lang)}`;
747
+ },
748
+ codeBlockIndent: gutter,
749
+ highlightCode,
750
+ quote: (s) => ansi.gray(s),
751
+ quoteBorder: (s) => ansi.gray(s),
752
+ hr: (s) => ansi.gray(s),
753
+ listBullet: (s) => ansi.violet(s),
754
+ bold: (s) => ansi.bold(ansi.text(s)),
755
+ italic: (s) => ansi.italic(s),
756
+ strikethrough: (s) => ansi.gray(s),
757
+ underline: (s) => ansi.cyan(s)
758
+ };
759
+ };
760
+
761
+ // src/ui/reasoning-component.ts
762
+ var ReasoningComponent = class {
763
+ constructor(now = Date.now, icons = bruineIcons()) {
764
+ this.now = now;
765
+ this.icons = icons;
766
+ }
767
+ now;
768
+ icons;
769
+ transcriptStyle = "reasoning";
770
+ onActivityChange;
771
+ #current = "";
772
+ #lastFinished = "";
773
+ /** Everything the model has said in this thought, kept so it can be read back. */
774
+ #thought = "";
775
+ #expanded = false;
776
+ #startTime = 0;
777
+ #started = false;
778
+ #ended = false;
779
+ #endTime = 0;
780
+ get active() {
781
+ return this.#started && !this.#ended;
782
+ }
783
+ /** True when a press on this block does something: a finished thought only. */
784
+ get clickable() {
785
+ return this.#ended;
786
+ }
787
+ /** Open or close the thought. A press on a live thought changes nothing. */
788
+ click() {
789
+ if (this.#ended) this.#expanded = !this.#expanded;
790
+ }
791
+ push(delta) {
792
+ if (!delta || this.#ended) return;
793
+ if (!this.#started) {
794
+ this.#started = true;
795
+ this.#startTime = this.now();
796
+ this.onActivityChange?.(true);
797
+ }
798
+ this.#thought += sanitize(delta);
799
+ const split = splitReasoningSegments(this.#current, this.#lastFinished, delta);
800
+ this.#current = split.current;
801
+ this.#lastFinished = split.lastFinished;
802
+ }
803
+ end() {
804
+ if (!this.active) return;
805
+ this.#ended = true;
806
+ this.#endTime = this.now();
807
+ this.onActivityChange?.(false);
808
+ }
809
+ render(width) {
810
+ if (!this.#started) return [];
811
+ const words = completeWords(this.#current);
812
+ if (!this.#ended) return [ansi.violet(dim(thinkingWords(words, width, this.now() - this.#startTime, this.icons)))];
813
+ const head = ansi.gray(dim(clipCells(`${this.icons.think} Thought for ${this.#endTime - this.#startTime < 1e3 ? "<1s" : formatElapsed(this.#endTime - this.#startTime)}`, width)));
814
+ if (!this.#expanded) return [head];
815
+ const indent = " ".repeat(2);
816
+ const body = this.#thought.trim();
817
+ if (body === "") return [head];
818
+ const room = Math.max(1, width - indent.length);
819
+ return [
820
+ head,
821
+ ...wrapTextWithAnsi(body, room).map((line) => `${indent}${ansi.gray(ansi.italic(line))}`)
822
+ ];
823
+ }
824
+ invalidate() {
825
+ }
826
+ };
827
+
828
+ // src/ui/tool-call-component.ts
829
+ import stringWidth4 from "string-width";
830
+ import { isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "path";
831
+
832
+ // src/ui/tool-group.ts
833
+ import stringWidth2 from "string-width";
834
+ function formatDurationParts(seconds) {
835
+ if (!(seconds >= 0.05)) return void 0;
836
+ if (seconds < 10) return { value: seconds.toFixed(1), unit: "s" };
837
+ if (seconds < 60) return { value: String(Math.round(seconds)), unit: "s" };
838
+ const m = Math.floor(seconds / 60);
839
+ return { value: `${String(m)}m${String(Math.round(seconds % 60)).padStart(2, "0")}`, unit: "s" };
840
+ }
841
+ function formatDuration(seconds) {
842
+ const parts = formatDurationParts(seconds);
843
+ return parts === void 0 ? void 0 : `${parts.value}${parts.unit}`;
844
+ }
845
+
846
+ // src/ui/links.ts
847
+ import { isAbsolute } from "path";
848
+ import { pathToFileURL } from "url";
849
+ function canLink() {
850
+ return appEnv("NO_LINKS") !== "1";
851
+ }
852
+ function fileLink(text, absolutePath) {
853
+ if (!canLink() || text === "" || absolutePath === "" || !isAbsolute(absolutePath)) return text;
854
+ let url;
855
+ try {
856
+ url = pathToFileURL(absolutePath).href;
857
+ } catch {
858
+ return text;
859
+ }
860
+ return `\x1B]8;;${url}\x1B\\${text}\x1B]8;;\x1B\\`;
861
+ }
862
+
863
+ // src/ui/diff-view.ts
864
+ import { diffLines } from "diff";
865
+ import { readFileSync } from "fs";
866
+ import { resolve } from "path";
867
+ import stringWidth3 from "string-width";
868
+ import { visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
869
+ var MAX_DIFF_LINES = 12;
870
+ function splitKeep(s) {
871
+ const parts = s.split("\n");
872
+ if (parts.at(-1) === "") parts.pop();
873
+ return parts;
874
+ }
875
+ function lineOf(hay, needle) {
876
+ if (needle === "") return void 0;
877
+ const at = hay.indexOf(needle);
878
+ if (at < 0) return void 0;
879
+ return hay.slice(0, at).split("\n").length;
880
+ }
881
+ function readAfter(path7) {
882
+ if (path7 === void 0 || path7 === "") return void 0;
883
+ try {
884
+ return readFileSync(resolve(process.cwd(), path7), "utf8");
885
+ } catch {
886
+ return void 0;
887
+ }
888
+ }
889
+ function replacement(oldText, newText, start) {
890
+ const a = splitKeep(oldText);
891
+ const b = splitKeep(newText);
892
+ let head = 0;
893
+ while (head < a.length && head < b.length && a[head] === b[head]) head += 1;
894
+ let tail = 0;
895
+ while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail += 1;
896
+ const lines = [];
897
+ let added = 0;
898
+ let removed = 0;
899
+ let nNew = start;
900
+ let nOld = start;
901
+ const ctx = (text) => {
902
+ lines.push({
903
+ kind: "ctx",
904
+ text,
905
+ ...nNew !== void 0 ? { n: nNew } : {},
906
+ ...nOld !== void 0 ? { old: nOld } : {}
907
+ });
908
+ if (nNew !== void 0) nNew += 1;
909
+ if (nOld !== void 0) nOld += 1;
910
+ };
911
+ for (const text of b.slice(0, head)) ctx(text);
912
+ const midOld = a.slice(head, a.length - tail).join("\n");
913
+ const midNew = b.slice(head, b.length - tail).join("\n");
914
+ for (const part of diffLines(midOld === "" ? "" : `${midOld}
915
+ `, midNew === "" ? "" : `${midNew}
916
+ `)) {
917
+ for (const text of splitKeep(part.value)) {
918
+ if (part.added === true) {
919
+ lines.push({ kind: "add", text, ...nNew !== void 0 ? { n: nNew } : {} });
920
+ added += 1;
921
+ if (nNew !== void 0) nNew += 1;
922
+ } else if (part.removed === true) {
923
+ lines.push({ kind: "del", text, ...nOld !== void 0 ? { old: nOld } : {} });
924
+ removed += 1;
925
+ if (nOld !== void 0) nOld += 1;
926
+ } else {
927
+ ctx(text);
928
+ }
929
+ }
930
+ }
931
+ for (const text of b.slice(b.length - tail)) ctx(text);
932
+ return { lines, added, removed };
933
+ }
934
+ function diffForCall(tool, rawArgs) {
935
+ let args;
936
+ try {
937
+ const v = JSON.parse(rawArgs);
938
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return void 0;
939
+ args = v;
940
+ } catch {
941
+ return void 0;
942
+ }
943
+ const path7 = typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : void 0;
944
+ if (tool === "write" && typeof args.content === "string") {
945
+ const lines = splitKeep(args.content).map((text, i) => ({ kind: "add", text, n: i + 1 }));
946
+ return { lines, added: lines.length, removed: 0 };
947
+ }
948
+ if (tool === "edit" && typeof args.old_string === "string" && typeof args.new_string === "string") {
949
+ const after = readAfter(path7);
950
+ const start = after === void 0 ? void 0 : lineOf(after, args.new_string);
951
+ return replacement(args.old_string, args.new_string, start);
952
+ }
953
+ if (tool === "multi_edit" && Array.isArray(args.edits)) {
954
+ const after = readAfter(path7);
955
+ const all = { lines: [], added: 0, removed: 0 };
956
+ for (const e of args.edits) {
957
+ if (typeof e.old_string !== "string" || typeof e.new_string !== "string") continue;
958
+ const d = replacement(e.old_string, e.new_string, after === void 0 ? void 0 : lineOf(after, e.new_string));
959
+ all.lines.push(...d.lines);
960
+ all.added += d.added;
961
+ all.removed += d.removed;
962
+ }
963
+ return all.lines.length > 0 ? all : void 0;
964
+ }
965
+ return void 0;
966
+ }
967
+ function diffCounter(d) {
968
+ return `${paint("mint", `+${String(d.added)}`)} ${paint("rose", `-${String(d.removed)}`)}`;
969
+ }
970
+ function renderDiff(d, width, ascii = false) {
971
+ const shown = d.lines.slice(0, MAX_DIFF_LINES);
972
+ const digits = (n) => n > 0 ? String(n).length : 0;
973
+ let oldW = Math.max(0, ...shown.map((l) => digits(l.old ?? 0)));
974
+ const newW = Math.max(0, ...shown.map((l) => digits(l.n ?? 0)));
975
+ if (oldW + newW + 3 > Math.max(0, width - 8)) oldW = 0;
976
+ const gutter = " ";
977
+ const head = (l) => {
978
+ const parts = [oldW > 0 ? String(l.old ?? "").padStart(oldW) : "", newW > 0 ? String(l.n ?? "").padStart(newW) : ""];
979
+ const numbers = parts.filter((c2) => c2 !== "").join(" ");
980
+ return numbers === "" ? "" : `${numbers} `;
981
+ };
982
+ const ellipsis = ascii ? "..." : "\u2026";
983
+ const row = (l) => {
984
+ const prefix = head(l);
985
+ const sign = l.kind === "add" ? "+" : l.kind === "del" ? "-" : " ";
986
+ const room = Math.max(1, width - visibleWidth2(prefix) - sign.length - gutter.length);
987
+ const body = clipCells(l.text.replace(/\t/g, " "), room, ellipsis);
988
+ const pad = " ".repeat(Math.max(0, room - stringWidth3(body)));
989
+ if (l.kind === "ctx") return `${paint("muted", prefix)}${paint("faint", sign)}${paint("muted", `${gutter}${body}${pad}`)}`;
990
+ const role = l.kind === "add" ? "mint" : "rose";
991
+ const ink = l.kind === "add" ? "addFg" : "delFg";
992
+ const line = `${paint("muted", prefix)}${paint(role, sign)}${paint(ink, `${gutter}${body}${pad}`)}`;
993
+ return bgEnabled() ? onBg(l.kind === "add" ? "addBg" : "delBg", line) : line;
994
+ };
995
+ const out = shown.map(row);
996
+ const rest = d.lines.length - shown.length;
997
+ if (rest > 0) out.push(paint("muted", `${" ".repeat(Math.min(width, 4))}${ellipsis} ${String(rest)} more lines`));
998
+ return out;
999
+ }
1000
+
1001
+ // src/ui/partial-json.ts
1002
+ function parsePartialJson(raw) {
1003
+ const text = raw.trim();
1004
+ if (text === "") return void 0;
1005
+ try {
1006
+ return JSON.parse(text);
1007
+ } catch {
1008
+ }
1009
+ let cut = text;
1010
+ for (let pass = 0; pass < 64; pass += 1) {
1011
+ try {
1012
+ return JSON.parse(closeAt(cut));
1013
+ } catch {
1014
+ const back = lastSafePoint(cut);
1015
+ if (back < 1) return void 0;
1016
+ cut = cut.slice(0, back);
1017
+ }
1018
+ }
1019
+ return void 0;
1020
+ }
1021
+ function closeAt(text) {
1022
+ const stack = [];
1023
+ let inString = false;
1024
+ let escaped = false;
1025
+ for (const ch of text) {
1026
+ if (inString) {
1027
+ if (escaped) escaped = false;
1028
+ else if (ch === "\\") escaped = true;
1029
+ else if (ch === '"') inString = false;
1030
+ continue;
1031
+ }
1032
+ if (ch === '"') inString = true;
1033
+ else if (ch === "{") stack.push("}");
1034
+ else if (ch === "[") stack.push("]");
1035
+ else if (ch === "}" || ch === "]") stack.pop();
1036
+ }
1037
+ let out = text;
1038
+ if (inString) {
1039
+ out = out.replace(/\\u[0-9a-fA-F]{0,3}$/, "").replace(/(?<!\\)(\\\\)*\\$/, "$1");
1040
+ out += '"';
1041
+ }
1042
+ out = out.replace(/\s+$/, "");
1043
+ if (out.endsWith(":")) out += "null";
1044
+ else if (out.endsWith(",")) out = out.slice(0, -1);
1045
+ return out + stack.reverse().join("");
1046
+ }
1047
+ function lastSafePoint(text) {
1048
+ let inString = false;
1049
+ let escaped = false;
1050
+ let point = -1;
1051
+ for (let i = 0; i < text.length; i += 1) {
1052
+ const ch = text[i];
1053
+ if (inString) {
1054
+ if (escaped) escaped = false;
1055
+ else if (ch === "\\") escaped = true;
1056
+ else if (ch === '"') inString = false;
1057
+ continue;
1058
+ }
1059
+ if (ch === '"') inString = true;
1060
+ else if (ch === ",") point = i;
1061
+ else if ((ch === "{" || ch === "[") && i + 1 < text.length) point = i + 1;
1062
+ }
1063
+ return point === text.length ? -1 : point;
1064
+ }
1065
+
1066
+ // src/ui/tool-summary.ts
1067
+ import { readFileSync as readFileSync2 } from "fs";
1068
+ function toolSummariesEnabled(env = process.env) {
1069
+ if (appEnv("TOOL_SUMMARIES", env) === "0") return false;
1070
+ if (appEnv("TOOL_SUMMARIES", env) === "1") return true;
1071
+ if (!env.DSH_HOME) return true;
1072
+ try {
1073
+ const doc = JSON.parse(readFileSync2(configReadPath(env.DSH_HOME), "utf8"));
1074
+ return doc?.toolSummaries !== false;
1075
+ } catch {
1076
+ return true;
1077
+ }
1078
+ }
1079
+ function readableToolSummary(tool, args) {
1080
+ if (!args) return void 0;
1081
+ for (const key of ["description", "title"]) {
1082
+ const value = args[key];
1083
+ if (typeof value === "string" && value.trim()) return value.trim();
1084
+ }
1085
+ if (tool === "subagent" || tool === "subagent_fork") {
1086
+ const prompt = args.prompt;
1087
+ if (typeof prompt === "string") return prompt.split(/\r?\n/).find((line) => line.trim())?.trim();
1088
+ }
1089
+ return void 0;
1090
+ }
1091
+
1092
+ // src/ui/image-preview.ts
1093
+ import { readFileSync as readFileSync3, statSync } from "fs";
1094
+ import { createRequire } from "module";
1095
+ import { inflateSync } from "zlib";
1096
+ var MAX_PREVIEW_BYTES = 16 * 1024 * 1024;
1097
+ var MAX_PREVIEW_PIXELS = 4e7;
1098
+ var PNG_SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
1099
+ function sniffImage(bytes) {
1100
+ if (PNG_SIGNATURE.every((b, i) => bytes[i] === b)) return "image/png";
1101
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
1102
+ const ascii = (from, n) => String.fromCharCode(...bytes.subarray(from, from + n));
1103
+ if (ascii(0, 4) === "GIF8") return "image/gif";
1104
+ if (ascii(0, 4) === "RIFF" && ascii(8, 4) === "WEBP") return "image/webp";
1105
+ return void 0;
1106
+ }
1107
+ function imageSize(bytes) {
1108
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1109
+ const type = sniffImage(bytes);
1110
+ try {
1111
+ if (type === "image/png") return { width: view.getUint32(16), height: view.getUint32(20) };
1112
+ if (type === "image/gif") return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
1113
+ if (type === "image/jpeg") {
1114
+ let at = 2;
1115
+ while (at + 9 < bytes.length) {
1116
+ if (bytes[at] !== 255) return void 0;
1117
+ const marker = bytes[at + 1];
1118
+ const length = view.getUint16(at + 2);
1119
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
1120
+ return { height: view.getUint16(at + 5), width: view.getUint16(at + 7) };
1121
+ }
1122
+ at += 2 + length;
1123
+ }
1124
+ }
1125
+ } catch {
1126
+ return void 0;
1127
+ }
1128
+ return void 0;
1129
+ }
1130
+ function decodePng(bytes) {
1131
+ if (sniffImage(bytes) !== "image/png") return void 0;
1132
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1133
+ let width = 0;
1134
+ let height = 0;
1135
+ let depth = 0;
1136
+ let colorType = 0;
1137
+ let interlace = 0;
1138
+ let palette;
1139
+ let transparency;
1140
+ const idat = [];
1141
+ let at = 8;
1142
+ while (at + 8 <= bytes.length) {
1143
+ const length = view.getUint32(at);
1144
+ const type = String.fromCharCode(...bytes.subarray(at + 4, at + 8));
1145
+ const data2 = bytes.subarray(at + 8, at + 8 + length);
1146
+ if (type === "IHDR") {
1147
+ width = view.getUint32(at + 8);
1148
+ height = view.getUint32(at + 12);
1149
+ depth = bytes[at + 16];
1150
+ colorType = bytes[at + 17];
1151
+ interlace = bytes[at + 20];
1152
+ } else if (type === "PLTE") palette = data2;
1153
+ else if (type === "tRNS") transparency = data2;
1154
+ else if (type === "IDAT") idat.push(data2);
1155
+ else if (type === "IEND") break;
1156
+ at += 12 + length;
1157
+ }
1158
+ if (width === 0 || height === 0 || interlace !== 0 || width * height > MAX_PREVIEW_PIXELS) return void 0;
1159
+ const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[colorType];
1160
+ if (channels === void 0 || ![1, 2, 4, 8, 16].includes(depth)) return void 0;
1161
+ let raw;
1162
+ try {
1163
+ raw = inflateSync(Buffer.concat(idat));
1164
+ } catch {
1165
+ return void 0;
1166
+ }
1167
+ const bitsPerPixel = channels * depth;
1168
+ const stride = Math.ceil(width * bitsPerPixel / 8);
1169
+ const bpp = Math.max(1, bitsPerPixel >> 3);
1170
+ if (raw.length < height * (stride + 1)) return void 0;
1171
+ const rows = new Uint8Array(height * stride);
1172
+ for (let y = 0; y < height; y += 1) {
1173
+ const filter = raw[y * (stride + 1)];
1174
+ const src = y * (stride + 1) + 1;
1175
+ const out = y * stride;
1176
+ for (let x = 0; x < stride; x += 1) {
1177
+ const a = x >= bpp ? rows[out + x - bpp] : 0;
1178
+ const b = y > 0 ? rows[out - stride + x] : 0;
1179
+ const c2 = x >= bpp && y > 0 ? rows[out - stride + x - bpp] : 0;
1180
+ let v = raw[src + x];
1181
+ if (filter === 1) v += a;
1182
+ else if (filter === 2) v += b;
1183
+ else if (filter === 3) v += a + b >> 1;
1184
+ else if (filter === 4) {
1185
+ const p = a + b - c2;
1186
+ const pa = Math.abs(p - a);
1187
+ const pb = Math.abs(p - b);
1188
+ const pc = Math.abs(p - c2);
1189
+ v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c2;
1190
+ }
1191
+ rows[out + x] = v & 255;
1192
+ }
1193
+ }
1194
+ const sample = (y, index) => {
1195
+ if (depth === 16) return rows[y * stride + index * 2];
1196
+ if (depth === 8) return rows[y * stride + index];
1197
+ const bit = index * depth;
1198
+ const byte = rows[y * stride + (bit >> 3)];
1199
+ const value = byte >> 8 - depth - (bit & 7) & (1 << depth) - 1;
1200
+ return colorType === 3 ? value : Math.round(value * 255 / ((1 << depth) - 1));
1201
+ };
1202
+ const data = new Uint8Array(width * height * 4);
1203
+ for (let y = 0; y < height; y += 1) {
1204
+ for (let x = 0; x < width; x += 1) {
1205
+ const o = (y * width + x) * 4;
1206
+ if (colorType === 3) {
1207
+ const i = sample(y, x);
1208
+ data[o] = palette?.[i * 3] ?? 0;
1209
+ data[o + 1] = palette?.[i * 3 + 1] ?? 0;
1210
+ data[o + 2] = palette?.[i * 3 + 2] ?? 0;
1211
+ data[o + 3] = transparency?.[i] ?? 255;
1212
+ } else if (colorType === 0 || colorType === 4) {
1213
+ const g = sample(y, x * channels);
1214
+ data[o] = data[o + 1] = data[o + 2] = g;
1215
+ data[o + 3] = colorType === 4 ? sample(y, x * channels + 1) : 255;
1216
+ } else {
1217
+ data[o] = sample(y, x * channels);
1218
+ data[o + 1] = sample(y, x * channels + 1);
1219
+ data[o + 2] = sample(y, x * channels + 2);
1220
+ data[o + 3] = colorType === 6 ? sample(y, x * channels + 3) : 255;
1221
+ }
1222
+ }
1223
+ }
1224
+ return { width, height, data };
1225
+ }
1226
+ function decodeJpeg(bytes) {
1227
+ try {
1228
+ const jpeg = createRequire(import.meta.url)("jpeg-js");
1229
+ const img = jpeg.decode(bytes, { useTArray: true, formatAsRGBA: true, maxResolutionInMP: MAX_PREVIEW_PIXELS / 1e6, maxMemoryUsageInMB: 512 });
1230
+ return { width: img.width, height: img.height, data: img.data };
1231
+ } catch {
1232
+ return void 0;
1233
+ }
1234
+ }
1235
+ function decodeImage(bytes) {
1236
+ const type = sniffImage(bytes);
1237
+ if (type === "image/png") return decodePng(bytes);
1238
+ if (type === "image/jpeg") return decodeJpeg(bytes);
1239
+ return void 0;
1240
+ }
1241
+ function boxColor(img, x0, y0, x1, y1, bg) {
1242
+ let r = 0;
1243
+ let g = 0;
1244
+ let b = 0;
1245
+ let n = 0;
1246
+ const xa = Math.floor(x0);
1247
+ const ya = Math.floor(y0);
1248
+ const xb = Math.max(xa + 1, Math.ceil(x1));
1249
+ const yb = Math.max(ya + 1, Math.ceil(y1));
1250
+ for (let y = ya; y < Math.min(yb, img.height); y += 1) {
1251
+ for (let x = xa; x < Math.min(xb, img.width); x += 1) {
1252
+ const o = (y * img.width + x) * 4;
1253
+ const a = img.data[o + 3] / 255;
1254
+ r += img.data[o] * a + bg[0] * (1 - a);
1255
+ g += img.data[o + 1] * a + bg[1] * (1 - a);
1256
+ b += img.data[o + 2] * a + bg[2] * (1 - a);
1257
+ n += 1;
1258
+ }
1259
+ }
1260
+ return n === 0 ? bg : [r / n, g / n, b / n];
1261
+ }
1262
+ function previewSize(width, height, cols, rows) {
1263
+ let w = Math.max(1, Math.min(cols, width));
1264
+ let h = Math.max(1, Math.round(w * height / width / 2));
1265
+ if (h > rows) {
1266
+ h = rows;
1267
+ w = Math.max(1, Math.min(cols, Math.round(rows * 2 * width / height)));
1268
+ }
1269
+ return { cols: w, rows: h };
1270
+ }
1271
+ var esc = (c2, depth, ground) => {
1272
+ const [r, g, b] = c2.map((v) => Math.max(0, Math.min(255, Math.round(v))));
1273
+ if (depth === "truecolor") return `\x1B[${String(ground)};2;${String(r)};${String(g)};${String(b)}m`;
1274
+ const hex = `#${[r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("")}`;
1275
+ return `\x1B[${String(ground)};5;${String(to256(hex, true))}m`;
1276
+ };
1277
+ function halfBlocks(img, cols, rows, depth = colorDepth(), bg = [14, 16, 24]) {
1278
+ if (depth !== "truecolor" && depth !== "256" || !bgEnabled(depth)) return void 0;
1279
+ const size = previewSize(img.width, img.height, cols, rows);
1280
+ const sx = img.width / size.cols;
1281
+ const sy = img.height / (size.rows * 2);
1282
+ const out = [];
1283
+ for (let row = 0; row < size.rows; row += 1) {
1284
+ let line = "";
1285
+ for (let col = 0; col < size.cols; col += 1) {
1286
+ const top = boxColor(img, col * sx, row * 2 * sy, (col + 1) * sx, (row * 2 + 1) * sy, bg);
1287
+ const bottom = boxColor(img, col * sx, (row * 2 + 1) * sy, (col + 1) * sx, (row * 2 + 2) * sy, bg);
1288
+ line += `${esc(top, depth, 38)}${esc(bottom, depth, 48)}\u2580`;
1289
+ }
1290
+ out.push(`${line}\x1B[39m\x1B[49m`);
1291
+ }
1292
+ return out;
1293
+ }
1294
+ function describeImage(bytes) {
1295
+ const type = sniffImage(bytes)?.replace("image/", "").toUpperCase() ?? "image";
1296
+ const size = imageSize(bytes);
1297
+ return size === void 0 ? type : `${type} ${String(size.width)}\xD7${String(size.height)}`;
1298
+ }
1299
+ function previewImageFile(path7, cols, rows) {
1300
+ try {
1301
+ if (statSync(path7).size > MAX_PREVIEW_BYTES) return void 0;
1302
+ const bytes = new Uint8Array(readFileSync3(path7));
1303
+ const description = describeImage(bytes);
1304
+ const img = decodeImage(bytes);
1305
+ const lines = img === void 0 ? void 0 : halfBlocks(img, cols, rows);
1306
+ return lines === void 0 ? { description } : { lines, description };
1307
+ } catch {
1308
+ return void 0;
1309
+ }
1310
+ }
1311
+
1312
+ // src/ui/tool-call-component.ts
1313
+ var IMAGE_TOOLS = /* @__PURE__ */ new Set(["read_image", "view_image"]);
1314
+ var IMAGE_FILE = /\.(png|jpe?g|gif|webp)$/i;
1315
+ var IMAGE_PREVIEW_COLS = 64;
1316
+ var IMAGE_PREVIEW_ROWS = 16;
1317
+ var SUMMARY_KEYS2 = ["command", "cmd", "path", "file_path", "url", "query", "pattern", "name"];
1318
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "multi_edit", "notebook_edit"]);
1319
+ var MAX_OUTPUT_LINES2 = 5;
1320
+ var MAX_PENDING_COMMAND_ROWS = 4;
1321
+ var PREVIEW_ROWS = 10;
1322
+ var WRITTEN_TEXT_KEY = { write: "content", edit: "new_string" };
1323
+ function padCells(text, width) {
1324
+ const w = stringWidth4(text);
1325
+ if (w >= width) return text;
1326
+ return text + " ".repeat(width - w);
1327
+ }
1328
+ function relCwd(p) {
1329
+ if (p === "") return p;
1330
+ try {
1331
+ if (!isAbsolute2(p)) return p;
1332
+ const rel = relative(process.cwd(), p);
1333
+ if (rel === "") return ".";
1334
+ if (rel.startsWith("..") || isAbsolute2(rel)) return p;
1335
+ return rel;
1336
+ } catch {
1337
+ return p;
1338
+ }
1339
+ }
1340
+ var ToolCallComponent = class {
1341
+ constructor(tool, now = Date.now, icons = bruineIcons(), opts = {}) {
1342
+ this.tool = tool;
1343
+ this.now = now;
1344
+ this.icons = icons;
1345
+ this.#startTime = now();
1346
+ this.#readableSummaries = opts.readableSummaries ?? toolSummariesEnabled();
1347
+ }
1348
+ tool;
1349
+ now;
1350
+ icons;
1351
+ #rawArgs = "";
1352
+ #startTime;
1353
+ #done;
1354
+ /** What a successful edit/write changed, shown instead of "file updated". */
1355
+ #diff;
1356
+ /** The image file a successful image read showed the model. */
1357
+ #image;
1358
+ #readableSummaries;
1359
+ get active() {
1360
+ return this.#done === void 0;
1361
+ }
1362
+ /** T55 P1c: a tool still in flight owns the rail, so the live edge is findable. */
1363
+ get rail() {
1364
+ return this.#done === void 0 ? "active" : this.#done.ok ? "blue" : "red";
1365
+ }
1366
+ /**
1367
+ * A call that shows a diff asks for a NEUTRAL card.
1368
+ *
1369
+ * The transcript tints a finished call green, which was the right call for a tool
1370
+ * that just says it worked. It was the wrong one here: the diff paints its own
1371
+ * green band for an added line and its own red band for a removed one, so a green
1372
+ * card left the addition with nothing of its own while the deletion shouted. Two
1373
+ * bands of equal weight on a neutral block is a diff that can be read.
1374
+ */
1375
+ get diffCard() {
1376
+ return this.#diff !== void 0 && this.#diff.lines.length > 0;
1377
+ }
1378
+ get doneOk() {
1379
+ return this.#done?.ok;
1380
+ }
1381
+ get seconds() {
1382
+ return this.#done?.seconds;
1383
+ }
1384
+ args(delta) {
1385
+ this.#rawArgs += delta;
1386
+ this.#settled = void 0;
1387
+ }
1388
+ /** The durable event replaces streamed JSON, it must never be appended twice. */
1389
+ setArgs(json2) {
1390
+ this.#rawArgs = json2;
1391
+ this.#settled = void 0;
1392
+ }
1393
+ result(ok, output) {
1394
+ const lines = sanitize(output).split("\n").filter((l) => !/^\s*(<(path|type)>.*<\/(path|type)>|<\/?content>)\s*$/.test(l)).filter((l) => !/^\s*\(End of file - total \d+ lines?\)\s*$/.test(l));
1395
+ while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
1396
+ this.#diff = ok && WRITE_TOOLS.has(this.tool) ? diffForCall(this.tool, this.#rawArgs) : void 0;
1397
+ const path7 = this.#arg("path") ?? this.#arg("file_path");
1398
+ this.#image = ok && path7 !== void 0 && (IMAGE_TOOLS.has(this.tool) || this.tool === "read" && IMAGE_FILE.test(path7)) ? resolve2(process.cwd(), path7) : void 0;
1399
+ if (lines.at(-1) === "") lines.pop();
1400
+ this.#settled = void 0;
1401
+ this.#done = { ok, seconds: (this.now() - this.#startTime) / 1e3, lines: lines.slice(0, MAX_OUTPUT_LINES2), rest: Math.max(0, lines.length - MAX_OUTPUT_LINES2) };
1402
+ }
1403
+ cancel() {
1404
+ if (this.active) this.result(false, "Cancelled");
1405
+ }
1406
+ /**
1407
+ * `link` is only for a call that has settled (T55 P2): a streaming line is
1408
+ * replaced a frame later, so a link on it would be work thrown away.
1409
+ */
1410
+ /**
1411
+ * One argument out of the streamed JSON, or undefined.
1412
+ *
1413
+ * The args arrive as deltas and are only valid JSON once the tool is done, so
1414
+ * both the summary line and the turn's file list read them through here rather
1415
+ * than each parsing the same string a second way.
1416
+ */
1417
+ #arg(key) {
1418
+ try {
1419
+ const parsed = JSON.parse(this.#rawArgs);
1420
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
1421
+ const value = parsed[key];
1422
+ return typeof value === "string" && value !== "" ? value : void 0;
1423
+ } catch {
1424
+ return void 0;
1425
+ }
1426
+ }
1427
+ /**
1428
+ * T59: the file this call named, if it named one.
1429
+ *
1430
+ * Only the tools that write are asked: a `read` names a path too, and printing
1431
+ * it as a change would be a lie the workspace can contradict.
1432
+ */
1433
+ touchedPath() {
1434
+ if (!WRITE_TOOLS.has(this.tool)) return void 0;
1435
+ return this.#arg("file_path") ?? this.#arg("path") ?? this.#arg("notebook_path");
1436
+ }
1437
+ /**
1438
+ * What the arguments say so far. The args arrive as deltas and are only valid
1439
+ * JSON at the end, so the line is read out of the partial text: a command is seen
1440
+ * being written, word by word, instead of a bare ellipsis until it is whole.
1441
+ */
1442
+ #partialArgs() {
1443
+ if (this.#parsedAt !== this.#rawArgs.length) {
1444
+ const parsed = parsePartialJson(this.#rawArgs);
1445
+ this.#parsed = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1446
+ this.#parsedAt = this.#rawArgs.length;
1447
+ }
1448
+ return this.#parsed;
1449
+ }
1450
+ #parsed;
1451
+ #parsedAt = -1;
1452
+ /**
1453
+ * The text of a file being written, as it arrives: its first lines are shown, numbered,
1454
+ * and after them only a count of lines and bytes that keeps moving. Watching a hundred
1455
+ * lines scroll by is noise; watching a number climb is proof that it is working.
1456
+ */
1457
+ #writtenPreview(width) {
1458
+ const key = WRITTEN_TEXT_KEY[this.tool];
1459
+ const text = key === void 0 ? void 0 : this.#partialArgs()?.[key];
1460
+ if (typeof text !== "string" || text === "") return [];
1461
+ const ascii = this.icons.think === "*";
1462
+ const ellipsis = ascii ? "..." : "\u2026";
1463
+ const rows = text.split("\n");
1464
+ if (rows.at(-1) === "") rows.pop();
1465
+ const count = rows.length;
1466
+ const bar = ascii ? "|" : "\u2502";
1467
+ const out = rows.slice(0, PREVIEW_ROWS).map((row, i) => {
1468
+ const gutter = `${String(i + 1).padStart(4)} ${bar} `;
1469
+ return ansi.gray(gutter) + ansi.gray(clipCells(sanitize(row).replace(/\t/g, " "), Math.max(1, width - gutter.length), ellipsis));
1470
+ });
1471
+ if (count > PREVIEW_ROWS) {
1472
+ const bytes = Buffer.byteLength(text, "utf8");
1473
+ const size = bytes < 1024 ? `${String(bytes)} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
1474
+ out.push(ansi.gray(clipCells(` ${ellipsis} ${String(count)} lines ${ascii ? "-" : "\xB7"} ${size}`, width, ellipsis)));
1475
+ }
1476
+ return out;
1477
+ }
1478
+ summary(width = 60, link = false) {
1479
+ if (!this.#rawArgs) return "";
1480
+ const ellipsis = this.icons.think === "*" ? "..." : "\u2026";
1481
+ const obj = this.#partialArgs();
1482
+ if (obj === void 0) return ellipsis;
1483
+ const key = SUMMARY_KEYS2.find((k) => typeof obj[k] === "string" && obj[k] !== "");
1484
+ let text = key ? String(obj[key]) : Object.keys(obj).length > 0 ? JSON.stringify(obj) : "";
1485
+ if (text === "") return ellipsis;
1486
+ if (key === "path" || key === "file_path") text = relCwd(text);
1487
+ const shown = clipCells(sanitize(text).replace(/\n/g, " "), Math.max(1, width), ellipsis);
1488
+ return link && (key === "path" || key === "file_path") ? fileLink(shown, String(obj[key])) : shown;
1489
+ }
1490
+ /** The chat can show a description; approval still reads the actual command/path. */
1491
+ #displaySummary(width, link = false) {
1492
+ const text = this.#readableSummaries ? readableToolSummary(this.tool, this.#partialArgs()) : void 0;
1493
+ if (text === void 0) return this.summary(width, link);
1494
+ return clipCells(sanitize(text).replace(/\s+/g, " "), Math.max(1, width), this.icons.think === "*" ? "..." : "\u2026");
1495
+ }
1496
+ /** A command being written keeps its later lines visible while the call is in flight. */
1497
+ #commandTail(width) {
1498
+ const obj = this.#partialArgs();
1499
+ const command = obj && (typeof obj.command === "string" ? obj.command : typeof obj.cmd === "string" ? obj.cmd : void 0);
1500
+ if (command === void 0) return [];
1501
+ const rows = sanitize(command).split("\n");
1502
+ if (rows.length < 2) return [];
1503
+ const shown = rows.slice(1, 1 + MAX_PENDING_COMMAND_ROWS);
1504
+ const hidden = rows.length - 1 - shown.length;
1505
+ const ellipsis = this.icons.think === "*" ? "..." : "\u2026";
1506
+ return [
1507
+ ...shown.map((row) => ansi.gray(clipCells(` ${row}`, width, ellipsis))),
1508
+ ...hidden > 0 ? [ansi.gray(clipCells(` ${ellipsis} ${String(hidden)} more lines`, width))] : []
1509
+ ];
1510
+ }
1511
+ /** A settled call at one width in one set of colours is the same lines every time it is asked for. */
1512
+ #settled;
1513
+ render(width) {
1514
+ if (this.#done === void 0) return this.#draw(width);
1515
+ const ink = `${ansi.gray("x")}${ansi.green("x")}${ansi.red("x")}${ansi.text("x")}`;
1516
+ if (this.#settled?.width !== width || this.#settled.ink !== ink) this.#settled = { width, ink, lines: this.#draw(width) };
1517
+ return this.#settled.lines;
1518
+ }
1519
+ #draw(width) {
1520
+ const toolPad = padCells(this.tool, 7);
1521
+ if (!this.#done) {
1522
+ const fr = spinnerFrame(this.now() - this.#startTime, this.icons);
1523
+ const room = Math.max(1, width - stringWidth4(`${fr} ${toolPad} `));
1524
+ const summary = this.#displaySummary(room);
1525
+ const detail = summary ? ` ${summary}` : "";
1526
+ const plainLine = clipCells(`${fr} ${toolPad}${detail}`, width);
1527
+ const head2 = stringWidth4(plainLine) === stringWidth4(`${fr} ${toolPad}${detail}`) ? `${ansi.cyan(fr)} ${ansi.text(toolPad)}${ansi.gray(detail)}` : plainLine;
1528
+ return [head2, ...this.#commandTail(width), ...this.#writtenPreview(width)];
1529
+ }
1530
+ const mark = this.#done.ok ? ansi.green(this.icons.ok) : ansi.red(this.icons.fail);
1531
+ const dur = formatDuration(this.#done.seconds) ?? (this.#done.ok ? "0.0s" : void 0);
1532
+ const counter = this.#diff === void 0 ? "" : ` ${diffCounter(this.#diff)}`;
1533
+ const durCells = this.#diff === void 0 ? 0 : 2 + `+${String(this.#diff.added)} -${String(this.#diff.removed)}`.length;
1534
+ const prefixCells = 1 + 1 + stringWidth4(toolPad) + 2;
1535
+ const avail = Math.max(0, width - prefixCells - durCells);
1536
+ const budget = Math.min(60, avail);
1537
+ const durPart = counter;
1538
+ const plainSummary = this.#displaySummary(budget);
1539
+ const linkedPadded = padCells(this.#displaySummary(budget, true), avail);
1540
+ const plainPadded = padCells(plainSummary, avail);
1541
+ const plainHead = ` ${toolPad} ${plainPadded}${durPart}`;
1542
+ const fits = stringWidth4(plainHead) <= Math.max(0, width - 1);
1543
+ const head = width <= 0 ? "" : fits ? `${mark} ${ansi.text(toolPad)} ${ansi.gray(linkedPadded)}${durPart}` : mark + clipCells(plainHead, Math.max(0, width - 1));
1544
+ const out = [head];
1545
+ if (this.#diff !== void 0 && this.#diff.lines.length > 0) {
1546
+ out.push(...renderDiff(this.#diff, Math.max(1, width - 2), this.icons.think === "*").map((l) => ` ${l}`));
1547
+ if (dur !== void 0) out.push(ansi.gray(clipCells(`Took ${dur}`, width)));
1548
+ return out;
1549
+ }
1550
+ if (this.#image !== void 0) {
1551
+ const preview = previewImageFile(this.#image, Math.min(IMAGE_PREVIEW_COLS, Math.max(1, width - 6)), IMAGE_PREVIEW_ROWS);
1552
+ if (preview !== void 0) {
1553
+ if (preview.lines !== void 0) out.push(...preview.lines.map((l) => ` ${l}`));
1554
+ out.push(ansi.gray(clipCells(` ${preview.description}${dur === void 0 ? "" : ` ${this.icons.think === "*" ? "-" : "\xB7"} ${dur}`}`, width)));
1555
+ return out;
1556
+ }
1557
+ }
1558
+ const branch = this.icons.think === "*" ? ">" : "\u23BF";
1559
+ this.#done.lines.forEach((line, i) => out.push(ansi.gray(clipCells(`${i === 0 ? ` ${branch} ` : " "}${line}`, width))));
1560
+ if (this.#done.rest > 0) out.push(ansi.gray(clipCells(` ${this.icons.think === "*" ? "..." : "\u2026"} ${this.#done.rest} more lines`, width)));
1561
+ if (dur !== void 0) out.push(ansi.gray(clipCells(`Took ${dur}`, width)));
1562
+ return out;
1563
+ }
1564
+ invalidate() {
1565
+ }
1566
+ };
1567
+
1568
+ // src/ui/logo-motion.ts
1569
+ var LOGO = [
1570
+ "\u250F\u2513 \u250F\u2501\u2513\u257B \u257B\u257B\u250F\u2513\u257B\u250F\u2501\u2578",
1571
+ "\u2523\u253B\u2513\u2523\u2533\u251B\u2503 \u2503\u2503\u2503\u2517\u252B\u2523\u2578 ",
1572
+ "\u2517\u2501\u251B\u2579\u2517\u2578\u2517\u2501\u251B\u2579\u2579 \u2579\u2517\u2501\u2578"
1573
+ ];
1574
+ function terminalMotionAllowed(opts = {}) {
1575
+ const env = opts.env ?? process.env;
1576
+ const tty = opts.stdoutTTY ?? process.stdout.isTTY === true;
1577
+ return tty && opts.ascii !== true && appEnv("ASCII", env) !== "1" && env.CI !== "1" && appEnv("NO_ANIMATION", env) !== "1" && env.TERM !== "dumb";
1578
+ }
1579
+
1580
+ // src/ui/typewriter.ts
1581
+ var REVEAL_INTERVAL_MS = 40;
1582
+ var REVEAL_CATCH_UP = 0.18;
1583
+ var REVEAL_FLOOR = 1;
1584
+ var DELIMITER_WINDOW = 48;
1585
+ var MARKER_HOLD_TICKS = 4;
1586
+ var CLUSTER_LOOKAHEAD = 8;
1587
+ var SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1588
+ var DELIMITERS = ["```", "**", "__", "~~", "*", "_", "`", "[", "("];
1589
+ function graphemeCut(text, from, limit) {
1590
+ if (limit >= text.length) return text.length;
1591
+ const slice = text.slice(from, limit + CLUSTER_LOOKAHEAD);
1592
+ let cut = from;
1593
+ for (const segment of SEGMENTER.segment(slice)) {
1594
+ const end = from + segment.index + segment.segment.length;
1595
+ if (end > limit) return Math.min(end, text.length);
1596
+ cut = end;
1597
+ }
1598
+ return cut > from ? cut : Math.min(text.length, from + 1);
1599
+ }
1600
+ function countUnescaped(head, needle) {
1601
+ let count = 0;
1602
+ for (let at = head.indexOf(needle); at !== -1; at = head.indexOf(needle, at + needle.length)) {
1603
+ if (head.at(at - 1) === "\\") continue;
1604
+ count += 1;
1605
+ }
1606
+ return count;
1607
+ }
1608
+ var Typewriter = class {
1609
+ #buffer = "";
1610
+ /** Code units of the buffer already on screen. Always a grapheme boundary. */
1611
+ #shown = 0;
1612
+ #timer;
1613
+ /** The marker the reveal is waiting on, and how many ticks it has cost. */
1614
+ #heldAt;
1615
+ #heldTicks = 0;
1616
+ #onTick;
1617
+ #intervalMs;
1618
+ #catchUp;
1619
+ #floor;
1620
+ #setTimer;
1621
+ #clearTimer;
1622
+ constructor(opts = {}) {
1623
+ this.#onTick = opts.onTick;
1624
+ this.#intervalMs = REVEAL_INTERVAL_MS;
1625
+ this.#catchUp = REVEAL_CATCH_UP;
1626
+ this.#floor = REVEAL_FLOOR;
1627
+ this.#setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1628
+ this.#clearTimer = opts.clearTimer ?? ((handle) => clearTimeout(handle));
1629
+ }
1630
+ /** True when text is being held back, so a repaint is worth asking for. */
1631
+ get animating() {
1632
+ return this.#shown < this.#buffer.length;
1633
+ }
1634
+ /** How much is waiting. */
1635
+ get pending() {
1636
+ return this.#buffer.length - this.#shown;
1637
+ }
1638
+ /** What to paint: the whole answer when nothing is held back. */
1639
+ get text() {
1640
+ return this.#buffer.slice(0, this.#shown);
1641
+ }
1642
+ push(delta) {
1643
+ if (delta === "") return;
1644
+ this.#buffer += delta;
1645
+ if (this.#onTick === void 0) {
1646
+ this.#shown = this.#buffer.length;
1647
+ return;
1648
+ }
1649
+ this.#arm();
1650
+ }
1651
+ /** One step. Exposed so a test can drive time instead of waiting for it. */
1652
+ tick() {
1653
+ if (!this.animating) {
1654
+ this.#disarm();
1655
+ return;
1656
+ }
1657
+ const step = Math.max(this.#floor, Math.ceil(this.pending * this.#catchUp));
1658
+ const limit = Math.min(this.#buffer.length, this.#shown + step);
1659
+ const wait = this.#holdAt(limit);
1660
+ const next = wait ?? graphemeCut(this.#buffer, this.#shown, limit);
1661
+ if (next <= this.#shown) {
1662
+ this.#disarm();
1663
+ return;
1664
+ }
1665
+ this.#shown = next;
1666
+ this.#onTick?.();
1667
+ if (this.animating) this.#arm();
1668
+ else this.#disarm();
1669
+ }
1670
+ /** Everything, now. Called on finish, on abort and before a re-render. */
1671
+ flush() {
1672
+ this.#shown = this.#buffer.length;
1673
+ this.#heldAt = void 0;
1674
+ this.#heldTicks = 0;
1675
+ this.#disarm();
1676
+ }
1677
+ /**
1678
+ * Throw the buffer away and reveal `text` from the start.
1679
+ *
1680
+ * For the case `push` cannot serve: what is on screen is not a prefix of what
1681
+ * arrived, so the text changed shape rather than grew. `flush` is not it —
1682
+ * flushing shows what is already buffered, and the buffer still holds the old
1683
+ * text, which would then be painted twice.
1684
+ */
1685
+ reset(text) {
1686
+ this.#buffer = text;
1687
+ this.#heldAt = void 0;
1688
+ this.#heldTicks = 0;
1689
+ this.#shown = this.#onTick === void 0 ? text.length : 0;
1690
+ if (this.animating) this.#arm();
1691
+ else this.#disarm();
1692
+ }
1693
+ dispose() {
1694
+ this.#disarm();
1695
+ this.#onTick = void 0;
1696
+ }
1697
+ /**
1698
+ * Where to wait instead of advancing, or undefined to go ahead.
1699
+ *
1700
+ * The pairing is judged on the revealed prefix, because the prefix is what
1701
+ * the reader sees: "**b" whose pair is two characters further along the buffer
1702
+ * still paints two literal asterisks.
1703
+ */
1704
+ #holdAt(limit) {
1705
+ const cut = graphemeCut(this.#buffer, this.#shown, limit);
1706
+ const head = this.#buffer.slice(0, cut);
1707
+ let waiting;
1708
+ for (const needle of DELIMITERS) {
1709
+ if (countUnescaped(head, needle) % 2 !== 1) continue;
1710
+ const at = head.lastIndexOf(needle);
1711
+ if (at < this.#shown) continue;
1712
+ if (cut - at > DELIMITER_WINDOW) continue;
1713
+ if (this.#heldAt === at) {
1714
+ if (this.#heldTicks < MARKER_HOLD_TICKS) {
1715
+ this.#heldTicks += 1;
1716
+ waiting = at;
1717
+ }
1718
+ } else {
1719
+ this.#heldAt = at;
1720
+ this.#heldTicks = 0;
1721
+ waiting = at;
1722
+ }
1723
+ break;
1724
+ }
1725
+ if (waiting === void 0) {
1726
+ this.#heldAt = void 0;
1727
+ this.#heldTicks = 0;
1728
+ }
1729
+ return waiting;
1730
+ }
1731
+ #arm() {
1732
+ if (this.#timer !== void 0 || this.#onTick === void 0) return;
1733
+ this.#timer = this.#setTimer(() => {
1734
+ this.#timer = void 0;
1735
+ this.tick();
1736
+ }, this.#intervalMs);
1737
+ this.#timer.unref?.();
1738
+ }
1739
+ #disarm() {
1740
+ if (this.#timer === void 0) return;
1741
+ this.#clearTimer(this.#timer);
1742
+ this.#timer = void 0;
1743
+ }
1744
+ };
1745
+ function revealAllowed(icons, env = process.env) {
1746
+ return terminalMotionAllowed({ ascii: icons.think === "*", env });
1747
+ }
1748
+
1749
+ // src/ui/question-call-component.ts
1750
+ var QuestionCallComponent = class {
1751
+ constructor(now = Date.now, icons = bruineIcons(), opts = {}) {
1752
+ this.now = now;
1753
+ this.icons = icons;
1754
+ this.#start = now();
1755
+ const animate = opts.animate ?? revealAllowed(icons);
1756
+ this.#onTick = animate && opts.onTick !== void 0 ? opts.onTick : void 0;
1757
+ this.#typewriter = opts.typewriter ?? {};
1758
+ }
1759
+ now;
1760
+ icons;
1761
+ tool = "ask_user_question";
1762
+ #raw = "";
1763
+ #start;
1764
+ #answers;
1765
+ #ended;
1766
+ #tws = [];
1767
+ /** What each reveal was last handed: a live line only ever grows. */
1768
+ #fed = [];
1769
+ #onTick;
1770
+ #typewriter;
1771
+ get active() {
1772
+ return this.#ended === void 0;
1773
+ }
1774
+ get rail() {
1775
+ return this.#ended === void 0 ? "active" : this.#ended.ok ? "blue" : "red";
1776
+ }
1777
+ get doneOk() {
1778
+ return this.#ended?.ok;
1779
+ }
1780
+ get seconds() {
1781
+ return this.#ended === void 0 ? void 0 : (this.now() - this.#start) / 1e3;
1782
+ }
1783
+ args(delta) {
1784
+ this.#raw += delta;
1785
+ this.#reveal();
1786
+ }
1787
+ /** The durable event replaces streamed JSON, it must never be appended twice. */
1788
+ setArgs(json2) {
1789
+ this.#raw = json2;
1790
+ this.#reveal();
1791
+ }
1792
+ touchedPath() {
1793
+ return void 0;
1794
+ }
1795
+ summary(width = 60) {
1796
+ const first = this.#questions()[0]?.question ?? "";
1797
+ return clipCells(sanitize(first).replace(/\n/g, " "), Math.max(1, width), this.icons.think === "*" ? "..." : "\u2026");
1798
+ }
1799
+ /** What the user picked in the form shown for these questions. */
1800
+ answer(answers) {
1801
+ if (this.#ended !== void 0) return;
1802
+ this.#settle();
1803
+ this.#answers = answers;
1804
+ this.#ended = { ok: true };
1805
+ }
1806
+ /** The tool's own result: it only settles a call nobody answered through the form. */
1807
+ result(ok, output) {
1808
+ if (this.#ended !== void 0) return;
1809
+ this.#settle();
1810
+ this.#ended = ok ? { ok } : { ok, note: sanitize(output).split("\n")[0] ?? "" };
1811
+ }
1812
+ cancel() {
1813
+ if (this.#ended === void 0) {
1814
+ this.#settle();
1815
+ this.#ended = { ok: false, note: "Cancelled" };
1816
+ }
1817
+ }
1818
+ /** Nothing may stay hidden once the call is over: a record that reads short is a lie. */
1819
+ #settle() {
1820
+ for (const tw of this.#tws) tw.flush();
1821
+ }
1822
+ /**
1823
+ * Hand every line's reveal what the partial JSON says now, and nothing twice.
1824
+ *
1825
+ * One reveal per line, because a line is what grows: the question text and the
1826
+ * option summary both arrive a piece at a time, and each one only ever extends
1827
+ * what is already on screen. A shared buffer would restart the question from its
1828
+ * first word every time the option count changed, which reads as a glitch.
1829
+ */
1830
+ #reveal() {
1831
+ const lines = this.#liveLines();
1832
+ while (this.#tws.length < lines.length) {
1833
+ this.#tws.push(new Typewriter({ ...this.#typewriter, onTick: this.#onTick }));
1834
+ this.#fed.push("");
1835
+ }
1836
+ lines.forEach((line, i) => {
1837
+ const tw = this.#tws[i];
1838
+ const fed = this.#fed[i] ?? "";
1839
+ if (line === fed) return;
1840
+ if (line.startsWith(fed)) tw.push(line.slice(fed.length));
1841
+ else tw.reset(line);
1842
+ this.#fed[i] = line;
1843
+ });
1844
+ this.#tws.length = lines.length;
1845
+ this.#fed.length = lines.length;
1846
+ if (this.#complete()) this.#settle();
1847
+ }
1848
+ /** The questions the arguments describe so far; whatever has not arrived is absent. */
1849
+ #questions() {
1850
+ const parsed = parsePartialJson(this.#raw);
1851
+ const list = parsed?.questions;
1852
+ if (!Array.isArray(list)) return [];
1853
+ return list.flatMap((item, index) => {
1854
+ if (item === null || typeof item !== "object") return [];
1855
+ const q = item;
1856
+ const options = Array.isArray(q.options) ? q.options.flatMap((o) => {
1857
+ const opt = o;
1858
+ if (opt === null || typeof opt !== "object" || typeof opt.label !== "string") return [];
1859
+ return [typeof opt.description === "string" ? { label: opt.label, description: opt.description } : { label: opt.label }];
1860
+ }) : [];
1861
+ const header = typeof q.header === "string" && q.header.trim() !== "" ? q.header : void 0;
1862
+ return [{
1863
+ id: typeof q.id === "string" ? q.id : String(index),
1864
+ question: typeof q.question === "string" ? q.question : "",
1865
+ ...header !== void 0 ? { header } : {},
1866
+ ...options.length > 0 ? { options } : {},
1867
+ ...q.multiSelect === true ? { multiSelect: true } : {}
1868
+ }];
1869
+ });
1870
+ }
1871
+ /**
1872
+ * The live call, one line at a time: the question (or its header, while the
1873
+ * question is still arriving), then the options it offers.
1874
+ *
1875
+ * The line count is the layout's own: `render` walks the same questions in the
1876
+ * same order, so a line sits at the same index on both sides even mid-stream.
1877
+ */
1878
+ #liveLines() {
1879
+ const lines = [];
1880
+ for (const q of this.#questions()) {
1881
+ const text = q.question.trim();
1882
+ lines.push(text === "" ? q.header ?? "" : text);
1883
+ const options = q.options ?? [];
1884
+ if (options.length > 0) lines.push(` ${String(options.length)} option(s): ${options.map((o) => o.label).join(", ")}`);
1885
+ }
1886
+ return lines;
1887
+ }
1888
+ #complete() {
1889
+ try {
1890
+ JSON.parse(this.#raw);
1891
+ return true;
1892
+ } catch {
1893
+ return false;
1894
+ }
1895
+ }
1896
+ render(width) {
1897
+ const ascii = this.icons.think === "*";
1898
+ const ellipsis = ascii ? "..." : "\u2026";
1899
+ const fit = (text, cells = width) => clipCells(sanitize(text).replace(/\n/g, " "), Math.max(1, cells), ellipsis);
1900
+ const questions = this.#questions();
1901
+ const lead = this.#ended === void 0 ? ansi.cyan(spinnerFrame(this.now() - this.#start, this.icons)) : this.#ended.ok ? ansi.green(this.icons.ok) : ansi.red(this.icons.fail);
1902
+ const label = "ask_user";
1903
+ const headCells = 1 + 1 + label.length + 2;
1904
+ if (questions.length === 0) return [`${lead} ${ansi.text(label)} ${ansi.gray(ellipsis)}`];
1905
+ const revealed = (i) => this.#tws[i]?.text ?? "";
1906
+ const bullets = ascii ? { on: "(*)", off: "( )" } : { on: "\u25CF", off: "\u25CB" };
1907
+ const lines = [];
1908
+ let at = 0;
1909
+ questions.forEach((q, i) => {
1910
+ const prefix = i === 0 ? `${lead} ${ansi.text(label)} ` : " ".repeat(headCells);
1911
+ const asked = revealed(at++);
1912
+ lines.push(`${prefix}${ansi.gray(fit(asked === "" ? ellipsis : asked, width - headCells))}`);
1913
+ const options = q.options ?? [];
1914
+ if (options.length > 0) {
1915
+ const summary = revealed(at++);
1916
+ lines.push(ansi.gray(fit(summary === "" ? ellipsis : summary)));
1917
+ }
1918
+ const answer = this.#answers?.find((a) => a.id === q.id) ?? this.#answers?.[i];
1919
+ if (this.#ended !== void 0 && this.#answers !== void 0) {
1920
+ const text = answer === void 0 ? "" : [...answer.selected, ...answer.custom !== void 0 && answer.custom !== "" ? [answer.custom] : []].join(", ");
1921
+ lines.push(text === "" ? ansi.gray("skipped") : ansi.violet(fit(`${this.icons.ok} ${text}`)));
1922
+ lines.push(ansi.gray(fit(`Q: ${q.question}`)));
1923
+ if (options.length > 0) {
1924
+ lines.push(ansi.gray("Options:"));
1925
+ for (const o of options) {
1926
+ const chosen = answer?.selected.includes(o.label) === true;
1927
+ const rest = fit(`${o.label}${o.description !== void 0 ? ` - ${o.description}` : ""}`, width - 6);
1928
+ lines.push(` ${chosen ? ansi.green(bullets.on) : ansi.faint(bullets.off)} ${chosen ? ansi.text(rest) : ansi.gray(rest)}`);
1929
+ }
1930
+ }
1931
+ }
1932
+ });
1933
+ if (this.#ended === void 0 && this.#complete()) {
1934
+ lines.push(ansi.gray(fit(ascii ? "Waiting for user input..." : "Waiting for user input\u2026")));
1935
+ } else if (this.#ended?.ok === false && this.#ended.note !== void 0) {
1936
+ lines.push(ansi.red(fit(this.#ended.note)));
1937
+ }
1938
+ return lines;
1939
+ }
1940
+ invalidate() {
1941
+ }
1942
+ };
1943
+
1944
+ // src/ui/assistant-text.ts
1945
+ import { Markdown, Text } from "@earendil-works/pi-tui";
1946
+
1947
+ // src/render/markdown.ts
1948
+ import stringWidth5 from "string-width";
1949
+ var READ_WIDTH = 110;
1950
+ var FENCE = /^ {0,3}(`{3,}|~{3,})/;
1951
+ var HEADING = /^ {0,3}#{1,6}\s/;
1952
+ var LIST_ITEM = /^(\s*)([-*+]|\d{1,9}[.)])(\s+)(.*)$/;
1953
+ var QUOTE = /^(\s*)(>\s?)/;
1954
+ var TABLE_RULE = /^ {0,3}\|?[\s:|-]*-[\s:|-]*\|/;
1955
+ var SETEXT = /^ {0,3}(?:=+|-+)\s*$/;
1956
+ var INDENTED = /^(?: {4}|\t)/;
1957
+ var OPENS_BLOCK = /^[-*+>#|]|\d{1,9}[.)]/;
1958
+ var UNBREAKABLE = /[[<]|:\/\//;
1959
+ var plain = (line) => line.replace(/\x1b\[[0-9;]*m/g, "");
1960
+ var blank = (line) => stringWidth5(plain(line).trim()) === 0;
1961
+ function shapeAnswer(source, availableWidth) {
1962
+ const lines = source.split("\n");
1963
+ const width = Math.max(1, Math.min(availableWidth, READ_WIDTH));
1964
+ const cap = width < availableWidth;
1965
+ const out = [];
1966
+ let fence;
1967
+ let table = false;
1968
+ let words = [];
1969
+ let kind = "";
1970
+ let first = "";
1971
+ let next = "";
1972
+ let verbatim = false;
1973
+ const flush = () => {
1974
+ if (words.length > 0) {
1975
+ out.push(...cap && !verbatim ? fill(words, first, next, width) : [`${first}${words.join(" ")}`]);
1976
+ words = [];
1977
+ }
1978
+ kind = "";
1979
+ verbatim = false;
1980
+ };
1981
+ for (let i = 0; i < lines.length; i++) {
1982
+ const line = lines[i];
1983
+ if (fence !== void 0) {
1984
+ out.push(line);
1985
+ const close = /^ {0,3}(`{3,}|~{3,})\s*$/.exec(line)?.[1];
1986
+ if (close !== void 0 && close[0] === fence.char && close.length >= fence.len) fence = void 0;
1987
+ continue;
1988
+ }
1989
+ const open = FENCE.exec(line)?.[1];
1990
+ if (open !== void 0) {
1991
+ flush();
1992
+ fence = { char: open[0], len: open.length };
1993
+ table = false;
1994
+ out.push(line);
1995
+ continue;
1996
+ }
1997
+ if (blank(line)) {
1998
+ flush();
1999
+ table = false;
2000
+ out.push(line);
2001
+ continue;
2002
+ }
2003
+ if (line.includes("|") && (table || TABLE_RULE.test(lines[i + 1] ?? ""))) {
2004
+ flush();
2005
+ table = true;
2006
+ out.push(line);
2007
+ continue;
2008
+ }
2009
+ table = false;
2010
+ if (INDENTED.test(line)) {
2011
+ flush();
2012
+ out.push(line);
2013
+ continue;
2014
+ }
2015
+ if (words.length > 0 && SETEXT.test(line)) {
2016
+ flush();
2017
+ out.push(line);
2018
+ continue;
2019
+ }
2020
+ if (HEADING.test(line)) {
2021
+ flush();
2022
+ if (out.length > 0 && !blank(out[out.length - 1])) out.push("");
2023
+ out.push(...cap && !UNBREAKABLE.test(line) ? fill(line.trim().split(/\s+/), "", "", width) : [line]);
2024
+ continue;
2025
+ }
2026
+ const quote = QUOTE.exec(line);
2027
+ const item = LIST_ITEM.exec(line);
2028
+ if (item !== null) {
2029
+ flush();
2030
+ kind = "list";
2031
+ const [, indent = "", marker = "", gap = ""] = item;
2032
+ first = `${indent}${marker}${gap}`;
2033
+ next = `${indent}${" ".repeat(marker.length + gap.length)}`;
2034
+ } else if (quote !== null) {
2035
+ if (kind !== "quote") {
2036
+ flush();
2037
+ kind = "quote";
2038
+ first = quote[0];
2039
+ }
2040
+ next = quote[0];
2041
+ } else if (kind === "") {
2042
+ kind = "para";
2043
+ first = next = "";
2044
+ }
2045
+ if (UNBREAKABLE.test(line)) verbatim = true;
2046
+ if (verbatim) {
2047
+ out.push(line);
2048
+ continue;
2049
+ }
2050
+ const body = item !== null ? item[4] : quote !== null ? line.slice(quote[0].length) : line;
2051
+ words.push(...body.trim().split(/\s+/).filter((word) => word !== ""));
2052
+ }
2053
+ flush();
2054
+ return out.join("\n");
2055
+ }
2056
+ function fill(words, first, next, width) {
2057
+ const tokens = [];
2058
+ for (const [i, word] of words.entries()) {
2059
+ if (i > 0 && OPENS_BLOCK.test(word)) tokens[tokens.length - 1] = `${tokens[tokens.length - 1]} ${word}`;
2060
+ else tokens.push(word);
2061
+ }
2062
+ const out = [];
2063
+ let start = 0;
2064
+ while (start < tokens.length) {
2065
+ const prefix = start === 0 ? first : next;
2066
+ let cells = stringWidth5(prefix);
2067
+ let end = start;
2068
+ while (end < tokens.length) {
2069
+ const add = stringWidth5(tokens[end]) + (end === start ? 0 : 1);
2070
+ if (cells + add > width && end > start) break;
2071
+ cells += add;
2072
+ end += 1;
2073
+ }
2074
+ out.push(`${prefix}${tokens.slice(start, end).join(" ")}`);
2075
+ start = end;
2076
+ }
2077
+ return out;
2078
+ }
2079
+ function oneBlankBetween(lines) {
2080
+ const out = [];
2081
+ let previousBlank = false;
2082
+ for (const line of lines) {
2083
+ const empty = blank(line);
2084
+ if (empty && previousBlank) continue;
2085
+ out.push(line);
2086
+ previousBlank = empty;
2087
+ }
2088
+ return out;
2089
+ }
2090
+
2091
+ // src/ui/chat-layout.ts
2092
+ import {
2093
+ Container,
2094
+ Editor,
2095
+ sliceByColumn,
2096
+ truncateToWidth,
2097
+ visibleWidth as visibleWidth3
2098
+ } from "@earendil-works/pi-tui";
2099
+ import stringWidth6 from "string-width";
2100
+
2101
+ // src/ui/assistant-text.ts
2102
+ var AssistantTextComponent = class {
2103
+ #md;
2104
+ #tw;
2105
+ constructor(opts = {}) {
2106
+ const icons = opts.icons ?? bruineIcons();
2107
+ this.#md = new Markdown("", 0, 0, markdownThemeFor(icons), void 0, {
2108
+ // Prose is read at a fixed measure and code keeps the width it needs, so a
2109
+ // wide terminal gains air around the answer instead of lines through it.
2110
+ transform: shapeAnswer
2111
+ });
2112
+ const animate = opts.animate ?? revealAllowed(icons);
2113
+ const repaint = opts.onTick;
2114
+ this.#tw = new Typewriter({
2115
+ ...opts.typewriter,
2116
+ // The repaint hook is the switch: without one there is nothing to animate
2117
+ // for, so the text lands whole. With one, the reveal owns the painting:
2118
+ // the component has to re-set the markdown on every step, not only when a
2119
+ // delta arrives, or the screen keeps showing the text as it was.
2120
+ onTick: animate && repaint !== void 0 ? () => {
2121
+ this.#paint();
2122
+ repaint();
2123
+ } : void 0
2124
+ });
2125
+ }
2126
+ push(delta) {
2127
+ this.#tw.push(delta);
2128
+ this.#paint();
2129
+ }
2130
+ finish() {
2131
+ this.#tw.flush();
2132
+ this.#paint();
2133
+ }
2134
+ #paint() {
2135
+ this.#md.setText(withoutEmoji(this.#tw.text));
2136
+ }
2137
+ /** The shaped lines of the last markdown render: while the renderer hands back the same lines, so do we. */
2138
+ #shaped;
2139
+ render(width) {
2140
+ const rendered = this.#md.render(width);
2141
+ if (this.#shaped?.from !== rendered) this.#shaped = { from: rendered, lines: oneBlankBetween(rendered) };
2142
+ return this.#shaped.lines;
2143
+ }
2144
+ invalidate() {
2145
+ this.#md.invalidate();
2146
+ }
2147
+ };
2148
+
2149
+ // src/ui/changes.ts
2150
+ import { isAbsolute as isAbsolute4, join as join2 } from "path";
2151
+ import stringWidth7 from "string-width";
2152
+ import { visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
2153
+
2154
+ // src/platform/tool.ts
2155
+ import { execFile } from "child_process";
2156
+ import { access } from "fs/promises";
2157
+ import { delimiter, isAbsolute as isAbsolute3, join } from "path";
2158
+ function exitCode(error) {
2159
+ const code = error?.code;
2160
+ return typeof code === "number" ? code : 1;
2161
+ }
2162
+ var DEFAULT_TIMEOUT_MS = 5e3;
2163
+ var runTool = (cmd, args, opts = {}) => new Promise((resolve3) => {
2164
+ const child = execFile(
2165
+ cmd,
2166
+ [...args],
2167
+ {
2168
+ encoding: "buffer",
2169
+ timeout: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
2170
+ ...opts.maxBuffer === void 0 ? {} : { maxBuffer: opts.maxBuffer },
2171
+ ...opts.daemon === true ? { killSignal: "SIGKILL" } : {}
2172
+ },
2173
+ (error, stdout, stderr) => {
2174
+ const forked = opts.daemon === true && error?.killed === true;
2175
+ resolve3({
2176
+ code: error === null || forked ? 0 : exitCode(error),
2177
+ stdout: Buffer.isBuffer(stdout) ? stdout : Buffer.alloc(0),
2178
+ stderr: forked ? "" : typeof stderr === "string" ? stderr : String(stderr ?? "")
2179
+ });
2180
+ }
2181
+ );
2182
+ if (opts.input !== void 0) child.stdin?.end(opts.input);
2183
+ child.on("error", () => resolve3({ code: 1, stdout: Buffer.alloc(0), stderr: "" }));
2184
+ });
2185
+ async function hasTool(cmd, env = process.env) {
2186
+ const names = executableNames(cmd, env);
2187
+ if (isAbsolute3(cmd) || cmd.includes("/") || cmd.includes("\\")) {
2188
+ for (const name2 of names) if (await exists(name2)) return true;
2189
+ return false;
2190
+ }
2191
+ for (const dir of (env["PATH"] ?? "").split(delimiter)) {
2192
+ if (dir === "") continue;
2193
+ for (const name2 of names) if (await exists(join(dir, name2))) return true;
2194
+ }
2195
+ return false;
2196
+ }
2197
+ function executableNames(cmd, env) {
2198
+ if (process.platform !== "win32") return [cmd];
2199
+ const exts = (env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";");
2200
+ return [cmd, ...exts.filter((ext) => ext !== "").map((ext) => cmd + ext.toLowerCase())];
2201
+ }
2202
+ async function exists(path7) {
2203
+ try {
2204
+ await access(path7);
2205
+ return true;
2206
+ } catch {
2207
+ return false;
2208
+ }
2209
+ }
2210
+
2211
+ // src/ui/changes.ts
2212
+ var GIT_TIMEOUT_MS = 4e3;
2213
+ function parsePorcelain(raw) {
2214
+ const out = /* @__PURE__ */ new Map();
2215
+ const fields = raw.split("\0");
2216
+ for (let i = 0; i < fields.length; i += 1) {
2217
+ const field = fields[i];
2218
+ if (field.length < 4) continue;
2219
+ const code = `${field[0]}${field[1]}`.trim();
2220
+ const path7 = field.slice(3);
2221
+ if (code.startsWith("R") || code.startsWith("C")) i += 1;
2222
+ if (path7 !== "") out.set(path7, code);
2223
+ }
2224
+ return out;
2225
+ }
2226
+ function changedSince(before, after) {
2227
+ const out = [];
2228
+ for (const [path7, code] of after) {
2229
+ if (before.get(path7) === code) continue;
2230
+ out.push({ path: path7, status: code });
2231
+ }
2232
+ for (const [path7, code] of before) {
2233
+ if (after.has(path7)) continue;
2234
+ out.push({ path: path7, status: "gone" });
2235
+ }
2236
+ return out;
2237
+ }
2238
+ function parseNumstat(raw) {
2239
+ const out = /* @__PURE__ */ new Map();
2240
+ const fields = raw.split("\0");
2241
+ for (let i = 0; i < fields.length; i += 1) {
2242
+ const row = fields[i];
2243
+ const tab = row.indexOf(" ");
2244
+ if (tab < 0) continue;
2245
+ const added = Number(row.slice(0, tab));
2246
+ const removed = Number(row.slice(tab + 1, row.indexOf(" ", tab + 1)));
2247
+ const path7 = row.slice(row.indexOf(" ", tab + 1) + 1);
2248
+ if (!Number.isFinite(added) || !Number.isFinite(removed) || path7 === "") continue;
2249
+ out.set(path7, { added, removed });
2250
+ }
2251
+ return out;
2252
+ }
2253
+ async function gitStatus(cwd, run = runTool) {
2254
+ if (!await hasTool("git")) return void 0;
2255
+ const result = await run("git", ["status", "--porcelain=v1", "-z"], {
2256
+ timeoutMs: GIT_TIMEOUT_MS
2257
+ });
2258
+ if (result.code !== 0) return void 0;
2259
+ return parsePorcelain(result.stdout.toString("utf8"));
2260
+ }
2261
+ async function gitNumstat(cwd, run = runTool) {
2262
+ if (!await hasTool("git")) return void 0;
2263
+ const result = await run("git", ["diff", "--numstat", "-z", "HEAD"], { timeoutMs: GIT_TIMEOUT_MS });
2264
+ if (result.code !== 0) return void 0;
2265
+ return parseNumstat(result.stdout.toString("utf8"));
2266
+ }
2267
+ async function turnChanges(opts) {
2268
+ const after = await gitStatus(opts.cwd, opts.run);
2269
+ if (opts.before !== void 0 && after !== void 0) {
2270
+ const counts = await gitNumstat(opts.cwd, opts.run);
2271
+ return changedSince(opts.before, after).map((file) => {
2272
+ const n = counts?.get(file.path);
2273
+ return n === void 0 ? file : { ...file, added: n.added, removed: n.removed };
2274
+ });
2275
+ }
2276
+ const seen = /* @__PURE__ */ new Set();
2277
+ const out = [];
2278
+ for (const path7 of opts.declared ?? []) {
2279
+ if (path7 === "" || seen.has(path7)) continue;
2280
+ seen.add(path7);
2281
+ out.push({ path: path7, status: "M" });
2282
+ }
2283
+ return out;
2284
+ }
2285
+
2286
+ // src/plugins/modes.ts
2287
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
2288
+ import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
2289
+ import { Text as Text2 } from "@earendil-works/pi-tui";
2290
+
2291
+ // src/gate/rules.ts
2292
+ import path2 from "path";
2293
+ import { existsSync as existsSync2, lstatSync, realpathSync } from "fs";
2294
+ import { homedir as homedir2 } from "os";
2295
+
2296
+ // src/mcp/config.ts
2297
+ import { createHash } from "crypto";
2298
+ import { readFileSync as readFileSync4 } from "fs";
2299
+ import path3 from "path";
2300
+
2301
+ // src/mcp/state.ts
2302
+ import { mkdirSync, readFileSync as readFileSync5, renameSync, writeFileSync } from "fs";
2303
+ import path4 from "path";
2304
+
2305
+ // src/plugins/modes.ts
2306
+ var PLAN_ON_TEXT = "Plan mode is on: do not modify files or run modifying commands.";
2307
+ var PLAN_OFF_TEXT = "Plan mode is off.";
2308
+ var MODE_ANNOUNCEMENTS = /* @__PURE__ */ new Set([PLAN_ON_TEXT, PLAN_OFF_TEXT]);
2309
+
2310
+ // src/ui/tps.ts
2311
+ var MIN_DECODE_MS = 250;
2312
+ function cachePercent(cached2, totalInput) {
2313
+ if (!Number.isFinite(cached2) || !Number.isFinite(totalInput)) return void 0;
2314
+ if (cached2 < 0 || totalInput <= 0) return void 0;
2315
+ const total = cached2 <= totalInput ? totalInput : totalInput + cached2;
2316
+ if (total <= 0) return void 0;
2317
+ return Math.min(100, Math.max(0, cached2 / total * 100));
2318
+ }
2319
+ var TpsMeter = class {
2320
+ #callStart;
2321
+ #t0;
2322
+ #t1;
2323
+ #deltas = 0;
2324
+ #tps = 0;
2325
+ #measuredTps = 0;
2326
+ #estimatedTps = 0;
2327
+ #textBytes = 0;
2328
+ #firstTextBytes = 0;
2329
+ #pp;
2330
+ #cachePct;
2331
+ #cacheFirst = false;
2332
+ #calls = 0;
2333
+ /** Kept for compatibility; prefer startCall/delta/usage/endCall. */
2334
+ sample(timeMs, outputTokens) {
2335
+ this.usage(timeMs, { outputTokens });
2336
+ }
2337
+ /** Frame start: marks request start for prefill timing. */
2338
+ startCall(timeMs) {
2339
+ this.#callStart = timeMs;
2340
+ this.#t0 = void 0;
2341
+ this.#t1 = void 0;
2342
+ this.#deltas = 0;
2343
+ this.#measuredTps = 0;
2344
+ this.#estimatedTps = 0;
2345
+ this.#textBytes = 0;
2346
+ this.#firstTextBytes = 0;
2347
+ this.#pp = void 0;
2348
+ }
2349
+ /** One decode delta (reasoning/text/tool-call) at the given time. */
2350
+ delta(timeMs, text = "") {
2351
+ this.#textBytes += Buffer.byteLength(text, "utf8");
2352
+ if (this.#t0 === void 0) {
2353
+ this.#firstTextBytes = this.#textBytes;
2354
+ this.#t0 = timeMs;
2355
+ this.#t1 = timeMs;
2356
+ this.#deltas = 1;
2357
+ this.#tps = 0;
2358
+ return;
2359
+ }
2360
+ this.#t1 = timeMs;
2361
+ this.#deltas += 1;
2362
+ const dt = (timeMs - this.#t0) / 1e3;
2363
+ this.#tps = dt > 0 ? (this.#deltas - 1) / dt : 0;
2364
+ const bytes = this.#textBytes - this.#firstTextBytes;
2365
+ this.#estimatedTps = dt * 1e3 >= MIN_DECODE_MS && Number.isFinite(dt) && bytes > 0 ? bytes / 4 / dt : 0;
2366
+ }
2367
+ /** Usage chunk at the end of a call: final decode rate + prefill rate. */
2368
+ usage(timeMs, usage) {
2369
+ const out = Number(usage.outputTokens ?? NaN);
2370
+ const n = Number.isFinite(out) ? Math.max(0, out) : this.#deltas;
2371
+ const decodeMs = this.#t0 !== void 0 && this.#t1 !== void 0 ? this.#t1 - this.#t0 : 0;
2372
+ const measured = Number.isFinite(out) && out > 0 && Number.isFinite(decodeMs) && decodeMs >= MIN_DECODE_MS ? out / (decodeMs / 1e3) : 0;
2373
+ this.#measuredTps = Number.isFinite(measured) ? measured : 0;
2374
+ if (this.#t0 !== void 0 && this.#t1 !== void 0) {
2375
+ const dt = (this.#t1 - this.#t0) / 1e3;
2376
+ this.#tps = dt > 0 ? n / dt : 0;
2377
+ } else if (this.#deltas > 0) {
2378
+ this.#tps = 0;
2379
+ }
2380
+ const inp = Number(usage.inputTokens ?? NaN);
2381
+ if (Number.isFinite(inp) && this.#t0 !== void 0 && this.#callStart !== void 0) {
2382
+ const preDt = (this.#t0 - this.#callStart) / 1e3;
2383
+ const cached2 = Number(usage.cacheReadTokens ?? 0);
2384
+ const fresh = Number.isFinite(cached2) ? cached2 > inp ? Math.max(0, inp) : Math.max(0, inp - cached2) : Math.max(0, inp);
2385
+ this.#pp = preDt > 0 && fresh >= 512 ? fresh / preDt : void 0;
2386
+ }
2387
+ const cachedRaw = Number(usage.cacheReadTokens ?? NaN);
2388
+ if (Number.isFinite(cachedRaw) && Number.isFinite(inp)) {
2389
+ this.#cachePct = cachePercent(cachedRaw, inp);
2390
+ this.#cacheFirst = this.#calls === 0;
2391
+ } else {
2392
+ this.#cachePct = void 0;
2393
+ this.#cacheFirst = false;
2394
+ }
2395
+ }
2396
+ /** Frame end: when no usage arrived, estimate from delta count. */
2397
+ endCall() {
2398
+ if (this.#t0 !== void 0 && this.#t1 !== void 0 && this.#deltas > 0) {
2399
+ if (this.#tps === 0) {
2400
+ const dt = (this.#t1 - this.#t0) / 1e3;
2401
+ this.#tps = dt > 0 ? this.#deltas / dt : 0;
2402
+ }
2403
+ }
2404
+ this.#calls += 1;
2405
+ this.#callStart = void 0;
2406
+ this.#t0 = void 0;
2407
+ this.#t1 = void 0;
2408
+ this.#deltas = 0;
2409
+ }
2410
+ reset() {
2411
+ this.#callStart = void 0;
2412
+ this.#t0 = void 0;
2413
+ this.#t1 = void 0;
2414
+ this.#deltas = 0;
2415
+ this.#tps = 0;
2416
+ this.#measuredTps = 0;
2417
+ this.#estimatedTps = 0;
2418
+ this.#textBytes = 0;
2419
+ this.#firstTextBytes = 0;
2420
+ this.#pp = void 0;
2421
+ this.#cachePct = void 0;
2422
+ this.#cacheFirst = false;
2423
+ this.#calls = 0;
2424
+ }
2425
+ get tps() {
2426
+ return this.#tps;
2427
+ }
2428
+ /** Tokens/s backed by model usage; zero means there is no reliable reading. */
2429
+ get measuredTps() {
2430
+ return this.#measuredTps;
2431
+ }
2432
+ /** Local text-volume estimate for streams that report usage only at the end. */
2433
+ get estimatedTps() {
2434
+ return this.#estimatedTps;
2435
+ }
2436
+ get pp() {
2437
+ return this.#pp;
2438
+ }
2439
+ get cachePct() {
2440
+ return this.#cachePct;
2441
+ }
2442
+ get cacheFirst() {
2443
+ return this.#cacheFirst;
2444
+ }
2445
+ };
2446
+
2447
+ // src/ui/spend.ts
2448
+ function reported(value) {
2449
+ if (value === void 0 || value === null || value === "") return void 0;
2450
+ const n = Number(value);
2451
+ return Number.isFinite(n) ? Math.max(0, n) : void 0;
2452
+ }
2453
+ function loggedUsage(session) {
2454
+ const total = { input: 0, output: 0, cacheRead: 0 };
2455
+ const seen = { input: false, output: false, cacheRead: false };
2456
+ try {
2457
+ const events = session?.snapshotEvents?.();
2458
+ if (!Array.isArray(events)) return { total, seen };
2459
+ for (const event of events) {
2460
+ if (event?.type !== "assistant/message") continue;
2461
+ const usage = event.data?.usage;
2462
+ if (usage === void 0 || usage === null) continue;
2463
+ const counts = {
2464
+ input: reported(usage.inputTokens),
2465
+ output: reported(usage.outputTokens),
2466
+ cacheRead: reported(usage.cacheReadTokens)
2467
+ };
2468
+ for (const key of ["input", "output", "cacheRead"]) {
2469
+ const value = counts[key];
2470
+ if (value === void 0) continue;
2471
+ total[key] += value;
2472
+ seen[key] = true;
2473
+ }
2474
+ }
2475
+ } catch {
2476
+ }
2477
+ return { total, seen };
2478
+ }
2479
+ var SessionSpend = class {
2480
+ #session;
2481
+ #total;
2482
+ #seen;
2483
+ /** The last count each call reported, so growth can be told from a repeat. */
2484
+ #call = { input: 0, output: 0, cacheRead: 0 };
2485
+ constructor(session) {
2486
+ this.#session = session;
2487
+ const logged = loggedUsage(session);
2488
+ this.#total = logged.total;
2489
+ this.#seen = logged.seen;
2490
+ }
2491
+ /** The session these totals belong to; `/new` and `/resume` bring another. */
2492
+ get session() {
2493
+ return this.#session;
2494
+ }
2495
+ /**
2496
+ * Move to another session: a new conversation starts at nothing, a resumed one
2497
+ * at what its log already recorded. False when it is the session already read.
2498
+ */
2499
+ follow(session) {
2500
+ if (session === this.#session) return false;
2501
+ this.#session = session;
2502
+ const logged = loggedUsage(session);
2503
+ this.#total = logged.total;
2504
+ this.#seen = logged.seen;
2505
+ this.beginCall();
2506
+ return true;
2507
+ }
2508
+ /** A model call starts: nothing of this call has been reported yet. */
2509
+ beginCall() {
2510
+ this.#call = { input: 0, output: 0, cacheRead: 0 };
2511
+ }
2512
+ /** Add one usage report. A count the server did not send is not a zero. */
2513
+ add(report) {
2514
+ this.#grow("input", report.input);
2515
+ this.#grow("output", report.output);
2516
+ this.#grow("cacheRead", report.cacheRead);
2517
+ }
2518
+ #grow(key, value) {
2519
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
2520
+ const count = Math.max(0, value);
2521
+ const last = this.#call[key];
2522
+ this.#call[key] = count;
2523
+ this.#total[key] += count >= last ? count - last : count;
2524
+ this.#seen[key] = true;
2525
+ }
2526
+ /**
2527
+ * The row's readings. A glyph the session has never been told about reads
2528
+ * `undefined`, which is how the footer is told to leave it off the row rather
2529
+ * than print a zero.
2530
+ */
2531
+ get readings() {
2532
+ return {
2533
+ inputTokens: this.#seen.input ? this.#total.input : void 0,
2534
+ outputTokens: this.#seen.output ? this.#total.output : void 0,
2535
+ cacheRead: this.#seen.cacheRead ? this.#total.cacheRead : void 0
2536
+ };
2537
+ }
2538
+ };
2539
+
2540
+ // src/ui/task-panel.ts
2541
+ function safeText(value) {
2542
+ return withoutEmoji(value).replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/[\x00-\x1f\x7f-\x9f]/g, " ").replace(/\s+/g, " ").trim();
2543
+ }
2544
+ function taskItems(raw) {
2545
+ if (!Array.isArray(raw)) return void 0;
2546
+ const items = [];
2547
+ for (const item of raw) {
2548
+ if (item === null || typeof item !== "object") return void 0;
2549
+ const value = item;
2550
+ if (typeof value.content !== "string" || !["pending", "in_progress", "completed"].includes(String(value.status))) return void 0;
2551
+ items.push({ content: safeText(value.content), status: value.status });
2552
+ }
2553
+ return items;
2554
+ }
2555
+ var TaskPanel = class {
2556
+ constructor(icons, now = Date.now) {
2557
+ this.icons = icons;
2558
+ this.now = now;
2559
+ }
2560
+ icons;
2561
+ now;
2562
+ tasks = [];
2563
+ expanded = false;
2564
+ get active() {
2565
+ return this.visible && this.tasks.some((item) => item.status === "in_progress");
2566
+ }
2567
+ get visible() {
2568
+ return this.tasks.some((item) => item.status !== "completed");
2569
+ }
2570
+ get done() {
2571
+ return this.tasks.filter((item) => item.status === "completed").length;
2572
+ }
2573
+ /** Return a completion count only when an open list just became complete. */
2574
+ setTasks(tasks) {
2575
+ const wasOpen = this.visible;
2576
+ this.tasks = tasks.map((item) => ({ ...item, content: safeText(item.content) }));
2577
+ return wasOpen && tasks.length > 0 && !this.visible ? tasks.length : void 0;
2578
+ }
2579
+ clearTasks() {
2580
+ this.tasks = [];
2581
+ this.expanded = false;
2582
+ }
2583
+ toggleExpanded() {
2584
+ this.expanded = !this.expanded;
2585
+ }
2586
+ selectedIndexes() {
2587
+ if (this.expanded || this.tasks.length <= 6) return this.tasks.map((_, i) => i);
2588
+ const active = this.tasks.flatMap((item, i) => item.status === "in_progress" ? [i] : []);
2589
+ const anchors = active.length > 0 ? active : [this.tasks.findIndex((item) => item.status === "pending")];
2590
+ const ranked = this.tasks.map((_, i) => ({ i, distance: Math.min(...anchors.map((a) => Math.abs(i - a))) }));
2591
+ ranked.sort((a, b) => a.distance - b.distance || a.i - b.i);
2592
+ return ranked.slice(0, 5).map(({ i }) => i).sort((a, b) => a - b);
2593
+ }
2594
+ /** Plain text snapshot for pipes: one block per todo/write, no animation. */
2595
+ plainLines() {
2596
+ if (this.tasks.length === 0) return [];
2597
+ return [`Tasks ${this.done}/${this.tasks.length}`, ...this.tasks.map((item) => {
2598
+ const mark = item.status === "completed" ? this.icons.ok : item.status === "pending" ? this.icons.think === "*" ? "o" : "\u25CB" : this.icons.think === "*" ? "*" : "\u273B";
2599
+ return `${mark} ${item.content}`;
2600
+ })];
2601
+ }
2602
+ render(width) {
2603
+ if (!this.visible) return [];
2604
+ const available = Math.max(1, width - 4);
2605
+ const indexes = this.selectedIndexes();
2606
+ const lines = [` ${ansi.bold(`Tasks ${this.done}/${this.tasks.length}`)}`];
2607
+ for (const i of indexes) {
2608
+ const item = this.tasks[i];
2609
+ const mark = item.status === "completed" ? ansi.green(this.icons.ok) : item.status === "in_progress" ? ansi.bold(spinnerFrame(this.now(), this.icons)) : this.icons.think === "*" ? "o" : "\u25CB";
2610
+ const content = clipCells(item.content, Math.max(1, available - 2));
2611
+ lines.push(` ${mark} ${item.status === "completed" ? ansi.dim(content) : item.status === "in_progress" ? ansi.bold(content) : content}`);
2612
+ }
2613
+ if (indexes.length < this.tasks.length) {
2614
+ lines.push(` ${ansi.dim(`\u2026 ${this.tasks.length - indexes.length} more`)}`);
2615
+ }
2616
+ return lines;
2617
+ }
2618
+ invalidate() {
2619
+ }
2620
+ };
2621
+
2622
+ // src/ui/bruine-ui.ts
2623
+ import { parse as parseYaml2 } from "yaml";
2624
+ import {
2625
+ Container as Container4,
2626
+ matchesKey as matchesKey4,
2627
+ ProcessTerminal,
2628
+ SelectList,
2629
+ Spacer,
2630
+ Text as Text3,
2631
+ TuiMainScreen,
2632
+ truncateToWidth as truncateToWidth6,
2633
+ visibleWidth as visibleWidth15
2634
+ } from "@earendil-works/pi-tui";
2635
+
2636
+ // src/ui/footer.ts
2637
+ import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth5 } from "@earendil-works/pi-tui";
2638
+ import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
2639
+ import { homedir as homedir4 } from "os";
2640
+ import { isAbsolute as isAbsolute5, join as join3 } from "path";
2641
+
2642
+ // src/ui/place.ts
2643
+ import { homedir as homedir3 } from "os";
2644
+ import { posix, win32 } from "path";
2645
+
2646
+ // src/ui/jump-latest.ts
2647
+ import { stripTerminalSequences, visibleWidth as visibleWidth6 } from "@earendil-works/pi-tui";
2648
+
2649
+ // src/setup/skills.ts
2650
+ import { cp, lstat, mkdir, readFile, readdir, rename, rm, symlink, writeFile } from "fs/promises";
2651
+ import { existsSync as existsSync4 } from "fs";
2652
+ import { homedir as homedir5 } from "os";
2653
+ import path5, { dirname, join as join4 } from "path";
2654
+
2655
+ // src/ui/shell.ts
2656
+ import { Container as Container2 } from "@earendil-works/pi-tui";
2657
+
2658
+ // src/ui/questions.ts
2659
+ import { isKeyRelease, matchesKey, wrapTextWithAnsi as wrapTextWithAnsi2 } from "@earendil-works/pi-tui";
2660
+ import stringWidth8 from "string-width";
2661
+
2662
+ // src/ui/box.ts
2663
+ import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth7 } from "@earendil-works/pi-tui";
2664
+
2665
+ // src/ui/keys.ts
2666
+ import { decodeKittyPrintable } from "@earendil-works/pi-tui";
2667
+
2668
+ // src/ui/prompt-frame.ts
2669
+ import { stripTerminalSequences as stripTerminalSequences2, truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth9 } from "@earendil-works/pi-tui";
2670
+
2671
+ // src/ui/working.ts
2672
+ import { visibleWidth as visibleWidth8 } from "@earendil-works/pi-tui";
2673
+
2674
+ // src/llm/silence.ts
2675
+ var STREAM_CEILING_MS = 15 * 60 * 1e3;
2676
+ var SECOND = 1e3;
2677
+ var MINUTE = 60 * SECOND;
2678
+ var PHASE_BUDGET_MS = {
2679
+ waiting: 2 * MINUTE,
2680
+ thinking: 90 * SECOND,
2681
+ preparing: 3 * MINUTE,
2682
+ answering: 60 * SECOND,
2683
+ calling: 2 * MINUTE
2684
+ };
2685
+ var PER_THOUSAND_CHARS_MS = 6 * SECOND;
2686
+ var MAX_SIZE_BONUS_MS = 10 * MINUTE;
2687
+ var MIN_BUDGET_MS = 20 * SECOND;
2688
+ function formatSilence(ms) {
2689
+ const s = Math.round(ms / 1e3);
2690
+ if (s < 60) return `${String(s)}s`;
2691
+ const m = Math.floor(s / 60);
2692
+ const rest = s % 60;
2693
+ return rest === 0 ? `${String(m)}m` : `${String(m)}m ${String(rest)}s`;
2694
+ }
2695
+
2696
+ // src/ui/escape-filter.ts
2697
+ import { isKeyRelease as isKeyRelease2, isKeyRepeat, matchesKey as matchesKey2 } from "@earendil-works/pi-tui";
2698
+
2699
+ // src/ui/header.ts
2700
+ import { visibleWidth as visibleWidth10 } from "@earendil-works/pi-tui";
2701
+ var LOGO_CELLS = LOGO[0].length;
2702
+ var LOGO_GAP = 3;
2703
+ var LOGO_MIN_WIDTH = LOGO_CELLS + LOGO_GAP + 28;
2704
+ var RESOURCE_PREFIX = 2 + 9;
2705
+ var LOGO_BESIDE_GAP = " ".repeat(LOGO_GAP);
2706
+
2707
+ // src/ui/intro.ts
2708
+ import { existsSync as existsSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
2709
+ import { join as join5 } from "path";
2710
+
2711
+ // src/ui/intro-effects.ts
2712
+ var H = LOGO.length;
2713
+ var W = LOGO[0].length;
2714
+ var target = (r, c2) => LOGO[r][c2];
2715
+ var letters = () => {
2716
+ const out = [];
2717
+ for (let r = 0; r < H; r += 1) for (let c2 = 0; c2 < W; c2 += 1) if (target(r, c2) !== " ") out.push([r, c2]);
2718
+ return out;
2719
+ };
2720
+ var LETTERS = letters();
2721
+
2722
+ // src/ui/weather-effect.ts
2723
+ import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2724
+ import { readFileSync as readFileSync9 } from "fs";
2725
+ import { randomUUID } from "crypto";
2726
+ import { visibleWidth as visibleWidth11 } from "@earendil-works/pi-tui";
2727
+ var QUIET_INK = { far: ansi.faint, mid: ansi.faint, near: ansi.gray };
2728
+ var STORM_INK = { far: ansi.faint, mid: ansi.gray, near: ansi.violet };
2729
+
2730
+ // src/ui/queued-prompts.ts
2731
+ import { truncateToWidth as truncateToWidth5 } from "@earendil-works/pi-tui";
2732
+
2733
+ // src/ui/approval-band.ts
2734
+ import { matchesKey as matchesKey3, visibleWidth as visibleWidth12, wrapTextWithAnsi as wrapTextWithAnsi3 } from "@earendil-works/pi-tui";
2735
+
2736
+ // src/ui/file-complete.ts
2737
+ import { CombinedAutocompleteProvider } from "@earendil-works/pi-tui";
2738
+ import { existsSync as existsSync6, readdirSync } from "fs";
2739
+ import { basename, delimiter as delimiter2, join as join6, relative as relative2 } from "path";
2740
+
2741
+ // src/image/clipboard.ts
2742
+ import { readFileSync as readFileSync10 } from "fs";
2743
+ import { readFile as readFile3 } from "fs/promises";
2744
+ import { platform } from "os";
2745
+ import { extname } from "path";
2746
+ var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2747
+
2748
+ // src/ui/selection.ts
2749
+ import { Container as Container3, sliceByColumn as sliceByColumn2, stripTerminalSequences as stripTerminalSequences3, visibleWidth as visibleWidth13 } from "@earendil-works/pi-tui";
2750
+
2751
+ // src/ui/mouse.ts
2752
+ import { appendFileSync } from "fs";
2753
+
2754
+ // src/ui/clipboard-write.ts
2755
+ import { platform as platform2 } from "os";
2756
+
2757
+ // src/ui/toast.ts
2758
+ import { visibleWidth as visibleWidth14 } from "@earendil-works/pi-tui";
2759
+
2760
+ // src/setup/discover.ts
2761
+ import { readFileSync as readFileSync11 } from "fs";
2762
+ import { networkInterfaces } from "os";
2763
+ import { join as join7 } from "path";
2764
+ import { spawnSync } from "child_process";
2765
+ import { parse as parseYaml } from "yaml";
2766
+
2767
+ // src/image/vision.ts
2768
+ var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "vision", "multimodal", "image", "images"]);
2769
+ var VISION_KEYS = ["capabilities", "vision", "multimodal", "image", "images"];
2770
+ function declaresVision(value) {
2771
+ if (value === true) return true;
2772
+ if (typeof value === "string") {
2773
+ return value.split(/[\s,;|]+/).some((word) => TRUE_WORDS.has(word.toLowerCase()));
2774
+ }
2775
+ if (Array.isArray(value)) {
2776
+ return value.some((v) => typeof v === "string" && TRUE_WORDS.has(v.toLowerCase()));
2777
+ }
2778
+ return false;
2779
+ }
2780
+ function entryVisionAnswer(entry) {
2781
+ if (entry === null || typeof entry !== "object") return "unknown";
2782
+ const record = entry;
2783
+ let declared = false;
2784
+ for (const key of VISION_KEYS) {
2785
+ const value = record[key];
2786
+ if (value === void 0 || value === null) continue;
2787
+ declared = true;
2788
+ if (declaresVision(value)) return "yes";
2789
+ }
2790
+ return declared ? "no" : "unknown";
2791
+ }
2792
+ function settingsEntryAnswer(entry) {
2793
+ return entryVisionAnswer(entry);
2794
+ }
2795
+
2796
+ // src/ui/bruine-ui.ts
2797
+ import { existsSync as existsSync7, readFileSync as readFileSync12 } from "fs";
2798
+ import { join as join9 } from "path";
2799
+
2800
+ // src/update.ts
2801
+ import { spawn } from "child_process";
2802
+ import { realpathSync as realpathSync2 } from "fs";
2803
+ import { mkdir as mkdir3, readFile as readFile4, rename as rename3, writeFile as writeFile3 } from "fs/promises";
2804
+ import path6 from "path";
2805
+ var NPM_PACKAGE = "@ziamana/bruine";
2806
+ var REGISTRY_LATEST = `https://registry.npmjs.org/${NPM_PACKAGE.replace("/", "%2F")}/latest`;
2807
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2808
+
2809
+ // src/ui/bruine-ui.ts
2810
+ function readSettingsRoute(dshHome) {
2811
+ try {
2812
+ const home = dshHome ?? runtimeHome();
2813
+ const p = join9(home, "settings.yaml");
2814
+ if (!existsSync7(p)) return void 0;
2815
+ const doc = parseBruineSettingsYaml(readFileSync12(p, "utf8"));
2816
+ if (doc === void 0) return void 0;
2817
+ return doc;
2818
+ } catch {
2819
+ return void 0;
2820
+ }
2821
+ }
2822
+ function parseBruineSettingsYaml(text) {
2823
+ const doc = parseYaml2(text);
2824
+ const def = doc?.["agent-default-model"];
2825
+ const provider = typeof def?.provider === "string" ? def.provider : void 0;
2826
+ const model = typeof def?.model === "string" ? def.model : void 0;
2827
+ if (provider === void 0 || model === void 0) return void 0;
2828
+ const route = doc?.["llm-pi-ai"]?.providers?.[provider];
2829
+ const entry = Array.isArray(route?.models) ? route.models.find((m) => m !== null && typeof m === "object" && m.id === model) : void 0;
2830
+ const out = { provider, model };
2831
+ if (typeof route?.baseURL === "string") out.baseUrl = route.baseURL;
2832
+ if (typeof entry?.name === "string") out.name = entry.name;
2833
+ if (typeof entry?.contextWindow === "number") out.contextWindow = entry.contextWindow;
2834
+ if (typeof route?.displayName === "string") out.providerDisplayName = route.displayName;
2835
+ if (entry?.compat?.thinkingFormat !== void 0) out.hasCompat = true;
2836
+ const vision = settingsEntryAnswer(entry);
2837
+ if (vision !== "unknown") out.vision = vision;
2838
+ return out;
2839
+ }
2840
+
2841
+ // src/ui/errors.ts
2842
+ import { appendFile, mkdir as mkdir4 } from "fs/promises";
2843
+ import { join as join10 } from "path";
2844
+ import { truncateToWidth as truncateToWidth7, wrapTextWithAnsi as wrapTextWithAnsi4 } from "@earendil-works/pi-tui";
2845
+ function firstLine(text) {
2846
+ return (text.split(/\r?\n/)[0] ?? text).trim();
2847
+ }
2848
+ function statusOf(failure) {
2849
+ if (typeof failure.status === "number") return failure.status;
2850
+ const m = /\b(4\d\d|5\d\d)\b/.exec(typeof failure.message === "string" ? failure.message : "");
2851
+ return m?.[1] === void 0 ? void 0 : Number(m[1]);
2852
+ }
2853
+ function timeoutSeconds(message) {
2854
+ const s = /(\d+(?:[.,]\d+)?)\s*sec(?:onds?)?\b/i.exec(message) ?? /(\d+(?:[.,]\d+)?)\s*s\b/i.exec(message);
2855
+ if (s?.[1] !== void 0) return Number(s[1].replace(",", "."));
2856
+ const ms = /(\d+(?:[.,]\d+)?)\s*ms\b/i.exec(message);
2857
+ return ms?.[1] === void 0 ? void 0 : Math.round(Number(ms[1].replace(",", ".")) / 100) / 10;
2858
+ }
2859
+ function formatK(n) {
2860
+ const k = n / 1e3;
2861
+ return `${k >= 10 ? Math.round(k) : Math.round(k * 10) / 10}k`;
2862
+ }
2863
+ function describeLlmError(failure, route) {
2864
+ const code = typeof failure.code === "string" ? failure.code : "UNKNOWN";
2865
+ const message = typeof failure.message === "string" ? failure.message : "";
2866
+ const status = statusOf(failure);
2867
+ const server = route.baseUrl;
2868
+ const at = server === void 0 ? "the model server" : `your model server at ${server}`;
2869
+ if (code === "AUTH" || status === 401 || status === 403 || /\b40[13]\b/.test(message)) {
2870
+ return { message: `The API key was refused by ${route.provider}`, hint: "Set a new key: bruine setup" };
2871
+ }
2872
+ if (status === 404 || /\b404\b|model.{0,30}(not found|does not exist)/i.test(message)) {
2873
+ return {
2874
+ message: `Model "${route.model}" not found on ${server ?? "the model server"}`,
2875
+ hint: "Change it: bruine setup",
2876
+ wantAvailableModels: true
2877
+ };
2878
+ }
2879
+ if (code === "RATE_LIMIT" || status === 429) {
2880
+ return { message: `Rate limited by ${route.provider}`, hint: "Wait a moment and try again" };
2881
+ }
2882
+ if (code === "QUOTA_EXCEEDED") {
2883
+ return { message: `Quota exceeded at ${route.provider}`, hint: "Check the account balance: bruine setup" };
2884
+ }
2885
+ if (code === "CONTEXT_WINDOW_EXCEEDED") {
2886
+ const n = route.contextWindow !== void 0 && route.contextWindow > 0 ? ` (${formatK(route.contextWindow)})` : "";
2887
+ return { message: `The conversation is larger than the model's context${n}`, hint: "Use /compact or /new" };
2888
+ }
2889
+ if (code === "TIMEOUT" || /\btime(?:d)?\s*out\b|timeout/i.test(message)) {
2890
+ const secs = timeoutSeconds(message);
2891
+ return {
2892
+ message: `The model server did not answer in time${secs === void 0 ? "" : ` (${String(secs)}s)`}`,
2893
+ hint: "It may still be loading the model. Try again, or check the server logs"
2894
+ };
2895
+ }
2896
+ if (code === "TRANSPORT" || /\bECONN|\b(?:network|connection|socket|fetch|dns)\b/i.test(message)) {
2897
+ return {
2898
+ message: `Can't reach ${at}`,
2899
+ hint: "Is llama.cpp / Ollama / LM Studio running? Change it: bruine setup"
2900
+ };
2901
+ }
2902
+ if (code === "SERVER" || status !== void 0 && status >= 500) {
2903
+ return {
2904
+ message: `The model server returned an error (${String(status ?? code)})`,
2905
+ hint: firstLine(message) === "" ? "Try again" : firstLine(message)
2906
+ };
2907
+ }
2908
+ if (code === "ABORTED" || code === "CANCELLED" || /\b(cancell?ed|aborted|interrupted)\b/i.test(message)) {
2909
+ const said = firstLine(message);
2910
+ return {
2911
+ message: said === "" ? "The turn was cancelled" : said,
2912
+ hint: "You stopped this turn. Ask again, or /new to start clean"
2913
+ };
2914
+ }
2915
+ return {
2916
+ message: firstLine(message) === "" ? `${route.provider}: unknown model error` : firstLine(message),
2917
+ hint: "Details in logs/bruine.log; if it repeats: bruine setup"
2918
+ };
2919
+ }
2920
+ async function fetchAvailableModels(baseUrl) {
2921
+ try {
2922
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
2923
+ signal: AbortSignal.timeout(1e3)
2924
+ });
2925
+ if (!res.ok) return [];
2926
+ const body = await res.json();
2927
+ const raw = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
2928
+ return raw.map((m) => String(m?.id ?? m?.model ?? "")).filter((id) => id !== "" && id !== "undefined").slice(0, 3);
2929
+ } catch {
2930
+ return [];
2931
+ }
2932
+ }
2933
+ function bruineLogPath(env = process.env) {
2934
+ const home = runtimeHome(env);
2935
+ return join10(home, "logs", "bruine.log");
2936
+ }
2937
+ async function appendErrorLog(failure, extra = "") {
2938
+ const path7 = bruineLogPath();
2939
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} [llm-error] code=${String(failure.code ?? "UNKNOWN")}${failure.status === void 0 ? "" : ` status=${String(failure.status)}`} ${String(failure.message ?? "").slice(0, 2e3)}${extra === "" ? "" : ` ${extra}`}
2940
+ `;
2941
+ try {
2942
+ await mkdir4(join10(path7, ".."), { recursive: true });
2943
+ await appendFile(path7, line, "utf8");
2944
+ } catch {
2945
+ }
2946
+ }
2947
+
2948
+ // src/ui/retry.ts
2949
+ function silenceOf(failure) {
2950
+ if (typeof failure?.silenceMs === "number") return failure.silenceMs;
2951
+ const ms = /after (\d+)\s*ms/.exec(failure?.message ?? "")?.[1];
2952
+ return ms === void 0 ? void 0 : Number(ms);
2953
+ }
2954
+ function retryCause(failure) {
2955
+ switch (failure?.code) {
2956
+ case "TIMEOUT": {
2957
+ const quiet = silenceOf(failure);
2958
+ return quiet === void 0 ? "The model is not answering." : `The model has not answered for ${formatSilence(quiet)}.`;
2959
+ }
2960
+ case "RATE_LIMIT":
2961
+ return "The provider is limiting requests.";
2962
+ case "SERVER":
2963
+ return "The model's server returned an error.";
2964
+ case "TRANSPORT":
2965
+ return "The connection to the model dropped.";
2966
+ case "EMPTY_RESPONSE":
2967
+ return "The model sent an empty answer.";
2968
+ default:
2969
+ return failure?.code === void 0 ? "The request failed." : `The request failed (${failure.code}).`;
2970
+ }
2971
+ }
2972
+ function retryCount(event) {
2973
+ return event.maxRetries === void 0 ? String(event.retry) : `${String(event.retry)}/${String(event.maxRetries)}`;
2974
+ }
2975
+ function retryWait(delayMs) {
2976
+ if (delayMs < 100) return "now";
2977
+ if (delayMs < 1e4) return `in ${(delayMs / 1e3).toFixed(1).replace(/\.0$/, "")}s`;
2978
+ return `in ${formatSilence(delayMs)}`;
2979
+ }
2980
+ function retryLine(event) {
2981
+ return `${retryCause(event.failure)} Retry ${retryCount(event)} ${retryWait(event.delayMs)}.`;
2982
+ }
2983
+
2984
+ // src/plugins/silence.ts
2985
+ import { existsSync as existsSync8, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
2986
+ import { join as join11 } from "path";
2987
+ import { parse as parseYaml3 } from "yaml";
2988
+ var BRUINE_SILENCE_SERVICE = "bruineSilence";
2989
+
2990
+ // src/plugins/render.ts
2991
+ var name = "bruine-render";
2992
+ var BRUINE_RENDER_SERVICE = "bruineRender";
2993
+ function stdoutScreen() {
2994
+ return {
2995
+ write: (s) => void process.stdout.write(s),
2996
+ get columns() {
2997
+ return process.stdout.columns ?? 80;
2998
+ }
2999
+ };
3000
+ }
3001
+ function createUi(screen, icons = bruineIcons()) {
3002
+ return {
3003
+ screen,
3004
+ icons,
3005
+ reasoning: new ReasoningLine(screen, Date.now, icons),
3006
+ text: new TextStream(screen),
3007
+ tools: new ToolCallView(screen, Date.now, icons)
3008
+ };
3009
+ }
3010
+ function restoreTasks(session) {
3011
+ try {
3012
+ const events = session?.snapshotEvents?.();
3013
+ if (!Array.isArray(events)) return void 0;
3014
+ for (let i = events.length - 1; i >= 0; i--) {
3015
+ if (events[i]?.type === "todo/write") return taskItems(events[i]?.data?.todos);
3016
+ }
3017
+ } catch {
3018
+ }
3019
+ return void 0;
3020
+ }
3021
+ function reported2(value) {
3022
+ if (value === void 0 || value === null || value === "") return void 0;
3023
+ const n = Number(value);
3024
+ return Number.isFinite(n) ? n : void 0;
3025
+ }
3026
+ function attachTui(ctx, agent, ui, service, getAgent) {
3027
+ const tps = new TpsMeter();
3028
+ const tools = /* @__PURE__ */ new Map();
3029
+ const todoToolIds = /* @__PURE__ */ new Set();
3030
+ const setUiTasks = (tasks) => {
3031
+ ui.setTasks?.(tasks);
3032
+ };
3033
+ let reasoning;
3034
+ let text;
3035
+ let lastInputTokens = 0;
3036
+ let lastOutputTokens = 0;
3037
+ let lastCacheTokens = 0;
3038
+ const spend = new SessionSpend(agent.session);
3039
+ let turnStartWall = Date.now();
3040
+ let turnOutput = 0;
3041
+ let turnBefore;
3042
+ let turnToolIds = [];
3043
+ let turnBreaks = /* @__PURE__ */ new Set();
3044
+ let needBreak = false;
3045
+ let lastUser = "";
3046
+ let currentAnswer = "";
3047
+ let suggestController;
3048
+ service.describe = (callId) => {
3049
+ const comp = tools.get(callId);
3050
+ if (comp === void 0) return void 0;
3051
+ return { tool: comp.tool, summary: comp.summary(80) };
3052
+ };
3053
+ service.cancelSuggest = () => {
3054
+ suggestController?.abort();
3055
+ suggestController = void 0;
3056
+ };
3057
+ const hideWorking = () => {
3058
+ ui.activity?.setNote(void 0);
3059
+ if (ui.activity?.state === "Waiting for model") ui.activity.setState("Working");
3060
+ ui.requestRender();
3061
+ };
3062
+ const showWorking = () => {
3063
+ if (reasoning === void 0 && text === void 0) {
3064
+ ui.showWorking?.();
3065
+ ui.requestRender();
3066
+ }
3067
+ };
3068
+ const closeLive = () => {
3069
+ if (reasoning !== void 0) {
3070
+ reasoning.end();
3071
+ reasoning = void 0;
3072
+ ui.requestRender();
3073
+ }
3074
+ if (text !== void 0) {
3075
+ text.finish();
3076
+ text = void 0;
3077
+ }
3078
+ };
3079
+ const ensureReasoning = () => {
3080
+ if (reasoning === void 0) {
3081
+ reasoning = new ReasoningComponent(Date.now, ui.icons);
3082
+ ui.addChat(reasoning);
3083
+ }
3084
+ return reasoning;
3085
+ };
3086
+ const ensureText = () => {
3087
+ if (text === void 0) {
3088
+ text = new AssistantTextComponent({ onTick: () => ui.requestRender(), icons: ui.icons });
3089
+ ui.addChat(text);
3090
+ }
3091
+ return text;
3092
+ };
3093
+ const ensureTool = (id, toolName) => {
3094
+ let comp = tools.get(id);
3095
+ if (comp === void 0) {
3096
+ const question = toolName === "ask_user_question" ? new QuestionCallComponent(Date.now, ui.icons, { onTick: () => ui.requestRender() }) : void 0;
3097
+ comp = question ?? new ToolCallComponent(toolName, Date.now, ui.icons);
3098
+ tools.set(id, comp);
3099
+ if (needBreak) {
3100
+ turnBreaks.add(id);
3101
+ needBreak = false;
3102
+ }
3103
+ turnToolIds.push(id);
3104
+ if (question !== void 0) ui.addQuestionCall?.(question);
3105
+ else ui.addChat(comp);
3106
+ }
3107
+ return comp;
3108
+ };
3109
+ const footerState = () => ui.footer?.state ?? {};
3110
+ const reportTurnChanges = (ordered) => {
3111
+ const declared = ordered.map((t) => t.comp.touchedPath?.()).filter((p) => p !== void 0);
3112
+ const before = turnBefore;
3113
+ turnBefore = void 0;
3114
+ void turnChanges({ before, declared, cwd: process.cwd() }).then((changed) => {
3115
+ if (changed.length === 0) return;
3116
+ ui.showTurnChanges?.(changed);
3117
+ ui.requestRender();
3118
+ });
3119
+ };
3120
+ const showLlmError = async (failure) => {
3121
+ const f = failure ?? {};
3122
+ const settings = readSettingsRoute();
3123
+ const state2 = footerState();
3124
+ const route = {
3125
+ provider: settings?.providerDisplayName ?? settings?.provider ?? "the model server",
3126
+ model: settings?.model ?? "",
3127
+ ...settings?.baseUrl !== void 0 ? { baseUrl: settings.baseUrl } : {},
3128
+ ...typeof state2.contextWindow === "number" ? { contextWindow: state2.contextWindow } : {}
3129
+ };
3130
+ const lines = describeLlmError(f, route);
3131
+ const message = new Text4(ansi.red(`${ui.icons.fail} ${lines.message}`), 0, 0);
3132
+ const hint = new Text4(dim(lines.hint), 0, 0);
3133
+ ui.addChat(message);
3134
+ ui.addChat(hint);
3135
+ ui.requestRender();
3136
+ if (lines.wantAvailableModels === true && settings?.baseUrl !== void 0) {
3137
+ const ids = await fetchAvailableModels(settings.baseUrl);
3138
+ if (ids.length > 0) {
3139
+ hint.setText(dim(`Available: ${ids.join(", ")}. ${lines.hint}`));
3140
+ ui.requestRender();
3141
+ }
3142
+ }
3143
+ void appendErrorLog(f);
3144
+ };
3145
+ let compactionBefore;
3146
+ let compactionShadowed;
3147
+ const liveAgent = () => {
3148
+ try {
3149
+ return getAgent?.() ?? agent;
3150
+ } catch {
3151
+ return agent;
3152
+ }
3153
+ };
3154
+ ui.setSilenceProbe?.(() => {
3155
+ let silence;
3156
+ try {
3157
+ silence = ctx.get(BRUINE_SILENCE_SERVICE);
3158
+ } catch {
3159
+ return void 0;
3160
+ }
3161
+ const id = liveAgent().session?.id;
3162
+ const reading = silence?.reading(id === void 0 ? void 0 : String(id));
3163
+ return reading === void 0 ? void 0 : { quietMs: Date.now() - reading.quietSince, budgetMs: reading.budget };
3164
+ });
3165
+ let subagentCount = 0;
3166
+ const refreshSubagents = () => {
3167
+ const registry = ctx.get("agents");
3168
+ const candidates = registry?.list?.() ?? [];
3169
+ const root = liveAgent();
3170
+ const owned = /* @__PURE__ */ new Set([root]);
3171
+ let changed = true;
3172
+ while (changed && registry?.isOwnedBy) {
3173
+ changed = false;
3174
+ for (const candidate of candidates) {
3175
+ if (owned.has(candidate) || candidate.session?.id === void 0) continue;
3176
+ for (const parent of owned) {
3177
+ if (!registry.isOwnedBy(candidate.session.id, parent)) continue;
3178
+ owned.add(candidate);
3179
+ changed = true;
3180
+ break;
3181
+ }
3182
+ }
3183
+ }
3184
+ const count = candidates.filter((candidate) => candidate !== root && owned.has(candidate) && candidate.status === "running").length;
3185
+ if (count === subagentCount) return;
3186
+ subagentCount = count;
3187
+ ui.footer.set({ subagents: count });
3188
+ ui.requestRender();
3189
+ };
3190
+ const mainRequests = new MainRequestMemory();
3191
+ const offRequest = ctx.on(
3192
+ "llm/stream",
3193
+ (options, next) => {
3194
+ mainRequests.see(options, liveAgent().session?.id);
3195
+ return next();
3196
+ },
3197
+ { global: true, prepend: true }
3198
+ );
3199
+ const offAgentCreated = ctx.on("agent/created", refreshSubagents);
3200
+ const offAgentStatus = ctx.on("agent/status", refreshSubagents);
3201
+ const offAgentDisposed = ctx.on("agent/disposed", refreshSubagents);
3202
+ refreshSubagents();
3203
+ const offStream = ctx.on("agent/assistant-stream", ({ agent: subject, frame }) => {
3204
+ if (subject !== liveAgent()) return;
3205
+ const f = frame;
3206
+ const now = typeof f.time === "number" ? f.time : Date.now();
3207
+ if (f.type === "start") {
3208
+ tps.startCall(now);
3209
+ if (spend.follow(liveAgent().session)) ui.footer.set(spend.readings);
3210
+ else spend.beginCall();
3211
+ ui.footer.set({ tps: 0, tpsEstimated: false, pp: void 0 });
3212
+ closeLive();
3213
+ void gitStatus(process.cwd()).then((status) => {
3214
+ turnBefore = status;
3215
+ });
3216
+ showWorking();
3217
+ return;
3218
+ }
3219
+ if (f.type === "end") {
3220
+ tps.endCall();
3221
+ ui.footer.set({ tps: tps.measuredTps || tps.estimatedTps, tpsEstimated: tps.measuredTps === 0 && tps.estimatedTps > 0, pp: tps.pp, cachePct: tps.cachePct, cacheFirst: tps.cacheFirst });
3222
+ hideWorking();
3223
+ closeLive();
3224
+ ui.requestRender();
3225
+ return;
3226
+ }
3227
+ const chunk = f.chunk;
3228
+ if (chunk === void 0) return;
3229
+ const trackDelta = (content) => {
3230
+ if (typeof content !== "string" || content.length === 0) return;
3231
+ tps.delta(now, content);
3232
+ ui.footer.set({ tps: tps.measuredTps || tps.estimatedTps, tpsEstimated: tps.measuredTps === 0 && tps.estimatedTps > 0, pp: tps.pp, cachePct: tps.cachePct, cacheFirst: tps.cacheFirst });
3233
+ };
3234
+ switch (chunk.type) {
3235
+ case "reasoning-delta":
3236
+ hideWorking();
3237
+ trackDelta(chunk.text);
3238
+ ensureReasoning().push(chunk.text);
3239
+ ui.requestRender();
3240
+ return;
3241
+ case "text-delta":
3242
+ hideWorking();
3243
+ trackDelta(chunk.text);
3244
+ needBreak = true;
3245
+ currentAnswer += String(chunk.text ?? "");
3246
+ ensureText().push(chunk.text);
3247
+ ui.requestRender();
3248
+ return;
3249
+ case "block-start":
3250
+ if (chunk.blockType !== "reasoning") closeLive();
3251
+ return;
3252
+ case "block-end":
3253
+ if (chunk.block.type === "reasoning") {
3254
+ reasoning?.end();
3255
+ reasoning = void 0;
3256
+ ui.requestRender();
3257
+ } else if (chunk.block.type === "text") {
3258
+ text?.finish();
3259
+ text = void 0;
3260
+ }
3261
+ return;
3262
+ case "tool-call-delta": {
3263
+ hideWorking();
3264
+ trackDelta(chunk.argumentsDelta || chunk.name);
3265
+ const id = String(chunk.id);
3266
+ if (chunk.name === "todo_write") todoToolIds.add(id);
3267
+ if (todoToolIds.has(id)) return;
3268
+ if (chunk.name !== void 0) ensureTool(id, chunk.name);
3269
+ tools.get(id)?.args(chunk.argumentsDelta ?? "");
3270
+ ui.requestRender();
3271
+ return;
3272
+ }
3273
+ case "usage": {
3274
+ const u = chunk.usage ?? {};
3275
+ const tokens = Number(u.outputTokens ?? 0);
3276
+ lastOutputTokens = tokens;
3277
+ lastInputTokens = Number(u.inputTokens ?? lastInputTokens);
3278
+ if (Number.isFinite(tokens)) turnOutput += Math.max(0, tokens);
3279
+ const details = u.prompt_tokens_details ?? u.promptTokensDetails ?? {};
3280
+ const cached2 = Number(
3281
+ u.cacheReadTokens ?? u.cachedTokens ?? u.cache_read ?? details.cached_tokens ?? NaN
3282
+ );
3283
+ if (Number.isFinite(cached2)) lastCacheTokens = Math.max(0, cached2);
3284
+ tps.usage(now, {
3285
+ outputTokens: Number(u.outputTokens ?? NaN),
3286
+ inputTokens: Number(u.inputTokens ?? NaN),
3287
+ cacheReadTokens: cached2
3288
+ });
3289
+ const window = agent.session?.requestContext?.()?.contextWindow;
3290
+ if (typeof window === "number" && window > 0) {
3291
+ ui.footer.set({ contextUsed: lastInputTokens + lastCacheTokens + lastOutputTokens, contextWindow: window });
3292
+ }
3293
+ spend.add({ input: reported2(u.inputTokens), output: reported2(u.outputTokens), cacheRead: reported2(cached2) });
3294
+ ui.footer.set({ ...spend.readings, tps: tps.measuredTps || tps.estimatedTps, tpsEstimated: tps.measuredTps === 0 && tps.estimatedTps > 0, pp: tps.pp, cachePct: tps.cachePct, cacheFirst: tps.cacheFirst });
3295
+ ui.requestRender();
3296
+ return;
3297
+ }
3298
+ case "finish":
3299
+ tps.endCall();
3300
+ ui.footer.set({ tps: tps.measuredTps || tps.estimatedTps, tpsEstimated: tps.measuredTps === 0 && tps.estimatedTps > 0, pp: tps.pp, cachePct: tps.cachePct, cacheFirst: tps.cacheFirst });
3301
+ closeLive();
3302
+ ui.requestRender();
3303
+ return;
3304
+ default:
3305
+ return;
3306
+ }
3307
+ });
3308
+ const offSession = ctx.on("session/event", (session, event) => {
3309
+ if (session !== liveAgent().session) return;
3310
+ switch (event.type) {
3311
+ case "user/message": {
3312
+ if (event.data.source?.kind !== "user") return;
3313
+ const blocks = (event.data.content ?? []).filter((b) => b.type === "text");
3314
+ const content = blocks.map((b) => b.text).join(" ");
3315
+ const trimmed = content.trim();
3316
+ if (trimmed === "") return;
3317
+ if (MODE_ANNOUNCEMENTS.has(trimmed)) return;
3318
+ lastUser = trimmed;
3319
+ ui.addUserPrompt(trimmed);
3320
+ return;
3321
+ }
3322
+ case "turn/start":
3323
+ refreshSubagents();
3324
+ tps.reset();
3325
+ ui.footer.set({ tps: 0, tpsEstimated: false, pp: void 0, cachePct: void 0, cacheFirst: false });
3326
+ turnStartWall = Date.now();
3327
+ turnOutput = 0;
3328
+ turnToolIds = [];
3329
+ turnBreaks = /* @__PURE__ */ new Set();
3330
+ needBreak = false;
3331
+ currentAnswer = "";
3332
+ suggestController?.abort();
3333
+ suggestController = void 0;
3334
+ showWorking();
3335
+ return;
3336
+ case "tool/call": {
3337
+ hideWorking();
3338
+ const id = String(event.data.callId);
3339
+ if (event.data.name === "todo_write") {
3340
+ todoToolIds.add(id);
3341
+ return;
3342
+ }
3343
+ const comp = ensureTool(id, event.data.name);
3344
+ if (event.data.arguments) comp.setArgs(event.data.arguments);
3345
+ ui.requestRender();
3346
+ return;
3347
+ }
3348
+ case "tool/result": {
3349
+ const block = event.data.message?.content?.[0];
3350
+ if (block === void 0 || block.type !== "tool-result") return;
3351
+ if (todoToolIds.delete(String(block.toolCallId))) return;
3352
+ const output = block.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3353
+ tools.get(String(block.toolCallId))?.result(block.isError !== true, output);
3354
+ ui.requestRender();
3355
+ return;
3356
+ }
3357
+ case "todo/write": {
3358
+ const tasks = taskItems(event.data?.todos);
3359
+ if (tasks !== void 0) setUiTasks(tasks);
3360
+ return;
3361
+ }
3362
+ case "llm/retry": {
3363
+ const data = event.data;
3364
+ closeLive();
3365
+ const mark = ui.icons.think === "*" ? "~" : "\u21BB";
3366
+ ui.addChat(new Text4(`${ansi.yellow(mark)} ${ansi.gray(retryLine(data))}`, 1, 0));
3367
+ ui.activity?.setNote(`retry ${retryCount(data)}`);
3368
+ ui.requestRender();
3369
+ return;
3370
+ }
3371
+ case "llm/retry-started":
3372
+ ui.activity?.setState("Waiting for model");
3373
+ ui.requestRender();
3374
+ return;
3375
+ case "compaction/start": {
3376
+ const state2 = footerState();
3377
+ const pct = typeof state2.contextUsed === "number" && typeof state2.contextWindow === "number" && state2.contextWindow > 0 ? Math.round(state2.contextUsed / state2.contextWindow * 100) : 80;
3378
+ compactionBefore = typeof state2.contextUsed === "number" ? state2.contextUsed : void 0;
3379
+ compactionShadowed = void 0;
3380
+ ui.showNotice?.(
3381
+ `Context ${String(pct)}% full: summarizing the conversation\u2026`
3382
+ );
3383
+ return;
3384
+ }
3385
+ case "compaction/summary": {
3386
+ const n = Number(event.data?.shadowedTokenCount);
3387
+ if (Number.isFinite(n)) compactionShadowed = n;
3388
+ return;
3389
+ }
3390
+ case "compaction/end": {
3391
+ if (event.data?.error === void 0) {
3392
+ if (compactionBefore !== void 0 && compactionShadowed !== void 0) {
3393
+ const after = Math.max(0, compactionBefore - compactionShadowed);
3394
+ ui.addChat(new Text4(dim(`Compacted: ${formatK(compactionBefore)} \u2192 ${formatK(after)} tokens`), 0, 0));
3395
+ } else {
3396
+ ui.addChat(new Text4(dim("Compacted."), 0, 0));
3397
+ }
3398
+ }
3399
+ compactionBefore = void 0;
3400
+ compactionShadowed = void 0;
3401
+ ui.requestRender();
3402
+ return;
3403
+ }
3404
+ case "turn/end": {
3405
+ hideWorking();
3406
+ closeLive();
3407
+ for (const comp of tools.values()) comp.cancel();
3408
+ const window = agent.session?.requestContext?.()?.contextWindow;
3409
+ if (typeof window === "number" && window > 0) {
3410
+ ui.footer.set({
3411
+ contextUsed: lastInputTokens + lastCacheTokens + lastOutputTokens,
3412
+ contextWindow: window
3413
+ });
3414
+ }
3415
+ const reason = event.data.reason;
3416
+ const wallSec = Math.max(0, (Date.now() - turnStartWall) / 1e3);
3417
+ if (reason?.kind === "error") {
3418
+ void showLlmError(reason.error);
3419
+ ui.onTurnEnd?.({
3420
+ tools: [],
3421
+ wallSec,
3422
+ outputTokens: turnOutput,
3423
+ cancelled: false,
3424
+ error: true
3425
+ });
3426
+ } else if (reason?.kind === "aborted") {
3427
+ ui.onTurnEnd?.({
3428
+ tools: [],
3429
+ wallSec,
3430
+ outputTokens: 0,
3431
+ cancelled: true,
3432
+ error: false
3433
+ });
3434
+ } else {
3435
+ const ordered = turnToolIds.map((id) => {
3436
+ const comp = tools.get(id);
3437
+ if (comp === void 0) return void 0;
3438
+ return {
3439
+ tool: comp.tool,
3440
+ ok: comp.doneOk ?? false,
3441
+ seconds: comp.seconds ?? 0,
3442
+ comp,
3443
+ breakBefore: turnBreaks.has(id),
3444
+ hidden: comp.tool === "ask_user_question"
3445
+ };
3446
+ }).filter((t) => t !== void 0);
3447
+ reportTurnChanges(ordered);
3448
+ ui.onTurnEnd?.({
3449
+ tools: ordered,
3450
+ wallSec,
3451
+ outputTokens: turnOutput,
3452
+ cancelled: false,
3453
+ error: false
3454
+ });
3455
+ void triggerSuggest();
3456
+ }
3457
+ turnToolIds = [];
3458
+ turnBreaks = /* @__PURE__ */ new Set();
3459
+ needBreak = false;
3460
+ turnOutput = 0;
3461
+ ui.requestRender();
3462
+ return;
3463
+ }
3464
+ default:
3465
+ return;
3466
+ }
3467
+ });
3468
+ const restored = restoreTasks(agent.session);
3469
+ if (restored !== void 0) setUiTasks(restored);
3470
+ ui.footer.set(spend.readings);
3471
+ function suggestionsEnabled() {
3472
+ try {
3473
+ const home = process.env.DSH_HOME;
3474
+ if (home === void 0 || home === "") return true;
3475
+ const raw = readFileSync14(configReadPath(home), "utf8");
3476
+ const doc = JSON.parse(raw);
3477
+ return doc.suggestions ?? true;
3478
+ } catch {
3479
+ return true;
3480
+ }
3481
+ }
3482
+ function fastRoute() {
3483
+ try {
3484
+ const home = process.env.DSH_HOME;
3485
+ if (home !== void 0 && home !== "") {
3486
+ const raw = readFileSync14(configReadPath(home), "utf8");
3487
+ const doc = JSON.parse(raw);
3488
+ const fast = doc.models?.fast ?? doc.models?.main;
3489
+ if (fast?.provider !== void 0 && fast?.model !== void 0) {
3490
+ return { provider: fast.provider, model: fast.model };
3491
+ }
3492
+ }
3493
+ } catch {
3494
+ }
3495
+ return void 0;
3496
+ }
3497
+ async function triggerSuggest() {
3498
+ const modes = ctx.get("modes");
3499
+ if (!shouldSuggest({ enabled: suggestionsEnabled(), plan: modes?.plan === true, lastUser, answer: currentAnswer })) return;
3500
+ const route = fastRoute();
3501
+ if (route === void 0) return;
3502
+ const llm = ctx.get("llm");
3503
+ if (llm === void 0) return;
3504
+ suggestController?.abort();
3505
+ const controller = new AbortController();
3506
+ suggestController = controller;
3507
+ const timer = setTimeout(() => controller.abort(), 4e3);
3508
+ try {
3509
+ let off = false;
3510
+ try {
3511
+ const info = await llm.resolveModelInfo?.(route.provider, route.model);
3512
+ off = (info?.reasoning?.efforts ?? []).some((e) => e.id === "off");
3513
+ } catch {
3514
+ off = false;
3515
+ }
3516
+ const last = mainRequests.last;
3517
+ const effort = off ? { effort: "off" } : {};
3518
+ const request = last !== void 0 && sameRoute(route, last) ? sharedPrefixRequest(last, currentAnswer, effort) : standaloneRequest(route, lastUser, currentAnswer.slice(-800), effort);
3519
+ let text2 = "";
3520
+ for await (const chunk of llm.stream({ ...request, signal: controller.signal })) {
3521
+ if (controller.signal.aborted) return;
3522
+ if (chunk.type === "text-delta" && chunk.text !== void 0) text2 += chunk.text;
3523
+ }
3524
+ if (controller.signal.aborted) return;
3525
+ const words = text2.trim().split(/\s+/).filter((w) => w !== "").slice(0, 8);
3526
+ if (words.length === 0) return;
3527
+ if (controller.signal.aborted) return;
3528
+ ui.setGhost?.(words.join(" "));
3529
+ ui.requestRender();
3530
+ } catch {
3531
+ } finally {
3532
+ clearTimeout(timer);
3533
+ if (suggestController === controller) suggestController = void 0;
3534
+ }
3535
+ }
3536
+ return () => {
3537
+ hideWorking();
3538
+ closeLive();
3539
+ for (const comp of tools.values()) comp.cancel();
3540
+ ui.requestRender();
3541
+ offRequest();
3542
+ offAgentCreated();
3543
+ offAgentStatus();
3544
+ offAgentDisposed();
3545
+ if (subagentCount > 0) {
3546
+ ui.footer.set({ subagents: 0 });
3547
+ ui.requestRender();
3548
+ }
3549
+ offStream();
3550
+ offSession();
3551
+ };
3552
+ }
3553
+ function attach(ctx, agent, ui) {
3554
+ const started = /* @__PURE__ */ new Set();
3555
+ const todoToolIds = /* @__PURE__ */ new Set();
3556
+ const taskPanel = new TaskPanel(ui.icons);
3557
+ const showTasks = (raw) => {
3558
+ const tasks = taskItems(raw);
3559
+ if (tasks === void 0) return;
3560
+ taskPanel.setTasks(tasks);
3561
+ const lines = taskPanel.plainLines();
3562
+ if (lines.length > 0) ui.screen.write(`${lines.join("\n")}
3563
+ `);
3564
+ };
3565
+ let compactionBefore;
3566
+ let compactionShadowed;
3567
+ const offStream = ctx.on("agent/assistant-stream", ({ agent: subject, frame }) => {
3568
+ if (subject !== agent) return;
3569
+ const f = frame;
3570
+ if (f.type === "start") {
3571
+ ui.reasoning.end();
3572
+ return;
3573
+ }
3574
+ if (f.type === "end") {
3575
+ ui.reasoning.end();
3576
+ return;
3577
+ }
3578
+ const chunk = f.chunk;
3579
+ if (chunk === void 0) return;
3580
+ switch (chunk.type) {
3581
+ case "reasoning-delta":
3582
+ ui.reasoning.push(chunk.text);
3583
+ return;
3584
+ case "text-delta":
3585
+ ui.text.push(chunk.text);
3586
+ return;
3587
+ case "block-start":
3588
+ if (chunk.blockType !== "reasoning") ui.reasoning.end();
3589
+ return;
3590
+ case "block-end":
3591
+ if (chunk.block.type === "reasoning") ui.reasoning.end();
3592
+ else if (chunk.block.type === "text") ui.text.end();
3593
+ return;
3594
+ case "tool-call-delta": {
3595
+ const id = String(chunk.id);
3596
+ if (chunk.name === "todo_write") todoToolIds.add(id);
3597
+ if (todoToolIds.has(id)) return;
3598
+ if (chunk.name !== void 0 && !started.has(id)) {
3599
+ started.add(id);
3600
+ ui.tools.start(id, chunk.name);
3601
+ }
3602
+ if (chunk.argumentsDelta) ui.tools.args(id, chunk.argumentsDelta);
3603
+ return;
3604
+ }
3605
+ case "usage":
3606
+ case "finish":
3607
+ return;
3608
+ default:
3609
+ return;
3610
+ }
3611
+ });
3612
+ const offSession = ctx.on("session/event", (session, event) => {
3613
+ if (session !== agent.session) return;
3614
+ switch (event.type) {
3615
+ case "tool/call": {
3616
+ const id = String(event.data.callId);
3617
+ if (event.data.name === "todo_write") {
3618
+ todoToolIds.add(id);
3619
+ return;
3620
+ }
3621
+ if (started.has(id)) return;
3622
+ started.add(id);
3623
+ ui.tools.start(id, event.data.name);
3624
+ if (event.data.arguments) ui.tools.args(id, event.data.arguments);
3625
+ return;
3626
+ }
3627
+ case "tool/result": {
3628
+ const block = event.data.message?.content?.[0];
3629
+ if (block === void 0 || block.type !== "tool-result") return;
3630
+ if (todoToolIds.delete(String(block.toolCallId))) return;
3631
+ const output = block.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
3632
+ ui.tools.result(String(block.toolCallId), block.isError !== true, output);
3633
+ started.delete(String(block.toolCallId));
3634
+ return;
3635
+ }
3636
+ case "todo/write":
3637
+ showTasks(event.data?.todos);
3638
+ return;
3639
+ case "compaction/start":
3640
+ ui.screen.write("\nsummarizing the conversation\u2026\n");
3641
+ return;
3642
+ case "compaction/summary": {
3643
+ const n = Number(event.data?.shadowedTokenCount);
3644
+ if (Number.isFinite(n)) compactionShadowed = n;
3645
+ return;
3646
+ }
3647
+ case "compaction/end":
3648
+ if (compactionBefore !== void 0 && compactionShadowed !== void 0) {
3649
+ const after = Math.max(0, compactionBefore - compactionShadowed);
3650
+ ui.screen.write(`${dim(`Compacted: ${formatK(compactionBefore)} \u2192 ${formatK(after)} tokens`)}
3651
+ `);
3652
+ } else {
3653
+ ui.screen.write(`${dim("Compacted.")}
3654
+ `);
3655
+ }
3656
+ compactionBefore = void 0;
3657
+ compactionShadowed = void 0;
3658
+ return;
3659
+ case "turn/end": {
3660
+ ui.reasoning.end();
3661
+ ui.text.end();
3662
+ const reason = event.data.reason;
3663
+ if (reason?.kind === "error") {
3664
+ const settings = readSettingsRoute();
3665
+ const lines = describeLlmError(reason.error ?? {}, {
3666
+ provider: settings?.providerDisplayName ?? settings?.provider ?? "the model server",
3667
+ model: settings?.model ?? "",
3668
+ ...settings?.baseUrl !== void 0 ? { baseUrl: settings.baseUrl } : {}
3669
+ });
3670
+ ui.screen.write(`
3671
+ ${ui.icons.fail} ${lines.message}
3672
+ ${dim(lines.hint)}
3673
+ `);
3674
+ void appendErrorLog(reason.error ?? {});
3675
+ } else if (reason?.kind === "aborted") {
3676
+ ui.screen.write("\n- cancelled\n");
3677
+ }
3678
+ return;
3679
+ }
3680
+ default:
3681
+ return;
3682
+ }
3683
+ });
3684
+ const restored = restoreTasks(agent.session);
3685
+ if (restored !== void 0) showTasks(restored);
3686
+ return () => {
3687
+ offStream();
3688
+ offSession();
3689
+ };
3690
+ }
3691
+ function apply(ctx) {
3692
+ const service = {};
3693
+ if (appEnv("HEADLESS") === "1") {
3694
+ ctx.provide(BRUINE_RENDER_SERVICE, service);
3695
+ return;
3696
+ }
3697
+ if (process.stdin.isTTY === true && process.stdout.isTTY === true) {
3698
+ ctx.provide(BRUINE_RENDER_SERVICE, service);
3699
+ ctx.inject(["bruineRepl"], (c2) => {
3700
+ const repl = c2.bruineRepl;
3701
+ if (repl?.agent !== void 0 && repl.ui !== void 0) {
3702
+ attachTui(ctx, repl.agent, repl.ui, service, () => repl.agent);
3703
+ }
3704
+ });
3705
+ return;
3706
+ }
3707
+ const ui = createUi(stdoutScreen());
3708
+ service.screen = ui;
3709
+ ctx.provide(BRUINE_RENDER_SERVICE, service);
3710
+ ctx.inject(["bruineRepl"], (c2) => {
3711
+ const repl = c2.bruineRepl;
3712
+ if (repl?.agent !== void 0) attach(ctx, repl.agent, ui);
3713
+ });
3714
+ }
3715
+ export {
3716
+ BRUINE_RENDER_SERVICE,
3717
+ apply,
3718
+ attach,
3719
+ attachTui,
3720
+ createUi,
3721
+ name,
3722
+ stdoutScreen
3723
+ };