dsh-code 1.0.7 → 1.2.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 +70 -24
- package/README.md +71 -25
- package/bin/deepseek.mjs +202 -39
- package/cordis.patch.yml +13 -4
- package/lib/index.mjs +4063 -844
- package/lib/session-query.mjs +3 -2
- package/lib/startup.mjs +4 -4
- package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
- package/lib/types/app.d.ts +100 -63
- package/lib/types/authorization-panel.d.ts +3 -3
- package/lib/types/git-workflow.d.ts +91 -2
- package/lib/types/i18n.d.ts +39 -0
- package/lib/types/index.d.ts +73 -1
- package/lib/types/input-split.d.ts +1 -1
- package/lib/types/kernel-panels.d.ts +86 -31
- package/lib/types/language-panel.d.ts +12 -0
- package/lib/types/locales/en.d.ts +450 -0
- package/lib/types/locales/zh.d.ts +9 -0
- package/lib/types/mentions.d.ts +7 -3
- package/lib/types/models.d.ts +14 -0
- package/lib/types/panel-accent.d.ts +28 -0
- package/lib/types/rainbow.d.ts +69 -0
- package/lib/types/render/animations.d.ts +42 -0
- package/lib/types/render/inspector.d.ts +26 -0
- package/lib/types/render/lines.d.ts +21 -1
- package/lib/types/render/markdown.d.ts +1 -1
- package/lib/types/render/projection.d.ts +95 -4
- package/lib/types/render/status.d.ts +8 -8
- package/lib/types/render/text.d.ts +6 -0
- package/lib/types/render/usage.d.ts +113 -0
- package/lib/types/session-directory.d.ts +17 -0
- package/lib/types/startup.d.ts +1 -1
- package/lib/types/terminal-title.d.ts +8 -0
- package/lib/types/theme-panel.d.ts +2 -2
- package/lib/types/theme.d.ts +271 -52
- package/lib/types/update-panel.d.ts +5 -5
- package/lib/types/update.d.ts +10 -1
- package/lib/types/version.d.ts +4 -3
- package/package.json +24 -7
- package/src/app.ts +1155 -478
- package/src/approval.ts +166 -166
- package/src/authorization-panel.ts +19 -16
- package/src/editor-keys.ts +371 -371
- package/src/git-workflow.ts +229 -3
- package/src/i18n.ts +68 -0
- package/src/index.ts +412 -76
- package/src/input-split.ts +3 -3
- package/src/kernel-panels.ts +471 -89
- package/src/keyboard.ts +5 -4
- package/src/language-panel.ts +53 -0
- package/src/locales/en.ts +489 -0
- package/src/locales/zh.ts +488 -0
- package/src/mentions.ts +8 -4
- package/src/models.ts +264 -212
- package/src/panel-accent.ts +41 -0
- package/src/presets.ts +1 -1
- package/src/provider-settings.ts +1 -1
- package/src/rainbow.ts +208 -0
- package/src/render/animations.ts +104 -6
- package/src/render/editor.ts +20 -20
- package/src/render/export.ts +116 -95
- package/src/render/inspector.ts +42 -0
- package/src/render/lines.ts +628 -415
- package/src/render/markdown.ts +15 -3
- package/src/render/projection.ts +429 -19
- package/src/render/status.ts +41 -35
- package/src/render/text.ts +14 -0
- package/src/render/tool-preview.ts +77 -77
- package/src/render/usage.ts +430 -0
- package/src/render/width.ts +2 -2
- package/src/session-directory.ts +8 -6
- package/src/session-query.ts +8 -4
- package/src/startup.ts +3 -3
- package/src/subagents.ts +229 -229
- package/src/terminal-title.ts +22 -5
- package/src/theme-panel.ts +17 -21
- package/src/theme.ts +281 -33
- package/src/update-panel.ts +37 -27
- package/src/update.ts +19 -3
- package/src/version.ts +58 -20
- package/src/whale-glyph.ts +23 -23
package/src/theme.ts
CHANGED
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
* (`packages/client/ui-theme/src/styles/design-platform.css`). Truecolor RGB
|
|
5
5
|
* rides chalk, which degrades automatically on terminals without truecolor.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Three palettes — `dark` (the default), `light`, and `prismatic` (the
|
|
8
|
+
* neon synthwave skin) — share the same token keys with different values.
|
|
9
|
+
* Painters and the palette accessor read the ACTIVE
|
|
9
10
|
* palette selected through {@link setTheme}, so a theme switch recolors every
|
|
10
|
-
* painted surface on the next render without touching call sites
|
|
11
|
-
*
|
|
12
|
-
* theme-aware integration replaces those call sites with
|
|
13
|
-
* `inkColor(getPalette().token)`.
|
|
11
|
+
* painted surface on the next render without touching call sites; consumers
|
|
12
|
+
* read colors through {@link getPalette} or the painters below only.
|
|
14
13
|
*
|
|
15
14
|
* @module @deepseek-ai/dsh-tui/theme
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import chalk from 'chalk'
|
|
19
18
|
|
|
19
|
+
import { rainbowRoll } from './rainbow.ts'
|
|
20
|
+
|
|
20
21
|
/** One RGB triple for a palette token. */
|
|
21
22
|
export type RgbTriple = readonly [number, number, number]
|
|
22
23
|
|
|
@@ -33,15 +34,46 @@ export type ThemeToken =
|
|
|
33
34
|
| 'text'
|
|
34
35
|
| 'code'
|
|
35
36
|
| 'composerBand'
|
|
37
|
+
| 'diffAdd'
|
|
38
|
+
| 'diffDel'
|
|
39
|
+
| 'diffAddFg'
|
|
40
|
+
| 'diffDelFg'
|
|
41
|
+
| 'surface'
|
|
42
|
+
| 'prompt'
|
|
43
|
+
| 'queued'
|
|
44
|
+
| 'steered'
|
|
36
45
|
|
|
37
46
|
/** One full color palette: every token key mapped to an RGB triple. */
|
|
38
47
|
export type ThemePalette = Readonly<Record<ThemeToken, RgbTriple>>
|
|
39
48
|
|
|
40
|
-
/** Selectable theme names: dark, light, or auto (terminal-sensed). */
|
|
41
|
-
export type ThemeName = 'dark' | 'light' | 'auto'
|
|
49
|
+
/** Selectable theme names: dark, light, prismatic, rainbow, or auto (terminal-sensed). */
|
|
50
|
+
export type ThemeName = 'dark' | 'light' | 'prismatic' | 'rainbow' | 'auto'
|
|
42
51
|
|
|
43
52
|
/** Valid theme names in canonical picker order. */
|
|
44
|
-
export const THEME_NAMES: readonly ThemeName[] = ['dark', 'light', 'auto']
|
|
53
|
+
export const THEME_NAMES: readonly ThemeName[] = ['dark', 'light', 'prismatic', 'rainbow', 'auto']
|
|
54
|
+
|
|
55
|
+
/** One /theme picker row: the theme id plus its display copy. */
|
|
56
|
+
export interface ThemeDescriptor {
|
|
57
|
+
/** The selectable theme name. */
|
|
58
|
+
readonly id: ThemeName
|
|
59
|
+
/** Short row label shown in the picker. */
|
|
60
|
+
readonly label: string
|
|
61
|
+
/** One-line description shown after the label. */
|
|
62
|
+
readonly description: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every theme's picker metadata in canonical order — the single source the
|
|
67
|
+
* /theme panel rows derive from; {@link THEME_NAMES} stays the validation
|
|
68
|
+
* list. Adding a theme means adding its palette plus one row here.
|
|
69
|
+
*/
|
|
70
|
+
export const THEMES: readonly ThemeDescriptor[] = [
|
|
71
|
+
{ id: 'dark', label: 'dark', description: 'DeepSeek dark palette (default)' },
|
|
72
|
+
{ id: 'light', label: 'light', description: 'light palette for bright terminals' },
|
|
73
|
+
{ id: 'prismatic', label: 'prismatic', description: 'neon synthwave palette (violet/magenta/cyan)' },
|
|
74
|
+
{ id: 'rainbow', label: 'rainbow', description: 'randomized carnival palette (/rainbow to reroll)' },
|
|
75
|
+
{ id: 'auto', label: 'auto', description: 'follow the terminal; dark until detection lands' },
|
|
76
|
+
]
|
|
45
77
|
|
|
46
78
|
/**
|
|
47
79
|
* DeepSeek dark palette: the original TUI colors, one entry per
|
|
@@ -71,16 +103,35 @@ export const DARK_PALETTE = {
|
|
|
71
103
|
code: [125, 211, 252],
|
|
72
104
|
/** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
|
|
73
105
|
composerBand: [46, 48, 52],
|
|
106
|
+
/** Diff added-line background — Codex's muted dark green tint (#213A2B). */
|
|
107
|
+
diffAdd: [33, 58, 43],
|
|
108
|
+
/** Diff removed-line background — Codex's muted dark red tint (#4A221D). */
|
|
109
|
+
diffDel: [74, 34, 29],
|
|
110
|
+
/** Diff added-line foreground — green-500, 5.4:1 on the diffAdd tint. */
|
|
111
|
+
diffAddFg: [34, 197, 94],
|
|
112
|
+
/** Diff removed-line foreground — red-400, 4.9:1 on the diffDel tint (error's red-500 sinks to 3.6:1 on it). */
|
|
113
|
+
diffDelFg: [248, 113, 113],
|
|
114
|
+
/** Terminal background this palette is designed against; row tints blend toward it. */
|
|
115
|
+
surface: [0, 0, 0],
|
|
116
|
+
/**
|
|
117
|
+
* The three prompt-row colors, one per delivery kind. They are the palette's
|
|
118
|
+
* own accents (bright brand, warning amber, violet) rather than fixed RGBs,
|
|
119
|
+
* and every row's bar is these colors laid over {@link surface}.
|
|
120
|
+
*/
|
|
121
|
+
prompt: [103, 158, 254],
|
|
122
|
+
queued: [245, 158, 11],
|
|
123
|
+
steered: [167, 139, 250],
|
|
74
124
|
} as const satisfies ThemePalette
|
|
75
125
|
|
|
76
126
|
/**
|
|
77
127
|
* Light palette tuned for white terminals: the same token keys as dark with
|
|
78
128
|
* contrast-driven values (AA on a white background). Brand keeps its dark
|
|
79
|
-
* value (≈4.
|
|
80
|
-
*
|
|
129
|
+
* value (≈4.2:1 on white — reserved for bold/accent spans; body-size brand
|
|
130
|
+
* text uses brandBright at ≈5.4:1); the bright/mid/deep blues, muted grays,
|
|
131
|
+
* and status colors deepen so they stay legible on bright backgrounds.
|
|
81
132
|
*/
|
|
82
133
|
export const LIGHT_PALETTE = {
|
|
83
|
-
/** Primary brand blue — unchanged
|
|
134
|
+
/** Primary brand blue — unchanged design-platform value; ≈4.2:1 on white, so bold/accent spans only (body-size brand text uses brandBright). */
|
|
84
135
|
brand: [65, 118, 230],
|
|
85
136
|
/** Brighter brand blue deepened for white backgrounds (was 2.7:1). */
|
|
86
137
|
brandBright: [72, 104, 178],
|
|
@@ -102,24 +153,84 @@ export const LIGHT_PALETTE = {
|
|
|
102
153
|
code: [14, 116, 144],
|
|
103
154
|
/** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
|
|
104
155
|
composerBand: [229, 231, 235],
|
|
156
|
+
/** Diff added-line background — GitHub's pastel green (#dafbe1), Codex's light pick. */
|
|
157
|
+
diffAdd: [218, 251, 225],
|
|
158
|
+
/** Diff removed-line background — GitHub's pastel red (#ffebe9), Codex's light pick. */
|
|
159
|
+
diffDel: [255, 235, 233],
|
|
160
|
+
/** Diff added-line foreground — green-800, 6.4:1 on the pastel tint (green-700 clears 4.5:1 by a hair). */
|
|
161
|
+
diffAddFg: [22, 101, 52],
|
|
162
|
+
/** Diff removed-line foreground — red-700, 5.6:1 on the pastel tint (error's red-600 is 3.9:1). */
|
|
163
|
+
diffDelFg: [185, 28, 28],
|
|
164
|
+
/** Terminal background this palette is designed against; row tints blend toward it. */
|
|
165
|
+
surface: [255, 255, 255],
|
|
166
|
+
/**
|
|
167
|
+
* Prompt-row colors deepened for white: the amber and violet the dark theme
|
|
168
|
+
* uses sit at the AA edge on their own pastel bars, so each kind carries a
|
|
169
|
+
* value that clears 4.5:1 both on the bar and on plain white.
|
|
170
|
+
*/
|
|
171
|
+
prompt: [47, 76, 143],
|
|
172
|
+
queued: [124, 53, 10],
|
|
173
|
+
steered: [109, 40, 217],
|
|
105
174
|
} as const satisfies ThemePalette
|
|
106
175
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Prismatic palette — the neon synthwave skin (docs/prismatic-theme-design.md):
|
|
178
|
+
* electric violet replaces the brand blues, neon fuchsia carries live
|
|
179
|
+
* emphasis, neon cyan paints code, and a lavender gray dims captions. Paints
|
|
180
|
+
* on a black terminal; every value meets the same WCAG thresholds as the
|
|
181
|
+
* other palettes (body tokens ≥4.5:1, accents ≥3:1 on pure black). Semantic
|
|
182
|
+
* colors — success/error/warn and the four diff tokens — reuse the dark
|
|
183
|
+
* values verbatim so green still means added and red still means removed.
|
|
184
|
+
*/
|
|
185
|
+
export const PRISMATIC_PALETTE = {
|
|
186
|
+
/** Primary electric purple — violet-500 (#8B5CF6), 5.0:1 on black. */
|
|
187
|
+
brand: [139, 92, 246],
|
|
188
|
+
/** Live/streaming emphasis — neon fuchsia-300 (#F0ABFC), 11.9:1 on black. */
|
|
189
|
+
brandBright: [240, 171, 252],
|
|
190
|
+
/** Intermediate violet — violet-400 (#A78BFA), 7.7:1 on black. */
|
|
191
|
+
brandMid: [167, 139, 250],
|
|
192
|
+
/** Deep secondary chrome — violet-600 (#7C3AED), 3.7:1 on black (violet-700 misses 3:1). */
|
|
193
|
+
brandDeep: [124, 58, 237],
|
|
194
|
+
/** Muted captions — lavender gray (#A6A3B8), 8.6:1 on black. */
|
|
195
|
+
dim: [166, 163, 184],
|
|
196
|
+
/** Success green — unchanged from dark. */
|
|
197
|
+
success: [34, 197, 94],
|
|
198
|
+
/** Error red — unchanged from dark. */
|
|
199
|
+
error: [239, 68, 68],
|
|
200
|
+
/** Warning amber — unchanged from dark. */
|
|
201
|
+
warn: [245, 158, 11],
|
|
202
|
+
/** Default foreground text — white with a lavender cast (#F0EEF9). */
|
|
203
|
+
text: [240, 238, 249],
|
|
204
|
+
/** Inline/fenced code — neon cyan-300 (#67E8F9), 14.5:1 on black. */
|
|
205
|
+
code: [103, 232, 249],
|
|
206
|
+
/** Composer three-row band base — neutral dark gray, hue-free so wave tints read on it. */
|
|
207
|
+
composerBand: [46, 46, 52],
|
|
208
|
+
/** Diff added-line background — unchanged from dark. */
|
|
209
|
+
diffAdd: [33, 58, 43],
|
|
210
|
+
/** Diff removed-line background — unchanged from dark. */
|
|
211
|
+
diffDel: [74, 34, 29],
|
|
212
|
+
/** Diff added-line foreground — unchanged from dark. */
|
|
213
|
+
diffAddFg: [34, 197, 94],
|
|
214
|
+
/** Diff removed-line foreground — unchanged from dark. */
|
|
215
|
+
diffDelFg: [248, 113, 113],
|
|
216
|
+
/** Terminal background this palette is designed against; row tints blend toward it. */
|
|
217
|
+
surface: [0, 0, 0],
|
|
218
|
+
/** Prompt rows in the skin's own neon: fuchsia, amber, cyan. */
|
|
219
|
+
prompt: [240, 171, 252],
|
|
220
|
+
queued: [245, 158, 11],
|
|
221
|
+
steered: [103, 232, 249],
|
|
222
|
+
} as const satisfies ThemePalette
|
|
112
223
|
|
|
113
224
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* {@link getPalette} so a theme switch reaches it.
|
|
118
|
-
*
|
|
119
|
-
* @deprecated Read the active palette through {@link getPalette}; this
|
|
120
|
-
* compatibility alias is removed in the next minor release.
|
|
225
|
+
* Every STATIC palette by theme name; auto resolves through {@link resolveTheme}
|
|
226
|
+
* and rainbow is rolled per launch ({@link rainbowRoll}), so neither lives
|
|
227
|
+
* here — {@link setTheme} resolves both through the same key space.
|
|
121
228
|
*/
|
|
122
|
-
export const
|
|
229
|
+
export const PALETTES = {
|
|
230
|
+
dark: DARK_PALETTE,
|
|
231
|
+
light: LIGHT_PALETTE,
|
|
232
|
+
prismatic: PRISMATIC_PALETTE,
|
|
233
|
+
} as const satisfies Record<Exclude<ThemeName, 'auto' | 'rainbow'>, ThemePalette>
|
|
123
234
|
|
|
124
235
|
/** The theme name in force (the requested name; 'auto' included). */
|
|
125
236
|
let activeName: ThemeName = 'dark'
|
|
@@ -130,12 +241,13 @@ let activePalette: ThemePalette = DARK_PALETTE
|
|
|
130
241
|
/**
|
|
131
242
|
* Resolve a theme name to the palette actually in use. `auto` detection
|
|
132
243
|
* (OSC 11 terminal background query) is a later enhancement; until it lands,
|
|
133
|
-
* auto falls back to the dark palette
|
|
244
|
+
* auto falls back to the dark palette (prismatic and rainbow are always
|
|
245
|
+
* explicit choices, never auto-resolved).
|
|
134
246
|
* @param name - the requested theme name.
|
|
135
|
-
* @returns 'dark' or '
|
|
247
|
+
* @returns 'dark', 'light', 'prismatic', or 'rainbow' — the palette key.
|
|
136
248
|
*/
|
|
137
|
-
export function resolveTheme(name: ThemeName): 'dark' | 'light' {
|
|
138
|
-
return name === 'light' ? 'light' : 'dark'
|
|
249
|
+
export function resolveTheme(name: ThemeName): 'dark' | 'light' | 'prismatic' | 'rainbow' {
|
|
250
|
+
return name === 'light' ? 'light' : name === 'prismatic' ? 'prismatic' : name === 'rainbow' ? 'rainbow' : 'dark'
|
|
139
251
|
}
|
|
140
252
|
|
|
141
253
|
/**
|
|
@@ -146,7 +258,8 @@ export function resolveTheme(name: ThemeName): 'dark' | 'light' {
|
|
|
146
258
|
*/
|
|
147
259
|
export function setTheme(name: ThemeName): void {
|
|
148
260
|
activeName = name
|
|
149
|
-
|
|
261
|
+
const resolved = resolveTheme(name)
|
|
262
|
+
activePalette = resolved === 'rainbow' ? rainbowRoll().palette : PALETTES[resolved]
|
|
150
263
|
}
|
|
151
264
|
|
|
152
265
|
/**
|
|
@@ -164,13 +277,15 @@ export function getPalette(): ThemePalette {
|
|
|
164
277
|
}
|
|
165
278
|
|
|
166
279
|
/**
|
|
167
|
-
* Parse a persisted theme name: only
|
|
168
|
-
* else (missing, corrupt, or unknown) falls back to the dark default.
|
|
280
|
+
* Parse a persisted theme name: only names in {@link THEME_NAMES} survive;
|
|
281
|
+
* anything else (missing, corrupt, or unknown) falls back to the dark default.
|
|
169
282
|
* @param value - the raw parsed JSON value (expected string).
|
|
170
283
|
* @returns a valid theme name.
|
|
171
284
|
*/
|
|
172
285
|
export function parseThemeName(value: unknown): ThemeName {
|
|
173
|
-
return value === '
|
|
286
|
+
return typeof value === 'string' && (THEME_NAMES as readonly string[]).includes(value)
|
|
287
|
+
? (value as ThemeName)
|
|
288
|
+
: 'dark'
|
|
174
289
|
}
|
|
175
290
|
|
|
176
291
|
/** Ink `color` string for one RGB triple. */
|
|
@@ -178,6 +293,139 @@ export function inkColor(triple: RgbTriple): string {
|
|
|
178
293
|
return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`
|
|
179
294
|
}
|
|
180
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Diff-line background as an Ink `backgroundColor` string, theme-aware and
|
|
298
|
+
* depth-gated (the Codex diff renderer's rule): 16-color terminals paint
|
|
299
|
+
* backgrounds from the saturated system palette, which drowns the text, so
|
|
300
|
+
* they keep the foreground-only look; 256-color and truecolor terminals get
|
|
301
|
+
* the palette's tint (chalk quantizes the RGB form down automatically).
|
|
302
|
+
* @param token - which diff background, added or removed lines.
|
|
303
|
+
* @returns the Ink background color, or undefined to paint foreground-only.
|
|
304
|
+
*/
|
|
305
|
+
export function diffBackground(token: 'diffAdd' | 'diffDel'): string | undefined {
|
|
306
|
+
if (chalk.level < 2) return undefined
|
|
307
|
+
return inkColor(activePalette[token])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** One prompt-row color: the palette token whose color paints the row. */
|
|
311
|
+
export interface PromptRowTokens {
|
|
312
|
+
readonly fg: 'prompt' | 'queued' | 'steered'
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** How far one prompt-row tint leans toward its color, over the surface. */
|
|
316
|
+
const PROMPT_ROW_TINT = 0.22
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The color family one prompt row wears, chosen by how it was delivered: the
|
|
320
|
+
* ordinary brand accent, the warning amber for a queued prompt, and the
|
|
321
|
+
* palette's own third accent for a steered one. The bar behind it is derived
|
|
322
|
+
* from that same color, so every theme — including a freshly rolled rainbow —
|
|
323
|
+
* carries its own look instead of a fixed triple.
|
|
324
|
+
* @param delivery - how the prompt was delivered; undefined is an ordinary one.
|
|
325
|
+
* @returns the palette token whose color paints the row.
|
|
326
|
+
*/
|
|
327
|
+
export function promptRowTokens(delivery: 'queued' | 'steered' | undefined): PromptRowTokens {
|
|
328
|
+
if (delivery === 'queued') return { fg: 'queued' }
|
|
329
|
+
if (delivery === 'steered') return { fg: 'steered' }
|
|
330
|
+
return { fg: 'prompt' }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Blended prompt-row background for one row color: the palette color laid
|
|
335
|
+
* over the surface the palette is designed for (dark themes over black, light
|
|
336
|
+
* over white), depth-gated exactly like {@link diffBackground} — a 16-color
|
|
337
|
+
* terminal keeps the foreground-only look rather than painting a saturated
|
|
338
|
+
* system background behind the text.
|
|
339
|
+
* @param color - the row's palette token.
|
|
340
|
+
* @returns the Ink background color, or undefined to paint foreground-only.
|
|
341
|
+
*/
|
|
342
|
+
export function rowBackground(color: PromptRowTokens['fg']): string | undefined {
|
|
343
|
+
if (chalk.level < 2) return undefined
|
|
344
|
+
const surface = activePalette.surface
|
|
345
|
+
const tint = activePalette[color]
|
|
346
|
+
const blend = (index: number): number =>
|
|
347
|
+
Math.round(surface[index] + (tint[index] - surface[index]) * PROMPT_ROW_TINT)
|
|
348
|
+
return inkColor([blend(0), blend(1), blend(2)] as RgbTriple)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The prismatic surface ring: four neon accents panels cycle through by
|
|
353
|
+
* mount order (cyan → fuchsia → violet → lime), all ≥3:1 on black. Dark and
|
|
354
|
+
* light ignore the ring — {@link surfaceAccent} falls back to the passed-in
|
|
355
|
+
* base color so those themes stay pixel-identical.
|
|
356
|
+
*/
|
|
357
|
+
export const ACCENT_RING: readonly RgbTriple[] = [
|
|
358
|
+
[34, 211, 238],
|
|
359
|
+
[232, 121, 249],
|
|
360
|
+
[167, 139, 250],
|
|
361
|
+
[163, 230, 53],
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The prismatic flow anchors: brand violet → neon fuchsia → neon cyan. All
|
|
366
|
+
* three clear 4.5:1 on black, so a flowing foreground stays AA throughout
|
|
367
|
+
* the oscillation (render/animations.ts walks the triangle).
|
|
368
|
+
*/
|
|
369
|
+
export const FLOW_ANCHORS: readonly RgbTriple[] = [
|
|
370
|
+
[139, 92, 246],
|
|
371
|
+
[232, 121, 249],
|
|
372
|
+
[34, 211, 238],
|
|
373
|
+
]
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Whether the active theme is prismatic (the neon skin with surface rings
|
|
377
|
+
* and flowing accents).
|
|
378
|
+
*/
|
|
379
|
+
export function isPrismatic(): boolean {
|
|
380
|
+
return activeName === 'prismatic'
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Whether the active theme is rainbow (the per-launch carnival roll). */
|
|
384
|
+
export function isRainbow(): boolean {
|
|
385
|
+
return activeName === 'rainbow'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** The active theme's flow walk, if it has one (anchors plus phase offset). */
|
|
389
|
+
export interface ThemeFlow {
|
|
390
|
+
/** Anchor colors walked in order over one 2.4s lap. */
|
|
391
|
+
readonly anchors: readonly RgbTriple[]
|
|
392
|
+
/** Phase offset into the lap, milliseconds (0 for prismatic). */
|
|
393
|
+
readonly phaseMs: number
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The flowing theme's anchor walk: prismatic rides the fixed violet→fuchsia→
|
|
398
|
+
* cyan triangle; rainbow rides its rolled full-spectrum anchors with a random
|
|
399
|
+
* phase; dark and light have no flow (undefined) and keep static accents.
|
|
400
|
+
*/
|
|
401
|
+
export function themeFlow(): ThemeFlow | undefined {
|
|
402
|
+
if (isPrismatic()) return { anchors: FLOW_ANCHORS, phaseMs: 0 }
|
|
403
|
+
if (isRainbow()) {
|
|
404
|
+
const roll = rainbowRoll()
|
|
405
|
+
return { anchors: roll.flowAnchors, phaseMs: roll.flowPhaseMs }
|
|
406
|
+
}
|
|
407
|
+
return undefined
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The accent for one surface slot: prismatic rotates the {@link ACCENT_RING}
|
|
412
|
+
* by slot index; every other theme gets the caller's base color back.
|
|
413
|
+
* @param index - the slot's position in the mount-order sequence.
|
|
414
|
+
* @param base - the color to use outside prismatic (the surface's usual one).
|
|
415
|
+
* @returns the accent to paint the surface's border/title with.
|
|
416
|
+
*/
|
|
417
|
+
export function surfaceAccent(index: number, base: RgbTriple): RgbTriple {
|
|
418
|
+
if (isPrismatic()) {
|
|
419
|
+
const ring = ACCENT_RING
|
|
420
|
+
return ring[((index % ring.length) + ring.length) % ring.length]
|
|
421
|
+
}
|
|
422
|
+
if (isRainbow()) {
|
|
423
|
+
const ring = rainbowRoll().ring
|
|
424
|
+
return ring[((index % ring.length) + ring.length) % ring.length]
|
|
425
|
+
}
|
|
426
|
+
return base
|
|
427
|
+
}
|
|
428
|
+
|
|
181
429
|
/** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
|
|
182
430
|
export function brand(text: string): string {
|
|
183
431
|
return chalk.rgb(...activePalette.brand)(text)
|
package/src/update-panel.ts
CHANGED
|
@@ -14,7 +14,9 @@ import { Box, Text, useInput, useStdout } from 'ink'
|
|
|
14
14
|
import type { LauncherUpdateStatus } from './update.ts'
|
|
15
15
|
import { clampScroll, panelViewport } from './render/inspector.ts'
|
|
16
16
|
import { singleLineText, truncateColumns } from './render/text.ts'
|
|
17
|
+
import { panelAccent } from './panel-accent.ts'
|
|
17
18
|
import { getPalette, inkColor } from './theme.ts'
|
|
19
|
+
import { t } from './i18n.ts'
|
|
18
20
|
|
|
19
21
|
/** Retained apply-progress lines (ring tail; npm output is ephemeral). */
|
|
20
22
|
export const UPDATE_OUTPUT_CAP = 800
|
|
@@ -41,9 +43,11 @@ export interface UpdatePlanView {
|
|
|
41
43
|
export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
42
44
|
const rows: UpdateRow[] = []
|
|
43
45
|
const latest = status.code.latest ?? 'unknown'
|
|
44
|
-
rows.push(status.
|
|
45
|
-
? { key: 'code', text: `dsh-code ${status.code.running} (latest)` }
|
|
46
|
-
:
|
|
46
|
+
rows.push(status.aheadOfRegistry === true
|
|
47
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (newer than npm ${latest})` }
|
|
48
|
+
: status.code.latest !== null && status.code.latest === status.code.running
|
|
49
|
+
? { key: 'code', text: `dsh-code ${status.code.running} (latest)` }
|
|
50
|
+
: { key: 'code', text: `dsh-code ${status.code.running} → ${latest}` })
|
|
47
51
|
if (status.host.targetLine === null) {
|
|
48
52
|
rows.push({ key: 'host', text: 'harness pinned line unreadable — update would install @deepseek-ai/dsh@latest', tone: 'warn' })
|
|
49
53
|
} else if (status.host.installed === null) {
|
|
@@ -83,7 +87,9 @@ export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
|
83
87
|
rows.push({ key: `blocker:checkout:${index}`, text: line, tone: 'error' })
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
if (status.
|
|
90
|
+
if (status.aheadOfRegistry === true) {
|
|
91
|
+
rows.push({ key: 'ahead', text: 'this install is newer than npm latest — refusing to downgrade', tone: 'warn' })
|
|
92
|
+
} else if (status.upToDate) {
|
|
87
93
|
rows.push({ key: 'uptodate', text: 'everything is already on the pinned line — nothing to update', tone: 'ok' })
|
|
88
94
|
}
|
|
89
95
|
const runnable = status.upToDate !== true
|
|
@@ -97,13 +103,14 @@ export function updatePlanView(status: LauncherUpdateStatus): UpdatePlanView {
|
|
|
97
103
|
export type UpdatePhase = 'probe' | 'error' | 'plan' | 'apply' | 'done'
|
|
98
104
|
|
|
99
105
|
/** Footer hint line per phase; the plan phase names the confirm key only when runnable. */
|
|
100
|
-
export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean): string {
|
|
101
|
-
if (phase === 'probe') return '
|
|
102
|
-
if (phase === 'error') return '
|
|
103
|
-
if (phase === 'apply') return '
|
|
104
|
-
if (phase === 'done') return '
|
|
105
|
-
if (
|
|
106
|
-
|
|
106
|
+
export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: boolean, aheadOfRegistry = false): string {
|
|
107
|
+
if (phase === 'probe') return t('panel.update.footer.probe')
|
|
108
|
+
if (phase === 'error') return t('panel.update.footer.error')
|
|
109
|
+
if (phase === 'apply') return t('panel.update.footer.apply')
|
|
110
|
+
if (phase === 'done') return t('panel.update.footer.error')
|
|
111
|
+
if (aheadOfRegistry) return t('panel.update.footer.ahead')
|
|
112
|
+
if (upToDate) return t('panel.update.footer.current')
|
|
113
|
+
return runnable ? t('panel.update.footer.update') : t('panel.update.footer.blocked')
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
@@ -114,13 +121,13 @@ export function updateFooter(phase: UpdatePhase, runnable: boolean, upToDate: bo
|
|
|
114
121
|
*/
|
|
115
122
|
export function UpdatePanel({ probe, apply, close, notify }: {
|
|
116
123
|
/** Read-only probe of the launcher update status (never installs). */
|
|
117
|
-
probe()
|
|
124
|
+
probe: () => Promise<LauncherUpdateStatus>
|
|
118
125
|
/** Run the aligned update; streams sanitized progress lines. */
|
|
119
|
-
apply(onLine: (line: string) => void
|
|
126
|
+
apply: (onLine: (line: string) => void, plan: LauncherUpdateStatus['plan']) => Promise<number>
|
|
120
127
|
/** Close the panel (App keeps ownership of the flag). */
|
|
121
|
-
close()
|
|
128
|
+
close: () => void
|
|
122
129
|
/** One bounded cross-surface notice (phase completions). */
|
|
123
|
-
notify(text: string, tone?: 'info' | 'warning' | 'error')
|
|
130
|
+
notify: (text: string, tone?: 'info' | 'warning' | 'error') => void
|
|
124
131
|
}): ReactElement {
|
|
125
132
|
const [phase, setPhase] = useState<UpdatePhase>('probe')
|
|
126
133
|
const [status, setStatus] = useState<LauncherUpdateStatus>()
|
|
@@ -161,7 +168,7 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
161
168
|
setAnchor('tail')
|
|
162
169
|
apply(line => {
|
|
163
170
|
setLines(previous => clipUpdateLines([...previous, singleLineText(line)]))
|
|
164
|
-
}).then(code => {
|
|
171
|
+
}, status.plan).then(code => {
|
|
165
172
|
setExit(code)
|
|
166
173
|
setPhase('done')
|
|
167
174
|
notify(code === 0 ? 'update installed — restart dsh to activate' : `update failed (exit ${code})`, code === 0 ? 'info' : 'error')
|
|
@@ -212,12 +219,12 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
212
219
|
else if (key.downArrow) setAnchor(offset + 1)
|
|
213
220
|
})
|
|
214
221
|
if (viewport.maxHeight === 0 || viewport.compact) {
|
|
215
|
-
const summary = phase === 'probe' ? 'checking
|
|
216
|
-
: phase === 'error' ? '
|
|
217
|
-
: phase === 'apply' ? singleLineText(lines[lines.length - 1] ?? 'updating
|
|
218
|
-
: phase === 'done' ? (exit === 0 ? 'installed
|
|
219
|
-
: status?.
|
|
220
|
-
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(
|
|
222
|
+
const summary = phase === 'probe' ? t('panel.update.checking')
|
|
223
|
+
: phase === 'error' ? t('panel.update.probeFailed')
|
|
224
|
+
: phase === 'apply' ? singleLineText(lines[lines.length - 1] ?? t('panel.update.updating'))
|
|
225
|
+
: phase === 'done' ? (exit === 0 ? t('panel.update.installed') : t('panel.update.failed'))
|
|
226
|
+
: status?.aheadOfRegistry === true ? t('panel.update.aheadOfNpm') : status?.upToDate === true ? t('panel.update.upToDate') : planView?.runnable === true ? t('panel.update.enter') : t('panel.update.blocked')
|
|
227
|
+
return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(singleLineText(t('panel.update.compact', { summary })), viewport.contentColumns))
|
|
221
228
|
}
|
|
222
229
|
const visible = rows.slice(offset, offset + budget)
|
|
223
230
|
const toneColor = (tone: UpdateRow['tone']): ReturnType<typeof inkColor> | undefined => tone === 'ok'
|
|
@@ -230,17 +237,20 @@ export function UpdatePanel({ probe, apply, close, notify }: {
|
|
|
230
237
|
? inkColor(getPalette().dim)
|
|
231
238
|
: undefined
|
|
232
239
|
const title = status === undefined
|
|
233
|
-
? '
|
|
234
|
-
:
|
|
240
|
+
? t('panel.update.title')
|
|
241
|
+
: status.aheadOfRegistry === true
|
|
242
|
+
? `/update · dsh-code ${status.code.running}`
|
|
243
|
+
: `/update · dsh-code ${status.code.running}${status.code.latest !== null && status.code.latest !== status.code.running ? ` → ${status.code.latest}` : ''}`
|
|
244
|
+
const accent = panelAccent('update', getPalette().dim, getPalette().brandBright)
|
|
235
245
|
return createElement(
|
|
236
246
|
Box,
|
|
237
|
-
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(
|
|
238
|
-
createElement(Text, { color: inkColor(
|
|
247
|
+
{ width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 },
|
|
248
|
+
createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(singleLineText(title), viewport.contentColumns)),
|
|
239
249
|
...visible.map(row => createElement(Text, {
|
|
240
250
|
key: row.key,
|
|
241
251
|
color: toneColor(row.tone),
|
|
242
252
|
wrap: 'truncate-end',
|
|
243
253
|
}, truncateColumns(` ${singleLineText(row.text)}`, viewport.contentColumns))),
|
|
244
|
-
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true)), viewport.contentColumns)),
|
|
254
|
+
createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(singleLineText(updateFooter(phase, planView?.runnable ?? false, status?.upToDate === true, status?.aheadOfRegistry === true)), viewport.contentColumns)),
|
|
245
255
|
)
|
|
246
256
|
}
|
package/src/update.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface LauncherUpdateStatus {
|
|
|
20
20
|
readonly plan: { readonly dshSpec: string; readonly codeSpec: string; readonly pluginSpecs: readonly string[] }
|
|
21
21
|
readonly blockers: { readonly registry: string | null; readonly downgrade: boolean; readonly localCheckout: readonly string[] | null }
|
|
22
22
|
readonly upToDate: boolean
|
|
23
|
+
/** True when this install is newer than npm latest (apply would downgrade). */
|
|
24
|
+
readonly aheadOfRegistry?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Exact specs the TUI confirmed; apply must install these, not re-query latest. */
|
|
28
|
+
export interface LauncherUpdatePlan {
|
|
29
|
+
readonly dshSpec: string
|
|
30
|
+
readonly codeSpec: string
|
|
31
|
+
readonly pluginSpecs: readonly string[]
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/** The launcher entrypoint that ships beside this bundle (lib/../bin). */
|
|
@@ -89,10 +98,17 @@ export async function probeLauncherUpdate(spawnProcess: SpawnLike = spawn as Spa
|
|
|
89
98
|
* exit code (0 success); rejects only when the process could not start.
|
|
90
99
|
* No timeout: an npm install may legitimately take minutes.
|
|
91
100
|
*/
|
|
92
|
-
export function
|
|
93
|
-
const
|
|
101
|
+
export function applyPlanArgs(plan: LauncherUpdatePlan): string[] {
|
|
102
|
+
const args = ['update', '--apply', '--dsh', plan.dshSpec, '--code', plan.codeSpec]
|
|
103
|
+
for (const plugin of plan.pluginSpecs) args.push('--plugin', plugin)
|
|
104
|
+
return args
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function applyLauncherUpdate(onLine: (line: string) => void, spawnProcess?: SpawnLike, plan?: LauncherUpdatePlan): Promise<number> {
|
|
108
|
+
const spawnFn = spawnProcess ?? (spawn as SpawnLike)
|
|
109
|
+
const command = launcherUpdateCommand(plan === undefined ? ['update', '--apply'] : applyPlanArgs(plan))
|
|
94
110
|
return new Promise((resolve, reject) => {
|
|
95
|
-
const child =
|
|
111
|
+
const child = spawnFn(command.command, command.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
96
112
|
const split = createLineSplitter(onLine)
|
|
97
113
|
child.stdout?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|
|
98
114
|
child.stderr?.on('data', (chunk: Buffer) => { split(chunk.toString()) })
|