@zhijiewang/openharness 2.5.0 → 2.8.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.
@@ -1,489 +1,15 @@
1
1
  /**
2
2
  * Layout engine — rasterizes application state into a CellGrid.
3
3
  * Split screen: messages area (top) + footer (bottom).
4
+ *
5
+ * Section renderers are in layout-sections.ts.
6
+ * This file contains types and the two main rasterization functions.
4
7
  */
5
- import { getTheme } from "../utils/theme-data.js";
6
- import { renderDiff } from "./diff.js";
7
- import { isImageOutput, renderImageInline } from "./image.js";
8
+ import { computeCursorPosition, ensureStyles, getPromptText, renderAutocompleteSection, renderBannerSection, renderCompanionSection, renderContextWarningSection, renderErrorSection, renderInputSection, renderNotificationsSection, renderPermissionBoxSection, renderQuestionPromptSection, renderSpinnerSection, renderStatusLineSection, renderThinkingSection, renderThinkingSummarySection, renderToolCallsSection, S_ASSISTANT, S_BANNER, S_BANNER_DIM, S_BORDER, S_DIM, S_ERROR, S_TEXT, S_USER, } from "./layout-sections.js";
8
9
  import { measureMarkdown, renderMarkdown } from "./markdown.js";
9
10
  import { renderSessionBrowser } from "./session-browser.js";
