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/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.75.6] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **tools:** AskUser — a modal question with options (and a free-text escape)
|
|
15
|
+
- **skills:** tell the model to check and read a matching skill before a non-trivial task
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **deepseek:** retry a FINISHED turn with only a truncated RESPONSE head
|
|
20
|
+
- **tui:** default ui.mouseScroll back to true (wheel scrolls the log)
|
|
21
|
+
- **tui:** default ui.mouseScroll back to false so native selection works
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- prune dead code, untrack one-off TUI probes, ship only postinstall
|
|
26
|
+
- **tui:** drop the unused onCopy/hint affordance from CodeBlock
|
|
27
|
+
- **tui:** drop the code-block copy feature (Ctrl+Y / /copy-code)
|
|
28
|
+
|
|
29
|
+
## [2.75.5] - 2026-10-15
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **i18n:** clearer TUI key hints; localize the scrolled status; add sched-smoke
|
|
34
|
+
|
|
10
35
|
## [2.75.4] - 2026-10-15
|
|
11
36
|
|
|
12
37
|
### Added
|
package/README.md
CHANGED
|
@@ -258,9 +258,12 @@ 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
|
-
|
|
263
|
-
|
|
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).
|
|
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).
|
|
266
269
|
- To copy a LARGE block, use `/export` (writes the whole session as Markdown)
|
|
@@ -838,9 +841,12 @@ zames
|
|
|
838
841
|
В полноэкранном режиме добавляется навигация по диалогу, чтобы длинный ответ
|
|
839
842
|
можно было прочитать и выделить не покидая терминал:
|
|
840
843
|
|
|
841
|
-
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
+
- `ui.mouseScroll=false` (по умолчанию) — терминал владеет мышью: обычное
|
|
845
|
+
выделение текста работает, колесо инертно (и никогда не листает историю
|
|
846
|
+
ввода — история только стрелками).
|
|
847
|
+
- `ui.mouseScroll=true` — приложение перехватывает колесо и скроллит диалог;
|
|
848
|
+
выделение текста тогда — `Shift`+клик (стандарт TUI).
|
|
849
|
+
- `PgUp` / `PgDn` — вверх/вниз по диалогу на пол-экрана.
|
|
844
850
|
- `Home` / `End` — прыжок в самый верх / низ диалога (только когда строка ввода
|
|
845
851
|
пуста; при набранном тексте они двигают курсор, как обычно).
|
|
846
852
|
- Чтобы скопировать БОЛЬШОЙ кусок, используйте `/export` (пишет всю сессию в
|
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/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
|
@@ -78,8 +78,8 @@ export const helpMessages = {
|
|
|
78
78
|
en: 'Ctrl+← / Ctrl+→ move by words',
|
|
79
79
|
},
|
|
80
80
|
'help.key.slash': {
|
|
81
|
-
ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P —
|
|
82
|
-
en: '/ + Tab slash-command hints and completion (Ctrl+N/P
|
|
81
|
+
ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P — вверх/вниз)',
|
|
82
|
+
en: '/ + Tab slash-command hints and completion (Ctrl+N/P — up/down)',
|
|
83
83
|
},
|
|
84
84
|
'help.key.newline': {
|
|
85
85
|
ru: 'Ctrl+J / Ctrl+Enter новая строка (Shift+Enter в терминалах с поддержкой)',
|
|
@@ -98,12 +98,12 @@ export const helpMessages = {
|
|
|
98
98
|
en: 'Esc, Ctrl+C abort current generation',
|
|
99
99
|
},
|
|
100
100
|
'help.key.scroll': {
|
|
101
|
-
ru: 'PgUp / PgDn
|
|
102
|
-
en: 'PgUp / PgDn
|
|
101
|
+
ru: 'PgUp / PgDn вверх/вниз по диалогу (Home/End — в начало/конец) [full-screen]',
|
|
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',
|
package/dist/i18n/spinner.js
CHANGED
|
@@ -18,10 +18,15 @@ export const spinnerMessages = {
|
|
|
18
18
|
// flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
|
|
19
19
|
'state.generating': { ru: '▶ генерация', en: '▶ generating' },
|
|
20
20
|
'state.settled': { ru: '✓ готово', en: '✓ done' },
|
|
21
|
+
// Shown in the status line while the full-screen log is scrolled up.
|
|
22
|
+
'status.scrolled': {
|
|
23
|
+
ru: '↑ прокручено на {n} стр. · PgDn/End',
|
|
24
|
+
en: '↑ scrolled {n} lines · PgDn/End',
|
|
25
|
+
},
|
|
21
26
|
'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
|
|
22
27
|
'editor.page_hint': {
|
|
23
|
-
ru: '(Ctrl+N/Ctrl+P —
|
|
24
|
-
en: '(Ctrl+N/Ctrl+P
|
|
28
|
+
ru: '(Ctrl+N/Ctrl+P — вверх/вниз, Tab — вставить)',
|
|
29
|
+
en: '(Ctrl+N/Ctrl+P — up/down, Tab — insert)',
|
|
25
30
|
},
|
|
26
31
|
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
27
32
|
// Shown in the full-screen log viewport before the first turn, so a fresh
|
|
@@ -30,10 +35,16 @@ export const spinnerMessages = {
|
|
|
30
35
|
ru: 'Напишите задачу и нажмите Enter. /help — команды, Ctrl+J — новая строка.',
|
|
31
36
|
en: 'Type a task and press Enter. /help for commands, Ctrl+J for a new line.',
|
|
32
37
|
},
|
|
33
|
-
// 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.
|
|
34
41
|
'editor.keys_hint': {
|
|
35
|
-
ru: 'выделение — Shift
|
|
36
|
-
en: 'select — Shift+
|
|
42
|
+
ru: 'выделение — Shift+клик · колесо — скролл · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
|
|
43
|
+
en: 'select — Shift+click · wheel — scroll · PgUp/PgDn — up/down · Home/End — top/bottom',
|
|
44
|
+
},
|
|
45
|
+
'editor.keys_hint_select': {
|
|
46
|
+
ru: 'выделение — мышью · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
|
|
47
|
+
en: 'select — drag · PgUp/PgDn — up/down · Home/End — top/bottom',
|
|
37
48
|
},
|
|
38
49
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
39
50
|
'editor.search_prompt': {
|
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';
|
|
@@ -1528,7 +1527,6 @@ async function main() {
|
|
|
1528
1527
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1529
1528
|
return att;
|
|
1530
1529
|
};
|
|
1531
|
-
ed.onCopyText = (text) => writeClipboardText(text);
|
|
1532
1530
|
ed.onSubmit = (text, items) => {
|
|
1533
1531
|
// While the agent is busy the main command loop is blocked on
|
|
1534
1532
|
// `await runTask()`, so ONLY commands that make sense without the main
|
|
@@ -3239,53 +3237,6 @@ async function main() {
|
|
|
3239
3237
|
}
|
|
3240
3238
|
continue;
|
|
3241
3239
|
}
|
|
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
3240
|
if (lower === '/retry') {
|
|
3290
3241
|
if (!lastTaskText) {
|
|
3291
3242
|
console.log(theme.dim(t('retry.none')));
|
package/dist/net-capture.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { FINISHED_STATUS_RE
|
|
4
|
+
import { FINISHED_STATUS_RE } from './deepseek-ui.js';
|
|
5
5
|
// Extraction of the RAW model answer text from DeepSeek's network data.
|
|
6
6
|
//
|
|
7
7
|
// Why: the answer is read from the RENDERED DOM, and DeepSeek's rendering
|
|
@@ -160,15 +160,43 @@ export function extractAnswer(body) {
|
|
|
160
160
|
return sse;
|
|
161
161
|
return extractFromJson(body);
|
|
162
162
|
}
|
|
163
|
+
// The smallest RESPONSE that counts as a REAL answer rather than a truncated
|
|
164
|
+
// head. A live hang: DeepSeek began the RESPONSE fragment with a single `{`
|
|
165
|
+
// (`"type":"RESPONSE","content":"{"`), then the stream ended with
|
|
166
|
+
// `quasi_status: FINISHED` and no tool-call JSON ever followed. The OLD
|
|
167
|
+
// RESPONSE_CONTENT_RE (one non-quote char) matched that `{`, so the body looked
|
|
168
|
+
// "finished WITH an answer" (isFinishedWithAnswer === true,
|
|
169
|
+
// isFinishedWithoutAnswer === false): the finish loop kept polling for a
|
|
170
|
+
// complete answer and `ask()` gave up only at the askDeadlineMs watchdog — the
|
|
171
|
+
// operator watched a spinner for ~4 minutes with no tool call.
|
|
172
|
+
// A real answer (a tool-call JSON, prose, a `respond` call) is far longer, so
|
|
173
|
+
// anything below this is treated as a truncated/empty turn and retried.
|
|
174
|
+
const MIN_FINISHED_ANSWER_CHARS = 4;
|
|
175
|
+
// How many chars of RESPONSE text the body carries, or -1 when there is NO
|
|
176
|
+
// RESPONSE fragment at all. The fragment is SSE/JSON-encoded, so the content
|
|
177
|
+
// may contain `\"` escapes AND raw `"` inside the JSON (the capture is the RAW
|
|
178
|
+
// body — `{"type":"RESPONSE","content":"{"tool": ...` is a real shape). A naive
|
|
179
|
+
// `[^"]*` therefore stops at the first escaped quote and MIS-reads a long
|
|
180
|
+
// answer as one char. Reuse the real extractor instead of a regex: it decodes
|
|
181
|
+
// the content properly, so the length is exact (we only compare it to a small
|
|
182
|
+
// threshold; the answer text itself comes from extractAnswer()).
|
|
183
|
+
function responseContentLength(body) {
|
|
184
|
+
const b = String(body || '');
|
|
185
|
+
if (!/"type":"RESPONSE"/.test(b))
|
|
186
|
+
return -1;
|
|
187
|
+
return extractFromSse(b).length;
|
|
188
|
+
}
|
|
163
189
|
// True when the SSE body shows a turn that ENDED (`quasi_status: FINISHED`)
|
|
164
|
-
// but produced NO answer text — the reasoning (THINK fragment) was
|
|
165
|
-
// and then the model stopped WITHOUT writing a RESPONSE fragment
|
|
190
|
+
// but produced NO real answer text — the reasoning (THINK fragment) was
|
|
191
|
+
// generated and then the model stopped WITHOUT writing a RESPONSE fragment,
|
|
192
|
+
// OR the RESPONSE fragment is a truncated head (a lone `{`, a couple of
|
|
193
|
+
// chars).
|
|
166
194
|
//
|
|
167
195
|
// This is the "Stopped + Continue button" case: DeepSeek finishes the turn
|
|
168
196
|
// with only reasoning, the UI shows a Continue button, and extractAnswer()
|
|
169
|
-
// returns ''
|
|
170
|
-
//
|
|
171
|
-
//
|
|
197
|
+
// returns '' or a fragment. The old code saw a non-empty capture (the `{`),
|
|
198
|
+
// treated the turn as answered, and the finish loop waited out the whole
|
|
199
|
+
// askDeadlineMs — to the operator the agent "hung on Stopped".
|
|
172
200
|
// Unlike generation_err, `quasi_status` here is FINISHED, so
|
|
173
201
|
// isGenerationIncompleteText() does NOT catch it.
|
|
174
202
|
//
|
|
@@ -179,11 +207,12 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
179
207
|
const finished = FINISHED_STATUS_RE.test(b);
|
|
180
208
|
if (!finished)
|
|
181
209
|
return false;
|
|
182
|
-
// A RESPONSE fragment with
|
|
183
|
-
|
|
210
|
+
// A RESPONSE fragment with real content means the model DID answer.
|
|
211
|
+
const len = responseContentLength(b);
|
|
212
|
+
if (len >= MIN_FINISHED_ANSWER_CHARS)
|
|
184
213
|
return false;
|
|
185
|
-
//
|
|
186
|
-
// absence of
|
|
214
|
+
// No RESPONSE fragment at all (len === -1) or only a truncated head:
|
|
215
|
+
// treat the absence of a real answer as "no answer produced".
|
|
187
216
|
return true;
|
|
188
217
|
}
|
|
189
218
|
/**
|
|
@@ -202,7 +231,10 @@ export function isFinishedWithAnswer(body) {
|
|
|
202
231
|
return false;
|
|
203
232
|
if (!FINISHED_STATUS_RE.test(b))
|
|
204
233
|
return false;
|
|
205
|
-
|
|
234
|
+
// A truncated RESPONSE head (a lone `{`) is NOT a complete answer: the
|
|
235
|
+
// finish loop would return it as the model's text and the tool-call parser
|
|
236
|
+
// would fail on it. Require real content (see MIN_FINISHED_ANSWER_CHARS).
|
|
237
|
+
return responseContentLength(b) >= MIN_FINISHED_ANSWER_CHARS;
|
|
206
238
|
}
|
|
207
239
|
// The CONTEXT size (in tokens) DeepSeek reports for the current answer.
|
|
208
240
|
//
|
package/dist/provider-handoff.js
CHANGED
package/dist/qwen-ui.js
CHANGED
|
@@ -21,9 +21,6 @@ export const CHAT_ID_RE = /\/c\/([A-Za-z0-9_-]+)/;
|
|
|
21
21
|
export function chatUrl(id) {
|
|
22
22
|
return CHAT_URL + 'c/' + id;
|
|
23
23
|
}
|
|
24
|
-
// The two-step API. `NEW_CHAT_URL` creates a chat and returns its id;
|
|
25
|
-
// `COMPLETIONS_URL(id)` streams the answer.
|
|
26
|
-
export const NEW_CHAT_URL = 'https://chat.qwen.ai/api/v2/chats/new';
|
|
27
24
|
export const MODELS_URL = 'https://chat.qwen.ai/api/v2/models/';
|
|
28
25
|
// The chat list, newest first, EXCLUDING project chats (verified live on a
|
|
29
26
|
// signed-in session). Used by listChats() instead of scraping the sidebar.
|
|
@@ -36,10 +33,6 @@ export function chatHistoryUrl(chatId, limit = 50) {
|
|
|
36
33
|
'?direction=up&limit=' +
|
|
37
34
|
String(limit));
|
|
38
35
|
}
|
|
39
|
-
export function completionsUrl(chatId) {
|
|
40
|
-
return ('https://chat.qwen.ai/api/v2/chat/completions?chat_id=' +
|
|
41
|
-
encodeURIComponent(chatId));
|
|
42
|
-
}
|
|
43
36
|
// The completions SSE endpoint carries the real answer; used to gate the
|
|
44
37
|
// network capture so an unrelated JSON endpoint never clobbers it.
|
|
45
38
|
export const ANSWER_URL_RE = /\/api\/v2\/chat\/completions/i;
|
|
@@ -164,7 +157,6 @@ export const ATTACH_SELECTORS = ['input#filesUpload', 'input[type="file"]'];
|
|
|
164
157
|
// ---------- login ----------
|
|
165
158
|
// Qwen's sign-in is a SEPARATE origin (auth.qwen.ai) opened from the header
|
|
166
159
|
// "Log in" button. Email / Google / GitHub are accepted; no phone required.
|
|
167
|
-
export const LOGIN_BUTTON_RE = /^\s*(log ?in|sign ?in|войти)\s*$/i;
|
|
168
160
|
export const PASSWORD_SELECTORS = [
|
|
169
161
|
'input[type="password"]',
|
|
170
162
|
'input[autocomplete="current-password"]',
|