@bahulam/code 2.6.13 → 2.6.15

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.
@@ -52,6 +52,7 @@ const INPUT_RIGHT_PAD = 2;
52
52
  const META_INDENT = 4;
53
53
 
54
54
  const DEFAULT_MAX_INPUT_ROWS = 6;
55
+ const DEFAULT_OVERLAY_MAX_ROWS = 8;
55
56
  const MIN_INPUT_ROWS = 1;
56
57
  const MAX_INPUT_ROWS_CAP = 12;
57
58
 
@@ -71,10 +72,25 @@ let unsubResize = null;
71
72
  // the cursor at the correct input position. Without this, readline echoes
72
73
  // land on rows the dock briefly moved through mid-render and characters
73
74
  // appear above/below the input row.
74
- let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null };
75
+ let lastFrame = { context: '', meta: '', tips: '', prefix: '', value: '', cursor: null, overlayLines: null };
75
76
  let resetting = false;
76
-
77
- function write(s) { try { OUT.write(s); } catch {} }
77
+ let lastGeometry = null;
78
+ let contentCursorRow = 1;
79
+ let contentCursorCol = 1;
80
+ let contentTrackingActive = false;
81
+ let suppressWriteTracking = 0;
82
+ let originalStdoutWrite = null;
83
+ let originalStderrWrite = null;
84
+
85
+ function write(s) {
86
+ try {
87
+ suppressWriteTracking++;
88
+ OUT.write(s);
89
+ } catch {
90
+ } finally {
91
+ suppressWriteTracking = Math.max(0, suppressWriteTracking - 1);
92
+ }
93
+ }
78
94
  function setScrollRegion(top, bottom) { write(`${ESC}${top};${bottom}r`); }
79
95
  function clearScrollRegion() { write(`${ESC}r`); }
80
96
  function saveCursor() { write(`${ESC}s`); }
@@ -90,10 +106,80 @@ function cols() {
90
106
  return Math.max(40, term().columns || 80);
91
107
  }
92
108
 
109
+ function drawableColumns() {
110
+ // Avoid writing into the final terminal column. Many terminals enter
111
+ // autowrap state there; during rapid resizes that can push fixed dock
112
+ // frame lines into scrollback.
113
+ return Math.max(1, cols() - 1);
114
+ }
115
+
93
116
  function contentBottomRow() {
94
117
  return Math.max(1, rows() - reservedRows);
95
118
  }
96
119
 
120
+ function clampContentCursor() {
121
+ const bottom = contentBottomRow();
122
+ contentCursorRow = Math.max(1, Math.min(bottom, contentCursorRow || 1));
123
+ contentCursorCol = Math.max(1, Math.min(cols(), contentCursorCol || 1));
124
+ }
125
+
126
+ function resetContentCursor(row = 1, col = 1) {
127
+ contentCursorRow = Math.max(1, Math.min(contentBottomRow(), Math.floor(row || 1)));
128
+ contentCursorCol = Math.max(1, Math.min(cols(), Math.floor(col || 1)));
129
+ }
130
+
131
+ function trackContentWrite(chunk) {
132
+ if (!mounted || !contentTrackingActive || suppressWriteTracking > 0) return;
133
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? '');
134
+ if (!text) return;
135
+ const clean = text
136
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
137
+ .replace(/\x1b[()][A-Za-z0-9]/g, '');
138
+ const bottom = contentBottomRow();
139
+ const width = Math.max(1, drawableColumns());
140
+ for (const ch of clean) {
141
+ if (ch === '\r') {
142
+ contentCursorCol = 1;
143
+ continue;
144
+ }
145
+ if (ch === '\n') {
146
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
147
+ contentCursorCol = 1;
148
+ continue;
149
+ }
150
+ contentCursorCol++;
151
+ if (contentCursorCol > width) {
152
+ contentCursorRow = Math.min(bottom, contentCursorRow + 1);
153
+ contentCursorCol = 1;
154
+ }
155
+ }
156
+ }
157
+
158
+ function patchOutputTracking() {
159
+ if (originalStdoutWrite || originalStderrWrite) return;
160
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
161
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
162
+ process.stdout.write = function trackedStdoutWrite(chunk, ...args) {
163
+ trackContentWrite(chunk);
164
+ return originalStdoutWrite(chunk, ...args);
165
+ };
166
+ process.stderr.write = function trackedStderrWrite(chunk, ...args) {
167
+ trackContentWrite(chunk);
168
+ return originalStderrWrite(chunk, ...args);
169
+ };
170
+ }
171
+
172
+ function unpatchOutputTracking() {
173
+ if (originalStdoutWrite) {
174
+ process.stdout.write = originalStdoutWrite;
175
+ originalStdoutWrite = null;
176
+ }
177
+ if (originalStderrWrite) {
178
+ process.stderr.write = originalStderrWrite;
179
+ originalStderrWrite = null;
180
+ }
181
+ }
182
+
97
183
  // Row map (top → bottom of the reserved region).
