zames_pro 2.83.1 → 2.83.4

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.83.4] - 2026-10-27
11
+
12
+ ### Fixed
13
+
14
+ - **ui:** theme the input text, the status row and the input frame
15
+
16
+ ## [2.83.2] - 2026-10-27
17
+
18
+ ### Fixed
19
+
20
+ - **ui:** repaint the model answer and the code highlighting on a theme switch
21
+
10
22
  ## [2.83.1] - 2026-10-27
11
23
 
12
24
  ### Added
package/README.md CHANGED
@@ -633,6 +633,11 @@ are ports of well-known editor schemes:
633
633
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
634
634
  `solarized-dark`, `tokyo-night`.
635
635
 
636
+ 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
+ setting: `/config set ui.inputBorder <round|single|double|bold|none>`
639
+ (default `round`; `none` removes it).
640
+
636
641
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
637
642
 
638
643
  ## FAQ
@@ -1356,6 +1361,10 @@ enum переключаются на месте; числа и строки от
1356
1361
  `zames` (по умолчанию), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
1357
1362
  `solarized-dark`, `tokyo-night`.
1358
1363
 
1364
+ Тема перекрашивает ВСЁ: интерфейс, ответы агента, подсветку кода в ответах и
1365
+ рамку ввода. Саму рамку можно менять отдельно: `/config set ui.inputBorder
1366
+ <round|single|double|bold|none>` (по умолчанию `round`, `none` — без рамки).
1367
+
1359
1368
  Данные агента хранятся в `~/.zames`: профиль браузера, логи, история undo,
1360
1369
  сессии.
1361
1370
 
package/dist/config.js CHANGED
@@ -134,6 +134,7 @@ export const DEFAULTS = {
134
134
  ui: {
135
135
  locale: DEFAULT_LOCALE,
136
136
  theme: DEFAULT_THEME,
137
+ inputBorder: 'round',
137
138
  contextLimit: 1_000_000,
138
139
  // 0 = auto: terminal width, capped at 100 columns (the historical width).
139
140
  answerWidth: 0,
@@ -291,6 +292,13 @@ export const CONFIG_SCHEMA = [
291
292
  labelKey: 'cfg.f.maxIterations',
292
293
  groupKey: 'cfg.group.agent',
293
294
  },
295
+ {
296
+ path: 'ui.inputBorder',
297
+ type: 'enum',
298
+ values: ['round', 'single', 'double', 'bold', 'none'],
299
+ labelKey: 'cfg.f.ui_inputBorder',
300
+ groupKey: 'cfg.group.ui',
301
+ },
294
302
  {
295
303
  path: 'ui.theme',
296
304
  type: 'enum',
@@ -55,6 +55,10 @@ export const configMessages = {
55
55
  ru: 'Язык интерфейса и ответов агента',
56
56
  en: 'Interface and agent reply language',
57
57
  },
58
+ 'cfg.f.ui_inputBorder': {
59
+ ru: 'Рамка строки ввода: round | single | double | bold | none',
60
+ en: 'Input box frame: round | single | double | bold | none',
61
+ },
58
62
  'cfg.f.ui_theme': {
59
63
  ru: 'Цветовая тема терминала',
60
64
  en: 'Terminal color theme',
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 { setInputBorder } from './tui/ui-colors.js';
8
9
  import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
9
10
  import { QwenBrowser } from './qwen-browser.js';
10
11
  import { GlmBrowser } from './glm-browser.js';
@@ -123,6 +124,10 @@ setAnswerWidth(config.ui?.answerWidth ?? 0);
123
124
  // banner, startup notices): theme.ts starts on the default and every call site
124
125
  // reads the live object, so one apply here repaints the whole session.
125
126
  applyTheme(config.ui?.theme);
127
+ // Same for the input-box FRAME style: the TUI reads it per render, so one
128
+ // apply before the editor is created covers the first paint (a /config change
129
+ // goes through setConfigRuntime below).
130
+ setInputBorder(config.ui?.inputBorder);
126
131
  // Current interface/agent language. Changed by the /config lang <ru|en> command.
127
132
  let currentLocale = isLocale(config.ui?.locale)
128
133
  ? config.ui.locale
@@ -1943,6 +1948,11 @@ async function main() {
1943
1948
  // object is shared and mutated in place, so every later render uses it;
1944
1949
  // the editor is asked for a full redraw because the rows already on screen
1945
1950
  // (status, prompt, hints) were painted with the OLD colors.
1951
+ if (path === 'ui.inputBorder') {
1952
+ setInputBorder(value);
1953
+ if (editor)
1954
+ editor.setTheme?.(themeColors());
1955
+ }
1946
1956
  if (path === 'ui.theme') {
1947
1957
  const applied = applyTheme(value);
1948
1958
  config.ui.theme = applied;
@@ -2176,8 +2186,9 @@ async function main() {
2176
2186
  // read the shared UI map, and without a bump the operator would cycle
2177
2187
  // palette ids while the colors stayed those of the previous one.
2178
2188
  onAfterChange: (field) => {
2179
- if (field.path === 'ui.theme')
2189
+ if (field.path === 'ui.theme' || field.path === 'ui.inputBorder') {
2180
2190
  editor?.setTheme?.(themeColors());
2191
+ }
2181
2192
  },
2182
2193
  locale: currentLocale,
2183
2194
  };
@@ -353,24 +353,26 @@ export function truncateToWidth(s, maxCols) {
353
353
  const reset = out.includes(ESC + '[') ? ESC + '[0m' : '';
354
354
  return out + reset + '…';
355
355
  }
356
- // Inner content width of the framed input box: the rounded border takes 1
357
- // column on each side and the box adds 1 column of padding on each side, so
358
- // the text must wrap to `cols - 4`. Exported so the layout ABOVE the box
356
+ // Inner content width of the framed input box: the border takes 1 column on
357
+ // each side (2 total, 0 when ui.inputBorder is 'none') and the box adds
358
+ // 1 column of padding on each side, so the text wraps to `cols - border - 2`. Exported so the layout ABOVE the box
359
359
  // (FullScreenApp) and the box itself (InputLine) use ONE number — a mismatch
360
360
  // made the reserved height disagree with the rendered rows and the panel
361
361
  // overflowed.
362
- export function inputInnerCols(cols) {
363
- return Math.max(4, (Number(cols) || 80) - 4);
362
+ export function inputInnerCols(cols, borderCols = 2) {
363
+ return Math.max(4, (Number(cols) || 80) - borderCols - 2);
364
364
  }
365
365
  // Real height (rows) of the framed input box for the CURRENT buffer: 2 border
366
366
  // rows + the number of VISUAL rows the buffer wraps to. Using the LOGICAL line
367
367
  // count (buf.split('\n').length) was wrong: a long line wraps and renders MORE
368
368
  // rows than it has newlines, so the box was reserved too few rows, the second
369
369
  // line printed OVER the first and the cursor landed on the wrong row.
370
- export function inputBoxRows(prompt, buf, cursor, cols) {
371
- const inner = inputInnerCols(cols);
370
+ export function inputBoxRows(prompt, buf, cursor, cols, borderCols = 2) {
371
+ const inner = inputInnerCols(cols, borderCols);
372
372
  const rows = layoutInput(prompt, buf, cursor, inner).rows.length;
373
- return 2 + Math.max(1, rows);
373
+ // Two BORDER rows only when a frame is drawn ('none' reserves just the text).
374
+ const borderRows = borderCols > 0 ? 2 : 0;
375
+ return borderRows + Math.max(1, rows);
374
376
  }
375
377
  export function layoutInput(promptStr, buf, cursor, cols) {
376
378
  // Reserve the LAST column. If a printed row reaches exactly `cols` columns,
package/dist/markdown.js CHANGED
@@ -1,30 +1,73 @@
1
1
  import { render } from 'markdansi';
2
- import { highlight } from 'cli-highlight';
2
+ import { highlight, DEFAULT_THEME as HL_DEFAULT_THEME, plain, } from 'cli-highlight';
3
3
  import supportsHyperlinks from 'supports-hyperlinks';
4
- const theme = {
5
- heading: { color: '#9fb8d8', bold: true },
6
- strong: { color: '#e6e0cc', bold: true },
7
- emph: { color: '#cfc9b0', italic: true },
8
- inlineCode: { color: '#9ecb9e' },
9
- blockCode: { color: '#c9d1c9' },
10
- code: { color: '#9ecb9e' },
11
- link: { color: '#8fb8d8', underline: true },
12
- quote: { color: '#8a8f9a', italic: true },
13
- hr: { color: '#5b616e' },
14
- listMarker: { color: '#8a9bb5' },
15
- tableHeader: { color: '#9fb8d8', bold: true },
16
- tableCell: { color: '#d0d0d0' },
17
- };
18
- function highlighter(code, lang) {
19
- try {
20
- return highlight(code, {
21
- language: lang || 'plaintext',
22
- ignoreIllegals: true,
23
- });
24
- }
25
- catch {
26
- return code;
27
- }
4
+ import { themeColors } from './theme.js';
5
+ /**
6
+ * The Markdown theme for one palette. Rebuilt on every render (it is cheap) so
7
+ * the answers follow a live theme switch; the module no longer freezes the
8
+ * colors at import time.
9
+ */
10
+ function mdTheme(p) {
11
+ return {
12
+ heading: { color: p.taskSummary, bold: true },
13
+ strong: { color: p.assistant, bold: true },
14
+ emph: { color: p.assistant, italic: true },
15
+ inlineCode: { color: p.success },
16
+ blockCode: { color: p.toolResult },
17
+ code: { color: p.success },
18
+ link: { color: p.dir, underline: true },
19
+ quote: { color: p.system, italic: true },
20
+ hr: { color: p.dim },
21
+ listMarker: { color: p.tool },
22
+ tableHeader: { color: p.taskSummary, bold: true },
23
+ tableCell: { color: p.assistant },
24
+ };
25
+ }
26
+ /**
27
+ * Syntax highlighting for a code block, mapped onto the palette. Without this
28
+ * the highlighter used cli-highlight's DEFAULT_THEME (chalk's blue/red/green),
29
+ * which ignores the selected theme entirely and looked off in every non-zames
30
+ * palette. Tokens we do not map fall back to `plain` — NOT to the default
31
+ * theme — so nothing outside the palette can leak in.
32
+ */
33
+ function hlTheme(p) {
34
+ return {
35
+ keyword: (s) => wrap(p.prompt, s),
36
+ built_in: (s) => wrap(p.tool, s),
37
+ type: (s) => wrap(p.taskSummary, s),
38
+ literal: (s) => wrap(p.prompt, s),
39
+ number: (s) => wrap(p.success, s),
40
+ regexp: (s) => wrap(p.error, s),
41
+ string: (s) => wrap(p.success, s),
42
+ comment: (s) => wrap(p.dim, s),
43
+ class: (s) => wrap(p.taskSummary, s),
44
+ function: (s) => wrap(p.dir, s),
45
+ title: (s) => wrap(p.dir, s),
46
+ params: (s) => wrap(p.assistant, s),
47
+ attr: (s) => wrap(p.tool, s),
48
+ variable: (s) => wrap(p.assistant, s),
49
+ doctag: (s) => wrap(p.system, s),
50
+ meta: (s) => wrap(p.system, s),
51
+ tag: (s) => wrap(p.prompt, s),
52
+ name: (s) => wrap(p.prompt, s),
53
+ section: (s) => wrap(p.taskSummary, s),
54
+ link: (s) => wrap(p.dir, s),
55
+ addition: (s) => wrap(p.success, s),
56
+ deletion: (s) => wrap(p.error, s),
57
+ };
58
+ }
59
+ /** Apply a palette color to a highlighted token. */
60
+ function wrap(hex, s) {
61
+ return '\x1b[38;2;' + rgb(hex) + 'm' + s + '\x1b[39m';
62
+ }
63
+ /** `#rrggbb` -> the `r;g;b` part of a truecolor SGR sequence. */
64
+ function rgb(hex) {
65
+ const h = hex.replace('#', '');
66
+ return (parseInt(h.slice(0, 2), 16) +
67
+ ';' +
68
+ parseInt(h.slice(2, 4), 16) +
69
+ ';' +
70
+ parseInt(h.slice(4, 6), 16));
28
71
  }
29
72
  // The answer width is the SHARED content width (src/width.ts), the same one
30
73
  // the operator's echoed text, the tool previews, the warnings and the divider
@@ -43,6 +86,7 @@ export function renderMarkdown(text) {
43
86
  // stay colored while everything else went plain.
44
87
  if (process.env.NO_COLOR)
45
88
  return String(text);
89
+ const p = themeColors();
46
90
  try {
47
91
  const width = contentWidth();
48
92
  return render(String(text), {
@@ -53,8 +97,12 @@ export function renderMarkdown(text) {
53
97
  // looked unformatted). `color: true` makes it always emit SGR; the ONE
54
98
  // opt-out is NO_COLOR, handled above.
55
99
  color: true,
56
- theme: theme,
57
- highlighter,
100
+ theme: mdTheme(p),
101
+ highlighter: (code, lang) => highlight(code, {
102
+ language: lang || 'plaintext',
103
+ ignoreIllegals: true,
104
+ theme: hlTheme(p),
105
+ }),
58
106
  codeBox: true,
59
107
  tableBorder: 'unicode',
60
108
  listIndent: 2,
@@ -68,4 +116,8 @@ export function renderMarkdown(text) {
68
116
  return String(text);
69
117
  }
70
118
  }
119
+ // Keep the import meaningful for tree-shakers/readers: the default theme is
120
+ // deliberately NOT used (see hlTheme), `plain` is the fallback contract.
121
+ void HL_DEFAULT_THEME;
122
+ void plain;
71
123
  export default renderMarkdown;
@@ -20,7 +20,7 @@ import { suggestRowCount } from '../input/suggest.js';
20
20
  import { truncateToWidth, visLen } from '../input/layout.js';
21
21
  import { fullScreenLayout } from './layout.js';
22
22
  import { useMouseWheel } from './useMouseWheel.js';
23
- import { UI } from './ui-colors.js';
23
+ import { UI, inputBorderCols } from './ui-colors.js';
24
24
  /** The header bar: title, version, working directory. */
25
25
  function Header({ store }) {
26
26
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
@@ -65,6 +65,7 @@ export function FullScreenApp({ store, inputProps, }) {
65
65
  buf: state.edit.buf,
66
66
  cursor: state.edit.cursor,
67
67
  suggestRows: suggestRowCount(state.edit.buf, state.commands, cols),
68
+ borderCols: inputBorderCols(),
68
69
  });
69
70
  const logHeight = fsLayout.logHeight;
70
71
  // Window the log BY RENDERED ROW: a long wrapped answer can be taller than
@@ -16,11 +16,11 @@ import { insertText, insertPaste, insertNewline, backspace, del, moveLeft, moveR
16
16
  import { dispatchKey, parseCsiU } from './keys.js';
17
17
  import { useSyncExternalStore } from 'react';
18
18
  import { matchSlashCommands, suggestWindow, completeSlashCommand, } from '../input/suggest.js';
19
- import { UI } from './ui-colors.js';
19
+ import { UI, inputBorderStyle } from './ui-colors.js';
20
20
  /** Render the visual rows of the buffer with the prompt on the first row. */
21
21
  function InputRows({ prompt, buf, cols, }) {
22
22
  const lay = layoutInput(prompt, buf, 0, cols);
23
- return (_jsx(Box, { flexDirection: "column", children: lay.rows.map((r, i) => (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, bold: true, children: r.prefix }), r.text] }, i))) }));
23
+ return (_jsx(Box, { flexDirection: "column", children: lay.rows.map((r, i) => (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, bold: true, children: r.prefix }), _jsx(Text, { color: UI.text, children: r.text })] }, i))) }));
24
24
  }
25
25
  export function InputLine(props) {
26
26
  const { columns } = useWindowSize();
@@ -49,7 +49,11 @@ export function InputLine(props) {
49
49
  // `Math.max(4, cols - 4)` here was one independent copy that could drift from
50
50
  // the reservation (the box then rendered more rows than were reserved and the
51
51
  // input rows overprinted the status bar).
52
- const innerCols = inputInnerCols(cols);
52
+ // The frame style comes from ui.inputBorder (set live by /config): round
53
+ // (default), single, double, bold, none. Read per render so a switch applies
54
+ // immediately; 'none' also frees the two border columns for the text.
55
+ const borderStyle = inputBorderStyle();
56
+ const innerCols = inputInnerCols(cols, borderStyle ? 2 : 0);
53
57
  // Mirror the state into a ref so the useInput callback (a stable closure Ink
54
58
  // calls for every key) always sees the latest values.
55
59
  const stRef = useRef(st);
@@ -321,5 +325,5 @@ export function InputLine(props) {
321
325
  }), moreCount > 0 && (_jsx(Text, { color: UI.dim, children: ' ' +
322
326
  t('editor.more', { n: String(moreCount) }) +
323
327
  ' ' +
324
- t('editor.page_hint') }))] })), _jsx(Box, { borderStyle: "round", borderColor: props.locked ? UI.dim : UI.accent, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: _jsx(InputRows, { prompt: props.prompt, buf: st.buf, cols: innerCols }) })] }));
328
+ t('editor.page_hint') }))] })), _jsx(Box, { borderStyle: borderStyle, borderColor: props.locked ? UI.dim : UI.accent, paddingLeft: 1, paddingRight: 1, flexDirection: "column", children: _jsx(InputRows, { prompt: props.prompt, buf: st.buf, cols: innerCols }) })] }));
325
329
  }
