@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.
@@ -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',
@@ -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 head = `${indent}${iconChar} ${paint.brand.data(t)} ${paint.text.dim(`"${query || ''}"`)}`;
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 query
81
+ return shown
67
82
  ? `\n${head} ${tag1}`
68
83
  : `\n${indent}${iconChar} ${paint.brand.data(t)} ${tag1}`;
69
84
  }