@zhijiewang/openharness 2.4.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.
- package/README.md +2 -2
- package/dist/Tool.d.ts +2 -0
- package/dist/commands/ai.d.ts +6 -0
- package/dist/commands/ai.js +244 -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 -1052
- 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 +117 -0
- package/dist/commands/types.d.ts +36 -0
- package/dist/commands/types.js +5 -0
- package/dist/components/InitWizard.js +61 -61
- package/dist/harness/config.d.ts +2 -0
- package/dist/harness/hooks.js +9 -6
- package/dist/harness/memory.js +28 -1
- package/dist/harness/plugins.d.ts +2 -0
- package/dist/harness/plugins.js +44 -11
- package/dist/harness/session-db.js +3 -1
- package/dist/harness/skill-registry.d.ts +21 -0
- package/dist/harness/skill-registry.js +35 -0
- package/dist/lsp/client.js +2 -1
- package/dist/main.js +10 -2
- package/dist/mcp/client.js +2 -1
- package/dist/mcp/server-mode.d.ts +10 -0
- package/dist/mcp/server-mode.js +17 -0
- package/dist/providers/anthropic.js +7 -8
- package/dist/providers/fallback.js +2 -3
- package/dist/providers/openai.js +3 -2
- package/dist/query/index.js +30 -6
- package/dist/query/tools.js +11 -0
- package/dist/query/types.d.ts +4 -0
- 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/repl.js +3 -1
- package/dist/services/SkillExtractor.js +2 -0
- package/dist/tools/SkillTool/index.js +26 -2
- package/dist/tools/TodoWriteTool/index.d.ts +37 -0
- package/dist/tools/TodoWriteTool/index.js +78 -0
- package/dist/tools.js +2 -0
- package/package.json +1 -1
package/dist/query/tools.js
CHANGED
|
@@ -120,6 +120,17 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
|
|
|
120
120
|
/* verification should never break tool execution */
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
// Auto-commit per tool (if enabled and file was modified)
|
|
124
|
+
if (!result.isError && context.gitCommitPerTool && !tool.isReadOnly(parsed.data)) {
|
|
125
|
+
try {
|
|
126
|
+
const { autoCommitAIEdits } = await import("../git/index.js");
|
|
127
|
+
const filePaths = getAffectedFiles(tool.name, parsed.data);
|
|
128
|
+
autoCommitAIEdits(tool.name, filePaths);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* auto-commit is optional */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
123
134
|
// Strip ANSI and cap output, then append verification suffix
|
|
124
135
|
let output = result.output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "") + verificationSuffix;
|
|
125
136
|
if (output.length > MAX_TOOL_RESULT_CHARS) {
|
package/dist/query/types.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type QueryConfig = {
|
|
|
18
18
|
abortSignal?: AbortSignal;
|
|
19
19
|
/** Working directory for tool execution (defaults to process.cwd()) */
|
|
20
20
|
workingDir?: string;
|
|
21
|
+
/** Auto-commit after each file-modifying tool */
|
|
22
|
+
gitCommitPerTool?: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
|
|
23
25
|
export type QueryLoopState = {
|
|
@@ -29,5 +31,7 @@ export type QueryLoopState = {
|
|
|
29
31
|
consecutiveErrors: number;
|
|
30
32
|
transition?: TransitionReason;
|
|
31
33
|
promptTooLongRetries?: number;
|
|
34
|
+
/** Track consecutive compression failures for circuit breaker */
|
|
35
|
+
compressionFailures?: number;
|
|
32
36
|
};
|
|
33
37
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
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 type { CellGrid, Style } from "./cells.js";
|
|
6
|
+
import type { LayoutState } from "./layout.js";
|
|
7
|
+
export declare const S_TEXT: Style;
|
|
8
|
+
export declare const S_DIM: Style;
|
|
9
|
+
export declare const S_BORDER: Style;
|
|
10
|
+
export declare const S_BANNER: Style;
|
|
11
|
+
export declare const S_BANNER_DIM: Style;
|
|
12
|
+
export declare let S_USER: Style;
|
|
13
|
+
export declare let S_ASSISTANT: Style;
|
|
14
|
+
export declare let S_ERROR: Style;
|
|
15
|
+
export declare let S_YELLOW: Style;
|
|
16
|
+
export declare let S_GREEN: Style;
|
|
17
|
+
/** Reset style cache — call after theme change */
|
|
18
|
+
export declare function resetStyleCache(): void;
|
|
19
|
+
export declare function ensureStyles(): void;
|
|
20
|
+
export declare const SPINNER_CHARS: string[];
|
|
21
|
+
export declare function renderBannerSection(state: LayoutState, grid: CellGrid, r: number, limit: number, opts: {
|
|
22
|
+
compact: boolean;
|
|
23
|
+
}): number;
|
|
24
|
+
export declare function renderThinkingSection(state: LayoutState, grid: CellGrid, r: number, limit: number): number;
|
|
25
|
+
export declare function renderThinkingSummarySection(state: LayoutState, grid: CellGrid, r: number, limit: number): number;
|
|
26
|
+
export declare function renderSpinnerSection(state: LayoutState, grid: CellGrid, r: number, limit: number): number;
|
|
27
|
+
export declare function renderErrorSection(state: LayoutState, grid: CellGrid, r: number, limit: number): number;
|
|
28
|
+
export declare function renderToolCallsSection(state: LayoutState, grid: CellGrid, r: number, limit: number, opts: {
|
|
29
|
+
maxLiveLines: number;
|
|
30
|
+
showOverflow: boolean;
|
|
31
|
+
}): number;
|
|
32
|
+
export declare function renderContextWarningSection(state: LayoutState, grid: CellGrid, r: number, limit: number): number;
|
|
33
|
+
export declare function renderPermissionBoxSection(state: LayoutState, grid: CellGrid, nextRow: number, h: number, opts: {
|
|
34
|
+
boxed: boolean;
|
|
35
|
+
maxDiffHeight: number;
|
|
36
|
+
}): number;
|
|
37
|
+
export declare function renderQuestionPromptSection(state: LayoutState, grid: CellGrid, nextRow: number, h: number, opts: {
|
|
38
|
+
boxed: boolean;
|
|
39
|
+
}): {
|
|
40
|
+
nextRow: number;
|
|
41
|
+
questionInputRow: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function renderStatusLineSection(state: LayoutState, grid: CellGrid, nextRow: number, limit: number): number;
|
|
44
|
+
export declare function renderAutocompleteSection(state: LayoutState, grid: CellGrid, nextRow: number, limit: number, promptWidth: number): number;
|
|
45
|
+
export declare function renderNotificationsSection(state: LayoutState, grid: CellGrid, nextRow: number, limit: number): number;
|
|
46
|
+
export declare function renderInputSection(state: LayoutState, grid: CellGrid, inputRow: number, limit: number, promptText: string, promptWidth: number): number;
|
|
47
|
+
export declare function renderCompanionSection(state: LayoutState, grid: CellGrid, anchorRow: number, limit: number, promptWidth: number): void;
|
|
48
|
+
export declare function computeCursorPosition(state: LayoutState, inputRow: number, inputStart: number, questionInputRow: number): {
|
|
49
|
+
cursorRow: number;
|
|
50
|
+
cursorCol: number;
|
|
51
|
+
};
|
|
52
|
+
export declare function getPromptText(state: LayoutState): {
|
|
53
|
+
promptText: string;
|
|
54
|
+
promptWidth: number;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=layout-sections.d.ts.map
|
|
@@ -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.
|