dsh-code 0.6.1 → 0.7.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/src/startup.ts CHANGED
@@ -1,109 +1,119 @@
1
- /**
2
- * The interactive terminal app's command-line provider: parses `--resume`,
3
- * `--continue`, `--session`, `--mode`, and `--help`, then publishes
4
- * {@link TUI_STARTUP_SERVICE} for the runner to consume lazily. Follows the
5
- * headless bundle's startup shape (a commander action publishing a service
6
- * through {@link parseCmdline}).
7
- *
8
- * Semantics:
9
- * - `--resume <id|prefix>` — continue the persisted session whose id or unique
10
- * id-prefix matches; the TUI replays its transcript and appends to the same
11
- * durable log.
12
- * - `--continue` / `-c` — resume the most recently modified persisted session
13
- * whose project directory matches the current working directory.
14
- * - `--session <id>` — create a new session under an explicit identity (the
15
- * id must not exist yet).
16
- * - no flagsa fresh session with a minted id.
17
- *
18
- * @module @deepseek-ai/dsh-tui/startup
19
- */
20
-
21
- import { Command } from 'commander'
22
- import type { Context } from '@deepseek-ai/cordis'
23
- import { parseCmdline } from '@deepseek-ai/dsh-cmdline'
24
-
25
- /** Stable Cordis plugin name. */
26
- export const name = 'tui-startup'
27
-
28
- /** Services required before the invocation can be resolved. */
29
- export const inject = ['cmdlineArgs']
30
-
31
- /** Service provided by this plugin and injected by the terminal runner. */
32
- export const TUI_STARTUP_SERVICE = 'tuiStartup'
33
-
34
- /** How the runner obtains its session identity. */
35
- export type TuiStartup =
36
- | { readonly kind: 'fresh'; readonly mode?: string }
37
- | { readonly kind: 'named'; readonly sessionId: string; readonly mode?: string }
38
- | { readonly kind: 'resume'; readonly sessionId: string }
39
- | { readonly kind: 'latest' }
40
-
41
- export interface TuiStartupOptions {
42
- readonly resume?: string
43
- readonly continue?: boolean
44
- readonly session?: string
45
- readonly mode?: string
46
- }
47
-
48
- /** Pure option policy shared by Commander and tests. */
49
- export function resolveTuiStartup(options: TuiStartupOptions): TuiStartup {
50
- const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined]
51
- if (selected.filter(Boolean).length > 1) throw new Error('--resume, --continue, and --session are mutually exclusive')
52
- if (options.session === '') throw new Error('--session needs an id')
53
- if (options.resume === '') throw new Error('--resume needs a session id or id prefix')
54
- if (options.mode === '') throw new Error('--mode needs a preset id')
55
- if (options.mode !== undefined && (options.resume !== undefined || options.continue === true)) {
56
- throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue')
57
- }
58
- return options.resume !== undefined
59
- ? { kind: 'resume', sessionId: options.resume }
60
- : options.continue === true
61
- ? { kind: 'latest' }
62
- : options.session !== undefined
63
- ? { kind: 'named', sessionId: options.session, ...options.mode === undefined ? {} : { mode: options.mode } }
64
- : { kind: 'fresh', ...options.mode === undefined ? {} : { mode: options.mode } }
65
- }
66
-
67
- /**
68
- * This app's command: the launcher's flags this app owns, its description,
69
- * and its help text.
70
- * @returns a fresh program, so one process can parse more than once (tests).
71
- */
72
- function tuiCommand(): Command {
73
- return new Command()
74
- .name('dsh --profile cli')
75
- .description('Claude-Code-style interactive terminal for DeepSeek Harness.')
76
- .helpOption('-h, --help', 'show this help')
77
- .option('-r, --resume <session>', 'resume the persisted session with this id (or unique id prefix)')
78
- .option('-c, --continue', 'resume the most recent persisted session for this working directory')
79
- .option('--session <id>', 'create a new session under this explicit id')
80
- .option('--mode <preset>', 'agent preset for a newly created session')
81
- .addHelpText('after', `
82
- Examples:
83
- dsh --profile cli fresh session, minted id
84
- dsh --profile cli --resume abc123 resume session by id prefix
85
- dsh --profile cli --continue resume the latest local session
86
- dsh --profile cli --mode minimal fresh session using the minimal preset
87
- `)
88
- }
89
-
90
- /**
91
- * Parse the invocation and publish the startup service. Mutual exclusions are
92
- * usage errors rejected from the action before anything is provided.
93
- * @param ctx - plugin context carrying the command line and exit request.
94
- */
95
- export function apply(ctx: Context): void {
96
- const program = tuiCommand()
97
- program.action(() => {
98
- const options = program.opts<TuiStartupOptions>()
99
- let startup: TuiStartup | undefined
100
- try {
101
- startup = resolveTuiStartup(options)
102
- } catch (error: unknown) {
103
- program.error(`error: ${error instanceof Error ? error.message : String(error)}`)
104
- }
105
- if (startup === undefined) return
106
- ctx.provide(TUI_STARTUP_SERVICE, { startup } satisfies { startup: TuiStartup })
107
- })
108
- parseCmdline(ctx, program)
109
- }
1
+ /**
2
+ * The interactive terminal app's command-line provider: parses `--resume`,
3
+ * `--continue`, `--session`, `--mode`, `--theme`, and `--help`, then
4
+ * publishes {@link TUI_STARTUP_SERVICE} for the runner to consume lazily.
5
+ * Follows the headless bundle's startup shape (a commander action publishing
6
+ * a service through {@link parseCmdline}).
7
+ *
8
+ * Semantics:
9
+ * - `--resume <id|prefix>` — continue the persisted session whose id or unique
10
+ * id-prefix matches; the TUI replays its transcript and appends to the same
11
+ * durable log.
12
+ * - `--continue` / `-c` — resume the most recently modified persisted session
13
+ * whose project directory matches the current working directory.
14
+ * - `--session <id>` — create a new session under an explicit identity (the
15
+ * id must not exist yet).
16
+ * - `--theme <dark|light|auto>`the color palette; auto follows the
17
+ * terminal (dark fallback until OSC-11 detection lands).
18
+ * - no flags — a fresh session with a minted id.
19
+ *
20
+ * @module @deepseek-ai/dsh-tui/startup
21
+ */
22
+
23
+ import { Command } from 'commander'
24
+ import type { Context } from '@deepseek-ai/cordis'
25
+ import { parseCmdline } from '@deepseek-ai/dsh-cmdline'
26
+ import { THEME_NAMES, type ThemeName } from './theme.ts'
27
+
28
+ /** Stable Cordis plugin name. */
29
+ export const name = 'tui-startup'
30
+
31
+ /** Services required before the invocation can be resolved. */
32
+ export const inject = ['cmdlineArgs']
33
+
34
+ /** Service provided by this plugin and injected by the terminal runner. */
35
+ export const TUI_STARTUP_SERVICE = 'tuiStartup'
36
+
37
+ /** How the runner obtains its session identity. */
38
+ export type TuiStartup =
39
+ | { readonly kind: 'fresh'; readonly mode?: string; readonly theme?: ThemeName }
40
+ | { readonly kind: 'named'; readonly sessionId: string; readonly mode?: string; readonly theme?: ThemeName }
41
+ | { readonly kind: 'resume'; readonly sessionId: string; readonly theme?: ThemeName }
42
+ | { readonly kind: 'latest'; readonly theme?: ThemeName }
43
+
44
+ export interface TuiStartupOptions {
45
+ readonly resume?: string
46
+ readonly continue?: boolean
47
+ readonly session?: string
48
+ readonly mode?: string
49
+ readonly theme?: ThemeName
50
+ }
51
+
52
+ /** Pure option policy shared by Commander and tests. */
53
+ export function resolveTuiStartup(options: TuiStartupOptions): TuiStartup {
54
+ const selected = [options.resume !== undefined, options.continue === true, options.session !== undefined]
55
+ if (selected.filter(Boolean).length > 1) throw new Error('--resume, --continue, and --session are mutually exclusive')
56
+ if (options.session === '') throw new Error('--session needs an id')
57
+ if (options.resume === '') throw new Error('--resume needs a session id or id prefix')
58
+ if (options.mode === '') throw new Error('--mode needs a preset id')
59
+ if (options.mode !== undefined && (options.resume !== undefined || options.continue === true)) {
60
+ throw new Error('--mode applies only to a new session; it cannot be combined with --resume or --continue')
61
+ }
62
+ if (options.theme !== undefined && !THEME_NAMES.includes(options.theme)) {
63
+ throw new Error('--theme must be dark, light, or auto')
64
+ }
65
+ const theme = options.theme === undefined ? {} : { theme: options.theme }
66
+ return options.resume !== undefined
67
+ ? { kind: 'resume', sessionId: options.resume, ...theme }
68
+ : options.continue === true
69
+ ? { kind: 'latest', ...theme }
70
+ : options.session !== undefined
71
+ ? { kind: 'named', sessionId: options.session, ...options.mode === undefined ? {} : { mode: options.mode }, ...theme }
72
+ : { kind: 'fresh', ...options.mode === undefined ? {} : { mode: options.mode }, ...theme }
73
+ }
74
+
75
+ /**
76
+ * This app's command: the launcher's flags this app owns, its description,
77
+ * and its help text.
78
+ * @returns a fresh program, so one process can parse more than once (tests).
79
+ */
80
+ function tuiCommand(): Command {
81
+ return new Command()
82
+ .name('dsh --profile cli')
83
+ .description('Claude-Code-style interactive terminal for DeepSeek Harness.')
84
+ .helpOption('-h, --help', 'show this help')
85
+ .option('-r, --resume <session>', 'resume the persisted session with this id (or unique id prefix)')
86
+ .option('-c, --continue', 'resume the most recent persisted session for this working directory')
87
+ .option('--session <id>', 'create a new session under this explicit id')
88
+ .option('--mode <preset>', 'agent preset for a newly created session')
89
+ .option('--theme <name>', 'color theme: dark (default), light, or auto')
90
+ .addHelpText('after', `
91
+ Examples:
92
+ dsh --profile cli fresh session, minted id
93
+ dsh --profile cli --resume abc123 resume session by id prefix
94
+ dsh --profile cli --continue resume the latest local session
95
+ dsh --profile cli --mode minimal fresh session using the minimal preset
96
+ dsh --profile cli --theme light light palette for bright terminals
97
+ `)
98
+ }
99
+
100
+ /**
101
+ * Parse the invocation and publish the startup service. Mutual exclusions are
102
+ * usage errors rejected from the action before anything is provided.
103
+ * @param ctx - plugin context carrying the command line and exit request.
104
+ */
105
+ export function apply(ctx: Context): void {
106
+ const program = tuiCommand()
107
+ program.action(() => {
108
+ const options = program.opts<TuiStartupOptions>()
109
+ let startup: TuiStartup | undefined
110
+ try {
111
+ startup = resolveTuiStartup(options)
112
+ } catch (error: unknown) {
113
+ program.error(`error: ${error instanceof Error ? error.message : String(error)}`)
114
+ }
115
+ if (startup === undefined) return
116
+ ctx.provide(TUI_STARTUP_SERVICE, { startup } satisfies { startup: TuiStartup })
117
+ })
118
+ parseCmdline(ctx, program)
119
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The `/theme` picker (the Codex `/theme` contract): one bounded list over
3
+ * the three color themes — dark, light, and auto (terminal-sensed; auto
4
+ * falls back to dark until OSC-11 detection lands). Enter applies the row
5
+ * and the runner persists it; Esc closes without changing the theme.
6
+ *
7
+ * @module @deepseek-ai/dsh-tui/theme-panel
8
+ */
9
+
10
+ import { createElement, useState, type ReactElement } from 'react'
11
+ import { Box, Text, useInput, useStdout } from 'ink'
12
+ import { panelViewport } from './render/inspector.ts'
13
+ import { truncateColumns } from './render/text.ts'
14
+ import { getPalette, inkColor, type ThemeName } from './theme.ts'
15
+
16
+ /** The three theme rows in canonical order (the /theme selection surface). */
17
+ const THEME_ROWS: readonly { id: ThemeName; label: string; description: string }[] = [
18
+ { id: 'dark', label: 'dark', description: 'DeepSeek dark palette (default)' },
19
+ { id: 'light', label: 'light', description: 'light palette for bright terminals' },
20
+ { id: 'auto', label: 'auto', description: 'follow the terminal; dark until detection lands' },
21
+ ]
22
+
23
+ /**
24
+ * The /theme list: one row per theme, the current one marked with ●, the
25
+ * focused one with ›. Enter applies the focused theme (the runner persists
26
+ * it), Esc/q closes without changing anything. Colors read the ACTIVE
27
+ * palette, so the panel itself adapts to a light theme once applied.
28
+ */
29
+ export function ThemePanel({ current, select, close }: {
30
+ /** Theme name in force (the requested name; 'auto' included). */
31
+ current: ThemeName
32
+ /** Accept one theme name: applied immediately and persisted by the runner. */
33
+ select(name: ThemeName): void
34
+ /** Close without changing the theme. */
35
+ close(): void
36
+ }): ReactElement {
37
+ const [cursor, setCursor] = useState(() => {
38
+ const index = THEME_ROWS.findIndex(theme => theme.id === current)
39
+ return index < 0 ? 0 : index
40
+ })
41
+ const stdout = useStdout().stdout
42
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
43
+ useInput((input, key) => {
44
+ if (key.escape || input === 'q') return close()
45
+ if (key.upArrow) return setCursor(value => (value + THEME_ROWS.length - 1) % THEME_ROWS.length)
46
+ if (key.downArrow) return setCursor(value => (value + 1) % THEME_ROWS.length)
47
+ if (key.return) return select(THEME_ROWS[cursor]!.id)
48
+ })
49
+ if (viewport.maxHeight === 0) return createElement(Box, { display: 'none' })
50
+ if (viewport.compact) {
51
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('/theme · esc close', viewport.contentColumns))
52
+ }
53
+ return createElement(
54
+ Box,
55
+ { width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(getPalette().dim), flexDirection: 'column', paddingX: 1 },
56
+ createElement(Text, { color: inkColor(getPalette().brandBright), wrap: 'truncate-end' }, truncateColumns('/theme — color palette', viewport.contentColumns)),
57
+ ...THEME_ROWS.map((theme, index) => {
58
+ const selected = index === cursor
59
+ const active = theme.id === current
60
+ return createElement(
61
+ Text,
62
+ {
63
+ key: theme.id,
64
+ color: selected ? inkColor(getPalette().brandBright) : undefined,
65
+ wrap: 'truncate-end',
66
+ },
67
+ truncateColumns(`${selected ? '› ' : ' '}${active ? '● ' : '○ '}${theme.label}${active ? ' · current' : ''} · ${theme.description}`, viewport.contentColumns),
68
+ )
69
+ }),
70
+ createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns('↑↓ choose · enter apply · esc/q close', viewport.contentColumns)),
71
+ )
72
+ }
package/src/theme.ts CHANGED
@@ -1,70 +1,206 @@
1
- /**
2
- * Terminal color tokens for the dsh TUI, mapped from the product design
3
- * platform's DeepSeek palette
4
- * (`packages/client/ui-theme/src/styles/design-platform.css`). Truecolor RGB
5
- * rides chalk, which degrades automatically on terminals without truecolor.
6
- *
7
- * @module @deepseek-ai/dsh-tui/theme
8
- */
9
-
10
- import chalk from 'chalk'
11
-
12
- /**
13
- * RGB triples for the TUI, one entry per design-platform token in use.
14
- * Keep names and values in sync with the CSS custom properties cited inline.
15
- */
16
- export const TUI_RGB = {
17
- /** Primary brand blue — `--dsw-static-deepseek-500`. */
18
- brand: [65, 118, 230],
19
- /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
20
- brandBright: [103, 158, 254],
21
- /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
22
- brandDeep: [72, 104, 178],
23
- /** Muted caption gray `--dsw-static-neutral-bluish-600`. */
24
- dim: [129, 133, 140],
25
- /** Success green — `--dsw-static-green-500`. */
26
- success: [34, 197, 94],
27
- /** Error red — `--dsw-static-red-500`. */
28
- error: [239, 68, 68],
29
- /** Warning amber — `--dsw-static-amber-500`. */
30
- warn: [245, 158, 11],
31
- /** Default foreground text — `--dsw-static-neutral-50`. */
32
- text: [236, 240, 246],
33
- /** Inline/fenced code — soft sky blue, distinct from brand accents. */
34
- code: [125, 211, 252],
35
- } as const satisfies Record<string, readonly [number, number, number]>
36
-
37
- /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
38
- export function brand(text: string): string {
39
- return chalk.rgb(...TUI_RGB.brand)(text)
40
- }
41
-
42
- /** Paint with the bright brand blue: streaming output and active spinners. */
43
- export function brandBright(text: string): string {
44
- return chalk.rgb(...TUI_RGB.brandBright)(text)
45
- }
46
-
47
- /** Paint with the deep brand blue: borders and secondary chrome. */
48
- export function brandDeep(text: string): string {
49
- return chalk.rgb(...TUI_RGB.brandDeep)(text)
50
- }
51
-
52
- /** Paint muted captions, hints, and meta lines. */
53
- export function dim(text: string): string {
54
- return chalk.rgb(...TUI_RGB.dim)(text)
55
- }
56
-
57
- /** Paint completed tool results and confirmations. */
58
- export function success(text: string): string {
59
- return chalk.rgb(...TUI_RGB.success)(text)
60
- }
61
-
62
- /** Paint failures and error entries. */
63
- export function error(text: string): string {
64
- return chalk.rgb(...TUI_RGB.error)(text)
65
- }
66
-
67
- /** Paint warnings. */
68
- export function warn(text: string): string {
69
- return chalk.rgb(...TUI_RGB.warn)(text)
70
- }
1
+ /**
2
+ * Terminal color tokens for the dsh TUI, mapped from the product design
3
+ * platform's DeepSeek palette
4
+ * (`packages/client/ui-theme/src/styles/design-platform.css`). Truecolor RGB
5
+ * rides chalk, which degrades automatically on terminals without truecolor.
6
+ *
7
+ * Two palettes — `dark` (the default) and `light` — share the same token keys
8
+ * with different values. Painters and the palette accessor read the ACTIVE
9
+ * palette selected through {@link setTheme}, so a theme switch recolors every
10
+ * painted surface on the next render without touching call sites. Raw token
11
+ * consumers keep reading {@link TUI_RGB} (the dark values) until the
12
+ * theme-aware integration replaces those call sites with
13
+ * `inkColor(getPalette().token)`.
14
+ *
15
+ * @module @deepseek-ai/dsh-tui/theme
16
+ */
17
+
18
+ import chalk from 'chalk'
19
+
20
+ /** One RGB triple for a palette token. */
21
+ export type RgbTriple = readonly [number, number, number]
22
+
23
+ /** Palette token keys shared by every theme. */
24
+ export type ThemeToken =
25
+ | 'brand'
26
+ | 'brandBright'
27
+ | 'brandMid'
28
+ | 'brandDeep'
29
+ | 'dim'
30
+ | 'success'
31
+ | 'error'
32
+ | 'warn'
33
+ | 'text'
34
+ | 'code'
35
+
36
+ /** One full color palette: every token key mapped to an RGB triple. */
37
+ export type ThemePalette = Readonly<Record<ThemeToken, RgbTriple>>
38
+
39
+ /** Selectable theme names: dark, light, or auto (terminal-sensed). */
40
+ export type ThemeName = 'dark' | 'light' | 'auto'
41
+
42
+ /** Valid theme names in canonical picker order. */
43
+ export const THEME_NAMES: readonly ThemeName[] = ['dark', 'light', 'auto']
44
+
45
+ /**
46
+ * DeepSeek dark palette: the original TUI colors, one entry per
47
+ * design-platform token in use. Keep names and values in sync with the CSS
48
+ * custom properties cited inline.
49
+ */
50
+ export const DARK_PALETTE = {
51
+ /** Primary brand blue — `--dsw-static-deepseek-500`. */
52
+ brand: [65, 118, 230],
53
+ /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
54
+ brandBright: [103, 158, 254],
55
+ /** Intermediate brand blue between brand and brandBright — `--dsw-static-deepseek-450`. */
56
+ brandMid: [86, 134, 254],
57
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
58
+ brandDeep: [72, 104, 178],
59
+ /** Muted caption gray — `--dsw-static-neutral-bluish-600`. */
60
+ dim: [129, 133, 140],
61
+ /** Success green — `--dsw-static-green-500`. */
62
+ success: [34, 197, 94],
63
+ /** Error red `--dsw-static-red-500`. */
64
+ error: [239, 68, 68],
65
+ /** Warning amber — `--dsw-static-amber-500`. */
66
+ warn: [245, 158, 11],
67
+ /** Default foreground text — `--dsw-static-neutral-50`. */
68
+ text: [236, 240, 246],
69
+ /** Inline/fenced code — soft sky blue, distinct from brand accents. */
70
+ code: [125, 211, 252],
71
+ } as const satisfies ThemePalette
72
+
73
+ /**
74
+ * Light palette tuned for white terminals: the same token keys as dark with
75
+ * contrast-driven values (AA on a white background). Brand keeps its dark
76
+ * value (≈4.9:1); the bright/mid/deep blues, muted grays, and status colors
77
+ * deepen so they stay legible on bright backgrounds.
78
+ */
79
+ export const LIGHT_PALETTE = {
80
+ /** Primary brand blue — unchanged, ≈4.9:1 AA on white. */
81
+ brand: [65, 118, 230],
82
+ /** Brighter brand blue deepened for white backgrounds (was 2.7:1). */
83
+ brandBright: [72, 104, 178],
84
+ /** Intermediate brand blue — Tailwind blue-500. */
85
+ brandMid: [59, 130, 246],
86
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-700`. */
87
+ brandDeep: [47, 76, 143],
88
+ /** Muted caption gray deepened for white backgrounds. */
89
+ dim: [101, 103, 107],
90
+ /** Success green — Tailwind green-700. */
91
+ success: [21, 128, 61],
92
+ /** Error red — Tailwind red-600. */
93
+ error: [236, 19, 19],
94
+ /** Warning amber — Tailwind amber-700. */
95
+ warn: [180, 83, 9],
96
+ /** Default foreground text — near-black. */
97
+ text: [21, 21, 23],
98
+ /** Inline/fenced code — Tailwind cyan-700, distinct from brand accents. */
99
+ code: [14, 116, 144],
100
+ } as const satisfies ThemePalette
101
+
102
+ /** Every palette by theme name; auto resolves through {@link resolveTheme}. */
103
+ export const PALETTES = {
104
+ dark: DARK_PALETTE,
105
+ light: LIGHT_PALETTE,
106
+ } as const satisfies Record<Exclude<ThemeName, 'auto'>, ThemePalette>
107
+
108
+ /**
109
+ * The dark palette under its original name: call sites that predate the
110
+ * two-palette switch keep compiling and painting identically (the default
111
+ * theme IS dark). New code should read the active palette through
112
+ * {@link getPalette} so a theme switch reaches it.
113
+ */
114
+ export const TUI_RGB = DARK_PALETTE
115
+
116
+ /** The theme name in force (the requested name; 'auto' included). */
117
+ let activeName: ThemeName = 'dark'
118
+
119
+ /** The palette painters and {@link getPalette} read for the active theme. */
120
+ let activePalette: ThemePalette = DARK_PALETTE
121
+
122
+ /**
123
+ * Resolve a theme name to the palette actually in use. `auto` detection
124
+ * (OSC 11 terminal background query) is a later enhancement; until it lands,
125
+ * auto falls back to the dark palette.
126
+ * @param name - the requested theme name.
127
+ * @returns 'dark' or 'light' — the palette key to paint with.
128
+ */
129
+ export function resolveTheme(name: ThemeName): 'dark' | 'light' {
130
+ return name === 'light' ? 'light' : 'dark'
131
+ }
132
+
133
+ /**
134
+ * Switch the active theme: painters and {@link getPalette} reflect the new
135
+ * palette from the next render onward. The default is dark, so a process
136
+ * that never calls this paints exactly as before.
137
+ * @param name - the theme to activate ('auto' resolves to dark for now).
138
+ */
139
+ export function setTheme(name: ThemeName): void {
140
+ activeName = name
141
+ activePalette = PALETTES[resolveTheme(name)]
142
+ }
143
+
144
+ /**
145
+ * The theme name in force. Returns the requested name ('auto' included) so
146
+ * the /theme picker and persistence can round-trip the user's choice; the
147
+ * palette actually used is {@link getPalette}.
148
+ */
149
+ export function getTheme(): ThemeName {
150
+ return activeName
151
+ }
152
+
153
+ /** The palette in force; theme-aware call sites read colors through it. */
154
+ export function getPalette(): ThemePalette {
155
+ return activePalette
156
+ }
157
+
158
+ /**
159
+ * Parse a persisted theme name: only 'light' and 'auto' survive; anything
160
+ * else (missing, corrupt, or unknown) falls back to the dark default.
161
+ * @param value - the raw parsed JSON value (expected string).
162
+ * @returns a valid theme name.
163
+ */
164
+ export function parseThemeName(value: unknown): ThemeName {
165
+ return value === 'light' || value === 'auto' ? value : 'dark'
166
+ }
167
+
168
+ /** Ink `color` string for one RGB triple. */
169
+ export function inkColor(triple: RgbTriple): string {
170
+ return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`
171
+ }
172
+
173
+ /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
174
+ export function brand(text: string): string {
175
+ return chalk.rgb(...activePalette.brand)(text)
176
+ }
177
+
178
+ /** Paint with the bright brand blue: streaming output and active spinners. */
179
+ export function brandBright(text: string): string {
180
+ return chalk.rgb(...activePalette.brandBright)(text)
181
+ }
182
+
183
+ /** Paint with the deep brand blue: borders and secondary chrome. */
184
+ export function brandDeep(text: string): string {
185
+ return chalk.rgb(...activePalette.brandDeep)(text)
186
+ }
187
+
188
+ /** Paint muted captions, hints, and meta lines. */
189
+ export function dim(text: string): string {
190
+ return chalk.rgb(...activePalette.dim)(text)
191
+ }
192
+
193
+ /** Paint completed tool results and confirmations. */
194
+ export function success(text: string): string {
195
+ return chalk.rgb(...activePalette.success)(text)
196
+ }
197
+
198
+ /** Paint failures and error entries. */
199
+ export function error(text: string): string {
200
+ return chalk.rgb(...activePalette.error)(text)
201
+ }
202
+
203
+ /** Paint warnings. */
204
+ export function warn(text: string): string {
205
+ return chalk.rgb(...activePalette.warn)(text)
206
+ }