@agentuity/coder 1.0.37
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/README.md +57 -0
- package/dist/chain-preview.d.ts +55 -0
- package/dist/chain-preview.d.ts.map +1 -0
- package/dist/chain-preview.js +472 -0
- package/dist/chain-preview.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +402 -0
- package/dist/client.js.map +1 -0
- package/dist/commands.d.ts +22 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +99 -0
- package/dist/commands.js.map +1 -0
- package/dist/footer.d.ts +34 -0
- package/dist/footer.d.ts.map +1 -0
- package/dist/footer.js +249 -0
- package/dist/footer.js.map +1 -0
- package/dist/handlers.d.ts +24 -0
- package/dist/handlers.d.ts.map +1 -0
- package/dist/handlers.js +83 -0
- package/dist/handlers.js.map +1 -0
- package/dist/hub-overlay.d.ts +107 -0
- package/dist/hub-overlay.d.ts.map +1 -0
- package/dist/hub-overlay.js +1794 -0
- package/dist/hub-overlay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1585 -0
- package/dist/index.js.map +1 -0
- package/dist/output-viewer.d.ts +49 -0
- package/dist/output-viewer.d.ts.map +1 -0
- package/dist/output-viewer.js +389 -0
- package/dist/output-viewer.js.map +1 -0
- package/dist/overlay.d.ts +40 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +225 -0
- package/dist/overlay.js.map +1 -0
- package/dist/protocol.d.ts +118 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -0
- package/dist/remote-session.d.ts +113 -0
- package/dist/remote-session.d.ts.map +1 -0
- package/dist/remote-session.js +645 -0
- package/dist/remote-session.js.map +1 -0
- package/dist/remote-tui.d.ts +40 -0
- package/dist/remote-tui.d.ts.map +1 -0
- package/dist/remote-tui.js +606 -0
- package/dist/remote-tui.js.map +1 -0
- package/dist/renderers.d.ts +34 -0
- package/dist/renderers.d.ts.map +1 -0
- package/dist/renderers.js +669 -0
- package/dist/renderers.js.map +1 -0
- package/dist/review.d.ts +15 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +154 -0
- package/dist/review.js.map +1 -0
- package/dist/titlebar.d.ts +3 -0
- package/dist/titlebar.d.ts.map +1 -0
- package/dist/titlebar.js +59 -0
- package/dist/titlebar.js.map +1 -0
- package/dist/todo/index.d.ts +3 -0
- package/dist/todo/index.d.ts.map +1 -0
- package/dist/todo/index.js +3 -0
- package/dist/todo/index.js.map +1 -0
- package/dist/todo/store.d.ts +6 -0
- package/dist/todo/store.d.ts.map +1 -0
- package/dist/todo/store.js +43 -0
- package/dist/todo/store.js.map +1 -0
- package/dist/todo/types.d.ts +13 -0
- package/dist/todo/types.d.ts.map +1 -0
- package/dist/todo/types.js +2 -0
- package/dist/todo/types.js.map +1 -0
- package/package.json +44 -0
- package/src/chain-preview.ts +621 -0
- package/src/client.ts +515 -0
- package/src/commands.ts +132 -0
- package/src/footer.ts +305 -0
- package/src/handlers.ts +113 -0
- package/src/hub-overlay.ts +2324 -0
- package/src/index.ts +1907 -0
- package/src/output-viewer.ts +480 -0
- package/src/overlay.ts +294 -0
- package/src/protocol.ts +157 -0
- package/src/remote-session.ts +800 -0
- package/src/remote-tui.ts +707 -0
- package/src/renderers.ts +740 -0
- package/src/review.ts +201 -0
- package/src/titlebar.ts +63 -0
- package/src/todo/index.ts +2 -0
- package/src/todo/store.ts +49 -0
- package/src/todo/types.ts +14 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { type Theme, getMarkdownTheme } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { matchesKey, Markdown as MdComponent } from '@mariozechner/pi-tui';
|
|
3
|
+
import { truncateToWidth } from './renderers.ts';
|
|
4
|
+
|
|
5
|
+
export interface StoredResult {
|
|
6
|
+
agentName: string;
|
|
7
|
+
text: string;
|
|
8
|
+
thinking: string; // Accumulated thinking tokens from streaming
|
|
9
|
+
timestamp: number;
|
|
10
|
+
tokenInfo?: string; // e.g. "scout: 1200ms | 500 in 800 out | $0.0123"
|
|
11
|
+
description?: string; // Short 3-5 word task description
|
|
12
|
+
prompt?: string; // Full detailed prompt sent to the agent
|
|
13
|
+
isStreaming: boolean; // true while agent is still running
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Component {
|
|
17
|
+
render(width: number): string[];
|
|
18
|
+
handleInput?(data: string): void;
|
|
19
|
+
invalidate(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Focusable {
|
|
23
|
+
focused: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type DoneFn = (result: undefined) => void;
|
|
27
|
+
|
|
28
|
+
interface TUIRef {
|
|
29
|
+
requestRender(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
33
|
+
|
|
34
|
+
function visibleWidth(text: string): number {
|
|
35
|
+
return text.replace(ANSI_RE, '').length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function padRight(text: string, width: number): string {
|
|
39
|
+
if (width <= 0) return '';
|
|
40
|
+
const truncated = truncateToWidth(text, width);
|
|
41
|
+
const remaining = width - visibleWidth(truncated);
|
|
42
|
+
return remaining > 0 ? truncated + ' '.repeat(remaining) : truncated;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hLine(width: number): string {
|
|
46
|
+
return width > 0 ? '\u2500'.repeat(width) : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildTopBorder(width: number, title: string): string {
|
|
50
|
+
if (width <= 0) return '';
|
|
51
|
+
if (width === 1) return '\u256D';
|
|
52
|
+
if (width === 2) return '\u256D\u256E';
|
|
53
|
+
|
|
54
|
+
const inner = width - 2;
|
|
55
|
+
const titleText = ` ${title} `;
|
|
56
|
+
if (titleText.length >= inner) {
|
|
57
|
+
return `\u256D${hLine(inner)}\u256E`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const left = Math.floor((inner - titleText.length) / 2);
|
|
61
|
+
const right = inner - titleText.length - left;
|
|
62
|
+
return `\u256D${hLine(left)}${titleText}${hLine(right)}\u256E`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildBottomBorder(width: number): string {
|
|
66
|
+
if (width <= 0) return '';
|
|
67
|
+
if (width === 1) return '\u2570';
|
|
68
|
+
if (width === 2) return '\u2570\u256F';
|
|
69
|
+
return `\u2570${hLine(width - 2)}\u256F`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class OutputViewerOverlay implements Component, Focusable {
|
|
73
|
+
public focused = true;
|
|
74
|
+
|
|
75
|
+
private readonly tui: TUIRef;
|
|
76
|
+
private readonly theme: Theme;
|
|
77
|
+
private readonly results: StoredResult[];
|
|
78
|
+
private readonly done: DoneFn;
|
|
79
|
+
|
|
80
|
+
private currentIndex: number;
|
|
81
|
+
private scrollOffset = 0;
|
|
82
|
+
private disposed = false;
|
|
83
|
+
private viewMode: 'output' | 'prompt' = 'output';
|
|
84
|
+
private showThinking = false;
|
|
85
|
+
private following = true;
|
|
86
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
87
|
+
private mdRenderer: MdComponent | null = null;
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
tui: TUIRef,
|
|
91
|
+
theme: Theme,
|
|
92
|
+
results: StoredResult[],
|
|
93
|
+
done: DoneFn,
|
|
94
|
+
startIndex?: number
|
|
95
|
+
) {
|
|
96
|
+
this.tui = tui;
|
|
97
|
+
this.theme = theme;
|
|
98
|
+
this.results = results;
|
|
99
|
+
this.done = done;
|
|
100
|
+
this.currentIndex = startIndex ?? 0;
|
|
101
|
+
|
|
102
|
+
// Initialize markdown renderer
|
|
103
|
+
try {
|
|
104
|
+
const mdTheme = getMarkdownTheme?.();
|
|
105
|
+
if (mdTheme) {
|
|
106
|
+
this.mdRenderer = new MdComponent('', 0, 0, mdTheme);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Fallback: no markdown rendering
|
|
110
|
+
this.mdRenderer = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Poll for streaming updates — when viewing a streaming result, trigger re-renders
|
|
114
|
+
this.pollTimer = setInterval(() => {
|
|
115
|
+
if (this.disposed) return;
|
|
116
|
+
const current = this.results[this.currentIndex];
|
|
117
|
+
if (current?.isStreaming) {
|
|
118
|
+
this.tui.requestRender();
|
|
119
|
+
}
|
|
120
|
+
}, 100);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
handleInput(data: string): void {
|
|
124
|
+
if (this.disposed) return;
|
|
125
|
+
|
|
126
|
+
// Close overlay
|
|
127
|
+
if (matchesKey(data, 'escape')) {
|
|
128
|
+
this.close();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = this.results[this.currentIndex];
|
|
133
|
+
if (!result) {
|
|
134
|
+
this.close();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Toggle thinking visibility
|
|
139
|
+
if (matchesKey(data, 't') || data.toLowerCase() === 't') {
|
|
140
|
+
if (result?.thinking) {
|
|
141
|
+
this.showThinking = !this.showThinking;
|
|
142
|
+
this.scrollOffset = 0;
|
|
143
|
+
this.invalidate();
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Toggle follow mode (auto-scroll)
|
|
149
|
+
if (matchesKey(data, 'f') || data.toLowerCase() === 'f') {
|
|
150
|
+
this.following = !this.following;
|
|
151
|
+
if (this.following) {
|
|
152
|
+
const lines = this.getContentLines();
|
|
153
|
+
const budget = this.getContentBudget();
|
|
154
|
+
this.scrollOffset = Math.max(0, lines.length - budget);
|
|
155
|
+
}
|
|
156
|
+
this.invalidate();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Toggle between output and prompt views
|
|
161
|
+
if (matchesKey(data, 'p') || data.toLowerCase() === 'p') {
|
|
162
|
+
if (this.viewMode === 'output' && result?.prompt) {
|
|
163
|
+
this.viewMode = 'prompt';
|
|
164
|
+
} else {
|
|
165
|
+
this.viewMode = 'output';
|
|
166
|
+
}
|
|
167
|
+
this.scrollOffset = 0;
|
|
168
|
+
this.invalidate();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const contentLines = this.getContentLines();
|
|
173
|
+
const contentBudget = this.getContentBudget();
|
|
174
|
+
const maxScroll = Math.max(0, contentLines.length - contentBudget);
|
|
175
|
+
const halfPage = Math.max(1, Math.floor(contentBudget / 2));
|
|
176
|
+
|
|
177
|
+
// Vim: g = top, G = bottom
|
|
178
|
+
if (data === 'g') {
|
|
179
|
+
this.following = false;
|
|
180
|
+
this.scrollOffset = 0;
|
|
181
|
+
this.invalidate();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (data === 'G') {
|
|
186
|
+
this.following = false;
|
|
187
|
+
this.scrollOffset = maxScroll;
|
|
188
|
+
this.invalidate();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Vim: { = jump back 25%, } = jump forward 25%
|
|
193
|
+
if (data === '{') {
|
|
194
|
+
this.following = false;
|
|
195
|
+
const jump = Math.max(1, Math.floor(contentLines.length * 0.25));
|
|
196
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - jump);
|
|
197
|
+
this.invalidate();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (data === '}') {
|
|
202
|
+
this.following = false;
|
|
203
|
+
const jump = Math.max(1, Math.floor(contentLines.length * 0.25));
|
|
204
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + jump);
|
|
205
|
+
this.invalidate();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Navigate between results
|
|
210
|
+
if (matchesKey(data, 'left')) {
|
|
211
|
+
if (this.results.length > 1) {
|
|
212
|
+
this.currentIndex = (this.currentIndex + 1) % this.results.length;
|
|
213
|
+
this.scrollOffset = 0;
|
|
214
|
+
this.viewMode = 'output';
|
|
215
|
+
this.showThinking = false;
|
|
216
|
+
this.invalidate();
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (matchesKey(data, 'right')) {
|
|
222
|
+
if (this.results.length > 1) {
|
|
223
|
+
this.currentIndex = (this.currentIndex - 1 + this.results.length) % this.results.length;
|
|
224
|
+
this.scrollOffset = 0;
|
|
225
|
+
this.viewMode = 'output';
|
|
226
|
+
this.showThinking = false;
|
|
227
|
+
this.invalidate();
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Scroll content — manual scroll disables following
|
|
233
|
+
if (matchesKey(data, 'up')) {
|
|
234
|
+
if (this.scrollOffset > 0) {
|
|
235
|
+
this.following = false;
|
|
236
|
+
this.scrollOffset -= 1;
|
|
237
|
+
this.invalidate();
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (matchesKey(data, 'down')) {
|
|
243
|
+
if (this.scrollOffset < maxScroll) {
|
|
244
|
+
this.following = false;
|
|
245
|
+
this.scrollOffset += 1;
|
|
246
|
+
this.invalidate();
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Page up (Shift+Up or PageUp)
|
|
252
|
+
if (matchesKey(data, 'shift+up') || matchesKey(data, 'pageUp')) {
|
|
253
|
+
this.following = false;
|
|
254
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - halfPage);
|
|
255
|
+
this.invalidate();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Page down (Shift+Down or PageDown)
|
|
260
|
+
if (matchesKey(data, 'shift+down') || matchesKey(data, 'pageDown')) {
|
|
261
|
+
this.following = false;
|
|
262
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + halfPage);
|
|
263
|
+
this.invalidate();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Home — jump to top
|
|
268
|
+
if (matchesKey(data, 'home')) {
|
|
269
|
+
this.following = false;
|
|
270
|
+
this.scrollOffset = 0;
|
|
271
|
+
this.invalidate();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// End — jump to bottom
|
|
276
|
+
if (matchesKey(data, 'end')) {
|
|
277
|
+
this.following = false;
|
|
278
|
+
this.scrollOffset = maxScroll;
|
|
279
|
+
this.invalidate();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
render(width: number): string[] {
|
|
285
|
+
const safeWidth = Math.max(4, width);
|
|
286
|
+
const inner = Math.max(0, safeWidth - 2);
|
|
287
|
+
const termHeight = process.stdout.rows || 40;
|
|
288
|
+
// Match overlay maxHeight of 95%, leave margin for overlay chrome
|
|
289
|
+
const maxLines = Math.max(10, Math.floor(termHeight * 0.95) - 2);
|
|
290
|
+
|
|
291
|
+
const result = this.results[this.currentIndex];
|
|
292
|
+
if (!result) {
|
|
293
|
+
const lines = [
|
|
294
|
+
buildTopBorder(safeWidth, 'Output Viewer'),
|
|
295
|
+
this.contentLine(this.theme.fg('muted', ' No results available'), inner),
|
|
296
|
+
this.contentLine(this.theme.fg('dim', ' [Esc] Close'), inner),
|
|
297
|
+
buildBottomBorder(safeWidth),
|
|
298
|
+
];
|
|
299
|
+
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Build header title with streaming indicator
|
|
303
|
+
const streamLabel = result.isStreaming ? ' LIVE' : '';
|
|
304
|
+
const nameLabel = result.description
|
|
305
|
+
? `${result.agentName} - ${result.description}`
|
|
306
|
+
: result.agentName;
|
|
307
|
+
const posLabel =
|
|
308
|
+
this.results.length > 1
|
|
309
|
+
? `${nameLabel}${streamLabel} (${this.currentIndex + 1} of ${this.results.length})`
|
|
310
|
+
: `${nameLabel}${streamLabel}`;
|
|
311
|
+
const titleLabel = this.viewMode === 'prompt' ? `${posLabel} [PROMPT]` : posLabel;
|
|
312
|
+
|
|
313
|
+
const header: string[] = [buildTopBorder(safeWidth, titleLabel)];
|
|
314
|
+
|
|
315
|
+
// Sub-header: token info or prompt-mode indicator
|
|
316
|
+
if (this.viewMode === 'prompt') {
|
|
317
|
+
header.push(this.contentLine(this.theme.fg('dim', ' Prompt sent to agent:'), inner));
|
|
318
|
+
} else if (result.tokenInfo) {
|
|
319
|
+
header.push(this.contentLine(this.theme.fg('dim', ` ${result.tokenInfo}`), inner));
|
|
320
|
+
} else {
|
|
321
|
+
header.push(this.contentLine('', inner));
|
|
322
|
+
}
|
|
323
|
+
header.push(this.contentLine('', inner)); // padding line after header
|
|
324
|
+
|
|
325
|
+
// Footer with position indicator and new hints
|
|
326
|
+
const thinkingHint = result.thinking
|
|
327
|
+
? `[t] ${this.showThinking ? 'Hide' : 'Show'} thinking `
|
|
328
|
+
: '';
|
|
329
|
+
const followHint = result.isStreaming
|
|
330
|
+
? `[f] ${this.following ? 'Unfollow' : 'Follow'} `
|
|
331
|
+
: '';
|
|
332
|
+
const promptHint = result.prompt ? '[p] Prompt ' : '';
|
|
333
|
+
const navHint = this.results.length > 1 ? '[<- ->] Switch ' : '';
|
|
334
|
+
|
|
335
|
+
// Content assembly via helper
|
|
336
|
+
const contentLines = this.getContentLines();
|
|
337
|
+
const totalLines = contentLines.length;
|
|
338
|
+
|
|
339
|
+
// Content area
|
|
340
|
+
const contentBudget = Math.max(1, maxLines - header.length - 3); // 3 = footer lines (padding + help + border)
|
|
341
|
+
|
|
342
|
+
// Auto-follow when streaming
|
|
343
|
+
if (this.following && result.isStreaming) {
|
|
344
|
+
this.scrollOffset = Math.max(0, totalLines - contentBudget);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const maxScroll = Math.max(0, totalLines - contentBudget);
|
|
348
|
+
|
|
349
|
+
// Clamp scroll offset
|
|
350
|
+
if (this.scrollOffset > maxScroll) {
|
|
351
|
+
this.scrollOffset = maxScroll;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Position indicator
|
|
355
|
+
const currentLine = Math.min(this.scrollOffset + 1, totalLines);
|
|
356
|
+
const pct = totalLines > 0 ? Math.round((currentLine / totalLines) * 100) : 0;
|
|
357
|
+
const posInfo = totalLines > 0 ? `L${currentLine}/${totalLines} ${pct}% ` : '';
|
|
358
|
+
|
|
359
|
+
const vimHint = totalLines > contentBudget ? '[g/G] Top/Bot [{/}] Jump ' : '';
|
|
360
|
+
const footer: string[] = [
|
|
361
|
+
this.contentLine('', inner), // padding line before footer
|
|
362
|
+
this.contentLine(
|
|
363
|
+
this.theme.fg(
|
|
364
|
+
'dim',
|
|
365
|
+
` ${posInfo}${vimHint}[Up/Dn] Scroll ${thinkingHint}${followHint}${promptHint}${navHint}[Esc] Close`
|
|
366
|
+
),
|
|
367
|
+
inner
|
|
368
|
+
),
|
|
369
|
+
buildBottomBorder(safeWidth),
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const content: string[] = [];
|
|
373
|
+
|
|
374
|
+
// Scroll indicator: above
|
|
375
|
+
const aboveCount = this.scrollOffset;
|
|
376
|
+
if (aboveCount > 0) {
|
|
377
|
+
content.push(
|
|
378
|
+
this.contentLine(this.theme.fg('dim', ` ^ ${aboveCount} more above`), inner)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Visible lines
|
|
383
|
+
const visibleBudget =
|
|
384
|
+
aboveCount > 0
|
|
385
|
+
? contentBudget - 1 // reserve 1 line for "above" indicator
|
|
386
|
+
: contentBudget;
|
|
387
|
+
const belowCount = totalLines - this.scrollOffset - visibleBudget;
|
|
388
|
+
const actualVisible = belowCount > 0 ? visibleBudget - 1 : visibleBudget; // reserve 1 for "below"
|
|
389
|
+
|
|
390
|
+
const sliceEnd = Math.min(this.scrollOffset + actualVisible, totalLines);
|
|
391
|
+
for (let i = this.scrollOffset; i < sliceEnd; i++) {
|
|
392
|
+
const line = contentLines[i] ?? '';
|
|
393
|
+
content.push(
|
|
394
|
+
this.contentLine(' ' + truncateToWidth(line, Math.max(0, inner - 3)), inner)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Scroll indicator: below
|
|
399
|
+
const remainingBelow = totalLines - sliceEnd;
|
|
400
|
+
if (remainingBelow > 0) {
|
|
401
|
+
content.push(
|
|
402
|
+
this.contentLine(this.theme.fg('dim', ` v ${remainingBelow} more below`), inner)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const lines = [...header, ...content, ...footer];
|
|
407
|
+
return lines.map((line) => truncateToWidth(line, safeWidth));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
invalidate(): void {
|
|
411
|
+
// Stateless rendering; no cache invalidation required.
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
dispose(): void {
|
|
415
|
+
this.disposed = true;
|
|
416
|
+
if (this.pollTimer) {
|
|
417
|
+
clearInterval(this.pollTimer);
|
|
418
|
+
this.pollTimer = null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private getContentLines(): string[] {
|
|
423
|
+
const result = this.results[this.currentIndex];
|
|
424
|
+
if (!result) return [];
|
|
425
|
+
|
|
426
|
+
const lines: string[] = [];
|
|
427
|
+
|
|
428
|
+
// Thinking section (dimmed) — keep as plain text, it's raw thinking
|
|
429
|
+
if (this.showThinking && result.thinking) {
|
|
430
|
+
const thinkingLines = result.thinking.split('\n');
|
|
431
|
+
for (const line of thinkingLines) {
|
|
432
|
+
lines.push(this.theme.fg('dim', line));
|
|
433
|
+
}
|
|
434
|
+
lines.push(this.theme.fg('muted', '--- end thinking ---'));
|
|
435
|
+
lines.push(''); // empty line separator
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Main content — render as markdown for syntax highlighting
|
|
439
|
+
const mainText = this.viewMode === 'prompt' && result.prompt ? result.prompt : result.text;
|
|
440
|
+
if (mainText) {
|
|
441
|
+
if (this.mdRenderer) {
|
|
442
|
+
try {
|
|
443
|
+
this.mdRenderer.setText(mainText);
|
|
444
|
+
// Render with inner width minus padding (3 spaces left + border chars)
|
|
445
|
+
const termWidth = Math.max(20, (process.stdout.columns || 80) - 10);
|
|
446
|
+
const rendered = this.mdRenderer.render(termWidth);
|
|
447
|
+
lines.push(...rendered);
|
|
448
|
+
} catch {
|
|
449
|
+
// Fallback: plain text if markdown rendering fails
|
|
450
|
+
lines.push(...mainText.split('\n'));
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// Fallback: plain text
|
|
454
|
+
lines.push(...mainText.split('\n'));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return lines;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private getContentBudget(): number {
|
|
462
|
+
const termHeight = process.stdout.rows || 40;
|
|
463
|
+
const maxLines = Math.max(10, Math.floor(termHeight * 0.95) - 2);
|
|
464
|
+
return Math.max(1, maxLines - 4); // 4 = header + footer lines
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private contentLine(content: string, innerWidth: number): string {
|
|
468
|
+
return `\u2502${padRight(content, innerWidth)}\u2502`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private close(): void {
|
|
472
|
+
if (this.disposed) return;
|
|
473
|
+
this.disposed = true;
|
|
474
|
+
if (this.pollTimer) {
|
|
475
|
+
clearInterval(this.pollTimer);
|
|
476
|
+
this.pollTimer = null;
|
|
477
|
+
}
|
|
478
|
+
this.done(undefined);
|
|
479
|
+
}
|
|
480
|
+
}
|