zames_pro 2.75.5 → 2.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +10 -4
- 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/context.js +99 -0
- 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 +48 -6
- package/dist/i18n/spinner.js +7 -1
- package/dist/index.js +10 -50
- 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 +0 -9
- package/dist/tui/TuiEditor.js +4 -43
- package/dist/tui/edit.js +13 -22
- package/dist/tui/keys.js +12 -14
- 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 +2 -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/sched-smoke.mts +0 -150
- 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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.76.0] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- bake methodology skills in, localize skill hints, fix Ctrl+Del and scroll-to-bottom
|
|
15
|
+
|
|
16
|
+
## [2.75.6] - 2026-10-16
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **tools:** AskUser — a modal question with options (and a free-text escape)
|
|
21
|
+
- **skills:** tell the model to check and read a matching skill before a non-trivial task
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **deepseek:** retry a FINISHED turn with only a truncated RESPONSE head
|
|
26
|
+
- **tui:** default ui.mouseScroll back to true (wheel scrolls the log)
|
|
27
|
+
- **tui:** default ui.mouseScroll back to false so native selection works
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- prune dead code, untrack one-off TUI probes, ship only postinstall
|
|
32
|
+
- **tui:** drop the unused onCopy/hint affordance from CodeBlock
|
|
33
|
+
- **tui:** drop the code-block copy feature (Ctrl+Y / /copy-code)
|
|
34
|
+
|
|
10
35
|
## [2.75.5] - 2026-10-15
|
|
11
36
|
|
|
12
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -258,8 +258,11 @@ The prompt is a small line editor with persistent history:
|
|
|
258
258
|
Full-screen mode adds dialog navigation, so a long answer can be read and
|
|
259
259
|
selected without leaving the terminal:
|
|
260
260
|
|
|
261
|
-
-
|
|
262
|
-
|
|
261
|
+
- `ui.mouseScroll=false` (default) — the terminal owns the mouse: normal text
|
|
262
|
+
selection works and the wheel is inert (it never scrolls the input history —
|
|
263
|
+
history is arrow keys only).
|
|
264
|
+
- `ui.mouseScroll=true` — the app consumes the wheel and scrolls the dialog;
|
|
265
|
+
selecting text then needs `Shift`+click (the TUI convention).
|
|
263
266
|
- `PgUp` / `PgDn` — move up/down the dialog half a screen.
|
|
264
267
|
- `Home` / `End` — jump to the very top / bottom of the dialog (only while the
|
|
265
268
|
input line is empty; with text typed they move the caret as usual).
|
|
@@ -838,8 +841,11 @@ zames
|
|
|
838
841
|
В полноэкранном режиме добавляется навигация по диалогу, чтобы длинный ответ
|
|
839
842
|
можно было прочитать и выделить не покидая терминал:
|
|
840
843
|
|
|
841
|
-
-
|
|
842
|
-
|
|
844
|
+
- `ui.mouseScroll=false` (по умолчанию) — терминал владеет мышью: обычное
|
|
845
|
+
выделение текста работает, колесо инертно (и никогда не листает историю
|
|
846
|
+
ввода — история только стрелками).
|
|
847
|
+
- `ui.mouseScroll=true` — приложение перехватывает колесо и скроллит диалог;
|
|
848
|
+
выделение текста тогда — `Shift`+клик (стандарт TUI).
|
|
843
849
|
- `PgUp` / `PgDn` — вверх/вниз по диалогу на пол-экрана.
|
|
844
850
|
- `Home` / `End` — прыжок в самый верх / низ диалога (только когда строка ввода
|
|
845
851
|
пуста; при набранном тексте они двигают курсор, как обычно).
|
package/dist/agent-loop.js
CHANGED
|
@@ -7,7 +7,7 @@ import { substituteAttachmentMarkers } from './commands.js';
|
|
|
7
7
|
import { loadHooks, runPreToolUse, runPostToolUse, runLifecycleHooks, } from './hooks.js';
|
|
8
8
|
import { loadPermissions, decidePermission, } from './permissions.js';
|
|
9
9
|
import { shouldRunDiagnostics, runDiagnostics, } from './diagnostics.js';
|
|
10
|
-
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, }) {
|
|
10
|
+
export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 0, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onSendPause = () => { }, onSendState = () => { }, onNotice = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', askDeadlineMs = 240_000, maxAfterToolRetries = 6, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, hooks = undefined, permissions = undefined, diagnostics = null, onAskPermission = undefined, selfImprovement = false, onSubagent = null, onAskUser = null, }) {
|
|
11
11
|
// Resolve the hook config ONCE per task: a read per tool call would be
|
|
12
12
|
// wasteful, and a mid-task edit of hooks.json is not something to chase.
|
|
13
13
|
// `undefined` means "read .zames/hooks.json"; an explicit null disables
|
|
@@ -884,6 +884,66 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
884
884
|
}
|
|
885
885
|
continue;
|
|
886
886
|
}
|
|
887
|
+
// `AskUser` is a SEAM like `Task`: the CALLER owns the modal UI, the
|
|
888
|
+
// loop only relays the question and folds the answer back as the tool
|
|
889
|
+
// result. Intercepted BEFORE the unknown-tool branch so the tool can be
|
|
890
|
+
// handled even though its `fn` is a stub.
|
|
891
|
+
if (call.tool === 'AskUser') {
|
|
892
|
+
const rawQ = typeof call.args.question === 'string'
|
|
893
|
+
? call.args.question.trim()
|
|
894
|
+
: '';
|
|
895
|
+
const opts = Array.isArray(call.args.options)
|
|
896
|
+
? call.args.options
|
|
897
|
+
.map((o) => String(o ?? '').trim())
|
|
898
|
+
.filter(Boolean)
|
|
899
|
+
.slice(0, 6)
|
|
900
|
+
: [];
|
|
901
|
+
const recRaw = Number(call.args.recommended);
|
|
902
|
+
const recommended = Number.isInteger(recRaw) && recRaw >= 0 && recRaw < opts.length
|
|
903
|
+
? recRaw
|
|
904
|
+
: null;
|
|
905
|
+
const allowText = call.args.allow_text !== false;
|
|
906
|
+
if (!onAskUser || !rawQ || opts.length < 2) {
|
|
907
|
+
const err = !onAskUser
|
|
908
|
+
? 'AskUser is not available in this run. Choose a sensible default, proceed, and state the assumption in your final answer.'
|
|
909
|
+
: 'AskUser requires a non-empty `question` and at least two `options`. Proceed with your best judgement instead.';
|
|
910
|
+
safeToolResult(err);
|
|
911
|
+
transcript?.log('tool_error', { tool: 'AskUser', error: err });
|
|
912
|
+
results.push({ tool: 'AskUser', result: err });
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
syncToolAbort();
|
|
916
|
+
safeToolCall('AskUser', call.args);
|
|
917
|
+
transcript?.log('ask_user', {
|
|
918
|
+
question: rawQ,
|
|
919
|
+
options: opts,
|
|
920
|
+
recommended,
|
|
921
|
+
allowText,
|
|
922
|
+
});
|
|
923
|
+
let answer;
|
|
924
|
+
try {
|
|
925
|
+
answer = await onAskUser({
|
|
926
|
+
question: rawQ,
|
|
927
|
+
options: opts,
|
|
928
|
+
recommended,
|
|
929
|
+
allowText,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
catch (e) {
|
|
933
|
+
answer = `AskUser error: ${e.message}`;
|
|
934
|
+
}
|
|
935
|
+
const out = answer === null || answer === ''
|
|
936
|
+
? '(the operator dismissed the question — choose a sensible default, proceed, and state the assumption)'
|
|
937
|
+
: 'The operator answered: ' + answer;
|
|
938
|
+
safeToolResult(out);
|
|
939
|
+
transcript?.log('ask_user_result', { answer: out });
|
|
940
|
+
results.push({ tool: 'AskUser', result: out });
|
|
941
|
+
if (browser._abort || browser._stopped) {
|
|
942
|
+
transcript?.log('user_aborted');
|
|
943
|
+
return '(прервано пользователем)';
|
|
944
|
+
}
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
887
947
|
const tool = tools.find((t) => t.name === call.tool);
|
|
888
948
|
if (!tool) {
|
|
889
949
|
const err = `Unknown tool: ${call.tool}`;
|
package/dist/browser.js
CHANGED
|
@@ -459,15 +459,20 @@ export class DeepSeekBrowser {
|
|
|
459
459
|
const cid = url.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
460
460
|
if (cid)
|
|
461
461
|
this._netChatId = cid[0];
|
|
462
|
+
// A live hang: the turn ENDED (`quasi_status: FINISHED`) with no real
|
|
463
|
+
// answer — only reasoning, or a RESPONSE fragment truncated to a lone
|
|
464
|
+
// `{`. The extractor yields '' or the fragment, so the OLD
|
|
465
|
+
// `!extracted` guard skipped the check and the finish loop waited out
|
|
466
|
+
// the whole askDeadlineMs. Run the check on the RAW body regardless of
|
|
467
|
+
// whether a (bogus) fragment was extracted.
|
|
468
|
+
if (isFinishedWithoutAnswer(body)) {
|
|
469
|
+
// The UI shows Stopped + Continue. Record it with a fresh timestamp
|
|
470
|
+
// so the ask() loops do not wait out the timeout — they click
|
|
471
|
+
// Continue / resend.
|
|
472
|
+
this._netNoAnswer = true;
|
|
473
|
+
this._netCapture = '';
|
|
474
|
+
}
|
|
462
475
|
if (!extracted) {
|
|
463
|
-
if (isFinishedWithoutAnswer(body)) {
|
|
464
|
-
// A FINISHED turn with NO RESPONSE fragment (only reasoning): the
|
|
465
|
-
// UI shows Stopped + Continue. Record it with a fresh timestamp
|
|
466
|
-
// so the ask() loops do not wait out the timeout — they click
|
|
467
|
-
// Continue / resend.
|
|
468
|
-
this._netNoAnswer = true;
|
|
469
|
-
this._netCapture = '';
|
|
470
|
-
}
|
|
471
476
|
// A truncated turn (generation_err / INCOMPLETE) also has no
|
|
472
477
|
// RESPONSE; make the body visible to the incomplete check by
|
|
473
478
|
// refreshing the timestamp.
|
|
@@ -2044,6 +2049,12 @@ export class DeepSeekBrowser {
|
|
|
2044
2049
|
// Ticks since the SSE body reported FINISHED-with-answer while the DOM
|
|
2045
2050
|
// heuristics kept the loop waiting (see the safety net below).
|
|
2046
2051
|
let netFinishedTicks = 0;
|
|
2052
|
+
// Ticks since the SSE body reported FINISHED but produced NO real answer
|
|
2053
|
+
// (only reasoning / a truncated head) while no Continue button was found.
|
|
2054
|
+
// Waiting out the full answerTimeoutMs (up to 15 min) on a turn the server
|
|
2055
|
+
// already ENDED with nothing is pointless — after a short grace period the
|
|
2056
|
+
// loop gives up so ask() resends the prompt instead of a 4-minute spinner.
|
|
2057
|
+
let deadTicks = 0;
|
|
2047
2058
|
while (Date.now() < deadline) {
|
|
2048
2059
|
if (this._abort || this._askCancelled) {
|
|
2049
2060
|
return last || '(прервано пользователем)';
|
|
@@ -2160,6 +2171,27 @@ export class DeepSeekBrowser {
|
|
|
2160
2171
|
else {
|
|
2161
2172
|
netFinishedTicks = 0;
|
|
2162
2173
|
}
|
|
2174
|
+
// DEAD-TURN SAFETY NET. The server ENDED the turn (`quasi_status:
|
|
2175
|
+
// FINISHED`) but produced no real answer (only reasoning, or a truncated
|
|
2176
|
+
// RESPONSE head) AND there is no Continue button to resume it. The
|
|
2177
|
+
// `truncated`/`noAnswer` branches above throw at once when the raw body
|
|
2178
|
+
// says so; this is the belt-and-braces variant that also covers a body
|
|
2179
|
+
// whose flags were cleared by an earlier Continue click. Waiting out the
|
|
2180
|
+
// full answerTimeoutMs (up to 15 min) is pointless — after a short grace
|
|
2181
|
+
// period give up so ask() resends the prompt instead of a long spinner.
|
|
2182
|
+
if (this._netCaptureAt >= this._lastSentAt &&
|
|
2183
|
+
isFinishedWithAnswer(this._netBody) === false &&
|
|
2184
|
+
isGenerationIncompleteText(this._netBody) === false &&
|
|
2185
|
+
(this._netNoAnswer || isFinishedWithoutAnswer(this._netBody))) {
|
|
2186
|
+
deadTicks++;
|
|
2187
|
+
if (deadTicks >= 6 && !(await this._continueButtonVisible())) {
|
|
2188
|
+
this._askDebug('THROW dead-turn (finished without answer)');
|
|
2189
|
+
throw new GenerationIncompleteError('finished without answer');
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
else {
|
|
2193
|
+
deadTicks = 0;
|
|
2194
|
+
}
|
|
2163
2195
|
if (isNew && cur === last) {
|
|
2164
2196
|
stable++;
|
|
2165
2197
|
// `stabilityChecks` / `stabilityDelayMs` are the real knobs here.
|
package/dist/commands.js
CHANGED
|
@@ -987,7 +987,6 @@ export const SLASH_COMMANDS = [
|
|
|
987
987
|
{ name: '/rename', key: 'help.cmd.rename' },
|
|
988
988
|
{ name: '/context', key: 'help.cmd.context' },
|
|
989
989
|
{ name: '/copy', key: 'help.cmd.copy' },
|
|
990
|
-
{ name: '/copy-code', key: 'help.cmd.copy_code' },
|
|
991
990
|
{ name: '/chat', key: 'help.cmd.chat' },
|
|
992
991
|
{ name: '/cd', key: 'help.cmd.cd' },
|
|
993
992
|
{ name: '/pwd', key: 'help.cmd.pwd' },
|
package/dist/config.js
CHANGED
|
@@ -71,7 +71,11 @@ export const DEFAULTS = {
|
|
|
71
71
|
incompleteWaitMs: 2000,
|
|
72
72
|
autoContinue: true,
|
|
73
73
|
continueMinGapMs: 1500,
|
|
74
|
-
|
|
74
|
+
// The agent-loop watchdog for ONE ask(). It only bounds a SILENT send:
|
|
75
|
+
// a turn the server already ENDED with no answer (FINISHED + truncated
|
|
76
|
+
// RESPONSE) is detected by the network detectors and retried in seconds,
|
|
77
|
+
// not waited out. 240s used to mean a 4-minute spinner per dead turn.
|
|
78
|
+
askDeadlineMs: 120000,
|
|
75
79
|
maxAfterToolRetries: 6,
|
|
76
80
|
autoCompact: false,
|
|
77
81
|
autoCompactPct: 95,
|
|
@@ -113,8 +117,9 @@ export const DEFAULTS = {
|
|
|
113
117
|
// REPORTING (1000/1006) plus alternate-scroll OFF (1007l), so the wheel
|
|
114
118
|
// arrives as SGR events we consume and never as Up/Down arrows. The
|
|
115
119
|
// trade-off is that the terminal no longer does its own drag-select, so
|
|
116
|
-
// selecting text needs SHIFT+DRAG (the standard TUI convention
|
|
117
|
-
//
|
|
120
|
+
// selecting text needs SHIFT+DRAG (the standard TUI convention: Claude
|
|
121
|
+
// Code / Codex / opencode do the same). Set to false to give the terminal
|
|
122
|
+
// the mouse back (native select, wheel inert).
|
|
118
123
|
mouseScroll: true,
|
|
119
124
|
},
|
|
120
125
|
// Off by default: a full tsc/eslint run after every edit is slow; opt in.
|
package/dist/context.js
CHANGED
|
@@ -57,6 +57,105 @@ const BUILTIN_SKILLS = [
|
|
|
57
57
|
'couplings, and any WHY-comments that matter.\n' +
|
|
58
58
|
'4. Do NOT change code. Answer in the operator language.',
|
|
59
59
|
},
|
|
60
|
+
// Methodology skills, ported from superpowers (github.com/obra/superpowers)
|
|
61
|
+
// as plain prompt bodies — the zames equivalent of its SessionStart
|
|
62
|
+
// bootstrap, without a hook. The model is nudged to read the matching one
|
|
63
|
+
// before a non-trivial task by SKILLS_DISCIPLINE (src/system-prompt.ts).
|
|
64
|
+
// Baked in (not a file on disk) so a user who installed the npm package has
|
|
65
|
+
// them out of the box; a project skill of the same name overrides them.
|
|
66
|
+
{
|
|
67
|
+
name: 'brainstorming',
|
|
68
|
+
description: 'Use when the request is a GOAL, not a spec — "add X", "make it better", "integrate Y" — and it is unclear what "done" means.',
|
|
69
|
+
body: 'When a request is a goal ("make scrolling feel right"), building ' +
|
|
70
|
+
'immediately produces the wrong thing fast. First make the intent concrete.\n' +
|
|
71
|
+
'Pin down: (a) the REAL outcome — what should be TRUE after that is not ' +
|
|
72
|
+
'true now; (b) the acceptance check — how the operator will know it works; ' +
|
|
73
|
+
'(c) scope edges — what is explicitly NOT part of this; (d) the ' +
|
|
74
|
+
'alternatives considered and why you chose one.\n' +
|
|
75
|
+
'zames has NO conversational turn: the operator sees only your final ' +
|
|
76
|
+
'respond. So do the brainstorming YOURSELF and return a SHORT, concrete ' +
|
|
77
|
+
'proposal in ONE respond — the goal as you understood it, your ' +
|
|
78
|
+
'assumptions and defaults, what you will NOT do — then either proceed if ' +
|
|
79
|
+
'the request was clearly actionable, or stop and ask the ONE decision you ' +
|
|
80
|
+
'genuinely cannot make alone. For an already-precise request ("read X", ' +
|
|
81
|
+
'"rename Y") skip this entirely.',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'writing-plans',
|
|
85
|
+
description: 'Use when a task needs more than one or two tool calls — a multi-file change, a new feature, a refactor. Breaks it into small ordered steps with exact paths and a verification for each.',
|
|
86
|
+
body: 'A plan is a sequence of steps a careful engineer with NO project ' +
|
|
87
|
+
'context could follow. Each step (2-5 minutes) names: the EXACT file ' +
|
|
88
|
+
'path(s); the concrete edit; how to verify it (command, test name, ' +
|
|
89
|
+
'expected output). Order matters: put the step that DE-RISKS the task ' +
|
|
90
|
+
'first (a spike, a failing test, a proof the API works) — do not build ' +
|
|
91
|
+
'three layers on an unverified assumption.\n' +
|
|
92
|
+
'Rules: one concern per step ("add the flag AND refactor the caller" is ' +
|
|
93
|
+
'two); prefer a step that FAILS LOUDLY over one that silently ' +
|
|
94
|
+
'half-works; every behavioural change gets a test step (see ' +
|
|
95
|
+
'test-driven-development); if a step turns out wrong, STOP and revise the ' +
|
|
96
|
+
'plan instead of improvising; a 30-step plan means the task should be ' +
|
|
97
|
+
'split. In zames the plan lives in your reasoning and, for a long task, ' +
|
|
98
|
+
'in a TodoWrite list — one entry per step. Do not narrate it between tool ' +
|
|
99
|
+
'calls; the todo list IS the visible plan.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'test-driven-development',
|
|
103
|
+
description: 'Use when writing or changing testable behaviour — a bug fix, a new function, a parser, a formatter. Write the failing test first, watch it fail, then make it pass.',
|
|
104
|
+
body: 'RED -> GREEN -> REFACTOR. The order is the point, not a formality.\n' +
|
|
105
|
+
'1. RED — write the smallest test that expresses the new behaviour and ' +
|
|
106
|
+
'RUN it. It must FAIL for the RIGHT reason ("not implemented", not a ' +
|
|
107
|
+
'typo). A test you never saw fail proves nothing.\n' +
|
|
108
|
+
'2. GREEN — write the MINIMUM code that makes it pass; do not add the ' +
|
|
109
|
+
'next feature "while you are there".\n' +
|
|
110
|
+
'3. REFACTOR — clean up with the test green, then re-run.\n' +
|
|
111
|
+
'A good test asserts BEHAVIOUR (input -> output), is deterministic (no ' +
|
|
112
|
+
'sleeps, no network, no wall-clock or test-order dependence) and fails ' +
|
|
113
|
+
'for ONE reason with a message that says what broke. In this project a ' +
|
|
114
|
+
'bug fix ALWAYS gets a regression test reproducing the original bug. ' +
|
|
115
|
+
'When a test needs fixing, ask whether the TEST or the CODE was wrong; ' +
|
|
116
|
+
'if the code was written before its test, rewrite it driven by the test.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'systematic-debugging',
|
|
120
|
+
description: 'Use when something is broken and the cause is not obvious — a failing test, a wrong result, a hang, a crash. Four phases: reproduce, isolate, hypothesize, fix.',
|
|
121
|
+
body: 'Do NOT start by editing code. Work the four phases in order; each ' +
|
|
122
|
+
'produces an artifact you can point at.\n' +
|
|
123
|
+
'1. REPRODUCE — get a command that FAILS the way the operator describes; ' +
|
|
124
|
+
'copy the exact error. If it passes you do not have the bug yet: narrow ' +
|
|
125
|
+
'the input until it fails.\n' +
|
|
126
|
+
'2. ISOLATE — shrink to the smallest failing unit (bisect, stub, ' +
|
|
127
|
+
'binary-search). Read the code the failing path ACTUALLY executes. Check ' +
|
|
128
|
+
'the obvious first: wrong file, stale build, cached state, different ' +
|
|
129
|
+
'config, an error swallowed by a catch.\n' +
|
|
130
|
+
'3. HYPOTHESIZE — state it in one sentence and predict what you should ' +
|
|
131
|
+
'see if it is right; test exactly that. One change at a time; prefer an ' +
|
|
132
|
+
'observation (log, test assertion) over an edit. If the prediction is ' +
|
|
133
|
+
'wrong, DISCARD the hypothesis.\n' +
|
|
134
|
+
'4. FIX AND VERIFY — fix the CAUSE, not the symptom; add a regression ' +
|
|
135
|
+
'test that fails WITHOUT the fix; re-run the phase-1 reproduction ' +
|
|
136
|
+
'verbatim; look for the same bug elsewhere.\n' +
|
|
137
|
+
'Anti-patterns: editing before reproducing; "it works now" without ' +
|
|
138
|
+
'knowing what changed; silencing an error you do not understand with a ' +
|
|
139
|
+
'try/catch; trusting a green test never seen red.',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'verification-before-completion',
|
|
143
|
+
description: 'Use BEFORE declaring a task done — a feature, a bug fix, a generated file. Forces you to run the verification you promised instead of trusting the code looks right.',
|
|
144
|
+
body: '"It should work" is not done. Before reporting success, produce ' +
|
|
145
|
+
'EVIDENCE from the real artifact, not from your intent.\n' +
|
|
146
|
+
'1. Re-read the original request; tick every requirement (including the ' +
|
|
147
|
+
'ones in a parenthetical) against something you can show.\n' +
|
|
148
|
+
'2. Run the thing: build/typecheck/tests for code; run a script; for a ' +
|
|
149
|
+
'generated file, open/parse it and confirm it is COMPLETE, not ' +
|
|
150
|
+
'truncated; for a UI, the real screen, not the diff.\n' +
|
|
151
|
+
'3. Re-read your own diff as a reviewer: leftover debug output, dead ' +
|
|
152
|
+
'code, a wrong path, a test that asserts nothing.\n' +
|
|
153
|
+
'4. State the evidence in the final report — the exact command and its ' +
|
|
154
|
+
'result — so the operator can reproduce it. "Tests pass" without the ' +
|
|
155
|
+
'command is not evidence.\n' +
|
|
156
|
+
'If you cannot run the verification, SAY SO explicitly and describe what ' +
|
|
157
|
+
'you did instead; never report "done" implying a check you did not run.',
|
|
158
|
+
},
|
|
60
159
|
];
|
|
61
160
|
// Per-file budgets. AGENTS.md/MEMORY.md are hand-maintained and grow over
|
|
62
161
|
// time, so the caps are generous: an AGENTS.md of a real project (this repo's
|
package/dist/deepseek-ui.js
CHANGED
|
@@ -18,7 +18,7 @@ export const CHAT_URL = 'https://chat.deepseek.com/';
|
|
|
18
18
|
export const CHAT_ID_RE = /\/chat\/s\/([a-zA-Z0-9_-]+)/;
|
|
19
19
|
// DeepSeek's chat URL shape. A direct navigation always yields the requested
|
|
20
20
|
// chat (the sidebar link click is tried first, this is the fallback).
|
|
21
|
-
|
|
21
|
+
const CHAT_PATH_PREFIX = 'a/chat/s/';
|
|
22
22
|
export function chatUrl(id) {
|
|
23
23
|
return CHAT_URL + CHAT_PATH_PREFIX + id;
|
|
24
24
|
}
|
|
@@ -177,7 +177,6 @@ export const MFA_CODE_SELECTORS = [
|
|
|
177
177
|
'.ds-mfa-verification-modal input[type="tel"]',
|
|
178
178
|
'input[inputmode="numeric"][maxlength="6"]',
|
|
179
179
|
];
|
|
180
|
-
export const MFA_MODAL_SELECTOR = '.ds-mfa-verification-modal, [role="dialog"]';
|
|
181
180
|
export const MFA_SUBMIT_SELECTORS = [
|
|
182
181
|
'div[role="button"].ds-mfa-verification-submit',
|
|
183
182
|
'.ds-mfa-verification-modal div[role="button"].ds-button--primary',
|
|
@@ -264,7 +263,6 @@ export const INCOMPLETE_STATUS_RE = /"quasi_status"(?::|,"v":)"INCOMPLETE"/i;
|
|
|
264
263
|
// fragment. The UI shows Stopped + Continue. Matches both the JSON-property
|
|
265
264
|
// and the BATCH shape (see INCOMPLETE_STATUS_RE).
|
|
266
265
|
export const FINISHED_STATUS_RE = /"quasi_status"(?::|,"v":)"FINISHED"/i;
|
|
267
|
-
export const RESPONSE_CONTENT_RE = /"type":"RESPONSE","content":"[^"]/;
|
|
268
266
|
// Only a completion/continue turn carries a real answer. Gating the capture
|
|
269
267
|
// assignment to these URLs keeps a future endpoint (which happens to return a
|
|
270
268
|
// `content`/`text` field) from clobbering the answer and looking fresh.
|
package/dist/i18n/common.js
CHANGED
|
@@ -117,6 +117,19 @@ export const commonMessages = {
|
|
|
117
117
|
en: 'Allow this call?',
|
|
118
118
|
},
|
|
119
119
|
'perm.title': { ru: 'Подтверждение', en: 'Confirmation' },
|
|
120
|
+
// The AskUser modal (the model asks the operator a question with options).
|
|
121
|
+
'askuser.title': { ru: 'Вопрос от агента', en: 'Question from the agent' },
|
|
122
|
+
'askuser.other': {
|
|
123
|
+
ru: 'Свой вариант — введу текстом',
|
|
124
|
+
en: 'Other — I will type my own answer',
|
|
125
|
+
},
|
|
126
|
+
'askuser.recommended': { ru: 'рекомендую', en: 'recommended' },
|
|
127
|
+
'askuser.your_answer': { ru: 'Ваш ответ:', en: 'Your answer:' },
|
|
128
|
+
'askuser.hint_select': { ru: 'выбор', en: 'select' },
|
|
129
|
+
'askuser.hint_ok': { ru: 'ок', en: 'ok' },
|
|
130
|
+
'askuser.hint_dismiss': { ru: 'закрыть', en: 'dismiss' },
|
|
131
|
+
'askuser.hint_send': { ru: 'отправить', en: 'send' },
|
|
132
|
+
'askuser.hint_cancel': { ru: 'отмена', en: 'cancel' },
|
|
120
133
|
'confirm.yes': { ru: 'Да', en: 'Yes' },
|
|
121
134
|
'confirm.no': { ru: 'Нет', en: 'No' },
|
|
122
135
|
'perm.ask_sub': {
|
|
@@ -246,22 +259,6 @@ export const commonMessages = {
|
|
|
246
259
|
ru: 'Не удалось скопировать (нет {tool} или он недоступен).',
|
|
247
260
|
en: 'Could not copy ({tool} missing or unavailable).',
|
|
248
261
|
},
|
|
249
|
-
'copy.no_code': {
|
|
250
|
-
ru: 'В последнем ответе нет блоков кода.',
|
|
251
|
-
en: 'The last answer has no code blocks.',
|
|
252
|
-
},
|
|
253
|
-
'copy.bad_index': {
|
|
254
|
-
ru: 'Нет блока кода #{n}.',
|
|
255
|
-
en: 'No code block #{n}.',
|
|
256
|
-
},
|
|
257
|
-
'copy_code.list': {
|
|
258
|
-
ru: 'Блоки кода последнего ответа:',
|
|
259
|
-
en: 'Code blocks in the last answer:',
|
|
260
|
-
},
|
|
261
|
-
'copy_code.item': {
|
|
262
|
-
ru: ' #{n} {lang} — {lines} стр.',
|
|
263
|
-
en: ' #{n} {lang} — {lines} lines',
|
|
264
|
-
},
|
|
265
262
|
'retry.running': { ru: 'Повторяю задачу…', en: 'Retrying the task...' },
|
|
266
263
|
'rename.usage': {
|
|
267
264
|
ru: 'Использование: /rename <title>',
|
package/dist/i18n/config.js
CHANGED
|
@@ -44,8 +44,8 @@ export const configMessages = {
|
|
|
44
44
|
en: 'Full-screen TUI mode (alternate screen)',
|
|
45
45
|
},
|
|
46
46
|
'cfg.f.ui_mouseScroll': {
|
|
47
|
-
ru: '
|
|
48
|
-
en: '
|
|
47
|
+
ru: 'Колесо: лог или терминал (выкл = выделение без Shift)',
|
|
48
|
+
en: 'Wheel: log or terminal (off = select without Shift)',
|
|
49
49
|
},
|
|
50
50
|
'cfg.f.maxIterations': {
|
|
51
51
|
ru: 'Лимит итераций на задачу',
|
package/dist/i18n/help.js
CHANGED
|
@@ -102,8 +102,8 @@ export const helpMessages = {
|
|
|
102
102
|
en: 'PgUp / PgDn up/down the dialog (Home/End — top/bottom) [full-screen]',
|
|
103
103
|
},
|
|
104
104
|
'help.key.wheel': {
|
|
105
|
-
ru: 'колесо
|
|
106
|
-
en: '
|
|
105
|
+
ru: 'ui.mouseScroll on (умолч.) — колесо скроллит диалог (выделение Shift); off — выделение мышью',
|
|
106
|
+
en: 'ui.mouseScroll on (default) — wheel scrolls the dialog (select: Shift); off — select with the mouse',
|
|
107
107
|
},
|
|
108
108
|
'help.key.bang': {
|
|
109
109
|
ru: '!команда выполнить shell-команду напрямую (мимо модели)',
|
|
@@ -201,10 +201,6 @@ export const helpMessages = {
|
|
|
201
201
|
ru: '/copy скопировать последний ответ в буфер обмена',
|
|
202
202
|
en: '/copy copy the last answer to the clipboard',
|
|
203
203
|
},
|
|
204
|
-
'help.cmd.copy_code': {
|
|
205
|
-
ru: '/copy-code [номер] копировать блок кода из последнего ответа (Ctrl+Y)',
|
|
206
|
-
en: '/copy-code [n] copy a code block from the last answer (Ctrl+Y)',
|
|
207
|
-
},
|
|
208
204
|
'help.cmd.cost': { ru: '/cost статистика сессии', en: '/cost session stats' },
|
|
209
205
|
'help.cmd.export': {
|
|
210
206
|
ru: '/export [file] выгрузить сессию в Markdown',
|
|
@@ -578,4 +574,50 @@ export const helpMessages = {
|
|
|
578
574
|
ru: 'Индикаторы в статус-строке: 🧠 — глубокое размышление, 🌐 — поиск в интернете (видны, когда включены).',
|
|
579
575
|
en: 'Status-line icons: 🧠 — Deep thinking, 🌐 — web search (shown when enabled).',
|
|
580
576
|
},
|
|
577
|
+
// Localized descriptions for the WELL-KNOWN skills (built-in + bundled
|
|
578
|
+
// methodology). A skill's SKILL.md frontmatter is agent-facing English, so
|
|
579
|
+
// showing it verbatim in the «/» hints would leave an English line in a
|
|
580
|
+
// Russian UI. Keyed by skill NAME, applied regardless of source, so a local
|
|
581
|
+
// `.zames/skills/` copy of a bundled skill still shows the translated text.
|
|
582
|
+
// An unknown (user-authored) skill has no key and keeps its own description.
|
|
583
|
+
'skill.desc.commit': {
|
|
584
|
+
ru: 'подготовить аккуратный git-коммит из текущих изменений',
|
|
585
|
+
en: 'prepare a clean git commit from the current changes',
|
|
586
|
+
},
|
|
587
|
+
'skill.desc.review': {
|
|
588
|
+
ru: 'ревью текущего диффа на баги и риски (без правок)',
|
|
589
|
+
en: 'review the current diff for bugs and risks (no changes)',
|
|
590
|
+
},
|
|
591
|
+
'skill.desc.test': {
|
|
592
|
+
ru: 'прогнать тесты проекта и чинить падения до зелёного',
|
|
593
|
+
en: 'run the project tests and fix failures until green',
|
|
594
|
+
},
|
|
595
|
+
'skill.desc.debug': {
|
|
596
|
+
ru: 'системно диагностировать и починить баг',
|
|
597
|
+
en: 'systematically diagnose and fix a bug',
|
|
598
|
+
},
|
|
599
|
+
'skill.desc.explain': {
|
|
600
|
+
ru: 'объяснить, как работает файл/модуль/фича',
|
|
601
|
+
en: 'explain how a file/module/feature works',
|
|
602
|
+
},
|
|
603
|
+
'skill.desc.brainstorming': {
|
|
604
|
+
ru: 'когда запрос — это цель, а не спека: сначала прояснить замысел',
|
|
605
|
+
en: 'when the request is a goal, not a spec: pin down the intent first',
|
|
606
|
+
},
|
|
607
|
+
'skill.desc.writing-plans': {
|
|
608
|
+
ru: 'разбить многошаговую задачу на маленькие шаги с проверкой каждого',
|
|
609
|
+
en: 'break a multi-step task into small steps, each with a check',
|
|
610
|
+
},
|
|
611
|
+
'skill.desc.test-driven-development': {
|
|
612
|
+
ru: 'сначала падающий тест, потом код (RED → GREEN → REFACTOR)',
|
|
613
|
+
en: 'write the failing test first, then the code (RED → GREEN → REFACTOR)',
|
|
614
|
+
},
|
|
615
|
+
'skill.desc.systematic-debugging': {
|
|
616
|
+
ru: 'четыре фазы отладки: воспроизвести, изолировать, гипотеза, фикс',
|
|
617
|
+
en: 'four debugging phases: reproduce, isolate, hypothesize, fix',
|
|
618
|
+
},
|
|
619
|
+
'skill.desc.verification-before-completion': {
|
|
620
|
+
ru: 'прогнать проверку ПЕРЕД тем, как объявить задачу готовой',
|
|
621
|
+
en: 'run the verification BEFORE declaring the task done',
|
|
622
|
+
},
|
|
581
623
|
};
|
package/dist/i18n/spinner.js
CHANGED
|
@@ -35,11 +35,17 @@ export const spinnerMessages = {
|
|
|
35
35
|
ru: 'Напишите задачу и нажмите Enter. /help — команды, Ctrl+J — новая строка.',
|
|
36
36
|
en: 'Type a task and press Enter. /help for commands, Ctrl+J for a new line.',
|
|
37
37
|
},
|
|
38
|
-
// One-line key hint UNDER the input box (full-screen mode).
|
|
38
|
+
// One-line key hint UNDER the input box (full-screen mode). Two variants:
|
|
39
|
+
// with in-app wheel scroll the mouse belongs to the app, so selection needs
|
|
40
|
+
// Shift; without it the terminal keeps the mouse and the wheel is inert.
|
|
39
41
|
'editor.keys_hint': {
|
|
40
42
|
ru: 'выделение — Shift+клик · колесо — скролл · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
|
|
41
43
|
en: 'select — Shift+click · wheel — scroll · PgUp/PgDn — up/down · Home/End — top/bottom',
|
|
42
44
|
},
|
|
45
|
+
'editor.keys_hint_select': {
|
|
46
|
+
ru: 'выделение — мышью · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
|
|
47
|
+
en: 'select — drag · PgUp/PgDn — up/down · Home/End — top/bottom',
|
|
48
|
+
},
|
|
43
49
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
44
50
|
'editor.search_prompt': {
|
|
45
51
|
ru: '(обратный поиск) `{q}` — Ctrl+R дальше, Enter принять, Esc отмена',
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,6 @@ import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } f
|
|
|
35
35
|
import { createCliArgs } from './cli-args.js';
|
|
36
36
|
import { qwenContextWindow } from './qwen-ui.js';
|
|
37
37
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
38
|
-
import { extractCodeBlocks } from './code-blocks.js';
|
|
39
38
|
import { contentWidth } from './width.js';
|
|
40
39
|
import { renderStartupBanner } from './banner.js';
|
|
41
40
|
import { runStatusLineCommand } from './statusline.js';
|
|
@@ -531,9 +530,18 @@ async function refreshDynamicCommands(workdir) {
|
|
|
531
530
|
for (const s of skills) {
|
|
532
531
|
if (!s.userInvokable)
|
|
533
532
|
continue;
|
|
533
|
+
// A well-known skill (built-in or bundled methodology) has a localized
|
|
534
|
+
// description keyed by NAME — its SKILL.md frontmatter is agent-facing
|
|
535
|
+
// English, which must not leak into a Russian UI. `t` returns the key
|
|
536
|
+
// itself when it is missing, so an unknown (user-authored) skill falls
|
|
537
|
+
// back to its own description.
|
|
538
|
+
const descKey = 'skill.desc.' + s.name;
|
|
539
|
+
const localized = t(descKey);
|
|
534
540
|
out.push({
|
|
535
541
|
name: '/' + s.name,
|
|
536
|
-
description:
|
|
542
|
+
description: localized !== descKey
|
|
543
|
+
? localized
|
|
544
|
+
: s.description || t('help.cmd.skill'),
|
|
537
545
|
});
|
|
538
546
|
}
|
|
539
547
|
const cmds = await loadCommands(workdir);
|
|
@@ -1528,7 +1536,6 @@ async function main() {
|
|
|
1528
1536
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1529
1537
|
return att;
|
|
1530
1538
|
};
|
|
1531
|
-
ed.onCopyText = (text) => writeClipboardText(text);
|
|
1532
1539
|
ed.onSubmit = (text, items) => {
|
|
1533
1540
|
// While the agent is busy the main command loop is blocked on
|
|
1534
1541
|
// `await runTask()`, so ONLY commands that make sense without the main
|
|
@@ -3239,53 +3246,6 @@ async function main() {
|
|
|
3239
3246
|
}
|
|
3240
3247
|
continue;
|
|
3241
3248
|
}
|
|
3242
|
-
if (lower === '/copy-code' || lower.startsWith('/copy-code ')) {
|
|
3243
|
-
const arg = trimmed.slice('/copy-code'.length).trim();
|
|
3244
|
-
const blocks = extractCodeBlocks(lastAssistantMessage ?? '');
|
|
3245
|
-
if (!blocks.length) {
|
|
3246
|
-
console.log(theme.dim(t('copy.no_code')));
|
|
3247
|
-
continue;
|
|
3248
|
-
}
|
|
3249
|
-
if (arg === 'list') {
|
|
3250
|
-
console.log(theme.system(t('copy_code.list')));
|
|
3251
|
-
for (const b of blocks) {
|
|
3252
|
-
console.log(theme.assistant(t('copy_code.item', {
|
|
3253
|
-
n: String(b.index),
|
|
3254
|
-
lang: b.lang || 'text',
|
|
3255
|
-
lines: String(b.text.split(String.fromCharCode(10)).length),
|
|
3256
|
-
})));
|
|
3257
|
-
}
|
|
3258
|
-
continue;
|
|
3259
|
-
}
|
|
3260
|
-
// Prefer the TUI copy path (it reports into the log); fall back to a
|
|
3261
|
-
// direct clipboard write for the legacy editor / non-TTY.
|
|
3262
|
-
const n = arg ? Number(arg) : undefined;
|
|
3263
|
-
if (n !== undefined && (!Number.isInteger(n) || n < 1)) {
|
|
3264
|
-
console.log(theme.warn(t('copy.bad_index', { n: arg })));
|
|
3265
|
-
continue;
|
|
3266
|
-
}
|
|
3267
|
-
if (editor &&
|
|
3268
|
-
'copyCode' in editor &&
|
|
3269
|
-
typeof editor.copyCode === 'function') {
|
|
3270
|
-
editor.copyCode(n);
|
|
3271
|
-
continue;
|
|
3272
|
-
}
|
|
3273
|
-
const pick = n !== undefined
|
|
3274
|
-
? blocks.find((b) => b.index === n)
|
|
3275
|
-
: blocks[blocks.length - 1];
|
|
3276
|
-
if (!pick) {
|
|
3277
|
-
console.log(theme.warn(t('copy.bad_index', { n: String(n) })));
|
|
3278
|
-
continue;
|
|
3279
|
-
}
|
|
3280
|
-
const res2 = writeClipboardText(pick.text);
|
|
3281
|
-
if (res2.ok) {
|
|
3282
|
-
console.log(theme.assistant(t('copy.done', { tool: res2.via })));
|
|
3283
|
-
}
|
|
3284
|
-
else {
|
|
3285
|
-
console.error(theme.warn(t('copy.failed', { tool: res2.via })));
|
|
3286
|
-
}
|
|
3287
|
-
continue;
|
|
3288
|
-
}
|
|
3289
3249
|
if (lower === '/retry') {
|
|
3290
3250
|
if (!lastTaskText) {
|
|
3291
3251
|
console.log(theme.dim(t('retry.none')));
|