@semalt-ai/code 1.8.3 → 1.8.4
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/index.js +14 -7
- package/lib/agent.js +189 -58
- package/lib/api.js +11 -34
- package/lib/commands.js +206 -121
- package/lib/config.js +1 -0
- package/lib/constants.js +1 -1
- package/lib/permissions.js +9 -8
- package/lib/prompts.js +4 -7
- package/lib/tools.js +14 -7
- package/lib/ui/chat-history.js +19 -8
- package/lib/ui/create-ui.js +63 -38
- package/lib/ui/diff.js +4 -3
- package/lib/ui/format.js +247 -0
- package/lib/ui/input-field.js +134 -59
- package/lib/ui/layout.js +0 -2
- package/lib/ui/messages.js +44 -0
- package/lib/ui/select.js +114 -0
- package/lib/ui/status-bar.js +135 -28
- package/lib/ui/stream.js +8 -12
- package/lib/ui/terminal.js +2 -0
- package/lib/ui/theme.js +25 -4
- package/lib/ui/utils.js +94 -27
- package/lib/ui/writer.js +393 -45
- package/lib/ui.js +6 -6
- package/package.json +1 -1
- package/lib/ui/legacy.js +0 -130
package/lib/permissions.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const writer = require('./ui/writer');
|
|
4
|
+
const messages = require('./ui/messages');
|
|
5
|
+
|
|
3
6
|
const TIER_FS = ['read_file', 'write_file', 'append_file', 'delete_file', 'list_dir', 'make_dir', 'move_file', 'copy_file', 'file_stat', 'search_files', 'store_memory', 'recall_memory'];
|
|
4
7
|
const TIER_EXEC = ['exec'];
|
|
5
8
|
const TIER_NET = ['http_get', 'download'];
|
|
@@ -117,7 +120,7 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
117
120
|
if (uiCallbacks) {
|
|
118
121
|
uiCallbacks.onAddMessage({ role: 'system', content: `✓ Auto-approved: ${description}` });
|
|
119
122
|
} else {
|
|
120
|
-
|
|
123
|
+
messages.sysSuccess(`Auto-approved: ${description}`);
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -133,7 +136,7 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
136
|
-
|
|
139
|
+
writer.scrollback(` [non-TTY] Auto-approving: ${description}`);
|
|
137
140
|
return true;
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -155,13 +158,11 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
155
158
|
return true;
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
// Fallback:
|
|
161
|
+
// Fallback: TTY interactive select (used outside of chat UI)
|
|
159
162
|
const alwaysLabel = tag ? `Yes, always for <${tag}>` : 'Yes, always';
|
|
160
163
|
const choices = ['Yes', alwaysLabel, 'No'];
|
|
161
164
|
|
|
162
|
-
|
|
163
|
-
console.log(` ${FG_YELLOW}${BOLD}⚠ Permission required${RST}`);
|
|
164
|
-
console.log(` ${FG_GRAY}${actionType}: ${description}${RST}`);
|
|
165
|
+
writer.scrollback(`\n ${FG_YELLOW}${BOLD}⚠ Permission required${RST}\n ${FG_GRAY}${actionType}: ${description}${RST}`);
|
|
165
166
|
|
|
166
167
|
const selectedIndex = await interactiveSelect(
|
|
167
168
|
choices,
|
|
@@ -174,13 +175,13 @@ function createPermissionManager(ui, { allowedTiers = [], readonly = false } = {
|
|
|
174
175
|
);
|
|
175
176
|
|
|
176
177
|
if (selectedIndex === null || selectedIndex === 2) {
|
|
177
|
-
|
|
178
|
+
writer.scrollback(` ${FG_RED}✗${RST} ${FG_DARK}Denied${RST}`);
|
|
178
179
|
return false;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
if (selectedIndex === 1 && tag) {
|
|
182
183
|
state.sessionApprovedTags.add(tag);
|
|
183
|
-
|
|
184
|
+
writer.scrollback(` ${FG_GREEN}✓${RST} ${FG_DARK}Auto-approve enabled for <${tag}> this session${RST}`);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
return true;
|
package/lib/prompts.js
CHANGED
|
@@ -94,12 +94,9 @@ ${TAG_INVENTORY}
|
|
|
94
94
|
7. Be concise. Provide working solutions. Use markdown for code blocks in explanations.
|
|
95
95
|
8. Current working directory: __CWD__
|
|
96
96
|
|
|
97
|
-
Response contract
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
(b) <final_answer>...</final_answer> containing your answer to the user.
|
|
101
|
-
A response containing neither is invalid and will be rejected.
|
|
102
|
-
Do not describe actions in prose outside these two forms. Either call the tool, or wrap your final reply in <final_answer>.`;
|
|
97
|
+
Response contract:
|
|
98
|
+
- If the task requires an action, emit the appropriate tool tag(s) — do not narrate intended actions in prose without the tag.
|
|
99
|
+
- If the task is complete or the user's question can be answered directly without running a tool, reply in plain prose. No special wrapper is needed.`;
|
|
103
100
|
|
|
104
101
|
const NATIVE_SYSTEM_PROMPT_TEMPLATE = `You are Semalt.AI, an expert AI coding assistant running in the user's terminal. Use the provided tools to execute shell commands and file operations; do not just print instructions. Each call is approved by the user before execution, and the result is returned to you for the next step.
|
|
105
102
|
|
|
@@ -107,7 +104,7 @@ Use \`<think>...</think>\` for internal reasoning (runtime-handled; never emit a
|
|
|
107
104
|
|
|
108
105
|
Be concise. Use markdown for code blocks in explanations. Current working directory: __CWD__
|
|
109
106
|
|
|
110
|
-
Response contract:
|
|
107
|
+
Response contract: if the task requires an action, emit one or more tool calls — do not narrate intended actions in prose without the tool call. Otherwise, answer in plain prose; no special wrapper is needed.`;
|
|
111
108
|
|
|
112
109
|
function getSystemPrompt(nativeTools = false) {
|
|
113
110
|
const template = nativeTools ? NATIVE_SYSTEM_PROMPT_TEMPLATE : SYSTEM_PROMPT_TEMPLATE;
|
package/lib/tools.js
CHANGED
|
@@ -9,6 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const { spawn } = require('child_process');
|
|
10
10
|
|
|
11
11
|
const { logToolCall } = require('./audit');
|
|
12
|
+
const writer = require('./ui/writer');
|
|
12
13
|
|
|
13
14
|
const MEMORY_PATH = path.join(os.homedir(), '.semalt-ai', 'memory.json');
|
|
14
15
|
|
|
@@ -22,6 +23,7 @@ function getSkippedOps() { return _skippedOps.slice(); }
|
|
|
22
23
|
let _uiActive = false;
|
|
23
24
|
function setUIActive(v) { _uiActive = v; }
|
|
24
25
|
function isUIActive() { return _uiActive; }
|
|
26
|
+
// audit: allowed — fires only when TUI is inactive (one-shot non-TUI commands), no live region to protect.
|
|
25
27
|
function _log(...args) { if (!_uiActive) console.log(...args); }
|
|
26
28
|
|
|
27
29
|
// Reject writes outside the project CWD and in sensitive system/home dirs
|
|
@@ -164,7 +166,7 @@ function createToolExecutor(permissionManager, ui, getConfig) {
|
|
|
164
166
|
_log(` ${FG_GREEN}✓${RST} ${FG_GRAY}Read ${filePath}${RST}`);
|
|
165
167
|
}
|
|
166
168
|
logToolCall('read_file', { path: filePath }, true, 'ok');
|
|
167
|
-
return { content: data, path: filePath };
|
|
169
|
+
return { content: data, path: filePath, bytes: Buffer.byteLength(data, 'utf8') };
|
|
168
170
|
} catch (error) {
|
|
169
171
|
_log(` ${FG_RED}✗ ${error.message}${RST}`);
|
|
170
172
|
logToolCall('read_file', { path: filePath }, true, 'error');
|
|
@@ -202,7 +204,7 @@ function createToolExecutor(permissionManager, ui, getConfig) {
|
|
|
202
204
|
const diffOutput = _uiActive
|
|
203
205
|
? renderDiff(existing, finalContent, filePath, { inset: DIFF_BUBBLE_INSET })
|
|
204
206
|
: renderDiff(existing, finalContent, filePath);
|
|
205
|
-
if (!_uiActive)
|
|
207
|
+
if (!_uiActive) writer.scrollback(diffOutput);
|
|
206
208
|
|
|
207
209
|
// Dry-run: record the skipped op and return without writing
|
|
208
210
|
if (_dryRun) {
|
|
@@ -212,7 +214,7 @@ function createToolExecutor(permissionManager, ui, getConfig) {
|
|
|
212
214
|
return { status: 'dry-run', message: 'dry-run: write skipped', path: filePath };
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
// Permission check — routes through TUI dialog in chat mode, interactiveSelect in
|
|
217
|
+
// Permission check — routes through TUI dialog in chat mode, interactiveSelect in non-TUI flows
|
|
216
218
|
let desc = `${action === 'write' ? 'Write' : 'Append to'} ${filePath}`;
|
|
217
219
|
if (content) desc += ` (${content.length} chars)`;
|
|
218
220
|
if (_uiActive) desc = `${desc}\n${diffOutput}`;
|
|
@@ -715,18 +717,21 @@ function createToolExecutor(permissionManager, ui, getConfig) {
|
|
|
715
717
|
}
|
|
716
718
|
_log(` ${FG_GREEN}✓${RST} ${FG_GRAY}HTTP GET ${target} (${res.statusCode}, ${totalBytes} bytes${capped ? `, truncated to ${keptBytes}` : ''})${RST}`);
|
|
717
719
|
logToolCall('http_get', { url: target }, true, res.statusCode < 400 ? 'ok' : 'error');
|
|
718
|
-
|
|
720
|
+
// `bytes` is the total transferred payload length (pre-cap);
|
|
721
|
+
// consumers that want to know the wire size without parsing
|
|
722
|
+
// the appended truncation note rely on this.
|
|
723
|
+
resolve({ status_code: res.statusCode, body, bytes: totalBytes });
|
|
719
724
|
});
|
|
720
725
|
});
|
|
721
726
|
req.on('error', (err) => {
|
|
722
727
|
_log(` ${FG_RED}✗ ${err.message}${RST}`);
|
|
723
728
|
logToolCall('http_get', { url: target }, true, 'error');
|
|
724
|
-
resolve({ error: err.message });
|
|
729
|
+
resolve({ error: err.message, error_code: err.code });
|
|
725
730
|
});
|
|
726
731
|
req.setTimeout(reqTimeoutMs, () => {
|
|
727
732
|
req.destroy();
|
|
728
733
|
logToolCall('http_get', { url: target }, true, 'error');
|
|
729
|
-
resolve({ error: 'Request timeout' });
|
|
734
|
+
resolve({ error: 'Request timeout', error_code: 'ETIMEDOUT' });
|
|
730
735
|
});
|
|
731
736
|
}
|
|
732
737
|
doGet(url, 5);
|
|
@@ -747,10 +752,12 @@ function createToolExecutor(permissionManager, ui, getConfig) {
|
|
|
747
752
|
return { question, answer: selected || options[0] };
|
|
748
753
|
}
|
|
749
754
|
if (!process.stdout.isTTY || process.stdin.isRaw) {
|
|
750
|
-
|
|
755
|
+
writer.scrollback(`\n ${FG_YELLOW}?${RST} ${question}\n ${DIM}[auto-answering 'y']${RST}`);
|
|
751
756
|
logToolCall('ask_user', { question }, true, 'ok');
|
|
752
757
|
return { question, answer: 'y' };
|
|
753
758
|
}
|
|
759
|
+
// audit: allowed — inline prompt without trailing newline; unreachable when TUI writer is active
|
|
760
|
+
// (process.stdin.isRaw is true while the TUI input field holds raw mode).
|
|
754
761
|
process.stdout.write(`\n ${FG_YELLOW}?${RST} ${question}\n ${FG_GRAY}>${RST} `);
|
|
755
762
|
const buf = Buffer.alloc(4096);
|
|
756
763
|
let input = '';
|
package/lib/ui/chat-history.js
CHANGED
|
@@ -142,14 +142,24 @@ class ChatHistory {
|
|
|
142
142
|
}
|
|
143
143
|
out += '\n';
|
|
144
144
|
} else if (msg.role === 'tool') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
// Tool summary header. The writer's activity region now commits the
|
|
146
|
+
// primary "glyph · category · op · duration · meta" line directly to
|
|
147
|
+
// scrollback via endActivity — when a caller supplies an empty
|
|
148
|
+
// `content` they're signalling that the header is already present
|
|
149
|
+
// and only the expandable output body should render here.
|
|
150
|
+
if (content) {
|
|
151
|
+
const indicator = msg.isError
|
|
152
|
+
? `${UI_THEME.error}${UI_ICONS.error}${RST}`
|
|
153
|
+
: `${UI_THEME.success}${UI_ICONS.success}${RST}`;
|
|
154
|
+
const category = TOOL_CATEGORIES[msg.tag] || msg.tag || 'tool';
|
|
155
|
+
const head = `${UI_THEME.accent}${category}${UI_THEME.muted}:${RST}`;
|
|
156
|
+
const desc = _dimPaths(content);
|
|
157
|
+
out = ` ${indicator} ${head} ${desc}\n`;
|
|
158
|
+
lineCount = 1;
|
|
159
|
+
} else {
|
|
160
|
+
out = '';
|
|
161
|
+
lineCount = 0;
|
|
162
|
+
}
|
|
153
163
|
if (msg.output) {
|
|
154
164
|
const wrapAt = Math.max(60, getCols() - 8);
|
|
155
165
|
const outLines = [];
|
|
@@ -309,6 +319,7 @@ class ChatHistory {
|
|
|
309
319
|
// the clear can't land inside a pending scrollback burst, then redraw
|
|
310
320
|
// the live region under the new cursor.
|
|
311
321
|
writer.enqueue(() => {
|
|
322
|
+
// audit: allowed — viewport clear inside writer.enqueue (sanctioned escape hatch).
|
|
312
323
|
try { process.stdout.write('\x1b[3J\x1b[2J\x1b[H'); } catch {}
|
|
313
324
|
});
|
|
314
325
|
writer.redrawLive();
|
package/lib/ui/create-ui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
|
|
5
|
-
const { RST,
|
|
5
|
+
const { RST, FG_YELLOW, FG_CYAN, FG_GRAY } = require('./ansi');
|
|
6
6
|
const { ChatHistory } = require('./chat-history');
|
|
7
7
|
const writer = require('./writer');
|
|
8
8
|
const { registerTerminalCleanup } = require('./terminal');
|
|
@@ -10,9 +10,11 @@ const { registerTerminalCleanup } = require('./terminal');
|
|
|
10
10
|
function _createNoOpUI() {
|
|
11
11
|
const chatHistory = {
|
|
12
12
|
addMessage: (msg) => {
|
|
13
|
+
// audit: allowed — non-TUI no-op UI fallback, no live region to protect.
|
|
13
14
|
if (msg.role === 'assistant' || msg.role === 'user')
|
|
14
15
|
process.stdout.write(`[${msg.role}] ${msg.content || ''}\n`);
|
|
15
16
|
},
|
|
17
|
+
// audit: allowed — non-TUI no-op UI fallback, no live region to protect.
|
|
16
18
|
streamToken: (token) => process.stdout.write(token),
|
|
17
19
|
clearStreamingContent: () => {},
|
|
18
20
|
finalizeLastMessage: () => {},
|
|
@@ -25,8 +27,9 @@ function _createNoOpUI() {
|
|
|
25
27
|
};
|
|
26
28
|
const statusBar = {
|
|
27
29
|
update: () => {}, updateMetrics: () => {}, onToken: () => {},
|
|
28
|
-
drawSeparator: () => {},
|
|
29
|
-
setModel: () => {},
|
|
30
|
+
drawSeparator: () => {}, _renderBar: () => {},
|
|
31
|
+
setModel: () => {}, setContextLimit: () => {}, setReportedContext: () => {},
|
|
32
|
+
addPendingTokens: () => {}, destroy: () => {},
|
|
30
33
|
};
|
|
31
34
|
let _submitCb = null;
|
|
32
35
|
const inputField = {
|
|
@@ -59,7 +62,7 @@ function createUI(opts) {
|
|
|
59
62
|
const { LayoutManager } = require('./layout');
|
|
60
63
|
const { FullStatusBar } = require('./status-bar');
|
|
61
64
|
const { InputField } = require('./input-field');
|
|
62
|
-
const { interactiveSelect } = require('./
|
|
65
|
+
const { interactiveSelect } = require('./select');
|
|
63
66
|
|
|
64
67
|
const layout = new LayoutManager();
|
|
65
68
|
const chatHistory = new ChatHistory();
|
|
@@ -93,24 +96,38 @@ function createUI(opts) {
|
|
|
93
96
|
// chrome has a visible top edge.
|
|
94
97
|
if (_sb) lines.push(_sb.renderSeparator());
|
|
95
98
|
|
|
96
|
-
// Status bar.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
}
|
|
99
|
+
// Status bar. ALWAYS pushed — the row is a permanent fixture of the
|
|
100
|
+
// live region so the input and hint rows below it keep a stable
|
|
101
|
+
// vertical position. `renderLine()` is contractually non-null; missing
|
|
102
|
+
// data (model, tokens) renders as a short placeholder inside the row.
|
|
103
|
+
if (_sb) lines.push(_sb.renderLine());
|
|
103
104
|
|
|
104
105
|
// Input row(s).
|
|
106
|
+
let caret = null;
|
|
105
107
|
if (_inputField) {
|
|
106
|
-
|
|
108
|
+
const inputLines = _inputField.renderInputLines();
|
|
109
|
+
for (const row of inputLines) {
|
|
107
110
|
lines.push(row);
|
|
108
111
|
}
|
|
109
112
|
// Hints row.
|
|
110
113
|
lines.push(_inputField.renderHintsLine());
|
|
114
|
+
|
|
115
|
+
// Caret: translate the input-local { line, col } into the live
|
|
116
|
+
// region's "rows up from the cursor's post-draw position". The
|
|
117
|
+
// post-draw cursor lands one row below the final hints row, so:
|
|
118
|
+
// rowsFromBottom = 1 (for hints) + (inputLines.length - caret.line)
|
|
119
|
+
// Leaving caret null keeps the OS cursor hidden — used while input
|
|
120
|
+
// is disabled or a navigation capture is active.
|
|
121
|
+
const inCaret = _inputField.getCaretPosition && _inputField.getCaretPosition();
|
|
122
|
+
if (inCaret && inputLines.length > 0) {
|
|
123
|
+
caret = {
|
|
124
|
+
rowsFromBottom: 1 + (inputLines.length - inCaret.line),
|
|
125
|
+
col: inCaret.col,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
writer.setLive(lines);
|
|
130
|
+
writer.setLive(lines, caret);
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
_sb = new FullStatusBar(layout, _updateLive);
|
|
@@ -136,31 +153,34 @@ function createUI(opts) {
|
|
|
136
153
|
|
|
137
154
|
// ── captureSelect (modal menu) ───────────────────────────────────────────────
|
|
138
155
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
156
|
+
// Numbered-options picker for tools like ask_user. interactiveSelect
|
|
157
|
+
// renders each frame into the writer's modal region (above status,
|
|
158
|
+
// below scrollback) and routes keys through the input field's
|
|
159
|
+
// captureNavigation API — so the menu cohabits with the live region
|
|
160
|
+
// instead of taking over the screen, and Enter/Esc resolve in place.
|
|
142
161
|
inputField.captureSelect = (menu) => new Promise((resolve) => {
|
|
143
162
|
if (!process.stdin.isTTY) {
|
|
144
163
|
resolve(menu.options[0]);
|
|
145
164
|
return;
|
|
146
165
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
interactiveSelect(
|
|
167
|
+
menu.options,
|
|
168
|
+
(opt, isSelected) => isSelected
|
|
169
|
+
? ` ${FG_YELLOW}❯${RST} ${FG_CYAN}${opt}${RST}`
|
|
170
|
+
: ` ${FG_GRAY}${opt}${RST}`,
|
|
171
|
+
{
|
|
172
|
+
initialIndex: 0,
|
|
173
|
+
onExpand: () => chatHistory.toggleLastExpand(),
|
|
174
|
+
captureNavigation: (handler) => {
|
|
175
|
+
inputField.captureNavigation(handler);
|
|
176
|
+
return () => inputField.releaseNavigation();
|
|
157
177
|
},
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
}
|
|
179
|
+
).then((idx) => {
|
|
180
|
+
// Cancel returns null. Match the prior contract: pick the last
|
|
181
|
+
// option (typically "No"/decline) so callers don't need to
|
|
182
|
+
// special-case cancellation.
|
|
183
|
+
resolve(idx === null ? menu.options[menu.options.length - 1] : menu.options[idx]);
|
|
164
184
|
});
|
|
165
185
|
});
|
|
166
186
|
|
|
@@ -181,6 +201,7 @@ function createUI(opts) {
|
|
|
181
201
|
// starts in a known state, then paint the first live frame below a fresh
|
|
182
202
|
// cursor position.
|
|
183
203
|
writer.enqueue(() => {
|
|
204
|
+
// audit: allowed — terminal-mode raw escape inside writer.enqueue (sanctioned escape hatch).
|
|
184
205
|
try { process.stdout.write('\x1b[2J\x1b[3J\x1b[H\x1b[?25l'); } catch {}
|
|
185
206
|
});
|
|
186
207
|
// Pre-render both input and hints so the first _updateLive has valid
|
|
@@ -198,17 +219,21 @@ function createUI(opts) {
|
|
|
198
219
|
// ── Destroy ──────────────────────────────────────────────────────────────────
|
|
199
220
|
// Stop timers + stdin listeners FIRST so no further writes can be queued,
|
|
200
221
|
// then run writer.teardown(). teardown is a single synchronous stdout
|
|
201
|
-
// write that erases the live region
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
222
|
+
// write that erases the live region, emits any end-of-session artifacts
|
|
223
|
+
// passed in (session summary, resume hint, goodbye) as regular scrollback
|
|
224
|
+
// content, and resets terminal state. After it returns, the cursor sits
|
|
225
|
+
// at column 0 of the row immediately below those artifacts — ready for
|
|
226
|
+
// the shell prompt. Callers that want artifacts emitted at exit must
|
|
227
|
+
// pass them here rather than console.log-ing after destroy(); doing it
|
|
228
|
+
// after destroy races with terminal-mode restoration and can leave
|
|
229
|
+
// artifacts overlaid on scrollback with the cursor drifting mid-viewport.
|
|
230
|
+
function destroy(teardownOpts) {
|
|
206
231
|
if (_destroyCalled) return;
|
|
207
232
|
_destroyCalled = true;
|
|
208
233
|
try { inputField.destroy(); } catch {}
|
|
209
234
|
try { sb.destroy(); } catch {}
|
|
210
235
|
try { layout.destroy(); } catch {}
|
|
211
|
-
writer.teardown();
|
|
236
|
+
writer.teardown(teardownOpts);
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
return { chatHistory, statusBar: sb, inputField, layout, destroy, redrawFixed: _updateLive };
|
package/lib/ui/diff.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { FG_DARK, FG_RED, FG_GREEN, FG_GRAY, FG_YELLOW, FG_TEAL, RST, THEME, EL, hasTruecolor } = require('./ansi');
|
|
4
4
|
const { getCols, stripAnsi, termWidth } = require('./utils');
|
|
5
5
|
const { DIFF_THEME, UI_THEME } = require('./theme');
|
|
6
|
+
const writer = require('./writer');
|
|
6
7
|
|
|
7
8
|
function diffLines(oldLines, newLines) {
|
|
8
9
|
const m = oldLines.length, n = newLines.length;
|
|
@@ -206,7 +207,7 @@ function _mdInline(text) {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
function renderMarkdown(text) {
|
|
209
|
-
if (!process.stdout.isTTY) {
|
|
210
|
+
if (!process.stdout.isTTY) { writer.scrollback(text); return; }
|
|
210
211
|
const { loadConfig } = require('../config');
|
|
211
212
|
const maxLines = (loadConfig().max_output_lines) || 50;
|
|
212
213
|
const cols = getCols();
|
|
@@ -248,8 +249,8 @@ function renderMarkdown(text) {
|
|
|
248
249
|
}
|
|
249
250
|
let overflow = 0, printLines = output;
|
|
250
251
|
if (output.length > maxLines) { overflow = output.length - maxLines; printLines = output.slice(0, maxLines); }
|
|
251
|
-
|
|
252
|
-
if (overflow > 0)
|
|
252
|
+
if (printLines.length > 0) writer.scrollback(printLines.join('\n'));
|
|
253
|
+
if (overflow > 0) writer.scrollback(THEME.dim + '[... ' + overflow + ' more lines]' + THEME.reset);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
module.exports = { renderDiff, renderMarkdown, _mdInline };
|