zames_pro 2.75.5 → 2.75.6

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +10 -4
  3. package/dist/agent-loop.js +61 -1
  4. package/dist/browser.js +40 -8
  5. package/dist/commands.js +0 -1
  6. package/dist/config.js +8 -3
  7. package/dist/deepseek-ui.js +1 -3
  8. package/dist/i18n/common.js +13 -16
  9. package/dist/i18n/config.js +2 -2
  10. package/dist/i18n/help.js +2 -6
  11. package/dist/i18n/spinner.js +7 -1
  12. package/dist/index.js +0 -49
  13. package/dist/net-capture.js +43 -11
  14. package/dist/provider-handoff.js +0 -4
  15. package/dist/qwen-ui.js +0 -8
  16. package/dist/run-task.js +24 -1
  17. package/dist/shell.js +0 -6
  18. package/dist/system-prompt.js +52 -2
  19. package/dist/tools.js +33 -0
  20. package/dist/tui/AskQuestion.js +108 -0
  21. package/dist/tui/FullScreenApp.js +2 -1
  22. package/dist/tui/InputLine.js +0 -3
  23. package/dist/tui/StatusBar.js +0 -9
  24. package/dist/tui/TuiEditor.js +0 -43
  25. package/dist/tui/keys.js +0 -2
  26. package/dist/tui/run-ask.js +22 -0
  27. package/dist/tui/store.js +0 -2
  28. package/dist/tui/widgets.js +3 -9
  29. package/package.json +2 -2
  30. package/scripts/build-readme.mts +0 -101
  31. package/scripts/changelog-draft.mts +0 -93
  32. package/scripts/clean-dist.mts +0 -17
  33. package/scripts/coverage-gate.mts +0 -112
  34. package/scripts/ctd-live-driver.mts +0 -51
  35. package/scripts/ctd-live.tsx +0 -33
  36. package/scripts/escd-live-driver.mts +0 -37
  37. package/scripts/eslint-ts6.mts +0 -58
  38. package/scripts/inline-status-driver.mts +0 -31
  39. package/scripts/inline-status.tsx +0 -33
  40. package/scripts/mouse-churn.mts +0 -34
  41. package/scripts/qwen-models-smoke.mts +0 -117
  42. package/scripts/qwen-smoke.mts +0 -221
  43. package/scripts/release.mts +0 -199
  44. package/scripts/render-demo.mts +0 -154
  45. package/scripts/render-how-it-works.mts +0 -229
  46. package/scripts/ru-shot.mts +0 -20
  47. package/scripts/sched-smoke.mts +0 -150
  48. package/scripts/self-smoke.mts +0 -309
  49. package/scripts/tui-comp-shot.mts +0 -63
  50. package/scripts/tui-components.tsx +0 -62
  51. package/scripts/tui-config-driver.mts +0 -82
  52. package/scripts/tui-config-probe.tsx +0 -74
  53. package/scripts/tui-edit-driver.mts +0 -100
  54. package/scripts/tui-edit-probe.tsx +0 -38
  55. package/scripts/tui-editor-probe.tsx +0 -24
  56. package/scripts/tui-input-driver.mts +0 -54
  57. package/scripts/tui-input-shot.tsx +0 -59
  58. package/scripts/tui-log-driver.mts +0 -35
  59. package/scripts/tui-log-shot.tsx +0 -48
  60. package/scripts/tui-longpaste-driver.mts +0 -56
  61. package/scripts/tui-longpaste.tsx +0 -35
  62. package/scripts/tui-manyselect-shot.mts +0 -34
  63. package/scripts/tui-manyselect.tsx +0 -17
  64. package/scripts/tui-mouse-churn-probe.tsx +0 -28
  65. package/scripts/tui-paste-driver.mts +0 -88
  66. package/scripts/tui-paste-kinds.tsx +0 -76
  67. package/scripts/tui-pastetest.mts +0 -38
  68. package/scripts/tui-probe.tsx +0 -62
  69. package/scripts/tui-repro.tsx +0 -64
  70. package/scripts/tui-resize.mts +0 -70
  71. package/scripts/tui-scroll-driver.mts +0 -53
  72. package/scripts/tui-scroll-probe.tsx +0 -46
  73. package/scripts/tui-shot.mts +0 -106
  74. package/scripts/tui-status-driver.mts +0 -24
  75. package/scripts/tui-status-shot.tsx +0 -27
  76. package/scripts/tui-submit.tsx +0 -57
  77. package/scripts/tui-wheel-driver.mts +0 -61
  78. package/scripts/tui-wheel-probe.tsx +0 -48
  79. package/scripts/tui-widgets-driver.mts +0 -20
  80. package/scripts/tui-widgets.tsx +0 -44
