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/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/index.mjs +1473 -899
- package/lib/startup.mjs +21 -9
- package/lib/theme-BEi4i_aN.mjs +624 -0
- package/lib/types/app.d.ts +31 -3
- package/lib/types/index.d.ts +1 -0
- package/lib/types/kernel-panels.d.ts +21 -0
- package/lib/types/mentions.d.ts +29 -12
- package/lib/types/models.d.ts +66 -0
- package/lib/types/render/animations.d.ts +175 -2
- package/lib/types/render/projection.d.ts +28 -0
- package/lib/types/render/status.d.ts +34 -13
- package/lib/types/startup.d.ts +12 -4
- package/lib/types/theme-panel.d.ts +24 -0
- package/lib/types/theme.d.ts +158 -2
- package/package.json +1 -1
- package/src/app.ts +510 -130
- package/src/index.ts +964 -905
- package/src/kernel-panels.ts +481 -419
- package/src/mentions.ts +57 -27
- package/src/models.ts +200 -66
- package/src/render/animations.ts +359 -2
- package/src/render/projection.ts +764 -659
- package/src/render/status.ts +744 -603
- package/src/startup.ts +119 -109
- package/src/theme-panel.ts +72 -0
- package/src/theme.ts +206 -70
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
|
|
4
|
-
* {@link TUI_STARTUP_SERVICE} for the runner to consume lazily.
|
|
5
|
-
* headless bundle's startup shape (a commander action publishing
|
|
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
|
-
* -
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
export const
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
export const
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| { readonly kind: '
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
readonly
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
code
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|