@wrongstack/tui 0.4.1 → 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,5 +1,5 @@
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
5
  import { InputBuilder, formatTodosList, buildChildEnv } from '@wrongstack/core';
@@ -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: [
@@ -2341,6 +2358,8 @@ function reducer(state, action) {
2341
2358
  ...cur,
2342
2359
  status: "running",
2343
2360
  streamingText: "",
2361
+ budgetWarning: void 0,
2362
+ // clear on restart
2344
2363
  startedAt: Date.now()
2345
2364
  }
2346
2365
  }
@@ -2423,6 +2442,23 @@ function reducer(state, action) {
2423
2442
  toolCalls: action.toolCalls,
2424
2443
  streamingText: "",
2425
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() },
2426
2462
  lastEventAt: Date.now()
2427
2463
  }
2428
2464
  }
@@ -2552,6 +2588,10 @@ function App({
2552
2588
  banner = true,
2553
2589
  queueStore,
2554
2590
  yolo = false,
2591
+ getYolo,
2592
+ getAutonomy,
2593
+ getSDDContext,
2594
+ onSDDOutput,
2555
2595
  appVersion,
2556
2596
  provider,
2557
2597
  family,
@@ -2570,6 +2610,8 @@ function App({
2570
2610
  const { exit } = useApp();
2571
2611
  const [liveModel, setLiveModel] = useState(model);
2572
2612
  const [liveProvider, setLiveProvider] = useState(provider ?? "agent");
2613
+ const [yoloLive, setYoloLive] = useState(yolo);
2614
+ const [autonomyLive, setAutonomyLive] = useState(getAutonomy?.() ?? "off");
2573
2615
  const [state, dispatch] = useReducer(reducer, {
2574
2616
  entries: banner ? [
2575
2617
  {
@@ -2622,7 +2664,7 @@ function App({
2622
2664
  const inputGateRef = useRef(false);
2623
2665
  const lastEnterAtRef = useRef(0);
2624
2666
  const projectRoot = agent.ctx.projectRoot;
2625
- const projectName = React4.useMemo(() => {
2667
+ const projectName = React2.useMemo(() => {
2626
2668
  const base = path2.basename(projectRoot);
2627
2669
  return base && base !== path2.sep ? base : void 0;
2628
2670
  }, [projectRoot]);
@@ -2642,13 +2684,13 @@ function App({
2642
2684
  dispatch({ type: "clearInput" });
2643
2685
  };
2644
2686
  const startedAtRef = useRef(Date.now());
2645
- const [nowTick, setNowTick] = React4.useState(Date.now());
2687
+ const [nowTick, setNowTick] = React2.useState(Date.now());
2646
2688
  useEffect(() => {
2647
2689
  const t = setInterval(() => setNowTick(Date.now()), 1e3);
2648
2690
  return () => clearInterval(t);
2649
2691
  }, []);
2650
2692
  const elapsedMs = nowTick - startedAtRef.current;
2651
- const [gitInfo, setGitInfo] = React4.useState(null);
2693
+ const [gitInfo, setGitInfo] = React2.useState(null);
2652
2694
  useEffect(() => {
2653
2695
  let cancelled = false;
2654
2696
  const refresh = () => {
@@ -2663,12 +2705,13 @@ function App({
2663
2705
  clearInterval(t);
2664
2706
  };
2665
2707
  }, [agent.ctx.cwd]);
2666
- const lastInputTokens = tokenCounter?.total().input ?? 0;
2708
+ (tokenCounter?.total().input ?? 0) + (tokenCounter?.total().cacheRead ?? 0) + (tokenCounter?.total().cacheWrite ?? 0);
2667
2709
  const maxContext = effectiveMaxContext ?? agent.ctx.provider.capabilities.maxContext;
2710
+ const currentContextTokens = (tokenCounter?.currentRequestTokens()?.input ?? 0) + (tokenCounter?.currentRequestTokens()?.cacheRead ?? 0);
2668
2711
  const contextWindow = useMemo(() => {
2669
2712
  void state.contextChipVersion;
2670
- return lastInputTokens > 0 && maxContext > 0 ? { used: lastInputTokens, max: maxContext } : void 0;
2671
- }, [lastInputTokens, maxContext, state.contextChipVersion]);
2713
+ return currentContextTokens > 0 && maxContext > 0 ? { used: currentContextTokens, max: maxContext } : void 0;
2714
+ }, [currentContextTokens, maxContext, state.contextChipVersion]);
2672
2715
  const todos = useMemo(() => {
2673
2716
  const counts = { pending: 0, inProgress: 0, completed: 0 };
2674
2717
  for (const t of agent.ctx.todos) {
@@ -3264,6 +3307,20 @@ function App({
3264
3307
  }
3265
3308
  });
3266
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
+ });
3267
3324
  const offTool = events.on("subagent.tool_executed", (e) => {
3268
3325
  if (director) return;
3269
3326
  dispatch({
@@ -3280,6 +3337,7 @@ function App({
3280
3337
  offSpawned();
3281
3338
  offStarted();
3282
3339
  offCompleted();
3340
+ offBudgetWarning();
3283
3341
  offTool();
3284
3342
  };
3285
3343
  }, [events, director]);
@@ -3513,7 +3571,7 @@ function App({
3513
3571
  return () => {
3514
3572
  process.off("SIGINT", onSigint);
3515
3573
  };
3516
- }, [exit, onExit, director]);
3574
+ }, [director]);
3517
3575
  const handleKey = async (input, key) => {
3518
3576
  if (state.status === "aborting" && state.interrupts === 0) return;
3519
3577
  if (state.confirmQueue.length > 0) return;
@@ -3840,6 +3898,15 @@ function App({
3840
3898
  entry: { kind: "warn", text: `Hit max iterations (${result.iterations}).` }
3841
3899
  });
3842
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
+ }
3843
3910
  if (tokenCounter && before) {
3844
3911
  const after = tokenCounter.total();
3845
3912
  const costAfter = tokenCounter.estimateCost().total;
@@ -3895,6 +3962,14 @@ function App({
3895
3962
  if (ctxModel && ctxModel !== liveModel) setLiveModel(ctxModel);
3896
3963
  const ctxProviderId = agent.ctx.provider?.id;
3897
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
+ }
3898
3973
  if (res?.exit) {
3899
3974
  exit();
3900
3975
  onExit(0);
@@ -3926,6 +4001,15 @@ function App({
3926
4001
  const builder = builderRef.current;
3927
4002
  if (!builder) return;
3928
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
+ }
3929
4013
  if (trimmed) {
3930
4014
  const toAppend = steering ? buildSteeringPreamble(state.steerSnapshot, trimmed) : trimmed;
3931
4015
  builder.appendText(toAppend);
@@ -4056,7 +4140,8 @@ function App({
4056
4140
  tokenCounter,
4057
4141
  hint: renderRunningTools(state.runningTools) || state.hint,
4058
4142
  queueCount: state.queue.length,
4059
- yolo,
4143
+ yolo: yoloLive,
4144
+ autonomy: autonomyLive,
4060
4145
  elapsedMs,
4061
4146
  todos,
4062
4147
  plan: planCounts ?? void 0,
@@ -4179,7 +4264,7 @@ async function runTui(opts) {
4179
4264
  let instance;
4180
4265
  try {
4181
4266
  instance = render(
4182
- React4.createElement(App, {
4267
+ React2.createElement(App, {
4183
4268
  agent: opts.agent,
4184
4269
  slashRegistry: opts.slashRegistry,
4185
4270
  attachments: opts.attachments,
@@ -4191,6 +4276,8 @@ async function runTui(opts) {
4191
4276
  banner: opts.banner ?? true,
4192
4277
  queueStore: opts.queueStore,
4193
4278
  yolo: opts.yolo,
4279
+ getYolo: opts.getYolo,
4280
+ getAutonomy: opts.getAutonomy,
4194
4281
  appVersion: opts.appVersion,
4195
4282
  provider: opts.provider,
4196
4283
  family: opts.family,
@@ -4204,7 +4291,9 @@ async function runTui(opts) {
4204
4291
  onClearHistory: opts.onClearHistory ? (dispatch) => opts.onClearHistory(dispatch) : void 0,
4205
4292
  fleetStreamController: opts.fleetStreamController,
4206
4293
  initialGoal: opts.initialGoal,
4207
- initialAsk: opts.initialAsk
4294
+ initialAsk: opts.initialAsk,
4295
+ getSDDContext: opts.getSDDContext,
4296
+ onSDDOutput: opts.onSDDOutput
4208
4297
  }),
4209
4298
  { exitOnCtrlC: false }
4210
4299
  );