zames_pro 2.50.2 → 2.52.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/dist/input.js CHANGED
@@ -316,6 +316,9 @@ export class LineEditor {
316
316
  // only the base text (remaining seconds) without restarting the timer.
317
317
  _animating;
318
318
  _dotPhase;
319
+ // When the current animated phase started, so the status can show its
320
+ // elapsed time. 0 when nothing is animating.
321
+ _animStart;
319
322
  _thinkBase;
320
323
  // Explicit lifecycle state of the send ('generating' | 'paused' |
321
324
  // 'settled' | ''). The thinking spinner already shows ACTIVITY, but it does
@@ -348,6 +351,21 @@ export class LineEditor {
348
351
  history;
349
352
  _histIndex;
350
353
  _histDraft;
354
+ // Incremental reverse-search over the history (Ctrl+R), bash-style.
355
+ _searchMode;
356
+ _searchQuery;
357
+ _searchIndex;
358
+ _searchSavedBuf;
359
+ // Called after every submit with the new full history, so the caller can
360
+ // persist it (the history otherwise dies with the process).
361
+ onHistoryChange;
362
+ // Snapshots of {buf, cursor} taken before each edit, so Ctrl+_ can undo a
363
+ // slip (a fat-fingered Ctrl+U / Ctrl+K used to wipe the whole line with no
364
+ // way back). Capped so a long session cannot grow it without bound.
365
+ _undoStack;
366
+ // True while a run of plain typing is in progress, so one undo snapshot
367
+ // covers the whole run instead of every character.
368
+ _typingRun;
351
369
  // Slash-command hints (shown when you type «/»).
352
370
  slashCommands;
353
371
  _suggestCount;
@@ -420,6 +438,7 @@ export class LineEditor {
420
438
  this._dotTimer = null;
421
439
  this._animating = false;
422
440
  this._dotPhase = 0;
441
+ this._animStart = 0;
423
442
  this._thinkBase = '';
424
443
  this._sendState = '';
425
444
  this._lastStatusBlock = '';
@@ -449,6 +468,13 @@ export class LineEditor {
449
468
  this.history = [];
450
469
  this._histIndex = 0;
451
470
  this._histDraft = '';
471
+ this._searchMode = false;
472
+ this._searchQuery = '';
473
+ this._searchIndex = -1;
474
+ this._searchSavedBuf = '';
475
+ this.onHistoryChange = null;
476
+ this._undoStack = [];
477
+ this._typingRun = false;
452
478
  this.slashCommands = commands;
453
479
  this._suggestCount = 0;
454
480
  this.pastes = [];
@@ -504,16 +530,17 @@ export class LineEditor {
504
530
  return this.contextStatus;
505
531
  }
506
532
  // The context text colored by fill level: green (ok), yellow (warn),
507
- // red (high).
533
+ // red (high). A leading marker repeats the level as TEXT, so the signal
534
+ // survives on a terminal without color and for a color-blind operator.
508
535
  _contextText() {
509
536
  const ctx = this._contextForRender();
510
537
  if (!ctx)
511
538
  return '';
512
539
  const level = tokenStatusLevel(this.contextTokens, this.contextLimit);
513
540
  if (level === 'high')
514
- return theme.error(ctx);
541
+ return theme.error('⛔ ' + ctx);
515
542
  if (level === 'warn')
516
- return theme.warn(ctx);
543
+ return theme.warn('▲ ' + ctx);
517
544
  return theme.success(ctx);
518
545
  }
519
546
  // The queue badge ("⧗N") shown before the context counter when messages are
@@ -1109,16 +1136,34 @@ export class LineEditor {
1109
1136
  _startAnimated(baseText) {
1110
1137
  this._thinkBase = theme.brown(stripEllipsis(baseText));
1111
1138
  this._dotPhase = 0;
1112
- this.setStatus(this._thinkBase + this._dots(0) + this._hint());
1139
+ // Remember when the animation began so the status can show how long the
1140
+ // current phase has been running ("12s"): a long reasoning turn otherwise
1141
+ // looks identical to a hung one.
1142
+ this._animStart = Date.now();
1143
+ this.setStatus(this._thinkBase + this._dots(0) + this._elapsedLabel() + this._hint());
1113
1144
  this._stopDots();
1114
1145
  this._dotTimer = setInterval(() => {
1115
1146
  this._dotPhase = (this._dotPhase + 1) % DOTS.length;
1116
- this.setStatus(this._thinkBase + this._dots(this._dotPhase) + this._hint());
1147
+ this.setStatus(this._thinkBase +
1148
+ this._dots(this._dotPhase) +
1149
+ this._elapsedLabel() +
1150
+ this._hint());
1117
1151
  }, 400);
1118
1152
  this._animating = true;
1119
1153
  if (this._dotTimer.unref)
1120
1154
  this._dotTimer.unref();
1121
1155
  }
1156
+ // Elapsed seconds of the current animated phase, formatted for the status
1157
+ // line ("0s", "45s", "2m 05s"). Empty while nothing is animating.
1158
+ _elapsedLabel() {
1159
+ if (!this._animating || !this._animStart)
1160
+ return '';
1161
+ const sec = Math.floor((Date.now() - this._animStart) / 1000);
1162
+ const m = Math.floor(sec / 60);
1163
+ const s = sec % 60;
1164
+ const text = m > 0 ? m + 'm ' + String(s).padStart(2, '0') + 's' : s + 's';
1165
+ return theme.dim(' · ' + text);
1166
+ }
1122
1167
  thinking() {
1123
1168
  this._startThinking();
1124
1169
  }
@@ -1248,6 +1293,9 @@ export class LineEditor {
1248
1293
  // Add to history only non-empty messages that don't duplicate the previous one.
1249
1294
  if (text.trim() && this.history[this.history.length - 1] !== text) {
1250
1295
  this.history.push(text);
1296
+ // Persist the new entry: the in-memory history dies with the process.
1297
+ if (this.onHistoryChange)
1298
+ this.onHistoryChange(this.history.slice());
1251
1299
  }
1252
1300
  this._histIndex = this.history.length;
1253
1301
  this._histDraft = '';
@@ -1272,6 +1320,26 @@ export class LineEditor {
1272
1320
  if (this.busy)
1273
1321
  this._startThinking();
1274
1322
  }
1323
+ // Record the current buffer for Ctrl+_ undo. `coalesce` keeps one snapshot
1324
+ // for a whole run of plain typing (so undo removes the run, not one char) —
1325
+ // the run ends as soon as a non-typing key is handled.
1326
+ _pushUndo(coalesce = false) {
1327
+ if (coalesce && this._typingRun)
1328
+ return;
1329
+ this._undoStack.push({ buf: this.buf, cursor: this.cursor });
1330
+ if (this._undoStack.length > 200)
1331
+ this._undoStack.shift();
1332
+ this._typingRun = coalesce;
1333
+ }
1334
+ // Ctrl+_ — restore the buffer to before the last edit.
1335
+ _undoEdit() {
1336
+ const prev = this._undoStack.pop();
1337
+ if (!prev)
1338
+ return;
1339
+ this.buf = prev.buf;
1340
+ this.cursor = Math.min(prev.cursor, Array.from(prev.buf).length);
1341
+ this._typingRun = false;
1342
+ }
1275
1343
  _insert(text) {
1276
1344
  const chars = Array.from(this.buf);
1277
1345
  const ins = Array.from(String(text));
@@ -1483,6 +1551,93 @@ export class LineEditor {
1483
1551
  }
1484
1552
  this.cursor = Array.from(this.buf).length;
1485
1553
  }
1554
+ // ---------- reverse-search (Ctrl+R) ----------
1555
+ // Start an incremental reverse search, keeping the current buffer aside so
1556
+ // Esc restores it (bash behavior). An empty history leaves the editor
1557
+ // untouched.
1558
+ _searchStart() {
1559
+ if (!this.history.length)
1560
+ return;
1561
+ this._searchMode = true;
1562
+ this._searchQuery = '';
1563
+ this._searchIndex = this.history.length - 1;
1564
+ this._searchSavedBuf = this.buf;
1565
+ this._searchRefresh(true);
1566
+ this._searchStatus();
1567
+ }
1568
+ // Recompute the match for the current query. `resetToNewest` moves the
1569
+ // cursor back to the newest entry (used when the query changes; a repeated
1570
+ // Ctrl+R keeps scanning older entries from the current index).
1571
+ _searchRefresh(resetToNewest = false) {
1572
+ const q = this._searchQuery;
1573
+ if (resetToNewest)
1574
+ this._searchIndex = this.history.length - 1;
1575
+ for (let i = this._searchIndex; i >= 0; i--) {
1576
+ if (q === '' || this.history[i].toLowerCase().includes(q.toLowerCase())) {
1577
+ this._searchIndex = i;
1578
+ return;
1579
+ }
1580
+ }
1581
+ // No match from here: signal it and leave the buffer as-is.
1582
+ this._searchIndex = -1;
1583
+ }
1584
+ // Show the search state in the status line. The matched entry is previewed
1585
+ // in the input line live, so Enter can accept it at once.
1586
+ _searchStatus() {
1587
+ const t = translate(this.locale);
1588
+ const q = this._searchQuery;
1589
+ if (this._searchIndex >= 0) {
1590
+ // Set the matched buffer BEFORE setStatus(): setStatus renders the
1591
+ // block, so the preview must already be in the buffer.
1592
+ this.buf = this.history[this._searchIndex];
1593
+ this.cursor = Array.from(this.buf).length;
1594
+ this.setStatus(t('editor.search_prompt', { q }));
1595
+ }
1596
+ else {
1597
+ this.setStatus(t('editor.search_fail', { q }));
1598
+ }
1599
+ }
1600
+ // One more Ctrl+R: move to the PREVIOUS (older) match.
1601
+ _searchNext() {
1602
+ if (this._searchIndex > 0) {
1603
+ const prev = this._searchIndex;
1604
+ this._searchIndex = prev - 1;
1605
+ this._searchRefresh(false);
1606
+ // If nothing matched above `prev`, keep the previous match.
1607
+ if (this._searchIndex < 0)
1608
+ this._searchIndex = prev;
1609
+ }
1610
+ this._searchStatus();
1611
+ }
1612
+ // Enter: keep the matched buffer and leave search mode.
1613
+ _searchAccept() {
1614
+ this._searchMode = false;
1615
+ this._searchQuery = '';
1616
+ this.setStatus('');
1617
+ this._render();
1618
+ }
1619
+ // Esc / Ctrl+C: leave search mode and restore the buffer typed before it.
1620
+ _searchCancel() {
1621
+ this._searchMode = false;
1622
+ this._searchQuery = '';
1623
+ this.buf = this._searchSavedBuf;
1624
+ this.cursor = Array.from(this.buf).length;
1625
+ this.setStatus('');
1626
+ this._render();
1627
+ }
1628
+ // A character typed during search extends the query; Backspace trims it.
1629
+ _searchInput(ch) {
1630
+ this._searchQuery += ch;
1631
+ this._searchRefresh(true);
1632
+ this._searchStatus();
1633
+ }
1634
+ _searchBackspace() {
1635
+ if (!this._searchQuery.length)
1636
+ return;
1637
+ this._searchQuery = this._searchQuery.slice(0, -1);
1638
+ this._searchRefresh(true);
1639
+ this._searchStatus();
1640
+ }
1486
1641
  _handle(data) {
1487
1642
  let s = data.toString('utf-8');
1488
1643
  // While locked (a long operation is running), swallow all input except
@@ -1504,11 +1659,40 @@ export class LineEditor {
1504
1659
  }
1505
1660
  return;
1506
1661
  }
1507
- if (!this._inPaste && s === ESC) {
1662
+ if (!this._inPaste && s === ESC && !this._searchMode) {
1508
1663
  if (this.onEscape)
1509
1664
  this.onEscape();
1510
1665
  return;
1511
1666
  }
1667
+ // While the reverse search (Ctrl+R) is active, the editor is in a modal
1668
+ // state: every printable key extends the query, Ctrl+R scans older
1669
+ // entries, Enter accepts and Esc/Ctrl+C cancels. Handling this BEFORE the
1670
+ // normal key dispatch keeps the search self-contained.
1671
+ if (this._searchMode) {
1672
+ let i = 0;
1673
+ while (i < s.length) {
1674
+ const code = s.charCodeAt(i);
1675
+ if (code === 18) {
1676
+ this._searchNext();
1677
+ }
1678
+ else if (code === 13 || code === 10) {
1679
+ this._searchAccept();
1680
+ return;
1681
+ }
1682
+ else if (code === 27 || code === 3) {
1683
+ this._searchCancel();
1684
+ return;
1685
+ }
1686
+ else if (code === 127 || code === 8) {
1687
+ this._searchBackspace();
1688
+ }
1689
+ else if (code >= 32) {
1690
+ this._searchInput(s[i]);
1691
+ }
1692
+ i++;
1693
+ }
1694
+ return;
1695
+ }
1512
1696
  while (s.length) {
1513
1697
  if (this._inPaste) {
1514
1698
  const end = s.indexOf(PASTE_END);
@@ -1626,12 +1810,19 @@ export class LineEditor {
1626
1810
  continue;
1627
1811
  }
1628
1812
  if (code === 21) {
1813
+ this._pushUndo();
1629
1814
  this.buf = '';
1630
1815
  this.cursor = 0;
1631
1816
  this.pastes = [];
1632
1817
  this._renderInputOnly();
1633
1818
  continue;
1634
1819
  }
1820
+ if (code === 31) {
1821
+ // Ctrl+_ — undo the last edit in the input line.
1822
+ this._undoEdit();
1823
+ this._renderInputOnly();
1824
+ continue;
1825
+ }
1635
1826
  if (code === 22) {
1636
1827
  // Ctrl+V: terminals rarely deliver an image as text here, so we try the
1637
1828
  // OS clipboard first; if there is no image we fall back to reading the
@@ -1639,13 +1830,20 @@ export class LineEditor {
1639
1830
  void this._tryClipboard();
1640
1831
  continue;
1641
1832
  }
1833
+ if (code === 18) {
1834
+ // Ctrl+R — incremental reverse search over the input history.
1835
+ this._searchStart();
1836
+ continue;
1837
+ }
1642
1838
  if (code === 23) {
1839
+ this._pushUndo();
1643
1840
  this._deleteWordLeft();
1644
1841
  this._renderInputOnly();
1645
1842
  continue;
1646
1843
  }
1647
1844
  if (code === 11) {
1648
1845
  // Ctrl+K — delete from the cursor to the end of the line.
1846
+ this._pushUndo();
1649
1847
  const arr = Array.from(this.buf);
1650
1848
  let e = this.cursor;
1651
1849
  while (e < arr.length && arr[e] !== NL)
@@ -1656,6 +1854,7 @@ export class LineEditor {
1656
1854
  continue;
1657
1855
  }
1658
1856
  if (code === 127 || code === 8) {
1857
+ this._pushUndo(true);
1659
1858
  this._backspace();
1660
1859
  this._renderInputOnly();
1661
1860
  continue;
@@ -1719,6 +1918,7 @@ export class LineEditor {
1719
1918
  continue;
1720
1919
  }
1721
1920
  if (s.startsWith('[3~')) {
1921
+ this._pushUndo(true);
1722
1922
  this._delete();
1723
1923
  s = s.slice(3);
1724
1924
  this._renderInputOnly();
@@ -1745,6 +1945,8 @@ export class LineEditor {
1745
1945
  }
1746
1946
  if (code < 32)
1747
1947
  continue;
1948
+ // Coalesce a run of typing into ONE undo snapshot.
1949
+ this._pushUndo(true);
1748
1950
  this._insert(ch);
1749
1951
  this._renderInputOnly();
1750
1952
  }
package/dist/sessions.js CHANGED
@@ -132,3 +132,29 @@ export function listSessions() {
132
132
  export function sessionsDir() {
133
133
  return SESSIONS_DIR;
134
134
  }
135
+ // ---------- input history ----------
136
+ // The LineEditor history otherwise dies with the process (it was in-memory
137
+ // only), so the arrow keys / Ctrl+R could not recall anything from a previous
138
+ // run. We persist it in ~/.zames/history.json, capped so the file cannot grow
139
+ // without bound.
140
+ const HISTORY_FILE = path.join(os.homedir(), '.zames', 'history.json');
141
+ const HISTORY_LIMIT = 500;
142
+ export function loadHistory() {
143
+ try {
144
+ const raw = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
145
+ if (!Array.isArray(raw))
146
+ return [];
147
+ return raw.filter((x) => typeof x === 'string');
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
153
+ export function saveHistory(history) {
154
+ try {
155
+ ensureDir();
156
+ const trimmed = history.slice(-HISTORY_LIMIT);
157
+ atomicWriteJson(HISTORY_FILE, trimmed);
158
+ }
159
+ catch { }
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.50.2",
3
+ "version": "2.52.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {