@standardagents/code 0.7.1 → 0.8.0

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/dist/index.js CHANGED
@@ -2274,6 +2274,13 @@ function buildBoxBottom(boxW, borderColor, reset = "\x1B[0m") {
2274
2274
  function buildBoxTopPlain(boxW, borderColor, reset = "\x1B[0m") {
2275
2275
  return `${borderColor}\u256D${"\u2500".repeat(Math.max(1, boxW - 2))}\u256E${reset}`;
2276
2276
  }
2277
+ function buildBoxTopLeftLabel(boxW, label, borderColor, labelColor = "\x1B[90m", reset = "\x1B[0m") {
2278
+ const plain = ` ${label} `;
2279
+ const budget = Math.max(1, boxW - 3);
2280
+ const lab = plain.length > budget ? plain.slice(0, Math.max(1, budget)) : plain;
2281
+ const fill = Math.max(1, boxW - 2 - lab.length);
2282
+ return `${borderColor}\u256D${reset}${labelColor}${lab}${reset}${borderColor}${"\u2500".repeat(fill)}\u256E${reset}`;
2283
+ }
2277
2284
  function buildBoxBody(content, contentW, borderColor, reset = "\x1B[0m") {
2278
2285
  const vw = boxVisibleWidth(content);
2279
2286
  const pad = Math.max(0, contentW - vw);
@@ -2302,17 +2309,104 @@ function wrapInputBodyLines(inputBuffer, prefix, prefixWidth, contentW) {
2302
2309
  if (out.length === 0) out.push(prefix);
2303
2310
  return out;
2304
2311
  }
2312
+ function borderHighlightGrey(intensity) {
2313
+ const t = Math.max(0, Math.min(1, intensity));
2314
+ const e = t * t;
2315
+ const lo = 88;
2316
+ const hi = 236;
2317
+ const v = Math.round(lo + (hi - lo) * e);
2318
+ const ct = process.env.COLORTERM ?? "";
2319
+ const tc = /truecolor|24bit/i.test(ct) || /iterm|kitty|wezterm|ghostty|alacritty/i.test(process.env.TERM_PROGRAM ?? "");
2320
+ if (tc) return `\x1B[38;2;${v};${v};${v}m`;
2321
+ return `\x1B[38;5;${232 + Math.round(23 * e)}m`;
2322
+ }
2323
+ function rotatingBorderCellColor(index, perimeter, phase, trailFrac = 0.2) {
2324
+ const P = Math.max(1, perimeter);
2325
+ const pos = index / P;
2326
+ const dBehind = ((phase - pos) % 1 + 1) % 1;
2327
+ if (dBehind <= trailFrac) {
2328
+ return borderHighlightGrey(1 - dBehind / trailFrac);
2329
+ }
2330
+ return inputBoxBorderColor();
2331
+ }
2332
+ function perimeterIndex(edge, offset, boxW, bodyH) {
2333
+ const W = boxW;
2334
+ const H = Math.max(1, bodyH);
2335
+ if (edge === "top") return Math.max(0, Math.min(W - 1, offset));
2336
+ if (edge === "right") return W + Math.max(0, Math.min(H - 1, offset));
2337
+ if (edge === "bottom") return W + H + Math.max(0, Math.min(W - 1, offset));
2338
+ return 2 * W + H + Math.max(0, Math.min(H - 1, offset));
2339
+ }
2305
2340
  function buildInputBoxRows(opts) {
2306
2341
  const { cols: cols2, inputBuffer, prefix, prefixWidth, level, levelColor } = opts;
2307
2342
  const borderColor = opts.borderColor ?? inputBoxBorderColor();
2308
2343
  const geo = inputBoxGeometry(cols2);
2309
2344
  const body = wrapInputBodyLines(inputBuffer, prefix, prefixWidth, geo.contentW);
2310
- const dots = borderLevelDots(level);
2311
- const dotsStyled = borderLevelDotsStyled(level, levelColor);
2312
- const top = buildBoxTop(geo.boxW, dots, borderColor, levelColor, "\x1B[0m", dotsStyled);
2313
- const bottom = buildBoxBottom(geo.boxW, borderColor);
2314
2345
  const pad = " ".repeat(geo.margin);
2315
- return [pad + top, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom];
2346
+ const phase = opts.borderPhase;
2347
+ if (phase == null || !Number.isFinite(phase)) {
2348
+ const dots2 = borderLevelDots(level);
2349
+ const dotsStyled2 = borderLevelDotsStyled(level, levelColor);
2350
+ const top2 = buildBoxTop(geo.boxW, dots2, borderColor, levelColor, "\x1B[0m", dotsStyled2);
2351
+ const bottom2 = buildBoxBottom(geo.boxW, borderColor);
2352
+ return [pad + top2, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom2];
2353
+ }
2354
+ const W = geo.boxW;
2355
+ const bodyH = Math.max(1, body.length);
2356
+ const P = 2 * W + 2 * bodyH;
2357
+ const reset = "\x1B[0m";
2358
+ const colorAt = (i) => rotatingBorderCellColor(i, P, phase);
2359
+ const dots = borderLevelDots(level);
2360
+ borderLevelDotsStyled(level, levelColor);
2361
+ const labelPlain = ` ${dots} `;
2362
+ const rightFillN = 1;
2363
+ const budget = Math.max(1, W - 2 - rightFillN);
2364
+ const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
2365
+ const leftFill = Math.max(1, W - 2 - plain.length - rightFillN);
2366
+ let top = "";
2367
+ let xi = 0;
2368
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u256D" + reset;
2369
+ for (let k = 0; k < leftFill; k++) {
2370
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
2371
+ }
2372
+ {
2373
+ const plainChars = [...plain];
2374
+ let di = 0;
2375
+ for (const ch of plainChars) {
2376
+ const c2 = colorAt(perimeterIndex("top", xi++, W, bodyH));
2377
+ if (ch === "\u25CF" || ch === "\u25CB") {
2378
+ const levelN = Math.max(1, Math.min(5, level));
2379
+ const filled = di < levelN;
2380
+ di++;
2381
+ top += (filled ? levelColor || c2 : "\x1B[38;5;240m") + (filled ? "\u25CF" : "\u25CB") + reset;
2382
+ } else {
2383
+ top += c2 + ch + reset;
2384
+ }
2385
+ }
2386
+ }
2387
+ for (let k = 0; k < rightFillN; k++) {
2388
+ top += colorAt(perimeterIndex("top", xi++, W, bodyH)) + "\u2500" + reset;
2389
+ }
2390
+ top += colorAt(perimeterIndex("top", Math.min(xi, W - 1), W, bodyH)) + "\u256E" + reset;
2391
+ const bodyRows = [];
2392
+ for (let y = 0; y < bodyH; y++) {
2393
+ const leftC = colorAt(perimeterIndex("left", bodyH - 1 - y, W, bodyH));
2394
+ const rightC = colorAt(perimeterIndex("right", y, W, bodyH));
2395
+ const vw = boxVisibleWidth(body[y]);
2396
+ const sp = Math.max(0, geo.contentW - vw);
2397
+ bodyRows.push(
2398
+ `${pad}${leftC}\u2502${reset} ${body[y]}${" ".repeat(sp)} ${rightC}\u2502${reset}`
2399
+ );
2400
+ }
2401
+ let bottom = "";
2402
+ for (let x = 0; x < W; x++) {
2403
+ const idx = perimeterIndex("bottom", W - 1 - x, W, bodyH);
2404
+ const c2 = colorAt(idx);
2405
+ if (x === 0) bottom += c2 + "\u2570" + reset;
2406
+ else if (x === W - 1) bottom += c2 + "\u256F" + reset;
2407
+ else bottom += c2 + "\u2500" + reset;
2408
+ }
2409
+ return [pad + top, ...bodyRows, pad + bottom];
2316
2410
  }
2317
2411
  var C = {
2318
2412
  reset: "\x1B[0m",
@@ -2367,7 +2461,7 @@ var Tui = class _Tui {
2367
2461
  process.stdin.resume();
2368
2462
  process.stdout.write("\x1B[?2004h");
2369
2463
  process.on("exit", () => process.stdout.write("\x1B[?2004l\x1B[?25h"));
2370
- process.stdout.on("resize", () => this.renderBottom());
2464
+ process.stdout.on("resize", () => this.scheduleResizeRedraw());
2371
2465
  }
2372
2466
  level;
2373
2467
  // input + indicators
@@ -2426,13 +2520,22 @@ var Tui = class _Tui {
2426
2520
  connected = true;
2427
2521
  bottomDrawn = false;
2428
2522
  started = false;
2429
- // Resize bookkeeping: the width the region was last drawn at, and the visible
2430
- // width of every HUD row written above the input. When the terminal is
2431
- // resized, previously drawn rows re-wrap (a full-width ruler becomes 2+ rows
2432
- // when narrowed), so the move-up count recorded at draw time is wrong these
2433
- // let moveToRegionTop recompute the region height under the NEW wrap instead
2434
- // of leaving stale rulers behind.
2523
+ // Resize bookkeeping. When the terminal width changes, previously drawn HUD
2524
+ // rows re-wrap (a full-width ruler becomes 2+ physical rows when narrowed),
2525
+ // so the caret-relative move-up from the last paint is stale. We store the
2526
+ // visible width of EVERY region row (not just above the body) plus the caret
2527
+ // row index so moveToRegionTop can recompute physical height under the new
2528
+ // wrap. Resize events are debounced — drag-resizing fires dozens of events
2529
+ // and redrawing each one desyncs and leaves ghost chrome.
2435
2530
  lastDrawnCols = 0;
2531
+ /** Visible width of every HUD row from the last paint, top → bottom. */
2532
+ drawnRegionWidths = [];
2533
+ /** Index into drawnRegionWidths of the row the caret sat on last paint. */
2534
+ lastCaretRegionIndex = 0;
2535
+ resizeTimer = null;
2536
+ /** True between a resize event and the debounced repaint — spinner ticks no-op. */
2537
+ resizePending = false;
2538
+ /** @deprecated kept as alias during paint — prefer drawnRegionWidths */
2436
2539
  drawnHudWidths = [];
2437
2540
  // takeover (approval / menu) state
2438
2541
  takeoverHandler = null;
@@ -2471,6 +2574,23 @@ var Tui = class _Tui {
2471
2574
  };
2472
2575
  onQuit = () => process.exit(0);
2473
2576
  levelListeners = [];
2577
+ /**
2578
+ * Terminal resize fires continuously while the user drags. Redrawing on every
2579
+ * event desyncs the region-height math (each paint uses a half-rewrapped
2580
+ * intermediate width) and stamps ghost boxes into the scrollback. Wait for
2581
+ * the size to settle (~1–2 frames), then do one clean clear+repaint. While
2582
+ * pending, spinner ticks skip paint so they don't thrash mid-drag.
2583
+ */
2584
+ scheduleResizeRedraw() {
2585
+ this.resizePending = true;
2586
+ if (this.resizeTimer) clearTimeout(this.resizeTimer);
2587
+ this.resizeTimer = setTimeout(() => {
2588
+ this.resizeTimer = null;
2589
+ this.resizePending = false;
2590
+ if (!this.started || this.takeoverHandler) return;
2591
+ this.renderBottom();
2592
+ }, 40);
2593
+ }
2474
2594
  get colors() {
2475
2595
  return C;
2476
2596
  }
@@ -2512,6 +2632,10 @@ var Tui = class _Tui {
2512
2632
  clearTimeout(this.streamRedrawTimer);
2513
2633
  this.streamRedrawTimer = null;
2514
2634
  }
2635
+ if (this.resizeTimer) {
2636
+ clearTimeout(this.resizeTimer);
2637
+ this.resizeTimer = null;
2638
+ }
2515
2639
  if (this.spinnerTimer) {
2516
2640
  clearInterval(this.spinnerTimer);
2517
2641
  this.spinnerTimer = null;
@@ -3109,25 +3233,38 @@ var Tui = class _Tui {
3109
3233
  return line;
3110
3234
  });
3111
3235
  }
3236
+ /**
3237
+ * Physical rows a previously painted line of visible width `w` occupies after
3238
+ * the terminal rewraps it to `cols`. Hard-written HUD lines reflow this way.
3239
+ */
3240
+ rewrapRows(w, cols2) {
3241
+ return Math.max(1, Math.ceil(Math.max(w, 1) / Math.max(1, cols2)));
3242
+ }
3112
3243
  /**
3113
3244
  * Move the cursor to the top-left of the current bottom region.
3114
3245
  *
3115
3246
  * Same width as the last draw → the caret's recorded row offset is exact.
3116
3247
  * Width CHANGED (terminal resized) → previously drawn rows re-wrapped, so
3117
- * that offset is stale; recompute it under the new wrap instead: each drawn
3118
- * HUD row of visible width w now occupies ceil(w / cols) physical rows
3119
- * (reflowing terminals re-wrap hard lines; the cursor follows its logical
3120
- * position in the input text, which inputLayout locates at the new width).
3248
+ * that offset is stale. Recompute using every stored region row width under
3249
+ * the NEW wrap: rows above the caret row + (caret row's rewrap − 1) so we
3250
+ * prefer a slight over-move (clean wipe) over under-move (ghost chrome).
3121
3251
  */
3122
3252
  moveToRegionTop() {
3123
3253
  process.stdout.write("\r");
3124
3254
  if (!this.bottomDrawn) return;
3125
3255
  const cols2 = process.stdout.columns || 80;
3126
3256
  let up;
3127
- if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0) {
3257
+ if (cols2 !== this.lastDrawnCols && this.lastDrawnCols > 0 && this.drawnRegionWidths.length) {
3258
+ const widths = this.drawnRegionWidths.length > 0 ? this.drawnRegionWidths : this.drawnHudWidths;
3259
+ const caretIdx = Math.max(
3260
+ 0,
3261
+ Math.min(this.lastCaretRegionIndex, Math.max(0, widths.length - 1))
3262
+ );
3128
3263
  let above = 0;
3129
- for (const w of this.drawnHudWidths) above += Math.max(1, Math.ceil(Math.max(w, 1) / cols2));
3130
- up = above + this.inputLayout().caretRow;
3264
+ for (let i = 0; i < caretIdx; i++) above += this.rewrapRows(widths[i], cols2);
3265
+ const caretPhysical = this.rewrapRows(widths[caretIdx] ?? 1, cols2);
3266
+ up = above + Math.max(0, caretPhysical - 1);
3267
+ up += 1;
3131
3268
  } else {
3132
3269
  up = this.lastCursorRow;
3133
3270
  }
@@ -3145,6 +3282,7 @@ var Tui = class _Tui {
3145
3282
  */
3146
3283
  renderBottom() {
3147
3284
  if (!this.started || this.takeoverHandler) return;
3285
+ if (this.resizePending) return;
3148
3286
  const cols2 = process.stdout.columns || 80;
3149
3287
  this.moveToRegionTop();
3150
3288
  const hudWidths = [];
@@ -3181,6 +3319,8 @@ var Tui = class _Tui {
3181
3319
  for (const line of goalLines) writeHudRow(workPad + line);
3182
3320
  const prefix = this.promptPrefix();
3183
3321
  const pw = this.visibleWidth(prefix);
3322
+ const borderAnimating = this.working;
3323
+ const borderPhase = borderAnimating ? Date.now() % 1400 / 1400 : null;
3184
3324
  const boxRows = buildInputBoxRows({
3185
3325
  cols: cols2,
3186
3326
  inputBuffer: this.inputBuffer,
@@ -3188,14 +3328,14 @@ var Tui = class _Tui {
3188
3328
  prefixWidth: pw,
3189
3329
  level: this.level,
3190
3330
  levelColor: this.levelColor(),
3191
- borderColor: inputBoxBorderColor()
3331
+ borderColor: inputBoxBorderColor(),
3332
+ borderPhase
3192
3333
  });
3193
3334
  const boxTop = boxRows[0];
3194
3335
  const boxBottom = boxRows[boxRows.length - 1];
3195
3336
  const boxBody = boxRows.slice(1, -1);
3196
3337
  writeHudRow(boxTop);
3197
- const aboveBodyWidths = hudWidths.slice();
3198
- const aboveBodyRows = aboveBodyWidths.length;
3338
+ const bodyStartIdx = hudWidths.length;
3199
3339
  for (const row of boxBody) writeHudRow(row);
3200
3340
  writeHudRow(boxBottom);
3201
3341
  const paletteBlock = this.paletteBlockLines(cols2);
@@ -3203,10 +3343,12 @@ var Tui = class _Tui {
3203
3343
  const layout = this.inputLayout();
3204
3344
  const bodyRowCount = Math.max(1, boxBody.length);
3205
3345
  const caretBodyRow = Math.max(0, Math.min(layout.caretRow, bodyRowCount - 1));
3206
- const caretRegionRow = aboveBodyRows + caretBodyRow;
3346
+ const caretRegionRow = bodyStartIdx + caretBodyRow;
3207
3347
  const caretScreenCol = Math.min(rowCap - 1, geo.leftPad + layout.caretCol);
3208
3348
  this.lastCursorRow = caretRegionRow;
3209
- this.drawnHudWidths = aboveBodyWidths;
3349
+ this.lastCaretRegionIndex = caretRegionRow;
3350
+ this.drawnRegionWidths = hudWidths.slice();
3351
+ this.drawnHudWidths = hudWidths.slice(0, bodyStartIdx);
3210
3352
  this.lastDrawnCols = cols2;
3211
3353
  this.bottomDrawn = true;
3212
3354
  const totalRows = hudRows.length;
@@ -3563,70 +3705,249 @@ var Tui = class _Tui {
3563
3705
  }
3564
3706
  this.renderBottom();
3565
3707
  }
3566
- /** Approval prompt: arrow-navigable with y/a/l/n shortcuts. Pauses input.
3567
- * Tab resolves "deny_with_reason" so the caller can collect a free-text reason. */
3708
+ /**
3709
+ * ## Boxed prompt reusable HUD free-text component
3710
+ *
3711
+ * Single-line free-text capture using the **same chrome as the main input
3712
+ * box** (side margin, grey rounded rim, level-tinted `❯`, real caret). A
3713
+ * left-aligned label sits in the top border rim instead of the permission
3714
+ * level dots.
3715
+ *
3716
+ * Prefer this over {@link Tui.prompt} whenever the capture should feel like
3717
+ * part of the HUD rather than a legacy bar — denial feedback, handoff notes,
3718
+ * reject-with-reason, rename, or any other short free-text moment.
3719
+ *
3720
+ * ```
3721
+ * ╭ Denial feedback ────────────────────────╮
3722
+ * │ ❯ the user-typed note │
3723
+ * ╰──────────────────────────────────────────╯
3724
+ * ```
3725
+ *
3726
+ * **Keys**
3727
+ * - Enter — submit (resolves the trimmed string; may be `""`)
3728
+ * - Esc — clear a non-empty draft; Esc again (empty) cancels → `null`
3729
+ * (override with `escClearsFirst: false` to always cancel)
3730
+ *
3731
+ * **Nested mode** (`nested: true`) — call from inside another takeover
3732
+ * (e.g. approval → feedback) so this component only steals the key handler
3733
+ * and paints/erases its own three rows; the parent still owns begin/end.
3734
+ *
3735
+ * Do **not** use brand-gradient text for the label — gradient is reserved
3736
+ * for success moments (`✓ Goal complete.`, farewell, etc.).
3737
+ *
3738
+ * @returns trimmed text on Enter, or `null` if the user cancelled with Esc
3739
+ */
3740
+ boxedPrompt(opts) {
3741
+ return new Promise((resolve) => {
3742
+ const nested = !!opts.nested;
3743
+ const escClearsFirst = opts.escClearsFirst !== false;
3744
+ const borderLabel = opts.borderLabel || "Feedback";
3745
+ const labelColor = opts.labelColor ?? "\x1B[90m";
3746
+ let buf = opts.initial ?? "";
3747
+ if (!nested) this.beginTakeover();
3748
+ process.stdout.write("\x1B[?25h");
3749
+ const cols2 = process.stdout.columns || 80;
3750
+ const geo = inputBoxGeometry(cols2);
3751
+ const border = inputBoxBorderColor();
3752
+ const pad = " ".repeat(geo.margin);
3753
+ const rowCap = Math.max(1, cols2 - 1);
3754
+ const reset = C.reset;
3755
+ const prompt = `${this.levelColor()}\u276F${reset} `;
3756
+ const promptW = 2;
3757
+ const writeRow = (line) => {
3758
+ const row = this.clampVisible(sanitizeHudRow(line), rowCap);
3759
+ process.stdout.write(`\r\x1B[K${row}
3760
+ `);
3761
+ };
3762
+ const draw = () => {
3763
+ writeRow(pad + buildBoxTopLeftLabel(geo.boxW, borderLabel, border, labelColor));
3764
+ writeRow(pad + buildBoxBody(prompt + buf, geo.contentW, border));
3765
+ writeRow(pad + buildBoxBottom(geo.boxW, border));
3766
+ process.stdout.write("\r\x1B[2A");
3767
+ const col = geo.leftPad + promptW + buf.length;
3768
+ if (col > 0) process.stdout.write(`\x1B[${Math.min(rowCap - 1, col)}C`);
3769
+ };
3770
+ const clearFrame = () => {
3771
+ process.stdout.write("\r\x1B[1A\x1B[J");
3772
+ };
3773
+ const redraw = () => {
3774
+ process.stdout.write("\r\x1B[1A");
3775
+ draw();
3776
+ };
3777
+ const finish = (value) => {
3778
+ clearFrame();
3779
+ if (!nested) this.endTakeover();
3780
+ else this.takeoverHandler = null;
3781
+ resolve(value);
3782
+ };
3783
+ draw();
3784
+ this.takeoverHandler = (str, key) => {
3785
+ if (key?.name === "escape") {
3786
+ if (escClearsFirst && buf.length > 0) {
3787
+ buf = "";
3788
+ redraw();
3789
+ return;
3790
+ }
3791
+ finish(null);
3792
+ return;
3793
+ }
3794
+ if (key?.name === "return" || key?.name === "enter") {
3795
+ finish(buf.trim());
3796
+ return;
3797
+ }
3798
+ if (key?.name === "backspace") {
3799
+ if (!buf.length) return;
3800
+ buf = buf.slice(0, -1);
3801
+ redraw();
3802
+ return;
3803
+ }
3804
+ if (str && !key?.ctrl && !key?.meta && str >= " ") {
3805
+ if (buf.length < Math.max(8, geo.contentW - promptW - 1)) {
3806
+ buf += str;
3807
+ redraw();
3808
+ }
3809
+ }
3810
+ };
3811
+ });
3812
+ }
3813
+ /**
3814
+ * Permission prompt: boxed chrome matching the HUD, arrow-navigable options
3815
+ * with shortcuts. Includes "Deny with feedback" — choosing it (or Tab)
3816
+ * opens {@link Tui.boxedPrompt} so the reason can ride back to the agent.
3817
+ */
3568
3818
  approval(question, risk) {
3569
3819
  return new Promise((resolve) => {
3570
3820
  const options = [
3571
3821
  { value: "allow", label: "Allow once", shortcut: "y", color: C.green },
3572
3822
  { value: "always", label: "Always allow this tool", shortcut: "a", color: C.cyan },
3573
- { value: "always_risk", label: `Allow level ${risk} and below this session`, shortcut: "l", color: C.cyan },
3574
- { value: "deny", label: "Deny", shortcut: "n", color: C.red }
3823
+ {
3824
+ value: "always_risk",
3825
+ label: `Allow level ${risk} and below this session`,
3826
+ shortcut: "l",
3827
+ color: C.cyan
3828
+ },
3829
+ { value: "deny", label: "Deny", shortcut: "n", color: C.red },
3830
+ {
3831
+ value: "deny_feedback",
3832
+ label: "Deny with feedback",
3833
+ shortcut: "d",
3834
+ color: C.magenta,
3835
+ hint: "tell the agent why"
3836
+ }
3575
3837
  ];
3576
3838
  let idx = 0;
3577
- const riskBar = `${C.red}${"\u25CF".repeat(risk)}${C.gray}${"\u25CB".repeat(5 - risk)}${C.reset}`;
3839
+ const riskLevel = Math.max(1, Math.min(5, Math.round(risk)));
3840
+ const riskBar = borderLevelDotsStyled(riskLevel, C.yellow, "\x1B[38;5;240m");
3578
3841
  this.beginTakeover();
3579
- const cols2 = Math.max(1, process.stdout.columns || 80);
3580
- const physRows = (line) => Math.max(1, Math.ceil(this.visibleWidth(line) / cols2));
3581
- const headerText = `${C.yellow}\u2503${C.reset} ${C.bold}Permission needed${C.reset} risk ${riskBar}`;
3582
- const questionLines = question.split("\n").map((line) => `${C.yellow}\u2503${C.reset} ${line}`);
3583
- const hintLine = `${C.yellow}\u2503${C.reset} ${C.gray}\u21E5 tab \u2014 deny with a reason${C.reset}`;
3584
- const blockLines = [headerText, ...questionLines, hintLine];
3585
- const headerRows = 1 + blockLines.reduce((n, line) => n + physRows(line), 0);
3586
- process.stdout.write(`
3587
- ` + blockLines.map((line) => `${line}
3588
- `).join(""));
3589
- const renderLine = (i) => {
3842
+ const cols2 = process.stdout.columns || 80;
3843
+ const geo = inputBoxGeometry(cols2);
3844
+ const border = inputBoxBorderColor();
3845
+ const pad = " ".repeat(geo.margin);
3846
+ const rowCap = Math.max(1, cols2 - 1);
3847
+ const reset = C.reset;
3848
+ const wrapContent = (text) => {
3849
+ const flat2 = text.replace(/\s+/g, " ").trim();
3850
+ if (!flat2) return [""];
3851
+ const out = [];
3852
+ let rest = flat2;
3853
+ while (rest.length > geo.contentW) {
3854
+ out.push(rest.slice(0, geo.contentW - 1) + "\u2026");
3855
+ rest = rest.slice(geo.contentW - 1);
3856
+ if (out.length >= 4) {
3857
+ break;
3858
+ }
3859
+ }
3860
+ if (out.length < 4) out.push(rest);
3861
+ return out.length ? out : [""];
3862
+ };
3863
+ const summaryLines = question.split("\n").flatMap((line) => {
3864
+ const plain = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
3865
+ if (!plain) return [];
3866
+ const isWhy = /^why:/i.test(plain);
3867
+ return wrapContent(plain).map((l) => isWhy ? `${C.dim}${l}${reset}` : l);
3868
+ });
3869
+ if (!summaryLines.length) summaryLines.push(`${C.dim}(no details)${reset}`);
3870
+ const writeRow = (line) => {
3871
+ const row = this.clampVisible(sanitizeHudRow(line), rowCap);
3872
+ process.stdout.write(`\r\x1B[K${row}
3873
+ `);
3874
+ };
3875
+ const optionCount = options.length;
3876
+ const summaryCount = summaryLines.length;
3877
+ const boxRows = 1 + summaryCount + 1 + optionCount + 1;
3878
+ const titleRows = 2;
3879
+ const totalRows = titleRows + boxRows;
3880
+ const renderOptionContent = (i) => {
3590
3881
  const o = options[i];
3591
3882
  const sel = i === idx;
3592
- const pointer = sel ? `${o.color}\u276F${C.reset}` : " ";
3593
- const label = sel ? `${C.bold}${o.label}${C.reset}` : o.label;
3594
- return `${C.yellow}\u2503${C.reset} ${pointer} ${label} ${C.gray}(${o.shortcut})${C.reset}`;
3883
+ const pointer = sel ? `${o.color}\u276F${reset} ` : " ";
3884
+ const label = sel ? `${C.bold}${o.color}${o.label}${reset}` : `${C.dim}${o.label}${reset}`;
3885
+ const shortcut = `${C.gray}(${o.shortcut})${reset}`;
3886
+ const hint = o.hint && sel ? `${C.dim} ${o.hint}${reset}` : "";
3887
+ const plainLabel = o.label;
3888
+ const used = 2 + plainLabel.length + 1 + 3 + (o.hint && sel ? 2 + o.hint.length : 0);
3889
+ const gap = Math.max(1, geo.contentW - used);
3890
+ return `${pointer}${label}${" ".repeat(gap)}${shortcut}${hint}`;
3595
3891
  };
3596
- const draw = (moveUp) => {
3597
- if (moveUp) process.stdout.write(`\x1B[${options.length}A`);
3598
- for (let i = 0; i < options.length; i++) process.stdout.write(`\r\x1B[K${renderLine(i)}
3599
- `);
3892
+ const drawFrame = (moveUp) => {
3893
+ if (moveUp) process.stdout.write(`\x1B[${totalRows}A`);
3894
+ writeRow("");
3895
+ writeRow(
3896
+ `${pad}${C.bold}Permission needed${reset} ${C.dim}risk${reset} ${riskBar}`
3897
+ );
3898
+ writeRow(pad + buildBoxTopPlain(geo.boxW, border));
3899
+ for (const line of summaryLines) {
3900
+ writeRow(pad + buildBoxBody(line, geo.contentW, border));
3901
+ }
3902
+ writeRow(pad + buildBoxBody("", geo.contentW, border));
3903
+ for (let i = 0; i < optionCount; i++) {
3904
+ writeRow(pad + buildBoxBody(renderOptionContent(i), geo.contentW, border));
3905
+ }
3906
+ writeRow(pad + buildBoxBottom(geo.boxW, border));
3600
3907
  };
3601
- draw(false);
3908
+ drawFrame(false);
3602
3909
  const erase = () => {
3603
3910
  process.stdout.write("\r");
3604
- const up = headerRows + options.length;
3605
- if (up > 0) process.stdout.write(`\x1B[${up}A`);
3911
+ if (totalRows > 0) process.stdout.write(`\x1B[${totalRows}A`);
3606
3912
  process.stdout.write("\x1B[J");
3607
3913
  };
3608
- const finish = (choice) => {
3914
+ const done = (choice, reason) => {
3609
3915
  erase();
3610
3916
  this.endTakeover();
3611
- resolve(choice);
3917
+ resolve({ choice });
3918
+ };
3919
+ const collectFeedback = () => {
3920
+ erase();
3921
+ void this.boxedPrompt({ borderLabel: "Denial feedback", nested: true }).then((why) => {
3922
+ this.endTakeover();
3923
+ if (why) resolve({ choice: "deny", reason: why });
3924
+ else resolve({ choice: "deny" });
3925
+ });
3926
+ };
3927
+ const pick = (value) => {
3928
+ if (value === "deny_feedback") collectFeedback();
3929
+ else done(value);
3612
3930
  };
3613
3931
  this.takeoverHandler = (str, key) => {
3614
3932
  if (key?.name === "up" || str === "k") {
3615
- idx = (idx - 1 + options.length) % options.length;
3616
- draw(true);
3933
+ idx = (idx - 1 + optionCount) % optionCount;
3934
+ drawFrame(true);
3617
3935
  } else if (key?.name === "down" || str === "j") {
3618
- idx = (idx + 1) % options.length;
3619
- draw(true);
3936
+ idx = (idx + 1) % optionCount;
3937
+ drawFrame(true);
3620
3938
  } else if (key?.name === "tab") {
3621
- finish("deny_with_reason");
3939
+ pick("deny_feedback");
3622
3940
  } else if (key?.name === "return" || key?.name === "enter") {
3623
- finish(options[idx].value);
3941
+ pick(options[idx].value);
3942
+ } else if (key?.name === "escape") {
3943
+ done("deny");
3624
3944
  } else {
3625
3945
  const k = (str || "").toLowerCase();
3626
- if (k === "y") finish("allow");
3627
- else if (k === "a") finish("always");
3628
- else if (k === "l") finish("always_risk");
3629
- else if (k === "n" || key?.name === "escape") finish("deny");
3946
+ if (k === "y") pick("allow");
3947
+ else if (k === "a") pick("always");
3948
+ else if (k === "l") pick("always_risk");
3949
+ else if (k === "n") pick("deny");
3950
+ else if (k === "d" || k === "f") pick("deny_feedback");
3630
3951
  }
3631
3952
  };
3632
3953
  });
@@ -5191,18 +5512,11 @@ async function runInteractive(tui, api, threadId, projectDir, machine, resumed,
5191
5512
  }
5192
5513
  },
5193
5514
  requestApproval: async (req, summary, risk) => {
5194
- const choice = await tui.approval(
5515
+ return tui.approval(
5195
5516
  `${summary}${req.requestPermission ? `
5196
- ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5517
+ why: ${req.requestPermission}` : ""}`,
5197
5518
  risk
5198
5519
  );
5199
- if (choice === "deny_with_reason") {
5200
- const reason = await tui.prompt(
5201
- "Why are you denying this? (sent to the agent \u2014 enter to send, esc to skip)"
5202
- );
5203
- return { choice: "deny", reason: reason ?? void 0 };
5204
- }
5205
- return { choice };
5206
5520
  }
5207
5521
  });
5208
5522
  const stream = new MessageStream(api, threadId, {
@@ -5445,16 +5759,27 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5445
5759
  if (upgradeInFlight) return;
5446
5760
  upgradeInFlight = true;
5447
5761
  try {
5762
+ if (opts.auto) {
5763
+ tui.print(
5764
+ `${c.yellow}You're out of simultaneous sessions \u2014 another Standard Code session is using your slot.${c.reset}`
5765
+ );
5766
+ }
5448
5767
  const quote = await api.sessionsQuote(threadId);
5449
5768
  if (!quote) {
5450
5769
  const link = await api.accountLink(threadId).catch(() => null);
5451
5770
  const target = link?.url ?? "https://standardcode.ai/account";
5771
+ tui.print(
5772
+ `${c.gray}Close the other session (its slot frees within ~90s) \u2014 or add another simultaneous session to your plan, then resend your message.${c.reset}`
5773
+ );
5452
5774
  openUrl(target);
5453
5775
  tui.print(`${c.gray}\u2192 opened ${target} to manage your plan${c.reset}`);
5454
5776
  return;
5455
5777
  }
5456
5778
  if (quote.current >= quote.max) {
5457
- tui.print(`${c.yellow}You're at the maximum of ${quote.max} parallel sessions.${c.reset}`);
5779
+ tui.print(
5780
+ `${c.yellow}You're at the maximum of ${quote.max} parallel session${quote.max === 1 ? "" : "s"}.${c.reset}
5781
+ ${c.gray}Close another session (its slot frees within ~90s), then resend your message.${c.reset}`
5782
+ );
5458
5783
  return;
5459
5784
  }
5460
5785
  tui.print(renderUpgradePanel(quote));