@wrongstack/tui 0.302.2 → 0.303.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.
@@ -160,6 +160,20 @@ export type Action = {
160
160
  } | {
161
161
  type: 'autonomyPickerHint';
162
162
  text?: string | undefined;
163
+ }
164
+ /** Theme picker — opened by `/theme`. Selecting a row swaps the palette and
165
+ * persists the choice via the ConfigStore bridge from the host. */
166
+ | {
167
+ type: 'themePickerOpen';
168
+ selected?: number;
169
+ } | {
170
+ type: 'themePickerClose';
171
+ } | {
172
+ type: 'themePickerMove';
173
+ delta: number;
174
+ } | {
175
+ type: 'themePickerHint';
176
+ text?: string | undefined;
163
177
  } | {
164
178
  type: 'modePickerOpen';
165
179
  modes: ModeOption[];
@@ -1,9 +1,9 @@
1
1
  import type { Agent } from '@wrongstack/core/agent';
2
2
  import type { CoordinatorEvent, Director } from '@wrongstack/core/coordination';
3
+ import type { EventBus } from '@wrongstack/core/kernel';
3
4
  import type { SlashCommandRegistry } from '@wrongstack/core/registry';
4
5
  import type { QueueStore } from '@wrongstack/core/storage';
5
- import type { AttachmentStore, AutonomyStage, ContextSnapshot, FleetChatVerbosity, Message, TokenCounter, TokenSavingTier } from '@wrongstack/core/types';
6
- import type { EventBus } from '@wrongstack/core/kernel';
6
+ import type { AttachmentStore, AutonomyStage, ConfigStore, ContextSnapshot, FleetChatVerbosity, Message, TokenCounter, TokenSavingTier } from '@wrongstack/core/types';
7
7
  import type { VisionAdapters } from '@wrongstack/runtime/vision';
8
8
  import type { SddLifecycleResult, SddRunControl } from '@wrongstack/sdd';
9
9
  import type React from 'react';
@@ -205,11 +205,25 @@ export interface AppProps {
205
205
  */
206
206
  /** Settings shape — shared between getSettings and saveSettings. */
207
207
  getSettings?: (() => Settings) | undefined;
208
+ /**
209
+ * Live view over the persisted user config. The TUI uses this to:
210
+ * - apply `themePreset` on boot (so `/theme` choices persist across
211
+ * restarts), and
212
+ * - write `themePreset` back when the picker Enter handler fires
213
+ * (so the picker shows `[active]` on the right row next session).
214
+ *
215
+ * Optional for hosts that don't expose a config store (e.g. tests);
216
+ * when omitted the TUI stays on the default catppuccin palette and
217
+ * picker changes are ephemeral.
218
+ */
219
+ configStore?: ConfigStore | undefined;
208
220
  /**
209
221
  * Persist settings changes. Returns null on success, or an
210
222
  * error string on failure (so the TUI can display it as a hint).
211
223
  */
212
224
  saveSettings?: ((s: Settings) => string | null | Promise<string | null>) | undefined;
225
+ /** Persist the active theme preset to disk so the next boot starts with it. */
226
+ saveThemePreset?: ((preset: import('@wrongstack/core/types').ThemePresetId) => Promise<void>) | undefined;
213
227
  /** Load toggleable plugin rows for the interactive plugin picker. */
214
228
  getPluginItems?: (() => PluginPickerItem[]) | undefined;
215
229
  /** Toggle one plugin from the interactive picker and return the refreshed rows. */
@@ -167,6 +167,13 @@ export type State = {
167
167
  selected: number;
168
168
  hint?: string | undefined;
169
169
  };
170
+ /** Single-step theme picker — opened by `/theme`. Lists every preset and
171
+ * applies the chosen one instantly (in-place palette mutation + persist). */
172
+ themePicker: {
173
+ open: boolean;
174
+ selected: number;
175
+ hint?: string | undefined;
176
+ };
170
177
  /** Agent mode picker — opened by `/mode`. Selects teach/brief/code-reviewer/etc. */
171
178
  modePicker: {
172
179
  open: boolean;
@@ -9,6 +9,9 @@ export interface DesignPickerProps {
9
9
  * Design Studio kit picker overlay (opened by `/design`). Presentational only —
10
10
  * navigation/selection state lives in the reducer; Enter runs `/design <id>
11
11
  * <stack>` through the normal submit path, which pins the kit + loads its spec.
12
+ *
13
+ * Windowed via {@link useWindowedPicker} so 50+ installed kits stay
14
+ * navigable on small terminals.
12
15
  */
13
16
  export declare function DesignPicker({ kits, selected, stack }: DesignPickerProps): React.ReactElement;
14
17
  //# sourceMappingURL=design-picker.d.ts.map
@@ -11,6 +11,10 @@ export interface ResumePickerProps {
11
11
  * Interactive session-resume picker. Renders a scrollable list of recent
12
12
  * sessions with metadata (id, date, tokens, tools, outcome badge) so the user
13
13
  * can pick one to resume. The current session is grayed out and non-selectable.
14
+ *
15
+ * Windowed via {@link useWindowedPicker} with `rowSpan=3` — each session takes
16
+ * three rows (title line, meta line, preview line). The window auto-recenters
17
+ * on the focused session so 30+ entries stay navigable on a 24-row terminal.
14
18
  */
15
19
  export declare function ResumePicker({ sessions, selected, busy, error, hint, }: ResumePickerProps): React.ReactElement;
16
20
  //# sourceMappingURL=resume-picker.d.ts.map
@@ -0,0 +1,21 @@
1
+ import type React from 'react';
2
+ import type { ThemeName, ThemePickerOption } from '../theme.js';
3
+ export interface ThemePickerProps {
4
+ options: readonly ThemePickerOption[];
5
+ selected: number;
6
+ activeId: ThemeName;
7
+ hint?: string | undefined;
8
+ }
9
+ /**
10
+ * Interactive theme picker. Mirrors the autonomy-picker visual pattern
11
+ * (bordered box, ↑/↓ navigate, Enter apply, Esc cancel) so the two
12
+ * pickers look and feel consistent. The currently active preset is
13
+ * highlighted with a `[active]` marker so the user can see whether
14
+ * their selection will actually change anything.
15
+ *
16
+ * The list is windowed via {@link useWindowedPicker} so 15+ presets stay
17
+ * usable on small terminals — a top-aligned `…` marker appears when the
18
+ * focused row is below the start of the visible window.
19
+ */
20
+ export declare function ThemePicker({ options, selected, activeId, hint, }: ThemePickerProps): React.ReactElement;
21
+ //# sourceMappingURL=theme-picker.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Subscribes to the module-scoped active theme so a render reads the
3
+ * current `theme` snapshot. Used by the App shell to force a re-render
4
+ * when `setActiveTheme()` mutates the global theme — child components
5
+ * read `theme` directly via destructuring, so the parent re-render
6
+ * propagates the new palette into the whole tree.
7
+ *
8
+ * The hook itself returns the live `theme` reference; callers don't
9
+ * typically need to use it (they can keep reading `theme` directly),
10
+ * but exposing it lets test code and stat panels introspect the active
11
+ * palette without re-implementing the listener.
12
+ */
13
+ export declare function useActiveTheme(): import("../theme.js").Theme;
14
+ //# sourceMappingURL=use-active-theme.d.ts.map
@@ -31,7 +31,8 @@ interface UseAppPickerKeysOptions {
31
31
  handleShadowStop: () => Promise<void>;
32
32
  statuslineHiddenForPicker: () => StatuslineItem[];
33
33
  onPickerEnter: () => Promise<void>;
34
+ onThemePickerEnter?: () => void;
34
35
  }
35
- export declare function useAppPickerKeys({ host, state, dispatch, environment, statusbar, panelControllers, authPanelController, brainController, lastEnterAtRef, inputGateRef, submitRef, promptUsageRef, setDraft, acceptSlashPickerSelection, changeBrainRisk, handleModelPicked, handleShadowStart, handleShadowStop, statuslineHiddenForPicker, onPickerEnter, }: UseAppPickerKeysOptions): (input: string, key: import("../components/input.js").KeyEvent, isEnter: boolean) => boolean;
36
+ export declare function useAppPickerKeys({ host, state, dispatch, environment, statusbar, panelControllers, authPanelController, brainController, lastEnterAtRef, inputGateRef, submitRef, promptUsageRef, setDraft, acceptSlashPickerSelection, changeBrainRisk, handleModelPicked, handleShadowStart, handleShadowStop, statuslineHiddenForPicker, onPickerEnter, onThemePickerEnter, }: UseAppPickerKeysOptions): (input: string, key: import("../components/input.js").KeyEvent, isEnter: boolean) => boolean;
36
37
  export {};
37
38
  //# sourceMappingURL=use-app-picker-keys.d.ts.map
@@ -51,6 +51,13 @@ export interface PickerKeysHost {
51
51
  onShadowStop: (() => Promise<void> | void) | undefined;
52
52
  onFKeyPickerEnter: (() => void) | undefined;
53
53
  onPickerEnter: (() => Promise<void>) | undefined;
54
+ /**
55
+ * Enter-handler for the `/theme` picker. The key dispatcher delegates
56
+ * the apply step here so the parent can call `setActiveTheme()` and
57
+ * persist the choice to configStore. Optional — omitted hosts simply
58
+ * leave Enter as a no-op (the picker still navigates, just doesn't apply).
59
+ */
60
+ onThemePickerEnter?: (() => void) | undefined;
54
61
  onSlashPickerTab: (() => void) | undefined;
55
62
  }
56
63
  //# sourceMappingURL=use-picker-keys-types.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { ConfigStore } from '@wrongstack/core/types';
2
+ /**
3
+ * Boot-time theme wiring: reads `config.themePreset` once on mount, applies it
4
+ * via `setActiveTheme()`, and subscribes to the live `ConfigStore` so any
5
+ * in-session update (e.g. from the REPL or a future hot-reload) is mirrored
6
+ * into the visible palette immediately.
7
+ *
8
+ * Falls back to the current preset (catppuccin at first mount) if the config
9
+ * has no `themePreset` set — that's the legacy behaviour for users without an
10
+ * explicit choice, and it makes the picker UI immediately meaningful on first
11
+ * run because the active row is highlighted.
12
+ */
13
+ export declare function useThemeState({ configStore }: {
14
+ configStore: ConfigStore | undefined;
15
+ }): void;
16
+ //# sourceMappingURL=use-theme-state.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * One shared viewport-windowing rule for the pickers that overlay the chat
3
+ * history (theme / autonomy / design / resume).
4
+ *
5
+ * Before this hook, every picker rendered every option as a static
6
+ * `<Box flexDirection="column">`. That was fine for autonomy's 5 options but
7
+ * started overflowing as soon as `/theme` carried 15 presets and `/resume`
8
+ * could list 30+ sessions. The pickers live inside the scrollable chat
9
+ * viewport — they share its row budget — so windowing has to be driven off
10
+ * `useTerminalSize` and per-picker chrome, not a hardcoded count.
11
+ *
12
+ * The hook returns the inclusive `start` and exclusive `end` of the visible
13
+ * slice plus `hasAbove` / `hasBelow` indicators the caller can render as
14
+ * `…` markers. When the caller moves `selected`, the next call re-centers
15
+ * the viewport on the new index — no imperative scroll needed.
16
+ */
17
+ export interface UseWindowedPickerOptions {
18
+ /** Total option count. */
19
+ readonly total: number;
20
+ /** Currently focused option index. The window re-centers on this. */
21
+ readonly selected: number;
22
+ /** Rows each option consumes (resume-picker entries take 3 lines). */
23
+ readonly rowSpan?: number | undefined;
24
+ /**
25
+ * Rows the picker itself reserves around the list — top/bottom border,
26
+ * header, hint, padding. Defaults to 4 which matches the current
27
+ * `borderStyle="round"` + 2-line header pattern. Pass 0 for a naked list.
28
+ */
29
+ readonly chromeRows?: number | undefined;
30
+ /**
31
+ * Rows the surrounding app shell reserves — input bar + status bar + the
32
+ * scrollable history's own chrome. Defaults to 6, which covers the input
33
+ * bar (1), status bar (1), and a 4-row safety margin for header / hint /
34
+ * padding. Bump this if your caller knows it needs more breathing room.
35
+ */
36
+ readonly shellReservedRows?: number | undefined;
37
+ /** Minimum visible options even on a tiny terminal. Defaults to 3. */
38
+ readonly minVisible?: number | undefined;
39
+ }
40
+ export interface WindowedPickerSlice {
41
+ /** Inclusive start index of the visible window. */
42
+ readonly start: number;
43
+ /** Exclusive end index of the visible window. */
44
+ readonly end: number;
45
+ /** True when at least one option is hidden above `start`. */
46
+ readonly hasAbove: boolean;
47
+ /** True when at least one option is hidden below `end - 1`. */
48
+ readonly hasBelow: boolean;
49
+ }
50
+ /**
51
+ * Compute a windowed slice of a picker list. Pure given the inputs — the only
52
+ * "reactive" part is `useTerminalSize` so the window re-flows when the user
53
+ * resizes their terminal while a picker is open.
54
+ *
55
+ * Behavior contract:
56
+ * - Visible count is `floor((rows - chrome) / rowSpan)`, clamped to
57
+ * `[minVisible, total]`. A 1-row terminal always shows at least
58
+ * `minVisible` options even if that overflows — better to bleed than to
59
+ * hide the focused row.
60
+ * - The window stays centered on `selected` once the picker is past the
61
+ * first page; on the first page it stays top-aligned so the user sees
62
+ * the list start. `selected < 0 || selected >= total` is treated as no
63
+ * focus and the window anchors to index 0.
64
+ * - Empty / single-option lists always render `[0, total)` with both
65
+ * `hasAbove` and `hasBelow` false.
66
+ */
67
+ export declare function useWindowedPicker({ total, selected, rowSpan, chromeRows, shellReservedRows, minVisible, }: UseWindowedPickerOptions): WindowedPickerSlice;
68
+ //# sourceMappingURL=use-windowed-picker.d.ts.map