dsh-code 0.9.1 → 1.0.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.
Files changed (59) hide show
  1. package/README.en.md +29 -13
  2. package/README.md +264 -248
  3. package/bin/deepseek.mjs +100 -6
  4. package/cordis.patch.yml +29 -1
  5. package/lib/index.mjs +2223 -687
  6. package/lib/startup.mjs +21 -11
  7. package/lib/{theme-BEi4i_aN.mjs → theme-DCT8Y2xf.mjs} +13 -9
  8. package/lib/types/app.d.ts +66 -14
  9. package/lib/types/attachments.d.ts +7 -0
  10. package/lib/types/editor.d.ts +6 -0
  11. package/lib/types/fork.d.ts +8 -0
  12. package/lib/types/git-workflow.d.ts +23 -0
  13. package/lib/types/index.d.ts +6 -0
  14. package/lib/types/kernel-panels.d.ts +39 -0
  15. package/lib/types/keyboard.d.ts +43 -0
  16. package/lib/types/mentions.d.ts +28 -38
  17. package/lib/types/presets.d.ts +1 -3
  18. package/lib/types/provider-settings.d.ts +16 -0
  19. package/lib/types/render/animations.d.ts +10 -39
  20. package/lib/types/render/editor.d.ts +137 -0
  21. package/lib/types/render/export.d.ts +1 -1
  22. package/lib/types/render/lines.d.ts +6 -2
  23. package/lib/types/render/markdown.d.ts +3 -1
  24. package/lib/types/render/projection.d.ts +29 -3
  25. package/lib/types/render/status.d.ts +5 -12
  26. package/lib/types/session-directory.d.ts +1 -3
  27. package/lib/types/startup.d.ts +14 -11
  28. package/lib/types/store.d.ts +11 -9
  29. package/lib/types/subagents.d.ts +3 -3
  30. package/lib/types/theme.d.ts +14 -1
  31. package/lib/types/version.d.ts +15 -2
  32. package/package.json +153 -141
  33. package/src/app.ts +4455 -3917
  34. package/src/attachments.ts +44 -0
  35. package/src/editor.ts +51 -0
  36. package/src/fork.ts +31 -0
  37. package/src/git-workflow.ts +87 -0
  38. package/src/index.ts +1510 -1374
  39. package/src/internals.ts +14 -1
  40. package/src/kernel-panels.ts +914 -798
  41. package/src/keyboard.ts +125 -0
  42. package/src/mentions.ts +72 -117
  43. package/src/presets.ts +1 -4
  44. package/src/provider-settings.ts +94 -0
  45. package/src/render/animations.ts +25 -55
  46. package/src/render/editor.ts +398 -0
  47. package/src/render/export.ts +79 -79
  48. package/src/render/lines.ts +342 -236
  49. package/src/render/markdown.ts +99 -26
  50. package/src/render/projection.ts +102 -19
  51. package/src/render/status.ts +713 -650
  52. package/src/render/text.ts +150 -150
  53. package/src/render/tool-detail.ts +3 -1
  54. package/src/session-directory.ts +3 -3
  55. package/src/startup.ts +136 -119
  56. package/src/store.ts +23 -11
  57. package/src/subagents.ts +13 -5
  58. package/src/theme.ts +214 -206
  59. package/src/version.ts +58 -1
package/src/store.ts CHANGED
@@ -5,15 +5,17 @@
5
5
  *
6
6
  * Notification coalescing: the fold stays synchronous — `getView()` always
7
7
  * returns the latest state the moment `apply` returns — but listener
