@wrongstack/tui 0.236.0 → 0.255.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
@@ -338,8 +338,8 @@ interface RunTuiOptions {
338
338
  * visible bar without a round-trip. The initial value is loaded from
339
339
  * the config file before App mounts.
340
340
  */
341
- statuslineHiddenItems: Array<'todos' | 'plan' | 'fleet' | 'git' | 'elapsed' | 'context' | 'cost'>;
342
- setStatuslineHiddenItems: (items: Array<'todos' | 'plan' | 'fleet' | 'git' | 'elapsed' | 'context' | 'cost'>) => void;
341
+ statuslineHiddenItems: Array<'todos' | 'plan' | 'tasks' | 'fleet' | 'git' | 'elapsed' | 'context' | 'cost'>;
342
+ setStatuslineHiddenItems: (items: Array<'todos' | 'plan' | 'tasks' | 'fleet' | 'git' | 'elapsed' | 'context' | 'cost'>) => void;
343
343
  /**
344
344
  * Controller for the agents monitor overlay. App installs a dispatch-backed
345
345
  * setter on mount so the `/agents on|off` slash command can toggle the
@@ -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
@@ -1,6 +1,7 @@
1
- import { writeErr, resolveWstackPaths, loadGoal, InputBuilder, DefaultSessionRewinder, writeOut, formatTodosList, buildGoalPreamble, expectDefined as expectDefined$1, wstackGlobalRoot, shouldEnhance, enhanceUserPrompt, recentTextTurns, normalizedEqual, buildChildEnv } from '@wrongstack/core';
1
+ import { writeErr, resolveProjectDir, wstackGlobalRoot, GlobalMailbox, resolveWstackPaths, loadGoal, InputBuilder, DefaultSessionRewinder, writeOut, formatTodosList, buildGoalPreamble, expectDefined as expectDefined$1, shouldEnhance, enhanceUserPrompt, recentTextTurns, normalizedEqual, buildChildEnv } from '@wrongstack/core';
2
2
  export { buildGoalPreamble } from '@wrongstack/core';
3
3
  import { Box as Box$1, useInput, useStdin, useStdout, Text as Text$1, render, useApp, measureElement, Static } from 'ink';
4
+ import { randomUUID } from 'crypto';
4
5
  import * as path4 from 'path';
5
6
  import React5, { forwardRef, useState, useEffect, useMemo, memo, useRef, useCallback, useReducer, useLayoutEffect } from 'react';
6
7
  import * as fs2 from 'fs/promises';
@@ -86,6 +87,27 @@ var Box = forwardRef(function Box2({ borderColor, backgroundColor, ...rest }, re
86
87
  }
87
88
  );
88
89
  });
90
+ function useTokenCounterRefresh(tokenCounter, events) {
91
+ const [data, setData] = useState(
92
+ () => tokenCounter ? {
93
+ usage: tokenCounter.total(),
94
+ cost: tokenCounter.estimateCost(),
95
+ cacheStats: tokenCounter.cacheStats()
96
+ } : void 0
97
+ );
98
+ useEffect(() => {
99
+ if (!tokenCounter || !events) return;
100
+ const off = events.on("token.accounted", () => {
101
+ setData({
102
+ usage: tokenCounter.total(),
103
+ cost: tokenCounter.estimateCost(),
104
+ cacheStats: tokenCounter.cacheStats()
105
+ });
106
+ });
107
+ return off;
108
+ }, [tokenCounter, events]);
109
+ return data;
110
+ }
89
111
  var MODE_ICONS = {
90
112
  teach: "\u{1F9D1}\u200D\u{1F3EB}",
91
113
  brief: "\u26A1",
@@ -131,6 +153,7 @@ function StatusBar({
131
153
  processCount,
132
154
  context,
133
155
  hiddenItems,
156
+ events,
134
157
  eternalStage,
135
158
  goalSummary,
136
159
  indexState,
@@ -155,9 +178,10 @@ function StatusBar({
155
178
  const isCompact = termWidth < COMPACT_THRESHOLD;
156
179
  const isComfortable = termWidth >= COMFORTABLE_THRESHOLD;
157
180
  const hiddenSet = new Set(hiddenItems);
158
- const usage = tokenCounter?.total();
159
- const cost = tokenCounter?.estimateCost();
160
- const cache2 = tokenCounter?.cacheStats();
181
+ const tokenData = useTokenCounterRefresh(tokenCounter, events);
182
+ const usage = tokenData?.usage;
183
+ const cost = tokenData?.cost;
184
+ const cache2 = tokenData?.cacheStats;
161
185
  const [elapsedMs, setElapsedMs] = useState(startedAt ? Date.now() - startedAt : 0);
162
186
  useEffect(() => {
163
187
  if (startedAt == null) return;
@@ -295,6 +319,9 @@ function StatusBar({
295
319
  "/",
296
320
  indexState.totalFiles
297
321
  ] })
322
+ ] }) : indexState?.circuit?.state === "open" ? /* @__PURE__ */ jsxs(Fragment, { children: [
323
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
324
+ /* @__PURE__ */ jsx(Text, { color: "red", children: "\u2699 index paused (/reindex)" })
298
325
  ] }) : null
299
326
  ] })
300
327
  ) }),
@@ -552,7 +579,22 @@ function StatusBar({
552
579
  /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
553
580
  "\u{1F465} ",
554
581
  mailbox.onlineAgents,
555
- " online"
582
+ " agent",
583
+ mailbox.onlineAgents === 1 ? "" : "s",
584
+ mailbox.onlineClients.tui + mailbox.onlineClients.webui + mailbox.onlineClients.repl > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
585
+ mailbox.onlineClients.tui > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
586
+ " \xB7 \u{1F5A5} TUI",
587
+ mailbox.onlineClients.tui > 1 ? `\xD7${mailbox.onlineClients.tui}` : ""
588
+ ] }) : null,
589
+ mailbox.onlineClients.webui > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
590
+ " \xB7 \u{1F310} WebUI",
591
+ mailbox.onlineClients.webui > 1 ? `\xD7${mailbox.onlineClients.webui}` : ""
592
+ ] }) : null,
593
+ mailbox.onlineClients.repl > 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
594
+ " \xB7 \u2328 REPL",
595
+ mailbox.onlineClients.repl > 1 ? `\xD7${mailbox.onlineClients.repl}` : ""
596
+ ] }) : null
597
+ ] }) : null
556
598
  ] }),
557
599
  mailbox.lastSubject ? /* @__PURE__ */ jsxs(Fragment, { children: [
558
600
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }),
@@ -3756,19 +3798,107 @@ function Banner({
3756
3798
  ] })
3757
3799
  ] });
3758
3800
  }
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];
3801
+
3802
+ // src/components/suggestions.ts
3803
+ var STRICT_HEADING_RE = /💡\s*Next steps?\s*\n+/i;
3804
+ var PERMISSIVE_HEADING_PATTERNS = [
3805
+ { re: /💡\s*Next steps?\s*\n+/i, label: "emoji" },
3806
+ { re: /##?\s*Next steps?\s*\n+/i, label: "markdown" },
3807
+ { re: /\n{1,2}Next steps?\s*\n+/i, label: "plain" }
3808
+ ];
3809
+ var ITEM_RE = /^(?:(\d+)[.)]\s*|[-*•]\s*)(.+)$/;
3810
+ var MAX_STEPS = 6;
3811
+ function parseNextSteps(content, strict = false, requireHeading = true) {
3812
+ if (requireHeading) {
3813
+ return parseWithHeading(content, strict);
3814
+ }
3815
+ return parseRawNumbered(content);
3816
+ }
3817
+ function parseRawNumbered(content) {
3818
+ const lines = content.split("\n");
3764
3819
  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() });
3820
+ const seenNumbers = /* @__PURE__ */ new Set();
3821
+ for (const rawLine of lines) {
3822
+ const line = rawLine.trim();
3823
+ if (!line) continue;
3824
+ const m = ITEM_RE.exec(line);
3825
+ if (!m) continue;
3826
+ const numPart = m[1];
3827
+ let text = m[2].trim();
3828
+ let index;
3829
+ if (numPart !== void 0) {
3830
+ index = Number.parseInt(numPart, 10);
3831
+ } else {
3832
+ index = steps.length + 1;
3833
+ }
3834
+ if (seenNumbers.has(index)) continue;
3835
+ if (text.length < 3) continue;
3836
+ seenNumbers.add(index);
3837
+ steps.push({ index, text });
3838
+ if (steps.length >= MAX_STEPS) break;
3839
+ }
3840
+ return { steps, texts: steps.map((s2) => s2.text), stripped: content };
3841
+ }
3842
+ function parseWithHeading(content, strict) {
3843
+ const headingRe = strict ? STRICT_HEADING_RE : buildPermissiveHeadingRe();
3844
+ const headingMatch = headingRe.exec(content);
3845
+ if (!headingMatch) {
3846
+ return { steps: [], texts: [], stripped: content };
3847
+ }
3848
+ const headingEnd = headingMatch.index + headingMatch[0].length;
3849
+ const afterHeading = content.slice(headingEnd);
3850
+ const lines = afterHeading.split("\n");
3851
+ const steps = [];
3852
+ const seenNumbers = /* @__PURE__ */ new Set();
3853
+ for (const rawLine of lines) {
3854
+ const line = rawLine.trim();
3855
+ if (!line) continue;
3856
+ const m = ITEM_RE.exec(line);
3857
+ if (!m) break;
3858
+ const numPart = m[1];
3859
+ let text = m[2].trim();
3860
+ let index;
3861
+ if (numPart !== void 0) {
3862
+ index = Number.parseInt(numPart, 10);
3863
+ } else {
3864
+ index = steps.length + 1;
3865
+ }
3866
+ if (seenNumbers.has(index)) continue;
3867
+ if (text.length < 3) continue;
3868
+ seenNumbers.add(index);
3869
+ steps.push({ index, text });
3870
+ if (steps.length >= MAX_STEPS) break;
3871
+ }
3872
+ if (steps.length === 0) {
3873
+ return { steps: [], texts: [], stripped: content };
3874
+ }
3875
+ const texts = steps.map((s2) => s2.text);
3876
+ const blockStart = headingMatch.index;
3877
+ const blockEnd = headingEnd + findBlockEnd(afterHeading, steps.length);
3878
+ const stripped = (content.slice(0, blockStart) + content.slice(blockStart + blockEnd)).replace(/\n{3,}/g, "\n\n").trim();
3879
+ return { steps, texts, stripped };
3880
+ }
3881
+ function buildPermissiveHeadingRe() {
3882
+ const variants = PERMISSIVE_HEADING_PATTERNS.map(({ re }) => `(?:${re.source})`).join("|");
3883
+ return new RegExp(variants, "i");
3884
+ }
3885
+ function findBlockEnd(afterHeading, stepCount) {
3886
+ const lines = afterHeading.split("\n");
3887
+ let consumed = 0;
3888
+ let found = 0;
3889
+ for (const rawLine of lines) {
3890
+ const line = rawLine.trim();
3891
+ if (!line) {
3892
+ consumed += rawLine.length + 1;
3893
+ continue;
3894
+ }
3895
+ const m = ITEM_RE.exec(line);
3896
+ if (!m) break;
3897
+ consumed += rawLine.length + 1;
3898
+ found++;
3899
+ if (found >= stepCount) break;
3769
3900
  }
3770
- const stripped = content.replace(NEXT_STEPS_RE, "").replace(/\n{3,}/g, "\n\n").trim();
3771
- return { steps: steps.slice(0, 6), stripped };
3901
+ return consumed;
3772
3902
  }
