@zhijiewang/openharness 0.9.3 → 0.11.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 +87 -24
- package/dist/Tool.d.ts.map +1 -1
- package/dist/Tool.js +7 -1
- package/dist/Tool.js.map +1 -1
- package/dist/Tool.test.js +8 -2
- package/dist/Tool.test.js.map +1 -1
- package/dist/agents/roles.d.ts +25 -0
- package/dist/agents/roles.d.ts.map +1 -0
- package/dist/agents/roles.js +116 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/agents/roles.test.d.ts +2 -0
- package/dist/agents/roles.test.d.ts.map +1 -0
- package/dist/agents/roles.test.js +38 -0
- package/dist/agents/roles.test.js.map +1 -0
- package/dist/commands/commands-new.test.d.ts +5 -0
- package/dist/commands/commands-new.test.d.ts.map +1 -0
- package/dist/commands/commands-new.test.js +132 -0
- package/dist/commands/commands-new.test.js.map +1 -0
- package/dist/commands/commands.test.js +31 -0
- package/dist/commands/commands.test.js.map +1 -1
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +199 -6
- package/dist/commands/index.js.map +1 -1
- package/dist/components/REPL.js +1 -1
- package/dist/components/REPL.js.map +1 -1
- package/dist/git/git.test.js +33 -1
- package/dist/git/git.test.js.map +1 -1
- package/dist/git/index.d.ts +12 -0
- package/dist/git/index.d.ts.map +1 -1
- package/dist/git/index.js +47 -2
- package/dist/git/index.js.map +1 -1
- package/dist/harness/checkpoints.d.ts +36 -0
- package/dist/harness/checkpoints.d.ts.map +1 -0
- package/dist/harness/checkpoints.js +156 -0
- package/dist/harness/checkpoints.js.map +1 -0
- package/dist/harness/config.d.ts +3 -0
- package/dist/harness/config.d.ts.map +1 -1
- package/dist/harness/config.js +35 -2
- package/dist/harness/config.js.map +1 -1
- package/dist/harness/config.test.js +21 -1
- package/dist/harness/config.test.js.map +1 -1
- package/dist/harness/hooks-env.test.d.ts +5 -0
- package/dist/harness/hooks-env.test.d.ts.map +1 -0
- package/dist/harness/hooks-env.test.js +41 -0
- package/dist/harness/hooks-env.test.js.map +1 -0
- package/dist/harness/hooks.d.ts +7 -0
- package/dist/harness/hooks.d.ts.map +1 -1
- package/dist/harness/hooks.js +14 -0
- package/dist/harness/hooks.js.map +1 -1
- package/dist/harness/keybindings.d.ts.map +1 -1
- package/dist/harness/keybindings.js +4 -0
- package/dist/harness/keybindings.js.map +1 -1
- package/dist/harness/memory.d.ts +19 -0
- package/dist/harness/memory.d.ts.map +1 -1
- package/dist/harness/memory.js +85 -0
- package/dist/harness/memory.js.map +1 -1
- package/dist/harness/onboarding.d.ts +1 -1
- package/dist/harness/onboarding.d.ts.map +1 -1
- package/dist/harness/onboarding.js +59 -4
- package/dist/harness/onboarding.js.map +1 -1
- package/dist/harness/onboarding.test.d.ts +5 -0
- package/dist/harness/onboarding.test.d.ts.map +1 -0
- package/dist/harness/onboarding.test.js +93 -0
- package/dist/harness/onboarding.test.js.map +1 -0
- package/dist/harness/rules.d.ts +6 -1
- package/dist/harness/rules.d.ts.map +1 -1
- package/dist/harness/rules.js +52 -5
- package/dist/harness/rules.js.map +1 -1
- package/dist/harness/rules.test.js +30 -1
- package/dist/harness/rules.test.js.map +1 -1
- package/dist/harness/session.d.ts +8 -1
- package/dist/harness/session.d.ts.map +1 -1
- package/dist/harness/session.js +13 -5
- package/dist/harness/session.js.map +1 -1
- package/dist/harness/store.d.ts +46 -0
- package/dist/harness/store.d.ts.map +1 -0
- package/dist/harness/store.js +56 -0
- package/dist/harness/store.js.map +1 -0
- package/dist/harness/store.test.d.ts +2 -0
- package/dist/harness/store.test.d.ts.map +1 -0
- package/dist/harness/store.test.js +71 -0
- package/dist/harness/store.test.js.map +1 -0
- package/dist/harness/submit-handler.d.ts +2 -0
- package/dist/harness/submit-handler.d.ts.map +1 -1
- package/dist/harness/submit-handler.js +3 -0
- package/dist/harness/submit-handler.js.map +1 -1
- package/dist/main.js +153 -26
- package/dist/main.js.map +1 -1
- package/dist/mcp/client.d.ts +2 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +10 -2
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/loader.d.ts +2 -0
- package/dist/mcp/loader.d.ts.map +1 -1
- package/dist/mcp/loader.js +34 -18
- package/dist/mcp/loader.js.map +1 -1
- package/dist/mcp/loader.test.d.ts +7 -0
- package/dist/mcp/loader.test.d.ts.map +1 -0
- package/dist/mcp/loader.test.js +25 -0
- package/dist/mcp/loader.test.js.map +1 -0
- package/dist/providers/anthropic-convert.test.d.ts +5 -0
- package/dist/providers/anthropic-convert.test.d.ts.map +1 -0
- package/dist/providers/anthropic-convert.test.js +98 -0
- package/dist/providers/anthropic-convert.test.js.map +1 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +23 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/stream-parsing.test.d.ts +6 -0
- package/dist/providers/stream-parsing.test.d.ts.map +1 -0
- package/dist/providers/stream-parsing.test.js +174 -0
- package/dist/providers/stream-parsing.test.js.map +1 -0
- package/dist/query/compress.d.ts +17 -0
- package/dist/query/compress.d.ts.map +1 -0
- package/dist/query/compress.js +115 -0
- package/dist/query/compress.js.map +1 -0
- package/dist/query/errors.d.ts +10 -0
- package/dist/query/errors.d.ts.map +1 -0
- package/dist/query/errors.js +22 -0
- package/dist/query/errors.js.map +1 -0
- package/dist/query/index.d.ts +15 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +199 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/tools.d.ts +17 -0
- package/dist/query/tools.d.ts.map +1 -0
- package/dist/query/tools.js +129 -0
- package/dist/query/tools.js.map +1 -0
- package/dist/query/types.d.ts +31 -0
- package/dist/query/types.d.ts.map +1 -0
- package/dist/query/types.js +5 -0
- package/dist/query/types.js.map +1 -0
- package/dist/query.d.ts +8 -38
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +7 -444
- package/dist/query.js.map +1 -1
- package/dist/query.test.js +1 -1
- package/dist/query.test.js.map +1 -1
- package/dist/renderer/cells.d.ts.map +1 -1
- package/dist/renderer/cells.js +15 -2
- package/dist/renderer/cells.js.map +1 -1
- package/dist/renderer/colors.d.ts +8 -0
- package/dist/renderer/colors.d.ts.map +1 -0
- package/dist/renderer/colors.js +18 -0
- package/dist/renderer/colors.js.map +1 -0
- package/dist/renderer/diff.test.d.ts +5 -0
- package/dist/renderer/diff.test.d.ts.map +1 -0
- package/dist/renderer/diff.test.js +140 -0
- package/dist/renderer/diff.test.js.map +1 -0
- package/dist/renderer/differ.d.ts +1 -5
- package/dist/renderer/differ.d.ts.map +1 -1
- package/dist/renderer/differ.js +3 -20
- package/dist/renderer/differ.js.map +1 -1
- package/dist/renderer/e2e.test.js +136 -53
- package/dist/renderer/e2e.test.js.map +1 -1
- package/dist/renderer/image.test.d.ts +5 -0
- package/dist/renderer/image.test.d.ts.map +1 -0
- package/dist/renderer/image.test.js +66 -0
- package/dist/renderer/image.test.js.map +1 -0
- package/dist/renderer/index.d.ts +28 -16
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +289 -222
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/layout.d.ts +14 -5
- package/dist/renderer/layout.d.ts.map +1 -1
- package/dist/renderer/layout.js +522 -388
- package/dist/renderer/layout.js.map +1 -1
- package/dist/renderer/markdown.d.ts.map +1 -1
- package/dist/renderer/markdown.js +42 -36
- package/dist/renderer/markdown.js.map +1 -1
- package/dist/renderer/perf.test.js +1 -4
- package/dist/renderer/perf.test.js.map +1 -1
- package/dist/renderer/session-browser.test.d.ts +6 -0
- package/dist/renderer/session-browser.test.d.ts.map +1 -0
- package/dist/renderer/session-browser.test.js +95 -0
- package/dist/renderer/session-browser.test.js.map +1 -0
- package/dist/renderer/ui-ux.test.d.ts +15 -0
- package/dist/renderer/ui-ux.test.d.ts.map +1 -0
- package/dist/renderer/ui-ux.test.js +470 -0
- package/dist/renderer/ui-ux.test.js.map +1 -0
- package/dist/repl.d.ts.map +1 -1
- package/dist/repl.js +192 -67
- package/dist/repl.js.map +1 -1
- package/dist/services/StreamingToolExecutor.d.ts.map +1 -1
- package/dist/services/StreamingToolExecutor.js +4 -2
- package/dist/services/StreamingToolExecutor.js.map +1 -1
- package/dist/services/agent-messaging.d.ts +68 -0
- package/dist/services/agent-messaging.d.ts.map +1 -0
- package/dist/services/agent-messaging.js +121 -0
- package/dist/services/agent-messaging.js.map +1 -0
- package/dist/services/agent-messaging.test.d.ts +2 -0
- package/dist/services/agent-messaging.test.d.ts.map +1 -0
- package/dist/services/agent-messaging.test.js +88 -0
- package/dist/services/agent-messaging.test.js.map +1 -0
- package/dist/services/cron.d.ts +40 -0
- package/dist/services/cron.d.ts.map +1 -0
- package/dist/services/cron.js +90 -0
- package/dist/services/cron.js.map +1 -0
- package/dist/services/cron.test.d.ts +2 -0
- package/dist/services/cron.test.d.ts.map +1 -0
- package/dist/services/cron.test.js +49 -0
- package/dist/services/cron.test.js.map +1 -0
- package/dist/tools/AgentTool/index.d.ts +9 -0
- package/dist/tools/AgentTool/index.d.ts.map +1 -1
- package/dist/tools/AgentTool/index.js +89 -6
- package/dist/tools/AgentTool/index.js.map +1 -1
- package/dist/tools/BashTool/index.d.ts +6 -0
- package/dist/tools/BashTool/index.d.ts.map +1 -1
- package/dist/tools/BashTool/index.js +39 -1
- package/dist/tools/BashTool/index.js.map +1 -1
- package/dist/tools/FileEditTool/index.js +4 -4
- package/dist/tools/FileEditTool/index.js.map +1 -1
- package/dist/tools/FileReadTool/index.d.ts +3 -0
- package/dist/tools/FileReadTool/index.d.ts.map +1 -1
- package/dist/tools/FileReadTool/index.js +102 -4
- package/dist/tools/FileReadTool/index.js.map +1 -1
- package/dist/tools/FileWriteTool/index.d.ts.map +1 -1
- package/dist/tools/FileWriteTool/index.js +20 -5
- package/dist/tools/FileWriteTool/index.js.map +1 -1
- package/dist/tools/GlobTool/index.d.ts.map +1 -1
- package/dist/tools/GlobTool/index.js +4 -61
- package/dist/tools/GlobTool/index.js.map +1 -1
- package/dist/tools/GrepTool/index.d.ts +30 -0
- package/dist/tools/GrepTool/index.d.ts.map +1 -1
- package/dist/tools/GrepTool/index.js +153 -72
- package/dist/tools/GrepTool/index.js.map +1 -1
- package/dist/tools/LSTool/index.d.ts +3 -0
- package/dist/tools/LSTool/index.d.ts.map +1 -1
- package/dist/tools/LSTool/index.js +44 -29
- package/dist/tools/LSTool/index.js.map +1 -1
- package/dist/tools/TaskCreateTool/index.d.ts +6 -0
- package/dist/tools/TaskCreateTool/index.d.ts.map +1 -1
- package/dist/tools/TaskCreateTool/index.js +8 -2
- package/dist/tools/TaskCreateTool/index.js.map +1 -1
- package/dist/tools/TaskGetTool/index.d.ts +12 -0
- package/dist/tools/TaskGetTool/index.d.ts.map +1 -0
- package/dist/tools/TaskGetTool/index.js +50 -0
- package/dist/tools/TaskGetTool/index.js.map +1 -0
- package/dist/tools/TaskOutputTool/index.d.ts +15 -0
- package/dist/tools/TaskOutputTool/index.d.ts.map +1 -0
- package/dist/tools/TaskOutputTool/index.js +45 -0
- package/dist/tools/TaskOutputTool/index.js.map +1 -0
- package/dist/tools/TaskStopTool/index.d.ts +15 -0
- package/dist/tools/TaskStopTool/index.d.ts.map +1 -0
- package/dist/tools/TaskStopTool/index.js +51 -0
- package/dist/tools/TaskStopTool/index.js.map +1 -0
- package/dist/tools/TaskUpdateTool/index.d.ts +21 -3
- package/dist/tools/TaskUpdateTool/index.d.ts.map +1 -1
- package/dist/tools/TaskUpdateTool/index.js +48 -3
- package/dist/tools/TaskUpdateTool/index.js.map +1 -1
- package/dist/tools/tools-basic.test.js +191 -2
- package/dist/tools/tools-basic.test.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +6 -0
- package/dist/tools.js.map +1 -1
- package/dist/types/permissions.d.ts +2 -2
- package/dist/types/permissions.d.ts.map +1 -1
- package/dist/types/permissions.js +59 -13
- package/dist/types/permissions.js.map +1 -1
- package/dist/types/permissions.test.js +57 -0
- package/dist/types/permissions.test.js.map +1 -1
- package/dist/utils/bash-safety.d.ts +18 -0
- package/dist/utils/bash-safety.d.ts.map +1 -0
- package/dist/utils/bash-safety.js +227 -0
- package/dist/utils/bash-safety.js.map +1 -0
- package/dist/utils/bash-safety.test.d.ts +2 -0
- package/dist/utils/bash-safety.test.d.ts.map +1 -0
- package/dist/utils/bash-safety.test.js +112 -0
- package/dist/utils/bash-safety.test.js.map +1 -0
- package/dist/utils/fs.d.ts +15 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +64 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/fs.test.d.ts +5 -0
- package/dist/utils/fs.test.d.ts.map +1 -0
- package/dist/utils/fs.test.js +82 -0
- package/dist/utils/fs.test.js.map +1 -0
- package/dist/utils/safe-env.d.ts +10 -0
- package/dist/utils/safe-env.d.ts.map +1 -0
- package/dist/utils/safe-env.js +40 -0
- package/dist/utils/safe-env.js.map +1 -0
- package/package.json +3 -1
package/dist/renderer/layout.js
CHANGED
|
@@ -7,13 +7,18 @@ import { renderDiff } from './diff.js';
|
|
|
7
7
|
import { isImageOutput, renderImageInline } from './image.js';
|
|
8
8
|
import { renderSessionBrowser } from './session-browser.js';
|
|
9
9
|
import { getTheme } from '../utils/theme-data.js';
|
|
10
|
-
//
|
|
11
|
-
// Style helper
|
|
10
|
+
// ── Style constants ──
|
|
12
11
|
const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
|
|
13
|
-
// Theme-independent styles
|
|
14
12
|
const S_TEXT = s(null);
|
|
15
13
|
const S_DIM = s(null, false, true);
|
|
16
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);
|
|
17
22
|
// Theme-dependent styles — lazily initialized on first rasterize() call
|
|
18
23
|
let S_USER;
|
|
19
24
|
let S_ASSISTANT;
|
|
@@ -37,9 +42,441 @@ function ensureStyles() {
|
|
|
37
42
|
S_GREEN = s(t.success);
|
|
38
43
|
}
|
|
39
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' ? '⊕' : tc.status === 'done' ? '◈' : '◇')
|
|
139
|
+
: (tc.status === 'running' ? SPINNER_CHARS[state.spinnerFrame % SPINNER_CHARS.length] : tc.status === 'done' ? '✓' : '✗');
|
|
140
|
+
const statusStyle = tc.status === 'error' ? S_ERROR : tc.status === 'done' ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
|
|
141
|
+
const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
|
|
142
|
+
const isExpanded = state.expandedToolCalls.has(callId);
|
|
143
|
+
const canExpand = tc.status !== 'running' && tc.output;
|
|
144
|
+
// Collapse/expand indicator
|
|
145
|
+
if (canExpand) {
|
|
146
|
+
grid.writeText(r, 0, isExpanded ? '▼' : '▶', S_DIM);
|
|
147
|
+
}
|
|
148
|
+
grid.writeText(r, 2, `${icon} `, statusStyle);
|
|
149
|
+
grid.writeText(r, 4, tc.toolName, nameStyle);
|
|
150
|
+
let afterName = 4 + tc.toolName.length + 1;
|
|
151
|
+
if (tc.args) {
|
|
152
|
+
const maxArgs = w - afterName - 15;
|
|
153
|
+
if (maxArgs > 5) {
|
|
154
|
+
const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? '…' : '');
|
|
155
|
+
grid.writeText(r, afterName, argsText, S_DIM);
|
|
156
|
+
afterName += argsText.length + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Elapsed time for running tools
|
|
160
|
+
if (tc.status === 'running' && tc.startedAt) {
|
|
161
|
+
const elapsed = Math.floor((Date.now() - tc.startedAt) / 1000);
|
|
162
|
+
if (elapsed > 0) {
|
|
163
|
+
const lineCount = tc.liveOutput?.length ?? 0;
|
|
164
|
+
const elapsedStr = lineCount > 0 ? `${elapsed}s · ${lineCount} lines` : `${elapsed}s`;
|
|
165
|
+
grid.writeText(r, Math.min(afterName, w - elapsedStr.length - 2), elapsedStr, S_DIM);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Result summary for completed tools
|
|
169
|
+
if (tc.status !== 'running' && tc.resultSummary) {
|
|
170
|
+
const elapsed = tc.startedAt ? Math.floor((Date.now() - tc.startedAt) / 1000) : 0;
|
|
171
|
+
const suffix = elapsed > 0 ? `${tc.resultSummary} · ${elapsed}s` : tc.resultSummary;
|
|
172
|
+
grid.writeText(r, Math.min(afterName, w - suffix.length - 2), suffix, S_DIM);
|
|
173
|
+
}
|
|
174
|
+
r++;
|
|
175
|
+
// Agent description line
|
|
176
|
+
if (isAgent && tc.agentDescription && r < limit) {
|
|
177
|
+
grid.writeText(r, 6, tc.agentDescription.slice(0, w - 8), S_DIM);
|
|
178
|
+
r++;
|
|
179
|
+
}
|
|
180
|
+
// Live streaming output while running
|
|
181
|
+
if (tc.status === 'running' && tc.liveOutput && tc.liveOutput.length > 0) {
|
|
182
|
+
const overflow = tc.liveOutput.length > opts.maxLiveLines ? tc.liveOutput.length - opts.maxLiveLines : 0;
|
|
183
|
+
if (opts.showOverflow && overflow > 0 && r < limit) {
|
|
184
|
+
grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
|
|
185
|
+
r++;
|
|
186
|
+
}
|
|
187
|
+
const visible = overflow > 0 ? tc.liveOutput.slice(-opts.maxLiveLines) : tc.liveOutput;
|
|
188
|
+
for (const line of visible) {
|
|
189
|
+
if (r >= limit)
|
|
190
|
+
break;
|
|
191
|
+
grid.writeText(r, 6, line.slice(0, w - 8), S_DIM);
|
|
192
|
+
r++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Final output — collapsed by default (only show when expanded via Tab)
|
|
196
|
+
if (tc.output && tc.status !== 'running' && isExpanded && r < limit) {
|
|
197
|
+
// Image results: show inline placeholder
|
|
198
|
+
if (isImageOutput(tc.output)) {
|
|
199
|
+
const label = renderImageInline(tc.output);
|
|
200
|
+
grid.writeText(r, 6, label.slice(0, w - 8), S_DIM);
|
|
201
|
+
r++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const outLines = tc.output.split('\n');
|
|
205
|
+
const maxOut = 20;
|
|
206
|
+
const showLines = outLines.slice(0, maxOut);
|
|
207
|
+
for (const line of showLines) {
|
|
208
|
+
if (r >= limit)
|
|
209
|
+
break;
|
|
210
|
+
const lineStyle = tc.status === 'error' ? S_ERROR : S_DIM;
|
|
211
|
+
grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
|
|
212
|
+
r++;
|
|
213
|
+
}
|
|
214
|
+
if (outLines.length > maxOut && r < limit) {
|
|
215
|
+
grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
|
|
216
|
+
r++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return r;
|
|
221
|
+
}
|
|
222
|
+
function renderContextWarningSection(state, grid, r, limit) {
|
|
223
|
+
if (!state.contextWarning || r >= limit)
|
|
224
|
+
return r;
|
|
225
|
+
const warnStyle = { fg: 'yellow', bg: null, bold: state.contextWarning.critical, dim: false, underline: false };
|
|
226
|
+
grid.writeText(r, 0, state.contextWarning.text, warnStyle);
|
|
227
|
+
return r + 1;
|
|
228
|
+
}
|
|
229
|
+
function renderPermissionBoxSection(state, grid, nextRow, h, opts) {
|
|
230
|
+
if (!state.permissionBox || grid.width < 20)
|
|
231
|
+
return nextRow;
|
|
232
|
+
const w = grid.width;
|
|
233
|
+
const { toolName, description, riskLevel } = state.permissionBox;
|
|
234
|
+
const riskColor = riskLevel === 'high' ? 'red' : riskLevel === 'medium' ? 'yellow' : 'green';
|
|
235
|
+
const riskStyle = { fg: riskColor, bg: null, bold: true, dim: false, underline: false };
|
|
236
|
+
if (opts.boxed) {
|
|
237
|
+
if ((h - nextRow) < 6)
|
|
238
|
+
return nextRow;
|
|
239
|
+
const riskDim = { fg: riskColor, bg: null, bold: false, dim: true, underline: false };
|
|
240
|
+
const boxWidth = Math.max(15, Math.min(w - 2, 70));
|
|
241
|
+
// Top border
|
|
242
|
+
grid.writeText(nextRow, 1, '╭' + '─'.repeat(boxWidth - 2) + '╮', riskDim);
|
|
243
|
+
nextRow++;
|
|
244
|
+
// Tool name + risk
|
|
245
|
+
grid.writeText(nextRow, 1, '│ ', riskDim);
|
|
246
|
+
grid.writeText(nextRow, 3, '⚠ ', riskStyle);
|
|
247
|
+
grid.writeText(nextRow, 5, toolName, { ...riskStyle });
|
|
248
|
+
grid.writeText(nextRow, 5 + toolName.length, ` ${riskLevel} risk`, S_DIM);
|
|
249
|
+
grid.writeText(nextRow, boxWidth, '│', riskDim);
|
|
250
|
+
nextRow++;
|
|
251
|
+
// Description (truncated)
|
|
252
|
+
const rawDesc = state.permissionBox.suggestion || 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
|
+
// Inline diff
|
|
259
|
+
if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
|
|
260
|
+
grid.writeText(nextRow, 1, '│', riskDim);
|
|
261
|
+
nextRow++;
|
|
262
|
+
const availDiffRows = Math.min(opts.maxDiffHeight, h - nextRow - 3);
|
|
263
|
+
const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, boxWidth - 2, availDiffRows);
|
|
264
|
+
for (let dr = 0; dr < diffRows; dr++) {
|
|
265
|
+
if (nextRow + dr < grid.height) {
|
|
266
|
+
grid.setCell(nextRow + dr, 1, '│', riskDim);
|
|
267
|
+
grid.setCell(nextRow + dr, boxWidth, '│', riskDim);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
nextRow += diffRows;
|
|
271
|
+
}
|
|
272
|
+
// Action keys
|
|
273
|
+
grid.writeText(nextRow, 1, '│ ', riskDim);
|
|
274
|
+
let kc = 3;
|
|
275
|
+
grid.writeText(nextRow, kc, 'Y', S_KEY_GREEN);
|
|
276
|
+
kc += 1;
|
|
277
|
+
grid.writeText(nextRow, kc, 'es', S_DIM);
|
|
278
|
+
kc += 2;
|
|
279
|
+
grid.writeText(nextRow, kc, ' ', S_DIM);
|
|
280
|
+
kc += 2;
|
|
281
|
+
grid.writeText(nextRow, kc, 'N', S_KEY_RED);
|
|
282
|
+
kc += 1;
|
|
283
|
+
grid.writeText(nextRow, kc, 'o', S_DIM);
|
|
284
|
+
kc += 1;
|
|
285
|
+
if (state.permissionDiffInfo) {
|
|
286
|
+
grid.writeText(nextRow, kc, ' ', S_DIM);
|
|
287
|
+
kc += 2;
|
|
288
|
+
grid.writeText(nextRow, kc, 'D', S_KEY_CYAN);
|
|
289
|
+
kc += 1;
|
|
290
|
+
grid.writeText(nextRow, kc, 'iff', S_DIM);
|
|
291
|
+
kc += 3;
|
|
292
|
+
}
|
|
293
|
+
grid.writeText(nextRow, boxWidth, '│', riskDim);
|
|
294
|
+
nextRow++;
|
|
295
|
+
// Bottom border
|
|
296
|
+
grid.writeText(nextRow, 1, '╰' + '─'.repeat(boxWidth - 2) + '╯', riskDim);
|
|
297
|
+
nextRow++;
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Compact mode (rasterizeLive)
|
|
301
|
+
if ((h - nextRow) < 4)
|
|
302
|
+
return nextRow;
|
|
303
|
+
grid.writeText(nextRow, 1, `⚠ ${toolName} (${riskLevel} risk)`, riskStyle);
|
|
304
|
+
nextRow++;
|
|
305
|
+
grid.writeText(nextRow, 1, 'Y', S_KEY_GREEN);
|
|
306
|
+
grid.writeText(nextRow, 2, 'es ', S_DIM);
|
|
307
|
+
grid.writeText(nextRow, 6, 'N', S_KEY_RED);
|
|
308
|
+
grid.writeText(nextRow, 7, 'o', S_DIM);
|
|
309
|
+
if (state.permissionDiffInfo) {
|
|
310
|
+
grid.writeText(nextRow, 10, 'D', S_KEY_CYAN);
|
|
311
|
+
grid.writeText(nextRow, 11, 'iff', S_DIM);
|
|
312
|
+
}
|
|
313
|
+
nextRow++;
|
|
314
|
+
// Inline diff (when toggled)
|
|
315
|
+
if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
|
|
316
|
+
const availDiffRows = Math.min(15, h - nextRow - 3);
|
|
317
|
+
const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, Math.min(w - 2, 70), availDiffRows);
|
|
318
|
+
nextRow += diffRows;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return nextRow;
|
|
322
|
+
}
|
|
323
|
+
function renderQuestionPromptSection(state, grid, nextRow, h, opts) {
|
|
324
|
+
if (!state.questionPrompt || grid.width < 20)
|
|
325
|
+
return { nextRow, questionInputRow: -1 };
|
|
326
|
+
const w = grid.width;
|
|
327
|
+
const { question, options, input, cursor } = state.questionPrompt;
|
|
328
|
+
const qStyle = { fg: 'yellow', bg: null, bold: false, dim: false, underline: false };
|
|
329
|
+
if (opts.boxed) {
|
|
330
|
+
const qBorder = { fg: 'yellow', bg: null, bold: false, dim: true, underline: false };
|
|
331
|
+
const qBoxWidth = Math.max(15, Math.min(w - 2, 70));
|
|
332
|
+
grid.writeText(nextRow, 1, '╭' + '─'.repeat(qBoxWidth - 2) + '╮', qBorder);
|
|
333
|
+
nextRow++;
|
|
334
|
+
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
335
|
+
grid.writeText(nextRow, 3, `❓ ${question}`, qStyle);
|
|
336
|
+
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
337
|
+
nextRow++;
|
|
338
|
+
if (options && options.length > 0) {
|
|
339
|
+
for (let oi = 0; oi < options.length; oi++) {
|
|
340
|
+
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
341
|
+
grid.writeText(nextRow, 5, `${oi + 1}. ${options[oi]}`, S_DIM);
|
|
342
|
+
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
343
|
+
nextRow++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const questionInputRow = nextRow;
|
|
347
|
+
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
348
|
+
grid.writeText(nextRow, 3, '❯ ', qStyle);
|
|
349
|
+
grid.writeText(nextRow, 5, input, S_TEXT);
|
|
350
|
+
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
351
|
+
nextRow++;
|
|
352
|
+
grid.writeText(nextRow, 1, '╰' + '─'.repeat(qBoxWidth - 2) + '╯', qBorder);
|
|
353
|
+
nextRow++;
|
|
354
|
+
return { nextRow, questionInputRow };
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
// Compact mode (rasterizeLive)
|
|
358
|
+
if ((h - nextRow) < 3)
|
|
359
|
+
return { nextRow, questionInputRow: -1 };
|
|
360
|
+
grid.writeText(nextRow, 1, `❓ ${question}`, S_TEXT);
|
|
361
|
+
nextRow++;
|
|
362
|
+
if (options) {
|
|
363
|
+
for (const opt of options) {
|
|
364
|
+
if (nextRow >= h)
|
|
365
|
+
break;
|
|
366
|
+
grid.writeText(nextRow, 3, opt, S_DIM);
|
|
367
|
+
nextRow++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const questionInputRow = nextRow;
|
|
371
|
+
grid.writeText(nextRow, 1, '❯ ', S_USER);
|
|
372
|
+
grid.writeText(nextRow, 3, input, S_TEXT);
|
|
373
|
+
nextRow++;
|
|
374
|
+
return { nextRow, questionInputRow };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function renderStatusLineSection(state, grid, nextRow, limit) {
|
|
378
|
+
if (!state.statusLine || nextRow >= limit)
|
|
379
|
+
return nextRow;
|
|
380
|
+
grid.writeText(nextRow, 0, state.statusLine, S_DIM);
|
|
381
|
+
return nextRow + 1;
|
|
382
|
+
}
|
|
383
|
+
function renderAutocompleteSection(state, grid, nextRow, limit, promptWidth) {
|
|
384
|
+
if (state.autocomplete.length === 0)
|
|
385
|
+
return nextRow;
|
|
386
|
+
const w = grid.width;
|
|
387
|
+
for (let ai = 0; ai < state.autocomplete.length; ai++) {
|
|
388
|
+
if (nextRow >= limit)
|
|
389
|
+
break;
|
|
390
|
+
const cmd = state.autocomplete[ai];
|
|
391
|
+
const desc = state.autocompleteDescriptions[ai] ?? '';
|
|
392
|
+
const selected = ai === state.autocompleteIndex;
|
|
393
|
+
const acStyle = selected ? s(getTheme().user, true) : s(null, false, true);
|
|
394
|
+
grid.writeText(nextRow, promptWidth, `/${cmd.padEnd(12)}`, acStyle);
|
|
395
|
+
if (desc && w > promptWidth + 15)
|
|
396
|
+
grid.writeText(nextRow, promptWidth + 13, desc.slice(0, w - promptWidth - 15), S_DIM);
|
|
397
|
+
nextRow++;
|
|
398
|
+
}
|
|
399
|
+
return nextRow;
|
|
400
|
+
}
|
|
401
|
+
function renderNotificationsSection(state, grid, nextRow, limit) {
|
|
402
|
+
if (!state.notifications || state.notifications.length === 0)
|
|
403
|
+
return nextRow;
|
|
404
|
+
for (const note of state.notifications.slice(-2)) {
|
|
405
|
+
if (nextRow >= limit)
|
|
406
|
+
break;
|
|
407
|
+
grid.writeText(nextRow, 0, ` ⚡ ${note.text}`, S_YELLOW);
|
|
408
|
+
nextRow++;
|
|
409
|
+
}
|
|
410
|
+
return nextRow;
|
|
411
|
+
}
|
|
412
|
+
function renderInputSection(state, grid, inputRow, limit, promptText, promptWidth) {
|
|
413
|
+
grid.writeText(inputRow, 0, promptText, S_USER);
|
|
414
|
+
const inputStart = promptWidth;
|
|
415
|
+
const inputLines = state.inputText.split('\n');
|
|
416
|
+
const maxInputLines = Math.min(inputLines.length, 5);
|
|
417
|
+
for (let li = 0; li < maxInputLines; li++) {
|
|
418
|
+
if (inputRow + li >= limit)
|
|
419
|
+
break;
|
|
420
|
+
if (li === 0) {
|
|
421
|
+
grid.writeText(inputRow, inputStart, inputLines[0], S_TEXT);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
grid.writeText(inputRow + li, inputStart, inputLines[li], S_TEXT);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Line count indicator for multi-line input
|
|
428
|
+
if (inputLines.length > 1) {
|
|
429
|
+
const lineCountStr = ` [${inputLines.length} lines]`;
|
|
430
|
+
const lineCountCol = Math.min(inputStart + (inputLines[0]?.length ?? 0) + 1, grid.width - lineCountStr.length - 1);
|
|
431
|
+
if (lineCountCol > inputStart)
|
|
432
|
+
grid.writeText(inputRow, lineCountCol, lineCountStr, S_DIM);
|
|
433
|
+
}
|
|
434
|
+
const hintsRow = inputRow + maxInputLines;
|
|
435
|
+
if (hintsRow < limit) {
|
|
436
|
+
const hintsText = inputLines.length > 1
|
|
437
|
+
? `${state.statusHints} | Alt+Enter newline`
|
|
438
|
+
: state.statusHints;
|
|
439
|
+
grid.writeText(hintsRow, 0, hintsText, S_DIM);
|
|
440
|
+
}
|
|
441
|
+
return inputRow + maxInputLines + 1;
|
|
442
|
+
}
|
|
443
|
+
function renderCompanionSection(state, grid, anchorRow, limit, promptWidth) {
|
|
444
|
+
if (!state.companionLines || grid.width < 50)
|
|
445
|
+
return;
|
|
446
|
+
const w = grid.width;
|
|
447
|
+
const compWidth = Math.max(...state.companionLines.map(l => l.length), 0);
|
|
448
|
+
const compStartCol = Math.max(0, w - compWidth - 1);
|
|
449
|
+
if (compStartCol <= promptWidth + 20)
|
|
450
|
+
return;
|
|
451
|
+
const compStyle = { fg: state.companionColor || 'cyan', bg: null, bold: false, dim: false, underline: false };
|
|
452
|
+
for (let i = 0; i < state.companionLines.length; i++) {
|
|
453
|
+
const compRow = anchorRow + i;
|
|
454
|
+
if (compRow >= limit)
|
|
455
|
+
break;
|
|
456
|
+
grid.writeText(compRow, compStartCol, state.companionLines[i], compStyle);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function computeCursorPosition(state, inputRow, inputStart, questionInputRow) {
|
|
460
|
+
if (state.questionPrompt && questionInputRow >= 0) {
|
|
461
|
+
// In boxed mode cursor col is 5 (after "│ ❯ "), in compact mode it's 3 (after "❯ ")
|
|
462
|
+
return { cursorRow: questionInputRow, cursorCol: 5 + state.questionPrompt.cursor };
|
|
463
|
+
}
|
|
464
|
+
const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
|
|
465
|
+
const cursorLines = textBeforeCursor.split('\n');
|
|
466
|
+
const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
|
|
467
|
+
const cursorColInLine = cursorLines[cursorLines.length - 1].length;
|
|
468
|
+
return { cursorRow: inputRow + cursorLineIdx, cursorCol: inputStart + cursorColInLine };
|
|
469
|
+
}
|
|
470
|
+
function getPromptText(state) {
|
|
471
|
+
const vimIndicator = state.vimMode ? (state.vimMode === 'normal' ? '[N] ' : '[I] ') : '';
|
|
472
|
+
const promptText = vimIndicator + '❯ ';
|
|
473
|
+
return { promptText, promptWidth: promptText.length };
|
|
474
|
+
}
|
|
475
|
+
// ── Main rasterization functions ──
|
|
40
476
|
/**
|
|
41
477
|
* Rasterize application state into the cell grid.
|
|
42
|
-
*
|
|
478
|
+
* Full-screen mode with message area + scrollbar + footer.
|
|
479
|
+
* Used by tests; production uses rasterizeLive().
|
|
43
480
|
*/
|
|
44
481
|
export function rasterize(state, grid) {
|
|
45
482
|
ensureStyles();
|
|
@@ -61,9 +498,7 @@ export function rasterize(state, grid) {
|
|
|
61
498
|
// ── Session browser overlay ──
|
|
62
499
|
if (state.sessionBrowser) {
|
|
63
500
|
const browserRows = renderSessionBrowser(grid, 0, 0, state.sessionBrowser, w, msgAreaHeight);
|
|
64
|
-
// Skip normal message rendering — show browser instead
|
|
65
501
|
const footerStart = Math.min(browserRows, msgAreaHeight);
|
|
66
|
-
// Render minimal footer (just input)
|
|
67
502
|
for (let c = 0; c < w; c++)
|
|
68
503
|
grid.setCell(footerStart, c, '─', S_BORDER);
|
|
69
504
|
const inputRow = footerStart + 1;
|
|
@@ -72,7 +507,6 @@ export function rasterize(state, grid) {
|
|
|
72
507
|
return { cursorRow: inputRow, cursorCol: 2 };
|
|
73
508
|
}
|
|
74
509
|
// ── Messages area (top) ──
|
|
75
|
-
// Compute total height of all messages + streaming
|
|
76
510
|
const allContent = [];
|
|
77
511
|
for (const msg of state.messages) {
|
|
78
512
|
if (msg.role === 'user') {
|
|
@@ -85,27 +519,21 @@ export function rasterize(state, grid) {
|
|
|
85
519
|
allContent.push({ role: 'system', content: msg.content, style: S_DIM, prefixStyle: S_DIM, prefix: ' ' });
|
|
86
520
|
}
|
|
87
521
|
}
|
|
88
|
-
// Add streaming text
|
|
89
522
|
if (state.loading && state.streamingText) {
|
|
90
523
|
allContent.push({ role: 'streaming', content: state.streamingText, style: S_TEXT, prefixStyle: S_ASSISTANT, prefix: '◆ ' });
|
|
91
524
|
}
|
|
92
|
-
// Thinking text rendered directly below (shimmer effect needs per-char styling)
|
|
93
|
-
// Spinner rendered directly below (shimmer effect needs per-char styling)
|
|
94
|
-
// Add error
|
|
95
525
|
if (state.errorText) {
|
|
96
526
|
allContent.push({ role: 'error', content: state.errorText, style: S_ERROR, prefixStyle: S_ERROR, prefix: '✗ ' });
|
|
97
527
|
}
|
|
98
|
-
// Render messages top-down (scroll up when content exceeds area)
|
|
99
528
|
const prefixLen = 2;
|
|
100
529
|
const contentWidth = w - 1; // reserve rightmost column for scrollbar
|
|
101
530
|
const textWidth = contentWidth - prefixLen;
|
|
102
531
|
// Pre-compute total height to handle scrolling
|
|
103
532
|
let totalRows = 0;
|
|
104
|
-
// Banner height (compact on small terminals, hidden if very small)
|
|
105
533
|
if (state.bannerLines && h >= 30) {
|
|
106
534
|
const compact = h < 40;
|
|
107
535
|
const visibleLines = compact ? Math.min(2, state.bannerLines.length) : state.bannerLines.length;
|
|
108
|
-
totalRows += visibleLines + 1;
|
|
536
|
+
totalRows += visibleLines + 1;
|
|
109
537
|
}
|
|
110
538
|
for (const item of allContent) {
|
|
111
539
|
if (item.role === 'user' && totalRows > 0)
|
|
@@ -120,32 +548,28 @@ export function rasterize(state, grid) {
|
|
|
120
548
|
}
|
|
121
549
|
}
|
|
122
550
|
}
|
|
123
|
-
// Include non-message content in totalRows for accurate scroll indicator
|
|
124
551
|
if (state.thinkingText) {
|
|
125
552
|
totalRows += state.thinkingExpanded ? Math.min(state.thinkingText.split('\n').length, 10) : 1;
|
|
126
553
|
}
|
|
127
554
|
if (!state.loading && state.lastThinkingSummary)
|
|
128
555
|
totalRows += 1;
|
|
129
556
|
if (state.loading && !state.streamingText && !state.thinkingText)
|
|
130
|
-
totalRows += 1;
|
|
557
|
+
totalRows += 1;
|
|
131
558
|
for (const [callId, tc] of state.toolCalls) {
|
|
132
|
-
totalRows += 1;
|
|
559
|
+
totalRows += 1;
|
|
133
560
|
if (tc.isAgent && tc.agentDescription)
|
|
134
|
-
totalRows += 1;
|
|
561
|
+
totalRows += 1;
|
|
135
562
|
if (tc.status === 'running' && tc.liveOutput)
|
|
136
563
|
totalRows += Math.min(tc.liveOutput.length, 5);
|
|
137
|
-
// Collapsed tools show 0 output lines; expanded show up to 20
|
|
138
564
|
if (tc.output && tc.status !== 'running' && state.expandedToolCalls.has(callId)) {
|
|
139
565
|
totalRows += Math.min(tc.output.split('\n').length, 20);
|
|
140
566
|
}
|
|
141
567
|
}
|
|
142
568
|
if (state.contextWarning)
|
|
143
569
|
totalRows += 1;
|
|
144
|
-
// If content exceeds area, scroll: offset so latest content is visible at bottom
|
|
145
|
-
// manualScroll > 0 means user scrolled up by that many rows
|
|
146
570
|
const autoOffset = totalRows > msgAreaHeight ? totalRows - msgAreaHeight : 0;
|
|
147
571
|
const scrollOffset = Math.max(0, autoOffset - state.manualScroll);
|
|
148
|
-
// Scrollbar geometry
|
|
572
|
+
// Scrollbar geometry
|
|
149
573
|
const hasScrollbar = totalRows > msgAreaHeight;
|
|
150
574
|
let thumbStart = 0;
|
|
151
575
|
let thumbSize = msgAreaHeight;
|
|
@@ -154,13 +578,10 @@ export function rasterize(state, grid) {
|
|
|
154
578
|
thumbStart = Math.round((scrollOffset / Math.max(1, totalRows)) * (msgAreaHeight - thumbSize));
|
|
155
579
|
}
|
|
156
580
|
let r = 0;
|
|
157
|
-
let virtualR = 0;
|
|
581
|
+
let virtualR = 0;
|
|
158
582
|
let contentIdx = 0;
|
|
159
|
-
// ── Banner
|
|
583
|
+
// ── Banner ──
|
|
160
584
|
if (state.bannerLines && h >= 30) {
|
|
161
|
-
const S_BANNER = s('cyan');
|
|
162
|
-
const S_BANNER_DIM = s(null, false, true);
|
|
163
|
-
// On small terminals, show only the last 2 lines (version + cwd info)
|
|
164
585
|
const compact = h < 40;
|
|
165
586
|
const startLine = compact ? Math.max(0, state.bannerLines.length - 2) : 0;
|
|
166
587
|
for (let i = startLine; i < state.bannerLines.length; i++) {
|
|
@@ -172,16 +593,15 @@ export function rasterize(state, grid) {
|
|
|
172
593
|
}
|
|
173
594
|
virtualR++;
|
|
174
595
|
}
|
|
175
|
-
// Blank line after banner
|
|
176
596
|
if (virtualR >= scrollOffset && r < msgAreaHeight) {
|
|
177
597
|
r++;
|
|
178
598
|
}
|
|
179
599
|
virtualR++;
|
|
180
600
|
}
|
|
601
|
+
// ── Messages ──
|
|
181
602
|
for (const item of allContent) {
|
|
182
603
|
if (r >= msgAreaHeight)
|
|
183
604
|
break;
|
|
184
|
-
// Divider before user messages (except first)
|
|
185
605
|
if (item.role === 'user' && contentIdx > 0) {
|
|
186
606
|
if (virtualR >= scrollOffset) {
|
|
187
607
|
for (let c = 0; c < w; c++) {
|
|
@@ -191,7 +611,6 @@ export function rasterize(state, grid) {
|
|
|
191
611
|
}
|
|
192
612
|
virtualR++;
|
|
193
613
|
}
|
|
194
|
-
// Compute how many rows this content will take
|
|
195
614
|
let itemRows;
|
|
196
615
|
if (item.role === 'assistant' || item.role === 'streaming') {
|
|
197
616
|
itemRows = measureMarkdown(item.content, contentWidth);
|
|
@@ -204,14 +623,11 @@ export function rasterize(state, grid) {
|
|
|
204
623
|
}
|
|
205
624
|
}
|
|
206
625
|
if (virtualR + itemRows <= scrollOffset) {
|
|
207
|
-
// Entirely above viewport — skip
|
|
208
626
|
virtualR += itemRows;
|
|
209
627
|
contentIdx++;
|
|
210
628
|
continue;
|
|
211
629
|
}
|
|
212
|
-
// Write prefix
|
|
213
630
|
grid.writeText(r, 0, item.prefix, item.prefixStyle);
|
|
214
|
-
// Write content — use markdown renderer for assistant messages
|
|
215
631
|
let rows;
|
|
216
632
|
if (item.role === 'assistant' || item.role === 'streaming') {
|
|
217
633
|
rows = renderMarkdown(grid, r, prefixLen, item.content, contentWidth, state.codeBlocksExpanded, msgAreaHeight);
|
|
@@ -223,177 +639,13 @@ export function rasterize(state, grid) {
|
|
|
223
639
|
virtualR += itemRows;
|
|
224
640
|
contentIdx++;
|
|
225
641
|
}
|
|
226
|
-
// ── Thinking
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const tLine of thinkLines) {
|
|
234
|
-
if (r >= msgAreaHeight)
|
|
235
|
-
break;
|
|
236
|
-
grid.writeText(r, 0, '💭 ', S_DIM);
|
|
237
|
-
const chars = [...tLine];
|
|
238
|
-
for (let ci = 0; ci < chars.length && ci + 3 < w; ci++) {
|
|
239
|
-
grid.setCell(r, 3 + ci, chars[ci], Math.abs(ci - shimmerPos) <= 2 ? S_BRIGHT : S_DIM);
|
|
240
|
-
}
|
|
241
|
-
r++;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
// Collapsed: single line with live indicator
|
|
246
|
-
const lineCount = state.thinkingText.split('\n').length;
|
|
247
|
-
const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
|
|
248
|
-
const summary = `∴ Thinking${elapsed > 0 ? ` (${elapsed}s)` : ''} — ${lineCount} lines [Ctrl+O expand]`;
|
|
249
|
-
grid.writeText(r, 0, summary, S_DIM);
|
|
250
|
-
r++;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
// ── Collapsed thinking summary (after completion) ──
|
|
254
|
-
if (!state.loading && state.lastThinkingSummary && r < msgAreaHeight) {
|
|
255
|
-
if (state.thinkingExpanded) {
|
|
256
|
-
// Expanded mode not applicable after completion since text was cleared
|
|
257
|
-
grid.writeText(r, 0, state.lastThinkingSummary, S_DIM);
|
|
258
|
-
r++;
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
grid.writeText(r, 0, state.lastThinkingSummary, S_DIM);
|
|
262
|
-
r++;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// ── Shimmer spinner ──
|
|
266
|
-
if (state.loading && !state.streamingText && !state.thinkingText && r < msgAreaHeight) {
|
|
267
|
-
const thinkText = 'Thinking';
|
|
268
|
-
const elapsed = state.thinkingStartedAt ? Math.floor((Date.now() - state.thinkingStartedAt) / 1000) : 0;
|
|
269
|
-
// Color transitions: magenta → yellow (30s+) → red (60s+)
|
|
270
|
-
const t = getTheme();
|
|
271
|
-
const baseColor = elapsed > 60 ? t.error : elapsed > 30 ? t.stall : t.primary;
|
|
272
|
-
const shimmerColor = elapsed > 60 ? t.stallShimmer : elapsed > 30 ? t.warning : t.primaryShimmer;
|
|
273
|
-
const baseStyle = { fg: baseColor, bg: null, bold: false, dim: false, underline: false };
|
|
274
|
-
// Prefix
|
|
275
|
-
const prefixStyle = { ...baseStyle, bold: true };
|
|
276
|
-
grid.writeText(r, 0, '◆ ', prefixStyle);
|
|
277
|
-
// Shimmer effect: bright color sweeps across text
|
|
278
|
-
const shimmerPos = state.spinnerFrame % (thinkText.length + 6);
|
|
279
|
-
const shimmerStyle = { fg: shimmerColor, bg: null, bold: true, dim: false, underline: false };
|
|
280
|
-
for (let ci = 0; ci < thinkText.length; ci++) {
|
|
281
|
-
grid.setCell(r, 2 + ci, thinkText[ci], Math.abs(ci - shimmerPos) <= 1 ? shimmerStyle : baseStyle);
|
|
282
|
-
}
|
|
283
|
-
// Suffix: elapsed + tokens
|
|
284
|
-
let suffix = '';
|
|
285
|
-
if (elapsed > 0)
|
|
286
|
-
suffix += ` ${elapsed}s`;
|
|
287
|
-
if (state.tokenCount > 0) {
|
|
288
|
-
const tokStr = state.tokenCount >= 1000 ? `${(state.tokenCount / 1000).toFixed(1)}K` : `${state.tokenCount}`;
|
|
289
|
-
suffix += ` | ${tokStr} tokens`;
|
|
290
|
-
}
|
|
291
|
-
suffix += '...';
|
|
292
|
-
grid.writeText(r, 2 + thinkText.length, suffix, S_DIM);
|
|
293
|
-
r++;
|
|
294
|
-
}
|
|
295
|
-
// ── Tool calls (below messages, above footer) ──
|
|
296
|
-
for (const [callId, tc] of state.toolCalls) {
|
|
297
|
-
if (r >= msgAreaHeight)
|
|
298
|
-
break;
|
|
299
|
-
const spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
300
|
-
const isAgent = tc.isAgent || tc.toolName === 'Agent' || tc.toolName === 'ParallelAgents';
|
|
301
|
-
// Agent-specific icons and colors
|
|
302
|
-
const icon = isAgent
|
|
303
|
-
? (tc.status === 'running' ? '⊕' : tc.status === 'done' ? '◈' : '◇')
|
|
304
|
-
: (tc.status === 'running' ? spinnerChars[state.spinnerFrame % spinnerChars.length] : tc.status === 'done' ? '✓' : '✗');
|
|
305
|
-
const S_AGENT = { fg: 'cyan', bg: null, bold: true, dim: false, underline: false };
|
|
306
|
-
const statusStyle = tc.status === 'error' ? S_ERROR : tc.status === 'done' ? S_GREEN : isAgent ? S_AGENT : S_YELLOW;
|
|
307
|
-
const nameStyle = isAgent ? S_AGENT : { ...S_YELLOW, bold: true };
|
|
308
|
-
const isExpanded = state.expandedToolCalls.has(callId);
|
|
309
|
-
const canExpand = tc.status !== 'running' && tc.output;
|
|
310
|
-
// Collapse/expand indicator
|
|
311
|
-
if (canExpand) {
|
|
312
|
-
grid.writeText(r, 0, isExpanded ? '▼' : '▶', S_DIM);
|
|
313
|
-
}
|
|
314
|
-
grid.writeText(r, 2, `${icon} `, statusStyle);
|
|
315
|
-
grid.writeText(r, 4, tc.toolName, nameStyle);
|
|
316
|
-
// Show args + elapsed time on the same line
|
|
317
|
-
let afterName = 4 + tc.toolName.length + 1;
|
|
318
|
-
if (tc.args) {
|
|
319
|
-
const maxArgs = w - afterName - 15; // leave room for elapsed
|
|
320
|
-
if (maxArgs > 5) {
|
|
321
|
-
const argsText = tc.args.slice(0, maxArgs) + (tc.args.length > maxArgs ? '…' : '');
|
|
322
|
-
grid.writeText(r, afterName, argsText, S_DIM);
|
|
323
|
-
afterName += argsText.length + 1;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
// Elapsed time for running tools
|
|
327
|
-
if (tc.status === 'running' && tc.startedAt) {
|
|
328
|
-
const elapsed = Math.floor((Date.now() - tc.startedAt) / 1000);
|
|
329
|
-
if (elapsed > 0) {
|
|
330
|
-
const lineCount = tc.liveOutput?.length ?? 0;
|
|
331
|
-
const elapsedStr = lineCount > 0 ? `${elapsed}s · ${lineCount} lines` : `${elapsed}s`;
|
|
332
|
-
grid.writeText(r, Math.min(afterName, w - elapsedStr.length - 2), elapsedStr, S_DIM);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Result summary for completed tools (e.g., "42 lines", "exit 0")
|
|
336
|
-
if (tc.status !== 'running' && tc.resultSummary) {
|
|
337
|
-
const elapsed = tc.startedAt ? Math.floor((Date.now() - tc.startedAt) / 1000) : 0;
|
|
338
|
-
const suffix = elapsed > 0 ? `${tc.resultSummary} · ${elapsed}s` : tc.resultSummary;
|
|
339
|
-
grid.writeText(r, Math.min(afterName, w - suffix.length - 2), suffix, S_DIM);
|
|
340
|
-
}
|
|
341
|
-
r++;
|
|
342
|
-
// Agent description line
|
|
343
|
-
if (isAgent && tc.agentDescription && r < msgAreaHeight) {
|
|
344
|
-
grid.writeText(r, 6, tc.agentDescription.slice(0, w - 8), S_DIM);
|
|
345
|
-
r++;
|
|
346
|
-
}
|
|
347
|
-
// Live streaming output while running
|
|
348
|
-
if (tc.status === 'running' && tc.liveOutput && tc.liveOutput.length > 0) {
|
|
349
|
-
const maxLines = 5;
|
|
350
|
-
const overflow = tc.liveOutput.length > maxLines ? tc.liveOutput.length - maxLines : 0;
|
|
351
|
-
if (overflow > 0 && r < msgAreaHeight) {
|
|
352
|
-
grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
|
|
353
|
-
r++;
|
|
354
|
-
}
|
|
355
|
-
const visible = overflow > 0 ? tc.liveOutput.slice(-maxLines) : tc.liveOutput;
|
|
356
|
-
for (const line of visible) {
|
|
357
|
-
if (r >= msgAreaHeight)
|
|
358
|
-
break;
|
|
359
|
-
grid.writeText(r, 6, line.slice(0, w - 8), S_DIM);
|
|
360
|
-
r++;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Final output — collapsed by default (only show when expanded via Tab)
|
|
364
|
-
if (tc.output && tc.status !== 'running' && isExpanded && r < msgAreaHeight) {
|
|
365
|
-
// Image results: show inline placeholder
|
|
366
|
-
if (isImageOutput(tc.output)) {
|
|
367
|
-
const label = renderImageInline(tc.output);
|
|
368
|
-
grid.writeText(r, 6, label.slice(0, w - 8), S_DIM);
|
|
369
|
-
r++;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const outLines = tc.output.split('\n');
|
|
373
|
-
const maxOut = 20;
|
|
374
|
-
const showLines = outLines.slice(0, maxOut);
|
|
375
|
-
for (const line of showLines) {
|
|
376
|
-
if (r >= msgAreaHeight)
|
|
377
|
-
break;
|
|
378
|
-
const lineStyle = tc.status === 'error' ? S_ERROR : S_DIM;
|
|
379
|
-
grid.writeText(r, 6, line.slice(0, w - 8), lineStyle);
|
|
380
|
-
r++;
|
|
381
|
-
}
|
|
382
|
-
if (outLines.length > maxOut && r < msgAreaHeight) {
|
|
383
|
-
grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
|
|
384
|
-
r++;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// ── Context warning (above footer) ──
|
|
389
|
-
if (state.contextWarning) {
|
|
390
|
-
if (r < msgAreaHeight) {
|
|
391
|
-
const warnStyle = { fg: 'yellow', bg: null, bold: state.contextWarning.critical, dim: false, underline: false };
|
|
392
|
-
grid.writeText(r, 0, state.contextWarning.text, warnStyle);
|
|
393
|
-
r++;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// ── Scrollbar (right edge of message area) ──
|
|
642
|
+
// ── Thinking, spinner, tool calls, context warning (shared helpers) ──
|
|
643
|
+
r = renderThinkingSection(state, grid, r, msgAreaHeight);
|
|
644
|
+
r = renderThinkingSummarySection(state, grid, r, msgAreaHeight);
|
|
645
|
+
r = renderSpinnerSection(state, grid, r, msgAreaHeight);
|
|
646
|
+
r = renderToolCallsSection(state, grid, r, msgAreaHeight, { maxLiveLines: 5, showOverflow: true });
|
|
647
|
+
r = renderContextWarningSection(state, grid, r, msgAreaHeight);
|
|
648
|
+
// ── Scrollbar ──
|
|
397
649
|
if (hasScrollbar) {
|
|
398
650
|
const S_TRACK = { fg: null, bg: null, bold: false, dim: true, underline: false };
|
|
399
651
|
const S_THUMB = { fg: null, bg: null, bold: false, dim: false, underline: false };
|
|
@@ -402,204 +654,46 @@ export function rasterize(state, grid) {
|
|
|
402
654
|
grid.setCell(sr, w - 1, isThumb ? '█' : '░', isThumb ? S_THUMB : S_TRACK);
|
|
403
655
|
}
|
|
404
656
|
}
|
|
405
|
-
// ── Footer
|
|
657
|
+
// ── Footer ──
|
|
406
658
|
const footerStart = Math.min(r, msgAreaHeight);
|
|
407
|
-
// Border line with scroll indicator
|
|
408
659
|
for (let c = 0; c < w; c++) {
|
|
409
660
|
grid.setCell(footerStart, c, '─', S_BORDER);
|
|
410
661
|
}
|
|
411
|
-
// Connect scrollbar to footer border
|
|
412
662
|
if (hasScrollbar) {
|
|
413
663
|
grid.setCell(footerStart, w - 1, '┤', S_BORDER);
|
|
414
664
|
}
|
|
415
665
|
if (state.manualScroll > 0 && totalRows > msgAreaHeight) {
|
|
416
|
-
// User scrolled up — show how many lines are hidden below
|
|
417
666
|
const hiddenBelow = state.manualScroll;
|
|
418
667
|
const indicator = ` ↓ ${hiddenBelow} more below `;
|
|
419
668
|
const startCol = Math.max(0, Math.floor((w - indicator.length) / 2));
|
|
420
669
|
grid.writeText(footerStart, startCol, indicator, S_DIM);
|
|
421
670
|
}
|
|
422
671
|
else if (totalRows > msgAreaHeight && scrollOffset > 0) {
|
|
423
|
-
// Content overflows but auto-scrolled to bottom — show lines hidden above
|
|
424
672
|
const indicator = ` ↑ ${scrollOffset} more above `;
|
|
425
673
|
const startCol = Math.max(0, Math.floor((w - indicator.length) / 2));
|
|
426
674
|
grid.writeText(footerStart, startCol, indicator, S_DIM);
|
|
427
675
|
}
|
|
428
676
|
let nextRow = footerStart + 1;
|
|
429
|
-
// Permission
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
grid.writeText(nextRow, 1, '╭' + '─'.repeat(boxWidth - 2) + '╮', riskDim);
|
|
439
|
-
nextRow++;
|
|
440
|
-
// Tool name + risk
|
|
441
|
-
grid.writeText(nextRow, 1, '│ ', riskDim);
|
|
442
|
-
grid.writeText(nextRow, 3, '⚠ ', riskStyle);
|
|
443
|
-
grid.writeText(nextRow, 5, toolName, { ...riskStyle });
|
|
444
|
-
grid.writeText(nextRow, 5 + toolName.length, ` ${riskLevel} risk`, S_DIM);
|
|
445
|
-
grid.writeText(nextRow, boxWidth, '│', riskDim);
|
|
446
|
-
nextRow++;
|
|
447
|
-
// Description (truncated)
|
|
448
|
-
const rawDesc = state.permissionBox.suggestion || description.slice(0, boxWidth - 6);
|
|
449
|
-
const descText = rawDesc.replace(/\|/g, ' ').replace(/\\/g, '/'); // sanitize pipe/backslash for display
|
|
450
|
-
grid.writeText(nextRow, 1, '│ ', riskDim);
|
|
451
|
-
grid.writeText(nextRow, 3, descText.slice(0, boxWidth - 4), S_DIM);
|
|
452
|
-
grid.writeText(nextRow, boxWidth, '│', riskDim);
|
|
453
|
-
nextRow++;
|
|
454
|
-
// Inline diff (when toggled, capped to available space)
|
|
455
|
-
if (state.permissionDiffVisible && state.permissionDiffInfo && nextRow + 3 < h) {
|
|
456
|
-
grid.writeText(nextRow, 1, '│', riskDim);
|
|
457
|
-
nextRow++;
|
|
458
|
-
const availDiffRows = Math.min(maxDiffHeight, h - nextRow - 3); // reserve 3 for keys + border + input
|
|
459
|
-
const diffRows = renderDiff(grid, nextRow, 3, state.permissionDiffInfo, boxWidth - 2, availDiffRows);
|
|
460
|
-
// Draw left border for diff rows
|
|
461
|
-
for (let dr = 0; dr < diffRows; dr++) {
|
|
462
|
-
if (nextRow + dr < grid.height) {
|
|
463
|
-
grid.setCell(nextRow + dr, 1, '│', riskDim);
|
|
464
|
-
grid.setCell(nextRow + dr, boxWidth, '│', riskDim);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
nextRow += diffRows;
|
|
468
|
-
}
|
|
469
|
-
// Action keys — prominent colored letters
|
|
470
|
-
const hasDiff = state.permissionDiffInfo !== null;
|
|
471
|
-
const S_KEY_GREEN = { fg: 'green', bg: null, bold: true, dim: false, underline: false };
|
|
472
|
-
const S_KEY_RED = { fg: 'red', bg: null, bold: true, dim: false, underline: false };
|
|
473
|
-
const S_KEY_CYAN = { fg: 'cyan', bg: null, bold: true, dim: false, underline: false };
|
|
474
|
-
grid.writeText(nextRow, 1, '│ ', riskDim);
|
|
475
|
-
let kc = 3;
|
|
476
|
-
grid.writeText(nextRow, kc, 'Y', S_KEY_GREEN);
|
|
477
|
-
kc += 1;
|
|
478
|
-
grid.writeText(nextRow, kc, 'es', S_DIM);
|
|
479
|
-
kc += 2;
|
|
480
|
-
grid.writeText(nextRow, kc, ' ', S_DIM);
|
|
481
|
-
kc += 2;
|
|
482
|
-
grid.writeText(nextRow, kc, 'N', S_KEY_RED);
|
|
483
|
-
kc += 1;
|
|
484
|
-
grid.writeText(nextRow, kc, 'o', S_DIM);
|
|
485
|
-
kc += 1;
|
|
486
|
-
if (hasDiff) {
|
|
487
|
-
grid.writeText(nextRow, kc, ' ', S_DIM);
|
|
488
|
-
kc += 2;
|
|
489
|
-
grid.writeText(nextRow, kc, 'D', S_KEY_CYAN);
|
|
490
|
-
kc += 1;
|
|
491
|
-
grid.writeText(nextRow, kc, 'iff', S_DIM);
|
|
492
|
-
kc += 3;
|
|
493
|
-
}
|
|
494
|
-
grid.writeText(nextRow, boxWidth, '│', riskDim);
|
|
495
|
-
nextRow++;
|
|
496
|
-
// Bottom border
|
|
497
|
-
grid.writeText(nextRow, 1, '╰' + '─'.repeat(boxWidth - 2) + '╯', riskDim);
|
|
498
|
-
nextRow++;
|
|
499
|
-
}
|
|
500
|
-
// Question prompt (if active)
|
|
501
|
-
let questionInputRow = -1;
|
|
502
|
-
if (state.questionPrompt && w >= 20) {
|
|
503
|
-
const { question, options, input, cursor } = state.questionPrompt;
|
|
504
|
-
const qStyle = { fg: 'yellow', bg: null, bold: false, dim: false, underline: false };
|
|
505
|
-
const qBorder = { fg: 'yellow', bg: null, bold: false, dim: true, underline: false };
|
|
506
|
-
const qBoxWidth = Math.max(15, Math.min(w - 2, 70));
|
|
507
|
-
grid.writeText(nextRow, 1, '╭' + '─'.repeat(qBoxWidth - 2) + '╮', qBorder);
|
|
508
|
-
nextRow++;
|
|
509
|
-
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
510
|
-
grid.writeText(nextRow, 3, `❓ ${question}`, qStyle);
|
|
511
|
-
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
512
|
-
nextRow++;
|
|
513
|
-
if (options && options.length > 0) {
|
|
514
|
-
for (let oi = 0; oi < options.length; oi++) {
|
|
515
|
-
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
516
|
-
grid.writeText(nextRow, 5, `${oi + 1}. ${options[oi]}`, S_DIM);
|
|
517
|
-
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
518
|
-
nextRow++;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
questionInputRow = nextRow;
|
|
522
|
-
grid.writeText(nextRow, 1, '│ ', qBorder);
|
|
523
|
-
grid.writeText(nextRow, 3, '❯ ', qStyle);
|
|
524
|
-
grid.writeText(nextRow, 5, input, S_TEXT);
|
|
525
|
-
grid.writeText(nextRow, qBoxWidth, '│', qBorder);
|
|
526
|
-
nextRow++;
|
|
527
|
-
grid.writeText(nextRow, 1, '╰' + '─'.repeat(qBoxWidth - 2) + '╯', qBorder);
|
|
528
|
-
nextRow++;
|
|
529
|
-
}
|
|
530
|
-
// Status line (model | tokens | cost)
|
|
531
|
-
if (state.statusLine) {
|
|
532
|
-
grid.writeText(nextRow, 0, state.statusLine, S_DIM);
|
|
533
|
-
nextRow++;
|
|
534
|
-
}
|
|
535
|
-
// Pre-compute prompt width for alignment
|
|
536
|
-
const vimIndicator = state.vimMode ? (state.vimMode === 'normal' ? '[N] ' : '[I] ') : '';
|
|
537
|
-
const promptText = vimIndicator + '❯ ';
|
|
538
|
-
const promptWidth = promptText.length;
|
|
539
|
-
// Autocomplete suggestions (above input, aligned to prompt)
|
|
540
|
-
if (state.autocomplete.length > 0) {
|
|
541
|
-
for (let ai = 0; ai < state.autocomplete.length; ai++) {
|
|
542
|
-
const cmd = state.autocomplete[ai];
|
|
543
|
-
const desc = state.autocompleteDescriptions[ai] ?? '';
|
|
544
|
-
const selected = ai === state.autocompleteIndex;
|
|
545
|
-
const acStyle = selected
|
|
546
|
-
? s(getTheme().user, true)
|
|
547
|
-
: s(null, false, true);
|
|
548
|
-
grid.writeText(nextRow, promptWidth, `/${cmd.padEnd(12)}`, acStyle);
|
|
549
|
-
if (desc && w > promptWidth + 15)
|
|
550
|
-
grid.writeText(nextRow, promptWidth + 13, desc.slice(0, w - promptWidth - 15), S_DIM);
|
|
551
|
-
nextRow++;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
// Input line (or search bar in search mode)
|
|
677
|
+
// ── Permission, question, status, autocomplete, notifications, input, companion (shared helpers) ──
|
|
678
|
+
nextRow = renderPermissionBoxSection(state, grid, nextRow, h, { boxed: true, maxDiffHeight });
|
|
679
|
+
const questionResult = renderQuestionPromptSection(state, grid, nextRow, h, { boxed: true });
|
|
680
|
+
nextRow = questionResult.nextRow;
|
|
681
|
+
const questionInputRow = questionResult.questionInputRow;
|
|
682
|
+
nextRow = renderStatusLineSection(state, grid, nextRow, h);
|
|
683
|
+
const { promptText, promptWidth } = getPromptText(state);
|
|
684
|
+
nextRow = renderAutocompleteSection(state, grid, nextRow, h, promptWidth);
|
|
685
|
+
nextRow = renderNotificationsSection(state, grid, nextRow, h);
|
|
555
686
|
const inputRow = nextRow;
|
|
556
|
-
|
|
557
|
-
if (state.searchMode) {
|
|
558
|
-
const searchPrompt = '🔍 ';
|
|
559
|
-
grid.writeText(inputRow, 0, searchPrompt, S_USER);
|
|
560
|
-
inputStart = 3; // emoji + space
|
|
561
|
-
grid.writeText(inputRow, inputStart, state.searchQuery, S_TEXT);
|
|
562
|
-
// Match count
|
|
563
|
-
const matchInfo = state.searchMatchCount > 0
|
|
564
|
-
? ` ${state.searchCurrentMatch + 1}/${state.searchMatchCount}`
|
|
565
|
-
: state.searchQuery ? ' No matches' : '';
|
|
566
|
-
grid.writeText(inputRow, inputStart + state.searchQuery.length, matchInfo, S_DIM);
|
|
567
|
-
// Hints
|
|
568
|
-
grid.writeText(inputRow + 1, 0, 'Enter/↓ next | ↑ prev | Esc close', S_DIM);
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
grid.writeText(inputRow, 0, promptText, S_USER);
|
|
572
|
-
inputStart = promptWidth;
|
|
573
|
-
// Multi-line input rendering
|
|
574
|
-
const inputLines = state.inputText.split('\n');
|
|
575
|
-
const maxInputLines = Math.min(inputLines.length, 5);
|
|
576
|
-
for (let li = 0; li < maxInputLines; li++) {
|
|
577
|
-
if (li === 0) {
|
|
578
|
-
grid.writeText(inputRow, inputStart, inputLines[0], S_TEXT);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
// Align continuation to prompt position
|
|
582
|
-
grid.writeText(inputRow + li, inputStart, inputLines[li], S_TEXT);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
// Hints
|
|
586
|
-
const hintsRow = inputRow + maxInputLines;
|
|
587
|
-
const hintsText = inputLines.length > 1
|
|
588
|
-
? `${state.statusHints} | Alt+Enter newline`
|
|
589
|
-
: state.statusHints;
|
|
590
|
-
grid.writeText(hintsRow, 0, hintsText, S_DIM);
|
|
591
|
-
}
|
|
687
|
+
renderInputSection(state, grid, inputRow, h, promptText, promptWidth);
|
|
592
688
|
// Companion (right-aligned in footer, skipped if it would overlap input)
|
|
593
689
|
if (state.companionLines && w >= 50) {
|
|
594
690
|
const compWidth = Math.max(...state.companionLines.map(l => l.length), 0);
|
|
595
691
|
const compStartCol = Math.max(0, w - compWidth - 1);
|
|
596
692
|
const inputEndCol = promptWidth + (state.inputText.split('\n')[0]?.length ?? 0);
|
|
597
|
-
// Only render if companion has horizontal clearance from input
|
|
598
693
|
if (compStartCol > inputEndCol + 3) {
|
|
599
694
|
const compStyle = { fg: state.companionColor || 'cyan', bg: null, bold: false, dim: false, underline: false };
|
|
600
695
|
for (let i = 0; i < state.companionLines.length; i++) {
|
|
601
696
|
const compRow = footerStart + i;
|
|
602
|
-
// Skip rows that would overlap with input/status area
|
|
603
697
|
if (compRow >= inputRow)
|
|
604
698
|
break;
|
|
605
699
|
if (compRow >= h)
|
|
@@ -608,25 +702,65 @@ export function rasterize(state, grid) {
|
|
|
608
702
|
}
|
|
609
703
|
}
|
|
610
704
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
705
|
+
return computeCursorPosition(state, inputRow, promptWidth, questionInputRow);
|
|
706
|
+
}
|
|
707
|
+
// extractSuggestion moved to shared utils/tool-summary.ts as summarizeToolArgs
|
|
708
|
+
/**
|
|
709
|
+
* Rasterize only the "live area" — streaming text, thinking, tool calls, and footer.
|
|
710
|
+
* Used in hybrid mode where completed messages are flushed to terminal scrollback.
|
|
711
|
+
* The grid should be sized to fit just the live content.
|
|
712
|
+
*/
|
|
713
|
+
export function rasterizeLive(state, grid) {
|
|
714
|
+
ensureStyles();
|
|
715
|
+
const w = grid.width;
|
|
716
|
+
const h = grid.height;
|
|
717
|
+
let r = 0;
|
|
718
|
+
// ── Banner (shown when no messages have been flushed yet) ──
|
|
719
|
+
if (state.bannerLines && state.messages.length === 0 && !state.loading) {
|
|
720
|
+
r = renderBannerSection(state, grid, r, h - 4, { compact: h < 15 });
|
|
721
|
+
}
|
|
722
|
+
// ── Streaming text ──
|
|
723
|
+
if (state.loading && state.streamingText) {
|
|
724
|
+
grid.writeText(r, 0, '◆ ', S_ASSISTANT);
|
|
725
|
+
const rows = renderMarkdown(grid, r, 2, state.streamingText, w, state.codeBlocksExpanded, h);
|
|
726
|
+
r += rows;
|
|
617
727
|
}
|
|
618
|
-
|
|
619
|
-
|
|
728
|
+
// ── Thinking, spinner, error, tool calls, context warning (shared helpers) ──
|
|
729
|
+
r = renderThinkingSection(state, grid, r, h);
|
|
730
|
+
r = renderThinkingSummarySection(state, grid, r, h);
|
|
731
|
+
r = renderSpinnerSection(state, grid, r, h);
|
|
732
|
+
r = renderErrorSection(state, grid, r, h);
|
|
733
|
+
r = renderToolCallsSection(state, grid, r, h, { maxLiveLines: 3, showOverflow: false });
|
|
734
|
+
r = renderContextWarningSection(state, grid, r, h);
|
|
735
|
+
// ── Footer border ──
|
|
736
|
+
if (r < h) {
|
|
737
|
+
for (let c = 0; c < w; c++)
|
|
738
|
+
grid.setCell(r, c, '─', S_BORDER);
|
|
739
|
+
r++;
|
|
740
|
+
}
|
|
741
|
+
let nextRow = r;
|
|
742
|
+
const borderRow = r - 1; // for companion anchoring
|
|
743
|
+
// ── Permission, question, status, autocomplete, notifications, input (shared helpers) ──
|
|
744
|
+
nextRow = renderPermissionBoxSection(state, grid, nextRow, h, { boxed: false, maxDiffHeight: 15 });
|
|
745
|
+
const questionResult = renderQuestionPromptSection(state, grid, nextRow, h, { boxed: false });
|
|
746
|
+
nextRow = questionResult.nextRow;
|
|
747
|
+
const questionInputRow = questionResult.questionInputRow;
|
|
748
|
+
nextRow = renderStatusLineSection(state, grid, nextRow, h);
|
|
749
|
+
const { promptText, promptWidth } = getPromptText(state);
|
|
750
|
+
nextRow = renderAutocompleteSection(state, grid, nextRow, h, promptWidth);
|
|
751
|
+
nextRow = renderNotificationsSection(state, grid, nextRow, h);
|
|
752
|
+
const inputRow = nextRow;
|
|
753
|
+
renderInputSection(state, grid, inputRow, h, promptText, promptWidth);
|
|
754
|
+
// ── Companion (right-aligned, anchored at footer border) ──
|
|
755
|
+
renderCompanionSection(state, grid, borderRow, h, promptWidth);
|
|
756
|
+
// ── Cursor position ──
|
|
757
|
+
if (state.questionPrompt && questionInputRow >= 0) {
|
|
758
|
+
return { cursorRow: questionInputRow, cursorCol: 3 + state.questionPrompt.cursor };
|
|
620
759
|
}
|
|
621
|
-
// 2D cursor positioning for multi-line input (all lines aligned to inputStart)
|
|
622
760
|
const textBeforeCursor = state.inputText.slice(0, state.inputCursor);
|
|
623
761
|
const cursorLines = textBeforeCursor.split('\n');
|
|
624
|
-
const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
|
|
762
|
+
const cursorLineIdx = Math.min(cursorLines.length - 1, 4);
|
|
625
763
|
const cursorColInLine = cursorLines[cursorLines.length - 1].length;
|
|
626
|
-
return {
|
|
627
|
-
cursorRow: inputRow + cursorLineIdx,
|
|
628
|
-
cursorCol: inputStart + cursorColInLine,
|
|
629
|
-
};
|
|
764
|
+
return { cursorRow: inputRow + cursorLineIdx, cursorCol: promptWidth + cursorColInLine };
|
|
630
765
|
}
|
|
631
|
-
// extractSuggestion moved to shared utils/tool-summary.ts as summarizeToolArgs
|
|
632
766
|
//# sourceMappingURL=layout.js.map
|