zames_pro 2.19.0 → 2.20.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 CHANGED
@@ -158,8 +158,9 @@ Codex CLI:
158
158
 
159
159
  The token context is also shown live: the status line above the input has the
160
160
  spinner/text on the left and the context on the right (e.g. `125k · 13%`,
161
- percent of a 1M context). It comes from DeepSeek's `accumulated_token_usage`
162
- and is hidden until the first answer delivers it.
161
+ percent of a 1M context). It is COLORED by fill level: green below 50%,
162
+ yellow 50-80%, red above 80%. It comes from DeepSeek's
163
+ `accumulated_token_usage` and is hidden until the first answer delivers it.
163
164
 
164
165
 
165
166
  ## Project context, skills and memory
package/dist/browser.js CHANGED
@@ -1196,13 +1196,14 @@ export class DeepSeekBrowser {
1196
1196
  // replied to the wrong thing (or nothing), and the loop looked stalled.
1197
1197
  if (norm(got) !== norm(text)) {
1198
1198
  await input.click();
1199
- await this.page.keyboard.press("Control+A");
1200
- await this.page.keyboard.press("Delete");
1199
+ await this.page.keyboard.press('Control+A');
1200
+ await this.page.keyboard.press('Delete');
1201
1201
  await this.page.keyboard.insertText(text);
1202
1202
  const got2 = await input.evaluate((el) => {
1203
- if (el.tagName.toLowerCase() === "textarea" || el.tagName.toLowerCase() === "input")
1203
+ if (el.tagName.toLowerCase() === 'textarea' ||
1204
+ el.tagName.toLowerCase() === 'input')
1204
1205
  return el.value;
1205
- return el.innerText || el.textContent || "";
1206
+ return el.innerText || el.textContent || '';
1206
1207
  });
1207
1208
  if (norm(got2) !== norm(text)) {
1208
1209
  throw new Error(this._t('ds.input_partial', {
@@ -1366,7 +1367,14 @@ export class DeepSeekBrowser {
1366
1367
  // the "new answer" check never fired — the source of the
1367
1368
  // ds.send_no_new_answer flapping.
1368
1369
  const beforeText = await this._readLastAnswerTextCleanDom().catch(() => '');
1369
- this._askDebug('SEND agent=' + agent + ' len=' + prompt.length + ' beforeLen=' + beforeText.length + ' beforeHead=' + JSON.stringify(beforeText.slice(0, 60)));
1370
+ this._askDebug('SEND agent=' +
1371
+ agent +
1372
+ ' len=' +
1373
+ prompt.length +
1374
+ ' beforeLen=' +
1375
+ beforeText.length +
1376
+ ' beforeHead=' +
1377
+ JSON.stringify(beforeText.slice(0, 60)));
1370
1378
  await this._waitForSendSlot(agent);
1371
1379
  // Esc/Ctrl+C pressed during the pause — do not send anything.
1372
1380
  if (this._abort)
@@ -1459,7 +1467,12 @@ export class DeepSeekBrowser {
1459
1467
  const netStarted = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
1460
1468
  if (changed || netStarted || bodyLen > startBodyLen) {
1461
1469
  started = true;
1462
- this._askDebug('STARTED changed=' + changed + ' netStarted=' + netStarted + ' bodyGrew=' + (bodyLen > startBodyLen));
1470
+ this._askDebug('STARTED changed=' +
1471
+ changed +
1472
+ ' netStarted=' +
1473
+ netStarted +
1474
+ ' bodyGrew=' +
1475
+ (bodyLen > startBodyLen));
1463
1476
  break;
1464
1477
  }
1465
1478
  // Fallback for an echo: the text equals beforeText, so it is the OLD
@@ -1480,7 +1493,18 @@ export class DeepSeekBrowser {
1480
1493
  settledTicks = 0;
1481
1494
  }
1482
1495
  lastStartCur = cur;
1483
- this._askDebug('START-loop curLen=' + cur.length + ' changed=' + changed + ' netStarted=' + netStarted + ' bodyLen=' + bodyLen + ' settled=' + settledTicks + ' generating=' + notGenerating);
1496
+ this._askDebug('START-loop curLen=' +
1497
+ cur.length +
1498
+ ' changed=' +
1499
+ changed +
1500
+ ' netStarted=' +
1501
+ netStarted +
1502
+ ' bodyLen=' +
1503
+ bodyLen +
1504
+ ' settled=' +
1505
+ settledTicks +
1506
+ ' generating=' +
1507
+ notGenerating);
1484
1508
  await this.page.waitForTimeout(300);
1485
1509
  }
1486
1510
  if (!started) {
@@ -1490,7 +1514,10 @@ export class DeepSeekBrowser {
1490
1514
  // previous tool call.
1491
1515
  const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
1492
1516
  const fresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
1493
- if (cur && cur.trim() && normText(cur) !== normText(beforeText) && !(await this._isGenerating())) {
1517
+ if (cur &&
1518
+ cur.trim() &&
1519
+ normText(cur) !== normText(beforeText) &&
1520
+ !(await this._isGenerating())) {
1494
1521
  return cur;
1495
1522
  }
1496
1523
  if (fresh) {
@@ -1550,14 +1577,20 @@ export class DeepSeekBrowser {
1550
1577
  }
1551
1578
  }
1552
1579
  const netFresh = !!this._netCapture && this._netCaptureAt >= this._lastSentAt;
1553
- const cur = await this._readLastAnswerTextCleanDom().catch(() => '');
1580
+ // The DOM may be empty while the answer is already in the network
1581
+ // capture (a resumed chat re-renders its history, the answer selector
1582
+ // may lag, or page.evaluate times out). In that case fall back to the
1583
+ // raw capture: it is the CURRENT answer and must not be ignored just
1584
+ // because the DOM has not caught up.
1585
+ let cur = await this._readLastAnswerTextCleanDom().catch(() => '');
1586
+ if (!cur && netFresh)
1587
+ cur = this._cleanAnswer(this._netCapture);
1554
1588
  // Ignore an "answer" that is identical to what was on the page BEFORE we
1555
1589
  // sent the message: that is the previous answer, not a new one. Returning
1556
1590
  // it would make the agent re-process the old tool call (or silently
1557
1591
  // stop). We keep waiting instead. A fresh network capture is exempt: it
1558
1592
  // belongs to the CURRENT send even if the DOM still shows the old text.
1559
- const isNew = !!cur &&
1560
- (netFresh || normText(cur) !== normText(beforeText));
1593
+ const isNew = !!cur && (netFresh || normText(cur) !== normText(beforeText));
1561
1594
  // An echo/stale answer equals beforeText, so isNew stays false and the
1562
1595
  // old loop waited until the full timeout — the "agent stopped after a
1563
1596
  // tool call" hang. If generation has clearly ENDED (no Stop button) and
@@ -1581,7 +1614,18 @@ export class DeepSeekBrowser {
1581
1614
  }
1582
1615
  if (isNew)
1583
1616
  last = cur;
1584
- this._askDebug('FIN-loop isNew=' + isNew + ' sameAsBefore=' + sameAsBefore + ' stable=' + stable + ' curLen=' + cur.length + ' lastLen=' + last.length + ' netFresh=' + netFresh);
1617
+ this._askDebug('FIN-loop isNew=' +
1618
+ isNew +
1619
+ ' sameAsBefore=' +
1620
+ sameAsBefore +
1621
+ ' stable=' +
1622
+ stable +
1623
+ ' curLen=' +
1624
+ cur.length +
1625
+ ' lastLen=' +
1626
+ last.length +
1627
+ ' netFresh=' +
1628
+ netFresh);
1585
1629
  await this.page.waitForTimeout(Math.max(0, this.stabilityDelayMs));
1586
1630
  }
1587
1631
  if (last && (normText(last) !== normText(beforeText) || this._netCapture)) {
@@ -1593,7 +1637,10 @@ export class DeepSeekBrowser {
1593
1637
  if (this._netCapture && this._netCaptureAt >= this._lastSentAt) {
1594
1638
  return this._netCapture;
1595
1639
  }
1596
- this._askDebug('THROW no-new-answer lastLen=' + last.length + ' beforeLen=' + beforeText.length);
1640
+ this._askDebug('THROW no-new-answer lastLen=' +
1641
+ last.length +
1642
+ ' beforeLen=' +
1643
+ beforeText.length);
1597
1644
  throw new Error(this._t('ds.send_no_new_answer'));
1598
1645
  }
1599
1646
  async dumpDom(filePath) {
package/dist/input.js CHANGED
@@ -174,6 +174,24 @@ export function layoutInput(promptStr, buf, cursor, cols) {
174
174
  // percentage of CONTEXT_LIMIT. Exported so it is unit-tested without a live
175
175
  // editor. A null/undefined/NaN count renders an empty string (no status).
176
176
  export const CONTEXT_LIMIT = 1_000_000;
177
+ // Thresholds for coloring the context counter. Below YELLOW the value is
178
+ // green (plenty of room), between YELLOW and RED it is yellow (getting full),
179
+ // above RED it is red (nearly exhausted). Percent of CONTEXT_LIMIT.
180
+ export const CONTEXT_YELLOW_PCT = 50;
181
+ export const CONTEXT_RED_PCT = 80;
182
+ // The color role for a context fill level, as a stable string so it can be
183
+ // unit-tested without a terminal: 'ok' | 'warn' | 'high'. Null tokens -> null.
184
+ export function tokenStatusLevel(tokens, limit = CONTEXT_LIMIT) {
185
+ if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
186
+ return null;
187
+ }
188
+ const pct = (tokens / limit) * 100;
189
+ if (pct >= CONTEXT_RED_PCT)
190
+ return 'high';
191
+ if (pct >= CONTEXT_YELLOW_PCT)
192
+ return 'warn';
193
+ return 'ok';
194
+ }
177
195
  export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
178
196
  if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
179
197
  return '';
@@ -247,6 +265,8 @@ export class LineEditor {
247
265
  // percentage of the context limit, right-aligned above the input line. Null
248
266
  // hides it. Updated by the caller from browser.getLastTokenUsage().
249
267
  contextStatus;
268
+ // The raw token count behind contextStatus (for the color level).
269
+ contextTokens;
250
270
  // A callback the editor calls to fetch the CURRENT token count before every
251
271
  // status render, so the status line stays fresh without the caller polling.
252
272
  onContextQuery;
@@ -283,6 +303,7 @@ export class LineEditor {
283
303
  this.onClipboard = null;
284
304
  this.locked = false;
285
305
  this.contextStatus = null;
306
+ this.contextTokens = null;
286
307
  this.onContextQuery = null;
287
308
  }
288
309
  // The token status for the CURRENT render: refreshed from onContextQuery
@@ -291,7 +312,9 @@ export class LineEditor {
291
312
  if (this.onContextQuery) {
292
313
  try {
293
314
  const n = this.onContextQuery();
294
- this.contextStatus = n === null || n === undefined ? null : formatTokenStatus(n) || null;
315
+ this.contextTokens = typeof n === 'number' ? n : null;
316
+ this.contextStatus =
317
+ n === null || n === undefined ? null : formatTokenStatus(n) || null;
295
318
  }
296
319
  catch {
297
320
  // A broken callback must never break the render.
@@ -299,6 +322,19 @@ export class LineEditor {
299
322
  }
300
323
  return this.contextStatus;
301
324
  }
325
+ // The context text colored by fill level: green (ok), yellow (warn),
326
+ // red (high).
327
+ _contextText() {
328
+ const ctx = this._contextForRender();
329
+ if (!ctx)
330
+ return '';
331
+ const level = tokenStatusLevel(this.contextTokens);
332
+ if (level === 'high')
333
+ return theme.error(ctx);
334
+ if (level === 'warn')
335
+ return theme.warn(ctx);
336
+ return theme.success(ctx);
337
+ }
302
338
  // Read the OS clipboard for an image and insert its marker. Used when the
303
339
  // terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
304
340
  async _tryClipboard() {
@@ -420,6 +456,8 @@ export class LineEditor {
420
456
  }
421
457
  // Update the token-context text shown at the right of the status line.
422
458
  setContextStatus(tokens) {
459
+ this.contextTokens =
460
+ typeof tokens === 'number' && Number.isFinite(tokens) ? tokens : null;
423
461
  this.contextStatus = formatTokenStatus(tokens) || null;
424
462
  this._render();
425
463
  }
@@ -459,19 +497,31 @@ export class LineEditor {
459
497
  // token context right-aligned on the SAME row (10k · 12%). The context is
460
498
  // refreshed from onContextQuery() on every render, so it follows the live
461
499
  // DeepSeek counter without a polling timer of its own.
462
- const ctx = this._contextForRender();
463
- const ctxText = ctx ? theme.dim(ctx) : '';
500
+ const ctxText = this._contextText();
464
501
  if (this.statusText) {
465
502
  if (ctxText) {
466
- const pad = Math.max(1, cols - visLen(this.statusText) - visLen(ctxText));
467
- out += this.statusText + ' '.repeat(pad) + ctxText + NL;
503
+ // Right-align the context on the SAME row as the status. When the
504
+ // status is too long to leave room, do NOT cram them together (that
505
+ // pushed the trailing `%` past the right edge and it got truncated):
506
+ // put the context on its own line instead.
507
+ const space = cols - visLen(this.statusText) - visLen(ctxText);
508
+ if (space >= 2) {
509
+ out += this.statusText + ' '.repeat(space) + ctxText + NL;
510
+ top = visRows(this.statusText + ' '.repeat(2) + ctxText, cols);
511
+ }
512
+ else {
513
+ out += this.statusText + NL;
514
+ top = visRows(this.statusText, cols);
515
+ out += ' '.repeat(Math.max(0, cols - visLen(ctxText))) + ctxText + NL;
516
+ top += visRows(ctxText, cols);
517
+ }
468
518
  }
469
519
  else {
470
520
  out += this.statusText + NL;
521
+ // The status may wrap onto several lines — we account for this,
522
+ // otherwise the block erase misses and statuses pile up.
523
+ top = visRows(this.statusText, cols);
471
524
  }
472
- // The status may wrap onto several lines — we account for this,
473
- // otherwise the block erase misses and statuses pile up.
474
- top = visRows(this.statusText + (ctxText ? ' '.repeat(2) + ctxText : ''), cols);
475
525
  }
476
526
  else if (ctxText) {
477
527
  // Idle: no spinner, but the context still belongs on its own line just
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "start": "node ./dist/index.js",
18
- "dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev",
18
+ "dev": "ZAMES_NET_DEBUG=1 tsx ./src/index.ts --dev --headed",
19
19
  "build": "tsc -p tsconfig.build.json",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "prepublishOnly": "npm run build",