@jun133/kitty 0.0.10 → 0.0.12

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.
@@ -0,0 +1,823 @@
1
+ // src/shell/tui/theme.ts
2
+ var TUI_COLORS = {
3
+ background: "#080808",
4
+ panel: "#191919",
5
+ panelStrong: "#222222",
6
+ border: "#303030",
7
+ muted: "#8d8d8d",
8
+ text: "#e5e7eb",
9
+ user: "#d6a84f",
10
+ assistant: "#e5e7eb",
11
+ reasoning: "#a8a8a8",
12
+ thought: "#c99a3f",
13
+ system: "#cbd5e1",
14
+ success: "#34d399",
15
+ warning: "#d6a84f",
16
+ error: "#f87171"
17
+ };
18
+
19
+ // src/shell/tui/transcriptLayout.ts
20
+ import stringWidth2 from "string-width";
21
+
22
+ // src/shell/tui/markdown.ts
23
+ import { marked } from "marked";
24
+
25
+ // src/shell/tui/markdownInline.ts
26
+ import { Lexer } from "marked";
27
+ var MARKED_OPTIONS = { gfm: true };
28
+ function renderInlineSpans(source) {
29
+ const tokens = typeof source === "string" ? Lexer.lexInline(source, MARKED_OPTIONS) : source;
30
+ return compactSpans(tokens.flatMap((token) => renderInlineToken(token, {})));
31
+ }
32
+ function renderInlineText(source) {
33
+ return renderInlineSpans(source).map((span) => span.text).join("");
34
+ }
35
+ function textSpan(text, marks = {}) {
36
+ return text ? [{ text, ...marks }] : [];
37
+ }
38
+ function renderInlineToken(token, marks) {
39
+ switch (token.type) {
40
+ case "text":
41
+ case "escape":
42
+ return textSpan(token.text, marks);
43
+ case "codespan":
44
+ return textSpan(token.text, { ...marks, code: true });
45
+ case "strong":
46
+ return renderInlineSpansWithMarks(token.tokens, { ...marks, bold: true });
47
+ case "em":
48
+ return renderInlineSpansWithMarks(token.tokens, { ...marks, italic: true });
49
+ case "del":
50
+ return renderInlineSpansWithMarks(token.tokens, { ...marks, strike: true });
51
+ case "link":
52
+ return renderLinkToken(token, marks);
53
+ case "image":
54
+ return textSpan(token.text, marks);
55
+ case "br":
56
+ return textSpan("\n", marks);
57
+ case "html":
58
+ return renderHtmlInline(token.text, marks);
59
+ default:
60
+ return renderGenericToken(token, marks);
61
+ }
62
+ }
63
+ function renderInlineSpansWithMarks(tokens, marks) {
64
+ return compactSpans(tokens.flatMap((token) => renderInlineToken(token, marks)));
65
+ }
66
+ function renderLinkToken(token, marks) {
67
+ const linked = renderInlineSpansWithMarks(token.tokens, { ...marks, href: token.href });
68
+ const visible = linked.map((span) => span.text).join("");
69
+ if (!token.href || token.href === visible) {
70
+ return linked;
71
+ }
72
+ return compactSpans([
73
+ ...linked,
74
+ ...textSpan(` (${token.href})`, { ...marks, href: token.href })
75
+ ]);
76
+ }
77
+ function renderHtmlInline(text, marks) {
78
+ if (/^<br\s*\/?>$/i.test(text.trim())) {
79
+ return textSpan("\n", marks);
80
+ }
81
+ return textSpan(decodeHtmlEntities(text), marks);
82
+ }
83
+ function renderGenericToken(token, marks) {
84
+ if ("tokens" in token && Array.isArray(token.tokens)) {
85
+ return renderInlineSpansWithMarks(token.tokens, marks);
86
+ }
87
+ if ("text" in token && typeof token.text === "string") {
88
+ return textSpan(token.text, marks);
89
+ }
90
+ if ("raw" in token && typeof token.raw === "string") {
91
+ return textSpan(token.raw, marks);
92
+ }
93
+ return [];
94
+ }
95
+ function compactSpans(spans) {
96
+ const compacted = [];
97
+ for (const span of spans) {
98
+ if (!span.text) {
99
+ continue;
100
+ }
101
+ const last = compacted[compacted.length - 1];
102
+ if (last && sameMarks(last, span)) {
103
+ compacted[compacted.length - 1] = { ...last, text: `${last.text}${span.text}` };
104
+ continue;
105
+ }
106
+ compacted.push(span);
107
+ }
108
+ return compacted;
109
+ }
110
+ function sameMarks(left, right) {
111
+ return left.bold === right.bold && left.italic === right.italic && left.code === right.code && left.strike === right.strike && left.href === right.href;
112
+ }
113
+ function decodeHtmlEntities(text) {
114
+ return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
115
+ }
116
+
117
+ // src/shell/tui/markdownTable.ts
118
+ import stringWidth from "string-width";
119
+ function renderMarkdownTableLines(header, rows) {
120
+ const headerTexts = header.map(readCellText);
121
+ const rowTexts = rows.map((row) => row.map(readCellText));
122
+ const widths = headerTexts.map((cell, index) => Math.max(
123
+ stringWidth(cell),
124
+ ...rowTexts.map((row) => stringWidth(row[index] ?? ""))
125
+ ));
126
+ return [
127
+ tableLine(joinTableRow(headerTexts, widths)),
128
+ tableLine(widths.map((width) => "\u2500".repeat(Math.max(3, width))).join("\u2500\u253C\u2500")),
129
+ ...rowTexts.map((row) => tableLine(joinTableRow(row, widths)))
130
+ ];
131
+ }
132
+ function readCellText(cell) {
133
+ return cell.tokens ? renderInlineText(cell.tokens) : renderInlineText(cell.text);
134
+ }
135
+ function tableLine(text) {
136
+ return {
137
+ kind: "table",
138
+ text,
139
+ spans: [{ text }]
140
+ };
141
+ }
142
+ function joinTableRow(cells, widths) {
143
+ return cells.map((cell, index) => padDisplayEnd(cell, widths[index] ?? stringWidth(cell))).join(" \u2502 ");
144
+ }
145
+ function padDisplayEnd(text, width) {
146
+ return `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
147
+ }
148
+
149
+ // src/shell/tui/markdown.ts
150
+ var MARKED_OPTIONS2 = { gfm: true };
151
+ function renderMarkdownLines(markdown) {
152
+ try {
153
+ const tokens = marked.lexer(markdown, MARKED_OPTIONS2);
154
+ const lines = [];
155
+ for (const token of tokens) {
156
+ appendToken(lines, token);
157
+ }
158
+ return trimOuterBlankLines(lines);
159
+ } catch {
160
+ return markdown.split(/\r?\n/).map((text) => line("text", text, textSpan(text)));
161
+ }
162
+ }
163
+ function appendToken(lines, token) {
164
+ switch (token.type) {
165
+ case "heading":
166
+ appendInlineLine(lines, "heading", token.tokens);
167
+ pushBlank(lines);
168
+ return;
169
+ case "paragraph":
170
+ appendInlineLine(lines, "text", token.tokens);
171
+ pushBlank(lines);
172
+ return;
173
+ case "list":
174
+ appendList(lines, token);
175
+ pushBlank(lines);
176
+ return;
177
+ case "code":
178
+ appendCode(lines, token);
179
+ pushBlank(lines);
180
+ return;
181
+ case "blockquote":
182
+ appendBlockquote(lines, token);
183
+ pushBlank(lines);
184
+ return;
185
+ case "hr":
186
+ push(lines, line("rule", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", textSpan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")));
187
+ pushBlank(lines);
188
+ return;
189
+ case "space":
190
+ pushBlank(lines);
191
+ return;
192
+ case "table":
193
+ lines.push(...renderMarkdownTableLines(
194
+ token.header,
195
+ token.rows
196
+ ));
197
+ pushBlank(lines);
198
+ return;
199
+ default:
200
+ appendUnknownToken(lines, token);
201
+ }
202
+ }
203
+ function appendInlineLine(lines, kind, tokens) {
204
+ const spans = renderInlineSpans(tokens);
205
+ const text = spans.map((span) => span.text).join("").trimEnd();
206
+ push(lines, line(kind, text, trimEndSpans(spans)));
207
+ }
208
+ function appendList(lines, token) {
209
+ const start = typeof token.start === "number" ? token.start : Number.parseInt(String(token.start || 1), 10);
210
+ const orderedStart = Number.isFinite(start) ? start : 1;
211
+ token.items.forEach((item, index) => {
212
+ const marker = token.ordered ? `${orderedStart + index}.` : "\u2022";
213
+ const markerSpans = textSpan(`${marker} `, { bold: true });
214
+ const bodySpans = readListItemSpans(item);
215
+ const rows = splitSpansOnNewline([...markerSpans, ...bodySpans]);
216
+ rows.forEach((spans, rowIndex) => {
217
+ const prefix = rowIndex === 0 ? "" : " ";
218
+ const lineSpans = prefix ? [...textSpan(prefix), ...spans] : spans;
219
+ push(lines, line("list", spansText(lineSpans), lineSpans));
220
+ });
221
+ });
222
+ }
223
+ function readListItemSpans(item) {
224
+ const paragraph = item.tokens.find((token) => token.type === "paragraph");
225
+ if (paragraph) {
226
+ return renderInlineSpans(paragraph.tokens);
227
+ }
228
+ return renderInlineSpans(item.text);
229
+ }
230
+ function appendCode(lines, token) {
231
+ const language = normalizeLanguage(token.lang);
232
+ if (language) {
233
+ const text = ` ${language} `;
234
+ push(lines, line("code", text, textSpan(text, { code: true }), language));
235
+ }
236
+ for (const codeLine of token.text.split(/\r?\n/)) {
237
+ push(lines, line("code", codeLine, textSpan(codeLine, { code: true }), language));
238
+ }
239
+ }
240
+ function appendBlockquote(lines, token) {
241
+ const nested = token.tokens.length > 0 ? renderNestedLines(token.tokens) : renderMarkdownLines(token.text);
242
+ for (const nestedLine of nested) {
243
+ const prefix = nestedLine.text ? "\u2502 " : "\u2502";
244
+ const spans = nestedLine.text ? [...textSpan("\u2502 ", { italic: true }), ...nestedLine.spans] : textSpan("\u2502", { italic: true });
245
+ push(lines, line("quote", `${prefix}${nestedLine.text}`, spans));
246
+ }
247
+ }
248
+ function renderNestedLines(tokens) {
249
+ const lines = [];
250
+ for (const token of tokens) {
251
+ appendToken(lines, token);
252
+ }
253
+ return trimOuterBlankLines(lines);
254
+ }
255
+ function appendUnknownToken(lines, token) {
256
+ if ("tokens" in token && Array.isArray(token.tokens)) {
257
+ appendInlineLine(lines, "text", token.tokens);
258
+ pushBlank(lines);
259
+ return;
260
+ }
261
+ const text = "text" in token && typeof token.text === "string" ? renderInlineText(token.text) : "raw" in token && typeof token.raw === "string" ? token.raw : "";
262
+ if (text.trim()) {
263
+ push(lines, line("text", text, textSpan(text)));
264
+ pushBlank(lines);
265
+ }
266
+ }
267
+ function splitSpansOnNewline(spans) {
268
+ const rows = [[]];
269
+ for (const span of spans) {
270
+ const parts = span.text.split("\n");
271
+ parts.forEach((part, index) => {
272
+ if (index > 0) {
273
+ rows.push([]);
274
+ }
275
+ if (part) {
276
+ rows[rows.length - 1].push({ ...span, text: part });
277
+ }
278
+ });
279
+ }
280
+ return rows.length > 0 ? rows : [[]];
281
+ }
282
+ function trimEndSpans(spans) {
283
+ const next = spans.slice();
284
+ while (next.length > 0) {
285
+ const last = next[next.length - 1];
286
+ const trimmed = last.text.trimEnd();
287
+ if (trimmed.length === last.text.length) {
288
+ break;
289
+ }
290
+ if (trimmed) {
291
+ next[next.length - 1] = { ...last, text: trimmed };
292
+ break;
293
+ }
294
+ next.pop();
295
+ }
296
+ return next;
297
+ }
298
+ function normalizeLanguage(language) {
299
+ const [name] = (language ?? "").trim().split(/\s+/);
300
+ return name || void 0;
301
+ }
302
+ function line(kind, text, spans, language) {
303
+ return {
304
+ kind,
305
+ text,
306
+ spans,
307
+ language
308
+ };
309
+ }
310
+ function push(lines, next) {
311
+ lines.push(next);
312
+ }
313
+ function pushBlank(lines) {
314
+ push(lines, line("text", "", []));
315
+ }
316
+ function trimOuterBlankLines(lines) {
317
+ let start = 0;
318
+ let end = lines.length;
319
+ while (start < end && lines[start]?.text === "") {
320
+ start += 1;
321
+ }
322
+ while (end > start && lines[end - 1]?.text === "") {
323
+ end -= 1;
324
+ }
325
+ return lines.slice(start, end);
326
+ }
327
+ function spansText(spans) {
328
+ return spans.map((span) => span.text).join("");
329
+ }
330
+
331
+ // src/shell/tui/transcriptLayout.ts
332
+ var TRANSCRIPT_OUTER_PADDING_X = 3;
333
+ var MIN_BODY_WIDTH = 8;
334
+ var REASONING_PREFIX = "Thinking: ";
335
+ function renderTranscriptLineViews(entries, viewportWidth, theme) {
336
+ return entries.flatMap((entry) => renderTranscriptEntryLineViews(entry, viewportWidth, theme));
337
+ }
338
+ function measureTranscriptRows(entries, viewportWidth, theme) {
339
+ return renderTranscriptLineViews(entries, viewportWidth, theme).length;
340
+ }
341
+ function renderTranscriptEntryLineViews(entry, viewportWidth, theme) {
342
+ const frame = readRoleFrame(entry.role, viewportWidth);
343
+ const style = readRoleStyle(entry.role, theme);
344
+ const sourceRows = readEntryDisplayRows(entry);
345
+ const contentRows = wrapEntryRows(entry, sourceRows, frame.bodyWidth);
346
+ const rows = contentRows.length > 0 ? contentRows : [{ markdownKind: void 0, language: void 0, prefix: "", text: "", spans: [] }];
347
+ const entryId = entry.id;
348
+ return [
349
+ {
350
+ id: `${entryId}-spacer`,
351
+ entryId,
352
+ role: entry.role,
353
+ kind: "spacer",
354
+ text: "",
355
+ spans: [],
356
+ prefix: "",
357
+ markdownKind: void 0,
358
+ language: void 0,
359
+ isFirstContentLine: false,
360
+ frame,
361
+ style
362
+ },
363
+ ...rows.map((row, index) => ({
364
+ id: `${entryId}-line-${index + 1}`,
365
+ entryId,
366
+ role: entry.role,
367
+ kind: "content",
368
+ text: row.text,
369
+ spans: row.spans,
370
+ prefix: row.prefix,
371
+ markdownKind: row.markdownKind,
372
+ language: row.language,
373
+ isFirstContentLine: index === 0,
374
+ frame,
375
+ style: applyMarkdownStyle(style, row.markdownKind, theme)
376
+ }))
377
+ ];
378
+ }
379
+ function readRoleFrame(role, viewportWidth) {
380
+ const frameWidth = Math.max(1, viewportWidth - TRANSCRIPT_OUTER_PADDING_X * 2);
381
+ const base = readRoleFrameBase(role);
382
+ const bodyWidth = Math.max(
383
+ MIN_BODY_WIDTH,
384
+ frameWidth - base.marginLeft - base.paddingLeft - base.paddingRight - stringWidth2(base.gutter) - base.gap
385
+ );
386
+ return {
387
+ ...base,
388
+ bodyWidth
389
+ };
390
+ }
391
+ function readRoleFrameBase(role) {
392
+ switch (role) {
393
+ case "user":
394
+ return {
395
+ gap: 2,
396
+ gutter: "\u2503",
397
+ marginLeft: 1,
398
+ paddingLeft: 1,
399
+ paddingRight: 1
400
+ };
401
+ case "reasoning":
402
+ return {
403
+ gap: 2,
404
+ gutter: "\u2503",
405
+ marginLeft: 1,
406
+ paddingLeft: 1,
407
+ paddingRight: 1
408
+ };
409
+ case "system":
410
+ return {
411
+ gap: 2,
412
+ gutter: "\u2502",
413
+ marginLeft: 2,
414
+ paddingLeft: 1,
415
+ paddingRight: 1
416
+ };
417
+ case "assistant":
418
+ return {
419
+ gap: 2,
420
+ gutter: " ",
421
+ marginLeft: 2,
422
+ paddingLeft: 1,
423
+ paddingRight: 1
424
+ };
425
+ }
426
+ }
427
+ function readRoleStyle(role, theme) {
428
+ switch (role) {
429
+ case "user":
430
+ return {
431
+ accent: theme.user,
432
+ background: theme.panelStrong,
433
+ text: theme.text,
434
+ bold: true,
435
+ dim: false,
436
+ italicPrefix: false
437
+ };
438
+ case "reasoning":
439
+ return {
440
+ accent: theme.border,
441
+ background: void 0,
442
+ text: theme.reasoning,
443
+ bold: false,
444
+ dim: true,
445
+ italicPrefix: true
446
+ };
447
+ case "system":
448
+ return {
449
+ accent: theme.border,
450
+ background: theme.panel,
451
+ text: theme.system,
452
+ bold: false,
453
+ dim: false,
454
+ italicPrefix: false
455
+ };
456
+ case "assistant":
457
+ return {
458
+ accent: theme.background,
459
+ background: void 0,
460
+ text: theme.assistant,
461
+ bold: false,
462
+ dim: false,
463
+ italicPrefix: false
464
+ };
465
+ }
466
+ }
467
+ function readEntryDisplayRows(entry) {
468
+ if (entry.role === "assistant" || entry.role === "reasoning") {
469
+ const markdownRows = renderMarkdownLines(entry.text);
470
+ return markdownRows.length > 0 ? markdownRows.map((row) => ({
471
+ markdownKind: row.kind,
472
+ text: row.text,
473
+ spans: row.spans,
474
+ language: row.language
475
+ })) : [{ markdownKind: void 0, text: "", spans: [], language: void 0 }];
476
+ }
477
+ return entry.text.split(/\r?\n/).map((text) => ({
478
+ markdownKind: void 0,
479
+ text,
480
+ spans: [{ text }],
481
+ language: void 0
482
+ }));
483
+ }
484
+ function wrapEntryRows(entry, sourceRows, bodyWidth) {
485
+ if (entry.role !== "reasoning") {
486
+ return sourceRows.flatMap((line2) => wrapSourceRow(line2, bodyWidth, ""));
487
+ }
488
+ const rows = [];
489
+ const firstBodyWidth = Math.max(1, bodyWidth - stringWidth2(REASONING_PREFIX));
490
+ sourceRows.forEach((line2, sourceIndex) => {
491
+ const width = sourceIndex === 0 ? firstBodyWidth : bodyWidth;
492
+ const prefix = sourceIndex === 0 ? REASONING_PREFIX : "";
493
+ rows.push(...wrapSourceRow(line2, width, prefix));
494
+ });
495
+ return rows;
496
+ }
497
+ function wrapSourceRow(source, width, firstPrefix) {
498
+ const spanRows = wrapMarkdownSpans(source.spans.length > 0 ? source.spans : [{ text: source.text }], width);
499
+ return spanRows.map((spans, index) => ({
500
+ markdownKind: source.markdownKind,
501
+ language: source.language,
502
+ prefix: index === 0 ? firstPrefix : "",
503
+ text: transcriptSpansText(spans),
504
+ spans
505
+ }));
506
+ }
507
+ function wrapMarkdownSpans(spans, width) {
508
+ const rows = [[]];
509
+ let cursorWidth = 0;
510
+ for (const span of spans) {
511
+ const parts = span.text.split(/\r?\n/);
512
+ parts.forEach((part, partIndex) => {
513
+ if (partIndex > 0) {
514
+ rows.push([]);
515
+ cursorWidth = 0;
516
+ }
517
+ cursorWidth = appendWrappedSpan(rows, cursorWidth, span, part, width);
518
+ });
519
+ }
520
+ return rows.length > 0 ? rows : [[]];
521
+ }
522
+ function appendWrappedSpan(rows, cursorWidth, span, text, width) {
523
+ if (!text) {
524
+ return cursorWidth;
525
+ }
526
+ let nextCursor = cursorWidth;
527
+ for (const segment of splitWrapSegments(text)) {
528
+ const segmentWidth = stringWidth2(segment);
529
+ const isBlank = segment.trim() === "";
530
+ if (!isBlank && nextCursor > 0 && nextCursor + segmentWidth > width) {
531
+ rows.push([]);
532
+ nextCursor = 0;
533
+ }
534
+ const chunks = splitSegmentByWidth(segment, Math.max(1, width), nextCursor);
535
+ for (const chunk of chunks) {
536
+ if (chunk.newRow) {
537
+ rows.push([]);
538
+ nextCursor = 0;
539
+ }
540
+ pushTranscriptSpan(rows[rows.length - 1], toTranscriptSpan(span, chunk.text));
541
+ nextCursor += stringWidth2(chunk.text);
542
+ }
543
+ }
544
+ return nextCursor;
545
+ }
546
+ function splitWrapSegments(text) {
547
+ return text.match(/\s+|\S+/gu) ?? [];
548
+ }
549
+ function splitSegmentByWidth(segment, width, cursorWidth) {
550
+ const rows = [];
551
+ let current = "";
552
+ let currentWidth = cursorWidth;
553
+ for (const char of Array.from(segment)) {
554
+ const charWidth = stringWidth2(char);
555
+ if (current && currentWidth + charWidth > width) {
556
+ rows.push({ text: current, newRow: rows.length > 0 || cursorWidth > 0 });
557
+ current = "";
558
+ currentWidth = 0;
559
+ }
560
+ if (!current && currentWidth > 0 && currentWidth + charWidth > width) {
561
+ rows.push({ text: "", newRow: true });
562
+ currentWidth = 0;
563
+ }
564
+ current += char;
565
+ currentWidth += charWidth;
566
+ }
567
+ if (current) {
568
+ rows.push({ text: current, newRow: rows.length > 0 && rows[rows.length - 1]?.text !== "" });
569
+ }
570
+ return rows;
571
+ }
572
+ function toTranscriptSpan(span, text) {
573
+ return {
574
+ text,
575
+ bold: span.bold ?? false,
576
+ italic: span.italic ?? false,
577
+ code: span.code ?? false,
578
+ dim: false,
579
+ strike: span.strike ?? false,
580
+ href: span.href
581
+ };
582
+ }
583
+ function pushTranscriptSpan(row, span) {
584
+ if (!span.text) {
585
+ return;
586
+ }
587
+ const last = row[row.length - 1];
588
+ if (last && sameTranscriptSpanStyle(last, span)) {
589
+ row[row.length - 1] = { ...last, text: `${last.text}${span.text}` };
590
+ return;
591
+ }
592
+ row.push(span);
593
+ }
594
+ function sameTranscriptSpanStyle(left, right) {
595
+ return left.bold === right.bold && left.italic === right.italic && left.code === right.code && left.dim === right.dim && left.strike === right.strike && left.href === right.href;
596
+ }
597
+ function transcriptSpansText(spans) {
598
+ return spans.map((span) => span.text).join("");
599
+ }
600
+ function applyMarkdownStyle(base, kind, theme) {
601
+ switch (kind) {
602
+ case "heading":
603
+ return {
604
+ ...base,
605
+ text: theme.user,
606
+ bold: true
607
+ };
608
+ case "code":
609
+ return {
610
+ ...base,
611
+ background: theme.panel,
612
+ text: theme.system
613
+ };
614
+ case "quote":
615
+ return {
616
+ ...base,
617
+ text: theme.reasoning,
618
+ dim: true
619
+ };
620
+ case "rule":
621
+ case "table":
622
+ return {
623
+ ...base,
624
+ text: theme.muted
625
+ };
626
+ case "list":
627
+ case "text":
628
+ case void 0:
629
+ return base;
630
+ }
631
+ }
632
+
633
+ // src/shell/tui/store.ts
634
+ var DEFAULT_DOCK = {
635
+ work: {
636
+ active: false,
637
+ label: "\u7A7A\u95F2",
638
+ detail: "\u6CA1\u6709\u540E\u53F0\u4EFB\u52A1\u6216\u5B50\u4EE3\u7406\u6B63\u5728\u6267\u884C"
639
+ },
640
+ background: "\u7A7A\u95F2",
641
+ subagent: "\u7A7A\u95F2",
642
+ context: "0 chars (0%)"
643
+ };
644
+ function createInitialTuiState(session) {
645
+ return {
646
+ transcript: session ? session.messages.flatMap(toTranscriptEntry) : [],
647
+ dock: {
648
+ ...DEFAULT_DOCK,
649
+ context: formatContextBudget(session)
650
+ },
651
+ scroll: {
652
+ offset: 0,
653
+ stickToBottom: true,
654
+ newContentPending: false
655
+ },
656
+ composer: {
657
+ promptLabel: "> ",
658
+ visibleRows: 1
659
+ }
660
+ };
661
+ }
662
+ function appendTranscriptEntry(state, entry, viewport, options = {}) {
663
+ const next = {
664
+ ...state,
665
+ transcript: [...state.transcript, { ...entry, id: createEntryId(state.transcript.length) }]
666
+ };
667
+ return applyContentChange(next, viewport, options);
668
+ }
669
+ function appendTranscriptText(state, role, text, viewport, options = {}) {
670
+ const last = state.transcript[state.transcript.length - 1];
671
+ if (last && last.role === role && (role === "assistant" || role === "reasoning")) {
672
+ const transcript = state.transcript.slice(0, -1);
673
+ const next = {
674
+ ...state,
675
+ transcript: [...transcript, { ...last, text: `${last.text}${text}` }]
676
+ };
677
+ return applyContentChange(next, viewport, options);
678
+ }
679
+ return appendTranscriptEntry(state, { role, text }, viewport, options);
680
+ }
681
+ function updateRuntimeDock(state, dock) {
682
+ return {
683
+ ...state,
684
+ dock: {
685
+ ...state.dock,
686
+ ...dock,
687
+ work: {
688
+ ...state.dock.work,
689
+ ...dock.work
690
+ }
691
+ }
692
+ };
693
+ }
694
+ function updateComposerState(state, composer) {
695
+ return {
696
+ ...state,
697
+ composer: {
698
+ ...state.composer,
699
+ ...composer
700
+ }
701
+ };
702
+ }
703
+ function scrollTuiTranscript(state, viewport, delta, options = {}) {
704
+ const maxOffset = getMaxScrollOffset(state, viewport, options);
705
+ const offset = clamp(state.scroll.offset + delta, 0, maxOffset);
706
+ return {
707
+ ...state,
708
+ scroll: {
709
+ offset,
710
+ stickToBottom: offset >= maxOffset,
711
+ newContentPending: offset >= maxOffset ? false : state.scroll.newContentPending
712
+ }
713
+ };
714
+ }
715
+ function scrollTuiTranscriptToTop(state) {
716
+ return {
717
+ ...state,
718
+ scroll: {
719
+ offset: 0,
720
+ stickToBottom: false,
721
+ newContentPending: state.scroll.newContentPending
722
+ }
723
+ };
724
+ }
725
+ function scrollTuiTranscriptToBottom(state, viewport, options = {}) {
726
+ return {
727
+ ...state,
728
+ scroll: {
729
+ offset: getMaxScrollOffset(state, viewport, options),
730
+ stickToBottom: true,
731
+ newContentPending: false
732
+ }
733
+ };
734
+ }
735
+ function applyViewportResize(state, viewport, options = {}) {
736
+ if (state.scroll.stickToBottom) {
737
+ return scrollTuiTranscriptToBottom(state, viewport, options);
738
+ }
739
+ return {
740
+ ...state,
741
+ scroll: {
742
+ ...state.scroll,
743
+ offset: clamp(state.scroll.offset, 0, getMaxScrollOffset(state, viewport, options))
744
+ }
745
+ };
746
+ }
747
+ function getMaxScrollOffset(state, viewport, options = {}) {
748
+ const rows = options.projection ? options.projection.measureRows(state.transcript, viewport.width) : measureTranscriptRows2(state.transcript, viewport.width);
749
+ return Math.max(0, rows - viewport.height);
750
+ }
751
+ function measureTranscriptRows2(entries, width) {
752
+ return measureTranscriptRows(entries, width, TUI_COLORS);
753
+ }
754
+ function renderTranscriptLineViews2(entries, width) {
755
+ return renderTranscriptLineViews(entries, width, TUI_COLORS);
756
+ }
757
+ function formatContextBudget(session) {
758
+ const budget = session?.contextBudget;
759
+ if (!budget) {
760
+ return "0 chars (0%)";
761
+ }
762
+ const percent = Math.round(budget.usageRatio * 100);
763
+ return `${budget.estimatedChars}/${budget.limitChars} chars (${percent}%)`;
764
+ }
765
+ function applyContentChange(state, viewport, options) {
766
+ if (state.scroll.stickToBottom) {
767
+ return scrollTuiTranscriptToBottom(state, viewport, options);
768
+ }
769
+ return {
770
+ ...state,
771
+ scroll: {
772
+ ...state.scroll,
773
+ newContentPending: true
774
+ }
775
+ };
776
+ }
777
+ function toTranscriptEntry(message, index) {
778
+ if (message.source === "internal" || !message.content?.trim()) {
779
+ return [];
780
+ }
781
+ if (message.role === "user") {
782
+ return [{ id: createEntryId(index), role: "user", text: message.content }];
783
+ }
784
+ if (message.role === "assistant") {
785
+ return [{ id: createEntryId(index), role: "assistant", text: message.content }];
786
+ }
787
+ return [];
788
+ }
789
+ function parseSubmittedInputEcho(text) {
790
+ const lines = text.split(/\r?\n/);
791
+ if (lines.length === 0 || !lines[0]?.startsWith("> ")) {
792
+ return void 0;
793
+ }
794
+ const parsed = lines.map((line2, index) => {
795
+ const prefix = index === 0 ? "> " : "\u2026 ";
796
+ return line2.startsWith(prefix) ? line2.slice(prefix.length) : line2;
797
+ }).join("\n");
798
+ return parsed.trim() ? parsed : void 0;
799
+ }
800
+ function createEntryId(index) {
801
+ return `entry-${index + 1}`;
802
+ }
803
+ function clamp(value, min, max) {
804
+ return Math.max(min, Math.min(max, value));
805
+ }
806
+
807
+ export {
808
+ TUI_COLORS,
809
+ TRANSCRIPT_OUTER_PADDING_X,
810
+ renderTranscriptEntryLineViews,
811
+ createInitialTuiState,
812
+ appendTranscriptEntry,
813
+ appendTranscriptText,
814
+ updateRuntimeDock,
815
+ updateComposerState,
816
+ scrollTuiTranscript,
817
+ scrollTuiTranscriptToTop,
818
+ scrollTuiTranscriptToBottom,
819
+ applyViewportResize,
820
+ renderTranscriptLineViews2 as renderTranscriptLineViews,
821
+ formatContextBudget,
822
+ parseSubmittedInputEcho
823
+ };