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/README.md +22 -1
- package/dist/commands.js +73 -45
- package/dist/config-menu.js +143 -21
- package/dist/i18n.js +152 -0
- package/dist/index.js +104 -27
- package/dist/input.js +208 -6
- package/dist/sessions.js +26 -0
- package/package.json +1 -1
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
|
-
|
|
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 +
|
|
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
|
+
}
|