zames_pro 2.82.51 → 2.82.53

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,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.82.53] - 2026-10-23
11
+
12
+ ### Fixed
13
+
14
+ - **glm:** keep a message without a timestamp in dialogue order
15
+ - **qwen:** keep a message without a timestamp in dialogue order
16
+ - **rewind:** resolve a full checkpoint stamp against the whole index
17
+
18
+ ## [2.82.52] - 2026-10-23
19
+
20
+ ### Fixed
21
+
22
+ - **config:** tolerate an unparsable config file when writing
23
+ - **tui:** use the shared width table when estimating log rows
24
+
25
+ ### Changed
26
+
27
+ - **tui:** derive the input box width from the shared helper
28
+
10
29
  ## [2.82.51] - 2026-10-23
11
30
 
12
31
  ### Added
@@ -44,10 +44,30 @@ export function rotateRecords(records, max) {
44
44
  * Resolve a `/rewind` selector against records sorted NEWEST FIRST.
45
45
  * `1` = most recent; a non-numeric selector is matched as a stamp prefix.
46
46
  */
47
- export function pickRecord(records, selector) {
47
+ /** True for a selector that is exactly a full epoch-ms stamp (13 digits). */
48
+ export function isFullStampSelector(selector) {
49
+ return /^\d{13}$/.test(String(selector ?? '').trim());
50
+ }
51
+ const isFullStamp = isFullStampSelector;
52
+ export function pickRecord(records, selector,
53
+ /** The FULL record set, when `records` is only a display window. */
54
+ allRecords) {
48
55
  const sel = String(selector ?? '').trim();
49
56
  if (!sel)
50
57
  return null;
58
+ // A FULL epoch-ms stamp names exactly ONE checkpoint, wherever it sits in
59
+ // the index. The command resolves against the newest-10 display window while
60
+ // restore() searches the WHOLE index, so an older checkpoint picked by its
61
+ // full stamp used to report "/rewind failed" — and, worse, a stamp that
62
+ // happened to share its first digits with a NEWER one was resolved to that
63
+ // WRONG record. A full stamp is therefore matched against the full set
64
+ // FIRST, and only then treated as an index.
65
+ if (isFullStamp(sel)) {
66
+ const pool = allRecords && allRecords.length ? allRecords : records;
67
+ const exact = pool.find((r) => String(r.stamp) === sel);
68
+ if (exact)
69
+ return exact;
70
+ }
51
71
  if (/^\d+$/.test(sel)) {
52
72
  const n = Number(sel);
53
73
  // A small number is an index (1 = newest).
@@ -60,7 +80,10 @@ export function pickRecord(records, selector) {
60
80
  if (sel.length < 2)
61
81
  return null;
62
82
  }
63
- return records.find((r) => String(r.stamp).startsWith(sel)) || null;
83
+ const pool = allRecords && allRecords.length ? allRecords : records;
84
+ return (records.find((r) => String(r.stamp).startsWith(sel)) ||
85
+ pool.find((r) => String(r.stamp).startsWith(sel)) ||
86
+ null);
64
87
  }
65
88
  // ---------- tar / fs plumbing ----------
66
89
  function runTar(args, cwd, timeout = 60_000) {
package/dist/config.js CHANGED
@@ -696,19 +696,47 @@ export function configPathFor(scope) {
696
696
  * writing does not "freeze" all defaults into the user's file.
697
697
  */
698
698
  export function readConfigFile(file) {
699
+ let raw;
699
700
  try {
700
- const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
701
- if (data && typeof data === 'object' && !Array.isArray(data)) {
702
- return data;
703
- }
701
+ raw = fs.readFileSync(file, 'utf-8');
704
702
  }
705
703
  catch (e) {
706
704
  const err = e;
705
+ // A MISSING file is the normal first-write case. Anything else (EACCES,
706
+ // EISDIR) is reported — the caller must know the write cannot land.
707
707
  if (err.code !== 'ENOENT') {
708
708
  throw new Error(`failed to read ${file}: ${err.message}`);
709
709
  }
710
+ return {};
711
+ }
712
+ try {
713
+ const data = JSON.parse(raw);
714
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
715
+ return data;
716
+ }
717
+ return {};
718
+ }
719
+ catch (e) {
720
+ // A file that does not PARSE (a hand edit with a trailing comma, a
721
+ // merge-conflict marker, a bare-word value) is NOT a reason to fail the
722
+ // write: loadConfig() already tolerates it with a warning. Failing here
723
+ // made /config set useless on such a project, and a caller that ignored
724
+ // the error overwrote the WHOLE file with the single edited key.
725
+ //
726
+ // The unparsable text is backed up beside the file first, so the operator's
727
+ // other settings are recoverable by hand; the warning goes to stderr, the
728
+ // same channel loadConfig() uses.
729
+ const backup = file + '.broken-' + Date.now();
730
+ try {
731
+ fs.copyFileSync(file, backup);
732
+ console.error(`config: ${file} is not valid JSON (${e.message});` +
733
+ ` its content was kept in ${backup} and the write starts a fresh file`);
734
+ }
735
+ catch {
736
+ // Best-effort: an unreadable backup must not block the write either.
737
+ }
738
+ return {};
710
739
  }
711
- return {};
712
740
  }
713
741
  /**
714
742
  * Write a setting value to the file. Returns the final object that was
package/dist/glm-chats.js CHANGED
@@ -176,7 +176,19 @@ export function parseGlmHistory(json) {
176
176
  };
177
177
  const sorted = arr
178
178
  .filter((x) => !!x && typeof x === 'object')
179
- .sort((a, b) => stampOf(a) - stampOf(b));
179
+ // Order by INSERTION INDEX first and by stamp only when BOTH messages
180
+ // carry one. `stampOf` answers 0 for a missing stamp, so a half-stamped
181
+ // payload pushed every undated turn BEFORE all dated ones and the dialogue
182
+ // came back as "the model answered before it was asked".
183
+ .map((m, i) => ({ m, i, t: stampOf(m) }))
184
+ .sort((a, b) => {
185
+ const da = a.t !== 0;
186
+ const db = b.t !== 0;
187
+ if (da && db && a.t !== b.t)
188
+ return a.t - b.t;
189
+ return a.i - b.i;
190
+ })
191
+ .map((x) => x.m);
180
192
  const out = [];
181
193
  for (const m of sorted) {
182
194
  // The role comes from `role` (confirmed by the live fixture). Only the
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import { handleConfigCommand as runConfigCommand } from './config-command.js';
28
28
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
29
29
  import { Transcript } from './transcript.js';
30
30
  import { UndoStore } from './undo.js';
31
- import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
31
+ import { CheckpointStore, pickRecord as pickCheckpoint, isFullStampSelector, } from './checkpoint.js';
32
32
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
33
33
  import { performCompact } from './compact.js';
34
34
  import { clampGoalCondition } from './goal.js';
@@ -3913,8 +3913,16 @@ async function main() {
3913
3913
  if (editor)
3914
3914
  editor.lock(t('msg.input_locked'));
3915
3915
  try {
3916
+ // The DISPLAY window is the newest 10, but a FULL stamp must resolve
3917
+ // against the whole index (restore() searches all of it): an older
3918
+ // checkpoint picked by its stamp used to report "/rewind failed", and a
3919
+ // stamp sharing its first digits with a newer one could resolve to
3920
+ // THAT wrong record.
3916
3921
  const list = await checkpoints.list(10);
3917
- const target = pickCheckpoint(list, selector);
3922
+ const all = isFullStampSelector(selector)
3923
+ ? await checkpoints.list(Number.MAX_SAFE_INTEGER)
3924
+ : list;
3925
+ const target = pickCheckpoint(list, selector, all);
3918
3926
  if (!target) {
3919
3927
  console.error(theme.error(t('rewind.failed', { v: selector })));
3920
3928
  continue;
@@ -208,7 +208,28 @@ export function parseQwenHistory(json) {
208
208
  // The map is UNORDERED; `timestamp` (seconds) restores the dialogue order.
209
209
  const sorted = arr
210
210
  .filter((x) => !!x && typeof x === 'object')
211
- .sort((a, b) => (Number(a.timestamp) || 0) - (Number(b.timestamp) || 0));
211
+ // A message WITHOUT a usable timestamp must not be forced to 0: that put
212
+ // it BEFORE every dated turn, so a partially stamped payload reordered the
213
+ // dialogue (the operator's question surfaced after the model's answer).
214
+ // Order by index first — the map is in insertion order — and only then by
215
+ // stamp; a missing stamp inherits its neighbour's position instead of
216
+ // jumping to the front. A stable sort keeps the original order on a tie.
217
+ .map((m, i) => ({ m, i }))
218
+ .sort((a, b) => {
219
+ const ta = Number(a.m.timestamp);
220
+ const tb = Number(b.m.timestamp);
221
+ const va = Number.isFinite(ta) ? ta : NaN;
222
+ const vb = Number.isFinite(tb) ? tb : NaN;
223
+ if (Number.isFinite(va) && Number.isFinite(vb)) {
224
+ if (va !== vb)
225
+ return va - vb;
226
+ return a.i - b.i;
227
+ }
228
+ // Only ONE side is dated: keep the insertion order (a half-stamped
229
+ // payload has no trustworthy clock to sort by).
230
+ return a.i - b.i;
231
+ })
232
+ .map((x) => x.m);
212
233
  const out = [];
213
234
  for (const m of sorted) {
214
235
  // Role detection is DEFENSIVE: the field has been `role` (string,
@@ -11,7 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  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
- import { layoutInput, SUGGEST_PAGE, } from '../input/layout.js';
14
+ import { inputInnerCols, layoutInput, SUGGEST_PAGE, } from '../input/layout.js';
15
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';
16
16
  import { dispatchKey, parseCsiU } from './keys.js';
17
17
  import { useSyncExternalStore } from 'react';
@@ -42,9 +42,13 @@ export function InputLine(props) {
42
42
  }));
43
43
  }, [props.store]);
44
44
  // The input lives inside a rounded box: 2 columns of border (left/right) plus
45
- // 1 column of inner padding on each side. The layout must wrap to the INNER
46
- // width, or a long line would overflow the frame.
47
- const innerCols = Math.max(4, cols - 4);
45
+ // 1 column of inner padding on each side, so the CONTENT width is `cols - 4`.
46
+ // Use the SHARED helper: `inputBoxRows()` — the height the full-screen layout
47
+ // reserves — is computed from the SAME function, and a hand-written
48
+ // `Math.max(4, cols - 4)` here was one independent copy that could drift from
49
+ // the reservation (the box then rendered more rows than were reserved and the
50
+ // input rows overprinted the status bar).
51
+ const innerCols = inputInnerCols(cols);
48
52
  // Mirror the state into a ref so the useInput callback (a stable closure Ink
49
53
  // calls for every key) always sees the latest values.
50
54
  const stRef = useRef(st);
@@ -7,6 +7,7 @@
7
7
  // purpose: measuring the real Yoga layout per entry would need a ref per row
8
8
  // and a re-render, while counting wrapped lines is exact enough for a log and
9
9
  // costs nothing.
10
+ import { charWidth } from '../input/layout.js';
10
11
  /** Visible width of a line without ANSI escapes (wide chars count 2). */
11
12
  function visWidth(s) {
12
13
  let w = 0;
@@ -43,16 +44,14 @@ function visWidth(s) {
43
44
  continue;
44
45
  }
45
46
  const cp = s.codePointAt(i);
46
- // Wide ranges (CJK/emoji) count 2; combining marks 0. Approximate — a log
47
- // windowing error of one cell is invisible.
48
- if (cp >= 0x1f300 && cp <= 0x1faff)
49
- w += 2;
50
- else if (cp >= 0x1100 && cp <= 0x9fff)
51
- w += 2;
52
- else if ((cp >= 0x300 && cp <= 0x36f) || cp === 0x200d)
53
- w += 0;
54
- else
55
- w += 1;
47
+ // Use the SAME width rule as the input/status layout (charWidth in
48
+ // src/input/layout.ts). The old table here counted EVERYTHING up to
49
+ // U+9FFF as two cells, so a line of box arrows (`→`, U+2192), the
50
+ // hourglass `⧗` (U+29D7) or Cyrillic/Greek letters over-estimated its
51
+ // rows; at a narrow width that reserves an extra row per entry and the
52
+ // pane believes it is full, stopping the scroll early. Sharing ONE table
53
+ // keeps the viewport and the rest of the TUI from disagreeing.
54
+ w += charWidth(cp);
56
55
  i += cp > 0xffff ? 2 : 1;
57
56
  }
58
57
  return w;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.82.51",
3
+ "version": "2.82.53",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {