@zhijiewang/openharness 2.5.0 → 2.9.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/README.md +1 -1
  2. package/data/registry.json +262 -0
  3. package/data/skills/code-review.md +19 -0
  4. package/data/skills/commit.md +17 -0
  5. package/data/skills/debug.md +24 -0
  6. package/data/skills/diagnose.md +24 -0
  7. package/data/skills/plan.md +25 -0
  8. package/data/skills/simplify.md +24 -0
  9. package/data/skills/tdd.md +22 -0
  10. package/dist/agents/roles.d.ts +12 -2
  11. package/dist/agents/roles.js +65 -6
  12. package/dist/commands/ai.d.ts +6 -0
  13. package/dist/commands/ai.js +264 -0
  14. package/dist/commands/git.d.ts +6 -0
  15. package/dist/commands/git.js +167 -0
  16. package/dist/commands/index.d.ts +10 -31
  17. package/dist/commands/index.js +22 -1096
  18. package/dist/commands/info.d.ts +8 -0
  19. package/dist/commands/info.js +671 -0
  20. package/dist/commands/session.d.ts +6 -0
  21. package/dist/commands/session.js +214 -0
  22. package/dist/commands/settings.d.ts +6 -0
  23. package/dist/commands/settings.js +187 -0
  24. package/dist/commands/skills.d.ts +6 -0
  25. package/dist/commands/skills.js +162 -0
  26. package/dist/commands/types.d.ts +36 -0
  27. package/dist/commands/types.js +5 -0
  28. package/dist/components/App.js +7 -1
  29. package/dist/components/InitWizard.js +60 -62
  30. package/dist/harness/config.d.ts +11 -0
  31. package/dist/harness/hooks.d.ts +14 -0
  32. package/dist/harness/hooks.js +56 -10
  33. package/dist/harness/marketplace.d.ts +77 -2
  34. package/dist/harness/marketplace.js +260 -38
  35. package/dist/harness/memory.d.ts +34 -0
  36. package/dist/harness/memory.js +96 -0
  37. package/dist/harness/plugins.d.ts +13 -3
  38. package/dist/harness/plugins.js +98 -17
  39. package/dist/harness/session-db.d.ts +8 -1
  40. package/dist/harness/session-db.js +24 -3
  41. package/dist/harness/skill-registry.d.ts +26 -2
  42. package/dist/harness/skill-registry.js +42 -4
  43. package/dist/providers/anthropic.js +7 -8
  44. package/dist/providers/openai.js +3 -2
  45. package/dist/renderer/layout-sections.d.ts +56 -0
  46. package/dist/renderer/layout-sections.js +462 -0
  47. package/dist/renderer/layout.d.ts +4 -2
  48. package/dist/renderer/layout.js +25 -500
  49. package/dist/tools/AgentTool/index.d.ts +2 -2
  50. package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
  51. package/dist/tools/GrepTool/index.d.ts +6 -6
  52. package/dist/tools/MemoryTool/index.d.ts +6 -6
  53. package/dist/tools/MonitorTool/index.js +5 -1
  54. package/dist/tools/TodoWriteTool/index.d.ts +37 -0
  55. package/dist/tools/TodoWriteTool/index.js +78 -0
  56. package/dist/tools.js +2 -0
  57. package/dist/types/permissions.js +104 -42
  58. package/dist/utils/bash-safety.d.ts +19 -0
  59. package/dist/utils/bash-safety.js +179 -1
  60. package/dist/utils/safe-env.d.ts +5 -1
  61. package/dist/utils/safe-env.js +19 -1
  62. package/package.json +3 -1
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Layout section renderers — individual UI widgets rasterized into a CellGrid.
3
+ * Each function takes (state, grid, row, limit, ...options) and returns next row.
4
+ */
5
+ import { getTheme } from "../utils/theme-data.js";
6
+ import { renderDiff } from "./diff.js";
7
+ import { isImageOutput, renderImageInline } from "./image.js";
8
+ // ── Style constants ──
9
+ const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
10
+ export const S_TEXT = s(null);
11
+ export const S_DIM = s(null, false, true);
12
+ export const S_BORDER = s(null, false, true);
13
+ const S_BRIGHT = s(null);
14
+ export const S_BANNER = s("cyan");
15
+ export const S_BANNER_DIM = s(null, false, true);
16
+ const S_AGENT = s("cyan", true);
17
+ const S_KEY_GREEN = s("green", true);
18
+ const S_KEY_RED = s("red", true);
19
+ const S_KEY_CYAN = s("cyan", true);
20
+ // Theme-dependent styles — lazily initialized on first rasterize() call
21
+ export let S_USER;
22
+ export let S_ASSISTANT;
23
+ export let S_ERROR;
24
+ export let S_YELLOW;
25
+ export let S_GREEN;
26
+ let _stylesInit = false;
27
+ /** Reset style cache — call after theme change */
28
+ export function resetStyleCache() {
29
+ _stylesInit = false;
30
+ }
31
+ export function ensureStyles() {
32
+ if (_stylesInit)
33
+ return;
34
+ _stylesInit = true;
35
+ const t = getTheme();
36
+ S_USER = s(t.user, true);
37
+ S_ASSISTANT = s(t.assistant, true);
38
+ S_ERROR = s(t.error);
39
+ S_YELLOW = s(t.tool);
40
+ S_GREEN = s(t.success);
41
+ }
42
+ export const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
43
+ // ── Section renderers ──
44
+ export function renderBannerSection(state, grid, r, limit, opts) {
45
+ if (!state.bannerLines)
46
+ return r;
47
+ const startLine = opts.compact ? Math.max(0, state.bannerLines.length - 2) : 0;
48
+ for (let i = startLine; i < state.bannerLines.length; i++) {
49
+ if (r >= limit)
50
+ break;
51
+ const line = state.bannerLines[i];
52
+ const isBannerArt = i < state.bannerLines.length - 2;
53
+ grid.writeText(r, 0, line, isBannerArt ? S_BANNER : S_BANNER_DIM);
54
+ r++;
55
+ }
56
+ if (r < limit)
57
+ r++; // blank line after banner
58
+ return r;
59
+ }
60
+ export function renderThinkingSection(state, grid, r, limit) {
61
+ if (!state.thinkingText || r >= limit)
62
+ return r;
63
+ const w = grid.width;
64
+ if (state.thinkingExpanded) {
65
+ const thinkLines = state.thinkingText.split("\n").slice(-10);
66
+ const shimmerPos = state.spinnerFrame % 20;
67
+ for (const tLine of thinkLines) {
68
+ if (r >= limit)
69
+ break;
70
+ grid.writeText(r, 0, "💭 ", S_DIM);
71
+ const chars = [...tLine];
72
+ for (let ci = 0; ci < chars.length && ci + 3 < w; ci++) {
73
+ grid.setCell(r, 3 + ci, chars[ci], Math.abs(ci - shimmerPos) <= 2 ? S_BRIGHT : S_DIM);
74
+ }
75
+ r++;
76
+ }
77
+ }
78
+ else {
79
+ const lineCount = state.thinkingText.split("\n").length;
80
+ const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
81
+ const summary = `∴ Thinking${elapsed > 0 ? ` (${elapsed}s)` : ""} — ${lineCount} lines [Ctrl+O expand]`;
82
+ grid.writeText(r, 0, summary, S_DIM);
83
+ r++;
84
+ }
85
+ return r;
86
+ }
87
+ export function renderThinkingSummarySection(state, grid, r, limit) {
88
+ if (state.loading || !state.lastThinkingSummary || r >= limit)
89
+ return r;
90
+ grid.writeText(r, 0, state.lastThinkingSummary, S_DIM);
91
+ return r + 1;
92
+ }
93
+ export function renderSpinnerSection(state, grid, r, limit) {
94
+ if (!state.loading || state.streamingText || state.thinkingText || r >= limit)
95
+ return r;
96
+ const thinkText = "Thinking";
97
+ const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
98
+ const t = getTheme();
99
+ const baseColor = elapsed > 60 ? t.error : elapsed > 30 ? t.stall : t.primary;
100
+ const shimmerColor = elapsed > 60 ? t.stallShimmer : elapsed > 30 ? t.warning : t.primaryShimmer;
101
+ const baseStyle = { fg: baseColor, bg: null, bold: false, dim: false, underline: false };
102
+ grid.writeText(r, 0, "◆ ", { ...baseStyle, bold: true });
103
+ const shimmerPos = state.spinnerFrame % (thinkText.length + 6);
104
+ const shimmerStyle = { fg: shimmerColor, bg: null, bold: true, dim: false, underline: false };
105
+ for (let ci = 0; ci < thinkText.length; ci++) {
106
+ grid.setCell(r, 2 + ci, thinkText[ci], Math.abs(ci - shimmerPos) <= 1 ? shimmerStyle : baseStyle);
107
+ }
108
+ let suffix = "";
109
+ if (elapsed > 0)
110
+ suffix += ` ${elapsed}s`;
111
+ if (state.tokenCount > 0) {
112
+ const tokStr = state.tokenCount >= 1000 ? `${(state.tokenCount / 1000).toFixed(1)}K` : `${state.tokenCount}`;
113
+ suffix += ` | ${tokStr} tokens`;
114
+ }
115
+ suffix += "...";
116
+ grid.writeText(r, 2 + thinkText.length, suffix, S_DIM);
117
+ return r + 1;
118
+ }
119
+ export function renderErrorSection(state, grid, r, limit) {
120
+ if (!state.errorText || r >= limit)
121
+ return r;
122
+ const w = grid.width;
123
+ grid.writeText(r, 0, "✗ ", S_ERROR);
124
+ grid.writeText(r, 2, state.errorText.slice(0, w - 4), S_ERROR);
125
+ return r + 1;
126
+ }
127
+ export function renderToolCallsSection(state, grid, r, limit, opts) {
128
+ const w = grid.width;
129
+ for (const [callId, tc] of state.toolCalls) {
130
+ if (r >= limit)
131
+ break;
132
+ const isAgent = tc.isAgent || tc.toolName === "Agent" || tc.toolName === "ParallelAgents";
133
+ const icon = isAgent
134
+ ? tc.status === "running"
135
+ ? "⊕"
136
+ : tc.status === "done"
137
+ ? "◈"
138
+ : "◇"
139
+ : tc.status === "running"
140
+ ? SPINNER_CHARS[state.spinnerFrame % SPINNER_CHARS.length]
141
+ : tc.status === "done"
142
+ ? "✓"
143
+ : "✗";
144
+ const statusStyle = tc.status === "error" ? S_ERROR : tc.status === "done" ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
145
+ const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
146
+ const isExpanded = state.expandedToolCalls.has(callId);
147
+ const canExpand = tc.status !== "running" && tc.output;
148
+ if (canExpand) {
149
+ grid.writeText(r, 0, isExpanded ? "▼" : "▶", S_DIM);
150
+ }
151
+ grid.writeText(r, 2, `${icon} `, statusStyle);
152
+ grid.writeText(r, 4, tc.toolName, nameStyle);
153
+ let afterName = 4 + tc.toolName.length + 1;
154
+ if (tc.args) {
155
+ const maxArgs = w - afterName - 15;
156
+ if (maxArgs > 5) {
157
+ const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? "…" : "");
158
+ grid.writeText(r, afterName, argsText, S_DIM);
159
+ afterName += argsText.length + 1;
160
+ }
161
+ }
162
+ if (tc.status === "running" && tc.startedAt) {
163
+ const elapsed = Math.floor((Date.now() - tc.startedAt) / 1000);
164
+ if (elapsed > 0) {
165
+ const lineCount = tc.liveOutput?.length ?? 0;
166
+ const elapsedStr = lineCount > 0 ? `${elapsed}s · ${lineCount} lines` : `${elapsed}s`;
167
+ grid.writeText(r, Math.min(afterName, w - elapsedStr.length - 2), elapsedStr, S_DIM);
168
+ }
169
+ }
170
+ if (tc.status !== "running" && tc.resultSummary) {
171
+ const elapsed = tc.startedAt ? Math.floor((Date.now() - tc.startedAt) / 1000) : 0;
172
+ const suffix = elapsed > 0 ? `${tc.resultSummary} · ${elapsed}s` : tc.resultSummary;
173
+ grid.writeText(r, Math.min(afterName, w - suffix.length - 2), suffix, S_DIM);
174
+ }
175
+ r++;
176
+ if (isAgent && tc.agentDescription && r < limit) {
177
+ grid.writeText(r, 6, tc.agentDescription.slice(0, w - 8), S_DIM);
178
+ r++;
179
+ }
180
+ if (tc.status === "running" && tc.liveOutput && tc.liveOutput.length > 0) {
181
+ const overflow = tc.liveOutput.length > opts.maxLiveLines ? tc.liveOutput.length - opts.maxLiveLines : 0;
182
+ if (opts.showOverflow && overflow > 0 && r < limit) {
183
+ grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
184
+ r++;
185
+ }
186
+ const visible = overflow > 0 ? tc.liveOutput.slice(-opts.maxLiveLines) : tc.liveOutput;
187
+ for (const line of visible) {
188
+ if (r >= limit)
189
+ break;
190
+ grid.writeText(r, 6, line.slice(0, w - 8), S_DIM);
191
+ r++;
192
+ }
193
+ }
194
+ if (tc.output && tc.status !== "running" && isExpanded && r < limit) {
195
+ if (isImageOutput(tc.output)) {
196
+ const label = renderImageInline(tc.output);
197
+ grid.writeText(r, 6, label.slice(0, w - 8), S_DIM);
198
+ r++;
199
+ continue;
200
+ }
201
+ const outLines = tc.output.split("\n");
202
+ const maxOut = 20;
203
+ const showLines = outLines.slice(0, maxOut);
204
+ for (const line of showLines) {
205
+ if (r >= limit)
206
+ break;
207
+ const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
208
+ grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
209
+ r++;
210
+ }
211
+ if (outLines.length > maxOut && r < limit) {
212
+ grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
213
+ r++;
214
+ }
215
+ }
216
+ }
217
+ return r;
218
+ }
219
+ export function renderContextWarningSection(state, grid, r, limit) {
220
+ if (!state.contextWarning || r >= limit)
221
+ return r;
222
+ const warnStyle = {
223
+ fg: "yellow",
224
+ bg: null,
225
+ bold: state.contextWarning.critical,
226
+ dim: false,
227
+ underline: false,
228
+ };
229
+ grid.writeText(r, 0, state.contextWarning.text, warnStyle);
230
+ return r + 1;
231
+ }
232
+ export function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
233
+ if (!state.permissionBox || grid.width < 20)
234
+ return nextRow;
235
+ const w = grid.width;
236
+ const { toolName, riskLevel } = state.permissionBox;
237
+ const riskColor = riskLevel === "high" ? "red" : riskLevel === "medium" ? "yellow" : "green";
238
+ const riskStyle = { fg: riskColor, bg: null, bold: true, dim: false, underline: false };
239
+ if (opts.boxed) {
240
+ if (h - nextRow < 6)
241
+ return nextRow;
242
+ const riskDim = { fg: riskColor, bg: null, bold: false, dim: true, underline: false };
243
+ const boxWidth = Math.max(15, Math.min(w - 2, 70));
244
+ grid.writeText(nextRow, 1, `╭${"─".repeat(boxWidth - 2)}╮`, riskDim);
245
+ nextRow++;
246
+ grid.writeText(nextRow, 1, "│ ", riskDim);
247
+ grid.writeText(nextRow, 3, "⚠ ", riskStyle);
248
+ grid.writeText(nextRow, 5, toolName, { ...riskStyle });
249
+ grid.writeText(nextRow, 5 + toolName.length, ` ${riskLevel} risk`, S_DIM);
250
+ grid.writeText(nextRow, boxWidth, "│", riskDim);
251
+ nextRow++;
252
+ const rawDesc = state.permissionBox.suggestion || state.permissionBox.description.slice(0, boxWidth - 6);
253
+ const descText = rawDesc.replace(/\|/g, " ").replace(/\\/g, "/");
254
+ grid.writeText(nextRow, 1, "│ ", riskDim);
255
+ grid.writeText(nextRow, 3, descText.slice(0, boxWidth - 4), S_DIM);
256
+ grid.writeText(nextRow, boxWidth, "│", riskDim);
257
+ nextRow++;
258
+ if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
259
+ grid.writeText(nextRow, 1, "│", riskDim);
260
+ nextRow++;
261
+ const availDiffRows = Math.min(opts.maxDiffHeight, h - nextRow - 3);
262
+ const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, boxWidth - 2, availDiffRows);
263
+ for (let dr = 0; dr < diffRows; dr++) {
264
+ if (nextRow + dr < grid.height) {
265
+ grid.setCell(nextRow + dr, 1, "│", riskDim);
266
+ grid.setCell(nextRow + dr, boxWidth, "│", riskDim);
267
+ }
268
+ }
269
+ nextRow += diffRows;
270
+ }
271
+ grid.writeText(nextRow, 1, "│ ", riskDim);
272
+ let kc = 3;
273
+ grid.writeText(nextRow, kc, "Y", S_KEY_GREEN);
274
+ kc += 1;
275
+ grid.writeText(nextRow, kc, "es", S_DIM);
276
+ kc += 2;
277
+ grid.writeText(nextRow, kc, " ", S_DIM);
278
+ kc += 2;
279
+ grid.writeText(nextRow, kc, "N", S_KEY_RED);
280
+ kc += 1;
281
+ grid.writeText(nextRow, kc, "o", S_DIM);
282
+ kc += 1;
283
+ if (state.permissionDiffInfo) {
284
+ grid.writeText(nextRow, kc, " ", S_DIM);
285
+ kc += 2;
286
+ grid.writeText(nextRow, kc, "D", S_KEY_CYAN);
287
+ kc += 1;
288
+ grid.writeText(nextRow, kc, "iff", S_DIM);
289
+ }
290
+ grid.writeText(nextRow, boxWidth, "│", riskDim);
291
+ nextRow++;
292
+ grid.writeText(nextRow, 1, `╰${"─".repeat(boxWidth - 2)}╯`, riskDim);
293
+ nextRow++;
294
+ }
295
+ else {
296
+ if (h - nextRow < 4)
297
+ return nextRow;
298
+ grid.writeText(nextRow, 1, `⚠ ${toolName} (${riskLevel} risk)`, riskStyle);
299
+ nextRow++;
300
+ grid.writeText(nextRow, 1, "Y", S_KEY_GREEN);
301
+ grid.writeText(nextRow, 2, "es ", S_DIM);
302
+ grid.writeText(nextRow, 6, "N", S_KEY_RED);
303
+ grid.writeText(nextRow, 7, "o", S_DIM);
304
+ if (state.permissionDiffInfo) {
305
+ grid.writeText(nextRow, 10, "D", S_KEY_CYAN);
306
+ grid.writeText(nextRow, 11, "iff", S_DIM);
307
+ }
308
+ nextRow++;
309
+ if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
310
+ const availDiffRows = Math.min(15, h - nextRow - 3);
311
+ const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, Math.min(w - 2, 70), availDiffRows);
312
+ nextRow += diffRows;
313
+ }
314
+ }
315
+ return nextRow;
316
+ }
317
+ export function renderQuestionPromptSection(state, grid, nextRow, h, opts) {
318
+ if (!state.questionPrompt || grid.width < 20)
319
+ return { nextRow, questionInputRow: -1 };
320
+ const w = grid.width;
321
+ const { question, options, input } = state.questionPrompt;
322
+ const qStyle = { fg: "yellow", bg: null, bold: false, dim: false, underline: false };
323
+ if (opts.boxed) {
324
+ const qBorder = { fg: "yellow", bg: null, bold: false, dim: true, underline: false };
325
+ const qBoxWidth = Math.max(15, Math.min(w - 2, 70));
326
+ grid.writeText(nextRow, 1, `╭${"─".repeat(qBoxWidth - 2)}╮`, qBorder);
327
+ nextRow++;
328
+ grid.writeText(nextRow, 1, "│ ", qBorder);
329
+ grid.writeText(nextRow, 3, `❓ ${question}`, qStyle);
330
+ grid.writeText(nextRow, qBoxWidth, "│", qBorder);
331
+ nextRow++;
332
+ if (options && options.length > 0) {
333
+ for (let oi = 0; oi < options.length; oi++) {
334
+ grid.writeText(nextRow, 1, "│ ", qBorder);
335
+ grid.writeText(nextRow, 5, `${oi + 1}. ${options[oi]}`, S_DIM);
336
+ grid.writeText(nextRow, qBoxWidth, "│", qBorder);
337
+ nextRow++;
338
+ }
339
+ }
340
+ const questionInputRow = nextRow;
341
+ grid.writeText(nextRow, 1, "│ ", qBorder);
342
+ grid.writeText(nextRow, 3, "❯ ", qStyle);
343
+ grid.writeText(nextRow, 5, input, S_TEXT);
344
+ grid.writeText(nextRow, qBoxWidth, "│", qBorder);
345
+ nextRow++;
346
+ grid.writeText(nextRow, 1, `╰${"─".repeat(qBoxWidth - 2)}╯`, qBorder);
347
+ nextRow++;
348
+ return { nextRow, questionInputRow };
349
+ }
350
+ if (h - nextRow < 3)
351
+ return { nextRow, questionInputRow: -1 };
352
+ grid.writeText(nextRow, 1, `❓ ${question}`, S_TEXT);
353
+ nextRow++;
354
+ if (options) {
355
+ for (const opt of options) {
356
+ if (nextRow >= h)
357
+ break;
358
+ grid.writeText(nextRow, 3, opt, S_DIM);
359
+ nextRow++;
360
+ }
361
+ }
362
+ const questionInputRow = nextRow;
363
+ grid.writeText(nextRow, 1, "❯ ", S_USER);
364
+ grid.writeText(nextRow, 3, input, S_TEXT);
365
+ nextRow++;
366
+ return { nextRow, questionInputRow };
367
+ }
368
+ export function renderStatusLineSection(state, grid, nextRow, limit) {
369
+ if (!state.statusLine || nextRow >= limit)
370
+ return nextRow;
371
+ grid.writeText(nextRow, 0, state.statusLine, S_DIM);
372
+ return nextRow + 1;
373
+ }
374
+ export function renderAutocompleteSection(state, grid, nextRow, limit, promptWidth) {
375
+ if (state.autocomplete.length === 0)
376
+ return nextRow;
377
+ const w = grid.width;
378
+ for (let ai = 0; ai < state.autocomplete.length; ai++) {
379
+ if (nextRow >= limit)
380
+ break;
381
+ const cmd = state.autocomplete[ai];
382
+ const desc = state.autocompleteDescriptions[ai] ?? "";
383
+ const selected = ai === state.autocompleteIndex;
384
+ const acStyle = selected ? s(getTheme().user, true) : s(null, false, true);
385
+ grid.writeText(nextRow, promptWidth, `/${cmd.padEnd(12)}`, acStyle);
386
+ if (desc && w > promptWidth + 15)
387
+ grid.writeText(nextRow, promptWidth + 13, desc.slice(0, w - promptWidth - 15), S_DIM);
388
+ nextRow++;
389
+ }
390
+ return nextRow;
391
+ }
392
+ export function renderNotificationsSection(state, grid, nextRow, limit) {
393
+ if (!state.notifications || state.notifications.length === 0)
394
+ return nextRow;
395
+ for (const note of state.notifications.slice(-2)) {
396
+ if (nextRow >= limit)
397
+ break;
398
+ grid.writeText(nextRow, 0, ` ⚡ ${note.text}`, S_YELLOW);
399
+ nextRow++;
400
+ }
401
+ return nextRow;
402
+ }
403
+ export function renderInputSection(state, grid, inputRow, limit, promptText, promptWidth) {
404
+ grid.writeText(inputRow, 0, promptText, S_USER);
405
+ const inputStart = promptWidth;
406
+ const inputLines = state.inputText.split("\n");
407
+ const maxInputLines = Math.min(inputLines.length, 5);
408
+ for (let li = 0; li < maxInputLines; li++) {
409
+ if (inputRow + li >= limit)
410
+ break;
411
+ if (li === 0) {
412
+ grid.writeText(inputRow, inputStart, inputLines[0], S_TEXT);
413
+ }
414
+ else {
415
+ grid.writeText(inputRow + li, inputStart, inputLines[li], S_TEXT);
416
+ }
417
+ }
418
+ if (inputLines.length > 1) {
419
+ const lineCountStr = ` [${inputLines.length} lines]`;
420
+ const lineCountCol = Math.min(inputStart + (inputLines[0]?.length ?? 0) + 1, grid.width - lineCountStr.length - 1);
421
+ if (lineCountCol > inputStart)
422
+ grid.writeText(inputRow, lineCountCol, lineCountStr, S_DIM);
423
+ }
424
+ const hintsRow = inputRow + maxInputLines;
425
+ if (hintsRow < limit) {
426
+ const hintsText = inputLines.length > 1 ? `${state.statusHints} | Alt+Enter newline` : state.statusHints;
427
+ grid.writeText(hintsRow, 0, hintsText, S_DIM);
428
+ }
429
+ return inputRow + maxInputLines + 1;
430
+ }
431
+ export function renderCompanionSection(state, grid, anchorRow, limit, promptWidth) {
432
+ if (!state.companionLines || grid.width < 50)
433
+ return;
434
+ const w = grid.width;
435
+ const compWidth = Math.max(...state.companionLines.map((l) => l.length), 0);
436
+ const compStartCol = Math.max(0, w - compWidth - 1);
437
+ if (compStartCol <= promptWidth + 20)
438
+ return;
439
+ const compStyle = { fg: state.companionColor || "cyan", bg: null, bold: false, dim: false, underline: false };
440
+ for (let i = 0; i < state.companionLines.length; i++) {
441
+ const compRow = anchorRow + i;
442
+ if (compRow >= limit)
443
+ break;
444
+ grid.writeText(compRow, compStartCol, state.companionLines[i], compStyle);
445
+ }
446
+ }
447
+ export function computeCursorPosition(state, inputRow, inputStart, questionInputRow) {
448
+ if (state.questionPrompt && questionInputRow >= 0) {
449
+ return { cursorRow: questionInputRow, cursorCol: 5 + state.questionPrompt.cursor };
450
+ }
451
+ const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
452
+ const cursorLines = textBeforeCursor.split("\n");
453
+ const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
454
+ const cursorColInLine = cursorLines[cursorLines.length - 1].length;
455
+ return { cursorRow: inputRow + cursorLineIdx, cursorCol: inputStart + cursorColInLine };
456
+ }
457
+ export function getPromptText(state) {
458
+ const vimIndicator = state.vimMode ? (state.vimMode === "normal" ? "[N] " : "[I] ") : "";
459
+ const promptText = `${vimIndicator}❯ `;
460
+ return { promptText, promptWidth: promptText.length };
461
+ }
462
+ //# sourceMappingURL=layout-sections.js.map
@@ -1,9 +1,13 @@
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
8
  import type { Message } from "../types/message.js";
6
9
  import type { CellGrid } from "./cells.js";
10
+ export { resetStyleCache } from "./layout-sections.js";
7
11
  export type ToolCallInfo = {
8
12
  toolName: string;
9
13
  status: "running" | "done" | "error";
@@ -64,8 +68,6 @@ export type LayoutState = {
64
68
  text: string;
65
69
  }>;
66
70
  };
67
- /** Reset style cache — call after theme change */
68
- export declare function resetStyleCache(): void;
69
71
  /**
70
72
  * Rasterize application state into the cell grid.
71
73
  * Full-screen mode with message area + scrollbar + footer.