@wrongstack/tui 0.309.0 → 0.310.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.
@@ -14,10 +14,19 @@ export interface ContextPanelData {
14
14
  * Surfaced in the panel so `/context` answers "how much of my spend
15
15
  * is hitting the cache" without asking the user to chase the CLI
16
16
  * `/context cache` subcommand.
17
+ *
18
+ * `savedUsd` is the gross read-discount USD figure computed by the
19
+ * counter (cache-read tokens × (input price − cache-read price)).
20
+ * `cacheWrite5m` / `cacheWrite1h` are present when the upstream exposed
21
+ * an Anthropic-style TTL split (Anthropic family, including MiniMax
22
+ * routed through the Anthropic Messages surface); OpenAI-family
23
+ * gateways emit only the aggregate and leave them undefined.
17
24
  */
18
25
  cacheStats?: {
19
26
  readTokens: number;
20
27
  writeTokens: number;
28
+ cacheWrite5m?: number | undefined;
29
+ cacheWrite1h?: number | undefined;
21
30
  hitRatio: number;
22
31
  savedUsd: number;
23
32
  } | undefined;
@@ -27,6 +36,16 @@ export interface ContextPanelData {
27
36
  * exact extent of the cache, not just a percentage.
28
37
  */
29
38
  cacheCoverageTokens?: number | undefined;
39
+ /** Session cache telemetry split by provider, including fallback/model switches. */
40
+ providerCacheStats?: Array<{
41
+ provider: string;
42
+ input: number;
43
+ cacheRead: number;
44
+ cacheWrite: number;
45
+ cacheWrite5m?: number | undefined;
46
+ cacheWrite1h?: number | undefined;
47
+ hitRatio: number;
48
+ }> | undefined;
30
49
  /**
31
50
  * Real, measured per-category token accounting for the live request. When
32
51
  * present the Composition tab shows honest numbers; when absent (no request
@@ -1,3 +1,4 @@
1
+ import { type Theme } from '../theme.js';
1
2
  /**
2
3
  * Subscribes to the module-scoped active theme so a render reads the
3
4
  * current `theme` snapshot. Used by the App shell to force a re-render
@@ -10,5 +11,5 @@
10
11
  * but exposing it lets test code and stat panels introspect the active
11
12
  * palette without re-implementing the listener.
12
13
  */
13
- export declare function useActiveTheme(): import("../theme-types.js").Theme;
14
+ export declare function useActiveTheme(): Theme;
14
15
  //# sourceMappingURL=use-active-theme.d.ts.map
@@ -1,101 +1,104 @@
1
1
  import type { AppProps } from '../app-props.js';
2
- import type { State } from '../app-reducer.js';
3
- import type { AppExecutionPipelineArgs } from './use-app-execution-pipeline.js';
2
+ import type { AppExecutionPipelineArgs, KeyHandlerParams, RunBlocksParams, SubmitParams } from './use-app-execution-pipeline.js';
3
+ type K = KeyHandlerParams;
4
+ type R = RunBlocksParams;
5
+ type S = SubmitParams;
4
6
  export declare function buildAppPipelineArgs(params: {
5
7
  props: AppProps;
6
- state: State;
7
- dispatch: any;
8
- historyScrollRef: any;
9
- runInterruptLadder: any;
10
- enhanceCancelledRef: any;
11
- enhanceAbortRef: any;
12
- enhanceOriginalRef: any;
13
- enhanceEnabledRef: any;
14
- inputGateRef: any;
15
- lastEscAtRef: any;
16
- dismissedEscAtRef: any;
17
- streamingTextRef: any;
18
- streamSegmentsRef: any;
19
- pendingDeltaRef: any;
20
- flushTimerRef: any;
21
- sessionGenerationRef: any;
22
- activeRunGenerationRef: any;
23
- activeRunSettledRef: any;
24
- assistantCommittedThisRunRef: any;
25
- confirmExitRef: any;
26
- chimeRef: any;
27
- activeCtrlRef: any;
28
- clearPendingConfirms: any;
29
- liveDirector: any;
30
- openProjectPicker: any;
31
- loadLiveSessions: any;
32
- openStatuslinePicker: any;
33
- statuslineHiddenItems: any;
34
- lastEnterAtRef: any;
35
- draftRef: any;
36
- setDraft: any;
37
- clearDraft: any;
38
- mouseMode: any;
39
- nativeMouse: any;
40
- termRows: any;
41
- terminalColumns: any;
42
- terminalRows: any;
43
- mainColumnWidth: any;
44
- overlayOpen: any;
45
- effectiveSwarmOnSidebar: any;
46
- sidebarTwinRowCount: any;
47
- statusBarWrapRef: any;
48
- belowStatusBarRef: any;
49
- statusBarClickMapRef: any;
50
- openModelPicker: any;
51
- nextStepsAutoSubmitTimerRef: any;
52
- nextStepsAutoSubmitSuggestionRef: any;
53
- nextStepsAutoSubmitLabel: any;
54
- setNextStepsAutoSubmitCountdown: any;
55
- setNextStepsAutoSubmitLabel: any;
56
- cancelNextStepsCountdown: any;
57
- pasteClipboardText: any;
58
- pasteClipboardImage: any;
59
- onHistoryCopy: any;
60
- tryPickerKey: any;
61
- pasteAccumRef: any;
62
- pasteFlushTimerRef: any;
63
- commitPaste: any;
64
- builderRef: any;
65
- tokenPreviewsRef: any;
66
- interruptsSyncRef: any;
67
- stateRef: any;
68
- runBlocksRef: any;
69
- submitRef: any;
70
- liveModel: any;
71
- liveProvider: any;
72
- activeMaxContext: any;
73
- yoloLive: any;
74
- autonomyLive: any;
75
- liveModeLabel: any;
76
- setMouseMode: any;
77
- setNativeMouse: any;
78
- setLiveModel: any;
79
- setLiveProvider: any;
80
- setActiveMaxContext: any;
81
- setYoloLive: any;
82
- setAutonomyLive: any;
83
- setLiveModeLabel: any;
84
- setLiveToolCount: any;
85
- autoSubmitStreakRef: any;
86
- autoSubmitCapWarnedRef: any;
87
- autoSubmitLoopGuardRef: any;
88
- runEternalLoopRef: any;
89
- runParallelLoopRef: any;
90
- midRunSendPickerRef: any;
91
- openPromptPicker: any;
92
- refreshGoalSummary: any;
93
- exit: any;
94
- setMemoryContextMonitor: any;
95
- runSteerSequence: any;
96
- setEnhanceStartedAt: any;
97
- setEnhanceDurationMs: any;
98
- setRefineProviderId: any;
99
- setRefineModel: any;
8
+ state: K['state'];
9
+ dispatch: K['dispatch'];
10
+ historyScrollRef: K['historyScrollRef'];
11
+ runInterruptLadder: K['runInterruptLadder'];
12
+ enhanceCancelledRef: K['enhanceCancelledRef'];
13
+ enhanceAbortRef: K['enhanceAbortRef'];
14
+ enhanceOriginalRef: S['refs']['enhanceOriginal'];
15
+ enhanceEnabledRef: S['refs']['enhanceEnabled'];
16
+ inputGateRef: K['inputGateRef'];
17
+ lastEscAtRef: K['lastEscAtRef'];
18
+ dismissedEscAtRef: K['dismissedEscAtRef'];
19
+ streamingTextRef: K['streamingTextRef'];
20
+ streamSegmentsRef: R['refs']['streamSegments'];
21
+ pendingDeltaRef: R['refs']['pendingDelta'];
22
+ flushTimerRef: R['refs']['flushTimer'];
23
+ sessionGenerationRef: R['refs']['sessionGeneration'];
24
+ activeRunGenerationRef: R['refs']['activeRunGeneration'];
25
+ activeRunSettledRef: R['refs']['activeRunSettled'];
26
+ assistantCommittedThisRunRef: R['refs']['assistantCommitted'];
27
+ confirmExitRef: K['confirmExitRef'];
28
+ chimeRef: R['refs']['chime'];
29
+ activeCtrlRef: K['activeCtrlRef'];
30
+ clearPendingConfirms: K['clearPendingConfirms'];
31
+ liveDirector: K['liveDirector'];
32
+ openProjectPicker: K['openProjectPicker'];
33
+ loadLiveSessions: K['loadLiveSessions'];
34
+ openStatuslinePicker: K['openStatuslinePicker'];
35
+ statuslineHiddenItems: K['statuslineHiddenItems'];
36
+ lastEnterAtRef: K['lastEnterAtRef'];
37
+ draftRef: K['draftRef'];
38
+ setDraft: K['setDraft'];
39
+ clearDraft: S['actions']['clearDraft'];
40
+ mouseMode: K['mouseMode'];
41
+ nativeMouse: S['live']['nativeMouse'];
42
+ termRows: K['termRows'];
43
+ terminalColumns: K['terminalColumns'];
44
+ terminalRows: K['terminalRows'];
45
+ mainColumnWidth: K['mainColumnWidth'];
46
+ overlayOpen: K['overlayOpen'];
47
+ effectiveSwarmOnSidebar: K['effectiveSwarmOnSidebar'];
48
+ sidebarTwinRowCount: K['sidebarTwinRowCount'];
49
+ statusBarWrapRef: K['statusBarWrapRef'];
50
+ belowStatusBarRef: K['belowStatusBarRef'];
51
+ statusBarClickMapRef: K['statusBarClickMapRef'];
52
+ openModelPicker: K['openModelPicker'];
53
+ nextStepsAutoSubmitTimerRef: K['nextStepsAutoSubmitTimerRef'];
54
+ nextStepsAutoSubmitSuggestionRef: K['nextStepsAutoSubmitSuggestionRef'];
55
+ nextStepsAutoSubmitLabel: K['nextStepsAutoSubmitLabel'];
56
+ setNextStepsAutoSubmitCountdown: K['setNextStepsAutoSubmitCountdown'];
57
+ setNextStepsAutoSubmitLabel: K['setNextStepsAutoSubmitLabel'];
58
+ cancelNextStepsCountdown: K['cancelNextStepsCountdown'];
59
+ pasteClipboardText: K['pasteClipboardText'];
60
+ pasteClipboardImage: K['pasteClipboardImage'];
61
+ onHistoryCopy: K['onHistoryCopy'];
62
+ tryPickerKey: K['tryPickerKey'];
63
+ pasteAccumRef: K['pasteAccumRef'];
64
+ pasteFlushTimerRef: K['pasteFlushTimerRef'];
65
+ commitPaste: K['commitPaste'];
66
+ builderRef: S['refs']['builder'];
67
+ tokenPreviewsRef: S['refs']['tokenPreviews'];
68
+ interruptsSyncRef: R['refs']['interrupts'];
69
+ stateRef: R['refs']['state'];
70
+ runBlocksRef: AppExecutionPipelineArgs['runBlocksRef'];
71
+ submitRef: AppExecutionPipelineArgs['submitRef'];
72
+ liveModel: S['live']['model'];
73
+ liveProvider: S['live']['provider'];
74
+ activeMaxContext: S['live']['maxContext'];
75
+ yoloLive: S['live']['yolo'];
76
+ autonomyLive: S['live']['autonomy'];
77
+ liveModeLabel: S['live']['modeLabel'];
78
+ setMouseMode: S['live']['setMouseMode'];
79
+ setNativeMouse: S['live']['setNativeMouse'];
80
+ setLiveModel: S['live']['setModel'];
81
+ setLiveProvider: S['live']['setProvider'];
82
+ setActiveMaxContext: S['live']['setMaxContext'];
83
+ setYoloLive: S['live']['setYolo'];
84
+ setAutonomyLive: S['live']['setAutonomy'];
85
+ setLiveModeLabel: S['live']['setModeLabel'];
86
+ setLiveToolCount: S['live']['setToolCount'];
87
+ autoSubmitStreakRef: S['refs']['autoSubmitStreak'];
88
+ autoSubmitCapWarnedRef: S['refs']['autoSubmitCapWarned'];
89
+ autoSubmitLoopGuardRef: S['refs']['autoSubmitLoopGuard'];
90
+ runEternalLoopRef: S['refs']['eternalLoop'];
91
+ runParallelLoopRef: S['refs']['parallelLoop'];
92
+ midRunSendPickerRef: S['refs']['midRunSendPicker'];
93
+ openPromptPicker: S['actions']['openPromptPicker'];
94
+ refreshGoalSummary: S['actions']['refreshGoalSummary'];
95
+ exit: S['actions']['exit'];
96
+ setMemoryContextMonitor: S['actions']['setMemoryContextMonitor'];
97
+ runSteerSequence: S['actions']['runSteerSequence'];
98
+ setEnhanceStartedAt: S['actions']['setEnhanceStartedAt'];
99
+ setEnhanceDurationMs: S['actions']['setEnhanceDuration'];
100
+ setRefineProviderId: S['actions']['setRefineProvider'];
101
+ setRefineModel: S['actions']['setRefineModel'];
100
102
  }): AppExecutionPipelineArgs;
103
+ export {};
101
104
  //# sourceMappingURL=use-app-pipeline-builders.d.ts.map
package/dist/index.js CHANGED
@@ -1429,31 +1429,27 @@ function softColor(color) {
1429
1429
  function getActiveTheme() {
1430
1430
  return theme;
1431
1431
  }
1432
+ function isThemeName(name) {
1433
+ return name in themePresets;
1434
+ }
1432
1435
  function setActiveTheme(name) {
1433
- let applied = currentPresetName;
1434
- const preset = name ? themePresets[name] : void 0;
1435
- if (preset && name in themePresets) {
1436
- applied = name;
1437
- }
1438
- if (!preset) {
1439
- return applied;
1440
- }
1441
- const t = theme;
1442
- const p = preset;
1443
- for (const k of Object.keys(t)) {
1444
- if (!(k in p)) delete t[k];
1436
+ if (name === void 0 || !isThemeName(name)) {
1437
+ return currentPresetName;
1445
1438
  }
1446
- for (const k of Object.keys(p)) {
1447
- t[k] = p[k];
1439
+ const preset = themePresets[name];
1440
+ const mutable = theme;
1441
+ for (const key of Object.keys(theme)) {
1442
+ if (!(key in preset)) delete mutable[key];
1448
1443
  }
1449
- currentPresetName = applied;
1444
+ Object.assign(theme, preset);
1445
+ currentPresetName = name;
1450
1446
  for (const cb of themeSubscribers) {
1451
1447
  try {
1452
- cb(applied);
1448
+ cb(name);
1453
1449
  } catch {
1454
1450
  }
1455
1451
  }
1456
- return applied;
1452
+ return name;
1457
1453
  }
1458
1454
  var themeSubscribers = /* @__PURE__ */ new Set();
1459
1455
  function subscribeToTheme(cb) {
@@ -5443,6 +5439,7 @@ function MetricsSection({ data }) {
5443
5439
  function CacheSection({ data }) {
5444
5440
  const cs = data.cacheStats ?? { readTokens: 0, writeTokens: 0, hitRatio: 0, savedUsd: 0 };
5445
5441
  const hasCache = cs.readTokens > 0 || cs.writeTokens > 0;
5442
+ const hasTtlSplit = cs.cacheWrite5m !== void 0 && cs.cacheWrite1h !== void 0;
5446
5443
  const coverage = Math.max(0, data.cacheCoverageTokens ?? 0);
5447
5444
  const max = data.ctxMaxTokens ?? 0;
5448
5445
  const covBarLen = 30;
@@ -5459,8 +5456,58 @@ function CacheSection({ data }) {
5459
5456
  /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 read " }),
5460
5457
  /* @__PURE__ */ jsx10(Text, { color: theme.textPrimary, children: cs.readTokens.toLocaleString("en-US") }),
5461
5458
  /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 write " }),
5462
- /* @__PURE__ */ jsx10(Text, { color: theme.textSecondary, children: cs.writeTokens.toLocaleString("en-US") })
5459
+ /* @__PURE__ */ jsx10(Text, { color: theme.textSecondary, children: cs.writeTokens.toLocaleString("en-US") }),
5460
+ cs.savedUsd > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
5461
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 saved " }),
5462
+ /* @__PURE__ */ jsxs9(Text, { color: theme.success, children: [
5463
+ "~$",
5464
+ cs.savedUsd.toFixed(2)
5465
+ ] })
5466
+ ] }) : null
5463
5467
  ] }),
5468
+ hasTtlSplit ? /* @__PURE__ */ jsxs9(Box, { children: [
5469
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: "Write TTL " }),
5470
+ /* @__PURE__ */ jsxs9(Text, { color: theme.textPrimary, children: [
5471
+ "5m ",
5472
+ (cs.cacheWrite5m ?? 0).toLocaleString("en-US")
5473
+ ] }),
5474
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 " }),
5475
+ /* @__PURE__ */ jsxs9(Text, { color: theme.textSecondary, children: [
5476
+ "1h ",
5477
+ (cs.cacheWrite1h ?? 0).toLocaleString("en-US")
5478
+ ] })
5479
+ ] }) : null,
5480
+ (data.providerCacheStats?.length ?? 0) > 0 ? /* @__PURE__ */ jsxs9(Box, { flexDirection: "column", children: [
5481
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: "Providers" }),
5482
+ data.providerCacheStats?.map((provider) => /* @__PURE__ */ jsxs9(Text, { children: [
5483
+ /* @__PURE__ */ jsxs9(Text, { color: theme.textPrimary, children: [
5484
+ " ",
5485
+ provider.provider
5486
+ ] }),
5487
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 " }),
5488
+ /* @__PURE__ */ jsxs9(Text, { color: provider.cacheRead > 0 ? theme.success : theme.textMuted, children: [
5489
+ (provider.hitRatio * 100).toFixed(1),
5490
+ "%"
5491
+ ] }),
5492
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 read " }),
5493
+ /* @__PURE__ */ jsx10(Text, { color: theme.textPrimary, children: provider.cacheRead.toLocaleString("en-US") }),
5494
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " \xB7 write " }),
5495
+ /* @__PURE__ */ jsx10(Text, { color: theme.textSecondary, children: provider.cacheWrite.toLocaleString("en-US") }),
5496
+ provider.cacheWrite5m !== void 0 && provider.cacheWrite1h !== void 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
5497
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: " (" }),
5498
+ /* @__PURE__ */ jsxs9(Text, { color: theme.textPrimary, children: [
5499
+ "5m ",
5500
+ provider.cacheWrite5m.toLocaleString("en-US")
5501
+ ] }),
5502
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: ", " }),
5503
+ /* @__PURE__ */ jsxs9(Text, { color: theme.textSecondary, children: [
5504
+ "1h ",
5505
+ provider.cacheWrite1h.toLocaleString("en-US")
5506
+ ] }),
5507
+ /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: ")" })
5508
+ ] }) : null
5509
+ ] }, provider.provider))
5510
+ ] }) : null,
5464
5511
  coverage > 0 && max > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