8
- * notification is scheduled on a microtask and deduplicated, so N events
9
- * delivered inside one synchronous drain (the zai/GLM adapter drains its
10
- * token buffer in sub-millisecond bursts) produce ONE React re-render.
11
- * Synchronous per-event notification instead cascades one
12
- * `useSyncExternalStore` force-update per token inside a single flush; the
13
- * reconciler counts those as nested passive updates and floods React's
14
- * "Maximum update depth exceeded" warning past 50 events, besides rendering
15
- * the whole live tree once per token. A microtask keeps latency within the
16
- * same macrotask, before Ink's throttled paint.
8
+ * notification is frame-throttled (~16ms) and deduplicated. The zai/GLM
9
+ * adapter delivers tokens as a sustained stream of sub-millisecond,
10
+ * microtask-spaced bursts: per-burst notification renders at microtask
11
+ * cadence, which chained SyncLane `useSyncExternalStore` rerenders past
12
+ * React's nested-update limit ("Maximum update depth exceeded"), while a
13
+ * bare `setImmediate` merges a whole macrotask turn's bursts into one
14
+ * chunky repaint (streaming text visibly staggers). The frame budget gives
15
+ * both: an event ≥16ms after the last paint notifies via `setImmediate`
16
+ * (sub-millisecond latency for sparse/first tokens), and anything denser
17
+ * defers to the next 16ms boundary — a 60fps render cap that also breaks
18
+ * the nesting chain by construction.
17
19
  *
18
20
  * @module @deepseek-ai/dsh-tui/store
19
21
  */
@@ -21,6 +23,9 @@
21
23
  import type { SessionEvent } from '@deepseek-ai/dsh-session'
22
24
  import { createTranscriptView, projectEvent, projectEvents, type TranscriptView } from './render/projection.ts'
23
25
 
26
+ /** Render frame budget: the notification cadence's upper bound. */
27
+ const NOTIFY_FRAME_MS = 16
28
+
24
29
  /** The externally readable, event-fed transcript store for one session. */
25
30
  export interface TranscriptStore {
26
31
  /** The current view; the same object identity until an event changes it. */
@@ -47,15 +52,22 @@ export function createTranscriptStore(replay?: readonly SessionEvent[]): Transcr
47
52
  let view = replay === undefined ? createTranscriptView() : projectEvents(replay)
48
53
  const listeners = new Set<() => void>()
49
54
  let scheduled = false
55
+ let lastNotifyAt = 0
50
56
  const notify = (): void => {
51
57
  if (scheduled) return
52
58
  scheduled = true
53
- queueMicrotask(() => {
59
+ const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt)
60
+ const dispatch = (): void => {
54
61
  scheduled = false
62
+ lastNotifyAt = Date.now()
55
63
  for (const listener of listeners) {
56
64
  listener()
57
65
  }
58
- })
66
+ }
67
+ // Sparse streams paint with setImmediate latency; a denser burst defers
68
+ // to the next frame boundary instead of repainting per microtask batch.
69
+ if (wait <= 0) setImmediate(dispatch)
70
+ else setTimeout(dispatch, wait)
59
71
  }
60
72
  return {
61
73
  getView: () => view,
package/src/subagents.ts CHANGED
@@ -9,9 +9,9 @@
9
9
  * running state, bounded last-activity text), capped at
10
10
  * {@link MAX_SUBAGENT_ROWS}. Rows are advisory display state, rebuilt from
11
11
  * live events; nothing here persists or replays. Notification is coalesced
