@wrongstack/tui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -26,6 +26,10 @@ interface RunTuiOptions {
26
26
  queueStore?: QueueStore;
27
27
  /** Surfaces the "⚠ YOLO" chip in the status bar. */
28
28
  yolo?: boolean;
29
+ /** Query live YOLO state from the permission policy. */
30
+ getYolo?: () => boolean;
31
+ /** Query the live autonomy mode. */
32
+ getAutonomy?: () => 'off' | 'suggest' | 'auto';
29
33
  /** Renders in the startup banner. Read from the CLI's package.json. */
30
34
  appVersion?: string;
31
35
  /** Provider id for the startup banner ("openai", "anthropic", ...). */
@@ -110,6 +114,16 @@ interface RunTuiOptions {
110
114
  * Ignored when `initialGoal` is also set.
111
115
  */
112
116
  initialAsk?: string;
117
+ /**
118
+ * SDD session context getter. When an SDD session is active, returns
119
+ * the AI prompt context to inject into user messages.
120
+ */
121
+ getSDDContext?: () => string | null;
122
+ /**
123
+ * Process AI output for SDD auto-detection (spec, tasks, plan).
124
+ * Returns displayable status messages.
125
+ */
126
+ onSDDOutput?: (output: string) => Promise<string[]>;
113
127
  }
114
128
  declare function runTui(opts: RunTuiOptions): Promise<number>;
115
129
 
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { render, useApp, Box, useStdout, Static, Text, useInput, useStdin } from 'ink';
2
- import React4, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
2
+ import React2, { useState, useReducer, useRef, useEffect, useMemo } from 'react';
3
3
  import * as fs2 from 'fs/promises';
4
4
  import * as path2 from 'path';
5
- import { InputBuilder, formatTodosList } from '@wrongstack/core';
5
+ import { InputBuilder, formatTodosList, buildChildEnv } from '@wrongstack/core';
6
6
  import { routeImagesForModel } from '@wrongstack/runtime/vision';
7
7
  import { readClipboardImage } from '@wrongstack/runtime/clipboard';
8
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -39,7 +39,7 @@ function ConfirmPrompt({
39
39
  suggestedPattern,
40
40
  onDecision
41
41
  }) {
42
- React4.useEffect(() => {
42
+ React2.useEffect(() => {
43
43
  process.stdout.write("\x07");
44
44
  }, []);
45
45
  useInput((input2, key) => {
@@ -244,6 +244,15 @@ function FleetPanel({ entries, totalCost, roster }) {
244
244
  "msg: ",
245
245
  message
246
246
  ] }) }, `${entry.id}-msg-${index}-${message}`)),
247
+ entry.budgetWarning ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
248
+ "\u26A1 hitting ",
249
+ entry.budgetWarning.kind,
250
+ " limit (",
251
+ entry.budgetWarning.used,
252
+ "/",
253
+ entry.budgetWarning.limit,
254
+ ") \u2014 extending"
255
+ ] }) }) : null,
247
256
  entry.status === "running" && entry.streamingText ? /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
248
257
  ">",
249
258
  " ",
@@ -1563,6 +1572,7 @@ function StatusBar({
1563
1572
  hint,
1564
1573
  queueCount = 0,
1565
1574
  yolo = false,
1575
+ autonomy,
1566
1576
  elapsedMs,
1567
1577
  todos,
1568
1578
  plan,
@@ -1578,7 +1588,7 @@ function StatusBar({
1578
1588
  const cache2 = tokenCounter?.cacheStats();
1579
1589
  const stateColor = state === "idle" ? "cyan" : state === "aborting" ? "yellow" : "green";
1580
1590
  const stateLabel = state === "idle" ? "idle" : state === "aborting" ? "aborting\u2026" : "thinking\u2026";
1581
- const hasSecondLine = yolo || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0;
1591
+ const hasSecondLine = yolo || autonomy && autonomy !== "off" || elapsedMs !== void 0 || git !== null && git !== void 0 || projectName !== void 0 && projectName.length > 0;
1582
1592
  const fleetHasActivity = fleet && (fleet.running > 0 || fleet.idle > 0 || fleet.pending > 0 || fleet.completed > 0) || subagentCount > 0;
1583
1593
  const hasThirdLine = todos && (todos.pending > 0 || todos.inProgress > 0 || todos.completed > 0) || plan && (plan.open > 0 || plan.inProgress > 0 || plan.done > 0) || fleetHasActivity;
1584
1594
  return /* @__PURE__ */ jsxs(
@@ -1643,6 +1653,13 @@ function StatusBar({
1643
1653
  ] }),
1644
1654
  hasSecondLine ? /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 2, children: [
1645
1655
  yolo ? /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "\u26A0 YOLO" }) : null,
1656
+ autonomy && autonomy !== "off" ? /* @__PURE__ */ jsxs(Fragment, { children: [
1657
+ yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1658
+ /* @__PURE__ */ jsxs(Text, { color: autonomy === "auto" ? "yellow" : "cyan", bold: true, children: [
1659
+ "\u221E ",
1660
+ autonomy.toUpperCase()
1661
+ ] })
1662
+ ] }) : null,
1646
1663
  elapsedMs !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1647
1664
  yolo ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }) : null,
1648
1665
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
@@ -1927,6 +1944,7 @@ function runGit(cwd, args) {
1927
1944
  try {
1928
1945
  const child = spawn("git", args, {
1929
1946
  cwd,
1947
+ env: buildChildEnv(),
1930
1948
  // Inherit stderr (silent) — we don't care about git's noise.
1931
1949
  stdio: ["ignore", "pipe", "ignore"],
1932
1950
  // Don't let a slow git hang the TUI.
@@ -2340,6 +2358,8 @@ function reducer(state, action) {
2340
2358
  ...cur,
2341
2359
  status: "running",
2342
2360
  streamingText: "",
2361
+ budgetWarning: void 0,
2362
+ // clear on restart
2343
2363
  startedAt: Date.now()
2344
2364
  }
2345
2365
  }
@@ -2422,6 +2442,23 @@ function reducer(state, action) {
2422
2442
  toolCalls: action.toolCalls,
2423
2443
  streamingText: "",
2424
2444
  currentTool: void 0,
2445
+ budgetWarning: void 0,
2446
+ // clear on done/restart
2447
+ lastEventAt: Date.now()
2448
+ }
2449
+ }
2450
+ };
2451
+ }
2452
+ case "fleetBudgetWarning": {
2453
+ const cur = state.fleet[action.id];
2454
+ if (!cur) return state;
2455
+ return {
2456
+ ...state,
2457
+ fleet: {
2458
+ ...state.fleet,
2459
+ [action.id]: {
2460
+ ...cur,
2461
+ budgetWarning: { kind: action.kind, used: action.used, limit: action.limit, at: Date.now() },
2425
2462
  lastEventAt: Date.now()
2426
2463
  }
2427
2464
  }
@@ -2551,6 +2588,10 @@ function App({
2551
2588
  banner = true,
2552
2589
  queueStore,
2553
2590
  yolo = false,
2591
+ getYolo,
2592
+ getAutonomy,
2593
+ getSDDContext,
2594
+ onSDDOutput,
2554
2595
  appVersion,
2555
2596
  provider,
2556
2597
  family,
@@ -2569,6 +2610,8 @@ function App({
2569
2610
  const { exit } = useApp();
2570
2611
  const [liveModel, setLiveModel] = useState(model);
2571
2612
  const [liveProvider, setLiveProvider] = useState(provider ?? "agent");
2613
+ const [yoloLive, setYoloLive] = useState(yolo);
2614
+ const [autonomyLive, setAutonomyLive] = useState(getAutonomy?.() ?? "off");
2572
2615
  const [state, dispatch] = useReducer(reducer, {
2573
2616
  entries: banner ? [
2574
2617
  {
@@ -2621,7 +2664,7 @@ function App({
2621
2664
  const inputGateRef = useRef(false);
2622
2665
  const lastEnterAtRef = useRef(0);
2623
2666
  const projectRoot = agent.ctx.projectRoot;
2624
- const projectName = React4.useMemo(() => {
2667
+ const projectName = React2.useMemo(() => {
2625
2668
  const base = path2.basename(projectRoot);
2626
2669
  return base && base !== path2.sep ? base : void 0;
2627
2670
  }, [projectRoot]);
@@ -2641,13 +2684,13 @@ function App({
2641
2684
  dispatch({ type: "clearInput" });
2642
2685
  };
2643
2686
  const startedAtRef = useRef(Date.now());
2644
- const [nowTick, setNowTick] = React4.useState(Date.now());
2687
+ const [nowTick, setNowTick] = React2.useState(Date.now());
2645
2688
  useEffect(() => {
2646
2689
  const t = setInterval(() => setNowTick(Date.now()), 1e3);
2647
2690
  return () => clearInterval(t);
2648
2691
  }, []);
2649
2692
  const elapsedMs = nowTick - startedAtRef.current;
2650
- const [gitInfo, setGitInfo] = React4.useState(null);
2693
+ const [gitInfo, setGitInfo] = React2.useState(null);
2651
2694
  useEffect(() => {
2652
2695
  let cancelled = false;
2653
2696
  const refresh = () => {
@@ -2662,21 +2705,13 @@ function App({
2662
2705
  clearInterval(t);
2663
2706
  };
2664
2707
  }, [agent.ctx.cwd]);
2665
- const [lastInputTokens, setLastInputTokens] = React4.useState(0);
2666
- useEffect(() => {
2667
- const off = events.on("provider.response", (e) => {
2668
- const total = (e.usage.input ?? 0) + (e.usage.cacheRead ?? 0) + (e.usage.cacheWrite ?? 0);
2669
- setLastInputTokens(total);
2670
- });
2671
- return () => {
2672
- off();
2673
- };
2674
- }, [events]);
2708
+ (tokenCounter?.total().input ?? 0) + (tokenCounter?.total().cacheRead ?? 0) + (tokenCounter?.total().cacheWrite ?? 0);
2675
2709
  const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
2710
+ const currentContextTokens = (tokenCounter?.currentRequestTokens()?.input ?? 0) + (tokenCounter?.currentRequestTokens()?.cacheRead ?? 0);
2676
2711
  const contextWindow = useMemo(() => {
2677
2712
  void state.contextChipVersion;
2678
- return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2679
- }, [lastInputTokens, maxContext, state.contextChipVersion]);
2713
+ return currentContextTokens > 0 && maxContext > 0 ? { used: currentContextTokens, max: maxContext } : void 0;
2714
+ }, [currentContextTokens, maxContext, state.contextChipVersion]);
2680
2715
  const todos = useMemo(() => {
2681
2716
  const counts = { pending: 0, inProgress: 0, completed: 0 };
2682
2717
  for (const t of agent.ctx.todos) {
@@ -3272,6 +3307,20 @@ function App({
3272
3307
  }
3273
3308
  });
3274
3309
  });
3310
+ const offBudgetWarning = events.on("subagent.budget_warning", (e) => {
3311
+ const lbl = labelFor(e.subagentId);
3312
+ dispatch({ type: "fleetBudgetWarning", id: e.subagentId, kind: e.kind, used: e.used, limit: e.limit });
3313
+ dispatch({
3314
+ type: "addEntry",
3315
+ entry: {
3316
+ kind: "subagent",
3317
+ agentLabel: lbl.label,
3318
+ agentColor: lbl.color,
3319
+ icon: "\u26A1",
3320
+ text: `hitting ${e.kind} limit (${e.used}/${e.limit}) \u2014 extending`
3321
+ }
3322
+ });
3323
+ });
3275
3324
  const offTool = events.on("subagent.tool_executed", (e) => {
3276
3325
  if (director) return;
3277
3326
  dispatch({
@@ -3288,6 +3337,7 @@ function App({
3288
3337
  offSpawned();
3289
3338
  offStarted();
3290
3339
  offCompleted();
3340
+ offBudgetWarning();
3291
3341
  offTool();
3292
3342
  };
3293
3343
  }, [events, director]);
@@ -3521,7 +3571,7 @@ function App({
3521
3571
  return () => {
3522
3572
  process.off("SIGINT", onSigint);
3523
3573
  };
3524
- }, [exit, onExit, director]);
3574
+ }, [director]);
3525
3575
  const handleKey = async (input, key) => {
3526
3576
  if (state.status === "aborting" && state.interrupts === 0) return;
3527
3577
  if (state.confirmQueue.length > 0) return;
@@ -3848,6 +3898,15 @@ function App({
3848
3898
  entry: { kind: "warn", text: `Hit max iterations (${result.iterations}).` }
3849
3899
  });
3850
3900
  }
3901
+ if (result.status === "done" && result.finalText && onSDDOutput) {
3902
+ try {
3903
+ const sddMessages = await onSDDOutput(result.finalText);
3904
+ for (const msg of sddMessages) {
3905
+ dispatch({ type: "addEntry", entry: { kind: "info", text: msg } });
3906
+ }
3907
+ } catch {
3908
+ }
3909
+ }
3851
3910
  if (tokenCounter && before) {
3852
3911
  const after = tokenCounter.total();
3853
3912
  const costAfter = tokenCounter.estimateCost().total;
@@ -3903,6 +3962,14 @@ function App({
3903
3962
  if (ctxModel && ctxModel !== liveModel) setLiveModel(ctxModel);
3904
3963
  const ctxProviderId = agent.ctx.provider?.id;
3905
3964
  if (ctxProviderId && ctxProviderId !== liveProvider) setLiveProvider(ctxProviderId);
3965
+ if (getYolo) {
3966
+ const currentYolo = getYolo();
3967
+ if (currentYolo !== yoloLive) setYoloLive(currentYolo);
3968
+ }
3969
+ if (getAutonomy) {
3970
+ const currentAutonomy = getAutonomy();
3971
+ if (currentAutonomy !== autonomyLive) setAutonomyLive(currentAutonomy);
3972
+ }
3906
3973
  if (res?.exit) {
3907
3974
  exit();
3908
3975
  onExit(0);
@@ -3934,6 +4001,15 @@ function App({
3934
4001
  const builder = builderRef.current;
3935
4002
  if (!builder) return;
3936
4003
  const steering = state.steeringPending;
4004
+ const sddContext = getSDDContext?.();
4005
+ if (sddContext && trimmed) {
4006
+ builder.appendText(`[SDD SESSION ACTIVE]
4007
+ ${sddContext}
4008
+
4009
+ ---
4010
+ User message:
4011
+ `);
4012
+ }
3937
4013
  if (trimmed) {
3938
4014
  const toAppend = steering ? buildSteeringPreamble(state.steerSnapshot, trimmed) : trimmed;
3939
4015
  builder.appendText(toAppend);
@@ -4064,7 +4140,8 @@ function App({
4064
4140
  tokenCounter,
4065
4141
  hint: renderRunningTools(state.runningTools) || state.hint,
4066
4142
  queueCount: state.queue.length,
4067
- yolo,
4143
+ yolo: yoloLive,
4144
+ autonomy: autonomyLive,
4068
4145
  elapsedMs,
4069
4146
  todos,
4070
4147
  plan: planCounts ?? void 0,
@@ -4187,7 +4264,7 @@ async function runTui(opts) {
4187
4264
  let instance;
4188
4265
  try {
4189
4266
  instance = render(
4190
- React4.createElement(App, {
4267
+ React2.createElement(App, {
4191
4268
  agent: opts.agent,
4192
4269
  slashRegistry: opts.slashRegistry,
4193
4270
  attachments: opts.attachments,
@@ -4199,6 +4276,8 @@ async function runTui(opts) {
4199
4276
  banner: opts.banner ?? true,
4200
4277
  queueStore: opts.queueStore,
4201
4278
  yolo: opts.yolo,
4279
+ getYolo: opts.getYolo,
4280
+ getAutonomy: opts.getAutonomy,
4202
4281
  appVersion: opts.appVersion,
4203
4282
  provider: opts.provider,
4204
4283
  family: opts.family,
@@ -4212,7 +4291,9 @@ async function runTui(opts) {
4212
4291
  onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0,
4213
4292
  fleetStreamController: opts.fleetStreamController,
4214
4293
  initialGoal: opts.initialGoal,
4215
- initialAsk: opts.initialAsk
4294
+ initialAsk: opts.initialAsk,
4295
+ getSDDContext: opts.getSDDContext,
4296
+ onSDDOutput: opts.onSDDOutput
4216
4297
  }),
4217
4298
  { exitOnCtrlC: false }
4218
4299
  );