@fx-/chat-ink 0.0.0-stage → 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.
package/dist/index.js ADDED
@@ -0,0 +1,3918 @@
1
+ import stringWidth from "string-width";
2
+ import { createCoreHooks } from "@fx-/chat-core/hooks";
3
+ import { approxTokenCount, countActivity, earlierLabel, formatClockTime, formatDuration, formatElapsed, formatTokensPerSecond, getBlockTimelineBehavior, isBlockActive, isCoreBlockType, livePhase, lookupExtension, planFooter, planGroupHeader, planMessage, positiveTimestamp, shouldShowTimer, statusLabel, summarizeBlock, toolDisplayInput, toolHeadline, toolLabel, toolResultText } from "@fx-/chat-core";
4
+ import { layoutJson, looksLikeJson, parseLooseJson } from "@fx-/chat-core/json";
5
+ import { Buffer } from "node:buffer";
6
+
7
+ //#region src/lines/measure.ts
8
+ const segmenter = new Intl.Segmenter();
9
+ /**
10
+ * `width`'s results (default: `string-width`, which Ink measures with), with
11
+ * each distinct grapheme cluster measured once. The package sums independent
12
+ * per-cluster widths, so caching changes no result; on Bun its emoji check is
13
+ * pathologically slow and the same few clusters are measured over and over.
14
+ * Text with ANSI escapes goes to `width` as it is (stripping spans clusters).
15
+ * The cache is bounded and starts over when full.
16
+ */
17
+ function cachedMeasure(width$2 = stringWidth, limit = 4096) {
18
+ const cache = new Map();
19
+ return (text) => {
20
+ if (/^[\u0020-\u007E]*$/.test(text)) return text.length;
21
+ if (text.includes("\x1B") || text.includes("›")) return width$2(text);
22
+ let total = 0;
23
+ for (const { segment } of segmenter.segment(text)) {
24
+ let cluster = cache.get(segment);
25
+ if (cluster === void 0) {
26
+ cluster = width$2(segment);
27
+ if (cache.size >= limit) cache.clear();
28
+ cache.set(segment, cluster);
29
+ }
30
+ total += cluster;
31
+ }
32
+ return total;
33
+ };
34
+ }
35
+ const defaultMeasure = cachedMeasure();
36
+ /** Grapheme clusters of `text` (so a wide or combined character is never split). */
37
+ function graphemes(text) {
38
+ return Array.from(segmenter.segment(text), (s) => s.segment);
39
+ }
40
+ /** The first grapheme cluster of `text` ('' when empty), segmenting no further. */
41
+ function firstGrapheme(text) {
42
+ for (const { segment } of segmenter.segment(text)) return segment;
43
+ return "";
44
+ }
45
+ /** Cut `text` to at most `cells`, ending with `…` when cut. */
46
+ function truncate(text, cells$1, measure = defaultMeasure) {
47
+ if (measure(text) <= cells$1) return text;
48
+ if (cells$1 <= 0) return "";
49
+ let out = "";
50
+ let width$2 = 0;
51
+ for (const g of graphemes(text)) {
52
+ const w = measure(g);
53
+ if (width$2 + w > cells$1 - 1) break;
54
+ out += g;
55
+ width$2 += w;
56
+ }
57
+ return `${out}…`;
58
+ }
59
+ /** Split `text` into pieces of at most `cells` each, never inside a grapheme. */
60
+ function hardSplit(text, cells$1, measure = defaultMeasure) {
61
+ const pieces = [];
62
+ let current = "";
63
+ let width$2 = 0;
64
+ for (const g of graphemes(text)) {
65
+ const w = measure(g);
66
+ if (width$2 + w > cells$1 && current) {
67
+ pieces.push(current);
68
+ current = "";
69
+ width$2 = 0;
70
+ }
71
+ current += g;
72
+ width$2 += w;
73
+ }
74
+ if (current || pieces.length === 0) pieces.push(current);
75
+ return pieces;
76
+ }
77
+ /**
78
+ * Wrap one paragraph (no newlines) to `width` cells at spaces. A word longer
79
+ * than the line is split with a `hard` join; other continuation rows are
80
+ * `soft` (a copy rejoins them with a space).
81
+ */
82
+ function wrapText(text, width$2, measure = defaultMeasure) {
83
+ const max = Math.max(1, width$2);
84
+ const words$1 = text.split(" ");
85
+ const rows = [];
86
+ let current = "";
87
+ let currentWidth = 0;
88
+ let join;
89
+ const push = (next) => {
90
+ rows.push(join ? {
91
+ text: current,
92
+ wrap: join
93
+ } : { text: current });
94
+ current = "";
95
+ currentWidth = 0;
96
+ join = next;
97
+ };
98
+ for (const word of words$1) {
99
+ const w = measure(word);
100
+ const gap = current ? 1 : 0;
101
+ if (current && currentWidth + gap + w > max) push("soft");
102
+ if (w > max) {
103
+ const pieces = hardSplit(word, max, measure);
104
+ pieces.forEach((piece, i) => {
105
+ if (i > 0) push("hard");
106
+ current = piece;
107
+ currentWidth = measure(piece);
108
+ });
109
+ continue;
110
+ }
111
+ current = current ? `${current} ${word}` : word;
112
+ currentWidth += (current === word ? 0 : 1) + w;
113
+ }
114
+ push(void 0);
115
+ return rows;
116
+ }
117
+ /** Wrap text with newlines: each line wrapped on its own (hard line breaks stay). */
118
+ function wrapLines(text, width$2, measure = defaultMeasure) {
119
+ return text.split("\n").flatMap((line) => wrapText(line, width$2, measure));
120
+ }
121
+
122
+ //#endregion
123
+ //#region src/editor.ts
124
+ const emptyEditor = {
125
+ value: "",
126
+ caret: 0
127
+ };
128
+ const clamp = (n, max) => Math.max(0, Math.min(n, max));
129
+ /** Start and end offsets of the line containing `caret`. */
130
+ function lineBounds(value, caret) {
131
+ const start = value.lastIndexOf("\n", caret - 1) + 1;
132
+ const nl = value.indexOf("\n", caret);
133
+ return [start, nl === -1 ? value.length : nl];
134
+ }
135
+ /**
136
+ * The grapheme boundaries of the line holding `at`, both line ends included.
137
+ * A newline always separates graphemes, so only the one line is segmented.
138
+ */
139
+ function boundaries(value, at) {
140
+ const [start, end] = lineBounds(value, at);
141
+ const out = [start];
142
+ let offset = start;
143
+ for (const g of graphemes(value.slice(start, end))) {
144
+ offset += g.length;
145
+ out.push(offset);
146
+ }
147
+ return out;
148
+ }
149
+ /** `caret` clamped to the value and moved back to the start of the grapheme it is inside. */
150
+ function caretBoundary(value, caret) {
151
+ const at = clamp(caret, value.length);
152
+ return lastBelow(boundaries(value, at), at + 1);
153
+ }
154
+ /** The last of ascending `list` (which starts below `limit`) that is below `limit`. */
155
+ function lastBelow(list$1, limit) {
156
+ let found = list$1[0];
157
+ for (const b of list$1) if (b < limit) found = b;
158
+ return found;
159
+ }
160
+ /** The boundary one grapheme before `caret` (a boundary), or 0. */
161
+ function before(value, caret) {
162
+ if (caret === 0) return 0;
163
+ if (value[caret - 1] === "\n") return caret - 1;
164
+ return lastBelow(boundaries(value, caret), caret);
165
+ }
166
+ /** The boundary one grapheme after `caret` (a boundary), or the end. */
167
+ function after(value, caret) {
168
+ if (caret >= value.length) return value.length;
169
+ if (value[caret] === "\n") return caret + 1;
170
+ return boundaries(value, caret).find((b) => b > caret);
171
+ }
172
+ /**
173
+ * Split `text` at a caret for drawing: before it, the grapheme under it ('' at
174
+ * the end) and after it. The grapheme is whole, so the caret cell covers all
175
+ * of it (two cells for a wide one) and no character is split around it.
176
+ */
177
+ function splitAtCaret(text, caret) {
178
+ const at = caretBoundary(text, caret);
179
+ const under = firstGrapheme(text.slice(at));
180
+ return [
181
+ text.slice(0, at),
182
+ under,
183
+ text.slice(at + under.length)
184
+ ];
185
+ }
186
+ const snapped = (state) => {
187
+ const caret = caretBoundary(state.value, state.caret);
188
+ return caret === state.caret ? state : {
189
+ ...state,
190
+ caret
191
+ };
192
+ };
193
+ /**
194
+ * Insert at the caret. Text that joins the grapheme after it (a combining
195
+ * mark's base, half a flag) leaves the caret after the joined grapheme.
196
+ */
197
+ function insert(state, text) {
198
+ const { caret } = snapped(state);
199
+ const value = state.value.slice(0, caret) + text + state.value.slice(caret);
200
+ const end = caret + text.length;
201
+ const at = caretBoundary(value, end);
202
+ return {
203
+ value,
204
+ caret: at === end ? end : after(value, at)
205
+ };
206
+ }
207
+ function backspace(state) {
208
+ const { caret } = snapped(state);
209
+ if (caret === 0) return {
210
+ ...state,
211
+ caret
212
+ };
213
+ const start = before(state.value, caret);
214
+ return {
215
+ value: state.value.slice(0, start) + state.value.slice(caret),
216
+ caret: start
217
+ };
218
+ }
219
+ function deleteForward(state) {
220
+ const { caret } = snapped(state);
221
+ const end = after(state.value, caret);
222
+ return {
223
+ value: state.value.slice(0, caret) + state.value.slice(end),
224
+ caret
225
+ };
226
+ }
227
+ /** Move the caret `delta` graphemes (negative: left), stopping at the ends. */
228
+ function moveBy(state, delta) {
229
+ let { caret } = snapped(state);
230
+ const step = delta < 0 ? before : after;
231
+ for (let n = Math.abs(delta); n > 0; n--) {
232
+ const next = step(state.value, caret);
233
+ if (next === caret) break;
234
+ caret = next;
235
+ }
236
+ return {
237
+ ...state,
238
+ caret
239
+ };
240
+ }
241
+ function lineStart(state) {
242
+ return {
243
+ ...state,
244
+ caret: lineBounds(state.value, clamp(state.caret, state.value.length))[0]
245
+ };
246
+ }
247
+ function lineEnd(state) {
248
+ return {
249
+ ...state,
250
+ caret: lineBounds(state.value, clamp(state.caret, state.value.length))[1]
251
+ };
252
+ }
253
+ /** Delete from the line start to the caret. */
254
+ function killToLineStart(state) {
255
+ const { caret } = snapped(state);
256
+ const [start] = lineBounds(state.value, caret);
257
+ return {
258
+ value: state.value.slice(0, start) + state.value.slice(caret),
259
+ caret: start
260
+ };
261
+ }
262
+ /** Delete from the caret to the line end. */
263
+ function killToLineEnd(state) {
264
+ const { caret } = snapped(state);
265
+ const [, end] = lineBounds(state.value, caret);
266
+ return {
267
+ value: state.value.slice(0, caret) + state.value.slice(end),
268
+ caret
269
+ };
270
+ }
271
+ const blank$2 = (g) => /^\s+$/.test(g);
272
+ /** The start of the whitespace-separated word before `caret` (skipping whitespace first). */
273
+ function wordStart(value, caret) {
274
+ let i = caret;
275
+ const skip = (word) => {
276
+ while (i > 0) {
277
+ const prev = before(value, i);
278
+ if (blank$2(value.slice(prev, i)) === word) return;
279
+ i = prev;
280
+ }
281
+ };
282
+ skip(false);
283
+ skip(true);
284
+ return i;
285
+ }
286
+ /** Delete the word before the caret (and the whitespace after it). */
287
+ function deleteWordBack(state) {
288
+ const { caret } = snapped(state);
289
+ const i = wordStart(state.value, caret);
290
+ return {
291
+ value: state.value.slice(0, i) + state.value.slice(caret),
292
+ caret: i
293
+ };
294
+ }
295
+ /**
296
+ * Move the caret one line up (`-1`) or down (`+1`), keeping its cell column
297
+ * where the target line is long enough. Returns `undefined` at the first/last
298
+ * line, so the caller can fall through to history navigation.
299
+ */
300
+ function moveLine(state, direction, measure = defaultMeasure) {
301
+ return moveVisual(state, visualRows(state.value), direction, measure);
302
+ }
303
+ /** Split into lines, each with the caret column (an offset into the line) when the caret is on it. */
304
+ function caretLines(state) {
305
+ const { caret } = snapped(state);
306
+ const out = [];
307
+ let offset = 0;
308
+ for (const text of state.value.split("\n")) {
309
+ const end = offset + text.length;
310
+ out.push(caret >= offset && caret <= end ? {
311
+ text,
312
+ caret: caret - offset
313
+ } : { text });
314
+ offset = end + 1;
315
+ }
316
+ return out;
317
+ }
318
+ /** Jump to the start of the previous whitespace-separated word. */
319
+ function wordLeft(state) {
320
+ return {
321
+ ...state,
322
+ caret: wordStart(state.value, snapped(state).caret)
323
+ };
324
+ }
325
+ /** Jump past the end of the next whitespace-separated word. */
326
+ function wordRight(state) {
327
+ const { value } = state;
328
+ let i = snapped(state).caret;
329
+ const skip = (word) => {
330
+ while (i < value.length) {
331
+ const next = after(value, i);
332
+ if (blank$2(value.slice(i, next)) === word) return;
333
+ i = next;
334
+ }
335
+ };
336
+ skip(false);
337
+ skip(true);
338
+ return {
339
+ ...state,
340
+ caret: i
341
+ };
342
+ }
343
+ /**
344
+ * Lay the value out in rows: at newlines, and (with a width) wherever the
345
+ * next grapheme would pass `width` cells — never inside a wide character.
346
+ */
347
+ function visualRows(value, width$2, measure = defaultMeasure) {
348
+ const rows = [];
349
+ let offset = 0;
350
+ for (const line of value.split("\n")) {
351
+ let start = offset;
352
+ let cells$1 = 0;
353
+ if (width$2 !== void 0) {
354
+ let at = offset;
355
+ for (const g of graphemes(line)) {
356
+ const w = measure(g);
357
+ if (cells$1 + w > Math.max(1, width$2) && at > start) {
358
+ rows.push({
359
+ start,
360
+ end: at,
361
+ soft: true
362
+ });
363
+ start = at;
364
+ cells$1 = 0;
365
+ }
366
+ cells$1 += w;
367
+ at += g.length;
368
+ }
369
+ }
370
+ rows.push({
371
+ start,
372
+ end: offset + line.length,
373
+ soft: false
374
+ });
375
+ offset += line.length + 1;
376
+ }
377
+ return rows;
378
+ }
379
+ /** The row holding the caret: a soft row's end belongs to the next row. */
380
+ function caretRow(rows, caret) {
381
+ const i = rows.findIndex((row) => caret >= row.start && (caret < row.end || caret === row.end && !row.soft));
382
+ return i === -1 ? rows.length - 1 : i;
383
+ }
384
+ /**
385
+ * Move the caret one visual row up or down, keeping its cell column (landing
386
+ * at the end of a shorter row). Undefined at the first or last row.
387
+ */
388
+ function moveVisual(state, rows, direction, measure = defaultMeasure) {
389
+ const at = caretBoundary(state.value, state.caret);
390
+ const from = caretRow(rows, at);
391
+ const target = rows[from + direction];
392
+ if (!target) return void 0;
393
+ const row = rows[from];
394
+ const column = measure(state.value.slice(row.start, at));
395
+ let caret = target.start;
396
+ let cells$1 = 0;
397
+ for (const g of graphemes(state.value.slice(target.start, target.end))) {
398
+ const w = measure(g);
399
+ if (cells$1 + w > column) break;
400
+ if (target.soft && caret + g.length === target.end) break;
401
+ cells$1 += w;
402
+ caret += g.length;
403
+ }
404
+ return {
405
+ ...state,
406
+ caret
407
+ };
408
+ }
409
+ const toEntry = (entry) => typeof entry === "string" ? { text: entry } : entry;
410
+ function createHistory(entries = []) {
411
+ const list$1 = entries.map(toEntry);
412
+ return {
413
+ entries: list$1,
414
+ index: list$1.length,
415
+ draft: { text: "" }
416
+ };
417
+ }
418
+ function pushHistory(history, entry) {
419
+ const next = toEntry(entry);
420
+ const last = history.entries.at(-1);
421
+ const attached = Boolean(next.attachments?.length);
422
+ const repeat = last?.text === next.text && !attached && !last.attachments?.length;
423
+ const entries = (next.text.trim() || attached) && !repeat ? [...history.entries, next] : history.entries;
424
+ return {
425
+ entries,
426
+ index: entries.length,
427
+ draft: { text: "" }
428
+ };
429
+ }
430
+ /**
431
+ * Replace the entries (the host's list changed), keeping the draft and a
432
+ * browse in progress (clamped to the new list).
433
+ */
434
+ function withEntries(history, entries) {
435
+ const list$1 = entries.map(toEntry);
436
+ const browsing = history.index < history.entries.length;
437
+ return {
438
+ entries: list$1,
439
+ index: browsing ? Math.min(history.index, list$1.length) : list$1.length,
440
+ draft: history.draft
441
+ };
442
+ }
443
+ /** Step through history; returns the new history and the entry to show, or undefined at an end. */
444
+ function stepHistory(history, direction, current) {
445
+ const index = history.index + direction;
446
+ if (index < 0 || index > history.entries.length) return void 0;
447
+ const draft = history.index === history.entries.length ? toEntry(current) : history.draft;
448
+ const entry = index === history.entries.length ? draft : history.entries[index];
449
+ return {
450
+ history: {
451
+ ...history,
452
+ index,
453
+ draft
454
+ },
455
+ value: entry.text,
456
+ ...entry.attachments?.length ? { attachments: entry.attachments } : {}
457
+ };
458
+ }
459
+
460
+ //#endregion
461
+ //#region src/runtime.ts
462
+ const UNBOUND = "@fx-/chat-ink is not bound to React and Ink yet: call createChatInk({ React, ink }) first.";
463
+ /** A stand-in that names the missing call on any use. */
464
+ function unbound() {
465
+ return new Proxy({}, { get() {
466
+ throw new Error(UNBOUND);
467
+ } });
468
+ }
469
+ let React = unbound();
470
+ let ink = unbound();
471
+ /** The core render hooks, bound to the host's React. */
472
+ let hooks = unbound();
473
+ let bound;
474
+ /** Bind the package to the host's React and Ink. */
475
+ function bindRuntime(deps) {
476
+ if (bound) {
477
+ if (bound.React === deps.React && bound.ink === deps.ink) return;
478
+ throw new Error("@fx-/chat-ink is already bound to other React and Ink instances; one process renders with one.");
479
+ }
480
+ bound = deps;
481
+ React = deps.React;
482
+ ink = deps.ink;
483
+ hooks = createCoreHooks(deps.React);
484
+ }
485
+ /** Whether `createChatInk` has run. */
486
+ function isBound() {
487
+ return bound !== void 0;
488
+ }
489
+
490
+ //#endregion
491
+ //#region src/sanitize.ts
492
+ /**
493
+ * Content must never drive the terminal. Model output, tool output, titles
494
+ * and other host- or model-provided strings may carry escape sequences (an
495
+ * OSC 52 clipboard write, an OSC 8 link, SGR colours, cursor moves) that Ink
496
+ * would pass straight through. Everything this package draws is cleaned
497
+ * here first; its own styling travels as Ink props, never as escapes in text.
498
+ *
499
+ * Removed: whole ESC/C1 sequences — CSI, OSC, DCS, SOS, PM, APC (a string
500
+ * command up to its terminator: BEL, `ESC \` or ST), and any other ESC
501
+ * sequence — then every remaining C0 control but `\n` and `\t` (so `\r` and
502
+ * BEL go), DEL, and the C1 controls U+0080–U+009F. An unterminated string
503
+ * command loses its introducer only: the rest is plain text once no control
504
+ * can read it, and the scan stays linear.
505
+ */
506
+ const ESC$2 = "\\u001B";
507
+ const BEL = "\\u0007";
508
+ const ST = "\\u009C";
509
+ const STRING_START = `(?:${ESC$2}[P\\]X^_]|[\\u0090\\u0098\\u009D-\\u009F])`;
510
+ const STRING_BODY = `[^${BEL}${ESC$2}\\u0090\\u0098\\u009C-\\u009F]*`;
511
+ const STRING_END = `(?:${BEL}|${ESC$2}\\\\|${ST})`;
512
+ const CSI = `(?:${ESC$2}\\[|\\u009B)[\\u0030-\\u003F]*[\\u0020-\\u002F]*[\\u0040-\\u007E]`;
513
+ const OTHER_ESC = `${ESC$2}[\\u0020-\\u002F]*[\\u0030-\\u007E]`;
514
+ const CONTROL = "[\\u0000-\\u0008\\u000B-\\u001F\\u007F-\\u009F]";
515
+ const SEQUENCES = new RegExp(`${STRING_START}${STRING_BODY}${STRING_END}|${CSI}|${OTHER_ESC}|${CONTROL}`, "g");
516
+ const HAS_CONTROL = new RegExp(CONTROL);
517
+ /** `text` without terminal control sequences or control characters (bar `\n` and `\t`). */
518
+ function stripControls(text) {
519
+ return HAS_CONTROL.test(text) ? text.replace(SEQUENCES, "") : text;
520
+ }
521
+ const isPlain = (value) => {
522
+ const proto = Object.getPrototypeOf(value);
523
+ return proto === Object.prototype || proto === null;
524
+ };
525
+ const CONTROLS = new RegExp(CONTROL, "g");
526
+ /** `key` with each control character spelled as visible `\uXXXX` text. */
527
+ const escapeControls = (key) => key.replace(CONTROLS, (c) => `\\u${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`);
528
+ /**
529
+ * Clean keys for `keys`, one per key, all distinct. A clean key keeps its
530
+ * name; a dirty one takes its stripped form unless that is taken, then its
531
+ * visibly escaped form, then that with ` (2)`, ` (3)`… — so no value is ever
532
+ * dropped by two keys cleaning to the same name.
533
+ */
534
+ const cleanKeys = (keys) => {
535
+ const taken = new Set(keys.filter((key) => stripControls(key) === key));
536
+ return keys.map((key) => {
537
+ if (stripControls(key) === key) return key;
538
+ let name = stripControls(key);
539
+ if (taken.has(name)) name = escapeControls(key);
540
+ const base = name;
541
+ for (let n = 2; taken.has(name); n++) name = `${base} (${n})`;
542
+ taken.add(name);
543
+ return name;
544
+ });
545
+ };
546
+ /**
547
+ * `value` with `stripControls` applied to every string in it (object keys
548
+ * included), through arrays and plain objects; other objects (Map, Set,
549
+ * Date, class instances) pass through as they are. Anything unchanged keeps
550
+ * its identity, so clean content costs a scan and no copies. Keys that clean
551
+ * to the same name all survive (see `cleanKeys`); a `__proto__` key stays an
552
+ * own property, never a prototype; a cycle is left as it is where it closes.
553
+ */
554
+ function stripControlsDeep(value) {
555
+ return deep(value, new Set());
556
+ }
557
+ function deep(value, path) {
558
+ if (typeof value === "string") return stripControls(value);
559
+ if (value === null || typeof value !== "object" || path.has(value)) return value;
560
+ if (!Array.isArray(value) && !isPlain(value)) return value;
561
+ path.add(value);
562
+ try {
563
+ if (Array.isArray(value)) {
564
+ let out;
565
+ value.forEach((item, i) => {
566
+ const next = deep(item, path);
567
+ if (next !== item) {
568
+ out ??= value.slice();
569
+ out[i] = next;
570
+ }
571
+ });
572
+ return out ?? value;
573
+ }
574
+ const entries = Object.entries(value);
575
+ const keys = cleanKeys(entries.map(([key]) => key));
576
+ let changed = false;
577
+ const values = entries.map(([key, item], i) => {
578
+ const next = deep(item, path);
579
+ if (next !== item || keys[i] !== key) changed = true;
580
+ return next;
581
+ });
582
+ if (!changed) return value;
583
+ return Object.fromEntries(keys.map((key, i) => [key, values[i]]));
584
+ } finally {
585
+ path.delete(value);
586
+ }
587
+ }
588
+
589
+ //#endregion
590
+ //#region src/text.tsx
591
+ function clean(node) {
592
+ if (typeof node === "string") return stripControls(node);
593
+ if (Array.isArray(node)) return node.map(clean);
594
+ if (React.isValidElement(node) && node.type === React.Fragment) return withChildren(node.props.children, (kids) => React.cloneElement(node, void 0, ...kids));
595
+ return node;
596
+ }
597
+ /** Rebuild with cleaned children, passed as arguments (as JSX does) so none needs a key. */
598
+ function withChildren(children, build) {
599
+ return build(Array.isArray(children) ? children.map(clean) : [clean(children)]);
600
+ }
601
+ function Text({ children,...props }) {
602
+ return withChildren(children, (kids) => React.createElement(ink.Text, props, ...kids));
603
+ }
604
+
605
+ //#endregion
606
+ //#region src/theme.ts
607
+ /** Greyscale defaults: no hue anywhere. */
608
+ const plainTheme = {
609
+ content: {},
610
+ muted: { dim: true },
611
+ strong: { bold: true },
612
+ marker: { dim: true },
613
+ chrome: { dim: true },
614
+ cursor: { inverse: true },
615
+ positive: {},
616
+ caution: {},
617
+ danger: { bold: true },
618
+ user: { bold: true },
619
+ assistant: { dim: true },
620
+ accent: {},
621
+ code: {},
622
+ link: { underline: true },
623
+ selection: { inverse: true }
624
+ };
625
+ /**
626
+ * The default glyphs: monochrome text, one cell each. No glyph is
627
+ * emoji-capable (Unicode `Emoji=No`), so terminals cannot widen or colour one,
628
+ * and every glyph is one that common monospace fonts carry.
629
+ */
630
+ const plainGlyphs = {
631
+ collapsed: "▸",
632
+ expanded: "▾",
633
+ brain: "✻",
634
+ wrench: "ƒ",
635
+ gear: "§",
636
+ checklist: "☐",
637
+ planDone: "✓",
638
+ planActive: "…",
639
+ planPending: "·",
640
+ success: "✓",
641
+ error: "✗",
642
+ user: "›",
643
+ assistant: "•",
644
+ prompt: "›",
645
+ spinner: [
646
+ "⠋",
647
+ "⠙",
648
+ "⠹",
649
+ "⠸",
650
+ "⠼",
651
+ "⠴",
652
+ "⠦",
653
+ "⠧",
654
+ "⠇",
655
+ "⠏"
656
+ ],
657
+ separator: " · ",
658
+ tool: "ƒ",
659
+ earlier: "⋮",
660
+ toolSuccess: "✓",
661
+ toolError: "✗",
662
+ toolRunning: "…",
663
+ toolIdle: "·",
664
+ boxTopLeft: "╭",
665
+ boxTopRight: "╮",
666
+ boxBottomLeft: "╰",
667
+ boxBottomRight: "╯",
668
+ boxJunction: "├",
669
+ boxHorizontal: "─",
670
+ boxVertical: "│",
671
+ footer: "╰",
672
+ outputBar: "┊",
673
+ attachment: "⊕",
674
+ image: "▣",
675
+ pending: "‖",
676
+ pulse: [
677
+ "●··",
678
+ "·●·",
679
+ "··●"
680
+ ],
681
+ bullet: "•",
682
+ taskOpen: "☐",
683
+ taskDone: "☒",
684
+ rule: "─",
685
+ quoteBar: "│",
686
+ codeBar: "│",
687
+ tableColumn: "│",
688
+ tableCross: "┼"
689
+ };
690
+ /** Glyphs using only ASCII, for terminals without box-drawing or emoji symbols. */
691
+ const asciiGlyphs = {
692
+ collapsed: ">",
693
+ expanded: "v",
694
+ brain: "~",
695
+ wrench: "*",
696
+ gear: "#",
697
+ checklist: "=",
698
+ planDone: "x",
699
+ planActive: ">",
700
+ planPending: "-",
701
+ success: "+",
702
+ error: "x",
703
+ user: ">",
704
+ assistant: "*",
705
+ prompt: ">",
706
+ spinner: [
707
+ "|",
708
+ "/",
709
+ "-",
710
+ "\\"
711
+ ],
712
+ separator: " | ",
713
+ tool: "*",
714
+ earlier: "^",
715
+ toolSuccess: "+",
716
+ toolError: "x",
717
+ toolRunning: "~",
718
+ toolIdle: ".",
719
+ boxTopLeft: "+",
720
+ boxTopRight: "+",
721
+ boxBottomLeft: "+",
722
+ boxBottomRight: "+",
723
+ boxJunction: "+",
724
+ boxHorizontal: "-",
725
+ boxVertical: "|",
726
+ footer: "+",
727
+ outputBar: ":",
728
+ attachment: "@",
729
+ image: "[img]",
730
+ pending: "..",
731
+ pulse: [
732
+ "o..",
733
+ ".o.",
734
+ "..o"
735
+ ],
736
+ bullet: "-",
737
+ taskOpen: "[ ]",
738
+ taskDone: "[x]",
739
+ rule: "-",
740
+ quoteBar: "|",
741
+ codeBar: "|",
742
+ tableColumn: "|",
743
+ tableCross: "+"
744
+ };
745
+ /**
746
+ * Opt-in colour icons over {@link plainGlyphs}: only code points that are
747
+ * emoji by default (`Emoji_Presentation=Yes`), which terminals and the
748
+ * default measure both treat as two cells. Turn it on with
749
+ * `<InkChatProvider components={{ glyphs: emojiGlyphs }}>`.
750
+ */
751
+ const emojiGlyphs = {
752
+ ...plainGlyphs,
753
+ brain: "🧠",
754
+ wrench: "🔧",
755
+ tool: "🔧",
756
+ gear: "📜",
757
+ checklist: "📋",
758
+ attachment: "📎",
759
+ image: "📷",
760
+ pending: "⏳"
761
+ };
762
+
763
+ //#endregion
764
+ //#region src/registry.tsx
765
+ /** Prints markdown source as-is. */
766
+ function PlainMarkdown({ content }) {
767
+ return /* @__PURE__ */ React.createElement(Styled, { tone: "content" }, content);
768
+ }
769
+ const plainComponents = {
770
+ theme: plainTheme,
771
+ glyphs: plainGlyphs,
772
+ Markdown: PlainMarkdown,
773
+ spinnerIntervalMs: 80,
774
+ flashComplete: true
775
+ };
776
+ let context;
777
+ const inkContext = () => {
778
+ context ??= React.createContext(plainComponents);
779
+ return context;
780
+ };
781
+ /** Merge a partial registry over a base. */
782
+ function mergeInkComponents(base, partial = {}) {
783
+ return {
784
+ theme: {
785
+ ...base.theme,
786
+ ...partial.theme
787
+ },
788
+ glyphs: {
789
+ ...base.glyphs,
790
+ ...partial.glyphs
791
+ },
792
+ Markdown: partial.Markdown ?? base.Markdown,
793
+ spinnerIntervalMs: partial.spinnerIntervalMs ?? base.spinnerIntervalMs,
794
+ flashComplete: partial.flashComplete ?? base.flashComplete
795
+ };
796
+ }
797
+ function InkChatProvider({ components, children }) {
798
+ const value = React.useMemo(() => mergeInkComponents(plainComponents, components), [components]);
799
+ const InkContext = inkContext();
800
+ return /* @__PURE__ */ React.createElement(InkContext.Provider, { value }, children);
801
+ }
802
+ function useInkComponents() {
803
+ return React.useContext(inkContext());
804
+ }
805
+ /** Ink `<Text>` props for an appearance. */
806
+ function appearanceProps(appearance) {
807
+ return {
808
+ ...appearance.color ? { color: appearance.color } : {},
809
+ ...appearance.dim ? { dimColor: true } : {},
810
+ ...appearance.bold ? { bold: true } : {},
811
+ ...appearance.italic ? { italic: true } : {},
812
+ ...appearance.inverse ? { inverse: true } : {},
813
+ ...appearance.underline ? { underline: true } : {},
814
+ ...appearance.strikethrough ? { strikethrough: true } : {}
815
+ };
816
+ }
817
+ /** Text drawn in a theme role (`tone`). */
818
+ function Styled({ tone, wrap, children }) {
819
+ const { theme } = useInkComponents();
820
+ return /* @__PURE__ */ React.createElement(Text, {
821
+ ...appearanceProps(theme[tone]),
822
+ ...wrap ? { wrap } : {}
823
+ }, children);
824
+ }
825
+
826
+ //#endregion
827
+ //#region src/terminal.ts
828
+ /**
829
+ * Terminal protocol helpers shared by the composer and the viewport.
830
+ */
831
+ const ESC$1 = String.fromCharCode(27);
832
+ const TERMINAL_REPORT = new RegExp(`^(?:${ESC$1})?\\[(?:<\\d+;\\d+;\\d+[Mm]|\\d+;\\d+;\\d+t|\\?\\d[\\d;]*[a-zA-Z])`);
833
+ /** Mouse reports and terminal replies arrive as input; key handlers ignore them. */
834
+ function isTerminalReport(input) {
835
+ return TERMINAL_REPORT.test(input);
836
+ }
837
+ /** The longest unfinished report followed across key events; a longer one is noise. */
838
+ const MAX_TAIL = 64;
839
+ const CSI_HEAD = /^\[[\x30-\x3f]*[\x20-\x2f]*/;
840
+ /**
841
+ * Where the CSI sequence opening `text` (ESC stripped) ends: past its final
842
+ * byte, 0 while it is unfinished, or -1 when `text` opens none.
843
+ */
844
+ function csiEnd(text) {
845
+ const head = CSI_HEAD.exec(text);
846
+ if (!head) return -1;
847
+ const at = head[0].length;
848
+ if (at === text.length) return 0;
849
+ const code = text.charCodeAt(at);
850
+ return code >= 64 && code <= 126 ? at + 1 : -1;
851
+ }
852
+ /**
853
+ * A reader of terminal reports across key events. Ink holds an unfinished
854
+ * escape sequence only briefly, then delivers it with its ESC stripped (so a
855
+ * report split across stdin chunks arrives as `[<0;3` and then `;4M`). Each
856
+ * call classifies one event's input, following such a start into the next
857
+ * event; the start itself cannot be told from typing (a typed `[` looks the
858
+ * same), so it is `partial` and the caller decides what to hold back.
859
+ */
860
+ function createKeyReportReader() {
861
+ let tail = "";
862
+ return (input) => {
863
+ const held = tail;
864
+ tail = "";
865
+ if (isTerminalReport(input)) return {
866
+ kind: "report",
867
+ joined: false,
868
+ rest: ""
869
+ };
870
+ const text = input.startsWith(ESC$1) ? input.slice(1) : input;
871
+ if (held && text) {
872
+ const joined = held + text;
873
+ const end = csiEnd(joined);
874
+ if (end > 0 && isTerminalReport(joined.slice(0, end))) return {
875
+ kind: "report",
876
+ joined: true,
877
+ rest: joined.slice(end)
878
+ };
879
+ if (end === 0 && joined.length <= MAX_TAIL) {
880
+ tail = joined;
881
+ return {
882
+ kind: "partial",
883
+ continued: true
884
+ };
885
+ }
886
+ }
887
+ if (csiEnd(text) === 0 && text.length <= MAX_TAIL) {
888
+ tail = text;
889
+ return {
890
+ kind: "partial",
891
+ continued: false
892
+ };
893
+ }
894
+ return { kind: "text" };
895
+ };
896
+ }
897
+ /** Kitty keyboard protocol: push the disambiguate flag (Shift+Enter is then reported). */
898
+ const KITTY_PUSH = `${ESC$1}[>1u`;
899
+ /** Kitty keyboard protocol: pop the flags pushed last. */
900
+ const KITTY_POP = `${ESC$1}[<u`;
901
+
902
+ //#endregion
903
+ //#region src/Composer.tsx
904
+ const emptyComposerValue = {
905
+ text: "",
906
+ caret: 0,
907
+ attachments: []
908
+ };
909
+ function findMatch(providers, state) {
910
+ for (const provider of providers ?? []) {
911
+ if (!state.value.slice(0, state.caret).includes(provider.trigger)) continue;
912
+ const match = provider.match(state.value, state.caret);
913
+ if (match) return {
914
+ provider,
915
+ match
916
+ };
917
+ }
918
+ return void 0;
919
+ }
920
+ /** Stable identity of a matched token (provider, start, query). */
921
+ function keyOf(found) {
922
+ return found ? `${found.provider.id}:${found.match.range[0]}:${found.match.query}` : void 0;
923
+ }
924
+ /** State plus a ref that always holds the latest value, updated synchronously. */
925
+ function useLive(initial) {
926
+ const [value, setValue] = React.useState(initial);
927
+ const ref = React.useRef(value);
928
+ const set = React.useCallback((next) => {
929
+ ref.current = next;
930
+ setValue(() => next);
931
+ }, []);
932
+ return [
933
+ value,
934
+ ref,
935
+ set
936
+ ];
937
+ }
938
+ const toEditor = (v) => ({
939
+ value: v.text,
940
+ caret: v.caret
941
+ });
942
+ const sameEditor = (a, b) => a.value === b.value && a.caret === b.caret;
943
+ /**
944
+ * Cells left for text at `width`: the prompt, the border and padding of the
945
+ * boxed variant, and one cell kept for the caret come off.
946
+ */
947
+ function composerInnerWidth(width$2, variant = "boxed", promptWidth = 2) {
948
+ return Math.max(1, width$2 - promptWidth - (variant === "boxed" ? 4 : 0) - 1);
949
+ }
950
+ /**
951
+ * Rows the composer takes at `width` for `value`: its input rows (up to
952
+ * `maxRows`), the attachments row, and the boxed variant's border — not the
953
+ * hint or completion lines under it. Lets a host size the rest of the screen.
954
+ */
955
+ function composerRows(value, width$2, options = {}) {
956
+ const { variant = "boxed", maxRows, promptWidth = 2, measure = defaultMeasure } = options;
957
+ const rows = visualRows(value.text, composerInnerWidth(width$2, variant, promptWidth), measure).length;
958
+ const shown = maxRows === void 0 ? rows : Math.min(rows, Math.max(1, maxRows));
959
+ return shown + (value.attachments.length > 0 ? 1 : 0) + (variant === "boxed" ? 2 : 0);
960
+ }
961
+ /** Keep the caret's row inside a window of `max` rows, moving the window as little as possible. */
962
+ function scrollTop(top, row, rows, max) {
963
+ const last = Math.max(0, rows - max);
964
+ if (row < top) return row;
965
+ if (row >= top + max) return Math.min(row - max + 1, last);
966
+ return Math.min(top, last);
967
+ }
968
+ function Composer({ onSubmit, onEscape, providers, history: initialHistory, placeholder = "Message", disabled = false, hint, maxItems = 6, value: controlled, onChange, variant = "boxed", width: width$2, maxRows, focused = true, attachmentHint, killLine = "line", onKey, onPaste, inputSource = "own", ref, kittyKeyboard = false, measure = defaultMeasure }) {
969
+ const { glyphs, theme } = useInkComponents();
970
+ const { stdout } = ink.useStdout();
971
+ const [own, valueRef, setOwn] = useLive(controlled ?? emptyComposerValue);
972
+ const seen = React.useRef(controlled);
973
+ if (controlled && controlled !== seen.current) {
974
+ seen.current = controlled;
975
+ valueRef.current = controlled;
976
+ }
977
+ const current = controlled ?? own;
978
+ const [, historyRef, setHistory] = useLive(() => createHistory(initialHistory));
979
+ const historyLength = initialHistory?.length ?? 0;
980
+ const seenHistory = React.useRef(initialHistory);
981
+ React.useEffect(() => {
982
+ if (seenHistory.current === initialHistory && historyLength === historyRef.current.entries.length) return;
983
+ seenHistory.current = initialHistory;
984
+ setHistory(withEntries(historyRef.current, initialHistory ?? []));
985
+ }, [
986
+ initialHistory,
987
+ historyLength,
988
+ historyRef,
989
+ setHistory
990
+ ]);
991
+ const [completion, completionRef, setCompletion] = useLive(void 0);
992
+ const [dismissed, dismissedRef, setDismissed] = useLive(void 0);
993
+ const recalled = React.useRef(false);
994
+ const active = focused && !disabled;
995
+ const prompt = `${glyphs.prompt} `;
996
+ const promptWidth = measure(prompt);
997
+ const inner = width$2 === void 0 ? void 0 : composerInnerWidth(width$2, variant, promptWidth);
998
+ const rowsOf = (text) => visualRows(text, inner, measure);
999
+ const found = findMatch(providers, toEditor(current));
1000
+ const tokenKey = keyOf(found);
1001
+ const latest = React.useRef(found);
1002
+ latest.current = found;
1003
+ React.useEffect(() => {
1004
+ const match = latest.current;
1005
+ if (!tokenKey || !match || tokenKey === dismissed) {
1006
+ setCompletion(void 0);
1007
+ return;
1008
+ }
1009
+ const controller = new AbortController();
1010
+ Promise.resolve(match.provider.items(match.match.query, controller.signal)).then((items) => {
1011
+ if (controller.signal.aborted) return;
1012
+ setCompletion(items.length > 0 ? {
1013
+ provider: match.provider,
1014
+ match: match.match,
1015
+ items,
1016
+ active: 0
1017
+ } : void 0);
1018
+ }, () => {
1019
+ if (!controller.signal.aborted) setCompletion(void 0);
1020
+ });
1021
+ return () => controller.abort();
1022
+ }, [tokenKey, dismissed]);
1023
+ React.useEffect(() => {
1024
+ if (!kittyKeyboard || !active) return;
1025
+ stdout.write(KITTY_PUSH);
1026
+ const pop = () => stdout.write(KITTY_POP);
1027
+ process.once("exit", pop);
1028
+ return () => {
1029
+ process.off("exit", pop);
1030
+ pop();
1031
+ };
1032
+ }, [
1033
+ kittyKeyboard,
1034
+ active,
1035
+ stdout
1036
+ ]);
1037
+ const commit = (next) => {
1038
+ setOwn(next);
1039
+ onChange?.(next);
1040
+ };
1041
+ const setEditor = (state, attachments$1 = valueRef.current.attachments) => commit({
1042
+ text: state.value,
1043
+ caret: state.caret,
1044
+ attachments: attachments$1
1045
+ });
1046
+ /** A change from the user: an edit ends a recall and reopens completion. */
1047
+ const edit = (next) => {
1048
+ if (dismissedRef.current !== void 0) setDismissed(void 0);
1049
+ if (next.value !== valueRef.current.text) recalled.current = false;
1050
+ setEditor(next);
1051
+ };
1052
+ const apply = (c) => {
1053
+ const item = c.items[c.active];
1054
+ const [start, end] = c.match.range;
1055
+ const text = valueRef.current.text;
1056
+ const result = c.provider.apply(item, text.slice(0, start), text.slice(end));
1057
+ const next = {
1058
+ value: result.value,
1059
+ caret: result.caret
1060
+ };
1061
+ setEditor(next);
1062
+ setCompletion(void 0);
1063
+ setDismissed(keyOf(findMatch(providers, next)));
1064
+ };
1065
+ /** Submit `text` with the current attachments (if either has content); returns whether it was sent. */
1066
+ const send = (text) => {
1067
+ const attachments$1 = valueRef.current.attachments;
1068
+ if (!text.trim() && attachments$1.length === 0) return false;
1069
+ if (attachments$1.length > 0) onSubmit(text, attachments$1);
1070
+ else onSubmit(text);
1071
+ setHistory(pushHistory(historyRef.current, {
1072
+ text,
1073
+ attachments: attachments$1
1074
+ }));
1075
+ setCompletion(void 0);
1076
+ recalled.current = false;
1077
+ return true;
1078
+ };
1079
+ const cleared = {
1080
+ value: "",
1081
+ caret: 0
1082
+ };
1083
+ const submit = () => {
1084
+ if (send(valueRef.current.text)) setEditor(cleared, []);
1085
+ };
1086
+ /**
1087
+ * A chunk of typed characters. Terminals and multiplexers may deliver
1088
+ * several keystrokes at once (e.g. `"hi\r"`), which arrive as text without
1089
+ * the return flag: a `\r` inside the chunk submits what precedes it and a
1090
+ * `\n` inserts a newline, as if each key had arrived alone.
1091
+ */
1092
+ const typeChunk = (chunk) => {
1093
+ let state = toEditor(valueRef.current);
1094
+ for (const part of chunk.split(/(\r\n?|\n)/)) if (part === "\n") state = insert(state, "\n");
1095
+ else if (part.startsWith("\r")) {
1096
+ if (send(state.value)) {
1097
+ state = cleared;
1098
+ setEditor(state, []);
1099
+ }
1100
+ } else if (part) state = insert(state, stripControls(part));
1101
+ edit(state);
1102
+ };
1103
+ const paste = (text) => {
1104
+ if (onPaste?.(text)) return;
1105
+ edit(insert(toEditor(valueRef.current), stripControls(text.replace(/\r\n?/g, "\n"))));
1106
+ };
1107
+ const stepThroughHistory = (direction) => {
1108
+ const value = valueRef.current;
1109
+ const stepped = stepHistory(historyRef.current, direction, {
1110
+ text: value.text,
1111
+ attachments: value.attachments
1112
+ });
1113
+ if (!stepped) return;
1114
+ setHistory(stepped.history);
1115
+ recalled.current = stepped.history.index < stepped.history.entries.length;
1116
+ setEditor({
1117
+ value: stepped.value,
1118
+ caret: stepped.value.length
1119
+ }, stepped.attachments ?? []);
1120
+ };
1121
+ const [readReport] = React.useState(createKeyReportReader);
1122
+ const held = React.useRef(void 0);
1123
+ const handleKey = (input$1, key) => {
1124
+ const report = readReport(input$1);
1125
+ const start = held.current;
1126
+ held.current = void 0;
1127
+ if (report.kind === "report") {
1128
+ const now = toEditor(valueRef.current);
1129
+ if (report.joined && sameEditor(now, start.after) && !sameEditor(now, start.before)) edit(start.before);
1130
+ if (report.rest) handleKey(report.rest, key);
1131
+ return;
1132
+ }
1133
+ const from = report.kind === "partial" && report.continued ? start.before : toEditor(valueRef.current);
1134
+ if (report.kind === "partial") held.current = {
1135
+ before: from,
1136
+ after: from
1137
+ };
1138
+ handleOwnKey(input$1, key);
1139
+ if (held.current) held.current.after = toEditor(valueRef.current);
1140
+ };
1141
+ const handleOwnKey = (input$1, key) => {
1142
+ if (onKey?.(input$1, key)) return;
1143
+ const completion$1 = completionRef.current;
1144
+ const editor = toEditor(valueRef.current);
1145
+ if (completion$1) {
1146
+ if (key.upArrow || key.downArrow) {
1147
+ const n = completion$1.items.length;
1148
+ setCompletion({
1149
+ ...completion$1,
1150
+ active: (completion$1.active + (key.upArrow ? n - 1 : 1)) % n,
1151
+ navigated: true
1152
+ });
1153
+ return;
1154
+ }
1155
+ if (key.tab || key.return && completion$1.navigated) {
1156
+ apply(completion$1);
1157
+ return;
1158
+ }
1159
+ if (key.escape) {
1160
+ setDismissed(keyOf(findMatch(providers, editor)));
1161
+ setCompletion(void 0);
1162
+ return;
1163
+ }
1164
+ }
1165
+ if (key.escape) {
1166
+ onEscape?.();
1167
+ return;
1168
+ }
1169
+ if (key.return) {
1170
+ if (key.meta || key.shift) edit(insert(editor, "\n"));
1171
+ else if (editor.value.slice(0, editor.caret).endsWith("\\")) edit(insert(backspace(editor), "\n"));
1172
+ else submit();
1173
+ return;
1174
+ }
1175
+ if (key.backspace) return edit(backspace(editor));
1176
+ if (key.delete) return edit(deleteForward(editor));
1177
+ if ((key.ctrl || key.meta) && key.leftArrow) return edit(wordLeft(editor));
1178
+ if ((key.ctrl || key.meta) && key.rightArrow) return edit(wordRight(editor));
1179
+ if (key.meta && input$1 === "b") return edit(wordLeft(editor));
1180
+ if (key.meta && input$1 === "f") return edit(wordRight(editor));
1181
+ if (key.leftArrow) return edit(moveBy(editor, -1));
1182
+ if (key.rightArrow) return edit(moveBy(editor, 1));
1183
+ if (key.home || key.ctrl && input$1 === "a") return edit(lineStart(editor));
1184
+ if (key.end || key.ctrl && input$1 === "e") return edit(lineEnd(editor));
1185
+ if (key.ctrl && input$1 === "u") return edit(killLine === "input" ? cleared : killToLineStart(editor));
1186
+ if (key.ctrl && input$1 === "k") return edit(killToLineEnd(editor));
1187
+ if (key.ctrl && input$1 === "w") return edit(deleteWordBack(editor));
1188
+ if (key.upArrow || key.downArrow) {
1189
+ const direction = key.upArrow ? -1 : 1;
1190
+ const moved = recalled.current ? void 0 : moveVisual(editor, rowsOf(editor.value), direction, measure);
1191
+ if (moved) return edit(moved);
1192
+ stepThroughHistory(direction);
1193
+ return;
1194
+ }
1195
+ if (key.ctrl || key.meta || key.tab || !input$1) return;
1196
+ typeChunk(input$1);
1197
+ };
1198
+ React.useImperativeHandle(ref, () => ({
1199
+ key: handleKey,
1200
+ paste
1201
+ }));
1202
+ const listening = active && inputSource === "own";
1203
+ ink.usePaste(paste, { isActive: listening });
1204
+ ink.useInput(handleKey, { isActive: listening });
1205
+ const rows = rowsOf(current.text);
1206
+ const at = caretBoundary(current.text, current.caret);
1207
+ const row = caretRow(rows, at);
1208
+ const top = React.useRef(0);
1209
+ top.current = maxRows === void 0 ? 0 : scrollTop(top.current, row, rows.length, Math.max(1, maxRows));
1210
+ const shown = rows.slice(top.current, maxRows === void 0 ? void 0 : top.current + maxRows);
1211
+ const empty = current.text === "" && current.attachments.length === 0;
1212
+ const input = empty ? /* @__PURE__ */ React.createElement(Text, null, focused ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, appearanceProps(theme.cursor), placeholder.slice(0, 1) || " "), /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, placeholder.slice(1))) : /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, placeholder)) : shown.map((r, i) => {
1213
+ const text = current.text.slice(r.start, r.end);
1214
+ const caret = focused && top.current + i === row ? splitAtCaret(text, at - r.start) : void 0;
1215
+ return /* @__PURE__ */ React.createElement(Text, {
1216
+ key: i,
1217
+ wrap: inner === void 0 ? "wrap" : "truncate"
1218
+ }, caret === void 0 ? text || " " : /* @__PURE__ */ React.createElement(React.Fragment, null, caret[0], /* @__PURE__ */ React.createElement(Text, appearanceProps(theme.cursor), caret[1] || " "), caret[2]));
1219
+ });
1220
+ const attachments = current.attachments.length > 0 ? /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Styled, { tone: "link" }, `${glyphs.attachment} `), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, truncate([current.attachments.map((a) => a.description ?? a.name ?? a.mediaType).join(glyphs.separator), attachmentHint].filter(Boolean).join(" "), Math.max(1, (width$2 ?? Number.POSITIVE_INFINITY) - measure(`${glyphs.attachment} `)), measure))) : null;
1221
+ const promptTone = focused ? "marker" : "chrome";
1222
+ const below = completion ? /* @__PURE__ */ React.createElement(ink.Box, {
1223
+ flexDirection: "column",
1224
+ paddingLeft: 2
1225
+ }, completion.items.slice(0, maxItems).map((item, i) => /* @__PURE__ */ React.createElement(Text, { key: item.id }, /* @__PURE__ */ React.createElement(Text, i === completion.active ? appearanceProps(theme.cursor) : {}, item.label), item.description ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, " ", item.description) : null))) : hint ? /* @__PURE__ */ React.createElement(ink.Box, { paddingLeft: 2 }, /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, hint)) : null;
1226
+ if (variant === "plain") return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, attachments, /* @__PURE__ */ React.createElement(ink.Box, null, /* @__PURE__ */ React.createElement(ink.Box, {
1227
+ flexDirection: "column",
1228
+ width: promptWidth,
1229
+ flexShrink: 0
1230
+ }, /* @__PURE__ */ React.createElement(Styled, { tone: promptTone }, prompt)), /* @__PURE__ */ React.createElement(ink.Box, {
1231
+ flexDirection: "column",
1232
+ flexGrow: 1
1233
+ }, input)), below);
1234
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, attachments, /* @__PURE__ */ React.createElement(ink.Box, {
1235
+ ...width$2 === void 0 ? {} : { width: width$2 },
1236
+ borderStyle: "round",
1237
+ borderColor: theme.chrome.color ?? "gray",
1238
+ borderDimColor: Boolean(theme.chrome.dim),
1239
+ paddingX: 1
1240
+ }, /* @__PURE__ */ React.createElement(ink.Box, { marginRight: 1 }, /* @__PURE__ */ React.createElement(Styled, { tone: promptTone }, glyphs.prompt)), /* @__PURE__ */ React.createElement(ink.Box, {
1241
+ flexDirection: "column",
1242
+ flexGrow: 1
1243
+ }, input)), below);
1244
+ }
1245
+
1246
+ //#endregion
1247
+ //#region src/Spinner.tsx
1248
+ /**
1249
+ * An animated spinner in the `marker` role, using the injected frames. It
1250
+ * steps once per tick of the shared clock when a ClockProvider is in scope,
1251
+ * else every `spinnerIntervalMs`.
1252
+ */
1253
+ function Spinner() {
1254
+ const { glyphs, spinnerIntervalMs } = useInkComponents();
1255
+ const frame = hooks.useTicks(true, spinnerIntervalMs);
1256
+ const frames = glyphs.spinner;
1257
+ return /* @__PURE__ */ React.createElement(Styled, { tone: "marker" }, frames[frame % frames.length] ?? "");
1258
+ }
1259
+
1260
+ //#endregion
1261
+ //#region src/Row.tsx
1262
+ function Row({ label, detail, qualifier, iconName, prefix: prefix$1, isActive = false, startedAt, durationMs, showTimer = false, tokensPerSecond, error = false, expanded = false, children }) {
1263
+ const { glyphs, flashComplete } = useInkComponents();
1264
+ const flash = hooks.useFlashComplete(isActive, 1200, flashComplete);
1265
+ const elapsed = hooks.useElapsedTimer({
1266
+ startedAt,
1267
+ isActive: isActive && showTimer,
1268
+ durationMs
1269
+ });
1270
+ const hasBody = children != null && children !== false;
1271
+ const timer = shouldShowTimer({
1272
+ showTimer,
1273
+ isActive,
1274
+ startedAt,
1275
+ durationMs
1276
+ });
1277
+ const tps = formatTokensPerSecond(tokensPerSecond);
1278
+ let affordance;
1279
+ if (isActive) affordance = /* @__PURE__ */ React.createElement(Spinner, null);
1280
+ else if (flash) affordance = /* @__PURE__ */ React.createElement(Styled, { tone: "positive" }, glyphs.success);
1281
+ else if (hasBody) affordance = /* @__PURE__ */ React.createElement(Styled, { tone: "marker" }, expanded ? glyphs.expanded : glyphs.collapsed);
1282
+ else affordance = /* @__PURE__ */ React.createElement(Text, null, " ");
1283
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(ink.Box, { gap: 1 }, affordance, prefix$1, iconName ? /* @__PURE__ */ React.createElement(Styled, { tone: "marker" }, glyphs[iconName]) : null, /* @__PURE__ */ React.createElement(Styled, {
1284
+ tone: error ? "danger" : "strong",
1285
+ wrap: "truncate-end"
1286
+ }, label), detail ? /* @__PURE__ */ React.createElement(Styled, {
1287
+ tone: "muted",
1288
+ wrap: "truncate-end"
1289
+ }, detail) : null, error ? /* @__PURE__ */ React.createElement(Styled, { tone: "danger" }, glyphs.error, " Error") : null, qualifier ? /* @__PURE__ */ React.createElement(Styled, { tone: "danger" }, "· ", qualifier) : null, timer ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, formatDuration(elapsed)) : null, tps ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, tps) : null), hasBody && expanded ? /* @__PURE__ */ React.createElement(ink.Box, {
1290
+ flexDirection: "column",
1291
+ paddingLeft: 2
1292
+ }, children) : null);
1293
+ }
1294
+ /** Clip long text to `maxLines`, noting how many lines were hidden. */
1295
+ function clipLines(text, maxLines) {
1296
+ const lines = text.split("\n");
1297
+ if (lines.length <= maxLines) return text;
1298
+ return [...lines.slice(0, maxLines), `… ${lines.length - maxLines} more lines`].join("\n");
1299
+ }
1300
+ /** Pretty JSON, tolerating values JSON cannot encode. */
1301
+ function toJson(value) {
1302
+ if (typeof value === "string") return value;
1303
+ try {
1304
+ return JSON.stringify(value, null, 2) ?? String(value);
1305
+ } catch {
1306
+ return String(value);
1307
+ }
1308
+ }
1309
+
1310
+ //#endregion
1311
+ //#region src/MessageView.tsx
1312
+ const APPROVAL_TEXT = {
1313
+ denied: "denied",
1314
+ unavailable: "refused",
1315
+ pending: "awaiting approval"
1316
+ };
1317
+ function BlockRow({ block, ctx }) {
1318
+ const { Markdown } = useInkComponents();
1319
+ const live = isBlockActive(block, ctx);
1320
+ if (!isCoreBlockType(block.type)) {
1321
+ const custom = block;
1322
+ const Renderer = lookupExtension(ctx.extensions?.blockRenderers, custom.type);
1323
+ if (Renderer) return /* @__PURE__ */ React.createElement(Renderer, {
1324
+ block: custom,
1325
+ message: ctx.message,
1326
+ isStreaming: live
1327
+ });
1328
+ return /* @__PURE__ */ React.createElement(Row, { label: summarizeBlock(custom).label });
1329
+ }
1330
+ const core = block;
1331
+ switch (core.type) {
1332
+ case "text": return ctx.message.role === "user" ? /* @__PURE__ */ React.createElement(Styled, { tone: "user" }, core.text) : /* @__PURE__ */ React.createElement(Markdown, {
1333
+ content: core.text,
1334
+ ...live ? { streaming: true } : {}
1335
+ });
1336
+ case "reasoning": {
1337
+ const summary = summarizeBlock(core, { isActive: live });
1338
+ return /* @__PURE__ */ React.createElement(Row, {
1339
+ label: summary.label,
1340
+ iconName: "brain",
1341
+ isActive: live,
1342
+ startedAt: positiveTimestamp(core.startedAt) ?? ctx.startedAt,
1343
+ durationMs: core.durationMs,
1344
+ showTimer: summary.showTimer,
1345
+ expanded: ctx.expanded
1346
+ }, core.text ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, clipLines(core.text, ctx.bodyLines)) : null);
1347
+ }
1348
+ case "tool_call": {
1349
+ const isError = core.status === "error";
1350
+ const input = toolDisplayInput(core);
1351
+ const approval = core.approval ? APPROVAL_TEXT[core.approval.state] : void 0;
1352
+ return /* @__PURE__ */ React.createElement(Row, {
1353
+ label: toolLabel(core) || "tool",
1354
+ ...input ? { detail: input } : {},
1355
+ ...approval ? { qualifier: approval } : {},
1356
+ iconName: "wrench",
1357
+ isActive: live,
1358
+ startedAt: core.startedAt,
1359
+ durationMs: core.durationMs,
1360
+ showTimer: true,
1361
+ error: isError,
1362
+ expanded: ctx.expanded
1363
+ }, /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, "input"), /* @__PURE__ */ React.createElement(Styled, { tone: "content" }, clipLines(toJson(core.arguments), ctx.bodyLines)), core.result !== void 0 ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, "output"), /* @__PURE__ */ React.createElement(Styled, { tone: isError ? "danger" : "content" }, clipLines(toolResultText(core.result), ctx.bodyLines))) : null));
1364
+ }
1365
+ case "status": return /* @__PURE__ */ React.createElement(Row, {
1366
+ label: statusLabel(core.phase, live),
1367
+ isActive: live,
1368
+ startedAt: ctx.startedAt,
1369
+ durationMs: ctx.durationMs,
1370
+ showTimer: true,
1371
+ ...live ? { tokensPerSecond: ctx.tokensPerSecond } : {}
1372
+ });
1373
+ case "plan": {
1374
+ const summary = summarizeBlock(core);
1375
+ return /* @__PURE__ */ React.createElement(Row, {
1376
+ label: summary.label,
1377
+ iconName: "checklist",
1378
+ expanded: ctx.expanded
1379
+ }, core.entries.length > 0 ? /* @__PURE__ */ React.createElement(PlanEntries, {
1380
+ entries: core.entries,
1381
+ live: ctx.isActive
1382
+ }) : null);
1383
+ }
1384
+ case "system": return /* @__PURE__ */ React.createElement(Row, {
1385
+ label: "System prompt",
1386
+ iconName: "gear",
1387
+ expanded: ctx.expanded
1388
+ }, /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, clipLines(core.text, ctx.bodyLines)));
1389
+ }
1390
+ }
1391
+ function PlanEntries({ entries, live }) {
1392
+ const { glyphs } = useInkComponents();
1393
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, entries.map((entry, index) => {
1394
+ const marker = entry.status === "completed" ? glyphs.planDone : entry.status === "in_progress" ? glyphs.planActive : glyphs.planPending;
1395
+ const tone = entry.status === "completed" ? "muted" : entry.status === "in_progress" && live ? "caution" : "content";
1396
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1397
+ key: entry.id ?? index,
1398
+ gap: 1
1399
+ }, entry.status === "in_progress" && live ? /* @__PURE__ */ React.createElement(Spinner, null) : /* @__PURE__ */ React.createElement(Styled, { tone: entry.status === "completed" ? "positive" : "marker" }, marker), /* @__PURE__ */ React.createElement(Styled, { tone }, entry.content));
1400
+ }));
1401
+ }
1402
+ function GroupRow({ blocks, ctx }) {
1403
+ const plan = planGroupHeader(blocks, ctx);
1404
+ const tokensPerSecond = plan.showRate ? ctx.tokensPerSecond : void 0;
1405
+ const only = plan.tools?.only;
1406
+ const approval = only?.approval ? APPROVAL_TEXT[only.approval.state] : void 0;
1407
+ return /* @__PURE__ */ React.createElement(Row, {
1408
+ label: plan.summary.label,
1409
+ ...approval ? { qualifier: approval } : {},
1410
+ error: only !== void 0 && plan.tools?.failed === 1,
1411
+ iconName: plan.summary.iconName,
1412
+ prefix: plan.hiddenCount > 0 ? /* @__PURE__ */ React.createElement(Styled, { tone: "marker" }, "(+", plan.hiddenCount, ")") : null,
1413
+ isActive: plan.isActive,
1414
+ startedAt: plan.startedAt,
1415
+ durationMs: plan.durationMs,
1416
+ showTimer: plan.showTimer,
1417
+ tokensPerSecond,
1418
+ expanded: ctx.expanded
1419
+ }, blocks.map((block) => /* @__PURE__ */ React.createElement(BlockRow, {
1420
+ key: block.id,
1421
+ block,
1422
+ ctx
1423
+ })));
1424
+ }
1425
+ function MessageView({ message: original, isActive = false, expanded = false, minimal = false, showFooter = true, bodyLines = 12, extensions, retryHint, activityPlacement, liveTail, liveFooter = false }) {
1426
+ const { glyphs } = useInkComponents();
1427
+ const message = stripControlsDeep(original);
1428
+ const live = isActive && liveFooter;
1429
+ const now = hooks.useElapsedTimer({
1430
+ startedAt: 0,
1431
+ isActive: live,
1432
+ intervalMs: 1e3
1433
+ });
1434
+ const tokensPerSecond = hooks.useLiveTokensPerSecond({
1435
+ startedAt: message.createdAt,
1436
+ isActive,
1437
+ tokenCount: isActive ? approxTokenCount(message.blocks) : void 0,
1438
+ metrics: message.metrics
1439
+ });
1440
+ const plan = planMessage(message, {
1441
+ isActive,
1442
+ minimal,
1443
+ showFooter,
1444
+ ...activityPlacement ? { activityPlacement } : {},
1445
+ ...liveTail !== void 0 && !expanded ? { liveTail } : {}
1446
+ });
1447
+ const ctx = {
1448
+ message,
1449
+ isActive,
1450
+ activeBlockId: plan.activeBlockId,
1451
+ startedAt: message.createdAt,
1452
+ durationMs: message.metrics?.durationMs,
1453
+ tokensPerSecond,
1454
+ expanded,
1455
+ bodyLines,
1456
+ extensions,
1457
+ outcome: message.outcome
1458
+ };
1459
+ const showsSomething$1 = message.blocks.length > 0 || message.outcome !== void 0;
1460
+ const footerFacts = plan.showFooter || live && showsSomething$1 ? planFooter(message, {
1461
+ isActive,
1462
+ detail: expanded ? "details" : "collapsed",
1463
+ ...live ? { now } : {}
1464
+ }) : void 0;
1465
+ const factGlyph = {
1466
+ reasoning: glyphs.brain,
1467
+ plan: glyphs.checklist,
1468
+ tools: glyphs.wrench
1469
+ };
1470
+ const footer = footerFacts ? [...footerFacts.map((fact) => factGlyph[fact.key] ? `${factGlyph[fact.key]} ${fact.text}` : fact.text), ...message.outcome && retryHint ? [retryHint] : []].join(glyphs.separator) || void 0 : void 0;
1471
+ const isUser = message.role === "user";
1472
+ const attachments = message.attachments ?? [];
1473
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1474
+ flexDirection: "column",
1475
+ marginBottom: 1
1476
+ }, /* @__PURE__ */ React.createElement(ink.Box, { gap: 1 }, /* @__PURE__ */ React.createElement(Styled, { tone: isUser ? "user" : "assistant" }, isUser ? glyphs.user : glyphs.assistant), /* @__PURE__ */ React.createElement(ink.Box, {
1477
+ flexDirection: "column",
1478
+ flexGrow: 1
1479
+ }, plan.rows.map((row) => row.kind === "pending" ? /* @__PURE__ */ React.createElement(BlockRow, {
1480
+ key: row.id,
1481
+ block: {
1482
+ type: "status",
1483
+ id: row.id,
1484
+ phase: "pre-stream"
1485
+ },
1486
+ ctx: {
1487
+ ...ctx,
1488
+ activeBlockId: row.id
1489
+ }
1490
+ }) : row.kind === "bare" ? /* @__PURE__ */ React.createElement(BlockRow, {
1491
+ key: row.block.id,
1492
+ block: row.block,
1493
+ ctx
1494
+ }) : row.kind === "earlier" ? /* @__PURE__ */ React.createElement(Styled, {
1495
+ key: row.key,
1496
+ tone: "muted"
1497
+ }, glyphs.earlier ? `${glyphs.earlier} ${earlierLabel(row)}` : earlierLabel(row)) : row.kind === "outcome" ? /* @__PURE__ */ React.createElement(Styled, {
1498
+ key: "outcome",
1499
+ tone: "danger"
1500
+ }, glyphs.error, " ", row.outcome.message) : /* @__PURE__ */ React.createElement(GroupRow, {
1501
+ key: row.key,
1502
+ blocks: row.blocks,
1503
+ ctx
1504
+ })), attachments.map((a, i) => /* @__PURE__ */ React.createElement(Styled, {
1505
+ key: `${a.url}-${i}`,
1506
+ tone: "muted"
1507
+ }, "[", a.kind, "] ", a.name ?? a.mediaType)), footer ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, footer) : null)));
1508
+ }
1509
+
1510
+ //#endregion
1511
+ //#region src/Transcript.tsx
1512
+ function Transcript({ messages, isStreaming = false, streamingMessageId, expanded = false, expandFinished = false, minimal = false, showFooter = true, bodyLines, extensions, retryHint, activityPlacement, liveTail, liveFooter }) {
1513
+ const liveIndex = isStreaming ? messages.findIndex((message) => message.id === streamingMessageId) : -1;
1514
+ const finished = liveIndex === -1 ? messages : messages.slice(0, liveIndex);
1515
+ const live = liveIndex === -1 ? [] : messages.slice(liveIndex);
1516
+ const common = {
1517
+ minimal,
1518
+ showFooter,
1519
+ ...bodyLines !== void 0 ? { bodyLines } : {},
1520
+ ...extensions ? { extensions } : {},
1521
+ ...retryHint ? { retryHint } : {},
1522
+ ...activityPlacement ? { activityPlacement } : {},
1523
+ ...liveTail !== void 0 ? { liveTail } : {},
1524
+ ...liveFooter !== void 0 ? { liveFooter } : {}
1525
+ };
1526
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Static, { items: finished }, (message) => /* @__PURE__ */ React.createElement(MessageView, {
1527
+ key: message.id,
1528
+ message,
1529
+ expanded: expandFinished,
1530
+ ...common
1531
+ })), live.length > 0 ? /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, live.map((message) => /* @__PURE__ */ React.createElement(MessageView, {
1532
+ key: message.id,
1533
+ message,
1534
+ isActive: message.id === streamingMessageId,
1535
+ expanded,
1536
+ ...common
1537
+ }))) : null);
1538
+ }
1539
+
1540
+ //#endregion
1541
+ //#region src/lines/Lines.tsx
1542
+ /** A span's resolved text props. */
1543
+ function spanProps(s, theme, highlight) {
1544
+ const base = {
1545
+ ...s.role ? theme[s.role] : {},
1546
+ ...s.style,
1547
+ ...s.bold !== void 0 ? { bold: s.bold } : {},
1548
+ ...s.italic ? { italic: true } : {},
1549
+ ...s.dim ? { dim: true } : {},
1550
+ ...s.underline ? { underline: true } : {},
1551
+ ...s.strike ? { strikethrough: true } : {},
1552
+ ...s.inverse ? { inverse: true } : {},
1553
+ ...s.color ? { color: s.color } : {},
1554
+ ...highlight
1555
+ };
1556
+ return appearanceProps(base);
1557
+ }
1558
+ const CHUNK = 8;
1559
+ /** Split text into runs that keep width measurement cheap. */
1560
+ function splitRuns(text) {
1561
+ const runs = [];
1562
+ let ascii = "";
1563
+ const chars = Array.from(text);
1564
+ let i = 0;
1565
+ while (i < chars.length) {
1566
+ const ch = chars[i];
1567
+ if (ch.charCodeAt(0) < 128) {
1568
+ ascii += ch;
1569
+ i++;
1570
+ continue;
1571
+ }
1572
+ if (ascii) {
1573
+ runs.push(ascii);
1574
+ ascii = "";
1575
+ }
1576
+ let j = i;
1577
+ while (j < chars.length && chars[j] === ch) j++;
1578
+ const repeat = j - i;
1579
+ if (repeat > 1) {
1580
+ for (let k = 0; k < repeat; k += CHUNK) runs.push(ch.repeat(Math.min(CHUNK, repeat - k)));
1581
+ i = j;
1582
+ continue;
1583
+ }
1584
+ let stretch = "";
1585
+ while (i < chars.length && chars[i].charCodeAt(0) >= 128 && chars[i] !== chars[i + 1]) {
1586
+ stretch += chars[i];
1587
+ i++;
1588
+ }
1589
+ runs.push(stretch);
1590
+ }
1591
+ if (ascii) runs.push(ascii);
1592
+ return runs;
1593
+ }
1594
+ const runCache = new WeakMap();
1595
+ function lineNode(line, theme) {
1596
+ const cached$1 = runCache.get(line);
1597
+ if (cached$1 && cached$1.theme === theme) return cached$1.node;
1598
+ const node = /* @__PURE__ */ React.createElement(ink.Box, {
1599
+ height: 1,
1600
+ overflow: "hidden",
1601
+ flexShrink: 0
1602
+ }, line.spans.length === 0 ? /* @__PURE__ */ React.createElement(Text, null, " ") : line.spans.flatMap((s, si) => {
1603
+ const props = spanProps(s, theme);
1604
+ return splitRuns(s.text).map((run, ri) => /* @__PURE__ */ React.createElement(Text, {
1605
+ key: `${si}:${ri}`,
1606
+ wrap: "truncate",
1607
+ ...props
1608
+ }, run));
1609
+ }));
1610
+ runCache.set(line, {
1611
+ theme,
1612
+ node
1613
+ });
1614
+ return node;
1615
+ }
1616
+ function Lines({ lines, theme, override }) {
1617
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1618
+ flexDirection: "column",
1619
+ flexShrink: 0
1620
+ }, lines.map((line, i) => /* @__PURE__ */ React.createElement(React.Fragment, { key: i }, override?.(line, i) ?? lineNode(line, theme))));
1621
+ }
1622
+
1623
+ //#endregion
1624
+ //#region src/lines/Status.tsx
1625
+ function defaultPhaseLabel(phase, headline) {
1626
+ switch (phase.kind) {
1627
+ case "waiting": return "waiting";
1628
+ case "thinking": return "thinking";
1629
+ case "writing": return "writing";
1630
+ case "tool": return `running ${headline}`;
1631
+ case "approval": return `awaiting approval ${headline}`;
1632
+ }
1633
+ }
1634
+ function StatusLine({ width: width$2, live, notice, idle, below = 0, extras = [], phaseLabel, slowHint, accent, measure = defaultMeasure }) {
1635
+ const { glyphs, theme } = useInkComponents();
1636
+ const ticks = hooks.useTicks(live !== void 0, 1e3);
1637
+ const now = hooks.useClock(false);
1638
+ const rule = glyphs.boxHorizontal;
1639
+ const phase = live ? livePhase(live.message) : void 0;
1640
+ const call = phase?.kind === "tool" || phase?.kind === "approval" ? phase.call : void 0;
1641
+ const headline = React.useMemo(() => call ? truncate(toolHeadline(call), 32) : void 0, [
1642
+ call,
1643
+ call?.title,
1644
+ call?.name,
1645
+ call?.inputSummary,
1646
+ call?.arguments
1647
+ ]);
1648
+ let label = null;
1649
+ let labelText = "";
1650
+ if (live) {
1651
+ const current = phase;
1652
+ const elapsed = now - live.startedAt;
1653
+ const facts = [phaseLabel?.(current) ?? defaultPhaseLabel(current, headline), formatElapsed(elapsed, { live: true })];
1654
+ const tools = countActivity(live.message.blocks).tools.total;
1655
+ if (tools > 0) facts.push(`${tools} tool${tools === 1 ? "" : "s"}`);
1656
+ if (slowHint && elapsed >= slowHint.afterMs && slowHint.phases.includes(current.kind)) facts.push(slowHint.text);
1657
+ facts.push(...extras);
1658
+ const pulse = glyphs.pulse[ticks % glyphs.pulse.length] ?? "";
1659
+ const factText = stripControls(facts.join(glyphs.separator));
1660
+ labelText = ` ${pulse} ${factText} `;
1661
+ label = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, null, " "), /* @__PURE__ */ React.createElement(Text, appearanceProps(accent ?? theme.accent), pulse), /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, " ", factText, " "));
1662
+ } else if (notice) {
1663
+ const text = ` ${notice.tone === "positive" ? glyphs.success : glyphs.error} ${stripControls(notice.text)} `;
1664
+ labelText = text;
1665
+ label = /* @__PURE__ */ React.createElement(Styled, { tone: notice.tone }, text);
1666
+ } else if (idle || below > 0) {
1667
+ labelText = ` ${[idle && stripControls(idle), below > 0 ? `↓ ${below} more` : void 0].filter(Boolean).join(glyphs.separator)} `;
1668
+ label = /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, labelText);
1669
+ }
1670
+ const lead = rule.repeat(2);
1671
+ const rest = Math.max(0, width$2 - measure(lead) - measure(labelText));
1672
+ if (measure(lead) + measure(labelText) > width$2) return /* @__PURE__ */ React.createElement(ink.Box, {
1673
+ height: 1,
1674
+ overflow: "hidden"
1675
+ }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Styled, { tone: "chrome" }, lead), truncate(labelText, Math.max(0, width$2 - measure(lead)), measure)));
1676
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1677
+ height: 1,
1678
+ overflow: "hidden"
1679
+ }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Styled, { tone: "chrome" }, lead), label, /* @__PURE__ */ React.createElement(Styled, { tone: "chrome" }, rule.repeat(rest))));
1680
+ }
1681
+ /** Messages queued while an answer streams (the queue itself is host state). */
1682
+ function PendingRows({ items, width: width$2, maxRows = 3, measure = defaultMeasure }) {
1683
+ const { glyphs } = useInkComponents();
1684
+ if (items.length === 0) return null;
1685
+ const shown = items.slice(-maxRows);
1686
+ const hidden = items.length - shown.length;
1687
+ return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column" }, shown.map((item, i) => {
1688
+ const more = i === 0 && hidden > 0 ? `+${hidden} · ` : "";
1689
+ const clip = item.attachments ? `${glyphs.attachment}${item.attachments} ` : "";
1690
+ const text = `${more}${clip}${stripControls(item.text).replace(/\s+/g, " ").trim()}`;
1691
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1692
+ key: i,
1693
+ height: 1,
1694
+ overflow: "hidden"
1695
+ }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Styled, { tone: "caution" }, glyphs.pending, " "), /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, truncate(text, Math.max(1, width$2 - measure(glyphs.pending) - 1), measure))));
1696
+ }));
1697
+ }
1698
+ /** A centred empty state; pass it as the viewport's `empty`. */
1699
+ function EmptyState({ title, subtitle, notes = [], accent }) {
1700
+ const { theme } = useInkComponents();
1701
+ return /* @__PURE__ */ React.createElement(ink.Box, {
1702
+ flexDirection: "column",
1703
+ alignItems: "center"
1704
+ }, /* @__PURE__ */ React.createElement(Text, {
1705
+ ...appearanceProps(accent ?? theme.accent),
1706
+ bold: true
1707
+ }, title), subtitle ? /* @__PURE__ */ React.createElement(Styled, { tone: "muted" }, subtitle) : null, notes.map((note) => /* @__PURE__ */ React.createElement(Styled, {
1708
+ key: note,
1709
+ tone: "muted"
1710
+ }, note)));
1711
+ }
1712
+
1713
+ //#endregion
1714
+ //#region src/lines/model.ts
1715
+ /** A span. */
1716
+ function span(text, extra = {}) {
1717
+ return {
1718
+ text,
1719
+ ...extra
1720
+ };
1721
+ }
1722
+ /** Prepend spans, keeping the line's metadata. */
1723
+ function prefix(line, spans) {
1724
+ return {
1725
+ ...line,
1726
+ spans: [...spans, ...line.spans]
1727
+ };
1728
+ }
1729
+ /** Append spans, keeping the line's metadata. */
1730
+ function suffix(line, spans) {
1731
+ return {
1732
+ ...line,
1733
+ spans: [...line.spans, ...spans]
1734
+ };
1735
+ }
1736
+ /** The line's text as drawn. */
1737
+ function lineText(line) {
1738
+ return line.spans.map((s) => s.text).join("");
1739
+ }
1740
+
1741
+ //#endregion
1742
+ //#region src/lines/frames.ts
1743
+ const blank$1 = () => ({
1744
+ spans: [],
1745
+ frame: true
1746
+ });
1747
+ /**
1748
+ * Compact: a role glyph before the first row (`›` user, `•` answer), rows
1749
+ * indented under it, a dim footer line, a blank line after each message.
1750
+ */
1751
+ const compactFrame = {
1752
+ id: "compact",
1753
+ innerWidth: (_message, width$2) => Math.max(1, width$2 - 2),
1754
+ layout({ message, body, footer, ctx, accent }) {
1755
+ const user = message.role === "user";
1756
+ const glyph = user ? ctx.glyphs.user : ctx.glyphs.assistant;
1757
+ const lead = span(`${glyph} `, user ? {
1758
+ role: "user",
1759
+ chrome: true
1760
+ } : {
1761
+ style: accent,
1762
+ chrome: true
1763
+ });
1764
+ const pad$1 = span(" ", { chrome: true });
1765
+ const lines = body.map((line, i) => ({
1766
+ ...line,
1767
+ spans: [i === 0 ? lead : pad$1, ...line.spans]
1768
+ }));
1769
+ if (lines.length === 0 && footer) lines.push({
1770
+ spans: [lead],
1771
+ frame: true
1772
+ });
1773
+ if (footer) lines.push({
1774
+ spans: [pad$1, ...footer],
1775
+ frame: true
1776
+ });
1777
+ if (lines.length > 0) lines.push(blank$1());
1778
+ return lines;
1779
+ }
1780
+ };
1781
+ /**
1782
+ * Threaded: the user's message in a round-cornered box with its time set into
1783
+ * the top edge; an answer hangs from the box on a gutter in its accent and
1784
+ * ends with a footer corner (bare when the answer has no footer facts). A live answer that shows nothing yet draws
1785
+ * nothing.
1786
+ */
1787
+ const threadedFrame = {
1788
+ id: "threaded",
1789
+ innerWidth: (message, width$2) => Math.max(1, width$2 - (message.role === "user" ? 4 : 2)),
1790
+ layout(input) {
1791
+ return input.message.role === "user" ? userBox(input) : answerGutter(input);
1792
+ }
1793
+ };
1794
+ function userBox({ message, ctx, nextShows }) {
1795
+ const g = ctx.glyphs;
1796
+ const max = Math.max(1, ctx.width - 4);
1797
+ const text = message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
1798
+ const rows = wrapLines(text, max, ctx.measure);
1799
+ for (const attachment of message.attachments ?? []) {
1800
+ const label = attachment.description ?? attachment.name ?? attachment.mediaType;
1801
+ rows.push({
1802
+ text: truncate(`${g.attachment} ${label}`, max, ctx.measure),
1803
+ dim: true
1804
+ });
1805
+ }
1806
+ const time = message.createdAt !== void 0 ? formatClockTime(message.createdAt).slice(0, 5) : "";
1807
+ const timeLabel = time ? ` ${time} ` : "";
1808
+ const inner = Math.min(max, Math.max(ctx.measure(timeLabel) + 2, ...rows.map((row) => ctx.measure(row.text))));
1809
+ const edge = (n) => g.boxHorizontal.repeat(Math.max(0, n));
1810
+ const chrome = (text$1) => span(text$1, {
1811
+ role: "chrome",
1812
+ chrome: true
1813
+ });
1814
+ const topFill = inner + 2 - ctx.measure(timeLabel) - 1;
1815
+ const lines = [
1816
+ {
1817
+ spans: [chrome(`${g.boxTopLeft}${edge(topFill)}${timeLabel}${edge(1)}${g.boxTopRight}`)],
1818
+ frame: true
1819
+ },
1820
+ ...rows.map((row) => ({
1821
+ spans: [
1822
+ chrome(`${g.boxVertical} `),
1823
+ span(row.text, {
1824
+ role: "user",
1825
+ ...row.dim ? {
1826
+ dim: true,
1827
+ bold: false
1828
+ } : {}
1829
+ }),
1830
+ chrome(`${" ".repeat(Math.max(0, inner - ctx.measure(row.text)))} ${g.boxVertical}`)
1831
+ ],
1832
+ ...row.wrap ? { wrap: row.wrap } : {}
1833
+ })),
1834
+ {
1835
+ spans: [chrome(`${nextShows ? g.boxJunction : g.boxBottomLeft}${edge(inner + 2)}${g.boxBottomRight}`)],
1836
+ frame: true
1837
+ }
1838
+ ];
1839
+ if (!nextShows) lines.push(blank$1());
1840
+ return lines;
1841
+ }
1842
+ function answerGutter({ body, footer, shows, accent, ctx }) {
1843
+ if (!shows) return [];
1844
+ const g = ctx.glyphs;
1845
+ const gutter = span(`${g.boxVertical} `, {
1846
+ style: accent,
1847
+ chrome: true
1848
+ });
1849
+ const bare = {
1850
+ spans: [span(g.boxVertical, {
1851
+ style: accent,
1852
+ chrome: true
1853
+ })],
1854
+ frame: true
1855
+ };
1856
+ const lines = [bare, ...body.map((line) => ({
1857
+ ...line,
1858
+ spans: [gutter, ...line.spans]
1859
+ }))];
1860
+ if (footer) lines.push(bare, {
1861
+ spans: [span(`${g.footer} `, {
1862
+ style: accent,
1863
+ chrome: true
1864
+ }), ...footer],
1865
+ frame: true
1866
+ });
1867
+ else lines.push({
1868
+ spans: [span(g.footer, {
1869
+ style: accent,
1870
+ chrome: true
1871
+ })],
1872
+ frame: true
1873
+ });
1874
+ lines.push(blank$1());
1875
+ return lines;
1876
+ }
1877
+
1878
+ //#endregion
1879
+ //#region src/lines/markdown/inline.ts
1880
+ const PUNCTUATION = /[!-/:-@[-`{-~]/;
1881
+ const WORD = /[\p{L}\p{N}]/u;
1882
+ const SPACE = /\s/;
1883
+ const ENTITY = /^&(?:#(\d{1,7})|#[xX]([\da-fA-F]{1,6})|(amp|lt|gt|quot|apos|nbsp));/;
1884
+ const NAMED = {
1885
+ amp: "&",
1886
+ lt: "<",
1887
+ gt: ">",
1888
+ quot: "\"",
1889
+ apos: "'",
1890
+ nbsp: "\xA0"
1891
+ };
1892
+ /**
1893
+ * The character reference at the start of `text` and its length, or
1894
+ * undefined. A numeric one naming a control character (or no character)
1895
+ * stays as written: decoding must not smuggle controls past sanitising.
1896
+ */
1897
+ function entity(text) {
1898
+ const match = ENTITY.exec(text);
1899
+ if (!match) return void 0;
1900
+ if (match[3]) return [NAMED[match[3]], match[0].length];
1901
+ const code = match[1] ? Number.parseInt(match[1], 10) : Number.parseInt(match[2], 16);
1902
+ const control = code < 32 || code >= 127 && code < 160;
1903
+ if (control || code > 1114111 || code >= 55296 && code < 57344) return void 0;
1904
+ return [String.fromCodePoint(code), match[0].length];
1905
+ }
1906
+ /** Show a source the way a reader can use it: a `data:` URL as its type. */
1907
+ function describeSource(src) {
1908
+ if (!src.startsWith("data:")) return src;
1909
+ return src.slice(5).split(/[;,]/)[0] || "data";
1910
+ }
1911
+ /** Merge adjacent spans with identical attributes. */
1912
+ function mergeSpans(spans) {
1913
+ const out = [];
1914
+ for (const s of spans) {
1915
+ if (s.text === "") continue;
1916
+ const last = out.at(-1);
1917
+ if (last && sameStyle(last, s)) out[out.length - 1] = {
1918
+ ...last,
1919
+ text: last.text + s.text
1920
+ };
1921
+ else out.push(s);
1922
+ }
1923
+ return out;
1924
+ }
1925
+ function sameStyle(a, b) {
1926
+ return a.role === b.role && a.style === b.style && a.bold === b.bold && a.italic === b.italic && a.dim === b.dim && a.underline === b.underline && a.strike === b.strike && a.inverse === b.inverse && a.color === b.color && a.chrome === b.chrome && a.copyEach === b.copyEach;
1927
+ }
1928
+ /** End of the code span opening at `i` (index after its closing run), or -1. */
1929
+ function codeSpanEnd(text, i) {
1930
+ let run = 0;
1931
+ while (text[i + run] === "`") run++;
1932
+ const fence$1 = "`".repeat(run);
1933
+ let j = text.indexOf(fence$1, i + run);
1934
+ while (j !== -1) {
1935
+ let k = 0;
1936
+ while (text[j + k] === "`") k++;
1937
+ if (k === run) return j + run;
1938
+ j = text.indexOf(fence$1, j + k);
1939
+ }
1940
+ return -1;
1941
+ }
1942
+ /** Index of the bracket closing the one at `open`, skipping code spans and escapes. */
1943
+ function matching(text, open, left, right) {
1944
+ let depth = 0;
1945
+ for (let i = open; i < text.length; i++) {
1946
+ const ch = text[i];
1947
+ if (ch === "\\") i++;
1948
+ else if (ch === "`") {
1949
+ const end = codeSpanEnd(text, i);
1950
+ if (end !== -1) i = end - 1;
1951
+ } else if (ch === left) depth++;
1952
+ else if (ch === right && --depth === 0) return i;
1953
+ }
1954
+ return -1;
1955
+ }
1956
+ function runLength(text, i) {
1957
+ let n = 0;
1958
+ while (text[i + n] === text[i]) n++;
1959
+ return n;
1960
+ }
1961
+ const isWordChar = (ch) => ch !== void 0 && WORD.test(ch);
1962
+ const isSpace = (ch) => ch === void 0 || SPACE.test(ch);
1963
+ /** Where the emphasis run opening at `i` (length `run`) closes, or -1. */
1964
+ function closer(text, i, run) {
1965
+ const marker = text[i];
1966
+ for (let j = i + run; j < text.length; j++) {
1967
+ const ch = text[j];
1968
+ if (ch === "\\") {
1969
+ j++;
1970
+ continue;
1971
+ }
1972
+ if (ch === "`") {
1973
+ const end = codeSpanEnd(text, j);
1974
+ if (end !== -1) j = end - 1;
1975
+ continue;
1976
+ }
1977
+ if (ch !== marker) continue;
1978
+ const n = runLength(text, j);
1979
+ if (n === run && !isSpace(text[j - 1]) && !isWordChar(text[j + n]) && j > i + run) return j;
1980
+ j += n - 1;
1981
+ }
1982
+ return -1;
1983
+ }
1984
+ /** A bare `http(s)://` URL at `i`, trailing punctuation excluded. */
1985
+ function bareUrl(text, i) {
1986
+ const match = /^https?:\/\/[^\s<>]+/.exec(text.slice(i));
1987
+ if (!match) return void 0;
1988
+ return match[0].replace(/[.,;:!?'")\]]+$/, "");
1989
+ }
1990
+ /** Parse inline markdown into spans, every span starting from `base`. */
1991
+ function parseInline(text, base, glyphs) {
1992
+ const out = [];
1993
+ let plain = "";
1994
+ const flush = () => {
1995
+ if (plain) out.push({
1996
+ text: plain,
1997
+ ...base
1998
+ });
1999
+ plain = "";
2000
+ };
2001
+ const emit = (spans) => {
2002
+ flush();
2003
+ out.push(...spans);
2004
+ };
2005
+ let i = 0;
2006
+ while (i < text.length) {
2007
+ const ch = text[i];
2008
+ const prev = text[i - 1];
2009
+ if (ch === "\\" && i + 1 < text.length && PUNCTUATION.test(text[i + 1])) {
2010
+ plain += text[i + 1];
2011
+ i += 2;
2012
+ continue;
2013
+ }
2014
+ if (ch === "`") {
2015
+ const end = codeSpanEnd(text, i);
2016
+ if (end !== -1) {
2017
+ const run$1 = runLength(text, i);
2018
+ let code = text.slice(i + run$1, end - run$1);
2019
+ if (code.startsWith(" ") && code.endsWith(" ") && code.trim()) code = code.slice(1, -1);
2020
+ emit([{
2021
+ text: code,
2022
+ ...base,
2023
+ role: "code"
2024
+ }]);
2025
+ i = end;
2026
+ continue;
2027
+ }
2028
+ const run = runLength(text, i);
2029
+ plain += text.slice(i, i + run);
2030
+ i += run;
2031
+ continue;
2032
+ }
2033
+ if ((ch === "[" || ch === "!" && text[i + 1] === "[") && text.length > i + 1) {
2034
+ const image = ch === "!";
2035
+ const open = image ? i + 1 : i;
2036
+ const close = matching(text, open, "[", "]");
2037
+ if (close !== -1 && text[close + 1] === "(") {
2038
+ const end = matching(text, close + 1, "(", ")");
2039
+ if (end !== -1) {
2040
+ const label = text.slice(open + 1, close);
2041
+ const url = text.slice(close + 2, end).trim().replace(/\s+"[^"]*"$/, "").replace(/^<(.*)>$/, "$1");
2042
+ if (image) emit([
2043
+ {
2044
+ text: `${glyphs.image} `,
2045
+ ...base
2046
+ },
2047
+ ...label ? [{
2048
+ text: `${label} `,
2049
+ ...base
2050
+ }] : [],
2051
+ {
2052
+ text: `(${describeSource(url)})`,
2053
+ ...base,
2054
+ role: "muted"
2055
+ }
2056
+ ]);
2057
+ else {
2058
+ emit(parseInline(label, {
2059
+ ...base,
2060
+ role: "link"
2061
+ }, glyphs));
2062
+ if (url && url !== label) emit([{
2063
+ text: ` (${url})`,
2064
+ ...base,
2065
+ role: "muted"
2066
+ }]);
2067
+ }
2068
+ i = end + 1;
2069
+ continue;
2070
+ }
2071
+ }
2072
+ }
2073
+ if (ch === "<") {
2074
+ const match = /^<((?:https?|mailto):[^\s<>]+)>/.exec(text.slice(i));
2075
+ if (match) {
2076
+ emit([{
2077
+ text: match[1],
2078
+ ...base,
2079
+ role: "link"
2080
+ }]);
2081
+ i += match[0].length;
2082
+ continue;
2083
+ }
2084
+ }
2085
+ if (ch === "h" && (prev === void 0 || /[\s(]/.test(prev))) {
2086
+ const url = bareUrl(text, i);
2087
+ if (url) {
2088
+ emit([{
2089
+ text: url,
2090
+ ...base,
2091
+ role: "link"
2092
+ }]);
2093
+ i += url.length;
2094
+ continue;
2095
+ }
2096
+ }
2097
+ if (ch === "*" || ch === "_" || ch === "~") {
2098
+ const run = runLength(text, i);
2099
+ const valid = ch === "~" ? run === 2 : run <= 3;
2100
+ if (valid && !isWordChar(prev) && !isSpace(text[i + run])) {
2101
+ const end = closer(text, i, run);
2102
+ if (end !== -1) {
2103
+ const style = ch === "~" ? { strike: true } : run === 1 ? { italic: true } : run === 2 ? { bold: true } : {
2104
+ bold: true,
2105
+ italic: true
2106
+ };
2107
+ emit(parseInline(text.slice(i + run, end), {
2108
+ ...base,
2109
+ ...style
2110
+ }, glyphs));
2111
+ i = end + run;
2112
+ continue;
2113
+ }
2114
+ }
2115
+ plain += text.slice(i, i + run);
2116
+ i += run;
2117
+ continue;
2118
+ }
2119
+ if (ch === "&") {
2120
+ const ref = entity(text.slice(i, i + 12));
2121
+ if (ref) {
2122
+ plain += ref[0];
2123
+ i += ref[1];
2124
+ continue;
2125
+ }
2126
+ }
2127
+ plain += ch;
2128
+ i++;
2129
+ }
2130
+ flush();
2131
+ return mergeSpans(out);
2132
+ }
2133
+
2134
+ //#endregion
2135
+ //#region src/lines/markdown/wrap.ts
2136
+ function words(spans, measure) {
2137
+ const out = [{
2138
+ pieces: [],
2139
+ width: 0
2140
+ }];
2141
+ for (const s of spans) s.text.split(" ").forEach((part, i) => {
2142
+ if (i > 0) out.push({
2143
+ pieces: [],
2144
+ width: 0,
2145
+ gap: {
2146
+ ...s,
2147
+ text: " "
2148
+ }
2149
+ });
2150
+ const word = out.at(-1);
2151
+ if (part) {
2152
+ word.pieces.push({
2153
+ ...s,
2154
+ text: part
2155
+ });
2156
+ word.width += measure(part);
2157
+ }
2158
+ });
2159
+ return out;
2160
+ }
2161
+ /** Cut a word's pieces into chunks of at most `max` cells, never inside a grapheme. */
2162
+ function split(word, max, measure) {
2163
+ const chunks$1 = [{
2164
+ pieces: [],
2165
+ width: 0
2166
+ }];
2167
+ for (const piece of word.pieces) for (const g of graphemes(piece.text)) {
2168
+ const w = measure(g);
2169
+ let chunk = chunks$1.at(-1);
2170
+ if (chunk.width + w > max && chunk.width > 0) {
2171
+ chunk = {
2172
+ pieces: [],
2173
+ width: 0
2174
+ };
2175
+ chunks$1.push(chunk);
2176
+ }
2177
+ chunk.pieces.push({
2178
+ ...piece,
2179
+ text: g
2180
+ });
2181
+ chunk.width += w;
2182
+ }
2183
+ return chunks$1;
2184
+ }
2185
+ function wrapSpans(spans, width$2, measure) {
2186
+ const max = Math.max(1, width$2);
2187
+ const rows = [];
2188
+ let current = [];
2189
+ let currentWidth = 0;
2190
+ let join;
2191
+ let started = false;
2192
+ const push = (next) => {
2193
+ const spans$1 = mergeSpans(current);
2194
+ rows.push(join ? {
2195
+ spans: spans$1,
2196
+ wrap: join
2197
+ } : { spans: spans$1 });
2198
+ current = [];
2199
+ currentWidth = 0;
2200
+ started = false;
2201
+ join = next;
2202
+ };
2203
+ for (const word of words(spans, measure)) {
2204
+ const gap = started && word.gap ? 1 : 0;
2205
+ if (started && currentWidth + gap + word.width > max) push("soft");
2206
+ else if (started && word.gap) {
2207
+ current.push(word.gap);
2208
+ currentWidth += 1;
2209
+ }
2210
+ if (word.width > max) {
2211
+ split(word, max, measure).forEach((chunk, i) => {
2212
+ if (i > 0) push("hard");
2213
+ current.push(...chunk.pieces);
2214
+ currentWidth = chunk.width;
2215
+ started = true;
2216
+ });
2217
+ continue;
2218
+ }
2219
+ current.push(...word.pieces);
2220
+ currentWidth += word.width;
2221
+ started = true;
2222
+ }
2223
+ push(void 0);
2224
+ return rows;
2225
+ }
2226
+
2227
+ //#endregion
2228
+ //#region src/lines/images.ts
2229
+ /** The largest thumbnail, in cells. */
2230
+ const THUMBNAIL = {
2231
+ columns: 32,
2232
+ rows: 8
2233
+ };
2234
+ const HINT = {
2235
+ ask: () => "click to show",
2236
+ loading: () => "loading…",
2237
+ failed: (state) => `✗ ${state.error}`,
2238
+ ready: (_, expanded) => expanded ? "click to shrink" : "click to enlarge"
2239
+ };
2240
+ /** Lay out a block image: its ready rows, then a wrapped caption; every line targets the source. */
2241
+ function layoutImage(src, alt, width$2, { provider, expanded, maxHeight, glyphs, measure }) {
2242
+ const box = expanded ? {
2243
+ columns: Math.max(1, width$2),
2244
+ rows: Math.max(1, maxHeight)
2245
+ } : {
2246
+ columns: Math.max(1, Math.min(THUMBNAIL.columns, width$2)),
2247
+ rows: THUMBNAIL.rows
2248
+ };
2249
+ const state = provider?.get(src, box);
2250
+ const target = {
2251
+ kind: "image",
2252
+ src
2253
+ };
2254
+ const image = state?.kind === "ready" ? state.rows.map((row) => ({
2255
+ spans: [span(row, state.color ? { color: state.color } : {})],
2256
+ target
2257
+ })) : [];
2258
+ const caption = [
2259
+ span(`${glyphs.image} `, { role: "muted" }),
2260
+ ...alt ? [span(alt, {
2261
+ role: "content",
2262
+ italic: true
2263
+ }), span(" ")] : [],
2264
+ span(describeSource(src), { role: "link" }),
2265
+ ...state ? [span(` ${HINT[state.kind](state, expanded)}`, {
2266
+ role: "muted",
2267
+ dim: true
2268
+ })] : []
2269
+ ];
2270
+ const clean$1 = caption.map((s) => ({
2271
+ ...s,
2272
+ text: stripControls(s.text)
2273
+ }));
2274
+ return [...image, ...wrapSpans(clean$1, width$2, measure).map((row) => ({
2275
+ ...row,
2276
+ target
2277
+ }))];
2278
+ }
2279
+
2280
+ //#endregion
2281
+ //#region src/lines/layout.ts
2282
+ const NOTHING_OPEN = {
2283
+ version: 0,
2284
+ isOpen: () => false
2285
+ };
2286
+ /** The path a collapsible row's open state is kept under. */
2287
+ const ROW_PATH = "#row";
2288
+ /** The default markdown layout: plain text, wrapped, in the `content` role. */
2289
+ const plainMarkdownLayout = (source, width$2, { measure }) => wrapLines(source, width$2, measure).map((row) => ({
2290
+ spans: [span(row.text, { role: "content" })],
2291
+ ...row.wrap ? { wrap: row.wrap } : {}
2292
+ }));
2293
+ const width$1 = (spans, measure) => spans.reduce((sum, s) => sum + measure(s.text), 0);
2294
+ /** Resolve an accent to an explicit appearance. */
2295
+ function resolveAccent(message, ctx) {
2296
+ const accent = ctx.accentFor?.(message) ?? "assistant";
2297
+ return typeof accent === "string" ? ctx.theme[accent] : accent;
2298
+ }
2299
+ function wrapped(text, w, measure, extra) {
2300
+ return wrapLines(text, w, measure).map((row) => ({
2301
+ spans: [span(row.text, extra)],
2302
+ ...row.wrap ? { wrap: row.wrap } : {}
2303
+ }));
2304
+ }
2305
+ function indent(lines, cells$1) {
2306
+ const pad$1 = span(" ".repeat(cells$1), { chrome: true });
2307
+ return lines.map((line) => ({
2308
+ ...line,
2309
+ spans: [pad$1, ...line.spans]
2310
+ }));
2311
+ }
2312
+ function owned(lines, owner) {
2313
+ return lines.map((line) => line.owner ? line : {
2314
+ ...line,
2315
+ owner
2316
+ });
2317
+ }
2318
+ /**
2319
+ * Cells of label (up to its whole width) a header keeps before it gives up its
2320
+ * time, then its `after` spans.
2321
+ */
2322
+ const LABEL_MIN = 16;
2323
+ /**
2324
+ * One header row, truncated to `w` (affordance, icon and qualifier exempt). The
2325
+ * timer and the `after` spans are exempt too, until the label would be cut
2326
+ * below {@link LABEL_MIN} cells: then the timer goes, then the `after` spans.
2327
+ */
2328
+ function headerRow(parts, w, ctx) {
2329
+ const fixed = [parts.affordance, span(" ")];
2330
+ if (parts.prefix) fixed.push(parts.prefix, span(" "));
2331
+ if (parts.icon) fixed.push(span(parts.icon, { role: "marker" }), span(" "));
2332
+ const label = parts.label.replace(/\s+/g, " ").trim();
2333
+ const least = Math.min(LABEL_MIN, ctx.measure(label));
2334
+ let trailing = parts.trailing;
2335
+ let after$1 = parts.after ?? [];
2336
+ const tailOf = () => [...parts.qualifier ? [span(" "), parts.qualifier] : [], ...trailing ? [span(` ${trailing}`, { role: "muted" })] : []];
2337
+ const room = () => w - width$1(fixed, ctx.measure) - width$1(after$1, ctx.measure) - width$1(tailOf(), ctx.measure);
2338
+ if (room() < least) trailing = void 0;
2339
+ if (room() < least) after$1 = [];
2340
+ const tail = tailOf();
2341
+ const budget = Math.max(1, room());
2342
+ const detail = parts.detail ? parts.detail.replace(/\s+/g, " ").trim() : void 0;
2343
+ const full = detail ? `${label} ${detail}` : label;
2344
+ const cut = truncate(full, budget, ctx.measure);
2345
+ const labelPart = cut.slice(0, Math.min(label.length, cut.length));
2346
+ const detailPart = cut.slice(labelPart.length);
2347
+ const error = parts.error || parts.errorWithoutAfter && after$1.length === 0;
2348
+ const middle = [span(labelPart, { role: error ? "danger" : "strong" }), ...after$1];
2349
+ if (detailPart) middle.push(span(detailPart, { role: "muted" }));
2350
+ return {
2351
+ spans: [
2352
+ ...fixed,
2353
+ ...middle,
2354
+ ...tail
2355
+ ],
2356
+ ...parts.target ? { target: parts.target } : {}
2357
+ };
2358
+ }
2359
+ function spinnerFrame(ctx) {
2360
+ const frames = ctx.glyphs.spinner;
2361
+ return frames[Math.floor(ctx.now / 1e3) % frames.length] ?? "";
2362
+ }
2363
+ function liveTimer(startedAt, durationMs, live, ctx) {
2364
+ if (durationMs !== void 0) return formatDuration(durationMs);
2365
+ if (live && startedAt !== void 0 && Number.isFinite(startedAt)) return formatElapsed(ctx.now - startedAt, { live: true });
2366
+ return void 0;
2367
+ }
2368
+ function toolGlyph(tool, live, ctx) {
2369
+ const g = ctx.glyphs;
2370
+ if (tool.status === "success") return span(g.toolSuccess, {
2371
+ role: "positive",
2372
+ dim: true
2373
+ });
2374
+ if (tool.status === "error") return span(g.toolError, { role: "danger" });
2375
+ return live ? span(spinnerFrame(ctx), { role: "marker" }) : span(g.toolIdle, { role: "caution" });
2376
+ }
2377
+ const APPROVAL = {
2378
+ denied: ["· denied", "danger"],
2379
+ unavailable: ["· refused", "danger"],
2380
+ pending: ["· awaiting approval", "caution"]
2381
+ };
2382
+ function approvalQualifier(tool) {
2383
+ const approval = tool?.approval ? APPROVAL[tool.approval.state] : void 0;
2384
+ return approval ? span(approval[0], { role: approval[1] }) : void 0;
2385
+ }
2386
+ function toolOutput(tool, b) {
2387
+ if (tool.result === void 0) return [];
2388
+ const { ctx } = b;
2389
+ const text = toolResultText(tool.result);
2390
+ const bar = span(` ${ctx.glyphs.outputBar} `, {
2391
+ chrome: true,
2392
+ dim: true
2393
+ });
2394
+ const inner = Math.max(1, b.width - ctx.measure(bar.text));
2395
+ const target = {
2396
+ kind: "tool-output",
2397
+ blockId: tool.id
2398
+ };
2399
+ let rows;
2400
+ if (looksLikeJson(text)) {
2401
+ const parsed = parseLooseJson(text);
2402
+ const full = ctx.detail === "full";
2403
+ const jsonRows = layoutJson(parsed.node, {
2404
+ ...full ? {} : { maxString: 80 },
2405
+ collapsed: (path) => !full && !ctx.opened.isOpen(tool.id, path)
2406
+ });
2407
+ rows = jsonRows.flatMap((row) => {
2408
+ const pad$1 = " ".repeat(row.depth);
2409
+ if (row.toggle) {
2410
+ const marker = row.toggle.open ? ctx.glyphs.expanded : ctx.glyphs.collapsed;
2411
+ const lead = " ".repeat(row.depth - 1);
2412
+ const markerSpan = span(`${marker} `, {
2413
+ role: "link",
2414
+ chrome: true,
2415
+ copyEach: " "
2416
+ });
2417
+ const size = row.toggle.open ? "" : ` · ${row.toggle.size}`;
2418
+ const available = Math.max(1, inner - ctx.measure(lead) - 2 - ctx.measure(size));
2419
+ return [{
2420
+ spans: [
2421
+ span(lead),
2422
+ markerSpan,
2423
+ span(truncate(row.text, available, ctx.measure), {
2424
+ role: "content",
2425
+ dim: true
2426
+ }),
2427
+ ...size ? [span(size, {
2428
+ chrome: true,
2429
+ dim: true
2430
+ })] : []
2431
+ ],
2432
+ target: {
2433
+ kind: "json-toggle",
2434
+ blockId: tool.id,
2435
+ path: row.toggle.path,
2436
+ open: row.toggle.open
2437
+ }
2438
+ }];
2439
+ }
2440
+ const pieces = wrapLines(row.text, Math.max(1, inner - ctx.measure(pad$1)), ctx.measure);
2441
+ return pieces.map((piece, i) => ({
2442
+ spans: [span(i === 0 ? pad$1 : `${pad$1} `), span(piece.text, {
2443
+ role: "content",
2444
+ dim: true
2445
+ })],
2446
+ ...piece.wrap ? { wrap: piece.wrap } : {},
2447
+ target
2448
+ }));
2449
+ });
2450
+ if (parsed.rest) rows.push(...wrapped(parsed.rest, inner, ctx.measure, {
2451
+ role: "content",
2452
+ dim: true
2453
+ }).map((r) => ({
2454
+ ...r,
2455
+ target
2456
+ })));
2457
+ } else rows = wrapped(text, inner, ctx.measure, {
2458
+ role: "content",
2459
+ dim: true
2460
+ }).map((r) => ({
2461
+ ...r,
2462
+ target
2463
+ }));
2464
+ return rows.map((row) => ({
2465
+ ...row,
2466
+ spans: [bar, ...row.spans]
2467
+ }));
2468
+ }
2469
+ function toolRow(tool, b) {
2470
+ const live = isBlockActive(tool, {
2471
+ isActive: b.live,
2472
+ outcome: b.message.outcome
2473
+ });
2474
+ const row = headerRow({
2475
+ affordance: toolGlyph(tool, live, b.ctx),
2476
+ label: toolLabel(tool) || "tool",
2477
+ detail: toolDisplayInput(tool),
2478
+ qualifier: approvalQualifier(tool),
2479
+ trailing: liveTimer(positiveTimestamp(tool.startedAt), tool.durationMs, live, b.ctx),
2480
+ error: tool.status === "error",
2481
+ target: {
2482
+ kind: "row-toggle",
2483
+ rowKey: tool.id
2484
+ }
2485
+ }, b.width, b.ctx);
2486
+ const showOutput = b.ctx.detail !== "collapsed" || b.ctx.opened.isOpen(tool.id, ROW_PATH);
2487
+ return [row, ...showOutput ? toolOutput(tool, b) : []];
2488
+ }
2489
+ function planRows(plan, b) {
2490
+ const g = b.ctx.glyphs;
2491
+ return plan.entries.flatMap((entry) => {
2492
+ const done = entry.status === "completed";
2493
+ const active = entry.status === "in_progress";
2494
+ const marker = done ? span(g.planDone, {
2495
+ role: "positive",
2496
+ dim: true
2497
+ }) : active ? b.live ? span(spinnerFrame(b.ctx), { role: "marker" }) : span(g.planActive, {
2498
+ role: "caution",
2499
+ dim: true
2500
+ }) : span(g.planPending, { dim: true });
2501
+ const lead = b.ctx.measure(marker.text) + 1;
2502
+ const rows = wrapLines(entry.content, Math.max(1, b.width - lead), b.ctx.measure);
2503
+ return rows.map((row, i) => ({
2504
+ spans: [
2505
+ i === 0 ? marker : span(" ".repeat(b.ctx.measure(marker.text)), { chrome: true }),
2506
+ span(" ", { chrome: true }),
2507
+ span(row.text, {
2508
+ role: "content",
2509
+ ...done ? { dim: true } : {}
2510
+ })
2511
+ ],
2512
+ ...row.wrap ? { wrap: row.wrap } : {}
2513
+ }));
2514
+ });
2515
+ }
2516
+ function blockLines(block, b) {
2517
+ const { ctx } = b;
2518
+ const live = isBlockActive(block, {
2519
+ isActive: b.live,
2520
+ activeBlockId: b.activeBlockId,
2521
+ outcome: b.message.outcome
2522
+ });
2523
+ if (!isCoreBlockType(block.type)) {
2524
+ const custom = block;
2525
+ const layout = lookupExtension(ctx.blockLayouts, custom.type);
2526
+ if (layout) return layout(custom, {
2527
+ width: b.width,
2528
+ isLive: live,
2529
+ measure: ctx.measure
2530
+ });
2531
+ return [{ spans: [span(summarizeBlock(custom).label, { role: "marker" })] }];
2532
+ }
2533
+ const core = block;
2534
+ switch (core.type) {
2535
+ case "text": return b.message.role === "user" ? wrapped(core.text, b.width, ctx.measure, { role: "user" }) : ctx.markdown(core.text, b.width, {
2536
+ ...live ? { streaming: true } : {},
2537
+ measure: ctx.measure,
2538
+ glyphs: ctx.glyphs,
2539
+ image: ctx.image
2540
+ });
2541
+ case "reasoning": {
2542
+ const summary = summarizeBlock(core, { isActive: live });
2543
+ const open = ctx.detail !== "collapsed" || ctx.opened.isOpen(core.id, ROW_PATH);
2544
+ const header = headerRow({
2545
+ affordance: live ? span(spinnerFrame(ctx), { role: "marker" }) : span(open ? ctx.glyphs.expanded : ctx.glyphs.collapsed, { role: "marker" }),
2546
+ icon: ctx.glyphs.brain,
2547
+ label: summary.label,
2548
+ trailing: summary.showTimer ? liveTimer(positiveTimestamp(core.startedAt) ?? b.message.createdAt, core.durationMs, live, ctx) : void 0,
2549
+ target: {
2550
+ kind: "row-toggle",
2551
+ rowKey: core.id
2552
+ }
2553
+ }, b.width, ctx);
2554
+ const text = wrapped(core.text, Math.max(1, b.width - 2), ctx.measure, {
2555
+ role: "muted",
2556
+ italic: true
2557
+ });
2558
+ if (open) return [header, ...indent(text, 2)];
2559
+ return live ? [header, ...indent(text.slice(-3), 2)] : [header];
2560
+ }
2561
+ case "tool_call": return toolRow(core, b);
2562
+ case "plan": {
2563
+ const summary = summarizeBlock(core);
2564
+ const header = headerRow({
2565
+ affordance: span(" "),
2566
+ icon: ctx.glyphs.checklist,
2567
+ label: summary.label
2568
+ }, b.width, ctx);
2569
+ return [header, ...indent(planRows(core, {
2570
+ ...b,
2571
+ width: Math.max(1, b.width - 2)
2572
+ }), 2)];
2573
+ }
2574
+ case "status": return [headerRow({
2575
+ affordance: live ? span(spinnerFrame(ctx), { role: "marker" }) : span(" "),
2576
+ label: statusLabel(core.phase, live),
2577
+ trailing: liveTimer(b.message.createdAt, b.live ? void 0 : b.message.metrics?.durationMs, live, ctx)
2578
+ }, b.width, ctx)];
2579
+ case "system": return [headerRow({
2580
+ affordance: span(" "),
2581
+ icon: ctx.glyphs.gear,
2582
+ label: "System prompt"
2583
+ }, b.width, ctx), ...ctx.detail !== "collapsed" ? indent(wrapped(core.text, Math.max(1, b.width - 2), ctx.measure, { role: "muted" }), 2) : []];
2584
+ }
2585
+ }
2586
+ function groupLines(key, blocks, b) {
2587
+ const { ctx } = b;
2588
+ const g = ctx.glyphs;
2589
+ const plan = planGroupHeader(blocks, {
2590
+ isActive: b.live,
2591
+ activeBlockId: b.activeBlockId,
2592
+ outcome: b.message.outcome,
2593
+ startedAt: b.message.createdAt,
2594
+ durationMs: b.message.metrics?.durationMs
2595
+ });
2596
+ const open = ctx.detail !== "collapsed" || ctx.opened.isOpen(key, ROW_PATH);
2597
+ const icon = plan.summary.iconName ? g[plan.summary.iconName] : void 0;
2598
+ const rate = plan.showRate && !b.live ? formatTokensPerSecond(b.message.metrics?.tokensPerSecond) : void 0;
2599
+ const timer = plan.showTimer ? liveTimer(plan.startedAt, plan.durationMs, plan.isActive, ctx) : void 0;
2600
+ const affordance = plan.isActive ? span(spinnerFrame(ctx), { role: "marker" }) : span(open ? g.expanded : g.collapsed, { role: "marker" });
2601
+ const target = {
2602
+ kind: "row-toggle",
2603
+ rowKey: key
2604
+ };
2605
+ let header;
2606
+ if (plan.tools) {
2607
+ const t = plan.tools;
2608
+ const sep = g.separator;
2609
+ const counts = !t.only && t.failed > 0 ? [
2610
+ span(sep, { role: "muted" }),
2611
+ span(`${t.succeeded} ${g.success}`, { role: "positive" }),
2612
+ span(" "),
2613
+ span(`${t.failed} ${g.error}`, { role: "danger" })
2614
+ ] : [];
2615
+ const detail = t.only ? toolDisplayInput(t.only) : t.running ? `${sep.trimStart()}${toolHeadline(t.running)}` : void 0;
2616
+ header = headerRow({
2617
+ affordance,
2618
+ icon: g.wrench,
2619
+ label: t.only ? toolLabel(t.only) || "tool" : `${t.total} tool calls`,
2620
+ after: counts,
2621
+ detail,
2622
+ qualifier: approvalQualifier(t.only),
2623
+ trailing: timer ? `${sep.trimStart()}${timer}` : void 0,
2624
+ error: t.only !== void 0 && t.failed > 0,
2625
+ errorWithoutAfter: t.failed > 0,
2626
+ target
2627
+ }, b.width, ctx);
2628
+ } else header = headerRow({
2629
+ affordance,
2630
+ ...plan.hiddenCount > 0 ? { prefix: span(`(+${plan.hiddenCount})`, { role: "marker" }) } : {},
2631
+ ...icon ? { icon } : {},
2632
+ label: plan.summary.label,
2633
+ trailing: [timer, rate].filter(Boolean).join(" ") || void 0,
2634
+ target
2635
+ }, b.width, ctx);
2636
+ if (!open) return [header];
2637
+ const inner = {
2638
+ ...b,
2639
+ width: Math.max(1, b.width - 2)
2640
+ };
2641
+ return [header, ...indent(blocks.flatMap((block) => blockLines(block, inner)), 2)];
2642
+ }
2643
+ /** Which side of the content/activity boundary a planned row is on. */
2644
+ function rowSide(row) {
2645
+ if (row.kind === "bare") return getBlockTimelineBehavior(row.block).auxEligible ? "activity" : "content";
2646
+ return row.kind === "outcome" ? "content" : "activity";
2647
+ }
2648
+ /** Lay out a message's body (no frame) at `w` cells. */
2649
+ function layoutBody(original, w, ctx) {
2650
+ const live = ctx.isActive(original);
2651
+ const message = stripControlsDeep(original);
2652
+ const plan = planMessage(message, {
2653
+ isActive: live,
2654
+ activityPlacement: ctx.placement,
2655
+ ...live && ctx.detail === "collapsed" ? { liveTail: ctx.liveTail } : {},
2656
+ ...ctx.detail === "collapsed" && ctx.finishedActivity ? { finishedActivity: ctx.finishedActivity } : {}
2657
+ });
2658
+ const b = {
2659
+ message,
2660
+ live,
2661
+ activeBlockId: plan.activeBlockId,
2662
+ width: w,
2663
+ ctx
2664
+ };
2665
+ const out = [];
2666
+ const spacing = ctx.activitySpacing ?? true;
2667
+ let lastWasText = false;
2668
+ let lastSide;
2669
+ for (const row of plan.rows) {
2670
+ if (row.kind === "bare" && row.block.type === "text" && !row.block.text.trim()) continue;
2671
+ if (row.kind === "bare" && row.block.type === "text" && lastWasText) out.push({ spans: [] });
2672
+ lastWasText = row.kind === "bare" && row.block.type === "text";
2673
+ const side = rowSide(row);
2674
+ if (spacing && lastSide !== void 0 && side !== lastSide) out.push({
2675
+ spans: [],
2676
+ frame: true
2677
+ });
2678
+ lastSide = side;
2679
+ switch (row.kind) {
2680
+ case "pending":
2681
+ out.push(...blockLines({
2682
+ type: "status",
2683
+ id: row.id,
2684
+ phase: "pre-stream"
2685
+ }, {
2686
+ ...b,
2687
+ activeBlockId: row.id
2688
+ }));
2689
+ break;
2690
+ case "bare":
2691
+ out.push(...owned(blockLines(row.block, b), {
2692
+ messageId: message.id,
2693
+ blockId: row.block.id
2694
+ }));
2695
+ break;
2696
+ case "group":
2697
+ out.push(...groupLines(row.key, row.blocks, b));
2698
+ break;
2699
+ case "earlier":
2700
+ out.push({ spans: [...ctx.glyphs.earlier ? [span(`${ctx.glyphs.earlier} `, {
2701
+ role: "marker",
2702
+ chrome: true
2703
+ })] : [], span(earlierLabel(row), { role: "muted" })] });
2704
+ break;
2705
+ case "outcome":
2706
+ out.push(...wrapped(`${ctx.glyphs.error} ${row.outcome.message}`, w, ctx.measure, { role: "danger" }));
2707
+ break;
2708
+ }
2709
+ }
2710
+ for (const attachment of message.attachments ?? []) {
2711
+ const label = attachment.description ?? attachment.name ?? attachment.mediaType;
2712
+ out.push({ spans: [span(`${ctx.glyphs.attachment} `, {
2713
+ role: "link",
2714
+ chrome: true
2715
+ }), span(truncate(label, Math.max(1, w - 3), ctx.measure), { dim: true })] });
2716
+ }
2717
+ return out;
2718
+ }
2719
+ /** The footer spans of a message: planned facts with injected glyphs, then hints. */
2720
+ function footerSpans(original, ctx) {
2721
+ const live = ctx.isActive(original);
2722
+ const message = stripControlsDeep(original);
2723
+ const plan = planMessage(message, { isActive: live });
2724
+ const shows = showsSomething(message);
2725
+ if (!(plan.showFooter || live && ctx.liveFooter && shows)) return void 0;
2726
+ const facts = planFooter(message, {
2727
+ isActive: live,
2728
+ detail: ctx.detail,
2729
+ now: ctx.now
2730
+ });
2731
+ const glyph = {
2732
+ reasoning: ctx.glyphs.brain,
2733
+ plan: ctx.glyphs.checklist,
2734
+ tools: ctx.glyphs.wrench
2735
+ };
2736
+ const texts = facts.map((fact) => glyph[fact.key] ? `${glyph[fact.key]} ${fact.text}` : fact.text);
2737
+ texts.push(...ctx.footerExtras?.(original, ctx.detail) ?? []);
2738
+ const counts = countActivity(message.blocks);
2739
+ const hasActivity = counts.reasoningChars > 0 || counts.tools.total > 0 || counts.plan !== void 0;
2740
+ const hasOutput = message.blocks.some((block) => block.type === "tool_call" && block.result !== void 0);
2741
+ const { hints } = ctx;
2742
+ if (message.outcome && hints.retry) texts.push(hints.retry);
2743
+ if (ctx.detail === "collapsed" && hasActivity && hints.details) texts.push(hints.details);
2744
+ if (ctx.detail === "details" && hasOutput && hints.openAll) texts.push(hints.openAll);
2745
+ if (ctx.detail !== "collapsed" && hints.close) texts.push(hints.close);
2746
+ if (texts.length === 0) return void 0;
2747
+ return [span(stripControls(texts.join(ctx.glyphs.separator)), {
2748
+ role: "muted",
2749
+ chrome: true
2750
+ })];
2751
+ }
2752
+ /** Whether an answer shows anything: content, reasoning, an outcome, a tool call or a plan. */
2753
+ function showsSomething(message) {
2754
+ return message.outcome !== void 0 || message.blocks.some((block) => block.type !== "status");
2755
+ }
2756
+ /** Lay out one message inside its frame. */
2757
+ function layoutMessage(original, ctx, next) {
2758
+ const live = ctx.isActive(original);
2759
+ const message = stripControlsDeep(original);
2760
+ const inner = ctx.frame.innerWidth(message, ctx.width);
2761
+ const footer = footerSpans(original, ctx);
2762
+ const drafts = ctx.frame.layout({
2763
+ message,
2764
+ body: layoutBody(original, inner, ctx),
2765
+ ...footer ? { footer } : {},
2766
+ shows: message.role !== "assistant" || showsSomething(message),
2767
+ nextShows: next !== void 0 && next.role === "assistant" && showsSomething(next),
2768
+ live,
2769
+ accent: resolveAccent(original, ctx),
2770
+ ctx
2771
+ });
2772
+ return owned(drafts, { messageId: original.id });
2773
+ }
2774
+
2775
+ //#endregion
2776
+ //#region src/lines/markdown/table.ts
2777
+ /** The narrowest a wrapped column may get before the table falls back. */
2778
+ const MIN_COLUMN = 6;
2779
+ const SEPARATOR = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
2780
+ /** Whether `line` is a GFM table separator row. */
2781
+ function isSeparator(line) {
2782
+ return line.includes("-") && SEPARATOR.test(line);
2783
+ }
2784
+ /** Split a row into cells on pipes that are neither escaped nor inside a code span. */
2785
+ function splitRow(line) {
2786
+ let row = line.trim();
2787
+ if (row.startsWith("|")) row = row.slice(1);
2788
+ if (row.endsWith("|") && !row.endsWith("\\|")) row = row.slice(0, -1);
2789
+ const cells$1 = [];
2790
+ let cell = "";
2791
+ let code = 0;
2792
+ for (let i = 0; i < row.length; i++) {
2793
+ const ch = row[i];
2794
+ if (ch === "\\" && row[i + 1] === "|") {
2795
+ cell += "|";
2796
+ i++;
2797
+ } else if (ch === "`") {
2798
+ let run = 0;
2799
+ while (row[i + run] === "`") run++;
2800
+ code = code === run ? 0 : code || run;
2801
+ cell += row.slice(i, i + run);
2802
+ i += run - 1;
2803
+ } else if (ch === "|" && code === 0) {
2804
+ cells$1.push(cell.trim());
2805
+ cell = "";
2806
+ } else cell += ch;
2807
+ }
2808
+ cells$1.push(cell.trim());
2809
+ return cells$1;
2810
+ }
2811
+ /** Column alignments from a separator row. */
2812
+ function alignments(line) {
2813
+ return splitRow(line).map((cell) => cell.startsWith(":") && cell.endsWith(":") ? "center" : cell.endsWith(":") ? "right" : "left");
2814
+ }
2815
+ /**
2816
+ * Fit natural widths into `available` cells: columns within a fair share keep
2817
+ * theirs, the rest share what remains. Undefined when a column would fall
2818
+ * below its minimum (the minimums together exceed the available cells).
2819
+ */
2820
+ function fitColumns(natural, available) {
2821
+ const minimum = natural.map((w) => Math.min(w, MIN_COLUMN));
2822
+ if (minimum.reduce((a, b) => a + b, 0) > available) return void 0;
2823
+ const widths = [...natural];
2824
+ let open = natural.map((_, i) => i);
2825
+ let budget = available;
2826
+ for (;;) {
2827
+ const fair = Math.floor(budget / open.length);
2828
+ const small = open.filter((i) => natural[i] <= fair);
2829
+ if (small.length === 0 || small.length === open.length) break;
2830
+ for (const i of small) budget -= natural[i];
2831
+ open = open.filter((i) => !small.includes(i));
2832
+ }
2833
+ if (open.reduce((sum, i) => sum + natural[i], 0) <= budget) return widths;
2834
+ const share = Math.floor(budget / open.length);
2835
+ open.forEach((i, k) => {
2836
+ widths[i] = share + (k < budget - share * open.length ? 1 : 0);
2837
+ });
2838
+ return widths;
2839
+ }
2840
+ const width = (spans, measure) => spans.reduce((sum, s) => sum + measure(s.text), 0);
2841
+ function pad(spans, cells$1, align, last, measure) {
2842
+ if (last && spans.length === 0) return [];
2843
+ const room = Math.max(0, cells$1 - width(spans, measure));
2844
+ const before$1 = align === "right" ? room : align === "center" ? Math.floor(room / 2) : 0;
2845
+ const after$1 = last ? 0 : room - before$1;
2846
+ return [
2847
+ ...before$1 ? [span(" ".repeat(before$1), { chrome: true })] : [],
2848
+ ...spans,
2849
+ ...after$1 ? [span(" ".repeat(after$1), { chrome: true })] : []
2850
+ ];
2851
+ }
2852
+ /** Drop trailing spaces (an empty last cell leaves the column joint's). */
2853
+ function trimEnd(spans) {
2854
+ const merged = mergeSpans(spans);
2855
+ const last = merged.at(-1);
2856
+ if (last?.text.endsWith(" ")) merged[merged.length - 1] = {
2857
+ ...last,
2858
+ text: last.text.trimEnd()
2859
+ };
2860
+ return mergeSpans(merged);
2861
+ }
2862
+ /** Lay out a table from its header, separator and body source rows. */
2863
+ function layoutTable(header, separator, body, maxWidth, { glyphs, measure }) {
2864
+ const heads = splitRow(header);
2865
+ const columns = heads.length;
2866
+ const align = alignments(separator);
2867
+ const cells$1 = (row, style) => Array.from({ length: columns }, (_, c) => parseInline(splitRow(row)[c] ?? "", style, glyphs));
2868
+ const head = cells$1(header, {
2869
+ role: "strong",
2870
+ bold: true
2871
+ });
2872
+ const rows = body.map((row) => cells$1(row, { role: "content" }));
2873
+ const natural = Array.from({ length: columns }, (_, c) => Math.max(1, ...[head, ...rows].map((row) => width(row[c], measure))));
2874
+ const joint = 3;
2875
+ const widths = fitColumns(natural, maxWidth - joint * (columns - 1));
2876
+ if (!widths) return fallback(head, rows, maxWidth, measure);
2877
+ const column = [
2878
+ span(" ", { chrome: true }),
2879
+ span(glyphs.tableColumn, {
2880
+ role: "chrome",
2881
+ chrome: true,
2882
+ copyEach: " "
2883
+ }),
2884
+ span(" ", { chrome: true })
2885
+ ];
2886
+ const drawRow = (row) => {
2887
+ const wrapped$1 = row.map((cell, c) => wrapSpans(cell, widths[c], measure));
2888
+ const height = Math.max(...wrapped$1.map((w) => w.length));
2889
+ return Array.from({ length: height }, (_, r) => ({ spans: trimEnd(wrapped$1.flatMap((w, c) => [...c > 0 ? column : [], ...pad(w[r]?.spans ?? [], widths[c], align[c] ?? "left", c === columns - 1, measure)])) }));
2890
+ };
2891
+ const rule = widths.map((w) => glyphs.rule.repeat(w)).join(`${glyphs.rule}${glyphs.tableCross}${glyphs.rule}`);
2892
+ return [
2893
+ ...drawRow(head),
2894
+ {
2895
+ spans: [span(rule, { role: "chrome" })],
2896
+ frame: true
2897
+ },
2898
+ ...rows.flatMap(drawRow)
2899
+ ];
2900
+ }
2901
+ function fallback(head, rows, maxWidth, measure) {
2902
+ return rows.flatMap((row, r) => [...r > 0 ? [{ spans: [] }] : [], ...row.flatMap((cell, c) => wrapSpans([
2903
+ ...head[c],
2904
+ span(": ", { role: "muted" }),
2905
+ ...cell
2906
+ ], maxWidth, measure))]);
2907
+ }
2908
+
2909
+ //#endregion
2910
+ //#region src/lines/markdown/blocks.ts
2911
+ const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)/;
2912
+ const HEADING = /^ {0,3}(#{1,6})(?:\s+(.*?))?(?:\s+#+)?\s*$/;
2913
+ const RULE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
2914
+ const QUOTE = /^ {0,3}> ?/;
2915
+ const ITEM = /^( *)([-*+]|\d{1,9}[.)])(?: +(.*))?$/;
2916
+ const IMAGE_LINE = /^!\[([^\]]*)\]\(\s*<?([^\s)>]+)>?(?:\s+"[^"]*")?\s*\)$/;
2917
+ const blank = (line) => line === void 0 || line.trim() === "";
2918
+ function startsBlock(lines, i) {
2919
+ const line = lines[i];
2920
+ return FENCE.test(line) || HEADING.test(line) || RULE.test(line) || QUOTE.test(line) || ITEM.test(line) || isTableStart(lines, i);
2921
+ }
2922
+ function isTableStart(lines, i) {
2923
+ return lines[i].includes("|") && isSeparator(lines[i + 1] ?? "");
2924
+ }
2925
+ const indentOf = (line) => line.length - line.trimStart().length;
2926
+ /** Prefix laid-out lines: `first` on the first line, `rest` on the others. */
2927
+ function hang(lines, first, rest) {
2928
+ return lines.map((line, i) => ({
2929
+ ...line,
2930
+ spans: [...i === 0 ? first : rest, ...line.spans]
2931
+ }));
2932
+ }
2933
+ function paragraph(source, width$2, env) {
2934
+ const only = source.length === 1 ? IMAGE_LINE.exec(source[0].trim()) : null;
2935
+ if (only && env.image) {
2936
+ const lines = env.image(only[2], only[1], width$2);
2937
+ if (lines) return lines;
2938
+ }
2939
+ const segments = [""];
2940
+ source.forEach((raw, i) => {
2941
+ const hard = i < source.length - 1 && /(?: {2,}|\\)$/.test(raw);
2942
+ const text = raw.trim().replace(/\\$/, "");
2943
+ const at = segments.length - 1;
2944
+ segments[at] = segments[at] ? `${segments[at]} ${text}` : text;
2945
+ if (hard) segments.push("");
2946
+ });
2947
+ return segments.flatMap((text) => wrapSpans(parseInline(text, { role: "content" }, env.glyphs), width$2, env.measure));
2948
+ }
2949
+ function fence(lines, i, width$2, env) {
2950
+ const [, marker, lang] = FENCE.exec(lines[i]);
2951
+ const indent$1 = indentOf(lines[i]);
2952
+ const close = new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}\\s*$`);
2953
+ let j = i + 1;
2954
+ const code = [];
2955
+ while (j < lines.length && !close.test(lines[j])) {
2956
+ const line = lines[j];
2957
+ code.push(line.slice(Math.min(indent$1, indentOf(line))));
2958
+ j++;
2959
+ }
2960
+ const bar = span(`${env.glyphs.codeBar} `, {
2961
+ role: "chrome",
2962
+ dim: true,
2963
+ chrome: true
2964
+ });
2965
+ const inner = Math.max(1, width$2 - 2);
2966
+ const out = [];
2967
+ if (lang) out.push({
2968
+ spans: [bar, span(lang, {
2969
+ role: "muted",
2970
+ dim: true
2971
+ })],
2972
+ frame: true
2973
+ });
2974
+ for (const line of code) hardSplit(line.replace(/\t/g, " "), inner, env.measure).forEach((piece, k) => {
2975
+ out.push({
2976
+ spans: [bar, ...piece ? [span(piece, { role: "code" })] : []],
2977
+ ...k > 0 ? { wrap: "hard" } : {}
2978
+ });
2979
+ });
2980
+ return [out, j < lines.length ? j + 1 : j];
2981
+ }
2982
+ function list(lines, i, width$2, env) {
2983
+ const first = ITEM.exec(lines[i]);
2984
+ const indent$1 = first[1].length;
2985
+ const ordered$1 = /\d/.test(first[2]);
2986
+ let number = ordered$1 ? Number.parseInt(first[2], 10) : 0;
2987
+ const out = [];
2988
+ let j = i;
2989
+ while (j < lines.length) {
2990
+ const match = ITEM.exec(lines[j]);
2991
+ if (!match || match[1].length !== indent$1) break;
2992
+ if (/\d/.test(match[2]) !== ordered$1) break;
2993
+ const content = indent$1 + match[2].length + 1;
2994
+ const body = [match[3] ?? ""];
2995
+ j++;
2996
+ while (j < lines.length) {
2997
+ const line = lines[j];
2998
+ if (blank(line)) {
2999
+ const next = lines[j + 1];
3000
+ if (next !== void 0 && !blank(next) && indentOf(next) >= content) {
3001
+ body.push("");
3002
+ j++;
3003
+ continue;
3004
+ }
3005
+ break;
3006
+ }
3007
+ if (indentOf(line) > indent$1) body.push(line.slice(Math.min(content, indentOf(line))));
3008
+ else if (!startsBlock(lines, j)) body.push(line.trim());
3009
+ else break;
3010
+ j++;
3011
+ }
3012
+ let marker;
3013
+ if (ordered$1) marker = `${number++}${match[2].slice(-1)}`;
3014
+ else marker = env.glyphs.bullet;
3015
+ const task = /^\[([ xX])\] /.exec(body[0]);
3016
+ if (task) {
3017
+ marker = task[1] === " " ? env.glyphs.taskOpen : env.glyphs.taskDone;
3018
+ body[0] = body[0].slice(4);
3019
+ }
3020
+ const markerWidth = env.measure(marker) + 1;
3021
+ const inner = layoutBlocks(body, Math.max(1, width$2 - markerWidth), env, true);
3022
+ out.push(...hang(inner.length ? inner : [{ spans: [] }], [span(`${marker} `, {
3023
+ role: "marker",
3024
+ chrome: true
3025
+ })], [span(" ".repeat(markerWidth), { chrome: true })]));
3026
+ if (blank(lines[j]) && ITEM.test(lines[j + 1] ?? "") && indentOf(lines[j + 1]) === indent$1) j++;
3027
+ }
3028
+ return [out, j];
3029
+ }
3030
+ /** Lay out markdown source lines as blocks; `tight` drops the blank lines between blocks. */
3031
+ function layoutBlocks(lines, width$2, env, tight = false) {
3032
+ const out = [];
3033
+ const add = (block) => {
3034
+ if (out.length && !tight) out.push({ spans: [] });
3035
+ out.push(...block);
3036
+ };
3037
+ let i = 0;
3038
+ while (i < lines.length) {
3039
+ const line = lines[i];
3040
+ if (blank(line)) {
3041
+ i++;
3042
+ continue;
3043
+ }
3044
+ if (FENCE.test(line)) {
3045
+ const [block, next] = fence(lines, i, width$2, env);
3046
+ add(block);
3047
+ i = next;
3048
+ continue;
3049
+ }
3050
+ const heading = HEADING.exec(line);
3051
+ if (heading) {
3052
+ add(wrapSpans(parseInline(heading[2] ?? "", {
3053
+ role: "strong",
3054
+ bold: true
3055
+ }, env.glyphs), width$2, env.measure));
3056
+ i++;
3057
+ continue;
3058
+ }
3059
+ if (RULE.test(line)) {
3060
+ add([{
3061
+ spans: [span(env.glyphs.rule.repeat(width$2), {
3062
+ role: "chrome",
3063
+ dim: true
3064
+ })],
3065
+ frame: true
3066
+ }]);
3067
+ i++;
3068
+ continue;
3069
+ }
3070
+ if (QUOTE.test(line)) {
3071
+ const quoted = [];
3072
+ while (i < lines.length && QUOTE.test(lines[i])) {
3073
+ quoted.push(lines[i].replace(QUOTE, ""));
3074
+ i++;
3075
+ }
3076
+ const bar = span(`${env.glyphs.quoteBar} `, {
3077
+ role: "chrome",
3078
+ dim: true,
3079
+ chrome: true
3080
+ });
3081
+ const inner = layoutBlocks(quoted, Math.max(1, width$2 - 2), env);
3082
+ add(hang(inner, [bar], [bar]));
3083
+ continue;
3084
+ }
3085
+ if (ITEM.test(line)) {
3086
+ const [block, next] = list(lines, i, width$2, env);
3087
+ add(block);
3088
+ i = next;
3089
+ continue;
3090
+ }
3091
+ if (isTableStart(lines, i)) {
3092
+ const body = [];
3093
+ let j = i + 2;
3094
+ while (j < lines.length && !blank(lines[j]) && lines[j].includes("|")) {
3095
+ body.push(lines[j]);
3096
+ j++;
3097
+ }
3098
+ add(layoutTable(line, lines[i + 1], body, width$2, env));
3099
+ i = j;
3100
+ continue;
3101
+ }
3102
+ const source = [line];
3103
+ i++;
3104
+ while (i < lines.length && !blank(lines[i]) && !startsBlock(lines, i)) {
3105
+ source.push(lines[i]);
3106
+ i++;
3107
+ }
3108
+ add(paragraph(source, width$2, env));
3109
+ }
3110
+ return out;
3111
+ }
3112
+ /**
3113
+ * Split source lines into top-level chunks at blank lines that end a block:
3114
+ * never inside a fence, and not where an indented line or a list item
3115
+ * continues the block before. Chunks lay out independently.
3116
+ */
3117
+ function chunks(lines) {
3118
+ const out = [];
3119
+ let current = [];
3120
+ let fence$1;
3121
+ const nextText = [];
3122
+ for (let i = lines.length - 1, seen; i >= 0; i--) {
3123
+ nextText[i] = seen;
3124
+ if (!blank(lines[i])) seen = lines[i];
3125
+ }
3126
+ for (let i = 0; i < lines.length; i++) {
3127
+ const line = lines[i];
3128
+ if (fence$1) {
3129
+ if (fence$1.test(line)) fence$1 = void 0;
3130
+ current.push(line);
3131
+ continue;
3132
+ }
3133
+ const open = FENCE.exec(line);
3134
+ if (open) {
3135
+ const marker = open[1];
3136
+ fence$1 = new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}\\s*$`);
3137
+ current.push(line);
3138
+ continue;
3139
+ }
3140
+ if (blank(line)) {
3141
+ if (current.length === 0) continue;
3142
+ const next = nextText[i];
3143
+ if (next !== void 0 && indentOf(next) === 0 && !ITEM.test(next)) {
3144
+ out.push(current);
3145
+ current = [];
3146
+ continue;
3147
+ }
3148
+ }
3149
+ current.push(line);
3150
+ }
3151
+ if (current.length) out.push(current);
3152
+ return out;
3153
+ }
3154
+ /** Laid-out chunks per glyph set and measure, keyed by width and source. */
3155
+ const chunkCache = new WeakMap();
3156
+ const CHUNK_CACHE_SIZE = 4e3;
3157
+ function cached(env) {
3158
+ let byMeasure = chunkCache.get(env.glyphs);
3159
+ if (!byMeasure) {
3160
+ byMeasure = new WeakMap();
3161
+ chunkCache.set(env.glyphs, byMeasure);
3162
+ }
3163
+ let map = byMeasure.get(env.measure);
3164
+ if (!map) {
3165
+ map = new Map();
3166
+ byMeasure.set(env.measure, map);
3167
+ }
3168
+ return map;
3169
+ }
3170
+ /**
3171
+ * The default terminal markdown: dependency-free, injected roles and glyphs.
3172
+ * Finished blocks are cached, so a streamed delta lays out only the tail.
3173
+ */
3174
+ const terminalMarkdown = (source, width$2, options) => {
3175
+ const w = Math.max(1, width$2);
3176
+ const cache = cached(options);
3177
+ const out = [];
3178
+ for (const chunk of chunks(source.replace(/\r\n?/g, "\n").split("\n"))) {
3179
+ const text = chunk.join("\n");
3180
+ const cacheable = !(options.image && text.includes("!["));
3181
+ const key = `${w}\n${text}`;
3182
+ let lines = cacheable ? cache.get(key) : void 0;
3183
+ if (!lines) {
3184
+ lines = layoutBlocks(chunk, w, options);
3185
+ if (cacheable) {
3186
+ if (cache.size >= CHUNK_CACHE_SIZE) cache.clear();
3187
+ cache.set(key, lines);
3188
+ }
3189
+ }
3190
+ if (lines.length === 0) continue;
3191
+ if (out.length) out.push({ spans: [] });
3192
+ out.push(...lines);
3193
+ }
3194
+ return out;
3195
+ };
3196
+
3197
+ //#endregion
3198
+ //#region src/lines/mouse.ts
3199
+ const ESC = String.fromCharCode(27);
3200
+ /** Button-event tracking with SGR encoding, on and off. */
3201
+ const MOUSE_ON = `${ESC}[?1002h${ESC}[?1006h`;
3202
+ const MOUSE_OFF = `${ESC}[?1006l${ESC}[?1002l`;
3203
+ const BUTTONS = [
3204
+ "left",
3205
+ "middle",
3206
+ "right",
3207
+ "none"
3208
+ ];
3209
+ const SGR = new RegExp(`${ESC}\\[<(\\d+);(\\d+);(\\d+)([Mm])`, "g");
3210
+ /**
3211
+ * Every SGR mouse report in a chunk of terminal input. Horizontal wheel
3212
+ * reports (buttons 6 and 7) are dropped: the viewport only scrolls vertically.
3213
+ */
3214
+ function parseMouse(data) {
3215
+ const reports = Array.from(data.matchAll(SGR)).filter((m) => (Number(m[1]) & 66) !== 66);
3216
+ return reports.map((m) => {
3217
+ const code = Number(m[1]);
3218
+ const wheel = (code & 64) !== 0;
3219
+ const motion = (code & 32) !== 0;
3220
+ const kind = wheel ? code & 1 ? "wheel-down" : "wheel-up" : m[4] === "m" ? "release" : motion ? "drag" : "press";
3221
+ return {
3222
+ kind,
3223
+ button: wheel ? "none" : BUTTONS[code & 3],
3224
+ x: Number(m[2]) - 1,
3225
+ y: Number(m[3]) - 1,
3226
+ ctrl: (code & 16) !== 0,
3227
+ shift: (code & 4) !== 0,
3228
+ meta: (code & 8) !== 0
3229
+ };
3230
+ });
3231
+ }
3232
+ /** The longest incomplete report kept for the next chunk; a longer one is noise. */
3233
+ const MAX_PENDING = 64;
3234
+ const CSI_SEQUENCE = new RegExp(`${ESC}\\[[\\x30-\\x3f]*[\\x20-\\x2f]*[\\x40-\\x7e]`, "g");
3235
+ const CSI_PARTIAL = new RegExp(`${ESC}(?:\\[[\\x30-\\x3f]*[\\x20-\\x2f]*)?$`);
3236
+ /**
3237
+ * A reader of terminal reports across input chunks: each call returns the
3238
+ * complete CSI sequences in what has arrived, keeping an unfinished one at
3239
+ * the end for the next chunk (a terminal may split a report anywhere). The
3240
+ * kept tail is bounded; past the bound it is dropped as noise.
3241
+ */
3242
+ function createReportReader() {
3243
+ let pending = "";
3244
+ return (data) => {
3245
+ let text = pending + data;
3246
+ pending = "";
3247
+ const partial = CSI_PARTIAL.exec(text);
3248
+ if (partial) {
3249
+ text = text.slice(0, partial.index);
3250
+ if (partial[0].length <= MAX_PENDING) pending = partial[0];
3251
+ }
3252
+ return Array.from(text.matchAll(CSI_SEQUENCE), (m) => m[0]);
3253
+ };
3254
+ }
3255
+ /** An OSC 52 clipboard write of `text`. */
3256
+ function osc52(text) {
3257
+ return `${ESC}]52;c;${Buffer.from(text, "utf8").toString("base64")}\u0007`;
3258
+ }
3259
+ /**
3260
+ * The element's position on screen, in cells. This relies on the renderer's
3261
+ * layout internals (each node's computed offset from its parent), so it is
3262
+ * isolated here.
3263
+ */
3264
+ function screenOrigin(node) {
3265
+ let x = 0;
3266
+ let y = 0;
3267
+ for (let n = node ?? void 0; n; n = n.parentNode) {
3268
+ x += n.yogaNode?.getComputedLeft() ?? 0;
3269
+ y += n.yogaNode?.getComputedTop() ?? 0;
3270
+ }
3271
+ return {
3272
+ x,
3273
+ y
3274
+ };
3275
+ }
3276
+ /** The two ends of a selection, in reading order. */
3277
+ function ordered(a, b) {
3278
+ return a.line < b.line || a.line === b.line && a.col <= b.col ? [a, b] : [b, a];
3279
+ }
3280
+ function cells(line, measure) {
3281
+ const out = [];
3282
+ let at = 0;
3283
+ for (const s of line.spans) for (const g of graphemes(s.text)) {
3284
+ const width$2 = measure(g);
3285
+ out.push({
3286
+ text: g,
3287
+ start: at,
3288
+ width: width$2,
3289
+ span: s
3290
+ });
3291
+ at += width$2;
3292
+ }
3293
+ return out;
3294
+ }
3295
+ /**
3296
+ * Whether a grapheme's cells overlap the columns `start`..`end` (inclusive):
3297
+ * a point inside a wide grapheme's later cell still takes the whole grapheme.
3298
+ */
3299
+ const within = (cell, start, end) => cell.start <= end && cell.start + Math.max(cell.width, 1) > start;
3300
+ /** What a span's grapheme copies as: its replacement, nothing for chrome, else itself. */
3301
+ const copied = (cell) => cell.span.copyEach !== void 0 ? cell.span.copyEach : cell.span.chrome ? "" : cell.text;
3302
+ /**
3303
+ * Copy text between two points (inclusive of the end cell): content spans
3304
+ * only, decoration lines skipped, lines right-trimmed, soft joins as a space,
3305
+ * hard joins as nothing, other lines by newlines.
3306
+ */
3307
+ function copyText(lines, a, b, measure) {
3308
+ const [from, to] = ordered(a, b);
3309
+ let out = "";
3310
+ let first = true;
3311
+ for (let i = from.line; i <= to.line; i++) {
3312
+ const line = lines[i];
3313
+ if (!line || line.frame) continue;
3314
+ const start = i === from.line ? from.col : 0;
3315
+ const end = i === to.line ? to.col : Number.POSITIVE_INFINITY;
3316
+ const text = cells(line, measure).filter((c) => within(c, start, end)).map(copied).join("").trimEnd();
3317
+ if (!first) out += line.wrap === "soft" ? " " : line.wrap === "hard" ? "" : "\n";
3318
+ out += text;
3319
+ first = false;
3320
+ }
3321
+ return out;
3322
+ }
3323
+ /**
3324
+ * Highlight the content cells of a line from `start` to `end` (inclusive) with
3325
+ * the selection appearance; selecting past the line's end highlights one more
3326
+ * cell. Decoration lines are returned as they are.
3327
+ */
3328
+ function highlightLine(line, start, end, selection, measure) {
3329
+ if (line.frame) return line;
3330
+ const all = cells(line, measure);
3331
+ const runs = [];
3332
+ for (const cell of all) {
3333
+ const selected = !cell.span.chrome && within(cell, start, end);
3334
+ const last = runs.at(-1);
3335
+ if (last && last.span === cell.span && last.selected === selected) last.text += cell.text;
3336
+ else runs.push({
3337
+ span: cell.span,
3338
+ selected,
3339
+ text: cell.text
3340
+ });
3341
+ }
3342
+ const spans = runs.map(({ span: span$1, selected, text }) => selected ? {
3343
+ ...span$1,
3344
+ text,
3345
+ style: {
3346
+ ...span$1.style,
3347
+ ...selection
3348
+ }
3349
+ } : {
3350
+ ...span$1,
3351
+ text
3352
+ });
3353
+ const width$2 = all.reduce((sum, c) => sum + c.width, 0);
3354
+ if (end >= width$2 && start <= width$2) spans.push({
3355
+ text: " ",
3356
+ style: selection
3357
+ });
3358
+ return {
3359
+ ...line,
3360
+ spans
3361
+ };
3362
+ }
3363
+ /** Call `handler` with every chunk of raw terminal input while active. */
3364
+ function useTerminalData(handler, active) {
3365
+ const { stdin } = ink.useStdin();
3366
+ const ref = React.useRef(handler);
3367
+ ref.current = handler;
3368
+ React.useEffect(() => {
3369
+ if (!active) return;
3370
+ const listener = (data) => ref.current(String(data));
3371
+ stdin.on("data", listener);
3372
+ return () => {
3373
+ stdin.off("data", listener);
3374
+ };
3375
+ }, [active, stdin]);
3376
+ }
3377
+ /** Enable mouse reporting while active; disable on deactivation, unmount and process exit. */
3378
+ function useMouseReporting(active, write) {
3379
+ const ref = React.useRef(write);
3380
+ ref.current = write;
3381
+ React.useEffect(() => {
3382
+ if (!active) return;
3383
+ ref.current(MOUSE_ON);
3384
+ const off = () => ref.current(MOUSE_OFF);
3385
+ process.once("exit", off);
3386
+ return () => {
3387
+ process.off("exit", off);
3388
+ off();
3389
+ };
3390
+ }, [active]);
3391
+ }
3392
+
3393
+ //#endregion
3394
+ //#region src/lines/transcript.ts
3395
+ const identities = new WeakMap();
3396
+ let nextIdentity = 0;
3397
+ /** A stable token per object or function: injected inputs compare by identity. */
3398
+ function identity(value) {
3399
+ if (value === void 0) return 0;
3400
+ let id = identities.get(value);
3401
+ if (id === void 0) {
3402
+ id = ++nextIdentity;
3403
+ identities.set(value, id);
3404
+ }
3405
+ return id;
3406
+ }
3407
+ /**
3408
+ * Everything a message's layout reads, as a string. Plain values (hint labels,
3409
+ * the resolved accent, host footer facts) count by value; injected functions
3410
+ * and the glyph set count by identity, so hosts memoise them.
3411
+ */
3412
+ function layoutSignature(message, ctx, next, imagesVersion = 0) {
3413
+ const live = ctx.isActive(message);
3414
+ return [
3415
+ ctx.width,
3416
+ ctx.detail,
3417
+ ctx.placement,
3418
+ ctx.frame.id,
3419
+ live ? `live:${Math.floor(ctx.now / 1e3)}` : "done",
3420
+ ctx.opened.version,
3421
+ imagesVersion,
3422
+ next !== void 0 && next.role === "assistant" && showsSomething(next) ? "n" : "-",
3423
+ ctx.liveTail,
3424
+ ctx.liveFooter ? "f" : "-",
3425
+ ctx.finishedActivity ?? "row",
3426
+ ctx.activitySpacing === false ? "-" : "s",
3427
+ ctx.footerExtras?.(message, ctx.detail).join("\0") ?? "",
3428
+ JSON.stringify(ctx.hints),
3429
+ JSON.stringify(resolveAccent(message, ctx)),
3430
+ [
3431
+ ctx.glyphs,
3432
+ ctx.markdown,
3433
+ ctx.measure,
3434
+ ctx.image,
3435
+ ctx.blockLayouts
3436
+ ].map(identity).join(",")
3437
+ ].join("|");
3438
+ }
3439
+ function createLayoutCache(imagesVersion = () => 0) {
3440
+ const entries = new WeakMap();
3441
+ let misses = 0;
3442
+ return {
3443
+ get(message, ctx, next) {
3444
+ const signature = layoutSignature(message, ctx, next, imagesVersion());
3445
+ const hit = entries.get(message);
3446
+ if (hit && hit.signature === signature) return hit.lines;
3447
+ misses++;
3448
+ const lines = layoutMessage(message, ctx, next);
3449
+ entries.set(message, {
3450
+ signature,
3451
+ lines
3452
+ });
3453
+ return lines;
3454
+ },
3455
+ get misses() {
3456
+ return misses;
3457
+ }
3458
+ };
3459
+ }
3460
+ /** Lay out a transcript: all lines, and where each message starts. */
3461
+ function layoutTranscript(messages, ctx, cache = createLayoutCache()) {
3462
+ const lines = [];
3463
+ const messageStart = [];
3464
+ messages.forEach((message, i) => {
3465
+ messageStart.push(lines.length);
3466
+ lines.push(...cache.get(message, ctx, messages[i + 1]));
3467
+ });
3468
+ return {
3469
+ lines,
3470
+ messageStart
3471
+ };
3472
+ }
3473
+
3474
+ //#endregion
3475
+ //#region src/lines/Viewport.tsx
3476
+ const scrollHome = {
3477
+ follow: false,
3478
+ top: 0
3479
+ };
3480
+ const scrollEnd = { follow: true };
3481
+ /** The window's first line for a scroll state. */
3482
+ function firstLine(scroll, total, rows) {
3483
+ const last = Math.max(0, total - rows);
3484
+ return scroll.follow ? last : Math.max(0, Math.min(scroll.top, last));
3485
+ }
3486
+ /** Scroll by `delta` lines, clamped; reaching the bottom returns to follow. */
3487
+ function scrollBy(scroll, delta, g) {
3488
+ const last = Math.max(0, g.total - g.rows);
3489
+ const top = Math.max(0, Math.min(firstLine(scroll, g.total, g.rows) + delta, last));
3490
+ return top >= last ? scrollEnd : {
3491
+ follow: false,
3492
+ top
3493
+ };
3494
+ }
3495
+ /** Scroll by pages; a page keeps two lines of context. */
3496
+ function pageBy(scroll, pages, g) {
3497
+ return scrollBy(scroll, pages * Math.max(1, g.rows - 2), g);
3498
+ }
3499
+ const NEXT_DETAIL = {
3500
+ collapsed: "details",
3501
+ details: "full",
3502
+ full: "collapsed"
3503
+ };
3504
+ /** Uncontrolled open state: a version plus the set of open `blockId␟path` keys. */
3505
+ function useOpenState() {
3506
+ const [state, setState] = React.useState({
3507
+ version: 0,
3508
+ open: new Set()
3509
+ });
3510
+ const toggle = React.useCallback((blockId, path) => {
3511
+ setState((current) => {
3512
+ const open = new Set(current.open);
3513
+ const key = `${blockId}␟${path}`;
3514
+ if (open.has(key)) open.delete(key);
3515
+ else open.add(key);
3516
+ return {
3517
+ version: current.version + 1,
3518
+ open
3519
+ };
3520
+ });
3521
+ }, []);
3522
+ const opened = React.useMemo(() => ({
3523
+ version: state.version,
3524
+ isOpen: (b, p) => state.open.has(`${b}␟${p}`)
3525
+ }), [state]);
3526
+ return [opened, toggle];
3527
+ }
3528
+ /** Lay out the transcript and compute the window (for hosts that draw extra chrome). */
3529
+ function useTranscriptLayout(props) {
3530
+ const { theme, glyphs } = useInkComponents();
3531
+ const live = props.streamingMessageId !== void 0;
3532
+ const now = Math.floor(hooks.useClock(live, 1e3) / 1e3) * 1e3;
3533
+ const [ownOpened, ownToggle] = useOpenState();
3534
+ const opened = props.opened ?? ownOpened;
3535
+ const provider = props.images;
3536
+ const [, bump] = React.useState(0);
3537
+ React.useEffect(() => provider?.subscribe(() => bump((n) => n + 1)), [provider]);
3538
+ const [expanded, setExpanded] = React.useState({
3539
+ version: 0,
3540
+ open: new Set()
3541
+ });
3542
+ const toggleExpanded = React.useCallback((src) => {
3543
+ setExpanded((current) => {
3544
+ const open = new Set(current.open);
3545
+ if (open.has(src)) open.delete(src);
3546
+ else open.add(src);
3547
+ return {
3548
+ version: current.version + 1,
3549
+ open
3550
+ };
3551
+ });
3552
+ }, []);
3553
+ const imageStates = React.useRef(new Map());
3554
+ const imagesVersion = (props.imagesVersion ?? 0) + (provider?.version ?? 0) + expanded.version;
3555
+ const cache = React.useRef(null);
3556
+ const versionRef = React.useRef(imagesVersion);
3557
+ versionRef.current = imagesVersion;
3558
+ cache.current ??= createLayoutCache(() => versionRef.current);
3559
+ const maxHeight = props.maxImageHeight ?? Math.max(1, props.height - 8);
3560
+ const measure = props.measure ?? defaultMeasure;
3561
+ const image = React.useMemo(() => props.image ?? (provider ? (src, alt, width$2) => layoutImage(src, alt, width$2, {
3562
+ provider: {
3563
+ ...provider,
3564
+ get: (source, box) => {
3565
+ const state = provider.get(source, box);
3566
+ imageStates.current.set(source, state);
3567
+ return state;
3568
+ }
3569
+ },
3570
+ expanded: expanded.open.has(src),
3571
+ maxHeight,
3572
+ glyphs,
3573
+ measure
3574
+ }) : void 0), [
3575
+ props.image,
3576
+ provider,
3577
+ expanded,
3578
+ maxHeight,
3579
+ glyphs,
3580
+ measure
3581
+ ]);
3582
+ const ctx = {
3583
+ width: props.width,
3584
+ detail: props.detail ?? "collapsed",
3585
+ placement: props.activityPlacement ?? "inline",
3586
+ liveTail: props.liveTail ?? 3,
3587
+ isActive: (message) => message.id === props.streamingMessageId,
3588
+ now,
3589
+ opened,
3590
+ markdown: props.markdown ?? terminalMarkdown,
3591
+ glyphs,
3592
+ theme,
3593
+ hints: props.hints ?? {},
3594
+ liveFooter: props.liveFooter ?? true,
3595
+ ...props.blockLayouts ? { blockLayouts: props.blockLayouts } : {},
3596
+ frame: props.frame ?? compactFrame,
3597
+ accentFor: props.accentFor,
3598
+ measure,
3599
+ footerExtras: props.footerExtras,
3600
+ finishedActivity: props.finishedActivity,
3601
+ activitySpacing: props.activitySpacing,
3602
+ image
3603
+ };
3604
+ const { lines } = layoutTranscript(props.messages, ctx, cache.current);
3605
+ const rows = Math.max(0, props.height);
3606
+ const first = firstLine(props.scroll, lines.length, rows);
3607
+ const geometry = {
3608
+ total: lines.length,
3609
+ first,
3610
+ rows,
3611
+ below: Math.max(0, lines.length - first - rows)
3612
+ };
3613
+ return {
3614
+ lines,
3615
+ geometry,
3616
+ window: lines.slice(first, first + rows),
3617
+ toggle: props.onToggle ?? ownToggle,
3618
+ now,
3619
+ imageStates: imageStates.current,
3620
+ toggleExpanded
3621
+ };
3622
+ }
3623
+ /** Contiguous lines around `index` in the same tool output box, if it is in one. */
3624
+ function outputBox(lines, index) {
3625
+ const blockOf = (line) => line?.target && (line.target.kind === "tool-output" || line.target.kind === "json-toggle") ? `${line.owner.messageId}␟${line.target.blockId}` : void 0;
3626
+ const box = blockOf(lines[index]);
3627
+ if (!box) return void 0;
3628
+ let a = index;
3629
+ let b = index;
3630
+ while (blockOf(lines[a - 1]) === box) a--;
3631
+ while (blockOf(lines[b + 1]) === box) b++;
3632
+ return [a, b];
3633
+ }
3634
+ /** A tool's whole output, JSON pretty-printed. */
3635
+ function outputText(tool) {
3636
+ const text = toolResultText(tool.result);
3637
+ try {
3638
+ return JSON.stringify(JSON.parse(text), null, 2);
3639
+ } catch {
3640
+ return text;
3641
+ }
3642
+ }
3643
+ /** A message as written (an answer's raw markdown). */
3644
+ function messageText(message) {
3645
+ return message.blocks.filter((block) => block.type === "text").map((block) => block.text).join("\n\n");
3646
+ }
3647
+ const ids = new WeakMap();
3648
+ let nextId = 0;
3649
+ /** A stable id per line object (lines are cached per message, so identity is content). */
3650
+ function lineId(line) {
3651
+ let id = ids.get(line);
3652
+ if (id === void 0) {
3653
+ id = ++nextId;
3654
+ ids.set(line, id);
3655
+ }
3656
+ return id;
3657
+ }
3658
+ /** What a key does in the viewport (pure, so every binding is testable). */
3659
+ function viewportKeyAction(input, key, keys, scroll, geometry, detail) {
3660
+ if (keys === false || isTerminalReport(input)) return void 0;
3661
+ if (keys.page !== false && key.pageUp) return { scroll: pageBy(scroll, -1, geometry) };
3662
+ if (keys.page !== false && key.pageDown) return { scroll: pageBy(scroll, 1, geometry) };
3663
+ if (keys.lines !== false && key.shift && key.upArrow) return { scroll: scrollBy(scroll, -3, geometry) };
3664
+ if (keys.lines !== false && key.shift && key.downArrow) return { scroll: scrollBy(scroll, 3, geometry) };
3665
+ if (keys.homeEnd && key.home) return { scroll: scrollHome };
3666
+ if (keys.homeEnd && key.end) return { scroll: scrollEnd };
3667
+ if (keys.detail !== false && key.ctrl && input === "t") return { detail: NEXT_DETAIL[detail] };
3668
+ return void 0;
3669
+ }
3670
+ function TranscriptViewport(props) {
3671
+ const { theme } = useInkComponents();
3672
+ const { lines, geometry, window, toggle, imageStates, toggleExpanded } = useTranscriptLayout(props);
3673
+ const { onGeometry, onScroll, onDetailChange, scroll, focused = true, keys = {} } = props;
3674
+ const detail = props.detail ?? "collapsed";
3675
+ const measure = props.measure ?? defaultMeasure;
3676
+ const { stdout } = ink.useStdout();
3677
+ const last = React.useRef("");
3678
+ React.useEffect(() => {
3679
+ const key = `${geometry.total}:${geometry.first}:${geometry.rows}:${geometry.below}`;
3680
+ if (key === last.current) return;
3681
+ last.current = key;
3682
+ onGeometry?.(geometry);
3683
+ });
3684
+ const [selection, setSelection] = React.useState(void 0);
3685
+ ink.useInput((input, key) => {
3686
+ const action = viewportKeyAction(input, key, keys, scroll, geometry, detail);
3687
+ if (action && "scroll" in action) onScroll(action.scroll);
3688
+ else if (action) onDetailChange?.(action.detail);
3689
+ }, { isActive: focused && keys !== false });
3690
+ const [readReport] = React.useState(createKeyReportReader);
3691
+ ink.useInput((input) => {
3692
+ const report = readReport(input);
3693
+ if (report.kind === "text" || report.kind === "report" && report.rest) setSelection(void 0);
3694
+ }, { isActive: focused && selection !== void 0 });
3695
+ const mouse = props.mouse;
3696
+ const mouseActive = mouse !== void 0 && focused;
3697
+ const write = mouse?.write ?? props.write ?? ((data) => stdout.write(data));
3698
+ useMouseReporting(mouseActive, write);
3699
+ const box = React.useRef(null);
3700
+ const press = React.useRef(void 0);
3701
+ const lastClick = React.useRef(void 0);
3702
+ const copy = (text, description) => {
3703
+ write(osc52(text));
3704
+ mouse?.onCopy?.(text, description);
3705
+ };
3706
+ /** The point under a screen cell, rows clamped to the visible window. */
3707
+ const pointAt = (e) => {
3708
+ const origin = screenOrigin(box.current);
3709
+ const row = e.y - origin.y;
3710
+ const inside = row >= 0 && row < window.length;
3711
+ const clamped = Math.max(0, Math.min(row, window.length - 1));
3712
+ return {
3713
+ line: geometry.first + clamped,
3714
+ col: Math.max(0, e.x - origin.x),
3715
+ inside
3716
+ };
3717
+ };
3718
+ const click = (at, e) => {
3719
+ const line = lines[at.line];
3720
+ const target = line.target;
3721
+ if (target?.kind === "json-toggle") return toggle(target.blockId, target.path);
3722
+ if (target?.kind === "row-toggle") return toggle(target.rowKey, ROW_PATH);
3723
+ if (target?.kind === "image") {
3724
+ if (e.ctrl) return mouse?.onOpenUrl?.(target.src);
3725
+ const state = imageStates.get(target.src);
3726
+ if (state?.kind === "ask") props.images?.allow(target.src);
3727
+ else if (state?.kind === "ready") toggleExpanded(target.src);
3728
+ return;
3729
+ }
3730
+ const now = Date.now();
3731
+ const previous = lastClick.current;
3732
+ if (previous?.line === at.line && now - previous.at <= (mouse?.doubleClickMs ?? 400)) {
3733
+ lastClick.current = void 0;
3734
+ const message = props.messages.find((m) => m.id === line.owner.messageId);
3735
+ if (target?.kind === "tool-output") {
3736
+ const tool = message.blocks.find((b) => b.type === "tool_call" && b.id === target.blockId);
3737
+ copy(outputText(tool), "tool output");
3738
+ return;
3739
+ }
3740
+ copy(messageText(message), "message");
3741
+ return;
3742
+ }
3743
+ lastClick.current = {
3744
+ line: at.line,
3745
+ at: now
3746
+ };
3747
+ };
3748
+ const onMouse = (e) => {
3749
+ if (window.length === 0) return;
3750
+ if (e.kind === "wheel-up" || e.kind === "wheel-down") {
3751
+ const step = mouse?.wheelStep ?? 3;
3752
+ onScroll(scrollBy(scroll, e.kind === "wheel-up" ? -step : step, geometry));
3753
+ return;
3754
+ }
3755
+ if (e.shift) {
3756
+ if (e.kind === "release") press.current = void 0;
3757
+ return;
3758
+ }
3759
+ if (e.button !== "left" && e.kind !== "release") return;
3760
+ const point = pointAt(e);
3761
+ if (e.kind === "press") {
3762
+ if (!point.inside) return;
3763
+ const clamp$1 = outputBox(lines, point.line);
3764
+ press.current = {
3765
+ at: point,
3766
+ moved: false,
3767
+ ...clamp$1 ? { clamp: clamp$1 } : {}
3768
+ };
3769
+ setSelection(void 0);
3770
+ return;
3771
+ }
3772
+ const current = press.current;
3773
+ if (!current) return;
3774
+ const head = current.clamp ? {
3775
+ line: Math.max(current.clamp[0], Math.min(point.line, current.clamp[1])),
3776
+ col: point.col
3777
+ } : {
3778
+ line: point.line,
3779
+ col: point.col
3780
+ };
3781
+ if (e.kind === "drag") {
3782
+ current.moved = true;
3783
+ setSelection({
3784
+ anchor: current.at,
3785
+ head
3786
+ });
3787
+ return;
3788
+ }
3789
+ press.current = void 0;
3790
+ if (!current.moved && head.line === current.at.line && head.col === current.at.col) {
3791
+ click(current.at, e);
3792
+ return;
3793
+ }
3794
+ const text = copyText(lines, current.at, head, measure);
3795
+ setSelection({
3796
+ anchor: current.at,
3797
+ head
3798
+ });
3799
+ const count = text.split("\n").length;
3800
+ if (text) copy(text, `${count} line${count === 1 ? "" : "s"}`);
3801
+ };
3802
+ const images = props.images;
3803
+ const writeRef = React.useRef(write);
3804
+ writeRef.current = write;
3805
+ React.useEffect(() => {
3806
+ if (images?.query) writeRef.current(images.query);
3807
+ }, [images]);
3808
+ const [readReports] = React.useState(createReportReader);
3809
+ useTerminalData((data) => {
3810
+ for (const report of readReports(data)) {
3811
+ if (images?.takeReply?.(report)) continue;
3812
+ if (mouseActive) for (const e of parseMouse(report)) onMouse(e);
3813
+ }
3814
+ }, mouseActive || images?.takeReply !== void 0);
3815
+ const [from, to] = selection ? ordered(selection.anchor, selection.head) : [];
3816
+ const shown = from && to ? window.map((line, i) => {
3817
+ const at = geometry.first + i;
3818
+ if (at < from.line || at > to.line) return line;
3819
+ const start = at === from.line ? from.col : 0;
3820
+ const end = at === to.line ? to.col : Number.POSITIVE_INFINITY;
3821
+ return highlightLine(line, start, end, theme.selection, measure);
3822
+ }) : window;
3823
+ const selected = from && to ? `${from.line}.${from.col}-${to.line}.${to.col}` : "";
3824
+ const signature = `${geometry.first}:${lines.length}:${props.width}:${selected}:${window.map(lineId).join(",")}`;
3825
+ const body = React.useMemo(() => lines.length === 0 && props.empty ? /* @__PURE__ */ React.createElement(ink.Box, {
3826
+ height: props.height,
3827
+ width: props.width,
3828
+ alignItems: "center",
3829
+ justifyContent: "center"
3830
+ }, props.empty) : /* @__PURE__ */ React.createElement(Lines, {
3831
+ lines: shown,
3832
+ theme,
3833
+ ...props.override ? { override: (line, i) => props.override?.(line, i, geometry.first + i) } : {}
3834
+ }), [
3835
+ signature,
3836
+ theme,
3837
+ props.override,
3838
+ props.empty,
3839
+ props.height
3840
+ ]);
3841
+ return /* @__PURE__ */ React.createElement(ink.Box, {
3842
+ ref: box,
3843
+ height: props.height,
3844
+ width: props.width,
3845
+ flexDirection: "column",
3846
+ overflow: "hidden",
3847
+ flexShrink: 0
3848
+ }, body);
3849
+ }
3850
+
3851
+ //#endregion
3852
+ //#region src/create.ts
3853
+ /**
3854
+ * One shared clock for every timer, rate, spinner and pulse this package
3855
+ * draws (the core hooks' provider, on the host's React).
3856
+ */
3857
+ function ClockProvider(props) {
3858
+ return React.createElement(hooks.ClockProvider, props);
3859
+ }
3860
+ function createChatInk(deps) {
3861
+ bindRuntime(deps);
3862
+ return {
3863
+ React: deps.React,
3864
+ ink: deps.ink,
3865
+ hooks,
3866
+ ClockProvider,
3867
+ Composer,
3868
+ EmptyState,
3869
+ InkChatProvider,
3870
+ Lines,
3871
+ MessageView,
3872
+ PendingRows,
3873
+ PlainMarkdown,
3874
+ Row,
3875
+ Spinner,
3876
+ StatusLine,
3877
+ Styled,
3878
+ Transcript,
3879
+ TranscriptViewport,
3880
+ useInkComponents,
3881
+ useMouseReporting,
3882
+ useOpenState,
3883
+ useTerminalData,
3884
+ useTranscriptLayout
3885
+ };
3886
+ }
3887
+
3888
+ //#endregion
3889
+ //#region src/lines/Markdown.tsx
3890
+ function TerminalMarkdown({ content, streaming, width: width$2, measure = defaultMeasure }) {
3891
+ const { theme, glyphs } = useInkComponents();
3892
+ const ref = React.useRef(null);
3893
+ const box = ink.useBoxMetrics(ref);
3894
+ const { columns } = ink.useWindowSize();
3895
+ const cells$1 = width$2 ?? (box.hasMeasured && box.width > 0 ? box.width : columns);
3896
+ const lines = React.useMemo(() => terminalMarkdown(stripControls(content), cells$1, {
3897
+ measure,
3898
+ glyphs,
3899
+ ...streaming ? { streaming } : {}
3900
+ }), [
3901
+ content,
3902
+ cells$1,
3903
+ measure,
3904
+ glyphs,
3905
+ streaming
3906
+ ]);
3907
+ return /* @__PURE__ */ React.createElement(ink.Box, {
3908
+ ref,
3909
+ flexDirection: "column",
3910
+ flexGrow: 1
3911
+ }, lines.length > 0 && /* @__PURE__ */ React.createElement(Lines, {
3912
+ lines,
3913
+ theme
3914
+ }));
3915
+ }
3916
+
3917
+ //#endregion
3918
+ export { ClockProvider, Composer, EmptyState, InkChatProvider, KITTY_POP, KITTY_PUSH, Lines, MIN_COLUMN, MOUSE_OFF, MOUSE_ON, MessageView, NOTHING_OPEN, PendingRows, PlainMarkdown, ROW_PATH, React, Row, Spinner, StatusLine, Styled, THUMBNAIL, TerminalMarkdown, Transcript, TranscriptViewport, alignments, appearanceProps, asciiGlyphs, backspace, cachedMeasure, caretBoundary, caretLines, caretRow, chunks, clipLines, compactFrame, composerInnerWidth, composerRows, copyText, createChatInk, createHistory, createLayoutCache, createReportReader, defaultMeasure, deleteForward, deleteWordBack, describeSource, emojiGlyphs, emptyComposerValue, emptyEditor, firstLine, fitColumns, footerSpans, graphemes, hardSplit, highlightLine, hooks, ink, insert, isBound, isSeparator, isTerminalReport, killToLineEnd, killToLineStart, layoutBlocks, layoutBody, layoutImage, layoutMessage, layoutSignature, layoutTable, layoutTranscript, lineEnd, lineStart, lineText, mergeInkComponents, mergeSpans, moveBy, moveLine, moveVisual, ordered, osc52, pageBy, parseInline, parseMouse, plainComponents, plainGlyphs, plainMarkdownLayout, plainTheme, prefix, pushHistory, resolveAccent, screenOrigin, scrollBy, scrollEnd, scrollHome, showsSomething, span, spanProps, splitAtCaret, splitRow, splitRuns, stepHistory, stripControls, stripControlsDeep, suffix, terminalMarkdown, threadedFrame, toJson, truncate, useInkComponents, useMouseReporting, useOpenState, useTerminalData, useTranscriptLayout, viewportKeyAction, visualRows, withEntries, wordLeft, wordRight, wrapLines, wrapSpans, wrapText };