@@ -21,11 +21,56 @@ Rules for that note:
21
21
  - this is the ONE exception to "do not touch unrelated files". If nothing
22
22
  worth noting, add nothing.
23
23
  `;
24
+ // The skills DISCIPLINE, appended to the `## Skills` section (and mirrored in
25
+ // the section above it): the model must CHECK the list before a non-trivial
26
+ // task and READ a matching skill's body, instead of only seeing a static list.
27
+ // This is the zames equivalent of superpowers' `using-superpowers` bootstrap —
28
+ // no SessionStart hook needed, the text simply lives in the system prompt. Kept
29
+ // narrow ("when relevant", not "always") so the model does not burn iterations
30
+ // Reading SKILL.md files for a trivial edit.
31
+ const SKILLS_DISCIPLINE = `
32
+ Before starting a NON-TRIVIAL task (a new feature, a bug hunt, a multi-file
33
+ change), scan the list above for a matching skill and, if one applies, Read
34
+ its \`file:\` path FIRST — the body is the workflow you must follow. Skip this
35
+ for a trivial edit or a single read: the scan must never cost more than the
36
+ task itself. If a skill looks relevant but you are unsure, reading it is
37
+ cheap; guessing is not.
38
+ `;
24
39
  // Guidance for the `Task` tool (subagents), injected only when that tool is in
25
40
  // the model's set. Without it the model under-uses subagents (it does every
26
41
  // broad search itself and floods the main context) or misuses them (delegates
27
42
  // work that needs THIS conversation's history, which the isolated subagent
28
43
  // cannot see). Kept short: it competes with the tool list for space.
44
+ // Guidance for the `AskUser` tool, injected only when that tool is offered.
45
+ // Without it the model either never asks (and silently guesses) or asks about
46
+ // things it could verify itself. Kept short and biased toward NOT asking.
47
+ const ASKUSER_SECTION = `
48
+ ## AskUser tool (asking the operator)
49
+
50
+ AskUser shows the operator a MODAL multi-choice question: a short list of
51
+ options, one of them marked recommended, plus an "other" escape where they can
52
+ type free text. It BLOCKS the run until answered and costs the operator a
53
+ context switch, so use it SPARINGLY — only when the decision is genuinely
54
+ ambiguous AND you cannot proceed safely by choosing a sensible default.
55
+
56
+ Ask when:
57
+ - the request has two or more materially different interpretations and the
58
+ wrong one wastes real work (e.g. "delete the feature" — code only, or code
59
+ plus its tests and docs?);
60
+ - a destructive/irreversible action needs a decision you cannot derive from
61
+ the repo (which remote, which branch to force-push);
62
+ - the operator explicitly asked you to check before deciding.
63
+
64
+ Do NOT ask when:
65
+ - you can read the answer from the code, the config or the git history;
66
+ - one option is clearly right — pick it and state the assumption in your
67
+ final answer instead;
68
+ - the task is trivial or already precise.
69
+
70
+ Keep 2–4 options, put the RECOMMENDED one FIRST and mark it, and give each a
71
+ one-line consequence ("X — because …"). The question text says WHY you are
72
+ asking and what you will do with the answer.
73
+ `;
29
74
  const SUBAGENT_SECTION = `
30
75
  ## Task tool (subagents)
31
76
 
@@ -126,7 +171,8 @@ export function renderContextSection(context) {
126
171
  '\n## Skills\n\n' +
127
172
  'Specialized workflows. A skill with a `file:` is a SKILL.md on disk (read it when it applies); a `(built-in)` skill has no file — the operator can invoke it with /<name> and its instructions arrive as the task. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
128
173
  lines.join('\n') +
129
- '\n';
174
+ '\n' +
175
+ SKILLS_DISCIPLINE;
130
176
  }
131
177
  if (context.commands.length) {
132
178
  const lines = context.commands.map((c) => '- /' + c.name + ': ' + (c.description || '(no description)'));
@@ -158,6 +204,10 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
158
204
  const subagentSection = tools.some((x) => x.name === 'Task')
159
205
  ? SUBAGENT_SECTION
160
206
  : '';
207
+ // Same idea for AskUser: guidance only when the tool is in the set.
208
+ const askUserSection = tools.some((x) => x.name === 'AskUser')
209
+ ? ASKUSER_SECTION
210
+ : '';
161
211
  const gitSection = gitContext
162
212
  ? `\n## Git context\n\n${gitContext}\n`
163
213
  : '\n## Git context\n\nNot a git repository (or git is not installed).\n';
@@ -261,7 +311,7 @@ Rules for a batch:
261
311
  - Typical wins: read all the files a task mentions at once; grep for all the
262
312
  symbols you need at once; after planning a multi-file change, write all the
263
313
  files you can write independently at once.
264
- ${subagentSection}
314
+ ${subagentSection}${askUserSection}
265
315
  ## MCP tools
266
316
 
267
317
  Some tools are provided by external MCP servers and their names look
package/dist/tools.js CHANGED
@@ -303,6 +303,33 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
303
303
  },
304
304
  fn: async () => 'Subagents are not available in this run. Do the work yourself.',
305
305
  };
306
+ // The AskUser seam: a MODAL question to the operator (multi-choice with a
307
+ // recommended option and a free-text escape). Like `Task`, it is handled by
308
+ // the CALLER (onAskUser), not by `fn` — the live path intercepts the name in
309
+ // agent-loop. `fn` only answers a direct unit-test call. It is not offered in
310
+ // plan/read-only mode (a plan must not interrupt).
311
+ const askUserTool = {
312
+ name: 'AskUser',
313
+ description: 'Ask the operator a question with a short list of OPTIONS when a decision ' +
314
+ 'is genuinely ambiguous and you cannot proceed safely without it. One ' +
315
+ 'option may be marked recommended (the model puts it FIRST and sets ' +
316
+ '`recommended` to its index/label). The operator can also pick "other" ' +
317
+ 'and type free text. Use it SPARINGLY: prefer a reasonable default and ' +
318
+ 'state it in your final answer — a question costs the operator a ' +
319
+ 'context switch and blocks the run until answered. Do NOT ask for ' +
320
+ 'confirmation of something you can verify yourself (read the file, run ' +
321
+ 'the test). Returns the chosen option label, or the typed text, or an ' +
322
+ 'empty answer when the operator dismissed the dialog.',
323
+ parameters: {
324
+ question: 'string',
325
+ options: 'array',
326
+ recommended: 'number?',
327
+ allow_text: 'boolean?',
328
+ },
329
+ fn: async ({ question }) => 'AskUser is not available in this run (no interactive UI). Proceed with ' +
330
+ 'your best judgement and state the assumption. Question was: ' +
331
+ String(question ?? ''),
332
+ };
306
333
  // In plan (read-only) mode `Task` is dropped too: a `general` subagent could
307
334
  // otherwise write through it and defeat the read-only promise.
308
335
  const all = [
@@ -311,6 +338,7 @@ export function createTools(workdir, { undo, todos, readOnly = false, subagents
311
338
  ...gitTools,
312
339
  ...webTools,
313
340
  ...(subagents && !readOnly ? [taskTool] : []),
341
+ ...(readOnly ? [] : [askUserTool]),
314
342
  respondTool,
315
343
  ];
316
344
  return readOnly ? filterToolsForReadOnly(all) : all;
@@ -330,6 +358,11 @@ export const MUTATING_TOOLS = new Set([
330
358
  'GitAdd',
331
359
  'GitCommit',
332
360
  'GitPush',
361
+ // AskUser is NOT mutating (it touches no file), but plan mode exists to
362
+ // PRODUCE a plan with no side effects and no interruptions; a modal question
363
+ // mid-plan is exactly what plan mode must not do. Dropped in read-only mode
364
+ // so the model records the open question as a plan note instead.
365
+ 'AskUser',
333
366
  ]);
334
367
  /** Drop mutating tools for read-only / plan mode. Pure; unit-tested. */
335
368
  export function filterToolsForReadOnly(tools) {
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A modal QUESTION with CHOICES for the model's `AskUser` tool.
3
+ //
4
+ // The operator sees the model's question and a short list of options; one may
5
+ // be marked as the model's recommendation and is highlighted. The operator can
6
+ // ALWAYS answer in their own words via a last "other" row, so imperfect
7
+ // options never block the run. Esc dismisses the question (the model is then
8
+ // told to pick a default and continue).
9
+ //
10
+ // Visual language matches SelectList / ConfirmDialog (rounded border, title,
11
+ // hint bar); it is a separate component because the free-text escape needs an
12
+ // inline one-line editor, which SelectList does not have.
13
+ import { useState } from 'react';
14
+ import { Box, Text, useInput, useApp, useWindowSize } from 'ink';
15
+ import { UI, HintBar } from './widgets.js';
16
+ import { translate } from '../i18n.js';
17
+ export function AskQuestion({ question, options, recommended, allowText, locale = 'ru', onResult, }) {
18
+ const { exit } = useApp();
19
+ const { columns } = useWindowSize();
20
+ const t = translate(locale);
21
+ const innerCols = Math.max(10, (columns || 80) - 6);
22
+ const otherLabel = t('askuser.other');
23
+ const rows = allowText ? [...options, otherLabel] : options;
24
+ const [cursor, setCursor] = useState(recommended !== null && recommended >= 0 && recommended < options.length
25
+ ? recommended
26
+ : 0);
27
+ const [typing, setTyping] = useState(false);
28
+ const [text, setText] = useState('');
29
+ const finish = (v) => {
30
+ onResult(v);
31
+ exit();
32
+ };
33
+ useInput((input, key) => {
34
+ if (key.escape || (key.ctrl && input === 'c')) {
35
+ finish(null);
36
+ return;
37
+ }
38
+ if (typing) {
39
+ if (key.return) {
40
+ const v = text.trim();
41
+ finish(v || null);
42
+ return;
43
+ }
44
+ if (key.backspace || key.delete) {
45
+ setText((s) => Array.from(s).slice(0, -1).join(''));
46
+ return;
47
+ }
48
+ if (input && input >= ' ')
49
+ setText((s) => s + input);
50
+ return;
51
+ }
52
+ if (key.upArrow || input === 'k') {
53
+ setCursor((c) => (rows.length ? (c - 1 + rows.length) % rows.length : 0));
54
+ return;
55
+ }
56
+ if (key.downArrow || input === 'j') {
57
+ setCursor((c) => (rows.length ? (c + 1) % rows.length : 0));
58
+ return;
59
+ }
60
+ if (key.return) {
61
+ if (allowText && cursor === options.length) {
62
+ setTyping(true);
63
+ return;
64
+ }
65
+ finish(rows[cursor] ?? null);
66
+ return;
67
+ }
68
+ // A printable char on the "other" row opens the editor immediately.
69
+ if (allowText && cursor === options.length && input && input >= ' ') {
70
+ setTyping(true);
71
+ setText(input);
72
+ }
73
+ });
74
+ // Word-wrap the question to the inner width (Ink wraps Text too, but a
75
+ // wrapped box row is not counted by the layout, so wrap it here).
76
+ const wrap = (s) => {
77
+ const words = String(s).split(/\s+/);
78
+ const out = [];
79
+ let line = '';
80
+ for (const w of words) {
81
+ const cand = line ? line + ' ' + w : w;
82
+ if (cand.length > innerCols && line) {
83
+ out.push(line);
84
+ line = w;
85
+ }
86
+ else {
87
+ line = cand;
88
+ }
89
+ }
90
+ if (line)
91
+ out.push(line);
92
+ return out.length ? out : [''];
93
+ };
94
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.accent, paddingX: 2, children: [_jsx(Text, { color: UI.title, bold: true, children: '◆ ' + t('askuser.title') }), wrap(question).map((line, i) => (_jsx(Text, { color: UI.text, children: line }, i))), _jsx(Text, { color: "#3a3f4b", children: '─'.repeat(innerCols) }), typing ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: UI.dim, children: t('askuser.your_answer') }), _jsxs(Text, { color: UI.accent, children: ['❯ ', text, '▏'] })] })) : (rows.map((label, i) => {
95
+ const sel = i === cursor;
96
+ const isRec = i === recommended;
97
+ return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: sel ? '❯ ' : ' ' }), _jsx(Text, { color: sel ? UI.accent : undefined, bold: sel, children: label }), isRec ? (_jsx(Text, { color: "#6fd0b0", children: ' (' + t('askuser.recommended') + ')' })) : null] }, i));
98
+ })), _jsx(HintBar, { hints: typing
99
+ ? [
100
+ ['Enter', t('askuser.hint_send')],
101
+ ['Esc', t('askuser.hint_cancel')],
102
+ ]
103
+ : [
104
+ ['↑/↓', t('askuser.hint_select')],
105
+ ['Enter', t('askuser.hint_ok')],
106
+ ['Esc', t('askuser.hint_dismiss')],
107
+ ] })] }));
108
+ }
@@ -113,6 +113,7 @@ export function FullScreenApp({ store, inputProps, }) {
113
113
  }, state.mouseScroll);
114
114
  return (_jsxs(Box, { flexDirection: "column", height: termRows, children: [_jsx(Header, { store: store }), _jsx(Box, { flexDirection: "column", height: logHeight, overflow: "hidden", justifyContent: "flex-end", children: all.length === 0 ? (_jsx(Text, { color: "#5b616e", children: translate(state.locale)('editor.empty_hint') })) : (_jsx(LogList, { entries: visible })) }), _jsx(StatusBar, { state: state, divider: false }), _jsx(InputLine, { ...inputProps, store: store,
115
115
  // Cursor rows above the input box: header + log viewport + status.
116
- cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: "#5b616e", wrap: "truncate", children: ' ' + translate(state.locale)('editor.keys_hint') })] }));
116
+ cursorTop: HEADER_ROWS + logHeight + suggestRows + STATUS_ROWS + 1, prompt: state.prompt, locale: state.locale, commands: state.commands, locked: state.locked }), _jsx(Text, { color: "#5b616e", wrap: "truncate", children: ' ' +
117
+ translate(state.locale)(state.mouseScroll ? 'editor.keys_hint' : 'editor.keys_hint_select') })] }));
117
118
  }
118
119
  void contentWidth;
@@ -213,9 +213,6 @@ export function InputLine(props) {
213
213
  case 'clipboard':
214
214
  void tryClipboard();
215
215
  return;
216
- case 'copyCode':
217
- props.onCopyCode();
218
- return;
219
216
  default:
220
217
  return;
221
218
  }
