zames_pro 2.65.0 → 2.66.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.
@@ -53,11 +53,6 @@ export function runConfigMenu(opts) {
53
53
  }
54
54
  // cursor indexes the VISIBLE (filtered) list, not the full `fields`.
55
55
  let cursor = 0;
56
- // Index of the first VISIBLE field drawn. The menu is WINDOWED so the whole
57
- // block always fits the terminal height; otherwise a block taller than the
58
- // screen scrolled the top rows off and the `ESC[lastRows A` cursor restore
59
- // landed on the wrong line, so arrow-key navigation did not scroll the list.
60
- let scrollTop = 0;
61
56
  let editing = false;
62
57
  let editBuf = '';
63
58
  // Reverse/forward filter over the list (/ to enter, typing narrows it).
@@ -141,21 +136,25 @@ export function runConfigMenu(opts) {
141
136
  // Bias one row to the SAFE side (a spare blank row is harmless, a block
142
137
  // that overflows scrolls the top away and breaks the cursor restore).
143
138
  const maxItemRows = Math.max(3, termRows - 2 - tail.length);
144
- // WINDOW over the fields: keep the cursor inside [scrollTop, +maxItemRows).
145
- // Because a group header also consumes a row we use the cursor index as the
146
- // primary bound and let the header be counted when the first shown field
147
- // starts a new group (a rare one-row overshoot is harmless — a spare row is
148
- // reserved above).
149
- if (cursor < scrollTop)
150
- scrollTop = cursor;
151
- if (cursor >= scrollTop + maxItemRows)
152
- scrollTop = cursor - maxItemRows + 1;
153
- const maxTop = Math.max(0, vis.length - maxItemRows);
154
- if (scrollTop > maxTop)
155
- scrollTop = maxTop;
156
- if (scrollTop < 0)
157
- scrollTop = 0;
158
- const start = scrollTop;
139
+ // WINDOW over the fields. We walk BACK from the cursor, counting the rows
140
+ // the window would take (a group header consumes a row too), until adding
141
+ // one more field would overflow. This guarantees the SELECTED item is always
142
+ // on screen — the old index-based scrollTop could push it out when many
143
+ // group headers sat between the window top and the cursor.
144
+ const groupStart = (i) => i === 0 || vis[i].groupKey !== vis[i - 1].groupKey;
145
+ // Rows needed to draw the window [from, cursor] (inclusive).
146
+ const rowsFor = (from) => {
147
+ let rows = 0;
148
+ for (let i = from; i <= cursor; i++) {
149
+ if (i === from || groupStart(i))
150
+ rows++;
151
+ rows++;
152
+ }
153
+ return rows;
154
+ };
155
+ let start = vis.length ? Math.max(0, Math.min(cursor, vis.length - 1)) : 0;
156
+ while (start > 0 && rowsFor(start - 1) <= maxItemRows)
157
+ start--;
159
158
  const lines = [];
160
159
  // Title with a position counter ("Settings — 12/49") so the operator knows
161
160
  // how far down a long list they are; a filter narrows both numbers.
package/dist/index.js CHANGED
@@ -19,13 +19,10 @@ import { Transcript } from './transcript.js';
19
19
  import { UndoStore } from './undo.js';
20
20
  import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
21
21
  import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
22
- import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTranscript, summarizeTranscript, renderCost, formatExport, defaultExportPath, renderDoctor, resolveExtraDir, buildReviewPrompt, isDevOnlyCommand, parseBacklogItems, nextBacklogItem, buildImprovePrompt, formatRelativeTime, trimRestoredMessages, RESTORED_HISTORY_LIMIT, hasQueuedJob, buildSlashCommandHints, parseQueueCommand, parseLiveToggle, parseGoalCommand, isLiveConfigCommand, formatQueueList, ctrlCEscalation, expandCommandArgs, missingCommandArgs, } from './commands.js';
23
22
  import { performCompact } from './compact.js';
24
23
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
25
24
  import { renderMarkdown, setAnswerWidth } from './markdown.js';
26
- import { resolveAttachPath, inlineAtRefs } from './attach-refs.js';
27
25
  import { parseBacklogNote } from './backlog.js';
28
- import { runTask } from './run-task.js';
29
26
  import { closeWeb } from './web.js';
30
27
  import { assertCommandInsideRoot, runShell } from './shell.js';
31
28
  import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