3773
3903
  function brainStatusStyle(status) {
3774
3904
  switch (status) {
@@ -3798,12 +3928,19 @@ function brainRiskColor(risk) {
3798
3928
  }
3799
3929
  var Entry = React5.memo(function Entry2({
3800
3930
  entry,
3801
- termWidth
3931
+ termWidth,
3932
+ setSuggestions
3802
3933
  }) {
3803
3934
  const nextSteps = useMemo(() => {
3804
3935
  if (entry.kind !== "assistant") return { steps: [], stripped: "" };
3805
- return parseNextSteps(entry.text);
3936
+ return parseNextSteps(entry.text, true);
3806
3937
  }, [entry.kind, entry.text]);
3938
+ useEffect(() => {
3939
+ if (!setSuggestions) return;
3940
+ const text = entry.text ?? "";
3941
+ const { texts } = parseNextSteps(text, true);
3942
+ if (texts.length > 0) setSuggestions(texts);
3943
+ }, [entry.kind, entry.text, setSuggestions]);
3807
3944
  switch (entry.kind) {
3808
3945
  case "user":
3809
3946
  return /* @__PURE__ */ jsx(
@@ -4044,7 +4181,7 @@ var Entry = React5.memo(function Entry2({
4044
4181
  }
4045
4182
  }
4046
4183
  });
4047
- function History({ entries, generation, streamingText, toolStream }) {
4184
+ function History({ entries, generation, streamingText, toolStream, setSuggestions }) {
4048
4185
  const { stdout } = useStdout();
4049
4186
  const [termSize, setTermSize] = useState({
4050
4187
  columns: stdout?.columns ?? 80,
@@ -4062,7 +4199,7 @@ function History({ entries, generation, streamingText, toolStream }) {
4062
4199
  const termWidth = termSize.columns;
4063
4200
  const tail = streamingText ? tailForDisplay(streamingText, MAX_STREAM_DISPLAY_CHARS) : "";
4064
4201
  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),
4202
+ /* @__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
4203
  /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: tail ? /* @__PURE__ */ jsx(AssistantTail, { text: tail, termWidth }) : null })
4067
4204
  ] });
4068
4205
  }
@@ -4111,7 +4248,8 @@ function ScrollableHistory({
4111
4248
  viewportRows,
4112
4249
  totalLines,
4113
4250
  onMeasure,
4114
- maxWidth
4251
+ maxWidth,
4252
+ setSuggestions
4115
4253
  }) {
4116
4254
  const { stdout } = useStdout();
4117
4255
  const rawWidth = stdout?.columns ?? 80;
@@ -4150,7 +4288,7 @@ function ScrollableHistory({
4150
4288
  flexShrink: 0,
4151
4289
  children: [
4152
4290
  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)),
4291
+ 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
4292
  tail ? /* @__PURE__ */ jsx(AssistantTail, { text: tail, termWidth }) : null,
4155
4293
  toolTail && toolStream ? /* @__PURE__ */ jsx(
4156
4294
  ToolStreamBox,
@@ -4209,6 +4347,15 @@ function startHeapWatchdog(opts = {}) {
4209
4347
  }).catch(() => void 0);
4210
4348
  };
4211
4349
  const tick = () => {
4350
+ let userTimings = 0;
4351
+ try {
4352
+ userTimings = performance.getEntriesByType("mark").length + performance.getEntriesByType("measure").length;
4353
+ if (userTimings > 0) {
4354
+ performance.clearMarks();
4355
+ performance.clearMeasures();
4356
+ }
4357
+ } catch {
4358
+ }
4212
4359
  const s2 = takeHeapSample();
4213
4360
  if (s2.load >= criticalAt && criticalArmed) {
4214
4361
  criticalArmed = false;
@@ -4237,7 +4384,7 @@ function startHeapWatchdog(opts = {}) {
4237
4384
  extras = opts.collectStats?.() ?? {};
4238
4385
  } catch {
4239
4386
  }
4240
- append(JSON.stringify({ pid: process.pid, ...s2, ...extras }));
4387
+ append(JSON.stringify({ pid: process.pid, ...s2, userTimings, ...extras }));
4241
4388
  }
4242
4389
  };
4243
4390
  const timer = setInterval(tick, sampleEveryMs);
@@ -5396,7 +5543,7 @@ var MODE_DESC = {
5396
5543
  suggest: "Shows next-step suggestions after each turn",
5397
5544
  auto: "Self-driving \u2014 agent continues automatically"
5398
5545
  };
5399
- var SETTINGS_FIELD_COUNT = 24;
5546
+ var SETTINGS_FIELD_COUNT = 25;
5400
5547
  var CONFIG_SCOPES = ["global", "project"];
5401
5548
  function SettingsPicker({
5402
5549
  field,
@@ -6832,6 +6979,7 @@ function useSessionEvents(events, dispatch, onClearHistory) {
6832
6979
  const offRewound = events.on("session.rewound", () => {
6833
6980
  dispatch({ type: "sessionRewound", toPromptIndex: 0 });
6834
6981
  dispatch({ type: "clearHistory" });
6982
+ dispatch({ type: "resetContextChip" });
6835
6983
  onClearHistory?.(dispatch);
6836
6984
  });
6837
6985
  return () => {
@@ -7756,20 +7904,20 @@ function reducer(state, action) {
7756
7904
  const enext = (ebase + action.delta + ENHANCE_DELAY_PRESETS.length) % ENHANCE_DELAY_PRESETS.length;
7757
7905
  return { ...state, settingsPicker: { ...sp, enhanceDelayMs: expectDefined$1(ENHANCE_DELAY_PRESETS[enext]), hint: void 0 } };
7758
7906
  }
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) {
7907
+ if (f === 21) return { ...state, settingsPicker: { ...sp, enhanceEnabled: !sp.enhanceEnabled, hint: void 0 } };
7908
+ if (f === 22) {
7768
7909
  const i = ENHANCE_LANGUAGES.indexOf(sp.enhanceLanguage);
7769
7910
  const base = i < 0 ? 0 : i;
7770
7911
  const next = (base + action.delta + ENHANCE_LANGUAGES.length) % ENHANCE_LANGUAGES.length;
7771
7912
  return { ...state, settingsPicker: { ...sp, enhanceLanguage: expectDefined$1(ENHANCE_LANGUAGES[next]), hint: void 0 } };
7772
7913
  }
7914
+ if (f === 23) return { ...state, settingsPicker: { ...sp, debugStream: !sp.debugStream, hint: void 0 } };
7915
+ if (f === 24) {
7916
+ const i = CONFIG_SCOPES.indexOf(sp.configScope);
7917
+ const base = i < 0 ? 0 : i;
7918
+ const next = (base + action.delta + CONFIG_SCOPES.length) % CONFIG_SCOPES.length;
7919
+ return { ...state, settingsPicker: { ...sp, configScope: expectDefined$1(CONFIG_SCOPES[next]), hint: void 0 } };
7920
+ }
7773
7921
  return state;
7774
7922
  }
7775
7923
  case "settingsHint":
@@ -8610,6 +8758,7 @@ function App({
8610
8758
  predictNext,
8611
8759
  onSuggestionsParsed,
8612
8760
  getSuggestions,
8761
+ setSuggestions,
8613
8762
  switchAutonomy,
8614
8763
  effectiveMaxContext,
8615
8764
  onExit,
@@ -9048,7 +9197,8 @@ function App({
9048
9197
  }, [getLiveSessions]);
9049
9198
  const [mailboxStatus, setMailboxStatus] = useState({
9050
9199
  unread: 0,
9051
- onlineAgents: 0
9200
+ onlineAgents: 0,
9201
+ onlineClients: { tui: 0, webui: 0, repl: 0 }
9052
9202
  });
9053
9203
  useEffect(() => {
9054
9204
  const seenAgents = /* @__PURE__ */ new Set();
@@ -9074,11 +9224,25 @@ function App({
9074
9224
  if (p?.agentId) seenAgents.add(p.agentId);
9075
9225
  setMailboxStatus((prev) => ({ ...prev, onlineAgents: seenAgents.size }));
9076
9226
  });
9227
+ const unsub5 = events.onPattern("mailbox.sync_clients", (_e, payload) => {
9228
+ const p = payload;
9229
+ if (p) {
9230
+ setMailboxStatus((prev) => ({
9231
+ ...prev,
9232
+ onlineClients: {
9233
+ tui: p.tui ?? 0,
9234
+ webui: p.webui ?? 0,
9235
+ repl: p.repl ?? 0
9236
+ }
9237
+ }));
9238
+ }
9239
+ });
9077
9240
  return () => {
9078
9241
  unsub1();
9079
9242
  unsub2();
9080
9243
  unsub3();
9081
9244
  unsub4();
9245
+ unsub5();
9082
9246
  };
9083
9247
  }, [events]);
9084
9248
  const [mailboxPanelOpen, setMailboxPanelOpen] = useState(false);
@@ -10639,14 +10803,14 @@ function App({
10639
10803
  return;
10640
10804
  }
10641
10805
  if (item.kind === "project") {
10642
- onProjectSelect?.(item.key, item.kind);
10806
+ await onProjectSelect?.(item.key, item.kind);
10643
10807
  dispatch({ type: "projectPickerClose" });
10644
10808
  requestExit?.(42);
10645
10809
  return;
10646
10810
  }
10647
10811
  dispatch({ type: "projectPickerClose" });
10648
10812
  if (item.key === "new-session") {
10649
- onProjectSelect?.(item.key, item.kind);
10813
+ await onProjectSelect?.(item.key, item.kind);
10650
10814
  requestExit?.(42);
10651
10815
  } else if (item.key === "prev-sessions") {
10652
10816
  void submit("/resume");
@@ -11887,7 +12051,8 @@ User message:
11887
12051
  scrollOffset: state.scrollOffset,
11888
12052
  viewportRows: state.viewportRows,
11889
12053
  totalLines: state.totalLines,
11890
- onMeasure: (totalLines) => dispatch({ type: "setMeasuredLines", totalLines })
12054
+ onMeasure: (totalLines) => dispatch({ type: "setMeasuredLines", totalLines }),
12055
+ setSuggestions
11891
12056
  }
11892
12057
  ) : /* @__PURE__ */ jsx(
11893
12058
  History,
@@ -11895,7 +12060,8 @@ User message:
11895
12060
  entries: state.entries,
11896
12061
  generation: state.historyGen,
11897
12062
  streamingText: state.streamingText,
11898
- toolStream: state.toolStream
12063
+ toolStream: state.toolStream,
12064
+ setSuggestions
11899
12065
  }
11900
12066
  ),
11901
12067
  /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexShrink: 0, ref: bottomRegionRef, children: [
@@ -12146,6 +12312,7 @@ User message:
12146
12312
  subagentCount: Object.keys(state.fleet).length,
12147
12313
  processCount: getProcessRegistry().activeCount,
12148
12314
  hiddenItems,
12315
+ events,
12149
12316
  eternalStage: state.eternalStage,
12150
12317
  goalSummary: state.goalSummary,
12151
12318
  indexState,
@@ -12421,6 +12588,7 @@ async function runTui(opts) {
12421
12588
  const cleanup = () => {
12422
12589
  if (cleaned) return;
12423
12590
  cleaned = true;
12591
+ unregisterTuiClient();
12424
12592
  unsilenceTerminal();
12425
12593
  try {
12426
12594
  stopTitle();
@@ -12447,6 +12615,64 @@ async function runTui(opts) {
12447
12615
  }
12448
12616
  process.off("exit", exitHandler);
12449
12617
  };
12618
+ let clientHeartbeatTimer = null;
12619
+ let clientSyncTimer = null;
12620
+ const CLIENT_HEARTBEAT_MS = 15e3;
12621
+ const CLIENT_SYNC_MS = 3e4;
12622
+ const registerTuiClient = async () => {
12623
+ if (!opts.projectRoot) return null;
12624
+ try {
12625
+ const projectDir = resolveProjectDir(opts.projectRoot, wstackGlobalRoot());
12626
+ const mailbox = new GlobalMailbox(projectDir, opts.events);
12627
+ const clientId = `tui@${randomUUID().slice(0, 8)}`;
12628
+ await mailbox.registerClient({
12629
+ clientId,
12630
+ sessionId: opts.projectRoot,
12631
+ name: `TUI [${path4.basename(opts.projectRoot)}]`,
12632
+ source: "tui",
12633
+ pid: process.pid
12634
+ });
12635
+ clientHeartbeatTimer = setInterval(() => {
12636
+ mailbox.clientHeartbeat({ clientId }).catch(() => {
12637
+ });
12638
+ }, CLIENT_HEARTBEAT_MS);
12639
+ clientHeartbeatTimer.unref();
12640
+ const syncClients = async () => {
12641
+ try {
12642
+ const statuses = await mailbox.getClientStatuses();
12643
+ const counts = { tui: 0, webui: 0, repl: 0 };
12644
+ for (const s2 of statuses) {
12645
+ if (s2.online && s2.source in counts) {
12646
+ counts[s2.source]++;
12647
+ }
12648
+ }
12649
+ opts.events.emitCustom("mailbox.sync_clients", counts);
12650
+ } catch {
12651
+ }
12652
+ };
12653
+ setTimeout(() => {
12654
+ void syncClients();
12655
+ }, 5e3);
12656
+ clientSyncTimer = setInterval(() => {
12657
+ void syncClients();
12658
+ }, CLIENT_SYNC_MS);
12659
+ clientSyncTimer.unref();
12660
+ return clientId;
12661
+ } catch {
12662
+ return null;
12663
+ }
12664
+ };
12665
+ const unregisterTuiClient = () => {
12666
+ if (clientHeartbeatTimer) {
12667
+ clearInterval(clientHeartbeatTimer);
12668
+ clientHeartbeatTimer = null;
12669
+ }
12670
+ if (clientSyncTimer) {
12671
+ clearInterval(clientSyncTimer);
12672
+ clientSyncTimer = null;
12673
+ }
12674
+ };
12675
+ registerTuiClient();
12450
12676
  return new Promise((resolve) => {
12451
12677
  let exitCode = 0;
12452
12678
  let hardExitTimer = null;
@@ -12751,6 +12977,6 @@ function eventToEntry(ev, pendingTools, completedTools) {
12751
12977
  }
12752
12978
  }
12753
12979
 
12754
- export { parseInline, replaySessionEvents, runTui };
12980
+ export { parseInline, parseNextSteps, replaySessionEvents, runTui };
12755
12981
  //# sourceMappingURL=index.js.map
12756
12982
  //# sourceMappingURL=index.js.map