@@ -115,12 +115,13 @@ function Badges({ state, budget = Infinity, }) {
115
115
  }
116
116
  return _jsx(_Fragment, { children: all.slice(start).map((b) => b.node) });
117
117
  }
118
- export function StatusBar({ state, divider = true, }) {
119
- // Drive the spinner from Ink's shared animation timer (one timer for the
120
- // whole tree). The interval matches the old 400ms dot cadence.
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) {
121
124
  const animating = state.busy || !!state.animBase;
122
- const { frame } = useAnimation({ interval: 400, isActive: animating });
123
- const t = translate(state.locale);
124
125
  const stateLbl = stateLabel(state, state.locale);
125
126
  let left;
126
127
  if (state.pendingText) {
@@ -147,20 +148,31 @@ export function StatusBar({ state, divider = true, }) {
147
148
  // When the log is scrolled up, show a marker in the status line (NOT as a
148
149
  // separate row below the input — that pushed the panel up and made the input
149
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.
150
153
  const scrollNote = state.scroll > 0
151
- ? translate(state.locale)('status.scrolled', {
152
- n: String(state.scroll),
153
- })
154
+ ? translate(state.locale)('status.scrolled', { n: String(state.scroll) })
154
155
  : '';
155
- const leftText = scrollNote || prefix + left;
156
+ return scrollNote || prefix + left;
157
+ }
158
+ export function StatusBar({ state, divider = true, }) {
159
+ // Drive the spinner from Ink's shared animation timer (one timer for the
160
+ // whole tree). The interval matches the old 400ms dot cadence.
161
+ const animating = state.busy || !!state.animBase;
162
+ const { frame } = useAnimation({ interval: 400, isActive: animating });
163
+ const t = translate(state.locale);
164
+ const leftText = statusLeftText(state, t, frame);
156
165
  const showLeft = !!leftText;
157
- // Keep the row on ONE line. The left status gets at most a third of the
158
- // width (it is the least important); the badges take the rest and DROP whole
159
- // badges until they fit (the context counter survives longest). Estimating
160
- // the badge width for the left budget was circular and let the row overshoot
161
- // to a second line on a narrow terminal.
166
+ // Keep the row on ONE line: the left status may use whatever the BADGES do
167
+ // not need, minus a small gap so the two never touch. A fixed "a third of
168
+ // the width" budget cut the pause counter mid-number («5…») while half the
169
+ // row sat empty; measuring the badges (they are DROPPED whole when the row
170
+ // is tight, the context counter survives longest) is both accurate and
171
+ // keeps the layout stable.
162
172
  const cols = useWindowSize().columns || 80;
163
- const leftBudget = Math.max(0, Math.min(Math.floor(cols / 3), cols - 14));
173
+ const badgeW = badgeTextWidth(state);
174
+ const gap = badgeW > 0 ? 2 : 0;
175
+ const leftBudget = Math.max(0, cols - 2 - badgeW - gap);
164
176
  const leftOut = showLeft ? truncateToWidth(leftText, leftBudget) : '';
165
177
  return (_jsxs(Box, { flexDirection: "column", children: [divider ? _jsx(Text, { color: UI.border, children: '─'.repeat(cols) }) : null, _jsxs(Box, { flexDirection: "row", width: Math.max(1, cols - 2), height: 1, children: [_jsx(Text, { color: UI.accentDim, children: leftOut || ' ' }), _jsx(Box, { flexGrow: 1 }), _jsx(Badges, { state: state, budget: Math.max(0, cols - 2 - visLen(leftOut)) })] })] }));
166
178
  }
@@ -13,7 +13,7 @@ export const HINT_ROWS = 1;
13
13
  export function fullScreenLayout(input) {
14
14
  const termRows = Math.max(1, Math.floor(input.termRows) || 24);
15
15
  const cols = Math.max(1, Math.floor(input.cols) || 80);
16
- const box = inputBoxRows(input.prompt, input.buf, input.cursor, cols);
16
+ const box = inputBoxRows(input.prompt, input.buf, input.cursor, cols, input.borderCols ?? 2);
17
17
  const suggestRows = Math.max(0, Math.floor(input.suggestRows) || 0);
18
18
  const panelRows = HEADER_ROWS + STATUS_ROWS + box + suggestRows + HINT_ROWS;
19
19
  const logHeight = Math.max(1, termRows - panelRows);
@@ -26,6 +26,36 @@ export function uiColorsFrom(p) {
26
26
  };
27
27
  }
28
28
  export const UI = uiColorsFrom(PALETTES[DEFAULT_THEME]);
29
+ // Border STYLE of the framed surfaces (the input box today). Ink only ships a
30
+ // handful of box styles; 'none' draws no frame at all. Kept next to the colors
31
+ // because it is the same kind of presentation state, and mutated in place so a
32
+ // /config switch applies without re-importing anything.
33
+ let inputBorder = 'round';
34
+ /** Switch the input frame style ('round'|'single'|'double'|'bold'|'none'). */
35
+ export function setInputBorder(name) {
36
+ const v = String(name ?? '')
37
+ .trim()
38
+ .toLowerCase();
39
+ inputBorder =
40
+ v === 'none' ||
41
+ v === 'single' ||
42
+ v === 'double' ||
43
+ v === 'bold' ||
44
+ v === 'round'
45
+ ? v
46
+ : 'round';
47
+ return inputBorder;
48
+ }
49
+ /** The current input frame style as an Ink borderStyle value (none = no box). */
50
+ export function inputBorderStyle() {
51
+ if (inputBorder === 'none')
52
+ return undefined;
53
+ return inputBorder;
54
+ }
55
+ /** Border columns the frame consumes (2 = left + right, 0 when off). */
56
+ export function inputBorderCols() {
57
+ return inputBorderStyle() ? 2 : 0;
58
+ }
29
59
  /** Repaint the Ink widgets with the palette of `name` (unknown -> default). */
30
60
  export function syncUiColors(name) {
31
61
  const id = isThemeName(name) ? name : DEFAULT_THEME;
package/llms-full.txt CHANGED
@@ -618,6 +618,11 @@ are ports of well-known editor schemes:
618
618
  `zames` (default), `one-dark`, `dracula`, `gruvbox`, `nord`, `monokai`,
619
619
  `solarized-dark`, `tokyo-night`.
620
620
 
621
+ 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
+ setting: `/config set ui.inputBorder <round|single|double|bold|none>`
624
+ (default `round`; `none` removes it).
625
+
621
626
  Agent data is stored in `~/.zames`: browser profile, logs, undo history, sessions.
622
627
 
623
628
  ## FAQ
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.83.1",
3
+ "version": "2.83.4",
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": {