@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.
Files changed (62) hide show
  1. package/lib/CHANGELOG.md +18 -1
  2. package/lib/chrome/tool-args.d.ts +9 -0
  3. package/lib/chrome/tool-args.js +20 -0
  4. package/lib/chrome/tool-args.js.map +1 -0
  5. package/lib/chrome/tool-renderers.js +4 -13
  6. package/lib/chrome/tool-renderers.js.map +1 -1
  7. package/lib/commands/session.js +2 -4
  8. package/lib/commands/session.js.map +1 -1
  9. package/lib/runtime/composer-images.d.ts +44 -0
  10. package/lib/runtime/composer-images.js +149 -0
  11. package/lib/runtime/composer-images.js.map +1 -0
  12. package/lib/runtime/herdr-agent.d.ts +95 -0
  13. package/lib/runtime/herdr-agent.js +258 -0
  14. package/lib/runtime/herdr-agent.js.map +1 -0
  15. package/lib/runtime/plain-tui.d.ts +34 -0
  16. package/lib/runtime/plain-tui.js +119 -0
  17. package/lib/runtime/plain-tui.js.map +1 -0
  18. package/lib/runtime/provider-local.d.ts +3 -0
  19. package/lib/runtime/provider-local.js +107 -268
  20. package/lib/runtime/provider-local.js.map +1 -1
  21. package/lib/runtime/tool-presentation.js +15 -0
  22. package/lib/runtime/tool-presentation.js.map +1 -1
  23. package/lib/session/content-text.d.ts +12 -0
  24. package/lib/session/content-text.js +13 -0
  25. package/lib/session/content-text.js.map +1 -0
  26. package/lib/session/history-store.js.map +1 -0
  27. package/lib/session/json-file.d.ts +5 -0
  28. package/lib/session/json-file.js +21 -0
  29. package/lib/session/json-file.js.map +1 -0
  30. package/lib/session/model-favorites.js +15 -25
  31. package/lib/session/model-favorites.js.map +1 -1
  32. package/lib/session/release-notes.js +1 -18
  33. package/lib/session/release-notes.js.map +1 -1
  34. package/lib/session/session-controller.js +64 -140
  35. package/lib/session/session-controller.js.map +1 -1
  36. package/lib/session/session-library.js +4 -14
  37. package/lib/session/session-library.js.map +1 -1
  38. package/lib/session/subagent-roster.js +13 -43
  39. package/lib/session/subagent-roster.js.map +1 -1
  40. package/lib/session/subagent-tracker.d.ts +58 -0
  41. package/lib/session/subagent-tracker.js +129 -0
  42. package/lib/session/subagent-tracker.js.map +1 -0
  43. package/lib/session/update-check.js +2 -15
  44. package/lib/session/update-check.js.map +1 -1
  45. package/lib/session/version-compare.d.ts +3 -0
  46. package/lib/session/version-compare.js +21 -0
  47. package/lib/session/version-compare.js.map +1 -0
  48. package/lib/views/event-views.d.ts +11 -270
  49. package/lib/views/event-views.js +12 -964
  50. package/lib/views/event-views.js.map +1 -1
  51. package/lib/views/settings-list.js +12 -11
  52. package/lib/views/settings-list.js.map +1 -1
  53. package/lib/views/transcript-render.d.ts +179 -0
  54. package/lib/views/transcript-render.js +916 -0
  55. package/lib/views/transcript-render.js.map +1 -0
  56. package/lib/views/transcript-types.d.ts +104 -0
  57. package/lib/views/transcript-types.js +53 -0
  58. package/lib/views/transcript-types.js.map +1 -0
  59. package/package.json +2 -1
  60. package/lib/views/history-store.js.map +0 -1
  61. /package/lib/{views → session}/history-store.d.ts +0 -0
  62. /package/lib/{views → session}/history-store.js +0 -0
@@ -1,85 +1,17 @@
1
1
  /**
2
- * Transcript state machine: SessionEvent -> display blocks, and the pure
3
- * view that composes blocks + status + input into a render frame.
4
-
5
- * applyEvent is the single writer of TranscriptState; renderView is the
6
- * single reader. windowTranscript clips the body the way OMP's ScrollView
7
- * does. Both are pure so the whole rendering pipeline is testable without
8
- * a terminal.
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 { leadingSlashCommandNameRange, renderAutocomplete, slashInlineHint } from "./autocomplete.js";
12
- import { HISTORY_SEARCH_MAX_VISIBLE, renderHistorySearch } from "./history-search.js";
13
- import { renderEditor, renderFramedBlock, renderWelcome, renderWorking } from "../chrome/box.js";
14
- import { renderMarkdown } from "../chrome/markdown.js";
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
- /** Collapsed tool-output preview height (OMP `DEFAULT_TERMINAL_PREVIEW_LINES`). */
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