10
- // ── Style constants ──
11
- const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
12
- const S_TEXT = s(null);
13
- const S_DIM = s(null, false, true);
14
- const S_BORDER = s(null, false, true);
15
- const S_BRIGHT = s(null);
16
- const S_BANNER = s("cyan");
17
- const S_BANNER_DIM = s(null, false, true);
18
- const S_AGENT = s("cyan", true);
19
- const S_KEY_GREEN = s("green", true);
20
- const S_KEY_RED = s("red", true);
21
- const S_KEY_CYAN = s("cyan", true);
22
- // Theme-dependent styles — lazily initialized on first rasterize() call
23
- let S_USER;
24
- let S_ASSISTANT;
25
- let S_ERROR;
26
- let S_YELLOW;
27
- let S_GREEN;
28
- let _stylesInit = false;
29
- /** Reset style cache — call after theme change */
30
- export function resetStyleCache() {
31
- _stylesInit = false;
32
- }
33
- function ensureStyles() {
34
- if (_stylesInit)
35
- return;
36
- _stylesInit = true;
37
- const t = getTheme();
38
- S_USER = s(t.user, true);
39
- S_ASSISTANT = s(t.assistant, true);
40
- S_ERROR = s(t.error);
41
- S_YELLOW = s(t.tool);
42
- S_GREEN = s(t.success);
43
- }
44
- const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
45
- // ── Shared rendering helpers ──
46
- // Each takes (state, grid, row, limit, ...options) and returns next row.
47
- function renderBannerSection(state, grid, r, limit, opts) {
48
- if (!state.bannerLines)
49
- return r;
50
- const startLine = opts.compact ? Math.max(0, state.bannerLines.length - 2) : 0;
51
- for (let i = startLine; i < state.bannerLines.length; i++) {
52
- if (r >= limit)
53
- break;
54
- const line = state.bannerLines[i];
55
- const isBannerArt = i < state.bannerLines.length - 2;
56
- grid.writeText(r, 0, line, isBannerArt ? S_BANNER : S_BANNER_DIM);
57
- r++;
58
- }
59
- if (r < limit)
60
- r++; // blank line after banner
61
- return r;
62
- }
63
- function renderThinkingSection(state, grid, r, limit) {
64
- if (!state.thinkingText || r >= limit)
65
- return r;
66
- const w = grid.width;
67
- if (state.thinkingExpanded) {
68
- const thinkLines = state.thinkingText.split("\n").slice(-10);
69
- const shimmerPos = state.spinnerFrame % 20;
70
- for (const tLine of thinkLines) {
71
- if (r >= limit)
72
- break;
73
- grid.writeText(r, 0, "💭 ", S_DIM);
74
- const chars = [...tLine];
75
- for (let ci = 0; ci < chars.length && ci + 3 < w; ci++) {
76
- grid.setCell(r, 3 + ci, chars[ci], Math.abs(ci - shimmerPos) <= 2 ? S_BRIGHT : S_DIM);
77
- }
78
- r++;
79
- }
80
- }
81
- else {
82
- const lineCount = state.thinkingText.split("\n").length;
83
- const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
84
- const summary = `∴ Thinking${elapsed > 0 ? ` (${elapsed}s)` : ""} — ${lineCount} lines [Ctrl+O expand]`;
85
- grid.writeText(r, 0, summary, S_DIM);
86
- r++;
87
- }
88
- return r;
89
- }
90
- function renderThinkingSummarySection(state, grid, r, limit) {
91
- if (state.loading || !state.lastThinkingSummary || r >= limit)
92
- return r;
93
- grid.writeText(r, 0, state.lastThinkingSummary, S_DIM);
94
- return r + 1;
95
- }
96
- function renderSpinnerSection(state, grid, r, limit) {
97
- if (!state.loading || state.streamingText || state.thinkingText || r >= limit)
98
- return r;
99
- const _w = grid.width;
100
- const thinkText = "Thinking";
101
- const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
102
- const t = getTheme();
103
- const baseColor = elapsed > 60 ? t.error : elapsed > 30 ? t.stall : t.primary;
104
- const shimmerColor = elapsed > 60 ? t.stallShimmer : elapsed > 30 ? t.warning : t.primaryShimmer;
105
- const baseStyle = { fg: baseColor, bg: null, bold: false, dim: false, underline: false };
106
- grid.writeText(r, 0, "◆ ", { ...baseStyle, bold: true });
107
- const shimmerPos = state.spinnerFrame % (thinkText.length + 6);
108
- const shimmerStyle = { fg: shimmerColor, bg: null, bold: true, dim: false, underline: false };
109
- for (let ci = 0; ci < thinkText.length; ci++) {
110
- grid.setCell(r, 2 + ci, thinkText[ci], Math.abs(ci - shimmerPos) <= 1 ? shimmerStyle : baseStyle);
111
- }
112
- let suffix = "";
113
- if (elapsed > 0)
114
- suffix += ` ${elapsed}s`;
115
- if (state.tokenCount > 0) {
116
- const tokStr = state.tokenCount >= 1000 ? `${(state.tokenCount / 1000).toFixed(1)}K` : `${state.tokenCount}`;
117
- suffix += ` | ${tokStr} tokens`;
118
- }
119
- suffix += "...";
120
- grid.writeText(r, 2 + thinkText.length, suffix, S_DIM);
121
- return r + 1;
122
- }
123
- function renderErrorSection(state, grid, r, limit) {
124
- if (!state.errorText || r >= limit)
125
- return r;
126
- const w = grid.width;
127
- grid.writeText(r, 0, "✗ ", S_ERROR);
128
- grid.writeText(r, 2, state.errorText.slice(0, w - 4), S_ERROR);
129
- return r + 1;
130
- }
131
- function renderToolCallsSection(state, grid, r, limit, opts) {
132
- const w = grid.width;
133
- for (const [callId, tc] of state.toolCalls) {
134
- if (r >= limit)
135
- break;
136
- const isAgent = tc.isAgent || tc.toolName === "Agent" || tc.toolName === "ParallelAgents";
137
- const icon = isAgent
138
- ? tc.status === "running"
139
- ? "⊕"
140
- : tc.status === "done"
141
- ? "◈"
142
- : "◇"
143
- : tc.status === "running"
144
- ? SPINNER_CHARS[state.spinnerFrame % SPINNER_CHARS.length]
145
- : tc.status === "done"
146
- ? "✓"
147
- : "✗";
148
- const statusStyle = tc.status === "error" ? S_ERROR : tc.status === "done" ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
149
- const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
150
- const isExpanded = state.expandedToolCalls.has(callId);
151
- const canExpand = tc.status !== "running" && tc.output;
152
- // Collapse/expand indicator
153
- if (canExpand) {
154
- grid.writeText(r, 0, isExpanded ? "▼" : "▶", S_DIM);
155
- }
156
- grid.writeText(r, 2, `${icon} `, statusStyle);
157
- grid.writeText(r, 4, tc.toolName, nameStyle);
158
- let afterName = 4 + tc.toolName.length + 1;
159
- if (tc.args) {
160
- const maxArgs = w - afterName - 15;
161
- if (maxArgs > 5) {
162
- const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? "…" : "");
163
- grid.writeText(r, afterName, argsText, S_DIM);
164
- afterName += argsText.length + 1;
165
- }
166
- }
167
- // Elapsed time for running tools
168
- if (tc.status === "running" && tc.startedAt) {
169
- const elapsed = Math.floor((Date.now() - tc.startedAt) / 1000);
170
- if (elapsed > 0) {
171
- const lineCount = tc.liveOutput?.length ?? 0;
172
- const elapsedStr = lineCount > 0 ? `${elapsed}s · ${lineCount} lines` : `${elapsed}s`;
173
- grid.writeText(r, Math.min(afterName, w - elapsedStr.length - 2), elapsedStr, S_DIM);
174
- }
175
- }
176
- // Result summary for completed tools
177
- if (tc.status !== "running" && tc.resultSummary) {
178
- const elapsed = tc.startedAt ? Math.floor((Date.now() - tc.startedAt) / 1000) : 0;
179
- const suffix = elapsed > 0 ? `${tc.resultSummary} · ${elapsed}s` : tc.resultSummary;
180
- grid.writeText(r, Math.min(afterName, w - suffix.length - 2), suffix, S_DIM);
181
- }
182
- r++;
183
- // Agent description line
184
- if (isAgent && tc.agentDescription && r < limit) {
185
- grid.writeText(r, 6, tc.agentDescription.slice(0, w - 8), S_DIM);
186
- r++;
187
- }
188
- // Live streaming output while running
189
- if (tc.status === "running" && tc.liveOutput && tc.liveOutput.length > 0) {
190
- const overflow = tc.liveOutput.length > opts.maxLiveLines ? tc.liveOutput.length - opts.maxLiveLines : 0;
191
- if (opts.showOverflow && overflow > 0 && r < limit) {
192
- grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
193
- r++;
194
- }
195
- const visible = overflow > 0 ? tc.liveOutput.slice(-opts.maxLiveLines) : tc.liveOutput;
196
- for (const line of visible) {
197
- if (r >= limit)
198
- break;
199
- grid.writeText(r, 6, line.slice(0, w - 8), S_DIM);
200
- r++;
201
- }
202
- }
203
- // Final output — collapsed by default (only show when expanded via Tab)
204
- if (tc.output && tc.status !== "running" && isExpanded && r < limit) {
205
- // Image results: show inline placeholder
206
- if (isImageOutput(tc.output)) {
207
- const label = renderImageInline(tc.output);
208
- grid.writeText(r, 6, label.slice(0, w - 8), S_DIM);
209
- r++;
210
- continue;
211
- }
212
- const outLines = tc.output.split("\n");
213
- const maxOut = 20;
214
- const showLines = outLines.slice(0, maxOut);
215
- for (const line of showLines) {
216
- if (r >= limit)
217
- break;
218
- const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
219
- grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
220
- r++;
221
- }
222
- if (outLines.length > maxOut && r < limit) {
223
- grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
224
- r++;
225
- }
226
- }
227
- }
228
- return r;
229
- }
230
- function renderContextWarningSection(state, grid, r, limit) {
231
- if (!state.contextWarning || r >= limit)
232
- return r;
233
- const warnStyle = {
234
- fg: "yellow",
235
- bg: null,
236
- bold: state.contextWarning.critical,
237
- dim: false,
238
- underline: false,
239
- };
240
- grid.writeText(r, 0, state.contextWarning.text, warnStyle);
241
- return r + 1;
242
- }
243
- function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
244
- if (!state.permissionBox || grid.width < 20)
245
- return nextRow;
246
- const w = grid.width;
247
- const { toolName, description, riskLevel } = state.permissionBox;
248
- const riskColor = riskLevel === "high" ? "red" : riskLevel === "medium" ? "yellow" : "green";
249
- const riskStyle = { fg: riskColor, bg: null, bold: true, dim: false, underline: false };
250
- if (opts.boxed) {
251
- if (h - nextRow < 6)
252
- return nextRow;
253
- const riskDim = { fg: riskColor, bg: null, bold: false, dim: true, underline: false };
254
- const boxWidth = Math.max(15, Math.min(w - 2, 70));
255
- // Top border
256
- grid.writeText(nextRow, 1, `╭${"─".repeat(boxWidth - 2)}╮`, riskDim);
257
- nextRow++;
258
- // Tool name + risk
259
- grid.writeText(nextRow, 1, "│ ", riskDim);
260
- grid.writeText(nextRow, 3, "⚠ ", riskStyle);
261
- grid.writeText(nextRow, 5, toolName, { ...riskStyle });
262
- grid.writeText(nextRow, 5 + toolName.length, ` ${riskLevel} risk`, S_DIM);
263
- grid.writeText(nextRow, boxWidth, "│", riskDim);
264
- nextRow++;
265
- // Description (truncated)
266
- const rawDesc = state.permissionBox.suggestion || description.slice(0, boxWidth - 6);
267
- const descText = rawDesc.replace(/\|/g, " ").replace(/\\/g, "/");
268
- grid.writeText(nextRow, 1, "│ ", riskDim);
269
- grid.writeText(nextRow, 3, descText.slice(0, boxWidth - 4), S_DIM);
270
- grid.writeText(nextRow, boxWidth, "│", riskDim);
271
- nextRow++;
272
- // Inline diff
273
- if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
274
- grid.writeText(nextRow, 1, "│", riskDim);
275
- nextRow++;
276
- const availDiffRows = Math.min(opts.maxDiffHeight, h - nextRow - 3);
277
- const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, boxWidth - 2, availDiffRows);
278
- for (let dr = 0; dr < diffRows; dr++) {
279
- if (nextRow + dr < grid.height) {
280
- grid.setCell(nextRow + dr, 1, "│", riskDim);
281
- grid.setCell(nextRow + dr, boxWidth, "│", riskDim);
282
- }
283
- }
284
- nextRow += diffRows;
285
- }
286
- // Action keys
287
- grid.writeText(nextRow, 1, "│ ", riskDim);
288
- let kc = 3;
289
- grid.writeText(nextRow, kc, "Y", S_KEY_GREEN);
290
- kc += 1;
291
- grid.writeText(nextRow, kc, "es", S_DIM);
292
- kc += 2;
293
- grid.writeText(nextRow, kc, " ", S_DIM);
294
- kc += 2;
295
- grid.writeText(nextRow, kc, "N", S_KEY_RED);
296
- kc += 1;
297
- grid.writeText(nextRow, kc, "o", S_DIM);
298
- kc += 1;
299
- if (state.permissionDiffInfo) {
300
- grid.writeText(nextRow, kc, " ", S_DIM);
301
- kc += 2;
302
- grid.writeText(nextRow, kc, "D", S_KEY_CYAN);
303
- kc += 1;
304
- grid.writeText(nextRow, kc, "iff", S_DIM);
305
- kc += 3;
306
- }
307
- grid.writeText(nextRow, boxWidth, "│", riskDim);
308
- nextRow++;
309
- // Bottom border
310
- grid.writeText(nextRow, 1, `╰${"─".repeat(boxWidth - 2)}╯`, riskDim);
311
- nextRow++;
312
- }
313
- else {
314
- // Compact mode (rasterizeLive)
315
- if (h - nextRow < 4)
316
- return nextRow;
317
- grid.writeText(nextRow, 1, `⚠ ${toolName} (${riskLevel} risk)`, riskStyle);
318
- nextRow++;
319
- grid.writeText(nextRow, 1, "Y", S_KEY_GREEN);
320
- grid.writeText(nextRow, 2, "es ", S_DIM);
321
- grid.writeText(nextRow, 6, "N", S_KEY_RED);
322
- grid.writeText(nextRow, 7, "o", S_DIM);
323
- if (state.permissionDiffInfo) {
324
- grid.writeText(nextRow, 10, "D", S_KEY_CYAN);
325
- grid.writeText(nextRow, 11, "iff", S_DIM);
326
- }
327
- nextRow++;
328
- // Inline diff (when toggled)
329
- if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
330
- const availDiffRows = Math.min(15, h - nextRow - 3);
331
- const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, Math.min(w - 2, 70), availDiffRows);
332
- nextRow += diffRows;
333
- }
334
- }
335
- return nextRow;
336
- }
337
- function renderQuestionPromptSection(state, grid, nextRow, h, opts) {
338
- if (!state.questionPrompt || grid.width < 20)
339
- return { nextRow, questionInputRow: -1 };
340
- const w = grid.width;
341
- const { question, options, input, cursor } = state.questionPrompt;
342
- const qStyle = { fg: "yellow", bg: null, bold: false, dim: false, underline: false };
343
- if (opts.boxed) {
344
- const qBorder = { fg: "yellow", bg: null, bold: false, dim: true, underline: false };
345
- const qBoxWidth = Math.max(15, Math.min(w - 2, 70));
346
- grid.writeText(nextRow, 1, `╭${"─".repeat(qBoxWidth - 2)}╮`, qBorder);
347
- nextRow++;
348
- grid.writeText(nextRow, 1, "│ ", qBorder);
349
- grid.writeText(nextRow, 3, `❓ ${question}`, qStyle);
350
- grid.writeText(nextRow, qBoxWidth, "│", qBorder);
351
- nextRow++;
352
- if (options && options.length > 0) {
353
- for (let oi = 0; oi < options.length; oi++) {
354
- grid.writeText(nextRow, 1, "│ ", qBorder);
355
- grid.writeText(nextRow, 5, `${oi + 1}. ${options[oi]}`, S_DIM);
356
- grid.writeText(nextRow, qBoxWidth, "│", qBorder);
357
- nextRow++;
358
- }
359
- }
360
- const questionInputRow = nextRow;
361
- grid.writeText(nextRow, 1, "│ ", qBorder);
362
- grid.writeText(nextRow, 3, "❯ ", qStyle);
363
- grid.writeText(nextRow, 5, input, S_TEXT);
364
- grid.writeText(nextRow, qBoxWidth, "│", qBorder);
365
- nextRow++;
366
- grid.writeText(nextRow, 1, `╰${"─".repeat(qBoxWidth - 2)}╯`, qBorder);
367
- nextRow++;
368
- return { nextRow, questionInputRow };
369
- }
370
- else {
371
- // Compact mode (rasterizeLive)
372
- if (h - nextRow < 3)
373
- return { nextRow, questionInputRow: -1 };
374
- grid.writeText(nextRow, 1, `❓ ${question}`, S_TEXT);
375
- nextRow++;
376
- if (options) {
377
- for (const opt of options) {
378
- if (nextRow >= h)
379
- break;
380
- grid.writeText(nextRow, 3, opt, S_DIM);
381
- nextRow++;
382
- }
383
- }
384
- const questionInputRow = nextRow;
385
- grid.writeText(nextRow, 1, "❯ ", S_USER);
386
- grid.writeText(nextRow, 3, input, S_TEXT);
387
- nextRow++;
388
- return { nextRow, questionInputRow };
389
- }
390
- }
391
- function renderStatusLineSection(state, grid, nextRow, limit) {
392
- if (!state.statusLine || nextRow >= limit)
393
- return nextRow;
394
- grid.writeText(nextRow, 0, state.statusLine, S_DIM);
395
- return nextRow + 1;
396
- }
397
- function renderAutocompleteSection(state, grid, nextRow, limit, promptWidth) {
398
- if (state.autocomplete.length === 0)
399
- return nextRow;
400
- const w = grid.width;
401
- for (let ai = 0; ai < state.autocomplete.length; ai++) {
402
- if (nextRow >= limit)
403
- break;
404
- const cmd = state.autocomplete[ai];
405
- const desc = state.autocompleteDescriptions[ai] ?? "";
406
- const selected = ai === state.autocompleteIndex;
407
- const acStyle = selected ? s(getTheme().user, true) : s(null, false, true);
408
- grid.writeText(nextRow, promptWidth, `/${cmd.padEnd(12)}`, acStyle);
409
- if (desc && w > promptWidth + 15)
410
- grid.writeText(nextRow, promptWidth + 13, desc.slice(0, w - promptWidth - 15), S_DIM);
411
- nextRow++;
412
- }
413
- return nextRow;
414
- }
415
- function renderNotificationsSection(state, grid, nextRow, limit) {
416
- if (!state.notifications || state.notifications.length === 0)
417
- return nextRow;
418
- for (const note of state.notifications.slice(-2)) {
419
- if (nextRow >= limit)
420
- break;
421
- grid.writeText(nextRow, 0, ` ⚡ ${note.text}`, S_YELLOW);
422
- nextRow++;
423
- }
424
- return nextRow;
425
- }
426
- function renderInputSection(state, grid, inputRow, limit, promptText, promptWidth) {
427
- grid.writeText(inputRow, 0, promptText, S_USER);
428
- const inputStart = promptWidth;
429
- const inputLines = state.inputText.split("\n");
430
- const maxInputLines = Math.min(inputLines.length, 5);
431
- for (let li = 0; li < maxInputLines; li++) {
432
- if (inputRow + li >= limit)
433
- break;
434
- if (li === 0) {
435
- grid.writeText(inputRow, inputStart, inputLines[0], S_TEXT);
436
- }
437
- else {
438
- grid.writeText(inputRow + li, inputStart, inputLines[li], S_TEXT);
439
- }
440
- }
441
- // Line count indicator for multi-line input
442
- if (inputLines.length > 1) {
443
- const lineCountStr = ` [${inputLines.length} lines]`;
444
- const lineCountCol = Math.min(inputStart + (inputLines[0]?.length ?? 0) + 1, grid.width - lineCountStr.length - 1);
445
- if (lineCountCol > inputStart)
446
- grid.writeText(inputRow, lineCountCol, lineCountStr, S_DIM);
447
- }
448
- const hintsRow = inputRow + maxInputLines;
449
- if (hintsRow < limit) {
450
- const hintsText = inputLines.length > 1 ? `${state.statusHints} | Alt+Enter newline` : state.statusHints;
451
- grid.writeText(hintsRow, 0, hintsText, S_DIM);
452
- }
453
- return inputRow + maxInputLines + 1;
454
- }
455
- function renderCompanionSection(state, grid, anchorRow, limit, promptWidth) {
456
- if (!state.companionLines || grid.width < 50)
457
- return;
458
- const w = grid.width;
459
- const compWidth = Math.max(...state.companionLines.map((l) => l.length), 0);
460
- const compStartCol = Math.max(0, w - compWidth - 1);
461
- if (compStartCol <= promptWidth + 20)
462
- return;
463
- const compStyle = { fg: state.companionColor || "cyan", bg: null, bold: false, dim: false, underline: false };
464
- for (let i = 0; i < state.companionLines.length; i++) {
465
- const compRow = anchorRow + i;
466
- if (compRow >= limit)
467
- break;
468
- grid.writeText(compRow, compStartCol, state.companionLines[i], compStyle);
469
- }
470
- }
471
- function computeCursorPosition(state, inputRow, inputStart, questionInputRow) {
472
- if (state.questionPrompt && questionInputRow >= 0) {
473
- // In boxed mode cursor col is 5 (after "│ ❯ "), in compact mode it's 3 (after "❯ ")
474
- return { cursorRow: questionInputRow, cursorCol: 5 + state.questionPrompt.cursor };
475
- }
476
- const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
477
- const cursorLines = textBeforeCursor.split("\n");
478
- const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
479
- const cursorColInLine = cursorLines[cursorLines.length - 1].length;
480
- return { cursorRow: inputRow + cursorLineIdx, cursorCol: inputStart + cursorColInLine };
481
- }
482
- function getPromptText(state) {
483
- const vimIndicator = state.vimMode ? (state.vimMode === "normal" ? "[N] " : "[I] ") : "";
484
- const promptText = `${vimIndicator}❯ `;
485
- return { promptText, promptWidth: promptText.length };
486
- }
11
+ // Re-export for consumers
12
+ export { resetStyleCache } from "./layout-sections.js";
487
13
  // ── Main rasterization functions ──
