@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.
- package/README.md +1 -1
- package/data/registry.json +262 -0
- package/data/skills/code-review.md +19 -0
- package/data/skills/commit.md +17 -0
- package/data/skills/debug.md +24 -0
- package/data/skills/diagnose.md +24 -0
- package/data/skills/plan.md +25 -0
- package/data/skills/simplify.md +24 -0
- package/data/skills/tdd.md +22 -0
- package/dist/agents/roles.d.ts +12 -2
- package/dist/agents/roles.js +65 -6
- package/dist/commands/ai.d.ts +6 -0
- package/dist/commands/ai.js +264 -0
- package/dist/commands/git.d.ts +6 -0
- package/dist/commands/git.js +167 -0
- package/dist/commands/index.d.ts +10 -31
- package/dist/commands/index.js +22 -1096
- package/dist/commands/info.d.ts +8 -0
- package/dist/commands/info.js +671 -0
- package/dist/commands/session.d.ts +6 -0
- package/dist/commands/session.js +214 -0
- package/dist/commands/settings.d.ts +6 -0
- package/dist/commands/settings.js +187 -0
- package/dist/commands/skills.d.ts +6 -0
- package/dist/commands/skills.js +162 -0
- package/dist/commands/types.d.ts +36 -0
- package/dist/commands/types.js +5 -0
- package/dist/components/App.js +7 -1
- package/dist/components/InitWizard.js +60 -62
- package/dist/harness/config.d.ts +11 -0
- package/dist/harness/hooks.d.ts +14 -0
- package/dist/harness/hooks.js +56 -10
- package/dist/harness/marketplace.d.ts +77 -2
- package/dist/harness/marketplace.js +260 -38
- package/dist/harness/memory.d.ts +34 -0
- package/dist/harness/memory.js +96 -0
- package/dist/harness/plugins.d.ts +13 -3
- package/dist/harness/plugins.js +98 -17
- package/dist/harness/session-db.d.ts +8 -1
- package/dist/harness/session-db.js +24 -3
- package/dist/harness/skill-registry.d.ts +26 -2
- package/dist/harness/skill-registry.js +42 -4
- package/dist/providers/anthropic.js +7 -8
- package/dist/providers/openai.js +3 -2
- package/dist/renderer/layout-sections.d.ts +56 -0
- package/dist/renderer/layout-sections.js +462 -0
- package/dist/renderer/layout.d.ts +4 -2
- package/dist/renderer/layout.js +25 -500
- package/dist/tools/AgentTool/index.d.ts +2 -2
- package/dist/tools/DiagnosticsTool/index.d.ts +1 -1
- package/dist/tools/GrepTool/index.d.ts +6 -6
- package/dist/tools/MemoryTool/index.d.ts +6 -6
- package/dist/tools/MonitorTool/index.js +5 -1
- package/dist/tools/TodoWriteTool/index.d.ts +37 -0
- package/dist/tools/TodoWriteTool/index.js +78 -0
- package/dist/tools.js +2 -0
- package/dist/types/permissions.js +104 -42
- package/dist/utils/bash-safety.d.ts +19 -0
- package/dist/utils/bash-safety.js +179 -1
- package/dist/utils/safe-env.d.ts +5 -1
- package/dist/utils/safe-env.js +19 -1
- package/package.json +3 -1
package/dist/renderer/layout.js
CHANGED
|
@@ -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 {
|
|
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
|
-
//
|
|
11
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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;
|
|
88
|
+
const contentWidth = w - 1;
|
|
563
89
|
const textWidth = contentWidth - prefixLen;
|
|
564
|
-
// Pre-compute total height
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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;
|
|
782
|
-
//
|
|
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
|
-
//
|
|
318
|
+
// Companion (right-aligned, anchored at footer border)
|
|
794
319
|
renderCompanionSection(state, grid, borderRow, h, promptWidth);
|
|
795
|
-
//
|
|
320
|
+
// Cursor position
|
|
796
321
|
if (state.questionPrompt && questionInputRow >= 0) {
|
|
797
322
|
return { cursorRow: questionInputRow, cursorCol: 3 + state.questionPrompt.cursor };
|
|
798
323
|
}
|
|
@@ -13,8 +13,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
13
13
|
prompt: string;
|
|
14
14
|
model?: string | undefined;
|
|
15
15
|
description?: string | undefined;
|
|
16
|
-
isolated?: boolean | undefined;
|
|
17
16
|
isolation?: "worktree" | undefined;
|
|
17
|
+
isolated?: boolean | undefined;
|
|
18
18
|
run_in_background?: boolean | undefined;
|
|
19
19
|
subagent_type?: string | undefined;
|
|
20
20
|
allowed_tools?: string[] | undefined;
|
|
@@ -22,8 +22,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
22
22
|
prompt: string;
|
|
23
23
|
model?: string | undefined;
|
|
24
24
|
description?: string | undefined;
|
|
25
|
-
isolated?: boolean | undefined;
|
|
26
25
|
isolation?: "worktree" | undefined;
|
|
26
|
+
isolated?: boolean | undefined;
|
|
27
27
|
run_in_background?: boolean | undefined;
|
|
28
28
|
subagent_type?: string | undefined;
|
|
29
29
|
allowed_tools?: string[] | undefined;
|
|
@@ -6,8 +6,8 @@ declare const inputSchema: z.ZodObject<{
|
|
|
6
6
|
line: z.ZodOptional<z.ZodNumber>;
|
|
7
7
|
character: z.ZodOptional<z.ZodNumber>;
|
|
8
8
|
}, "strip", z.ZodTypeAny, {
|
|
9
|
-
file_path: string;
|
|
10
9
|
action: "diagnostics" | "definition" | "references" | "hover";
|
|
10
|
+
file_path: string;
|
|
11
11
|
line?: number | undefined;
|
|
12
12
|
character?: number | undefined;
|
|
13
13
|
}, {
|