@toddzheng024/dscode-bundle 0.7.11 → 0.7.13

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 (154) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/i18n/messages.mjs +6 -6
  5. package/plugins/session-metrics/view.mjs +16 -5
  6. package/plugins/tui-tools/index.mjs +14 -1
  7. package/vendor/tui/lib/app.mjs +6674 -0
  8. package/vendor/tui/lib/approval.mjs +122 -0
  9. package/vendor/tui/lib/attachments.mjs +218 -0
  10. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  11. package/vendor/tui/lib/authorization.mjs +111 -0
  12. package/vendor/tui/lib/commands.mjs +123 -0
  13. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  14. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  15. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  16. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  17. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  18. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  19. package/vendor/tui/lib/editor-keys.mjs +346 -0
  20. package/vendor/tui/lib/editor.mjs +50 -0
  21. package/vendor/tui/lib/fork.mjs +24 -0
  22. package/vendor/tui/lib/git-workflow.mjs +278 -0
  23. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  24. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  25. package/vendor/tui/lib/index.mjs +2198 -0
  26. package/vendor/tui/lib/input-split.mjs +170 -0
  27. package/vendor/tui/lib/internals.mjs +90 -0
  28. package/vendor/tui/lib/invariant.mjs +22 -0
  29. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  30. package/vendor/tui/lib/keyboard.mjs +283 -0
  31. package/vendor/tui/lib/language-panel.mjs +41 -0
  32. package/vendor/tui/lib/locales/en.mjs +495 -0
  33. package/vendor/tui/lib/locales/zh.mjs +495 -0
  34. package/vendor/tui/lib/mentions.mjs +150 -0
  35. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  36. package/vendor/tui/lib/models.mjs +215 -0
  37. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  38. package/vendor/tui/lib/permissions.mjs +45 -0
  39. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  40. package/vendor/tui/lib/presets.mjs +46 -0
  41. package/vendor/tui/lib/provider-settings.mjs +600 -0
  42. package/vendor/tui/lib/questions.mjs +116 -0
  43. package/vendor/tui/lib/rainbow.mjs +181 -0
  44. package/vendor/tui/lib/render/animations.mjs +665 -0
  45. package/vendor/tui/lib/render/editor.mjs +437 -0
  46. package/vendor/tui/lib/render/export.mjs +119 -0
  47. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  48. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  49. package/vendor/tui/lib/render/inspector.mjs +93 -0
  50. package/vendor/tui/lib/render/lines.mjs +538 -0
  51. package/vendor/tui/lib/render/markdown.mjs +592 -0
  52. package/vendor/tui/lib/render/projection.mjs +1862 -0
  53. package/vendor/tui/lib/render/status.mjs +591 -0
  54. package/vendor/tui/lib/render/text.mjs +157 -0
  55. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  56. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  57. package/vendor/tui/lib/render/usage.mjs +336 -0
  58. package/vendor/tui/lib/render/width.mjs +191 -0
  59. package/vendor/tui/lib/session-directory.mjs +329 -0
  60. package/vendor/tui/lib/session-query.mjs +184 -0
  61. package/vendor/tui/lib/session-switch.mjs +51 -0
  62. package/vendor/tui/lib/settings-file.mjs +75 -0
  63. package/vendor/tui/lib/skills.mjs +119 -0
  64. package/vendor/tui/lib/startup.mjs +111 -0
  65. package/vendor/tui/lib/store.mjs +129 -0
  66. package/vendor/tui/lib/subagents.mjs +200 -0
  67. package/vendor/tui/lib/terminal-title.mjs +186 -0
  68. package/vendor/tui/lib/theme-panel.mjs +60 -0
  69. package/vendor/tui/lib/theme.mjs +379 -0
  70. package/vendor/tui/lib/update-panel.mjs +239 -0
  71. package/vendor/tui/lib/update.mjs +101 -0
  72. package/vendor/tui/lib/version.mjs +105 -0
  73. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  74. package/vendor/tui/LICENSE +0 -21
  75. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  76. package/vendor/tui/dscode-email/cli.mjs +0 -32
  77. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  78. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  79. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  80. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  81. package/vendor/tui/dscode-email/imap.mjs +0 -136
  82. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  83. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  84. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  85. package/vendor/tui/dscode-email/store.mjs +0 -32
  86. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  87. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  88. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  89. package/vendor/tui/index.mjs +0 -44483
  90. package/vendor/tui/invariant.mjs +0 -21
  91. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  92. package/vendor/tui/session-query.mjs +0 -150
  93. package/vendor/tui/startup.mjs +0 -109
  94. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  95. package/vendor/tui/types/app.d.ts +0 -348
  96. package/vendor/tui/types/approval.d.ts +0 -59
  97. package/vendor/tui/types/attachments.d.ts +0 -52
  98. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  99. package/vendor/tui/types/authorization.d.ts +0 -36
  100. package/vendor/tui/types/commands.d.ts +0 -52
  101. package/vendor/tui/types/editor-keys.d.ts +0 -105
  102. package/vendor/tui/types/editor.d.ts +0 -6
  103. package/vendor/tui/types/fork.d.ts +0 -8
  104. package/vendor/tui/types/git-workflow.d.ts +0 -121
  105. package/vendor/tui/types/index.d.ts +0 -223
  106. package/vendor/tui/types/input-split.d.ts +0 -54
  107. package/vendor/tui/types/internals.d.ts +0 -26
  108. package/vendor/tui/types/invariant.d.ts +0 -15
  109. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  110. package/vendor/tui/types/keyboard.d.ts +0 -80
  111. package/vendor/tui/types/language-panel.d.ts +0 -12
  112. package/vendor/tui/types/locales/en.d.ts +0 -450
  113. package/vendor/tui/types/locales/zh.d.ts +0 -9
  114. package/vendor/tui/types/mentions.d.ts +0 -85
  115. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  116. package/vendor/tui/types/models.d.ts +0 -133
  117. package/vendor/tui/types/permissions.d.ts +0 -27
  118. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  119. package/vendor/tui/types/presets.d.ts +0 -22
  120. package/vendor/tui/types/provider-settings.d.ts +0 -239
  121. package/vendor/tui/types/questions.d.ts +0 -54
  122. package/vendor/tui/types/rainbow.d.ts +0 -69
  123. package/vendor/tui/types/render/animations.d.ts +0 -307
  124. package/vendor/tui/types/render/editor.d.ts +0 -163
  125. package/vendor/tui/types/render/export.d.ts +0 -9
  126. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  127. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  128. package/vendor/tui/types/render/inspector.d.ts +0 -62
  129. package/vendor/tui/types/render/lines.d.ts +0 -86
  130. package/vendor/tui/types/render/markdown.d.ts +0 -29
  131. package/vendor/tui/types/render/projection.d.ts +0 -587
  132. package/vendor/tui/types/render/status.d.ts +0 -196
  133. package/vendor/tui/types/render/text.d.ts +0 -64
  134. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  135. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  136. package/vendor/tui/types/render/usage.d.ts +0 -113
  137. package/vendor/tui/types/render/width.d.ts +0 -29
  138. package/vendor/tui/types/session-directory.d.ts +0 -190
  139. package/vendor/tui/types/session-query.d.ts +0 -92
  140. package/vendor/tui/types/session-switch.d.ts +0 -17
  141. package/vendor/tui/types/settings-file.d.ts +0 -41
  142. package/vendor/tui/types/skills.d.ts +0 -47
  143. package/vendor/tui/types/startup.d.ts +0 -65
  144. package/vendor/tui/types/store.d.ts +0 -58
  145. package/vendor/tui/types/subagents.d.ts +0 -70
  146. package/vendor/tui/types/terminal-title.d.ts +0 -66
  147. package/vendor/tui/types/theme-panel.d.ts +0 -24
  148. package/vendor/tui/types/theme.d.ts +0 -434
  149. package/vendor/tui/types/update-panel.d.ts +0 -49
  150. package/vendor/tui/types/update.d.ts +0 -75
  151. package/vendor/tui/types/version.d.ts +0 -19
  152. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  153. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  154. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Terminal tab/window title management for the TUI.