@@ -37,10 +37,6 @@ export function badgeTextWidth(state) {
37
37
  w += visLen('[' + state.provider + '] ');
38
38
  if (state.queueLength > 0)
39
39
  w += visLen('⧗' + state.queueLength + ' ');
40
- if (state.codeBlocks.length > 0) {
41
- const idx = state.codeFocus >= 0 ? state.codeFocus + 1 : state.codeBlocks.length;
42
- w += visLen('⌘ ' + idx + '/' + state.codeBlocks.length + ' ');
43
- }
44
40
  if (state.tasks)
45
41
  w += visLen(state.tasks) + 1;
46
42
  const toggles = [];
@@ -80,11 +76,6 @@ function Badges({ state, budget = Infinity, }) {
80
76
  if (state.queueLength > 0) {
81
77
  add('q', visLen('⧗' + state.queueLength + ' '), _jsxs(Text, { color: "#c9a86a", children: ['⧗' + state.queueLength, ' '] }, "q"));
82
78
  }
83
- if (state.codeBlocks.length > 0) {
84
- const idx = state.codeFocus >= 0 ? state.codeFocus + 1 : state.codeBlocks.length;
85
- const label = '⌘ ' + idx + '/' + state.codeBlocks.length + ' ';
86
- add('code', visLen(label), _jsx(Text, { color: "#7fc4f0", children: label }, "code"));
87
- }
88
79
  if (state.tasks) {
89
80
  add('t', visLen(state.tasks) + 1, _jsxs(Text, { color: "#808896", children: [state.tasks, ' '] }, "t"));
90
81
  }
@@ -26,7 +26,6 @@ import { contentWidth } from '../width.js';
26
26
  import { theme } from '../theme.js';
27
27
  import { renderMarkdown } from '../markdown.js';
28
28
  import { summarizeToolCall, looksLikeToolError } from '../tool-summary.js';
29
- import { extractCodeBlocks } from '../code-blocks.js';
30
29
  import { randomThinkingPhrase, stripEllipsis } from '../spinner.js';
31
30
  export class TuiEditor {
32
31
  store = new TuiStore();
@@ -42,9 +41,6 @@ export class TuiEditor {
42
41
  onTasksQuery = null;
43
42
  onContextQuery = null;
44
43
  onToggleQuery = null;
45
- /** Copy text to the OS clipboard. Wired to writeClipboardText in index.ts;
46
- * returns { ok, via } so the editor can report the outcome. */
47
- onCopyText = null;
48
44
  busy = false;
49
45
  locked = false;
50
46
  history = [];
@@ -98,7 +94,6 @@ export class TuiEditor {
98
94
  onSubmit: (text, atts) => this.handleSubmit(text, atts),
99
95
  onEscape: () => this.onEscape?.(),
100
96
  onCtrlC: () => this.onCtrlC?.(),
101
- onCopyCode: () => this.copyCode(),
102
97
  onAttach: (raw) => this.onAttach?.(raw) ?? null,
103
98
  onClipboard: () => this.onClipboard?.() ?? Promise.resolve(null),
104
99
  onHistoryChange: (h) => {
@@ -472,44 +467,6 @@ export class TuiEditor {
472
467
  this.printAbove('\n' + theme.assistant(translate(this.locale)('editor.answer')));
473
468
  this.printAbove(rendered);
474
469
  this.printAbove('\n');
475
- // Remember the answer's code blocks so the operator can copy one by hotkey
476
- // (a terminal has no copy buttons). Extraction is on the RAW markdown —
477
- // the rendered text is full of ANSI and must never be copied.
478
- const blocks = extractCodeBlocks(msg);
479
- this.store.set({ codeBlocks: blocks, codeFocus: -1 });
480
- if (blocks.length)
481
- this.refreshBadges();
482
- }
483
- /** Copy the focused code block (or the LAST when none is focused). */
484
- copyCode(index) {
485
- const snap = this.store.getSnapshot();
486
- const blocks = snap.codeBlocks;
487
- if (!blocks.length) {
488
- this.printAbove(theme.dim(translate(this.locale)('copy.no_code')));
489
- return false;
490
- }
491
- const pick = typeof index === 'number'
492
- ? (blocks.find((b) => b.index === index) ?? null)
493
- : snap.codeFocus >= 0
494
- ? (blocks[snap.codeFocus] ?? null)
495
- : blocks[blocks.length - 1];
496
- if (!pick) {
497
- this.printAbove(theme.warn(translate(this.locale)('copy.bad_index', { n: String(index) })));
498
- return false;
499
- }
500
- const res = this.onCopyText?.(pick.text) ?? { ok: false, via: 'none' };
501
- const label = 'code #' + pick.index + (pick.lang ? ' (' + pick.lang + ')' : '');
502
- if (res.ok) {
503
- this.printAbove(theme.assistant(translate(this.locale)('copy.done', { tool: res.via }) +
504
- ' — ' +
505
- label));
506
- }
507
- else {
508
- this.printAbove(theme.warn(translate(this.locale)('copy.failed', { tool: res.via }) +
509
- ' — ' +
510
- label));
511
- }
512
- return res.ok;
513
470
  }
514
471
  warning(msg) {
515
472
  const wrapped = wrapToWidth('⚠ ' + msg, contentWidth());
package/dist/tui/keys.js CHANGED
@@ -127,8 +127,6 @@ export function dispatchKey(input, key, ctx, isVisualEdge) {
127
127
  return { type: 'suggestPrev' };
128
128
  case 'v':
129
129
  return { type: 'clipboard' };
130
- case 'y':
131
- return { type: 'copyCode' };
132
130
  case 'j':
133
131
  return { type: 'newline' };
134
132
  default:
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { AskQuestion } from './AskQuestion.js';
4
+ /**
5
+ * Show the modal question and resolve with the chosen option, the typed text,
6
+ * or null when dismissed / stdin is not a TTY.
7
+ */
8
+ export async function askQuestionInk(opts) {
9
+ if (!(process.stdin.isTTY && process.stdout.isTTY))
10
+ return null;
11
+ return await new Promise((resolve) => {
12
+ let answered = false;
13
+ const app = render(_jsx(AskQuestion, { ...opts, onResult: (v) => {
14
+ answered = true;
15
+ resolve(v);
16
+ } }), { exitOnCtrlC: false });
17
+ app.waitUntilExit().then(() => {
18
+ if (!answered)
19
+ resolve(null);
20
+ });
21
+ });
22
+ }
package/dist/tui/store.js CHANGED
@@ -44,8 +44,6 @@ function initialState() {
44
44
  thinkingEnabled: false,
45
45
  searchEnabled: false,
46
46
  statusLine: '',
47
- codeBlocks: [],
48
- codeFocus: -1,
49
47
  columns: process.stdout.columns || 80,
50
48
  };
51
49
  }
@@ -42,19 +42,13 @@ export function HintBar({ hints, }) {
42
42
  return (_jsx(Text, { children: hints.map(([k, l], i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { color: UI.dim, children: ' · ' }) : null, _jsx(Text, { color: UI.accent, children: k }), _jsx(Text, { color: UI.dim, children: ' ' + l })] }, i))) }));
43
43
  }
44
44
  /**
45
- * A CODE BLOCK with a syntax-highlighted body and a copy affordance.
45
+ * A CODE BLOCK with a syntax-highlighted body and an optional title label.
46
46
  *
47
47
  * The agent's answers are printed as pre-rendered ANSI (markdown.ts already
48
48
  * highlights with cli-highlight), so the live log does NOT use this component —
49
49
  * it exists for menus/panels that show a snippet inline (and for a future
50
- * structured log). Copying is a KEY, not a button: a terminal has no pointer.
51
- * Pass `onCopy` and show `hint` so the operator learns the binding.
50
+ * structured log).
52
51
  */
53
- export function CodeBlock({ code, lang, highlighted, width, hint, focused, onCopy, }) {
54
- // React cannot call `onCopy` from a keydown here (Ink has no per-element
55
- // focus ring by default); the caller wires the key and passes `focused` to
56
- // show which block it will copy. `onCopy` is kept so a future focus manager
57
- // can invoke it without changing the call sites.
58
- void onCopy;
52
+ export function CodeBlock({ code, lang, highlighted, width, hint, focused, }) {
59
53
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? UI.accent : UI.border, paddingX: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.dim, children: lang || 'code' }), hint ? _jsx(Text, { color: UI.accent, children: hint }) : null] }), (highlighted || code).split(String.fromCharCode(10)).map((line, i) => (_jsx(Text, { children: line || ' ' }, i)))] }));
60
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.75.5",
3
+ "version": "2.75.6",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — 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": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "scripts",
11
+ "scripts/postinstall.mjs",
12
12
  "README.md",