5465
5512
  /* @__PURE__ */ jsxs9(Box, { marginTop: 1, children: [
5466
5513
  /* @__PURE__ */ jsx10(Text, { color: theme.textMuted, children: "Coverage " }),
@@ -10320,6 +10367,7 @@ function AppStatusRegion({
10320
10367
  })(),
10321
10368
  cacheStats: cacheStats ?? { readTokens: 0, writeTokens: 0, hitRatio: 0, savedUsd: 0 },
10322
10369
  cacheCoverageTokens: cacheCoverageTokens ?? 0,
10370
+ providerCacheStats: cacheStats?.providers ?? [],
10323
10371
  fleetEntries: Object.values(state.fleet).map((e) => ({
10324
10372
  name: e.name,
10325
10373
  status: e.status,
@@ -20905,6 +20953,7 @@ function SidebarContent({
20905
20953
  const cs = cacheStats ?? { readTokens: 0, writeTokens: 0, hitRatio: 0, savedUsd: 0 };
20906
20954
  const hasCacheActivity = cs.readTokens > 0 || cs.writeTokens > 0;
20907
20955
  const cacheHitPct = Math.round(cs.hitRatio * 100);
20956
+ const activeProviderCache = cs.providers?.find((entry) => entry.provider === provider);
20908
20957
  const cacheCoveragePct = contextWindow && contextWindow.max > 0 ? Math.min(100, Math.round(cacheCoverageTokens / contextWindow.max * 100)) : 0;
20909
20958
  const MAX_SIDEBAR_AGENTS = 12;
20910
20959
  const allEntries = Object.values(entries);
@@ -21069,6 +21118,16 @@ function SidebarContent({
21069
21118
  )
21070
21119
  ) })
21071
21120
  ] }),
21121
+ /* @__PURE__ */ jsx78(
21122
+ SidebarStatRow,
21123
+ {
21124
+ label: `${activeProviderCache?.provider ?? provider ?? "provider"} hit`,
21125
+ value: activeProviderCache ? `${(activeProviderCache.hitRatio * 100).toFixed(1)}%` : `${cacheHitPct}%`,
21126
+ color: theme.success,
21127
+ accent: theme.success,
21128
+ innerWidth: bodyWidth
21129
+ }
21130
+ ),
21072
21131
  /* @__PURE__ */ jsx78(
21073
21132
  SidebarStatRow,
21074
21133
  {
@@ -46258,6 +46317,50 @@ function createRunTuiClientRegistration(opts) {
46258
46317
  return { register, unregister };
46259
46318
  }
46260
46319
 
46320
+ // src/run-tui-teardown.ts
46321
+ var DEFAULT_CLOSE_BUDGET_MS = 1500;
46322
+ function createDurableTeardown(opts) {
46323
+ const closeBudgetMs = opts.closeBudgetMs ?? DEFAULT_CLOSE_BUDGET_MS;
46324
+ const salvageSync = () => {
46325
+ try {
46326
+ opts.getSession()?.flushSync?.();
46327
+ } catch {
46328
+ }
46329
+ };
46330
+ const awaitCloseBounded = async () => {
46331
+ await Promise.race([
46332
+ (async () => {
46333
+ try {
46334
+ await opts.getSession()?.close();
46335
+ } catch {
46336
+ }
46337
+ })(),
46338
+ new Promise((resolve) => {
46339
+ const t = setTimeout(resolve, closeBudgetMs);
46340
+ t.unref();
46341
+ })
46342
+ ]);
46343
+ };
46344
+ const awaitDurableClose = async () => {
46345
+ salvageSync();
46346
+ try {
46347
+ opts.cleanup();
46348
+ } catch {
46349
+ }
46350
+ await awaitCloseBounded();
46351
+ salvageSync();
46352
+ };
46353
+ const shutdownViaSignal = async (exitCode) => {
46354
+ try {
46355
+ opts.killChildren?.();
46356
+ } catch {
46357
+ }
46358
+ await awaitDurableClose();
46359
+ opts.exit(exitCode);
46360
+ };
46361
+ return { shutdownViaSignal, awaitDurableClose, salvageSync };
46362
+ }
46363
+
46261
46364
  // src/run-tui-title-controller.ts
46262
46365
  import * as path11 from "node:path";
46263
46366
 
@@ -46512,6 +46615,24 @@ async function runTui(opts) {
46512
46615
  unsilenceTerminal();
46513
46616
  }
46514
46617
  };
46618
+ let resolveRun;
46619
+ let runExitCode = 0;
46620
+ const finishRun = (code) => {
46621
+ detachListeners();
46622
+ if (!resolveRun) return;
46623
+ const resolve = resolveRun;
46624
+ resolveRun = void 0;
46625
+ resolve(code);
46626
+ };
46627
+ const durableTeardown = createDurableTeardown({
46628
+ getSession: () => opts.agent.ctx.session,
46629
+ killChildren: () => getProcessRegistry10().killAll({ force: true, preserveBackground: true }),
46630
+ cleanup,
46631
+ // Resolve the host instead of process.exit so CLI teardown (plugin
46632
+ // stop, Telegram lock.release, vector-memory close) still runs.
46633
+ // Rapid Ctrl+C and the hung-unmount timer still hard-exit.
46634
+ exit: (code) => finishRun(code)
46635
+ });
46515
46636
  const RAPID_EXIT_WINDOW_MS = 2e3;
46516
46637
  const RAPID_EXIT_THRESHOLD = 2;
46517
46638
  let ctrlCPressTimestamps = [];
@@ -46522,21 +46643,20 @@ async function runTui(opts) {
46522
46643
  getProcessRegistry10().killAll({ force: true, preserveBackground: true });
46523
46644
  } catch {
46524
46645
  }
46525
- try {
46526
- opts.agent.ctx.session.flushSync?.();
46527
- } catch {
46528
- }
46646
+ durableTeardown.salvageSync();
46529
46647
  cleanup();
46530
46648
  process.exit(130);
46531
46649
  };
46532
46650
  const signals = ["SIGTERM", "SIGHUP", "SIGINT"];
46533
46651
  const signalHandler = () => {
46652
+ runExitCode = 143;
46534
46653
  inkInstance?.unmount();
46654
+ void durableTeardown.shutdownViaSignal(143);
46655
+ };
46656
+ const exitHandler = () => {
46657
+ durableTeardown.salvageSync();
46535
46658
  cleanup();
46536
- const sig = setTimeout(() => process.exit(143), 5e3);
46537
- sig.unref();
46538
46659
  };
46539
- const exitHandler = () => cleanup();
46540
46660
  const sigintHandler = () => {
46541
46661
  const now = Date.now();
46542
46662
  ctrlCPressTimestamps = ctrlCPressTimestamps.filter((t) => now - t < RAPID_EXIT_WINDOW_MS);
@@ -46546,10 +46666,9 @@ async function runTui(opts) {
46546
46666
  forceExitViaRapidCtrlC();
46547
46667
  return;
46548
46668
  }
46669
+ runExitCode = 130;
46549
46670
  inkInstance?.unmount();
46550
- cleanup();
46551
- const sig = setTimeout(() => process.exit(130), 5e3);
46552
- sig.unref();
46671
+ void durableTeardown.shutdownViaSignal(130);
46553
46672
  };
46554
46673
  process.on("SIGINT", sigintHandler);
46555
46674
  for (const s2 of ["SIGTERM", "SIGHUP", "SIGBREAK"]) {
@@ -46588,24 +46707,32 @@ async function runTui(opts) {
46588
46707
  };
46589
46708
  void tuiClientRegistration.register();
46590
46709
  return new Promise((resolve) => {
46591
- let exitCode = 0;
46710
+ resolveRun = resolve;
46592
46711
  let hardExitTimer = null;
46593
46712
  const onExit = (code) => {
46594
- exitCode = code;
46713
+ runExitCode = code;
46595
46714
  };
46596
46715
  const settle = (code) => {
46597
46716
  if (hardExitTimer) {
46598
46717
  clearTimeout(hardExitTimer);
46599
46718
  hardExitTimer = null;
46600
46719
  }
46601
- cleanup();
46602
- detachListeners();
46603
- resolve(code);
46720
+ void durableTeardown.awaitDurableClose().then(
46721
+ () => finishRun(code),
46722
+ () => finishRun(code)
46723
+ );
46604
46724
  };
46605
46725
  const requestExit = (code) => {
46606
46726
  onExit(code);
46607
46727
  inkInstance?.unmount();
46608
- hardExitTimer = setTimeout(() => process.exit(code), 5e3);
46728
+ hardExitTimer = setTimeout(() => {
46729
+ durableTeardown.salvageSync();
46730
+ try {
46731
+ getProcessRegistry10().killAll({ force: true, preserveBackground: true });
46732
+ } catch {
46733
+ }
46734
+ process.exit(code);
46735
+ }, 5e3);
46609
46736
  hardExitTimer.unref();
46610
46737
  };
46611
46738
  opts.requestExit = requestExit;
@@ -46776,7 +46903,7 @@ async function runTui(opts) {
46776
46903
  detachResize = () => stdout.off("resize", onResize2);
46777
46904
  instance.waitUntilExit().then(() => {
46778
46905
  detachResize?.();
46779
- settle(exitCode);
46906
+ settle(runExitCode);
46780
46907
  }).catch(() => {
46781
46908
  detachResize?.();
46782
46909
  settle(1);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Durable teardown primitives for the TUI host.
3
+ *
4
+ * Extracted from run-tui.ts so the kill-safety semantics are unit-testable
5
+ * without an Ink render loop.
6
+ *
7
+ * Why both a sync and an async step exist:
8
+ * - `flushSync()` drains the writer's in-memory buffer with blocking
9
+ * writes. That data survives ANY concurrent hard exit (the host's
10
+ * 500ms exit-grace timer, a second Ctrl+C, a supervisor SIGKILL racing
11
+ * us) because by the time it returns, the bytes are already handed to
12
+ * the OS.
13
+ * - `close()` additionally drains the async write chain, `datasync()`s
14
+ * the handle, and writes the `.summary.json` sidecar + `_index.jsonl`
15
+ * row. That work is async, cannot be forced synchronous, and therefore
16
+ * gets a bounded budget instead of being trusted to outlive the loop.
17
+ *
18
+ * Ordering contract of `shutdownViaSignal`:
19
+ * 1. killChildren — stop tool subprocesses from producing new events.
20
+ * 2. salvageSync — buffered events are durably on disk from here on.
21
+ * 3. cleanup — terminal restore; also kicks off close() (idempotent).
22
+ * 4. await close — bounded by closeBudgetMs; never hangs shutdown.
23
+ * 5. salvageSync — catch anything close() re-buffered while failing.
24
+ * 6. exit — caller-supplied exit function.
25
+ */
26
+ /** Structural subset of SessionWriter the teardown needs. */
27
+ export interface TeardownSession {
28
+ /** Durable drain: buffer → disk → datasync → sidecar/index. Idempotent. */
29
+ close(): Promise<void>;
30
+ /** Last-gasp synchronous drain of whatever is still in memory. */
31
+ flushSync?(): void;
32
+ }
33
+ export interface DurableTeardownOptions {
34
+ /** Reads the CURRENT writer so swaps (e.g. /resume) stay covered. */
35
+ getSession: () => TeardownSession | undefined;
36
+ /** Foreground child cleanup (process registry killAll). Best-effort. */
37
+ killChildren?: (() => void) | undefined;
38
+ /** Terminal restore + client unregister. Idempotent. */
39
+ cleanup: () => void;
40
+ /** Hard ceiling for awaiting close(). Default 1500ms. */
41
+ closeBudgetMs?: number;
42
+ /** Exit function — injectable so tests don't need process.exit. */
43
+ exit: (code: number) => void;
44
+ }
45
+ export interface DurableTeardown {
46
+ /**
47
+ * Signal-driven shutdown (SIGTERM/SIGHUP/Ctrl+C): full ordered sequence,
48
+ * ends by calling exit(). Safe to fire concurrently from several handlers
49
+ * — every step is idempotent and exit wins once.
50
+ */
51
+ shutdownViaSignal(exitCode: number): Promise<void>;
52
+ /**
53
+ * Awaited close WITHOUT exiting — used by the natural exit path (settle)
54
+ * so the host continues only after durability, and project-switch respawns
55
+ * keep working.
56
+ */
57
+ awaitDurableClose(): Promise<void>;
58
+ /** Sync-only last-chance drain for 'exit' listeners / process.exit contexts. */
59
+ salvageSync(): void;
60
+ }
61
+ export declare function createDurableTeardown(opts: DurableTeardownOptions): DurableTeardown;
62
+ //# sourceMappingURL=run-tui-teardown.d.ts.map
package/dist/theme.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { SyntaxPalette, SyntaxRole, Theme, ThemeName } from './theme-types.js';
1
+ import type { SyntaxPalette, SyntaxRole, Theme, ThemeListener, ThemeName } from './theme-types.js';
2
2
  export { baseTheme, THEME_OPTIONS, themePickerOptions, themePresets } from './theme-presets.js';
3
3
  export type { SyntaxPalette, SyntaxRole, Theme, ThemeName, ThemePickerOption, } from './theme-types.js';
4
4
  export { catppuccin, detectSupportsBackground, mixHexColors, pastel, SYNTAX_TOKEN, } from './theme-utils.js';
@@ -30,7 +30,6 @@ export declare function resolveSyntaxPalette(palette?: Theme): SyntaxPalette;
30
30
  export declare function softColor(color?: string): string | undefined;
31
31
  export declare function getActiveTheme(): Theme;
32
32
  export declare function setActiveTheme(name: string | undefined): ThemeName;
33
- type ThemeListener = (name: ThemeName) => void;
34
33
  export declare function subscribeToTheme(cb: ThemeListener): () => void;
35
34
  export declare function getActiveThemeName(): ThemeName;
36
35
  //# sourceMappingURL=theme.d.ts.map
@@ -21,7 +21,7 @@ export interface RunBlocksCapabilities {
21
21
  agent: Agent;
22
22
  tokenCounter?: TokenCounter | undefined;
23
23
  supportsVision?: (() => boolean | Promise<boolean>) | undefined;
24
- visionAdapters: VisionAdapters;
24
+ visionAdapters?: VisionAdapters | undefined;
25
25
  onSDDOutput?: ((output: string) => Promise<string[]>) | undefined;
26
26
  onSuggestionsParsed?: ((finalText: string) => void) | undefined;
27
27
  predictNext?: ((input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/tui",
3
- "version": "0.309.0",
3
+ "version": "0.310.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack terminal UI: Ink-based interactive renderer with history, pickers, and slash menus.",
6
6
  "repository": {
@@ -29,12 +29,12 @@
29
29
  "ink": "7.1.1",
30
30
  "react": "^19.2.8",
31
31
  "string-width": "8.2.2",
32
- "@wrongstack/kanban": "0.309.0",
33
- "@wrongstack/sdd": "0.309.0",
34
- "@wrongstack/core": "0.309.0",
35
- "@wrongstack/runtime": "0.309.0",
36
- "@wrongstack/sage": "0.309.0",
37
- "@wrongstack/tools": "0.309.0"
32
+ "@wrongstack/core": "0.310.0",
33
+ "@wrongstack/kanban": "0.310.0",
34
+ "@wrongstack/runtime": "0.310.0",
35
+ "@wrongstack/sage": "0.310.0",
36
+ "@wrongstack/sdd": "0.310.0",
37
+ "@wrongstack/tools": "0.310.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@testing-library/dom": "^10.4.1",