zames_pro 2.83.4 → 2.85.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,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.85.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - **ui:** theme the window background, keep the unsent draft, drop the extra prompt caret
15
+
16
+ ## [2.84.0] - 2026-10-27
17
+
18
+ ### Added
19
+
20
+ - **ui:** theme previews, 6 more palettes, inline /config preview
21
+
10
22
  ## [2.83.4] - 2026-10-27
11
23
 
12
24
  ### Fixed
package/README.md CHANGED
@@ -38,7 +38,7 @@ lock-in. Install with `npm install -g zames_pro`, run `zames` in any project
38
38
  folder, and describe the task in plain language.
39
39
 
40
40
  <p align="center">
41
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
41
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/shots/theme-zames.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
42
42
  </p>
43
43
 
44
44
  ## Quick start
@@ -631,10 +631,19 @@ The switch is instant, no restart. `zames` is the default; the bundled palettes
631
631
  are ports of well-known editor schemes:
632
632
 
633
633
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
634
- `solarized-dark`, `tokyo-night`.
634
+ `solarized-dark`, `tokyo-night`, `catppuccin`, `github-dark`, `ayu`,
635
+ `rose-pine`, `everforest`, `kanagawa`.
635
636
 
636
637
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
637
- highlighting inside them and the input frame. The frame itself is a separate
638
+ highlighting inside them, the status row and the input frame. The terminal's
639
+ BACKGROUND follows too: the theme emits OSC-11 with the palette's `bg`
640
+ (supported by iTerm2, Windows Terminal, kitty, WezTerm, Alacritty, GNOME
641
+ Terminal and others; where it is not supported nothing happens). Opt out with
642
+ `ZAMES_NO_BG=1` or `NO_COLOR`.
643
+ To make the choice
644
+ concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
645
+ below the list, in `/config` press `p` on the theme row); after
646
+ `/theme <id>` the sample is printed too. The frame itself is a separate
638
647
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
639
648
  (default `round`; `none` removes it).
640
649
 
@@ -757,7 +766,7 @@ Qwen или GLM) в агента для командной строки в ду
757
766
  можно описать обычным языком.
758
767
 
759
768
  <p align="center">
760
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="сессия zames в терминале — терминальный coding-агент и AI-ассистент программиста без API-ключа" width="860">
769
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/shots/theme-zames.png" alt="сессия zames в терминале — терминальный coding-агент и AI-ассистент программиста без API-ключа" width="860">
761
770
  </p>
762
771
 
763
772
  ## Быстрый старт
@@ -1359,11 +1368,20 @@ enum переключаются на месте; числа и строки от
1359
1368
  редакторов:
1360
1369
 
1361
1370
  `zames` (по умолчанию), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
1362
- `solarized-dark`, `tokyo-night`.
1363
-
1364
- Тема перекрашивает ВСЁ: интерфейс, ответы агента, подсветку кода в ответах и
1365
- рамку ввода. Саму рамку можно менять отдельно: `/config set ui.inputBorder
1366
- <round|single|double|bold|none>` (по умолчанию `round`, `none` — без рамки).
1371
+ `solarized-dark`, `tokyo-night`, `catppuccin`, `github-dark`, `ayu`,
1372
+ `rose-pine`, `everforest`, `kanagawa`.
1373
+
1374
+ Тема перекрашивает ВСЁ: интерфейс, ответы агента, подсветку кода в ответах,
1375
+ строку статуса и рамку ввода. ФОН окна терминала тоже перекрашивается: тема
1376
+ шлёт OSC-11 с цветом `bg` палитры (поддерживают iTerm2, Windows Terminal,
1377
+ kitty, WezTerm, Alacritty, GNOME Terminal и др.; где не поддерживается — просто
1378
+ ничего не происходит). Отключить фон: `ZAMES_NO_BG=1` или `NO_COLOR`.
1379
+ Чтобы было видно, ЧТО меняется, перед выбором
1380
+ показывается образец сессии в этой палитре (в `/theme` — под списком, в
1381
+ `/config` — клавиша `p` на строке темы). После применения `/theme <id>`
1382
+ образец печатается в терминал. Саму рамку можно менять отдельно:
1383
+ `/config set ui.inputBorder <round|single|double|bold|none>` (по умолчанию
1384
+ `round`, `none` — без рамки).
1367
1385
 
1368
1386
  Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo,
1369
1387
  сессии.
@@ -212,6 +212,14 @@ export const deepseekMessages = {
212
212
  ru: '✔ Сохранено: {v} = {value}',
213
213
  en: '✔ Saved: {v} = {value}',
214
214
  },
215
+ 'cfg.menu.theme_preview': {
216
+ ru: 'Так тема выглядит в работе:',
217
+ en: 'How the theme looks in a session:',
218
+ },
219
+ 'cfg.menu.theme_preview_key': {
220
+ ru: 'пример темы',
221
+ en: 'theme sample',
222
+ },
215
223
  'cfg.menu.default': { ru: 'по умолчанию', en: 'default' },
216
224
  'cfg.menu.filter_hint': {
217
225
  ru: '/ — поиск по настройкам, d — сбросить (дважды), * — изменено от дефолта',
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import fs from 'fs/promises';
5
5
  import { readFileSync, statSync } from 'fs';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { theme, applyTheme, themeColors, themeNames, currentThemeName, isThemeName, } from './theme.js';
8
+ import { themePreviewAnsi } from './tui/theme-preview.js';
8
9
  import { setInputBorder } from './tui/ui-colors.js';
9
10
  import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
10
11
  import { QwenBrowser } from './qwen-browser.js';
@@ -1518,8 +1519,9 @@ async function main() {
1518
1519
  theme.dim(') '));
1519
1520
  }
1520
1521
  // No working-directory tail: it ate input width and is already in the
1521
- // header / /pwd. A bare prompt keeps room for the task text.
1522
- return theme.prompt('❯ ') + theme.dim('› ');
1522
+ // header / /pwd. A bare `❯ ` keeps room for the task text and does not
1523
+ // read as a SECOND prompt marker next to the cursor.
1524
+ return theme.prompt('❯ ');
1523
1525
  };
1524
1526
  await refreshDynamicCommands(currentWorkdir);
1525
1527
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -2249,6 +2251,11 @@ async function main() {
2249
2251
  return;
2250
2252
  }
2251
2253
  emit(theme.assistant(t('theme.set', { v: applyThemeById(arg) })));
2254
+ // A terminal can only show colors it can paint, and the operator asked
2255
+ // "what exactly changes?" — print a sample of every role in the new
2256
+ // palette right below the confirmation.
2257
+ emit('');
2258
+ emit(themePreviewAnsi(arg));
2252
2259
  return;
2253
2260
  }
2254
2261
  // No argument: a pick list in the TUI, the id list otherwise. The cursor
@@ -2278,6 +2285,8 @@ async function main() {
2278
2285
  }
2279
2286
  if (picked) {
2280
2287
  emit(theme.assistant(t('theme.set', { v: applyThemeById(picked) })));
2288
+ emit('');
2289
+ emit(themePreviewAnsi(picked));
2281
2290
  }
2282
2291
  else {
2283
2292
  // Cancelled: the previews must not stick. Restore WITHOUT persisting.
@@ -2438,9 +2447,6 @@ async function main() {
2438
2447
  theme.dir(reviewMode.snapName) +
2439
2448
  theme.dim(') ');
2440
2449
  }
2441
- else {
2442
- tail = theme.dim('› ');
2443
- }
2444
2450
  input = await promptOnce(theme.prompt('❯ ') + tail);
2445
2451
  }
2446
2452
  }
package/dist/theme.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { contentWidth } from './width.js';
3
+ import { applyBackground } from './tui/background.js';
3
4
  // Respect NO_COLOR (https://no-color.org): when the operator sets it (any
4
5
  // non-empty value) drop ALL SGR codes so the output can be piped into a file
5
6
  // or a log viewer without escape noise. chalk 5 only auto-detects TTY/CI, it
@@ -179,6 +180,134 @@ export const PALETTES = {
179
180
  brown: '#ff9e64',
180
181
  toggleOn: '#73daca',
181
182
  },
183
+ // Catppuccin Mocha — the soft pastel palette that took over the editor
184
+ // ecosystem (Neovim, VS Code, terminals).
185
+ catppuccin: {
186
+ bg: '#1e1e2e',
187
+ user: '#89b4fa',
188
+ userBar: '#5477a8',
189
+ turnRule: '#313244',
190
+ turnLabel: '#6c7086',
191
+ prompt: '#f9e2af',
192
+ dir: '#89dceb',
193
+ assistant: '#cdd6f4',
194
+ tool: '#fab387',
195
+ toolResult: '#a6adc8',
196
+ system: '#9399b2',
197
+ dim: '#6c7086',
198
+ taskSummary: '#89b4fa',
199
+ warn: '#f9e2af',
200
+ error: '#f38ba8',
201
+ success: '#a6e3a1',
202
+ brown: '#eba0ac',
203
+ toggleOn: '#94e2d5',
204
+ },
205
+ // GitHub Dark — the palette of GitHub's dark UI (familiar to anyone who
206
+ // reads diffs all day).
207
+ 'github-dark': {
208
+ bg: '#0d1117',
209
+ user: '#58a6ff',
210
+ userBar: '#3a6ea8',
211
+ turnRule: '#21262d',
212
+ turnLabel: '#6e7681',
213
+ prompt: '#d29922',
214
+ dir: '#79c0ff',
215
+ assistant: '#c9d1d9',
216
+ tool: '#ffa657',
217
+ toolResult: '#8b949e',
218
+ system: '#8b949e',
219
+ dim: '#6e7681',
220
+ taskSummary: '#a5d6ff',
221
+ warn: '#d29922',
222
+ error: '#f85149',
223
+ success: '#3fb950',
224
+ brown: '#db6d28',
225
+ toggleOn: '#56d4dd',
226
+ },
227
+ // Ayu Dark — the minimal warm-accent scheme from the Ayu editor theme.
228
+ ayu: {
229
+ bg: '#0f1419',
230
+ user: '#59c2ff',
231
+ userBar: '#35789e',
232
+ turnRule: '#1f2430',
233
+ turnLabel: '#5c6773',
234
+ prompt: '#ffb454',
235
+ dir: '#59c2ff',
236
+ assistant: '#c7ccd1',
237
+ tool: '#ff8f40',
238
+ toolResult: '#a0a8b0',
239
+ system: '#8a9199',
240
+ dim: '#5c6773',
241
+ taskSummary: '#39bae6',
242
+ warn: '#ffb454',
243
+ error: '#f07178',
244
+ success: '#aad94c',
245
+ brown: '#e6b450',
246
+ toggleOn: '#95e6cb',
247
+ },
248
+ // Rosé Pine — muted rose-pine tones, a calmer alternative to Dracula.
249
+ 'rose-pine': {
250
+ bg: '#191724',
251
+ user: '#9ccfd8',
252
+ userBar: '#5b8490',
253
+ turnRule: '#26233a',
254
+ turnLabel: '#6e6a86',
255
+ prompt: '#f6c177',
256
+ dir: '#9ccfd8',
257
+ assistant: '#e0def4',
258
+ tool: '#ebbcba',
259
+ toolResult: '#908caa',
260
+ system: '#908caa',
261
+ dim: '#6e6a86',
262
+ taskSummary: '#c4a7e7',
263
+ warn: '#f6c177',
264
+ error: '#eb6f92',
265
+ success: '#31748f',
266
+ brown: '#ebbcba',
267
+ toggleOn: '#9ccfd8',
268
+ },
269
+ // Everforest — the soft green forest palette (low contrast, easy on eyes).
270
+ everforest: {
271
+ bg: '#2d353b',
272
+ user: '#7fbbb3',
273
+ userBar: '#5a8b85',
274
+ turnRule: '#3d484d',
275
+ turnLabel: '#859289',
276
+ prompt: '#dbbc7f',
277
+ dir: '#7fbbb3',
278
+ assistant: '#d3c6aa',
279
+ tool: '#e69875',
280
+ toolResult: '#9da9a0',
281
+ system: '#859289',
282
+ dim: '#7a8478',
283
+ taskSummary: '#a7c080',
284
+ warn: '#dbbc7f',
285
+ error: '#e67e80',
286
+ success: '#a7c080',
287
+ brown: '#e69875',
288
+ toggleOn: '#83c092',
289
+ },
290
+ // Kanagawa — Hokusai-inspired muted indigo (the "wave" theme).
291
+ kanagawa: {
292
+ bg: '#1f1f28',
293
+ user: '#7e9cd8',
294
+ userBar: '#55709e',
295
+ turnRule: '#2a2a37',
296
+ turnLabel: '#727169',
297
+ prompt: '#e6c384',
298
+ dir: '#7fb4ca',
299
+ assistant: '#dcd7ba',
300
+ tool: '#ffa066',
301
+ toolResult: '#a6a69c',
302
+ system: '#c8c093',
303
+ dim: '#727169',
304
+ taskSummary: '#957fb8',
305
+ warn: '#e6c384',
306
+ error: '#e46876',
307
+ success: '#98bb6c',
308
+ brown: '#d27e59',
309
+ toggleOn: '#7aa89f',
310
+ },
182
311
  };
