@wrongstack/tui 0.32.0 → 0.51.3

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
@@ -1,10 +1,10 @@
1
1
  import { Readable } from 'stream';
2
+ import { writeErr, InputBuilder, DefaultSessionRewinder, writeOut, formatTodosList, buildGoalPreamble, buildChildEnv } from '@wrongstack/core';
3
+ export { buildGoalPreamble } from '@wrongstack/core';
2
4
  import { Box, Text, render, useApp, useStdout, measureElement, Static, useInput, useStdin } from 'ink';
3
5
  import React4, { useState, useEffect, useReducer, useRef, useMemo, useCallback, useLayoutEffect } from 'react';
4
6
  import * as fs2 from 'fs/promises';
5
7
  import * as path2 from 'path';
6
- import { InputBuilder, DefaultSessionRewinder, formatTodosList, buildGoalPreamble, buildChildEnv } from '@wrongstack/core';
7
- export { buildGoalPreamble } from '@wrongstack/core';
8
8
  import { routeImagesForModel } from '@wrongstack/runtime/vision';
9
9
  import { getProcessRegistry } from '@wrongstack/tools';
10
10
  import { readClipboardImage } from '@wrongstack/runtime/clipboard';
@@ -54,6 +54,7 @@ function StatusBar({
54
54
  git,
55
55
  subagentCount = 0,
56
56
  context,
57
+ brain,
57
58
  projectName,
58
59
  processCount,
59
60
  hiddenItems,
@@ -79,7 +80,8 @@ function StatusBar({
79
80
  const { label: stateLabel, color: stateColor } = stateChip(state, fleet?.running ?? 0);
80
81
  const hasSecondLine = yolo || autonomy && autonomy !== "off" || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0 || goalSummary !== null && goalSummary !== void 0;
81
82
  const fleetHasActivity = fleet && (fleet.running > 0 || fleet.idle > 0 || fleet.pending > 0 || fleet.completed > 0) || subagentCount > 0;
82
- const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity;
83
+ const hasBrainActivity = !!brain && brain.state !== "idle";
84
+ const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity || hasBrainActivity;
83
85
  return /* @__PURE__ */ jsxs(
84
86
  Box,
85
87
  {
@@ -316,6 +318,10 @@ function StatusBar({
316
318
  " agent",
317
319
  subagentCount === 1 ? "" : "s"
318
320
  ] })
321
+ ] }) : null,
322
+ hasBrainActivity && brain ? /* @__PURE__ */ jsxs(Fragment, { children: [
323
+ todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
324
+ /* @__PURE__ */ jsx(BrainChip, { brain })
319
325
  ] }) : null
320
326
  ] }) : null,
321
327
  fleetAgents && fleetAgents.length > 0 ? /* @__PURE__ */ jsx(Box, { flexDirection: "row", gap: 2, children: fleetAgents.map((a, i) => (
@@ -348,6 +354,18 @@ function StatusBar({
348
354
  }
349
355
  );
350
356
  }
357
+ function BrainChip({ brain }) {
358
+ const color = brain.state === "denied" ? "red" : brain.state === "ask_human" ? "yellow" : brain.state === "deciding" ? "magenta" : "cyan";
359
+ const label = brain.state === "deciding" ? "deciding" : brain.state === "ask_human" ? "human" : brain.state;
360
+ const scope = brain.source ? ` ${brain.source}` : "";
361
+ const summary = brain.summary ? ` \xB7 ${brain.summary.slice(0, 40)}` : "";
362
+ return /* @__PURE__ */ jsxs(Text, { color, children: [
363
+ "\u{1F9E0} ",
364
+ label,
365
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: scope }),
366
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: summary })
367
+ ] });
368
+ }
351
369
  function EternalStageChip({
352
370
  stage
353
371
  }) {
@@ -376,6 +394,25 @@ function EternalStageChip({
376
394
  ]
377
395
  }
378
396
  );
397
+ case "decompose":
398
+ return /* @__PURE__ */ jsx(Text, { color: "cyan", children: "\u2B07 decompose" });
399
+ case "fanout":
400
+ return /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
401
+ "\u21C4 fanout: ",
402
+ stage.slots
403
+ ] });
404
+ case "await":
405
+ return /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
406
+ "\u23F3 await: ",
407
+ stage.taskIds.length
408
+ ] });
409
+ case "aggregate":
410
+ return /* @__PURE__ */ jsxs(Text, { color: stage.goalComplete ? "green" : "magenta", children: [
411
+ "\u21A9 aggregate: ",
412
+ stage.successCount,
413
+ "/",
414
+ stage.total
415
+ ] });
379
416
  case "sleep":
380
417
  return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
