zames_pro 2.73.11 → 2.75.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 +48 -0
- package/README.md +37 -7
- package/dist/attachments.js +6 -1
- package/dist/banner.js +71 -0
- package/dist/browser.js +84 -27
- package/dist/code-blocks.js +48 -0
- package/dist/commands.js +1 -0
- package/dist/config-live.js +53 -0
- package/dist/config.js +38 -0
- package/dist/goal.js +81 -0
- package/dist/i18n/common.js +35 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +30 -2
- package/dist/i18n/qwen.js +4 -0
- package/dist/i18n/self-review.js +8 -0
- package/dist/i18n/spinner.js +6 -0
- package/dist/index.js +487 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +29 -1
- package/dist/input.js +14 -3
- package/dist/net-capture.js +18 -0
- package/dist/qwen-browser.js +43 -20
- package/dist/run-task.js +97 -4
- package/dist/theme.js +16 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +337 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +113 -0
- package/dist/tui/InputLine.js +318 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +54 -0
- package/dist/tui/StatusBar.js +170 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +29 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +317 -0
- package/dist/tui/keys.js +195 -0
- package/dist/tui/log-window.js +93 -0
- package/dist/tui/login-pure.js +20 -0
- package/dist/tui/run-config-menu.js +19 -0
- package/dist/tui/run-confirm.js +22 -0
- package/dist/tui/run-login.js +43 -0
- package/dist/tui/run-select.js +18 -0
- package/dist/tui/store.js +104 -0
- package/dist/tui/types.js +10 -0
- package/dist/tui/useMouseWheel.js +164 -0
- package/dist/tui/widgets.js +60 -0
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/inline-status-driver.mts +31 -0
- package/scripts/inline-status.tsx +33 -0
- package/scripts/mouse-churn.mts +34 -0
- package/scripts/qwen-models-smoke.mts +117 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-comp-shot.mts +63 -0
- package/scripts/tui-components.tsx +62 -0
- package/scripts/tui-config-driver.mts +82 -0
- package/scripts/tui-config-probe.tsx +74 -0
- package/scripts/tui-edit-driver.mts +100 -0
- package/scripts/tui-edit-probe.tsx +38 -0
- package/scripts/tui-editor-probe.tsx +24 -0
- package/scripts/tui-input-driver.mts +54 -0
- package/scripts/tui-input-shot.tsx +59 -0
- package/scripts/tui-log-driver.mts +35 -0
- package/scripts/tui-log-shot.tsx +48 -0
- package/scripts/tui-longpaste-driver.mts +56 -0
- package/scripts/tui-longpaste.tsx +35 -0
- package/scripts/tui-manyselect-shot.mts +34 -0
- package/scripts/tui-manyselect.tsx +17 -0
- package/scripts/tui-mouse-churn-probe.tsx +28 -0
- package/scripts/tui-paste-driver.mts +88 -0
- package/scripts/tui-paste-kinds.tsx +76 -0
- package/scripts/tui-pastetest.mts +38 -0
- package/scripts/tui-probe.tsx +62 -0
- package/scripts/tui-repro.tsx +64 -0
- package/scripts/tui-resize.mts +70 -0
- package/scripts/tui-scroll-driver.mts +53 -0
- package/scripts/tui-scroll-probe.tsx +46 -0
- package/scripts/tui-shot.mts +106 -0
- package/scripts/tui-status-driver.mts +24 -0
- package/scripts/tui-status-shot.tsx +27 -0
- package/scripts/tui-submit.tsx +57 -0
- package/scripts/tui-wheel-driver.mts +61 -0
- package/scripts/tui-wheel-probe.tsx +48 -0
- package/scripts/tui-widgets-driver.mts +20 -0
- package/scripts/tui-widgets.tsx +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.75.0] - 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **tui:** show a one-time hint about mouse selection
|
|
15
|
+
- **tui:** Home/End jump to the top/bottom of the dialog
|
|
16
|
+
- **tui:** mouse wheel scrolls the dialog by default
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **tui:** the status row keeps its height (inline caret on the input row)
|
|
21
|
+
- **tui:** the status bar never wraps to a second line
|
|
22
|
+
- **tui:** the caret lands correctly in a CJK buffer
|
|
23
|
+
- **tui:** responsive dividers and wrapping-aware menu window
|
|
24
|
+
- **tui:** log scroll reads the LIVE offset (no stale closure)
|
|
25
|
+
- **tui:** Ctrl+Delete works (kitty CSI-u) and deletes left at end of line
|
|
26
|
+
- **tui:** the mouse wheel no longer scrolls the input history (for good)
|
|
27
|
+
- **attachments:** keep the items array identity across reset()
|
|
28
|
+
- **tui:** the config menu now redraws on change and fits the terminal
|
|
29
|
+
- **tui:** reserve the real number of rows for a wrapping suggest popup
|
|
30
|
+
- **tui:** exact badge width (T0.7) and honor NO_COLOR (T0.11)
|
|
31
|
+
- **tui:** deliver the FULL pasted text to the agent, not the [Pasted lines#N] marker
|
|
32
|
+
- **tui:** separate SELECT/SCROLL mouse modes and stop the header wrapping the panel
|
|
33
|
+
|
|
34
|
+
## [2.74.0] - 2026-10-13
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **tui:** interactive /chats and /sessions list menus
|
|
39
|
+
- **tui:** task progress bar and tool-result outcome icons (TUI-12)
|
|
40
|
+
- **tui:** in-TUI login and one-time-code forms (TUI-10)
|
|
41
|
+
- **tui:** copy code blocks (Ctrl+Y / /copy-code) and a CodeBlock widget
|
|
42
|
+
- **tui:** Ink-based terminal UI with full-screen mode, rich menus and mouse scrolling
|
|
43
|
+
- **goal:** completion-condition semantics with evaluator; make /config live
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **tui:** the mouse wheel now scrolls the DIALOG (not the input history)
|
|
48
|
+
- **tui:** window the full-screen log by rendered ROW, not by entry
|
|
49
|
+
- **tui:** split-safe wheel parser, empty-log hint, selection-aware suggest window
|
|
50
|
+
- **tui:** real visual-row input box height, Ctrl+J, and wheel-vs-history
|
|
51
|
+
- **tui:** multi-line input, growing input box, optional mouse scroll
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- **tui:** drop the task progress bar — keep just the "tasks: n/m" text
|
|
56
|
+
- **tui:** distinct color and a divider for operator messages
|
|
57
|
+
|
|
10
58
|
## [2.73.11] - 2026-10-12
|
|
11
59
|
|
|
12
60
|
### Fixed
|
package/README.md
CHANGED
|
@@ -255,6 +255,18 @@ The prompt is a small line editor with persistent history:
|
|
|
255
255
|
Claude Code's bash mode). The same sandbox guard as the `Bash` tool applies,
|
|
256
256
|
so a direct command cannot leave the project either.
|
|
257
257
|
|
|
258
|
+
Full-screen mode adds dialog navigation, so a long answer can be read and
|
|
259
|
+
selected without leaving the terminal:
|
|
260
|
+
|
|
261
|
+
- mouse wheel — scroll the dialog (never the input history; history is arrow
|
|
262
|
+
keys only). Selecting text needs `Shift`+drag, the standard TUI convention.
|
|
263
|
+
- `PgUp` / `PgDn` — scroll the dialog half a screen.
|
|
264
|
+
- `Home` / `End` — jump to the very top / bottom of the dialog (only while the
|
|
265
|
+
input line is empty; with text typed they move the caret as usual).
|
|
266
|
+
- To copy a LARGE block, use `/export` (writes the whole session as Markdown)
|
|
267
|
+
or the transcript file (`/transcript` shows the path) — the terminal's own
|
|
268
|
+
drag-select cannot reach past one screen.
|
|
269
|
+
|
|
258
270
|
Set `NO_COLOR=1` to disable colors (a calm default palette is used otherwise).
|
|
259
271
|
|
|
260
272
|
### Images and files
|
|
@@ -375,9 +387,12 @@ Codex CLI:
|
|
|
375
387
|
- /compact — ask the active backend to compress the current chat into a handover
|
|
376
388
|
summary, then open a NEW chat, resend the system prompt and post the summary
|
|
377
389
|
as the carried-over context. Use it when the context gets long.
|
|
378
|
-
- /goal
|
|
379
|
-
|
|
380
|
-
|
|
390
|
+
- /goal <condition> — set a **completion condition** (Claude Code parity). The
|
|
391
|
+
agent starts working toward it at once and keeps starting new turns until a
|
|
392
|
+
separate evaluator model judges the condition met. The goal clears itself
|
|
393
|
+
automatically when it is met (or judged impossible), or with `/goal clear`.
|
|
394
|
+
The condition is stored in `<project>/.zames-goal` (git-ignored) and restored
|
|
395
|
+
on the next launch. `/goal` with no argument shows the current state.
|
|
381
396
|
- /loop <interval> <task> — repeat a task periodically (e.g. `/loop 10m run the
|
|
382
397
|
tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> — run on a schedule.
|
|
383
398
|
/jobs [rm <id>|clear] lists and stops them. A fired job is put into the same
|
|
@@ -820,6 +835,18 @@ zames
|
|
|
820
835
|
Claude Code). Действует та же песочница, что и для инструмента `Bash`, так
|
|
821
836
|
что прямая команда тоже не может выйти за пределы проекта.
|
|
822
837
|
|
|
838
|
+
В полноэкранном режиме добавляется навигация по диалогу, чтобы длинный ответ
|
|
839
|
+
можно было прочитать и выделить не покидая терминал:
|
|
840
|
+
|
|
841
|
+
- колесо мыши — скролл диалога (историю ввода колесо НЕ листает; история —
|
|
842
|
+
только стрелки). Выделение текста — `Shift`+протяжка, стандарт TUI.
|
|
843
|
+
- `PgUp` / `PgDn` — листать диалог на пол-экрана.
|
|
844
|
+
- `Home` / `End` — прыжок в самый верх / низ диалога (только когда строка ввода
|
|
845
|
+
пуста; при набранном тексте они двигают курсор, как обычно).
|
|
846
|
+
- Чтобы скопировать БОЛЬШОЙ кусок, используйте `/export` (пишет всю сессию в
|
|
847
|
+
Markdown) или файл транскрипта (`/transcript` покажет путь) — собственным
|
|
848
|
+
выделением терминала за пределы одного экрана не выделить.
|
|
849
|
+
|
|
823
850
|
Установите `NO_COLOR=1`, чтобы отключить цвета (иначе используется спокойная
|
|
824
851
|
палитра по умолчанию).
|
|
825
852
|
|
|
@@ -942,10 +969,13 @@ zames --task "..." --output-format jsonl \
|
|
|
942
969
|
- /compact — попросить активный бэкенд сжать текущий чат в передаточное резюме,
|
|
943
970
|
затем открыть НОВЫЙ чат, заново отправить системный промпт и положить резюме как
|
|
944
971
|
перенесённый контекст. Используйте, когда контекст разрастается.
|
|
945
|
-
- /goal
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
972
|
+
- /goal <условие> — задать **условие завершения** (как в Claude Code). Агент
|
|
973
|
+
сразу начинает работать над ним и продолжает запускать новые ходы, пока
|
|
974
|
+
отдельная модель-оценщик не признает условие выполненным. Цель снимается
|
|
975
|
+
автоматически при достижении (или если оценщик признал её недостижимой),
|
|
976
|
+
либо командой `/goal clear`. Условие хранится в `<project>/.zames-goal`
|
|
977
|
+
(git-ignored) и восстанавливается при следующем запуске. `/goal` без
|
|
978
|
+
аргумента показывает текущее состояние.
|
|
949
979
|
- /loop <interval> <task> — повторять задачу периодически (например,
|
|
950
980
|
`/loop 10m run the tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> —
|
|
951
981
|
запуск по расписанию. /jobs [rm <id>|clear] перечисляет и останавливает их.
|
package/dist/attachments.js
CHANGED
|
@@ -141,7 +141,12 @@ export class AttachmentStore {
|
|
|
141
141
|
imageCount = 0;
|
|
142
142
|
fileCount = 0;
|
|
143
143
|
reset() {
|
|
144
|
-
this.items
|
|
144
|
+
// Empty IN PLACE: the Ink tree captures `this.items` by reference at mount
|
|
145
|
+
// (InputLine's `attachments` prop). Reassigning to a new array left the
|
|
146
|
+
// tree holding the OLD one, so after the first submit every new attachment
|
|
147
|
+
// was appended to an array the UI no longer read — `onSubmit` got `[]` and
|
|
148
|
+
// the pasted image/file was silently dropped.
|
|
149
|
+
this.items.length = 0;
|
|
145
150
|
this.imageCount = 0;
|
|
146
151
|
this.fileCount = 0;
|
|
147
152
|
}
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The startup banner — a rounded box shown once at launch with the app name,
|
|
2
|
+
// version, working directory and active provider. Pure string building (no
|
|
3
|
+
// terminal access), so it is unit-testable and usable by both the TUI and the
|
|
4
|
+
// non-TTY path.
|
|
5
|
+
import { theme } from './theme.js';
|
|
6
|
+
import { visLen } from './input/layout.js';
|
|
7
|
+
/** Visible width of a string (ANSI stripped, wide chars counted as 2). */
|
|
8
|
+
function width(s) {
|
|
9
|
+
return visLen(s.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ''));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Draw a rounded box around the given content lines. The box width is the
|
|
13
|
+
* widest line (clamped to `maxWidth`), padded by one space each side. Lines
|
|
14
|
+
* longer than the inner width are truncated with an ellipsis.
|
|
15
|
+
*/
|
|
16
|
+
function box(lines, maxWidth, border = '#c8b06a') {
|
|
17
|
+
const inner = Math.max(10, Math.min(maxWidth - 4, Math.max(...lines.map(width)) + 2));
|
|
18
|
+
const top = theme.prompt('╭' + '─'.repeat(inner) + '╮');
|
|
19
|
+
const bottom = theme.prompt('╰' + '─'.repeat(inner) + '╯');
|
|
20
|
+
const body = lines.map((l) => {
|
|
21
|
+
// Truncate to the inner width so a long path never breaks the frame.
|
|
22
|
+
const maxContent = Math.max(0, inner - 2);
|
|
23
|
+
const clipped = clip(l, maxContent);
|
|
24
|
+
const w = width(clipped);
|
|
25
|
+
const pad = Math.max(0, inner - 1 - w);
|
|
26
|
+
return (theme.prompt('│') + ' ' + clipped + ' '.repeat(pad) + theme.prompt('│'));
|
|
27
|
+
});
|
|
28
|
+
void border;
|
|
29
|
+
return [top, ...body, bottom].join('\n');
|
|
30
|
+
}
|
|
31
|
+
/** Truncate an (ANSI-carrying) string to `max` visible columns. */
|
|
32
|
+
function clip(s, max) {
|
|
33
|
+
if (width(s) <= max)
|
|
34
|
+
return s;
|
|
35
|
+
// The banner content has at most a couple of ANSI runs; drop the tail by
|
|
36
|
+
// visible width while keeping escape sequences intact.
|
|
37
|
+
let out = '';
|
|
38
|
+
let visible = 0;
|
|
39
|
+
let i = 0;
|
|
40
|
+
const esc = /\x1b\[[0-9;?]*[A-Za-z]/y;
|
|
41
|
+
while (i < s.length && visible < max - 1) {
|
|
42
|
+
esc.lastIndex = i;
|
|
43
|
+
const m = esc.exec(s);
|
|
44
|
+
if (m && m.index === i) {
|
|
45
|
+
out += m[0];
|
|
46
|
+
i += m[0].length;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const cp = s.codePointAt(i);
|
|
50
|
+
const ch = String.fromCodePoint(cp);
|
|
51
|
+
visible += cp >= 0x1100 ? 2 : 1;
|
|
52
|
+
if (visible > max - 1)
|
|
53
|
+
break;
|
|
54
|
+
out += ch;
|
|
55
|
+
i += ch.length;
|
|
56
|
+
}
|
|
57
|
+
return out + '…';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The full startup banner string. `contentWidth` is the target outer width
|
|
61
|
+
* (the terminal width minus a safety column).
|
|
62
|
+
*/
|
|
63
|
+
export function renderStartupBanner(info, contentWidth) {
|
|
64
|
+
const title = theme.assistant(' ' + info.title) + theme.dim(' v' + info.version);
|
|
65
|
+
const dir = theme.system(' cwd: ') + theme.dir(info.workdir);
|
|
66
|
+
const prov = theme.system(' provider: ') + theme.toggleOn(info.provider);
|
|
67
|
+
const lines = [title, dir, prov];
|
|
68
|
+
if (info.hint)
|
|
69
|
+
lines.push(theme.dim(' ' + info.hint));
|
|
70
|
+
return box(lines, contentWidth);
|
|
71
|
+
}
|
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chromium, } from 'playwright';
|
|
2
|
-
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, } from './net-capture.js';
|
|
2
|
+
import { extractAnswer, extractTokenUsage, dumpNetBody, isFinishedWithoutAnswer, isFinishedWithAnswer, } from './net-capture.js';
|
|
3
3
|
import { rewriteFailedAttachments } from './commands.js';
|
|
4
4
|
import { awaitUploadPreview, readAttachBuffers } from './attach-flow.js';
|
|
5
5
|
import { parseHistoryMessages, extractChatMessages, scrapeChatList, scrapeChatMessages, } from './browser-chats.js';
|
|
@@ -64,6 +64,9 @@ export class DeepSeekBrowser {
|
|
|
64
64
|
// Called after a successful interactive login so index.ts can persist the
|
|
65
65
|
// credentials into the user config (the browser does not write config).
|
|
66
66
|
onAuthSave;
|
|
67
|
+
// TUI-injected interactive prompts; null falls back to readline/askPassword.
|
|
68
|
+
onLoginPrompt;
|
|
69
|
+
onMfaPrompt;
|
|
67
70
|
_lastSentAt;
|
|
68
71
|
// Timestamp of the last Continue click. Clicks send a `chat/continue`
|
|
69
72
|
// request and used to bypass `_waitForSendSlot()` entirely — a run that
|
|
@@ -158,7 +161,7 @@ export class DeepSeekBrowser {
|
|
|
158
161
|
// must be stripped (DeepSeek's CDN answers 403 to it); computed in the
|
|
159
162
|
// constructor, overridable by an explicit UA in the options/config.
|
|
160
163
|
userAgent;
|
|
161
|
-
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
|
|
164
|
+
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, thinkingExtraMs = 2000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, autoContinue = true, continueMinGapMs = 1500, deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, onMfaPrompt, userAgent, } = {}) {
|
|
162
165
|
this.headless = headless;
|
|
163
166
|
this.debug = debug;
|
|
164
167
|
this.channel = channel;
|
|
@@ -185,6 +188,8 @@ export class DeepSeekBrowser {
|
|
|
185
188
|
};
|
|
186
189
|
this.locale = locale || DEFAULT_LOCALE;
|
|
187
190
|
this.onAuthSave = onAuthSave || null;
|
|
191
|
+
this.onLoginPrompt = onLoginPrompt || null;
|
|
192
|
+
this.onMfaPrompt = onMfaPrompt || null;
|
|
188
193
|
this._lastSentAt = 0;
|
|
189
194
|
this._lastContinueAt = 0;
|
|
190
195
|
this._abort = false;
|
|
@@ -593,25 +598,41 @@ export class DeepSeekBrowser {
|
|
|
593
598
|
return false;
|
|
594
599
|
}
|
|
595
600
|
console.log('\n' + theme.warn(this._t('auth.need_login')));
|
|
596
|
-
// Username via a normal readline question (echoed).
|
|
597
601
|
let user = this.auth.username;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
602
|
+
let password = this.auth.password;
|
|
603
|
+
if (this.onLoginPrompt) {
|
|
604
|
+
// TUI path: ONE Ink form for username + masked password (a readline
|
|
605
|
+
// question would pause the whole TUI and fight Ink for stdin).
|
|
606
|
+
const res = await this.onLoginPrompt({
|
|
607
|
+
savedUsername: this.auth.username,
|
|
608
|
+
hasSavedPassword: !!this.auth.password,
|
|
604
609
|
});
|
|
605
|
-
|
|
606
|
-
|
|
610
|
+
if (!res)
|
|
611
|
+
return false;
|
|
612
|
+
user = res.username.trim();
|
|
613
|
+
password = res.password;
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
// Username via a normal readline question (echoed).
|
|
617
|
+
if (!user) {
|
|
618
|
+
const readline = await import('readline');
|
|
619
|
+
const rl = readline.createInterface({
|
|
620
|
+
input: process.stdin,
|
|
621
|
+
output: process.stdout,
|
|
622
|
+
terminal: true,
|
|
623
|
+
});
|
|
624
|
+
user = (await new Promise((resolve) => rl.question(this._t('auth.prompt_login'), (a) => resolve(a)))).trim();
|
|
625
|
+
rl.close();
|
|
626
|
+
}
|
|
627
|
+
// Password via raw keystroke reading, echoed as `*` (readline would echo
|
|
628
|
+
// it in clear or swallow the prompt — see askPassword).
|
|
629
|
+
const passPromptKey = hasSaved
|
|
630
|
+
? 'auth.prompt_password_saved'
|
|
631
|
+
: 'auth.prompt_password';
|
|
632
|
+
const pass = await askPassword(this._t(passPromptKey));
|
|
633
|
+
if (pass)
|
|
634
|
+
password = pass;
|
|
607
635
|
}
|
|
608
|
-
// Password via raw keystroke reading, echoed as `*` (readline would echo
|
|
609
|
-
// it in clear or swallow the prompt — see askPassword).
|
|
610
|
-
const passPromptKey = hasSaved
|
|
611
|
-
? 'auth.prompt_password_saved'
|
|
612
|
-
: 'auth.prompt_password';
|
|
613
|
-
const pass = await askPassword(this._t(passPromptKey));
|
|
614
|
-
const password = pass || this.auth.password;
|
|
615
636
|
if (!user || !password)
|
|
616
637
|
return false;
|
|
617
638
|
console.log(theme.system(this._t('auth.auto_login')));
|
|
@@ -817,15 +838,22 @@ export class DeepSeekBrowser {
|
|
|
817
838
|
v: account || '—',
|
|
818
839
|
})));
|
|
819
840
|
const readline = await import('readline');
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
.
|
|
827
|
-
|
|
828
|
-
|
|
841
|
+
let code;
|
|
842
|
+
if (this.onMfaPrompt) {
|
|
843
|
+
// TUI path: the Ink form reads the code without pausing the whole UI.
|
|
844
|
+
code = ((await this.onMfaPrompt()) ?? '').trim().replace(/\s+/g, '');
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
const rl = readline.createInterface({
|
|
848
|
+
input: process.stdin,
|
|
849
|
+
output: process.stdout,
|
|
850
|
+
terminal: true,
|
|
851
|
+
});
|
|
852
|
+
code = (await new Promise((resolve) => rl.question(this._t('auth.mfa_prompt'), (a) => resolve(a))))
|
|
853
|
+
.trim()
|
|
854
|
+
.replace(/\s+/g, '');
|
|
855
|
+
rl.close();
|
|
856
|
+
}
|
|
829
857
|
if (!code)
|
|
830
858
|
return false;
|
|
831
859
|
await this._setInputText(codeInput, code);
|
|
@@ -2006,6 +2034,9 @@ export class DeepSeekBrowser {
|
|
|
2006
2034
|
const deadline = Date.now() + timeout;
|
|
2007
2035
|
let last = '';
|
|
2008
2036
|
let stable = 0;
|
|
2037
|
+
// Ticks since the SSE body reported FINISHED-with-answer while the DOM
|
|
2038
|
+
// heuristics kept the loop waiting (see the safety net below).
|
|
2039
|
+
let netFinishedTicks = 0;
|
|
2009
2040
|
while (Date.now() < deadline) {
|
|
2010
2041
|
if (this._abort || this._askCancelled) {
|
|
2011
2042
|
return last || '(прервано пользователем)';
|
|
@@ -2089,6 +2120,32 @@ export class DeepSeekBrowser {
|
|
|
2089
2120
|
// tool call" hang. If generation has clearly ENDED (no Stop button) and
|
|
2090
2121
|
// the text is stable, accept it (even when it repeats the previous one).
|
|
2091
2122
|
const sameAsBefore = !!cur && !isNew && normText(cur) === normText(beforeText);
|
|
2123
|
+
// NETWORK-AUTHORITATIVE SAFETY NET. The DOM heuristics above
|
|
2124
|
+
// (`isNew`/`sameAsBefore`/`_isGenerating`) can all be wrong at once on a
|
|
2125
|
+
// resumed chat: the DOM may still show the PREVIOUS answer, the Stop
|
|
2126
|
+
// button may linger, and `beforeText` may equal the fresh answer. The
|
|
2127
|
+
// finish loop then waits out the whole askDeadlineMs while the spinner
|
|
2128
|
+
// runs — the operator sees "generation is running but nothing happens"
|
|
2129
|
+
// (reported from the live agent). The SSE body is the ground truth: if it
|
|
2130
|
+
// says FINISHED **with** a RESPONSE fragment, the turn is over and the
|
|
2131
|
+
// captured answer is complete. Return it after a short grace period so a
|
|
2132
|
+
// genuinely still-streaming answer is never cut (the capture keeps
|
|
2133
|
+
// growing until FINISHED).
|
|
2134
|
+
const netFinishedWithAnswer = !!this._netCapture &&
|
|
2135
|
+
this._netCaptureAt >= this._lastSentAt &&
|
|
2136
|
+
isFinishedWithAnswer(this._netBody);
|
|
2137
|
+
if (netFinishedWithAnswer) {
|
|
2138
|
+
netFinishedTicks++;
|
|
2139
|
+
// Two ticks (~2 x stabilityDelayMs) is enough for the final DOM/capture
|
|
2140
|
+
// settle without risking a truncated answer.
|
|
2141
|
+
if (netFinishedTicks >= 2) {
|
|
2142
|
+
this._askDebug('RETURN net-finished curLen=' + this._netCapture.length);
|
|
2143
|
+
return this._cleanAnswer(this._netCapture) || cur;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
else {
|
|
2147
|
+
netFinishedTicks = 0;
|
|
2148
|
+
}
|
|
2092
2149
|
if (isNew && cur === last) {
|
|
2093
2150
|
stable++;
|
|
2094
2151
|
// `stabilityChecks` / `stabilityDelayMs` are the real knobs here.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// PURE extraction of fenced code blocks from a Markdown answer.
|
|
2
|
+
//
|
|
3
|
+
// The TUI cannot offer a real "copy button", so copying a code block is bound
|
|
4
|
+
// to a key / command. That needs the RAW fenced text (the highlighted render is
|
|
5
|
+
// full of ANSI and must NOT be copied), which is what this module returns.
|
|
6
|
+
// Kept pure so it is unit-tested without a terminal.
|
|
7
|
+
/**
|
|
8
|
+
* Extract every fenced code block (``` or ~~~) from `md`, in order. Handles a
|
|
9
|
+
* longer closing fence than the opening one and an unterminated final block
|
|
10
|
+
* (the answer was truncated) — a half block is still copyable.
|
|
11
|
+
*/
|
|
12
|
+
export function extractCodeBlocks(md) {
|
|
13
|
+
const lines = String(md ?? '').split(String.fromCharCode(10));
|
|
14
|
+
const out = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
let n = 0;
|
|
17
|
+
while (i < lines.length) {
|
|
18
|
+
const m = /^(\s*)(`{3,}|~{3,})\s*([^`]*)$/.exec(lines[i]);
|
|
19
|
+
if (!m) {
|
|
20
|
+
i++;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const fence = m[2];
|
|
24
|
+
const marker = fence[0];
|
|
25
|
+
const minLen = fence.length;
|
|
26
|
+
const lang = (m[3] || '').trim();
|
|
27
|
+
i++;
|
|
28
|
+
const body = [];
|
|
29
|
+
let closed = false;
|
|
30
|
+
while (i < lines.length) {
|
|
31
|
+
// A closing fence is the SAME marker, at least as long, with nothing
|
|
32
|
+
// else on the line (whitespace allowed).
|
|
33
|
+
const c = new RegExp('^\\s*' + marker + '{' + minLen + ',}\\s*$');
|
|
34
|
+
if (c.test(lines[i])) {
|
|
35
|
+
closed = true;
|
|
36
|
+
i++;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
body.push(lines[i]);
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
if (body.length === 0 && closed)
|
|
43
|
+
continue;
|
|
44
|
+
n++;
|
|
45
|
+
out.push({ lang, text: body.join(String.fromCharCode(10)), index: n });
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -988,6 +988,7 @@ export const SLASH_COMMANDS = [
|
|
|
988
988
|
{ name: '/rename', key: 'help.cmd.rename' },
|
|
989
989
|
{ name: '/context', key: 'help.cmd.context' },
|
|
990
990
|
{ name: '/copy', key: 'help.cmd.copy' },
|
|
991
|
+
{ name: '/copy-code', key: 'help.cmd.copy_code' },
|
|
991
992
|
{ name: '/chat', key: 'help.cmd.chat' },
|
|
992
993
|
{ name: '/cd', key: 'help.cmd.cd' },
|
|
993
994
|
{ name: '/pwd', key: 'help.cmd.pwd' },
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Live application of /config changes to the objects built ONCE at
|
|
2
|
+
// startup (the browser, the undo/checkpoint stores). setConfigRuntime only
|
|
3
|
+
// mutates the config object; these objects COPIED their values at
|
|
4
|
+
// construction, so a change must be pushed onto the live instance too or it
|
|
5
|
+
// is silently a no-op until a restart. The path->target mapping is PURE and
|
|
6
|
+
// unit-tested here; index.ts wires it to the live instances.
|
|
7
|
+
/** config path -> field name on the browser instance. Both backends
|
|
8
|
+
* share these names; a field that is absent on the active backend
|
|
9
|
+
* (e.g. DeepSeek-only thinkingExtraMs on Qwen) is skipped, not thrown. */
|
|
10
|
+
export const BROWSER_LIVE_CONFIG = {
|
|
11
|
+
'browser.answerTimeoutMs': 'answerTimeoutMs',
|
|
12
|
+
'browser.askRetries': 'askRetries',
|
|
13
|
+
'browser.stabilityChecks': 'stabilityChecks',
|
|
14
|
+
'browser.stabilityDelayMs': 'stabilityDelayMs',
|
|
15
|
+
'browser.minSendIntervalMs': 'minSendIntervalMs',
|
|
16
|
+
'browser.thinkingExtraMs': 'thinkingExtraMs',
|
|
17
|
+
'browser.rateLimitWaitMs': 'rateLimitWaitMs',
|
|
18
|
+
'browser.maxRateLimitRetries': 'maxRateLimitRetries',
|
|
19
|
+
'browser.maxIncompleteRetries': 'maxIncompleteRetries',
|
|
20
|
+
'browser.incompleteWaitMs': 'incompleteWaitMs',
|
|
21
|
+
'browser.autoContinue': 'autoContinue',
|
|
22
|
+
'browser.continueMinGapMs': 'continueMinGapMs',
|
|
23
|
+
'browser.askDeadlineMs': 'askDeadlineMs',
|
|
24
|
+
'browser.maxAfterToolRetries': 'maxAfterToolRetries',
|
|
25
|
+
};
|
|
26
|
+
/** Push a browser.* change onto the live instance. Returns true when a
|
|
27
|
+
* field was written (false = not a browser timing knob, or absent on this
|
|
28
|
+
* backend). */
|
|
29
|
+
export function applyBrowserLiveConfig(path, value, browser) {
|
|
30
|
+
const field = BROWSER_LIVE_CONFIG[path];
|
|
31
|
+
if (!field || !(field in browser))
|
|
32
|
+
return false;
|
|
33
|
+
browser[field] = value;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/** Push undo.* / checkpoint.* changes onto the live stores. The stores copy
|
|
37
|
+
* their config at construction, so a change must be written to the instance
|
|
38
|
+
* too. */
|
|
39
|
+
export function applyStoreLiveConfig(path, value, stores) {
|
|
40
|
+
if (path === 'undo.enabled' || path === 'undo.maxBackups') {
|
|
41
|
+
if (!stores.undo)
|
|
42
|
+
return false;
|
|
43
|
+
stores.undo[path === 'undo.enabled' ? 'enabled' : 'maxBackups'] = value;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (path === 'checkpoint.enabled' || path === 'checkpoint.maxBackups') {
|
|
47
|
+
if (!stores.checkpoints)
|
|
48
|
+
return false;
|
|
49
|
+
stores.checkpoints[path === 'checkpoint.enabled' ? 'enabled' : 'maxBackups'] = value;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -89,6 +89,26 @@ export const DEFAULTS = {
|
|
|
89
89
|
// N37 statusLine hook. Off by default; edit config.json to set a command.
|
|
90
90
|
statusLineCommand: '',
|
|
91
91
|
statusLineIntervalMs: 10_000,
|
|
92
|
+
// Use the Ink/React TUI for the input area (the rich editor). Set to false
|
|
93
|
+
// to fall back to the legacy hand-rolled LineEditor.
|
|
94
|
+
tui: true,
|
|
95
|
+
// Render the TUI in the terminal's alternate screen buffer (FULL-SCREEN
|
|
96
|
+
// mode, like Codex / Claude Code / opencode). This is the default because
|
|
97
|
+
// it is the only mode where a resize cannot leave ghost/duplicate lines:
|
|
98
|
+
// the whole screen belongs to the app and the terminal never reflows our
|
|
99
|
+
// output. The trade-off is that the terminal SCROLLBACK is not available
|
|
100
|
+
// (PgUp/PgDn scroll the in-app log instead). Set to false for the inline
|
|
101
|
+
// layout, which keeps the scrollback but is subject to reflow artifacts.
|
|
102
|
+
alternateScreen: true,
|
|
103
|
+
// In-app MOUSE-WHEEL scrolling of the log. ON by default: the operator
|
|
104
|
+
// asked that the wheel scroll the DIALOG (and it must NEVER scroll the
|
|
105
|
+
// input history — history is arrow keys only). That needs terminal mouse
|
|
106
|
+
// REPORTING (1000/1006) plus alternate-scroll OFF (1007l), so the wheel
|
|
107
|
+
// arrives as SGR events we consume and never as Up/Down arrows. The
|
|
108
|
+
// trade-off is that the terminal no longer does its own drag-select, so
|
|
109
|
+
// selecting text needs SHIFT+DRAG (the standard TUI convention). Set to
|
|
110
|
+
// false to give the terminal the mouse back (native select, wheel inert).
|
|
111
|
+
mouseScroll: true,
|
|
92
112
|
},
|
|
93
113
|
// Off by default: a full tsc/eslint run after every edit is slow; opt in.
|
|
94
114
|
diagnostics: { ...DEFAULT_DIAGNOSTICS },
|
|
@@ -162,6 +182,24 @@ export const CONFIG_SCHEMA = [
|
|
|
162
182
|
labelKey: 'cfg.f.ui_answerWidth',
|
|
163
183
|
groupKey: 'cfg.group.ui',
|
|
164
184
|
},
|
|
185
|
+
{
|
|
186
|
+
path: 'ui.tui',
|
|
187
|
+
type: 'boolean',
|
|
188
|
+
labelKey: 'cfg.f.ui_tui',
|
|
189
|
+
groupKey: 'cfg.group.ui',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
path: 'ui.alternateScreen',
|
|
193
|
+
type: 'boolean',
|
|
194
|
+
labelKey: 'cfg.f.ui_alternateScreen',
|
|
195
|
+
groupKey: 'cfg.group.ui',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
path: 'ui.mouseScroll',
|
|
199
|
+
type: 'boolean',
|
|
200
|
+
labelKey: 'cfg.f.ui_mouseScroll',
|
|
201
|
+
groupKey: 'cfg.group.ui',
|
|
202
|
+
},
|
|
165
203
|
{
|
|
166
204
|
path: 'headless',
|
|
167
205
|
type: 'boolean',
|
package/dist/goal.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Completion-goal semantics (Claude Code parity). A /goal sets a CONDITION
|
|
2
|
+
// that must be satisfied; after every turn a fresh model judges it (MET ->
|
|
3
|
+
// clear, IMPOSSIBLE -> clear, NOT_MET -> keep working). The PURE half -- the
|
|
4
|
+
// evaluator prompt and the verdict parser -- lives here so it is unit-tested
|
|
5
|
+
// without a browser. The `browser.ask()` call and the continuation loop live
|
|
6
|
+
// in run-task.ts.
|
|
7
|
+
/** Hard cap on the condition length, mirroring Claude Code's 4 000-char bound. */
|
|
8
|
+
export const GOAL_CONDITION_CAP = 4000;
|
|
9
|
+
/** Default cap on how many auto-continue turns a goal may spawn. */
|
|
10
|
+
export const GOAL_MAX_TURNS = 25;
|
|
11
|
+
/** Trim a goal condition to the cap. */
|
|
12
|
+
export function clampGoalCondition(condition) {
|
|
13
|
+
const g = String(condition ?? '').trim();
|
|
14
|
+
if (g.length <= GOAL_CONDITION_CAP)
|
|
15
|
+
return g;
|
|
16
|
+
return g.slice(0, GOAL_CONDITION_CAP);
|
|
17
|
+
}
|
|
18
|
+
// The evaluator prompt is AGENT-facing (English), like the system prompt and
|
|
19
|
+
// the compact summary prompt. It runs in the SAME chat as the work so the
|
|
20
|
+
// judge sees the actual transcript (command output, file state), which is
|
|
21
|
+
// what Claude Code's evaluator reads too. A clean, machine-parsable verdict
|
|
22
|
+
// is requested; no tools.
|
|
23
|
+
export function buildEvaluatorPrompt(condition) {
|
|
24
|
+
const NL = String.fromCharCode(10);
|
|
25
|
+
return ('You are a strict COMPLETION EVALUATOR. You do NOT do the work and you do NOT call tools. Read ' +
|
|
26
|
+
'the conversation above and judge - from what the agent has ALREADY surfaced - whether ' +
|
|
27
|
+
'the completion condition below is satisfied.' +
|
|
28
|
+
NL +
|
|
29
|
+
NL +
|
|
30
|
+
'Completion condition:' +
|
|
31
|
+
NL +
|
|
32
|
+
condition +
|
|
33
|
+
NL +
|
|
34
|
+
NL +
|
|
35
|
+
'Reply with EXACTLY two lines, nothing else:' +
|
|
36
|
+
NL +
|
|
37
|
+
'VERDICT: MET | NOT_MET | IMPOSSIBLE' +
|
|
38
|
+
NL +
|
|
39
|
+
'REASON: <one short sentence>' +
|
|
40
|
+
NL +
|
|
41
|
+
NL +
|
|
42
|
+
'Rules:' +
|
|
43
|
+
NL +
|
|
44
|
+
'- MET only if the condition is demonstrably satisfied: evidence appears in the conversation ' +
|
|
45
|
+
'(command output, test result, file state). A promise to do it later is NOT MET.' +
|
|
46
|
+
NL +
|
|
47
|
+
'- NOT_MET if more work is needed; state briefly what is missing.' +
|
|
48
|
+
NL +
|
|
49
|
+
'- IMPOSSIBLE only if the condition can never be satisfied as written.' +
|
|
50
|
+
NL +
|
|
51
|
+
'- Judge from the conversation only; do not run commands or read files independently.');
|
|
52
|
+
}
|
|
53
|
+
// The continuation directive injected as the next turn's task when the
|
|
54
|
+
// evaluator says NOT_MET. Agent-facing (English). The goal itself is prepended
|
|
55
|
+
// again by withGoal(), so this only adds the verdict + a continue instruction.
|
|
56
|
+
export function buildContinuationDirective(reason) {
|
|
57
|
+
const NL = String.fromCharCode(10);
|
|
58
|
+
return ('The completion goal is NOT yet met. Continue working toward it ' +
|
|
59
|
+
'until it is satisfied. Do not call respond with a final summary until the ' +
|
|
60
|
+
'condition is met.' +
|
|
61
|
+
NL +
|
|
62
|
+
(argumentReason(reason) ? 'Evaluator note: ' + reason : ''));
|
|
63
|
+
}
|
|
64
|
+
function argumentReason(reason) {
|
|
65
|
+
return String(reason ?? '').trim().length > 0;
|
|
66
|
+
}
|
|
67
|
+
// Parse the evaluator's reply into a verdict. Tolerant of markdown emphasis
|
|
68
|
+
// and casing (```VErdict: MET``). Returns null when the verdict line is
|
|
69
|
+
// missing, so the caller can fail closed (treat an unparsable answer as
|
|
70
|
+
// "continue", never as "done").
|
|
71
|
+
export function parseGoalVerdict(text) {
|
|
72
|
+
const t = String(text ?? '');
|
|
73
|
+
const m = /VERDICT:\s*(`{0,3})?\s*(MET|NOT_MET|IMPOSSIBLE)/i.exec(t);
|
|
74
|
+
if (!m)
|
|
75
|
+
return null;
|
|
76
|
+
const raw = m[2].toUpperCase();
|
|
77
|
+
const status = raw === 'MET' ? 'met' : raw === 'IMPOSSIBLE' ? 'impossible' : 'not_met';
|
|
78
|
+
const rm = /REASON:\s*(.+)/i.exec(t);
|
|
79
|
+
const reason = rm ? rm[1].replace(/`{2,}/g, '').trim() : '';
|
|
80
|
+
return { status, reason };
|
|
81
|
+
}
|