xshat-lite 1.0.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.
@@ -0,0 +1,2204 @@
1
+ import ora from 'ora';
2
+ import { Marked } from 'marked';
3
+ import { markedTerminal } from 'marked-terminal';
4
+ import stringWidth from 'string-width';
5
+ import * as color from 'yoctocolors';
6
+
7
+ const COLORS = {
8
+ user: color.blueBright,
9
+ ai: color.greenBright,
10
+ time: color.gray,
11
+ text: color.whiteBright,
12
+ spin: color.yellowBright,
13
+ error: color.redBright,
14
+ border: color.gray,
15
+ reset: (text) => text
16
+ };
17
+
18
+ const ANSI_PATTERN = /\x1B\[[0-9;]*m/g;
19
+
20
+ export function stripAnsi(text) {
21
+ return String(text ?? '').replace(ANSI_PATTERN, '');
22
+ }
23
+
24
+ export function truncateText(text, width) {
25
+ const plain = stripAnsi(text);
26
+ if (stringWidth(plain) <= width) {
27
+ return plain;
28
+ }
29
+
30
+ if (width <= 1) {
31
+ return plain.slice(0, 1);
32
+ }
33
+
34
+ let output = '';
35
+ for (const char of plain) {
36
+ if (stringWidth(output + char) > width - 1) {
37
+ break;
38
+ }
39
+ output += char;
40
+ }
41
+
42
+ return `${output}…`;
43
+ }
44
+
45
+ export function wrapText(text, width) {
46
+ const normalized = String(text ?? '').replace(/\r/g, '');
47
+
48
+ if (width <= 0) {
49
+ return [normalized];
50
+ }
51
+
52
+ const lines = [];
53
+
54
+ for (const block of normalized.split('\n')) {
55
+ if (!block) {
56
+ lines.push('');
57
+ continue;
58
+ }
59
+
60
+ let rest = block;
61
+ while (stringWidth(stripAnsi(rest)) > width) {
62
+ let slice = '';
63
+ for (const char of rest) {
64
+ if (stringWidth(stripAnsi(slice + char)) > width) {
65
+ break;
66
+ }
67
+ slice += char;
68
+ }
69
+ const breakpoint = slice.lastIndexOf(' ');
70
+ if (breakpoint > Math.floor(width / 2)) {
71
+ slice = slice.slice(0, breakpoint);
72
+ }
73
+ lines.push(slice.trimEnd());
74
+ rest = rest.slice(slice.length).trimStart();
75
+ }
76
+ lines.push(rest);
77
+ }
78
+
79
+ return lines;
80
+ }
81
+
82
+ function measureText(text) {
83
+ return stringWidth(stripAnsi(text));
84
+ }
85
+
86
+ export function formatKeyValueRows(rows, totalWidth = 72) {
87
+ const safeRows = rows.filter((row) => row && row.label);
88
+ const contentWidth = Math.max(36, totalWidth - 4);
89
+ const labelWidth = Math.min(
90
+ 14,
91
+ Math.max(6, ...safeRows.map((row) => measureText(row.label)))
92
+ );
93
+ const valueWidth = Math.max(10, contentWidth - labelWidth - 3);
94
+ const lines = [];
95
+
96
+ for (const row of safeRows) {
97
+ const wrapped = wrapText(row.value || '', valueWidth);
98
+ wrapped.forEach((line, index) => {
99
+ const label = index === 0 ? truncateText(row.label, labelWidth) : '';
100
+ const left = label.padEnd(labelWidth, ' ');
101
+ const right = truncateText(line, valueWidth).padEnd(valueWidth, ' ');
102
+ lines.push(` ${left} │ ${right} `);
103
+ });
104
+ }
105
+
106
+ return {
107
+ labelWidth,
108
+ valueWidth,
109
+ lines
110
+ };
111
+ }
112
+
113
+ function isMarkdownTableLine(line) {
114
+ const trimmed = String(line ?? '').trim();
115
+ return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.includes('|');
116
+ }
117
+
118
+ function isMarkdownTableSeparator(line) {
119
+ return /^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|$/.test(
120
+ String(line ?? '').trim()
121
+ );
122
+ }
123
+
124
+ function splitMarkdownRow(line) {
125
+ return String(line ?? '')
126
+ .trim()
127
+ .slice(1, -1)
128
+ .split('|')
129
+ .map((cell) => cell.trim());
130
+ }
131
+
132
+ export function parseMarkdownTable(lines = []) {
133
+ if (!Array.isArray(lines) || lines.length < 2) {
134
+ return null;
135
+ }
136
+
137
+ const header = splitMarkdownRow(lines[0]);
138
+ const bodyLines = isMarkdownTableSeparator(lines[1]) ? lines.slice(2) : lines.slice(1);
139
+ const rows = bodyLines
140
+ .filter((line) => isMarkdownTableLine(line))
141
+ .map((line) => splitMarkdownRow(line));
142
+
143
+ if (header.length === 0 || rows.length === 0) {
144
+ return null;
145
+ }
146
+
147
+ return { header, rows };
148
+ }
149
+
150
+ export function formatMarkdownTable(lines = [], totalWidth = 88) {
151
+ const parsed = parseMarkdownTable(lines);
152
+ if (!parsed) {
153
+ return [];
154
+ }
155
+
156
+ const columns = parsed.header.length;
157
+ const availableWidth = Math.max(48, totalWidth - 4);
158
+ const frameWidth = 2 + columns * 3 + widthsPlaceholder(columns);
159
+ const minColumnWidth = columns <= 3 ? 12 : columns <= 5 ? 10 : 8;
160
+ const maxColumnWidth = columns <= 3 ? 36 : columns <= 5 ? 28 : 20;
161
+
162
+ const preferredWidths = parsed.header.map((cell, index) => {
163
+ const candidates = [cell, ...parsed.rows.map((row) => row[index] || '')];
164
+ return Math.min(
165
+ maxColumnWidth,
166
+ Math.max(minColumnWidth, ...candidates.map((item) => measureText(item)))
167
+ );
168
+ });
169
+
170
+ const borderWidth = 1 + columns + columns * 2;
171
+ const widthBudget = Math.max(columns * minColumnWidth, availableWidth - borderWidth);
172
+ const widths = fitColumnWidths(preferredWidths, widthBudget, minColumnWidth);
173
+ const preferredTotalWidth = preferredWidths.reduce((sum, width) => sum + width, 0) + borderWidth;
174
+
175
+ if (
176
+ (columns >= 5 && (widths.some((width) => width <= 10) || preferredTotalWidth > availableWidth)) ||
177
+ (columns === 4 && (widths.some((width) => width <= 9) || preferredTotalWidth > availableWidth + 4)) ||
178
+ (columns >= 4 && totalWidth < 96)
179
+ ) {
180
+ return formatMarkdownTableAsCards(parsed, totalWidth);
181
+ }
182
+
183
+ const border = ` ├${widths.map((width) => '─'.repeat(width + 2)).join('┼')}┤`;
184
+ const top = ` ┌${widths.map((width) => '─'.repeat(width + 2)).join('┬')}┐`;
185
+ const bottom = ` └${widths.map((width) => '─'.repeat(width + 2)).join('┴')}┘`;
186
+ const renderRow = (cells) => renderWrappedTableRow(cells, widths);
187
+
188
+ return [
189
+ top,
190
+ ...renderRow(parsed.header),
191
+ border,
192
+ ...parsed.rows.flatMap((row, index) => {
193
+ const rendered = renderRow(row);
194
+ if (index === parsed.rows.length - 1) {
195
+ return rendered;
196
+ }
197
+ return [...rendered, border];
198
+ }),
199
+ bottom
200
+ ];
201
+ }
202
+
203
+ function widthsPlaceholder(columns) {
204
+ return columns * 8;
205
+ }
206
+
207
+ function fitColumnWidths(preferredWidths, totalBudget, minColumnWidth) {
208
+ const widths = preferredWidths.map((width) => Math.max(minColumnWidth, width));
209
+ let total = widths.reduce((sum, width) => sum + width, 0);
210
+
211
+ while (total > totalBudget) {
212
+ let changed = false;
213
+
214
+ for (let index = 0; index < widths.length && total > totalBudget; index++) {
215
+ if (widths[index] > minColumnWidth) {
216
+ widths[index] -= 1;
217
+ total -= 1;
218
+ changed = true;
219
+ }
220
+ }
221
+
222
+ if (!changed) {
223
+ break;
224
+ }
225
+ }
226
+
227
+ return widths;
228
+ }
229
+
230
+ function renderWrappedTableRow(cells, widths) {
231
+ const wrappedCells = cells.map((cell, index) => {
232
+ const lines = wrapText(cell || '', widths[index]).map((line) => line.trimEnd());
233
+ return lines.length > 0 ? lines : [''];
234
+ });
235
+ const height = Math.max(...wrappedCells.map((lines) => lines.length));
236
+ const output = [];
237
+
238
+ for (let rowIndex = 0; rowIndex < height; rowIndex++) {
239
+ const line = wrappedCells.map((lines, index) => {
240
+ const cell = lines[rowIndex] || '';
241
+ const width = widths[index];
242
+ return padDisplay(cell, width);
243
+ });
244
+ output.push(` │ ${line.join(' │ ')} │`);
245
+ }
246
+
247
+ return output;
248
+ }
249
+
250
+ function padDisplay(text, width) {
251
+ const plain = String(text ?? '');
252
+ const padding = Math.max(0, width - measureText(plain));
253
+ return `${plain}${' '.repeat(padding)}`;
254
+ }
255
+
256
+ function formatMarkdownTableAsCards(parsed, totalWidth = 88) {
257
+ const output = [];
258
+
259
+ parsed.rows.forEach((row, index) => {
260
+ output.push(` [${String(index + 1).padStart(2, '0')}]`);
261
+ parsed.header.forEach((label, cellIndex) => {
262
+ const lines = wrapText(row[cellIndex] || '', Math.max(18, totalWidth - 22));
263
+ lines.forEach((line, lineIndex) => {
264
+ const left = lineIndex === 0 ? truncateText(label, 12).padEnd(12, ' ') : ' '.repeat(12);
265
+ output.push(` ${left} : ${line}`);
266
+ });
267
+ });
268
+ if (index < parsed.rows.length - 1) {
269
+ output.push(` ${'─'.repeat(Math.max(20, totalWidth - 4))}`);
270
+ }
271
+ });
272
+
273
+ return output;
274
+ }
275
+
276
+ function parseChartRows(text = '') {
277
+ const trimmed = String(text ?? '').trim();
278
+ if (!trimmed) {
279
+ return [];
280
+ }
281
+
282
+ if (trimmed.startsWith('[')) {
283
+ try {
284
+ const value = JSON.parse(trimmed);
285
+ if (Array.isArray(value)) {
286
+ return value
287
+ .map((item) => ({
288
+ label: String(item.label ?? item.name ?? '').trim(),
289
+ value: Number(item.value ?? item.count ?? item.score)
290
+ }))
291
+ .filter((item) => item.label && Number.isFinite(item.value));
292
+ }
293
+ } catch {
294
+ // ignore and continue with line-based parsing
295
+ }
296
+ }
297
+
298
+ return trimmed
299
+ .split('\n')
300
+ .map((line) => line.trim())
301
+ .filter(Boolean)
302
+ .map((line) => {
303
+ const match = line.match(/^(.+?)(?:\s*[,|:]\s*|\s{2,})(-?\d+(?:\.\d+)?)$/);
304
+ if (!match) {
305
+ return null;
306
+ }
307
+ return {
308
+ label: match[1].trim(),
309
+ value: Number(match[2])
310
+ };
311
+ })
312
+ .filter((item) => item && item.label && Number.isFinite(item.value));
313
+ }
314
+
315
+ function formatChartBlock(text = '', totalWidth = 88) {
316
+ const rows = parseChartRows(text);
317
+ if (!rows.length) {
318
+ return null;
319
+ }
320
+
321
+ const labelWidth = Math.min(
322
+ 18,
323
+ Math.max(6, ...rows.map((row) => measureText(row.label)))
324
+ );
325
+ const valueWidth = Math.max(6, ...rows.map((row) => String(row.value).length));
326
+ const barWidth = Math.max(10, totalWidth - labelWidth - valueWidth - 12);
327
+ const maxValue = Math.max(...rows.map((row) => Math.abs(row.value)), 1);
328
+
329
+ return rows.map((row) => {
330
+ const normalized = Math.abs(row.value) / maxValue;
331
+ const filled = Math.max(1, Math.round(normalized * barWidth));
332
+ return ` ${padDisplay(truncateText(row.label, labelWidth), labelWidth)} │ ${'█'.repeat(filled).padEnd(barWidth, ' ')} │ ${String(row.value).padStart(valueWidth, ' ')}`;
333
+ });
334
+ }
335
+
336
+ function normalizeKeyValueLabel(label = '') {
337
+ return String(label ?? '')
338
+ .replace(/^[-*•]\s+/, '')
339
+ .replace(/^\d+\.\s+/, '')
340
+ .trim();
341
+ }
342
+
343
+ function parseKeyValueRows(lines = []) {
344
+ const rows = lines
345
+ .map((line) => {
346
+ const match = line.match(/^[-*•]?\s*\d*\.?\s*([^::\n]{1,48})\s*(?:[::]|[-—]\s+)\s*(.+)$/);
347
+ if (!match) {
348
+ return null;
349
+ }
350
+ return {
351
+ label: normalizeKeyValueLabel(match[1]),
352
+ value: String(match[2] ?? '').trim()
353
+ };
354
+ })
355
+ .filter((row) => row?.label && row?.value);
356
+
357
+ return rows.length >= 2 ? rows : null;
358
+ }
359
+
360
+ function parseKeyValueBlock(text = '') {
361
+ const normalized = String(text ?? '').replace(/\r/g, '').trim();
362
+ if (!normalized) {
363
+ return null;
364
+ }
365
+
366
+ const lines = normalized
367
+ .split('\n')
368
+ .map((line) => line.trim())
369
+ .filter(Boolean);
370
+
371
+ if (lines.length >= 2 && lines.length <= 14) {
372
+ const directRows = parseKeyValueRows(lines);
373
+ if (directRows) {
374
+ return directRows;
375
+ }
376
+ }
377
+
378
+ if (!normalized.includes('\n')) {
379
+ const segments = normalized
380
+ .split(/[;;]/)
381
+ .map((item) => item.trim())
382
+ .filter(Boolean);
383
+
384
+ if (segments.length >= 3 && segments.length <= 8) {
385
+ return parseKeyValueRows(segments);
386
+ }
387
+ }
388
+
389
+ return null;
390
+ }
391
+
392
+ export function formatAgentStepRows({
393
+ step,
394
+ maxSteps,
395
+ command,
396
+ phase = 'plan',
397
+ agentLabel = '',
398
+ multiAgent = false
399
+ } = {}) {
400
+ const rows = [];
401
+
402
+ if (Number.isFinite(step) && Number.isFinite(maxSteps)) {
403
+ rows.push({
404
+ label: '步骤',
405
+ value: `${step}/${maxSteps}`
406
+ });
407
+ }
408
+
409
+ if (phase) {
410
+ rows.push({
411
+ label: '阶段',
412
+ value:
413
+ phase === 'plan'
414
+ ? '规划下一步'
415
+ : phase === 'execute'
416
+ ? '执行命令'
417
+ : phase === 'observe'
418
+ ? '读取结果'
419
+ : String(phase)
420
+ });
421
+ }
422
+
423
+ if (multiAgent) {
424
+ rows.push({
425
+ label: '协作',
426
+ value: '多 Agent'
427
+ });
428
+ }
429
+
430
+ if (agentLabel) {
431
+ rows.push({
432
+ label: '角色',
433
+ value: agentLabel
434
+ });
435
+ }
436
+
437
+ if (command?.name) {
438
+ rows.push({
439
+ label: '命令',
440
+ value: command.name
441
+ });
442
+ }
443
+
444
+ if (command?.args && Object.keys(command.args).length > 0) {
445
+ rows.push({
446
+ label: '参数',
447
+ value: JSON.stringify(command.args)
448
+ });
449
+ }
450
+
451
+ return rows;
452
+ }
453
+
454
+ export function summarizeAgentCommand(command = null) {
455
+ if (!command?.name) {
456
+ return '(无命令)';
457
+ }
458
+
459
+ const selector = command.args?.selector;
460
+ const text = command.args?.text;
461
+ const url = command.args?.url;
462
+ const path = command.args?.path;
463
+
464
+ if (url) {
465
+ return `${command.name} · ${truncateText(url, 42)}`;
466
+ }
467
+
468
+ if (selector && text) {
469
+ return `${command.name} · ${truncateText(selector, 20)} <= ${truncateText(text, 18)}`;
470
+ }
471
+
472
+ if (selector) {
473
+ return `${command.name} · ${truncateText(selector, 42)}`;
474
+ }
475
+
476
+ if (path) {
477
+ return `${command.name} · ${truncateText(path, 42)}`;
478
+ }
479
+
480
+ return command.name;
481
+ }
482
+
483
+ export function summarizeMessage(message = '', maxWidth = 48) {
484
+ const compact = String(message ?? '')
485
+ .replace(/\s+/g, ' ')
486
+ .trim();
487
+ if (!compact) {
488
+ return '(空)';
489
+ }
490
+ return truncateText(compact, maxWidth);
491
+ }
492
+
493
+ export function formatStatusBar(state = {}, totalWidth = 88) {
494
+ const segments = [
495
+ `Provider ${state.provider || '-'}`,
496
+ `Mode ${state.mode || '-'}`,
497
+ `Browser ${state.browser || '-'}`,
498
+ state.channel ? `Channel ${state.channel}` : '',
499
+ `Tasks ${state.tasks || '0'}`,
500
+ `Session ${state.session || '-'}`
501
+ ].filter(Boolean);
502
+
503
+ return truncateText(segments.join(' │ '), totalWidth - 4);
504
+ }
505
+
506
+ function isAnsiToken(token = '') {
507
+ return /^\x1B\[[0-9;]*m$/.test(token);
508
+ }
509
+
510
+ function clipStyledText(text, width) {
511
+ const value = String(text ?? '');
512
+ const plainWidth = stringWidth(stripAnsi(value));
513
+
514
+ if (plainWidth <= width) {
515
+ return value;
516
+ }
517
+
518
+ if (width <= 1) {
519
+ return '…';
520
+ }
521
+
522
+ const tokens = value.match(/\x1B\[[0-9;]*m|./gu) || [];
523
+ let output = '';
524
+ let visibleWidth = 0;
525
+
526
+ for (const token of tokens) {
527
+ if (isAnsiToken(token)) {
528
+ output += token;
529
+ continue;
530
+ }
531
+
532
+ const tokenWidth = stringWidth(token);
533
+ if (visibleWidth + tokenWidth > width - 1) {
534
+ break;
535
+ }
536
+
537
+ output += token;
538
+ visibleWidth += tokenWidth;
539
+ }
540
+
541
+ return `${output}\x1b[0m…`;
542
+ }
543
+
544
+ function padStyledText(text, width) {
545
+ const clipped = clipStyledText(text, width);
546
+ const padding = Math.max(0, width - stringWidth(stripAnsi(clipped)));
547
+ return `${clipped}${' '.repeat(padding)}`;
548
+ }
549
+
550
+ function wrapStyledText(text, width) {
551
+ const value = String(text ?? '');
552
+ if (width <= 0) {
553
+ return [value];
554
+ }
555
+
556
+ if (stringWidth(stripAnsi(value)) <= width) {
557
+ return [value];
558
+ }
559
+
560
+ const tokens = value.match(/\x1B\[[0-9;]*m|./gu) || [];
561
+ const lines = [];
562
+ let current = '';
563
+ let visibleWidth = 0;
564
+
565
+ for (const token of tokens) {
566
+ if (isAnsiToken(token)) {
567
+ current += token;
568
+ continue;
569
+ }
570
+
571
+ const tokenWidth = stringWidth(token);
572
+ if (visibleWidth > 0 && visibleWidth + tokenWidth > width) {
573
+ lines.push(current);
574
+ current = '';
575
+ visibleWidth = 0;
576
+ }
577
+
578
+ current += token;
579
+ visibleWidth += tokenWidth;
580
+ }
581
+
582
+ if (current || lines.length === 0) {
583
+ lines.push(current);
584
+ }
585
+
586
+ return lines;
587
+ }
588
+
589
+ function renderWorkspaceLogLine(line, width) {
590
+ const value = String(line ?? '');
591
+ const plain = stripAnsi(value);
592
+
593
+ if (!plain) {
594
+ return [''];
595
+ }
596
+
597
+ if (stringWidth(plain) <= width) {
598
+ return [value];
599
+ }
600
+
601
+ if (/^[\s]*[╭╰├┌└┐┘┬┴┼│]/u.test(plain)) {
602
+ return [clipStyledText(value, width)];
603
+ }
604
+
605
+ return wrapStyledText(value, width);
606
+ }
607
+
608
+ function buildWorkspaceInputLine({
609
+ prefix = 'YOU > ',
610
+ value = '',
611
+ placeholder = '',
612
+ cursor = 0,
613
+ width = 40
614
+ } = {}) {
615
+ const safeWidth = Math.max(8, width);
616
+ const safePrefix = String(prefix ?? '');
617
+ const textValue = String(value ?? '');
618
+ const prefixWidth = stringWidth(stripAnsi(safePrefix));
619
+ const available = Math.max(4, safeWidth - prefixWidth);
620
+
621
+ if (!textValue) {
622
+ const hint = placeholder ? color.gray(truncateText(placeholder, Math.max(1, available - 1))) : '';
623
+ return `${safePrefix}${hint}${color.cyan('\x1b[7m \x1b[0m')}`;
624
+ }
625
+
626
+ const chars = Array.from(textValue);
627
+ const safeCursor = Math.max(0, Math.min(Number(cursor) || 0, chars.length));
628
+ const measureSegment = (segment) => stringWidth(segment.join(''));
629
+ let start = 0;
630
+ let end = chars.length;
631
+
632
+ while (measureSegment(chars.slice(start, end)) > available - 1 && start < safeCursor) {
633
+ start++;
634
+ }
635
+ while (measureSegment(chars.slice(start, end)) > available - 1 && end > safeCursor) {
636
+ end--;
637
+ }
638
+
639
+ if (start > 0) {
640
+ while (start > 0 && measureSegment(['…', ...chars.slice(start - 1, end)]) <= available - 1) {
641
+ start--;
642
+ }
643
+ }
644
+ if (end < chars.length) {
645
+ while (end < chars.length && measureSegment([...chars.slice(start, end + 1), '…']) <= available - 1) {
646
+ end++;
647
+ }
648
+ }
649
+
650
+ const visibleChars = chars.slice(start, end);
651
+ const leftEllipsis = start > 0 ? '…' : '';
652
+ const rightEllipsis = end < chars.length ? '…' : '';
653
+ const cursorInside = safeCursor >= start && safeCursor <= end;
654
+ const cursorOffset = cursorInside ? safeCursor - start : visibleChars.length;
655
+ const beforeCursor = visibleChars.slice(0, cursorOffset).join('');
656
+ const cursorChar = visibleChars[cursorOffset] ?? ' ';
657
+ const afterCursor = visibleChars.slice(cursorOffset + (cursorOffset < visibleChars.length ? 1 : 0)).join('');
658
+
659
+ return `${safePrefix}${leftEllipsis}${beforeCursor}\x1b[7m${cursorChar}\x1b[0m${afterCursor}${rightEllipsis}`;
660
+ }
661
+
662
+ function formatInlineMarkdownText(text = '') {
663
+ return String(text ?? '')
664
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1')
665
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `${color.cyan(label)} ${color.gray(`<${href}>`)}`)
666
+ .replace(/`([^`]+)`/g, (_, value) => color.yellowBright(value))
667
+ .replace(/\*\*([^*]+)\*\*/g, (_, value) => color.whiteBright(value))
668
+ .replace(/__([^_]+)__/g, (_, value) => color.whiteBright(value))
669
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, value) => color.white(value))
670
+ .replace(/(?<!_)_([^_]+)_(?!_)/g, (_, value) => color.white(value))
671
+ .replace(/~~([^~]+)~~/g, (_, value) => color.gray(value));
672
+ }
673
+
674
+ function shouldRenderTextAsMarkdown(text = '') {
675
+ const value = String(text ?? '');
676
+ if (!value.trim()) {
677
+ return false;
678
+ }
679
+
680
+ return (
681
+ /(^|\n)```/.test(value) ||
682
+ /(^|\n)#{1,6}\s+/.test(value) ||
683
+ /(^|\n)>\s+/.test(value) ||
684
+ /(^|\n)[*-]\s+/.test(value) ||
685
+ /(^|\n)\d+\.\s+/.test(value) ||
686
+ /(^|\n)\|.+\|/.test(value) ||
687
+ /(^|\n)-{3,}($|\n)/.test(value) ||
688
+ /`[^`\n]+`/.test(value) ||
689
+ /\*\*[^*\n]+\*\*/.test(value) ||
690
+ /__[^_\n]+__/.test(value) ||
691
+ /~~[^~\n]+~~/.test(value) ||
692
+ /\[[^\]]+\]\([^)]+\)/.test(value) ||
693
+ /(?<!\*)\*[^*\n]+\*(?!\*)/.test(value) ||
694
+ /(?<!_)_[^_\n]+_(?!_)/.test(value)
695
+ );
696
+ }
697
+
698
+ function buildPanelLines(title, lines = [], width) {
699
+ const safeWidth = Math.max(24, width);
700
+ const innerWidth = safeWidth - 4;
701
+ const top = `╭${'─'.repeat(safeWidth - 2)}╮`;
702
+ const bottom = `╰${'─'.repeat(safeWidth - 2)}╯`;
703
+ const titleLine = ` ${truncateText(title, innerWidth - 1)}`;
704
+ const body = [titleLine, ...lines]
705
+ .flatMap((line) => wrapText(String(line ?? ''), innerWidth).map((part) => ` ${formatInlineMarkdownText(part)}`))
706
+ .map((line) => `│${line}${' '.repeat(Math.max(0, innerWidth - measureText(line)))} │`);
707
+
708
+ return [top, ...body, bottom];
709
+ }
710
+
711
+ function buildKeyValueTableLines(title, rows = [], width) {
712
+ const safeWidth = Math.max(28, width);
713
+ const top = `╭${'─'.repeat(safeWidth - 2)}╮`;
714
+ const divider = `├${'─'.repeat(safeWidth - 2)}┤`;
715
+ const bottom = `╰${'─'.repeat(safeWidth - 2)}╯`;
716
+ const titleLine = ` ${truncateText(title, safeWidth - 4)}`;
717
+ const titleRow = `│${titleLine}${' '.repeat(Math.max(0, safeWidth - 3 - measureText(titleLine)))}│`;
718
+ const formatted = formatKeyValueRows(rows, safeWidth);
719
+ const body = formatted.lines.map((line) => `│${line}│`);
720
+ return [top, titleRow, divider, ...body, bottom];
721
+ }
722
+
723
+ function buildCompactSectionLines(title, {
724
+ rows = [],
725
+ lines = []
726
+ } = {}, width) {
727
+ const safeWidth = Math.max(18, width);
728
+ const output = [];
729
+ const header = color.greenBright(truncateText(title, safeWidth));
730
+ output.push(header);
731
+ output.push(color.gray('─'.repeat(Math.max(6, Math.min(safeWidth, 14)))));
732
+
733
+ if (Array.isArray(rows) && rows.length > 0) {
734
+ for (const row of rows) {
735
+ if (!row?.label) {
736
+ continue;
737
+ }
738
+ const label = color.gray(truncateText(`${row.label}`, safeWidth));
739
+ const wrapped = wrapText(String(row.value ?? ''), Math.max(10, safeWidth - 2));
740
+ if (wrapped.length === 0) {
741
+ output.push(`${label}`);
742
+ continue;
743
+ }
744
+ output.push(`${label}`);
745
+ wrapped.forEach((line) => {
746
+ output.push(` ${formatInlineMarkdownText(line.trimEnd())}`);
747
+ });
748
+ }
749
+ } else if (Array.isArray(lines) && lines.length > 0) {
750
+ for (const line of lines) {
751
+ const wrapped = wrapText(String(line ?? ''), Math.max(10, safeWidth));
752
+ output.push(...wrapped.map((item) => formatInlineMarkdownText(item.trimEnd())));
753
+ }
754
+ }
755
+
756
+ return output;
757
+ }
758
+
759
+ function buildLightSectionLines(title, lines = [], width = 72) {
760
+ const safeWidth = Math.max(24, width);
761
+ const output = [
762
+ color.greenBright(title),
763
+ color.gray('─'.repeat(Math.max(8, Math.min(safeWidth, Math.max(12, measureText(title) + 2)))))
764
+ ];
765
+
766
+ for (const rawLine of lines) {
767
+ const wrapped = wrapText(String(rawLine ?? ''), Math.max(20, safeWidth - 2));
768
+ output.push(...wrapped.map((line) => ` ${formatInlineMarkdownText(line)}`));
769
+ }
770
+
771
+ return output;
772
+ }
773
+
774
+ function buildLightKeyValueLines(title, rows = [], width = 72) {
775
+ const safeRows = Array.isArray(rows) ? rows.filter((row) => row && row.label) : [];
776
+ const safeWidth = Math.max(28, width);
777
+ const labelWidth = Math.min(
778
+ 14,
779
+ Math.max(6, ...safeRows.map((row) => measureText(row.label)))
780
+ );
781
+ const inlineValueWidth = Math.max(16, safeWidth - labelWidth - 8);
782
+ const preferStackedByWidth = safeWidth < 76 || safeRows.length >= 5;
783
+ const useStackedLayout = safeRows.some((row) => {
784
+ const value = String(row.value ?? '');
785
+ return (
786
+ preferStackedByWidth ||
787
+ value.includes('\n') ||
788
+ /^https?:\/\//i.test(value) ||
789
+ measureText(value) > Math.max(24, inlineValueWidth + 4)
790
+ );
791
+ });
792
+ const output = [
793
+ color.greenBright(title),
794
+ color.gray('─'.repeat(Math.max(8, Math.min(safeWidth, Math.max(12, measureText(title) + 2)))))
795
+ ];
796
+
797
+ if (useStackedLayout) {
798
+ safeRows.forEach((row, rowIndex) => {
799
+ const label = color.gray(truncateText(row.label, safeWidth - 2));
800
+ output.push(` ${label}`);
801
+
802
+ const wrapped = wrapText(
803
+ String(row.value ?? ''),
804
+ Math.max(20, safeWidth - 4)
805
+ );
806
+
807
+ wrapped.forEach((line) => {
808
+ output.push(` ${formatInlineMarkdownText(line)}`);
809
+ });
810
+
811
+ if (rowIndex < safeRows.length - 1) {
812
+ output.push('');
813
+ }
814
+ });
815
+
816
+ return output;
817
+ }
818
+
819
+ for (const row of safeRows) {
820
+ const wrapped = wrapText(String(row.value ?? ''), inlineValueWidth);
821
+ wrapped.forEach((line, index) => {
822
+ const label = index === 0
823
+ ? color.gray(padDisplay(truncateText(row.label, labelWidth), labelWidth))
824
+ : ' '.repeat(labelWidth);
825
+ const separator = index === 0 ? color.gray(' │ ') : ' ';
826
+ output.push(` ${label}${separator}${formatInlineMarkdownText(line)}`);
827
+ });
828
+ }
829
+
830
+ return output;
831
+ }
832
+
833
+ function wrapCodeLine(text, width) {
834
+ const value = String(text ?? '').replace(/\r/g, '');
835
+ if (width <= 0) {
836
+ return [value];
837
+ }
838
+
839
+ if (measureText(value) <= width) {
840
+ return [value];
841
+ }
842
+
843
+ const lines = [];
844
+ let rest = value;
845
+
846
+ while (measureText(rest) > width) {
847
+ let slice = '';
848
+ for (const char of rest) {
849
+ if (measureText(slice + char) > width) {
850
+ break;
851
+ }
852
+ slice += char;
853
+ }
854
+ lines.push(slice);
855
+ rest = rest.slice(slice.length);
856
+ }
857
+
858
+ lines.push(rest);
859
+ return lines;
860
+ }
861
+
862
+ function buildCodeBlockLines(lines = [], width = 72, language = '') {
863
+ const safeWidth = Math.max(24, width);
864
+ const title = language ? `Code · ${language}` : 'Code';
865
+ const output = [
866
+ color.yellowBright(title),
867
+ color.gray('─'.repeat(Math.max(8, Math.min(safeWidth, Math.max(12, measureText(title) + 2)))))
868
+ ];
869
+
870
+ const contentWidth = Math.max(16, safeWidth - 6);
871
+ const safeLines = Array.isArray(lines) && lines.length > 0 ? lines : [''];
872
+
873
+ for (const rawLine of safeLines) {
874
+ const wrapped = wrapCodeLine(rawLine, contentWidth);
875
+ if (!wrapped.length) {
876
+ output.push(`${color.gray(' ▏')}`);
877
+ continue;
878
+ }
879
+
880
+ wrapped.forEach((line) => {
881
+ output.push(`${color.gray(' ▏')} ${color.white(line)}`);
882
+ });
883
+ }
884
+
885
+ return output;
886
+ }
887
+
888
+ function buildNoticeLines(kind = 'info', message = '', width = 72) {
889
+ const safeWidth = Math.max(24, width);
890
+ const normalized = String(message ?? '').replace(/\r/g, '').trim();
891
+ const theme = {
892
+ info: {
893
+ badge: color.cyan('i'),
894
+ text: color.cyan
895
+ },
896
+ muted: {
897
+ badge: color.gray('·'),
898
+ text: color.gray
899
+ },
900
+ success: {
901
+ badge: color.greenBright('✓'),
902
+ text: color.greenBright
903
+ },
904
+ warning: {
905
+ badge: color.yellowBright('!'),
906
+ text: color.yellowBright
907
+ },
908
+ error: {
909
+ badge: color.redBright('×'),
910
+ text: color.redBright
911
+ }
912
+ }[kind] || {
913
+ badge: color.cyan('i'),
914
+ text: color.cyan
915
+ };
916
+
917
+ const prefix = ` ${theme.badge} `;
918
+ const continuation = ' ';
919
+ const contentWidth = Math.max(18, safeWidth - measureText(prefix) - 1);
920
+ const wrapped = wrapText(normalized || '', contentWidth);
921
+
922
+ return wrapped.map((line, index) => {
923
+ const currentPrefix = index === 0 ? prefix : continuation;
924
+ return `${currentPrefix}${theme.text(line)}`;
925
+ });
926
+ }
927
+
928
+ function buildQuoteBlockLines(lines = [], width = 72) {
929
+ const safeWidth = Math.max(24, width);
930
+ const contentWidth = Math.max(16, safeWidth - 6);
931
+ const safeLines = Array.isArray(lines) ? lines.filter(Boolean) : [];
932
+ const output = [];
933
+
934
+ for (const rawLine of safeLines) {
935
+ const plain = String(rawLine ?? '').replace(/^>\s*/, '').trim();
936
+ if (!plain) {
937
+ output.push(color.gray(' ▎'));
938
+ continue;
939
+ }
940
+
941
+ const wrapped = wrapText(plain, contentWidth);
942
+ wrapped.forEach((line, index) => {
943
+ const prefix = index === 0 ? ' ▎ ' : ' ';
944
+ const painter =
945
+ /^(注意|提示|说明|建议|结论)[::]?/i.test(plain)
946
+ ? color.yellowBright
947
+ : color.gray;
948
+ output.push(`${color.gray(prefix)}${painter(formatInlineMarkdownText(line))}`);
949
+ });
950
+ }
951
+
952
+ return output;
953
+ }
954
+
955
+ function detectSemanticLead(text = '') {
956
+ const value = String(text ?? '').trim();
957
+ const match = value.match(/^(结论|建议|注意|提示|说明|风险|原因|观察|下一步|重点)[::]\s*(.+)$/);
958
+ if (!match) {
959
+ return null;
960
+ }
961
+
962
+ const label = match[1];
963
+ const content = match[2];
964
+ const kind =
965
+ label === '注意' || label === '风险'
966
+ ? 'warning'
967
+ : label === '结论' || label === '重点'
968
+ ? 'success'
969
+ : 'info';
970
+
971
+ return {
972
+ label,
973
+ content,
974
+ kind
975
+ };
976
+ }
977
+
978
+ function buildListBlockLines(lines = [], width = 72, ordered = false) {
979
+ const safeWidth = Math.max(24, width);
980
+ const output = [];
981
+
982
+ lines.forEach((rawLine, itemIndex) => {
983
+ const trimmed = String(rawLine ?? '').trim();
984
+ const match = ordered
985
+ ? trimmed.match(/^(\d+\.)\s+(.*)$/)
986
+ : trimmed.match(/^[-*]\s+(.*)$/);
987
+ const bullet = ordered
988
+ ? `${match?.[1] || `${itemIndex + 1}.`} `
989
+ : '• ';
990
+ const content = ordered
991
+ ? match?.[2] || trimmed
992
+ : match?.[1] || trimmed.replace(/^[-*]\s+/, '');
993
+ const wrapped = wrapText(content, Math.max(20, safeWidth - 4));
994
+
995
+ wrapped.forEach((line, lineIndex) => {
996
+ const prefix = lineIndex === 0 ? ` ${bullet}` : ` ${' '.repeat(bullet.length)}`;
997
+ output.push(`${prefix}${formatInlineMarkdownText(line)}`);
998
+ });
999
+
1000
+ if (itemIndex < lines.length - 1) {
1001
+ output.push('');
1002
+ }
1003
+ });
1004
+
1005
+ return output;
1006
+ }
1007
+
1008
+ function buildMarkdownLines(text = '', width = 72) {
1009
+ const normalized = String(text ?? '').replace(/\r/g, '');
1010
+ const chartBlock = formatChartBlock(normalized, width);
1011
+ if (chartBlock) {
1012
+ return chartBlock;
1013
+ }
1014
+
1015
+ const keyValueBlock = parseKeyValueBlock(normalized);
1016
+ if (keyValueBlock) {
1017
+ return buildLightKeyValueLines('结果', keyValueBlock, width);
1018
+ }
1019
+
1020
+ const lines = normalized.split('\n');
1021
+ const output = [];
1022
+ let index = 0;
1023
+
1024
+ while (index < lines.length) {
1025
+ const line = lines[index];
1026
+ const trimmed = line.trim();
1027
+
1028
+ if (!trimmed) {
1029
+ output.push('');
1030
+ index++;
1031
+ continue;
1032
+ }
1033
+
1034
+ if (isMarkdownTableLine(trimmed)) {
1035
+ const tableLines = [];
1036
+ while (index < lines.length && isMarkdownTableLine(lines[index].trim())) {
1037
+ tableLines.push(lines[index].trim());
1038
+ index++;
1039
+ }
1040
+ output.push(...formatMarkdownTable(tableLines, width));
1041
+ output.push('');
1042
+ continue;
1043
+ }
1044
+
1045
+ if (/^```/.test(trimmed)) {
1046
+ const language = trimmed.replace(/^```/, '').trim();
1047
+ const codeLines = [];
1048
+ index++;
1049
+ while (index < lines.length && !/^```/.test(lines[index].trim())) {
1050
+ codeLines.push(lines[index]);
1051
+ index++;
1052
+ }
1053
+ if (index < lines.length && /^```/.test(lines[index].trim())) {
1054
+ index++;
1055
+ }
1056
+ output.push(...buildCodeBlockLines(codeLines, width, language));
1057
+ output.push('');
1058
+ continue;
1059
+ }
1060
+
1061
+ if (/^-{3,}$/.test(trimmed)) {
1062
+ output.push(color.gray('─'.repeat(Math.max(8, Math.min(width, 24)))));
1063
+ output.push('');
1064
+ index++;
1065
+ continue;
1066
+ }
1067
+
1068
+ if (/^>\s+/.test(trimmed)) {
1069
+ const quoteLines = [];
1070
+ while (index < lines.length && /^>\s+/.test(lines[index].trim())) {
1071
+ quoteLines.push(lines[index].trim());
1072
+ index++;
1073
+ }
1074
+ output.push(...buildQuoteBlockLines(quoteLines, width));
1075
+ output.push('');
1076
+ continue;
1077
+ }
1078
+
1079
+ if (/^[-*]\s+/.test(trimmed)) {
1080
+ const listLines = [];
1081
+ while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
1082
+ listLines.push(lines[index].trim());
1083
+ index++;
1084
+ }
1085
+ output.push(...buildListBlockLines(listLines, width, false));
1086
+ output.push('');
1087
+ continue;
1088
+ }
1089
+
1090
+ if (/^\d+\.\s+/.test(trimmed)) {
1091
+ const listLines = [];
1092
+ while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
1093
+ listLines.push(lines[index].trim());
1094
+ index++;
1095
+ }
1096
+ output.push(...buildListBlockLines(listLines, width, true));
1097
+ output.push('');
1098
+ continue;
1099
+ }
1100
+
1101
+ if (/^#{1,6}\s+/.test(trimmed)) {
1102
+ output.push(color.greenBright(formatInlineMarkdownText(trimmed.replace(/^#{1,6}\s+/, ''))));
1103
+ output.push('');
1104
+ index++;
1105
+ continue;
1106
+ }
1107
+
1108
+ const semanticLead = detectSemanticLead(trimmed);
1109
+ if (semanticLead) {
1110
+ output.push(...buildNoticeLines(semanticLead.kind, `${semanticLead.label}: ${semanticLead.content}`, width));
1111
+ output.push('');
1112
+ index++;
1113
+ continue;
1114
+ }
1115
+
1116
+ const paragraph = [];
1117
+ while (
1118
+ index < lines.length &&
1119
+ lines[index].trim() &&
1120
+ !isMarkdownTableLine(lines[index].trim()) &&
1121
+ !/^```/.test(lines[index].trim()) &&
1122
+ !/^-{3,}$/.test(lines[index].trim()) &&
1123
+ !/^>\s+/.test(lines[index].trim()) &&
1124
+ !/^[-*]\s+/.test(lines[index].trim()) &&
1125
+ !/^\d+\.\s+/.test(lines[index].trim()) &&
1126
+ !/^#{1,6}\s+/.test(lines[index].trim()) &&
1127
+ !detectSemanticLead(lines[index].trim())
1128
+ ) {
1129
+ paragraph.push(lines[index].trim());
1130
+ index++;
1131
+ }
1132
+ output.push(...wrapText(paragraph.join(' '), Math.max(20, width)).map((line) => formatInlineMarkdownText(line)));
1133
+ output.push('');
1134
+ }
1135
+
1136
+ while (output.length > 0 && !output.at(-1)) {
1137
+ output.pop();
1138
+ }
1139
+
1140
+ return output;
1141
+ }
1142
+
1143
+ class UI {
1144
+ constructor() {
1145
+ this.spinner = null;
1146
+ this.stdoutLineOpen = false;
1147
+ this.lastTimelineSignature = '';
1148
+ this.lastStatusSignature = '';
1149
+ this.pinnedHeaderActive = false;
1150
+ this.pinnedHeaderHeight = 0;
1151
+ this.lastPinnedHeaderSignature = '';
1152
+ this.workspace = {
1153
+ active: false,
1154
+ title: 'XShat Workspace',
1155
+ status: '',
1156
+ hints: [],
1157
+ leftSections: [],
1158
+ rightSections: [],
1159
+ logLines: [],
1160
+ footer: '',
1161
+ inputLabel: 'YOU > ',
1162
+ inputPlaceholder: '输入消息后回车发送',
1163
+ inputValue: '',
1164
+ inputCursor: 0
1165
+ };
1166
+ }
1167
+
1168
+ getTime() {
1169
+ return new Date().toLocaleTimeString('zh-CN', { hour12: false });
1170
+ }
1171
+
1172
+ clear() {
1173
+ this.clearPinnedHeader();
1174
+ if (this.workspace.active) {
1175
+ this.workspace.logLines = [];
1176
+ this.renderWorkspace();
1177
+ return;
1178
+ }
1179
+ console.clear();
1180
+ }
1181
+
1182
+ getWidth() {
1183
+ return Math.min(Math.max(process.stdout.columns || 88, 60), 120);
1184
+ }
1185
+
1186
+ getWorkspaceDimensions() {
1187
+ const totalWidth = Math.max(96, process.stdout.columns || 120);
1188
+
1189
+ if (totalWidth >= 180) {
1190
+ return {
1191
+ totalWidth,
1192
+ leftWidth: 24,
1193
+ rightWidth: 26,
1194
+ showLeft: true,
1195
+ showRight: true,
1196
+ gapWidth: 2
1197
+ };
1198
+ }
1199
+
1200
+ if (totalWidth >= 150) {
1201
+ return {
1202
+ totalWidth,
1203
+ leftWidth: 22,
1204
+ rightWidth: 24,
1205
+ showLeft: true,
1206
+ showRight: true,
1207
+ gapWidth: 2
1208
+ };
1209
+ }
1210
+
1211
+ if (totalWidth >= 130) {
1212
+ return {
1213
+ totalWidth,
1214
+ leftWidth: 20,
1215
+ rightWidth: 18,
1216
+ showLeft: true,
1217
+ showRight: true,
1218
+ gapWidth: 2
1219
+ };
1220
+ }
1221
+
1222
+ if (totalWidth >= 112) {
1223
+ return {
1224
+ totalWidth,
1225
+ leftWidth: 18,
1226
+ rightWidth: 0,
1227
+ showLeft: true,
1228
+ showRight: false,
1229
+ gapWidth: 2
1230
+ };
1231
+ }
1232
+
1233
+ return {
1234
+ totalWidth,
1235
+ leftWidth: 0,
1236
+ rightWidth: 0,
1237
+ showLeft: false,
1238
+ showRight: false,
1239
+ gapWidth: 1
1240
+ };
1241
+ }
1242
+
1243
+ colorize(color, text) {
1244
+ const formatter = COLORS[color] || COLORS.reset;
1245
+ return formatter(String(text ?? ''));
1246
+ }
1247
+
1248
+ markLineStateFromText(text) {
1249
+ const value = String(text ?? '');
1250
+ if (!value) {
1251
+ return;
1252
+ }
1253
+ this.stdoutLineOpen = !value.endsWith('\n');
1254
+ }
1255
+
1256
+ ensureTrailingNewline() {
1257
+ if (this.workspace.active) {
1258
+ return;
1259
+ }
1260
+
1261
+ if (this.stdoutLineOpen) {
1262
+ process.stdout.write('\n');
1263
+ this.stdoutLineOpen = false;
1264
+ }
1265
+ }
1266
+
1267
+ printPrompt() {
1268
+ if (this.workspace.active) {
1269
+ const totalRows = Math.max(20, process.stdout.rows || 30);
1270
+ const row = Math.max(3, totalRows);
1271
+ const prefix = `${this.colorize('time', `[${this.getTime()}]`)} ${this.colorize('user', this.workspace.inputLabel || 'YOU > ')}`;
1272
+ return `\x1b[2K\x1b[${row};2H${prefix}`;
1273
+ }
1274
+ const timeStr = this.colorize('time', `[${this.getTime()}]`);
1275
+ const userStr = this.colorize('user', 'YOU > ');
1276
+ return `${timeStr} ${userStr}`;
1277
+ }
1278
+
1279
+ printUserMessage(message) {
1280
+ const value = String(message ?? '');
1281
+
1282
+ if (this.workspace.active) {
1283
+ this.appendWorkspaceLines(buildPanelLines('YOU', value.split('\n'), this.getWorkspaceMainWidth()));
1284
+ return;
1285
+ }
1286
+
1287
+ if (value.includes('\n')) {
1288
+ this.printNewline();
1289
+ this.printPanel('YOU', value.split('\n'));
1290
+ return;
1291
+ }
1292
+
1293
+ process.stdout.moveCursor(0, -1);
1294
+ process.stdout.clearLine(0);
1295
+ const timeStr = this.colorize('time', `[${this.getTime()}]`);
1296
+ const userStr = this.colorize('user', `YOU > ${value}`);
1297
+ process.stdout.write(`${timeStr} ${userStr}\n`);
1298
+ this.stdoutLineOpen = false;
1299
+ }
1300
+
1301
+ printAIPrefix() {
1302
+ if (this.workspace.active) {
1303
+ return;
1304
+ }
1305
+ const timeStr = this.colorize('time', `[${this.getTime()}]`);
1306
+ const aiStr = this.colorize('ai', 'AI > ');
1307
+ process.stdout.write(`${timeStr} ${aiStr}`);
1308
+ this.stdoutLineOpen = true;
1309
+ }
1310
+
1311
+ startSpinner() {
1312
+ if (this.workspace.active) {
1313
+ this.workspace.footer = 'AI 正在思考中...';
1314
+ this.renderWorkspace();
1315
+ return;
1316
+ }
1317
+ this.stopSpinner();
1318
+ this.spinner = ora({
1319
+ text: `${this.colorize('time', `[${this.getTime()}]`)} ${this.colorize('ai', 'AI >')} ${this.colorize('spin', '思考中...')}`,
1320
+ stream: process.stdout,
1321
+ discardStdin: false
1322
+ }).start();
1323
+ }
1324
+
1325
+ stopSpinner() {
1326
+ if (this.workspace.active) {
1327
+ this.workspace.footer = '';
1328
+ this.renderWorkspace();
1329
+ return;
1330
+ }
1331
+ if (this.spinner) {
1332
+ this.spinner.stop();
1333
+ this.spinner = null;
1334
+ process.stdout.cursorTo(0);
1335
+ process.stdout.clearLine(0);
1336
+ this.stdoutLineOpen = false;
1337
+ }
1338
+ }
1339
+
1340
+ printText(text) {
1341
+ if (this.workspace.active) {
1342
+ this.appendWorkspaceLines([String(text ?? '')]);
1343
+ return;
1344
+ }
1345
+ const rendered = this.colorize('text', text);
1346
+ process.stdout.write(rendered);
1347
+ this.markLineStateFromText(rendered);
1348
+ }
1349
+
1350
+ printInlineMarkdown(text, options = {}) {
1351
+ const streaming = Boolean(options.streaming);
1352
+ const normalized = String(text ?? '')
1353
+ .replace(/\r/g, '')
1354
+ .replace(/^#{1,6}\s+/gm, '')
1355
+ .replace(/^>\s+/gm, '')
1356
+ .replace(/^[-*]\s+/gm, '• ')
1357
+ .replace(/^(\d+)\.\s+/gm, '$1. ')
1358
+ .replace(/\n+/g, ' ');
1359
+ const value = streaming
1360
+ ? normalized
1361
+ .replace(/[`*_~]+/g, '')
1362
+ : formatInlineMarkdownText(normalized);
1363
+ this.printText(value);
1364
+ }
1365
+
1366
+ writeWrappedText(text, {
1367
+ indent = ' ',
1368
+ bullet = '',
1369
+ color = 'text'
1370
+ } = {}) {
1371
+ const width = this.getWidth() - stripAnsi(indent).length - stripAnsi(bullet).length;
1372
+ const lines = wrapText(text, Math.max(20, width));
1373
+
1374
+ lines.forEach((line, index) => {
1375
+ const prefix = index === 0 ? `${indent}${bullet}` : `${indent}${' '.repeat(stripAnsi(bullet).length)}`;
1376
+ process.stdout.write(
1377
+ `${this.colorize(color, `${prefix}${line}`)}\n`
1378
+ );
1379
+ });
1380
+ }
1381
+
1382
+ printNewline() {
1383
+ if (this.workspace.active) {
1384
+ this.appendWorkspaceLines(['']);
1385
+ return;
1386
+ }
1387
+ process.stdout.write('\n');
1388
+ this.stdoutLineOpen = false;
1389
+ }
1390
+
1391
+ printError(message) {
1392
+ if (this.workspace.active) {
1393
+ this.appendWorkspaceLines([`ERROR: ${message}`]);
1394
+ return;
1395
+ }
1396
+ const lines = buildNoticeLines('error', message, this.getWidth() - 2);
1397
+ process.stdout.write(`\r${lines.join('\n')}\n`);
1398
+ this.stdoutLineOpen = false;
1399
+ }
1400
+
1401
+ printWarning(message) {
1402
+ if (this.workspace.active) {
1403
+ this.appendWorkspaceLines([`⚠ ${message}`]);
1404
+ return;
1405
+ }
1406
+ const lines = buildNoticeLines('warning', message, this.getWidth() - 2);
1407
+ process.stdout.write(`\r${lines.join('\n')}\n`);
1408
+ this.stdoutLineOpen = false;
1409
+ }
1410
+
1411
+ printTitle(title) {
1412
+ if (this.workspace.active) {
1413
+ this.appendWorkspaceLines([`== ${title} ==`]);
1414
+ return;
1415
+ }
1416
+ const width = this.getWidth();
1417
+ const plain = truncateText(title, width - 6);
1418
+ const rule = '─'.repeat(Math.max(8, width - stripAnsi(plain).length - 4));
1419
+ process.stdout.write(
1420
+ `\n${this.colorize('ai', `╭─ ${plain} `)}${this.colorize('border', rule)}\n`
1421
+ );
1422
+ this.stdoutLineOpen = false;
1423
+ }
1424
+
1425
+ printSeparator() {
1426
+ if (this.workspace.active) {
1427
+ this.appendWorkspaceLines(['─'.repeat(this.getWorkspaceMainWidth())]);
1428
+ return;
1429
+ }
1430
+ process.stdout.write(`${this.colorize('border', '─'.repeat(this.getWidth()))}\n`);
1431
+ this.stdoutLineOpen = false;
1432
+ }
1433
+
1434
+ printMenuItem(key, label, hint = '') {
1435
+ const keyStr = this.colorize('spin', `[${key}]`);
1436
+ const labelStr = this.colorize('text', label);
1437
+ const hintStr = hint ? ` ${this.colorize('time', hint)}` : '';
1438
+ process.stdout.write(` ${keyStr} ${labelStr}${hintStr}\n`);
1439
+ this.stdoutLineOpen = false;
1440
+ }
1441
+
1442
+ printInfo(message) {
1443
+ if (this.workspace.active) {
1444
+ this.appendWorkspaceLines([String(message ?? '')]);
1445
+ return;
1446
+ }
1447
+ const lines = buildNoticeLines('info', String(message ?? ''), this.getWidth() - 2);
1448
+ process.stdout.write(`${lines.join('\n')}\n`);
1449
+ this.stdoutLineOpen = false;
1450
+ }
1451
+
1452
+ printMuted(message) {
1453
+ if (this.workspace.active) {
1454
+ this.appendWorkspaceLines([String(message ?? '')]);
1455
+ return;
1456
+ }
1457
+ const lines = buildNoticeLines('muted', String(message ?? ''), this.getWidth() - 2);
1458
+ process.stdout.write(`${lines.join('\n')}\n`);
1459
+ this.stdoutLineOpen = false;
1460
+ }
1461
+
1462
+ printSuccess(message) {
1463
+ if (this.workspace.active) {
1464
+ this.appendWorkspaceLines([`✓ ${message}`]);
1465
+ return;
1466
+ }
1467
+ const lines = buildNoticeLines('success', String(message ?? ''), this.getWidth() - 2);
1468
+ process.stdout.write(`${lines.join('\n')}\n`);
1469
+ this.stdoutLineOpen = false;
1470
+ }
1471
+
1472
+ printPromptLine() {
1473
+ process.stdout.write(this.colorize('spin', '❯ '));
1474
+ this.stdoutLineOpen = true;
1475
+ }
1476
+
1477
+ supportsPinnedHeader() {
1478
+ return Boolean(
1479
+ process.stdout.isTTY &&
1480
+ Number.isFinite(process.stdout.rows) &&
1481
+ process.stdout.rows >= 8
1482
+ );
1483
+ }
1484
+
1485
+ moveCursorTo(row = 1, column = 1) {
1486
+ process.stdout.write(`\x1b[${Math.max(1, row)};${Math.max(1, column)}H`);
1487
+ }
1488
+
1489
+ resetScrollRegion() {
1490
+ if (!process.stdout.isTTY) {
1491
+ return;
1492
+ }
1493
+ process.stdout.write('\x1b[r');
1494
+ }
1495
+
1496
+ clearPinnedHeader() {
1497
+ if (!this.pinnedHeaderActive && !this.lastPinnedHeaderSignature) {
1498
+ return;
1499
+ }
1500
+
1501
+ this.resetScrollRegion();
1502
+ this.pinnedHeaderActive = false;
1503
+ this.pinnedHeaderHeight = 0;
1504
+ this.lastPinnedHeaderSignature = '';
1505
+ }
1506
+
1507
+ renderPinnedHeader(lines = []) {
1508
+ if (!this.supportsPinnedHeader() || !Array.isArray(lines) || lines.length === 0) {
1509
+ return false;
1510
+ }
1511
+
1512
+ const terminalRows = process.stdout.rows || 0;
1513
+ const normalized = lines
1514
+ .map((line) => truncateText(String(line ?? '').replace(/\r?\n/g, ' '), this.getWidth() - 2))
1515
+ .filter(Boolean);
1516
+
1517
+ if (!normalized.length || terminalRows <= normalized.length + 1) {
1518
+ return false;
1519
+ }
1520
+
1521
+ const signature = JSON.stringify(normalized);
1522
+ const bodyTop = normalized.length + 1;
1523
+
1524
+ if (this.pinnedHeaderActive && signature === this.lastPinnedHeaderSignature) {
1525
+ return true;
1526
+ }
1527
+
1528
+ this.stopSpinner();
1529
+ process.stdout.write('\x1b7');
1530
+ this.resetScrollRegion();
1531
+
1532
+ const rowsToRender = Math.max(this.pinnedHeaderHeight, normalized.length);
1533
+ for (let index = 0; index < rowsToRender; index++) {
1534
+ this.moveCursorTo(index + 1, 1);
1535
+ process.stdout.clearLine(0);
1536
+ if (index < normalized.length) {
1537
+ process.stdout.write(this.colorize('time', ` ${normalized[index]}`));
1538
+ }
1539
+ }
1540
+
1541
+ process.stdout.write(`\x1b[${bodyTop};${terminalRows}r`);
1542
+ process.stdout.write('\x1b8');
1543
+
1544
+ this.pinnedHeaderActive = true;
1545
+ this.pinnedHeaderHeight = normalized.length;
1546
+ this.lastPinnedHeaderSignature = signature;
1547
+ return true;
1548
+ }
1549
+
1550
+ renderChatChrome(state = {}, hints = []) {
1551
+ const lines = [formatStatusBar(state, this.getWidth())];
1552
+ if (Array.isArray(hints) && hints.length > 0) {
1553
+ lines.push(hints.filter(Boolean).join(' · '));
1554
+ }
1555
+
1556
+ return this.renderPinnedHeader(lines);
1557
+ }
1558
+
1559
+ printStatusBar(state = {}) {
1560
+ const line = formatStatusBar(state, this.getWidth());
1561
+ process.stdout.write(`${this.colorize('time', ` ${line}`)}\n`);
1562
+ }
1563
+
1564
+ printInputHints(hints = []) {
1565
+ if (this.workspace.active) {
1566
+ this.workspace.hints = Array.isArray(hints) ? hints.filter(Boolean) : [];
1567
+ this.renderWorkspace();
1568
+ return;
1569
+ }
1570
+ if (!Array.isArray(hints) || hints.length === 0) {
1571
+ return;
1572
+ }
1573
+
1574
+ const line = hints
1575
+ .filter(Boolean)
1576
+ .map((item) => this.colorize('time', item))
1577
+ .join(this.colorize('border', ' · '));
1578
+ process.stdout.write(` ${line}\n`);
1579
+ }
1580
+
1581
+ printSelectableList(title, items = [], selectedIndex = 0) {
1582
+ if (!Array.isArray(items) || items.length === 0) {
1583
+ this.printInfo('(暂无条目)');
1584
+ return;
1585
+ }
1586
+
1587
+ const lines = items.map((item, index) => {
1588
+ const marker = index === selectedIndex
1589
+ ? this.colorize('ai', '›')
1590
+ : this.colorize('time', ' ');
1591
+ return `${marker} ${item}`;
1592
+ });
1593
+
1594
+ this.printPanel(title, lines);
1595
+ }
1596
+
1597
+ printChatHelp(agentMode = false) {
1598
+ const rows = [
1599
+ { label: '/h', value: '等价于 /help' },
1600
+ { label: '/s', value: '等价于 /status' },
1601
+ { label: '/t', value: '等价于 /task' },
1602
+ { label: '/m', value: '等价于 /menu' },
1603
+ { label: '/paste', value: '进入多行粘贴模式' },
1604
+ { label: '/status', value: '打开运行状态与推荐操作' },
1605
+ { label: '/memory', value: '查看当前会话记忆' },
1606
+ { label: '/pin', value: '把一条信息固定到会话记忆' },
1607
+ { label: '/retry-last', value: '重试上一条已发送消息' },
1608
+ { label: '/plan', value: '查看当前提炼出的轻量计划' },
1609
+ { label: '/sessions', value: '打开历史会话浏览器' },
1610
+ { label: '/resume', value: '恢复最近一条历史会话' },
1611
+ { label: '/switch-provider', value: '切换当前会话使用的提供方' },
1612
+ { label: '/model', value: '切换当前会话提供方或打开默认提供方设置' },
1613
+ { label: '/rename-session', value: '修改当前会话标题' },
1614
+ { label: '/delete-session', value: '删除当前会话' },
1615
+ { label: '/task', value: '打开任务面板' },
1616
+ { label: '/mode', value: '切换普通模式 / Agent 模式' },
1617
+ { label: '/inspect', value: '检查当前网页适配状态' },
1618
+ { label: '/config', value: '打开系统设置并调整默认提供方' },
1619
+ { label: '/doc', value: '查看简要文档' },
1620
+ { label: '/menu', value: '返回主菜单' },
1621
+ { label: '/quit', value: '退出程序'}
1622
+ ];
1623
+
1624
+ if (agentMode) {
1625
+ rows.splice(4, 0, { label: '/agent', value: '需要登录或验证时人工接管' });
1626
+ }
1627
+
1628
+ this.printKeyValueTable('聊天命令', rows);
1629
+ }
1630
+
1631
+ printMarkdown(text) {
1632
+ if (this.workspace.active) {
1633
+ this.appendWorkspaceLines(buildMarkdownLines(text, this.getWorkspaceMainWidth()));
1634
+ return;
1635
+ }
1636
+
1637
+ for (const line of buildMarkdownLines(text, this.getWidth() - 2)) {
1638
+ process.stdout.write(`${this.colorize('text', line)}\n`);
1639
+ }
1640
+ this.stdoutLineOpen = false;
1641
+ }
1642
+
1643
+ printMarkdownRich(text) {
1644
+ this.printMarkdown(text);
1645
+ }
1646
+
1647
+ printPanel(title, lines = []) {
1648
+ if (this.workspace.active) {
1649
+ this.appendWorkspaceLines(buildPanelLines(title, lines, this.getWorkspaceMainWidth()));
1650
+ return;
1651
+ }
1652
+ const rendered = buildLightSectionLines(title, lines, this.getWidth() - 2)
1653
+ .map((line) => this.colorize('text', line))
1654
+ .join('\n');
1655
+ process.stdout.write(`${rendered}\n`);
1656
+ this.stdoutLineOpen = false;
1657
+ }
1658
+
1659
+ printKeyValueTable(title, rows = []) {
1660
+ if (this.workspace.active) {
1661
+ this.appendWorkspaceLines(buildKeyValueTableLines(title, rows, this.getWorkspaceMainWidth()));
1662
+ return;
1663
+ }
1664
+ const rendered = buildLightKeyValueLines(title, rows, this.getWidth() - 2)
1665
+ .join('\n');
1666
+ process.stdout.write(`${rendered}\n`);
1667
+ this.stdoutLineOpen = false;
1668
+ }
1669
+
1670
+ printAgentStep({
1671
+ step,
1672
+ maxSteps,
1673
+ command,
1674
+ phase = 'plan',
1675
+ agentLabel = '',
1676
+ multiAgent = false
1677
+ } = {}) {
1678
+ const title =
1679
+ phase === 'execute'
1680
+ ? 'Agent 执行中'
1681
+ : phase === 'observe'
1682
+ ? 'Agent 读取结果'
1683
+ : 'Agent 下一步';
1684
+
1685
+ this.printKeyValueTable(title, formatAgentStepRows({
1686
+ step,
1687
+ maxSteps,
1688
+ command,
1689
+ phase,
1690
+ agentLabel,
1691
+ multiAgent
1692
+ }));
1693
+ }
1694
+
1695
+ printSessionList(sessions, page = 1, pageSize = 10) {
1696
+ if (sessions.length === 0) {
1697
+ this.printInfo('(没有匹配的聊天记录)');
1698
+ return { hasMore: false, totalPages: 0 };
1699
+ }
1700
+
1701
+ const start = (page - 1) * pageSize;
1702
+ const end = start + pageSize;
1703
+ const pageSessions = sessions.slice(start, end);
1704
+ const totalPages = Math.ceil(sessions.length / pageSize);
1705
+ const width = this.getWidth();
1706
+ const titleWidth = Math.max(16, width - 34);
1707
+
1708
+ this.printKeyValueTable('会话列表', pageSessions.map((s, i) => {
1709
+ const globalIndex = start + i + 1;
1710
+ const date = new Date(s.updatedAt).toLocaleString('zh-CN', {
1711
+ month: '2-digit',
1712
+ day: '2-digit',
1713
+ hour: '2-digit',
1714
+ minute: '2-digit',
1715
+ hour12: false
1716
+ });
1717
+
1718
+ return {
1719
+ label: `[${String(globalIndex).padStart(2)}]`,
1720
+ value: `${truncateText(s.title, titleWidth)} | ${s.provider} | ${s.messageCount}条 | ${date}`
1721
+ };
1722
+ }));
1723
+
1724
+ if (totalPages > 1) {
1725
+ this.printNewline();
1726
+ this.printInfo(`第 ${page}/${totalPages} 页(共 ${sessions.length} 条)`);
1727
+ }
1728
+
1729
+ return { hasMore: end < sessions.length, totalPages };
1730
+ }
1731
+
1732
+ printSessionPreview(session, details = null) {
1733
+ if (!session) {
1734
+ return;
1735
+ }
1736
+
1737
+ const rows = [
1738
+ { label: '提供方', value: session.provider || '(未知)' },
1739
+ Array.isArray(session.providerHistory) && session.providerHistory.length > 1
1740
+ ? {
1741
+ label: '切换轨迹',
1742
+ value: session.providerHistory.map((item) => item.provider).join(' -> ')
1743
+ }
1744
+ : null,
1745
+ {
1746
+ label: '最后更新',
1747
+ value: session.updatedAt
1748
+ ? new Date(session.updatedAt).toLocaleString('zh-CN', { hour12: false })
1749
+ : '(未知)'
1750
+ },
1751
+ { label: '消息数', value: String(session.messageCount ?? details?.messages?.length ?? 0) }
1752
+ ];
1753
+
1754
+ const lastUserMessage = details?.messages
1755
+ ?.filter((item) => item.role === 'user')
1756
+ ?.at(-1)?.content;
1757
+
1758
+ if (lastUserMessage) {
1759
+ rows.push({
1760
+ label: '最后用户消息',
1761
+ value: summarizeMessage(lastUserMessage, this.getWidth() - 24)
1762
+ });
1763
+ }
1764
+
1765
+ if (session.url || details?.url) {
1766
+ rows.push({
1767
+ label: '会话 URL',
1768
+ value: session.url || details.url
1769
+ });
1770
+ }
1771
+
1772
+ this.printKeyValueTable(
1773
+ `会话预览 · ${session.title || '(无标题)'}`,
1774
+ rows.filter(Boolean)
1775
+ );
1776
+ }
1777
+
1778
+ printTimeline(title = 'Agent 时间线', events = []) {
1779
+ if (!events.length) {
1780
+ return;
1781
+ }
1782
+
1783
+ const signature = JSON.stringify({
1784
+ title,
1785
+ events: events.map((event) => ({
1786
+ label: event.label || '',
1787
+ detail: event.detail || '',
1788
+ agentLabel: event.agentLabel || ''
1789
+ }))
1790
+ });
1791
+
1792
+ if (signature === this.lastTimelineSignature) {
1793
+ return;
1794
+ }
1795
+ this.lastTimelineSignature = signature;
1796
+
1797
+ const rows = events.map((event, index) => {
1798
+ const prefix = event.time ? `[${event.time}] ` : '';
1799
+ const rolePrefix = event.agentLabel ? `${event.agentLabel} · ` : '';
1800
+ const description = [rolePrefix + event.label, event.detail].filter(Boolean).join(' · ');
1801
+ return {
1802
+ label: `#${String(index + 1).padStart(2, '0')}`,
1803
+ value: `${prefix}${description}`
1804
+ };
1805
+ });
1806
+
1807
+ this.printKeyValueTable(title, rows);
1808
+ }
1809
+
1810
+ printAgentStatus({
1811
+ stage = '等待',
1812
+ summary = '',
1813
+ detail = '',
1814
+ command = null,
1815
+ agentLabel = ''
1816
+ } = {}) {
1817
+ const signature = JSON.stringify({
1818
+ stage,
1819
+ summary,
1820
+ detail,
1821
+ command: summarizeAgentCommand(command),
1822
+ agentLabel
1823
+ });
1824
+
1825
+ if (signature === this.lastStatusSignature) {
1826
+ return;
1827
+ }
1828
+ this.lastStatusSignature = signature;
1829
+
1830
+ const rows = [
1831
+ { label: '状态', value: stage },
1832
+ agentLabel ? { label: '角色', value: agentLabel } : null,
1833
+ summary ? { label: '摘要', value: summary } : null,
1834
+ detail ? { label: '细节', value: detail } : null,
1835
+ command ? { label: '命令概览', value: summarizeAgentCommand(command) } : null
1836
+ ].filter(Boolean);
1837
+
1838
+ this.printKeyValueTable('Agent 状态', rows);
1839
+ }
1840
+
1841
+ printActionPanel(title = '行动面板', rows = []) {
1842
+ this.printKeyValueTable(title, rows);
1843
+ }
1844
+
1845
+ printSuggestedActions(actions = [], title = '推荐操作') {
1846
+ if (!actions.length) {
1847
+ return;
1848
+ }
1849
+
1850
+ this.printKeyValueTable(title, actions.map((action, index) => ({
1851
+ label: `[${String(index + 1).padStart(2, '0')}]`,
1852
+ value: `${action.label}${action.hint ? ` | ${action.hint}` : ''}`
1853
+ })));
1854
+ }
1855
+
1856
+ printTaskList(tasks = []) {
1857
+ if (!tasks.length) {
1858
+ this.printInfo('(暂无任务)');
1859
+ return;
1860
+ }
1861
+
1862
+ this.printKeyValueTable('任务列表', tasks.map((task, index) => ({
1863
+ label: `[${String(index + 1).padStart(2, '0')}]`,
1864
+ value: `${task.title} | ${task.status} | ${task.provider || '-'} | ${task.mode || 'once'} | ${task.goal || ''}`
1865
+ })));
1866
+ }
1867
+
1868
+ printTaskRuntime(snapshot = null) {
1869
+ if (!snapshot) {
1870
+ return;
1871
+ }
1872
+
1873
+ this.printKeyValueTable('任务运行状态', [
1874
+ { label: '运行中', value: String(snapshot.running ?? 0) },
1875
+ { label: '排队中', value: String(snapshot.queued ?? 0) },
1876
+ { label: '待接管', value: String(snapshot.handoff ?? 0) },
1877
+ { label: '失败', value: String(snapshot.failed ?? 0) },
1878
+ {
1879
+ label: '活跃任务',
1880
+ value: Array.isArray(snapshot.activeTaskIds) && snapshot.activeTaskIds.length > 0
1881
+ ? snapshot.activeTaskIds.join(', ')
1882
+ : '(无)'
1883
+ }
1884
+ ]);
1885
+ }
1886
+
1887
+ printTaskDetails(task) {
1888
+ if (!task) {
1889
+ return;
1890
+ }
1891
+
1892
+ this.printKeyValueTable(`任务详情 · ${task.title}`, [
1893
+ { label: '状态', value: task.status },
1894
+ { label: '提供方', value: task.provider || '(未指定)' },
1895
+ { label: '模式', value: task.mode || 'once' },
1896
+ {
1897
+ label: '间隔',
1898
+ value:
1899
+ task.intervalMs && Number(task.intervalMs) > 0
1900
+ ? `${task.intervalMs} ms`
1901
+ : '(无)'
1902
+ },
1903
+ { label: '运行次数', value: String(task.runCount || 0) },
1904
+ { label: '重试次数', value: String(task.retryCount || 0) },
1905
+ { label: '最后执行', value: task.lastRunAt || '(暂无)' },
1906
+ { label: '下次执行', value: task.nextRunAt || '(无)' },
1907
+ { label: '最后结果', value: task.lastResult || '(暂无)' },
1908
+ { label: '错误', value: task.lastError || '(无)' },
1909
+ task.handoffUrl ? { label: '接管页面', value: task.handoffUrl } : null,
1910
+ { label: '目标', value: task.goal || '(无)' }
1911
+ ].filter(Boolean));
1912
+
1913
+ if (Array.isArray(task.history) && task.history.length > 0) {
1914
+ this.printNewline();
1915
+ this.printTimeline('任务时间线', task.history.slice(-10).map((item) => ({
1916
+ time: item.time,
1917
+ label: item.label || item.type,
1918
+ detail: item.detail || '',
1919
+ agentLabel: item.agentLabel || ''
1920
+ })));
1921
+ }
1922
+ }
1923
+
1924
+ printHistory(messages) {
1925
+ this.printSeparator();
1926
+ for (const m of messages) {
1927
+ const title = m.role === 'user' ? 'YOU' : 'AI';
1928
+ if (shouldRenderTextAsMarkdown(m.content)) {
1929
+ this.printTitle(title);
1930
+ this.printMarkdown(m.content);
1931
+ } else {
1932
+ this.printPanel(title, wrapText(m.content, this.getWidth() - 6));
1933
+ }
1934
+ }
1935
+ this.printSeparator();
1936
+ }
1937
+
1938
+ printRenderedHistory(messages, {
1939
+ renderMessage
1940
+ } = {}) {
1941
+ this.printSeparator();
1942
+
1943
+ for (const message of messages) {
1944
+ if (message.role === 'user') {
1945
+ if (shouldRenderTextAsMarkdown(message.content)) {
1946
+ this.printTitle('YOU');
1947
+ this.printMarkdown(message.content);
1948
+ } else {
1949
+ this.printPanel('YOU', wrapText(message.content, this.getWidth() - 6));
1950
+ }
1951
+ continue;
1952
+ }
1953
+
1954
+ const rendered = typeof renderMessage === 'function'
1955
+ ? renderMessage(message)
1956
+ : null;
1957
+ const title = rendered?.title || 'AI';
1958
+ const badges = Array.isArray(rendered?.badges) ? rendered.badges.filter(Boolean) : [];
1959
+ const titleWithBadges = badges.length > 0
1960
+ ? `${title} · ${badges.join(' · ')}`
1961
+ : title;
1962
+
1963
+ if ((rendered?.renderAs === 'markdown' || shouldRenderTextAsMarkdown(rendered?.text)) && rendered?.text) {
1964
+ this.printTitle(titleWithBadges);
1965
+ this.printMarkdown(rendered.text);
1966
+ continue;
1967
+ }
1968
+
1969
+ const plainText = rendered?.text || message.content || '';
1970
+ this.printPanel(titleWithBadges, wrapText(plainText, this.getWidth() - 6));
1971
+ }
1972
+
1973
+ this.printSeparator();
1974
+ }
1975
+
1976
+ enterWorkspace({
1977
+ title = 'XShat Workspace',
1978
+ status = '',
1979
+ hints = [],
1980
+ leftSections = [],
1981
+ rightSections = []
1982
+ } = {}) {
1983
+ this.clearPinnedHeader();
1984
+ this.workspace.active = true;
1985
+ this.workspace.title = title;
1986
+ this.workspace.status = status;
1987
+ this.workspace.hints = hints;
1988
+ this.workspace.leftSections = leftSections;
1989
+ this.workspace.rightSections = rightSections;
1990
+ this.workspace.footer = '';
1991
+ this.workspace.logLines = [];
1992
+ this.workspace.inputLabel = 'YOU > ';
1993
+ this.workspace.inputPlaceholder = '输入消息后回车发送';
1994
+ this.workspace.inputValue = '';
1995
+ this.workspace.inputCursor = 0;
1996
+ process.stdout.write('\x1b[?1049h\x1b[?25h');
1997
+ this.renderWorkspace();
1998
+ }
1999
+
2000
+ exitWorkspace() {
2001
+ if (!this.workspace.active) {
2002
+ return;
2003
+ }
2004
+ this.workspace.active = false;
2005
+ this.workspace.logLines = [];
2006
+ process.stdout.write('\x1b[?1049l\x1b[?25h');
2007
+ }
2008
+
2009
+ isWorkspaceActive() {
2010
+ return Boolean(this.workspace.active);
2011
+ }
2012
+
2013
+ resetWorkspaceLog() {
2014
+ if (!this.workspace.active) {
2015
+ return;
2016
+ }
2017
+ this.workspace.logLines = [];
2018
+ this.renderWorkspace();
2019
+ }
2020
+
2021
+ setWorkspaceLayout({
2022
+ title,
2023
+ status,
2024
+ hints,
2025
+ leftSections,
2026
+ rightSections,
2027
+ footer,
2028
+ inputLabel,
2029
+ inputPlaceholder,
2030
+ inputValue,
2031
+ inputCursor
2032
+ } = {}) {
2033
+ if (title !== undefined) {
2034
+ this.workspace.title = title;
2035
+ }
2036
+ if (status !== undefined) {
2037
+ this.workspace.status = status;
2038
+ }
2039
+ if (hints !== undefined) {
2040
+ this.workspace.hints = Array.isArray(hints) ? hints.filter(Boolean) : [];
2041
+ }
2042
+ if (leftSections !== undefined) {
2043
+ this.workspace.leftSections = Array.isArray(leftSections) ? leftSections : [];
2044
+ }
2045
+ if (rightSections !== undefined) {
2046
+ this.workspace.rightSections = Array.isArray(rightSections) ? rightSections : [];
2047
+ }
2048
+ if (footer !== undefined) {
2049
+ this.workspace.footer = footer;
2050
+ }
2051
+ if (inputLabel !== undefined) {
2052
+ this.workspace.inputLabel = inputLabel;
2053
+ }
2054
+ if (inputPlaceholder !== undefined) {
2055
+ this.workspace.inputPlaceholder = inputPlaceholder;
2056
+ }
2057
+ if (inputValue !== undefined) {
2058
+ this.workspace.inputValue = String(inputValue ?? '');
2059
+ }
2060
+ if (inputCursor !== undefined) {
2061
+ this.workspace.inputCursor = Math.max(0, Number(inputCursor) || 0);
2062
+ }
2063
+ if (this.workspace.active) {
2064
+ this.renderWorkspace();
2065
+ }
2066
+ }
2067
+
2068
+ setWorkspaceInput(value = '', cursor = null) {
2069
+ this.workspace.inputValue = String(value ?? '');
2070
+ this.workspace.inputCursor = cursor === null
2071
+ ? this.workspace.inputValue.length
2072
+ : Math.max(0, Math.min(Number(cursor) || 0, this.workspace.inputValue.length));
2073
+ if (this.workspace.active) {
2074
+ this.renderWorkspace();
2075
+ }
2076
+ }
2077
+
2078
+ getWorkspaceMainWidth() {
2079
+ const {
2080
+ totalWidth,
2081
+ leftWidth,
2082
+ rightWidth,
2083
+ showLeft,
2084
+ showRight,
2085
+ gapWidth
2086
+ } = this.getWorkspaceDimensions();
2087
+ const left = showLeft ? leftWidth : 0;
2088
+ const right = showRight ? rightWidth : 0;
2089
+ const gaps = (showLeft ? gapWidth : 0) + (showRight ? gapWidth : 0);
2090
+ return Math.max(40, totalWidth - left - right - gaps - 2);
2091
+ }
2092
+
2093
+ appendWorkspaceLines(lines = []) {
2094
+ if (!this.workspace.active) {
2095
+ return;
2096
+ }
2097
+ const safeLines = Array.isArray(lines) ? lines.map((line) => String(line ?? '')) : [];
2098
+ this.workspace.logLines.push(...safeLines);
2099
+ this.workspace.logLines = this.workspace.logLines.slice(-600);
2100
+ this.renderWorkspace();
2101
+ }
2102
+
2103
+ renderWorkspaceSections(sections = [], width, maxHeight) {
2104
+ const output = [];
2105
+ for (const section of sections) {
2106
+ if (!section) {
2107
+ continue;
2108
+ }
2109
+
2110
+ const sectionLines = buildCompactSectionLines(
2111
+ section.title || 'Section',
2112
+ {
2113
+ rows: Array.isArray(section.rows) ? section.rows : [],
2114
+ lines: Array.isArray(section.lines) ? section.lines : []
2115
+ },
2116
+ width
2117
+ );
2118
+
2119
+ if (output.length > 0) {
2120
+ output.push('');
2121
+ }
2122
+ output.push(...sectionLines);
2123
+ if (output.length >= maxHeight) {
2124
+ break;
2125
+ }
2126
+ }
2127
+ return output.slice(0, maxHeight);
2128
+ }
2129
+
2130
+ renderWorkspace() {
2131
+ if (!this.workspace.active) {
2132
+ return;
2133
+ }
2134
+
2135
+ const {
2136
+ totalWidth,
2137
+ leftWidth,
2138
+ rightWidth,
2139
+ showLeft,
2140
+ showRight,
2141
+ gapWidth
2142
+ } = this.getWorkspaceDimensions();
2143
+ const totalRows = Math.max(20, process.stdout.rows || 30);
2144
+ const visibleLeftWidth = showLeft ? leftWidth : 0;
2145
+ const visibleRightWidth = showRight ? rightWidth : 0;
2146
+ const gutterWidth = (showLeft ? gapWidth : 0) + (showRight ? gapWidth : 0);
2147
+ const mainWidth = Math.max(40, totalWidth - visibleLeftWidth - visibleRightWidth - gutterWidth - 2);
2148
+ const bodyHeight = Math.max(6, totalRows - 6);
2149
+ const statusLine = truncateText(
2150
+ [this.workspace.title, this.workspace.status].filter(Boolean).join(' · '),
2151
+ totalWidth - 2
2152
+ );
2153
+ const hintsLine = truncateText(
2154
+ (this.workspace.hints || []).join(' ') || (this.workspace.footer || ''),
2155
+ totalWidth - 2
2156
+ );
2157
+
2158
+ const leftLines = showLeft
2159
+ ? this.renderWorkspaceSections(this.workspace.leftSections, leftWidth, bodyHeight)
2160
+ : [];
2161
+ const rightLines = showRight
2162
+ ? this.renderWorkspaceSections(this.workspace.rightSections, rightWidth, bodyHeight)
2163
+ : [];
2164
+ const mainWrappedLines = this.workspace.logLines.flatMap((line) => renderWorkspaceLogLine(line, mainWidth));
2165
+ const mainLines = mainWrappedLines.slice(-bodyHeight);
2166
+
2167
+ const output = [];
2168
+ output.push(this.colorize('ai', ` ${statusLine}`.padEnd(totalWidth, ' ')));
2169
+ output.push(this.colorize('time', ` ${hintsLine}`.padEnd(totalWidth, ' ')));
2170
+
2171
+ for (let index = 0; index < bodyHeight; index++) {
2172
+ const segments = [];
2173
+ if (showLeft) {
2174
+ segments.push(padStyledText(leftLines[index] || '', leftWidth));
2175
+ }
2176
+ segments.push(padStyledText(mainLines[index] || '', mainWidth));
2177
+ if (showRight) {
2178
+ segments.push(padStyledText(rightLines[index] || '', rightWidth));
2179
+ }
2180
+ output.push(segments.join(' '.repeat(gapWidth || 1)));
2181
+ }
2182
+
2183
+ const footer = truncateText(this.workspace.footer || '输入消息,/cmd 打开命令面板,/menu 返回主菜单。', totalWidth - 2);
2184
+ const inputPrefix = this.workspace.inputLabel || 'YOU > ';
2185
+ const inputLine = buildWorkspaceInputLine({
2186
+ prefix: inputPrefix,
2187
+ value: this.workspace.inputValue,
2188
+ placeholder: this.workspace.inputPlaceholder,
2189
+ cursor: this.workspace.inputCursor,
2190
+ width: totalWidth - 4
2191
+ });
2192
+ const inputStrip = `${this.colorize('border', '▏')} ${padStyledText(inputLine, totalWidth - 4)}`;
2193
+
2194
+ output.push(this.colorize('border', '─'.repeat(totalWidth)));
2195
+ output.push(this.colorize('time', ` ${footer}`.padEnd(totalWidth, ' ')));
2196
+ output.push(inputStrip);
2197
+
2198
+ process.stdout.write('\x1b[2J\x1b[H');
2199
+ process.stdout.write(output.join('\n'));
2200
+ }
2201
+ }
2202
+
2203
+ export { UI };
2204
+ export default new UI();