zames_pro 2.82.142 → 2.83.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/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.83.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - **ui:** selectable terminal color themes
15
+
10
16
  ## [2.82.142] - 2026-10-27
11
17
 
12
18
  ### Fixed
package/README.md CHANGED
@@ -620,6 +620,15 @@ right away (where possible without a restart).
620
620
  (help, messages, spinner) and the language the agent answers you in. The
621
621
  locale lives in `ui.locale` in the config file.
622
622
 
623
+ ### Themes
624
+
625
+ `/config set ui.theme <id>` (or the `/config` menu, row «Terminal color theme»)
626
+ repaints the whole terminal in a different palette, live — no restart. `zames`
627
+ is the default; the bundled palettes are ports of well-known editor schemes:
628
+
629
+ `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
630
+ `solarized-dark`, `tokyo-night`.
631
+
623
632
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
624
633
 
625
634
  ## FAQ
@@ -1330,6 +1339,16 @@ enum переключаются на месте; числа и строки от
1330
1339
  (справка, сообщения, спиннер), и язык, на котором агент вам отвечает. Локаль
1331
1340
  хранится в `ui.locale` в файле конфига.
1332
1341
 
1342
+ ### Темы
1343
+
1344
+ `/config set ui.theme <id>` (или меню `/config`, строка «Цветовая тема
1345
+ терминала») перекрашивает весь терминал в другую палитру, сразу — без
1346
+ перезапуска. Тема по умолчанию — `zames`; в комплекте порты известных схем
1347
+ редакторов:
1348
+
1349
+ `zames` (по умолчанию), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
1350
+ `solarized-dark`, `tokyo-night`.
1351
+
1333
1352
  Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo,
1334
1353
  сессии.
1335
1354
 
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ import { DEFAULT_DIAGNOSTICS } from './diagnostics.js';
5
5
  import { DEFAULT_LOCALE } from './i18n.js';
6
6
  import { writeJsonAtomic } from './fsutil.js';
7
7
  import { PROVIDER_IDS } from './providers.js';
8
+ import { DEFAULT_THEME, themeNames } from './theme.js';
8
9
  const ZAMES_HOME = path.join(os.homedir(), '.zames');
9
10
  const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
10
11
  // The PROJECT config path is resolved LAZILY: `/cd` changes process.cwd() at
@@ -132,6 +133,7 @@ export const DEFAULTS = {
132
133
  },
133
134
  ui: {
134
135
  locale: DEFAULT_LOCALE,
136
+ theme: DEFAULT_THEME,
135
137
  contextLimit: 1_000_000,
136
138
  // 0 = auto: terminal width, capped at 100 columns (the historical width).
137
139
  answerWidth: 0,
@@ -289,6 +291,13 @@ export const CONFIG_SCHEMA = [
289
291
  labelKey: 'cfg.f.maxIterations',
290
292
  groupKey: 'cfg.group.agent',
291
293
  },
294
+ {
295
+ path: 'ui.theme',
296
+ type: 'enum',
297
+ values: themeNames(),
298
+ labelKey: 'cfg.f.ui_theme',
299
+ groupKey: 'cfg.group.ui',
300
+ },
292
301
  {
293
302
  path: 'ui.answerWidth',
294
303
  type: 'number',
@@ -55,6 +55,10 @@ export const configMessages = {
55
55
  ru: 'Язык интерфейса и ответов агента',
56
56
  en: 'Interface and agent reply language',
57
57
  },
58
+ 'cfg.f.ui_theme': {
59
+ ru: 'Цветовая тема терминала',
60
+ en: 'Terminal color theme',
61
+ },
58
62
  'cfg.f.ui_answerWidth': {
59
63
  ru: 'Ширина контента в колонках: ответы, ввод, инструменты (0 — авто, до 100)',
60
64
  en: 'Content width in columns: answers, input, tools (0 = auto, capped at 100)',
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import fs from 'fs/promises';
5
5
  import { readFileSync, statSync } from 'fs';
6
6
  import { fileURLToPath } from 'url';
7
- import { theme } from './theme.js';
7
+ import { theme, applyTheme, themeColors } from './theme.js';
8
8
  import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
9
9
  import { QwenBrowser } from './qwen-browser.js';
10
10
  import { GlmBrowser } from './glm-browser.js';
@@ -119,6 +119,10 @@ if (providerArg !== null) {
119
119
  }
120
120
  // Apply the configured answer width to the markdown renderer (0 = auto).
121
121
  setAnswerWidth(config.ui?.answerWidth ?? 0);
122
+ // The color palette must be selected BEFORE the first line is rendered (the
123
+ // banner, startup notices): theme.ts starts on the default and every call site
124
+ // reads the live object, so one apply here repaints the whole session.
125
+ applyTheme(config.ui?.theme);
122
126
  // Current interface/agent language. Changed by the /config lang <ru|en> command.
123
127
  let currentLocale = isLocale(config.ui?.locale)
124
128
  ? config.ui.locale
@@ -1934,6 +1938,18 @@ async function main() {
1934
1938
  editor.setPrompt(buildPrompt());
1935
1939
  }
1936
1940
  }
1941
+ // Repaint the WHOLE terminal in the new palette immediately. The theme
1942
+ // object is shared and mutated in place, so every later render uses it;
1943
+ // the editor is asked for a full redraw because the rows already on screen
1944
+ // (status, prompt, hints) were painted with the OLD colors.
1945
+ if (path === 'ui.theme') {
1946
+ const applied = applyTheme(value);
1947
+ config.ui.theme = applied;
1948
+ if (editor) {
1949
+ editor.setTheme?.(themeColors());
1950
+ editor.refreshStatus();
1951
+ }
1952
+ }
1937
1953
  // Apply a live change to the answer width immediately, so the next
1938
1954
  // rendered answer uses the new width without a restart.
1939
1955
  if (path === 'ui.answerWidth') {
@@ -2155,6 +2171,13 @@ async function main() {
2155
2171
  getDefault: (path) => getByPath(DEFAULTS, path),
2156
2172
  set: (field, raw) => configSetRaw(field, raw),
2157
2173
  reset: (field) => configResetField(field),
2174
+ // The menu itself must repaint when the THEME row changes: its widgets
2175
+ // read the shared UI map, and without a bump the operator would cycle
2176
+ // palette ids while the colors stayed those of the previous one.
2177
+ onAfterChange: (field) => {
2178
+ if (field.path === 'ui.theme')
2179
+ editor?.setTheme?.(themeColors());
2180
+ },
2158
2181
  locale: currentLocale,
2159
2182
  };
2160
2183
  try {
package/dist/theme.js CHANGED
@@ -9,44 +9,246 @@ import { contentWidth } from './width.js';
9
9
  if (process.env.NO_COLOR) {
10
10
  chalk.level = 0;
11
11
  }
12
- // A calm palette for a dark terminal.
13
- // Muted, slightly "faded" tones without high saturation
14
- // (in the spirit of Tokyo Night / Nord). The goal is comfortable reading of
15
- // long answers, not maximum contrast.
16
- //
17
- // Roles:
18
- // - user — soft lavender-gray (service messages)
19
- // - prompt — warm gold (input prompt, noticeable)
20
- // - dir — light blue (working directory name)
21
- // - assistant — warm sand (model answers)
22
- // - tool — muted amber
23
- // - toolResult — gray-blue
24
- // - system — neutral gray
25
- // - warn — calm ochre
26
- // - error — muted terracotta
27
- export const theme = {
28
- // Operator messages — a clearly DISTINCT cyan so they never blend with the
29
- // model's warm-sand answers or the neutral system text (operator request).
30
- user: chalk.hex('#8fd4f0'), // operator text — bright cyan-blue
31
- userBar: chalk.hex('#4a8fb5'), // the `▌` bar marking an operator turn
32
- // A dim rule + label separating one conversation TURN from the next.
33
- turnRule: chalk.hex('#3a3f4b'),
34
- turnLabel: chalk.hex('#5b616e'),
35
- prompt: chalk.hex('#c8b06a').bold, // input prompt — warm gold, noticeable
36
- dir: chalk.hex('#7fc4f0'), // working directory name — noticeably blue
37
- assistant: chalk.hex('#cfc9b0'), // warm sand
38
- tool: chalk.hex('#c6a97e'), // muted amber
39
- toolResult: chalk.hex('#8a9bb5'), // gray-blue
40
- system: chalk.hex('#808896'), // neutral gray
41
- dim: chalk.hex('#5b616e'), // dark gray
42
- taskSummary: chalk.hex('#8fa3c8'), // per-task summary — noticeable but calm, distinct from spinner brown and warn ochre
43
- warn: chalk.hex('#c9a86a'), // calm ochre
44
- error: chalk.hex('#c98a80'), // muted terracotta
45
- success: chalk.hex('#a9c08c'), // soft sage
46
- brown: chalk.hex('#a1723f'), // brown (status/spinner)
47
- toggleOn: chalk.hex('#6fd0b0'), // teal-green — a toggle is ON
48
- bold: chalk.bold,
12
+ export const PALETTES = {
13
+ // zames — the original calm palette (Tokyo Night / Nord spirit): muted,
14
+ // slightly faded tones, comfortable for long answers.
15
+ zames: {
16
+ bg: '#23272e',
17
+ user: '#8fd4f0',
18
+ userBar: '#4a8fb5',
19
+ turnRule: '#3a3f4b',
20
+ turnLabel: '#5b616e',
21
+ prompt: '#c8b06a',
22
+ dir: '#7fc4f0',
23
+ assistant: '#cfc9b0',
24
+ tool: '#c6a97e',
25
+ toolResult: '#8a9bb5',
26
+ system: '#808896',
27
+ dim: '#5b616e',
28
+ taskSummary: '#8fa3c8',
29
+ warn: '#c9a86a',
30
+ error: '#c98a80',
31
+ success: '#a9c08c',
32
+ brown: '#a1723f',
33
+ toggleOn: '#6fd0b0',
34
+ },
35
+ // One Dark — Atom's default theme, the most recognized "modern editor" look.
36
+ 'one-dark': {
37
+ bg: '#282c34',
38
+ user: '#61afef',
39
+ userBar: '#3e6f9e',
40
+ turnRule: '#3b4048',
41
+ turnLabel: '#5c6370',
42
+ prompt: '#e5c07b',
43
+ dir: '#61afef',
44
+ assistant: '#abb2bf',
45
+ tool: '#d19a66',
46
+ toolResult: '#7f8b9e',
47
+ system: '#828997',
48
+ dim: '#5c6370',
49
+ taskSummary: '#56b6c2',
50
+ warn: '#e5c07b',
51
+ error: '#e06c75',
52
+ success: '#98c379',
53
+ brown: '#d19a66',
54
+ toggleOn: '#56b6c2',
55
+ },
56
+ // Dracula — high-contrast purple/pink, extremely popular.
57
+ dracula: {
58
+ bg: '#282a36',
59
+ user: '#8be9fd',
60
+ userBar: '#4f8fa6',
61
+ turnRule: '#3d4152',
62
+ turnLabel: '#6272a4',
63
+ prompt: '#f1fa8c',
64
+ dir: '#8be9fd',
65
+ assistant: '#f8f8f2',
66
+ tool: '#ffb86c',
67
+ toolResult: '#b0b8d8',
68
+ system: '#9aa2c0',
69
+ dim: '#6272a4',
70
+ taskSummary: '#8be9fd',
71
+ warn: '#ffb86c',
72
+ error: '#ff5555',
73
+ success: '#50fa7b',
74
+ brown: '#bd93f9',
75
+ toggleOn: '#50fa7b',
76
+ },
77
+ // Gruvbox Dark — warm retro earth tones.
78
+ gruvbox: {
79
+ bg: '#282828',
80
+ user: '#83a598',
81
+ userBar: '#5a7a6e',
82
+ turnRule: '#3c3836',
83
+ turnLabel: '#665c54',
84
+ prompt: '#fabd2f',
85
+ dir: '#83a598',
86
+ assistant: '#ebdbb2',
87
+ tool: '#fe8019',
88
+ toolResult: '#a89984',
89
+ system: '#928374',
90
+ dim: '#665c54',
91
+ taskSummary: '#8ec07c',
92
+ warn: '#fabd2f',
93
+ error: '#fb4934',
94
+ success: '#b8bb26',
95
+ brown: '#d65d0e',
96
+ toggleOn: '#b8bb26',
97
+ },
98
+ // Nord — cool arctic blues, low contrast by design.
99
+ nord: {
100
+ bg: '#2e3440',
101
+ user: '#88c0d0',
102
+ userBar: '#5e81a8',
103
+ turnRule: '#3b4252',
104
+ turnLabel: '#616e88',
105
+ prompt: '#ebcb8b',
106
+ dir: '#88c0d0',
107
+ assistant: '#d8dee9',
108
+ tool: '#d08770',
109
+ toolResult: '#9aa7bd',
110
+ system: '#8b95a8',
111
+ dim: '#616e88',
112
+ taskSummary: '#81a1c1',
113
+ warn: '#ebcb8b',
114
+ error: '#bf616a',
115
+ success: '#a3be8c',
116
+ brown: '#d08770',
117
+ toggleOn: '#8fbcbb',
118
+ },
119
+ // Monokai — the classic Sublime Text scheme.
120
+ monokai: {
121
+ bg: '#272822',
122
+ user: '#66d9ef',
123
+ userBar: '#3f8c9e',
124
+ turnRule: '#3e3d32',
125
+ turnLabel: '#75715e',
126
+ prompt: '#e6db74',
127
+ dir: '#66d9ef',
128
+ assistant: '#f8f8f2',
129
+ tool: '#fd971f',
130
+ toolResult: '#a6a695',
131
+ system: '#a1a19a',
132
+ dim: '#75715e',
133
+ taskSummary: '#66d9ef',
134
+ warn: '#e6db74',
135
+ error: '#f92672',
136
+ success: '#a6e22e',
137
+ brown: '#fd971f',
138
+ toggleOn: '#a6e22e',
139
+ },
140
+ // Solarized Dark — Ethan Schoonover's precision palette.
141
+ 'solarized-dark': {
142
+ bg: '#002b36',
143
+ user: '#268bd2',
144
+ userBar: '#1f6a9e',
145
+ turnRule: '#073642',
146
+ turnLabel: '#586e75',
147
+ prompt: '#b58900',
148
+ dir: '#2aa198',
149
+ assistant: '#93a1a1',
150
+ tool: '#cb4b16',
151
+ toolResult: '#839496',
152
+ system: '#839496',
153
+ dim: '#586e75',
154
+ taskSummary: '#2aa198',
155
+ warn: '#b58900',
156
+ error: '#dc322f',
157
+ success: '#859900',
158
+ brown: '#cb4b16',
159
+ toggleOn: '#2aa198',
160
+ },
161
+ // Tokyo Night — the modern neon-on-midnight scheme.
162
+ 'tokyo-night': {
163
+ bg: '#1a1b26',
164
+ user: '#7dcfff',
165
+ userBar: '#3d7fa6',
166
+ turnRule: '#2a2e3f',
167
+ turnLabel: '#565f89',
168
+ prompt: '#e0af68',
169
+ dir: '#7dcfff',
170
+ assistant: '#c0caf5',
171
+ tool: '#ff9e64',
172
+ toolResult: '#9aa5ce',
173
+ system: '#787c99',
174
+ dim: '#565f89',
175
+ taskSummary: '#7aa2f7',
176
+ warn: '#e0af68',
177
+ error: '#f7768e',
178
+ success: '#9ece6a',
179
+ brown: '#ff9e64',
180
+ toggleOn: '#73daca',
181
+ },
49
182
  };
183
+ /** The palette id used when the configured theme is unknown/missing. */
184
+ export const DEFAULT_THEME = 'zames';
185
+ /**
186
+ * True for a palette id we actually ship. Used by the config layer to
187
+ * normalize `ui.theme` (a stale value from an older/newer build must not
188
+ * leave the operator with an unstyled terminal).
189
+ */
190
+ export function isThemeName(v) {
191
+ return (typeof v === 'string' && Object.prototype.hasOwnProperty.call(PALETTES, v));
192
+ }
193
+ /** All palette ids, in the order they are offered in /config. */
194
+ export function themeNames() {
195
+ return Object.keys(PALETTES);
196
+ }
197
+ /** Build the chalk formatters for one palette. */
198
+ function buildTheme(p) {
199
+ return {
200
+ user: chalk.hex(p.user),
201
+ userBar: chalk.hex(p.userBar),
202
+ turnRule: chalk.hex(p.turnRule),
203
+ turnLabel: chalk.hex(p.turnLabel),
204
+ prompt: chalk.hex(p.prompt).bold,
205
+ dir: chalk.hex(p.dir),
206
+ assistant: chalk.hex(p.assistant),
207
+ tool: chalk.hex(p.tool),
208
+ toolResult: chalk.hex(p.toolResult),
209
+ system: chalk.hex(p.system),
210
+ dim: chalk.hex(p.dim),
211
+ taskSummary: chalk.hex(p.taskSummary),
212
+ warn: chalk.hex(p.warn),
213
+ error: chalk.hex(p.error),
214
+ success: chalk.hex(p.success),
215
+ brown: chalk.hex(p.brown),
216
+ toggleOn: chalk.hex(p.toggleOn),
217
+ bold: chalk.bold,
218
+ };
219
+ }
220
+ // The LIVE theme object. It is mutated IN PLACE by applyTheme() so that every
221
+ // module that captured the import (`import { theme } from './theme.js'`) sees
222
+ // the switch without being re-imported — this is why the call sites read
223
+ // `theme.x` at RENDER time instead of destructuring a color at module load.
224
+ let activeName = DEFAULT_THEME;
225
+ export const theme = buildTheme(PALETTES[DEFAULT_THEME]);
226
+ /** The palette id currently rendered. */
227
+ export function currentThemeName() {
228
+ return activeName;
229
+ }
230
+ /**
231
+ * Switch the palette for every subsequent render. Unknown ids fall back to the
232
+ * default so a hand-edited config can never blank out the colors.
233
+ * Returns the id actually applied.
234
+ */
235
+ export function applyTheme(name) {
236
+ const next = isThemeName(name) ? name : DEFAULT_THEME;
237
+ const built = buildTheme(PALETTES[next]);
238
+ for (const key of Object.keys(built)) {
239
+ theme[key] = built[key];
240
+ }
241
+ activeName = next;
242
+ return next;
243
+ }
244
+ /**
245
+ * The active palette's RAW hex map — for the Ink TUI, which needs color
246
+ * STRINGS (Ink `<Text color>` does not take chalk functions) and for the
247
+ * README screenshot generator, which renders the real colors as HTML.
248
+ */
249
+ export function themeColors() {
250
+ return PALETTES[activeName];
251
+ }
50
252
  // Horizontal rule that separates the model's answer from the next block. It
51
253
  // spans the SHARED content width (src/width.ts) so the rule, the answers and
52
254
  // the tool previews all line up at the same margin. The `max` argument is kept
@@ -91,10 +91,10 @@ export function AskQuestion({ question, options, recommended, allowText, locale
91
91
  out.push(line);
92
92
  return out.length ? out : [''];
93
93
  };
94
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + t('askuser.title') }), wrap(question).map((line, i) => (_jsx(Text, { color: UI.text, children: line }, i))), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(innerCols) }), typing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('askuser.your_answer') }), _jsx(Text, { color: UI.dim, children: t('askuser.enter_send') }), _jsxs(Text, { color: UI.accent, children: ['❯ ', text, '▏'] })] })) : (rows.map((label, i) => {
94
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + t('askuser.title') }), wrap(question).map((line, i) => (_jsx(Text, { color: UI.text, children: line }, i))), _jsx(Text, { color: UI.border, children: '─'.repeat(innerCols) }), typing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('askuser.your_answer') }), _jsx(Text, { color: UI.dim, children: t('askuser.enter_send') }), _jsxs(Text, { color: UI.accent, children: ['❯ ', text, '▏'] })] })) : (rows.map((label, i) => {
95
95
  const sel = i === cursor;
96
96
  const isRec = i === recommended;
97
- return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: label }), isRec ? (_jsx(Text, { color: "#6fd0b0", children: ' (' + t('askuser.recommended') + ')' })) : null] }, i));
97
+ return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: label }), isRec ? (_jsx(Text, { color: UI.on, children: ' (' + t('askuser.recommended') + ')' })) : null] }, i));
98
98
  })), _jsx(HintBar, { hints: typing
99
99
  ? [
100
100
  ['Enter', t('askuser.hint_send')],
@@ -39,6 +39,7 @@ function displayValue(field, value, t) {
39
39
  }
40
40
  export function ConfigMenuApp(opts) {
41
41
  const { fields, t, get, set, reset, getDefault, locale = 'en' } = opts;
42
+ const { onAfterChange } = opts;
42
43
  const { exit } = useApp();
43
44
  // The hint bar is operator text: it follows the SAME locale the labels use
44
45
  // (a hardcoded Russian bar showed up in an English UI).
@@ -89,6 +90,7 @@ export function ConfigMenuApp(opts) {
89
90
  try {
90
91
  set(f, raw);
91
92
  bump();
93
+ onAfterChange?.(f);
92
94
  setMessage(t('cfg.menu.saved', {
93
95
  v: f.path,
94
96
  value: displayValue(f, get(f.path), t),
@@ -105,6 +107,7 @@ export function ConfigMenuApp(opts) {
105
107
  if (f.type === 'boolean') {
106
108
  set(f, get(f.path) === true ? 'false' : 'true');
107
109
  bump();
110
+ onAfterChange?.(f);
108
111
  setMessage(null);
109
112
  return;
110
113
  }
@@ -125,6 +128,7 @@ export function ConfigMenuApp(opts) {
125
128
  const next = f.values[(base + dir + f.values.length) % f.values.length];
126
129
  set(f, next);
127
130
  bump();
131
+ onAfterChange?.(f);
128
132
  setMessage(null);
129
133
  };
130
134
  /** ←/→ acts on the focused row: cycle an enum, toggle a boolean. */
@@ -137,6 +141,7 @@ export function ConfigMenuApp(opts) {
137
141
  else if (f.type === 'boolean') {
138
142
  set(f, get(f.path) === true ? 'false' : 'true');
139
143
  bump();
144
+ onAfterChange?.(f);
140
145
  setMessage(null);
141
146
  }
142
147
  };
@@ -244,6 +249,7 @@ export function ConfigMenuApp(opts) {
244
249
  if (pendingReset === f.path) {
245
250
  reset(f);
246
251
  bump();
252
+ onAfterChange?.(f);
247
253
  setMessage(t('cfg.reset', { v: f.path }));
248
254
  setPendingReset(null);
249
255
  }
@@ -330,7 +336,7 @@ export function ConfigMenuApp(opts) {
330
336
  if (!visible.length) {
331
337
  items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
332
338
  }
333
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: UI.title, children: t('cfg.menu.title') }), _jsx(Text, { color: UI.dim, children: (visible.length ? cursor + 1 : 0) + '/' + visible.length })] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), items, _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), editing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.edit_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + editBuf })] })) : filterMode ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.filter_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + filterQuery })] })) : message ? (_jsx(Text, { color: UI.ok, children: message })) : filterQuery ? (_jsx(Text, { color: UI.dim, children: t('cfg.menu.filter_active', { q: filterQuery }) })) : null, _jsx(HintBar, { hints: [
339
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: UI.title, children: t('cfg.menu.title') }), _jsx(Text, { color: UI.dim, children: (visible.length ? cursor + 1 : 0) + '/' + visible.length })] }), _jsx(Text, { color: UI.border, children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), items, _jsx(Text, { color: UI.border, children: '─'.repeat(Math.max(10, (columns || 80) - 6)) }), editing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.edit_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + editBuf })] })) : filterMode ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.text, children: t('cfg.menu.filter_hint') }), _jsx(Text, { color: UI.accent, children: '❯ ' + filterQuery })] })) : message ? (_jsx(Text, { color: UI.ok, children: message })) : filterQuery ? (_jsx(Text, { color: UI.dim, children: t('cfg.menu.filter_active', { q: filterQuery }) })) : null, _jsx(HintBar, { hints: [
334
340
  ['↑/↓', hint('tui.hint.select')],
335
341
  ['←/→', hint('tui.hint.change')],
336
342
  ['Enter', hint('tui.hint.edit')],
@@ -20,6 +20,7 @@ import { suggestRowCount } from '../input/suggest.js';
20
20
  import { truncateToWidth, visLen } from '../input/layout.js';
21
21
  import { fullScreenLayout } from './layout.js';
22
22
  import { useMouseWheel } from './useMouseWheel.js';
23
+ import { UI } from './ui-colors.js';
23
24
  /** The header bar: title, version, working directory. */
24
25
  function Header({ store }) {
25
26
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
@@ -28,6 +29,9 @@ function Header({ store }) {
28
29
  .filter(Boolean)
29
30
  .join(' ');
30
31
  const left = '❯ ' + state.title + (state.version ? ' v' + state.version : '');
32
+ // Referencing the nonce keeps the React dependency honest: the header colors
33
+ // come from the shared UI map, which is mutated in place on a theme switch.
34
+ void state.themeNonce;
31
35
  const rawDir = state.workdir ? ' ' + state.workdir : '';
32
36
  // The header MUST stay exactly ONE text row: FullScreenApp reserves
33
37
  // HEADER_ROWS = 2 (title + divider) and the whole panel height is computed
@@ -43,7 +47,7 @@ function Header({ store }) {
43
47
  dir = budget > 0 ? truncateToWidth(dir, budget) : '';
44
48
  // If even title + provider exceed the width, drop the directory entirely.
45
49
  const finalDir = gap >= 1 ? dir : '';
46
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#c8b06a", bold: true, children: left }), _jsx(Text, { color: "#7fc4f0", children: finalDir }), _jsx(Text, { children: ' '.repeat(Math.max(1, cols - visLen(left) - visLen(finalDir) - visLen(right) - 2)) }), right ? _jsx(Text, { color: "#6fd0b0", children: right }) : null] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(cols) })] }));
50
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: UI.accent, bold: true, children: left }), _jsx(Text, { color: UI.accent, children: finalDir }), _jsx(Text, { children: ' '.repeat(Math.max(1, cols - visLen(left) - visLen(finalDir) - visLen(right) - 2)) }), right ? _jsx(Text, { color: UI.on, children: right }) : null] }), _jsx(Text, { color: UI.border, children: '─'.repeat(cols) })] }));
47
51
  }
48
52
  export function FullScreenApp({ store, inputProps, }) {
49
53
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
@@ -107,12 +111,12 @@ export function FullScreenApp({ store, inputProps, }) {
107
111
  const next = cur + delta * 3;
108
112
  store.set({ scroll: Math.max(0, Math.min(maxScroll, next)) });
109
113
  }, state.mouseScroll);
110
- return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 1, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: "#5b616e", children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
114
+ return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, minHeight: 1, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: UI.dim, children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
111
115
  // Cursor rows above the input box: header + log viewport + status.
112
116
  // The log pane is FLEXIBLE now, so the panel below it starts right
113
117
  // after everything above: header + as many log rows as fit + the
114
118
  // suggestion popup + status. `cursorTop` counts the rows ABOVE the
115
119
  // input rows; the box border row is added by InputLine.
116
- cursorTop: fsLayout.cursorTop, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: "#5b616e", wrap: "truncate", children: ' ' +
120
+ cursorTop: fsLayout.cursorTop, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: UI.dim, wrap: "truncate", children: ' ' +
117
121
  translate(state.locale)(state.mouseScroll ? 'editor.keys_hint' : 'editor.keys_hint_select') })] }));
118
122
  }
@@ -16,10 +16,11 @@ import { insertText, insertPaste, insertNewline, backspace, del, moveLeft, moveR
16
16
  import { dispatchKey, parseCsiU } from './keys.js';
17
17
  import { useSyncExternalStore } from 'react';
18
18
  import { matchSlashCommands, suggestWindow, completeSlashCommand, } from '../input/suggest.js';
19
+ import { UI } from './ui-colors.js';
19
20
  /** Render the visual rows of the buffer with the prompt on the first row. */
20
21
  function InputRows({ prompt, buf, cols, }) {
21
22
  const lay = layoutInput(prompt, buf, 0, cols);
22
- return (_jsx(Box, { flexDirection: "column", children: lay.rows.map((r, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "#c8b06a", bold: true, children: r.prefix }), r.text] }, i))) }));
23
+ return (_jsx(Box, { flexDirection: "column", children: lay.rows.map((r, i) => (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, bold: true, children: r.prefix }), r.text] }, i))) }));
23
24
  }
24
25
  export function InputLine(props) {
25
26
  const { columns } = useWindowSize();
@@ -316,9 +317,9 @@ export function InputLine(props) {
316
317
  return (_jsxs(Box, { flexDirection: "column", children: [suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [shown.map((c, i) => {
317
318
  const label = c.hint ? c.name + ' ' + c.hint : c.name;
318
319
  const selected = suggestOffset + i === suggestSelected;
319
- return (_jsxs(Text, { children: [_jsx(Text, { color: "#c8b06a", children: selected ? ' ▸ ' : ' ' }), _jsx(Text, { color: "#c8b06a", children: label }), _jsx(Text, { color: "#5b616e", children: ' ' + c.description })] }, c.name));
320
- }), moreCount > 0 && (_jsx(Text, { color: "#5b616e", children: ' ' +
320
+ return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? ' ▸ ' : ' ' }), _jsx(Text, { color: UI.accent, children: label }), _jsx(Text, { color: UI.dim, children: ' ' + c.description })] }, c.name));
321
+ }), moreCount > 0 && (_jsx(Text, { color: UI.dim, children: ' ' +
321
322
  t('editor.more', { n: String(moreCount) }) +
322
323
  ' ' +
323
- t('editor.page_hint') }))] })), _jsx(Box, { borderStyle: "round", borderColor: props.locked ? '#5b616e' : '#c8b06a', paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: _jsx(InputRows, { prompt: props.prompt, buf: st.buf, cols: innerCols }) })] }));
324
+ t('editor.page_hint') }))] })), _jsx(Box, { borderStyle: "round", borderColor: props.locked ? UI.dim : UI.accent, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: _jsx(InputRows, { prompt: props.prompt, buf: st.buf, cols: innerCols }) })] }));
324
325
  }
@@ -44,7 +44,7 @@ export function SelectList({ title, items, initial, locale = 'en', onSelect, })
44
44
  finish(items[cursor]?.value ?? null);
45
45
  }
46
46
  });
47
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + title }), _jsx(Text, { color: UI.dim, children: items.length })] }), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(innerCols) }), items.slice(win.top, win.end).map((it, i) => {
47
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + title }), _jsx(Text, { color: UI.dim, children: items.length })] }), _jsx(Text, { color: UI.border, children: '─'.repeat(innerCols) }), items.slice(win.top, win.end).map((it, i) => {
48
48
  const idx = win.top + i;
49
49
  const sel = idx === cursor;
50
50
  return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: it.label }), it.detail ? _jsx(Text, { color: UI.dim, children: ' ' + it.detail }) : null] }, it.value));
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Box, Text, useAnimation, useWindowSize } from 'ink';
3
3
  import { translate } from '../i18n.js';
4
4
  import { DOTS, stripEllipsis } from '../spinner.js';
5
+ import { UI } from './ui-colors.js';
5
6
  import { truncateToWidth, visLen } from '../input/layout.js';
6
7
  /** Elapsed seconds of the current animated phase ("0s", "45s", "2m 05s"). */
7
8
  function elapsedLabel(startedAt, animating) {
@@ -68,16 +69,16 @@ function Badges({ state, budget = Infinity, }) {
68
69
  all.push({ key, w, node });
69
70
  };
70
71
  if (state.statusLine) {
71
- add('sl', visLen(state.statusLine) + 1, _jsxs(Text, { color: "#808896", children: [state.statusLine, ' '] }, "sl"));
72
+ add('sl', visLen(state.statusLine) + 1, _jsxs(Text, { color: UI.system, children: [state.statusLine, ' '] }, "sl"));
72
73
  }
73
74
  if (state.provider) {
74
- add('prov', visLen('[' + state.provider + '] '), _jsx(Text, { color: "#808896", children: '[' + state.provider + '] ' }, "prov"));
75
+ add('prov', visLen('[' + state.provider + '] '), _jsx(Text, { color: UI.system, children: '[' + state.provider + '] ' }, "prov"));
75
76
  }
76
77
  if (state.queueLength > 0) {
77
- add('q', visLen('⧗' + state.queueLength + ' '), _jsxs(Text, { color: "#c9a86a", children: ['⧗' + state.queueLength, ' '] }, "q"));
78
+ add('q', visLen('⧗' + state.queueLength + ' '), _jsxs(Text, { color: UI.warn, children: ['⧗' + state.queueLength, ' '] }, "q"));
78
79
  }
79
80
  if (state.tasks) {
80
- add('t', visLen(state.tasks) + 1, _jsxs(Text, { color: "#808896", children: [state.tasks, ' '] }, "t"));
81
+ add('t', visLen(state.tasks) + 1, _jsxs(Text, { color: UI.system, children: [state.tasks, ' '] }, "t"));
81
82
  }
82
83
  const toggles = [];
83
84
  if (state.thinkingEnabled)
@@ -86,14 +87,14 @@ function Badges({ state, budget = Infinity, }) {
86
87
  toggles.push('🌐\uFE0E');
87
88
  if (toggles.length) {
88
89
  const label = toggles.join(' ') + ' ';
89
- add('tg', visLen(label), _jsx(Text, { color: "#6fd0b0", children: label }, "tg"));
90
+ add('tg', visLen(label), _jsx(Text, { color: UI.on, children: label }, "tg"));
90
91
  }
91
92
  if (state.contextText) {
92
93
  const color = state.contextLevel === 'high'
93
- ? '#c98a80'
94
+ ? UI.err
94
95
  : state.contextLevel === 'warn'
95
- ? '#c9a86a'
96
- : '#a9c08c';
96
+ ? UI.warn
97
+ : UI.ok;
97
98
  const marker = state.contextLevel === 'high'
98
99
  ? '⛔ '
99
100
  : state.contextLevel === 'warn'
@@ -161,5 +162,5 @@ export function StatusBar({ state, divider = true, }) {
161
162
  const cols = useWindowSize().columns || 80;
162
163
  const leftBudget = Math.max(0, Math.min(Math.floor(cols / 3), cols - 14));
163
164
  const leftOut = showLeft ? truncateToWidth(leftText, leftBudget) : '';
164
- return (_jsxs(Box, { flexDirection: "column", children: [divider ? _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(cols) }) : null, _jsxs(Box, { flexDirection: "row", width: Math.max(1, cols - 2), height: 1, children: [_jsx(Text, { color: "#a1723f", children: leftOut || ' ' }), _jsx(Box, { flexGrow: 1 }), _jsx(Badges, { state: state, budget: Math.max(0, cols - 2 - visLen(leftOut)) })] })] }));
165
+ return (_jsxs(Box, { flexDirection: "column", children: [divider ? _jsx(Text, { color: UI.border, children: '─'.repeat(cols) }) : null, _jsxs(Box, { flexDirection: "row", width: Math.max(1, cols - 2), height: 1, children: [_jsx(Text, { color: UI.accentDim, children: leftOut || ' ' }), _jsx(Box, { flexGrow: 1 }), _jsx(Badges, { state: state, budget: Math.max(0, cols - 2 - visLen(leftOut)) })] })] }));
165
166
  }
@@ -344,6 +344,15 @@ export class TuiEditor {
344
344
  this.locale = locale;
345
345
  this.store.set({ locale });
346
346
  }
347
+ /**
348
+ * Repaint the Ink widgets after a palette switch. The hex map (UI) is
349
+ * mutated in place by theme.applyTheme(), but Ink only re-renders when the
350
+ * store emits — bump a nonce so every subscribed component redraws with the
351
+ * new colors.
352
+ */
353
+ setTheme(_colors) {
354
+ this.store.set({ themeNonce: Date.now() });
355
+ }
347
356
  setCommands(commands) {
348
357
  this.commands = commands;
349
358
  this.store.set({ commands });
package/dist/tui/store.js CHANGED
@@ -45,6 +45,7 @@ function initialState() {
45
45
  searchEnabled: false,
46
46
  statusLine: '',
47
47
  columns: process.stdout.columns || 80,
48
+ themeNonce: 0,
48
49
  };
49
50
  }
50
51
  /**
@@ -0,0 +1,37 @@
1
+ // The Ink TUI's color map, derived from the ACTIVE terminal palette
2
+ // (src/theme.ts). Ink `<Text color>` takes a plain hex STRING, not a chalk
3
+ // function, so the TUI cannot reuse `theme` directly — this module is the one
4
+ // bridge between the two.
5
+ //
6
+ // `UI` is mutated IN PLACE by syncUiColors() (like `theme`): every component
7
+ // reads `UI.x` at RENDER time, so a /config theme switch repaints without
8
+ // re-importing anything. Components must never destructure a color at module
9
+ // load — that would freeze the palette at startup.
10
+ import { PALETTES, DEFAULT_THEME, isThemeName } from '../theme.js';
11
+ /** Derive the Ink map from a terminal palette. */
12
+ export function uiColorsFrom(p) {
13
+ return {
14
+ accent: p.prompt,
15
+ accentDim: p.turnLabel,
16
+ text: p.assistant,
17
+ dim: p.dim,
18
+ border: p.turnRule,
19
+ on: p.toggleOn,
20
+ off: p.dim,
21
+ ok: p.success,
22
+ warn: p.warn,
23
+ err: p.error,
24
+ system: p.system,
25
+ title: p.taskSummary,
26
+ };
27
+ }
28
+ export const UI = uiColorsFrom(PALETTES[DEFAULT_THEME]);
29
+ /** Repaint the Ink widgets with the palette of `name` (unknown -> default). */
30
+ export function syncUiColors(name) {
31
+ const id = isThemeName(name) ? name : DEFAULT_THEME;
32
+ const next = uiColorsFrom(PALETTES[id]);
33
+ for (const key of Object.keys(next)) {
34
+ UI[key] = next[key];
35
+ }
36
+ return id;
37
+ }
@@ -1,19 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Text } from 'ink';
3
- export const UI = {
4
- accent: '#c8b06a',
5
- accentDim: '#7a6b45',
6
- text: '#cfc9b0',
7
- dim: '#5b616e',
8
- border: '#4a5058',
9
- on: '#6fd0b0',
10
- off: '#5b616e',
11
- ok: '#a9c08c',
12
- warn: '#c9a86a',
13
- err: '#c98a80',
14
- system: '#808896',
15
- title: '#9fb8d8',
16
- };
3
+ // The color map lives in ui-colors.ts because it is DERIVED from the active
4
+ // terminal palette and mutated in place on a /config theme switch. Re-exported
5
+ // here so every existing `import { UI } from './widgets.js'` keeps working.
6
+ export { UI } from './ui-colors.js';
7
+ import { UI } from './ui-colors.js';
17
8
  /** `[x]` / `[ ]` — a checkbox for a boolean value. */
18
9
  export function Checkbox({ checked, focused, }) {
19
10
  return (_jsx(Text, { color: checked ? UI.on : UI.off, bold: focused, children: checked ? '[x]' : '[ ]' }));
package/llms-full.txt CHANGED
@@ -605,6 +605,15 @@ right away (where possible without a restart).
605
605
  (help, messages, spinner) and the language the agent answers you in. The
606
606
  locale lives in `ui.locale` in the config file.
607
607
 
608
+ ### Themes
609
+
610
+ `/config set ui.theme <id>` (or the `/config` menu, row «Terminal color theme»)
611
+ repaints the whole terminal in a different palette, live — no restart. `zames`
612
+ is the default; the bundled palettes are ports of well-known editor schemes:
613
+
614
+ `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
615
+ `solarized-dark`, `tokyo-night`.
616
+
608
617
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
609
618
 
610
619
  ## FAQ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.142",
3
+ "version": "2.83.0",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {