@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 +397 -72
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
|
2430
|
-
//
|
|
2431
|
-
//
|
|
2432
|
-
//
|
|
2433
|
-
//
|
|
2434
|
-
//
|
|
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
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
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 (
|
|
3130
|
-
|
|
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
|
|
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 =
|
|
3346
|
+
const caretRegionRow = bodyStartIdx + caretBodyRow;
|
|
3207
3347
|
const caretScreenCol = Math.min(rowCap - 1, geo.leftPad + layout.caretCol);
|
|
3208
3348
|
this.lastCursorRow = caretRegionRow;
|
|
3209
|
-
this.
|
|
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
|
-
/**
|
|
3567
|
-
*
|
|
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
|
-
{
|
|
3574
|
-
|
|
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
|
|
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 =
|
|
3580
|
-
const
|
|
3581
|
-
const
|
|
3582
|
-
const
|
|
3583
|
-
const
|
|
3584
|
-
const
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
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${
|
|
3593
|
-
const label = sel ? `${C.bold}${o.label}${
|
|
3594
|
-
|
|
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
|
|
3597
|
-
if (moveUp) process.stdout.write(`\x1B[${
|
|
3598
|
-
|
|
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
|
-
|
|
3908
|
+
drawFrame(false);
|
|
3602
3909
|
const erase = () => {
|
|
3603
3910
|
process.stdout.write("\r");
|
|
3604
|
-
|
|
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
|
|
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 +
|
|
3616
|
-
|
|
3933
|
+
idx = (idx - 1 + optionCount) % optionCount;
|
|
3934
|
+
drawFrame(true);
|
|
3617
3935
|
} else if (key?.name === "down" || str === "j") {
|
|
3618
|
-
idx = (idx + 1) %
|
|
3619
|
-
|
|
3936
|
+
idx = (idx + 1) % optionCount;
|
|
3937
|
+
drawFrame(true);
|
|
3620
3938
|
} else if (key?.name === "tab") {
|
|
3621
|
-
|
|
3939
|
+
pick("deny_feedback");
|
|
3622
3940
|
} else if (key?.name === "return" || key?.name === "enter") {
|
|
3623
|
-
|
|
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")
|
|
3627
|
-
else if (k === "a")
|
|
3628
|
-
else if (k === "l")
|
|
3629
|
-
else if (k === "n"
|
|
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
|
-
|
|
5515
|
+
return tui.approval(
|
|
5195
5516
|
`${summary}${req.requestPermission ? `
|
|
5196
|
-
|
|
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(
|
|
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));
|