zames_pro 2.86.1 → 2.88.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.88.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - **ui:** switch the theme while the agent is generating
15
+
16
+ ## [2.87.0] - 2026-10-27
17
+
18
+ ### Added
19
+
20
+ - **ui:** light palettes, themed task badge, readable config menu on light themes
21
+
10
22
  ## [2.86.1] - 2026-10-27
11
23
 
12
24
  ### Fixed
package/README.md CHANGED
@@ -625,6 +625,10 @@ locale lives in `ui.locale` in the config file.
625
625
  `/theme` opens a pick list with a LIVE preview: the arrow keys repaint the
626
626
  terminal as you move, Enter saves, Esc restores the previous palette.
627
627
  `/theme <id>` applies a palette directly, `/theme list` prints every id.
628
+ The switch also works WHILE the agent is generating: `/theme <id>` and
629
+ `/config set ui.theme <id>` neither interrupt the turn nor touch the chat (it is
630
+ a pure terminal repaint); the bare `/theme` pick list is queued instead, because
631
+ it opens a full-screen list.
628
632
  The same setting is reachable from the settings side:
629
633
  `/config set ui.theme <id>` or the `/config` menu («Terminal color theme»).
630
634
  The switch is instant, no restart. `zames` is the default; the bundled palettes
@@ -633,7 +637,9 @@ are ports of well-known editor schemes:
633
637
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
634
638
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
635
639
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
636
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
640
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
641
+ plus the LIGHT ones: `one-light`, `github-light`, `solarized-light`,
642
+ `gruvbox-light`, `ayu-light`.
637
643
 
638
644
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
639
645
  highlighting inside them, the status row and the input frame. The terminal's
@@ -644,7 +650,7 @@ Terminal and others; where it is not supported nothing happens). Opt out with
644
650
  To make the choice
645
651
  concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
646
652
  below the list, in `/config` press `p` on the theme row); it is not dumped into
647
- the chat after a pick. Every one of the 24 themes has a screenshot in
653
+ the chat after a pick. Every one of the 29 themes has a screenshot in
648
654
  `docs/shots/`. The frame itself is a separate
649
655
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
650
656
  (default `round`; `none` removes it). A multi-line operator message keeps its
@@ -1365,6 +1371,10 @@ enum переключаются на месте; числа и строки от
1365
1371
  `/theme` открывает список тем с живым предпросмотром: стрелками перебираете —
1366
1372
  терминал сразу перекрашивается, Enter сохраняет, Esc возвращает прежнюю.
1367
1373
  `/theme <id>` применяет тему сразу, `/theme list` показывает все id.
1374
+ Переключение работает и ВО ВРЕМЯ работы агента: `/theme <id>` и
1375
+ `/config set ui.theme <id>` не прерывают генерацию и не трогают чат (это чистая
1376
+ перекраска терминала); голый `/theme` со списком ставится в очередь, т.к.
1377
+ открывает полноэкранный список.
1368
1378
  То же самое доступно и через настройки: `/config set ui.theme <id>` или меню
1369
1379
  `/config`, строка «Цветовая тема терминала». Переключение мгновенное, без
1370
1380
  перезапуска. Тема по умолчанию — `zames`; в комплекте порты известных схем
@@ -1373,7 +1383,9 @@ enum переключаются на месте; числа и строки от
1373
1383
  `zames` (по умолчанию), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
1374
1384
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
1375
1385
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
1376
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
1386
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
1387
+ а также СВЕТЛЫЕ: `one-light`, `github-light`, `solarized-light`,
1388
+ `gruvbox-light`, `ayu-light`.
1377
1389
 
1378
1390
  Тема перекрашивает ВСЁ: интерфейс, ответы агента, подсветку кода в ответах,
1379
1391
  строку статуса и рамку ввода. ФОН окна терминала тоже перекрашивается: тема
@@ -1383,7 +1395,7 @@ kitty, WezTerm, Alacritty, GNOME Terminal и др.; где не поддержи
1383
1395
  Чтобы было видно, ЧТО меняется, перед выбором
1384
1396
  показывается образец сессии в этой палитре (в `/theme` — под списком, в
1385
1397
  `/config` — клавиша `p` на строке темы); он не печатается в чат после выбора.
1386
- Все 24 темы есть на скриншотах в `docs/shots/`. Саму рамку можно менять отдельно:
1398
+ Все 29 тем есть на скриншотах в `docs/shots/`. Саму рамку можно менять отдельно:
1387
1399
  `/config set ui.inputBorder <round|single|double|bold|none>` (по умолчанию
1388
1400
  `round`, `none` — без рамки). Рамку (маркер `▌`) у многострочного сообщения
1389
1401
  пользователя видно на каждой строке.
package/dist/commands.js CHANGED
@@ -947,6 +947,18 @@ export function missingCommandArgs(names, rest) {
947
947
  export function isLiveConfigCommand(text) {
948
948
  return /^\/config\s+(?!menu\b|ui\b)\S+/i.test(String(text ?? '').trim());
949
949
  }
950
+ /**
951
+ * `/theme <id>` is safe WHILE the agent works: it only repaints the terminal
952
+ * (the theme object and the Ink colour map are mutated in place) and never
953
+ * touches the browser, the chat or the in-flight generation. So it is
954
+ * intercepted like `/config set` instead of being queued until the task ends.
955
+ *
956
+ * `/theme` with NO argument opens the pick LIST in the TUI, which pauses the
957
+ * editor and would fight the running task — that form stays queued.
958
+ */
959
+ export function isLiveThemeCommand(text) {
960
+ return /^\/theme\s+\S+/i.test(String(text ?? '').trim());
961
+ }
950
962
  // Parse a `/queue` invocation. Returns the subcommand ('list' | 'clear') or
951
963
  // null when the text is not a /queue command at all. This runs WHILE the agent
952
964
  // is busy (intercepted in the input handler), because the main command loop is
package/dist/index.js CHANGED
@@ -1807,6 +1807,12 @@ async function main() {
1807
1807
  void handleConfigCommand(text);
1808
1808
  return;
1809
1809
  }
1810
+ // `/theme <id>` is safe mid-run too: a pure repaint. The bare `/theme`
1811
+ // opens the pick list (pauses the editor) and stays queued.
1812
+ if (mod.commands.isLiveThemeCommand(text)) {
1813
+ void handleThemeCommand(text);
1814
+ return;
1815
+ }
1810
1816
  // `/goal` is safe mid-run: it only sets a variable and writes a file.
1811
1817
  if (/^\/goal(\s|$)/i.test(text.trim())) {
1812
1818
  void handleGoal(text);
@@ -0,0 +1,115 @@
1
+ // The LIGHT palettes, appended to the main palette map. They live in their own
2
+ // module only to keep src/theme.ts readable; the TYPE of a palette and the
3
+ // registry stay there.
4
+ // A dark terminal with a light theme looks broken: these are picked together
5
+ // with the window background (OSC-11), so the whole surface flips. The hues are
6
+ // the LIGHT variants of the corresponding editor schemes (One Light, GitHub
7
+ // Light, Solarized Light, Gruvbox Light, Ayu Light), not a desaturated copy of
8
+ // the dark ones.
9
+ export const LIGHT_PALETTES = {
10
+ // One Light — Atom's light counterpart of One Dark.
11
+ 'one-light': {
12
+ bg: '#fafafa',
13
+ user: '#0184bc',
14
+ userBar: '#7fb3cc',
15
+ turnRule: '#e5e5e6',
16
+ turnLabel: '#a0a1a7',
17
+ prompt: '#c18401',
18
+ dir: '#0184bc',
19
+ assistant: '#383a42',
20
+ tool: '#986801',
21
+ toolResult: '#696c77',
22
+ system: '#696c77',
23
+ dim: '#a0a1a7',
24
+ taskSummary: '#4078f2',
25
+ warn: '#c18401',
26
+ error: '#e45649',
27
+ success: '#50a14f',
28
+ brown: '#986801',
29
+ toggleOn: '#0184bc',
30
+ },
31
+ // GitHub Light — GitHub's light UI.
32
+ 'github-light': {
33
+ bg: '#ffffff',
34
+ user: '#0969da',
35
+ userBar: '#7ab0e8',
36
+ turnRule: '#eaeef2',
37
+ turnLabel: '#8c959f',
38
+ prompt: '#9a6700',
39
+ dir: '#0550ae',
40
+ assistant: '#1f2328',
41
+ tool: '#bc4c00',
42
+ toolResult: '#57606a',
43
+ system: '#57606a',
44
+ dim: '#8c959f',
45
+ taskSummary: '#0550ae',
46
+ warn: '#9a6700',
47
+ error: '#cf222e',
48
+ success: '#1a7f37',
49
+ brown: '#bc4c00',
50
+ toggleOn: '#0a7ea4',
51
+ },
52
+ // Solarized Light — the light side of Ethan Schoonover's palette.
53
+ 'solarized-light': {
54
+ bg: '#fdf6e3',
55
+ user: '#268bd2',
56
+ userBar: '#93b8d4',
57
+ turnRule: '#eee8d5',
58
+ turnLabel: '#93a1a1',
59
+ prompt: '#b58900',
60
+ dir: '#2aa198',
61
+ assistant: '#657b83',
62
+ tool: '#cb4b16',
63
+ toolResult: '#839496',
64
+ system: '#839496',
65
+ dim: '#93a1a1',
66
+ taskSummary: '#2aa198',
67
+ warn: '#b58900',
68
+ error: '#dc322f',
69
+ success: '#859900',
70
+ brown: '#cb4b16',
71
+ toggleOn: '#2aa198',
72
+ },
73
+ // Gruvbox Light — the warm light variant.
74
+ 'gruvbox-light': {
75
+ bg: '#fbf1c7',
76
+ user: '#458588',
77
+ userBar: '#8fb5b6',
78
+ turnRule: '#ebdbb2',
79
+ turnLabel: '#a89984',
80
+ prompt: '#b57614',
81
+ dir: '#458588',
82
+ assistant: '#3c3836',
83
+ tool: '#af3a03',
84
+ toolResult: '#7c6f64',
85
+ system: '#7c6f64',
86
+ dim: '#a89984',
87
+ taskSummary: '#79740e',
88
+ warn: '#b57614',
89
+ error: '#9d0006',
90
+ success: '#79740e',
91
+ brown: '#af3a03',
92
+ toggleOn: '#427b58',
93
+ },
94
+ // Ayu Light — the light side of the Ayu scheme.
95
+ 'ayu-light': {
96
+ bg: '#fafafa',
97
+ user: '#399ee6',
98
+ userBar: '#9dc8ea',
99
+ turnRule: '#eceef0',
100
+ turnLabel: '#abb0b6',
101
+ prompt: '#fa8d3e',
102
+ dir: '#399ee6',
103
+ assistant: '#5c6166',
104
+ tool: '#f2ae49',
105
+ toolResult: '#787b80',
106
+ system: '#787b80',
107
+ dim: '#abb0b6',
108
+ taskSummary: '#55b4d4',
109
+ warn: '#fa8d3e',
110
+ error: '#f51818',
111
+ success: '#86b300',
112
+ brown: '#f2ae49',
113
+ toggleOn: '#4cbf99',
114
+ },
115
+ };
package/dist/theme.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { contentWidth } from './width.js';
3
3
  import { applyBackground } from './tui/background.js';
4
+ import { LIGHT_PALETTES } from './theme-light.js';
4
5
  // Respect NO_COLOR (https://no-color.org): when the operator sets it (any
5
6
  // non-empty value) drop ALL SGR codes so the output can be piped into a file
6
7
  // or a log viewer without escape noise. chalk 5 only auto-detects TTY/CI, it
@@ -519,6 +520,11 @@ export const PALETTES = {
519
520
  toggleOn: '#7aa89f',
520
521
  },
521
522
  };
523
+ /** Every palette: the dark ones plus the light variants. */
524
+ export const ALL_PALETTES = {
525
+ ...PALETTES,
526
+ ...LIGHT_PALETTES,
527
+ };
522
528
  /** The palette id used when the configured theme is unknown/missing. */
523
529
  export const DEFAULT_THEME = 'zames';
524
530
  /**
@@ -527,11 +533,12 @@ export const DEFAULT_THEME = 'zames';
527
533
  * leave the operator with an unstyled terminal).
528
534
  */
529
535
  export function isThemeName(v) {
530
- return (typeof v === 'string' && Object.prototype.hasOwnProperty.call(PALETTES, v));
536
+ return (typeof v === 'string' &&
537
+ Object.prototype.hasOwnProperty.call(ALL_PALETTES, v));
531
538
  }
532
539
  /** All palette ids, in the order they are offered in /config. */
533
540
  export function themeNames() {
534
- return Object.keys(PALETTES);
541
+ return Object.keys(ALL_PALETTES);
535
542
  }
536
543
  /** Build the chalk formatters for one palette. */
537
544
  function buildTheme(p) {
@@ -561,7 +568,7 @@ function buildTheme(p) {
561
568
  // the switch without being re-imported — this is why the call sites read
562
569
  // `theme.x` at RENDER time instead of destructuring a color at module load.
563
570
  let activeName = DEFAULT_THEME;
564
- export const theme = buildTheme(PALETTES[DEFAULT_THEME]);
571
+ export const theme = buildTheme(ALL_PALETTES[DEFAULT_THEME]);
565
572
  /** The palette id currently rendered. */
566
573
  export function currentThemeName() {
567
574
  return activeName;
@@ -573,7 +580,7 @@ export function currentThemeName() {
573
580
  */
574
581
  export function applyTheme(name) {
575
582
  const next = isThemeName(name) ? name : DEFAULT_THEME;
576
- const built = buildTheme(PALETTES[next]);
583
+ const built = buildTheme(ALL_PALETTES[next]);
577
584
  for (const key of Object.keys(built)) {
578
585
  theme[key] = built[key];
579
586
  }
@@ -589,7 +596,7 @@ export function applyTheme(name) {
589
596
  * README screenshot generator, which renders the real colors as HTML.
590
597
  */
591
598
  export function themeColors() {
592
- return PALETTES[activeName];
599
+ return ALL_PALETTES[activeName];
593
600
  }
594
601
  // Horizontal rule that separates the model's answer from the next block. It
595
602
  // spans the SHARED content width (src/width.ts) so the rule, the answers and
@@ -40,6 +40,10 @@ function displayValue(field, value, t) {
40
40
  }
41
41
  export function ConfigMenuApp(opts) {
42
42
  const { fields, t, get, set, reset, getDefault, locale = 'en' } = opts;
43
+ // The menu must be readable on a LIGHT palette too: Ink's Text without an
44
+ // explicit color falls back to the terminal default (often white), which is
45
+ // invisible on a light background. Every row text below is therefore painted
46
+ // with an explicit palette color (UI.text for values, UI.title for groups).
43
47
  const { onAfterChange } = opts;
44
48
  const { exit } = useApp();
45
49
  // The hint bar is operator text: it follows the SAME locale the labels use
@@ -340,7 +344,7 @@ export function ConfigMenuApp(opts) {
340
344
  const f = visible[i];
341
345
  const g = f.groupKey;
342
346
  if (i === shown.start || g !== lastGroup) {
343
- items.push(_jsx(Text, { color: UI.system, children: ' ' + t(f.groupKey).toUpperCase() }, 'g' + i));
347
+ items.push(_jsx(Text, { color: UI.title, children: ' ' + t(f.groupKey).toUpperCase() }, 'g' + i));
344
348
  lastGroup = g;
345
349
  }
346
350
  const selected = i === cursor;
@@ -360,7 +364,7 @@ export function ConfigMenuApp(opts) {
360
364
  else {
361
365
  valueCell = (_jsx(Text, { color: selected ? UI.text : UI.system, children: displayValue(f, raw, t) }));
362
366
  }
363
- items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : undefined, bold: selected, children: label }), _jsx(Text, { color: UI.dim, children: ' ' }), valueCell, mod ? _jsx(Text, { color: UI.warn, children: ' ' + mod }) : null, selected ? _jsx(Text, { color: UI.dim, children: ' ' + f.path }) : null] }, 'f' + i));
367
+ items.push(_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: selected ? '❯ ' : ' ' }), _jsx(Text, { color: selected ? UI.accent : UI.text, bold: selected, children: label }), _jsx(Text, { color: UI.dim, children: ' ' }), valueCell, mod ? _jsx(Text, { color: UI.warn, children: ' ' + mod }) : null, selected ? _jsx(Text, { color: UI.dim, children: ' ' + f.path }) : null] }, 'f' + i));
364
368
  }
365
369
  if (!visible.length) {
366
370
  items.push(_jsx(Text, { color: UI.dim, children: ' ' + t('cfg.menu.no_match') }, "none"));
@@ -24,6 +24,43 @@ function stateLabel(state, locale) {
24
24
  }
25
25
  return '';
26
26
  }
27
+ /**
28
+ * The LEFT part of the status row (spinner/pause/scroll marker) as a string.
29
+ * Extracted from the component so the text and its rules are unit-testable
30
+ * without a terminal; the component only truncates and colorizes it.
31
+ */
32
+ export function statusLeftText(state, t, frame = 0) {
33
+ const animating = state.busy || !!state.animBase;
34
+ const stateLbl = stateLabel(state, state.locale);
35
+ let left;
36
+ if (state.pendingText) {
37
+ left = '✎ ' + state.pendingText + ' · ' + t('spinner.hint');
38
+ }
39
+ else if (state.animBase) {
40
+ const dots = DOTS[frame % DOTS.length] ?? '';
41
+ const pad = ' '.slice(dots.length);
42
+ left =
43
+ stripEllipsis(state.animBase) +
44
+ dots +
45
+ pad +
46
+ elapsedLabel(state.animStart, animating) +
47
+ ' · ' +
48
+ t('spinner.hint');
49
+ }
50
+ else if (state.statusText) {
51
+ left = state.statusText;
52
+ }
53
+ else {
54
+ left = '';
55
+ }
56
+ const prefix = stateLbl ? stateLbl + ' ' : '';
57
+ // The scrolled-up marker carries NO timer: the elapsed label belongs to an
58
+ // animation, and a counter running next to «scrolled 5 lines» only ate width.
59
+ const scrollNote = state.scroll > 0
60
+ ? translate(state.locale)('status.scrolled', { n: String(state.scroll) })
61
+ : '';
62
+ return scrollNote || prefix + left;
63
+ }
27
64
  /** Visible width of the badge row (used to reserve space for the left
28
65
  * status). Measured with the shared `visLen` so wide chars (CJK/emoji) count
29
66
  * as 2 and the emoji's text-presentation selector (\uFE0E) as 0 — a plain
@@ -57,103 +94,72 @@ export function badgeTextWidth(state) {
57
94
  }
58
95
  return w;
59
96
  }
60
- /** The badge row: statusLine, provider, queue, tasks, toggles, context.
61
- * `maxWidth` caps the row so it never wraps to a second line; when the badges
62
- * do not fit, the lowest-priority ones (statusLine, then provider) are
63
- * dropped first so the context counter — the most useful — survives. */
64
- function Badges({ state, budget = Infinity, }) {
65
- // Each badge is (node, visible width); lowest-priority first so the context
66
- // counter (last) survives when the row is tight.
67
- const all = [];
68
- const add = (key, w, node) => {
69
- all.push({ key, w, node });
70
- };
97
+ /**
98
+ * The badges to show, in priority order (the LAST survives a narrow terminal).
99
+ * The badge row is what tells the operator WHAT is on — provider, queue, the
100
+ * task counter, the toggles and the context fill — so it is derived here, once,
101
+ * with a palette role each, instead of being hardcoded in the component. The
102
+ * task counter used to be painted with the generic `system` gray, which is why
103
+ * it looked "not themed" next to the provider.
104
+ */
105
+ export function statusBadges(state) {
106
+ const out = [];
71
107
  if (state.statusLine) {
72
- add('sl', visLen(state.statusLine) + 1, _jsxs(Text, { color: UI.system, children: [state.statusLine, ' '] }, "sl"));
108
+ out.push({ key: 'sl', label: state.statusLine + ' ', role: 'dim' });
73
109
  }
74
110
  if (state.provider) {
75
- add('prov', visLen('[' + state.provider + '] '), _jsx(Text, { color: UI.system, children: '[' + state.provider + '] ' }, "prov"));
111
+ out.push({
112
+ key: 'prov',
113
+ label: '[' + state.provider + '] ',
114
+ role: 'system',
115
+ });
76
116
  }
77
117
  if (state.queueLength > 0) {
78
- add('q', visLen('⧗' + state.queueLength + ' '), _jsxs(Text, { color: UI.warn, children: ['⧗' + state.queueLength, ' '] }, "q"));
118
+ out.push({ key: 'q', label: '⧗' + state.queueLength + ' ', role: 'warn' });
79
119
  }
80
120
  if (state.tasks) {
81
- add('t', visLen(state.tasks) + 1, _jsxs(Text, { color: UI.system, children: [state.tasks, ' '] }, "t"));
121
+ // The task counter is the MODEL's todo progress: paint it with the same
122
+ // role the per-task summary line uses, so it reads as agent activity, not
123
+ // as the generic service gray it had before.
124
+ out.push({ key: 't', label: state.tasks + ' ', role: 'title' });
82
125
  }
83
126
  const toggles = [];
84
127
  if (state.thinkingEnabled)
85
- toggles.push('🧠\uFE0E');
128
+ toggles.push('🧠︎');
86
129
  if (state.searchEnabled)
87
- toggles.push('🌐\uFE0E');
130
+ toggles.push('🌐︎');
88
131
  if (toggles.length) {
89
- const label = toggles.join(' ') + ' ';
90
- add('tg', visLen(label), _jsx(Text, { color: UI.on, children: label }, "tg"));
132
+ out.push({ key: 'tg', label: toggles.join(' ') + ' ', role: 'on' });
91
133
  }
92
134
  if (state.contextText) {
93
- const color = state.contextLevel === 'high'
94
- ? UI.err
135
+ const role = state.contextLevel === 'high'
136
+ ? 'err'
95
137
  : state.contextLevel === 'warn'
96
- ? UI.warn
97
- : UI.ok;
138
+ ? 'warn'
139
+ : 'ok';
98
140
  const marker = state.contextLevel === 'high'
99
141
  ? '⛔ '
100
142
  : state.contextLevel === 'warn'
101
143
  ? '▲ '
102
144
  : '';
103
- const label = marker + state.contextText;
104
- add('ctx', visLen(label), _jsx(Text, { color: color, children: label }, "ctx"));
145
+ out.push({ key: 'ctx', label: marker + state.contextText, role });
105
146
  }
147
+ return out;
148
+ }
149
+ /** The badge row. `budget` caps the row so it never wraps to a second line;
150
+ * when the badges do not fit, the lowest-priority ones (statusLine, then
151
+ * provider) are dropped first so the context counter survives. */
152
+ function Badges({ state, budget = Infinity, }) {
153
+ const all = statusBadges(state);
106
154
  if (!all.length)
107
155
  return null;
108
- // Drop badges from the FRONT (statusLine, provider, ... least important)
109
- // until the row fits the budget. Keep at least the last (context).
110
- let total = all.reduce((n, b) => n + b.w, 0);
156
+ let total = all.reduce((n, b) => n + visLen(b.label), 0);
111
157
  let start = 0;
112
158
  while (total > budget && start < all.length - 1) {
113
- total -= all[start].w;
159
+ total -= visLen(all[start].label);
114
160
  start++;
115
161
  }
116
- return _jsx(_Fragment, { children: all.slice(start).map((b) => b.node) });
117
- }
118
- /**
119
- * The LEFT part of the status row (spinner/pause/scroll marker) as a string.
120
- * Extracted from the component so the text and its rules are unit-testable
121
- * without a terminal; the component only truncates and colorizes it.
122
- */
123
- export function statusLeftText(state, t, frame = 0) {
124
- const animating = state.busy || !!state.animBase;
125
- const stateLbl = stateLabel(state, state.locale);
126
- let left;
127
- if (state.pendingText) {
128
- left = '✎ ' + state.pendingText + ' · ' + t('spinner.hint');
129
- }
130
- else if (state.animBase) {
131
- const dots = DOTS[frame % DOTS.length] ?? '';
132
- const pad = ' '.slice(dots.length);
133
- left =
134
- stripEllipsis(state.animBase) +
135
- dots +
136
- pad +
137
- elapsedLabel(state.animStart, animating) +
138
- ' · ' +
139
- t('spinner.hint');
140
- }
141
- else if (state.statusText) {
142
- left = state.statusText;
143
- }
144
- else {
145
- left = '';
146
- }
147
- const prefix = stateLbl ? stateLbl + ' ' : '';
148
- // When the log is scrolled up, show a marker in the status line (NOT as a
149
- // separate row below the input — that pushed the panel up and made the input
150
- // jump). It replaces the spinner text while scrolled, so it never adds a row.
151
- // It carries NO timer: the elapsed label belongs to an animation, and a
152
- // counter running next to «scrolled 5 lines» only ate width.
153
- const scrollNote = state.scroll > 0
154
- ? translate(state.locale)('status.scrolled', { n: String(state.scroll) })
155
- : '';
156
- return scrollNote || prefix + left;
162
+ return (_jsx(_Fragment, { children: all.slice(start).map((b) => (_jsx(Text, { color: UI[b.role], children: b.label }, b.key))) }));
157
163
  }
158
164
  export function StatusBar({ state, divider = true, }) {
159
165
  // Drive the spinner from Ink's shared animation timer (one timer for the
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // The OSC sequence is written STRAIGHT to stdout (not through chalk), because
13
13
  // it is not an SGR attribute: `\x1b]11;rgb:rr/gg/bb\x07`.
14
- import { PALETTES, isThemeName, DEFAULT_THEME } from '../theme.js';
14
+ import { ALL_PALETTES, isThemeName, DEFAULT_THEME } from '../theme.js';
15
15
  /** `#rrggbb` -> the `rr/gg/bb` part of an OSC-11 payload. */
16
16
  function hexToOsc(hex) {
17
17
  const h = hex.replace('#', '');
@@ -26,7 +26,7 @@ export function backgroundOsc(name) {
26
26
  if (process.env.NO_COLOR || process.env.ZAMES_NO_BG)
27
27
  return '';
28
28
  const id = isThemeName(name) ? name : DEFAULT_THEME;
29
- return '\x1b]11;rgb:' + hexToOsc(PALETTES[id].bg) + '\x07';
29
+ return '\x1b]11;rgb:' + hexToOsc(ALL_PALETTES[id].bg) + '\x07';
30
30
  }
31
31
  /** Paint the window background for the palette `name` (best effort). */
32
32
  export function applyBackground(name) {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import chalk from 'chalk';
4
- import { PALETTES, DEFAULT_THEME, isThemeName } from '../theme.js';
4
+ import { ALL_PALETTES, DEFAULT_THEME, isThemeName } from '../theme.js';
5
5
  /**
6
6
  * The sample lines. Kept PURE DATA so tests can assert the preview covers the
7
7
  * important roles and the Ink/plain renderers cannot drift apart.
@@ -23,7 +23,7 @@ export function previewLines() {
23
23
  ];
24
24
  }
25
25
  function paletteOf(name) {
26
- return PALETTES[isThemeName(name) ? name : DEFAULT_THEME];
26
+ return ALL_PALETTES[isThemeName(name) ? name : DEFAULT_THEME];
27
27
  }
28
28
  /** The preview as one chalk-painted string (for a plain terminal line). */
29
29
  export function themePreviewAnsi(name) {
@@ -7,7 +7,7 @@
7
7
  // reads `UI.x` at RENDER time, so a /config theme switch repaints without
8
8
  // re-importing anything. Components must never destructure a color at module
9
9
  // load — that would freeze the palette at startup.
10
- import { PALETTES, DEFAULT_THEME, isThemeName } from '../theme.js';
10
+ import { ALL_PALETTES, DEFAULT_THEME, isThemeName, } from '../theme.js';
11
11
  /** Derive the Ink map from a terminal palette. */
12
12
  export function uiColorsFrom(p) {
13
13
  return {
@@ -25,7 +25,7 @@ export function uiColorsFrom(p) {
25
25
  title: p.taskSummary,
26
26
  };
27
27
  }
28
- export const UI = uiColorsFrom(PALETTES[DEFAULT_THEME]);
28
+ export const UI = uiColorsFrom(ALL_PALETTES[DEFAULT_THEME]);
29
29
  // Border STYLE of the framed surfaces (the input box today). Ink only ships a
30
30
  // handful of box styles; 'none' draws no frame at all. Kept next to the colors
31
31
  // because it is the same kind of presentation state, and mutated in place so a
@@ -59,7 +59,7 @@ export function inputBorderCols() {
59
59
  /** Repaint the Ink widgets with the palette of `name` (unknown -> default). */
60
60
  export function syncUiColors(name) {
61
61
  const id = isThemeName(name) ? name : DEFAULT_THEME;
62
- const next = uiColorsFrom(PALETTES[id]);
62
+ const next = uiColorsFrom(ALL_PALETTES[id]);
63
63
  for (const key of Object.keys(next)) {
64
64
  UI[key] = next[key];
65
65
  }
@@ -7,12 +7,18 @@ export { UI } from './ui-colors.js';
7
7
  import { UI } from './ui-colors.js';
8
8
  /** `[x]` / `[ ]` — a checkbox for a boolean value. */
9
9
  export function Checkbox({ checked, focused, }) {
10
+ // A checkbox is ONE unit: `[x]`/`[ ]` must not change color inside the
11
+ // brackets — an unchecked box used to render as two overlapping `[ ]` glyphs
12
+ // because the three parts were painted with different colors.
10
13
  return (_jsx(Text, { color: checked ? UI.on : UI.off, bold: focused, children: checked ? '[x]' : '[ ]' }));
11
14
  }
12
15
  /** `‹ value ›` — an inline SELECT for an enum value. The arrows hint that
13
16
  * ←/→ (or Space) cycle the option. */
14
17
  export function Select({ value, focused, }) {
15
- return (_jsxs(Text, { children: [_jsx(Text, { color: UI.off, children: '‹ ' }), _jsx(Text, { color: focused ? UI.accent : UI.text, bold: focused, children: value }), _jsx(Text, { color: UI.off, children: ' ›' })] }));
18
+ // A single Text: one SGR run per glyph (`‹` + value + `›`) let some terminals
19
+ // render the arrows as a SECOND row of the same cell, and on a LIGHT palette
20
+ // the dim `off` color made the arrows nearly invisible. One run, one color.
21
+ return (_jsx(Text, { color: focused ? UI.accent : UI.text, bold: focused, children: '‹ ' + value + ' ›' }));
16
22
  }
17
23
  /** A row of hint chips separated by ` · `. */
18
24
  export function HintBar({ hints, }) {
package/llms-full.txt CHANGED
@@ -610,6 +610,10 @@ locale lives in `ui.locale` in the config file.
610
610
  `/theme` opens a pick list with a LIVE preview: the arrow keys repaint the
611
611
  terminal as you move, Enter saves, Esc restores the previous palette.
612
612
  `/theme <id>` applies a palette directly, `/theme list` prints every id.
613
+ The switch also works WHILE the agent is generating: `/theme <id>` and
614
+ `/config set ui.theme <id>` neither interrupt the turn nor touch the chat (it is
615
+ a pure terminal repaint); the bare `/theme` pick list is queued instead, because
616
+ it opens a full-screen list.
613
617
  The same setting is reachable from the settings side:
614
618
  `/config set ui.theme <id>` or the `/config` menu («Terminal color theme»).
615
619
  The switch is instant, no restart. `zames` is the default; the bundled palettes
@@ -618,7 +622,9 @@ are ports of well-known editor schemes:
618
622
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
619
623
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
620
624
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
621
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
625
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
626
+ plus the LIGHT ones: `one-light`, `github-light`, `solarized-light`,
627
+ `gruvbox-light`, `ayu-light`.
622
628
 
623
629
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
624
630
  highlighting inside them, the status row and the input frame. The terminal's
@@ -629,7 +635,7 @@ Terminal and others; where it is not supported nothing happens). Opt out with
629
635
  To make the choice
630
636
  concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
631
637
  below the list, in `/config` press `p` on the theme row); it is not dumped into
632
- the chat after a pick. Every one of the 24 themes has a screenshot in
638
+ the chat after a pick. Every one of the 29 themes has a screenshot in
633
639
  `docs/shots/`. The frame itself is a separate
634
640
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
635
641
  (default `round`; `none` removes it). A multi-line operator message keeps its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.86.1",
3
+ "version": "2.88.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": {