183
312
  /** The palette id used when the configured theme is unknown/missing. */
184
313
  export const DEFAULT_THEME = 'zames';
@@ -239,6 +368,9 @@ export function applyTheme(name) {
239
368
  theme[key] = built[key];
240
369
  }
241
370
  activeName = next;
371
+ // The window background is part of the look: paint it (best effort) so a
372
+ // dark palette does not sit on a light terminal background.
373
+ applyBackground(next);
242
374
  return next;
243
375
  }
244
376
  /**
@@ -14,6 +14,7 @@ import { useMemo, useState } from 'react';
14
14
  import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
15
15
  import { translate } from '../i18n.js';
16
16
  import { Checkbox, Select, UI, HintBar } from './widgets.js';
17
+ import { ThemePreview } from './theme-preview.js';
17
18
  /** Strip ANSI to measure a line's visible width. */
18
19
  function stripAnsi(s) {
19
20
  return s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
@@ -52,6 +53,8 @@ export function ConfigMenuApp(opts) {
52
53
  const [editBuf, setEditBuf] = useState('');
53
54
  const [pendingReset, setPendingReset] = useState(null);
54
55
  const [message, setMessage] = useState(null);
56
+ // Inline palette preview of the theme row ('p').
57
+ const [previewTheme, setPreviewTheme] = useState(false);
55
58
  // `get`/`set` mutate the LIVE config object in place, so React has no state
56
59
  // change to react to and the row kept the OLD value on screen after a
57
60
  // toggle/cycle (the operator saw «ru» while the config already held «en»).
@@ -216,12 +219,14 @@ export function ConfigMenuApp(opts) {
216
219
  setCursor((c) => visible.length ? (c - 1 + visible.length) % visible.length : 0);
217
220
  setPendingReset(null);
218
221
  setMessage(null);
222
+ setPreviewTheme(false);
219
223
  return;
220
224
  }
221
225
  if (key.downArrow || input === 'j') {
222
226
  setCursor((c) => (visible.length ? (c + 1) % visible.length : 0));
223
227
  setPendingReset(null);
224
228
  setMessage(null);
229
+ setPreviewTheme(false);
225
230
  return;
226
231
  }
227
232
  if (input === 'g') {
@@ -238,6 +243,30 @@ export function ConfigMenuApp(opts) {
238
243
  setMessage(null);
239
244
  return;
240
245
  }
246
+ if (input === 'p' || input === 'P') {
247
+ // Inline palette PREVIEW for the theme row: the settings list is where
248
+ // most operators meet ui.theme, and cycling the enum only shows the id.
249
+ // 'p' opens the sample session painted with the CURRENT palette, so the
250
+ // choice can be judged without leaving /config. Other rows: no-op.
251
+ const f = visible[cursor];
252
+ if (f && f.path === 'ui.theme') {
253
+ setPreviewTheme((v) => !v);
254
+ setMessage(null);
255
+ return;
256
+ }
257
+ }
258
+ if (input === 'p' || input === 'P') {
259
+ // Inline palette PREVIEW on the theme row: /config is where most
260
+ // operators meet ui.theme, and cycling the enum only shows the id. 'p'
261
+ // toggles a sample session painted with the CURRENT palette, so the
262
+ // choice can be judged without leaving the menu. Other rows: no-op.
263
+ const f = visible[cursor];
264
+ if (f && f.path === 'ui.theme') {
265
+ setPreviewTheme((v) => !v);
266
+ setMessage(null);
267
+ return;
268
+ }
269
+ }
241
270
  if (key.return || input === ' ') {
242
271
  activate();
243
272
  return;
@@ -336,10 +365,13 @@ export function ConfigMenuApp(opts) {
336
365
  if (!visible.length) {
337
366
  items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
338
367
  }
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: [
368
+ 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)) }), previewTheme && visible[cursor]?.path === 'ui.theme' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('cfg.menu.theme_preview') }), _jsx(ThemePreview, { name: String(get('ui.theme') ?? 'zames'), compact: true })] })) : null, 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: [
340
369
  ['↑/↓', hint('tui.hint.select')],
341
370
  ['←/→', hint('tui.hint.change')],
342
371
  ['Enter', hint('tui.hint.edit')],
372
+ ...(visible[cursor]?.path === 'ui.theme'
373
+ ? [['p', hint('cfg.menu.theme_preview_key')]]
374
+ : []),
343
375
  ['/', hint('tui.hint.search')],
344
376
  ['d', hint('tui.hint.reset')],
345
377
  ['q', hint('tui.hint.quit')],
@@ -12,7 +12,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { Box, Text, useInput, usePaste, useCursor, useWindowSize, useStdin, } from 'ink';
13
13
  import { translate } from '../i18n.js';
14
14
  import { expandPastes, inputInnerCols, layoutInput, SUGGEST_PAGE, } from '../input/layout.js';
15
- import { insertText, insertPaste, insertNewline, backspace, del, moveLeft, moveRight, moveHome, moveEnd, wordLeft, wordRight, deleteWordLeft, deleteWordRight, killToLineEnd, moveVisualRows, pushUndo, undoEdit, historyUp, historyDown, searchStart, searchNext, searchAccept, searchCancel, searchInput, searchBackspace, recordHistory, afterSubmit, } from './edit.js';
15
+ import { insertText, insertPaste, insertNewline, backspace, del, moveLeft, moveRight, moveHome, moveEnd, wordLeft, wordRight, deleteWordLeft, deleteWordRight, killToLineEnd, moveVisualRows, pushUndo, undoEdit, historyUp, historyDown, searchStart, searchNext, searchAccept, searchCancel, searchInput, searchBackspace, recordHistory, afterSubmit, rememberDraft, } from './edit.js';
16
16
  import { dispatchKey, parseCsiU } from './keys.js';
17
17
  import { useSyncExternalStore } from 'react';
18
18
  import { matchSlashCommands, suggestWindow, completeSlashCommand, } from '../input/suggest.js';
@@ -125,17 +125,20 @@ export function InputLine(props) {
125
125
  case 'submit':
126
126
  doSubmit();
127
127
  return;
128
+ // The EDIT actions first write the line into the draft slot, so a
129
+ // remembered draft is replaced only by a REAL edit and an accidental
130
+ // Up cannot lose the typed message.
128
131
  case 'newline':
129
- apply(insertNewline);
132
+ apply((s) => pushUndo(rememberDraft(insertNewline(s)), true));
130
133
  return;
131
134
  case 'insert':
132
- apply((s) => pushUndo(insertText(s, action.text), true));
135
+ apply((s) => pushUndo(rememberDraft(insertText(s, action.text)), true));
133
136
  return;
134
137
  case 'backspace':
135
- apply((s) => backspace(pushUndo(s, true)));
138
+ apply((s) => backspace(pushUndo(rememberDraft(s), true)));
136
139
  return;
137
140
  case 'delete':
138
- apply((s) => del(pushUndo(s, true)));
141
+ apply((s) => del(pushUndo(rememberDraft(s), true)));
139
142
  return;
140
143
  case 'left':
141
144
  apply(moveLeft);
@@ -10,7 +10,7 @@ import { useState } from 'react';
10
10
  import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
11
11
  import { UI } from './ui-colors.js';
12
12
  import { HintBar } from './widgets.js';
13
- import { theme } from '../theme.js';
13
+ import { ThemePreview } from './theme-preview.js';
14
14
  import { translate } from '../i18n.js';
15
15
  export function ThemePicker({ themes, initial, onPreview, locale = 'en', onDone, }) {
16
16
  const { exit } = useApp();
@@ -30,20 +30,6 @@ export function ThemePicker({ themes, initial, onPreview, locale = 'en', onDone,
30
30
  setCursor(next);
31
31
  onPreview(themes[next]);
32
32
  };
33
- // A sample line of every important role, colored through `theme` (chalk):
34
- // Ink's <Text color> takes a hex STRING, not a formatter, so a gradient like
35
- // this has to be pre-rendered ANSI.
36
- const preview = () => [
37
- theme.user('user'),
38
- theme.assistant('answer'),
39
- theme.tool('tool'),
40
- theme.toolResult('result'),
41
- theme.system('system'),
42
- theme.warn('warn'),
43
- theme.error('error'),
44
- theme.success('ok'),
45
- theme.toggleOn('on'),
46
- ].join(' ');
47
33
  useInput((input, key) => {
48
34
  if (key.escape || (key.ctrl && input === 'c') || input === 'q') {
49
35
  finish(null);
@@ -63,7 +49,7 @@ export function ThemePicker({ themes, initial, onPreview, locale = 'en', onDone,
63
49
  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: '◆ ' + t('theme.pick_title') }), _jsx(Text, { color: UI.dim, children: themes.length })] }), _jsx(Text, { color: UI.border, children: '─'.repeat(innerCols) }), themes.map((name, idx) => {
64
50
  const sel = idx === cursor;
65
51
  return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: name }), idx === start ? (_jsx(Text, { color: UI.dim, children: ' ' + t('tui.list.current') })) : null] }, name));
