zames_pro 2.86.0 → 2.87.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.87.0] - 2026-10-27
11
+
12
+ ### Added
13
+
14
+ - **ui:** light palettes, themed task badge, readable config menu on light themes
15
+
16
+ ## [2.86.1] - 2026-10-27
17
+
18
+ ### Fixed
19
+
20
+ - **ui:** the operator bar on every row, screenshots for all themes
21
+
10
22
  ## [2.86.0] - 2026-10-27
11
23
 
12
24
  ### Added
package/README.md CHANGED
@@ -633,7 +633,9 @@ are ports of well-known editor schemes:
633
633
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
634
634
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
635
635
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
636
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
636
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
637
+ plus the LIGHT ones: `one-light`, `github-light`, `solarized-light`,
638
+ `gruvbox-light`, `ayu-light`.
637
639
 
638
640
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
639
641
  highlighting inside them, the status row and the input frame. The terminal's
@@ -643,10 +645,12 @@ Terminal and others; where it is not supported nothing happens). Opt out with
643
645
  `ZAMES_NO_BG=1` or `NO_COLOR`.
644
646
  To make the choice
645
647
  concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
646
- below the list, in `/config` press `p` on the theme row); after
647
- `/theme <id>` the sample is printed too. The frame itself is a separate
648
+ below the list, in `/config` press `p` on the theme row); it is not dumped into
649
+ the chat after a pick. Every one of the 29 themes has a screenshot in
650
+ `docs/shots/`. The frame itself is a separate
648
651
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
649
- (default `round`; `none` removes it).
652
+ (default `round`; `none` removes it). A multi-line operator message keeps its
653
+ `▌` bar on every row.
650
654
 
651
655
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
652
656
 
@@ -1371,7 +1375,9 @@ enum переключаются на месте; числа и строки от
1371
1375
  `zames` (по умолчанию), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
1372
1376
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
1373
1377
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
1374
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
1378
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
1379
+ а также СВЕТЛЫЕ: `one-light`, `github-light`, `solarized-light`,
1380
+ `gruvbox-light`, `ayu-light`.
1375
1381
 
1376
1382
  Тема перекрашивает ВСЁ: интерфейс, ответы агента, подсветку кода в ответах,
1377
1383
  строку статуса и рамку ввода. ФОН окна терминала тоже перекрашивается: тема
@@ -1380,10 +1386,11 @@ kitty, WezTerm, Alacritty, GNOME Terminal и др.; где не поддержи
1380
1386
  ничего не происходит). Отключить фон: `ZAMES_NO_BG=1` или `NO_COLOR`.
1381
1387
  Чтобы было видно, ЧТО меняется, перед выбором
1382
1388
  показывается образец сессии в этой палитре (в `/theme` — под списком, в
1383
- `/config` — клавиша `p` на строке темы). После применения `/theme <id>`
1384
- образец печатается в терминал. Саму рамку можно менять отдельно:
1389
+ `/config` — клавиша `p` на строке темы); он не печатается в чат после выбора.
1390
+ Все 29 тем есть на скриншотах в `docs/shots/`. Саму рамку можно менять отдельно:
1385
1391
  `/config set ui.inputBorder <round|single|double|bold|none>` (по умолчанию
1386
- `round`, `none` — без рамки).
1392
+ `round`, `none` — без рамки). Рамку (маркер `▌`) у многострочного сообщения
1393
+ пользователя видно на каждой строке.
1387
1394
 
1388
1395
  Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo,
1389
1396
  сессии.
package/dist/input.js CHANGED
@@ -1192,9 +1192,14 @@ export class LineEditor {
1192
1192
  // to the browser layer to be attached to the chat.
1193
1193
  const attached = this.attachments.items.slice();
1194
1194
  // A turn separator + a BAR-marked operator message (same look as the TUI).
1195
+ // The bar goes on EVERY visual row: a long message wrapped to N lines used
1196
+ // to mark only the first one, and the continuation rows looked shifted.
1195
1197
  this.printAbove(theme.turnRule('─'.repeat(contentWidth())));
1196
- this.printAbove(theme.userBar('▌ ') +
1197
- theme.user(wrapToWidth(display, contentWidth() - 2)));
1198
+ const wrapped = wrapToWidth(display, contentWidth() - 2);
1199
+ this.printAbove(wrapped
1200
+ .split(String.fromCharCode(10))
1201
+ .map((line) => theme.userBar('▌ ') + theme.user(line))
1202
+ .join(String.fromCharCode(10)));
1198
1203
  this.pastes = [];
1199
1204
  this.attachments.reset();
1200
1205
  if (this.onSubmit)
@@ -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"));
@@ -9,6 +9,12 @@ function LogRow({ entry }) {
9
9
  // embedded ANSI escape sequences through to the terminal.
10
10
  return _jsx(Text, { children: entry.text });
11
11
  }
12
+ if (entry.bar) {
13
+ // An operator turn: `▌` on EVERY visual row. Applying the bar to the whole
14
+ // (already wrapped) block keeps the indent equal on continuation lines —
15
+ // prefixing only the first line made a multi-line message look ragged.
16
+ return _jsx(Text, { children: theme.userBar('▌ ') + theme.user(entry.text) });
17
+ }
12
18
  switch (entry.kind) {
13
19
  case 'user':
14
20
  return _jsx(Text, { children: theme.user('❯ ') + entry.text });
@@ -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
@@ -258,8 +258,9 @@ export class TuiEditor {
258
258
  // A turn separator + a BAR-marked, clearly-colored operator message, so an
259
259
  // operator turn never blends with the model's answer (operator request).
260
260
  this.printAbove(theme.turnRule('─'.repeat(contentWidth())));
261
- this.printAbove(theme.userBar('▌ ') +
262
- theme.user(wrapToWidth(display, contentWidth() - 2)));
261
+ // The BAR is a per-row marker, drawn by LogView (entry.bar), so every
262
+ // visual row of a long message starts with `▌` instead of only the first.
263
+ this.store.pushLog('user', wrapToWidth(display, contentWidth() - 2), false, { bar: true });
263
264
  this.store.set({ pastes: [] });
264
265
  // Sending a message pins the log back to the newest content: the operator
265
266
  // expects their message (and the answer) at the BOTTOM, so a viewport left
@@ -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) {
package/dist/tui/store.js CHANGED
@@ -83,8 +83,14 @@ export class TuiStore {
83
83
  this.set(fn(this.state));
84
84
  }
85
85
  /** Append one permanent log entry (an Ink `<Static>` item). */
86
- pushLog(kind, text, raw = false) {
87
- const entry = { id: this.nextLogId++, kind, text, raw };
86
+ pushLog(kind, text, raw = false, extra) {
87
+ const entry = {
88
+ id: this.nextLogId++,
89
+ kind,
90
+ text,
91
+ raw,
92
+ ...extra,
93
+ };
88
94
  this.state = { ...this.state, log: [...this.state.log, entry] };
89
95
  this.emit();
90
96
  }
@@ -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
@@ -618,7 +618,9 @@ are ports of well-known editor schemes:
618
618
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
619
619
  `solarized-dark`, `tokyo-night`, `tokyo-storm`, `catppuccin`, `github-dark`,
620
620
  `ayu`, `rose-pine`, `everforest`, `kanagawa`, `night-owl`, `material`,
621
- `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`.
621
+ `palenight`, `zenburn`, `selenized`, `iceberg`, `sonokai`, `aurora`, `cobalt2`,
622
+ plus the LIGHT ones: `one-light`, `github-light`, `solarized-light`,
623
+ `gruvbox-light`, `ayu-light`.
622
624
 
623
625
  A theme repaints EVERYTHING: the interface, the agent's answers, the code
624
626
  highlighting inside them, the status row and the input frame. The terminal's
@@ -628,10 +630,12 @@ Terminal and others; where it is not supported nothing happens). Opt out with
628
630
  `ZAMES_NO_BG=1` or `NO_COLOR`.
629
631
  To make the choice
630
632
  concrete, a SAMPLE SESSION in that palette is shown before you pick (in `/theme`
631
- below the list, in `/config` press `p` on the theme row); after
632
- `/theme <id>` the sample is printed too. The frame itself is a separate
633
+ below the list, in `/config` press `p` on the theme row); it is not dumped into
634
+ the chat after a pick. Every one of the 29 themes has a screenshot in
635
+ `docs/shots/`. The frame itself is a separate
633
636
  setting: `/config set ui.inputBorder <round|single|double|bold|none>`
634
- (default `round`; `none` removes it).
637
+ (default `round`; `none` removes it). A multi-line operator message keeps its
638
+ `▌` bar on every row.
635
639
 
636
640
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
637
641
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.86.0",
3
+ "version": "2.87.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": {