@wrongstack/tui 0.236.0 → 0.250.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
@@ -417,6 +417,12 @@ interface RunTuiOptions {
417
417
  * Used by the TUI to display and auto-submit next steps in 'auto' mode.
418
418
  */
419
419
  getSuggestions?: (() => string[]) | undefined;
420
+ /**
421
+ * Write parsed next steps into the shared suggestion store.
422
+ * Called by the Entry component after parsing each assistant message
423
+ * so /next 1 and the auto-submit countdown can access them.
424
+ */
425
+ setSuggestions?: ((steps: string[]) => void) | undefined;
420
426
  /**
421
427
  * Messages restored from a previous session. When provided (non-empty),
422
428
  * the TUI renders the prior conversation as history entries so a resumed
@@ -531,4 +537,46 @@ declare function parseInline(text: string): InlineToken[];
531
537
  */
532
538
  declare function replaySessionEvents(events: SessionEvent[], startId: number): HistoryEntry[];
533
539
 
534
- export { type RunTuiOptions, type Settings, parseInline, replaySessionEvents, runTui };
540
+ /**
541
+ * Unified next-steps suggestion parser.
542
+ *
543
+ * Three code paths feed into the suggestion store:
544
+ * 1. TUI rendering — entry.tsx parses "💡 Next steps" from assistant output
545
+ * 2. REPL store — repl.ts parses "💡 Next steps" from final agent output
546
+ * 3. /suggest output — suggest.ts parses LLM-generated numbered lists
547
+ *
548
+ * Heading mode (`requireHeading = true`):
549
+ * strict=true — only 💡 emoji heading (TUI rendering)
550
+ * strict=false — 💡, ##, plain "Next steps" headings (REPL store)
551
+ *
552
+ * Raw mode (`requireHeading = false`):
553
+ * Parses numbered/bullet items from anywhere in text (subagent /suggest output).
554
+ */
555
+ interface ParsedNextStep {
556
+ index: number;
557
+ text: string;
558
+ }
559
+ interface ParseNextStepsResult {
560
+ /** Matched steps with their original index and stripped text. */
561
+ steps: ParsedNextStep[];
562
+ /** Flat string array — what gets stored in the suggestion store. */
563
+ texts: string[];
564
+ /**
565
+ * Content with the entire "💡 Next steps" block removed.
566
+ * Used by entry.tsx to strip suggestions from the rendered message body.
567
+ */
568
+ stripped: string;
569
+ }
570
+ /**
571
+ * Parse "💡 Next steps" blocks from assistant output (or raw numbered lines).
572
+ *
573
+ * @param content — raw assistant message text or subagent output
574
+ * @param strict — when true, only the 💡 emoji heading is accepted (TUI rendering).
575
+ * when false, also accepts ## / plain "Next steps" headings (REPL store).
576
+ * @param requireHeading — when true, a heading must precede the item list.
577
+ * when false, numbered/bullet items are parsed from anywhere in text
578
+ * (used by /suggest subagent output which has no heading).
579
+ */
580
+ declare function parseNextSteps(content: string, strict?: boolean, requireHeading?: boolean): ParseNextStepsResult;
581
+
582
+ export { type ParseNextStepsResult, type ParsedNextStep, type RunTuiOptions, type Settings, parseInline, parseNextSteps, replaySessionEvents, runTui };
package/dist/index.js CHANGED
@@ -295,6 +295,9 @@ function StatusBar({
295
295
  "/",
296
296
  indexState.totalFiles
297
297
  ] })
298
+ ] }) : indexState?.circuit?.state === "open" ? /* @__PURE__ */ jsxs(Fragment, { children: [
299
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
300
+ /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2699 index paused (/reindex)" })
298
301
  ] }) : null
299
302
  ] })
300
303
  ) }),
@@ -3756,19 +3759,107 @@ function Banner({
3756
3759
  ] })
3757
3760
  ] });
3758
3761
  }
3759
- var NEXT_STEPS_RE = /💡\s*Next steps?\s*\n+((?:\d+\.\s+.+\n?)+)/i;
3760
- function parseNextSteps(content) {
3761
- const match = NEXT_STEPS_RE.exec(content);
3762
- if (!match?.[1]) return { steps: [], stripped: content };
3763
- const block = match[1];
3762
+
3763
+ // src/components/suggestions.ts
3764
+ var STRICT_HEADING_RE = /💡\s*Next steps?\s*\n+/i;
3765
+ var PERMISSIVE_HEADING_PATTERNS = [
3766
+ { re: /💡\s*Next steps?\s*\n+/i, label: "emoji" },
3767
+ { re: /##?\s*Next steps?\s*\n+/i, label: "markdown" },
3768
+ { re: /\n{1,2}Next steps?\s*\n+/i, label: "plain" }
3769
+ ];
3770
+ var ITEM_RE = /^(?:(\d+)[.)]\s*|[-*•]\s*)(.+)$/;
3771
+ var MAX_STEPS = 6;
3772
+ function parseNextSteps(content, strict = false, requireHeading = true) {
3773
+ if (requireHeading) {
3774
+ return parseWithHeading(content, strict);
3775
+ }
3776
+ return parseRawNumbered(content);
3777
+ }
3778
+ function parseRawNumbered(content) {
3779
+ const lines = content.split("\n");
3764
3780
  const steps = [];
3765
- const lines = block.split("\n").filter(Boolean);
3766
- for (const line of lines) {
3767
- const m = /^(\d+)\.\s+(.+)$/.exec(line.trim());
3768
- if (m) steps.push({ index: Number.parseInt(m[1], 10), text: m[2].trim() });
3781
+ const seenNumbers = /* @__PURE__ */ new Set();
3782
+ for (const rawLine of lines) {
3783
+ const line = rawLine.trim();
3784
+ if (!line) continue;
3785
+ const m = ITEM_RE.exec(line);
3786
+ if (!m) continue;
3787
+ const numPart = m[1];
3788
+ let text = m[2].trim();
3789
+ let index;
3790
+ if (numPart !== void 0) {
3791
+ index = Number.parseInt(numPart, 10);
3792
+ } else {
3793
+ index = steps.length + 1;
3794
+ }
3795
+ if (seenNumbers.has(index)) continue;
3796
+ if (text.length < 3) continue;
3797
+ seenNumbers.add(index);
3798
+ steps.push({ index, text });
3799
+ if (steps.length >= MAX_STEPS) break;
3769
3800
  }
3770
- const stripped = content.replace(NEXT_STEPS_RE, "").replace(/\n{3,}/g, "\n\n").trim();
3771
- return { steps: steps.slice(0, 6), stripped };
3801
+ return { steps, texts: steps.map((s2) => s2.text), stripped: content };
3802
+ }
3803
+ function parseWithHeading(content, strict) {
3804
+ const headingRe = strict ? STRICT_HEADING_RE : buildPermissiveHeadingRe();
3805
+ const headingMatch = headingRe.exec(content);
3806
+ if (!headingMatch) {
3807
+ return { steps: [], texts: [], stripped: content };
3808
+ }
3809
+ const headingEnd = headingMatch.index + headingMatch[0].length;
3810
+ const afterHeading = content.slice(headingEnd);
3811
+ const lines = afterHeading.split("\n");
3812
+ const steps = [];
3813
+ const seenNumbers = /* @__PURE__ */ new Set();
3814
+ for (const rawLine of lines) {
3815
+ const line = rawLine.trim();
3816
+ if (!line) continue;
3817
+ const m = ITEM_RE.exec(line);
3818
+ if (!m) break;
3819
+ const numPart = m[1];
3820
+ let text = m[2].trim();
3821
+ let index;
3822
+ if (numPart !== void 0) {
3823
+ index = Number.parseInt(numPart, 10);
3824
+ } else {
3825
+ index = steps.length + 1;
3826
+ }
3827
+ if (seenNumbers.has(index)) continue;
3828
+ if (text.length < 3) continue;
3829
+ seenNumbers.add(index);
3830
+ steps.push({ index, text });
3831
+ if (steps.length >= MAX_STEPS) break;
3832
+ }
3833
+ if (steps.length === 0) {
3834
+ return { steps: [], texts: [], stripped: content };
3835
+ }
3836
+ const texts = steps.map((s2) => s2.text);
3837
+ const blockStart = headingMatch.index;
3838
+ const blockEnd = headingEnd + findBlockEnd(afterHeading, steps.length);
3839
+ const stripped = (content.slice(0, blockStart) + content.slice(blockStart + blockEnd)).replace(/\n{3,}/g, "\n\n").trim();
3840
+ return { steps, texts, stripped };
3841
+ }
3842
+ function buildPermissiveHeadingRe() {
3843
+ const variants = PERMISSIVE_HEADING_PATTERNS.map(({ re }) => `(?:${re.source})`).join("|");
3844
+ return new RegExp(variants, "i");
3845
+ }
3846
+ function findBlockEnd(afterHeading, stepCount) {
3847
+ const lines = afterHeading.split("\n");
3848
+ let consumed = 0;
3849
+ let found = 0;
3850
+ for (const rawLine of lines) {
3851
+ const line = rawLine.trim();
3852
+ if (!line) {
3853
+ consumed += rawLine.length + 1;
3854
+ continue;
3855
+ }
3856
+ const m = ITEM_RE.exec(line);
3857
+ if (!m) break;
3858
+ consumed += rawLine.length + 1;
3859
+ found++;
3860
+ if (found >= stepCount) break;
3861
+ }
3862
+ return consumed;
3772
3863
  }
3773
3864
  function brainStatusStyle(status) {
3774
3865
  switch (status) {
@@ -3798,12 +3889,19 @@ function brainRiskColor(risk) {
3798
3889
  }
3799
3890
  var Entry = React5.memo(function Entry2({
3800
3891
  entry,
3801
- termWidth
3892
+ termWidth,
3893
+ setSuggestions
3802
3894
  }) {
3803
3895
  const nextSteps = useMemo(() => {
3804
3896
  if (entry.kind !== "assistant") return { steps: [], stripped: "" };
3805
- return parseNextSteps(entry.text);
3897
+ return parseNextSteps(entry.text, true);
3806
3898
  }, [entry.kind, entry.text]);
3899
+ useEffect(() => {
3900
+ if (!setSuggestions) return;
3901
+ const text = entry.text ?? "";
3902
+ const { texts } = parseNextSteps(text, true);
3903
+ if (texts.length > 0) setSuggestions(texts);
3904
+ }, [entry.kind, entry.text, setSuggestions]);
3807
3905
  switch (entry.kind) {
3808
3906
  case "user":
3809
3907
  return /* @__PURE__ */ jsx(
@@ -4044,7 +4142,7 @@ var Entry = React5.memo(function Entry2({
4044
4142
  }
4045
4143
  }
4046
4144
  });
4047
- function History({ entries, generation, streamingText, toolStream }) {
4145
+ function History({ entries, generation, streamingText, toolStream, setSuggestions }) {
4048
4146
  const { stdout } = useStdout();
4049
4147
  const [termSize, setTermSize] = useState({
4050
4148
  columns: stdout?.columns ?? 80,
@@ -4062,7 +4160,7 @@ function History({ entries, generation, streamingText, toolStream }) {
4062
4160
  const termWidth = termSize.columns;
4063
4161
  const tail = streamingText ? tailForDisplay(streamingText, MAX_STREAM_DISPLAY_CHARS) : "";
4064
4162
  return /* @__PURE__ */ jsxs(Fragment, { children: [
4065
- /* @__PURE__ */ jsx(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth }) }, entry.id) }, generation ?? 0),
4163
+ /* @__PURE__ */ jsx(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth, setSuggestions }) }, entry.id) }, generation ?? 0),
4066
4164
  /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: tail ? /* @__PURE__ */ jsx(AssistantTail, { text: tail, termWidth }) : null })
4067
4165
  ] });
4068
4166
  }
@@ -4111,7 +4209,8 @@ function ScrollableHistory({
4111
4209
  viewportRows,
4112
4210
  totalLines,
4113
4211
  onMeasure,
4114
- maxWidth
4212
+ maxWidth,
4213
+ setSuggestions
4115
4214
  }) {
4116
4215
  const { stdout } = useStdout();
4117
4216
  const rawWidth = stdout?.columns ?? 80;
@@ -4150,7 +4249,7 @@ function ScrollableHistory({
4150
4249
  flexShrink: 0,
4151
4250
  children: [
4152
4251
  hiddenCount > 0 ? /* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: ` \u2191 ${hiddenCount} earlier ${hiddenCount === 1 ? "entry" : "entries"} (scroll lives in this session; full log on disk)` }) }) : null,
4153
- shown.map((entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, flexShrink: 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth }) }, entry.id)),
4252
+ shown.map((entry) => /* @__PURE__ */ jsx(Box, { marginBottom: entry.kind === "turn-summary" ? 1 : 0, flexShrink: 0, children: /* @__PURE__ */ jsx(Entry, { entry, termWidth, setSuggestions }) }, entry.id)),
4154
4253
  tail ? /* @__PURE__ */ jsx(AssistantTail, { text: tail, termWidth }) : null,
4155
4254
  toolTail && toolStream ? /* @__PURE__ */ jsx(
4156
4255
  ToolStreamBox,
@@ -5396,7 +5495,7 @@ var MODE_DESC = {
5396
5495
  suggest: "Shows next-step suggestions after each turn",
5397
5496
  auto: "Self-driving \u2014 agent continues automatically"
5398
5497
  };
5399
- var SETTINGS_FIELD_COUNT = 24;
5498
+ var SETTINGS_FIELD_COUNT = 25;
5400
5499
  var CONFIG_SCOPES = ["global", "project"];
5401
5500
  function SettingsPicker({
5402
5501
  field,
@@ -7756,20 +7855,20 @@ function reducer(state, action) {
7756
7855
  const enext = (ebase + action.delta + ENHANCE_DELAY_PRESETS.length) % ENHANCE_DELAY_PRESETS.length;
7757
7856
  return { ...state, settingsPicker: { ...sp, enhanceDelayMs: expectDefined$1(ENHANCE_DELAY_PRESETS[enext]), hint: void 0 } };
7758
7857
  }
7759
- if (f === 20) return { ...state, settingsPicker: { ...sp, debugStream: !sp.debugStream, hint: void 0 } };
7760
- if (f === 21) {
7761
- const i = CONFIG_SCOPES.indexOf(sp.configScope);
7762
- const base = i < 0 ? 0 : i;
7763
- const next = (base + action.delta + CONFIG_SCOPES.length) % CONFIG_SCOPES.length;
7764
- return { ...state, settingsPicker: { ...sp, configScope: expectDefined$1(CONFIG_SCOPES[next]), hint: void 0 } };
7765
- }
7766
- if (f === 22) return { ...state, settingsPicker: { ...sp, enhanceEnabled: !sp.enhanceEnabled, hint: void 0 } };
7767
- if (f === 23) {
7858
+ if (f === 21) return { ...state, settingsPicker: { ...sp, enhanceEnabled: !sp.enhanceEnabled, hint: void 0 } };
7859
+ if (f === 22) {
7768
7860
  const i = ENHANCE_LANGUAGES.indexOf(sp.enhanceLanguage);
7769
7861
  const base = i < 0 ? 0 : i;
7770
7862
  const next = (base + action.delta + ENHANCE_LANGUAGES.length) % ENHANCE_LANGUAGES.length;
7771
7863
  return { ...state, settingsPicker: { ...sp, enhanceLanguage: expectDefined$1(ENHANCE_LANGUAGES[next]), hint: void 0 } };
7772
7864
  }
7865
+ if (f === 23) return { ...state, settingsPicker: { ...sp, debugStream: !sp.debugStream, hint: void 0 } };
7866
+ if (f === 24) {
7867
+ const i = CONFIG_SCOPES.indexOf(sp.configScope);
7868
+ const base = i < 0 ? 0 : i;
7869
+ const next = (base + action.delta + CONFIG_SCOPES.length) % CONFIG_SCOPES.length;
7870
+ return { ...state, settingsPicker: { ...sp, configScope: expectDefined$1(CONFIG_SCOPES[next]), hint: void 0 } };
7871
+ }
7773
7872
  return state;
7774
7873
  }
7775
7874
  case "settingsHint":
@@ -8610,6 +8709,7 @@ function App({
8610
8709
  predictNext,
8611
8710
  onSuggestionsParsed,
8612
8711
  getSuggestions,
8712
+ setSuggestions,
8613
8713
  switchAutonomy,
8614
8714
  effectiveMaxContext,
8615
8715
  onExit,
@@ -10639,14 +10739,14 @@ function App({
10639
10739
  return;
10640
10740
  }
10641
10741
  if (item.kind === "project") {
10642
- onProjectSelect?.(item.key, item.kind);
10742
+ await onProjectSelect?.(item.key, item.kind);
10643
10743
  dispatch({ type: "projectPickerClose" });
10644
10744
  requestExit?.(42);
10645
10745
  return;
10646
10746
  }
10647
10747
  dispatch({ type: "projectPickerClose" });
10648
10748
  if (item.key === "new-session") {
10649
- onProjectSelect?.(item.key, item.kind);
10749
+ await onProjectSelect?.(item.key, item.kind);
10650
10750
  requestExit?.(42);
10651
10751
  } else if (item.key === "prev-sessions") {
10652
10752
  void submit("/resume");
@@ -11887,7 +11987,8 @@ User message:
11887
11987
  scrollOffset: state.scrollOffset,
11888
11988
  viewportRows: state.viewportRows,
11889
11989
  totalLines: state.totalLines,
11890
- onMeasure: (totalLines) => dispatch({ type: "setMeasuredLines", totalLines })
11990
+ onMeasure: (totalLines) => dispatch({ type: "setMeasuredLines", totalLines }),
11991
+ setSuggestions
11891
11992
  }
11892
11993
  ) : /* @__PURE__ */ jsx(
11893
11994
  History,
@@ -11895,7 +11996,8 @@ User message:
11895
11996
  entries: state.entries,
11896
11997
  generation: state.historyGen,
11897
11998
  streamingText: state.streamingText,
11898
- toolStream: state.toolStream
11999
+ toolStream: state.toolStream,
12000
+ setSuggestions
11899
12001
  }
11900
12002
  ),
11901
12003
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: bottomRegionRef, children: [
@@ -12751,6 +12853,6 @@ function eventToEntry(ev, pendingTools, completedTools) {
12751
12853
  }
12752
12854
  }
12753
12855
 
12754
- export { parseInline, replaySessionEvents, runTui };
12856
+ export { parseInline, parseNextSteps, replaySessionEvents, runTui };
12755
12857
  //# sourceMappingURL=index.js.map
12756
12858
  //# sourceMappingURL=index.js.map