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/dist/input/layout.js
CHANGED
|
@@ -227,6 +227,25 @@ export function truncateToWidth(s, maxCols) {
|
|
|
227
227
|
}
|
|
228
228
|
return out + '…';
|
|
229
229
|
}
|
|
230
|
+
// Inner content width of the framed input box: the rounded border takes 1
|
|
231
|
+
// column on each side and the box adds 1 column of padding on each side, so
|
|
232
|
+
// the text must wrap to `cols - 4`. Exported so the layout ABOVE the box
|
|
233
|
+
// (FullScreenApp) and the box itself (InputLine) use ONE number — a mismatch
|
|
234
|
+
// made the reserved height disagree with the rendered rows and the panel
|
|
235
|
+
// overflowed.
|
|
236
|
+
export function inputInnerCols(cols) {
|
|
237
|
+
return Math.max(4, (Number(cols) || 80) - 4);
|
|
238
|
+
}
|
|
239
|
+
// Real height (rows) of the framed input box for the CURRENT buffer: 2 border
|
|
240
|
+
// rows + the number of VISUAL rows the buffer wraps to. Using the LOGICAL line
|
|
241
|
+
// count (buf.split('\n').length) was wrong: a long line wraps and renders MORE
|
|
242
|
+
// rows than it has newlines, so the box was reserved too few rows, the second
|
|
243
|
+
// line printed OVER the first and the cursor landed on the wrong row.
|
|
244
|
+
export function inputBoxRows(prompt, buf, cursor, cols) {
|
|
245
|
+
const inner = inputInnerCols(cols);
|
|
246
|
+
const rows = layoutInput(prompt, buf, cursor, inner).rows.length;
|
|
247
|
+
return 2 + Math.max(1, rows);
|
|
248
|
+
}
|
|
230
249
|
export function layoutInput(promptStr, buf, cursor, cols) {
|
|
231
250
|
// Reserve the LAST column. If a printed row reaches exactly `cols` columns,
|
|
232
251
|
// terminals with autowrap (Tabby, iTerm, Windows Terminal, xterm's default
|
package/dist/input/suggest.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// editor state: given the buffer text and the command list these decide which
|
|
4
4
|
// commands to show, the visible window, and the Tab completion. Split out
|
|
5
5
|
// (C3) so the fuzzy match can be unit-tested without driving the editor.
|
|
6
|
-
import { SUGGEST_PAGE, isSubsequence } from './layout.js';
|
|
6
|
+
import { SUGGEST_PAGE, isSubsequence, visLen, } from './layout.js';
|
|
7
7
|
/**
|
|
8
8
|
* The commands matching the current buffer. Shown only when the line starts
|
|
9
9
|
* with «/» and is a single short word. The match is fuzzy: an exact prefix
|
|
@@ -57,6 +57,34 @@ export function suggestWindow(sugg, offset) {
|
|
|
57
57
|
const moreCount = sugg.length > pageSize ? sugg.length - shown.length : 0;
|
|
58
58
|
return { shown, off, moreCount };
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* How many terminal ROWS the suggestion popup occupies for the current buffer
|
|
62
|
+
* (0 when no list). Full-screen mode uses this to RESERVE space above the
|
|
63
|
+
* input panel so opening the list does not shift the layout (the operator saw
|
|
64
|
+
* the whole screen jump when `/chats` or `/resume` hints appeared).
|
|
65
|
+
*
|
|
66
|
+
* With `cols > 0` a row whose label+description is WIDER than the terminal is
|
|
67
|
+
* counted as the multiple rows it actually wraps to; counting one row per
|
|
68
|
+
* command under-reserved the panel and the caret drifted on a narrow terminal
|
|
69
|
+
* (the command list carries descriptions up to ~60 chars).
|
|
70
|
+
*/
|
|
71
|
+
export function suggestRowCount(buf, commands, cols = 0) {
|
|
72
|
+
const sugg = matchSlashCommands(buf, commands);
|
|
73
|
+
if (!sugg.length)
|
|
74
|
+
return 0;
|
|
75
|
+
const { shown, moreCount } = suggestWindow(sugg, 0);
|
|
76
|
+
const width = Math.max(1, cols);
|
|
77
|
+
let rows = 0;
|
|
78
|
+
for (const c of shown) {
|
|
79
|
+
const label = c.hint ? c.name + ' ' + c.hint : c.name;
|
|
80
|
+
// 3 = ' ▸ ' prefix, 2 = the gap before the description.
|
|
81
|
+
const textWidth = 3 + visLen(label) + 2 + visLen(c.description);
|
|
82
|
+
rows += cols > 0 ? Math.max(1, Math.ceil(textWidth / width)) : 1;
|
|
83
|
+
}
|
|
84
|
+
if (moreCount > 0)
|
|
85
|
+
rows += 1;
|
|
86
|
+
return rows;
|
|
87
|
+
}
|
|
60
88
|
/**
|
|
61
89
|
* Tab completion. With an explicit selection, insert that command. With a
|
|
62
90
|
* single match, insert it whole. Otherwise complete up to the longest common
|
package/dist/input.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { theme, divider } from './theme.js';
|
|
2
|
+
import { summarizeToolCall } from './tool-summary.js';
|
|
2
3
|
import { renderMarkdown } from './markdown.js';
|
|
3
4
|
import { contentWidth } from './width.js';
|
|
4
5
|
import { randomThinkingPhrase, stripEllipsis, DOTS, renderDots, } from './spinner.js';
|
|
@@ -940,6 +941,10 @@ export class LineEditor {
|
|
|
940
941
|
this.statusText = text || '';
|
|
941
942
|
this._render();
|
|
942
943
|
}
|
|
944
|
+
// No-op success/failure hooks, kept so LineEditor satisfies the shared
|
|
945
|
+
// UiEditor interface (the TUI and the spinner expose the same surface).
|
|
946
|
+
succeed() { }
|
|
947
|
+
fail() { }
|
|
943
948
|
_stopDots() {
|
|
944
949
|
if (this._dotTimer) {
|
|
945
950
|
clearInterval(this._dotTimer);
|
|
@@ -1070,8 +1075,11 @@ export class LineEditor {
|
|
|
1070
1075
|
// wider than the terminal and some terminals then showed a horizontal
|
|
1071
1076
|
// scrollbar. Keep one column free (autowrap safety), same as the status.
|
|
1072
1077
|
const maxW = contentWidth();
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1078
|
+
const summary = summarizeToolCall(name, (typeof args === 'object' && args !== null ? args : {}));
|
|
1079
|
+
const detail = summary.detail
|
|
1080
|
+
? truncateToWidth(summary.detail, Math.max(10, maxW - name.length - 6))
|
|
1081
|
+
: truncateToWidth(full, maxW);
|
|
1082
|
+
this.printAbove(theme.tool('🔧 ' + name) + (detail ? theme.dim('(' + detail + ')') : ''));
|
|
1075
1083
|
// A tool may run for a long time (Bash, npm test, MCP). Without an active
|
|
1076
1084
|
// animation the operator sees a frozen screen and cannot tell work is in
|
|
1077
1085
|
// progress. Start the animated status AFTER printAbove (which stops the
|
|
@@ -1186,7 +1194,10 @@ export class LineEditor {
|
|
|
1186
1194
|
// Attachments collected for this message (images/files). They are handed
|
|
1187
1195
|
// to the browser layer to be attached to the chat.
|
|
1188
1196
|
const attached = this.attachments.items.slice();
|
|
1189
|
-
|
|
1197
|
+
// A turn separator + a BAR-marked operator message (same look as the TUI).
|
|
1198
|
+
this.printAbove(theme.turnRule('─'.repeat(contentWidth())));
|
|
1199
|
+
this.printAbove(theme.userBar('▌ ') +
|
|
1200
|
+
theme.user(wrapToWidth(display, contentWidth() - 2)));
|
|
1190
1201
|
this.pastes = [];
|
|
1191
1202
|
this.attachments.reset();
|
|
1192
1203
|
if (this.onSubmit)
|
package/dist/net-capture.js
CHANGED
|
@@ -186,6 +186,24 @@ export function isFinishedWithoutAnswer(body) {
|
|
|
186
186
|
// absence of response content as "no answer produced".
|
|
187
187
|
return true;
|
|
188
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* True when the SSE body shows a turn that ENDED (`quasi_status: FINISHED`)
|
|
191
|
+
* AND carried a non-empty RESPONSE fragment — i.e. the server is done and the
|
|
192
|
+
* answer is COMPLETE. This is the network-authoritative "the turn is over"
|
|
193
|
+
* signal: the finish loop can trust it to stop waiting even when the DOM
|
|
194
|
+
* heuristics (`isNew`/`_isGenerating`) are confused (a resumed chat, a stale
|
|
195
|
+
* Stop button, a DOM that never caught up). Without it the loop waited out
|
|
196
|
+
* the whole askDeadlineMs while showing a spinner — the "generation is running
|
|
197
|
+
* but nothing happens" hang.
|
|
198
|
+
*/
|
|
199
|
+
export function isFinishedWithAnswer(body) {
|
|
200
|
+
const b = String(body || '');
|
|
201
|
+
if (!b)
|
|
202
|
+
return false;
|
|
203
|
+
if (!FINISHED_STATUS_RE.test(b))
|
|
204
|
+
return false;
|
|
205
|
+
return RESPONSE_CONTENT_RE.test(b);
|
|
206
|
+
}
|
|
189
207
|
// The CONTEXT size (in tokens) DeepSeek reports for the current answer.
|
|
190
208
|
//
|
|
191
209
|
// chat.deepseek.com does not expose prompt_tokens/completion_tokens the way
|
package/dist/qwen-browser.js
CHANGED
|
@@ -79,6 +79,7 @@ export class QwenBrowser {
|
|
|
79
79
|
auth;
|
|
80
80
|
locale;
|
|
81
81
|
onAuthSave;
|
|
82
|
+
onLoginPrompt;
|
|
82
83
|
// Watchdog budget the agent loop reads (same contract as DeepSeekBrowser).
|
|
83
84
|
maxAfterToolRetries;
|
|
84
85
|
askDeadlineMs;
|
|
@@ -121,7 +122,7 @@ export class QwenBrowser {
|
|
|
121
122
|
onSendState;
|
|
122
123
|
onNotice;
|
|
123
124
|
userAgent;
|
|
124
|
-
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = true, auth, locale, onAuthSave, userAgent, } = {}) {
|
|
125
|
+
constructor({ headless = false, debug = false, channel = null, answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 2, stabilityDelayMs = 400, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, maxIncompleteRetries = 4, incompleteWaitMs = 2000, startWaitMs = 30000, sendButtonWaitMs = 8000, model = '', deepThinking = false, webSearch = true, auth, locale, onAuthSave, onLoginPrompt, userAgent, } = {}) {
|
|
125
126
|
this.headless = headless;
|
|
126
127
|
this.debug = debug;
|
|
127
128
|
this.channel = channel;
|
|
@@ -148,6 +149,7 @@ export class QwenBrowser {
|
|
|
148
149
|
};
|
|
149
150
|
this.locale = locale || DEFAULT_LOCALE;
|
|
150
151
|
this.onAuthSave = onAuthSave || null;
|
|
152
|
+
this.onLoginPrompt = onLoginPrompt || null;
|
|
151
153
|
this.maxAfterToolRetries = 6;
|
|
152
154
|
this.askDeadlineMs = answerTimeoutMs + 60000;
|
|
153
155
|
this._lastSentAt = 0;
|
|
@@ -574,31 +576,52 @@ export class QwenBrowser {
|
|
|
574
576
|
// This is the DEFAULT Qwen flow (no password needed).
|
|
575
577
|
async _promptAndLogin() {
|
|
576
578
|
await this._openLoginPage();
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
579
|
+
let addr;
|
|
580
|
+
let code;
|
|
581
|
+
if (this.onLoginPrompt) {
|
|
582
|
+
// TUI path: the Ink prompt reads the email/code without pausing Ink.
|
|
583
|
+
const email = await this.onLoginPrompt({
|
|
584
|
+
savedUsername: this.auth.username,
|
|
585
|
+
step: 'email',
|
|
586
|
+
});
|
|
587
|
+
addr = (email ?? '').trim() || this.auth.username;
|
|
588
|
+
if (!addr) {
|
|
589
|
+
this._notice(this._t('qwen.login_failed'));
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const codeAns = await this.onLoginPrompt({
|
|
593
|
+
savedUsername: addr,
|
|
594
|
+
step: 'code',
|
|
595
|
+
});
|
|
596
|
+
code = (codeAns ?? '').trim();
|
|
590
597
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
598
|
+
else {
|
|
599
|
+
const readline = await import('readline');
|
|
600
|
+
const rl = readline.createInterface({
|
|
601
|
+
input: process.stdin,
|
|
602
|
+
output: process.stdout,
|
|
603
|
+
});
|
|
604
|
+
const email = await new Promise((resolve) => rl.question(this._t('qwen.login_email_prompt', {
|
|
605
|
+
def: this.auth.username ? ' [' + this.auth.username + ']' : '',
|
|
606
|
+
}), resolve));
|
|
607
|
+
rl.close();
|
|
608
|
+
addr = email.trim() || this.auth.username;
|
|
609
|
+
if (!addr) {
|
|
610
|
+
this._notice(this._t('qwen.login_failed'));
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
595
613
|
const rlCode = readline.createInterface({
|
|
596
614
|
input: process.stdin,
|
|
597
615
|
output: process.stdout,
|
|
598
616
|
});
|
|
599
|
-
|
|
617
|
+
code = (await new Promise((resolve) => rlCode.question(this._t('qwen.login_code_prompt'), resolve))).trim();
|
|
600
618
|
rlCode.close();
|
|
601
|
-
|
|
619
|
+
}
|
|
620
|
+
let ok;
|
|
621
|
+
try {
|
|
622
|
+
await this._fillEmailAndContinue(addr);
|
|
623
|
+
await this.page.waitForTimeout(1500);
|
|
624
|
+
await this._submitCode(code);
|
|
602
625
|
ok = await this._waitLoggedIn(30000);
|
|
603
626
|
}
|
|
604
627
|
catch (e) {
|
package/dist/run-task.js
CHANGED
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
import { theme } from './theme.js';
|
|
2
2
|
import { withGoal, mergeMessages, isSlashCommand, formatDuration, } from './commands.js';
|
|
3
|
+
import { buildEvaluatorPrompt, buildContinuationDirective, parseGoalVerdict, GOAL_MAX_TURNS, } from './goal.js';
|
|
3
4
|
import { formatCompactTokens, pasteReplacement, expandPastes } from './input.js';
|
|
4
5
|
import { providerAskDeadlineMs } from './config.js';
|
|
5
6
|
// Ask the operator to approve a tool call flagged by an `ask` permission rule
|
|
6
7
|
// (C1). While the LineEditor owns the terminal it must be paused first, or the
|
|
7
8
|
// confirmation prompt and the input line fight for the same keys. A non-TTY run
|
|
8
9
|
// has no way to answer, so it DENIES (never silently allows a guarded call).
|
|
9
|
-
export async function askOperatorConfirm(editor, question) {
|
|
10
|
+
export async function askOperatorConfirm(editor, question, t) {
|
|
10
11
|
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
11
12
|
return false;
|
|
13
|
+
// Prefer the Ink modal when the TUI owns the terminal: it keeps the layout
|
|
14
|
+
// and does not pause/restart the whole editor.
|
|
15
|
+
if (editor && 'isTui' in editor && editor.isTui) {
|
|
16
|
+
if (editor.pause)
|
|
17
|
+
editor.pause();
|
|
18
|
+
try {
|
|
19
|
+
const { askConfirmInk } = await import('./tui/run-confirm.js');
|
|
20
|
+
const tr = t ?? ((k) => k);
|
|
21
|
+
return await askConfirmInk({
|
|
22
|
+
title: tr('perm.title'),
|
|
23
|
+
message: question,
|
|
24
|
+
yesLabel: tr('confirm.yes'),
|
|
25
|
+
noLabel: tr('confirm.no'),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (editor.resume)
|
|
30
|
+
editor.resume();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
if (editor)
|
|
13
34
|
editor.pause();
|
|
14
35
|
try {
|
|
@@ -236,9 +257,34 @@ export function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
|
236
257
|
stdin.setRawMode(wasRaw || false);
|
|
237
258
|
};
|
|
238
259
|
}
|
|
260
|
+
// Run the completion-goal evaluator: one model call in the SAME chat that
|
|
261
|
+
// judges the condition against the transcript. Returns null when the call
|
|
262
|
+
// fails or the reply is unparsable (the caller then fails CLOSED and keeps
|
|
263
|
+
// working — an unreadable verdict must never be mistaken for "met").
|
|
264
|
+
// `agent: true` so it waits out the send throttle like any other agent turn.
|
|
265
|
+
async function evaluateGoal(browser, condition, config, transcript) {
|
|
266
|
+
try {
|
|
267
|
+
const raw = await browser.ask(buildEvaluatorPrompt(condition), {
|
|
268
|
+
agent: true,
|
|
269
|
+
timeout: providerAskDeadlineMs(config),
|
|
270
|
+
});
|
|
271
|
+
transcript?.log('goal_eval_raw', { response: raw.slice(0, 500) });
|
|
272
|
+
return parseGoalVerdict(raw);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
transcript?.log('goal_eval_error', { error: e.message });
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
239
279
|
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
240
280
|
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
241
|
-
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, onSubagent = null, } = opts;
|
|
281
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, goalIsCondition = false, onGoalCleared = null, goalMaxTurns = GOAL_MAX_TURNS, goalQuery = null, todosQuery = null, selfImprovement = false, onSubagent = null, } = opts;
|
|
282
|
+
// The ACTIVE goal for the duration of this run. It is mutable because a
|
|
283
|
+
// completion goal can be cleared mid-run (verdict MET/IMPOSSIBLE, or the
|
|
284
|
+
// operator runs `/goal clear` while the task is in flight). `goalQuery`
|
|
285
|
+
// re-reads the operator's current goal so a mid-run clear stops the loop.
|
|
286
|
+
let activeGoal = goal;
|
|
287
|
+
const currentGoal = () => goalQuery ? goalQuery() : activeGoal;
|
|
242
288
|
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
243
289
|
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
244
290
|
// mode (pipes) - a regular spinner + watchInput.
|
|
@@ -282,6 +328,8 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
282
328
|
let failure = null;
|
|
283
329
|
// N31: the machine-readable reason for the last task's outcome.
|
|
284
330
|
let outcomeKind = 'ok';
|
|
331
|
+
// How many auto-continue turns a completion goal has already spent.
|
|
332
|
+
let goalTurns = 0;
|
|
285
333
|
let next = {
|
|
286
334
|
text: taskText,
|
|
287
335
|
attachments,
|
|
@@ -307,7 +355,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
307
355
|
const outcome = await runAgentLoop({
|
|
308
356
|
browser,
|
|
309
357
|
tools,
|
|
310
|
-
task: withGoal(
|
|
358
|
+
task: withGoal(currentGoal(), next.text),
|
|
311
359
|
workdir,
|
|
312
360
|
maxIterations: maxIter,
|
|
313
361
|
freshChat: next.freshChat,
|
|
@@ -338,7 +386,7 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
338
386
|
// C1: a rule with action "ask". Show the reason and wait for the
|
|
339
387
|
// operator. In non-TTY this denies.
|
|
340
388
|
ui.warning(t('perm.ask', { tool: info.tool, reason: info.reason }));
|
|
341
|
-
const ok = await askOperatorConfirm(editor || null, t('perm.confirm'));
|
|
389
|
+
const ok = await askOperatorConfirm(editor || null, t('perm.confirm'), t);
|
|
342
390
|
ui.warning(ok ? t('perm.allowed') : t('perm.denied'));
|
|
343
391
|
transcript?.log('permission_ask', {
|
|
344
392
|
tool: info.tool,
|
|
@@ -407,6 +455,51 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
|
|
|
407
455
|
editor.printAbove(summary);
|
|
408
456
|
else
|
|
409
457
|
console.log(summary);
|
|
458
|
+
// Completion goal (Claude Code parity): after EVERY turn a fresh model
|
|
459
|
+
// judges the condition. NOT_MET starts another turn automatically instead
|
|
460
|
+
// of returning control to the operator; MET/IMPOSSIBLE ends the goal.
|
|
461
|
+
// The evaluation runs only when the operator queue is EMPTY, so a message
|
|
462
|
+
// typed mid-run is always delivered before the next auto-continue turn.
|
|
463
|
+
if (!browser._stopped && goalIsCondition && !queue.length) {
|
|
464
|
+
const cond = currentGoal();
|
|
465
|
+
if (cond) {
|
|
466
|
+
if (goalTurns >= goalMaxTurns) {
|
|
467
|
+
ui.warning(t('goal.turn_budget', { n: String(goalMaxTurns) }));
|
|
468
|
+
transcript?.log('goal_turn_budget', { turns: goalTurns });
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
const verdict = await evaluateGoal(browser, cond, config, transcript);
|
|
472
|
+
goalTurns++;
|
|
473
|
+
if (verdict && verdict.status === 'met') {
|
|
474
|
+
ui.warning(t('goal.met', { reason: verdict.reason }));
|
|
475
|
+
transcript?.log('goal_met', { reason: verdict.reason });
|
|
476
|
+
activeGoal = null;
|
|
477
|
+
if (onGoalCleared)
|
|
478
|
+
await onGoalCleared(verdict);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (verdict && verdict.status === 'impossible') {
|
|
482
|
+
ui.warning(t('goal.impossible', { reason: verdict.reason }));
|
|
483
|
+
transcript?.log('goal_impossible', { reason: verdict.reason });
|
|
484
|
+
activeGoal = null;
|
|
485
|
+
if (onGoalCleared)
|
|
486
|
+
await onGoalCleared(verdict);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
// NOT_MET (or an unparsable verdict -> fail closed as "continue"):
|
|
490
|
+
// start another turn with the continuation directive as the task.
|
|
491
|
+
const reason = verdict?.reason ?? '';
|
|
492
|
+
if (reason)
|
|
493
|
+
ui.warning(t('goal.progress', { reason }));
|
|
494
|
+
transcript?.log('goal_not_met', { reason, turn: goalTurns });
|
|
495
|
+
next = {
|
|
496
|
+
text: buildContinuationDirective(reason),
|
|
497
|
+
freshChat: false,
|
|
498
|
+
sendSystemPrompt: false,
|
|
499
|
+
};
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
410
503
|
// Aborted (Esc/Ctrl+C) - we don't start the next tasks from the queue and
|
|
411
504
|
// clear it, so "stop" really stops everything.
|
|
412
505
|
if (browser._stopped) {
|
package/dist/theme.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { contentWidth } from './width.js';
|
|
3
|
+
// Respect NO_COLOR (https://no-color.org): when the operator sets it (any
|
|
4
|
+
// non-empty value) drop ALL SGR codes so the output can be piped into a file
|
|
5
|
+
// or a log viewer without escape noise. chalk 5 only auto-detects TTY/CI, it
|
|
6
|
+
// does NOT read NO_COLOR, so the level is set explicitly here — the one place
|
|
7
|
+
// the palette is built, so every consumer (theme, spinner, Ink Text colors
|
|
8
|
+
// that go through chalk) is covered. chalk.level is the documented override.
|
|
9
|
+
if (process.env.NO_COLOR) {
|
|
10
|
+
chalk.level = 0;
|
|
11
|
+
}
|
|
3
12
|
// A calm palette for a dark terminal.
|
|
4
13
|
// Muted, slightly "faded" tones without high saturation
|
|
5
14
|
// (in the spirit of Tokyo Night / Nord). The goal is comfortable reading of
|
|
@@ -16,7 +25,13 @@ import { contentWidth } from './width.js';
|
|
|
16
25
|
// - warn — calm ochre
|
|
17
26
|
// - error — muted terracotta
|
|
18
27
|
export const theme = {
|
|
19
|
-
|
|
28
|
+
// Operator messages — a clearly DISTINCT cyan so they never blend with the
|
|
29
|
+
// model's warm-sand answers or the neutral system text (operator request).
|
|
30
|
+
user: chalk.hex('#8fd4f0'), // operator text — bright cyan-blue
|
|
31
|
+
userBar: chalk.hex('#4a8fb5'), // the `▌` bar marking an operator turn
|
|
32
|
+
// A dim rule + label separating one conversation TURN from the next.
|
|
33
|
+
turnRule: chalk.hex('#3a3f4b'),
|
|
34
|
+
turnLabel: chalk.hex('#5b616e'),
|
|
20
35
|
prompt: chalk.hex('#c8b06a').bold, // input prompt — warm gold, noticeable
|
|
21
36
|
dir: chalk.hex('#7fc4f0'), // working directory name — noticeably blue
|
|
22
37
|
assistant: chalk.hex('#cfc9b0'), // warm sand
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// PURE tool-call summarizer — turns a tool's raw args into a SHORT, readable
|
|
2
|
+
// one-liner for the log (Codex / Claude Code style): `Read(src/index.ts)`
|
|
3
|
+
// instead of the full JSON. No terminal access, so it is unit-testable.
|
|
4
|
+
//
|
|
5
|
+
// The full args are NOT lost: when a tool has no special summary the caller
|
|
6
|
+
// still shows the raw JSON, so nothing is hidden.
|
|
7
|
+
/** Collapse whitespace and clip to `max` chars (single line). */
|
|
8
|
+
function oneLine(s, max = 80) {
|
|
9
|
+
const t = String(s).replace(/\s+/g, ' ').trim();
|
|
10
|
+
return t.length > max ? t.slice(0, max - 1) + '…' : t;
|
|
11
|
+
}
|
|
12
|
+
function str(v) {
|
|
13
|
+
return typeof v === 'string' ? v : '';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A compact, human-readable summary of a tool call. Falls back to the JSON of
|
|
17
|
+
* the args when there is nothing specific to show, so no information is lost.
|
|
18
|
+
*/
|
|
19
|
+
export function summarizeToolCall(name, args) {
|
|
20
|
+
const a = args || {};
|
|
21
|
+
switch (name) {
|
|
22
|
+
case 'Read':
|
|
23
|
+
case 'Write':
|
|
24
|
+
case 'Edit':
|
|
25
|
+
case 'MultiEdit':
|
|
26
|
+
return { name, detail: oneLine(str(a.path) || str(a.file_path)) };
|
|
27
|
+
case 'LS':
|
|
28
|
+
case 'Glob':
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
detail: oneLine(str(a.path) || str(a.pattern) || '.'),
|
|
32
|
+
};
|
|
33
|
+
case 'Grep':
|
|
34
|
+
return {
|
|
35
|
+
name,
|
|
36
|
+
detail: oneLine((str(a.pattern) ? '/' + str(a.pattern) + '/' : '') +
|
|
37
|
+
(a.path ? ' ' + str(a.path) : '')),
|
|
38
|
+
};
|
|
39
|
+
case 'Bash': {
|
|
40
|
+
const cmd = str(a.command);
|
|
41
|
+
return { name, detail: oneLine(cmd) };
|
|
42
|
+
}
|
|
43
|
+
case 'Task':
|
|
44
|
+
return { name, detail: oneLine(str(a.description) || str(a.prompt)) };
|
|
45
|
+
case 'TodoWrite': {
|
|
46
|
+
const todos = Array.isArray(a.todos) ? a.todos.length : 0;
|
|
47
|
+
return { name, detail: todos ? todos + ' items' : '' };
|
|
48
|
+
}
|
|
49
|
+
case 'respond':
|
|
50
|
+
return { name, detail: oneLine(str(a.message), 60) };
|
|
51
|
+
default: {
|
|
52
|
+
const keys = Object.keys(a);
|
|
53
|
+
if (!keys.length)
|
|
54
|
+
return { name, detail: '' };
|
|
55
|
+
try {
|
|
56
|
+
return { name, detail: oneLine(JSON.stringify(a)) };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { name, detail: '' };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A cheap heuristic for the STATUS ICON next to a tool result: a result that
|
|
66
|
+
* clearly reports a failure gets ✖, a normal one ✔. The agent loop returns a
|
|
67
|
+
* failed tool as a string beginning with "Error"/"Missing required argument"
|
|
68
|
+
* (see src/tools.ts), so matching those prefixes is reliable enough — a false
|
|
69
|
+
* "✔" on an unusual error body is harmless (the text is still shown).
|
|
70
|
+
*/
|
|
71
|
+
export function looksLikeToolError(result) {
|
|
72
|
+
const head = String(result ?? '')
|
|
73
|
+
.trim()
|
|
74
|
+
.slice(0, 60)
|
|
75
|
+
.toLowerCase();
|
|
76
|
+
if (!head)
|
|
77
|
+
return false;
|
|
78
|
+
return (head.startsWith('error') ||
|
|
79
|
+
head.startsWith('missing required argument') ||
|
|
80
|
+
head.startsWith('failed') ||
|
|
81
|
+
head.includes('command failed') ||
|
|
82
|
+
head.includes('exit code'));
|
|
83
|
+
}
|