66
- }), _jsx(Text, { color: UI.border, children: '─'.repeat(innerCols) }), _jsx(Text, { children: preview() }), _jsx(HintBar, { hints: [
52
+ }), _jsx(Text, { color: UI.border, children: '─'.repeat(innerCols) }), _jsx(ThemePreview, { name: themes[cursor] ?? initial, compact: true }), _jsx(HintBar, { hints: [
67
53
  ['↑/↓', t('theme.pick_move')],
68
54
  ['Enter', t('tui.hint.ok')],
69
55
  ['Esc', t('theme.pick_cancel')],
@@ -0,0 +1,42 @@
1
+ // Terminal BACKGROUND handling.
2
+ //
3
+ // A terminal cannot be told to paint "the background" — only the cells an app
4
+ // writes are styled. So a theme's `bg` reaches the screen by
5
+ // 1. emitting an OSC-11 background colour for the whole window (most modern
6
+ // terminals support it; a terminal that does not answers nothing), and
7
+ // 2. re-emitting it AFTER a full-screen app exits: the alternate screen keeps
8
+ // the window's background, and Ink's exit sequence resets attributes
9
+ // (SGR reset does not touch OSC-11, but some terminals do reset the colour
10
+ // on 1049l). Re-emitting is harmless where unsupported.
11
+ //
12
+ // The OSC sequence is written STRAIGHT to stdout (not through chalk), because
13
+ // it is not an SGR attribute: `\x1b]11;rgb:rr/gg/bb\x07`.
14
+ import { PALETTES, isThemeName, DEFAULT_THEME } from '../theme.js';
15
+ /** `#rrggbb` -> the `rr/gg/bb` part of an OSC-11 payload. */
16
+ function hexToOsc(hex) {
17
+ const h = hex.replace('#', '');
18
+ return `${h.slice(0, 2)}/${h.slice(2, 4)}/${h.slice(4, 6)}`;
19
+ }
20
+ /**
21
+ * The OSC-11 sequence that sets the window background, or '' when the operator
22
+ * opted out (NO_COLOR / ZAMES_NO_BG). Exported pure, so the encoding is
23
+ * testable without a terminal.
24
+ */
25
+ export function backgroundOsc(name) {
26
+ if (process.env.NO_COLOR || process.env.ZAMES_NO_BG)
27
+ return '';
28
+ const id = isThemeName(name) ? name : DEFAULT_THEME;
29
+ return '\x1b]11;rgb:' + hexToOsc(PALETTES[id].bg) + '\x07';
30
+ }
31
+ /** Paint the window background for the palette `name` (best effort). */
32
+ export function applyBackground(name) {
33
+ const seq = backgroundOsc(name);
34
+ if (!seq)
35
+ return;
36
+ try {
37
+ process.stdout.write(seq);
38
+ }
39
+ catch {
40
+ // A closed/redirected stdout must not break a theme switch.
41
+ }
42
+ }
package/dist/tui/edit.js CHANGED
@@ -249,9 +249,11 @@ export function historyUp(s) {
249
249
  if (!s.history.length)
250
250
  return s;
251
251
  let histIndex = s.histIndex;
252
- let histDraft = s.histDraft;
253
- if (histIndex === s.history.length)
254
- histDraft = s.buf;
252
+ // Snapshot the unsent line when the browse STARTS (histIndex is at the end),
253
+ // but NEVER replace the remembered draft with a RECALLED entry's text: a
254
+ // second Up after a Down used to overwrite it, and the operator's typed
255
+ // message was lost ("pressed Up by accident").
256
+ const histDraft = histIndex === s.history.length ? s.buf : s.histDraft;
255
257
  if (histIndex > 0) {
256
258
  histIndex--;
257
259
  const buf = s.history[histIndex];
@@ -269,6 +271,10 @@ export function historyDown(s) {
269
271
  if (!s.history.length)
270
272
  return s;
271
273
  let histIndex = s.histIndex;
274
+ // The CURRENT line is the operator's, not a history entry: leaving the
275
+ // history must bring it back. Without this, editing a recalled line and then
276
+ // pressing Down restored the OLD draft and dropped the edit.
277
+ const draft = browsingHistory(s) ? s.histDraft : s.buf;
272
278
  let buf;
273
279
  if (histIndex < s.history.length - 1) {
274
280
  histIndex++;
@@ -276,9 +282,38 @@ export function historyDown(s) {
276
282
  }
277
283
  else {
278
284
  histIndex = s.history.length;
279
- buf = s.histDraft;
285
+ buf = draft;
280
286
  }
281
- return { ...s, histIndex, buf, cursor: Array.from(buf).length };
287
+ return {
288
+ ...s,
289
+ histIndex,
290
+ histDraft: draft,
291
+ buf,
292
+ cursor: Array.from(buf).length,
293
+ };
294
+ }
295
+ // ---------- draft guard ----------
296
+ //
297
+ // The operator's UNSUBMITTED line must survive a history browse, arrow-key
298
+ // scrolling and every other non-typing transition. `histDraft` alone did not:
299
+ // it is snapshotted only when the browse STARTS (histIndex === history.length),
300
+ // so once the cursor was inside the history an edit or a second Up path could
301
+ // replace it — a real report was "I pressed Up by accident and my typed message
302
+ // was gone".
303
+ //
304
+ // The rule: typing/recalling may change the visible buffer, but a transition
305
+ // that is NOT an explicit edit must keep the draft; leaving the history restores
306
+ // it. `typingRun` is true while the operator types (so a real edit adopts the new
307
+ // text as the draft on the next browse).
308
+ /** True while the buffer is a RECALLED history entry (not the draft). */
309
+ export function browsingHistory(s) {
310
+ return s.history.length > 0 && s.histIndex < s.history.length;
311
+ }
312
+ /** Remember the current buffer as the draft when it is NOT a history entry. */
313
+ export function rememberDraft(s) {
314
+ if (s.buf === s.histDraft)
315
+ return s;
316
+ return { ...s, histDraft: s.buf };
282
317
  }
283
318
  export function searchStart(s) {
284
319
  if (!s.history.length)
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import chalk from 'chalk';
4
+ import { PALETTES, DEFAULT_THEME, isThemeName } from '../theme.js';
5
+ /**
6
+ * The sample lines. Kept PURE DATA so tests can assert the preview covers the
7
+ * important roles and the Ink/plain renderers cannot drift apart.
8
+ */
9
+ export function previewLines() {
10
+ return [
11
+ { role: 'dir', text: '~/project' },
12
+ { role: 'prompt', text: '❯ объясни этот код', bold: true },
13
+ { role: 'assistant', text: '● Ответ: функция читает файл и парсит JSON.' },
14
+ { role: 'tool', text: '🔧 Read {"path":"package.json"}' },
15
+ { role: 'toolResult', text: '→ ✔ 42 строки' },
16
+ { role: 'taskSummary', text: '· duration: 6s · tools: 1' },
17
+ { role: 'warn', text: '▲ предупреждение' },
18
+ { role: 'error', text: '✖ ошибка' },
19
+ { role: 'success', text: '✔ готово' },
20
+ { role: 'toggleOn', text: '🌐 поиск вкл' },
21
+ { role: 'system', text: '● сервисное сообщение' },
22
+ { role: 'dim', text: '· подсказка' },
23
+ ];
24
+ }
25
+ function paletteOf(name) {
26
+ return PALETTES[isThemeName(name) ? name : DEFAULT_THEME];
27
+ }
28
+ /** The preview as one chalk-painted string (for a plain terminal line). */
29
+ export function themePreviewAnsi(name) {
30
+ const p = paletteOf(name);
31
+ return previewLines()
32
+ .map((l) => {
33
+ const c = chalk.hex(p[l.role]);
34
+ return l.bold ? c.bold(l.text) : c(l.text);
35
+ })
36
+ .join('\n');
37
+ }
38
+ /** The preview as an Ink block (for the picker / the config menu). */
39
+ export function ThemePreview({ name, compact = false, }) {
40
+ const p = paletteOf(name);
41
+ const lines = compact
42
+ ? previewLines().filter((l) => ['prompt', 'assistant', 'tool', 'error', 'success', 'dim'].includes(l.role))
43
+ : previewLines();
44
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((l, i) => (_jsx(Text, { color: p[l.role], bold: l.bold, children: l.text }, i))) }));
45
+ }
package/llms-full.txt CHANGED
@@ -23,7 +23,7 @@ lock-in. Install with `npm install -g zames_pro`, run `zames` in any project
23
23
  folder, and describe the task in plain language.
24
24
 
25
25
  <p align="center">
26
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
26
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/shots/theme-zames.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
27
27
  </p>
28
28
 
29
29
  ## Quick start
@@ -616,10 +616,19 @@ The switch is instant, no restart. `zames` is the default; the bundled palettes
616
616
  are ports of well-known editor schemes:
617
617
 
618
618
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
619
- `solarized-dark`, `tokyo-night`.
619
+ `solarized-dark`, `tokyo-night`, `catppuccin`, `github-dark`, `ayu`,
620
+ `rose-pine`, `everforest`, `kanagawa`.
620
621
 
621
622
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
622
- highlighting inside them and the input frame. The frame itself is a separate
623
+ highlighting inside them, the status row and the input frame. The terminal's
624
+ BACKGROUND follows too: the theme emits OSC-11 with the palette's `bg`
625
+ (supported by iTerm2, Windows Terminal, kitty, WezTerm, Alacritty, GNOME
626
+ Terminal and others; where it is not supported nothing happens). Opt out with
627
+ `ZAMES_NO_BG=1` or `NO_COLOR`.
628
+ To make the choice
629
+ concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
630
+ below the list, in `/config` press `p` on the theme row); after
631
+ `/theme <id>` the sample is printed too. The frame itself is a separate
623
632
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
624
633
  (default `round`; `none` removes it).
625
634
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.83.4",
3
+ "version": "2.85.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": {
@@ -47,7 +47,8 @@
47
47
  "postinstall": "node scripts/postinstall.mjs",
48
48
  "prepare": "husky",
49
49
  "glm-smoke": "tsx scripts/glm-smoke.mts",
50
- "seo:topics": "tsx scripts/seo-topics.mts"
50
+ "seo:topics": "tsx scripts/seo-topics.mts",
51
+ "ui-smoke": "tsx scripts/ui-smoke.mts"
51
52
  },
52
53
  "keywords": [
53
54
  "zames",