zames_pro 2.48.0 → 2.49.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/browser.js CHANGED
@@ -87,6 +87,13 @@ export function askPassword(question) {
87
87
  }
88
88
  const USER_DATA_DIR = path.join(os.homedir(), '.zames', 'profile');
89
89
  const CHAT_URL = 'https://chat.deepseek.com/';
90
+ // The "New chat" control is NOT a <button>/<a> on the current DeepSeek build
91
+ // (it is a <div>), so matching only button/a missed it, the click timed out
92
+ // and newChat() silently no-opped. Match any clickable-ish element whose text
93
+ // is EXACTLY the new-chat label (an anchored regex keeps a big outer container
94
+ // from matching its long descendant text).
95
+ const NEW_CHAT_SELECTOR = 'button, a, [role="button"], div, span';
96
+ const NEW_CHAT_TEXT_RE = /^\s*(new chat|новый чат|новый диалог)\s*$/i;
90
97
  // Cache of the sanitized (non-Headless) User-Agent, keyed by the browser
91
98
  // engine path. On the FIRST headless run we detect the real UA, strip the
92
99
  // "Headless" marker and store it here; later runs pass it straight to
@@ -1175,8 +1182,8 @@ export class DeepSeekBrowser {
1175
1182
  async newChat() {
1176
1183
  const before = await this.getCurrentChatId();
1177
1184
  const newChatBtn = this.page
1178
- .locator('button, a')
1179
- .filter({ hasText: /new chat|новый чат|новый диалог/i })
1185
+ .locator(NEW_CHAT_SELECTOR)
1186
+ .filter({ hasText: NEW_CHAT_TEXT_RE })
1180
1187
  .first();
1181
1188
  let clicked = false;
1182
1189
  try {
@@ -1184,26 +1191,34 @@ export class DeepSeekBrowser {
1184
1191
  clicked = true;
1185
1192
  }
1186
1193
  catch {
1187
- // fall through to the goto fallback below
1194
+ // No usable control — fall through to the goto fallback below.
1188
1195
  }
1189
- // Give the app a moment to react to the click, then poll for the positive
1190
- // signal (input ready AND chat changed). If the click did nothing, fall
1191
- // back to navigating to the base URL.
1196
+ // Poll for the POSITIVE signal: the chat id must actually change. The old
1197
+ // code accepted `!clicked` as success, so a failed click returned as if a
1198
+ // fresh chat had opened and the next prompt went into the OLD chat (this
1199
+ // is what made auto-compact loop on the same chat). The old chat also has
1200
+ // a ready input, so an input alone is not proof.
1192
1201
  let ok = false;
1193
- const deadline = Date.now() + 5000;
1194
- while (Date.now() < deadline) {
1195
- if (await this._awaitInput(500)) {
1196
- if (!clicked || (await this._chatIdChangedFrom(before))) {
1202
+ if (clicked) {
1203
+ const deadline = Date.now() + 5000;
1204
+ while (Date.now() < deadline) {
1205
+ if (await this._chatIdChangedFrom(before)) {
1197
1206
  ok = true;
1198
1207
  break;
1199
1208
  }
1209
+ await this.page.waitForTimeout(100);
1200
1210
  }
1201
- await this.page.waitForTimeout(100);
1202
1211
  }
1203
1212
  if (!ok) {
1213
+ // Navigating to the base URL always yields a fresh chat.
1204
1214
  await this.page.goto(CHAT_URL, { waitUntil: 'domcontentloaded' });
1205
1215
  await this._awaitInput(5000);
1206
1216
  }
1217
+ // The chat id sniffed off the network belongs to the PREVIOUS chat; the
1218
+ // base URL carries no id, so getCurrentChatId() would otherwise return the
1219
+ // STALE old id for the fresh chat. Clear it — a fresh chat has no id until
1220
+ // the first message creates one.
1221
+ this._netChatId = null;
1207
1222
  }
1208
1223
  async _findVisible(selectors, timeout = 1000) {
1209
1224
  for (const sel of selectors) {
@@ -15,15 +15,23 @@ function decodeContent(content, contentBase64) {
15
15
  }
16
16
  return String(content ?? '');
17
17
  }
18
- // The todo list is per-process session state. It is deliberately NOT
19
- // persisted: a fresh run starts with an empty list, like Claude Code's
20
- // session checklist.
21
- const todoList = [];
22
- export function getTodos() {
23
- return todoList.map((t) => ({ ...t }));
18
+ const defaultTodoStore = { items: [] };
19
+ export function createTodoStore(onChange) {
20
+ return { items: [], onChange };
24
21
  }
25
- export function resetTodos() {
26
- todoList.length = 0;
22
+ export function getTodos(store = defaultTodoStore) {
23
+ return store.items.map((t) => ({ ...t }));
24
+ }
25
+ export function resetTodos(store = defaultTodoStore) {
26
+ store.items.length = 0;
27
+ }
28
+ // Replace the whole list. Used by TodoWrite and by the session restore (which
29
+ // re-injects the persisted checklist).
30
+ export function setTodos(items, store = defaultTodoStore) {
31
+ const clean = normalizeTodos(items);
32
+ store.items.length = 0;
33
+ store.items.push(...clean);
34
+ store.onChange?.(store.items.map((t) => ({ ...t })));
27
35
  }
28
36
  export function renderTodos(items) {
29
37
  if (!items.length)
@@ -160,7 +168,8 @@ export function applyUpdateHunk(content, hunkLines) {
160
168
  };
161
169
  }
162
170
  // ---------- factory ----------
163
- export function createExtraTools(workdir, { undo } = {}) {
171
+ export function createExtraTools(workdir, { undo, todos } = {}) {
172
+ const todoStore = todos || defaultTodoStore;
164
173
  const root = path.resolve(workdir);
165
174
  const safe = (p) => {
166
175
  const resolved = path.resolve(root, p);
@@ -289,10 +298,8 @@ export function createExtraTools(workdir, { undo } = {}) {
289
298
  else {
290
299
  throw new Error('todos must be an array.');
291
300
  }
292
- const items = normalizeTodos(list);
293
- todoList.length = 0;
294
- todoList.push(...items);
295
- return renderTodos(todoList);
301
+ setTodos(normalizeTodos(list), todoStore);
302
+ return renderTodos(todoStore.items);
296
303
  },
297
304
  },
298
305
  {
package/dist/i18n.js CHANGED
@@ -205,6 +205,10 @@ const CATALOG = {
205
205
  ru: '/queue [clear] показать/очистить очередь сообщений',
206
206
  en: '/queue [clear] list/clear the pending message queue',
207
207
  },
208
+ 'help.cmd.tasks': {
209
+ ru: '/tasks список задач агента (TodoWrite)',
210
+ en: '/tasks the agent task list (TodoWrite)',
211
+ },
208
212
  'help.cmd.thinking': {
209
213
  ru: '/thinking [on|off] включить/выключить режим размышления (можно во время работы)',
210
214
  en: '/thinking [on|off] toggle Deep thinking (works while the agent is busy)',
@@ -496,6 +500,20 @@ const CATALOG = {
496
500
  'status.transcript': { ru: 'Транскрипт: {v}', en: 'Transcript: {v}' },
497
501
  'status.locale': { ru: 'Язык: {v}', en: 'Language: {v}' },
498
502
  'status.goal': { ru: 'Цель сессии: {v}', en: 'Session goal: {v}' },
503
+ 'status.tasks_summary': {
504
+ ru: 'задачи: {done}/{total}',
505
+ en: 'tasks: {done}/{total}',
506
+ },
507
+ // ---------- tasks (/tasks) ----------
508
+ 'tasks.title': { ru: 'Задачи агента:', en: 'Agent tasks:' },
509
+ 'tasks.empty': {
510
+ ru: 'Список задач пуст (агент ещё не вызывал TodoWrite).',
511
+ en: 'The task list is empty (the agent has not called TodoWrite yet).',
512
+ },
513
+ 'tasks.restored': {
514
+ ru: 'Восстановлен список задач: {done}/{total}',
515
+ en: 'Task list restored: {done}/{total}',
516
+ },
499
517
  // ---------- messages ----------
500
518
  'msg.working_dir': {
501
519
  ru: 'Рабочая директория: {v}',
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import { theme } from './theme.js';
7
7
  import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
8
8
  import { createTools } from './tools.js';
9
+ import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
9
10
  import { runAgentLoop } from './agent-loop.js';
10
11
  import { createSpinner } from './spinner.js';
11
12
  import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
@@ -21,7 +22,7 @@ import { performCompact } from './compact.js';
21
22
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
22
23
  import { renderMarkdown } from './markdown.js';
23
24
  import { closeWeb } from './web.js';
24
- import { saveSession, loadLastSession, listSessions, sessionsDir, } from './sessions.js';
25
+ import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, } from './sessions.js';
25
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
27
  const NL = String.fromCharCode(10);
27
28
  // Floor for a /loop interval. A loop faster than the send throttle (15s)
@@ -330,6 +331,7 @@ ${theme.bold(t('help.commands'))}
330
331
  ${t('help.cmd.web')}
331
332
  ${t('help.cmd.compact')}
332
333
  ${t('help.cmd.queue')}
334
+ ${t('help.cmd.tasks')}
333
335
  ${t('help.cmd.config')}
334
336
  ${t('help.cmd.lang')}
335
337
  ${t('help.cmd.debug_dom')}
@@ -393,6 +395,7 @@ const SLASH_COMMANDS = [
393
395
  { name: '/web', key: 'help.cmd.web' },
394
396
  { name: '/compact', key: 'help.cmd.compact' },
395
397
  { name: '/queue', key: 'help.cmd.queue' },
398
+ { name: '/tasks', key: 'help.cmd.tasks' },
396
399
  { name: '/config', key: 'help.cmd.config' },
397
400
  { name: '/skills', key: 'help.cmd.skills' },
398
401
  { name: '/memory', key: 'help.cmd.memory' },
@@ -1334,6 +1337,51 @@ async function main() {
1334
1337
  let lastChats = [];
1335
1338
  let currentChatId = null;
1336
1339
  let running = true;
1340
+ // The agent's TodoWrite checklist, shared with the tool factory and
1341
+ // persisted in the session JSON. A store (not module state) so /reload does
1342
+ // not silently drop the list, and so the writer in onTodosChange sees the
1343
+ // same instance the tools mutate.
1344
+ const todoStore = createTodoStore();
1345
+ const persistTodos = () => {
1346
+ if (!currentChatId)
1347
+ return;
1348
+ saveSession({
1349
+ id: currentChatId,
1350
+ workdir: currentWorkdir,
1351
+ todos: getTodos(todoStore),
1352
+ });
1353
+ };
1354
+ // Write the checklist whenever the agent changes it, so a restart/resume
1355
+ // brings the tasks back (they used to be in-memory only).
1356
+ todoStore.onChange = () => persistTodos();
1357
+ // A compact "tasks: 2/5" summary for the status line. Empty when the list
1358
+ // is empty, so an idle status is not cluttered.
1359
+ const todosSummary = () => {
1360
+ const items = getTodos(todoStore);
1361
+ if (!items.length)
1362
+ return '';
1363
+ const done = items.filter((t) => t.status === 'completed').length;
1364
+ return t('status.tasks_summary', {
1365
+ done: String(done),
1366
+ total: String(items.length),
1367
+ });
1368
+ };
1369
+ // Re-inject a persisted checklist after /resume or the startup restore.
1370
+ // The list is REPLACED (not merged): a resumed chat with no saved checklist
1371
+ // must clear the previous chat's tasks, otherwise the status line keeps
1372
+ // showing them for a chat that never had them.
1373
+ const loadSessionTodos = (id) => {
1374
+ const s = readSession(id);
1375
+ const items = s?.todos || [];
1376
+ setTodos(items, todoStore);
1377
+ if (!items.length)
1378
+ return;
1379
+ const done = items.filter((x) => x.status === 'completed').length;
1380
+ console.log(theme.system(t('tasks.restored', {
1381
+ done: String(done),
1382
+ total: String(items.length),
1383
+ })));
1384
+ };
1337
1385
  // Long-lived session goal (set via /goal). Prepended to every task message so
1338
1386
  // the model keeps the big picture across many turns. Persisted per working
1339
1387
  // directory in <project>/.zames-goal (best-effort); restored right away.
@@ -1441,6 +1489,7 @@ async function main() {
1441
1489
  sendSystemPromptNext = promptOnResume();
1442
1490
  saveLastChat(resumeId, currentWorkdir);
1443
1491
  console.log(theme.system(t('msg.chat_opened') + ' ' + resumeId + String.fromCharCode(10)));
1492
+ loadSessionTodos(resumeId);
1444
1493
  await printRestoredHistory(browser, null, resumeId);
1445
1494
  }
1446
1495
  catch (e) {
@@ -1490,6 +1539,9 @@ async function main() {
1490
1539
  // Queue badge: show "⧗N" while messages are waiting to be sent after the
1491
1540
  // current task. Pulled on every render (like the context counter).
1492
1541
  ed.onQueueQuery = () => pendingQueue.length;
1542
+ // Task-list summary ("tasks: 2/5") before the context counter. Pulled on
1543
+ // every render, like the queue badge.
1544
+ ed.onTasksQuery = () => todosSummary();
1493
1545
  // Use the configured context window for the fill percentage/color.
1494
1546
  ed.setContextLimit(config.ui.contextLimit);
1495
1547
  // Toggle icons (🧠 deep thinking, 🌐 web search) before the context
@@ -2061,6 +2113,9 @@ async function main() {
2061
2113
  await browser.newChat();
2062
2114
  freshChatNext = false;
2063
2115
  sendSystemPromptNext = true;
2116
+ // A new chat starts a fresh checklist: keeping the old tasks would
2117
+ // label them as "restored" in a chat that never saw them.
2118
+ resetTodos(todoStore);
2064
2119
  currentChatId = await browser.getCurrentChatId();
2065
2120
  saveLastChat(currentChatId, currentWorkdir);
2066
2121
  transcript.log('new_chat');
@@ -2310,6 +2365,7 @@ async function main() {
2310
2365
  theme.system(sendSystemPromptNext
2311
2366
  ? String.fromCharCode(10) + t('chats.prompt_will_resend')
2312
2367
  : String.fromCharCode(10) + t('chats.prompt_no_resend')));
2368
+ loadSessionTodos(pick.id);
2313
2369
  await printRestoredHistory(browser, editor, pick.id);
2314
2370
  }
2315
2371
  catch (e) {
@@ -2370,6 +2426,7 @@ async function main() {
2370
2426
  transcript.log('resume_chat', { id });
2371
2427
  console.log(theme.assistant(t('msg.chat_opened')) +
2372
2428
  theme.system(String.fromCharCode(10)));
2429
+ loadSessionTodos(id);
2373
2430
  await printRestoredHistory(browser, editor, id);
2374
2431
  }
2375
2432
  catch (e) {
@@ -2505,6 +2562,20 @@ async function main() {
2505
2562
  console.log(theme.dim(t('msg.queue_cleared_hint')));
2506
2563
  continue;
2507
2564
  }
2565
+ if (lower === '/tasks') {
2566
+ const items = getTodos(todoStore);
2567
+ if (!items.length) {
2568
+ console.log(theme.dim(t('tasks.empty')));
2569
+ continue;
2570
+ }
2571
+ console.log(theme.system(t('tasks.title')));
2572
+ // Skip the "Todo list (N/M done):" header line from renderTodos — the
2573
+ // /tasks title already says it.
2574
+ for (const line of renderTodos(items).split(NL).slice(1)) {
2575
+ console.log(theme.assistant(line));
2576
+ }
2577
+ continue;
2578
+ }
2508
2579
  if (lower.startsWith('/thinking') || lower.startsWith('/web')) {
2509
2580
  const tg = parseLiveToggle(trimmed);
2510
2581
  if (tg) {
@@ -2592,7 +2663,10 @@ async function main() {
2592
2663
  'layout; and any gotchas or rules an agent must follow. Keep it concise ' +
2593
2664
  'and factual, based only on what you find. Write the file with the Write ' +
2594
2665
  'tool, then reply via respond with a one-line summary.';
2595
- const initTools = mod.createTools(currentWorkdir, { undo });
2666
+ const initTools = mod.createTools(currentWorkdir, {
2667
+ undo,
2668
+ todos: todoStore,
2669
+ });
2596
2670
  if (editor)
2597
2671
  editor.busy = true;
2598
2672
  console.log(theme.system(t('init.analyzing')));
@@ -2874,7 +2948,7 @@ async function main() {
2874
2948
  transcript,
2875
2949
  ui: editor,
2876
2950
  buildSystemPrompt: mod.buildSystemPrompt,
2877
- tools: mod.createTools(currentWorkdir, { undo }),
2951
+ tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
2878
2952
  answerTimeoutMs: config.browser.answerTimeoutMs,
2879
2953
  fallbackLimit: COMPACT_FALLBACK_LIMIT,
2880
2954
  });
@@ -2892,7 +2966,10 @@ async function main() {
2892
2966
  const staged = rest.indexOf('--staged') !== -1;
2893
2967
  const focus = rest.replace(/--staged/g, '').trim();
2894
2968
  const reviewTask = buildReviewPrompt(focus, staged);
2895
- const reviewTools = mod.createTools(currentWorkdir, { undo });
2969
+ const reviewTools = mod.createTools(currentWorkdir, {
2970
+ undo,
2971
+ todos: todoStore,
2972
+ });
2896
2973
  if (editor)
2897
2974
  editor.busy = true;
2898
2975
  try {
@@ -3010,7 +3087,7 @@ async function main() {
3010
3087
  // Dev mode: pick up fresh logic modules before the task.
3011
3088
  await autoReload();
3012
3089
  transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
3013
- const tools = mod.createTools(currentWorkdir, { undo });
3090
+ const tools = mod.createTools(currentWorkdir, { undo, todos: todoStore });
3014
3091
  if (mcpPool)
3015
3092
  tools.push(...mcpPool.tools);
3016
3093
  if (editor)
@@ -3046,7 +3123,10 @@ async function main() {
3046
3123
  transcript,
3047
3124
  ui: editor,
3048
3125
  buildSystemPrompt: mod.buildSystemPrompt,
3049
- tools: mod.createTools(currentWorkdir, { undo }),
3126
+ tools: mod.createTools(currentWorkdir, {
3127
+ undo,
3128
+ todos: todoStore,
3129
+ }),
3050
3130
  answerTimeoutMs: config.browser.answerTimeoutMs,
3051
3131
  quiet: true,
3052
3132
  skipUiLock: true,
package/dist/input.js CHANGED
@@ -315,6 +315,9 @@ export class LineEditor {
315
315
  // A callback the editor calls before every render to read the CURRENT queue
316
316
  // length, so the badge stays live without the caller polling.
317
317
  onQueueQuery;
318
+ // A callback the editor calls before every render to read the agent's
319
+ // current task-list summary ("tasks: 2/5"), empty when there is no list.
320
+ onTasksQuery;
318
321
  // Whether the DeepSeek "Deep thinking" / "Smart search" toggles are ON.
319
322
  // Shown as two colored icons before the context counter: a dim gray icon
320
323
  // means off, a teal-green icon means on.
@@ -395,6 +398,7 @@ export class LineEditor {
395
398
  this.onContextQuery = null;
396
399
  this.queueLength = 0;
397
400
  this.onQueueQuery = null;
401
+ this.onTasksQuery = null;
398
402
  }
399
403
  // Refresh the toggle icon states from onToggleQuery (if wired) before a
400
404
  // render. A broken callback must never break the render.
@@ -460,6 +464,21 @@ export class LineEditor {
460
464
  return '';
461
465
  return theme.warn('⧗' + this.queueLength) + ' ';
462
466
  }
467
+ // The agent's task-list summary ("tasks: 2/5") shown before the context
468
+ // counter. Empty when there is no list, so an idle status is not cluttered.
469
+ // Refreshed from onTasksQuery before every render when wired.
470
+ _tasksBadge() {
471
+ if (!this.onTasksQuery)
472
+ return '';
473
+ try {
474
+ const s = this.onTasksQuery();
475
+ return s ? theme.system(s) + ' ' : '';
476
+ }
477
+ catch {
478
+ // A broken callback must never break the render.
479
+ return '';
480
+ }
481
+ }
463
482
  // Icons for the DeepSeek toggles: 🧠 "Deep thinking" and 🌐 "Smart search",
464
483
  // shown before the context counter. Only the ENABLED toggles are shown, so
465
484
  // the state is unambiguous WITHOUT relying on the terminal coloring the
@@ -695,7 +714,7 @@ export class LineEditor {
695
714
  // but the icons are still meaningful.
696
715
  const ctxRaw = this._contextText();
697
716
  this._refreshToggles();
698
- const ctxText = this._queueBadge() + this._toggleIcons() + ctxRaw;
717
+ const ctxText = this._queueBadge() + this._tasksBadge() + this._toggleIcons() + ctxRaw;
699
718
  let statusOut = '';
700
719
  let top = 0;
701
720
  // Never fill a row to the FULL terminal width: on terminals with
package/dist/sessions.js CHANGED
@@ -15,7 +15,7 @@ function safeName(id) {
15
15
  }
16
16
  // Save/update a session. workdir is needed so that when launched from the same
17
17
  // project we restore exactly its last chat.
18
- export function saveSession({ id, title = '', workdir = '', }) {
18
+ export function saveSession({ id, title = '', workdir = '', todos, }) {
19
19
  if (!id)
20
20
  return null;
21
21
  try {
@@ -34,6 +34,11 @@ export function saveSession({ id, title = '', workdir = '', }) {
34
34
  createdAt: prev.createdAt || now,
35
35
  updatedAt: now,
36
36
  };
37
+ // Keep the previous checklist unless a new one is explicitly passed —
38
+ // saveLastChat() calls this for title/workdir only and must not wipe it.
39
+ const nextTodos = todos !== undefined ? todos : prev.todos;
40
+ if (nextTodos !== undefined)
41
+ data.todos = nextTodos;
37
42
  fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8');
38
43
  writeIndex({ id, workdir: data.workdir });
39
44
  return data;
package/dist/tools.js CHANGED
@@ -8,7 +8,7 @@ import { createExtraTools } from './extraTools.js';
8
8
  // user-facing. They must be in ENGLISH (like Claude Code / Codex). Only the
9
9
  // strings the OPERATOR sees (help, spinner, service messages) are localized —
10
10
  // those live in src/i18n.ts (CATALOG) and are picked by the locale setting.
11
- export function createTools(workdir, { undo } = {}) {
11
+ export function createTools(workdir, { undo, todos } = {}) {
12
12
  const root = path.resolve(workdir);
13
13
  const safe = (p) => {
14
14
  const resolved = path.resolve(root, p);
@@ -256,7 +256,7 @@ export function createTools(workdir, { undo } = {}) {
256
256
  ];
257
257
  const gitTools = createGitTools(workdir);
258
258
  const webTools = createWebTools();
259
- const extraTools = createExtraTools(workdir, { undo });
259
+ const extraTools = createExtraTools(workdir, { undo, todos });
260
260
  const respondTool = {
261
261
  name: 'respond',
262
262
  description: 'Give the final answer to the user and finish the task.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.48.0",
3
+ "version": "2.49.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {