@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/index.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TerminalRenderer —
|
|
3
|
-
*
|
|
2
|
+
* TerminalRenderer — sequential output terminal renderer.
|
|
3
|
+
* Flushed messages flow to scrollback; live area is rewritten in-place
|
|
4
|
+
* right after the scrollback content each frame (no absolute positioning gap).
|
|
4
5
|
*/
|
|
5
6
|
import { CellGrid } from './cells.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { styleToSGR, syncWrite, hideCursor, showCursor } from './differ.js';
|
|
8
|
+
import { rasterizeLive } from './layout.js';
|
|
9
|
+
import { getTheme } from '../utils/theme-data.js';
|
|
10
|
+
import { FG } from './colors.js';
|
|
8
11
|
import { createSessionBrowser, browserUp, browserDown, browserSelectedId, browserLoadPreview, browserSearch } from './session-browser.js';
|
|
9
12
|
import { summarizeToolArgs } from '../utils/tool-summary.js';
|
|
10
13
|
import { extractDiffInfo } from './diff.js';
|
|
11
14
|
import { startRawInput } from './input.js';
|
|
12
15
|
export class TerminalRenderer {
|
|
13
16
|
current;
|
|
14
|
-
previous;
|
|
15
17
|
state;
|
|
16
18
|
stopInput = null;
|
|
19
|
+
notifyTimers = [];
|
|
17
20
|
animationTimer = null;
|
|
18
21
|
renderPending = false;
|
|
19
22
|
started = false;
|
|
23
|
+
flushedMessageCount = 0;
|
|
24
|
+
flushedToolCallIds = new Set();
|
|
25
|
+
lastLiveLines = 0; // lines the live area occupied last frame (for relative cursor movement)
|
|
20
26
|
// Callbacks
|
|
21
27
|
keypressHandler = null;
|
|
22
28
|
resizeHandler = null;
|
|
@@ -31,7 +37,6 @@ export class TerminalRenderer {
|
|
|
31
37
|
const w = process.stdout.columns ?? 80;
|
|
32
38
|
const h = process.stdout.rows ?? 24;
|
|
33
39
|
this.current = new CellGrid(w, h);
|
|
34
|
-
this.previous = new CellGrid(w, h);
|
|
35
40
|
this.state = {
|
|
36
41
|
messages: [],
|
|
37
42
|
streamingText: '',
|
|
@@ -64,85 +69,21 @@ export class TerminalRenderer {
|
|
|
64
69
|
bannerLines: null,
|
|
65
70
|
thinkingExpanded: false,
|
|
66
71
|
lastThinkingSummary: null,
|
|
67
|
-
|
|
68
|
-
searchQuery: '',
|
|
69
|
-
searchMatchCount: 0,
|
|
70
|
-
searchCurrentMatch: -1,
|
|
72
|
+
notifications: [],
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
// ── Lifecycle ──
|
|
74
76
|
start() {
|
|
75
77
|
this.started = true;
|
|
76
|
-
enterAltScreen();
|
|
77
78
|
// Enable SGR mouse tracking (scroll wheel support)
|
|
78
79
|
process.stdout.write('\x1b[?1000h\x1b[?1006h');
|
|
79
80
|
hideCursor();
|
|
80
81
|
// Raw input
|
|
81
82
|
this.stopInput = startRawInput((key) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (k === 'y') {
|
|
86
|
-
const resolve = this.permissionResolve;
|
|
87
|
-
this.permissionResolve = null;
|
|
88
|
-
this.permissionPrompt = null;
|
|
89
|
-
this.state.permissionBox = null;
|
|
90
|
-
this.state.permissionDiffVisible = false;
|
|
91
|
-
this.state.permissionDiffInfo = null;
|
|
92
|
-
this.scheduleRender();
|
|
93
|
-
resolve(true);
|
|
94
|
-
}
|
|
95
|
-
else if (k === 'n') {
|
|
96
|
-
const resolve = this.permissionResolve;
|
|
97
|
-
this.permissionResolve = null;
|
|
98
|
-
this.permissionPrompt = null;
|
|
99
|
-
this.state.permissionBox = null;
|
|
100
|
-
this.state.permissionDiffVisible = false;
|
|
101
|
-
this.state.permissionDiffInfo = null;
|
|
102
|
-
this.scheduleRender();
|
|
103
|
-
resolve(false);
|
|
104
|
-
}
|
|
105
|
-
else if (k === 'd' && this.state.permissionDiffInfo) {
|
|
106
|
-
this.state.permissionDiffVisible = !this.state.permissionDiffVisible;
|
|
107
|
-
this.scheduleRender();
|
|
108
|
-
}
|
|
109
|
-
return; // Swallow all other keys during permission prompt
|
|
110
|
-
}
|
|
111
|
-
// Question prompt intercepts text input
|
|
112
|
-
if (this.questionResolve && this.state.questionPrompt) {
|
|
113
|
-
const qp = this.state.questionPrompt;
|
|
114
|
-
if (key.name === 'return' && qp.input.trim()) {
|
|
115
|
-
const resolve = this.questionResolve;
|
|
116
|
-
const answer = qp.input.trim();
|
|
117
|
-
this.questionResolve = null;
|
|
118
|
-
this.state.questionPrompt = null;
|
|
119
|
-
this.scheduleRender();
|
|
120
|
-
resolve(answer);
|
|
121
|
-
}
|
|
122
|
-
else if (key.name === 'backspace') {
|
|
123
|
-
if (qp.cursor > 0) {
|
|
124
|
-
this.state.questionPrompt = { ...qp, input: qp.input.slice(0, qp.cursor - 1) + qp.input.slice(qp.cursor), cursor: qp.cursor - 1 };
|
|
125
|
-
this.scheduleRender();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else if (key.name === 'left') {
|
|
129
|
-
if (qp.cursor > 0) {
|
|
130
|
-
this.state.questionPrompt = { ...qp, cursor: qp.cursor - 1 };
|
|
131
|
-
this.scheduleRender();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else if (key.name === 'right') {
|
|
135
|
-
if (qp.cursor < qp.input.length) {
|
|
136
|
-
this.state.questionPrompt = { ...qp, cursor: qp.cursor + 1 };
|
|
137
|
-
this.scheduleRender();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
else if (key.char && key.char.length === 1 && !key.ctrl && !key.meta) {
|
|
141
|
-
this.state.questionPrompt = { ...qp, input: qp.input.slice(0, qp.cursor) + key.char + qp.input.slice(qp.cursor), cursor: qp.cursor + 1 };
|
|
142
|
-
this.scheduleRender();
|
|
143
|
-
}
|
|
83
|
+
if (this.handlePermissionKey(key))
|
|
84
|
+
return;
|
|
85
|
+
if (this.handleQuestionKey(key))
|
|
144
86
|
return;
|
|
145
|
-
}
|
|
146
87
|
if (this.keypressHandler)
|
|
147
88
|
this.keypressHandler(key);
|
|
148
89
|
});
|
|
@@ -175,16 +116,47 @@ export class TerminalRenderer {
|
|
|
175
116
|
this.stopInput();
|
|
176
117
|
this.stopInput = null;
|
|
177
118
|
}
|
|
178
|
-
|
|
119
|
+
for (const t of this.notifyTimers)
|
|
120
|
+
clearTimeout(t);
|
|
121
|
+
this.notifyTimers.length = 0;
|
|
122
|
+
// Restore terminal: disable mouse, show cursor, reset attributes
|
|
179
123
|
process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[0m');
|
|
180
124
|
showCursor();
|
|
181
|
-
|
|
125
|
+
// Move past live area for clean shell prompt
|
|
126
|
+
process.stdout.write('\n');
|
|
182
127
|
}
|
|
183
128
|
// ── State updates ──
|
|
184
|
-
setMessages(msgs) {
|
|
129
|
+
setMessages(msgs) {
|
|
130
|
+
// Reset flush counter if messages array was replaced (e.g., session resume)
|
|
131
|
+
if (msgs.length < this.flushedMessageCount)
|
|
132
|
+
this.flushedMessageCount = 0;
|
|
133
|
+
this.state.messages = msgs;
|
|
134
|
+
this.scheduleRender();
|
|
135
|
+
}
|
|
185
136
|
setStreamingText(text) { this.state.streamingText = text; this.scheduleRender(); }
|
|
186
137
|
setThinkingText(text) { this.state.thinkingText = text; this.scheduleRender(); }
|
|
187
138
|
setError(text) { this.state.errorText = text; this.scheduleRender(); }
|
|
139
|
+
/** Show a toast notification above the input for 5 seconds */
|
|
140
|
+
notify(text) {
|
|
141
|
+
const entry = { text };
|
|
142
|
+
this.state.notifications.push(entry);
|
|
143
|
+
this.scheduleRender();
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
const idx = this.state.notifications.indexOf(entry);
|
|
146
|
+
if (idx >= 0) {
|
|
147
|
+
this.state.notifications.splice(idx, 1);
|
|
148
|
+
this.scheduleRender();
|
|
149
|
+
}
|
|
150
|
+
const ti = this.notifyTimers.indexOf(timer);
|
|
151
|
+
if (ti >= 0)
|
|
152
|
+
this.notifyTimers.splice(ti, 1);
|
|
153
|
+
}, 5000);
|
|
154
|
+
this.notifyTimers.push(timer);
|
|
155
|
+
}
|
|
156
|
+
scrollBy(delta) {
|
|
157
|
+
this.state.manualScroll = Math.max(0, Math.min(this.state.manualScroll + delta, 500));
|
|
158
|
+
this.scheduleRender();
|
|
159
|
+
}
|
|
188
160
|
setLoading(loading) { this.state.loading = loading; this.scheduleRender(); }
|
|
189
161
|
setInputText(text) { this.state.inputText = text; this.scheduleRender(); }
|
|
190
162
|
setInputCursor(pos) { this.state.inputCursor = pos; this.scheduleRender(); }
|
|
@@ -193,8 +165,8 @@ export class TerminalRenderer {
|
|
|
193
165
|
this.state.companionColor = color;
|
|
194
166
|
this.scheduleRender();
|
|
195
167
|
}
|
|
196
|
-
setBanner(lines) { this.state.bannerLines = lines; this.scheduleRender(); }
|
|
197
168
|
setStatusHints(text) { this.state.statusHints = text; this.scheduleRender(); }
|
|
169
|
+
setBannerLines(lines) { this.state.bannerLines = lines; this.scheduleRender(); }
|
|
198
170
|
setAutocomplete(suggestions, index, descriptions) {
|
|
199
171
|
this.state.autocomplete = suggestions;
|
|
200
172
|
this.state.autocompleteDescriptions = descriptions ?? [];
|
|
@@ -208,125 +180,18 @@ export class TerminalRenderer {
|
|
|
208
180
|
getThinkingStartedAt() { return this.state.thinkingStartedAt; }
|
|
209
181
|
setLastThinkingSummary(summary) { this.state.lastThinkingSummary = summary; this.scheduleRender(); }
|
|
210
182
|
toggleThinkingExpanded() { this.state.thinkingExpanded = !this.state.thinkingExpanded; this.scheduleRender(); }
|
|
211
|
-
// Search mode
|
|
212
|
-
enterSearchMode() {
|
|
213
|
-
this.state.searchMode = true;
|
|
214
|
-
this.state.searchQuery = '';
|
|
215
|
-
this.state.searchMatchCount = 0;
|
|
216
|
-
this.state.searchCurrentMatch = -1;
|
|
217
|
-
this.scheduleRender();
|
|
218
|
-
}
|
|
219
|
-
exitSearchMode() {
|
|
220
|
-
this.state.searchMode = false;
|
|
221
|
-
this.state.searchQuery = '';
|
|
222
|
-
this.state.searchMatchCount = 0;
|
|
223
|
-
this.state.searchCurrentMatch = -1;
|
|
224
|
-
this.scheduleRender();
|
|
225
|
-
}
|
|
226
|
-
setSearchQuery(query) {
|
|
227
|
-
this.state.searchQuery = query;
|
|
228
|
-
// Count matches across all messages
|
|
229
|
-
if (query) {
|
|
230
|
-
const lq = query.toLowerCase();
|
|
231
|
-
let count = 0;
|
|
232
|
-
for (const msg of this.state.messages) {
|
|
233
|
-
const content = msg.content.toLowerCase();
|
|
234
|
-
let idx = 0;
|
|
235
|
-
while ((idx = content.indexOf(lq, idx)) !== -1) {
|
|
236
|
-
count++;
|
|
237
|
-
idx += lq.length;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
this.state.searchMatchCount = count;
|
|
241
|
-
this.state.searchCurrentMatch = count > 0 ? 0 : -1;
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
this.state.searchMatchCount = 0;
|
|
245
|
-
this.state.searchCurrentMatch = -1;
|
|
246
|
-
}
|
|
247
|
-
this.scheduleRender();
|
|
248
|
-
}
|
|
249
|
-
searchNext() {
|
|
250
|
-
if (this.state.searchMatchCount > 0) {
|
|
251
|
-
this.state.searchCurrentMatch = (this.state.searchCurrentMatch + 1) % this.state.searchMatchCount;
|
|
252
|
-
this.scrollToSearchMatch();
|
|
253
|
-
this.scheduleRender();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
searchPrev() {
|
|
257
|
-
if (this.state.searchMatchCount > 0) {
|
|
258
|
-
this.state.searchCurrentMatch = (this.state.searchCurrentMatch - 1 + this.state.searchMatchCount) % this.state.searchMatchCount;
|
|
259
|
-
this.scrollToSearchMatch();
|
|
260
|
-
this.scheduleRender();
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
scrollToSearchMatch() {
|
|
264
|
-
// Estimate which row the current match is on and scroll to it
|
|
265
|
-
const lq = this.state.searchQuery.toLowerCase();
|
|
266
|
-
const w = process.stdout.columns ?? 80;
|
|
267
|
-
let matchIdx = 0;
|
|
268
|
-
let rowEstimate = 0;
|
|
269
|
-
for (const msg of this.state.messages) {
|
|
270
|
-
const content = msg.content;
|
|
271
|
-
const lines = content.split('\n');
|
|
272
|
-
for (const line of lines) {
|
|
273
|
-
const lineRows = Math.max(1, Math.ceil((line.length || 1) / (w - 2)));
|
|
274
|
-
// Count matches in this line
|
|
275
|
-
const ll = line.toLowerCase();
|
|
276
|
-
let idx = 0;
|
|
277
|
-
while ((idx = ll.indexOf(lq, idx)) !== -1) {
|
|
278
|
-
if (matchIdx === this.state.searchCurrentMatch) {
|
|
279
|
-
// Found it — scroll so this row is visible
|
|
280
|
-
const h = process.stdout.rows ?? 24;
|
|
281
|
-
const targetScroll = Math.max(0, rowEstimate - Math.floor(h / 3));
|
|
282
|
-
// manualScroll is offset from bottom: convert
|
|
283
|
-
const totalRows = this.estimateTotalRows();
|
|
284
|
-
this.state.manualScroll = Math.max(0, totalRows - targetScroll - h + 15);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
matchIdx++;
|
|
288
|
-
idx += lq.length;
|
|
289
|
-
}
|
|
290
|
-
rowEstimate += lineRows;
|
|
291
|
-
}
|
|
292
|
-
rowEstimate++; // gap between messages
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
estimateTotalRows() {
|
|
296
|
-
const w = process.stdout.columns ?? 80;
|
|
297
|
-
let total = 0;
|
|
298
|
-
for (const msg of this.state.messages) {
|
|
299
|
-
const lines = msg.content.split('\n');
|
|
300
|
-
for (const line of lines) {
|
|
301
|
-
total += Math.max(1, Math.ceil((line.length || 1) / (w - 2)));
|
|
302
|
-
}
|
|
303
|
-
total++;
|
|
304
|
-
}
|
|
305
|
-
return total;
|
|
306
|
-
}
|
|
307
|
-
isSearchMode() { return this.state.searchMode; }
|
|
308
|
-
getSearchQuery() { return this.state.searchQuery; }
|
|
309
183
|
setTokenCount(count) { this.state.tokenCount = count; this.scheduleRender(); }
|
|
310
|
-
scrollUp(rows) {
|
|
311
|
-
// Cap manualScroll to prevent scrolling past the top
|
|
312
|
-
const h = process.stdout.rows ?? 24;
|
|
313
|
-
const maxScroll = Math.max(0, this.estimateTotalRows() - Math.floor(h / 3));
|
|
314
|
-
this.state.manualScroll = Math.min(this.state.manualScroll + rows, maxScroll);
|
|
315
|
-
this.scheduleRender();
|
|
316
|
-
}
|
|
317
|
-
scrollDown(rows) {
|
|
318
|
-
this.state.manualScroll = Math.max(0, this.state.manualScroll - rows);
|
|
319
|
-
this.scheduleRender();
|
|
320
|
-
}
|
|
321
|
-
scrollToBottom() {
|
|
322
|
-
this.state.manualScroll = 0;
|
|
323
|
-
this.scheduleRender();
|
|
324
|
-
}
|
|
325
184
|
toggleCodeBlockExpansion() {
|
|
326
185
|
this.state.codeBlocksExpanded = !this.state.codeBlocksExpanded;
|
|
327
186
|
this.scheduleRender();
|
|
328
187
|
}
|
|
329
188
|
// Session browser
|
|
189
|
+
withSessionBrowser(fn) {
|
|
190
|
+
if (this.state.sessionBrowser) {
|
|
191
|
+
this.state.sessionBrowser = fn(this.state.sessionBrowser);
|
|
192
|
+
this.scheduleRender();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
330
195
|
openSessionBrowser() {
|
|
331
196
|
this.state.sessionBrowser = createSessionBrowser();
|
|
332
197
|
this.scheduleRender();
|
|
@@ -336,16 +201,10 @@ export class TerminalRenderer {
|
|
|
336
201
|
this.scheduleRender();
|
|
337
202
|
}
|
|
338
203
|
sessionBrowserUp() {
|
|
339
|
-
|
|
340
|
-
this.state.sessionBrowser = browserLoadPreview(browserUp(this.state.sessionBrowser));
|
|
341
|
-
this.scheduleRender();
|
|
342
|
-
}
|
|
204
|
+
this.withSessionBrowser(sb => browserLoadPreview(browserUp(sb)));
|
|
343
205
|
}
|
|
344
206
|
sessionBrowserDown() {
|
|
345
|
-
|
|
346
|
-
this.state.sessionBrowser = browserLoadPreview(browserDown(this.state.sessionBrowser));
|
|
347
|
-
this.scheduleRender();
|
|
348
|
-
}
|
|
207
|
+
this.withSessionBrowser(sb => browserLoadPreview(browserDown(sb)));
|
|
349
208
|
}
|
|
350
209
|
sessionBrowserSelect() {
|
|
351
210
|
if (!this.state.sessionBrowser)
|
|
@@ -356,15 +215,11 @@ export class TerminalRenderer {
|
|
|
356
215
|
return id;
|
|
357
216
|
}
|
|
358
217
|
sessionBrowserType(char) {
|
|
359
|
-
|
|
360
|
-
this.state.sessionBrowser = browserSearch(this.state.sessionBrowser, this.state.sessionBrowser.searchQuery + char);
|
|
361
|
-
this.scheduleRender();
|
|
362
|
-
}
|
|
218
|
+
this.withSessionBrowser(sb => browserSearch(sb, sb.searchQuery + char));
|
|
363
219
|
}
|
|
364
220
|
sessionBrowserBackspace() {
|
|
365
221
|
if (this.state.sessionBrowser && this.state.sessionBrowser.searchQuery.length > 0) {
|
|
366
|
-
this.
|
|
367
|
-
this.scheduleRender();
|
|
222
|
+
this.withSessionBrowser(sb => browserSearch(sb, sb.searchQuery.slice(0, -1)));
|
|
368
223
|
}
|
|
369
224
|
}
|
|
370
225
|
isSessionBrowserOpen() {
|
|
@@ -375,7 +230,7 @@ export class TerminalRenderer {
|
|
|
375
230
|
this.scheduleRender();
|
|
376
231
|
}
|
|
377
232
|
getToolCall(callId) { return this.state.toolCalls.get(callId); }
|
|
378
|
-
clearToolCalls() { this.state.toolCalls.clear(); this.scheduleRender(); }
|
|
233
|
+
clearToolCalls() { this.state.toolCalls.clear(); this.flushedToolCallIds.clear(); this.scheduleRender(); }
|
|
379
234
|
collapseAllToolCalls() { this.state.expandedToolCalls.clear(); this.scheduleRender(); }
|
|
380
235
|
/** Show a question prompt and wait for text answer */
|
|
381
236
|
askQuestion(question, options) {
|
|
@@ -422,20 +277,91 @@ export class TerminalRenderer {
|
|
|
422
277
|
askPermission(toolName, description, riskLevel) {
|
|
423
278
|
this.permissionPrompt = { toolName, description, riskLevel };
|
|
424
279
|
this.state.permissionBox = { toolName, description, riskLevel, suggestion: summarizeToolArgs(toolName, description) };
|
|
425
|
-
this.state.permissionDiffVisible = false;
|
|
426
280
|
this.state.permissionDiffInfo = extractDiffInfo(toolName, description);
|
|
281
|
+
// Auto-show diffs for file-modifying tools
|
|
282
|
+
const isFileTool = /^(Edit|Write|FileEdit|FileWrite)/i.test(toolName);
|
|
283
|
+
this.state.permissionDiffVisible = isFileTool && this.state.permissionDiffInfo !== null;
|
|
427
284
|
this.scheduleRender();
|
|
428
285
|
return new Promise((resolve) => {
|
|
429
286
|
this.permissionResolve = resolve;
|
|
430
287
|
});
|
|
431
288
|
}
|
|
432
289
|
// ── Input ──
|
|
290
|
+
/** Clear the live area from screen using relative cursor movement */
|
|
291
|
+
clearLiveArea() {
|
|
292
|
+
if (!this.started)
|
|
293
|
+
return;
|
|
294
|
+
const cmd = this.lastLiveLines > 0
|
|
295
|
+
? `\x1b[${this.lastLiveLines}A\r\x1b[J`
|
|
296
|
+
: '\r\x1b[J';
|
|
297
|
+
syncWrite(cmd);
|
|
298
|
+
this.lastLiveLines = 0;
|
|
299
|
+
}
|
|
433
300
|
onKeypress(handler) {
|
|
434
301
|
this.keypressHandler = handler;
|
|
435
302
|
}
|
|
436
303
|
onAnimation(handler) {
|
|
437
304
|
this.animationCallback = handler;
|
|
438
305
|
}
|
|
306
|
+
// ── Input routing ──
|
|
307
|
+
/** Handle permission prompt keys (Y/N/D). Returns true if key was consumed. */
|
|
308
|
+
handlePermissionKey(key) {
|
|
309
|
+
if (!this.permissionResolve)
|
|
310
|
+
return false;
|
|
311
|
+
const k = key.char.toLowerCase();
|
|
312
|
+
if (k === 'y' || k === 'n') {
|
|
313
|
+
const resolve = this.permissionResolve;
|
|
314
|
+
this.permissionResolve = null;
|
|
315
|
+
this.permissionPrompt = null;
|
|
316
|
+
this.state.permissionBox = null;
|
|
317
|
+
this.state.permissionDiffVisible = false;
|
|
318
|
+
this.state.permissionDiffInfo = null;
|
|
319
|
+
this.scheduleRender();
|
|
320
|
+
resolve(k === 'y');
|
|
321
|
+
}
|
|
322
|
+
else if (k === 'd' && this.state.permissionDiffInfo) {
|
|
323
|
+
this.state.permissionDiffVisible = !this.state.permissionDiffVisible;
|
|
324
|
+
this.scheduleRender();
|
|
325
|
+
}
|
|
326
|
+
return true; // Swallow all keys during permission prompt
|
|
327
|
+
}
|
|
328
|
+
/** Handle question prompt text input. Returns true if key was consumed. */
|
|
329
|
+
handleQuestionKey(key) {
|
|
330
|
+
if (!this.questionResolve || !this.state.questionPrompt)
|
|
331
|
+
return false;
|
|
332
|
+
const qp = this.state.questionPrompt;
|
|
333
|
+
if (key.name === 'return' && qp.input.trim()) {
|
|
334
|
+
const resolve = this.questionResolve;
|
|
335
|
+
const answer = qp.input.trim();
|
|
336
|
+
this.questionResolve = null;
|
|
337
|
+
this.state.questionPrompt = null;
|
|
338
|
+
this.scheduleRender();
|
|
339
|
+
resolve(answer);
|
|
340
|
+
}
|
|
341
|
+
else if (key.name === 'backspace') {
|
|
342
|
+
if (qp.cursor > 0) {
|
|
343
|
+
this.state.questionPrompt = { ...qp, input: qp.input.slice(0, qp.cursor - 1) + qp.input.slice(qp.cursor), cursor: qp.cursor - 1 };
|
|
344
|
+
this.scheduleRender();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (key.name === 'left') {
|
|
348
|
+
if (qp.cursor > 0) {
|
|
349
|
+
this.state.questionPrompt = { ...qp, cursor: qp.cursor - 1 };
|
|
350
|
+
this.scheduleRender();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (key.name === 'right') {
|
|
354
|
+
if (qp.cursor < qp.input.length) {
|
|
355
|
+
this.state.questionPrompt = { ...qp, cursor: qp.cursor + 1 };
|
|
356
|
+
this.scheduleRender();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (key.char && key.char.length === 1 && !key.ctrl && !key.meta) {
|
|
360
|
+
this.state.questionPrompt = { ...qp, input: qp.input.slice(0, qp.cursor) + key.char + qp.input.slice(qp.cursor), cursor: qp.cursor + 1 };
|
|
361
|
+
this.scheduleRender();
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
439
365
|
// ── Rendering ──
|
|
440
366
|
scheduleRender() {
|
|
441
367
|
if (this.renderPending || !this.started)
|
|
@@ -447,29 +373,170 @@ export class TerminalRenderer {
|
|
|
447
373
|
this.render();
|
|
448
374
|
});
|
|
449
375
|
}
|
|
376
|
+
/** Apply lightweight markdown styling to a line for scrollback output */
|
|
377
|
+
styleMarkdownLine(line) {
|
|
378
|
+
return line
|
|
379
|
+
.replace(/\*\*(.+?)\*\*/g, '\x1b[1m$1\x1b[0m') // bold → full reset after
|
|
380
|
+
.replace(/`([^`]+)`/g, '\x1b[2m$1\x1b[0m') // inline code → dim, full reset after
|
|
381
|
+
.replace(/^(#{1,3})\s+(.+)$/, '\x1b[1m\x1b[36m$1 $2\x1b[0m'); // headings → bold cyan
|
|
382
|
+
}
|
|
383
|
+
/** Calculate the max text width for flushed scrollback output, reserving space for companion */
|
|
384
|
+
flushTextWidth() {
|
|
385
|
+
const maxWidth = process.stdout.columns ?? 80;
|
|
386
|
+
if (!this.state.companionLines)
|
|
387
|
+
return maxWidth;
|
|
388
|
+
const compWidth = Math.max(...this.state.companionLines.map(l => l.length), 0) + 2;
|
|
389
|
+
return Math.max(40, maxWidth - compWidth);
|
|
390
|
+
}
|
|
391
|
+
/** Flush completed messages to terminal scrollback (native scrollbar) */
|
|
392
|
+
flushMessages() {
|
|
393
|
+
const messages = this.state.messages;
|
|
394
|
+
let didFlush = false;
|
|
395
|
+
const textWidth = this.flushTextWidth();
|
|
396
|
+
while (this.flushedMessageCount < messages.length) {
|
|
397
|
+
const msg = messages[this.flushedMessageCount];
|
|
398
|
+
// Don't flush the message currently being streamed
|
|
399
|
+
if (this.state.loading && this.flushedMessageCount === messages.length - 1 && msg.meta?.isStreaming)
|
|
400
|
+
break;
|
|
401
|
+
const t = getTheme();
|
|
402
|
+
const colorCode = msg.role === 'user' ? `\x1b[${FG(t.user)}m\x1b[1m` : msg.role === 'assistant' ? `\x1b[${FG(t.assistant)}m` : '\x1b[2m';
|
|
403
|
+
const prefixChar = msg.role === 'user' ? '❯ ' : msg.role === 'assistant' ? '◆ ' : ' ';
|
|
404
|
+
const lines = msg.content.split('\n');
|
|
405
|
+
for (let i = 0; i < lines.length; i++) {
|
|
406
|
+
const styledLine = msg.role === 'assistant' ? this.styleMarkdownLine(lines[i]) : lines[i];
|
|
407
|
+
const linePrefix = i === 0 ? prefixChar : ' ';
|
|
408
|
+
const fullLine = linePrefix + styledLine;
|
|
409
|
+
process.stdout.write(colorCode + fullLine.slice(0, textWidth) + '\x1b[0m\n');
|
|
410
|
+
}
|
|
411
|
+
// Divider after each message
|
|
412
|
+
process.stdout.write('\x1b[2m' + '─'.repeat(Math.min(60, textWidth)) + '\x1b[0m\n');
|
|
413
|
+
this.flushedMessageCount++;
|
|
414
|
+
didFlush = true;
|
|
415
|
+
}
|
|
416
|
+
// Flush completed tool calls as single-line summaries (once each)
|
|
417
|
+
if (didFlush) {
|
|
418
|
+
for (const [callId, tc] of this.state.toolCalls) {
|
|
419
|
+
if (tc.status === 'running')
|
|
420
|
+
continue;
|
|
421
|
+
if (this.flushedToolCallIds.has(callId))
|
|
422
|
+
continue;
|
|
423
|
+
this.flushedToolCallIds.add(callId);
|
|
424
|
+
const t = getTheme();
|
|
425
|
+
const icon = tc.status === 'done' ? `\x1b[${FG(t.success)}m✓` : `\x1b[${FG(t.error)}m✗`;
|
|
426
|
+
const summary = tc.resultSummary ? ` ${tc.resultSummary}` : '';
|
|
427
|
+
const elapsed = tc.startedAt ? ` · ${Math.floor((Date.now() - tc.startedAt) / 1000)}s` : '';
|
|
428
|
+
const toolLine = `${icon} ${tc.toolName}\x1b[0m \x1b[2m${tc.args ?? ''}${summary}${elapsed}\x1b[0m`;
|
|
429
|
+
process.stdout.write(toolLine.slice(0, textWidth) + '\n');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** Convert a CellGrid to sequential ANSI output (line-by-line, no absolute positioning) */
|
|
434
|
+
renderGridToAnsi(grid) {
|
|
435
|
+
const parts = [];
|
|
436
|
+
for (let r = 0; r < grid.height; r++) {
|
|
437
|
+
// Find last non-space cell to avoid writing trailing whitespace
|
|
438
|
+
let lastCol = grid.width - 1;
|
|
439
|
+
while (lastCol >= 0 && grid.cells[r][lastCol].char === ' ' && !grid.cells[r][lastCol].style.bg) {
|
|
440
|
+
lastCol--;
|
|
441
|
+
}
|
|
442
|
+
let lastStyle = '';
|
|
443
|
+
for (let c = 0; c <= lastCol; c++) {
|
|
444
|
+
const cell = grid.cells[r][c];
|
|
445
|
+
const sgr = styleToSGR(cell.style);
|
|
446
|
+
if (sgr !== lastStyle) {
|
|
447
|
+
parts.push(sgr);
|
|
448
|
+
lastStyle = sgr;
|
|
449
|
+
}
|
|
450
|
+
parts.push(cell.char);
|
|
451
|
+
}
|
|
452
|
+
parts.push('\x1b[0m\x1b[K'); // reset + erase to end of line (clear stale content)
|
|
453
|
+
if (r < grid.height - 1)
|
|
454
|
+
parts.push('\n');
|
|
455
|
+
}
|
|
456
|
+
return parts.join('');
|
|
457
|
+
}
|
|
450
458
|
render() {
|
|
451
459
|
const w = process.stdout.columns ?? 80;
|
|
452
460
|
const h = process.stdout.rows ?? 24;
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
461
|
+
// 1. Move UP from cursor to start of old live area, then erase below.
|
|
462
|
+
// lastLiveLines = cursor.cursorRow from last frame (distance from cursor to live area start).
|
|
463
|
+
// Using relative movement so it works correctly even after terminal scroll.
|
|
464
|
+
let eraseCmd = '';
|
|
465
|
+
if (this.lastLiveLines > 0) {
|
|
466
|
+
eraseCmd += `\x1b[${this.lastLiveLines}A`;
|
|
457
467
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
468
|
+
eraseCmd += '\r\x1b[J'; // column 0, erase to end of screen
|
|
469
|
+
syncWrite(eraseCmd);
|
|
470
|
+
// 2. Flush completed messages to scrollback (sequential, cursor moves down)
|
|
471
|
+
this.flushMessages();
|
|
472
|
+
// 3. Calculate and render new live area
|
|
473
|
+
const liveHeight = Math.min(h, this.calculateLiveHeight());
|
|
474
|
+
if (w !== this.current.width || liveHeight !== this.current.height) {
|
|
475
|
+
this.current = new CellGrid(w, liveHeight);
|
|
463
476
|
}
|
|
464
|
-
|
|
465
|
-
|
|
477
|
+
this.current.clear();
|
|
478
|
+
const cursor = rasterizeLive(this.state, this.current);
|
|
479
|
+
// 4. Write live area sequentially, then position cursor using relative movement.
|
|
480
|
+
// After grid output, cursor may be in pending-wrap state at end of last row.
|
|
481
|
+
// \r resolves wrap and goes to column 0, then we move up to cursor row.
|
|
482
|
+
const liveOutput = this.renderGridToAnsi(this.current);
|
|
483
|
+
const upFromEnd = liveHeight - 1 - cursor.cursorRow;
|
|
484
|
+
syncWrite(liveOutput + '\r' +
|
|
485
|
+
(upFromEnd > 0 ? `\x1b[${upFromEnd}A` : '') +
|
|
486
|
+
`\x1b[${cursor.cursorCol + 1}G` // move to column (1-indexed)
|
|
487
|
+
);
|
|
466
488
|
showCursor();
|
|
467
|
-
//
|
|
468
|
-
this
|
|
489
|
+
// Track cursor's distance from live area start (NOT total height).
|
|
490
|
+
// Next frame moves up by this amount to get back to live area start.
|
|
491
|
+
this.lastLiveLines = cursor.cursorRow;
|
|
492
|
+
}
|
|
493
|
+
/** Estimate the height needed for the live area */
|
|
494
|
+
calculateLiveHeight() {
|
|
495
|
+
let rows = 3; // border + input + hints (minimum)
|
|
496
|
+
// Banner only shown when no messages and not loading (must match rasterizeLive condition)
|
|
497
|
+
if (this.state.bannerLines && this.state.messages.length === 0 && !this.state.loading) {
|
|
498
|
+
rows += this.state.bannerLines.length + 1;
|
|
499
|
+
}
|
|
500
|
+
if (this.state.loading && this.state.streamingText)
|
|
501
|
+
rows += Math.min(this.state.streamingText.split('\n').length, 10);
|
|
502
|
+
if (this.state.thinkingText)
|
|
503
|
+
rows += this.state.thinkingExpanded ? 10 : 1;
|
|
504
|
+
if (!this.state.loading && this.state.lastThinkingSummary)
|
|
505
|
+
rows += 1;
|
|
506
|
+
if (this.state.loading && !this.state.streamingText && !this.state.thinkingText)
|
|
507
|
+
rows += 1; // spinner
|
|
508
|
+
if (this.state.errorText)
|
|
509
|
+
rows += 1;
|
|
510
|
+
for (const [, tc] of this.state.toolCalls) {
|
|
511
|
+
rows += 2; // header + possible description/agent line
|
|
512
|
+
if (tc.status === 'running' && tc.liveOutput)
|
|
513
|
+
rows += Math.min(tc.liveOutput.length, 3);
|
|
514
|
+
}
|
|
515
|
+
if (this.state.contextWarning)
|
|
516
|
+
rows += 1;
|
|
517
|
+
rows += Math.min(this.state.notifications.length, 2); // toast notifications
|
|
518
|
+
if (this.state.statusLine)
|
|
519
|
+
rows += 1;
|
|
520
|
+
rows += this.state.autocomplete.length;
|
|
521
|
+
if (this.state.permissionBox) {
|
|
522
|
+
rows += 3;
|
|
523
|
+
if (this.state.permissionDiffVisible && this.state.permissionDiffInfo)
|
|
524
|
+
rows += 15;
|
|
525
|
+
}
|
|
526
|
+
if (this.state.questionPrompt)
|
|
527
|
+
rows += 3 + (this.state.questionPrompt.options?.length ?? 0);
|
|
528
|
+
if (this.state.companionLines)
|
|
529
|
+
rows = Math.max(rows, this.state.companionLines.length + 2);
|
|
530
|
+
const inputLineCount = Math.min(5, (this.state.inputText.match(/\n/g)?.length ?? 0) + 1);
|
|
531
|
+
rows += inputLineCount - 1;
|
|
532
|
+
const h = process.stdout.rows ?? 24;
|
|
533
|
+
// On initial screen with banner, fill the terminal
|
|
534
|
+
if (this.state.bannerLines && this.state.messages.length === 0 && !this.state.loading) {
|
|
535
|
+
return h;
|
|
536
|
+
}
|
|
537
|
+
return Math.min(rows, Math.floor(h * 0.7)); // never exceed 70% of terminal
|
|
469
538
|
}
|
|
470
539
|
handleResize() {
|
|
471
|
-
// Force full repaint on resize
|
|
472
|
-
this.previous = new CellGrid(1, 1);
|
|
473
540
|
this.scheduleRender();
|
|
474
541
|
}
|
|
475
542
|
}
|