@@ -198,8 +195,19 @@ const RELOADABLE = [
198
195
  'config',
199
196
  'fsutil',
200
197
  'backlog',
198
+ // C3 modules index.ts now calls through the mod bag (G2): without this a
199
+ // /reload silently kept the OLD commands/run-task/attach-refs until a restart.
200
+ // 'input/layout' is NOT listed: its only live consumers are input.ts (the
201
+ // long-lived LineEditor, itself not reloadable) and spinner.ts, which already
202
+ // picks up a fresh copy transitively.
203
+ 'commands',
204
+ 'run-task',
205
+ 'attach-refs',
201
206
  ];
202
207
  const mod = {
208
+ commands: null,
209
+ attach: null,
210
+ runTask: null,
203
211
  createTools,
204
212
  runAgentLoop,
205
213
  buildSystemPrompt: null,
@@ -260,6 +268,12 @@ async function reloadModules() {
260
268
  mod.backlogStats = pick('backlog', 'backlogStats');
261
269
  if (pick('backlog', 'backlogNeedsPruning'))
262
270
  mod.backlogNeedsPruning = pick('backlog', 'backlogNeedsPruning');
271
+ if (pick('commands', 'missingCommandArgs'))
272
+ mod.commands = loaded.get('commands');
273
+ if (pick('attach-refs', 'resolveAttachPath'))
274
+ mod.attach = loaded.get('attach-refs');
275
+ if (pick('run-task', 'runTask'))
276
+ mod.runTask = pick('run-task', 'runTask');
263
277
  return { count: loaded.size, errors };
264
278
  }
265
279
  // Initial load so that mod.buildSystemPrompt and the rest are populated.
@@ -508,10 +522,10 @@ async function expandSlashTarget(workdir, name, rest) {
508
522
  const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
509
523
  if (cmd) {
510
524
  // B7: refuse to run when declared positional arguments are missing.
511
- const missing = missingCommandArgs(cmd.arguments, rest);
525
+ const missing = mod.commands.missingCommandArgs(cmd.arguments, rest);
512
526
  if (missing.length)
513
527
  return { missing };
514
- const body = expandCommandArgs(cmd.body, rest, cmd.arguments);
528
+ const body = mod.commands.expandCommandArgs(cmd.body, rest, cmd.arguments);
515
529
  return { text: body || rest };
516
530
  }
517
531
  const skills = await loadSkills(workdir);
@@ -549,7 +563,11 @@ async function expandSlashTarget(workdir, name, rest) {
549
563
  // in commands.ts (buildSlashCommandHints); here we only supply the runtime
550
564
  // data (dev flag, translator, dynamic skills/commands).
551
565
  function buildSlashCommands() {
552
- return buildSlashCommandHints({ devMode, t, dynamic: dynamicCommands });
566
+ return mod.commands.buildSlashCommandHints({
567
+ devMode,
568
+ t,
569
+ dynamic: dynamicCommands,
570
+ });
553
571
  }
554
572
  // We put the agent's temporary files (one-off scripts, etc.) in
555
573
  // <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
@@ -805,8 +823,8 @@ async function printRestoredHistory(browser, ui, chatId = null) {
805
823
  // those. The "truncated" note must compare the DISPLAYABLE count with the
806
824
  // limit (comparing `all.length` always fired, because tool-calls are
807
825
  // dropped and the raw history is always longer).
808
- const displayable = trimRestoredMessages(all, 0);
809
- const messages = trimRestoredMessages(all);
826
+ const displayable = mod.commands.trimRestoredMessages(all, 0);
827
+ const messages = mod.commands.trimRestoredMessages(all);
810
828
  // Always surface the diagnostic line when the history could not be turned
811
829
  // into anything printable — otherwise "the dialogue is empty" is a dead end
812
830
  // (was the fetch blocked? did the chat really have no user turns?).
@@ -857,7 +875,9 @@ async function printRestoredHistory(browser, ui, chatId = null) {
857
875
  }
858
876
  }
859
877
  if (displayable.length > messages.length) {
860
- out(theme.dim(t('chats.history_truncated', { n: String(RESTORED_HISTORY_LIMIT) })));
878
+ out(theme.dim(t('chats.history_truncated', {
879
+ n: String(mod.commands.RESTORED_HISTORY_LIMIT),
880
+ })));
861
881
  }
862
882
  }
863
883
  // Build the explicit dependency bag for runTask() (src/run-task.ts). The
@@ -1011,7 +1031,7 @@ async function main() {
1011
1031
  }
1012
1032
  }
1013
1033
  await autoReload();
1014
- await runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
1034
+ await mod.runTask(runTaskDeps(), browser, tools, task, currentWorkdir, {
1015
1035
  transcript,
1016
1036
  freshChat,
1017
1037
  sendSystemPrompt,
@@ -1132,7 +1152,7 @@ async function main() {
1132
1152
  // this job already has a pending message in the queue, skip this fire.
1133
1153
  // Otherwise a 1-minute loop would add a new task every minute while a
1134
1154
  // long task is in flight, and they would all run back-to-back.
1135
- if (hasQueuedJob(pendingQueue, job.id)) {
1155
+ if (mod.commands.hasQueuedJob(pendingQueue, job.id)) {
1136
1156
  transcript.log('scheduled_skip_queued', {
1137
1157
  id: job.id,
1138
1158
  task: job.task,
@@ -1300,7 +1320,7 @@ async function main() {
1300
1320
  return att;
1301
1321
  }
1302
1322
  // Case 2: the paste is a path to a local file (drag&drop or copy path).
1303
- const filePath = await resolveAttachPath(currentWorkdir, raw);
1323
+ const filePath = await mod.attach.resolveAttachPath(currentWorkdir, raw);
1304
1324
  if (!filePath)
1305
1325
  return null;
1306
1326
  const data = await fs.readFile(filePath).catch(() => null);
@@ -1378,7 +1398,7 @@ async function main() {
1378
1398
  // `await runTask()`, so ONLY commands that make sense without the main
1379
1399
  // loop are handled here (a queued slash-command would run too late).
1380
1400
  if (ed.busy) {
1381
- const q = parseQueueCommand(text);
1401
+ const q = mod.commands.parseQueueCommand(text);
1382
1402
  if (q) {
1383
1403
  if (q.sub === 'clear') {
1384
1404
  const n = pendingQueue.length;
@@ -1390,7 +1410,7 @@ async function main() {
1390
1410
  }
1391
1411
  else {
1392
1412
  ed.printAbove(theme.system(t('msg.queue_title')));
1393
- for (const line of formatQueueList(pendingQueue)) {
1413
+ for (const line of mod.commands.formatQueueList(pendingQueue)) {
1394
1414
  ed.printAbove(theme.assistant(line));
1395
1415
  }
1396
1416
  ed.printAbove(theme.dim(t('msg.queue_cleared_hint')));
@@ -1401,7 +1421,7 @@ async function main() {
1401
1421
  // browser's DESIRED state and the status icons; it does not touch the
1402
1422
  // in-flight generation, so it is safe mid-run. The actual DeepSeek
1403
1423
  // toggles are applied on the next send by _applyToggles().
1404
- const tg = parseLiveToggle(text);
1424
+ const tg = mod.commands.parseLiveToggle(text);
1405
1425
  if (tg) {
1406
1426
  applyLiveToggle(tg);
1407
1427
  return;
@@ -1412,7 +1432,7 @@ async function main() {
1412
1432
  // it pauses the editor and would fight the running task, so it stays
1413
1433
  // queued. `setConfigRuntime` already applies hot values (locale,
1414
1434
  // toggles, context limit) to the live objects immediately.
1415
- if (isLiveConfigCommand(text)) {
1435
+ if (mod.commands.isLiveConfigCommand(text)) {
1416
1436
  void handleConfigCommand(text);
1417
1437
  return;
1418
1438
  }
@@ -1458,7 +1478,7 @@ async function main() {
1458
1478
  // (sets _stopped, which also halts the queue). This makes the two
1459
1479
  // intents distinguishable instead of one press doing everything.
1460
1480
  const now = Date.now();
1461
- if (ctrlCEscalation(now - lastCtrlCAt) === 'run') {
1481
+ if (mod.commands.ctrlCEscalation(now - lastCtrlCAt) === 'run') {
1462
1482
  ed.printAbove(theme.warn(t('msg.abort_ctrlc_short')));
1463
1483
  browser.stopGeneration().catch(() => { });
1464
1484
  }
@@ -1605,7 +1625,7 @@ async function main() {
1605
1625
  // variable and a file, never the chat). Output goes above the input line
1606
1626
  // when the editor is active, otherwise to stdout.
1607
1627
  async function handleGoal(input) {
1608
- const g = parseGoalCommand(input);
1628
+ const g = mod.commands.parseGoalCommand(input);
1609
1629
  if (!g)
1610
1630
  return;
1611
1631
  const emit = (line) => {
@@ -1864,7 +1884,7 @@ async function main() {
1864
1884
  // Hiding them from /help is not enough: a user could type the command by
1865
1885
  // hand, and it would rewrite their project's BACKLOG.md or snapshot src/.
1866
1886
  if (!devMode) {
1867
- const devCmd = isDevOnlyCommand(lower);
1887
+ const devCmd = mod.commands.isDevOnlyCommand(lower);
1868
1888
  if (devCmd) {
1869
1889
  console.error(theme.warn(t('msg.dev_only', { v: devCmd })));
1870
1890
  continue;
@@ -2240,7 +2260,7 @@ async function main() {
2240
2260
  const mark = s.id === currentChatId ? theme.user(' *') : '';
2241
2261
  const title = s.title ? ` ${s.title}` : '';
2242
2262
  const wd = s.workdir ? theme.dim(` [${dirLabel(s.workdir)}]`) : '';
2243
- const age = formatRelativeTime(s.updatedAt, t);
2263
+ const age = mod.commands.formatRelativeTime(s.updatedAt, t);
2244
2264
  const ageText = age ? theme.dim(' ' + age) : '';
2245
2265
  console.log(` ${theme.user(n)}. ${s.id.slice(0, 8)}…${title}${wd}${ageText}${mark}`);
2246
2266
  });
@@ -2415,7 +2435,7 @@ async function main() {
2415
2435
  continue;
2416
2436
  }
2417
2437
  if (lower === '/queue' || lower.startsWith('/queue ')) {
2418
- const q = parseQueueCommand(trimmed);
2438
+ const q = mod.commands.parseQueueCommand(trimmed);
2419
2439
  if (!q) {
2420
2440
  console.error(theme.error(t('msg.queue_usage')));
2421
2441
  continue;
@@ -2431,7 +2451,7 @@ async function main() {
2431
2451
  continue;
2432
2452
  }
2433
2453
  console.log(theme.system(t('msg.queue_title')));
2434
- for (const line of formatQueueList(pendingQueue)) {
2454
+ for (const line of mod.commands.formatQueueList(pendingQueue)) {
2435
2455
  console.log(theme.assistant(line));
2436
2456
  }
2437
2457
  console.log(theme.dim(t('msg.queue_cleared_hint')));
@@ -2452,7 +2472,7 @@ async function main() {
2452
2472
  continue;
2453
2473
  }
2454
2474
  if (lower.startsWith('/thinking') || lower.startsWith('/web')) {
2455
- const tg = parseLiveToggle(trimmed);
2475
+ const tg = mod.commands.parseLiveToggle(trimmed);
2456
2476
  if (tg) {
2457
2477
  applyLiveToggle(tg);
2458
2478
  continue;
@@ -2751,7 +2771,7 @@ async function main() {
2751
2771
  locale: currentLocale,
2752
2772
  context: ctx,
2753
2773
  }).length;
2754
- console.log(theme.system(formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
2774
+ console.log(theme.system(mod.commands.formatContextSources(ctx.agents, ctx.memory, ctx.skills, ctx.commands, { systemPromptChars: sysLen }, t)));
2755
2775
  }
2756
2776
  catch (e) {
2757
2777
  console.error(theme.error(e.message));
@@ -2807,7 +2827,7 @@ async function main() {
2807
2827
  if (editor)
2808
2828
  editor.busy = true;
2809
2829
  try {
2810
- await runTask(runTaskDeps(), browser, tools, lastTaskText, currentWorkdir, {
2830
+ await mod.runTask(runTaskDeps(), browser, tools, lastTaskText, currentWorkdir, {
2811
2831
  transcript,
2812
2832
  freshChat: false,
2813
2833
  sendSystemPrompt: false,
@@ -2838,8 +2858,8 @@ async function main() {
2838
2858
  console.error(theme.error(t('diff.not_repo')));
2839
2859
  continue;
2840
2860
  }
2841
- const out = await runGit(diffGitArgs(staged), currentWorkdir, 20_000);
2842
- console.log(theme.system(formatDiff(out, { maxLines: 400 }, t)));
2861
+ const out = await runGit(mod.commands.diffGitArgs(staged), currentWorkdir, 20_000);
2862
+ console.log(theme.system(mod.commands.formatDiff(out, { maxLines: 400 }, t)));
2843
2863
  continue;
2844
2864
  }
2845
2865
  if (lower === '/diffstat') {
@@ -2850,28 +2870,28 @@ async function main() {
2850
2870
  continue;
2851
2871
  }
2852
2872
  const out = await runGit('git diff --stat', currentWorkdir, 20_000);
2853
- console.log(theme.system(formatDiffStat(out, { maxLines: 60 }, t)));
2873
+ console.log(theme.system(mod.commands.formatDiffStat(out, { maxLines: 60 }, t)));
2854
2874
  continue;
2855
2875
  }
2856
2876
  if (lower === '/cost' || lower === '/usage') {
2857
- let stats = summarizeTranscript([]);
2877
+ let stats = mod.commands.summarizeTranscript([]);
2858
2878
  if (transcript.file) {
2859
2879
  try {
2860
2880
  const body = await fs.readFile(transcript.file, 'utf-8');
2861
- stats = summarizeTranscript(parseTranscript(body));
2881
+ stats = mod.commands.summarizeTranscript(mod.commands.parseTranscript(body));
2862
2882
  }
2863
2883
  catch {
2864
2884
  // best-effort
2865
2885
  }
2866
2886
  }
2867
- console.log(theme.system(renderCost(stats, transcript.file, browser.getLastTokenUsage(), t)));
2887
+ console.log(theme.system(mod.commands.renderCost(stats, transcript.file, browser.getLastTokenUsage(), t)));
2868
2888
  continue;
2869
2889
  }
2870
2890
  if (lower === '/export' || lower.startsWith('/export ')) {
2871
2891
  const arg = trimmed.slice('/export'.length).trim();
2872
2892
  const target = arg
2873
2893
  ? path.resolve(currentWorkdir, arg)
2874
- : defaultExportPath(currentWorkdir);
2894
+ : mod.commands.defaultExportPath(currentWorkdir);
2875
2895
  const rel = path.relative(sandboxRoot, target);
2876
2896
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
2877
2897
  console.error(theme.error(t('export.outside')));
@@ -2880,13 +2900,13 @@ async function main() {
2880
2900
  let entries = [];
2881
2901
  if (transcript.file) {
2882
2902
  try {
2883
- entries = parseTranscript(await fs.readFile(transcript.file, 'utf-8'));
2903
+ entries = mod.commands.parseTranscript(await fs.readFile(transcript.file, 'utf-8'));
2884
2904
  }
2885
2905
  catch {
2886
2906
  entries = [];
2887
2907
  }
2888
2908
  }
2889
- const md = formatExport(entries, {
2909
+ const md = mod.commands.formatExport(entries, {
2890
2910
  chatId: currentChatId,
2891
2911
  workdir: currentWorkdir,
2892
2912
  });
@@ -2918,7 +2938,7 @@ async function main() {
2918
2938
  const mcpStatus = mcpPool
2919
2939
  ? mcpPool.status()
2920
2940
  : { servers: [], toolCount: 0 };
2921
- console.log(theme.system(renderDoctor({
2941
+ console.log(theme.system(mod.commands.renderDoctor({
2922
2942
  nodeVersion: process.version,
2923
2943
  platform: process.platform,
2924
2944
  workdir: currentWorkdir,
@@ -2944,7 +2964,7 @@ async function main() {
2944
2964
  }
2945
2965
  if (lower === '/add-dir' || lower.startsWith('/add-dir ')) {
2946
2966
  const arg = trimmed.slice('/add-dir'.length).trim();
2947
- const res = resolveExtraDir(arg, currentWorkdir, t);
2967
+ const res = mod.commands.resolveExtraDir(arg, currentWorkdir, t);
2948
2968
  if ('error' in res) {
2949
2969
  console.error(theme.warn(res.error));
2950
2970
  continue;
@@ -3000,8 +3020,8 @@ async function main() {
3000
3020
  console.error(theme.warn(t('improve.no_backlog')));
3001
3021
  continue;
3002
3022
  }
3003
- const items = parseBacklogItems(backlogText);
3004
- const item = nextBacklogItem(items, arg || undefined);
3023
+ const items = mod.commands.parseBacklogItems(backlogText);
3024
+ const item = mod.commands.nextBacklogItem(items, arg || undefined);
3005
3025
  if (!item) {
3006
3026
  console.log(theme.dim(arg ? t('improve.not_found', { v: arg }) : t('improve.all_done')));
3007
3027
  continue;
@@ -3017,7 +3037,7 @@ async function main() {
3017
3037
  if (editor)
3018
3038
  editor.busy = true;
3019
3039
  try {
3020
- await runTask(runTaskDeps(), browser, improveTools, buildImprovePrompt(item), currentWorkdir, {
3040
+ await mod.runTask(runTaskDeps(), browser, improveTools, mod.commands.buildImprovePrompt(item), currentWorkdir, {
3021
3041
  transcript,
3022
3042
  freshChat: false,
3023
3043
  sendSystemPrompt: false,
@@ -3094,7 +3114,7 @@ async function main() {
3094
3114
  const rest = trimmed.slice('/review'.length).trim();
3095
3115
  const staged = rest.indexOf('--staged') !== -1;
3096
3116
  const focus = rest.replace(/--staged/g, '').trim();
3097
- const reviewTask = buildReviewPrompt(focus, staged);
3117
+ const reviewTask = mod.commands.buildReviewPrompt(focus, staged);
3098
3118
  const reviewTools = mod.createTools(currentWorkdir, {
3099
3119
  undo,
3100
3120
  todos: todoStore,
@@ -3102,7 +3122,7 @@ async function main() {
3102
3122
  if (editor)
3103
3123
  editor.busy = true;
3104
3124
  try {
3105
- await runTask(runTaskDeps(), browser, reviewTools, reviewTask, currentWorkdir, {
3125
+ await mod.runTask(runTaskDeps(), browser, reviewTools, reviewTask, currentWorkdir, {
3106
3126
  transcript,
3107
3127
  freshChat: false,
3108
3128
  sendSystemPrompt: false,
@@ -3317,7 +3337,7 @@ async function main() {
3317
3337
  // existing files are inlined; a bare `@name` in prose is left untouched.
3318
3338
  let finalTaskText = taskText;
3319
3339
  {
3320
- const refRes = await inlineAtRefs(currentWorkdir, taskText);
3340
+ const refRes = await mod.attach.inlineAtRefs(currentWorkdir, taskText);
3321
3341
  if (refRes.inlined.length) {
3322
3342
  finalTaskText = refRes.text;
3323
3343
  console.log(theme.system(t('msg.at_refs', { n: refRes.inlined.length }) +
@@ -3357,7 +3377,7 @@ async function main() {
3357
3377
  if (editor)
3358
3378
  editor.busy = true;
3359
3379
  try {
3360
- await runTask(runTaskDeps(), browser, tools, finalTaskText, currentWorkdir, {
3380
+ await mod.runTask(runTaskDeps(), browser, tools, finalTaskText, currentWorkdir, {
3361
3381
  transcript,
3362
3382
  freshChat: freshChatNext,
3363
3383
  sendSystemPrompt: sendSystemPromptNext,
@@ -150,6 +150,28 @@ export function charWidth(cp) {
150
150
  }
151
151
  return 1;
152
152
  }
153
+ // Truncate a PLAIN (no-ANSI) string to at most `maxCols` visible columns,
154
+ // appending an ellipsis when it was cut. Used for one-line previews (tool call
155
+ // args, tool results) so a long argument cannot print a line wider than the
156
+ // terminal — which made some terminals show a horizontal scrollbar.
157
+ export function truncateToWidth(s, maxCols) {
158
+ const width = Math.max(1, Math.floor(maxCols));
159
+ const text = String(s ?? '');
160
+ if (visLen(text) <= width)
161
+ return text;
162
+ // Reserve one column for the ellipsis.
163
+ const budget = Math.max(0, width - 1);
164
+ let used = 0;
165
+ let out = '';
166
+ for (const ch of text) {
167
+ const w = charWidth(ch.codePointAt(0));
168
+ if (used + w > budget)
169
+ break;
170
+ out += ch;
171
+ used += w;
172
+ }
173
+ return out + '…';
174
+ }
153
175
  export function layoutInput(promptStr, buf, cursor, cols) {
154
176
  // Reserve the LAST column. If a printed row reaches exactly `cols` columns,
155
177
  // terminals with autowrap (Tabby, iTerm, Windows Terminal, xterm's default
package/dist/input.js CHANGED
@@ -3,12 +3,12 @@ import { renderMarkdown } from './markdown.js';
3
3
  import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
4
4
  import { translate } from './i18n.js';
5
5
  import { AttachmentStore, parseImagePaste, looksLikeFilePath, } from './attachments.js';
6
- import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, layoutInput, formatTokenStatus, tokenStatusLevel, CONTEXT_LIMIT, } from './input/layout.js';
6
+ import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, isSubsequence, pasteReplacement, expandPastes, safeJson, visRows, visLen, layoutInput, formatTokenStatus, tokenStatusLevel, truncateToWidth, CONTEXT_LIMIT, } from './input/layout.js';
7
7
  // The pure layout/formatting helpers (visual rows, cursor position, visible
8
8
  // width, paste markers, token formatting) live in ./input/layout.js. They are
9
9
  // re-exported here so every existing import from './input.js' keeps working,
10
10
  // and so tests can import them from either module (C3 step 3).
11
- export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
11
+ export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, truncateToWidth, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
12
12
  // A permanent input line at the bottom of the terminal + a status/output area above it.
13
13
  //
14
14
  // Why a custom editor:
@@ -1044,7 +1044,11 @@ export class LineEditor {
1044
1044
  }
1045
1045
  toolCall(name, args) {
1046
1046
  const full = safeJson(args);
1047
- const preview = full.slice(0, 120) + (full.length > 120 ? ' …' : '');
1047
+ // Clip by VISIBLE WIDTH, not character count: a long arg line used to print
1048
+ // wider than the terminal and some terminals then showed a horizontal
1049
+ // scrollbar. Keep one column free (autowrap safety), same as the status.
1050
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
1051
+ const preview = truncateToWidth(full, maxW);
1048
1052
  this.printAbove(theme.tool('🔧 ' + name) + ' ' + theme.dim(preview));
1049
1053
  // A tool may run for a long time (Bash, npm test, MCP). Without an active
1050
1054
  // animation the operator sees a frozen screen and cannot tell work is in
@@ -1062,7 +1066,8 @@ export class LineEditor {
1062
1066
  toolResult(result) {
1063
1067
  this.stop();
1064
1068
  const text = typeof result === 'string' ? result : safeJson(result);
1065
- const preview = text.slice(0, 200).split(NL).join(' ↵ ') + (text.length > 200 ? ' …' : '');
1069
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
1070
+ const preview = truncateToWidth(text.split(NL).join(' ↵ '), maxW);
1066
1071
  this.printAbove(theme.toolResult(' → ' + preview));
1067
1072
  }
1068
1073
  assistant(msg) {
@@ -1373,6 +1378,19 @@ export class LineEditor {
1373
1378
  this.buf = chars.join('');
1374
1379
  this.cursor = i;
1375
1380
  }
1381
+ // Delete the word on the RIGHT (Ctrl+Delete / Alt+Delete): skip spaces after
1382
+ // the cursor, then delete up to the end of the next word. Mirror of
1383
+ // _deleteWordLeft(), which handles the left side.
1384
+ _deleteWordRight() {
1385
+ const chars = Array.from(this.buf);
1386
+ let i = this.cursor;
1387
+ while (i < chars.length && /\s/.test(chars[i]))
1388
+ i++;
1389
+ while (i < chars.length && !/\s/.test(chars[i]))
1390
+ i++;
1391
+ chars.splice(this.cursor, i - this.cursor);
1392
+ this.buf = chars.join('');
1393
+ }
1376
1394
  // ---------- history ----------
1377
1395
  _historyUp() {
1378
1396
  if (!this.history.length)
@@ -1731,6 +1749,37 @@ export class LineEditor {
1731
1749
  this._renderInputOnly();
1732
1750
  continue;
1733
1751
  }
1752
+ // Ctrl+Delete / Alt+Delete — delete the word on the RIGHT. Different
1753
+ // terminals encode it differently: xterm sends ESC[3;5~, some send
1754
+ // ESC[3;3~ (Alt). Without this the sequence fell through to the
1755
+ // generic skip and nothing happened.
1756
+ if (s.startsWith('[3;5~') || s.startsWith('[3;3~')) {
1757
+ this._pushUndo();
1758
+ this._deleteWordRight();
1759
+ s = s.slice(5);
1760
+ this._renderInputOnly();
1761
+ continue;
1762
+ }
1763
+ // Alt+Backspace / Ctrl+Backspace (ESC followed by DEL) — delete the
1764
+ // word on the LEFT. On many terminals Ctrl+Backspace is not a distinct
1765
+ // byte and arrives exactly like this.
1766
+ if (s.startsWith('\x7f') || s.startsWith('\x08')) {
1767
+ this._pushUndo();
1768
+ this._deleteWordLeft();
1769
+ s = s.slice(1);
1770
+ this._renderInputOnly();
1771
+ continue;
1772
+ }
1773
+ // Kitty / foot / WezTerm keyboard protocol: Ctrl+Backspace is
1774
+ // CSI 127;5u (Alt = 127;3u). Without this the sequence fell through
1775
+ // to the generic skip and nothing happened.
1776
+ if (s.startsWith('[127;5u') || s.startsWith('[127;3u')) {
1777
+ this._pushUndo();
1778
+ this._deleteWordLeft();
1779
+ s = s.slice(7);
1780
+ this._renderInputOnly();
1781
+ continue;
1782
+ }
1734
1783
  if (s.startsWith('[D')) {
1735
1784
  this._left();
1736
1785
  s = s.slice(2);
package/dist/spinner.js CHANGED
@@ -2,6 +2,7 @@ import ora from 'ora';
2
2
  import { theme, divider } from './theme.js';
3
3
  import { renderMarkdown } from './markdown.js';
4
4
  import { translate } from './i18n.js';
5
+ import { truncateToWidth } from './input/layout.js';
5
6
  // Phrases for the "agent is working" spinner. Chosen in random order.
6
7
  // The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
7
8
  export function randomThinkingPhrase(locale = 'ru') {
@@ -170,7 +171,8 @@ export function createSpinner(locale = 'ru') {
170
171
  },
171
172
  toolCall: (name, args) => {
172
173
  stop();
173
- const preview = JSON.stringify(args).slice(0, 120);
174
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
175
+ const preview = truncateToWidth(JSON.stringify(args), maxW);
174
176
  console.log(theme.tool('🔧 ' + name), theme.dim(preview));
175
177
  // Keep an animated status while the tool runs (Bash/npm/MCP can take a
176
178
  // long time): the operator must see that work is in progress.
@@ -181,10 +183,8 @@ export function createSpinner(locale = 'ru') {
181
183
  toolResult: (result) => {
182
184
  stop();
183
185
  const text = typeof result === 'string' ? result : JSON.stringify(result);
184
- const preview = text
185
- .slice(0, 200)
186
- .split(String.fromCharCode(10))
187
- .join(' ↵ ');
186
+ const maxW = Math.max(20, (process.stdout.columns || 80) - 1);
187
+ const preview = truncateToWidth(text.split(String.fromCharCode(10)).join(' ↵ '), maxW);
188
188
  console.log(theme.toolResult(' → ' + preview + String.fromCharCode(10)));
189
189
  },
190
190
  assistant: (msg) => {
@@ -98,11 +98,20 @@ export function renderContextSection(context) {
98
98
  }
99
99
  return out;
100
100
  }
101
- export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
102
- const t = translate(locale);
103
- const toolDescriptions = tools
101
+ // The tool list in the prompt is GENERATED from the ToolDefs (createTools),
102
+ // never hand-written: a second copy drifts (the prompt promises a tool that
103
+ // no longer exists, or misses one that was added). This is the single source
104
+ // of truth for `### <Tool>` blocks; the prose that cannot be derived
105
+ // ("Choosing the right tool", the call formats) stays hard-coded below.
106
+ // Guarded by test/system-prompt-tools.test.ts.
107
+ export function renderToolSection(tools) {
108
+ return tools
104
109
  .map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
105
110
  .join('\n\n');
111
+ }
112
+ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, selfImprovement = false, }) {
113
+ const t = translate(locale);
114
+ const toolDescriptions = renderToolSection(tools);
106
115
  const gitSection = gitContext
107
116
  ? `\n## Git context\n\n${gitContext}\n`
108
117
  : '\n## Git context\n\nNot a git repository (or git is not installed).\n';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.65.0",
4
- "description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
3
+ "version": "2.66.0",
4
+ "description": "Terminal coding agent that drives chat.deepseek.com through Playwright — 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": {
7
7
  "zames": "dist/index.js"
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "scripts",
12
12
  "README.md",
13
+ "README.ru.md",
13
14
  "CHANGELOG.md",
14
15
  "LICENSE",
15
16
  "logo-small.jpg"
@@ -21,6 +22,7 @@
21
22
  "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
22
23
  "coverage:gate": "node scripts/coverage-gate.mjs",
23
24
  "changelog:draft": "tsx scripts/changelog-draft.mts",
25
+ "release": "tsx scripts/release.mts",
24
26
  "typecheck": "tsc --noEmit",
25
27
  "lint": "node scripts/eslint-ts6.mjs",
26
28
  "lint:fix": "node scripts/eslint-ts6.mjs --fix",
@@ -37,14 +39,21 @@
37
39
  "keywords": [
38
40
  "agent",
39
41
  "ai",
40
- "deepseek",
41
- "cli",
42
+ "ai-agent",
43
+ "ai-coding-assistant",
42
44
  "coding-agent",
45
+ "code-assistant",
46
+ "terminal-agent",
47
+ "deepseek",
48
+ "deepseek-chat",
43
49
  "claude-code",
44
50
  "codex",
51
+ "cli",
45
52
  "terminal",
46
53
  "playwright",
54
+ "browser-automation",
47
55
  "llm",
56
+ "no-api-key",
48
57
  "automation"
49
58
  ],
50
59
  "license": "MIT",