@agi-fans/dsh-tui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/CHANGELOG.md +18 -1
- package/lib/chrome/tool-args.d.ts +9 -0
- package/lib/chrome/tool-args.js +20 -0
- package/lib/chrome/tool-args.js.map +1 -0
- package/lib/chrome/tool-renderers.js +4 -13
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/commands/session.js +2 -4
- package/lib/commands/session.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +44 -0
- package/lib/runtime/composer-images.js +149 -0
- package/lib/runtime/composer-images.js.map +1 -0
- package/lib/runtime/herdr-agent.d.ts +95 -0
- package/lib/runtime/herdr-agent.js +258 -0
- package/lib/runtime/herdr-agent.js.map +1 -0
- package/lib/runtime/plain-tui.d.ts +34 -0
- package/lib/runtime/plain-tui.js +119 -0
- package/lib/runtime/plain-tui.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +3 -0
- package/lib/runtime/provider-local.js +107 -268
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/tool-presentation.js +15 -0
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/content-text.d.ts +12 -0
- package/lib/session/content-text.js +13 -0
- package/lib/session/content-text.js.map +1 -0
- package/lib/session/history-store.js.map +1 -0
- package/lib/session/json-file.d.ts +5 -0
- package/lib/session/json-file.js +21 -0
- package/lib/session/json-file.js.map +1 -0
- package/lib/session/model-favorites.js +15 -25
- package/lib/session/model-favorites.js.map +1 -1
- package/lib/session/release-notes.js +1 -18
- package/lib/session/release-notes.js.map +1 -1
- package/lib/session/session-controller.js +64 -140
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.js +4 -14
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.js +13 -43
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +58 -0
- package/lib/session/subagent-tracker.js +129 -0
- package/lib/session/subagent-tracker.js.map +1 -0
- package/lib/session/update-check.js +2 -15
- package/lib/session/update-check.js.map +1 -1
- package/lib/session/version-compare.d.ts +3 -0
- package/lib/session/version-compare.js +21 -0
- package/lib/session/version-compare.js.map +1 -0
- package/lib/views/event-views.d.ts +11 -270
- package/lib/views/event-views.js +12 -964
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/settings-list.js +12 -11
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/transcript-render.d.ts +179 -0
- package/lib/views/transcript-render.js +916 -0
- package/lib/views/transcript-render.js.map +1 -0
- package/lib/views/transcript-types.d.ts +104 -0
- package/lib/views/transcript-types.js +53 -0
- package/lib/views/transcript-types.js.map +1 -0
- package/package.json +2 -1
- package/lib/views/history-store.js.map +0 -1
- /package/lib/{views → session}/history-store.d.ts +0 -0
- /package/lib/{views → session}/history-store.js +0 -0
package/lib/views/event-views.js
CHANGED
|
@@ -1,85 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transcript
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Transcript fold: SessionEvent/StreamDelta -> TranscriptState.
|
|
3
|
+
*
|
|
4
|
+
* applyEvent is the single writer of TranscriptState; replayEvents folds an
|
|
5
|
+
* immutable log without repeatedly copying its growing block array. Both are
|
|
6
|
+
* pure so the whole fold is testable without a terminal. Rendering lives in
|
|
7
|
+
* transcript-render.ts; consumers keep importing this module, which re-exports
|
|
8
|
+
* both halves.
|
|
9
9
|
* @module @agi-fans/dsh-tui
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import { renderCopySelector } from "./copy-selector.js";
|
|
16
|
-
import { renderSettings } from "./settings-list.js";
|
|
17
|
-
import { renderTrajectory } from "./trajectory.js";
|
|
18
|
-
import { renderAgentHub } from "./agent-hub.js";
|
|
19
|
-
import { renderPlanReviewPage, renderPromptSelector, renderPromptSelectorPage, } from "./prompt-selector.js";
|
|
20
|
-
import { resolveStatusBarConfig } from "../chrome/status-config.js";
|
|
21
|
-
import { renderPermissionBadge, renderStatusFooter, formatTokens } from "../chrome/status-line.js";
|
|
22
|
-
import { createTheme, SPINNER, SYMBOL } from "../chrome/theme.js";
|
|
23
|
-
import { renderGoalBar } from "../chrome/goal-bar.js";
|
|
24
|
-
import { padToWidth, stripAnsi, truncateToWidth, visibleWidth, wrapText } from "../chrome/width.js";
|
|
25
|
-
import { alignFileDiffs, countDiffStats, paintDiffStats, paintPrefixedDiffLine, wrapPaintedDiffRows, } from "../chrome/diff-render.js";
|
|
26
|
-
import { renderTool } from "../chrome/tool-renderers.js";
|
|
27
|
-
import { renderToolsPanel } from "../chrome/tools-list.js";
|
|
28
|
-
import { renderCommandOutput, renderCommandSeparator } from "../chrome/command-output.js";
|
|
29
|
-
import { renderPathMentionRows } from "../chrome/path-mentions.js";
|
|
30
|
-
import { blockMatchesQuery, transcriptSearchHint } from "./transcript-search.js";
|
|
31
|
-
/** True for blocks whose rendered text may still change. */
|
|
32
|
-
function isBlockPending(block) {
|
|
33
|
-
return (block.kind === 'assistant' && block.streaming) || (block.kind === 'tool' && block.status === 'running');
|
|
34
|
-
}
|
|
35
|
-
/** Empty starting state. */
|
|
36
|
-
export function initialTranscript() {
|
|
37
|
-
return {
|
|
38
|
-
blocks: [],
|
|
39
|
-
status: 'idle',
|
|
40
|
-
turn: 0,
|
|
41
|
-
todos: [],
|
|
42
|
-
compactCommandId: undefined,
|
|
43
|
-
compaction: undefined,
|
|
44
|
-
nextTurnInbox: [],
|
|
45
|
-
nextStepInbox: [],
|
|
46
|
-
turnError: undefined,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/** Extract plain text from text blocks, ignoring other block kinds. */
|
|
50
|
-
function contentToText(content) {
|
|
51
|
-
return content
|
|
52
|
-
.flatMap((block) => {
|
|
53
|
-
if (block.type === 'text')
|
|
54
|
-
return [block.text];
|
|
55
|
-
if (block.type === 'image') {
|
|
56
|
-
const ref = block.attachment;
|
|
57
|
-
return [`[image ${ref.width}×${ref.height} · ${ref.mediaType}]`];
|
|
58
|
-
}
|
|
59
|
-
if (block.type === 'file') {
|
|
60
|
-
const ref = block.attachment;
|
|
61
|
-
return [`[file ${ref.name} · ${ref.bytes} bytes]`];
|
|
62
|
-
}
|
|
63
|
-
return [];
|
|
64
|
-
})
|
|
65
|
-
.join('');
|
|
66
|
-
}
|
|
67
|
-
/** Extract reasoning text from reasoning blocks. */
|
|
68
|
-
function contentToReasoning(content) {
|
|
69
|
-
return content
|
|
70
|
-
.filter((block) => block.type === 'reasoning')
|
|
71
|
-
.map((block) => block.text)
|
|
72
|
-
.join('');
|
|
73
|
-
}
|
|
74
|
-
/** Compact pretty-print of a tool call's raw arguments JSON. */
|
|
75
|
-
function prettyArgs(raw) {
|
|
76
|
-
try {
|
|
77
|
-
return JSON.stringify(JSON.parse(raw));
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return raw;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
11
|
+
import { formatTokens } from "../chrome/status-line.js";
|
|
12
|
+
import { contentToReasoning, contentToText, initialTranscript, prettyArgs, } from "./transcript-types.js";
|
|
13
|
+
export * from "./transcript-types.js";
|
|
14
|
+
export * from "./transcript-render.js";
|
|
83
15
|
function isRetryNotice(block) {
|
|
84
16
|
return block?.kind === 'notice' && block.level === 'info' && block.text.startsWith('retrying ');
|
|
85
17
|
}
|
|
@@ -514,889 +446,5 @@ function settleTool(block, result, error, presentation) {
|
|
|
514
446
|
...(presentation === undefined ? {} : { presentation }),
|
|
515
447
|
};
|
|
516
448
|
}
|
|
517
|
-
/**
|
|
518
|
-
export const TOOL_COLLAPSED_LINES = 10;
|
|
519
|
-
export const TOOL_INPUT_COLLAPSED_LINES = 3;
|
|
520
|
-
function slicePreview(lines, limit, expanded, direction) {
|
|
521
|
-
if (expanded || lines.length <= limit)
|
|
522
|
-
return { lines: [...lines], hidden: 0 };
|
|
523
|
-
const hidden = lines.length - limit;
|
|
524
|
-
return {
|
|
525
|
-
lines: direction === 'tail' ? lines.slice(hidden) : lines.slice(0, limit),
|
|
526
|
-
hidden,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
function toolPreview(lines, width, limit, expanded, direction) {
|
|
530
|
-
const contentWidth = Math.max(1, width - 4);
|
|
531
|
-
return slicePreview(lines.flatMap(line => wrapText(line, contentWidth)), limit, expanded, direction);
|
|
532
|
-
}
|
|
533
|
-
function exclusiveDiffs(presentation) {
|
|
534
|
-
if (presentation?.result?.card === 'diff')
|
|
535
|
-
return presentation.result.diffs;
|
|
536
|
-
if (presentation?.result !== undefined)
|
|
537
|
-
return undefined;
|
|
538
|
-
if (presentation?.call?.card === 'diff')
|
|
539
|
-
return presentation.call.diffs;
|
|
540
|
-
return undefined;
|
|
541
|
-
}
|
|
542
|
-
/** OMP AssistantMessage uses one horizontal cell of padding and no vertical padding. */
|
|
543
|
-
const ASSISTANT_PADDING_X = 1;
|
|
544
|
-
function assistantContentLines(lines, width, paddingX) {
|
|
545
|
-
const margin = ' '.repeat(paddingX);
|
|
546
|
-
return lines.map((line) => padToWidth(margin + line + margin, width));
|
|
547
|
-
}
|
|
548
|
-
function lockThinkingLine(line, theme) {
|
|
549
|
-
if (line === '' || !theme.colors)
|
|
550
|
-
return line;
|
|
551
|
-
return theme.italic(theme.fg('thinkingText', stripAnsi(line)));
|
|
552
|
-
}
|
|
553
|
-
function hasExplicitTextColor(theme) {
|
|
554
|
-
const ansi = theme.getFgAnsi('text');
|
|
555
|
-
return ansi !== '' && ansi !== '\x1b[39m';
|
|
556
|
-
}
|
|
557
|
-
function assistantMarkdown(source, theme, width, style) {
|
|
558
|
-
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
559
|
-
const contentWidth = Math.max(1, width - paddingX * 2);
|
|
560
|
-
const rendered = renderMarkdown(source, theme, contentWidth, style);
|
|
561
|
-
if (style?.color !== 'thinkingText')
|
|
562
|
-
return assistantContentLines(rendered, width, paddingX);
|
|
563
|
-
return assistantContentLines(rendered.map(line => lockThinkingLine(line, theme)), width, paddingX);
|
|
564
|
-
}
|
|
565
|
-
function userBubble(text, theme, width) {
|
|
566
|
-
const inner = Math.max(1, width - 2);
|
|
567
|
-
const wrapped = renderPathMentionRows(text, inner, theme);
|
|
568
|
-
const rows = ['', ...wrapped, ''];
|
|
569
|
-
return rows.map((row) => {
|
|
570
|
-
const content = row === '' ? padToWidth('', width) : padToWidth(' ' + row, width);
|
|
571
|
-
return theme.colors ? theme.bg('userMessageBg', content) : content;
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
function toolIcon(status, theme, spinnerFrame) {
|
|
575
|
-
if (status === 'running') {
|
|
576
|
-
if (spinnerFrame < 0)
|
|
577
|
-
return theme.fg('accent', SYMBOL.running);
|
|
578
|
-
return theme.fg('accent', SPINNER[spinnerFrame % SPINNER.length] ?? SYMBOL.running);
|
|
579
|
-
}
|
|
580
|
-
if (status === 'ok')
|
|
581
|
-
return theme.fg('success', SYMBOL.success);
|
|
582
|
-
return theme.fg('error', SYMBOL.error);
|
|
583
|
-
}
|
|
584
|
-
/** Render one tool block as an OMP framed output box. */
|
|
585
|
-
function toolBlockLines(block, theme, width, spinnerFrame, expanded) {
|
|
586
|
-
const icon = toolIcon(block.status, theme, spinnerFrame);
|
|
587
|
-
const presentation = renderTool({
|
|
588
|
-
name: block.name,
|
|
589
|
-
arguments: prettyArgs(block.args),
|
|
590
|
-
output: block.output,
|
|
591
|
-
status: block.status,
|
|
592
|
-
expanded,
|
|
593
|
-
...(block.partial === true ? { partial: true } : {}),
|
|
594
|
-
...(block.presentation === undefined ? {} : { presentation: block.presentation }),
|
|
595
|
-
});
|
|
596
|
-
const diffs = exclusiveDiffs(block.presentation);
|
|
597
|
-
if (diffs !== undefined) {
|
|
598
|
-
const rows = alignFileDiffs(diffs);
|
|
599
|
-
const stats = countDiffStats(rows);
|
|
600
|
-
const statsLabel = paintDiffStats(stats.added, stats.removed, theme);
|
|
601
|
-
const header = icon + ' ' + theme.bold(presentation.title ?? block.name)
|
|
602
|
-
+ (statsLabel === '' ? '' : ' ' + statsLabel);
|
|
603
|
-
const painted = wrapPaintedDiffRows(rows, theme, width);
|
|
604
|
-
const output = slicePreview(painted, TOOL_COLLAPSED_LINES, expanded, 'head');
|
|
605
|
-
if (output.hidden > 0) {
|
|
606
|
-
output.lines.push(theme.fg('dim', `… ${output.hidden} more lines · ⟨Ctrl+O: Expand⟩`));
|
|
607
|
-
}
|
|
608
|
-
const state = block.status === 'running' ? 'running' : block.status === 'ok' ? 'ok' : 'error';
|
|
609
|
-
return renderFramedBlock({ header, state, sections: [{ lines: output.lines }], width }, theme);
|
|
610
|
-
}
|
|
611
|
-
const summary = presentation.summary === undefined || presentation.summary === ''
|
|
612
|
-
? ''
|
|
613
|
-
: theme.fg('dim', ' ' + presentation.summary);
|
|
614
|
-
const header = icon + ' ' + theme.bold(presentation.title ?? block.name) + summary;
|
|
615
|
-
const input = toolPreview(presentation.input, width, TOOL_INPUT_COLLAPSED_LINES, expanded, 'head');
|
|
616
|
-
const output = toolPreview(presentation.output, width, TOOL_COLLAPSED_LINES, expanded, presentation.outputPreview);
|
|
617
|
-
const hasOutput = presentation.output.length > 0;
|
|
618
|
-
const inputLines = input.lines.map(line => (block.presentation?.call?.card === 'diff' ? paintPrefixedDiffLine(line, theme) : theme.fg('toolOutput', line)));
|
|
619
|
-
if (input.hidden > 0) {
|
|
620
|
-
const hint = hasOutput && output.hidden > 0 ? '' : ' · ⟨Ctrl+O: Expand⟩';
|
|
621
|
-
inputLines.push(theme.fg('dim', `… ${input.hidden} more input lines${hint}`));
|
|
622
|
-
}
|
|
623
|
-
const outputLines = output.lines.map(line => (block.presentation?.result?.card === 'diff' ? paintPrefixedDiffLine(line, theme) : theme.fg('toolOutput', line)));
|
|
624
|
-
if (output.hidden > 0) {
|
|
625
|
-
const position = presentation.outputPreview === 'tail' ? 'earlier' : 'more';
|
|
626
|
-
const hint = theme.fg('dim', `… ${output.hidden} ${position} lines · ⟨Ctrl+O: Expand⟩`);
|
|
627
|
-
if (presentation.outputPreview === 'tail')
|
|
628
|
-
outputLines.unshift(hint);
|
|
629
|
-
else
|
|
630
|
-
outputLines.push(hint);
|
|
631
|
-
}
|
|
632
|
-
const sections = [
|
|
633
|
-
...(inputLines.length === 0 ? [] : [{ lines: inputLines }]),
|
|
634
|
-
...(outputLines.length === 0 ? [] : [{
|
|
635
|
-
...(inputLines.length === 0 ? {} : { label: theme.fg('toolTitle', 'Output') }),
|
|
636
|
-
lines: outputLines,
|
|
637
|
-
}]),
|
|
638
|
-
];
|
|
639
|
-
const state = block.status === 'running' ? 'running' : block.status === 'ok' ? 'ok' : 'error';
|
|
640
|
-
return renderFramedBlock({ header, state, sections, width }, theme);
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Render one transcript block to display lines. Pure; shared by the tty view
|
|
644
|
-
* and the non-tty plain printer.
|
|
645
|
-
* @param block - the block to render.
|
|
646
|
-
* @param theme - active theme.
|
|
647
|
-
* @param width - terminal width in columns.
|
|
648
|
-
* @param spinnerFrame - activity spinner phase for running tools.
|
|
649
|
-
* @param toolsExpanded - paint full tool output instead of the collapsed preview.
|
|
650
|
-
* @returns display lines (already width-fitted).
|
|
651
|
-
*/
|
|
652
|
-
export function blockLines(block, theme, width, spinnerFrame = 0, toolsExpanded = false) {
|
|
653
|
-
if (block.kind === 'user')
|
|
654
|
-
return userBubble(block.text, theme, width);
|
|
655
|
-
if (block.kind === 'assistant') {
|
|
656
|
-
const lines = [];
|
|
657
|
-
if (block.reasoning !== '') {
|
|
658
|
-
lines.push(...assistantMarkdown(block.reasoning, theme, width, { color: 'thinkingText', italic: true }));
|
|
659
|
-
if (block.text !== '')
|
|
660
|
-
lines.push('');
|
|
661
|
-
}
|
|
662
|
-
if (block.text === '' && block.streaming) {
|
|
663
|
-
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
664
|
-
lines.push(...assistantContentLines([theme.fg('dim', '…')], width, paddingX));
|
|
665
|
-
}
|
|
666
|
-
else if (block.text !== '') {
|
|
667
|
-
lines.push(...assistantMarkdown(block.text, theme, width, hasExplicitTextColor(theme) ? { color: 'text' } : undefined));
|
|
668
|
-
}
|
|
669
|
-
if (block.interrupted === true) {
|
|
670
|
-
const paddingX = width > ASSISTANT_PADDING_X * 2 ? ASSISTANT_PADDING_X : 0;
|
|
671
|
-
lines.push(...assistantContentLines([theme.fg('dim', '· interrupted')], width, paddingX));
|
|
672
|
-
}
|
|
673
|
-
return lines;
|
|
674
|
-
}
|
|
675
|
-
if (block.kind === 'tool')
|
|
676
|
-
return toolBlockLines(block, theme, width, spinnerFrame, toolsExpanded);
|
|
677
|
-
if (block.kind === 'toolCatalog')
|
|
678
|
-
return renderToolsPanel(block.tools, theme, width, toolsExpanded);
|
|
679
|
-
if (block.kind === 'commandOutput')
|
|
680
|
-
return renderCommandOutput(block.command, block.text, theme, width);
|
|
681
|
-
if (block.framed !== true) {
|
|
682
|
-
const prefix = ' ';
|
|
683
|
-
const continuation = ' '.repeat(visibleWidth(prefix));
|
|
684
|
-
const tone = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim';
|
|
685
|
-
return wrapText(block.text, Math.max(1, width - visibleWidth(prefix))).map((line, index) => truncateToWidth((index === 0 ? prefix : continuation) + theme.fg(tone, line), width));
|
|
686
|
-
}
|
|
687
|
-
const state = block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'info';
|
|
688
|
-
const paint = (text) => theme.fg(block.level === 'error' ? 'error' : block.level === 'warning' ? 'warning' : 'dim', text);
|
|
689
|
-
const wrapped = wrapText(block.text, Math.max(1, width - 4));
|
|
690
|
-
const title = paint(wrapped[0] ?? '');
|
|
691
|
-
return renderFramedBlock({
|
|
692
|
-
header: title,
|
|
693
|
-
state,
|
|
694
|
-
width,
|
|
695
|
-
lines: wrapped.slice(1).map(paint),
|
|
696
|
-
applyBg: false,
|
|
697
|
-
}, theme);
|
|
698
|
-
}
|
|
699
|
-
/** Plain text of one block, used by transcript search and match painting. */
|
|
700
|
-
export function blockSearchText(block) {
|
|
701
|
-
if (block.kind === 'assistant') {
|
|
702
|
-
return block.reasoning === '' ? block.text : `${block.reasoning}\n${block.text}`;
|
|
703
|
-
}
|
|
704
|
-
if (block.kind === 'tool')
|
|
705
|
-
return `${block.args}\n${block.output}`;
|
|
706
|
-
if (block.kind === 'toolCatalog') {
|
|
707
|
-
return block.tools.map(tool => `${tool.name} ${tool.description}`).join('\n');
|
|
708
|
-
}
|
|
709
|
-
return block.text;
|
|
710
|
-
}
|
|
711
|
-
function fitFrame(lines, width, stablePrefix = 0) {
|
|
712
|
-
let fitted;
|
|
713
|
-
const start = Math.max(0, Math.min(lines.length, stablePrefix));
|
|
714
|
-
for (let index = start; index < lines.length; index += 1) {
|
|
715
|
-
const line = lines[index];
|
|
716
|
-
if (visibleWidth(line) <= width)
|
|
717
|
-
continue;
|
|
718
|
-
fitted ??= lines.slice();
|
|
719
|
-
fitted[index] = truncateToWidth(line, width);
|
|
720
|
-
}
|
|
721
|
-
return fitted ?? lines;
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Scroll, activity, inbox, and composer changes do not alter transcript blocks.
|
|
725
|
-
* Cache the expensive Markdown/tool fold by immutable block-array identity so
|
|
726
|
-
* those frames slice already-rendered rows instead of formatting the complete
|
|
727
|
-
* session again. Spinner frames matter only while a visible tool block runs.
|
|
728
|
-
*/
|
|
729
|
-
const transcriptBodyCache = new WeakMap();
|
|
730
|
-
/** Settled immutable blocks keep their expensive Markdown/tool layout. */
|
|
731
|
-
const blockLinesCache = new WeakMap();
|
|
732
|
-
function cachedBlockLines(block, options, theme, themeName, trueColor, spinnerFrame, expanded) {
|
|
733
|
-
const animatedSpinnerFrame = block.kind === 'tool' && block.status === 'running' ? spinnerFrame : -1;
|
|
734
|
-
const cached = blockLinesCache.get(block);
|
|
735
|
-
if (cached !== undefined
|
|
736
|
-
&& cached.width === options.width
|
|
737
|
-
&& cached.colors === options.colors
|
|
738
|
-
&& cached.trueColor === trueColor
|
|
739
|
-
&& cached.themeName === themeName
|
|
740
|
-
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
741
|
-
&& cached.expanded === expanded)
|
|
742
|
-
return cached.lines;
|
|
743
|
-
const lines = blockLines(block, theme, options.width, spinnerFrame, expanded);
|
|
744
|
-
blockLinesCache.set(block, {
|
|
745
|
-
width: options.width,
|
|
746
|
-
colors: options.colors,
|
|
747
|
-
trueColor,
|
|
748
|
-
themeName,
|
|
749
|
-
spinnerFrame: animatedSpinnerFrame,
|
|
750
|
-
expanded,
|
|
751
|
-
lines,
|
|
752
|
-
});
|
|
753
|
-
return lines;
|
|
754
|
-
}
|
|
755
|
-
function renderTranscriptBody(state, options, theme, spinnerFrame) {
|
|
756
|
-
const toolsExpanded = options.toolsExpanded === true;
|
|
757
|
-
const expandedTools = [...(options.expandedTools ?? [])].sort().join('\0');
|
|
758
|
-
const themeName = options.themeName ?? 'dark';
|
|
759
|
-
const trueColor = options.trueColor === true;
|
|
760
|
-
const animatedSpinnerFrame = state.blocks.some(block => block.kind === 'tool' && block.status === 'running')
|
|
761
|
-
? spinnerFrame
|
|
762
|
-
: -1;
|
|
763
|
-
const search = options.transcriptSearch;
|
|
764
|
-
const searchKey = search === undefined
|
|
765
|
-
? ''
|
|
766
|
-
: `${search.query}\u0000${search.focus}\u0000${search.matches.join(',')}`;
|
|
767
|
-
const cached = transcriptBodyCache.get(state.blocks);
|
|
768
|
-
if (cached !== undefined
|
|
769
|
-
&& cached.width === options.width
|
|
770
|
-
&& cached.colors === options.colors
|
|
771
|
-
&& cached.trueColor === trueColor
|
|
772
|
-
&& cached.themeName === themeName
|
|
773
|
-
&& cached.spinnerFrame === animatedSpinnerFrame
|
|
774
|
-
&& cached.toolsExpanded === toolsExpanded
|
|
775
|
-
&& cached.expandedTools === expandedTools
|
|
776
|
-
&& cached.searchKey === searchKey) {
|
|
777
|
-
return { lines: cached.lines, blockStarts: cached.blockStarts };
|
|
778
|
-
}
|
|
779
|
-
const matches = new Set(search?.matches ?? []);
|
|
780
|
-
const lines = [];
|
|
781
|
-
const blockStarts = [];
|
|
782
|
-
let previous;
|
|
783
|
-
for (let index = 0; index < state.blocks.length; index += 1) {
|
|
784
|
-
const block = state.blocks[index];
|
|
785
|
-
if (lines.length > 0) {
|
|
786
|
-
const previousCommand = commandSurfaceName(previous);
|
|
787
|
-
const currentCommand = commandSurfaceName(block);
|
|
788
|
-
if (previousCommand !== undefined && currentCommand !== undefined && previousCommand !== currentCommand) {
|
|
789
|
-
lines.push('', renderCommandSeparator(theme, options.width), '');
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
lines.push('');
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
blockStarts.push(lines.length);
|
|
796
|
-
const expanded = toolsExpanded || (block.kind === 'tool' && options.expandedTools?.has(block.callId) === true);
|
|
797
|
-
const rendered = cachedBlockLines(block, options, theme, themeName, trueColor, spinnerFrame, expanded);
|
|
798
|
-
lines.push(...(search !== undefined && matches.has(index)
|
|
799
|
-
? rendered.map(line => blockMatchesQuery(stripAnsi(line), search.query) ? theme.inverse(line) : line)
|
|
800
|
-
: rendered));
|
|
801
|
-
previous = block;
|
|
802
|
-
}
|
|
803
|
-
transcriptBodyCache.set(state.blocks, {
|
|
804
|
-
width: options.width,
|
|
805
|
-
colors: options.colors,
|
|
806
|
-
trueColor,
|
|
807
|
-
themeName,
|
|
808
|
-
spinnerFrame: animatedSpinnerFrame,
|
|
809
|
-
toolsExpanded,
|
|
810
|
-
expandedTools,
|
|
811
|
-
searchKey,
|
|
812
|
-
lines,
|
|
813
|
-
blockStarts,
|
|
814
|
-
});
|
|
815
|
-
return { lines, blockStarts };
|
|
816
|
-
}
|
|
817
|
-
function commandSurfaceName(block) {
|
|
818
|
-
if (block?.kind === 'toolCatalog')
|
|
819
|
-
return 'tools';
|
|
820
|
-
if (block?.kind === 'commandOutput')
|
|
821
|
-
return block.command;
|
|
822
|
-
return undefined;
|
|
823
|
-
}
|
|
824
|
-
/** Rows moved per Shift+Arrow (OMP ScrollView `fastScrollLines`). */
|
|
825
|
-
export const TRANSCRIPT_FAST_SCROLL = 5;
|
|
826
|
-
function earlierLabel(count) {
|
|
827
|
-
return '… ↑ ' + count + ' earlier line' + (count === 1 ? '' : 's') + ' ⟨Pg↑⟩';
|
|
828
|
-
}
|
|
829
|
-
function laterLabel(count) {
|
|
830
|
-
return '… ↓ ' + count + ' later line' + (count === 1 ? '' : 's') + ' ⟨Pg↓⟩';
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* Window `body` into `budget` rows with ↑/↓ overflow markers.
|
|
834
|
-
* `start` is the first body row to keep; non-finite values pin to the tail.
|
|
835
|
-
*/
|
|
836
|
-
export function windowTranscript(body, budget, start, theme) {
|
|
837
|
-
const len = body.length;
|
|
838
|
-
if (budget <= 0) {
|
|
839
|
-
return { lines: [], start: 0, maxStart: 0, budget, hiddenAbove: len, hiddenBelow: 0 };
|
|
840
|
-
}
|
|
841
|
-
if (len <= budget) {
|
|
842
|
-
return { lines: [...body], start: 0, maxStart: 0, budget, hiddenAbove: 0, hiddenBelow: 0 };
|
|
843
|
-
}
|
|
844
|
-
const maxStart = Math.max(0, len - (budget - 1));
|
|
845
|
-
const s = Number.isFinite(start) ? Math.max(0, Math.min(Math.trunc(start), maxStart)) : maxStart;
|
|
846
|
-
const atTail = s >= maxStart;
|
|
847
|
-
const atTop = s <= 0;
|
|
848
|
-
if (budget === 1) {
|
|
849
|
-
const label = atTail
|
|
850
|
-
? earlierLabel(len)
|
|
851
|
-
: atTop
|
|
852
|
-
? laterLabel(len)
|
|
853
|
-
: '… ↑ ' + s + ' · ↓ ' + (len - s) + ' ⟨Pg↑/Pg↓⟩';
|
|
854
|
-
return {
|
|
855
|
-
lines: [theme.fg('dim', label)],
|
|
856
|
-
start: s,
|
|
857
|
-
maxStart,
|
|
858
|
-
budget,
|
|
859
|
-
hiddenAbove: atTail ? len : s,
|
|
860
|
-
hiddenBelow: atTail ? 0 : len - s,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
if (atTail) {
|
|
864
|
-
const take = budget - 1;
|
|
865
|
-
const hiddenAbove = len - take;
|
|
866
|
-
return {
|
|
867
|
-
lines: [theme.fg('dim', earlierLabel(hiddenAbove)), ...body.slice(hiddenAbove)],
|
|
868
|
-
start: hiddenAbove,
|
|
869
|
-
maxStart,
|
|
870
|
-
budget,
|
|
871
|
-
hiddenAbove,
|
|
872
|
-
hiddenBelow: 0,
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
if (atTop) {
|
|
876
|
-
const take = budget - 1;
|
|
877
|
-
const hiddenBelow = len - take;
|
|
878
|
-
return {
|
|
879
|
-
lines: [...body.slice(0, take), theme.fg('dim', laterLabel(hiddenBelow))],
|
|
880
|
-
start: 0,
|
|
881
|
-
maxStart,
|
|
882
|
-
budget,
|
|
883
|
-
hiddenAbove: 0,
|
|
884
|
-
hiddenBelow,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
if (budget === 2) {
|
|
888
|
-
return {
|
|
889
|
-
lines: [theme.fg('dim', earlierLabel(s)), body[s] ?? ''],
|
|
890
|
-
start: s,
|
|
891
|
-
maxStart,
|
|
892
|
-
budget,
|
|
893
|
-
hiddenAbove: s,
|
|
894
|
-
hiddenBelow: len - s - 1,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
const take = budget - 2;
|
|
898
|
-
const hiddenBelow = len - s - take;
|
|
899
|
-
return {
|
|
900
|
-
lines: [
|
|
901
|
-
theme.fg('dim', earlierLabel(s)),
|
|
902
|
-
...body.slice(s, s + take),
|
|
903
|
-
theme.fg('dim', laterLabel(hiddenBelow)),
|
|
904
|
-
],
|
|
905
|
-
start: s,
|
|
906
|
-
maxStart,
|
|
907
|
-
budget,
|
|
908
|
-
hiddenAbove: s,
|
|
909
|
-
hiddenBelow,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
const IMAGE_MARKER = /\[Image #\d+(?:, \d+x\d+)?\]/gu;
|
|
913
|
-
function imageMarkerRanges(text) {
|
|
914
|
-
return [...text.matchAll(IMAGE_MARKER)].map(match => ({
|
|
915
|
-
start: match.index,
|
|
916
|
-
end: match.index + match[0].length,
|
|
917
|
-
}));
|
|
918
|
-
}
|
|
919
|
-
function coveringRange(ranges, index) {
|
|
920
|
-
return ranges.find(range => range.start <= index && index < range.end);
|
|
921
|
-
}
|
|
922
|
-
function nextRangeStart(ranges, index) {
|
|
923
|
-
let next = Number.POSITIVE_INFINITY;
|
|
924
|
-
for (const range of ranges) {
|
|
925
|
-
if (range.start > index && range.start < next)
|
|
926
|
-
next = range.start;
|
|
927
|
-
}
|
|
928
|
-
return next;
|
|
929
|
-
}
|
|
930
|
-
/** Paint a wrapped composer slice: leading `/name` plus image markers. */
|
|
931
|
-
function paintComposerInputSlice(fullText, slice, sourceStart, theme) {
|
|
932
|
-
const sourceEnd = sourceStart + slice.length;
|
|
933
|
-
const slash = leadingSlashCommandNameRange(fullText);
|
|
934
|
-
const slashRanges = slash === null ? [] : [slash];
|
|
935
|
-
const images = imageMarkerRanges(fullText);
|
|
936
|
-
if (slashRanges.length === 0 && images.length === 0)
|
|
937
|
-
return slice;
|
|
938
|
-
let output = '';
|
|
939
|
-
let cursor = sourceStart;
|
|
940
|
-
while (cursor < sourceEnd) {
|
|
941
|
-
const image = coveringRange(images, cursor);
|
|
942
|
-
if (image !== undefined) {
|
|
943
|
-
const end = Math.min(image.end, sourceEnd);
|
|
944
|
-
output += theme.underline(theme.bold(theme.fg('accent', fullText.slice(cursor, end))));
|
|
945
|
-
cursor = end;
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
const command = coveringRange(slashRanges, cursor);
|
|
949
|
-
if (command !== undefined) {
|
|
950
|
-
const end = Math.min(command.end, sourceEnd, nextRangeStart(images, cursor));
|
|
951
|
-
output += theme.bold(theme.fg('accent', fullText.slice(cursor, end)));
|
|
952
|
-
cursor = end;
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
const end = Math.min(sourceEnd, nextRangeStart(images, cursor), nextRangeStart(slashRanges, cursor));
|
|
956
|
-
output += fullText.slice(cursor, end);
|
|
957
|
-
cursor = end;
|
|
958
|
-
}
|
|
959
|
-
return output;
|
|
960
|
-
}
|
|
961
|
-
/** Maximum number of queued composer submissions kept visible above the editor. */
|
|
962
|
-
export const QUEUED_SUBMISSION_PREVIEW = 3;
|
|
963
|
-
/** Maximum number of Todo items kept visible above the composer. */
|
|
964
|
-
export const TODO_PREVIEW = 5;
|
|
965
|
-
/** Maximum number of descendant subagents kept visible above the composer. */
|
|
966
|
-
export const SUBAGENT_PREVIEW = 5;
|
|
967
|
-
function todoPreviewStart(todos) {
|
|
968
|
-
if (todos.length <= TODO_PREVIEW)
|
|
969
|
-
return 0;
|
|
970
|
-
const active = todos.findIndex(todo => todo.status === 'in_progress');
|
|
971
|
-
if (active >= 0)
|
|
972
|
-
return active;
|
|
973
|
-
const pending = todos.findIndex(todo => todo.status === 'pending');
|
|
974
|
-
return pending >= 0 ? pending : Math.max(0, todos.length - TODO_PREVIEW);
|
|
975
|
-
}
|
|
976
|
-
/** Compact, unframed Todo tree placed above queued messages. */
|
|
977
|
-
export function renderTodos(todos, theme, width) {
|
|
978
|
-
if (todos.length === 0 || width <= 0)
|
|
979
|
-
return [];
|
|
980
|
-
const completed = todos.filter(todo => todo.status === 'completed').length;
|
|
981
|
-
const header = ' ' + theme.bold(theme.fg('accent', 'Todos'))
|
|
982
|
-
+ theme.fg('dim', ` · ${completed}/${todos.length}`);
|
|
983
|
-
const start = todoPreviewStart(todos);
|
|
984
|
-
const end = Math.min(todos.length, start + TODO_PREVIEW);
|
|
985
|
-
const rows = [
|
|
986
|
-
...(start === 0 ? [] : [`… ${start} earlier`]),
|
|
987
|
-
...todos.slice(start, end),
|
|
988
|
-
...(end === todos.length ? [] : [`… ${todos.length - end} more`]),
|
|
989
|
-
];
|
|
990
|
-
const lines = [header, ...rows.map((row, index) => {
|
|
991
|
-
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
992
|
-
if (typeof row === 'string')
|
|
993
|
-
return branch + theme.fg('dim', row);
|
|
994
|
-
const todo = row;
|
|
995
|
-
if (todo.status === 'completed') {
|
|
996
|
-
return branch + theme.fg('success', SYMBOL.success + ' ' + theme.strikethrough(todo.content));
|
|
997
|
-
}
|
|
998
|
-
if (todo.status === 'in_progress') {
|
|
999
|
-
return branch + theme.fg('accent', SYMBOL.pending + ' ' + todo.content);
|
|
1000
|
-
}
|
|
1001
|
-
return branch + theme.fg('dim', SYMBOL.pending + ' ' + todo.content);
|
|
1002
|
-
})];
|
|
1003
|
-
return lines.map(line => truncateToWidth(line, width));
|
|
1004
|
-
}
|
|
1005
|
-
function subagentPreviewStart(agents) {
|
|
1006
|
-
if (agents.length <= SUBAGENT_PREVIEW)
|
|
1007
|
-
return 0;
|
|
1008
|
-
const active = agents.findIndex(agent => agent.phase === 'running' || agent.phase === 'starting');
|
|
1009
|
-
if (active >= 0)
|
|
1010
|
-
return Math.max(0, Math.min(active, agents.length - SUBAGENT_PREVIEW));
|
|
1011
|
-
const waiting = agents.findIndex(agent => agent.phase === 'waiting');
|
|
1012
|
-
return waiting >= 0
|
|
1013
|
-
? Math.max(0, Math.min(waiting, agents.length - SUBAGENT_PREVIEW))
|
|
1014
|
-
: Math.max(0, agents.length - SUBAGENT_PREVIEW);
|
|
1015
|
-
}
|
|
1016
|
-
function subagentPhaseGlyph(phase, theme, spinnerFrame) {
|
|
1017
|
-
if (phase === 'running' || phase === 'starting') {
|
|
1018
|
-
if (spinnerFrame < 0)
|
|
1019
|
-
return theme.fg('accent', SYMBOL.running);
|
|
1020
|
-
return theme.fg('accent', SPINNER[spinnerFrame % SPINNER.length] ?? SYMBOL.running);
|
|
1021
|
-
}
|
|
1022
|
-
if (phase === 'error')
|
|
1023
|
-
return theme.fg('error', SYMBOL.error);
|
|
1024
|
-
return theme.fg('success', SYMBOL.success);
|
|
1025
|
-
}
|
|
1026
|
-
const SUBAGENT_PHASE_LABELS = {
|
|
1027
|
-
starting: 'Starting', running: 'Running', waiting: 'Waiting', completed: 'Done', error: 'Failed',
|
|
1028
|
-
};
|
|
1029
|
-
function subagentRowText(agent) {
|
|
1030
|
-
const indent = agent.depth > 1 ? ' '.repeat(agent.depth - 1) : '';
|
|
1031
|
-
return `${indent}${agent.label} · ${SUBAGENT_PHASE_LABELS[agent.phase]}`;
|
|
1032
|
-
}
|
|
1033
|
-
/** Compact, unframed descendant-subagent tree placed above Todos. */
|
|
1034
|
-
export function renderSubagents(roster, theme, width, spinnerFrame = 0, inspectedId, launcherFocused = false) {
|
|
1035
|
-
const agents = roster?.agents ?? [];
|
|
1036
|
-
if (agents.length === 0 || width <= 0)
|
|
1037
|
-
return [];
|
|
1038
|
-
const running = agents.filter(agent => agent.phase === 'running' || agent.phase === 'starting').length;
|
|
1039
|
-
const failed = agents.filter(agent => agent.phase === 'error').length;
|
|
1040
|
-
const waiting = agents.filter(agent => agent.phase === 'waiting').length;
|
|
1041
|
-
const done = agents.filter(agent => agent.phase === 'completed').length;
|
|
1042
|
-
const counts = [
|
|
1043
|
-
running === 0 ? undefined : `${running} running`,
|
|
1044
|
-
waiting === 0 ? undefined : `${waiting} waiting`,
|
|
1045
|
-
done === 0 ? undefined : `${done} done`,
|
|
1046
|
-
failed === 0 ? undefined : `${failed} failed`,
|
|
1047
|
-
].filter((part) => part !== undefined);
|
|
1048
|
-
const headerBody = theme.bold(theme.fg('accent', 'Agents'))
|
|
1049
|
-
+ (counts.length === 0 ? '' : theme.fg('dim', ` · ${counts.join(' · ')}`))
|
|
1050
|
-
+ theme.fg('dim', launcherFocused ? ' · Enter open · Esc return' : ' · ↓ select · Alt+A open');
|
|
1051
|
-
const header = ' ' + (launcherFocused ? theme.inverse(headerBody) : headerBody);
|
|
1052
|
-
const start = subagentPreviewStart(agents);
|
|
1053
|
-
const end = Math.min(agents.length, start + SUBAGENT_PREVIEW);
|
|
1054
|
-
const rows = [
|
|
1055
|
-
...(start === 0 ? [] : [`… ${start} earlier`]),
|
|
1056
|
-
...agents.slice(start, end),
|
|
1057
|
-
...(end === agents.length ? [] : [`… ${agents.length - end} more`]),
|
|
1058
|
-
];
|
|
1059
|
-
const lines = [header, ...rows.map((row, index) => {
|
|
1060
|
-
const branch = ' ' + theme.fg('dim', index === rows.length - 1 ? '└─' : '├─') + ' ';
|
|
1061
|
-
if (typeof row === 'string')
|
|
1062
|
-
return branch + theme.fg('dim', row);
|
|
1063
|
-
const selected = inspectedId === row.id;
|
|
1064
|
-
const paint = row.phase === 'completed'
|
|
1065
|
-
? (text) => theme.fg('dim', theme.strikethrough(text))
|
|
1066
|
-
: row.phase === 'error'
|
|
1067
|
-
? (text) => theme.fg('error', text)
|
|
1068
|
-
: row.phase === 'waiting'
|
|
1069
|
-
? (text) => theme.fg('dim', text)
|
|
1070
|
-
: (text) => text;
|
|
1071
|
-
const marker = selected ? theme.fg('accent', SYMBOL.cursor) + ' ' : '';
|
|
1072
|
-
const body = marker + subagentPhaseGlyph(row.phase, theme, spinnerFrame) + ' ' + paint(subagentRowText(row));
|
|
1073
|
-
return branch + (selected ? theme.bold(body) : body);
|
|
1074
|
-
})];
|
|
1075
|
-
return lines.map(line => truncateToWidth(line, width));
|
|
1076
|
-
}
|
|
1077
|
-
/** Persistent inspect chrome: stays visible while the child transcript scrolls. */
|
|
1078
|
-
export function renderInspectBanner(inspected, theme, width, spinnerFrame = 0) {
|
|
1079
|
-
if (inspected === undefined || width <= 0)
|
|
1080
|
-
return [];
|
|
1081
|
-
const guidance = inspected.writable ? 'Enter to steer · Esc to return' : 'read-only · Esc to return';
|
|
1082
|
-
const line = ' ' + theme.fg('accent', '←') + ' ' + subagentPhaseGlyph(inspected.phase, theme, spinnerFrame)
|
|
1083
|
-
+ ' ' + theme.bold(inspected.label)
|
|
1084
|
-
+ theme.fg('dim', ` · ${guidance}`);
|
|
1085
|
-
return [truncateToWidth(line, width)];
|
|
1086
|
-
}
|
|
1087
|
-
function queuedSubmissionLabel(submission) {
|
|
1088
|
-
const text = submission.text.replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
1089
|
-
.replaceAll('\n', ' ↵ ')
|
|
1090
|
-
.replace(/\s+/gu, ' ')
|
|
1091
|
-
.trim();
|
|
1092
|
-
if (text !== '')
|
|
1093
|
-
return text;
|
|
1094
|
-
const count = submission.images.length;
|
|
1095
|
-
return count === 0 ? '(empty message)' : `${count} image${count === 1 ? '' : 's'}`;
|
|
1096
|
-
}
|
|
1097
|
-
function queuedMessageLabel(message) {
|
|
1098
|
-
const text = contentToText(message.content).replaceAll('\r\n', '\n').replaceAll('\r', '\n')
|
|
1099
|
-
.replaceAll('\n', ' ↵ ')
|
|
1100
|
-
.replace(/\s+/gu, ' ')
|
|
1101
|
-
.trim();
|
|
1102
|
-
if (text !== '')
|
|
1103
|
-
return text;
|
|
1104
|
-
const count = message.content.filter(block => block.type === 'image').length;
|
|
1105
|
-
return count === 0 ? '(empty message)' : `${count} image${count === 1 ? '' : 's'}`;
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* One fixed row above the composer for a turn that ended in failure.
|
|
1109
|
-
*
|
|
1110
|
-
* The transcript keeps its own error notice, but transcript rows scroll into
|
|
1111
|
-
* native history while the composer stays put: without this row a turn that
|
|
1112
|
-
* failed while the user was reading elsewhere leaves an apparently idle screen.
|
|
1113
|
-
* The row is bounded to a single line so it cannot squeeze the transcript.
|
|
1114
|
-
*/
|
|
1115
|
-
export function renderTurnError(text, theme, width) {
|
|
1116
|
-
if (text === '' || width <= 0)
|
|
1117
|
-
return [];
|
|
1118
|
-
const prefix = SYMBOL.error + ' ';
|
|
1119
|
-
const hint = ' · Alt+R retry';
|
|
1120
|
-
// Drop the retry hint before the message itself when the terminal is narrow.
|
|
1121
|
-
const suffix = width - 2 - visibleWidth(prefix) - visibleWidth(hint) >= 8 ? hint : '';
|
|
1122
|
-
const budget = Math.max(1, width - 2 - visibleWidth(prefix) - visibleWidth(suffix));
|
|
1123
|
-
const line = ' '
|
|
1124
|
-
+ theme.fg('error', prefix)
|
|
1125
|
-
+ theme.fg('error', truncateToWidth(text, budget))
|
|
1126
|
-
+ (suffix === '' ? '' : theme.fg('dim', suffix));
|
|
1127
|
-
return [padToWidth(line, width)];
|
|
1128
|
-
}
|
|
1129
|
-
/** Compact, unframed pending-message view placed immediately above the composer. */
|
|
1130
|
-
export function renderQueuedSubmissions(submissions, theme, width, inbox = []) {
|
|
1131
|
-
const labels = [
|
|
1132
|
-
...inbox.filter(message => message.source.kind === 'user').map(queuedMessageLabel),
|
|
1133
|
-
...submissions.map(queuedSubmissionLabel),
|
|
1134
|
-
];
|
|
1135
|
-
if (labels.length === 0 || width <= 0)
|
|
1136
|
-
return [];
|
|
1137
|
-
const start = Math.max(0, labels.length - QUEUED_SUBMISSION_PREVIEW);
|
|
1138
|
-
const hidden = start;
|
|
1139
|
-
const queueLabel = theme.bold(theme.fg('accent', 'Queued'));
|
|
1140
|
-
const rail = ' ' + theme.fg('border', '│') + ' ';
|
|
1141
|
-
const alignAction = (left, action, preserveAction = true) => {
|
|
1142
|
-
const leftWidth = visibleWidth(left);
|
|
1143
|
-
const actionWidth = visibleWidth(action);
|
|
1144
|
-
if (leftWidth + actionWidth + 2 <= width) {
|
|
1145
|
-
return left + ' '.repeat(width - leftWidth - actionWidth) + action;
|
|
1146
|
-
}
|
|
1147
|
-
if (!preserveAction || actionWidth + 4 >= width)
|
|
1148
|
-
return truncateToWidth(left, width);
|
|
1149
|
-
const paintedLeft = truncateToWidth(left, width - actionWidth - 2);
|
|
1150
|
-
return paintedLeft + ' ' + action;
|
|
1151
|
-
};
|
|
1152
|
-
if (labels.length === 1) {
|
|
1153
|
-
return [alignAction(rail + queueLabel + theme.fg('dim', ' · ') + theme.fg('text', labels[0] ?? ''), theme.fg('dim', '↑ edit'))];
|
|
1154
|
-
}
|
|
1155
|
-
const summary = ` · ${labels.length}${hidden === 0 ? '' : ` · ${hidden} earlier`}`;
|
|
1156
|
-
const visibleLabels = labels.slice(start);
|
|
1157
|
-
const lines = [
|
|
1158
|
-
alignAction(rail + queueLabel + theme.fg('dim', summary), theme.fg('dim', '↑ edit latest'), false),
|
|
1159
|
-
...visibleLabels.map((label, index) => rail + theme.fg(index === visibleLabels.length - 1 ? 'accent' : 'dim', String(start + index + 1))
|
|
1160
|
-
+ ' ' + theme.fg('text', label)),
|
|
1161
|
-
];
|
|
1162
|
-
return lines.map(line => truncateToWidth(line, width));
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Compose the welcome card, transcript, working row, and rounded editor into
|
|
1166
|
-
* one frame — the oh-my-pi surface.
|
|
1167
|
-
* @param state - transcript state.
|
|
1168
|
-
* @param options - terminal geometry and input state.
|
|
1169
|
-
* @returns the frame to hand to a renderer.
|
|
1170
|
-
*/
|
|
1171
|
-
export function renderView(state, options) {
|
|
1172
|
-
const theme = createTheme(options.colors, options.trueColor === true, options.themeName ?? 'dark');
|
|
1173
|
-
const width = options.width;
|
|
1174
|
-
const height = options.height;
|
|
1175
|
-
const appName = options.appName ?? 'omdsh';
|
|
1176
|
-
const version = options.version ?? '0.1.0';
|
|
1177
|
-
const pwd = options.pwd ?? '';
|
|
1178
|
-
const motion = options.motion ?? 'full';
|
|
1179
|
-
const spinnerFrame = motion === 'off' ? -1 : options.spinnerFrame ?? 0;
|
|
1180
|
-
if (options.agentHub !== undefined) {
|
|
1181
|
-
const hub = renderAgentHub(options.agentHub, theme, width, height);
|
|
1182
|
-
return {
|
|
1183
|
-
lines: fitFrame(hub.lines, width),
|
|
1184
|
-
cursor: hub.cursor,
|
|
1185
|
-
cursorVisible: hub.cursorVisible,
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
if (options.trajectory !== undefined) {
|
|
1189
|
-
const trajectory = renderTrajectory(options.trajectory, theme, width, height);
|
|
1190
|
-
return {
|
|
1191
|
-
lines: fitFrame(trajectory.lines, width),
|
|
1192
|
-
cursor: trajectory.cursor,
|
|
1193
|
-
cursorVisible: trajectory.cursorVisible,
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
if (options.settings !== undefined && options.promptSelector === undefined) {
|
|
1197
|
-
const settings = renderSettings(options.settings, theme, width, height);
|
|
1198
|
-
return {
|
|
1199
|
-
lines: fitFrame(settings.lines, width),
|
|
1200
|
-
cursor: settings.cursor,
|
|
1201
|
-
cursorVisible: false,
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
if (options.promptSelector?.request.presentation === 'fullscreen-list') {
|
|
1205
|
-
const selector = renderPromptSelectorPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1206
|
-
return {
|
|
1207
|
-
lines: fitFrame(selector.lines, width),
|
|
1208
|
-
cursor: selector.cursor,
|
|
1209
|
-
cursorVisible: true,
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
if (options.promptSelector?.request.presentation === 'plan-review') {
|
|
1213
|
-
const review = renderPlanReviewPage(options.promptSelector, theme, width, height, options.input, options.inputCursor, appName);
|
|
1214
|
-
return {
|
|
1215
|
-
lines: fitFrame(review.lines, width),
|
|
1216
|
-
cursor: review.cursor,
|
|
1217
|
-
cursorVisible: review.cursorVisible === true,
|
|
1218
|
-
...(review.document === undefined ? {} : { promptDocument: review.document }),
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
const welcome = renderWelcome({
|
|
1222
|
-
width,
|
|
1223
|
-
model: options.model,
|
|
1224
|
-
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
|
1225
|
-
version,
|
|
1226
|
-
appName,
|
|
1227
|
-
...(options.recentSessions === undefined ? {} : { recentSessions: options.recentSessions }),
|
|
1228
|
-
...(options.welcomeTips === undefined ? {} : { tips: options.welcomeTips }),
|
|
1229
|
-
}, theme);
|
|
1230
|
-
const transcript = renderTranscriptBody(state, options, theme, spinnerFrame);
|
|
1231
|
-
const body = [...welcome];
|
|
1232
|
-
let transcriptStart = body.length;
|
|
1233
|
-
if (transcript.lines.length > 0) {
|
|
1234
|
-
if (body.length > 0)
|
|
1235
|
-
body.push('');
|
|
1236
|
-
transcriptStart = body.length;
|
|
1237
|
-
body.push(...transcript.lines);
|
|
1238
|
-
}
|
|
1239
|
-
const working = state.status === 'running'
|
|
1240
|
-
? renderWorking(theme, Math.max(0, spinnerFrame), undefined, width, motion)
|
|
1241
|
-
: state.status === 'compacting'
|
|
1242
|
-
? renderWorking(theme, Math.max(0, spinnerFrame), 'Compacting', width, motion)
|
|
1243
|
-
: [];
|
|
1244
|
-
const statusBar = resolveStatusBarConfig(options.statusBar, options.statusPreset);
|
|
1245
|
-
const inlineHint = options.transcriptSearch !== undefined
|
|
1246
|
-
? transcriptSearchHint({
|
|
1247
|
-
query: options.transcriptSearch.query,
|
|
1248
|
-
editing: options.transcriptSearch.editing,
|
|
1249
|
-
focus: options.transcriptSearch.focus,
|
|
1250
|
-
}, options.transcriptSearch.matches.length)
|
|
1251
|
-
: options.inspected === undefined
|
|
1252
|
-
? slashInlineHint(options.input, options.inputCursor, options.commands)
|
|
1253
|
-
: options.inspected.writable
|
|
1254
|
-
? slashInlineHint(options.input, options.inputCursor, options.commands) ?? 'Enter to steer · Esc to return'
|
|
1255
|
-
: 'Read-only · Esc to return';
|
|
1256
|
-
const statusFooter = renderStatusFooter({
|
|
1257
|
-
model: options.model,
|
|
1258
|
-
...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
|
|
1259
|
-
...(options.sessionTitle === undefined || options.sessionTitle === '' ? {} : { sessionTitle: options.sessionTitle }),
|
|
1260
|
-
...(options.sessionControls === undefined ? {} : { controls: options.sessionControls }),
|
|
1261
|
-
...(options.loopStatus === undefined ? {} : { loop: options.loopStatus }),
|
|
1262
|
-
...(pwd === '' ? {} : { pwd }),
|
|
1263
|
-
...(options.branch === undefined || options.branch === '' ? {} : { branch: options.branch }),
|
|
1264
|
-
...(options.sessionStats === undefined ? {} : { stats: options.sessionStats }),
|
|
1265
|
-
config: statusBar,
|
|
1266
|
-
width,
|
|
1267
|
-
}, theme);
|
|
1268
|
-
const permissionBadge = renderPermissionBadge(options.sessionControls?.permission, theme);
|
|
1269
|
-
const editorOpts = {
|
|
1270
|
-
width,
|
|
1271
|
-
input: options.input,
|
|
1272
|
-
inputCursor: options.inputCursor,
|
|
1273
|
-
status: ' ' + theme.fg('accent', '🐳') + ' ',
|
|
1274
|
-
...(permissionBadge === '' ? {} : { statusRight: ' ' + permissionBadge + ' ' }),
|
|
1275
|
-
border: options.inspected !== undefined && options.inspected.writable !== true
|
|
1276
|
-
|| state.status === 'idle'
|
|
1277
|
-
? 'border'
|
|
1278
|
-
: 'accent',
|
|
1279
|
-
...(theme.colors && (leadingSlashCommandNameRange(options.input) !== null
|
|
1280
|
-
|| (options.inputImages !== undefined && options.inputImages !== 0))
|
|
1281
|
-
? { paintInput: (text, start) => paintComposerInputSlice(options.input, text, start, theme) }
|
|
1282
|
-
: {}),
|
|
1283
|
-
...(inlineHint !== null ? { inlineHint } : {}),
|
|
1284
|
-
};
|
|
1285
|
-
const promptSelector = options.promptSelector === undefined
|
|
1286
|
-
? undefined
|
|
1287
|
-
: renderPromptSelector(options.promptSelector, theme, width, options.input, options.inputCursor, Math.max(3, Math.min(10, height - working.length - 14)));
|
|
1288
|
-
const settings = promptSelector !== undefined || options.settings === undefined
|
|
1289
|
-
? undefined
|
|
1290
|
-
: renderSettings(options.settings, theme, width);
|
|
1291
|
-
const copySelector = promptSelector !== undefined || settings !== undefined || options.copySelector === undefined
|
|
1292
|
-
? undefined
|
|
1293
|
-
: renderCopySelector(options.copySelector, theme, width);
|
|
1294
|
-
const search = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || options.historySearch === undefined
|
|
1295
|
-
? undefined
|
|
1296
|
-
: renderHistorySearch(options.historySearch, theme, width, Math.max(1, Math.min(HISTORY_SEARCH_MAX_VISIBLE, height - working.length - 8)));
|
|
1297
|
-
const editor = promptSelector === undefined && settings === undefined && copySelector === undefined && search === undefined
|
|
1298
|
-
? renderEditor(editorOpts, theme)
|
|
1299
|
-
: undefined;
|
|
1300
|
-
const queuedSubmissions = editor === undefined || options.inspected !== undefined
|
|
1301
|
-
? []
|
|
1302
|
-
: renderQueuedSubmissions(options.queuedSubmissions ?? [], theme, width, state.nextTurnInbox);
|
|
1303
|
-
// Above the composer and below the queue: the failure explains why the queue
|
|
1304
|
-
// is not moving, and the row survives until the next submission.
|
|
1305
|
-
const turnError = editor === undefined || options.inspected !== undefined || state.turnError === undefined
|
|
1306
|
-
? []
|
|
1307
|
-
: renderTurnError(state.turnError, theme, width);
|
|
1308
|
-
const todos = editor === undefined || options.inspected !== undefined ? [] : renderTodos(state.todos, theme, width);
|
|
1309
|
-
const goal = editor === undefined || options.inspected !== undefined || options.sessionControls?.goal === undefined
|
|
1310
|
-
? []
|
|
1311
|
-
: renderGoalBar(options.sessionControls.goal, theme, width);
|
|
1312
|
-
const inspect = editor === undefined ? [] : renderInspectBanner(options.inspected, theme, width, spinnerFrame);
|
|
1313
|
-
const subagents = editor === undefined
|
|
1314
|
-
? []
|
|
1315
|
-
: renderSubagents(options.subagents, theme, width, spinnerFrame, options.inspected?.id, options.subagentLauncherFocused);
|
|
1316
|
-
const autocomplete = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || search !== undefined
|
|
1317
|
-
|| options.autocomplete === undefined
|
|
1318
|
-
? []
|
|
1319
|
-
: renderAutocomplete(options.autocomplete.items, options.autocomplete.selected, theme, width);
|
|
1320
|
-
const inputLines = promptSelector?.lines ?? settings?.lines ?? copySelector?.lines ?? search?.lines
|
|
1321
|
-
?? (editor === undefined ? [] : editor.lines);
|
|
1322
|
-
const spacer = 1;
|
|
1323
|
-
const reserved = inputLines.length + working.length + inspect.length + subagents.length + todos.length + goal.length + queuedSubmissions.length + turnError.length + spacer + autocomplete.length + statusFooter.length;
|
|
1324
|
-
const budget = Math.max(0, height - reserved);
|
|
1325
|
-
const focusStart = options.focusBlock === undefined
|
|
1326
|
-
? undefined
|
|
1327
|
-
: transcript.blockStarts[Math.max(0, Math.min(options.focusBlock, transcript.blockStarts.length - 1))];
|
|
1328
|
-
const requestedStart = focusStart === undefined
|
|
1329
|
-
? (options.scrollStart ?? Number.POSITIVE_INFINITY)
|
|
1330
|
-
: transcriptStart + focusStart;
|
|
1331
|
-
const hasOverlay = promptSelector !== undefined || settings !== undefined || copySelector !== undefined || search !== undefined;
|
|
1332
|
-
const isFollowing = requestedStart === Number.POSITIVE_INFINITY && !hasOverlay;
|
|
1333
|
-
const livePinned = state.blocks.some(block => block.kind === 'tool' && block.status === 'running');
|
|
1334
|
-
const windowed = isFollowing
|
|
1335
|
-
? undefined
|
|
1336
|
-
: windowTranscript(body, budget, requestedStart, theme);
|
|
1337
|
-
const visible = windowed?.lines ?? body;
|
|
1338
|
-
const lines = [...visible];
|
|
1339
|
-
if (visible.length > 0)
|
|
1340
|
-
lines.push('');
|
|
1341
|
-
const bottomRows = working.length + goal.length + inspect.length + subagents.length + todos.length + queuedSubmissions.length + turnError.length + inputLines.length + autocomplete.length + statusFooter.length;
|
|
1342
|
-
const fill = Math.max(0, height - lines.length - bottomRows);
|
|
1343
|
-
lines.push(...Array.from({ length: fill }, () => ''));
|
|
1344
|
-
lines.push(...working);
|
|
1345
|
-
lines.push(...goal);
|
|
1346
|
-
lines.push(...inspect);
|
|
1347
|
-
lines.push(...subagents);
|
|
1348
|
-
lines.push(...todos);
|
|
1349
|
-
lines.push(...queuedSubmissions);
|
|
1350
|
-
lines.push(...turnError);
|
|
1351
|
-
const editorStart = lines.length;
|
|
1352
|
-
lines.push(...inputLines);
|
|
1353
|
-
lines.push(...autocomplete);
|
|
1354
|
-
lines.push(...statusFooter);
|
|
1355
|
-
let liveStart;
|
|
1356
|
-
if (isFollowing) {
|
|
1357
|
-
const firstPending = state.blocks.findIndex(isBlockPending);
|
|
1358
|
-
if (firstPending >= 0) {
|
|
1359
|
-
liveStart = transcriptStart + transcript.blockStarts[firstPending];
|
|
1360
|
-
}
|
|
1361
|
-
else {
|
|
1362
|
-
liveStart = lines.length - bottomRows;
|
|
1363
|
-
}
|
|
1364
|
-
// liveStart marks the first row that is still live/mutable. Rows above it
|
|
1365
|
-
// are committed. Off-screen live rows are bounded by the renderer: it only
|
|
1366
|
-
// ever writes the live viewport, so pending rows above the visible window
|
|
1367
|
-
// are not pushed into native scrollback until they become committed.
|
|
1368
|
-
}
|
|
1369
|
-
else {
|
|
1370
|
-
liveStart = 0;
|
|
1371
|
-
}
|
|
1372
|
-
// Every settled block was already rendered against this width. Mirroring
|
|
1373
|
-
// OMP's stable-prefix preparation, only validate the mutable suffix instead
|
|
1374
|
-
// of measuring the complete native-scrollback history on every frame.
|
|
1375
|
-
const trimmed = fitFrame(lines, width, isFollowing ? liveStart : 0);
|
|
1376
|
-
const caret = promptSelector?.cursor ?? settings?.cursor ?? copySelector?.cursor ?? search?.cursor ?? editor?.cursor ?? { row: 0, column: 0 };
|
|
1377
|
-
return {
|
|
1378
|
-
lines: trimmed,
|
|
1379
|
-
cursor: {
|
|
1380
|
-
row: editorStart + caret.row,
|
|
1381
|
-
column: Math.min(caret.column, width),
|
|
1382
|
-
},
|
|
1383
|
-
cursorVisible: state.status === 'compacting'
|
|
1384
|
-
|| (options.inspected !== undefined && options.inspected.writable !== true)
|
|
1385
|
-
? false
|
|
1386
|
-
: promptSelector?.cursorVisible ?? (settings === undefined && copySelector === undefined),
|
|
1387
|
-
liveStart,
|
|
1388
|
-
livePinned,
|
|
1389
|
-
// A transient frame is either a full-screen surface or the user browsing
|
|
1390
|
-
// history; the renderer needs to tell them apart because only the first may
|
|
1391
|
-
// borrow the alternate buffer.
|
|
1392
|
-
...(liveStart === 0 ? { transientSurface: hasOverlay ? 'overlay' : 'scroll' } : {}),
|
|
1393
|
-
transcript: windowed ?? {
|
|
1394
|
-
start: 0,
|
|
1395
|
-
maxStart: 0,
|
|
1396
|
-
budget,
|
|
1397
|
-
hiddenAbove: 0,
|
|
1398
|
-
hiddenBelow: 0,
|
|
1399
|
-
},
|
|
1400
|
-
};
|
|
1401
|
-
}
|
|
449
|
+
/** View options: terminal geometry and live input state. */
|
|
1402
450
|
//# sourceMappingURL=event-views.js.map
|