@standardagents/code 0.6.6 → 0.7.1

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
@@ -1916,6 +1916,90 @@ var SystemEvents = class {
1916
1916
  }
1917
1917
  };
1918
1918
 
1919
+ // src/subagent-streams.ts
1920
+ var MAX_PHRASE = 120;
1921
+ function cleanPhrase(raw) {
1922
+ const s = raw.replace(/\s+/g, " ").trim();
1923
+ return s.length > MAX_PHRASE ? `${s.slice(0, MAX_PHRASE - 1)}\u2026` : s;
1924
+ }
1925
+ function prettyToolName(name) {
1926
+ return name.replace(/^provider:/, "").replace(/[_:-]+/g, " ").trim();
1927
+ }
1928
+ var SubagentActivity = class {
1929
+ constructor(api, onChange, makeStream = (threadId, hooks) => new MessageStream(api, threadId, hooks)) {
1930
+ this.onChange = onChange;
1931
+ this.makeStream = makeStream;
1932
+ }
1933
+ onChange;
1934
+ makeStream;
1935
+ entries = /* @__PURE__ */ new Map();
1936
+ /** The current activity phrase for a subagent's child thread, if any. */
1937
+ phraseFor(threadId) {
1938
+ return this.entries.get(threadId)?.phrase ?? null;
1939
+ }
1940
+ /**
1941
+ * Reconcile the open streams against the authoritative set of RUNNING
1942
+ * subagent child-thread ids (from the parent's registry): open newcomers,
1943
+ * close and forget departed ones.
1944
+ */
1945
+ sync(activeIds) {
1946
+ const want = new Set(activeIds);
1947
+ for (const [id, entry] of [...this.entries]) {
1948
+ if (!want.has(id)) {
1949
+ entry.stream.close();
1950
+ this.entries.delete(id);
1951
+ }
1952
+ }
1953
+ for (const id of want) {
1954
+ if (!this.entries.has(id)) this.open(id);
1955
+ }
1956
+ }
1957
+ open(threadId) {
1958
+ const entry = { stream: null, steps: /* @__PURE__ */ new Map(), phrase: null };
1959
+ const setPhrase = (phrase) => {
1960
+ if (phrase === entry.phrase) return;
1961
+ entry.phrase = phrase;
1962
+ this.onChange(threadId);
1963
+ };
1964
+ const phraseFromSteps = () => {
1965
+ let last = null;
1966
+ for (const v of entry.steps.values()) last = v;
1967
+ return last ?? entry.phrase;
1968
+ };
1969
+ entry.stream = this.makeStream(threadId, {
1970
+ // Streamed output with no tool in flight means the model is composing —
1971
+ // reasoning reads as "thinking", visible answer text as "writing". These
1972
+ // only fire on TRANSITIONS (phrase comparison), not per chunk.
1973
+ onChunk: () => {
1974
+ if (entry.steps.size === 0) setPhrase("writing");
1975
+ },
1976
+ onReasoningChunk: () => {
1977
+ if (entry.steps.size === 0) setPhrase("thinking");
1978
+ },
1979
+ onAssistantText: () => {
1980
+ },
1981
+ onEvent: (eventType, data) => {
1982
+ if (eventType === "tool_call_started" && data?.id) {
1983
+ entry.steps.set(String(data.id), cleanPhrase(String(data.progress || prettyToolName(String(data.name || "")) || "working")));
1984
+ setPhrase(phraseFromSteps());
1985
+ } else if (eventType === "tool_call_done" && data?.id) {
1986
+ entry.steps.delete(String(data.id));
1987
+ setPhrase(phraseFromSteps());
1988
+ }
1989
+ },
1990
+ onError: () => {
1991
+ }
1992
+ });
1993
+ this.entries.set(threadId, entry);
1994
+ void entry.stream.connect();
1995
+ }
1996
+ /** Close every stream (session teardown). */
1997
+ closeAll() {
1998
+ for (const entry of this.entries.values()) entry.stream.close();
1999
+ this.entries.clear();
2000
+ }
2001
+ };
2002
+
1919
2003
  // src/wordmill.ts
1920
2004
  var MILL_WORDS = [
1921
2005
  "Working",
@@ -2144,6 +2228,92 @@ async function readClipboardImage() {
2144
2228
  function imagePlaceholder(seq) {
2145
2229
  return `[#Image ${seq}]`;
2146
2230
  }
2231
+ var INPUT_BOX_MARGIN = 1;
2232
+ function inputBoxBorderColor() {
2233
+ return "\x1B[38;5;240m";
2234
+ }
2235
+ function borderLevelDots(level) {
2236
+ const n = Math.max(1, Math.min(5, level));
2237
+ return "\u25CF".repeat(n) + "\u25CB".repeat(5 - n);
2238
+ }
2239
+ function borderLevelDotsStyled(level, levelColor, emptyColor = "\x1B[38;5;240m", reset = "\x1B[0m") {
2240
+ const n = Math.max(1, Math.min(5, level));
2241
+ let out = "";
2242
+ for (let i = 1; i <= 5; i++) {
2243
+ out += i <= n ? `${levelColor}\u25CF${reset}` : `${emptyColor}\u25CB${reset}`;
2244
+ }
2245
+ return out;
2246
+ }
2247
+ function inputBoxGeometry(cols2) {
2248
+ const margin = INPUT_BOX_MARGIN;
2249
+ const boxW = Math.max(12, cols2 - 1 - margin);
2250
+ const contentW = Math.max(4, boxW - 4);
2251
+ const leftPad = margin + 2;
2252
+ return { margin, boxW, contentW, leftPad };
2253
+ }
2254
+ function boxVisibleWidth(s) {
2255
+ let w = 0;
2256
+ for (const ch of s.replace(/\x1b\[[0-9;]*m/g, "")) {
2257
+ const cp = ch.codePointAt(0);
2258
+ w += isWideCodePoint(cp) ? 2 : 1;
2259
+ }
2260
+ return w;
2261
+ }
2262
+ function buildBoxTop(boxW, levelText, borderColor, levelColor, reset = "\x1B[0m", levelStyled) {
2263
+ const labelPlain = ` ${levelText} `;
2264
+ const rightFill = 1;
2265
+ const budget = Math.max(1, boxW - 2 - rightFill);
2266
+ const plain = labelPlain.length > budget ? labelPlain.slice(0, Math.max(1, budget)) : labelPlain;
2267
+ const leftFill = Math.max(1, boxW - 2 - plain.length - rightFill);
2268
+ const mid = levelStyled !== void 0 ? ` ${levelStyled} ` : `${levelColor}${plain}${reset}`;
2269
+ return `${borderColor}\u256D${"\u2500".repeat(leftFill)}${reset}` + mid + `${borderColor}${"\u2500".repeat(rightFill)}\u256E${reset}`;
2270
+ }
2271
+ function buildBoxBottom(boxW, borderColor, reset = "\x1B[0m") {
2272
+ return `${borderColor}\u2570${"\u2500".repeat(Math.max(1, boxW - 2))}\u256F${reset}`;
2273
+ }
2274
+ function buildBoxTopPlain(boxW, borderColor, reset = "\x1B[0m") {
2275
+ return `${borderColor}\u256D${"\u2500".repeat(Math.max(1, boxW - 2))}\u256E${reset}`;
2276
+ }
2277
+ function buildBoxBody(content, contentW, borderColor, reset = "\x1B[0m") {
2278
+ const vw = boxVisibleWidth(content);
2279
+ const pad = Math.max(0, contentW - vw);
2280
+ return `${borderColor}\u2502${reset} ${content}${" ".repeat(pad)} ${borderColor}\u2502${reset}`;
2281
+ }
2282
+ function wrapInputBodyLines(inputBuffer, prefix, prefixWidth, contentW) {
2283
+ const indent = " ".repeat(Math.max(0, prefixWidth));
2284
+ const lines = inputBuffer.split("\n");
2285
+ const out = [];
2286
+ let isFirstPhysical = true;
2287
+ for (let i = 0; i < lines.length; i++) {
2288
+ const text = lines[i];
2289
+ let remaining = text;
2290
+ do {
2291
+ const lead = isFirstPhysical ? prefix : indent;
2292
+ const avail = Math.max(0, contentW - prefixWidth);
2293
+ let take = Math.min(remaining.length, avail);
2294
+ if (take === 0 && avail === 0 && remaining.length > 0) take = 1;
2295
+ const chunk = remaining.slice(0, take);
2296
+ remaining = remaining.slice(take);
2297
+ out.push(lead + chunk);
2298
+ isFirstPhysical = false;
2299
+ if (remaining.length === 0) break;
2300
+ } while (remaining.length > 0);
2301
+ }
2302
+ if (out.length === 0) out.push(prefix);
2303
+ return out;
2304
+ }
2305
+ function buildInputBoxRows(opts) {
2306
+ const { cols: cols2, inputBuffer, prefix, prefixWidth, level, levelColor } = opts;
2307
+ const borderColor = opts.borderColor ?? inputBoxBorderColor();
2308
+ const geo = inputBoxGeometry(cols2);
2309
+ 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
+ const pad = " ".repeat(geo.margin);
2315
+ return [pad + top, ...body.map((b) => pad + buildBoxBody(b, geo.contentW, borderColor)), pad + bottom];
2316
+ }
2147
2317
  var C = {
2148
2318
  reset: "\x1B[0m",
2149
2319
  dim: "\x1B[2m",
@@ -2331,10 +2501,21 @@ var Tui = class _Tui {
2331
2501
  }
2332
2502
  /** Tear down the bottom region and restore the terminal (called on quit). */
2333
2503
  end() {
2504
+ this.started = false;
2505
+ this.working = false;
2506
+ this.subagents = [];
2334
2507
  if (this.quitTimer) clearTimeout(this.quitTimer);
2335
2508
  this.quitTimer = null;
2336
2509
  if (this.streamIdleTimer) clearTimeout(this.streamIdleTimer);
2337
2510
  this.streamIdleTimer = null;
2511
+ if (this.streamRedrawTimer) {
2512
+ clearTimeout(this.streamRedrawTimer);
2513
+ this.streamRedrawTimer = null;
2514
+ }
2515
+ if (this.spinnerTimer) {
2516
+ clearInterval(this.spinnerTimer);
2517
+ this.spinnerTimer = null;
2518
+ }
2338
2519
  this.clearBottom();
2339
2520
  process.stdout.write("\x1B[?2004l\x1B[?25h");
2340
2521
  }
@@ -2567,45 +2748,58 @@ var Tui = class _Tui {
2567
2748
  }
2568
2749
  // ─── input layout + vertical caret movement ────────────────────────────────
2569
2750
  /**
2570
- * The input's physical rows (same wrapping math as renderBottom: logical
2571
- * lines split on "\n", line 0 led by the prompt prefix, each wrapping at the
2572
- * terminal width) plus where the caret sits among them. Each row records the
2573
- * buffer index of its first character, its character count, and the visual
2574
- * column its first character renders at (only row 0 is offset, by the
2575
- * prompt). Drives ↑/↓: row 0 is "the top line" (history recall territory),
2576
- * anything below moves the caret instead.
2751
+ * The input's physical rows (same wrapping math as the boxed input body:
2752
+ * logical lines split on "\n", line 0 led by the prompt prefix, each wrapping
2753
+ * at the box content width) plus where the caret sits among them. Each row
2754
+ * records the buffer index of its first character, its character count, and
2755
+ * the visual column its first character renders at (only the first physical
2756
+ * row of line 0 is offset, by the prompt). Drives ↑/↓: row 0 is "the top
2757
+ * line" (history recall territory), anything below moves the caret instead.
2577
2758
  */
2578
2759
  inputLayout() {
2579
2760
  const cols2 = process.stdout.columns || 80;
2761
+ const { contentW } = inputBoxGeometry(cols2);
2580
2762
  const pw = this.visibleWidth(this.promptPrefix());
2581
2763
  const lines = this.inputBuffer.split("\n");
2582
2764
  const rows = [];
2583
2765
  let offset = 0;
2584
2766
  for (let i = 0; i < lines.length; i++) {
2585
- const lead = i === 0 ? pw : 0;
2586
2767
  const len = lines[i].length;
2587
- const nRows = Math.max(1, Math.ceil((lead + len) / cols2));
2588
- for (let j = 0; j < nRows; j++) {
2589
- const charStart = Math.max(0, j * cols2 - lead);
2590
- const charEnd = Math.min(len, (j + 1) * cols2 - lead);
2591
- rows.push({ start: offset + charStart, len: Math.max(0, charEnd - charStart), colOffset: j === 0 ? lead : 0 });
2592
- }
2768
+ let remaining = len;
2769
+ let charStart = 0;
2770
+ do {
2771
+ const avail = Math.max(0, contentW - pw);
2772
+ let take = Math.min(remaining, avail);
2773
+ if (take === 0 && avail === 0 && remaining > 0) take = 1;
2774
+ rows.push({ start: offset + charStart, len: take, colOffset: pw });
2775
+ charStart += take;
2776
+ remaining -= take;
2777
+ } while (remaining > 0);
2593
2778
  offset += len + 1;
2594
2779
  }
2595
- let pos = this.cursorPos;
2596
- let caretLine = 0;
2597
- while (caretLine < lines.length - 1 && pos > lines[caretLine].length) {
2598
- pos -= lines[caretLine].length + 1;
2599
- caretLine++;
2780
+ let caretRow = 0;
2781
+ let caretCol = 0;
2782
+ let found = false;
2783
+ for (let r = 0; r < rows.length; r++) {
2784
+ const row = rows[r];
2785
+ const rowEnd = row.start + row.len;
2786
+ const isLast = r === rows.length - 1;
2787
+ const nextStart = isLast ? rowEnd : rows[r + 1].start;
2788
+ if (this.cursorPos < rowEnd || this.cursorPos === rowEnd && (isLast || nextStart > rowEnd)) {
2789
+ caretRow = r;
2790
+ caretCol = row.colOffset + (this.cursorPos - row.start);
2791
+ found = true;
2792
+ break;
2793
+ }
2794
+ }
2795
+ if (!found && rows.length) {
2796
+ caretRow = rows.length - 1;
2797
+ const row = rows[caretRow];
2798
+ caretCol = row.colOffset + row.len;
2600
2799
  }
2601
- const caretCell = (caretLine === 0 ? pw : 0) + pos;
2602
- let caretRow = Math.floor(caretCell / cols2);
2603
- for (let i = 0; i < caretLine; i++) {
2604
- const lead = i === 0 ? pw : 0;
2605
- caretRow += Math.max(1, Math.ceil((lead + lines[i].length) / cols2));
2800
+ while (caretRow >= rows.length) {
2801
+ rows.push({ start: this.inputBuffer.length, len: 0, colOffset: 0 });
2606
2802
  }
2607
- const caretCol = caretCell % cols2;
2608
- while (caretRow >= rows.length) rows.push({ start: this.inputBuffer.length, len: 0, colOffset: 0 });
2609
2803
  return { rows, caretRow, caretCol, rowCount: rows.length };
2610
2804
  }
2611
2805
  /** Move the caret one visual row up/down, keeping the column when possible. */
@@ -2730,19 +2924,19 @@ var Tui = class _Tui {
2730
2924
  return parts.length ? `${C.gray}${parts.join(" ")}${C.reset}` : "";
2731
2925
  }
2732
2926
  /**
2733
- * Understated context gauge, right-aligned on the summary line: just "N%",
2734
- * dim, and INVISIBLE until the conversation is more than 85% of the way to
2735
- * the compaction point (the percentage is scaled so 100% = where background
2736
- * compaction triggers, not the raw model window). Most sessions never see it.
2927
+ * Context-to-compaction gauge, right-aligned on the status line: just "N%"
2928
+ * in dull grey. Shown whenever we know a value (any fill 5%, 10%, 20%…).
2929
+ * The percentage is scaled so 100% = where background compaction triggers,
2930
+ * not the raw model window.
2737
2931
  */
2738
2932
  contextGaugeText() {
2739
- if (this.contextPct == null || this.contextPct <= 85) return "";
2740
- return `${C.dim}${C.gray}${this.contextPct}%${C.reset}`;
2933
+ if (this.contextPct == null) return "";
2934
+ return `\x1B[38;5;240m${this.contextPct}%${C.reset}`;
2741
2935
  }
2742
2936
  /**
2743
2937
  * Set the context-window fill percentage (0–100), or null to hide it.
2744
- * Driven by the runtime's `context_usage` KV (latest request input tokens ÷
2745
- * model context window).
2938
+ * Driven by the runtime's `context_usage` KV, scaled to the compaction
2939
+ * trigger (see index.ts poll). Always painted on the status line far right.
2746
2940
  */
2747
2941
  setContextPct(pct) {
2748
2942
  const next = pct == null ? null : Math.max(0, Math.min(100, Math.round(pct)));
@@ -2940,10 +3134,14 @@ var Tui = class _Tui {
2940
3134
  if (up > 0) process.stdout.write(`\x1B[${up}A`);
2941
3135
  }
2942
3136
  /**
2943
- * Render the bottom region: an optional step line, then the prompt + input
2944
- * (wrapping across as many rows as needed), with the caret placed at cursorPos.
2945
- * Uses only relative cursor moves so it survives terminal scrolling when the
2946
- * region grows near the bottom of the screen.
3137
+ * Render the bottom region: status + subagents above a side-margined expanding
3138
+ * input box (level in the top border far right), with the caret on the input
3139
+ * body. Uses only relative cursor moves so it survives terminal scrolling when
3140
+ * the region grows near the bottom of the screen.
3141
+ *
3142
+ * Layout (top → bottom):
3143
+ * preview · ruler · notice · quit · status · subagents · goal ·
3144
+ * box top (level) · box body · box bottom · palette
2947
3145
  */
2948
3146
  renderBottom() {
2949
3147
  if (!this.started || this.takeoverHandler) return;
@@ -2959,69 +3157,66 @@ var Tui = class _Tui {
2959
3157
  };
2960
3158
  const previewLines = this.streamPreviewLines(cols2);
2961
3159
  for (const line of previewLines) writeHudRow(line);
2962
- const rulerRows = 1;
2963
3160
  writeHudRow(`${C.dim}${"\u2500".repeat(cols2)}${C.reset}`);
3161
+ const geo = inputBoxGeometry(cols2);
3162
+ const workPad = " ".repeat(geo.margin);
3163
+ const workCols = Math.max(8, cols2 - geo.margin);
2964
3164
  const noticeLine = this.connected ? null : `${C.yellow}\u26A0 lost connection to the workspace \u2014 reconnecting\u2026${C.reset}`;
2965
- const noticeRows = noticeLine ? 1 : 0;
2966
- if (noticeLine) writeHudRow(noticeLine);
3165
+ if (noticeLine) writeHudRow(workPad + noticeLine);
2967
3166
  const quitLine = this.quitArmed ? `${C.dim}Press Control-C again to exit${C.reset}` : null;
2968
- const quitRows = quitLine ? 1 : 0;
2969
- if (quitLine) writeHudRow(quitLine);
3167
+ if (quitLine) writeHudRow(workPad + quitLine);
3168
+ const statusLine = this.statusLineText(workCols);
3169
+ if (statusLine) writeHudRow(workPad + statusLine);
2970
3170
  const frame = FRAMES[Math.floor(Date.now() / SPINNER_MS) % FRAMES.length];
2971
3171
  for (const sub of this.subagents) {
2972
3172
  const color = sub.agentName === COMPACTION_AGENT ? COMPACTION_COLOR : SUBAGENT_COLORS[this.subagentColorByID.get(sub.id) ?? 0];
2973
- const budget = cols2 - 10;
3173
+ const budget = workCols - 10;
2974
3174
  let label = sub.label;
2975
3175
  if (budget < 1) label = "";
2976
3176
  else if (label.length > budget) label = label.slice(0, Math.max(0, budget - 1)) + "\u2026";
2977
3177
  const line = `${color}${frame}${C.reset} ${color}${label}${C.reset} ${C.dim}working${C.reset}`;
2978
- writeHudRow(line);
2979
- }
2980
- const statusLine = this.statusLineText(cols2);
2981
- const statusRows = statusLine ? 1 : 0;
2982
- if (statusLine) writeHudRow(statusLine);
2983
- const goalLines = this.goalLines(cols2);
2984
- for (const line of goalLines) writeHudRow(line);
2985
- const aboveRows = previewLines.length + rulerRows + noticeRows + quitRows + this.subagents.length + statusRows + goalLines.length;
3178
+ writeHudRow(workPad + line);
3179
+ }
3180
+ const goalLines = this.goalLines(workCols);
3181
+ for (const line of goalLines) writeHudRow(workPad + line);
2986
3182
  const prefix = this.promptPrefix();
2987
3183
  const pw = this.visibleWidth(prefix);
2988
- const lines = this.inputBuffer.split("\n");
2989
- const tail = [prefix + lines[0]];
2990
- for (let i = 1; i < lines.length; i++) tail.push("\r\n" + lines[i]);
2991
- const rowsOf = (len, lead) => Math.max(1, Math.ceil((lead + len) / cols2));
2992
- const lineRows = lines.map((l, i) => rowsOf(l.length, i === 0 ? pw : 0));
2993
- const inputRows = lineRows.reduce((a, b) => a + b, 0);
2994
- let pos = this.cursorPos;
2995
- let caretLine = 0;
2996
- while (caretLine < lines.length - 1 && pos > lines[caretLine].length) {
2997
- pos -= lines[caretLine].length + 1;
2998
- caretLine++;
2999
- }
3000
- const caretCell = (caretLine === 0 ? pw : 0) + pos;
3001
- let caretRow = Math.floor(caretCell / cols2);
3002
- for (let i = 0; i < caretLine; i++) caretRow += lineRows[i];
3003
- const caretCol = caretCell % cols2;
3184
+ const boxRows = buildInputBoxRows({
3185
+ cols: cols2,
3186
+ inputBuffer: this.inputBuffer,
3187
+ prefix,
3188
+ prefixWidth: pw,
3189
+ level: this.level,
3190
+ levelColor: this.levelColor(),
3191
+ borderColor: inputBoxBorderColor()
3192
+ });
3193
+ const boxTop = boxRows[0];
3194
+ const boxBottom = boxRows[boxRows.length - 1];
3195
+ const boxBody = boxRows.slice(1, -1);
3196
+ writeHudRow(boxTop);
3197
+ const aboveBodyWidths = hudWidths.slice();
3198
+ const aboveBodyRows = aboveBodyWidths.length;
3199
+ for (const row of boxBody) writeHudRow(row);
3200
+ writeHudRow(boxBottom);
3004
3201
  const paletteBlock = this.paletteBlockLines(cols2);
3005
- for (const line of paletteBlock) tail.push("\r\n" + line);
3006
- if (paletteBlock.length > 0) {
3007
- tail.push("\r");
3008
- const up = inputRows - 1 + paletteBlock.length - caretRow;
3009
- if (up > 0) tail.push(`\x1B[${up}A`);
3010
- if (caretCol > 0) tail.push(`\x1B[${caretCol}C`);
3011
- this.lastCursorRow = aboveRows + caretRow;
3012
- } else if (this.cursorPos < this.inputBuffer.length) {
3013
- tail.push("\r");
3014
- const up = inputRows - 1 - caretRow;
3015
- if (up > 0) tail.push(`\x1B[${up}A`);
3016
- if (caretCol > 0) tail.push(`\x1B[${caretCol}C`);
3017
- this.lastCursorRow = aboveRows + caretRow;
3018
- } else {
3019
- this.lastCursorRow = aboveRows + (inputRows - 1);
3020
- }
3021
- this.drawnHudWidths = hudWidths;
3202
+ for (const line of paletteBlock) writeHudRow(line);
3203
+ const layout = this.inputLayout();
3204
+ const bodyRowCount = Math.max(1, boxBody.length);
3205
+ const caretBodyRow = Math.max(0, Math.min(layout.caretRow, bodyRowCount - 1));
3206
+ const caretRegionRow = aboveBodyRows + caretBodyRow;
3207
+ const caretScreenCol = Math.min(rowCap - 1, geo.leftPad + layout.caretCol);
3208
+ this.lastCursorRow = caretRegionRow;
3209
+ this.drawnHudWidths = aboveBodyWidths;
3022
3210
  this.lastDrawnCols = cols2;
3023
3211
  this.bottomDrawn = true;
3024
- const out = "\x1B[J" + hudRows.join("\r\n") + "\r\n" + tail.join("");
3212
+ const totalRows = hudRows.length;
3213
+ const up = Math.max(0, totalRows - 1 - caretRegionRow);
3214
+ let out = "\x1B[J" + hudRows.join("\r\n");
3215
+ if (totalRows > 0) {
3216
+ out += "\r";
3217
+ if (up > 0) out += `\x1B[${up}A`;
3218
+ if (caretScreenCol > 0) out += `\x1B[${caretScreenCol}C`;
3219
+ }
3025
3220
  process.stdout.write(out);
3026
3221
  }
3027
3222
  clearBottom() {
@@ -3436,44 +3631,62 @@ var Tui = class _Tui {
3436
3631
  };
3437
3632
  });
3438
3633
  }
3439
- /** Arrow-key selection menu (slash menu, process menu, resume). Pauses input. */
3634
+ /**
3635
+ * Arrow-key selection menu (slash menu, process menu, resume). Pauses input.
3636
+ * Boxed chrome matches the bottom input HUD: side margin + grey rounded
3637
+ * border, selected row with a brand-tinted pointer.
3638
+ */
3440
3639
  select(title, items) {
3441
3640
  return new Promise((resolve) => {
3442
3641
  let idx = 0;
3443
3642
  this.beginTakeover();
3643
+ const cols2 = process.stdout.columns || 80;
3644
+ const geo = inputBoxGeometry(cols2);
3645
+ const border = inputBoxBorderColor();
3646
+ const pad = " ".repeat(geo.margin);
3647
+ const rowCap = Math.max(1, cols2 - 1);
3444
3648
  if (title) process.stdout.write(`
3445
- ${title}
3446
-
3649
+ ${pad}${title}
3447
3650
  `);
3448
- const renderLine = (i) => {
3449
- const w = process.stdout.columns || 80;
3651
+ else process.stdout.write("\n");
3652
+ const renderItemContent = (i) => {
3450
3653
  const it = items[i];
3451
3654
  const sel = i === idx;
3452
3655
  const hint = it.hint ?? "";
3453
3656
  const hintW = hint.length;
3454
3657
  const pointerW = 2;
3455
- const labelMax = Math.max(6, w - 2 - pointerW - (hintW ? hintW + 2 : 0));
3456
- let label = it.label;
3457
- if (label.length > labelMax) label = label.slice(0, labelMax - 1) + "\u2026";
3658
+ const labelMax = Math.max(4, geo.contentW - pointerW - (hintW ? hintW + 2 : 0));
3659
+ let label = it.label.replace(/\s+/g, " ").trim();
3660
+ if (label.length > labelMax) label = label.slice(0, Math.max(0, labelMax - 1)) + "\u2026";
3458
3661
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
3459
- const styledLabel = sel ? `${C.bold}${C.cyan}${label}${C.reset}` : label;
3460
- let line = `${pointer}${styledLabel}`;
3662
+ const styledLabel = sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
3663
+ let content = `${pointer}${styledLabel}`;
3461
3664
  if (hintW) {
3462
- const gap = Math.max(2, w - 2 - pointerW - label.length - hintW);
3463
- line += `${" ".repeat(gap)}${C.gray}${hint}${C.reset}`;
3665
+ const used = pointerW + label.length;
3666
+ const gap = Math.max(1, geo.contentW - used - hintW);
3667
+ content += `${" ".repeat(gap)}${sel ? C.gray : C.dim}${hint}${C.reset}`;
3464
3668
  }
3465
- return line;
3669
+ return content;
3466
3670
  };
3467
- const draw = (moveUp) => {
3468
- if (moveUp) process.stdout.write(`\x1B[${items.length}A`);
3469
- for (let i = 0; i < items.length; i++) process.stdout.write(`\r\x1B[K${renderLine(i)}
3671
+ const writeRow = (line) => {
3672
+ const row = this.clampVisible(sanitizeHudRow(line), rowCap);
3673
+ process.stdout.write(`\r\x1B[K${row}
3470
3674
  `);
3471
3675
  };
3676
+ const draw = (moveUp) => {
3677
+ if (moveUp) process.stdout.write(`\x1B[${items.length + 1}A`);
3678
+ else writeRow(pad + buildBoxTopPlain(geo.boxW, border));
3679
+ for (let i = 0; i < items.length; i++) {
3680
+ writeRow(pad + buildBoxBody(renderItemContent(i), geo.contentW, border));
3681
+ }
3682
+ writeRow(pad + buildBoxBottom(geo.boxW, border));
3683
+ };
3472
3684
  draw(false);
3473
- const titleRows = title ? 3 : 0;
3685
+ const titleRows = title ? 2 : 1;
3686
+ const boxRows = 1 + items.length + 1;
3474
3687
  const erase = () => {
3475
3688
  process.stdout.write("\r");
3476
- const up = titleRows + items.length;
3689
+ const up = titleRows + boxRows;
3477
3690
  if (up > 0) process.stdout.write(`\x1B[${up}A`);
3478
3691
  process.stdout.write("\x1B[J");
3479
3692
  };
@@ -4449,11 +4662,12 @@ function printAssistant(tui, text) {
4449
4662
  }
4450
4663
  function startLoader(label) {
4451
4664
  const frames = ["\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
4665
+ const pad = " ".repeat(INPUT_BOX_MARGIN);
4452
4666
  stdout.write("\x1B[?25l");
4453
4667
  const draw = () => {
4454
4668
  const now = Date.now();
4455
4669
  const f = frames[Math.floor(now / 70) % frames.length];
4456
- stdout.write(`\r\x1B[K${brandCycleColor(now)}${f}${c.reset} ${c.dim}${label}\u2026${c.reset}`);
4670
+ stdout.write(`\r\x1B[K${pad}${brandCycleColor(now)}${f}${c.reset} ${c.dim}${label}\u2026${c.reset}`);
4457
4671
  };
4458
4672
  draw();
4459
4673
  const timer = setInterval(draw, 70);
@@ -4776,7 +4990,7 @@ ${c.dim}Press Control-C again to exit${c.reset}
4776
4990
  const tilde = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
4777
4991
  const shortDir = tilde.length > 38 ? "\u2026" + tilde.slice(-37) : tilde;
4778
4992
  const picked = await tui.select(
4779
- `${c.bold}${c.magenta}Resume a session${c.reset} ${c.gray}${shortDir}${c.reset} ${c.dim}\u2191\u2193 \xB7 enter \xB7 esc to cancel${c.reset}`,
4993
+ `${c.bold}${gradientText("Resume a session")}${c.reset} ${c.gray}${shortDir}${c.reset} ${c.dim}\u2191\u2193 \xB7 enter \xB7 esc${c.reset}`,
4780
4994
  items
4781
4995
  );
4782
4996
  if (typeof picked === "string") {
@@ -5025,8 +5239,12 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5025
5239
  const agentTitlesReady = api.listAgents().then((list) => list.forEach((a) => agentTitles.set(a.name, a.title))).catch(() => {
5026
5240
  });
5027
5241
  const pushSubagents = () => tui.setSubagents(
5028
- [...activeSubagents.entries()].map(([id, s]) => ({ id, label: s.label, agentName: s.agentName }))
5242
+ [...activeSubagents.entries()].map(([id, s]) => {
5243
+ const detail = subActivity.phraseFor(id) ?? s.registryDetail;
5244
+ return { id, label: `${s.title}${detail ? ` \u2014 ${detail}` : ""}`, agentName: s.agentName };
5245
+ })
5029
5246
  );
5247
+ const subActivity = new SubagentActivity(api, () => pushSubagents());
5030
5248
  const reconcileSubagents = async () => {
5031
5249
  try {
5032
5250
  await agentTitlesReady;
@@ -5036,12 +5254,13 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5036
5254
  const status = (s.status || "").trim();
5037
5255
  if (status === "idle" || status === "terminated") continue;
5038
5256
  const oneLineStatus = status.replace(/\s+/g, " ");
5039
- const detail = oneLineStatus && oneLineStatus !== "running" ? ` \u2014 ${oneLineStatus.slice(0, 80)}` : "";
5040
5257
  activeSubagents.set(s.id, {
5041
- label: `${subagentLabel(s, agentTitles)}${detail}`,
5258
+ title: subagentLabel(s, agentTitles),
5259
+ registryDetail: oneLineStatus && oneLineStatus !== "running" ? oneLineStatus.slice(0, 80) : "",
5042
5260
  agentName: s.agent_name ?? void 0
5043
5261
  });
5044
5262
  }
5263
+ subActivity.sync(activeSubagents.keys());
5045
5264
  pushSubagents();
5046
5265
  } catch {
5047
5266
  }
@@ -5088,6 +5307,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5088
5307
  bridge.close();
5089
5308
  stream.close();
5090
5309
  events.close();
5310
+ subActivity.closeAll();
5091
5311
  mcp.closeAll();
5092
5312
  const [, killed2] = await Promise.race([
5093
5313
  Promise.all([stopped2, procsStopped2]),
@@ -5508,6 +5728,7 @@ ${c.bold}why: ${req.requestPermission}${c.reset}` : ""}`,
5508
5728
  bridge.close();
5509
5729
  stream.close();
5510
5730
  events.close();
5731
+ subActivity.closeAll();
5511
5732
  mcp.closeAll();
5512
5733
  const [, killed] = await Promise.race([
5513
5734
  Promise.all([stopped, procsStopped]),