@bahulam/code 2.6.14 → 2.6.16
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/package.json +5 -5
- package/src/auth/tarang-auth.mjs +6 -0
- package/src/config/cli-args.mjs +7 -1
- package/src/config/model-catalog.mjs +57 -0
- package/src/core/approval-log.mjs +23 -0
- package/src/core/approval.mjs +93 -17
- package/src/core/bundled-runtime.mjs +12 -0
- package/src/core/error-guidance.mjs +8 -4
- package/src/core/file-diff.mjs +1 -1
- package/src/core/local-agent.mjs +3 -3
- package/src/core/risk-tier.mjs +53 -2
- package/src/core/safety.mjs +61 -4
- package/src/core/tool-executor.mjs +25 -13
- package/src/core/trust.mjs +5 -3
- package/src/index.mjs +7 -3
- package/src/permissions/command-classifier.mjs +50 -2
- package/src/telemetry/index.mjs +97 -71
- package/src/terminal/main.mjs +40 -0
- package/src/terminal/repl-format.mjs +64 -4
- package/src/terminal/repl-model-form.mjs +132 -0
- package/src/terminal/repl-render.mjs +256 -33
- package/src/terminal/repl-resume.mjs +27 -12
- package/src/terminal/repl-state.mjs +16 -0
- package/src/terminal/repl.mjs +644 -85
- package/src/terminal/tool-display.mjs +20 -1
- package/src/ui/approval.mjs +33 -5
- package/src/ui/input-dock.mjs +192 -16
- package/src/ui/render-queue.mjs +500 -0
- package/src/ui/slash-commands.mjs +2 -0
- package/src/ui/sub-agent.mjs +17 -2
- package/src/ui/tool-card.mjs +156 -13
- package/src/ui/tool-details.mjs +96 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Present-progressive verbs — read more conversationally than "Read file":
|
|
2
2
|
// "Reading auth.py — 47 lines" reads like the agent narrating, not a log.
|
|
3
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
4
|
+
|
|
3
5
|
const TOOL_LABELS = Object.freeze({
|
|
4
6
|
shell: 'Running',
|
|
5
7
|
read_file: 'Reading',
|
|
@@ -129,6 +131,7 @@ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
|
|
|
129
131
|
.join(', ') || 'Project files';
|
|
130
132
|
case 'edit_file': {
|
|
131
133
|
const filePath = shortPath(args.file_path || args.path, cwd);
|
|
134
|
+
if (isSensitiveConfigPath(filePath)) return `${filePath} · match [redacted]`;
|
|
132
135
|
const search = String(args.search || '').trim();
|
|
133
136
|
return search ? `${filePath} · match "${search.slice(0, 40)}${search.length > 40 ? '...' : ''}"` : filePath;
|
|
134
137
|
}
|
|
@@ -229,8 +232,12 @@ export function shellCommandProfile(command, {
|
|
|
229
232
|
|| commandLineCount >= compactLines
|
|
230
233
|
|| commandByteCount > compactChars;
|
|
231
234
|
const kind = script?.kind || (lineCount > 1 ? 'shell script' : 'shell command');
|
|
235
|
+
const preview = script?.body ? scriptBodyPreview(script.body) : '';
|
|
232
236
|
const summary = compact
|
|
233
|
-
?
|
|
237
|
+
? [
|
|
238
|
+
`${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`,
|
|
239
|
+
preview ? `preview: ${preview}` : '',
|
|
240
|
+
].filter(Boolean).join(' · ')
|
|
234
241
|
: body;
|
|
235
242
|
|
|
236
243
|
return {
|
|
@@ -244,6 +251,7 @@ export function shellCommandProfile(command, {
|
|
|
244
251
|
compact,
|
|
245
252
|
kind,
|
|
246
253
|
summary,
|
|
254
|
+
preview,
|
|
247
255
|
script,
|
|
248
256
|
detailHint: compact ? 'details: F2 or /last' : '',
|
|
249
257
|
};
|
|
@@ -297,6 +305,17 @@ function interpreterKind(value) {
|
|
|
297
305
|
return 'shell script';
|
|
298
306
|
}
|
|
299
307
|
|
|
308
|
+
function scriptBodyPreview(body, maxChars = 20) {
|
|
309
|
+
const line = String(body || '')
|
|
310
|
+
.replace(/\r\n?/g, '\n')
|
|
311
|
+
.split('\n')
|
|
312
|
+
.map(value => value.trim())
|
|
313
|
+
.find(Boolean) || '';
|
|
314
|
+
const compact = line.replace(/\s+/g, ' ');
|
|
315
|
+
if (compact.length <= maxChars) return compact;
|
|
316
|
+
return `${compact.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
317
|
+
}
|
|
318
|
+
|
|
300
319
|
function byteLength(value) {
|
|
301
320
|
try {
|
|
302
321
|
return Buffer.byteLength(String(value || ''), 'utf8');
|
package/src/ui/approval.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { paint, width as visibleWidth } from './palette.mjs';
|
|
|
19
19
|
import { icon } from './icons.mjs';
|
|
20
20
|
import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
21
21
|
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
22
|
+
import { isSensitiveConfigPath } from '../core/safety.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Default option set per tier. Caller can override via `opts.options`.
|
|
@@ -100,7 +101,14 @@ export function renderApprovalDockPrompt({
|
|
|
100
101
|
} = {}) {
|
|
101
102
|
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
102
103
|
const opts = options || defaultOptions(tier, { tool, args });
|
|
103
|
-
|
|
104
|
+
// Multi-line shell/python scripts auto-expand: the user cannot approve
|
|
105
|
+
// what they cannot see. "shell script · 5 lines · 309 B" as the only
|
|
106
|
+
// subject made blind approval the default. After execution the script
|
|
107
|
+
// collapses back to the one-line tool card (details stay on /last).
|
|
108
|
+
const isScriptCommand = tool === 'shell'
|
|
109
|
+
&& /\n/.test(String(args.command || args.cmd || ''));
|
|
110
|
+
const detailView = showDetails || isScriptCommand;
|
|
111
|
+
const subject = approvalDockSubject(tool, args, cols, detailView);
|
|
104
112
|
const risks = riskTerms(tool, args, tier);
|
|
105
113
|
const reason = compactReason(tool, args, why);
|
|
106
114
|
const lines = [
|
|
@@ -111,14 +119,22 @@ export function renderApprovalDockPrompt({
|
|
|
111
119
|
...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
|
|
112
120
|
];
|
|
113
121
|
|
|
122
|
+
// Show the WHOLE script when it fits — a partial script (…lines cut at
|
|
123
|
+
// the top) makes blind approval the default. Cap at half the terminal
|
|
124
|
+
// so the dock never eats the whole screen. Non-detail approvals keep
|
|
125
|
+
// the tight 8-row cap.
|
|
126
|
+
const termRows = Math.max(12, Number(process.stderr.rows) || 24);
|
|
127
|
+
const detailCap = Math.max(12, Math.floor(termRows / 2));
|
|
128
|
+
const maxRows = detailView ? Math.min(detailCap, Math.max(12, lines.length + 1)) : 8;
|
|
129
|
+
|
|
114
130
|
return {
|
|
115
131
|
prefix: '? approve › ',
|
|
116
|
-
value: truncateForDock(subject,
|
|
132
|
+
value: truncateForDock(subject, detailView ? 1200 : 220),
|
|
117
133
|
context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
|
|
118
134
|
meta: '',
|
|
119
|
-
tips: approvalFooter(tool,
|
|
135
|
+
tips: approvalFooter(tool, detailView),
|
|
120
136
|
lines,
|
|
121
|
-
maxRows
|
|
137
|
+
maxRows,
|
|
122
138
|
};
|
|
123
139
|
}
|
|
124
140
|
|
|
@@ -158,6 +174,7 @@ export { TIERS };
|
|
|
158
174
|
function tierTitle(tier) {
|
|
159
175
|
switch (tier) {
|
|
160
176
|
case TIERS.SENSITIVE_READ: return 'SENSITIVE';
|
|
177
|
+
case TIERS.PROTECTED_EDIT: return 'PROTECTED';
|
|
161
178
|
case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
|
|
162
179
|
case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
|
|
163
180
|
case TIERS.SHELL_MEDIUM: return 'MEDIUM';
|
|
@@ -172,6 +189,7 @@ function tierTitle(tier) {
|
|
|
172
189
|
function approvalTitle(tier) {
|
|
173
190
|
switch (tier) {
|
|
174
191
|
case TIERS.SENSITIVE_READ:
|
|
192
|
+
case TIERS.PROTECTED_EDIT:
|
|
175
193
|
case TIERS.SHELL_DANGEROUS:
|
|
176
194
|
case TIERS.DESTRUCTIVE:
|
|
177
195
|
return tierTitle(tier);
|
|
@@ -286,6 +304,7 @@ function riskTerms(tool, args = {}, tier) {
|
|
|
286
304
|
}
|
|
287
305
|
}
|
|
288
306
|
if (tier === TIERS.SENSITIVE_READ) terms.push('sensitive read');
|
|
307
|
+
if (tier === TIERS.PROTECTED_EDIT) terms.push('protected edit');
|
|
289
308
|
if (tier === TIERS.DESTRUCTIVE) terms.push('destructive');
|
|
290
309
|
return [...new Set(terms)].slice(0, 3);
|
|
291
310
|
}
|
|
@@ -315,11 +334,18 @@ function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
|
315
334
|
}
|
|
316
335
|
if (tool === 'write_file') {
|
|
317
336
|
const file = args.file_path || args.path || summary || '';
|
|
337
|
+
if (isSensitiveConfigPath(file)) return [`${file} · content redacted`];
|
|
318
338
|
const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
|
|
319
339
|
return [`${file}${lineCount ? ` · ${lineCount} lines` : ''}`];
|
|
320
340
|
}
|
|
321
341
|
if (tool === 'edit_file') {
|
|
322
342
|
const file = args.file_path || args.path || '';
|
|
343
|
+
if (isSensitiveConfigPath(file)) {
|
|
344
|
+
const details = [`${file || summary}`];
|
|
345
|
+
if (args.search || args.old_string) details.push('match: [redacted]');
|
|
346
|
+
if (args.replace || args.new_string) details.push('replace: [redacted]');
|
|
347
|
+
return details;
|
|
348
|
+
}
|
|
323
349
|
const search = String(args.search || args.old_string || '').trim();
|
|
324
350
|
const replacement = String(args.replace || args.new_string || '').trim();
|
|
325
351
|
const details = [`${file || summary}`];
|
|
@@ -397,9 +423,11 @@ function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
|
|
|
397
423
|
function approvalDockSubjectRows(subject) {
|
|
398
424
|
const lines = String(subject || '').split('\n');
|
|
399
425
|
const first = lines.shift() || '';
|
|
426
|
+
// 12 continuation rows matches approvalDockDetails' script cap — a
|
|
427
|
+
// 12-line script renders fully in the approval prompt.
|
|
400
428
|
return [
|
|
401
429
|
`${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
|
|
402
|
-
...lines.slice(0,
|
|
430
|
+
...lines.slice(0, 12).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
|
|
403
431
|
];
|
|
404
432
|
}
|
|
405
433
|
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
38
38
|
import { term, onResize } from './term.mjs';
|
|
39
39
|
import { wrapToLines, tailWithEllipsis, cursorPositionInLines } from './text-layout.mjs';
|
|
40
|
+
import * as queue from './render-queue.mjs';
|
|
40
41
|
|
|
41
42
|
const ESC = '\x1b[';
|
|
42
43
|
const OUT = process.stderr;
|
|
@@ -52,6 +53,7 @@ const INPUT_RIGHT_PAD = 2;
|
|
|
52
53
|
const META_INDENT = 4;
|
|
53
54
|
|
|
54
55
|
const DEFAULT_MAX_INPUT_ROWS = 6;
|
|
56
|
+
const DEFAULT_OVERLAY_MAX_ROWS = 8;
|
|
55
57
|
const MIN_INPUT_ROWS = 1;
|
|
56
58
|
const MAX_INPUT_ROWS_CAP = 12;
|
|
57
59
|
|
|
@@ -74,10 +76,40 @@ let unsubResize = null;
|
|
|
74
76
|
let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
|
|
75
77
|
let resetting = false;
|
|
76
78
|
let lastGeometry = null;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let contentCursorRow = 1;
|
|
80
|
+
let contentCursorCol = 1;
|
|
81
|
+
let contentTrackingActive = false;
|
|
82
|
+
let suppressWriteTracking = 0;
|
|
83
|
+
let originalStdoutWrite = null;
|
|
84
|
+
let originalStderrWrite = null;
|
|
85
|
+
|
|
86
|
+
function write(s) {
|
|
87
|
+
// All dock frame bytes flow through the render queue's serialized raw
|
|
88
|
+
// channel when it is active — bypassing the content redirect so frame
|
|
89
|
+
// paints never land in the transcript. Legacy fallback writes straight
|
|
90
|
+
// to stderr with the old suppress-tracking guard.
|
|
91
|
+
if (queue.isActive()) {
|
|
92
|
+
queue.raw(s);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
suppressWriteTracking++;
|
|
97
|
+
OUT.write(s);
|
|
98
|
+
} catch {
|
|
99
|
+
} finally {
|
|
100
|
+
suppressWriteTracking = Math.max(0, suppressWriteTracking - 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function setScrollRegion(top, bottom) {
|
|
104
|
+
// Keep the queue's notion of the content region in sync — its content
|
|
105
|
+
// cursor clamps to this bottom.
|
|
106
|
+
if (queue.isActive()) { queue.setRegion(top, bottom); return; }
|
|
107
|
+
write(`${ESC}${top};${bottom}r`);
|
|
108
|
+
}
|
|
109
|
+
function clearScrollRegion() {
|
|
110
|
+
if (queue.isActive()) { queue.clearRegion(); return; }
|
|
111
|
+
write(`${ESC}r`);
|
|
112
|
+
}
|
|
81
113
|
function saveCursor() { write(`${ESC}s`); }
|
|
82
114
|
function restoreCursor() { write(`${ESC}u`); }
|
|
83
115
|
function moveTo(row, col) { write(`${ESC}${row};${col}H`); }
|
|
@@ -102,6 +134,69 @@ function contentBottomRow() {
|
|
|
102
134
|
return Math.max(1, rows() - reservedRows);
|
|
103
135
|
}
|
|
104
136
|
|
|
137
|
+
function clampContentCursor() {
|
|
138
|
+
const bottom = contentBottomRow();
|
|
139
|
+
contentCursorRow = Math.max(1, Math.min(bottom, contentCursorRow || 1));
|
|
140
|
+
contentCursorCol = Math.max(1, Math.min(cols(), contentCursorCol || 1));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resetContentCursor(row = 1, col = 1) {
|
|
144
|
+
contentCursorRow = Math.max(1, Math.min(contentBottomRow(), Math.floor(row || 1)));
|
|
145
|
+
contentCursorCol = Math.max(1, Math.min(cols(), Math.floor(col || 1)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function trackContentWrite(chunk) {
|
|
149
|
+
if (!mounted || !contentTrackingActive || suppressWriteTracking > 0) return;
|
|
150
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
|
|
151
|
+
if (!text) return;
|
|
152
|
+
const clean = text
|
|
153
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
154
|
+
.replace(/\x1b[()][A-Za-z0-9]/g, '');
|
|
155
|
+
const bottom = contentBottomRow();
|
|
156
|
+
const width = Math.max(1, drawableColumns());
|
|
157
|
+
for (const ch of clean) {
|
|
158
|
+
if (ch === '\r') {
|
|
159
|
+
contentCursorCol = 1;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (ch === '\n') {
|
|
163
|
+
contentCursorRow = Math.min(bottom, contentCursorRow + 1);
|
|
164
|
+
contentCursorCol = 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
contentCursorCol++;
|
|
168
|
+
if (contentCursorCol > width) {
|
|
169
|
+
contentCursorRow = Math.min(bottom, contentCursorRow + 1);
|
|
170
|
+
contentCursorCol = 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function patchOutputTracking() {
|
|
176
|
+
if (originalStdoutWrite || originalStderrWrite) return;
|
|
177
|
+
originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
178
|
+
originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
179
|
+
process.stdout.write = function trackedStdoutWrite(chunk, ...args) {
|
|
180
|
+
trackContentWrite(chunk);
|
|
181
|
+
return originalStdoutWrite(chunk, ...args);
|
|
182
|
+
};
|
|
183
|
+
process.stderr.write = function trackedStderrWrite(chunk, ...args) {
|
|
184
|
+
trackContentWrite(chunk);
|
|
185
|
+
return originalStderrWrite(chunk, ...args);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function unpatchOutputTracking() {
|
|
190
|
+
if (originalStdoutWrite) {
|
|
191
|
+
process.stdout.write = originalStdoutWrite;
|
|
192
|
+
originalStdoutWrite = null;
|
|
193
|
+
}
|
|
194
|
+
if (originalStderrWrite) {
|
|
195
|
+
process.stderr.write = originalStderrWrite;
|
|
196
|
+
originalStderrWrite = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
105
200
|
// Row map (top → bottom of the reserved region).
|
|
106
201
|
function topRuleRow() { return contentBottomRow() + 1; }
|
|
107
202
|
function spacerAboveRow() { return topRuleRow() + 1; }
|
|
@@ -125,6 +220,23 @@ function resolveMaxInputRows(requested) {
|
|
|
125
220
|
return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
|
|
126
221
|
}
|
|
127
222
|
|
|
223
|
+
function resolveOverlayRowCap(requested = DEFAULT_OVERLAY_MAX_ROWS) {
|
|
224
|
+
const n = Number.parseInt(String(requested), 10);
|
|
225
|
+
if (!Number.isFinite(n)) return DEFAULT_OVERLAY_MAX_ROWS;
|
|
226
|
+
// Overlays (approval scripts) may need more rows than the typing cap —
|
|
227
|
+
// the user cannot approve what they cannot see. Allow up to half the
|
|
228
|
+
// terminal so the transcript stays visible; typing input keeps the
|
|
229
|
+
// tight MAX_INPUT_ROWS_CAP via normalizeInputRows above.
|
|
230
|
+
const dynamicCap = Math.max(MAX_INPUT_ROWS_CAP, Math.floor((rows() || 24) / 2));
|
|
231
|
+
return Math.max(MIN_INPUT_ROWS, Math.min(dynamicCap, n));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function overlayRowsForWrapped(wrappedLength, requestedMaxRows = DEFAULT_OVERLAY_MAX_ROWS) {
|
|
235
|
+
const rowCap = resolveOverlayRowCap(requestedMaxRows);
|
|
236
|
+
const wanted = Math.max(MIN_INPUT_ROWS, Math.floor(Number(wrappedLength) || 0));
|
|
237
|
+
return Math.min(rowCap, wanted);
|
|
238
|
+
}
|
|
239
|
+
|
|
128
240
|
// How many input rows does this (prefix + value) buffer need? Wrapped line
|
|
129
241
|
// count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
|
|
130
242
|
// takes over inside drawInputLines so at most inputRowsMax rows render.
|
|
@@ -236,6 +348,10 @@ function parkCursorAtInput() {
|
|
|
236
348
|
const prefix = lastFrame.prefix || '';
|
|
237
349
|
const value = lastFrame.value || '';
|
|
238
350
|
if (!prefix && !value) {
|
|
351
|
+
if (queue.isActive()) {
|
|
352
|
+
queue.park(inputRowStart(), INPUT_INDENT + 1);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
239
355
|
moveTo(inputRowStart(), INPUT_INDENT + 1);
|
|
240
356
|
return;
|
|
241
357
|
}
|
|
@@ -250,6 +366,7 @@ function applyLayout({ clearPrevious = false } = {}) {
|
|
|
250
366
|
const bottom = contentBottomRow();
|
|
251
367
|
setScrollRegion(1, bottom);
|
|
252
368
|
renderFrame(lastFrame);
|
|
369
|
+
clampContentCursor();
|
|
253
370
|
// On (re)mount and resize, park at input if we have one; otherwise sit at
|
|
254
371
|
// the bottom of the content region so any pending content writes flush
|
|
255
372
|
// above the dock rather than into a stale mid-frame position.
|
|
@@ -385,7 +502,11 @@ export function isInputDockMounted() {
|
|
|
385
502
|
return mounted;
|
|
386
503
|
}
|
|
387
504
|
|
|
388
|
-
export function mountInputDock({
|
|
505
|
+
export function mountInputDock({
|
|
506
|
+
inputRowsMax: requestedMax,
|
|
507
|
+
initialContentRow = 1,
|
|
508
|
+
initialContentCol = 1,
|
|
509
|
+
} = {}) {
|
|
389
510
|
const t = term();
|
|
390
511
|
if (!t.isTTY || t.plain) return false;
|
|
391
512
|
if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
|
|
@@ -395,10 +516,29 @@ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
|
|
|
395
516
|
inputRowsMax = resolveMaxInputRows(requestedMax);
|
|
396
517
|
inputRows = MIN_INPUT_ROWS;
|
|
397
518
|
reservedRows = FIXED_ROWS + inputRows;
|
|
519
|
+
resetContentCursor(initialContentRow, initialContentCol);
|
|
520
|
+
contentTrackingActive = false;
|
|
398
521
|
mounted = true;
|
|
522
|
+
// Render queue becomes the sole writer + exact cursor tracker. The
|
|
523
|
+
// legacy simulate-by-parsing patch only engages if activation is
|
|
524
|
+
// refused (shouldn't happen — mount gating matches activate gating).
|
|
525
|
+
const queued = queue.activate({
|
|
526
|
+
initialRow: contentCursorRow,
|
|
527
|
+
initialCol: contentCursorCol,
|
|
528
|
+
bottom: contentBottomRow(),
|
|
529
|
+
});
|
|
530
|
+
if (!queued) patchOutputTracking();
|
|
399
531
|
applyLayout();
|
|
400
532
|
|
|
401
|
-
unsubResize = onResize(() =>
|
|
533
|
+
unsubResize = onResize(() => {
|
|
534
|
+
if (queue.isActive()) {
|
|
535
|
+
// Terminal reflow makes any tracked position fiction — hard
|
|
536
|
+
// re-anchor to the new content-region bottom before repainting.
|
|
537
|
+
queue.reanchor({ row: contentBottomRow(), col: 1, bottom: contentBottomRow() });
|
|
538
|
+
resetContentCursor(contentBottomRow(), 1);
|
|
539
|
+
}
|
|
540
|
+
applyLayout({ clearPrevious: true });
|
|
541
|
+
});
|
|
402
542
|
process.once('exit', safeUnmount);
|
|
403
543
|
process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
|
|
404
544
|
return true;
|
|
@@ -415,6 +555,9 @@ export function unmountInputDock() {
|
|
|
415
555
|
if (unsubResize) { unsubResize(); unsubResize = null; }
|
|
416
556
|
} finally {
|
|
417
557
|
mounted = false;
|
|
558
|
+
contentTrackingActive = false;
|
|
559
|
+
queue.deactivate();
|
|
560
|
+
unpatchOutputTracking();
|
|
418
561
|
resetting = false;
|
|
419
562
|
lastGeometry = null;
|
|
420
563
|
}
|
|
@@ -424,21 +567,32 @@ function safeUnmount() { try { unmountInputDock(); } catch {} }
|
|
|
424
567
|
|
|
425
568
|
export function moveToContent() {
|
|
426
569
|
if (!mounted) return false;
|
|
427
|
-
|
|
570
|
+
if (queue.isActive()) {
|
|
571
|
+
// Content self-positions through queue.content(); nothing to do.
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
clampContentCursor();
|
|
575
|
+
contentTrackingActive = true;
|
|
576
|
+
moveTo(contentCursorRow, contentCursorCol);
|
|
428
577
|
return true;
|
|
429
578
|
}
|
|
430
579
|
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
// without piling copies of itself into scrollback.
|
|
580
|
+
// The next transcript row. Spinner/status overlays live next to the latest
|
|
581
|
+
// content instead of near the scroll-region bottom; otherwise sparse agent
|
|
582
|
+
// events leave large blank holes between visible lines.
|
|
435
583
|
export function pinnedStatusRow() {
|
|
436
584
|
if (!mounted) return null;
|
|
437
|
-
|
|
585
|
+
clampContentCursor();
|
|
586
|
+
return Math.max(1, Math.min(contentBottomRow(), contentCursorRow));
|
|
438
587
|
}
|
|
439
588
|
|
|
440
589
|
export function drawPinnedStatus(line) {
|
|
441
590
|
if (!mounted) return false;
|
|
591
|
+
if (queue.isActive()) {
|
|
592
|
+
// Coalesced, serialized, no VT100 save-slot involvement.
|
|
593
|
+
queue.status(String(line || ''));
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
442
596
|
const row = pinnedStatusRow();
|
|
443
597
|
if (row == null) return false;
|
|
444
598
|
saveCursor();
|
|
@@ -451,6 +605,10 @@ export function drawPinnedStatus(line) {
|
|
|
451
605
|
|
|
452
606
|
export function clearPinnedStatus() {
|
|
453
607
|
if (!mounted) return false;
|
|
608
|
+
if (queue.isActive()) {
|
|
609
|
+
queue.clearStatus();
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
454
612
|
const row = pinnedStatusRow();
|
|
455
613
|
if (row == null) return false;
|
|
456
614
|
saveCursor();
|
|
@@ -466,6 +624,7 @@ export function clearPinnedStatus() {
|
|
|
466
624
|
// clearing their overlay.
|
|
467
625
|
export function redrawDockFrame() {
|
|
468
626
|
if (!mounted) return false;
|
|
627
|
+
contentTrackingActive = false;
|
|
469
628
|
renderFrame(lastFrame);
|
|
470
629
|
parkCursorAtInput();
|
|
471
630
|
return true;
|
|
@@ -473,6 +632,7 @@ export function redrawDockFrame() {
|
|
|
473
632
|
|
|
474
633
|
export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
|
|
475
634
|
if (!mounted) return false;
|
|
635
|
+
contentTrackingActive = false;
|
|
476
636
|
setInputRowsTo(MIN_INPUT_ROWS);
|
|
477
637
|
clearInputRows();
|
|
478
638
|
renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
|
|
@@ -482,9 +642,11 @@ export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {})
|
|
|
482
642
|
|
|
483
643
|
export function clearInputPrompt() {
|
|
484
644
|
if (!mounted) return false;
|
|
485
|
-
|
|
645
|
+
contentTrackingActive = false;
|
|
486
646
|
lastFrame.value = '';
|
|
487
647
|
lastFrame.overlayLines = null;
|
|
648
|
+
setInputRowsTo(MIN_INPUT_ROWS);
|
|
649
|
+
clearInputRows();
|
|
488
650
|
renderFrame(lastFrame);
|
|
489
651
|
parkCursorAtInput();
|
|
490
652
|
return true;
|
|
@@ -492,6 +654,7 @@ export function clearInputPrompt() {
|
|
|
492
654
|
|
|
493
655
|
export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
|
|
494
656
|
if (!mounted) return false;
|
|
657
|
+
contentTrackingActive = false;
|
|
495
658
|
setInputRowsTo(computeInputRowsForBuffer(prefix, value));
|
|
496
659
|
renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
|
|
497
660
|
const layout = layoutInput(prefix, value);
|
|
@@ -505,13 +668,14 @@ export function renderDockOverlay({
|
|
|
505
668
|
lines = [],
|
|
506
669
|
meta = '',
|
|
507
670
|
tips = '',
|
|
508
|
-
maxRows =
|
|
671
|
+
maxRows = DEFAULT_OVERLAY_MAX_ROWS,
|
|
509
672
|
} = {}) {
|
|
510
673
|
if (!mounted) return false;
|
|
674
|
+
contentTrackingActive = false;
|
|
511
675
|
const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
|
|
512
676
|
const wrapped = layoutOverlayLines(sourceLines);
|
|
513
|
-
const rowCap =
|
|
514
|
-
setInputRowsTo(
|
|
677
|
+
const rowCap = resolveOverlayRowCap(maxRows);
|
|
678
|
+
setInputRowsTo(overlayRowsForWrapped(wrapped.length, rowCap), { maxRows: rowCap });
|
|
515
679
|
const tail = tailWithEllipsis(wrapped, inputRows);
|
|
516
680
|
renderFrame({
|
|
517
681
|
context,
|
|
@@ -537,6 +701,7 @@ export function renderDockOverlay({
|
|
|
537
701
|
*/
|
|
538
702
|
export function focusDockInput(prefix, value = '', cursorInValue = null) {
|
|
539
703
|
if (!mounted) return false;
|
|
704
|
+
contentTrackingActive = false;
|
|
540
705
|
const layout = layoutInput(prefix, value);
|
|
541
706
|
const valueStr = String(value || '');
|
|
542
707
|
const rawCursor = cursorInValue == null
|
|
@@ -551,6 +716,12 @@ export function focusDockInput(prefix, value = '', cursorInValue = null) {
|
|
|
551
716
|
);
|
|
552
717
|
const row = inputRowStart() + visibleRowIdx;
|
|
553
718
|
const col = Math.min(cols(), INPUT_INDENT + 1 + Math.max(0, pos.col));
|
|
719
|
+
if (queue.isActive()) {
|
|
720
|
+
// Record the park position — every queue op re-parks here so readline
|
|
721
|
+
// echoes always land in the input row, even mid-stream.
|
|
722
|
+
queue.park(row, col);
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
554
725
|
moveTo(row, col);
|
|
555
726
|
return true;
|
|
556
727
|
}
|
|
@@ -570,7 +741,12 @@ export function _internals() {
|
|
|
570
741
|
bottomRuleLine,
|
|
571
742
|
padLine,
|
|
572
743
|
drawableColumns,
|
|
744
|
+
resetContentCursor,
|
|
745
|
+
contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
|
|
746
|
+
overlayRowsForWrapped,
|
|
573
747
|
FIXED_ROWS,
|
|
748
|
+
MAX_INPUT_ROWS_CAP,
|
|
749
|
+
DEFAULT_OVERLAY_MAX_ROWS,
|
|
574
750
|
BRAND_LABEL,
|
|
575
751
|
};
|
|
576
752
|
}
|