( state: MakaPiTranscriptState, metadata: MakaPiTranscriptMetadata, width: number, )
| 1084 | } |
| 1085 | |
| 1086 | export function renderMakaPiTranscript( |
| 1087 | state: MakaPiTranscriptState, |
| 1088 | metadata: MakaPiTranscriptMetadata, |
| 1089 | width: number, |
| 1090 | ): string[] { |
| 1091 | const safeWidth = Math.max(1, width); |
| 1092 | const lines: string[] = []; |
| 1093 | |
| 1094 | // A fresh session (no history, nothing pending) opens on a welcome block so the |
| 1095 | // first screen greets and orients instead of showing an empty pane. Once the |
| 1096 | // first prompt lands, entries take over and it never renders again. |
| 1097 | if (state.entries.length === 0 && !state.pendingInteraction) { |
| 1098 | return renderWelcomeBlock(safeWidth, metadata.uiLocale ?? 'en'); |
| 1099 | } |
| 1100 | |
| 1101 | const entryFirstLine = new Map<MakaPiTranscriptEntry, number>(); |
| 1102 | const viewportTop = state.renderGeometry.viewportTop; |
| 1103 | for (let i = 0; i < state.entries.length; i += 1) { |
| 1104 | const entry = state.entries[i]!; |
| 1105 | if (entry.kind === 'tool' && entry.hidden) { |
| 1106 | entryFirstLine.set(entry, lines.length); |
| 1107 | continue; |
| 1108 | } |
| 1109 | const prev = state.entries[i - 1]; |
| 1110 | // A blank gap separates human-facing boundaries (user/assistant/thinking/ |
| 1111 | // notice) and the edges of a tool stack; only consecutive tool entries (the |
| 1112 | // agent-work stack) have no blank line between them. Thinking reads as |
| 1113 | // model output, so it gets the same blank-line breathing room as assistant |
| 1114 | // text rather than packing against the tool rows. |
| 1115 | const continuesStack = entry.kind === 'tool' && prev?.kind === 'tool'; |
| 1116 | if (!continuesStack) lines.push(''); |
| 1117 | entryFirstLine.set(entry, lines.length); |
| 1118 | // An entry that sits entirely above the live viewport is in terminal |
| 1119 | // scrollback — freeze its rendered lines (#1135). An entry that straddles |
| 1120 | // the boundary (first line in scrollback, tail still visible) must still |
| 1121 | // re-render: append-only entries (assistant text, tool deltas) only change |
| 1122 | // the visible tail, and pi-tui's `firstChanged` will be inside the |
| 1123 | // viewport, so no full redraw is triggered. An entry with a zero-line |
| 1124 | // cache (e.g. blank thinking) is still off-screen if its first line is |
| 1125 | // above the viewport — it must not suddenly produce lines in scrollback. |
| 1126 | const cachedLines = transcriptEntryRenderCache.get(entry); |
| 1127 | const entryHeight = cachedLines?.lines.length ?? 0; |
| 1128 | const fullyOffScreen = |
| 1129 | lines.length < viewportTop && |
| 1130 | (entryHeight === 0 || lines.length + entryHeight <= viewportTop); |
| 1131 | lines.push(...renderTranscriptEntryMemoized(entry, safeWidth, fullyOffScreen)); |
| 1132 | } |
| 1133 | state.renderGeometry.entryFirstLine = entryFirstLine; |
| 1134 | |
| 1135 | if (state.pendingInteraction?.type === 'sandbox_boundary_request') { |
| 1136 | lines.push(''); |
| 1137 | lines.push(...renderSandboxBoundaryPrompt(state.pendingInteraction, safeWidth)); |
| 1138 | } |
| 1139 | |
| 1140 | return lines; |
| 1141 | } |
| 1142 | |
| 1143 | export function completePendingInteraction( |
no test coverage detected