zames_pro 2.73.10 → 2.74.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 +30 -0
- package/README.md +13 -7
- 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 +31 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +22 -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 +478 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +13 -0
- 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 +7 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +304 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +85 -0
- package/dist/tui/InputLine.js +285 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +50 -0
- package/dist/tui/StatusBar.js +140 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +28 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +297 -0
- package/dist/tui/keys.js +162 -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 +113 -0
- package/dist/tui/widgets.js +55 -0
- package/package.json +6 -4
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-probe.tsx +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.74.0] - 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **tui:** interactive /chats and /sessions list menus
|
|
15
|
+
- **tui:** task progress bar and tool-result outcome icons (TUI-12)
|
|
16
|
+
- **tui:** in-TUI login and one-time-code forms (TUI-10)
|
|
17
|
+
- **tui:** copy code blocks (Ctrl+Y / /copy-code) and a CodeBlock widget
|
|
18
|
+
- **tui:** Ink-based terminal UI with full-screen mode, rich menus and mouse scrolling
|
|
19
|
+
- **goal:** completion-condition semantics with evaluator; make /config live
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **tui:** the mouse wheel now scrolls the DIALOG (not the input history)
|
|
24
|
+
- **tui:** window the full-screen log by rendered ROW, not by entry
|
|
25
|
+
- **tui:** split-safe wheel parser, empty-log hint, selection-aware suggest window
|
|
26
|
+
- **tui:** real visual-row input box height, Ctrl+J, and wheel-vs-history
|
|
27
|
+
- **tui:** multi-line input, growing input box, optional mouse scroll
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **tui:** drop the task progress bar — keep just the "tasks: n/m" text
|
|
32
|
+
- **tui:** distinct color and a divider for operator messages
|
|
33
|
+
|
|
34
|
+
## [2.73.11] - 2026-10-12
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- drop the duplicate render-how script; sync package-lock version
|
|
39
|
+
|
|
10
40
|
## [2.73.10] - 2026-10-12
|
|
11
41
|
|
|
12
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -375,9 +375,12 @@ Codex CLI:
|
|
|
375
375
|
- /compact — ask the active backend to compress the current chat into a handover
|
|
376
376
|
summary, then open a NEW chat, resend the system prompt and post the summary
|
|
377
377
|
as the carried-over context. Use it when the context gets long.
|
|
378
|
-
- /goal
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
- /goal <condition> — set a **completion condition** (Claude Code parity). The
|
|
379
|
+
agent starts working toward it at once and keeps starting new turns until a
|
|
380
|
+
separate evaluator model judges the condition met. The goal clears itself
|
|
381
|
+
automatically when it is met (or judged impossible), or with `/goal clear`.
|
|
382
|
+
The condition is stored in `<project>/.zames-goal` (git-ignored) and restored
|
|
383
|
+
on the next launch. `/goal` with no argument shows the current state.
|
|
381
384
|
- /loop <interval> <task> — repeat a task periodically (e.g. `/loop 10m run the
|
|
382
385
|
tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> — run on a schedule.
|
|
383
386
|
/jobs [rm <id>|clear] lists and stops them. A fired job is put into the same
|
|
@@ -942,10 +945,13 @@ zames --task "..." --output-format jsonl \
|
|
|
942
945
|
- /compact — попросить активный бэкенд сжать текущий чат в передаточное резюме,
|
|
943
946
|
затем открыть НОВЫЙ чат, заново отправить системный промпт и положить резюме как
|
|
944
947
|
перенесённый контекст. Используйте, когда контекст разрастается.
|
|
945
|
-
- /goal
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
948
|
+
- /goal <условие> — задать **условие завершения** (как в Claude Code). Агент
|
|
949
|
+
сразу начинает работать над ним и продолжает запускать новые ходы, пока
|
|
950
|
+
отдельная модель-оценщик не признает условие выполненным. Цель снимается
|
|
951
|
+
автоматически при достижении (или если оценщик признал её недостижимой),
|
|
952
|
+
либо командой `/goal clear`. Условие хранится в `<project>/.zames-goal`
|
|
953
|
+
(git-ignored) и восстанавливается при следующем запуске. `/goal` без
|
|
954
|
+
аргумента показывает текущее состояние.
|
|
949
955
|
- /loop <interval> <task> — повторять задачу периодически (например,
|
|
950
956
|
`/loop 10m run the tests`). /cron "<min> <hour> <dom> <month> <dow>" <task> —
|
|
951
957
|
запуск по расписанию. /jobs [rm <id>|clear] перечисляет и останавливает их.
|
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
|
+
// wants the wheel to scroll the DIALOG. That needs terminal mouse
|
|
105
|
+
// REPORTING (1000/1006), which in turn means drag-select needs Shift (a
|
|
106
|
+
// terminal convention: Shift+drag bypasses the app). With this OFF the
|
|
107
|
+
// app does not enable reporting, and in the alternate screen most
|
|
108
|
+
// terminals then turn the wheel into Up/Down ARROW keys (DECSET 1007),
|
|
109
|
+
// which the editor maps to HISTORY — the exact bug reported. So ON is the
|
|
110
|
+
// only setting where the wheel behaves.
|
|
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
|
+
}
|
package/dist/i18n/common.js
CHANGED
|
@@ -80,6 +80,10 @@ export const commonMessages = {
|
|
|
80
80
|
},
|
|
81
81
|
'msg.chat_opened': { ru: 'Чат открыт.', en: 'Chat opened.' },
|
|
82
82
|
'msg.version': { ru: 'Версия: {v}', en: 'Version: {v}' },
|
|
83
|
+
'msg.banner_hint': {
|
|
84
|
+
ru: 'задачу — Enter · команды — /help · выход — /exit',
|
|
85
|
+
en: 'task — Enter · commands — /help · exit — /exit',
|
|
86
|
+
},
|
|
83
87
|
'msg.interactive': {
|
|
84
88
|
ru: 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.',
|
|
85
89
|
en: 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.',
|
|
@@ -112,6 +116,9 @@ export const commonMessages = {
|
|
|
112
116
|
ru: 'Разрешить выполнение?',
|
|
113
117
|
en: 'Allow this call?',
|
|
114
118
|
},
|
|
119
|
+
'perm.title': { ru: 'Подтверждение', en: 'Confirmation' },
|
|
120
|
+
'confirm.yes': { ru: 'Да', en: 'Yes' },
|
|
121
|
+
'confirm.no': { ru: 'Нет', en: 'No' },
|
|
115
122
|
'perm.ask_sub': {
|
|
116
123
|
ru: '⚠ Субагент запрашивает подтверждение: {tool} ({reason})',
|
|
117
124
|
en: '⚠ Subagent approval required: {tool} ({reason})',
|
|
@@ -239,6 +246,30 @@ export const commonMessages = {
|
|
|
239
246
|
ru: 'Не удалось скопировать (нет {tool} или он недоступен).',
|
|
240
247
|
en: 'Could not copy ({tool} missing or unavailable).',
|
|
241
248
|
},
|
|
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_hint': {
|
|
258
|
+
ru: 'блок кода {n}/{m} · Ctrl+Y — копировать',
|
|
259
|
+
en: 'code block {n}/{m} · Ctrl+Y to copy',
|
|
260
|
+
},
|
|
261
|
+
'copy_code.usage': {
|
|
262
|
+
ru: 'Использование: /copy-code [номер] — копировать блок кода последнего ответа (по умолчанию последний). /copy-code list — список.',
|
|
263
|
+
en: 'Usage: /copy-code [n] — copy a code block from the last answer (default: the last). /copy-code list — list them.',
|
|
264
|
+
},
|
|
265
|
+
'copy_code.list': {
|
|
266
|
+
ru: 'Блоки кода последнего ответа:',
|
|
267
|
+
en: 'Code blocks in the last answer:',
|
|
268
|
+
},
|
|
269
|
+
'copy_code.item': {
|
|
270
|
+
ru: ' #{n} {lang} — {lines} стр.',
|
|
271
|
+
en: ' #{n} {lang} — {lines} lines',
|
|
272
|
+
},
|
|
242
273
|
'retry.running': { ru: 'Повторяю задачу…', en: 'Retrying the task...' },
|
|
243
274
|
'rename.usage': {
|
|
244
275
|
ru: 'Использование: /rename <title>',
|
package/dist/i18n/config.js
CHANGED
|
@@ -32,6 +32,18 @@ export const configMessages = {
|
|
|
32
32
|
ru: 'Ширина контента в колонках: ответы, ввод, инструменты (0 — авто, до 100)',
|
|
33
33
|
en: 'Content width in columns: answers, input, tools (0 = auto, capped at 100)',
|
|
34
34
|
},
|
|
35
|
+
'cfg.f.ui_tui': {
|
|
36
|
+
ru: 'Современный TUI (Ink/React) для ввода и статуса',
|
|
37
|
+
en: 'Modern TUI (Ink/React) for input and status',
|
|
38
|
+
},
|
|
39
|
+
'cfg.f.ui_alternateScreen': {
|
|
40
|
+
ru: 'Полноэкранный режим TUI (альтернативный экран)',
|
|
41
|
+
en: 'Full-screen TUI mode (alternate screen)',
|
|
42
|
+
},
|
|
43
|
+
'cfg.f.ui_mouseScroll': {
|
|
44
|
+
ru: 'Прокрутка лога колесом мыши (выделение текста — Shift+протяжка)',
|
|
45
|
+
en: 'Mouse-wheel log scrolling (select text with Shift+drag)',
|
|
46
|
+
},
|
|
35
47
|
'cfg.f.maxIterations': {
|
|
36
48
|
ru: 'Лимит итераций на задачу',
|
|
37
49
|
en: 'Max agent loop iterations per task',
|
|
@@ -200,4 +212,12 @@ export const configMessages = {
|
|
|
200
212
|
ru: 'Использование: /config lang <ru|en>',
|
|
201
213
|
en: 'Usage: /config lang <ru|en>',
|
|
202
214
|
},
|
|
215
|
+
'cfg.provider_restart': {
|
|
216
|
+
ru: '⚠ Провайдер изменится только после перезапуска (для смены на лету используйте /provider).',
|
|
217
|
+
en: '⚠ Provider changes only after a restart (use /provider to switch live).',
|
|
218
|
+
},
|
|
219
|
+
'cfg.restart_required': {
|
|
220
|
+
ru: 'Этот параметр применится после перезапуска.',
|
|
221
|
+
en: 'This setting takes effect after a restart.',
|
|
222
|
+
},
|
|
203
223
|
};
|