12
- * to one microtask per delivery burst, mirroring the transcript store's
13
- * contract (per-token synchronous notify once cascaded past React's nested
14
- * update limit on the GLM thinking path).
12
+ * by the same ~16ms frame throttle as the transcript store (per-burst
13
+ * microtask notify chained SyncLane rerenders past React's nested update
14
+ * limit; a bare macrotask merge repaints a whole turn's bursts at once).
15
15
  *
16
16
  * @module @deepseek-ai/dsh-code/subagents
17
17
  */
@@ -21,6 +21,9 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
21
21
  /** Hard row cap: a fan-out larger than this stays summarized by the head. */
22
22
  export const MAX_SUBAGENT_ROWS = 8
23
23
 
24
+ /** Render frame budget: the notification cadence's upper bound. */
25
+ const NOTIFY_FRAME_MS = 16
26
+
24
27
  /** Bounded last-activity text (plain characters, display-sliced later). */
25
28
  const MAX_ACTIVITY_CHARS = 80
26
29
 
@@ -129,13 +132,18 @@ export function createSubagentFeed(): SubagentFeedView & {
129
132
  let rows: readonly SubagentRow[] = Object.freeze([])
130
133
  const listeners = new Set<() => void>()
131
134
  let scheduled = false
135
+ let lastNotifyAt = 0
132
136
  const notify = (): void => {
133
137
  if (scheduled) return
134
138
  scheduled = true
135
- queueMicrotask(() => {
139
+ const wait = NOTIFY_FRAME_MS - (Date.now() - lastNotifyAt)
140
+ const dispatch = (): void => {
136
141
  scheduled = false
142
+ lastNotifyAt = Date.now()
137
143
  for (const listener of listeners) listener()
138
- })
144
+ }
145
+ if (wait <= 0) setImmediate(dispatch)
146
+ else setTimeout(dispatch, wait)
139
147
  }
140
148
  return {
141
149
  apply(sessionId: string, event: SessionEvent): void {
package/src/theme.ts CHANGED
@@ -1,206 +1,214 @@
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
- }
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
+ | 'composerBand'
36
+
37
+ /** One full color palette: every token key mapped to an RGB triple. */
38
+ export type ThemePalette = Readonly<Record<ThemeToken, RgbTriple>>
39
+
40
+ /** Selectable theme names: dark, light, or auto (terminal-sensed). */
41
+ export type ThemeName = 'dark' | 'light' | 'auto'
42
+
43
+ /** Valid theme names in canonical picker order. */
44
+ export const THEME_NAMES: readonly ThemeName[] = ['dark', 'light', 'auto']
45
+
46
+ /**
47
+ * DeepSeek dark palette: the original TUI colors, one entry per
48
+ * design-platform token in use. Keep names and values in sync with the CSS
49
+ * custom properties cited inline.
50
+ */
51
+ export const DARK_PALETTE = {
52
+ /** Primary brand blue `--dsw-static-deepseek-500`. */
53
+ brand: [65, 118, 230],
54
+ /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
55
+ brandBright: [103, 158, 254],
56
+ /** Intermediate brand blue between brand and brandBright — `--dsw-static-deepseek-450`. */
57
+ brandMid: [86, 134, 254],
58
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
59
+ brandDeep: [72, 104, 178],
60
+ /** Muted caption gray — `--dsw-static-neutral-bluish-600`. */
61
+ dim: [129, 133, 140],
62
+ /** Success green — `--dsw-static-green-500`. */
63
+ success: [34, 197, 94],
64
+ /** Error red — `--dsw-static-red-500`. */
65
+ error: [239, 68, 68],
66
+ /** Warning amber — `--dsw-static-amber-500`. */
67
+ warn: [245, 158, 11],
68
+ /** Default foreground text `--dsw-static-neutral-50`. */
69
+ text: [236, 240, 246],
70
+ /** Inline/fenced code soft sky blue, distinct from brand accents. */
71
+ code: [125, 211, 252],
72
+ /** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
73
+ composerBand: [46, 48, 52],
74
+ } as const satisfies ThemePalette
75
+
76
+ /**
77
+ * Light palette tuned for white terminals: the same token keys as dark with
78
+ * contrast-driven values (AA on a white background). Brand keeps its dark
79
+ * value (≈4.9:1); the bright/mid/deep blues, muted grays, and status colors
80
+ * deepen so they stay legible on bright backgrounds.
81
+ */
82
+ export const LIGHT_PALETTE = {
83
+ /** Primary brand blue — unchanged, ≈4.9:1 AA on white. */
84
+ brand: [65, 118, 230],
85
+ /** Brighter brand blue deepened for white backgrounds (was 2.7:1). */
86
+ brandBright: [72, 104, 178],
87
+ /** Intermediate brand blue — Tailwind blue-500. */
88
+ brandMid: [59, 130, 246],
89
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-700`. */
90
+ brandDeep: [47, 76, 143],
91
+ /** Muted caption gray deepened for white backgrounds. */
92
+ dim: [101, 103, 107],
93
+ /** Success green — Tailwind green-700. */
94
+ success: [21, 128, 61],
95
+ /** Error red — Tailwind red-600. */
96
+ error: [236, 19, 19],
97
+ /** Warning amber — Tailwind amber-700. */
98
+ warn: [180, 83, 9],
99
+ /** Default foreground text — near-black. */
100
+ text: [21, 21, 23],
101
+ /** Inline/fenced code — Tailwind cyan-700, distinct from brand accents. */
102
+ code: [14, 116, 144],
103
+ /** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
104
+ composerBand: [229, 231, 235],
105
+ } as const satisfies ThemePalette
106
+
107
+ /** Every palette by theme name; auto resolves through {@link resolveTheme}. */
108
+ export const PALETTES = {
109
+ dark: DARK_PALETTE,
110
+ light: LIGHT_PALETTE,
111
+ } as const satisfies Record<Exclude<ThemeName, 'auto'>, ThemePalette>
112
+
113
+ /**
114
+ * The dark palette under its original name: call sites that predate the
115
+ * two-palette switch keep compiling and painting identically (the default
116
+ * theme IS dark). New code should read the active palette through
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.
121
+ */
122
+ export const TUI_RGB = DARK_PALETTE
123
+
124
+ /** The theme name in force (the requested name; 'auto' included). */
125
+ let activeName: ThemeName = 'dark'
126
+
127
+ /** The palette painters and {@link getPalette} read for the active theme. */
128
+ let activePalette: ThemePalette = DARK_PALETTE
129
+
130
+ /**
131
+ * Resolve a theme name to the palette actually in use. `auto` detection
132
+ * (OSC 11 terminal background query) is a later enhancement; until it lands,
133
+ * auto falls back to the dark palette.
134
+ * @param name - the requested theme name.
135
+ * @returns 'dark' or 'light' the palette key to paint with.
136
+ */
137
+ export function resolveTheme(name: ThemeName): 'dark' | 'light' {
138
+ return name === 'light' ? 'light' : 'dark'
139
+ }
140
+
141
+ /**
142
+ * Switch the active theme: painters and {@link getPalette} reflect the new
143
+ * palette from the next render onward. The default is dark, so a process
144
+ * that never calls this paints exactly as before.
145
+ * @param name - the theme to activate ('auto' resolves to dark for now).
146
+ */
147
+ export function setTheme(name: ThemeName): void {
148
+ activeName = name
149
+ activePalette = PALETTES[resolveTheme(name)]
150
+ }
151
+
152
+ /**
153
+ * The theme name in force. Returns the requested name ('auto' included) so
154
+ * the /theme picker and persistence can round-trip the user's choice; the
155
+ * palette actually used is {@link getPalette}.
156
+ */
157
+ export function getTheme(): ThemeName {
158
+ return activeName
159
+ }
160
+
161
+ /** The palette in force; theme-aware call sites read colors through it. */
162
+ export function getPalette(): ThemePalette {
163
+ return activePalette
164
+ }
165
+
166
+ /**
167
+ * Parse a persisted theme name: only 'light' and 'auto' survive; anything
168
+ * else (missing, corrupt, or unknown) falls back to the dark default.
169
+ * @param value - the raw parsed JSON value (expected string).
170
+ * @returns a valid theme name.
171
+ */
172
+ export function parseThemeName(value: unknown): ThemeName {
173
+ return value === 'light' || value === 'auto' ? value : 'dark'
174
+ }
175
+
176
+ /** Ink `color` string for one RGB triple. */
177
+ export function inkColor(triple: RgbTriple): string {
178
+ return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`
179
+ }
180
+
181
+ /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
182
+ export function brand(text: string): string {
183
+ return chalk.rgb(...activePalette.brand)(text)
184
+ }
185
+
186
+ /** Paint with the bright brand blue: streaming output and active spinners. */
187
+ export function brandBright(text: string): string {
188
+ return chalk.rgb(...activePalette.brandBright)(text)
189
+ }
190
+
191
+ /** Paint with the deep brand blue: borders and secondary chrome. */
192
+ export function brandDeep(text: string): string {
193
+ return chalk.rgb(...activePalette.brandDeep)(text)
194
+ }
195
+
196
+ /** Paint muted captions, hints, and meta lines. */
197
+ export function dim(text: string): string {
198
+ return chalk.rgb(...activePalette.dim)(text)
199
+ }
200
+
201
+ /** Paint completed tool results and confirmations. */
202
+ export function success(text: string): string {
203
+ return chalk.rgb(...activePalette.success)(text)
204
+ }
205
+
206
+ /** Paint failures and error entries. */
207
+ export function error(text: string): string {
208
+ return chalk.rgb(...activePalette.error)(text)
209
+ }
210
+
211
+ /** Paint warnings. */
212
+ export function warn(text: string): string {
213
+ return chalk.rgb(...activePalette.warn)(text)
214
+ }
package/src/version.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /** Installed dsh-code version exposed by the terminal header. */
2
2
 
3
3
  import { readFileSync } from 'node:fs'
4
+ import { dirname, join, resolve } from 'node:path'
4
5
 
5
6
  /** Read one package manifest version without making terminal startup depend on it. */
6
- export function readPackageVersion(manifest = new URL('../package.json', import.meta.url)): string {
7
+ function readPackageVersion(manifest = new URL('../package.json', import.meta.url)): string {
7
8
  try {
8
9
  const parsed = JSON.parse(readFileSync(manifest, 'utf8')) as { version?: unknown }
9
10
  return typeof parsed.version === 'string' && parsed.version.length > 0 ? parsed.version : '0.0.0'
@@ -14,3 +15,59 @@ export function readPackageVersion(manifest = new URL('../package.json', import.
14
15
 
15
16
  /** Version of the installed dsh-code package. */
16
17
  export const DSH_CODE_VERSION = readPackageVersion()
18
+
19
+ /** The harness host package: the dsh CLI whose process runs the TUI plugin. */
20
+ const DSH_HOST_PACKAGE_NAME = '@deepseek-ai/dsh'
21
+
22
+ /** Parent levels above the host entry allowed to hold its package manifest. */
23
+ const DSH_HOST_WALK_LIMIT = 4
24
+
25
+ /**
26
+ * Resolve the running dsh CLI host's version from its entry file
27
+ * (`process.argv[1]`, e.g. `.../@deepseek-ai/dsh/lib/bin.js`). Only a manifest
28
+ * literally named `@deepseek-ai/dsh` counts, so an unrelated entry (vitest, a
29
+ * plain node script) resolves to undefined instead of faking a kernel version.
30
+ */
31
+ export function resolveDshHostVersion(entry: string | undefined = process.argv[1]): string | undefined {
32
+ if (entry === undefined || entry === '') return undefined
33
+ let directory = dirname(resolve(entry))
34
+ for (let depth = 0; depth < DSH_HOST_WALK_LIMIT; depth += 1) {
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as {
37
+ name?: unknown
38
+ version?: unknown
39
+ }
40
+ if (parsed.name === DSH_HOST_PACKAGE_NAME && typeof parsed.version === 'string' && parsed.version.length > 0) {
41
+ return parsed.version
42
+ }
43
+ } catch {
44
+ // No readable manifest at this level: keep climbing.
45
+ }
46
+ const parent = dirname(directory)
47
+ if (parent === directory) return undefined
48
+ directory = parent
49
+ }
50
+ return undefined
51
+ }
52
+
53
+ let cachedDshKernelVersion: string | undefined
54
+ let dshKernelVersionResolved = false
55
+
56
+ /**
57
+ * The dsh kernel version the TUI runs on, memoized after the first probe: the
58
+ * host process never changes within a run, and the header reads this on every
59
+ * Static replay.
60
+ */
61
+ export function dshKernelVersion(): string | undefined {
62
+ if (!dshKernelVersionResolved) {
63
+ cachedDshKernelVersion = resolveDshHostVersion()
64
+ dshKernelVersionResolved = true
65
+ }
66
+ return cachedDshKernelVersion
67
+ }
68
+
69
+ /** Test-only: forget the memoized kernel version so a new argv can be probed. */
70
+ export function _resetDshKernelVersionForTests(): void {
71
+ cachedDshKernelVersion = undefined
72
+ dshKernelVersionResolved = false
73
+ }