98
184
  function topRuleRow() { return contentBottomRow() + 1; }
99
185
  function spacerAboveRow() { return topRuleRow() + 1; }
@@ -117,6 +203,18 @@ function resolveMaxInputRows(requested) {
117
203
  return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
118
204
  }
119
205
 
206
+ function resolveOverlayRowCap(requested = DEFAULT_OVERLAY_MAX_ROWS) {
207
+ const n = Number.parseInt(String(requested), 10);
208
+ if (!Number.isFinite(n)) return DEFAULT_OVERLAY_MAX_ROWS;
209
+ return Math.max(MIN_INPUT_ROWS, Math.min(MAX_INPUT_ROWS_CAP, n));
210
+ }
211
+
212
+ function overlayRowsForWrapped(wrappedLength, requestedMaxRows = DEFAULT_OVERLAY_MAX_ROWS) {
213
+ const rowCap = resolveOverlayRowCap(requestedMaxRows);
214
+ const wanted = Math.max(MIN_INPUT_ROWS, Math.floor(Number(wrappedLength) || 0));
215
+ return Math.min(rowCap, wanted);
216
+ }
217
+
120
218
  // How many input rows does this (prefix + value) buffer need? Wrapped line
121
219
  // count clamped to [1, inputRowsMax]. Beyond the cap, tail-with-ellipsis
122
220
  // takes over inside drawInputLines so at most inputRowsMax rows render.
@@ -137,8 +235,8 @@ function computeInputRowsForBuffer(prefix, value) {
137
235
  // meta line) which then leaks into the transcript as streamed content
138
236
  // scrolls past them. We clear the old dock region BEFORE moving the frame
139
237
  // so the freed rows are blank when they enter the scroll region.
140
- function setInputRowsTo(nextRows) {
141
- const clamped = Math.max(MIN_INPUT_ROWS, Math.min(inputRowsMax, Math.floor(nextRows)));
238
+ function setInputRowsTo(nextRows, { maxRows = inputRowsMax } = {}) {
239
+ const clamped = Math.max(MIN_INPUT_ROWS, Math.min(maxRows, Math.floor(nextRows)));
142
240
  if (clamped === inputRows) return false;
143
241
  const shrinking = clamped < inputRows;
144
242
  if (mounted && shrinking) {
@@ -162,7 +260,7 @@ function setInputRowsTo(nextRows) {
162
260
 
163
261
  function padLine(text) {
164
262
  const value = String(text || '');
165
- const pad = Math.max(0, cols() - visibleWidth(value));
263
+ const pad = Math.max(0, drawableColumns() - visibleWidth(value));
166
264
  return value + ' '.repeat(pad);
167
265
  }
168
266
 
@@ -183,7 +281,7 @@ function ruleChars(count) {
183
281
  // - Optional right-aligned context strip (session tokens/elapsed) with rule
184
282
  // fill between label and context
185
283
  function topRuleLine(context = '') {
186
- const w = Math.max(0, cols() - 1);
284
+ const w = Math.max(0, drawableColumns());
187
285
  const brand = paint.brand.primary;
188
286
  const bold = paint.bold;
189
287
 
@@ -192,14 +290,19 @@ function topRuleLine(context = '') {
192
290
  const label = bold(brand(BRAND_LABEL));
193
291
  const leftBlock = `${leadRule} ${accent}${label}`;
194
292
  const leftWidth = visibleWidth(leftBlock);
293
+ const rightPadRule = 2;
195
294
 
196
- const ctx = fitText(context, Math.max(0, Math.floor(w / 2)));
295
+ const maxCtxWidth = Math.max(0, Math.min(
296
+ Math.floor(w / 2),
297
+ w - leftWidth - 1 /* left gap */ - 1 /* minimum middle */ - 1 /* ctx gap */ - rightPadRule - 1 /* tail gap */,
298
+ ));
299
+ const ctx = fitText(context, maxCtxWidth);
197
300
  const ctxWidth = ctx ? visibleWidth(ctx) : 0;
198
301
 
199
302
  // Rule fill in the middle. Reserve 1 space around ctx when present.
200
- const gap = 1;
201
- const rightPadRule = 2;
202
- const middleWidth = Math.max(1, w - leftWidth - gap - ctxWidth - (ctx ? gap + rightPadRule : 0));
303
+ const middleWidth = ctx
304
+ ? Math.max(1, w - leftWidth - ctxWidth - rightPadRule - 3)
305
+ : Math.max(1, w - leftWidth - 1);
203
306
  const middleRule = brand(ruleChars(middleWidth));
204
307
 
205
308
  if (!ctx) {
@@ -210,7 +313,7 @@ function topRuleLine(context = '') {
210
313
  }
211
314
 
212
315
  function bottomRuleLine() {
213
- return paint.brand.primary(ruleChars(Math.max(0, cols() - 1)));
316
+ return paint.brand.primary(ruleChars(Math.max(0, drawableColumns())));
214
317
  }
215
318
 
216
319
  // Always park the cursor at the tracked (prefix, value) input position.
@@ -229,11 +332,15 @@ function parkCursorAtInput() {
229
332
  focusDockInput(prefix, value, lastFrame.cursor);
230
333
  }
231
334
 
232
- function applyLayout() {
335
+ function applyLayout({ clearPrevious = false } = {}) {
233
336
  if (!mounted) return;
337
+ if (clearPrevious && lastGeometry) {
338
+ clearDockArea({ restore: true, geometry: lastGeometry });
339
+ }
234
340
  const bottom = contentBottomRow();
235
341
  setScrollRegion(1, bottom);
236
342
  renderFrame(lastFrame);
343
+ clampContentCursor();
237
344
  // On (re)mount and resize, park at input if we have one; otherwise sit at
238
345
  // the bottom of the content region so any pending content writes flush
239
346
  // above the dock rather than into a stale mid-frame position.
@@ -257,6 +364,9 @@ function renderFrame(frame = {}) {
257
364
  clearLine();
258
365
 
259
366
  // (input rows are written by drawInputLines / clearInputRows)
367
+ if (Array.isArray(lastFrame.overlayLines)) {
368
+ drawInputLines(lastFrame.overlayLines);
369
+ }
260
370
 
261
371
  // Spacer below input.
262
372
  moveTo(spacerBelowRow(), 1);
@@ -290,6 +400,20 @@ function renderFrame(frame = {}) {
290
400
  // pinned-status writers clobber the outer save and restore to the wrong
291
401
  // place. Instead, callers that need the cursor parked at the input row
292
402
  // invoke parkCursorAtInput() after renderFrame returns.
403
+ lastGeometry = {
404
+ top: topRuleRow(),
405
+ bottom: rows(),
406
+ };
407
+ }
408
+
409
+ function clearDockRows(startRow, endRow) {
410
+ const maxRow = rows();
411
+ const start = Math.max(1, Math.min(maxRow, Math.floor(startRow || 1)));
412
+ const end = Math.max(start, Math.min(maxRow, Math.floor(endRow || maxRow)));
413
+ for (let row = start; row <= end; row++) {
414
+ moveTo(row, 1);
415
+ clearLine();
416
+ }
293
417
  }
294
418
 
295
419
  function clearInputRows() {
@@ -299,13 +423,10 @@ function clearInputRows() {
299
423
  }
300
424
  }
301
425
 
302
- export function clearDockArea({ restore = true } = {}) {
426
+ export function clearDockArea({ restore = true, geometry = null } = {}) {
303
427
  if (!mounted) return false;
304
428
  if (restore) saveCursor();
305
- for (let row = topRuleRow(); row <= rows(); row++) {
306
- moveTo(row, 1);
307
- clearLine();
308
- }
429
+ clearDockRows(geometry?.top || topRuleRow(), geometry?.bottom || rows());
309
430
  if (restore) restoreCursor();
310
431
  return true;
311
432
  }
@@ -329,6 +450,15 @@ function layoutInput(prefix, value) {
329
450
  };
330
451
  }
331
452
 
453
+ function layoutOverlayLines(lines) {
454
+ const budget = inputTextBudget();
455
+ const wrapped = [];
456
+ for (const line of lines) {
457
+ wrapped.push(...wrapToLines(line, budget));
458
+ }
459
+ return wrapped.length ? wrapped : [''];
460
+ }
461
+
332
462
  function drawInputLines(lines) {
333
463
  const indent = ' '.repeat(INPUT_INDENT);
334
464
  for (let i = 0; i < inputRows; i++) {
@@ -346,7 +476,11 @@ export function isInputDockMounted() {
346
476
  return mounted;
347
477
  }
348
478
 
349
- export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
479
+ export function mountInputDock({
480
+ inputRowsMax: requestedMax,
481
+ initialContentRow = 1,
482
+ initialContentCol = 1,
483
+ } = {}) {
350
484
  const t = term();
351
485
  if (!t.isTTY || t.plain) return false;
352
486
  if (t.ttyMode !== 'rich' || t.fixedInput === false) return false;
@@ -356,10 +490,13 @@ export function mountInputDock({ inputRowsMax: requestedMax } = {}) {
356
490
  inputRowsMax = resolveMaxInputRows(requestedMax);
357
491
  inputRows = MIN_INPUT_ROWS;
358
492
  reservedRows = FIXED_ROWS + inputRows;
493
+ resetContentCursor(initialContentRow, initialContentCol);
494
+ contentTrackingActive = false;
359
495
  mounted = true;
496
+ patchOutputTracking();
360
497
  applyLayout();
361
498
 
362
- unsubResize = onResize(() => applyLayout());
499
+ unsubResize = onResize(() => applyLayout({ clearPrevious: true }));
363
500
  process.once('exit', safeUnmount);
364
501
  process.once('SIGTERM', () => { safeUnmount(); process.exit(143); });
365
502
  return true;
@@ -376,7 +513,10 @@ export function unmountInputDock() {
376
513
  if (unsubResize) { unsubResize(); unsubResize = null; }
377
514
  } finally {
378
515
  mounted = false;
516
+ contentTrackingActive = false;
517
+ unpatchOutputTracking();
379
518
  resetting = false;
519
+ lastGeometry = null;
380
520
  }
381
521
  }
382
522
 
@@ -384,17 +524,19 @@ function safeUnmount() { try { unmountInputDock(); } catch {} }
384
524
 
385
525
  export function moveToContent() {
386
526
  if (!mounted) return false;
387
- moveTo(contentBottomRow(), 1);
527
+ clampContentCursor();
528
+ contentTrackingActive = true;
529
+ moveTo(contentCursorRow, contentCursorCol);
388
530
  return true;
389
531
  }
390
532
 
391
- // One row above the bottom of the scroll region. Writes here won't trigger
392
- // the scroll-on-LF that happens when writing '\n' at the region's bottom row
393
- // exactly what the persistent explore/spinner line needs to overwrite in place
394
- // without piling copies of itself into scrollback.
533
+ // The next transcript row. Spinner/status overlays live next to the latest
534
+ // content instead of near the scroll-region bottom; otherwise sparse agent
535
+ // events leave large blank holes between visible lines.
395
536
  export function pinnedStatusRow() {
396
537
  if (!mounted) return null;
397
- return Math.max(1, contentBottomRow() - 1);
538
+ clampContentCursor();
539
+ return Math.max(1, Math.min(contentBottomRow(), contentCursorRow));
398
540
  }
399
541
 
400
542
  export function drawPinnedStatus(line) {
@@ -426,6 +568,7 @@ export function clearPinnedStatus() {
426
568
  // clearing their overlay.
427
569
  export function redrawDockFrame() {
428
570
  if (!mounted) return false;
571
+ contentTrackingActive = false;
429
572
  renderFrame(lastFrame);
430
573
  parkCursorAtInput();
431
574
  return true;
@@ -433,17 +576,21 @@ export function redrawDockFrame() {
433
576
 
434
577
  export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
435
578
  if (!mounted) return false;
579
+ contentTrackingActive = false;
436
580
  setInputRowsTo(MIN_INPUT_ROWS);
437
581
  clearInputRows();
438
- renderFrame({ context, tips, meta, prefix: '', value: '' });
582
+ renderFrame({ context, tips, meta, prefix: '', value: '', overlayLines: null });
439
583
  moveTo(inputRowStart(), INPUT_INDENT + 1);
440
584
  return true;
441
585
  }
442
586
 
443
587
  export function clearInputPrompt() {
444
588
  if (!mounted) return false;
445
- clearInputRows();
589
+ contentTrackingActive = false;
446
590
  lastFrame.value = '';
591
+ lastFrame.overlayLines = null;
592
+ setInputRowsTo(MIN_INPUT_ROWS);
593
+ clearInputRows();
447
594
  renderFrame(lastFrame);
448
595
  parkCursorAtInput();
449
596
  return true;
@@ -451,14 +598,42 @@ export function clearInputPrompt() {
451
598
 
452
599
  export function renderDockInput(prefix, value, { context = '', tips = '', meta = '', cursor = null } = {}) {
453
600
  if (!mounted) return false;
601
+ contentTrackingActive = false;
454
602
  setInputRowsTo(computeInputRowsForBuffer(prefix, value));
455
- renderFrame({ context, tips, meta, prefix, value, cursor });
603
+ renderFrame({ context, tips, meta, prefix, value, cursor, overlayLines: null });
456
604
  const layout = layoutInput(prefix, value);
457
605
  drawInputLines(layout.lines);
458
606
  focusDockInput(prefix, value, cursor);
459
607
  return true;
460
608
  }
461
609
 
610
+ export function renderDockOverlay({
611
+ context = '',
612
+ lines = [],
613
+ meta = '',
614
+ tips = '',
615
+ maxRows = DEFAULT_OVERLAY_MAX_ROWS,
616
+ } = {}) {
617
+ if (!mounted) return false;
618
+ contentTrackingActive = false;
619
+ const sourceLines = Array.isArray(lines) ? lines : String(lines || '').split('\n');
620
+ const wrapped = layoutOverlayLines(sourceLines);
621
+ const rowCap = resolveOverlayRowCap(maxRows);
622
+ setInputRowsTo(overlayRowsForWrapped(wrapped.length, rowCap), { maxRows: rowCap });
623
+ const tail = tailWithEllipsis(wrapped, inputRows);
624
+ renderFrame({
625
+ context,
626
+ meta,
627
+ tips,
628
+ prefix: '',
629
+ value: '',
630
+ cursor: null,
631
+ overlayLines: tail.visible,
632
+ });
633
+ moveTo(rows(), 1);
634
+ return true;
635
+ }
636
+
462
637
  /**
463
638
  * Move the terminal cursor to the position that corresponds to
464
639
  * `prefix + value[0..cursorInValue]` within the (possibly wrapped and
@@ -470,6 +645,7 @@ export function renderDockInput(prefix, value, { context = '', tips = '', meta =
470
645
  */
471
646
  export function focusDockInput(prefix, value = '', cursorInValue = null) {
472
647
  if (!mounted) return false;
648
+ contentTrackingActive = false;
473
649
  const layout = layoutInput(prefix, value);
474
650
  const valueStr = String(value || '');
475
651
  const rawCursor = cursorInValue == null
@@ -499,7 +675,16 @@ export function _internals() {
499
675
  reservedRows: () => reservedRows,
500
676
  layoutInput,
501
677
  inputTextBudget,
678
+ topRuleLine,
679
+ bottomRuleLine,
680
+ padLine,
681
+ drawableColumns,
682
+ resetContentCursor,
683
+ contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
684
+ overlayRowsForWrapped,
502
685
  FIXED_ROWS,
686
+ MAX_INPUT_ROWS_CAP,
687
+ DEFAULT_OVERLAY_MAX_ROWS,
503
688
  BRAND_LABEL,
504
689
  };
505
690
  }
@@ -45,6 +45,7 @@ export const COMMANDS = {
45
45
  '/surgical': 'Verbosity: show everything (reasoning, expanded tools)',
46
46
  '/compact': 'Compact conversation context',
47
47
  '/agents': 'List available agents',
48
+ '/run': 'Run a sub-agent or synced workflow',
48
49
  '/explore': 'Code explorer agent',
49
50
  '/review': 'Code review agent',
50
51
  '/architect': 'Feature architect agent',
@@ -137,11 +138,20 @@ export const HELP_GROUPS = [
137
138
  ['/agents create <name>', 'Create .bahulam/agents/<name>.yaml'],
138
139
  ['/agents edit <name>', 'Open local agent YAML'],
139
140
  ['/agents sync [name]', 'Sync all or one local agent to cloud'],
141
+ ['/run <agent> [instruction]', 'Run a local or built-in agent'],
140
142
  ['/explore <instruction>', 'Explore code'],
141
143
  ['/review <instruction>', 'Review code'],
142
144
  ['/architect <instruction>', 'Design an approach'],
143
145
  ],
144
146
  },
147
+ {
148
+ key: 'workflows',
149
+ title: 'Workflows',
150
+ summary: 'saved automations',
151
+ commands: [
152
+ ['/run <workflow> [instruction]', 'Run a synced workflow if no agent matches'],
153
+ ],
154
+ },
145
155
  {
146
156
  key: 'session',
147
157
  title: 'Session',