3
+ *
4
+ * Terminals label their tab from the window title, which an application sets
5
+ * with an OSC 0 sequence; without one the tab shows the process name ("node").
6
+ * The title text is untrusted display content (session names arrive through
7
+ * events and user input), so it is sanitized before emission: control
8
+ * characters and bidi/invisible formatting codepoints are stripped, whitespace
9
+ * runs collapse to single spaces, and the result is bounded. Clearing writes
10
+ * an empty OSC payload and the terminal falls back to its own default; the
11
+ * previously set title is not portable to read back and is never restored.
12
+ *
13
+ * @module @deepseek-ai/dsh-code/terminal-title
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+ import { useLayoutEffect, useRef } from 'react';
18
+ import { useStdout } from 'ink';
19
+ /** The host process title at module load, restored on unmount. On Windows
20
+ * this reaches the tab through SetConsoleTitleW (ConPTY reflects it to VS
21
+ * Code and friends without any user setting); on POSIX it becomes the ps
22
+ * name. Node replaces the argv memory, so the original must be saved before
23
+ * the first assignment. */
24
+ const initialProcessTitle = process.title;
25
+ /** Tab label before a session carries a name. */
26
+ export const DEFAULT_TERMINAL_TITLE = 'deepseek';
27
+ /** Practical upper bound on title length, in visible characters: long enough
28
+ * for session names, short enough for tab bars and window managers. */
29
+ export const MAX_TERMINAL_TITLE_CHARS = 240;
30
+ /** Control characters, DEL/C1 range, and bidi or invisible formatting
31
+ * codepoints that could terminate the OSC sequence or visually reorder the
32
+ * title relative to its underlying text. */
33
+ const DISALLOWED_TITLE_CHARS = /[\u0000-\u001F\u007F-\u009F\u00AD\u034F\u061C\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/;
34
+ /** Normalize untrusted title text into one bounded display line: disallowed
35
+ * codepoints dropped, whitespace runs collapsed to single spaces, leading and
36
+ * trailing whitespace removed, length bounded. */
37
+ export function sanitizeTerminalTitle(text) {
38
+ const chars = [];
39
+ let pendingSpace = false;
40
+ for (const ch of text) {
41
+ if (DISALLOWED_TITLE_CHARS.test(ch))
42
+ continue;
43
+ if (ch.trim() === '') {
44
+ if (chars.length > 0)
45
+ pendingSpace = true;
46
+ continue;
47
+ }
48
+ if (pendingSpace) {
49
+ if (chars.length + 1 >= MAX_TERMINAL_TITLE_CHARS)
50
+ break;
51
+ chars.push(' ');
52
+ pendingSpace = false;
53
+ }
54
+ if (chars.length >= MAX_TERMINAL_TITLE_CHARS)
55
+ break;
56
+ chars.push(ch);
57
+ }
58
+ return chars.join('');
59
+ }
60
+ /** Build one OSC 0 title sequence. An empty sanitized title yields an empty
61
+ * string: emitting nothing is distinct from clearing, which is a separate
62
+ * policy decision made by the caller. */
63
+ export function terminalTitleSequence(text) {
64
+ const title = sanitizeTerminalTitle(text);
65
+ return title === '' ? '' : `\x1b]0;${title}\x07`;
66
+ }
67
+ /** Clear the managed title with an empty OSC payload; the terminal falls back
68
+ * to its own default label. */
69
+ export function clearTerminalTitleSequence() {
70
+ return '\x1b]0;\x07';
71
+ }
72
+ /**
73
+ * Resolve the VS Code stable user-settings file for one platform and
74
+ * environment: `%APPDATA%` on Windows, the bundle data folder on macOS (NOT
75
+ * XDG `~/.config` — VS Code never reads that path there, so writing it
76
+ * silently no-ops and the tab kept showing the process name "node"), and
77
+ * XDG config on Linux.
78
+ */
79
+ export function vscodeSettingsPath(platform, env) {
80
+ const windows = platform === 'win32';
81
+ const darwin = platform === 'darwin';
82
+ const base = windows ? env['APPDATA'] : env['HOME'];
83
+ if (base === undefined || base === '')
84
+ return undefined;
85
+ return windows
86
+ ? join(base, 'Code', 'User', 'settings.json')
87
+ : darwin
88
+ ? join(base, 'Library', 'Application Support', 'Code', 'User', 'settings.json')
89
+ : join(base, '.config', 'Code', 'User', 'settings.json');
90
+ }
91
+ /** VS Code renders an application-set tab title only when
92
+ * "terminal.integrated.tabs.title" maps to the sequence variable; the editor
93
+ * default shows the process name instead ("node" for a Node CLI). Inside a VS
94
+ * Code integrated terminal, align the user settings once: if the key is
95
+ * absent, insert it and keep a one-shot backup of the original file. A value
96
+ * the user already set is never overwritten, an unparseable file is never
97
+ * touched, and every failure degrades to a no-op - the OSC and process-title
98
+ * channels keep working everywhere else. */
99
+ export function ensureVsCodeTabTitleSetting(options = {}) {
100
+ const env = options.env ?? process.env;
101
+ if (env['TERM_PROGRAM'] !== 'vscode')
102
+ return { wrote: false, reason: 'not-vscode' };
103
+ if ((options.isTTY ?? process.stdout.isTTY) !== true)
104
+ return { wrote: false, reason: 'not-vscode' };
105
+ let file = options.settingsFile;
106
+ if (file === undefined) {
107
+ const resolved = vscodeSettingsPath(process.platform, env);
108
+ if (resolved === undefined)
109
+ return { wrote: false, reason: 'error' };
110
+ file = resolved;
111
+ }
112
+ try {
113
+ if (!existsSync(file)) {
114
+ mkdirSync(dirname(file), { recursive: true });
115
+ writeFileSync(file, '{\n "terminal.integrated.tabs.title": "${sequence}"\n}\n', 'utf8');
116
+ return { wrote: true };
117
+ }
118
+ const text = readFileSync(file, 'utf8');
119
+ if (text.includes('"terminal.integrated.tabs.title"'))
120
+ return { wrote: false, reason: 'key-present' };
121
+ try {
122
+ JSON.parse(text);
123
+ }
124
+ catch {
125
+ return { wrote: false, reason: 'unparseable' };
126
+ }
127
+ const close = text.lastIndexOf('}');
128
+ if (close < 0)
129
+ return { wrote: false, reason: 'unparseable' };
130
+ const before = text.slice(0, close);
131
+ const trimmedBefore = before.replace(/[ \t\r\n]+$/, '');
132
+ const tail = before.slice(trimmedBefore.length);
133
+ const needsComma = trimmedBefore.trim() !== '' && !trimmedBefore.trimEnd().endsWith('{');
134
+ const inserted = (needsComma ? ',' : '') + '\n "terminal.integrated.tabs.title": "${sequence}"\n';
135
+ writeFileSync(file + '.dsh-backup', text, 'utf8');
136
+ writeFileSync(file, trimmedBefore + inserted + tail + text.slice(close), 'utf8');
137
+ return { wrote: true };
138
+ }
139
+ catch {
140
+ return { wrote: false, reason: 'error' };
141
+ }
142
+ }
143
+ /**
144
+ * Keep the terminal tab label on `title` (sanitized; empty titles leave the
145
+ * current label alone). Two delivery channels run in parallel: the OSC 0
146
+ * sequence to stdout, and the host process title. Writes are deduplicated by
147
+ * title, and on unmount the managed title is cleared and the process title
148
+ * restored so the host shell regains its default label.
149
+ */
150
+ export function useTerminalTitle(title, options = {}) {
151
+ const { clearOnUnmount = true } = options;
152
+ const { stdout } = useStdout();
153
+ const writtenRef = useRef(undefined);
154
+ const processTitleRef = useRef(false);
155
+ // Layout effects: the clear must run synchronously at unmount (passive
156
+ // cleanups are not flushed synchronously when Ink tears the tree down).
157
+ useLayoutEffect(() => {
158
+ if (stdout === undefined)
159
+ return undefined;
160
+ return () => {
161
+ if (processTitleRef.current) {
162
+ processTitleRef.current = false;
163
+ process.title = initialProcessTitle;
164
+ }
165
+ if (clearOnUnmount && writtenRef.current !== undefined)
166
+ stdout.write(clearTerminalTitleSequence());
167
+ };
168
+ }, [stdout, clearOnUnmount]);
169
+ useLayoutEffect(() => {
170
+ if (stdout === undefined)
171
+ return;
172
+ if (title === writtenRef.current)
173
+ return;
174
+ const sequence = terminalTitleSequence(title);
175
+ if (sequence === '')
176
+ return;
177
+ stdout.write(sequence);
178
+ // The process title is the second delivery channel: on Windows it drives
179
+ // the console title that ConPTY reflects onto the tab without any user
180
+ // setting, where the OSC channel alone only shows once the host maps
181
+ // "terminal.integrated.tabs.title" to the sequence variable.
182
+ process.title = sanitizeTerminalTitle(title);
183
+ processTitleRef.current = true;
184
+ writtenRef.current = title;
185
+ }, [stdout, title]);
186
+ }
@@ -0,0 +1,60 @@
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
+ import { createElement, useState } from 'react';
10
+ import { Box, Text, useInput, useStdout } from 'ink';
11
+ import { clampScroll, panelViewport } from './render/inspector.mjs';
12
+ import { truncateColumns } from './render/text.mjs';
13
+ import { panelAccent } from './panel-accent.mjs';
14
+ import { getPalette, inkColor, THEMES } from './theme.mjs';
15
+ import { t } from './i18n.mjs';
16
+ /**
17
+ * The /theme list: one row per theme, the current one marked with ●, the
18
+ * focused one with ›. Enter applies the focused theme (the runner persists
19
+ * it), Esc/q closes without changing anything. Colors read the ACTIVE
20
+ * palette, so the panel itself adapts to a light theme once applied.
21
+ */
22
+ export function ThemePanel({ current, select, close }) {
23
+ const [cursor, setCursor] = useState(() => {
24
+ const index = THEMES.findIndex(theme => theme.id === current);
25
+ return index < 0 ? 0 : index;
26
+ });
27
+ const stdout = useStdout().stdout;
28
+ const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30);
29
+ useInput((input, key) => {
30
+ if (key.escape || input === 'q')
31
+ return close();
32
+ if (key.upArrow)
33
+ return setCursor(value => (value + THEMES.length - 1) % THEMES.length);
34
+ if (key.downArrow)
35
+ return setCursor(value => (value + 1) % THEMES.length);
36
+ if (key.return)
37
+ return select(THEMES[cursor].id);
38
+ });
39
+ if (viewport.maxHeight === 0 || viewport.compact) {
40
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('theme.compact'), viewport.contentColumns));
41
+ }
42
+ // The theme rows share the panel's body budget like every other panel: an
43
+ // unsliced three-row list reached terminal-height equality on short
44
+ // terminals, where Ink rewrites the whole Static region every frame.
45
+ // Reveal-cursor slicing keeps the focused row visible instead.
46
+ const rowBudget = Math.max(1, viewport.bodyRows);
47
+ const first = clampScroll(cursor, THEMES.length, rowBudget);
48
+ const visibleThemes = THEMES.slice(first, first + rowBudget);
49
+ const hiddenThemes = THEMES.length - visibleThemes.length;
50
+ const accent = panelAccent('theme', getPalette().dim, getPalette().brandBright);
51
+ return createElement(Box, { width: viewport.outerColumns, borderStyle: 'round', borderColor: inkColor(accent.border), flexDirection: 'column', paddingX: 1 }, createElement(Text, { color: inkColor(accent.title), wrap: 'truncate-end' }, truncateColumns(t('theme.title'), viewport.contentColumns)), ...visibleThemes.map((theme, index) => {
52
+ const selected = first + index === cursor;
53
+ const active = theme.id === current;
54
+ return createElement(Text, {
55
+ key: theme.id,
56
+ color: selected ? inkColor(getPalette().brandBright) : undefined,
57
+ wrap: 'truncate-end',
58
+ }, truncateColumns(`${selected ? '› ' : ' '}${active ? '● ' : '○ '}${t(`theme.${theme.id}.label`)}${active ? ` · ${t('theme.current')}` : ''} · ${t(`theme.${theme.id}.description`)}`, viewport.contentColumns));
59
+ }), createElement(Text, { dimColor: true, wrap: 'truncate-end' }, truncateColumns(`↑↓ choose · enter apply · esc/q close${hiddenThemes > 0 ? ` · +${hiddenThemes} more` : ''}`, viewport.contentColumns)));
60
+ }
@@ -0,0 +1,379 @@
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
+ * 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
10
+ * palette selected through {@link setTheme}, so a theme switch recolors every
11
+ * painted surface on the next render without touching call sites; consumers
12
+ * read colors through {@link getPalette} or the painters below only.
13
+ *
14
+ * @module @deepseek-ai/dsh-tui/theme
15
+ */
16
+ import chalk from 'chalk';
17
+ import { rainbowRoll } from './rainbow.mjs';
18
+ /** Valid theme names in canonical picker order. */
19
+ export const THEME_NAMES = ['dark', 'light', 'prismatic', 'rainbow', 'auto'];
20
+ /**
21
+ * Every theme's picker metadata in canonical order — the single source the
22
+ * /theme panel rows derive from; {@link THEME_NAMES} stays the validation
23
+ * list. Adding a theme means adding its palette plus one row here.
24
+ */
25
+ export const THEMES = [
26
+ { id: 'dark', label: 'dark', description: 'DeepSeek dark palette (default)' },
27
+ { id: 'light', label: 'light', description: 'light palette for bright terminals' },
28
+ { id: 'prismatic', label: 'prismatic', description: 'neon synthwave palette (violet/magenta/cyan)' },
29
+ { id: 'rainbow', label: 'rainbow', description: 'randomized carnival palette (/rainbow to reroll)' },
30
+ { id: 'auto', label: 'auto', description: 'follow the terminal; dark until detection lands' },
31
+ ];
32
+ /**
33
+ * DeepSeek dark palette: the original TUI colors, one entry per
34
+ * design-platform token in use. Keep names and values in sync with the CSS
35
+ * custom properties cited inline.
36
+ */
37
+ export const DARK_PALETTE = {
38
+ /** Primary brand blue — `--dsw-static-deepseek-500`. */
39
+ brand: [65, 118, 230],
40
+ /** Brighter brand blue for live/streaming emphasis — `--dsw-static-deepseek-400`. */
41
+ brandBright: [103, 158, 254],
42
+ /** Intermediate brand blue between brand and brandBright — `--dsw-static-deepseek-450`. */
43
+ brandMid: [86, 134, 254],
44
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-600`. */
45
+ brandDeep: [72, 104, 178],
46
+ /** Muted caption gray — `--dsw-static-neutral-bluish-600`. */
47
+ dim: [129, 133, 140],
48
+ /** Success green — `--dsw-static-green-500`. */
49
+ success: [34, 197, 94],
50
+ /** Error red — `--dsw-static-red-500`. */
51
+ error: [239, 68, 68],
52
+ /** Warning amber — `--dsw-static-amber-500`. */
53
+ warn: [245, 158, 11],
54
+ /** Default foreground text — `--dsw-static-neutral-50`. */
55
+ text: [236, 240, 246],
56
+ /** Inline/fenced code — soft sky blue, distinct from brand accents. */
57
+ code: [125, 211, 252],
58
+ /** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
59
+ composerBand: [46, 48, 52],
60
+ /** Diff added-line background — Codex's muted dark green tint (#213A2B). */
61
+ diffAdd: [33, 58, 43],
62
+ /** Diff removed-line background — Codex's muted dark red tint (#4A221D). */
63
+ diffDel: [74, 34, 29],
64
+ /** Diff added-line foreground — green-500, 5.4:1 on the diffAdd tint. */
65
+ diffAddFg: [34, 197, 94],
66
+ /** Diff removed-line foreground — red-400, 4.9:1 on the diffDel tint (error's red-500 sinks to 3.6:1 on it). */
67
+ diffDelFg: [248, 113, 113],
68
+ /** Terminal background this palette is designed against; row tints blend toward it. */
69
+ surface: [0, 0, 0],
70
+ /**
71
+ * The three prompt-row colors, one per delivery kind. They are the palette's
72
+ * own accents (bright brand, warning amber, violet) rather than fixed RGBs,
73
+ * and every row's bar is these colors laid over {@link surface}.
74
+ */
75
+ prompt: [103, 158, 254],
76
+ queued: [245, 158, 11],
77
+ steered: [167, 139, 250],
78
+ };
79
+ /**
80
+ * Light palette tuned for white terminals: the same token keys as dark with
81
+ * contrast-driven values (AA on a white background). Brand keeps its dark
82
+ * value (≈4.2:1 on white — reserved for bold/accent spans; body-size brand
83
+ * text uses brandBright at ≈5.4:1); the bright/mid/deep blues, muted grays,
84
+ * and status colors deepen so they stay legible on bright backgrounds.
85
+ */
86
+ export const LIGHT_PALETTE = {
87
+ /** Primary brand blue — unchanged design-platform value; ≈4.2:1 on white, so bold/accent spans only (body-size brand text uses brandBright). */
88
+ brand: [65, 118, 230],
89
+ /** Brighter brand blue deepened for white backgrounds (was 2.7:1). */
90
+ brandBright: [72, 104, 178],
91
+ /** Intermediate brand blue — Tailwind blue-500. */
92
+ brandMid: [59, 130, 246],
93
+ /** Deep brand blue for secondary chrome — `--dsw-static-deepseek-700`. */
94
+ brandDeep: [47, 76, 143],
95
+ /** Muted caption gray deepened for white backgrounds. */
96
+ dim: [101, 103, 107],
97
+ /** Success green — Tailwind green-700. */
98
+ success: [21, 128, 61],
99
+ /** Error red — Tailwind red-600. */
100
+ error: [236, 19, 19],
101
+ /** Warning amber — Tailwind amber-700. */
102
+ warn: [180, 83, 9],
103
+ /** Default foreground text — near-black. */
104
+ text: [21, 21, 23],
105
+ /** Inline/fenced code — Tailwind cyan-700, distinct from brand accents. */
106
+ code: [14, 116, 144],
107
+ /** Composer three-row band base — neutral light gray, hue-free so wave tints read on it. */
108
+ composerBand: [229, 231, 235],
109
+ /** Diff added-line background — GitHub's pastel green (#dafbe1), Codex's light pick. */
110
+ diffAdd: [218, 251, 225],
111
+ /** Diff removed-line background — GitHub's pastel red (#ffebe9), Codex's light pick. */
112
+ diffDel: [255, 235, 233],
113
+ /** Diff added-line foreground — green-800, 6.4:1 on the pastel tint (green-700 clears 4.5:1 by a hair). */
114
+ diffAddFg: [22, 101, 52],
115
+ /** Diff removed-line foreground — red-700, 5.6:1 on the pastel tint (error's red-600 is 3.9:1). */
116
+ diffDelFg: [185, 28, 28],
117
+ /** Terminal background this palette is designed against; row tints blend toward it. */
118
+ surface: [255, 255, 255],
119
+ /**
120
+ * Prompt-row colors deepened for white: the amber and violet the dark theme
121
+ * uses sit at the AA edge on their own pastel bars, so each kind carries a
122
+ * value that clears 4.5:1 both on the bar and on plain white.
123
+ */
124
+ prompt: [47, 76, 143],
125
+ queued: [124, 53, 10],
126
+ steered: [109, 40, 217],
127
+ };
128
+ /**
129
+ * Prismatic palette — the neon synthwave skin (docs/prismatic-theme-design.md):
130
+ * electric violet replaces the brand blues, neon fuchsia carries live
131
+ * emphasis, neon cyan paints code, and a lavender gray dims captions. Paints
132
+ * on a black terminal; every value meets the same WCAG thresholds as the
133
+ * other palettes (body tokens ≥4.5:1, accents ≥3:1 on pure black). Semantic
134
+ * colors — success/error/warn and the four diff tokens — reuse the dark
135
+ * values verbatim so green still means added and red still means removed.
136
+ */
137
+ export const PRISMATIC_PALETTE = {
138
+ /** Primary electric purple — violet-500 (#8B5CF6), 5.0:1 on black. */
139
+ brand: [139, 92, 246],
140
+ /** Live/streaming emphasis — neon fuchsia-300 (#F0ABFC), 11.9:1 on black. */
141
+ brandBright: [240, 171, 252],
142
+ /** Intermediate violet — violet-400 (#A78BFA), 7.7:1 on black. */
143
+ brandMid: [167, 139, 250],
144
+ /** Deep secondary chrome — violet-600 (#7C3AED), 3.7:1 on black (violet-700 misses 3:1). */
145
+ brandDeep: [124, 58, 237],
146
+ /** Muted captions — lavender gray (#A6A3B8), 8.6:1 on black. */
147
+ dim: [166, 163, 184],
148
+ /** Success green — unchanged from dark. */
149
+ success: [34, 197, 94],
150
+ /** Error red — unchanged from dark. */
151
+ error: [239, 68, 68],
152
+ /** Warning amber — unchanged from dark. */
153
+ warn: [245, 158, 11],
154
+ /** Default foreground text — white with a lavender cast (#F0EEF9). */
155
+ text: [240, 238, 249],
156
+ /** Inline/fenced code — neon cyan-300 (#67E8F9), 14.5:1 on black. */
157
+ code: [103, 232, 249],
158
+ /** Composer three-row band base — neutral dark gray, hue-free so wave tints read on it. */
159
+ composerBand: [46, 46, 52],
160
+ /** Diff added-line background — unchanged from dark. */
161
+ diffAdd: [33, 58, 43],
162
+ /** Diff removed-line background — unchanged from dark. */
163
+ diffDel: [74, 34, 29],
164
+ /** Diff added-line foreground — unchanged from dark. */
165
+ diffAddFg: [34, 197, 94],
166
+ /** Diff removed-line foreground — unchanged from dark. */
167
+ diffDelFg: [248, 113, 113],
168
+ /** Terminal background this palette is designed against; row tints blend toward it. */
169
+ surface: [0, 0, 0],
170
+ /** Prompt rows in the skin's own neon: fuchsia, amber, cyan. */
171
+ prompt: [240, 171, 252],
172
+ queued: [245, 158, 11],
173
+ steered: [103, 232, 249],
174
+ };
175
+ /**
176
+ * Every STATIC palette by theme name; auto resolves through {@link resolveTheme}
177
+ * and rainbow is rolled per launch ({@link rainbowRoll}), so neither lives
178
+ * here — {@link setTheme} resolves both through the same key space.
179
+ */
180
+ export const PALETTES = {
181
+ dark: DARK_PALETTE,
182
+ light: LIGHT_PALETTE,
183
+ prismatic: PRISMATIC_PALETTE,
184
+ };
185
+ /** The theme name in force (the requested name; 'auto' included). */
186
+ let activeName = 'dark';
187
+ /** The palette painters and {@link getPalette} read for the active theme. */
188
+ let activePalette = DARK_PALETTE;
189
+ /**
190
+ * Resolve a theme name to the palette actually in use. `auto` detection
191
+ * (OSC 11 terminal background query) is a later enhancement; until it lands,
192
+ * auto falls back to the dark palette (prismatic and rainbow are always
193
+ * explicit choices, never auto-resolved).
194
+ * @param name - the requested theme name.
195
+ * @returns 'dark', 'light', 'prismatic', or 'rainbow' — the palette key.
196
+ */
197
+ export function resolveTheme(name) {
198
+ return name === 'light' ? 'light' : name === 'prismatic' ? 'prismatic' : name === 'rainbow' ? 'rainbow' : 'dark';
199
+ }
200
+ /**
201
+ * Switch the active theme: painters and {@link getPalette} reflect the new
202
+ * palette from the next render onward. The default is dark, so a process
203
+ * that never calls this paints exactly as before.
204
+ * @param name - the theme to activate ('auto' resolves to dark for now).
205
+ */
206
+ export function setTheme(name) {
207
+ activeName = name;
208
+ const resolved = resolveTheme(name);
209
+ activePalette = resolved === 'rainbow' ? rainbowRoll().palette : PALETTES[resolved];
210
+ }
211
+ /**
212
+ * The theme name in force. Returns the requested name ('auto' included) so
213
+ * the /theme picker and persistence can round-trip the user's choice; the
214
+ * palette actually used is {@link getPalette}.
215
+ */
216
+ export function getTheme() {
217
+ return activeName;
218
+ }
219
+ /** The palette in force; theme-aware call sites read colors through it. */
220
+ export function getPalette() {
221
+ return activePalette;
222
+ }
223
+ /**
224
+ * Parse a persisted theme name: only names in {@link THEME_NAMES} survive;
225
+ * anything else (missing, corrupt, or unknown) falls back to the dark default.
226
+ * @param value - the raw parsed JSON value (expected string).
227
+ * @returns a valid theme name.
228
+ */
229
+ export function parseThemeName(value) {
230
+ return typeof value === 'string' && THEME_NAMES.includes(value)
231
+ ? value
232
+ : 'dark';
233
+ }
234
+ /** Ink `color` string for one RGB triple. */
235
+ export function inkColor(triple) {
236
+ return `rgb(${triple[0]}, ${triple[1]}, ${triple[2]})`;
237
+ }
238
+ /**
239
+ * Diff-line background as an Ink `backgroundColor` string, theme-aware and
240
+ * depth-gated (the Codex diff renderer's rule): 16-color terminals paint
241
+ * backgrounds from the saturated system palette, which drowns the text, so
242
+ * they keep the foreground-only look; 256-color and truecolor terminals get
243
+ * the palette's tint (chalk quantizes the RGB form down automatically).
244
+ * @param token - which diff background, added or removed lines.
245
+ * @returns the Ink background color, or undefined to paint foreground-only.
246
+ */
247
+ export function diffBackground(token) {
248
+ if (chalk.level < 2)
249
+ return undefined;
250
+ return inkColor(activePalette[token]);
251
+ }
252
+ /** How far one prompt-row tint leans toward its color, over the surface. */
253
+ const PROMPT_ROW_TINT = 0.22;
254
+ /**
255
+ * The color family one prompt row wears, chosen by how it was delivered: the
256
+ * ordinary brand accent, the warning amber for a queued prompt, and the
257
+ * palette's own third accent for a steered one. The bar behind it is derived
258
+ * from that same color, so every theme — including a freshly rolled rainbow —
259
+ * carries its own look instead of a fixed triple.
260
+ * @param delivery - how the prompt was delivered; undefined is an ordinary one.
261
+ * @returns the palette token whose color paints the row.
262
+ */
263
+ export function promptRowTokens(delivery) {
264
+ if (delivery === 'queued')
265
+ return { fg: 'queued' };
266
+ if (delivery === 'steered')
267
+ return { fg: 'steered' };
268
+ return { fg: 'prompt' };
269
+ }
270
+ /**
271
+ * Blended prompt-row background for one row color: the palette color laid
272
+ * over the surface the palette is designed for (dark themes over black, light
273
+ * over white), depth-gated exactly like {@link diffBackground} — a 16-color
274
+ * terminal keeps the foreground-only look rather than painting a saturated
275
+ * system background behind the text.
276
+ * @param color - the row's palette token.
277
+ * @returns the Ink background color, or undefined to paint foreground-only.
278
+ */
279
+ export function rowBackground(color) {
280
+ if (chalk.level < 2)
281
+ return undefined;
282
+ const surface = activePalette.surface;
283
+ const tint = activePalette[color];
284
+ const blend = (index) => Math.round(surface[index] + (tint[index] - surface[index]) * PROMPT_ROW_TINT);
285
+ return inkColor([blend(0), blend(1), blend(2)]);
286
+ }
287
+ /**
288
+ * The prismatic surface ring: four neon accents panels cycle through by
289
+ * mount order (cyan → fuchsia → violet → lime), all ≥3:1 on black. Dark and
290
+ * light ignore the ring — {@link surfaceAccent} falls back to the passed-in
291
+ * base color so those themes stay pixel-identical.
292
+ */
293
+ export const ACCENT_RING = [
294
+ [34, 211, 238],
295
+ [232, 121, 249],
296
+ [167, 139, 250],
297
+ [163, 230, 53],
298
+ ];
299
+ /**
300
+ * The prismatic flow anchors: brand violet → neon fuchsia → neon cyan. All
301
+ * three clear 4.5:1 on black, so a flowing foreground stays AA throughout
302
+ * the oscillation (render/animations.ts walks the triangle).
303
+ */
304
+ export const FLOW_ANCHORS = [
305
+ [139, 92, 246],
306
+ [232, 121, 249],
307
+ [34, 211, 238],
308
+ ];
309
+ /**
310
+ * Whether the active theme is prismatic (the neon skin with surface rings
311
+ * and flowing accents).
312
+ */
313
+ export function isPrismatic() {
314
+ return activeName === 'prismatic';
315
+ }
316
+ /** Whether the active theme is rainbow (the per-launch carnival roll). */
317
+ export function isRainbow() {
318
+ return activeName === 'rainbow';
319
+ }
320
+ /**
321
+ * The flowing theme's anchor walk: prismatic rides the fixed violet→fuchsia→
322
+ * cyan triangle; rainbow rides its rolled full-spectrum anchors with a random
323
+ * phase; dark and light have no flow (undefined) and keep static accents.
324
+ */
325
+ export function themeFlow() {
326
+ if (isPrismatic())
327
+ return { anchors: FLOW_ANCHORS, phaseMs: 0 };
328
+ if (isRainbow()) {
329
+ const roll = rainbowRoll();
330
+ return { anchors: roll.flowAnchors, phaseMs: roll.flowPhaseMs };
331
+ }
332
+ return undefined;
333
+ }
334
+ /**
335
+ * The accent for one surface slot: prismatic rotates the {@link ACCENT_RING}
336
+ * by slot index; every other theme gets the caller's base color back.
337
+ * @param index - the slot's position in the mount-order sequence.
338
+ * @param base - the color to use outside prismatic (the surface's usual one).
339
+ * @returns the accent to paint the surface's border/title with.
340
+ */
341
+ export function surfaceAccent(index, base) {
342
+ if (isPrismatic()) {
343
+ const ring = ACCENT_RING;
344
+ return ring[((index % ring.length) + ring.length) % ring.length];
345
+ }
346
+ if (isRainbow()) {
347
+ const ring = rainbowRoll().ring;
348
+ return ring[((index % ring.length) + ring.length) % ring.length];
349
+ }
350
+ return base;
351
+ }
352
+ /** Paint with the primary brand blue: whale, wordmark, tool names, accents. */
353
+ export function brand(text) {
354
+ return chalk.rgb(...activePalette.brand)(text);
355
+ }
356
+ /** Paint with the bright brand blue: streaming output and active spinners. */
357
+ export function brandBright(text) {
358
+ return chalk.rgb(...activePalette.brandBright)(text);
359
+ }
360
+ /** Paint with the deep brand blue: borders and secondary chrome. */
361
+ export function brandDeep(text) {
362
+ return chalk.rgb(...activePalette.brandDeep)(text);
363
+ }
364
+ /** Paint muted captions, hints, and meta lines. */
365
+ export function dim(text) {
366
+ return chalk.rgb(...activePalette.dim)(text);
367
+ }
368
+ /** Paint completed tool results and confirmations. */
369
+ export function success(text) {
370
+ return chalk.rgb(...activePalette.success)(text);
371
+ }
372
+ /** Paint failures and error entries. */
373
+ export function error(text) {
374
+ return chalk.rgb(...activePalette.error)(text);
375
+ }
376
+ /** Paint warnings. */
377
+ export function warn(text) {
378
+ return chalk.rgb(...activePalette.warn)(text);
379
+ }