zames_pro 2.75.4 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +12 -6
- package/dist/agent-loop.js +61 -1
- package/dist/browser.js +40 -8
- package/dist/commands.js +0 -1
- package/dist/config.js +8 -3
- package/dist/deepseek-ui.js +1 -3
- package/dist/i18n/common.js +13 -16
- package/dist/i18n/config.js +2 -2
- package/dist/i18n/help.js +6 -10
- package/dist/i18n/spinner.js +16 -5
- package/dist/index.js +0 -49
- package/dist/net-capture.js +43 -11
- package/dist/provider-handoff.js +0 -4
- package/dist/qwen-ui.js +0 -8
- package/dist/run-task.js +24 -1
- package/dist/shell.js +0 -6
- package/dist/system-prompt.js +52 -2
- package/dist/tools.js +33 -0
- package/dist/tui/AskQuestion.js +108 -0
- package/dist/tui/FullScreenApp.js +2 -1
- package/dist/tui/InputLine.js +0 -3
- package/dist/tui/StatusBar.js +5 -10
- package/dist/tui/TuiEditor.js +0 -43
- package/dist/tui/keys.js +0 -2
- package/dist/tui/run-ask.js +22 -0
- package/dist/tui/store.js +0 -2
- package/dist/tui/widgets.js +3 -9
- package/package.json +3 -2
- package/scripts/build-readme.mts +0 -101
- package/scripts/changelog-draft.mts +0 -93
- package/scripts/clean-dist.mts +0 -17
- package/scripts/coverage-gate.mts +0 -112
- package/scripts/ctd-live-driver.mts +0 -51
- package/scripts/ctd-live.tsx +0 -33
- package/scripts/escd-live-driver.mts +0 -37
- package/scripts/eslint-ts6.mts +0 -58
- package/scripts/inline-status-driver.mts +0 -31
- package/scripts/inline-status.tsx +0 -33
- package/scripts/mouse-churn.mts +0 -34
- package/scripts/qwen-models-smoke.mts +0 -117
- package/scripts/qwen-smoke.mts +0 -221
- package/scripts/release.mts +0 -199
- package/scripts/render-demo.mts +0 -154
- package/scripts/render-how-it-works.mts +0 -229
- package/scripts/ru-shot.mts +0 -20
- package/scripts/self-smoke.mts +0 -309
- package/scripts/tui-comp-shot.mts +0 -63
- package/scripts/tui-components.tsx +0 -62
- package/scripts/tui-config-driver.mts +0 -82
- package/scripts/tui-config-probe.tsx +0 -74
- package/scripts/tui-edit-driver.mts +0 -100
- package/scripts/tui-edit-probe.tsx +0 -38
- package/scripts/tui-editor-probe.tsx +0 -24
- package/scripts/tui-input-driver.mts +0 -54
- package/scripts/tui-input-shot.tsx +0 -59
- package/scripts/tui-log-driver.mts +0 -35
- package/scripts/tui-log-shot.tsx +0 -48
- package/scripts/tui-longpaste-driver.mts +0 -56
- package/scripts/tui-longpaste.tsx +0 -35
- package/scripts/tui-manyselect-shot.mts +0 -34
- package/scripts/tui-manyselect.tsx +0 -17
- package/scripts/tui-mouse-churn-probe.tsx +0 -28
- package/scripts/tui-paste-driver.mts +0 -88
- package/scripts/tui-paste-kinds.tsx +0 -76
- package/scripts/tui-pastetest.mts +0 -38
- package/scripts/tui-probe.tsx +0 -62
- package/scripts/tui-repro.tsx +0 -64
- package/scripts/tui-resize.mts +0 -70
- package/scripts/tui-scroll-driver.mts +0 -53
- package/scripts/tui-scroll-probe.tsx +0 -46
- package/scripts/tui-shot.mts +0 -106
- package/scripts/tui-status-driver.mts +0 -24
- package/scripts/tui-status-shot.tsx +0 -27
- package/scripts/tui-submit.tsx +0 -57
- package/scripts/tui-wheel-driver.mts +0 -61
- package/scripts/tui-wheel-probe.tsx +0 -48
- package/scripts/tui-widgets-driver.mts +0 -20
- package/scripts/tui-widgets.tsx +0 -44
package/dist/run-task.js
CHANGED
|
@@ -278,7 +278,7 @@ async function evaluateGoal(browser, condition, config, transcript) {
|
|
|
278
278
|
}
|
|
279
279
|
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
280
280
|
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
281
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, } = opts;
|
|
281
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, onAskUser = undefined, } = opts;
|
|
282
282
|
// The ACTIVE goal for the duration of this run. It is mutable because a
|
|
283
283
|
// completion goal can be cleared mid-run (verdict MET/IMPOSSIBLE, or the
|
|
284
284
|
// operator runs `/goal clear` while the task is in flight). `goalQuery`
|
|
@@ -410,6 +410,29 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
410
410
|
getTokenUsage,
|
|
411
411
|
selfImprovement,
|
|
412
412
|
onSubagent,
|
|
413
|
+
// The `AskUser` seam. Default: pause the editor, show the Ink modal,
|
|
414
|
+
// resume; on a non-TTY (or no editor) it resolves null so the model is
|
|
415
|
+
// told to pick a default rather than being stuck on a question nobody
|
|
416
|
+
// can see.
|
|
417
|
+
onAskUser: onAskUser === undefined
|
|
418
|
+
? async (req) => {
|
|
419
|
+
if (!editor)
|
|
420
|
+
return null;
|
|
421
|
+
editor.pause();
|
|
422
|
+
try {
|
|
423
|
+
const { askQuestionInk } = await import('./tui/run-ask.js');
|
|
424
|
+
return await askQuestionInk({
|
|
425
|
+
question: req.question,
|
|
426
|
+
options: req.options,
|
|
427
|
+
recommended: req.recommended,
|
|
428
|
+
allowText: req.allowText,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
editor.resume();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
: onAskUser,
|
|
413
436
|
// N38: the check command appended to a tool result after an edit
|
|
414
437
|
// (off by default; config.diagnostics).
|
|
415
438
|
diagnostics: deps.config.diagnostics,
|
package/dist/shell.js
CHANGED
|
@@ -195,12 +195,6 @@ export function killBackground(id) {
|
|
|
195
195
|
BG_PROCS.delete(id);
|
|
196
196
|
return `Killed background process ${id}.`;
|
|
197
197
|
}
|
|
198
|
-
/** List running background processes (for /status and cleanup). */
|
|
199
|
-
export function listBackground() {
|
|
200
|
-
return [...BG_PROCS.values()]
|
|
201
|
-
.filter((p) => p.status === 'running')
|
|
202
|
-
.map((p) => ({ id: p.id, command: p.command }));
|
|
203
|
-
}
|
|
204
198
|
/** Kill every background process — called on exit so no orphan survives. */
|
|
205
199
|
export function killAllBackground() {
|
|
206
200
|
for (const id of [...BG_PROCS.keys()])
|
package/dist/system-prompt.js
CHANGED
|
@@ -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: ' ' +
|
|
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;
|
package/dist/tui/InputLine.js
CHANGED
package/dist/tui/StatusBar.js
CHANGED
|
@@ -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
|
}
|
|
@@ -155,7 +146,11 @@ export function StatusBar({ state, divider = true, }) {
|
|
|
155
146
|
// When the log is scrolled up, show a marker in the status line (NOT as a
|
|
156
147
|
// separate row below the input — that pushed the panel up and made the input
|
|
157
148
|
// jump). It replaces the spinner text while scrolled, so it never adds a row.
|
|
158
|
-
const scrollNote = state.scroll > 0
|
|
149
|
+
const scrollNote = state.scroll > 0
|
|
150
|
+
? translate(state.locale)('status.scrolled', {
|
|
151
|
+
n: String(state.scroll),
|
|
152
|
+
})
|
|
153
|
+
: '';
|
|
159
154
|
const leftText = scrollNote || prefix + left;
|
|
160
155
|
const showLeft = !!leftText;
|
|
161
156
|
// Keep the row on ONE line. The left status gets at most a third of the
|
package/dist/tui/TuiEditor.js
CHANGED
|
@@ -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
|
@@ -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
package/dist/tui/widgets.js
CHANGED
|
@@ -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
|
|
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).
|
|
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,
|
|
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.
|
|
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",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
36
|
"qwen-smoke": "tsx scripts/qwen-smoke.mts",
|
|
37
37
|
"qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
|
|
38
|
+
"sched-smoke": "tsx scripts/sched-smoke.mts",
|
|
38
39
|
"render-demo": "tsx scripts/render-demo.mts",
|
|
39
40
|
"render-how": "tsx scripts/render-how-it-works.mts",
|
|
40
41
|
"build:readme": "tsx scripts/build-readme.mts",
|
package/scripts/build-readme.mts
DELETED
|
@@ -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
|
-
}
|