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/index.js
CHANGED
|
@@ -14,10 +14,12 @@ import { runAgentLoop } from './agent-loop.js';
|
|
|
14
14
|
import { createSubagentRunner } from './subagent.js';
|
|
15
15
|
import { createSpinner } from './spinner.js';
|
|
16
16
|
import { LineEditor } from './input.js';
|
|
17
|
+
import { TuiEditor } from './tui/TuiEditor.js';
|
|
17
18
|
import { promptOnce } from './prompt.js';
|
|
18
19
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
19
20
|
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, } from './config.js';
|
|
20
21
|
import { runConfigMenu } from './config-menu.js';
|
|
22
|
+
import { runConfigMenuInk } from './tui/run-config-menu.js';
|
|
21
23
|
import { formatConfigList } from './config-commands.js';
|
|
22
24
|
import { handleConfigCommand as runConfigCommand } from './config-command.js';
|
|
23
25
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
@@ -26,10 +28,15 @@ import { UndoStore } from './undo.js';
|
|
|
26
28
|
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
27
29
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
28
30
|
import { performCompact } from './compact.js';
|
|
31
|
+
import { clampGoalCondition } from './goal.js';
|
|
32
|
+
import { applyBrowserLiveConfig, applyStoreLiveConfig } from './config-live.js';
|
|
29
33
|
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
|
|
30
34
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
31
35
|
import { createCliArgs } from './cli-args.js';
|
|
32
36
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
37
|
+
import { extractCodeBlocks } from './code-blocks.js';
|
|
38
|
+
import { contentWidth } from './width.js';
|
|
39
|
+
import { renderStartupBanner } from './banner.js';
|
|
33
40
|
import { runStatusLineCommand } from './statusline.js';
|
|
34
41
|
import { parseBacklogNote } from './backlog.js';
|
|
35
42
|
import { askOperatorConfirm, } from './run-task.js';
|
|
@@ -108,10 +115,14 @@ const maxIterArg = getArg('--max-iter', null);
|
|
|
108
115
|
// A non-numeric `--max-iter abc` yields NaN, which the loop treats as
|
|
109
116
|
// "unlimited" (NaN > 0 is false). Fall back to the configured value instead
|
|
110
117
|
// of silently disabling the cap.
|
|
118
|
+
// The CLI value OVERRIDES the config for the whole run; when no flag is
|
|
119
|
+
// given, the value is read from config LIVE so `/config set maxIterations`
|
|
120
|
+
// takes effect on the next task.
|
|
111
121
|
const maxIterParsed = maxIterArg !== null ? Number(maxIterArg) : NaN;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
: config.maxIterations;
|
|
122
|
+
const maxIterOverride = Number.isFinite(maxIterParsed) ? maxIterParsed : null;
|
|
123
|
+
function currentMaxIter() {
|
|
124
|
+
return maxIterOverride !== null ? maxIterOverride : config.maxIterations;
|
|
125
|
+
}
|
|
115
126
|
const positional = getPositional();
|
|
116
127
|
const task = getArg('--task', positional.join(' ').trim() || null);
|
|
117
128
|
const chatIdArg = getArg('--chat', null);
|
|
@@ -426,6 +437,8 @@ ${theme.bold(t('help.while_working'))}
|
|
|
426
437
|
${t('help.key.attach')}
|
|
427
438
|
${t('help.key.esc')}
|
|
428
439
|
${t('help.key.bang')}
|
|
440
|
+
${t('help.key.scroll')}
|
|
441
|
+
${t('help.key.wheel')}
|
|
429
442
|
|
|
430
443
|
${theme.bold(t('help.commands'))}
|
|
431
444
|
|
|
@@ -780,7 +793,7 @@ function runTaskDeps() {
|
|
|
780
793
|
t,
|
|
781
794
|
locale: currentLocale,
|
|
782
795
|
debug,
|
|
783
|
-
maxIter,
|
|
796
|
+
maxIter: currentMaxIter(),
|
|
784
797
|
config,
|
|
785
798
|
runAgentLoop: mod.runAgentLoop,
|
|
786
799
|
createSpinner: mod.createSpinner,
|
|
@@ -817,8 +830,21 @@ async function main() {
|
|
|
817
830
|
console.error(theme.error(t('msg.workdir_error')), e.message);
|
|
818
831
|
process.exit(1);
|
|
819
832
|
}
|
|
820
|
-
|
|
821
|
-
|
|
833
|
+
// A styled startup banner in an interactive terminal; a plain two-line
|
|
834
|
+
// header when the output is piped (scripts/CI parse it).
|
|
835
|
+
if (process.stdout.isTTY && !jsonMode && config.ui.tui) {
|
|
836
|
+
console.log(renderStartupBanner({
|
|
837
|
+
title: 'zames',
|
|
838
|
+
version: pkgVersion || '?',
|
|
839
|
+
workdir: currentWorkdir,
|
|
840
|
+
provider: providerLabel(config.provider),
|
|
841
|
+
hint: t('msg.banner_hint'),
|
|
842
|
+
}, contentWidth()));
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
console.log(theme.system(t('msg.working_dir', { v: currentWorkdir })));
|
|
846
|
+
console.log(theme.system(t('msg.version', { v: pkgVersion || '?' })));
|
|
847
|
+
}
|
|
822
848
|
const transcript = new Transcript({
|
|
823
849
|
dir: config.transcript.dir,
|
|
824
850
|
enabled: config.transcript.enabled,
|
|
@@ -1193,6 +1219,10 @@ async function main() {
|
|
|
1193
1219
|
// the typed text is never overwritten by output. In non-TTY (pipe) —
|
|
1194
1220
|
// the old promptOnce.
|
|
1195
1221
|
let editor = null;
|
|
1222
|
+
// Re-wire the browser login callbacks after the editor exists / a provider
|
|
1223
|
+
// swap. Assigned where the editor is built (see the login wiring below);
|
|
1224
|
+
// a no-op until then so /provider before setup is harmless.
|
|
1225
|
+
let wireLoginCallbacks = () => { };
|
|
1196
1226
|
// N37 statusLine hook timer (cleared on exit).
|
|
1197
1227
|
let statusLineTimer = null;
|
|
1198
1228
|
let waiter = null;
|
|
@@ -1218,19 +1248,36 @@ async function main() {
|
|
|
1218
1248
|
};
|
|
1219
1249
|
await refreshDynamicCommands(currentWorkdir);
|
|
1220
1250
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
1221
|
-
const ed =
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1251
|
+
const ed = config.ui.tui
|
|
1252
|
+
? new TuiEditor({
|
|
1253
|
+
prompt: buildPrompt(),
|
|
1254
|
+
commands: buildSlashCommands(),
|
|
1255
|
+
locale: currentLocale,
|
|
1256
|
+
alternateScreen: config.ui.alternateScreen,
|
|
1257
|
+
})
|
|
1258
|
+
: new LineEditor({
|
|
1259
|
+
prompt: buildPrompt(),
|
|
1260
|
+
commands: buildSlashCommands(),
|
|
1261
|
+
locale: currentLocale,
|
|
1262
|
+
});
|
|
1226
1263
|
editor = ed;
|
|
1227
1264
|
ed.setTmpDir(TMP_DIR);
|
|
1228
1265
|
// Provider badge: show which backend is answering ([Qwen]/[DeepSeek]).
|
|
1229
1266
|
ed.setProvider(providerLabel(config.provider));
|
|
1267
|
+
// Header for full-screen mode (title / version / working directory).
|
|
1268
|
+
ed.setHeader?.({
|
|
1269
|
+
title: 'zames',
|
|
1270
|
+
version: pkgVersion || '',
|
|
1271
|
+
workdir: currentWorkdir,
|
|
1272
|
+
mouseScroll: config.ui.mouseScroll,
|
|
1273
|
+
});
|
|
1230
1274
|
// Restore the input history from the previous runs and persist every new
|
|
1231
1275
|
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1232
1276
|
ed.history = loadHistory();
|
|
1233
|
-
|
|
1277
|
+
// The legacy LineEditor keeps its own browse index; the TUI seeds it from
|
|
1278
|
+
// the history itself.
|
|
1279
|
+
if (ed instanceof LineEditor)
|
|
1280
|
+
ed._histIndex = ed.history.length;
|
|
1234
1281
|
ed.onHistoryChange = (h) => saveHistory(h);
|
|
1235
1282
|
// The token context right-aligned on the status line (above the input).
|
|
1236
1283
|
// The editor pulls the number on every render, so it follows the live
|
|
@@ -1247,6 +1294,81 @@ async function main() {
|
|
|
1247
1294
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
1248
1295
|
// counter. The editor pulls the live state on every render.
|
|
1249
1296
|
ed.onToggleQuery = () => browser.getToggleStatesSync();
|
|
1297
|
+
// Interactive login goes through the Ink form when the TUI owns the
|
|
1298
|
+
// terminal: the browser's readline/askPassword would PAUSE the whole TUI
|
|
1299
|
+
// and fight Ink for stdin. A NAMED function so /provider can re-wire the
|
|
1300
|
+
// freshly created browser (the factory runs before `editor` exists, so the
|
|
1301
|
+
// callbacks cannot be passed there without a forward reference).
|
|
1302
|
+
wireLoginCallbacks = (b) => {
|
|
1303
|
+
// The DeepSeek and Qwen browsers expose DIFFERENT login callback shapes
|
|
1304
|
+
// (DeepSeek: login+password / MFA; Qwen: email -> one-time code), so the
|
|
1305
|
+
// wiring is branched on the provider rather than forced through one
|
|
1306
|
+
// shape.
|
|
1307
|
+
const bb = b;
|
|
1308
|
+
if (config.provider === 'qwen') {
|
|
1309
|
+
bb.onLoginPrompt = async (info) => {
|
|
1310
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1311
|
+
return null;
|
|
1312
|
+
ed.pause();
|
|
1313
|
+
try {
|
|
1314
|
+
const { runTextPrompt } = await import('./tui/run-login.js');
|
|
1315
|
+
const isCode = info.step === 'code';
|
|
1316
|
+
return await runTextPrompt({
|
|
1317
|
+
t,
|
|
1318
|
+
label: isCode
|
|
1319
|
+
? t('qwen.login_code_prompt')
|
|
1320
|
+
: t('qwen.login_email_prompt', {
|
|
1321
|
+
def: info.savedUsername
|
|
1322
|
+
? ' [' + info.savedUsername + ']'
|
|
1323
|
+
: '',
|
|
1324
|
+
}),
|
|
1325
|
+
secret: isCode,
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
finally {
|
|
1329
|
+
ed.resume();
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
bb.onLoginPrompt = async (info) => {
|
|
1335
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1336
|
+
return null;
|
|
1337
|
+
ed.pause();
|
|
1338
|
+
try {
|
|
1339
|
+
const { runLoginForm } = await import('./tui/run-login.js');
|
|
1340
|
+
return await runLoginForm({
|
|
1341
|
+
t,
|
|
1342
|
+
initialUsername: config.browser.auth.username,
|
|
1343
|
+
initialPassword: config.browser.auth.password,
|
|
1344
|
+
note: info.hasSavedPassword || info.savedUsername
|
|
1345
|
+
? t('auth.form.note_saved')
|
|
1346
|
+
: undefined,
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
finally {
|
|
1350
|
+
ed.resume();
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
bb.onMfaPrompt = async () => {
|
|
1355
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1356
|
+
return null;
|
|
1357
|
+
ed.pause();
|
|
1358
|
+
try {
|
|
1359
|
+
const { runTextPrompt } = await import('./tui/run-login.js');
|
|
1360
|
+
return await runTextPrompt({
|
|
1361
|
+
t,
|
|
1362
|
+
label: t('auth.mfa_prompt'),
|
|
1363
|
+
secret: true,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
finally {
|
|
1367
|
+
ed.resume();
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
};
|
|
1371
|
+
wireLoginCallbacks(browser);
|
|
1250
1372
|
// N37 statusLine hook: run the configured command on its own timer (never
|
|
1251
1373
|
// per-render — a spawn would block typing) and show its first stdout line
|
|
1252
1374
|
// in the status line. Off when ui.statusLineCommand is empty.
|
|
@@ -1363,6 +1485,7 @@ async function main() {
|
|
|
1363
1485
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1364
1486
|
return att;
|
|
1365
1487
|
};
|
|
1488
|
+
ed.onCopyText = (text) => writeClipboardText(text);
|
|
1366
1489
|
ed.onSubmit = (text, items) => {
|
|
1367
1490
|
// While the agent is busy the main command loop is blocked on
|
|
1368
1491
|
// `await runTask()`, so ONLY commands that make sense without the main
|
|
@@ -1473,7 +1596,23 @@ async function main() {
|
|
|
1473
1596
|
}
|
|
1474
1597
|
}
|
|
1475
1598
|
};
|
|
1476
|
-
|
|
1599
|
+
try {
|
|
1600
|
+
ed.start();
|
|
1601
|
+
// One-time hint so the operator knows how selection works: with wheel
|
|
1602
|
+
// scrolling on (the default) the terminal no longer selects natively, so
|
|
1603
|
+
// a drag needs Shift. Printed ONCE, above the input, in full-screen TUI
|
|
1604
|
+
// only (the legacy editor keeps native selection).
|
|
1605
|
+
if (config.ui.tui && config.ui.alternateScreen) {
|
|
1606
|
+
ed.printAbove(theme.dim(t('msg.mouse_hint')));
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
catch (e) {
|
|
1610
|
+
// A TUI mount failure must not kill the agent. We surface it and let the
|
|
1611
|
+
// operator retry with `ui.tui: false` if it persists; the imperative
|
|
1612
|
+
// callbacks stay bound to the same editor object, so no state is lost.
|
|
1613
|
+
console.error(theme.warn('UI start failed (set ui.tui=false to use the legacy editor): ' +
|
|
1614
|
+
String(e)));
|
|
1615
|
+
}
|
|
1477
1616
|
// All command output (console.log/error) must go ABOVE the input line,
|
|
1478
1617
|
// otherwise it overwrites the text being typed. While the editor is
|
|
1479
1618
|
// active, we wrap both streams in ed.printAbove.
|
|
@@ -1560,6 +1699,63 @@ async function main() {
|
|
|
1560
1699
|
editor.refreshStatus();
|
|
1561
1700
|
}
|
|
1562
1701
|
}
|
|
1702
|
+
// Browser TIMING knobs (timeouts, retries, the send throttle) are COPIED
|
|
1703
|
+
// by the browser at construction, so push them onto the live instance.
|
|
1704
|
+
// (deepThinking/webSearch are handled above because they also repaint the
|
|
1705
|
+
// status icons.)
|
|
1706
|
+
if (path.startsWith('browser.')) {
|
|
1707
|
+
const b = browser;
|
|
1708
|
+
if (applyBrowserLiveConfig(path, value, b)) {
|
|
1709
|
+
// minSendIntervalMs feeds the send countdown the UI renders.
|
|
1710
|
+
if (editor)
|
|
1711
|
+
editor.refreshStatus();
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
// The undo/checkpoint stores also copy their config at construction.
|
|
1715
|
+
applyStoreLiveConfig(path, value, {
|
|
1716
|
+
undo: undo,
|
|
1717
|
+
checkpoints: checkpoints,
|
|
1718
|
+
});
|
|
1719
|
+
// Qwen timing knobs feed the QwenBrowser constructor. Its LIVE fields use
|
|
1720
|
+
// the SAME names, so push them there when the active backend is Qwen (a
|
|
1721
|
+
// no-op on DeepSeek, which has no qwen-only knobs).
|
|
1722
|
+
if (path === 'qwen.minSendIntervalMs' || path === 'qwen.answerTimeoutMs') {
|
|
1723
|
+
const b = browser;
|
|
1724
|
+
if (config.provider === 'qwen') {
|
|
1725
|
+
const field = path === 'qwen.minSendIntervalMs'
|
|
1726
|
+
? 'minSendIntervalMs'
|
|
1727
|
+
: 'answerTimeoutMs';
|
|
1728
|
+
if (field in b) {
|
|
1729
|
+
b[field] = value;
|
|
1730
|
+
if (editor)
|
|
1731
|
+
editor.refreshStatus();
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
// Switching the provider from /config cannot be applied live (it needs a
|
|
1736
|
+
// new browser); tell the operator instead of silently doing nothing.
|
|
1737
|
+
if (path === 'provider') {
|
|
1738
|
+
const line = t('cfg.provider_restart');
|
|
1739
|
+
if (editor)
|
|
1740
|
+
editor.printAbove(theme.warn(line));
|
|
1741
|
+
else
|
|
1742
|
+
console.log(theme.warn(line));
|
|
1743
|
+
}
|
|
1744
|
+
// headless is fixed at launch (the browser is already open); a /config set
|
|
1745
|
+
// only takes effect on the next run.
|
|
1746
|
+
if (path === 'headless') {
|
|
1747
|
+
const line = t('cfg.restart_required');
|
|
1748
|
+
if (editor)
|
|
1749
|
+
editor.printAbove(theme.dim(line));
|
|
1750
|
+
else
|
|
1751
|
+
console.log(theme.dim(line));
|
|
1752
|
+
}
|
|
1753
|
+
// transcript.enabled: the Transcript copies the flag at construction. A
|
|
1754
|
+
// false -> true flip cannot open a NEW stream mid-run, but true -> false
|
|
1755
|
+
// can (and should) stop writing immediately.
|
|
1756
|
+
if (path === 'transcript.enabled' && value === false) {
|
|
1757
|
+
transcript.enabled = false;
|
|
1758
|
+
}
|
|
1563
1759
|
}
|
|
1564
1760
|
// Apply `/thinking` / `/web` immediately: flip the browser's DESIRED state
|
|
1565
1761
|
// and the status icons, and persist to config (so it survives a restart).
|
|
@@ -1598,11 +1794,13 @@ async function main() {
|
|
|
1598
1794
|
// Handle a `/goal` command shared by the main loop and the live interception
|
|
1599
1795
|
// (so a goal can be set even while the agent is busy — it only touches a
|
|
1600
1796
|
// variable and a file, never the chat). Output goes above the input line
|
|
1601
|
-
// when the editor is active, otherwise to stdout.
|
|
1797
|
+
// when the editor is active, otherwise to stdout. Returns which subcommand
|
|
1798
|
+
// ran so the main loop can START A TURN when a goal is freshly set (Claude
|
|
1799
|
+
// Code parity: setting a goal begins working toward it immediately).
|
|
1602
1800
|
async function handleGoal(input) {
|
|
1603
1801
|
const g = mod.commands.parseGoalCommand(input);
|
|
1604
1802
|
if (!g)
|
|
1605
|
-
return;
|
|
1803
|
+
return null;
|
|
1606
1804
|
const emit = (line) => {
|
|
1607
1805
|
if (editor)
|
|
1608
1806
|
editor.printAbove(line);
|
|
@@ -1613,16 +1811,19 @@ async function main() {
|
|
|
1613
1811
|
sessionGoal = null;
|
|
1614
1812
|
await saveGoal(currentWorkdir, null);
|
|
1615
1813
|
emit(theme.system(t('goal.cleared')));
|
|
1814
|
+
return 'cleared';
|
|
1616
1815
|
}
|
|
1617
1816
|
else if (g.sub === 'show') {
|
|
1618
1817
|
emit(sessionGoal
|
|
1619
1818
|
? theme.system(t('goal.current', { goal: sessionGoal }))
|
|
1620
1819
|
: theme.dim(t('goal.none')));
|
|
1820
|
+
return 'shown';
|
|
1621
1821
|
}
|
|
1622
1822
|
else {
|
|
1623
|
-
sessionGoal = g.goal;
|
|
1624
|
-
await saveGoal(currentWorkdir,
|
|
1625
|
-
emit(theme.system(t('goal.set', { goal:
|
|
1823
|
+
sessionGoal = clampGoalCondition(g.goal);
|
|
1824
|
+
await saveGoal(currentWorkdir, sessionGoal);
|
|
1825
|
+
emit(theme.system(t('goal.set', { goal: sessionGoal })));
|
|
1826
|
+
return 'set';
|
|
1626
1827
|
}
|
|
1627
1828
|
}
|
|
1628
1829
|
function configSetRaw(field, raw) {
|
|
@@ -1650,19 +1851,27 @@ async function main() {
|
|
|
1650
1851
|
return;
|
|
1651
1852
|
}
|
|
1652
1853
|
// The menu draws directly to stdout and reads keys itself. So that its
|
|
1653
|
-
// output doesn't overlap the
|
|
1854
|
+
// output doesn't overlap the editor's permanent input line, we "pause"
|
|
1654
1855
|
// the editor for the duration of the menu and restore it afterwards.
|
|
1655
1856
|
if (editor)
|
|
1656
1857
|
editor.pause();
|
|
1858
|
+
const menuOpts = {
|
|
1859
|
+
fields: CONFIG_SCHEMA,
|
|
1860
|
+
t,
|
|
1861
|
+
get: (path) => getByPath(config, path),
|
|
1862
|
+
getDefault: (path) => getByPath(DEFAULTS, path),
|
|
1863
|
+
set: (field, raw) => configSetRaw(field, raw),
|
|
1864
|
+
reset: (field) => configResetField(field),
|
|
1865
|
+
};
|
|
1657
1866
|
try {
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1867
|
+
// Prefer the Ink menu (nicer, resize-aware) when the TUI is enabled;
|
|
1868
|
+
// fall back to the legacy hand-rolled menu otherwise.
|
|
1869
|
+
if (config.ui.tui) {
|
|
1870
|
+
await runConfigMenuInk(menuOpts);
|
|
1871
|
+
}
|
|
1872
|
+
else {
|
|
1873
|
+
await runConfigMenu(menuOpts);
|
|
1874
|
+
}
|
|
1666
1875
|
}
|
|
1667
1876
|
catch (e) {
|
|
1668
1877
|
console.error(theme.error(String(e.message || e)));
|
|
@@ -1690,9 +1899,114 @@ async function main() {
|
|
|
1690
1899
|
projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
|
|
1691
1900
|
});
|
|
1692
1901
|
}
|
|
1902
|
+
// Open a chat by its ChatInfo and print the restored dialogue. Shared by
|
|
1903
|
+
// /resume and the interactive /chats list, so both paths behave identically
|
|
1904
|
+
// (todos reloaded, prompt-resend decision, transcript event).
|
|
1905
|
+
const openResumedChat = async (pick) => {
|
|
1906
|
+
if (editor)
|
|
1907
|
+
editor.lock(t('msg.input_locked'));
|
|
1908
|
+
try {
|
|
1909
|
+
await browser.openChat(pick.id);
|
|
1910
|
+
currentChatId = pick.id;
|
|
1911
|
+
freshChatNext = false;
|
|
1912
|
+
sendSystemPromptNext = promptOnResume();
|
|
1913
|
+
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
1914
|
+
transcript.log('resume_chat', { id: pick.id, title: pick.title });
|
|
1915
|
+
if (editor)
|
|
1916
|
+
editor.clearScreen();
|
|
1917
|
+
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1918
|
+
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
1919
|
+
theme.system(sendSystemPromptNext
|
|
1920
|
+
? String.fromCharCode(10) + t('chats.prompt_will_resend')
|
|
1921
|
+
: String.fromCharCode(10) + t('chats.prompt_no_resend')));
|
|
1922
|
+
loadSessionTodos(pick.id);
|
|
1923
|
+
await printRestoredHistory(browser, editor, pick.id);
|
|
1924
|
+
}
|
|
1925
|
+
catch (e) {
|
|
1926
|
+
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1927
|
+
}
|
|
1928
|
+
finally {
|
|
1929
|
+
if (editor)
|
|
1930
|
+
editor.unlock();
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
// Show the recent chats in an Ink SelectList and open the chosen one.
|
|
1934
|
+
// Returns true when the operator picked a chat (the caller then skips the
|
|
1935
|
+
// text listing). Only called when the TUI owns the terminal.
|
|
1936
|
+
const pickChatInteractively = async (chats) => {
|
|
1937
|
+
if (!editor)
|
|
1938
|
+
return false;
|
|
1939
|
+
editor.pause();
|
|
1940
|
+
let picked = null;
|
|
1941
|
+
try {
|
|
1942
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
1943
|
+
picked = await selectListInk({
|
|
1944
|
+
title: t('chats.pick_title'),
|
|
1945
|
+
items: chats.map((c, i) => ({
|
|
1946
|
+
value: c.id,
|
|
1947
|
+
label: String(i + 1).padStart(2, ' ') + '. ' + c.title,
|
|
1948
|
+
detail: c.id.slice(0, 8) + '…',
|
|
1949
|
+
})),
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
catch {
|
|
1953
|
+
picked = null;
|
|
1954
|
+
}
|
|
1955
|
+
finally {
|
|
1956
|
+
editor.resume();
|
|
1957
|
+
}
|
|
1958
|
+
if (!picked)
|
|
1959
|
+
return false;
|
|
1960
|
+
const pick = chats.find((c) => c.id === picked);
|
|
1961
|
+
if (!pick)
|
|
1962
|
+
return false;
|
|
1963
|
+
await openResumedChat(pick);
|
|
1964
|
+
return true;
|
|
1965
|
+
};
|
|
1966
|
+
// Show the saved SESSIONS in an Ink SelectList and resume the chosen one.
|
|
1967
|
+
// Sessions carry the full chat id (unlike the /chats sidebar), so this is
|
|
1968
|
+
// the safe resume path; it reuses openResumedChat for identical behavior.
|
|
1969
|
+
const pickSessionInteractively = async (sessions) => {
|
|
1970
|
+
if (!editor)
|
|
1971
|
+
return false;
|
|
1972
|
+
editor.pause();
|
|
1973
|
+
let picked = null;
|
|
1974
|
+
try {
|
|
1975
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
1976
|
+
picked = await selectListInk({
|
|
1977
|
+
title: t('sessions.pick_title'),
|
|
1978
|
+
items: sessions.map((s, i) => ({
|
|
1979
|
+
value: s.id,
|
|
1980
|
+
label: String(i + 1).padStart(2, ' ') +
|
|
1981
|
+
'. ' +
|
|
1982
|
+
(s.title || s.id.slice(0, 8)),
|
|
1983
|
+
detail: s.id.slice(0, 8) +
|
|
1984
|
+
'…' +
|
|
1985
|
+
(s.workdir ? ' [' + dirLabel(s.workdir) + ']' : ''),
|
|
1986
|
+
})),
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
catch {
|
|
1990
|
+
picked = null;
|
|
1991
|
+
}
|
|
1992
|
+
finally {
|
|
1993
|
+
editor.resume();
|
|
1994
|
+
}
|
|
1995
|
+
if (!picked)
|
|
1996
|
+
return false;
|
|
1997
|
+
const pick = sessions.find((s) => s.id === picked);
|
|
1998
|
+
if (!pick)
|
|
1999
|
+
return false;
|
|
2000
|
+
await openResumedChat({ id: pick.id, title: pick.title || '' });
|
|
2001
|
+
return true;
|
|
2002
|
+
};
|
|
1693
2003
|
while (running) {
|
|
1694
2004
|
let input;
|
|
1695
2005
|
let inputAttachments = [];
|
|
2006
|
+
// Set by `/goal <condition>` to fall through into a task turn with this
|
|
2007
|
+
// text instead of returning to the prompt (Claude Code starts a turn the
|
|
2008
|
+
// moment a goal is set). Reset every iteration because it is declared here.
|
|
2009
|
+
let taskOverride = null;
|
|
1696
2010
|
try {
|
|
1697
2011
|
if (editor) {
|
|
1698
2012
|
editor.setPrompt(buildPrompt());
|
|
@@ -1898,7 +2212,7 @@ async function main() {
|
|
|
1898
2212
|
tools,
|
|
1899
2213
|
task: focus,
|
|
1900
2214
|
workdir: snapRoot,
|
|
1901
|
-
maxIterations:
|
|
2215
|
+
maxIterations: currentMaxIter(),
|
|
1902
2216
|
freshChat: false,
|
|
1903
2217
|
sendSystemPrompt: false,
|
|
1904
2218
|
transcript,
|
|
@@ -2003,6 +2317,15 @@ async function main() {
|
|
|
2003
2317
|
console.log(theme.system(t('chats.none_hint')));
|
|
2004
2318
|
}
|
|
2005
2319
|
else {
|
|
2320
|
+
// In the TUI, open a real SELECT LIST (like /model) instead of
|
|
2321
|
+
// dumping text: pick a chat and it opens directly. The text path
|
|
2322
|
+
// below stays for non-TTY / the legacy editor.
|
|
2323
|
+
const canPick = editor && 'isTui' in editor && config.ui.tui && !filter;
|
|
2324
|
+
if (canPick && editor) {
|
|
2325
|
+
const openChat = await pickChatInteractively(shown);
|
|
2326
|
+
if (openChat)
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2006
2329
|
console.log(theme.system(t('chats.recent')));
|
|
2007
2330
|
// The number is the index in the FULL list, so `/resume <n>` keeps
|
|
2008
2331
|
// pointing at the same chat even when a filter narrows the view.
|
|
@@ -2044,33 +2367,7 @@ async function main() {
|
|
|
2044
2367
|
console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
|
|
2045
2368
|
continue;
|
|
2046
2369
|
}
|
|
2047
|
-
|
|
2048
|
-
editor.lock(t('msg.input_locked'));
|
|
2049
|
-
try {
|
|
2050
|
-
await browser.openChat(pick.id);
|
|
2051
|
-
currentChatId = pick.id;
|
|
2052
|
-
freshChatNext = false;
|
|
2053
|
-
sendSystemPromptNext = promptOnResume();
|
|
2054
|
-
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
2055
|
-
transcript.log('resume_chat', { id: pick.id, title: pick.title });
|
|
2056
|
-
// Resume on a clean screen so the restored dialogue is readable.
|
|
2057
|
-
if (editor)
|
|
2058
|
-
editor.clearScreen();
|
|
2059
|
-
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
2060
|
-
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
2061
|
-
theme.system(sendSystemPromptNext
|
|
2062
|
-
? String.fromCharCode(10) + t('chats.prompt_will_resend')
|
|
2063
|
-
: String.fromCharCode(10) + t('chats.prompt_no_resend')));
|
|
2064
|
-
loadSessionTodos(pick.id);
|
|
2065
|
-
await printRestoredHistory(browser, editor, pick.id);
|
|
2066
|
-
}
|
|
2067
|
-
catch (e) {
|
|
2068
|
-
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
2069
|
-
}
|
|
2070
|
-
finally {
|
|
2071
|
-
if (editor)
|
|
2072
|
-
editor.unlock();
|
|
2073
|
-
}
|
|
2370
|
+
await openResumedChat(pick);
|
|
2074
2371
|
continue;
|
|
2075
2372
|
}
|
|
2076
2373
|
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
@@ -2195,6 +2492,9 @@ async function main() {
|
|
|
2195
2492
|
// launch failed, `browser` would point at a dead instance and every
|
|
2196
2493
|
// later command would break.
|
|
2197
2494
|
const nextBrowser = createProviderBrowser(arg);
|
|
2495
|
+
// Wire the Ink login BEFORE waitForLogin(), or the new backend would
|
|
2496
|
+
// fall back to readline and pause the TUI for its sign-in.
|
|
2497
|
+
wireLoginCallbacks(nextBrowser);
|
|
2198
2498
|
await nextBrowser.launch();
|
|
2199
2499
|
await nextBrowser.waitForLogin();
|
|
2200
2500
|
const oldBrowser = browser;
|
|
@@ -2248,11 +2548,46 @@ async function main() {
|
|
|
2248
2548
|
const models = qb.listModels ? await qb.listModels() : [];
|
|
2249
2549
|
if (!models.length) {
|
|
2250
2550
|
console.log(theme.system(t('qwen.model_list', { v: '(none)' })));
|
|
2551
|
+
continue;
|
|
2251
2552
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2553
|
+
// In the TUI, open a proper SELECT LIST instead of dumping text.
|
|
2554
|
+
if (editor && 'isTui' in editor && config.ui.tui) {
|
|
2555
|
+
if (editor.pause)
|
|
2556
|
+
editor.pause();
|
|
2557
|
+
let picked;
|
|
2558
|
+
try {
|
|
2559
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
2560
|
+
picked = await selectListInk({
|
|
2561
|
+
title: t('qwen.model_pick_title'),
|
|
2562
|
+
initial: config.qwen.model || models[0].id,
|
|
2563
|
+
items: models.map((m) => ({
|
|
2564
|
+
value: m.id,
|
|
2565
|
+
label: m.name || m.id,
|
|
2566
|
+
detail: m.id === config.qwen.model ? '← текущая' : m.id,
|
|
2567
|
+
})),
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
catch {
|
|
2571
|
+
picked = null;
|
|
2572
|
+
}
|
|
2573
|
+
finally {
|
|
2574
|
+
if (editor.resume)
|
|
2575
|
+
editor.resume();
|
|
2576
|
+
}
|
|
2577
|
+
if (picked) {
|
|
2578
|
+
config.qwen.model = picked;
|
|
2579
|
+
if (typeof qb.model === 'string')
|
|
2580
|
+
qb.model = picked;
|
|
2581
|
+
try {
|
|
2582
|
+
writeConfigValue('home', 'qwen.model', picked);
|
|
2583
|
+
}
|
|
2584
|
+
catch { }
|
|
2585
|
+
console.log(theme.system(t('qwen.model_set', { v: picked })));
|
|
2586
|
+
}
|
|
2587
|
+
continue;
|
|
2255
2588
|
}
|
|
2589
|
+
const lines = models.map((m) => ` ${m.id} — ${m.name}`).join(NL);
|
|
2590
|
+
console.log(theme.system(t('qwen.model_list', { v: lines })));
|
|
2256
2591
|
continue;
|
|
2257
2592
|
}
|
|
2258
2593
|
config.qwen.model = arg;
|
|
@@ -2294,6 +2629,14 @@ async function main() {
|
|
|
2294
2629
|
console.log(theme.system(t('sessions.none')));
|
|
2295
2630
|
}
|
|
2296
2631
|
else {
|
|
2632
|
+
// In the TUI, open a SELECT LIST and resume the chosen session
|
|
2633
|
+
// directly (same behavior as /chats), instead of only listing ids.
|
|
2634
|
+
const canPick = editor && 'isTui' in editor && config.ui.tui && !filter;
|
|
2635
|
+
if (canPick && editor) {
|
|
2636
|
+
const opened = await pickSessionInteractively(all);
|
|
2637
|
+
if (opened)
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2297
2640
|
all.forEach((s, i) => {
|
|
2298
2641
|
const n = String(i + 1).padStart(2, ' ');
|
|
2299
2642
|
const mark = s.id === currentChatId ? theme.user(' *') : '';
|
|
@@ -2377,8 +2720,20 @@ async function main() {
|
|
|
2377
2720
|
continue;
|
|
2378
2721
|
}
|
|
2379
2722
|
if (lower === '/goal' || lower.startsWith('/goal ')) {
|
|
2380
|
-
await handleGoal(trimmed);
|
|
2381
|
-
|
|
2723
|
+
const sub = await handleGoal(trimmed);
|
|
2724
|
+
// Claude Code parity: setting a goal STARTS a turn immediately, with the
|
|
2725
|
+
// condition itself as the directive (no separate prompt needed). The
|
|
2726
|
+
// completion loop in runTask() then keeps working until the evaluator
|
|
2727
|
+
// reports MET/IMPOSSIBLE. We fall through to the regular task path with
|
|
2728
|
+
// an override task text instead of returning to the prompt.
|
|
2729
|
+
if (sub === 'set' && sessionGoal) {
|
|
2730
|
+
taskOverride =
|
|
2731
|
+
'Work toward this completion goal until it is satisfied. ' +
|
|
2732
|
+
'Do not call respond with a final summary until the condition is met.';
|
|
2733
|
+
}
|
|
2734
|
+
else {
|
|
2735
|
+
continue;
|
|
2736
|
+
}
|
|
2382
2737
|
}
|
|
2383
2738
|
if (lower === '/loop' || lower.startsWith('/loop ')) {
|
|
2384
2739
|
const rest = trimmed.slice('/loop'.length).trim();
|
|
@@ -2662,7 +3017,7 @@ async function main() {
|
|
|
2662
3017
|
tools: initTools,
|
|
2663
3018
|
task: initTask,
|
|
2664
3019
|
workdir: currentWorkdir,
|
|
2665
|
-
maxIterations:
|
|
3020
|
+
maxIterations: currentMaxIter(),
|
|
2666
3021
|
freshChat: freshChatNext,
|
|
2667
3022
|
sendSystemPrompt: sendSystemPromptNext,
|
|
2668
3023
|
transcript,
|
|
@@ -2761,7 +3116,7 @@ async function main() {
|
|
|
2761
3116
|
console.log(theme.system(t('status.dev', {
|
|
2762
3117
|
v: devMode ? t('common.on') : t('common.off'),
|
|
2763
3118
|
})));
|
|
2764
|
-
console.log(theme.system(t('status.max_iter', { v:
|
|
3119
|
+
console.log(theme.system(t('status.max_iter', { v: currentMaxIter() })));
|
|
2765
3120
|
console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
|
|
2766
3121
|
console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
|
|
2767
3122
|
const statusToggles = browser.getToggleStatesSync();
|
|
@@ -2848,6 +3203,53 @@ async function main() {
|
|
|
2848
3203
|
}
|
|
2849
3204
|
continue;
|
|
2850
3205
|
}
|
|
3206
|
+
if (lower === '/copy-code' || lower.startsWith('/copy-code ')) {
|
|
3207
|
+
const arg = trimmed.slice('/copy-code'.length).trim();
|
|
3208
|
+
const blocks = extractCodeBlocks(lastAssistantMessage ?? '');
|
|
3209
|
+
if (!blocks.length) {
|
|
3210
|
+
console.log(theme.dim(t('copy.no_code')));
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (arg === 'list') {
|
|
3214
|
+
console.log(theme.system(t('copy_code.list')));
|
|
3215
|
+
for (const b of blocks) {
|
|
3216
|
+
console.log(theme.assistant(t('copy_code.item', {
|
|
3217
|
+
n: String(b.index),
|
|
3218
|
+
lang: b.lang || 'text',
|
|
3219
|
+
lines: String(b.text.split(String.fromCharCode(10)).length),
|
|
3220
|
+
})));
|
|
3221
|
+
}
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
// Prefer the TUI copy path (it reports into the log); fall back to a
|
|
3225
|
+
// direct clipboard write for the legacy editor / non-TTY.
|
|
3226
|
+
const n = arg ? Number(arg) : undefined;
|
|
3227
|
+
if (n !== undefined && (!Number.isInteger(n) || n < 1)) {
|
|
3228
|
+
console.log(theme.warn(t('copy.bad_index', { n: arg })));
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
if (editor &&
|
|
3232
|
+
'copyCode' in editor &&
|
|
3233
|
+
typeof editor.copyCode === 'function') {
|
|
3234
|
+
editor.copyCode(n);
|
|
3235
|
+
continue;
|
|
3236
|
+
}
|
|
3237
|
+
const pick = n !== undefined
|
|
3238
|
+
? blocks.find((b) => b.index === n)
|
|
3239
|
+
: blocks[blocks.length - 1];
|
|
3240
|
+
if (!pick) {
|
|
3241
|
+
console.log(theme.warn(t('copy.bad_index', { n: String(n) })));
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
const res2 = writeClipboardText(pick.text);
|
|
3245
|
+
if (res2.ok) {
|
|
3246
|
+
console.log(theme.assistant(t('copy.done', { tool: res2.via })));
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
console.error(theme.warn(t('copy.failed', { tool: res2.via })));
|
|
3250
|
+
}
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
2851
3253
|
if (lower === '/retry') {
|
|
2852
3254
|
if (!lastTaskText) {
|
|
2853
3255
|
console.log(theme.dim(t('retry.none')));
|
|
@@ -3351,13 +3753,16 @@ async function main() {
|
|
|
3351
3753
|
}
|
|
3352
3754
|
continue;
|
|
3353
3755
|
}
|
|
3756
|
+
// A `/goal <condition>` turn is injected directly: skip the slash-command
|
|
3757
|
+
// resolution (the text is NOT a command) and use the override as the task.
|
|
3758
|
+
const isGoalOverride = taskOverride !== null;
|
|
3354
3759
|
// Custom command or skill invoked as a slash command? Expand it into a
|
|
3355
3760
|
// task. Skills are instructions the agent follows; custom commands are
|
|
3356
3761
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3357
3762
|
let expandedTask = null;
|
|
3358
3763
|
let missingArgs = null;
|
|
3359
3764
|
let skillAllowedTools = null;
|
|
3360
|
-
if (lower.startsWith('/')) {
|
|
3765
|
+
if (lower.startsWith('/') && !isGoalOverride) {
|
|
3361
3766
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3362
3767
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3363
3768
|
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
@@ -3376,12 +3781,16 @@ async function main() {
|
|
|
3376
3781
|
console.error(theme.error(t('msg.missing_args', { v: missingArgs.join(', ') })));
|
|
3377
3782
|
continue;
|
|
3378
3783
|
}
|
|
3379
|
-
if (lower.startsWith('/') && expandedTask === null) {
|
|
3784
|
+
if (lower.startsWith('/') && expandedTask === null && !isGoalOverride) {
|
|
3380
3785
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
3381
3786
|
continue;
|
|
3382
3787
|
}
|
|
3383
3788
|
// ---- Regular task (including in review mode) ----
|
|
3384
|
-
const taskText =
|
|
3789
|
+
const taskText = isGoalOverride
|
|
3790
|
+
? taskOverride
|
|
3791
|
+
: expandedTask !== null
|
|
3792
|
+
? expandedTask
|
|
3793
|
+
: trimmed;
|
|
3385
3794
|
// B5: inline any `@path` references into the task so the agent does not
|
|
3386
3795
|
// spend a round-trip reading a file the operator already pointed at. Only
|
|
3387
3796
|
// existing files are inlined; a bare `@name` in prose is left untouched.
|
|
@@ -3506,6 +3915,19 @@ async function main() {
|
|
|
3506
3915
|
contextLimit: config.ui.contextLimit,
|
|
3507
3916
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3508
3917
|
goal: sessionGoal,
|
|
3918
|
+
// Completion-goal mode (Claude Code parity): the goal is a CONDITION,
|
|
3919
|
+
// judged by a fresh model after each turn; while NOT met the loop
|
|
3920
|
+
// keeps working. goalQuery re-reads sessionGoal so a mid-run
|
|
3921
|
+
// `/goal clear` stops the auto-continuation immediately.
|
|
3922
|
+
goalIsCondition: true,
|
|
3923
|
+
goalQuery: () => sessionGoal,
|
|
3924
|
+
onGoalCleared: async () => {
|
|
3925
|
+
// The evaluator ended the goal (MET or IMPOSSIBLE). Clear the
|
|
3926
|
+
// persisted condition so it does not re-arm on the next task; the
|
|
3927
|
+
// run-task loop already surfaced the verdict line to the operator.
|
|
3928
|
+
sessionGoal = null;
|
|
3929
|
+
await saveGoal(currentWorkdir, null);
|
|
3930
|
+
},
|
|
3509
3931
|
todosQuery: () => todosSummary(),
|
|
3510
3932
|
selfImprovement: devMode,
|
|
3511
3933
|
onSubagent: makeSubagentRunner(),
|