@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
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render queue — THE single writer for the rich-TTY screen.
|
|
3
|
+
*
|
|
4
|
+
* Every glitch class the old pipeline suffered (flicker, dock overlap,
|
|
5
|
+
* wrap artifacts, resize mangling, approval gaps) traced to the same root:
|
|
6
|
+
* multiple modules moved the cursor independently — spinner interval,
|
|
7
|
+
* content flush timer, dock repaints, approval prompts — with the single
|
|
8
|
+
* VT100 save/restore slot as their only (broken) coordination.
|
|
9
|
+
*
|
|
10
|
+
* Discipline (Ink's core insight, without adopting Ink):
|
|
11
|
+
* 1. Exactly ONE module writes to the terminal in rich mode. This one.
|
|
12
|
+
* 2. Every operation is a complete transaction: position → write →
|
|
13
|
+
* re-park. No saveCursor/restoreCursor anywhere.
|
|
14
|
+
* 3. Cursor position is tracked exactly — possible only because raw
|
|
15
|
+
* writes are banned. process.stdout/stderr are patched to REDIRECT
|
|
16
|
+
* through queue.content() (not merely observe), so a stray
|
|
17
|
+
* console.log from any dependency is serialized instead of
|
|
18
|
+
* corrupting the screen.
|
|
19
|
+
* 4. The transient status line (spinner) is coalesced last-wins and is
|
|
20
|
+
* always cleared before content lands, then redrawn after — content
|
|
21
|
+
* and spinner can no longer interleave.
|
|
22
|
+
*
|
|
23
|
+
* Plain / non-TTY mode: the queue degrades to a pass-through (content →
|
|
24
|
+
* stdout, status dropped, no cursor ops) so piped transcripts stay clean.
|
|
25
|
+
*
|
|
26
|
+
* Cell-width accounting: `cellWidth()` measures terminal CELLS (East
|
|
27
|
+
* Asian Wide/Fullwidth = 2, combining marks/ZWJ = 0, tabs → next stop),
|
|
28
|
+
* unlike palette.width() which counts codepoints. Wrap math that feeds
|
|
29
|
+
* cursor tracking MUST use cells, or CJK/emoji output drifts the dock.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { term, onResize } from './term.mjs';
|
|
33
|
+
|
|
34
|
+
const ESC = '\x1b[';
|
|
35
|
+
|
|
36
|
+
// ── ANSI / OSC stripping ─────────────────────────────────────────────────
|
|
37
|
+
// CSI (incl. private + intermediate bytes), OSC (BEL or ST terminated),
|
|
38
|
+
// charset selection, and simple ESC-letter sequences.
|
|
39
|
+
const ANSI_RE = new RegExp([
|
|
40
|
+
'\\x1b\\[[0-?]*[ -/]*[@-~]', // CSI
|
|
41
|
+
'\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)', // OSC ... BEL|ST
|
|
42
|
+
'\\x1b[()][A-Za-z0-9]', // charset
|
|
43
|
+
'\\x1b[@-Z\\\\-_]', // 2-byte ESC sequences
|
|
44
|
+
].join('|'), 'g');
|
|
45
|
+
|
|
46
|
+
export function stripSequences(text) {
|
|
47
|
+
return String(text ?? '').replace(ANSI_RE, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Cell width ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function isZeroWidth(cp) {
|
|
53
|
+
return (
|
|
54
|
+
(cp >= 0x0300 && cp <= 0x036f) || // combining diacriticals
|
|
55
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
56
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
57
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
58
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
59
|
+
cp === 0x200d || // ZWJ
|
|
60
|
+
cp === 0xfeff
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isWide(cp) {
|
|
65
|
+
return (
|
|
66
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
67
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals, punctuation
|
|
68
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana..CJK compat
|
|
69
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
70
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified
|
|
71
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
72
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
73
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
74
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
75
|
+
(cp >= 0xff00 && cp <= 0xff60) || // fullwidth forms
|
|
76
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
77
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji blocks
|
|
78
|
+
(cp >= 0x20000 && cp <= 0x3fffd)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Terminal cell width of `text` after stripping escape sequences. */
|
|
83
|
+
export function cellWidth(text) {
|
|
84
|
+
const plain = stripSequences(text);
|
|
85
|
+
let w = 0;
|
|
86
|
+
for (const ch of plain) {
|
|
87
|
+
const cp = ch.codePointAt(0);
|
|
88
|
+
if (cp === 0x09) { w = (Math.floor(w / 8) + 1) * 8; continue; }
|
|
89
|
+
if (cp < 0x20 || cp === 0x7f) continue;
|
|
90
|
+
if (isZeroWidth(cp)) continue;
|
|
91
|
+
w += isWide(cp) ? 2 : 1;
|
|
92
|
+
}
|
|
93
|
+
return w;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Queue state ──────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
let active = false; // rich mode engaged (dock mounted)
|
|
99
|
+
let out = process.stderr; // the one true stream in rich mode
|
|
100
|
+
let rawStderrWrite = null; // originals captured at activate()
|
|
101
|
+
let rawStdoutWrite = null;
|
|
102
|
+
|
|
103
|
+
let row = 1; // tracked content cursor (1-based)
|
|
104
|
+
let col = 1;
|
|
105
|
+
let regionBottom = null; // scroll-region bottom (content area)
|
|
106
|
+
let statusLines = []; // current transient status block ([] = none)
|
|
107
|
+
let statusPaintedRows = 0; // rows painted by the last paintStatus()
|
|
108
|
+
let statusVisible = false;
|
|
109
|
+
let parked = null; // {row, col} where input echo expects the cursor
|
|
110
|
+
let inTransaction = 0; // reentrancy guard for queue-internal writes
|
|
111
|
+
let contentEscapeCarry = ''; // incomplete ESC sequence crossing write chunks
|
|
112
|
+
|
|
113
|
+
function rawWrite(s) {
|
|
114
|
+
// Always bypass the redirect patch for queue-internal writes.
|
|
115
|
+
(rawStderrWrite || process.stderr.write.bind(process.stderr))(s);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function seq(s) { rawWrite(ESC + s); }
|
|
119
|
+
function moveTo(r, c) { seq(`${r};${c}H`); }
|
|
120
|
+
function clearLine() { seq('2K'); }
|
|
121
|
+
|
|
122
|
+
function cols() {
|
|
123
|
+
return Math.max(20, term().columns || out.columns || 80);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bottom() {
|
|
127
|
+
return regionBottom ?? Math.max(10, term().rows || 24);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Exact tracking ───────────────────────────────────────────────────────
|
|
131
|
+
// Advance the tracked (row, col) as the terminal will after printing
|
|
132
|
+
// `text`. Only printable content passes through here — queue ops position
|
|
133
|
+
// with moveTo(), never with embedded CSI in content.
|
|
134
|
+
|
|
135
|
+
function advance(text) {
|
|
136
|
+
const width = cols();
|
|
137
|
+
const plain = stripSequences(text);
|
|
138
|
+
for (const ch of plain) {
|
|
139
|
+
const cp = ch.codePointAt(0);
|
|
140
|
+
if (cp === 0x0a) { row = Math.min(bottom(), row + 1); col = 1; continue; }
|
|
141
|
+
if (cp === 0x0d) { col = 1; continue; }
|
|
142
|
+
if (cp === 0x09) { col = (Math.floor((col - 1) / 8) + 1) * 8 + 1; }
|
|
143
|
+
else if (cp < 0x20 || cp === 0x7f || isZeroWidth(cp)) { continue; }
|
|
144
|
+
else { col += isWide(cp) ? 2 : 1; }
|
|
145
|
+
if (col > width) { row = Math.min(bottom(), row + 1); col = 1; }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Status line (spinner) discipline ─────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
function eraseStatus() {
|
|
152
|
+
if (!statusVisible) return;
|
|
153
|
+
const anchor = Math.min(bottom(), row);
|
|
154
|
+
const count = Math.max(1, statusPaintedRows);
|
|
155
|
+
for (let i = 0; i < count && anchor + i <= bottom(); i++) {
|
|
156
|
+
moveTo(anchor + i, 1);
|
|
157
|
+
clearLine();
|
|
158
|
+
}
|
|
159
|
+
statusPaintedRows = 0;
|
|
160
|
+
statusVisible = false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ANSI-aware truncation to N terminal cells. Escape sequences pass
|
|
164
|
+
// through at zero width; printable chars accumulate cell width until the
|
|
165
|
+
// budget is hit. A trailing reset prevents color bleed on truncation.
|
|
166
|
+
const TOKEN_RE = new RegExp(`(${ANSI_RE.source})|([\\s\\S])`, 'g');
|
|
167
|
+
|
|
168
|
+
export function fitCells(text, maxCells) {
|
|
169
|
+
const s = String(text ?? '');
|
|
170
|
+
let out = '';
|
|
171
|
+
let w = 0;
|
|
172
|
+
let truncated = false;
|
|
173
|
+
TOKEN_RE.lastIndex = 0;
|
|
174
|
+
let m;
|
|
175
|
+
while ((m = TOKEN_RE.exec(s)) !== null) {
|
|
176
|
+
if (m[1] !== undefined) { out += m[1]; continue; }
|
|
177
|
+
const ch = m[2];
|
|
178
|
+
const cp = ch.codePointAt(0);
|
|
179
|
+
if (cp < 0x20 || cp === 0x7f) continue; // control chars never printable here
|
|
180
|
+
const cw = isZeroWidth(cp) ? 0 : (isWide(cp) ? 2 : 1);
|
|
181
|
+
if (w + cw > maxCells - 1) { truncated = true; break; }
|
|
182
|
+
out += ch;
|
|
183
|
+
w += cw;
|
|
184
|
+
}
|
|
185
|
+
if (truncated) out += '…\x1b[0m';
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function paintStatus() {
|
|
190
|
+
if (!statusLines.length) return;
|
|
191
|
+
const anchor = Math.min(bottom(), row);
|
|
192
|
+
const width = Math.max(8, cols() - 1);
|
|
193
|
+
let painted = 0;
|
|
194
|
+
for (const line of statusLines) {
|
|
195
|
+
const r = anchor + painted;
|
|
196
|
+
if (r > bottom()) break;
|
|
197
|
+
moveTo(r, 1);
|
|
198
|
+
clearLine();
|
|
199
|
+
// Clamp to the drawable width — a status line that reaches the final
|
|
200
|
+
// column triggers terminal autowrap, which scrolls the region and
|
|
201
|
+
// turns the "transient" line into permanent transcript content (one
|
|
202
|
+
// leaked line per animation frame). Newlines are equally forbidden.
|
|
203
|
+
rawWrite(fitCells(String(line).replace(/[\r\n]+/g, ' '), width));
|
|
204
|
+
painted++;
|
|
205
|
+
}
|
|
206
|
+
statusPaintedRows = painted;
|
|
207
|
+
statusVisible = painted > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function repark() {
|
|
211
|
+
if (parked) moveTo(parked.row, parked.col);
|
|
212
|
+
else moveTo(Math.min(bottom(), row), col);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Public API ───────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Engage rich mode. The queue captures both std streams and becomes the
|
|
219
|
+
* sole writer. `initial` seeds the tracked cursor (dock mount computes it).
|
|
220
|
+
*/
|
|
221
|
+
export function activate({ initialRow = 1, initialCol = 1, bottom: b = null } = {}) {
|
|
222
|
+
if (active) return true;
|
|
223
|
+
const t = term();
|
|
224
|
+
if (!t.isTTY || t.plain) return false;
|
|
225
|
+
active = true;
|
|
226
|
+
row = Math.max(1, initialRow);
|
|
227
|
+
col = Math.max(1, initialCol);
|
|
228
|
+
regionBottom = b;
|
|
229
|
+
rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
230
|
+
rawStderrWrite = process.stderr.write.bind(process.stderr);
|
|
231
|
+
// REDIRECT, don't observe: stray writers become serialized content.
|
|
232
|
+
process.stdout.write = redirectedWrite;
|
|
233
|
+
process.stderr.write = redirectedWrite;
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function deactivate() {
|
|
238
|
+
if (!active) return;
|
|
239
|
+
if (rawStdoutWrite) { process.stdout.write = rawStdoutWrite; rawStdoutWrite = null; }
|
|
240
|
+
if (rawStderrWrite) { process.stderr.write = rawStderrWrite; rawStderrWrite = null; }
|
|
241
|
+
active = false;
|
|
242
|
+
regionBottom = null;
|
|
243
|
+
statusLines = [];
|
|
244
|
+
statusPaintedRows = 0;
|
|
245
|
+
statusVisible = false;
|
|
246
|
+
parked = null;
|
|
247
|
+
contentEscapeCarry = '';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function isActive() { return active; }
|
|
251
|
+
|
|
252
|
+
function redirectedWrite(chunk, encoding, cb) {
|
|
253
|
+
if (inTransaction > 0) {
|
|
254
|
+
// Queue-internal writes that (incorrectly) went through the patched
|
|
255
|
+
// stream — pass straight through.
|
|
256
|
+
rawWrite(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? ''));
|
|
257
|
+
} else {
|
|
258
|
+
content(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? ''));
|
|
259
|
+
}
|
|
260
|
+
if (typeof encoding === 'function') encoding();
|
|
261
|
+
else if (typeof cb === 'function') cb();
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Content is TEXT, not cursor commands (Ink's discipline). SGR color
|
|
266
|
+
// sequences pass through; cursor movement / erase / scroll sequences are
|
|
267
|
+
// stripped — a content writer that embeds CUU/ED/DECSTBM would move the
|
|
268
|
+
// real cursor without tracking following (the old approval-redraw gap
|
|
269
|
+
// bug). Positioning belongs to the queue alone.
|
|
270
|
+
function sanitizeContent(s) {
|
|
271
|
+
const input = contentEscapeCarry + String(s ?? '');
|
|
272
|
+
contentEscapeCarry = '';
|
|
273
|
+
let out = '';
|
|
274
|
+
|
|
275
|
+
for (let i = 0; i < input.length;) {
|
|
276
|
+
const ch = input[i];
|
|
277
|
+
if (ch !== '\x1b') {
|
|
278
|
+
out += ch;
|
|
279
|
+
i++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (i + 1 >= input.length) {
|
|
284
|
+
contentEscapeCarry = input.slice(i);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const next = input[i + 1];
|
|
289
|
+
|
|
290
|
+
// CSI: ESC [ params/intermediates final. Preserve only SGR (`m`).
|
|
291
|
+
if (next === '[') {
|
|
292
|
+
let j = i + 2;
|
|
293
|
+
while (j < input.length) {
|
|
294
|
+
const code = input.charCodeAt(j);
|
|
295
|
+
if (code >= 0x40 && code <= 0x7e) break;
|
|
296
|
+
j++;
|
|
297
|
+
}
|
|
298
|
+
if (j >= input.length) {
|
|
299
|
+
contentEscapeCarry = input.slice(i);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
const seqText = input.slice(i, j + 1);
|
|
303
|
+
if (input[j] === 'm') out += seqText;
|
|
304
|
+
i = j + 1;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// OSC: ESC ] ... BEL or ST. Strip, buffering incomplete sequences.
|
|
309
|
+
if (next === ']') {
|
|
310
|
+
let j = i + 2;
|
|
311
|
+
let completeAt = -1;
|
|
312
|
+
while (j < input.length) {
|
|
313
|
+
if (input[j] === '\x07') { completeAt = j; break; }
|
|
314
|
+
if (input[j] === '\x1b' && input[j + 1] === '\\') { completeAt = j + 1; break; }
|
|
315
|
+
j++;
|
|
316
|
+
}
|
|
317
|
+
if (completeAt < 0) {
|
|
318
|
+
contentEscapeCarry = input.slice(i);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
i = completeAt + 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Charset selection: ESC ( X / ESC ) X. Strip, buffering if split.
|
|
326
|
+
if (next === '(' || next === ')') {
|
|
327
|
+
if (i + 2 >= input.length) {
|
|
328
|
+
contentEscapeCarry = input.slice(i);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
i += 3;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Other two-byte ESC commands. Strip the ESC command byte too.
|
|
336
|
+
i += 2;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Append transcript content at the tracked position. Multi-line safe.
|
|
344
|
+
* Clears the status line first and repaints it after, so spinner and
|
|
345
|
+
* content can never interleave.
|
|
346
|
+
*/
|
|
347
|
+
export function content(text) {
|
|
348
|
+
const s = String(text ?? '');
|
|
349
|
+
if (!s) return;
|
|
350
|
+
if (!active) { (rawStdoutWrite || process.stdout.write.bind(process.stdout))(s); return; }
|
|
351
|
+
const clean = sanitizeContent(s);
|
|
352
|
+
if (!clean) return;
|
|
353
|
+
inTransaction++;
|
|
354
|
+
try {
|
|
355
|
+
eraseStatus();
|
|
356
|
+
moveTo(Math.min(bottom(), row), col);
|
|
357
|
+
rawWrite(clean);
|
|
358
|
+
advance(clean);
|
|
359
|
+
paintStatus();
|
|
360
|
+
repark();
|
|
361
|
+
} finally {
|
|
362
|
+
inTransaction--;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Set / update the transient status block. Last-wins; a repaint happens
|
|
368
|
+
* only when the rendered content actually changed. Single-line callers
|
|
369
|
+
* use status(line); multi-line (sub-agent tool window) use
|
|
370
|
+
* statusBlock(lines). Rows are clamped to the space between the content
|
|
371
|
+
* cursor and the region bottom.
|
|
372
|
+
*/
|
|
373
|
+
export function statusBlock(lines) {
|
|
374
|
+
if (!active) return;
|
|
375
|
+
const next = (Array.isArray(lines) ? lines : [lines])
|
|
376
|
+
.map(l => String(l ?? ''))
|
|
377
|
+
.filter((l, i) => l !== '' || i === 0);
|
|
378
|
+
const isEmpty = !next.length || (next.length === 1 && !next[0]);
|
|
379
|
+
const same = statusVisible
|
|
380
|
+
&& next.length === statusLines.length
|
|
381
|
+
&& next.every((l, i) => l === statusLines[i]);
|
|
382
|
+
if (same) return;
|
|
383
|
+
inTransaction++;
|
|
384
|
+
try {
|
|
385
|
+
eraseStatus();
|
|
386
|
+
statusLines = isEmpty ? [] : next;
|
|
387
|
+
if (!isEmpty) paintStatus();
|
|
388
|
+
repark();
|
|
389
|
+
} finally {
|
|
390
|
+
inTransaction--;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function status(line) {
|
|
395
|
+
statusBlock([String(line ?? '')]);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function clearStatus() { statusBlock([]); }
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Absolute-positioned write for dock frame rows. Does NOT touch content
|
|
402
|
+
* tracking. `clear` wipes the line first.
|
|
403
|
+
*/
|
|
404
|
+
export function at(r, c, text, { clear = true } = {}) {
|
|
405
|
+
if (!active) return;
|
|
406
|
+
inTransaction++;
|
|
407
|
+
try {
|
|
408
|
+
moveTo(r, c);
|
|
409
|
+
if (clear) clearLine();
|
|
410
|
+
if (text) rawWrite(String(text));
|
|
411
|
+
} finally {
|
|
412
|
+
inTransaction--;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Set the scroll region (content area 1..b). */
|
|
417
|
+
export function setRegion(top, b) {
|
|
418
|
+
if (!active) return;
|
|
419
|
+
inTransaction++;
|
|
420
|
+
try {
|
|
421
|
+
seq(`${top};${b}r`);
|
|
422
|
+
regionBottom = b;
|
|
423
|
+
row = Math.min(b, row);
|
|
424
|
+
} finally {
|
|
425
|
+
inTransaction--;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function clearRegion() {
|
|
430
|
+
if (!active) return;
|
|
431
|
+
inTransaction++;
|
|
432
|
+
try { seq('r'); regionBottom = null; } finally { inTransaction--; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Batch several at()/setRegion() calls (a dock repaint) and finish with
|
|
437
|
+
* the cursor parked for input echo. Single place where parking happens.
|
|
438
|
+
*/
|
|
439
|
+
export function frame(fn, { park = null } = {}) {
|
|
440
|
+
if (!active) { if (typeof fn === 'function') fn(); return; }
|
|
441
|
+
inTransaction++;
|
|
442
|
+
try {
|
|
443
|
+
if (typeof fn === 'function') fn();
|
|
444
|
+
if (park) parked = { row: park.row, col: park.col };
|
|
445
|
+
repark();
|
|
446
|
+
} finally {
|
|
447
|
+
inTransaction--;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Serialized raw write for trusted frame painters (the input dock). The
|
|
453
|
+
* caller owns positioning via embedded escape sequences; content tracking
|
|
454
|
+
* is not touched. Bypasses the redirect patch.
|
|
455
|
+
*/
|
|
456
|
+
export function raw(s) {
|
|
457
|
+
if (!active) { (rawStderrWrite || process.stderr.write.bind(process.stderr))(String(s ?? '')); return; }
|
|
458
|
+
inTransaction++;
|
|
459
|
+
try { rawWrite(String(s ?? '')); } finally { inTransaction--; }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Park the input-echo cursor. Every op re-parks here afterwards. */
|
|
463
|
+
export function park(r, c) {
|
|
464
|
+
if (!active) return;
|
|
465
|
+
parked = r == null ? null : { row: r, col: c ?? 1 };
|
|
466
|
+
inTransaction++;
|
|
467
|
+
try { repark(); } finally { inTransaction--; }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Tracked content cursor (exact — no simulation drift). */
|
|
471
|
+
export function contentCursor() {
|
|
472
|
+
return { row, col };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Hard re-anchor after resize: tracking through a reflow is fiction. */
|
|
476
|
+
export function reanchor({ row: r, col: c = 1, bottom: b = null } = {}) {
|
|
477
|
+
if (!active) return;
|
|
478
|
+
if (b != null) regionBottom = b;
|
|
479
|
+
row = Math.max(1, Math.min(bottom(), r ?? bottom()));
|
|
480
|
+
col = Math.max(1, c);
|
|
481
|
+
statusVisible = false; // old status row is gone after reflow
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Reset tracked cursor without touching the screen (dock mount). */
|
|
485
|
+
export function seed({ row: r = 1, col: c = 1, bottom: b = null } = {}) {
|
|
486
|
+
row = Math.max(1, r);
|
|
487
|
+
col = Math.max(1, c);
|
|
488
|
+
if (b != null) regionBottom = b;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Test-only accessors.
|
|
492
|
+
export function _internals() {
|
|
493
|
+
return {
|
|
494
|
+
state: () => ({ active, row, col, regionBottom, statusLines, statusPaintedRows, statusVisible, parked }),
|
|
495
|
+
advance,
|
|
496
|
+
cellWidth,
|
|
497
|
+
stripSequences,
|
|
498
|
+
sanitizeContent,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
@@ -50,6 +50,8 @@ export const COMMANDS = {
|
|
|
50
50
|
'/review': 'Code review agent',
|
|
51
51
|
'/architect': 'Feature architect agent',
|
|
52
52
|
'/safety': 'Show safety guardrail status',
|
|
53
|
+
'/auto': 'Session autopilot: auto-approve routine tools (dangerous still prompts)',
|
|
54
|
+
'/approvals': 'List or edit session approval grants',
|
|
53
55
|
'/revoke': 'Revoke auto-approvals',
|
|
54
56
|
'/resume': 'Resume a previous session',
|
|
55
57
|
'/sessions': 'List resumable sessions',
|
package/src/ui/sub-agent.mjs
CHANGED
|
@@ -53,6 +53,20 @@ export function subAgentIndent(extraDepth = 0) {
|
|
|
53
53
|
*
|
|
54
54
|
* @returns {string} ANSI-styled multi-line block (no trailing newline).
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* Reduce a sub-agent query to its human-readable core. Handoff envelopes
|
|
58
|
+
* ([User intent], ## Work Scope, Schema:, Active roots:) are machine
|
|
59
|
+
* context — printing them flooded the transcript with 15+ lines per
|
|
60
|
+
* spawn. Full text stays available via /expand on the recorded card.
|
|
61
|
+
*/
|
|
62
|
+
export function displayQuery(query, max = 140) {
|
|
63
|
+
let s = String(query || '');
|
|
64
|
+
const cut = s.search(/\n\s*(?:\[User intent\]|##\s*Work Scope|Schema:\s*kepler\.|Active roots:)/);
|
|
65
|
+
if (cut >= 0) s = s.slice(0, cut);
|
|
66
|
+
s = s.replace(/^\[Thoroughness:\s*[^\]]*\]\s*/i, '').replace(/\s+/g, ' ').trim();
|
|
67
|
+
return truncate(s, max);
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
|
|
57
71
|
const t = type || 'sub-agent';
|
|
58
72
|
const depthBefore = _stack.length;
|
|
@@ -60,10 +74,11 @@ export function renderSubAgentOpen({ id, type, query, parentDepth } = {}) {
|
|
|
60
74
|
|
|
61
75
|
const indent = ' '.repeat(2 + depthBefore * 3);
|
|
62
76
|
const iconChar = SUB_ICONS[t] || icons.subAgent;
|
|
63
|
-
const
|
|
77
|
+
const shown = displayQuery(query);
|
|
78
|
+
const head = `${indent}${iconChar} ${paint.brand.data(t)} ${paint.text.dim(`"${shown}"`)}`;
|
|
64
79
|
const tag1 = paint.text.dim('▸ running');
|
|
65
80
|
|
|
66
|
-
return
|
|
81
|
+
return shown
|
|
67
82
|
? `\n${head} ${tag1}`
|
|
68
83
|
: `\n${indent}${iconChar} ${paint.brand.data(t)} ${tag1}`;
|
|
69
84
|
}
|