488
14
  /**
489
15
  * Rasterize application state into the cell grid.
@@ -510,7 +36,7 @@ export function rasterize(state, grid) {
510
36
  contextWarningHeight;
511
37
  const footerHeight = Math.min(rawFooterHeight, Math.floor(h / 2));
512
38
  const msgAreaHeight = Math.max(1, h - footerHeight);
513
- // ── Session browser overlay ──
39
+ // Session browser overlay
514
40
  if (state.sessionBrowser) {
515
41
  const browserRows = renderSessionBrowser(grid, 0, 0, state.sessionBrowser, w, msgAreaHeight);
516
42
  const footerStart = Math.min(browserRows, msgAreaHeight);
@@ -521,7 +47,7 @@ export function rasterize(state, grid) {
521
47
  grid.writeText(inputRow + 1, 0, "↑/↓ navigate | Enter resume | Esc cancel", S_DIM);
522
48
  return { cursorRow: inputRow, cursorCol: 2 };
523
49
  }
524
- // ── Messages area (top) ──
50
+ // Messages area (top)
525
51
  const allContent = [];
526
52
  for (const msg of state.messages) {
527
53
  if (msg.role === "user") {
@@ -559,9 +85,9 @@ export function rasterize(state, grid) {
559
85
  allContent.push({ role: "error", content: state.errorText, style: S_ERROR, prefixStyle: S_ERROR, prefix: "✗ " });
560
86
  }
561
87
  const prefixLen = 2;
562
- const contentWidth = w - 1; // reserve rightmost column for scrollbar
88
+ const contentWidth = w - 1;
563
89
  const textWidth = contentWidth - prefixLen;
564
- // Pre-compute total height to handle scrolling
90
+ // Pre-compute total height for scrolling
565
91
  let totalRows = 0;
566
92
  if (state.bannerLines && h >= 30) {
567
93
  const compact = h < 40;
@@ -613,7 +139,7 @@ export function rasterize(state, grid) {
613
139
  let r = 0;
614
140
  let virtualR = 0;
615
141
  let contentIdx = 0;
616
- // ── Banner ──
142
+ // Banner
617
143
  if (state.bannerLines && h >= 30) {
618
144
  const compact = h < 40;
619
145
  const startLine = compact ? Math.max(0, state.bannerLines.length - 2) : 0;
@@ -631,7 +157,7 @@ export function rasterize(state, grid) {
631
157
  }
632
158
  virtualR++;
633
159
  }
634
- // ── Messages ──
160
+ // Messages
635
161
  for (const item of allContent) {
636
162
  if (r >= msgAreaHeight)
637
163
  break;
@@ -672,13 +198,13 @@ export function rasterize(state, grid) {
672
198
  virtualR += itemRows;
673
199
  contentIdx++;
674
200
  }
675
- // ── Thinking, spinner, tool calls, context warning (shared helpers) ──
201
+ // Thinking, spinner, tool calls, context warning
676
202
  r = renderThinkingSection(state, grid, r, msgAreaHeight);
677
203
  r = renderThinkingSummarySection(state, grid, r, msgAreaHeight);
678
204
  r = renderSpinnerSection(state, grid, r, msgAreaHeight);
679
205
  r = renderToolCallsSection(state, grid, r, msgAreaHeight, { maxLiveLines: 5, showOverflow: true });
680
206
  r = renderContextWarningSection(state, grid, r, msgAreaHeight);
681
- // ── Scrollbar ──
207
+ // Scrollbar
682
208
  if (hasScrollbar) {
683
209
  const S_TRACK = { fg: null, bg: null, bold: false, dim: true, underline: false };
684
210
  const S_THUMB = { fg: null, bg: null, bold: false, dim: false, underline: false };
@@ -687,7 +213,7 @@ export function rasterize(state, grid) {
687
213
  grid.setCell(sr, w - 1, isThumb ? "█" : "░", isThumb ? S_THUMB : S_TRACK);
688
214
  }
689
215
  }
690
- // ── Footer ──
216
+ // Footer
691
217
  const footerStart = Math.min(r, msgAreaHeight);
692
218
  for (let c = 0; c < w; c++) {
693
219
  grid.setCell(footerStart, c, "─", S_BORDER);
@@ -707,7 +233,7 @@ export function rasterize(state, grid) {
707
233
  grid.writeText(footerStart, startCol, indicator, S_DIM);
708
234
  }
709
235
  let nextRow = footerStart + 1;
710
- // ── Permission, question, status, autocomplete, notifications, input, companion (shared helpers) ──
236
+ // Permission, question, status, autocomplete, notifications, input, companion
711
237
  nextRow = renderPermissionBoxSection(state, grid, nextRow, h, { boxed: true, maxDiffHeight });
712
238
  const questionResult = renderQuestionPromptSection(state, grid, nextRow, h, { boxed: true });
713
239
  nextRow = questionResult.nextRow;
@@ -718,7 +244,7 @@ export function rasterize(state, grid) {
718
244
  nextRow = renderNotificationsSection(state, grid, nextRow, h);
719
245
  const inputRow = nextRow;
720
246
  renderInputSection(state, grid, inputRow, h, promptText, promptWidth);
721
- // Companion (right-aligned in footer, skipped if it would overlap input)
247
+ // Companion (right-aligned in footer)
722
248
  if (state.companionLines && w >= 50) {
723
249
  const compWidth = Math.max(...state.companionLines.map((l) => l.length), 0);
724
250
  const compStartCol = Math.max(0, w - compWidth - 1);
@@ -743,7 +269,6 @@ export function rasterize(state, grid) {
743
269
  }
744
270
  return computeCursorPosition(state, inputRow, promptWidth, questionInputRow);
745
271
  }
746
- // extractSuggestion moved to shared utils/tool-summary.ts as summarizeToolArgs
747
272
  /**
748
273
  * Rasterize only the "live area" — streaming text, thinking, tool calls, and footer.
749
274
  * Used in hybrid mode where completed messages are flushed to terminal scrollback.
@@ -754,32 +279,32 @@ export function rasterizeLive(state, grid) {
754
279
  const w = grid.width;
755
280
  const h = grid.height;
756
281
  let r = 0;
757
- // ── Banner (shown when no messages have been flushed yet) ──
282
+ // Banner (shown when no messages have been flushed yet)
758
283
  if (state.bannerLines && state.messages.length === 0 && !state.loading) {
759
284
  r = renderBannerSection(state, grid, r, h - 4, { compact: h < 15 });
760
285
  }
761
- // ── Streaming text ──
286
+ // Streaming text
762
287
  if (state.loading && state.streamingText) {
763
288
  grid.writeText(r, 0, "◆ ", S_ASSISTANT);
764
289
  const rows = renderMarkdown(grid, r, 2, state.streamingText, w, state.codeBlocksExpanded, h);
765
290
  r += rows;
766
291
  }
767
- // ── Thinking, spinner, error, tool calls, context warning (shared helpers) ──
292
+ // Thinking, spinner, error, tool calls, context warning
768
293
  r = renderThinkingSection(state, grid, r, h);
769
294
  r = renderThinkingSummarySection(state, grid, r, h);
770
295
  r = renderSpinnerSection(state, grid, r, h);
771
296
  r = renderErrorSection(state, grid, r, h);
772
297
  r = renderToolCallsSection(state, grid, r, h, { maxLiveLines: 3, showOverflow: false });
773
298
  r = renderContextWarningSection(state, grid, r, h);
774
- // ── Footer border ──
299
+ // Footer border
775
300
  if (r < h) {
776
301
  for (let c = 0; c < w; c++)
777
302
  grid.setCell(r, c, "─", S_BORDER);
778
303
  r++;
779
304
  }
780
305
  let nextRow = r;
781
- const borderRow = r - 1; // for companion anchoring
782
- // ── Permission, question, status, autocomplete, notifications, input (shared helpers) ──
306
+ const borderRow = r - 1;
307
+ // Permission, question, status, autocomplete, notifications, input
783
308
  nextRow = renderPermissionBoxSection(state, grid, nextRow, h, { boxed: false, maxDiffHeight: 15 });
784
309
  const questionResult = renderQuestionPromptSection(state, grid, nextRow, h, { boxed: false });
785
310
  nextRow = questionResult.nextRow;
@@ -790,9 +315,9 @@ export function rasterizeLive(state, grid) {
790
315
  nextRow = renderNotificationsSection(state, grid, nextRow, h);
791
316
  const inputRow = nextRow;
792
317
  renderInputSection(state, grid, inputRow, h, promptText, promptWidth);
793
- // ── Companion (right-aligned, anchored at footer border) ──
318
+ // Companion (right-aligned, anchored at footer border)
794
319
  renderCompanionSection(state, grid, borderRow, h, promptWidth);
795
- // ── Cursor position ──
320
+ // Cursor position
796
321
  if (state.questionPrompt && questionInputRow >= 0) {
797
322
  return { cursorRow: questionInputRow, cursorCol: 3 + state.questionPrompt.cursor };
798
323
  }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import type { Tool } from "../../Tool.js";
3
+ declare const inputSchema: z.ZodObject<{
4
+ todos: z.ZodArray<z.ZodObject<{
5
+ id: z.ZodString;
6
+ content: z.ZodString;
7
+ status: z.ZodDefault<z.ZodEnum<["pending", "in_progress", "completed"]>>;
8
+ priority: z.ZodOptional<z.ZodEnum<["high", "medium", "low"]>>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ content: string;
11
+ status: "completed" | "pending" | "in_progress";
12
+ id: string;
13
+ priority?: "low" | "medium" | "high" | undefined;
14
+ }, {
15
+ content: string;
16
+ id: string;
17
+ status?: "completed" | "pending" | "in_progress" | undefined;
18
+ priority?: "low" | "medium" | "high" | undefined;
19
+ }>, "many">;
20
+ }, "strip", z.ZodTypeAny, {
21
+ todos: {
22
+ content: string;
23
+ status: "completed" | "pending" | "in_progress";
24
+ id: string;
25
+ priority?: "low" | "medium" | "high" | undefined;
26
+ }[];
27
+ }, {
28
+ todos: {
29
+ content: string;
30
+ id: string;
31
+ status?: "completed" | "pending" | "in_progress" | undefined;
32
+ priority?: "low" | "medium" | "high" | undefined;
33
+ }[];
34
+ }>;
35
+ export declare const TodoWriteTool: Tool<typeof inputSchema>;
36
+ export {};
37
+ //# sourceMappingURL=index.d.ts.map