13
13
  "CHANGELOG.md",
14
14
  "LICENSE",
@@ -1,101 +0,0 @@
1
- // Assemble README.md from the two language sources.
2
- //
3
- // Why: npm renders exactly ONE readme per package (README.md in the root) and
4
- // has no language tabs. npm renders the readme with GitHub Flavored Markdown
5
- // (via GitHub's API), so a <details> block DOES work there -- and on GitHub
6
- // too. We keep the English text on top and fold the Russian translation into
7
- // a collapsible block at the bottom. The language switcher is an in-page
8
- // `#readme-ru` anchor, so one README.md serves both languages -- there is
9
- // no separate README.ru.md anymore (it was invisible on npm and just
10
- // duplicated the folded block).
11
- //
12
- // The two sources (docs/readme.en.md, docs/readme.ru.md) are the single point
13
- // of edit for each language; this script wires the switcher and the collapsible
14
- // cross-language block so neither language has to be maintained by hand. The
15
- // generated file is committed; test/readme-built.test.ts re-runs buildReadmes()
16
- // and fails if it drifts from the sources.
17
- //
18
- // Usage: npom run build:readme
19
-
20
- import fs from 'node:fs'
21
- import path from 'node:path'
22
- import { fileURLToPath } from 'node:url'
23
-
24
- const NL = String.fromCharCode(10)
25
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
26
- export const SWITCHER = '<!-- SWITCHER -->'
27
-
28
- // In-page anchor the switcher links to. GitHub strips `id` on some elements but
29
- // keeps `name` on an <a>, so both are set.
30
- export const RU_ANCHOR = '<a id="readme-ru" name="readme-ru"></a>'
31
-
32
- const EN_SWITCH = [
33
- '<p align="center">',
34
- ' <strong>English</strong> | <a href="#readme-ru">Русский</a>',
35
- '</p>',
36
- ].join(NL)
37
-
38
- export function readSource(
39
- rootDir: string,
40
- name: string,
41
- ): { head: string; body: string } {
42
- const file = path.join(rootDir, 'docs', name)
43
- const text = fs.readFileSync(file, 'utf-8')
44
- const i = text.indexOf(SWITCHER)
45
- if (i < 0) {
46
- throw new Error(
47
- file + ' has no ' + SWITCHER + ' marker (was it edited by hand?)',
48
- )
49
- }
50
- return {
51
- // Everything above the marker: logo, title, badges.
52
- head: text.slice(0, i).replace(/\n+$/, ''),
53
- // Everything below: the actual documentation.
54
- body: text
55
- .slice(i + SWITCHER.length)
56
- .replace(/^\n+/, '')
57
- .replace(/\n+$/, ''),
58
- }
59
- }
60
-
61
- function details(summary: string, body: string): string {
62
- return [
63
- '<details>',
64
- '<summary>' + summary + '</summary>',
65
- '',
66
- body,
67
- '',
68
- '</details>',
69
- ].join(NL)
70
- }
71
-
72
- // Returns the generated README.md WITHOUT touching the disk, so a test can
73
- // compare it against the committed file. Only README.md is generated: the
74
- // Russian translation lives inside it as a <details> block, and the switcher
75
- // is an in-page `#readme-ru` anchor.
76
- export function buildReadmes(rootDir: string): { readmeEn: string } {
77
- const en = readSource(rootDir, 'readme.en.md')
78
- const ru = readSource(rootDir, 'readme.ru.md')
79
- const readmeEn = [
80
- en.head,
81
- '',
82
- EN_SWITCH,
83
- '',
84
- en.body,
85
- '',
86
- RU_ANCHOR,
87
- details('🇷🇺 Читать по-русски (Russian)', ru.body),
88
- '',
89
- ].join(NL)
90
- return { readmeEn }
91
- }
92
-
93
- // Run only when invoked directly (not when imported by a test).
94
- const invoked =
95
- process.argv[1] &&
96
- path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
97
- if (invoked) {
98
- const { readmeEn } = buildReadmes(root)
99
- fs.writeFileSync(path.join(root, 'README.md'), readmeEn)
100
- console.log('build-readme: wrote README.md')
101
- }
@@ -1,93 +0,0 @@
1
- import { spawnSync } from 'node:child_process'
2
- import fs from 'node:fs'
3
- import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
5
- import {
6
- parseConventionalCommit,
7
- groupCommits,
8
- renderChangelogDraft,
9
- syncUnreleased,
10
- } from '../src/changelog.js'
11
-
12
- // Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
13
- // D2). PRINTS to stdout and never touches CHANGELOG.md -- the operator (or
14
- // the agent) pastes the result in by hand, so a bad commit subject can never
15
- // corrupt the changelog.
16
- //
17
- // Usage:
18
- // npm run changelog:draft # since the latest tag, or an explicit ref
19
- // npm run changelog:draft -- v2.63.0
20
- // npm run changelog:draft -- --write # update the Unreleased section in place
21
- // npm run changelog:draft -- --check # exit 1 if Unreleased is out of date
22
-
23
- function git(args: string[]): string {
24
- const r = spawnSync('git', args, { encoding: 'utf-8' })
25
- if (r.status !== 0) return ''
26
- return (r.stdout || '').toString()
27
- }
28
-
29
- const argv = process.argv.slice(2)
30
- const write = argv.includes('--write')
31
- const check = argv.includes('--check')
32
- const argRef = argv.find((a) => !a.startsWith('-'))
33
-
34
- // Resolve the start ref: an explicit arg, otherwise the latest tag,
35
- // otherwise the root commit.
36
- const latestTag = git(['describe', '--tags', '--abbrev=0']).trim()
37
- const ref =
38
- argRef ||
39
- latestTag ||
40
- git(['revlist', 'HEAD']).trim().split('\n').pop() ||
41
- 'HEAD'
42
-
43
- const subjects = git([
44
- 'log',
45
- '--no-merges',
46
- '--pretty=format:%s',
47
- `${ref}..HEAD`,
48
- ])
49
- .split('\n')
50
- .map((s) => s.trim())
51
- .filter(Boolean)
52
-
53
- const parsed = subjects
54
- .map(parseConventionalCommit)
55
- .filter((c): c is NonNullable<typeof c> => c !== null)
56
- const body = renderChangelogDraft(groupCommits(parsed))
57
-
58
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
59
- const changelogPath = path.join(root, 'CHANGELOG.md')
60
-
61
- // D2: sync the `## [Unreleased]` section in CHANGELOG.md from the conventional
62
- // commits since the last tag. `--write` edits the file; `--check` fails when
63
- // the committed section is out of date (for CI); neither just prints the body.
64
- if (write || check) {
65
- const onDisk = fs.readFileSync(changelogPath, 'utf-8')
66
- const nextText = syncUnreleased(onDisk, body)
67
- if (write) {
68
- if (nextText !== onDisk) {
69
- fs.writeFileSync(changelogPath, nextText)
70
- console.log('changelog: updated the Unreleased section')
71
- } else {
72
- console.log('changelog: Unreleased already up to date')
73
- }
74
- process.exit(0)
75
- }
76
- if (nextText !== onDisk) {
77
- console.error(
78
- 'changelog: the Unreleased section is out of date — run `npm run changelog:sync`',
79
- )
80
- process.exit(1)
81
- }
82
- console.log('changelog: Unreleased is up to date')
83
- process.exit(0)
84
- }
85
-
86
- if (!body) {
87
- console.error('no conventional commits since ' + ref)
88
- process.exit(0)
89
- }
90
-
91
- console.log('## [Unreleased]')
92
- console.log('')
93
- console.log(body)
@@ -1,17 +0,0 @@
1
- import { rmSync, existsSync } from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- // tsc emits into dist/ without clearing it, so a source file DELETED from
6
- // src/ leaves an orphan .js in the published tree (this really happened:
7
- // dist/confirm.js survived after src/confirm.ts was removed). Wipe dist/ first
8
- // so the build output mirrors src/ exactly.
9
- //
10
- // Cross-platform: use node's fs.rm instead of a shell `rm -rf` (Windows has no
11
- // rm) and never touch anything outside this repo.
12
- const here = path.dirname(fileURLToPath(import.meta.url))
13
- const dist = path.resolve(here, '..', 'dist')
14
- if (existsSync(dist)) {
15
- rmSync(dist, { recursive: true, force: true })
16
- console.log('clean-dist: removed', dist)
17
- }