381
418
  "\u{1F4A4} sleep ",
@@ -526,6 +563,11 @@ function fmtCost(n) {
526
563
  if (n === 0) return "\u2014";
527
564
  return `$${n.toFixed(3)}`;
528
565
  }
566
+ function fmtModelLabel(provider, model) {
567
+ if (!model && !provider) return "";
568
+ const short = model ? model.includes("/") ? model.slice(model.lastIndexOf("/") + 1) : model : "?";
569
+ return provider ? `${provider}:${short}` : short;
570
+ }
529
571
  function FleetMonitor({
530
572
  entries,
531
573
  totalCost,
@@ -598,7 +640,7 @@ function FleetMonitor({
598
640
  "\u2717",
599
641
  failed
600
642
  ] }) : null,
601
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 Ctrl+F to close" })
643
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 Ctrl+F / F2 to close" })
602
644
  ] }),
603
645
  collabSession ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
604
646
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
@@ -658,20 +700,24 @@ function FleetMonitor({
658
700
  shown.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
659
701
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
660
702
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " " }),
661
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "name".padEnd(16) }),
662
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "status".padEnd(10) }),
663
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "L/t".padEnd(8) }),
703
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "name".padEnd(14) }),
704
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "model".padEnd(18) }),
705
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "status".padEnd(9) }),
706
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "L/t\xB7ctx".padEnd(12) }),
664
707
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "elapsed".padEnd(8) }),
665
708
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "cost" })
666
709
  ] }),
667
710
  shown.map((e) => {
668
711
  const s2 = STATUS[e.status];
669
712
  const elapsed = e.status === "running" ? fmtElapsed(Math.max(0, nowTick - e.startedAt)) : fmtElapsed(Math.max(0, nowTick - e.lastEventAt)) + " ago";
713
+ const model = fmtModelLabel(e.provider, e.model) || "\u2014";
714
+ const ltCtx = e.ctxPct !== void 0 ? `L${e.iterations} ${e.toolCalls}t ${Math.round(e.ctxPct * 100)}%` : `L${e.iterations} ${e.toolCalls}t`;
670
715
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
671
716
  /* @__PURE__ */ jsx(Text, { color: s2.color, children: s2.icon }),
672
- /* @__PURE__ */ jsx(Text, { children: e.name.padEnd(16).slice(0, 16) }),
673
- /* @__PURE__ */ jsx(Text, { color: s2.color, children: e.status.padEnd(10) }),
674
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: `L${e.iterations} ${e.toolCalls}t`.padEnd(8) }),
717
+ /* @__PURE__ */ jsx(Text, { children: e.name.padEnd(14).slice(0, 14) }),
718
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: model.padEnd(18).slice(0, 18) }),
719
+ /* @__PURE__ */ jsx(Text, { color: s2.color, children: e.status.padEnd(9) }),
720
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ltCtx.padEnd(12).slice(0, 12) }),
675
721
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: elapsed.padEnd(8).slice(0, 8) }),
676
722
  /* @__PURE__ */ jsx(Text, { color: "yellow", children: fmtCost(e.cost) }),
677
723
  e.extensions && e.extensions > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
@@ -725,6 +771,9 @@ function fmtTokens2(n) {
725
771
  if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
726
772
  return `${(n / 1e6).toFixed(1)}M`;
727
773
  }
774
+ function fmtExactTokens(n) {
775
+ return `${Math.round(n).toLocaleString("en-US")} tok`;
776
+ }
728
777
  function snippet(s2, max = 72) {
729
778
  const oneLine2 = s2.replace(/\s+/g, " ").trim();
730
779
  if (oneLine2.length <= max) return oneLine2;
@@ -786,7 +835,7 @@ function AgentsMonitor({
786
835
  "\u2717",
787
836
  totalFailed
788
837
  ] }) : null,
789
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 Ctrl+G to close" })
838
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7 Ctrl+G / F3 to close" })
790
839
  ] }),
791
840
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
792
841
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "shown" }),
@@ -821,6 +870,11 @@ function AgentsMonitor({
821
870
  /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
822
871
  /* @__PURE__ */ jsx(Text, { color: s2.color, bold: true, children: s2.icon }),
823
872
  /* @__PURE__ */ jsx(Text, { bold: true, children: e.name }),
873
+ fmtModelLabel(e.provider, e.model) ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: fmtModelLabel(e.provider, e.model) }) : null,
874
+ e.ctxMaxTokens && e.ctxMaxTokens > 0 ? /* @__PURE__ */ jsxs(Text, { color: "blue", children: [
875
+ "ctx max ",
876
+ fmtExactTokens(e.ctxMaxTokens)
877
+ ] }) : null,
824
878
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
825
879
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: elapsed }),
826
880
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
@@ -831,6 +885,10 @@ function AgentsMonitor({
831
885
  e.toolCalls,
832
886
  "t"
833
887
  ] }),
888
+ e.ctxPct !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
889
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
890
+ /* @__PURE__ */ jsx(ContextBar, { pct: e.ctxPct, tokens: e.ctxTokens, maxTokens: e.ctxMaxTokens })
891
+ ] }) : null,
834
892
  e.extensions && e.extensions > 0 ? /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
835
893
  "\u26A1\xD7",
836
894
  e.extensions
@@ -877,11 +935,7 @@ function AgentsMonitor({
877
935
  e.failureReason && e.status !== "success" ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
878
936
  "\u2717 ",
879
937
  e.failureReason
880
- ] }) }) : null,
881
- e.ctxPct !== void 0 ? /* @__PURE__ */ jsxs(Box, { paddingLeft: 2, children: [
882
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "ctx " }),
883
- /* @__PURE__ */ jsx(ContextBar, { pct: e.ctxPct, tokens: e.ctxTokens, maxTokens: e.ctxMaxTokens })
884
- ] }) : null
938
+ ] }) }) : null
885
939
  ] }, e.id);
886
940
  })
887
941
  ] });
@@ -942,6 +996,92 @@ function AutonomyPicker({
942
996
  hint ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: hint }) : null
943
997
  ] });
944
998
  }
999
+ function riskColor(risk) {
1000
+ switch (risk) {
1001
+ case "low":
1002
+ return "green";
1003
+ case "medium":
1004
+ return "cyan";
1005
+ case "high":
1006
+ return "yellow";
1007
+ case "critical":
1008
+ return "red";
1009
+ default:
1010
+ return "white";
1011
+ }
1012
+ }
1013
+ function optionKey(index) {
1014
+ return String.fromCharCode("A".charCodeAt(0) + index);
1015
+ }
1016
+ function contextLines(context) {
1017
+ if (!context?.trim()) return [];
1018
+ return context.trim().split("\n").slice(0, 5);
1019
+ }
1020
+ function BrainDecisionPrompt({
1021
+ requestId,
1022
+ source,
1023
+ risk,
1024
+ question,
1025
+ context,
1026
+ options = [],
1027
+ onAnswer
1028
+ }) {
1029
+ const color = riskColor(risk);
1030
+ const ctx = contextLines(context);
1031
+ useInput((input, key) => {
1032
+ if (!onAnswer) return;
1033
+ if (key.escape || input.toLowerCase() === "d") {
1034
+ onAnswer({ id: requestId, deny: true, text: "Denied by human from TUI." });
1035
+ return;
1036
+ }
1037
+ const ch = input.toLowerCase();
1038
+ const index = ch >= "a" && ch <= "z" ? ch.charCodeAt(0) - "a".charCodeAt(0) : Number(ch) - 1;
1039
+ const option = Number.isInteger(index) && index >= 0 ? options[index] : void 0;
1040
+ if (option) onAnswer({ id: requestId, optionId: option.id, text: option.label });
1041
+ });
1042
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [
1043
+ /* @__PURE__ */ jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: "\u{1F9E0} BRAIN REQUIRES HUMAN DECISION" }) }),
1044
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1045
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Source:" }),
1046
+ /* @__PURE__ */ jsx(Text, { children: source }),
1047
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Risk:" }),
1048
+ /* @__PURE__ */ jsx(Text, { color, children: risk })
1049
+ ] }),
1050
+ /* @__PURE__ */ jsx(Text, { color: "white", children: question }),
1051
+ ctx.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1052
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Context:" }),
1053
+ ctx.map((line, index) => (
1054
+ // biome-ignore lint/suspicious/noArrayIndexKey: context lines are static for this prompt render
1055
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: line }, index)
1056
+ ))
1057
+ ] }) : null,
1058
+ options.length > 0 ? /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
1059
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Options:" }),
1060
+ options.slice(0, 6).map((option, index) => {
1061
+ const key = optionKey(index);
1062
+ const optionColor = riskColor(String(option.risk ?? risk));
1063
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1064
+ /* @__PURE__ */ jsxs(Text, { children: [
1065
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
1066
+ "[",
1067
+ key,
1068
+ "]"
1069
+ ] }),
1070
+ " ",
1071
+ /* @__PURE__ */ jsx(Text, { color: optionColor, children: option.label }),
1072
+ option.recommended ? /* @__PURE__ */ jsx(Text, { color: "green", children: " recommended" }) : null
1073
+ ] }),
1074
+ option.consequence ? /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1075
+ " ",
1076
+ option.consequence
1077
+ ] }) : null
1078
+ ] }, option.id);
1079
+ })
1080
+ ] }) : null,
1081
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
1082
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press A/B/C or 1/2/3 to answer; Esc or D denies with the safe default." })
1083
+ ] });
1084
+ }
945
1085
  function CheckpointTimeline({
946
1086
  checkpoints,
947
1087
  selected,
@@ -1041,7 +1181,7 @@ function ConfirmPrompt({
1041
1181
  onDecision
1042
1182
  }) {
1043
1183
  React4.useEffect(() => {
1044
- process.stdout.write("\x07");
1184
+ writeOut("\x07");
1045
1185
  }, []);
1046
1186
  useInput((input2, _key) => {
1047
1187
  if (!input2 || input2 === "\r" || input2 === "\n") return;
@@ -1182,9 +1322,9 @@ function helpSections(opts) {
1182
1322
  {
1183
1323
  title: "Monitors",
1184
1324
  entries: [
1185
- { keys: "Ctrl+F", desc: "fleet orchestration monitor" },
1186
- { keys: "Ctrl+G", desc: "agents live monitor" },
1187
- { keys: "Ctrl+T", desc: "worktree monitor" },
1325
+ { keys: "Ctrl+F / F2", desc: "fleet orchestration monitor" },
1326
+ { keys: "Ctrl+G / F3", desc: "agents live monitor" },
1327
+ { keys: "Ctrl+T / F4", desc: "worktree monitor" },
1188
1328
  { keys: "Esc", desc: "close the open monitor / overlay" }
1189
1329
  ]
1190
1330
  },
@@ -2308,6 +2448,30 @@ function DiffBlock({ rows, hidden }) {
2308
2448
  hidden > 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `${blank} \u2026 ${hidden} more line${hidden === 1 ? "" : "s"}` }) : null
2309
2449
  ] });
2310
2450
  }
2451
+ function brainStatusStyle(status) {
2452
+ switch (status) {
2453
+ case "thinking":
2454
+ return { icon: "\u2026", color: "magenta" };
2455
+ case "answered":
2456
+ return { icon: "\u2696", color: "cyan" };
2457
+ case "ask_human":
2458
+ return { icon: "?", color: "yellow" };
2459
+ case "denied":
2460
+ return { icon: "\xD7", color: "red" };
2461
+ }
2462
+ }
2463
+ function brainRiskColor(risk) {
2464
+ switch (risk) {
2465
+ case "low":
2466
+ return "green";
2467
+ case "medium":
2468
+ return "cyan";
2469
+ case "high":
2470
+ return "yellow";
2471
+ case "critical":
2472
+ return "red";
2473
+ }
2474
+ }
2311
2475
  var Entry = React4.memo(function Entry2({
2312
2476
  entry,
2313
2477
  termWidth
@@ -2440,6 +2604,38 @@ var Entry = React4.memo(function Entry2({
2440
2604
  );
2441
2605
  case "turn-summary":
2442
2606
  return /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.text });
2607
+ case "brain": {
2608
+ const statusStyle = brainStatusStyle(entry.status);
2609
+ const riskColor2 = brainRiskColor(entry.risk);
2610
+ return /* @__PURE__ */ jsxs(
2611
+ Box,
2612
+ {
2613
+ flexDirection: "column",
2614
+ marginY: 1,
2615
+ borderStyle: "single",
2616
+ borderTop: false,
2617
+ borderRight: false,
2618
+ borderBottom: false,
2619
+ borderColor: "magenta",
2620
+ paddingLeft: 1,
2621
+ children: [
2622
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
2623
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: "BRAIN" }),
2624
+ /* @__PURE__ */ jsx(Text, { color: statusStyle.color, children: statusStyle.icon }),
2625
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.source }),
2626
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\xB7" }),
2627
+ /* @__PURE__ */ jsx(Text, { color: riskColor2, children: entry.risk })
2628
+ ] }),
2629
+ /* @__PURE__ */ jsx(Text, { color: "white", children: entry.question }),
2630
+ entry.decision ? /* @__PURE__ */ jsxs(Text, { children: [
2631
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Decision: " }),
2632
+ /* @__PURE__ */ jsx(Text, { color: statusStyle.color, children: entry.decision })
2633
+ ] }) : null,
2634
+ entry.rationale ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: entry.rationale }) : null
2635
+ ]
2636
+ }
2637
+ );
2638
+ }
2443
2639
  case "confirm":
2444
2640
  return /* @__PURE__ */ jsxs(
2445
2641
  Box,
@@ -3181,6 +3377,42 @@ function truncMid(s2, max) {
3181
3377
  return `${s2.slice(0, max - 1)}\u2026`;
3182
3378
  }
3183
3379
 
3380
+ // src/fn-keys.ts
3381
+ function fnKey(data) {
3382
+ switch (data) {
3383
+ case "\x1BOP":
3384
+ case "\x1B[11~":
3385
+ return 1;
3386
+ case "\x1BOQ":
3387
+ case "\x1B[12~":
3388
+ return 2;
3389
+ case "\x1BOR":
3390
+ case "\x1B[13~":
3391
+ return 3;
3392
+ case "\x1BOS":
3393
+ case "\x1B[14~":
3394
+ return 4;
3395
+ case "\x1B[15~":
3396
+ return 5;
3397
+ case "\x1B[17~":
3398
+ return 6;
3399
+ case "\x1B[18~":
3400
+ return 7;
3401
+ case "\x1B[19~":
3402
+ return 8;
3403
+ case "\x1B[20~":
3404
+ return 9;
3405
+ case "\x1B[21~":
3406
+ return 10;
3407
+ case "\x1B[23~":
3408
+ return 11;
3409
+ case "\x1B[24~":
3410
+ return 12;
3411
+ default:
3412
+ return null;
3413
+ }
3414
+ }
3415
+
3184
3416
  // src/input-tokens.ts
3185
3417
  var INLINE_TOKEN_SRC = "\\[(?:pasted|image|file) #\\d+[^\\]]*\\]|\\[file:[^\\]]+\\]";
3186
3418
  var AT_END = new RegExp(`(?:${INLINE_TOKEN_SRC})$`);
@@ -3375,9 +3607,18 @@ function Input({
3375
3607
  useEffect(() => {
3376
3608
  if (!stdin || disabled) return;
3377
3609
  const handleData = (data) => {
3378
- const kind = isHomeEnd(data.toString());
3379
- if (kind === "home") onKey("", { ...EMPTY_KEY, home: true });
3380
- else if (kind === "end") onKey("", { ...EMPTY_KEY, end: true });
3610
+ const s2 = data.toString();
3611
+ const kind = isHomeEnd(s2);
3612
+ if (kind === "home") {
3613
+ onKey("", { ...EMPTY_KEY, home: true });
3614
+ return;
3615
+ }
3616
+ if (kind === "end") {
3617
+ onKey("", { ...EMPTY_KEY, end: true });
3618
+ return;
3619
+ }
3620
+ const fn = fnKey(s2);
3621
+ if (fn !== null) onKey("", { ...EMPTY_KEY, fn });
3381
3622
  };
3382
3623
  stdin.on("data", handleData);
3383
3624
  return () => {
@@ -3974,7 +4215,7 @@ function WorktreeMonitor({
3974
4215
  failed
3975
4216
  ] })
3976
4217
  ] }) : null,
3977
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502 Ctrl+T / Esc to close" })
4218
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502 Ctrl+T / F4 / Esc to close" })
3978
4219
  ] }),
3979
4220
  list.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "No worktrees. They appear when AutoPhase runs with isolation on." }) : list.map((w) => {
3980
4221
  const s2 = fmt(w.status);
@@ -4519,6 +4760,21 @@ function reducer(state, action) {
4519
4760
  return { ...state, interrupts: 0 };
4520
4761
  case "hint":
4521
4762
  return { ...state, hint: action.text };
4763
+ case "brainStatus":
4764
+ return {
4765
+ ...state,
4766
+ brain: {
4767
+ state: action.state,
4768
+ source: action.source,
4769
+ risk: action.risk,
4770
+ summary: action.summary,
4771
+ updatedAt: Date.now()
4772
+ }
4773
+ };
4774
+ case "brainPromptSet":
4775
+ return { ...state, brainPrompt: action.prompt };
4776
+ case "brainPromptClear":
4777
+ return { ...state, brainPrompt: null };
4522
4778
  case "pickerOpen":
4523
4779
  return {
4524
4780
  ...state,
@@ -5479,6 +5735,7 @@ function App({
5479
5735
  const { exit } = useApp();
5480
5736
  const [liveModel, setLiveModel] = useState(model);
5481
5737
  const [liveProvider, setLiveProvider] = useState(provider ?? "agent");
5738
+ const [activeMaxContext, setActiveMaxContext] = useState(effectiveMaxContext);
5482
5739
  const [yoloLive, setYoloLive] = useState(yolo);
5483
5740
  const [autonomyLive, setAutonomyLive] = useState(getAutonomy?.() ?? "off");
5484
5741
  const [hiddenItems, setHiddenItems] = useState(statuslineHiddenItems);
@@ -5547,6 +5804,8 @@ function App({
5547
5804
  steeringPending: false,
5548
5805
  steerSnapshot: null,
5549
5806
  hint: "",
5807
+ brain: { state: "idle" },
5808
+ brainPrompt: null,
5550
5809
  nextId: 1,
5551
5810
  picker: { open: false, query: "", matches: [], selected: 0 },
5552
5811
  slashPicker: { open: false, query: "", matches: [], selected: 0 },
@@ -5888,7 +6147,7 @@ function App({
5888
6147
  clearInterval(t);
5889
6148
  };
5890
6149
  }, [agent.ctx.cwd]);
5891
- const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
6150
+ const maxContext = activeMaxContext ?? agent.ctx.provider.capabilities.maxContext;
5892
6151
  const currentContextTokens = (tokenCounter?.currentRequestTokens()?.input ?? 0) + (tokenCounter?.currentRequestTokens()?.cacheRead ?? 0);
5893
6152
  const contextWindow = useMemo(() => {
5894
6153
  void state.contextChipVersion;
@@ -5934,10 +6193,10 @@ function App({
5934
6193
  currentTool: state.leader.currentTool,
5935
6194
  ctxPct: state.leader.ctxPct,
5936
6195
  ctxTokens: state.leader.ctxTokens,
5937
- ctxMaxTokens: state.leader.ctxMaxTokens
6196
+ ctxMaxTokens: state.leader.ctxMaxTokens ?? effectiveMaxContext
5938
6197
  };
5939
6198
  return { leader: leaderEntry, ...state.fleet };
5940
- }, [state.fleet, state.leader, state.status, provider, model]);
6199
+ }, [state.fleet, state.leader, state.status, provider, model, effectiveMaxContext]);
5941
6200
  const STREAM_COLORS = ["cyan", "magenta", "yellow", "green", "blue"];
5942
6201
  const labelsRef = useRef(/* @__PURE__ */ new Map());
5943
6202
  const labelFor = (id, name) => {
@@ -5990,7 +6249,7 @@ function App({
5990
6249
  const prevEntriesCount = useRef(0);
5991
6250
  const eraseLiveRegion = useCallback(() => {
5992
6251
  try {
5993
- process.stdout.write("\x1B[J");
6252
+ writeOut("\x1B[J");
5994
6253
  } catch {
5995
6254
  }
5996
6255
  }, []);
@@ -6202,7 +6461,7 @@ function App({
6202
6461
  const arg = args.trim().toLowerCase();
6203
6462
  if (arg === "off") {
6204
6463
  try {
6205
- process.stdout.write(ALT_OFF);
6464
+ writeOut(ALT_OFF);
6206
6465
  } catch {
6207
6466
  return { message: "Failed to exit alt-screen." };
6208
6467
  }
@@ -6213,7 +6472,7 @@ function App({
6213
6472
  }
6214
6473
  if (arg === "on") {
6215
6474
  try {
6216
- process.stdout.write(ALT_ON);
6475
+ writeOut(ALT_ON);
6217
6476
  } catch {
6218
6477
  return { message: "Failed to re-enter alt-screen." };
6219
6478
  }
@@ -6254,9 +6513,9 @@ function App({
6254
6513
  };
6255
6514
  }
6256
6515
  try {
6257
- process.stdout.write(ALT_ON);
6258
- process.stdout.write("\x1B[H");
6259
- process.stdout.write(MOUSE_ON_SEQ);
6516
+ writeOut(ALT_ON);
6517
+ writeOut("\x1B[H");
6518
+ writeOut(MOUSE_ON_SEQ);
6260
6519
  } catch {
6261
6520
  return { message: "Failed to enable mouse mode." };
6262
6521
  }
@@ -6267,8 +6526,8 @@ function App({
6267
6526
  };
6268
6527
  }
6269
6528
  try {
6270
- process.stdout.write(MOUSE_OFF_SEQ);
6271
- process.stdout.write(ALT_OFF);
6529
+ writeOut(MOUSE_OFF_SEQ);
6530
+ writeOut(ALT_OFF);
6272
6531
  } catch {
6273
6532
  return { message: "Failed to disable mouse mode." };
6274
6533
  }
@@ -6714,6 +6973,7 @@ function App({
6714
6973
  });
6715
6974
  });
6716
6975
  const offLeaderCtxPct = events.on("ctx.pct", (e) => {
6976
+ setActiveMaxContext(e.maxContext);
6717
6977
  dispatch({
6718
6978
  type: "leaderCtxPct",
6719
6979
  load: e.load,
@@ -6721,6 +6981,9 @@ function App({
6721
6981
  maxContext: e.maxContext
6722
6982
  });
6723
6983
  });
6984
+ const offLeaderMaxContext = events.on("ctx.max_context", (e) => {
6985
+ if (e.maxContext > 0) setActiveMaxContext(e.maxContext);
6986
+ });
6724
6987
  const offTool = events.on("subagent.tool_executed", (e) => {
6725
6988
  dispatch({
6726
6989
  type: "fleetTool",
@@ -6741,6 +7004,7 @@ function App({
6741
7004
  offIterationSummary();
6742
7005
  offCtxPct();
6743
7006
  offLeaderCtxPct();
7007
+ offLeaderMaxContext();
6744
7008
  offTool();
6745
7009
  };
6746
7010
  }, [events, director]);
@@ -6768,6 +7032,71 @@ function App({
6768
7032
  offRewound();
6769
7033
  };
6770
7034
  }, [events, onClearHistory]);
7035
+ useEffect(() => {
7036
+ const requestSummary = (request) => `${request.source}: ${request.question}`.slice(0, 80);
7037
+ const addBrainEntry = (status, payload) => {
7038
+ const p = payload;
7039
+ const decision = p.decision.optionId ?? p.decision.text ?? p.decision.reason ?? p.decision.prompt ?? p.decision.type;
7040
+ dispatch({
7041
+ type: "brainStatus",
7042
+ state: status,
7043
+ source: p.request.source,
7044
+ risk: p.request.risk,
7045
+ summary: decision
7046
+ });
7047
+ if (status === "ask_human") {
7048
+ dispatch({
7049
+ type: "brainPromptSet",
7050
+ prompt: {
7051
+ requestId: p.request.id,
7052
+ source: p.request.source,
7053
+ risk: p.request.risk,
7054
+ question: p.request.question,
7055
+ context: p.request.context,
7056
+ options: p.request.options
7057
+ }
7058
+ });
7059
+ } else {
7060
+ dispatch({ type: "brainPromptClear" });
7061
+ }
7062
+ dispatch({
7063
+ type: "addEntry",
7064
+ entry: {
7065
+ kind: "brain",
7066
+ status,
7067
+ source: p.request.source,
7068
+ risk: p.request.risk,
7069
+ question: p.request.question,
7070
+ decision,
7071
+ rationale: p.decision.rationale
7072
+ }
7073
+ });
7074
+ };
7075
+ const offRequested = events.on("brain.decision_requested", ({ request }) => {
7076
+ dispatch({
7077
+ type: "brainStatus",
7078
+ state: "deciding",
7079
+ source: request.source,
7080
+ risk: request.risk,
7081
+ summary: requestSummary(request)
7082
+ });
7083
+ });
7084
+ const offAnswered = events.on("brain.decision_answered", (payload) => {
7085
+ addBrainEntry("answered", payload);
7086
+ });
7087
+ const offAskHuman = events.on("brain.decision_ask_human", (payload) => {
7088
+ addBrainEntry("ask_human", payload);
7089
+ });
7090
+ const offDenied = events.on("brain.decision_denied", (payload) => {
7091
+ addBrainEntry("denied", payload);
7092
+ });
7093
+ return () => {
7094
+ offRequested();
7095
+ offAnswered();
7096
+ offAskHuman();
7097
+ offDenied();
7098
+ };
7099
+ }, [events]);
6771
7100
  useEffect(() => {
6772
7101
  if (!subscribeAutoPhase) return;
6773
7102
  const handler = (event, payload) => {
@@ -7558,6 +7887,7 @@ function App({
7558
7887
  }
7559
7888
  setLiveProvider(providerId);
7560
7889
  setLiveModel(modelId);
7890
+ setActiveMaxContext(agent.ctx.provider.capabilities.maxContext);
7561
7891
  dispatch({
7562
7892
  type: "addEntry",
7563
7893
  entry: { kind: "info", text: `Switched to ${providerId} / ${modelId}.` }
@@ -7728,25 +8058,23 @@ function App({
7728
8058
  });
7729
8059
  return;
7730
8060
  }
7731
- if (key.ctrl && input === "f") {
8061
+ const toggleFleetOverlay = () => {
7732
8062
  if (state.agentsMonitorOpen) {
7733
8063
  dispatch({ type: "toggleAgentsMonitor" });
7734
8064
  dispatch({ type: "toggleMonitor" });
7735
8065
  } else {
7736
8066
  dispatch({ type: "toggleMonitor" });
7737
8067
  }
7738
- return;
7739
- }
7740
- if (key.ctrl && input === "g") {
8068
+ };
8069
+ const toggleAgentsOverlay = () => {
7741
8070
  if (state.monitorOpen) {
7742
8071
  dispatch({ type: "toggleMonitor" });
7743
8072
  dispatch({ type: "toggleAgentsMonitor" });
7744
8073
  } else {
7745
8074
  dispatch({ type: "toggleAgentsMonitor" });
7746
8075
  }
7747
- return;
7748
- }
7749
- if (key.ctrl && input === "t") {
8076
+ };
8077
+ const toggleWorktreeOverlay = () => {
7750
8078
  if (state.worktreeMonitorOpen) {
7751
8079
  dispatch({ type: "worktreeMonitorToggle" });
7752
8080
  return;
@@ -7755,6 +8083,17 @@ function App({
7755
8083
  if (state.monitorOpen) dispatch({ type: "toggleMonitor" });
7756
8084
  if (state.autoPhase?.monitorOpen) dispatch({ type: "autoPhaseMonitorToggle" });
7757
8085
  dispatch({ type: "worktreeMonitorToggle" });
8086
+ };
8087
+ if (key.ctrl && input === "f" || key.fn === 2) {
8088
+ toggleFleetOverlay();
8089
+ return;
8090
+ }
8091
+ if (key.ctrl && input === "g" || key.fn === 3) {
8092
+ toggleAgentsOverlay();
8093
+ return;
8094
+ }
8095
+ if (key.ctrl && input === "t" || key.fn === 4) {
8096
+ toggleWorktreeOverlay();
7758
8097
  return;
7759
8098
  }
7760
8099
  if (key.ctrl && input === "s") {
@@ -7910,6 +8249,7 @@ function App({
7910
8249
  return;
7911
8250
  }
7912
8251
  if (!input || key.ctrl || key.meta) return;
8252
+ if (input.charCodeAt(0) === 27) return;
7913
8253
  if (input.length > PASTE_THRESHOLD_CHARS) {
7914
8254
  await commitPaste(input);
7915
8255
  return;
@@ -8148,6 +8488,10 @@ function App({
8148
8488
  if (ctxModel && ctxModel !== liveModel) setLiveModel(ctxModel);
8149
8489
  const ctxProviderId = agent.ctx.provider?.id;
8150
8490
  if (ctxProviderId && ctxProviderId !== liveProvider) setLiveProvider(ctxProviderId);
8491
+ const ctxMaxContext = agent.ctx.provider.capabilities.maxContext;
8492
+ if (ctxMaxContext > 0 && ctxMaxContext !== activeMaxContext) {
8493
+ setActiveMaxContext(ctxMaxContext);
8494
+ }
8151
8495
  if (getYolo) {
8152
8496
  const currentYolo = getYolo();
8153
8497
  if (currentYolo !== yoloLive) setYoloLive(currentYolo);
@@ -8372,6 +8716,16 @@ User message:
8372
8716
  onClose: () => dispatch({ type: "rewindOverlayClose" })
8373
8717
  }
8374
8718
  ) : null,
8719
+ state.brainPrompt ? /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginY: 1, flexShrink: 0, children: /* @__PURE__ */ jsx(
8720
+ BrainDecisionPrompt,
8721
+ {
8722
+ ...state.brainPrompt,
8723
+ onAnswer: (answer) => {
8724
+ events.emit("brain.human_answered", { ...answer, at: Date.now() });
8725
+ dispatch({ type: "brainPromptClear" });
8726
+ }
8727
+ }
8728
+ ) }) : null,
8375
8729
  state.confirmQueue.length > 0 && (() => {
8376
8730
  const head = state.confirmQueue[0];
8377
8731
  let resolved = false;
@@ -8409,6 +8763,7 @@ User message:
8409
8763
  fleet: fleetCounts,
8410
8764
  git: gitInfo,
8411
8765
  context: contextWindow,
8766
+ brain: state.brain,
8412
8767
  projectName,
8413
8768
  subagentCount: Object.keys(state.fleet).length,
8414
8769
  processCount: getProcessRegistry().activeCount,
@@ -8634,7 +8989,7 @@ async function runTui(opts) {
8634
8989
  const stdout = process.stdout;
8635
8990
  const stdin = process.stdin;
8636
8991
  if (!stdout.isTTY || !stdin.isTTY) {
8637
- process.stderr.write(
8992
+ writeErr(
8638
8993
  "wstack: --tui requires an interactive terminal on both stdin and stdout.\n Drop the flag (use the plain REPL) or run wstack directly without piping.\n"
8639
8994
  );
8640
8995
  return 2;
@@ -8861,7 +9216,7 @@ async function runTui(opts) {
8861
9216
  { exitOnCtrlC: false, stdin: inkStdin }
8862
9217
  );
8863
9218
  } catch (err) {
8864
- process.stderr.write(
9219
+ writeErr(
8865
9220
  `wstack: TUI failed to start: ${err instanceof Error ? err.message : String(err)}
8866
9221
  `
8867
9222
  );