@wrongstack/tui 1.0.7 → 1.0.9

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.
@@ -22,6 +22,16 @@ import type { AgentTranscriptReader, McpPickerItem, PluginPickerItem, ProviderOp
22
22
  * own. `app.tsx` re-exports `AppProps` for consumers importing it from
23
23
  * '@wrongstack/tui' / '../src/app.js'.
24
24
  */
25
+ /**
26
+ * The TUI's view of core's `LeaderAutoWakeController` (structural, so tests
27
+ * can pass a fake). The TUI binds a port for its foreground session.
28
+ */
29
+ export interface TuiLeaderAutoWake {
30
+ attachPort(port: import('@wrongstack/core/coordination').LeaderWakePort): () => void;
31
+ onRunFinished(sessionId: string): unknown;
32
+ onSessionDisplayed(sessionId: string): unknown;
33
+ noteUserInput(sessionId: string): void;
34
+ }
25
35
  export interface AppProps {
26
36
  agent: Agent;
27
37
  slashRegistry: SlashCommandRegistry;
@@ -50,6 +60,8 @@ export interface AppProps {
50
60
  * awareness — see core's queued-messages.ts). Display state is unaffected.
51
61
  */
52
62
  onQueueChange?: ((items: string[]) => void) | undefined;
63
+ /** Background-delegation auto-wake controller; see {@link TuiLeaderAutoWake}. */
64
+ leaderAutoWake?: TuiLeaderAutoWake | undefined;
53
65
  /** Reflects the policy's --yolo flag for the status bar's "⚠ YOLO" chip. */
54
66
  yolo?: boolean | undefined;
55
67
  /** Play terminal bell when an agent run completes. */
@@ -9,7 +9,16 @@ export interface AutonomyPickerProps {
9
9
  options: AutonomyOption[];
10
10
  selected: number;
11
11
  hint?: string | undefined;
12
+ /**
13
+ * Measured vertical budget for the whole picker box (terminal rows minus
14
+ * status bar, input, and margins — see `pickerMaxRows` in app-view.tsx).
15
+ * When provided the window math uses it instead of the
16
+ * `rows - shellReservedRows` guess, which under-reserves whenever the
17
+ * status bar or input bar grows — the same overflow class the resume
18
+ * panel had before it consumed this budget.
19
+ */
20
+ maxRows?: number | undefined;
12
21
  }
13
22
  export declare const AUTONOMY_OPTIONS: AutonomyOption[];
14
- export declare function AutonomyPicker({ options, selected, hint, }: AutonomyPickerProps): React.ReactElement;
23
+ export declare function AutonomyPicker({ options, selected, hint, maxRows, }: AutonomyPickerProps): React.ReactElement;
15
24
  //# sourceMappingURL=autonomy-picker.d.ts.map
@@ -4,6 +4,15 @@ interface DesignPickerProps {
4
4
  kits: DesignKitEntry[];
5
5
  selected: number;
6
6
  stack: string;
7
+ /**
8
+ * Measured vertical budget for the whole picker box (terminal rows minus
9
+ * status bar, input, and margins — see `pickerMaxRows` in app-view.tsx).
10
+ * When provided the window math uses it instead of the
11
+ * `rows - shellReservedRows` guess, which under-reserves whenever the
12
+ * status bar or input bar grows — the same overflow class the resume
13
+ * panel had before it consumed this budget.
14
+ */
15
+ maxRows?: number | undefined;
7
16
  }
8
17
  /**
9
18
  * Design Studio kit picker overlay (opened by `/design`). Presentational only —
@@ -13,6 +22,6 @@ interface DesignPickerProps {
13
22
  * Windowed via {@link useWindowedPicker} so 50+ installed kits stay
14
23
  * navigable on small terminals.
15
24
  */
16
- export declare function DesignPicker({ kits, selected, stack }: DesignPickerProps): React.ReactElement;
25
+ export declare function DesignPicker({ kits, selected, stack, maxRows, }: DesignPickerProps): React.ReactElement;
17
26
  export {};
18
27
  //# sourceMappingURL=design-picker.d.ts.map
@@ -6,6 +6,15 @@ interface ResumePickerProps {
6
6
  busy: boolean;
7
7
  error?: string | undefined;
8
8
  hint?: string | undefined;
9
+ /**
10
+ * Measured vertical budget for the whole picker box (terminal rows minus
11
+ * status bar, input, and margins — see `pickerMaxRows` in app-view.tsx).
12
+ * When provided the window math uses it instead of the legacy
13
+ * `rows - shellReservedRows` guess, which reserved less than the real
14
+ * status bar + input bar chrome — the resume panel could then grow past
15
+ * the terminal on ordinary session counts.
16
+ */
17
+ maxRows?: number | undefined;
9
18
  }
10
19
  /**
11
20
  * Interactive session-resume picker. Renders a scrollable list of recent
@@ -16,6 +25,6 @@ interface ResumePickerProps {
16
25
  * three rows (title line, meta line, preview line). The window auto-recenters
17
26
  * on the focused session so 30+ entries stay navigable on a 24-row terminal.
18
27
  */
19
- export declare function ResumePicker({ sessions, selected, busy, error, hint, }: ResumePickerProps): React.ReactElement;
28
+ export declare function ResumePicker({ sessions, selected, busy, error, hint, maxRows, }: ResumePickerProps): React.ReactElement;
20
29
  export {};
21
30
  //# sourceMappingURL=resume-picker.d.ts.map
@@ -2,6 +2,7 @@ import type React from 'react';
2
2
  /**
3
3
  * Head-truncate a chip's free-text payload (branch, path, project name) with a
4
4
  * trailing ellipsis so one long value can't blow out the line width.
5
+ * Caps on terminal columns, not UTF-16 length (CJK/emoji are 2 cells).
5
6
  */
6
7
  export declare function truncateChip(text: string, max: number): string;
7
8
  /**
@@ -17,7 +17,7 @@ export interface AppExecutionPipelineArgs {
17
17
  }
18
18
  export declare function useAppExecutionPipeline(args: AppExecutionPipelineArgs): {
19
19
  handleKey: (input: string, key: import("../components/input.js").KeyEvent) => Promise<void>;
20
- runBlocks: (blocks: import("@wrongstack/core/types").ContentBlock[]) => Promise<void>;
20
+ runBlocks: (blocks: import("@wrongstack/core/types").ContentBlock[], opts?: import("../tui-host-capabilities.js").RunBlocksOptions) => Promise<void>;
21
21
  submit: (overrideRaw?: string) => Promise<void>;
22
22
  stableOnKey: (input: string, key: import("../components/input.js").KeyEvent) => Promise<void>;
23
23
  };
@@ -0,0 +1,41 @@
1
+ import type { LeaderWakePort } from '@wrongstack/core/coordination';
2
+ import type { EventBus } from '@wrongstack/core/kernel';
3
+ import type { ContentBlock } from '@wrongstack/core/types';
4
+ import type { Action, State } from '../app-reducer.js';
5
+ import type { TuiLeaderAutoWake } from '../app-props.js';
6
+ import type { RunBlocksOptions } from '../tui-host-capabilities.js';
7
+ interface Cell<T> {
8
+ current: T;
9
+ }
10
+ export interface TuiLeaderWakePortDeps {
11
+ getSessionId(): string | undefined;
12
+ activeController: Cell<AbortController | null>;
13
+ eternalLoopRunning: Cell<boolean>;
14
+ parallelLoopRunning: Cell<boolean>;
15
+ enhanceAbort?: Cell<AbortController | null> | undefined;
16
+ state: Cell<Pick<State, 'queue' | 'confirmQueue'>>;
17
+ /** Open structured user-input forms (user.input_requested not yet resolved). */
18
+ pendingForms: Cell<number>;
19
+ runBlocks: Cell<(blocks: ContentBlock[], opts?: RunBlocksOptions) => Promise<void>>;
20
+ }
21
+ /**
22
+ * The foreground TUI's wake port. The TUI shows exactly one session — the
23
+ * agent's current one — so open and displayed both mean "is that session".
24
+ * Idle is the race-free busy signal `runBlocks` itself uses (the controller
25
+ * cell), plus the autonomy loop drivers that call the engine directly.
26
+ */
27
+ export declare function createTuiLeaderWakePort(deps: TuiLeaderWakePortDeps): LeaderWakePort;
28
+ export interface UseLeaderAutoWakeArgs extends Omit<TuiLeaderWakePortDeps, 'pendingForms'> {
29
+ leaderAutoWake?: TuiLeaderAutoWake | undefined;
30
+ events: EventBus;
31
+ dispatch(action: Action): void;
32
+ }
33
+ /**
34
+ * Bind the auto-wake controller to the foreground TUI session and surface its
35
+ * activity in history: a notice line when a wake fires (the turn's own
36
+ * turn-summary line carries its token/cost), and a warning when the chain cap
37
+ * holds results until the user sends a message.
38
+ */
39
+ export declare function useLeaderAutoWake(args: UseLeaderAutoWakeArgs): void;
40
+ export {};
41
+ //# sourceMappingURL=use-leader-auto-wake.d.ts.map
package/dist/index.js CHANGED
@@ -342,7 +342,7 @@ var init_connections_health = __esm({
342
342
  });
343
343
 
344
344
  // src/app.tsx
345
- import { useCallback as useCallback31, useEffect as useEffect79 } from "react";
345
+ import { useCallback as useCallback31, useEffect as useEffect80 } from "react";
346
346
 
347
347
  // src/hooks/use-terminal-size.ts
348
348
  import { onResize } from "@wrongstack/core/utils";
@@ -3339,6 +3339,93 @@ function EmptyPanelState({
3339
3339
  // src/components/status-bar-format.tsx
3340
3340
  import { isValidElement } from "react";
3341
3341
 
3342
+ // src/terminal-width.ts
3343
+ import stringWidth from "string-width";
3344
+ var ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
3345
+ var MAX_STRING_BODY = 4096;
3346
+ var ANSI_OSC_RE = new RegExp(
3347
+ `\\x1b\\][^\\x07\\x1b]{0,${MAX_STRING_BODY}}(?:\\x07|\\x1b\\\\)`,
3348
+ "g"
3349
+ );
3350
+ var ANSI_CONTROL_STRING_RE = new RegExp(
3351
+ `\\x1b[P^_X][^\\x1b]{0,${MAX_STRING_BODY}}\\x1b\\\\`,
3352
+ "g"
3353
+ );
3354
+ var ANSI_ESCAPE_RE = /\x1b[ -/]*[@-~]/g;
3355
+ var GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
3356
+ var LIGATURE_PAIRS = /* @__PURE__ */ new Set(["->", "<-", "=>", "<=", ">=", "!=", "==", "~>", "<~"]);
3357
+ var LIGATURE_SEPARATOR = " ";
3358
+ function breakTerminalLigatures(value) {
3359
+ if (!/[-<=>!~]/.test(value)) return value;
3360
+ let safe = "";
3361
+ let previous = "";
3362
+ for (const char of value) {
3363
+ if (previous && LIGATURE_PAIRS.has(`${previous}${char}`)) safe += LIGATURE_SEPARATOR;
3364
+ safe += char;
3365
+ previous = char;
3366
+ }
3367
+ return safe;
3368
+ }
3369
+ function sanitizeTerminalText(value, tabWidth = 2) {
3370
+ const tab = " ".repeat(Math.max(1, Math.min(8, Math.floor(tabWidth))));
3371
+ const withoutEscapes = value.replace(ANSI_OSC_RE, "").replace(ANSI_CONTROL_STRING_RE, "").replace(ANSI_RE, "").replace(ANSI_ESCAPE_RE, "").replace(/\t/g, tab).replace(/\r/g, "");
3372
+ let safe = "";
3373
+ for (const char of withoutEscapes) {
3374
+ const code = char.codePointAt(0) ?? 0;
3375
+ if (char === "\n" || code >= 32 && code !== 127 && !(code >= 128 && code <= 159)) {
3376
+ safe += char;
3377
+ }
3378
+ }
3379
+ return safe;
3380
+ }
3381
+ function displayWidth(value) {
3382
+ return stringWidth(value);
3383
+ }
3384
+ function truncateDisplay(value, maxWidth, ellipsis = "\u2026") {
3385
+ if (maxWidth <= 0) return "";
3386
+ if (displayWidth(value) <= maxWidth) return value;
3387
+ const ellipsisWidth = displayWidth(ellipsis);
3388
+ if (ellipsisWidth >= maxWidth) return ellipsis;
3389
+ let out = "";
3390
+ let used = 0;
3391
+ for (const { segment } of GRAPHEME_SEGMENTER.segment(value)) {
3392
+ const segmentWidth = displayWidth(segment);
3393
+ if (used + segmentWidth + ellipsisWidth > maxWidth) break;
3394
+ out += segment;
3395
+ used += segmentWidth;
3396
+ }
3397
+ return `${out}${ellipsis}`;
3398
+ }
3399
+ function truncateDisplayStart(value, maxWidth, ellipsis = "\u2026") {
3400
+ if (maxWidth <= 0) return "";
3401
+ if (displayWidth(value) <= maxWidth) return value;
3402
+ const ellipsisWidth = displayWidth(ellipsis);
3403
+ if (ellipsisWidth >= maxWidth) return ellipsis;
3404
+ const segments = [...GRAPHEME_SEGMENTER.segment(value)].map(({ segment }) => segment);
3405
+ let out = "";
3406
+ let used = 0;
3407
+ for (let index = segments.length - 1; index >= 0; index--) {
3408
+ const segment = segments[index] ?? "";
3409
+ const segmentWidth = displayWidth(segment);
3410
+ if (used + segmentWidth + ellipsisWidth > maxWidth) break;
3411
+ out = `${segment}${out}`;
3412
+ used += segmentWidth;
3413
+ }
3414
+ return `${ellipsis}${out}`;
3415
+ }
3416
+ function splitDisplay(value, maxWidth) {
3417
+ if (maxWidth <= 0) return ["", value];
3418
+ let end = 0;
3419
+ let used = 0;
3420
+ for (const { segment, index } of GRAPHEME_SEGMENTER.segment(value)) {
3421
+ const segmentWidth = displayWidth(segment);
3422
+ if (used + segmentWidth > maxWidth) break;
3423
+ used += segmentWidth;
3424
+ end = index + segment.length;
3425
+ }
3426
+ return [value.slice(0, end), value.slice(end)];
3427
+ }
3428
+
3342
3429
  // src/thinking-word.ts
3343
3430
  var DEFAULT_TUI_THINKING_WORD = "thinking";
3344
3431
  var MAX_TUI_THINKING_WORD_LENGTH = 16;
@@ -3392,7 +3479,7 @@ function pickRandomTuiThinkingWord(previous) {
3392
3479
 
3393
3480
  // src/components/status-bar-format.tsx
3394
3481
  function truncateChip(text, max) {
3395
- return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
3482
+ return truncateDisplay(text, max);
3396
3483
  }
3397
3484
  function shortenPath(path12, segments) {
3398
3485
  const parts = path12.split(/[\\/]+/).filter(Boolean);
@@ -3481,7 +3568,8 @@ function fmtMemory(bytes) {
3481
3568
  return `${Math.round(bytes)}B`;
3482
3569
  }
3483
3570
  function fmtElapsed(ms) {
3484
- const totalSec = Math.floor(ms / 1e3);
3571
+ const safeMs = Number.isFinite(ms) && ms > 0 ? ms : 0;
3572
+ const totalSec = Math.floor(safeMs / 1e3);
3485
3573
  const h = Math.floor(totalSec / 3600);
3486
3574
  const m = Math.floor(totalSec % 3600 / 60);
3487
3575
  const s2 = totalSec % 60;
@@ -8482,95 +8570,6 @@ function useTokenCounterRefresh(tokenCounter, events, sessionId) {
8482
8570
 
8483
8571
  // src/components/powerline-rail.tsx
8484
8572
  import { isValidElement as isValidElement2 } from "react";
8485
-
8486
- // src/terminal-width.ts
8487
- import stringWidth from "string-width";
8488
- var ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
8489
- var MAX_STRING_BODY = 4096;
8490
- var ANSI_OSC_RE = new RegExp(
8491
- `\\x1b\\][^\\x07\\x1b]{0,${MAX_STRING_BODY}}(?:\\x07|\\x1b\\\\)`,
8492
- "g"
8493
- );
8494
- var ANSI_CONTROL_STRING_RE = new RegExp(
8495
- `\\x1b[P^_X][^\\x1b]{0,${MAX_STRING_BODY}}\\x1b\\\\`,
8496
- "g"
8497
- );
8498
- var ANSI_ESCAPE_RE = /\x1b[ -/]*[@-~]/g;
8499
- var GRAPHEME_SEGMENTER = new Intl.Segmenter(void 0, { granularity: "grapheme" });
8500
- var LIGATURE_PAIRS = /* @__PURE__ */ new Set(["->", "<-", "=>", "<=", ">=", "!=", "==", "~>", "<~"]);
8501
- var LIGATURE_SEPARATOR = " ";
8502
- function breakTerminalLigatures(value) {
8503
- if (!/[-<=>!~]/.test(value)) return value;
8504
- let safe = "";
8505
- let previous = "";
8506
- for (const char of value) {
8507
- if (previous && LIGATURE_PAIRS.has(`${previous}${char}`)) safe += LIGATURE_SEPARATOR;
8508
- safe += char;
8509
- previous = char;
8510
- }
8511
- return safe;
8512
- }
8513
- function sanitizeTerminalText(value, tabWidth = 2) {
8514
- const tab = " ".repeat(Math.max(1, Math.min(8, Math.floor(tabWidth))));
8515
- const withoutEscapes = value.replace(ANSI_OSC_RE, "").replace(ANSI_CONTROL_STRING_RE, "").replace(ANSI_RE, "").replace(ANSI_ESCAPE_RE, "").replace(/\t/g, tab).replace(/\r/g, "");
8516
- let safe = "";
8517
- for (const char of withoutEscapes) {
8518
- const code = char.codePointAt(0) ?? 0;
8519
- if (char === "\n" || code >= 32 && code !== 127 && !(code >= 128 && code <= 159)) {
8520
- safe += char;
8521
- }
8522
- }
8523
- return safe;
8524
- }
8525
- function displayWidth(value) {
8526
- return stringWidth(value);
8527
- }
8528
- function truncateDisplay(value, maxWidth, ellipsis = "\u2026") {
8529
- if (maxWidth <= 0) return "";
8530
- if (displayWidth(value) <= maxWidth) return value;
8531
- const ellipsisWidth = displayWidth(ellipsis);
8532
- if (ellipsisWidth >= maxWidth) return ellipsis;
8533
- let out = "";
8534
- let used = 0;
8535
- for (const { segment } of GRAPHEME_SEGMENTER.segment(value)) {
8536
- const segmentWidth = displayWidth(segment);
8537
- if (used + segmentWidth + ellipsisWidth > maxWidth) break;
8538
- out += segment;
8539
- used += segmentWidth;
8540
- }
8541
- return `${out}${ellipsis}`;
8542
- }
8543
- function truncateDisplayStart(value, maxWidth, ellipsis = "\u2026") {
8544
- if (maxWidth <= 0) return "";
8545
- if (displayWidth(value) <= maxWidth) return value;
8546
- const ellipsisWidth = displayWidth(ellipsis);
8547
- if (ellipsisWidth >= maxWidth) return ellipsis;
8548
- const segments = [...GRAPHEME_SEGMENTER.segment(value)].map(({ segment }) => segment);
8549
- let out = "";
8550
- let used = 0;
8551
- for (let index = segments.length - 1; index >= 0; index--) {
8552
- const segment = segments[index] ?? "";
8553
- const segmentWidth = displayWidth(segment);
8554
- if (used + segmentWidth + ellipsisWidth > maxWidth) break;
8555
- out = `${segment}${out}`;
8556
- used += segmentWidth;
8557
- }
8558
- return `${ellipsis}${out}`;
8559
- }
8560
- function splitDisplay(value, maxWidth) {
8561
- if (maxWidth <= 0) return ["", value];
8562
- let end = 0;
8563
- let used = 0;
8564
- for (const { segment, index } of GRAPHEME_SEGMENTER.segment(value)) {
8565
- const segmentWidth = displayWidth(segment);
8566
- if (used + segmentWidth > maxWidth) break;
8567
- used += segmentWidth;
8568
- end = index + segment.length;
8569
- }
8570
- return [value.slice(0, end), value.slice(end)];
8571
- }
8572
-
8573
- // src/components/powerline-rail.tsx
8574
8573
  import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
8575
8574
  function visibleNodeText(node) {
8576
8575
  if (node == null || typeof node === "boolean") return "";
@@ -12983,12 +12982,17 @@ var AUTONOMY_OPTIONS = [
12983
12982
  function AutonomyPicker({
12984
12983
  options,
12985
12984
  selected,
12986
- hint
12985
+ hint,
12986
+ maxRows
12987
12987
  }) {
12988
12988
  const { start, end, hasAbove, hasBelow } = useWindowedPicker({
12989
12989
  total: options.length,
12990
12990
  selected,
12991
- chromeRows: 4
12991
+ chromeRows: 4,
12992
+ // 2 marker slots + 1 conditional hint slot — worst case, so the window
12993
+ // never overflows whether or not those rows actually render.
12994
+ markerRows: 3,
12995
+ maxRows
12992
12996
  });
12993
12997
  const visibleOptions = options.slice(start, end);
12994
12998
  return /* @__PURE__ */ jsxs34(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [
@@ -14384,8 +14388,8 @@ function brainRiskColor(risk) {
14384
14388
 
14385
14389
  // src/components/history/basic-format.ts
14386
14390
  function shortenPath2(p, max) {
14387
- if (p.length <= max) return p;
14388
- return `\u2026${p.slice(p.length - (max - 1))}`;
14391
+ if (displayWidth(p) <= max) return p;
14392
+ return truncateDisplayStart(p, max);
14389
14393
  }
14390
14394
  function fmtTok3(n) {
14391
14395
  if (!Number.isFinite(n) || n <= 0) return "0";
@@ -14394,9 +14398,10 @@ function fmtTok3(n) {
14394
14398
  return String(n);
14395
14399
  }
14396
14400
  function fmtDuration2(ms) {
14397
- if (ms < 1e3) return `${ms}ms`;
14398
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
14399
- const totalSec = Math.floor(ms / 1e3);
14401
+ const safeMs = Number.isFinite(ms) && ms > 0 ? ms : 0;
14402
+ if (safeMs < 1e3) return `${safeMs}ms`;
14403
+ if (safeMs < 6e4) return `${(safeMs / 1e3).toFixed(1)}s`;
14404
+ const totalSec = Math.floor(safeMs / 1e3);
14400
14405
  return `${Math.floor(totalSec / 60)}m${totalSec % 60}s`;
14401
14406
  }
14402
14407
  function fmtBytes(n) {
@@ -14405,8 +14410,7 @@ function fmtBytes(n) {
14405
14410
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
14406
14411
  }
14407
14412
  function truncMid(s2, max) {
14408
- if (s2.length <= max) return s2;
14409
- return `${s2.slice(0, max - 1)}\u2026`;
14413
+ return truncateDisplay(s2, max);
14410
14414
  }
14411
14415
  function stringOf(v) {
14412
14416
  return typeof v === "string" && v.length > 0 ? v : void 0;
@@ -16058,6 +16062,7 @@ function formatToolOutput(toolName, output, ok, _outputBytes, outputLines) {
16058
16062
  const duration = numOf(o["duration_ms"]);
16059
16063
  const head = [];
16060
16064
  if (runner && runner !== "none") head.push(runner);
16065
+ if (o["status"] === "no_tests") return [[...head, "no tests"].join(" \xB7 ")];
16061
16066
  head.push(`${passed}/${total} passed`);
16062
16067
  if (failed > 0) head.push(`${failed} failed`);
16063
16068
  if (duration !== void 0) head.push(fmtDuration2(duration));
@@ -16433,7 +16438,7 @@ function visualVerifier(toolName, text, ok) {
16433
16438
  errors > 0 ? `${errors} error${errors === 1 ? "" : "s"}` : void 0,
16434
16439
  warnings > 0 ? `${warnings} warning${warnings === 1 ? "" : "s"}` : void 0,
16435
16440
  changed2 > 0 ? `${changed2} changed` : void 0,
16436
- toolName === "test" ? `${numOf(obj["passed"]) ?? 0}/${numOf(obj["tests_run"]) ?? 0} passed` : void 0
16441
+ toolName === "test" ? obj["status"] === "no_tests" ? "no tests" : `${numOf(obj["passed"]) ?? 0}/${numOf(obj["tests_run"]) ?? 0} passed` : void 0
16437
16442
  ].filter(Boolean);
16438
16443
  return [
16439
16444
  {
@@ -17572,7 +17577,12 @@ function CoordinatorPanel({
17572
17577
 
17573
17578
  // src/components/design-picker.tsx
17574
17579
  import { Fragment as Fragment17, jsx as jsx50, jsxs as jsxs48 } from "react/jsx-runtime";
17575
- function DesignPicker({ kits, selected, stack }) {
17580
+ function DesignPicker({
17581
+ kits,
17582
+ selected,
17583
+ stack,
17584
+ maxRows
17585
+ }) {
17576
17586
  const { start, end, hasAbove, hasBelow } = useWindowedPicker({
17577
17587
  total: kits.length,
17578
17588
  selected,
@@ -17580,7 +17590,8 @@ function DesignPicker({ kits, selected, stack }) {
17580
17590
  // 2 marker slots — the `… more above/below` rows render below the header
17581
17591
  // and hint but were never counted in the chrome, so a windowed list
17582
17592
  // overflowed its own budget by 2 rows on short terminals.
17583
- markerRows: 2
17593
+ markerRows: 2,
17594
+ maxRows
17584
17595
  });
17585
17596
  const visibleKits = kits.slice(start, end);
17586
17597
  return /* @__PURE__ */ jsxs48(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [
@@ -19844,13 +19855,16 @@ function ResumePicker({
19844
19855
  selected,
19845
19856
  busy,
19846
19857
  error,
19847
- hint
19858
+ hint,
19859
+ maxRows
19848
19860
  }) {
19849
19861
  const { start, end, hasAbove, hasBelow } = useWindowedPicker({
19850
19862
  total: sessions.length,
19851
19863
  selected,
19852
19864
  rowSpan: 3,
19853
- chromeRows: 4
19865
+ chromeRows: 4,
19866
+ markerRows: 3,
19867
+ maxRows
19854
19868
  });
19855
19869
  const visibleSessions = sessions.slice(start, end);
19856
19870
  return /* @__PURE__ */ jsxs65(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
@@ -22214,7 +22228,8 @@ function AppViewPickers({
22214
22228
  {
22215
22229
  options: state.autonomyPicker.options,
22216
22230
  selected: state.autonomyPicker.selected,
22217
- hint: state.autonomyPicker.hint
22231
+ hint: state.autonomyPicker.hint,
22232
+ maxRows: pickerMaxRows
22218
22233
  }
22219
22234
  ) : null,
22220
22235
  state.modePicker.open ? /* @__PURE__ */ jsx81(
@@ -22264,7 +22279,8 @@ function AppViewPickers({
22264
22279
  {
22265
22280
  kits: state.designPicker.kits,
22266
22281
  selected: state.designPicker.selected,
22267
- stack: state.designPicker.stack
22282
+ stack: state.designPicker.stack,
22283
+ maxRows: pickerMaxRows
22268
22284
  }
22269
22285
  ) : null,
22270
22286
  state.promptPicker.open ? /* @__PURE__ */ jsx81(
@@ -22290,7 +22306,8 @@ function AppViewPickers({
22290
22306
  selected: state.resumePicker.selected,
22291
22307
  busy: state.resumePicker.busy,
22292
22308
  error: state.resumePicker.error,
22293
- hint: state.resumePicker.hint
22309
+ hint: state.resumePicker.hint,
22310
+ maxRows: pickerMaxRows
22294
22311
  }
22295
22312
  ) : null,
22296
22313
  state.settingsPicker.open ? /* @__PURE__ */ jsx81(
@@ -25299,6 +25316,10 @@ var INLINE_TOKEN_SRC = `${ATTACHMENT_TOKEN_SRC}|${PROTOCOL_TOKEN_SRC}`;
25299
25316
  var AT_END = new RegExp(`(?:${INLINE_TOKEN_SRC})$`, "i");
25300
25317
  var AT_START = new RegExp(`^(?:${INLINE_TOKEN_SRC})`, "i");
25301
25318
  var GLOBAL = new RegExp(INLINE_TOKEN_SRC, "gi");
25319
+ function rowWouldOverflow(rowChars, next, width) {
25320
+ if (!rowChars) return false;
25321
+ return displayWidth(rowChars + next) > width;
25322
+ }
25302
25323
  function deleteTokenBackward(buffer, cursor) {
25303
25324
  const m = buffer.slice(0, cursor).match(AT_END);
25304
25325
  if (!m) return null;
@@ -25354,24 +25375,30 @@ function layoutInputRows(prompt, value, cursor, width) {
25354
25375
  }
25355
25376
  const rows = [];
25356
25377
  let row = [];
25378
+ let rowChars = "";
25357
25379
  for (const cell of cells) {
25358
25380
  if (cell.ch === "\n") {
25359
25381
  if (cell.cursor) {
25360
- if (row.length >= w) {
25382
+ if (rowWouldOverflow(rowChars, " ", w)) {
25361
25383
  rows.push(row);
25362
25384
  row = [];
25385
+ rowChars = "";
25363
25386
  }
25364
25387
  row.push({ ch: " ", chip: false, prompt: false, cursor: true });
25388
+ rowChars += " ";
25365
25389
  }
25366
25390
  rows.push(row);
25367
25391
  row = [];
25392
+ rowChars = "";
25368
25393
  continue;
25369
25394
  }
25370
- row.push(cell);
25371
- if (row.length >= w) {
25395
+ if (rowWouldOverflow(rowChars, cell.ch, w)) {
25372
25396
  rows.push(row);
25373
25397
  row = [];
25398
+ rowChars = "";
25374
25399
  }
25400
+ row.push(cell);
25401
+ rowChars += cell.ch;
25375
25402
  }
25376
25403
  if (row.length > 0 || rows.length === 0) rows.push(row);
25377
25404
  return rows;
@@ -25384,23 +25411,27 @@ function inputIndexAtRowCol(prompt, value, width, row, col) {
25384
25411
  const rows = [];
25385
25412
  const starts = [];
25386
25413
  let cur = [];
25414
+ let curChars = "";
25387
25415
  let curStart = 0;
25388
25416
  for (const cell of flat) {
25389
25417
  if (cell.ch === "\n") {
25390
25418
  rows.push(cur);
25391
25419
  starts.push(curStart);
25392
25420
  cur = [];
25421
+ curChars = "";
25393
25422
  curStart = cell.buf + 1;
25394
25423
  continue;
25395
25424
  }
25396
- if (cur.length === 0 && cell.buf >= 0) curStart = cell.buf;
25397
- cur.push({ buf: cell.buf });
25398
- if (cur.length >= w) {
25425
+ if (rowWouldOverflow(curChars, cell.ch, w)) {
25399
25426
  rows.push(cur);
25400
25427
  starts.push(curStart);
25401
25428
  cur = [];
25429
+ curChars = "";
25402
25430
  curStart = -1;
25403
25431
  }
25432
+ if (cur.length === 0 && cell.buf >= 0) curStart = cell.buf;
25433
+ cur.push({ buf: cell.buf, ch: cell.ch });
25434
+ curChars += cell.ch;
25404
25435
  }
25405
25436
  if (cur.length > 0 || rows.length === 0) {
25406
25437
  rows.push(cur);
@@ -25411,9 +25442,14 @@ function inputIndexAtRowCol(prompt, value, width, row, col) {
25411
25442
  if (row >= rows.length) return value.length;
25412
25443
  const r = rows[row];
25413
25444
  const c = Math.max(0, col);
25414
- if (c < r.length) {
25415
- const b = r[c].buf;
25416
- return b < 0 ? 0 : clamp2(b);
25445
+ let visual = 0;
25446
+ for (const cell of r) {
25447
+ const span = displayWidth(cell.ch);
25448
+ const hit = span <= 0 ? visual === c : c >= visual && c < visual + span;
25449
+ if (hit) {
25450
+ return cell.buf < 0 ? 0 : clamp2(cell.buf);
25451
+ }
25452
+ visual += span;
25417
25453
  }
25418
25454
  for (let k = r.length - 1; k >= 0; k--) {
25419
25455
  const b = r[k].buf;
@@ -26643,7 +26679,9 @@ function statusBarLineRow(opts) {
26643
26679
  }
26644
26680
 
26645
26681
  // src/layout-engine.ts
26646
- var LAYOUT_VERSION = 2;
26682
+ import { stripVTControlCharacters } from "node:util";
26683
+ var CONTROL_STRING = /(?:(?:\x1b\]|\x9d)[\s\S]*?(?:\x07|\x1b\\|\x9c|$)|(?:\x1b[PX^_]|[\x90\x98\x9e\x9f])[\s\S]*?(?:\x1b\\|\x9c|$))/g;
26684
+ var LAYOUT_VERSION = 3;
26647
26685
  var FALLBACK_ROWS = 3;
26648
26686
  var MAX_ESTIMATE_ROWS = 2e3;
26649
26687
  var MIN_CONTENT_WIDTH = 16;
@@ -26654,16 +26692,16 @@ function wrappedRows(text, width, maxRows = MAX_ESTIMATE_ROWS) {
26654
26692
  let lineStart = 0;
26655
26693
  for (let index = 0; index <= text.length; index++) {
26656
26694
  if (index < text.length && text.charCodeAt(index) !== 10) continue;
26657
- const lineLen = index - lineStart;
26658
- rows += lineLen === 0 ? 1 : Math.max(1, Math.ceil(lineLen / w));
26695
+ const line = text.slice(lineStart, index);
26696
+ const cols = displayWidth(line);
26697
+ rows += line.length === 0 ? 1 : Math.max(1, Math.ceil(cols / w));
26659
26698
  if (rows >= maxRows) return maxRows;
26660
26699
  lineStart = index + 1;
26661
26700
  }
26662
26701
  return Math.max(1, rows);
26663
26702
  }
26664
26703
  function visibleChars(text) {
26665
- const cleaned = text.replace(/\x1b\[[0-9;]*m/g, "");
26666
- return cleaned.length;
26704
+ return [...stripVTControlCharacters(text.replace(CONTROL_STRING, ""))].length;
26667
26705
  }
26668
26706
  var MAX_MEMORY_PROOF_ENTRIES = 4;
26669
26707
  function computeEntryRows(kind, text, termWidth, meta) {
@@ -26676,12 +26714,8 @@ function computeEntryRows(kind, text, termWidth, meta) {
26676
26714
  case "user": {
26677
26715
  const contentWidth = w - 2;
26678
26716
  const baseText = text;
26679
- const lines = baseText.split("\n");
26680
- let rows = 0;
26681
- for (const line of lines) {
26682
- rows += Math.max(1, Math.ceil(line.length / contentWidth));
26683
- }
26684
- return Math.max(1, rows + 1);
26717
+ const bodyRows = wrappedRows(baseText, contentWidth);
26718
+ return Math.max(1, bodyRows + 1);
26685
26719
  }
26686
26720
  // ── Assistant entry ──
26687
26721
  // Header (1) + body (wrapped at contentWidth) + chrome (2: border + padding)
@@ -33805,9 +33839,9 @@ function validatePasteContent(content) {
33805
33839
  var BEGIN = "[200~";
33806
33840
  var END = "[201~";
33807
33841
  var OVERFLOW_STATE = Object.freeze({ overflow: true });
33808
- var BEGIN_RE = /\x1b?\[200~/g;
33809
- var END_RE = /\x1b?\[201~/g;
33810
- var PARTIAL_ANSI_RE = /^\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/;
33842
+ var BEGIN_RE = /\x1b?\[200~/;
33843
+ var END_RE = /\x1b?\[201~/;
33844
+ var PARTIAL_ANSI_RE = /^\[[\x30-\x3f]*[\x20-\x2f]*[mHJKfA-Dhlnsu]$/;
33811
33845
  var ANSI_RE2 = new RegExp(
33812
33846
  [
33813
33847
  // CSI: ESC [ params* intermediates* final
@@ -33847,15 +33881,23 @@ function feedPaste(accum, input) {
33847
33881
  }
33848
33882
  }
33849
33883
  const combined = `${accum ?? ""}${input}`;
33850
- const stripped = combined.replace(BEGIN_RE, "").replace(END_RE, "").replace(ANSI_RE2, "");
33884
+ const stripOpeningBegin = accum === null || typeof accum === "string" && isMarkerPrefix(accum);
33885
+ let working = stripOpeningBegin ? combined.replace(BEGIN_RE, "") : combined;
33886
+ const endMatches = [...working.matchAll(new RegExp(END_RE, "g"))];
33887
+ const closed = endMatches.length > 0;
33888
+ if (closed) {
33889
+ const last = endMatches[endMatches.length - 1];
33890
+ if (last && last.index !== void 0) working = working.slice(0, last.index);
33891
+ }
33892
+ const stripped = working.replace(ANSI_RE2, "");
33851
33893
  if (stripped.length > MAX_PASTE_CHARS) {
33852
33894
  return {
33853
- accum: combined.includes(END) ? null : OVERFLOW_STATE,
33895
+ accum: closed ? null : OVERFLOW_STATE,
33854
33896
  complete: null,
33855
33897
  error: `Paste rejected: exceeds ${MAX_PASTE_CHARS.toLocaleString()} characters.`
33856
33898
  };
33857
33899
  }
33858
- if (combined.includes(END)) return { accum: null, complete: stripped };
33900
+ if (closed) return { accum: null, complete: stripped };
33859
33901
  return { accum: stripped, complete: null };
33860
33902
  }
33861
33903
 
@@ -34221,8 +34263,10 @@ import { PROMPT_JOURNAL_RAW_MARKER } from "@wrongstack/core/prompts";
34221
34263
  import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
34222
34264
  import { routeImagesForModel } from "@wrongstack/runtime/vision";
34223
34265
  function createRunBlocksController(host) {
34224
- const runBlocks = async (blocks) => {
34266
+ const runBlocks = async (blocks, opts) => {
34225
34267
  const { capabilities, refs, dispatch } = host;
34268
+ const origin = opts?.origin ?? "user";
34269
+ if (origin === "auto_wake" && refs.activeController.current) return;
34226
34270
  const { agent } = capabilities;
34227
34271
  const capturedRaw = typeof agent.ctx.meta[PROMPT_JOURNAL_RAW_MARKER] === "string" ? agent.ctx.meta[PROMPT_JOURNAL_RAW_MARKER] : void 0;
34228
34272
  if (capturedRaw !== void 0) {
@@ -34249,7 +34293,14 @@ function createRunBlocksController(host) {
34249
34293
  });
34250
34294
  const controller = new AbortController();
34251
34295
  refs.activeController.current = controller;
34296
+ let finishedStatus;
34252
34297
  refs.interrupts.current = 0;
34298
+ if (origin === "user") {
34299
+ try {
34300
+ capabilities.onUserRun?.();
34301
+ } catch {
34302
+ }
34303
+ }
34253
34304
  dispatch({ type: "resetInterrupts" });
34254
34305
  dispatch({ type: "status", status: "running" });
34255
34306
  try {
@@ -34348,8 +34399,10 @@ function createRunBlocksController(host) {
34348
34399
  } catch {
34349
34400
  }
34350
34401
  }
34402
+ finishedStatus = result.status;
34351
34403
  capabilities.onRunFinished?.(result.status);
34352
34404
  } catch (error) {
34405
+ finishedStatus = "failed";
34353
34406
  if (runGeneration === refs.sessionGeneration.current) {
34354
34407
  dispatch({ type: "addEntry", entry: { kind: "error", text: toErrorMessage4(error) } });
34355
34408
  capabilities.onRunFinished?.("failed");
@@ -34374,6 +34427,12 @@ function createRunBlocksController(host) {
34374
34427
  agent.ctx.meta[PROMPT_JOURNAL_RAW_MARKER] = head.journalRaw;
34375
34428
  }
34376
34429
  await runBlocks(head.blocks);
34430
+ return;
34431
+ }
34432
+ if (finishedStatus === "aborted") return;
34433
+ try {
34434
+ capabilities.onIdleAfterRun?.();
34435
+ } catch {
34377
34436
  }
34378
34437
  };
34379
34438
  return runBlocks;
@@ -35586,19 +35645,25 @@ function rankSlashCommand(entry2, query) {
35586
35645
  if (query === "" && cmd.hidden) return null;
35587
35646
  const name = displayName2(entry2);
35588
35647
  const lowerName = name.toLowerCase();
35648
+ const lowerBare = cmd.name.toLowerCase();
35589
35649
  const aliases = aliasCandidates(entry2);
35590
35650
  const category = cmd.category ?? "App";
35651
+ const namespaced = lowerBare !== lowerName;
35591
35652
  let rank = null;
35592
35653
  let matchedAlias;
35593
35654
  if (query === "") {
35594
35655
  rank = 100;
35595
35656
  } else if (lowerName === query) {
35596
35657
  rank = 0;
35658
+ } else if (namespaced && lowerBare === query) {
35659
+ rank = 2;
35597
35660
  } else if (aliases.find((alias) => alias.toLowerCase() === query)) {
35598
35661
  rank = 5;
35599
35662
  matchedAlias = aliases.find((alias) => alias.toLowerCase() === query);
35600
35663
  } else if (lowerName.startsWith(query)) {
35601
35664
  rank = 10;
35665
+ } else if (namespaced && lowerBare.startsWith(query)) {
35666
+ rank = 12;
35602
35667
  } else {
35603
35668
  const prefixAlias = aliases.find((alias) => alias.toLowerCase().startsWith(query));
35604
35669
  if (prefixAlias) {
@@ -35621,16 +35686,13 @@ function displayName2({ cmd, owner, fullName }) {
35621
35686
  if (owner === "core") return cmd.name;
35622
35687
  return fullName.includes(":") ? fullName : cmd.name;
35623
35688
  }
35624
- function aliasCandidates({ cmd, owner, fullName }) {
35689
+ function aliasCandidates({ cmd, owner }) {
35625
35690
  const aliases = cmd.aliases ?? [];
35626
35691
  if (owner === "core") return aliases;
35627
35692
  const out = [...aliases];
35628
35693
  for (const alias of aliases) {
35629
35694
  out.push(`${owner}:${alias}`);
35630
35695
  }
35631
- if (fullName.includes(":")) {
35632
- return out.filter((alias) => alias.includes(":"));
35633
- }
35634
35696
  return out;
35635
35697
  }
35636
35698
 
@@ -37429,7 +37491,13 @@ function buildAppPipelineArgs(params) {
37429
37491
  onSuggestionsParsed: props.onSuggestionsParsed,
37430
37492
  predictNext: props.predictNext,
37431
37493
  shouldSuppressNextSteps: shouldSuppressBugHuntNextSteps,
37432
- onRunFinished: onBugHuntRunFinished
37494
+ onRunFinished: onBugHuntRunFinished,
37495
+ ...props.leaderAutoWake ? {
37496
+ onUserRun: () => props.leaderAutoWake?.noteUserInput(props.agent.ctx.session.id),
37497
+ onIdleAfterRun: () => {
37498
+ props.leaderAutoWake?.onRunFinished(props.agent.ctx.session.id);
37499
+ }
37500
+ } : {}
37433
37501
  },
37434
37502
  refs: {
37435
37503
  sessionGeneration: sessionGenerationRef,
@@ -46207,7 +46275,7 @@ async function walk(root, rel, depth, out) {
46207
46275
  }
46208
46276
  function score(s2, query) {
46209
46277
  if (!query) return s2.length;
46210
- const ql = query.toLowerCase();
46278
+ const ql = query.toLowerCase().replaceAll("\\", "/");
46211
46279
  const sl = s2.toLowerCase();
46212
46280
  let si = 0;
46213
46281
  let firstHit = -1;
@@ -51371,6 +51439,90 @@ function useTuiSlashCommands({
51371
51439
 
51372
51440
  // src/app.tsx
51373
51441
  import { buildGoalPreamble as buildGoalPreamble2 } from "@wrongstack/core/execution";
51442
+
51443
+ // src/hooks/use-leader-auto-wake.ts
51444
+ import { useEffect as useEffect79, useRef as useRef47 } from "react";
51445
+ function normalizeSessionId(sessionId) {
51446
+ return (sessionId ?? "").trim().replace(/\\/g, "/");
51447
+ }
51448
+ function createTuiLeaderWakePort(deps) {
51449
+ const isForeground = (sessionId) => {
51450
+ const current = normalizeSessionId(deps.getSessionId());
51451
+ return current.length > 0 && current === normalizeSessionId(sessionId);
51452
+ };
51453
+ return {
51454
+ isOpen: isForeground,
51455
+ isDisplayed: isForeground,
51456
+ isIdle: (sessionId) => isForeground(sessionId) && deps.activeController.current == null && !deps.eternalLoopRunning.current && !deps.parallelLoopRunning.current && deps.state.current.queue.length === 0,
51457
+ hasPendingUserInput: () => deps.state.current.queue.length > 0 || deps.enhanceAbort?.current != null,
51458
+ isConfirmPending: () => deps.state.current.confirmQueue.length > 0 || deps.pendingForms.current > 0,
51459
+ startWakeTurn: async (_sessionId, prompt) => {
51460
+ void deps.runBlocks.current([{ type: "text", text: prompt }], { origin: "auto_wake" });
51461
+ }
51462
+ };
51463
+ }
51464
+ function useLeaderAutoWake(args) {
51465
+ const argsRef = useRef47(args);
51466
+ argsRef.current = args;
51467
+ const pendingForms = useRef47(0);
51468
+ const { leaderAutoWake, events } = args;
51469
+ useEffect79(() => {
51470
+ if (!leaderAutoWake) return;
51471
+ const live = argsRef;
51472
+ const port = createTuiLeaderWakePort({
51473
+ getSessionId: () => live.current.getSessionId(),
51474
+ activeController: live.current.activeController,
51475
+ eternalLoopRunning: live.current.eternalLoopRunning,
51476
+ parallelLoopRunning: live.current.parallelLoopRunning,
51477
+ enhanceAbort: live.current.enhanceAbort,
51478
+ state: live.current.state,
51479
+ pendingForms,
51480
+ runBlocks: live.current.runBlocks
51481
+ });
51482
+ const detach = leaderAutoWake.attachPort(port);
51483
+ const isMine = (sessionId) => normalizeSessionId(sessionId) === normalizeSessionId(live.current.getSessionId());
51484
+ const offs = [
51485
+ events.on("user.input_requested", () => {
51486
+ pendingForms.current += 1;
51487
+ }),
51488
+ events.on("user.input_resolved", () => {
51489
+ pendingForms.current = Math.max(0, pendingForms.current - 1);
51490
+ }),
51491
+ events.on("leader.auto_wake_started", (e) => {
51492
+ if (!isMine(e.sessionId)) return;
51493
+ const n = e.delegationIds.length;
51494
+ live.current.dispatch({
51495
+ type: "addEntry",
51496
+ entry: {
51497
+ kind: "info",
51498
+ text: `\u27F3 auto-wake: ${n} background delegation result${n === 1 ? "" : "s"} arrived (${e.delegationIds.join(", ")}) \u2014 starting a new turn (woken turn ${e.chain} without your input; it spends tokens like any turn).`
51499
+ }
51500
+ });
51501
+ }),
51502
+ events.on("leader.auto_wake_suppressed", (e) => {
51503
+ if (!isMine(e.sessionId) || e.reason !== "chain_cap") return;
51504
+ live.current.dispatch({
51505
+ type: "addEntry",
51506
+ entry: {
51507
+ kind: "warn",
51508
+ text: `auto-wake paused: ${e.pending} background result${e.pending === 1 ? "" : "s"} held after too many consecutive woken turns \u2014 send any message to receive ${e.pending === 1 ? "it" : "them"}.`
51509
+ }
51510
+ });
51511
+ })
51512
+ ];
51513
+ try {
51514
+ const sessionId = live.current.getSessionId();
51515
+ if (sessionId) leaderAutoWake.onSessionDisplayed(sessionId);
51516
+ } catch {
51517
+ }
51518
+ return () => {
51519
+ for (const off of offs) off();
51520
+ detach();
51521
+ };
51522
+ }, [leaderAutoWake, events]);
51523
+ }
51524
+
51525
+ // src/app.tsx
51374
51526
  import { jsx as jsx106 } from "react/jsx-runtime";
51375
51527
  function App(props) {
51376
51528
  const {
@@ -51700,7 +51852,7 @@ function App(props) {
51700
51852
  dispatch,
51701
51853
  open: state.authPanel.open
51702
51854
  });
51703
- useEffect79(() => {
51855
+ useEffect80(() => {
51704
51856
  if (!secretInputController) return;
51705
51857
  const previousSecret = secretInputController.readSecret;
51706
51858
  const previousText = secretInputController.readText;
@@ -52134,6 +52286,18 @@ function App(props) {
52134
52286
  shouldSuppressBugHuntNextSteps: bugHuntLoop.shouldSuppressNextSteps
52135
52287
  });
52136
52288
  const { stableOnKey } = useAppExecutionPipeline(pipelineArgs);
52289
+ useLeaderAutoWake({
52290
+ leaderAutoWake: props.leaderAutoWake,
52291
+ events,
52292
+ dispatch,
52293
+ getSessionId: () => agent.ctx.session?.id,
52294
+ activeController: activeCtrlRef,
52295
+ eternalLoopRunning: eternalLoopRunningRef,
52296
+ parallelLoopRunning: parallelLoopRunningRef,
52297
+ enhanceAbort: enhanceAbortRef,
52298
+ state: stateRef,
52299
+ runBlocks: runBlocksRef
52300
+ });
52137
52301
  useInitialPrompt({ initialGoal, initialAsk, builderRef, runBlocksRef, dispatch });
52138
52302
  const viewState = deriveAppViewState({
52139
52303
  state,
@@ -53004,6 +53168,7 @@ async function runTui(opts) {
53004
53168
  banner: opts.banner ?? true,
53005
53169
  queueStore: opts.queueStore,
53006
53170
  onQueueChange: opts.onQueueChange,
53171
+ leaderAutoWake: opts.leaderAutoWake,
53007
53172
  yolo: opts.yolo,
53008
53173
  getYolo: opts.getYolo,
53009
53174
  onYolo: opts.onYolo,
@@ -53069,6 +53234,7 @@ async function runTui(opts) {
53069
53234
  getBrainData: opts.getBrainData,
53070
53235
  onBrainRiskLevel: opts.onBrainRiskLevel,
53071
53236
  brainPanelHost: opts.brainPanelHost,
53237
+ subagentModelsHost: opts.subagentModelsHost,
53072
53238
  getShadowData: opts.getShadowData,
53073
53239
  onShadowStart: opts.onShadowStart,
53074
53240
  onShadowStop: opts.onShadowStop,
@@ -1,13 +1,3 @@
1
- /**
2
- * Inline chip token grammar, shared by the editable input for chip rendering
3
- * and whole-token cursor deletion. Attachment shapes stay in sync with the
4
- * AttachmentStore placeholder regex in @wrongstack/core:
5
- * - seq-keyed `[pasted|image|file #N …]` (a cosmetic suffix after the seq,
6
- * e.g. `, 123 lines`, is tolerated; legacy `[file #N]` is included)
7
- * - path-keyed `[file:<path>]`
8
- * `[VIBE]` is a protocol chip: it renders and deletes as a token, but it is
9
- * not an attachment and must stay in the prompt sent to the refiner/agent.
10
- */
11
1
  /** Attachment placeholders only. Protocol tags must not be stripped as chips. */
12
2
  export declare const ATTACHMENT_TOKEN_SRC = "\\[(?:pasted|image|file) #\\d+[^\\]]*\\]|\\[file:[^\\]]+\\]";
13
3
  /** Protocol trigger rendered as a chip but kept in the prompt text. */
@@ -50,7 +50,7 @@ export interface LayoutSnapshot {
50
50
  entries: Record<number, EntryLayout>;
51
51
  }
52
52
  /** Current layout computation version. Bump when the algorithm changes. */
53
- export declare const LAYOUT_VERSION = 2;
53
+ export declare const LAYOUT_VERSION = 3;
54
54
  /** Default fallback row count for entries without computed layout. */
55
55
  export declare const FALLBACK_ROWS = 3;
56
56
  /**
@@ -1,6 +1,6 @@
1
1
  import type { ContentBlock } from '@wrongstack/core/types';
2
2
  import type { Action, State } from './app-reducer.js';
3
- import type { RunBlocksCapabilities } from './tui-host-capabilities.js';
3
+ import type { RunBlocksCapabilities, RunBlocksOptions } from './tui-host-capabilities.js';
4
4
  import type { MutableCell, StreamSegment } from './shared-types.js';
5
5
  export interface RunBlocksRefs {
6
6
  readonly sessionGeneration: MutableCell<number>;
@@ -26,6 +26,6 @@ interface RunBlocksHost {
26
26
  * recovery, summaries, lifecycle cleanup, and FIFO queue draining; the App
27
27
  * shell only supplies stable capabilities and mutable lifecycle cells.
28
28
  */
29
- export declare function createRunBlocksController(host: RunBlocksHost): (blocks: ContentBlock[]) => Promise<void>;
29
+ export declare function createRunBlocksController(host: RunBlocksHost): (blocks: ContentBlock[], opts?: RunBlocksOptions) => Promise<void>;
30
30
  export {};
31
31
  //# sourceMappingURL=run-blocks-controller.d.ts.map
@@ -42,6 +42,12 @@ export interface RunTuiOptions {
42
42
  * its next iteration boundary without the queue being delivered early.
43
43
  */
44
44
  onQueueChange?: ((items: string[]) => void) | undefined;
45
+ /**
46
+ * Background-delegation auto-wake controller (core
47
+ * `LeaderAutoWakeController`). When present the TUI binds a wake port for
48
+ * the foreground session and reports user runs / idle-after-run to it.
49
+ */
50
+ leaderAutoWake?: import('./app-props.js').TuiLeaderAutoWake | undefined;
45
51
  /** Surfaces the "⚠ YOLO" chip in the status bar. */
46
52
  yolo?: boolean | undefined;
47
53
  /** Query live YOLO state from the permission policy. */
@@ -425,6 +431,8 @@ export interface RunTuiOptions {
425
431
  onBrainRiskLevel?: ((level: 'off' | 'low' | 'medium' | 'high' | 'all') => string | undefined) | undefined;
426
432
  /** Full Brain settings editor bridge (live apply + persist to global config). */
427
433
  brainPanelHost?: import('./brain-panel-model.js').BrainPanelHost | undefined;
434
+ /** Session-scoped model-lane bridge for the interactive `/subagent-models` panel. */
435
+ subagentModelsHost?: import('./subagent-models-panel-model.js').SubagentModelsPanelHost | undefined;
428
436
  /** Get current Shadow Agent state. */
429
437
  getShadowData?: (() => {
430
438
  activeId: string | null;
@@ -32,6 +32,22 @@ export interface RunBlocksCapabilities {
32
32
  }) => Promise<string[]>) | undefined;
33
33
  /** Observes a completed foreground run after its result has been rendered. */
34
34
  onRunFinished?: ((status: 'done' | 'aborted' | 'failed' | 'max_iterations') => void) | undefined;
35
+ /**
36
+ * A user-originated run is starting (typed input, queue drain, steer). The
37
+ * auto-wake chain cap resets on it. Not called for auto-wake turns.
38
+ */
39
+ onUserRun?: (() => void) | undefined;
40
+ /**
41
+ * The controller went idle with an empty queue after a run — the auto-wake
42
+ * post-run check (a background result that landed during the last
43
+ * iteration).
44
+ */
45
+ onIdleAfterRun?: (() => void) | undefined;
46
+ }
47
+ /** Where a `runBlocks` invocation came from. Default `user`. */
48
+ export type RunBlocksOrigin = 'user' | 'auto_wake';
49
+ export interface RunBlocksOptions {
50
+ origin?: RunBlocksOrigin | undefined;
35
51
  }
36
52
  type ClearAction = Extract<Action, {
37
53
  type: 'clearHistory';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tui",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack terminal UI: Ink-based interactive renderer with history, pickers, and slash menus.",
6
6
  "repository": {
@@ -26,26 +26,26 @@
26
26
  "!dist/**/*.map"
27
27
  ],
28
28
  "dependencies": {
29
- "@wrongstack/core": "1.0.7",
30
- "@wrongstack/kanban": "1.0.7",
31
- "@wrongstack/runtime": "1.0.7",
32
- "@wrongstack/sage": "1.0.7",
33
- "@wrongstack/sdd": "1.0.7",
34
- "@wrongstack/tools": "1.0.7",
29
+ "@wrongstack/core": "1.0.9",
30
+ "@wrongstack/kanban": "1.0.9",
31
+ "@wrongstack/runtime": "1.0.9",
32
+ "@wrongstack/sage": "1.0.9",
33
+ "@wrongstack/sdd": "1.0.9",
34
+ "@wrongstack/tools": "1.0.9",
35
35
  "ink": "7.1.1",
36
- "react": "^19.2.8",
36
+ "react": "^19.3.0",
37
37
  "string-width": "8.2.2",
38
38
  "wrap-ansi": "^10.0.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@testing-library/dom": "^10.4.1",
42
- "@testing-library/react": "^16.3.2",
43
- "@types/node": "^26.2.0",
44
- "@types/react": "^19.2.18",
45
- "@types/react-dom": "^19.2.4",
42
+ "@testing-library/react": "^16.3.3",
43
+ "@types/node": "^26.5.1",
44
+ "@types/react": "^19.3.0",
45
+ "@types/react-dom": "^19.3.0",
46
46
  "ink-testing-library": "^4.0.0",
47
47
  "jsdom": "^30.0.1",
48
- "react-dom": "^19.2.8",
48
+ "react-dom": "^19.3.0",
49
49
  "typescript": "^7.0.2"
50
50
  },
51
51
  "publishConfig": {