zames_pro 2.73.11 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +13 -7
  3. package/dist/banner.js +71 -0
  4. package/dist/browser.js +84 -27
  5. package/dist/code-blocks.js +48 -0
  6. package/dist/commands.js +1 -0
  7. package/dist/config-live.js +53 -0
  8. package/dist/config.js +38 -0
  9. package/dist/goal.js +81 -0
  10. package/dist/i18n/common.js +31 -0
  11. package/dist/i18n/config.js +20 -0
  12. package/dist/i18n/deepseek.js +14 -0
  13. package/dist/i18n/help.js +22 -2
  14. package/dist/i18n/qwen.js +4 -0
  15. package/dist/i18n/self-review.js +8 -0
  16. package/dist/i18n/spinner.js +6 -0
  17. package/dist/index.js +478 -65
  18. package/dist/input/layout.js +19 -0
  19. package/dist/input/suggest.js +13 -0
  20. package/dist/input.js +14 -3
  21. package/dist/net-capture.js +18 -0
  22. package/dist/qwen-browser.js +43 -20
  23. package/dist/run-task.js +97 -4
  24. package/dist/theme.js +7 -1
  25. package/dist/tool-summary.js +83 -0
  26. package/dist/tui/ConfigMenu.js +304 -0
  27. package/dist/tui/ConfirmDialog.js +42 -0
  28. package/dist/tui/FullScreenApp.js +85 -0
  29. package/dist/tui/InputLine.js +285 -0
  30. package/dist/tui/LogView.js +48 -0
  31. package/dist/tui/LoginForm.js +83 -0
  32. package/dist/tui/SelectList.js +50 -0
  33. package/dist/tui/StatusBar.js +140 -0
  34. package/dist/tui/TextPrompt.js +41 -0
  35. package/dist/tui/TuiApp.js +28 -0
  36. package/dist/tui/TuiEditor.js +490 -0
  37. package/dist/tui/edit.js +297 -0
  38. package/dist/tui/keys.js +162 -0
  39. package/dist/tui/log-window.js +93 -0
  40. package/dist/tui/login-pure.js +20 -0
  41. package/dist/tui/run-config-menu.js +19 -0
  42. package/dist/tui/run-confirm.js +22 -0
  43. package/dist/tui/run-login.js +43 -0
  44. package/dist/tui/run-select.js +18 -0
  45. package/dist/tui/store.js +104 -0
  46. package/dist/tui/types.js +10 -0
  47. package/dist/tui/useMouseWheel.js +113 -0
  48. package/dist/tui/widgets.js +55 -0
  49. package/package.json +6 -3
  50. package/scripts/coverage-gate.mts +10 -0
  51. package/scripts/render-demo.mts +1 -1
  52. package/scripts/tui-probe.tsx +62 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ 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
+
10
34
  ## [2.73.11] - 2026-10-12
11
35
 
12
36
  ### 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 [text|clear] — set a long-lived session goal. It is prepended to every
379
- task message, so the model keeps the big picture across many turns. Stored in
380
- `<project>/.zames-goal` (git-ignored) and restored on the next launch.
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 [text|clear] — задать долгоживущую цель сессии. Она добавляется в
946
- начало каждого сообщения задачи, поэтому модель держит общую картину через
947
- много ходов. Хранится в `<project>/.zames-goal` (git-ignored) и
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
- if (!user) {
599
- const readline = await import('readline');
600
- const rl = readline.createInterface({
601
- input: process.stdin,
602
- output: process.stdout,
603
- terminal: true,
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
- user = (await new Promise((resolve) => rl.question(this._t('auth.prompt_login'), (a) => resolve(a)))).trim();
606
- rl.close();
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
- const rl = readline.createInterface({
821
- input: process.stdin,
822
- output: process.stdout,
823
- terminal: true,
824
- });
825
- const code = (await new Promise((resolve) => rl.question(this._t('auth.mfa_prompt'), (a) => resolve(a))))
826
- .trim()
827
- .replace(/\s+/g, '');
828
- rl.close();
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
+ }
@@ -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>',
@@ -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
  };