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.
- package/CHANGELOG.md +24 -0
- package/README.md +13 -7
- package/dist/banner.js +71 -0
- package/dist/browser.js +84 -27
- package/dist/code-blocks.js +48 -0
- package/dist/commands.js +1 -0
- package/dist/config-live.js +53 -0
- package/dist/config.js +38 -0
- package/dist/goal.js +81 -0
- package/dist/i18n/common.js +31 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +22 -2
- package/dist/i18n/qwen.js +4 -0
- package/dist/i18n/self-review.js +8 -0
- package/dist/i18n/spinner.js +6 -0
- package/dist/index.js +478 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +13 -0
- package/dist/input.js +14 -3
- package/dist/net-capture.js +18 -0
- package/dist/qwen-browser.js +43 -20
- package/dist/run-task.js +97 -4
- package/dist/theme.js +7 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +304 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +85 -0
- package/dist/tui/InputLine.js +285 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +50 -0
- package/dist/tui/StatusBar.js +140 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +28 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +297 -0
- package/dist/tui/keys.js +162 -0
- package/dist/tui/log-window.js +93 -0
- package/dist/tui/login-pure.js +20 -0
- package/dist/tui/run-config-menu.js +19 -0
- package/dist/tui/run-confirm.js +22 -0
- package/dist/tui/run-login.js +43 -0
- package/dist/tui/run-select.js +18 -0
- package/dist/tui/store.js +104 -0
- package/dist/tui/types.js +10 -0
- package/dist/tui/useMouseWheel.js +113 -0
- package/dist/tui/widgets.js +55 -0
- package/package.json +6 -3
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-probe.tsx +62 -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);
|
|
@@ -780,7 +791,7 @@ function runTaskDeps() {
|
|
|
780
791
|
t,
|
|
781
792
|
locale: currentLocale,
|
|
782
793
|
debug,
|
|
783
|
-
maxIter,
|
|
794
|
+
maxIter: currentMaxIter(),
|
|
784
795
|
config,
|
|
785
796
|
runAgentLoop: mod.runAgentLoop,
|
|
786
797
|
createSpinner: mod.createSpinner,
|
|
@@ -817,8 +828,21 @@ async function main() {
|
|
|
817
828
|
console.error(theme.error(t('msg.workdir_error')), e.message);
|
|
818
829
|
process.exit(1);
|
|
819
830
|
}
|
|
820
|
-
|
|
821
|
-
|
|
831
|
+
// A styled startup banner in an interactive terminal; a plain two-line
|
|
832
|
+
// header when the output is piped (scripts/CI parse it).
|
|
833
|
+
if (process.stdout.isTTY && !jsonMode && config.ui.tui) {
|
|
834
|
+
console.log(renderStartupBanner({
|
|
835
|
+
title: 'zames',
|
|
836
|
+
version: pkgVersion || '?',
|
|
837
|
+
workdir: currentWorkdir,
|
|
838
|
+
provider: providerLabel(config.provider),
|
|
839
|
+
hint: t('msg.banner_hint'),
|
|
840
|
+
}, contentWidth()));
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
console.log(theme.system(t('msg.working_dir', { v: currentWorkdir })));
|
|
844
|
+
console.log(theme.system(t('msg.version', { v: pkgVersion || '?' })));
|
|
845
|
+
}
|
|
822
846
|
const transcript = new Transcript({
|
|
823
847
|
dir: config.transcript.dir,
|
|
824
848
|
enabled: config.transcript.enabled,
|
|
@@ -1193,6 +1217,10 @@ async function main() {
|
|
|
1193
1217
|
// the typed text is never overwritten by output. In non-TTY (pipe) —
|
|
1194
1218
|
// the old promptOnce.
|
|
1195
1219
|
let editor = null;
|
|
1220
|
+
// Re-wire the browser login callbacks after the editor exists / a provider
|
|
1221
|
+
// swap. Assigned where the editor is built (see the login wiring below);
|
|
1222
|
+
// a no-op until then so /provider before setup is harmless.
|
|
1223
|
+
let wireLoginCallbacks = () => { };
|
|
1196
1224
|
// N37 statusLine hook timer (cleared on exit).
|
|
1197
1225
|
let statusLineTimer = null;
|
|
1198
1226
|
let waiter = null;
|
|
@@ -1218,19 +1246,36 @@ async function main() {
|
|
|
1218
1246
|
};
|
|
1219
1247
|
await refreshDynamicCommands(currentWorkdir);
|
|
1220
1248
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
1221
|
-
const ed =
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1249
|
+
const ed = config.ui.tui
|
|
1250
|
+
? new TuiEditor({
|
|
1251
|
+
prompt: buildPrompt(),
|
|
1252
|
+
commands: buildSlashCommands(),
|
|
1253
|
+
locale: currentLocale,
|
|
1254
|
+
alternateScreen: config.ui.alternateScreen,
|
|
1255
|
+
})
|
|
1256
|
+
: new LineEditor({
|
|
1257
|
+
prompt: buildPrompt(),
|
|
1258
|
+
commands: buildSlashCommands(),
|
|
1259
|
+
locale: currentLocale,
|
|
1260
|
+
});
|
|
1226
1261
|
editor = ed;
|
|
1227
1262
|
ed.setTmpDir(TMP_DIR);
|
|
1228
1263
|
// Provider badge: show which backend is answering ([Qwen]/[DeepSeek]).
|
|
1229
1264
|
ed.setProvider(providerLabel(config.provider));
|
|
1265
|
+
// Header for full-screen mode (title / version / working directory).
|
|
1266
|
+
ed.setHeader?.({
|
|
1267
|
+
title: 'zames',
|
|
1268
|
+
version: pkgVersion || '',
|
|
1269
|
+
workdir: currentWorkdir,
|
|
1270
|
+
mouseScroll: config.ui.mouseScroll,
|
|
1271
|
+
});
|
|
1230
1272
|
// Restore the input history from the previous runs and persist every new
|
|
1231
1273
|
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1232
1274
|
ed.history = loadHistory();
|
|
1233
|
-
|
|
1275
|
+
// The legacy LineEditor keeps its own browse index; the TUI seeds it from
|
|
1276
|
+
// the history itself.
|
|
1277
|
+
if (ed instanceof LineEditor)
|
|
1278
|
+
ed._histIndex = ed.history.length;
|
|
1234
1279
|
ed.onHistoryChange = (h) => saveHistory(h);
|
|
1235
1280
|
// The token context right-aligned on the status line (above the input).
|
|
1236
1281
|
// The editor pulls the number on every render, so it follows the live
|
|
@@ -1247,6 +1292,81 @@ async function main() {
|
|
|
1247
1292
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
1248
1293
|
// counter. The editor pulls the live state on every render.
|
|
1249
1294
|
ed.onToggleQuery = () => browser.getToggleStatesSync();
|
|
1295
|
+
// Interactive login goes through the Ink form when the TUI owns the
|
|
1296
|
+
// terminal: the browser's readline/askPassword would PAUSE the whole TUI
|
|
1297
|
+
// and fight Ink for stdin. A NAMED function so /provider can re-wire the
|
|
1298
|
+
// freshly created browser (the factory runs before `editor` exists, so the
|
|
1299
|
+
// callbacks cannot be passed there without a forward reference).
|
|
1300
|
+
wireLoginCallbacks = (b) => {
|
|
1301
|
+
// The DeepSeek and Qwen browsers expose DIFFERENT login callback shapes
|
|
1302
|
+
// (DeepSeek: login+password / MFA; Qwen: email -> one-time code), so the
|
|
1303
|
+
// wiring is branched on the provider rather than forced through one
|
|
1304
|
+
// shape.
|
|
1305
|
+
const bb = b;
|
|
1306
|
+
if (config.provider === 'qwen') {
|
|
1307
|
+
bb.onLoginPrompt = async (info) => {
|
|
1308
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1309
|
+
return null;
|
|
1310
|
+
ed.pause();
|
|
1311
|
+
try {
|
|
1312
|
+
const { runTextPrompt } = await import('./tui/run-login.js');
|
|
1313
|
+
const isCode = info.step === 'code';
|
|
1314
|
+
return await runTextPrompt({
|
|
1315
|
+
t,
|
|
1316
|
+
label: isCode
|
|
1317
|
+
? t('qwen.login_code_prompt')
|
|
1318
|
+
: t('qwen.login_email_prompt', {
|
|
1319
|
+
def: info.savedUsername
|
|
1320
|
+
? ' [' + info.savedUsername + ']'
|
|
1321
|
+
: '',
|
|
1322
|
+
}),
|
|
1323
|
+
secret: isCode,
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
finally {
|
|
1327
|
+
ed.resume();
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
bb.onLoginPrompt = async (info) => {
|
|
1333
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1334
|
+
return null;
|
|
1335
|
+
ed.pause();
|
|
1336
|
+
try {
|
|
1337
|
+
const { runLoginForm } = await import('./tui/run-login.js');
|
|
1338
|
+
return await runLoginForm({
|
|
1339
|
+
t,
|
|
1340
|
+
initialUsername: config.browser.auth.username,
|
|
1341
|
+
initialPassword: config.browser.auth.password,
|
|
1342
|
+
note: info.hasSavedPassword || info.savedUsername
|
|
1343
|
+
? t('auth.form.note_saved')
|
|
1344
|
+
: undefined,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
finally {
|
|
1348
|
+
ed.resume();
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
bb.onMfaPrompt = async () => {
|
|
1353
|
+
if (!config.ui.tui || !(ed instanceof TuiEditor))
|
|
1354
|
+
return null;
|
|
1355
|
+
ed.pause();
|
|
1356
|
+
try {
|
|
1357
|
+
const { runTextPrompt } = await import('./tui/run-login.js');
|
|
1358
|
+
return await runTextPrompt({
|
|
1359
|
+
t,
|
|
1360
|
+
label: t('auth.mfa_prompt'),
|
|
1361
|
+
secret: true,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
finally {
|
|
1365
|
+
ed.resume();
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
};
|
|
1369
|
+
wireLoginCallbacks(browser);
|
|
1250
1370
|
// N37 statusLine hook: run the configured command on its own timer (never
|
|
1251
1371
|
// per-render — a spawn would block typing) and show its first stdout line
|
|
1252
1372
|
// in the status line. Off when ui.statusLineCommand is empty.
|
|
@@ -1363,6 +1483,7 @@ async function main() {
|
|
|
1363
1483
|
}) + theme.dim(' (' + res.via + ')')));
|
|
1364
1484
|
return att;
|
|
1365
1485
|
};
|
|
1486
|
+
ed.onCopyText = (text) => writeClipboardText(text);
|
|
1366
1487
|
ed.onSubmit = (text, items) => {
|
|
1367
1488
|
// While the agent is busy the main command loop is blocked on
|
|
1368
1489
|
// `await runTask()`, so ONLY commands that make sense without the main
|
|
@@ -1473,7 +1594,16 @@ async function main() {
|
|
|
1473
1594
|
}
|
|
1474
1595
|
}
|
|
1475
1596
|
};
|
|
1476
|
-
|
|
1597
|
+
try {
|
|
1598
|
+
ed.start();
|
|
1599
|
+
}
|
|
1600
|
+
catch (e) {
|
|
1601
|
+
// A TUI mount failure must not kill the agent. We surface it and let the
|
|
1602
|
+
// operator retry with `ui.tui: false` if it persists; the imperative
|
|
1603
|
+
// callbacks stay bound to the same editor object, so no state is lost.
|
|
1604
|
+
console.error(theme.warn('UI start failed (set ui.tui=false to use the legacy editor): ' +
|
|
1605
|
+
String(e)));
|
|
1606
|
+
}
|
|
1477
1607
|
// All command output (console.log/error) must go ABOVE the input line,
|
|
1478
1608
|
// otherwise it overwrites the text being typed. While the editor is
|
|
1479
1609
|
// active, we wrap both streams in ed.printAbove.
|
|
@@ -1560,6 +1690,63 @@ async function main() {
|
|
|
1560
1690
|
editor.refreshStatus();
|
|
1561
1691
|
}
|
|
1562
1692
|
}
|
|
1693
|
+
// Browser TIMING knobs (timeouts, retries, the send throttle) are COPIED
|
|
1694
|
+
// by the browser at construction, so push them onto the live instance.
|
|
1695
|
+
// (deepThinking/webSearch are handled above because they also repaint the
|
|
1696
|
+
// status icons.)
|
|
1697
|
+
if (path.startsWith('browser.')) {
|
|
1698
|
+
const b = browser;
|
|
1699
|
+
if (applyBrowserLiveConfig(path, value, b)) {
|
|
1700
|
+
// minSendIntervalMs feeds the send countdown the UI renders.
|
|
1701
|
+
if (editor)
|
|
1702
|
+
editor.refreshStatus();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
// The undo/checkpoint stores also copy their config at construction.
|
|
1706
|
+
applyStoreLiveConfig(path, value, {
|
|
1707
|
+
undo: undo,
|
|
1708
|
+
checkpoints: checkpoints,
|
|
1709
|
+
});
|
|
1710
|
+
// Qwen timing knobs feed the QwenBrowser constructor. Its LIVE fields use
|
|
1711
|
+
// the SAME names, so push them there when the active backend is Qwen (a
|
|
1712
|
+
// no-op on DeepSeek, which has no qwen-only knobs).
|
|
1713
|
+
if (path === 'qwen.minSendIntervalMs' || path === 'qwen.answerTimeoutMs') {
|
|
1714
|
+
const b = browser;
|
|
1715
|
+
if (config.provider === 'qwen') {
|
|
1716
|
+
const field = path === 'qwen.minSendIntervalMs'
|
|
1717
|
+
? 'minSendIntervalMs'
|
|
1718
|
+
: 'answerTimeoutMs';
|
|
1719
|
+
if (field in b) {
|
|
1720
|
+
b[field] = value;
|
|
1721
|
+
if (editor)
|
|
1722
|
+
editor.refreshStatus();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
// Switching the provider from /config cannot be applied live (it needs a
|
|
1727
|
+
// new browser); tell the operator instead of silently doing nothing.
|
|
1728
|
+
if (path === 'provider') {
|
|
1729
|
+
const line = t('cfg.provider_restart');
|
|
1730
|
+
if (editor)
|
|
1731
|
+
editor.printAbove(theme.warn(line));
|
|
1732
|
+
else
|
|
1733
|
+
console.log(theme.warn(line));
|
|
1734
|
+
}
|
|
1735
|
+
// headless is fixed at launch (the browser is already open); a /config set
|
|
1736
|
+
// only takes effect on the next run.
|
|
1737
|
+
if (path === 'headless') {
|
|
1738
|
+
const line = t('cfg.restart_required');
|
|
1739
|
+
if (editor)
|
|
1740
|
+
editor.printAbove(theme.dim(line));
|
|
1741
|
+
else
|
|
1742
|
+
console.log(theme.dim(line));
|
|
1743
|
+
}
|
|
1744
|
+
// transcript.enabled: the Transcript copies the flag at construction. A
|
|
1745
|
+
// false -> true flip cannot open a NEW stream mid-run, but true -> false
|
|
1746
|
+
// can (and should) stop writing immediately.
|
|
1747
|
+
if (path === 'transcript.enabled' && value === false) {
|
|
1748
|
+
transcript.enabled = false;
|
|
1749
|
+
}
|
|
1563
1750
|
}
|
|
1564
1751
|
// Apply `/thinking` / `/web` immediately: flip the browser's DESIRED state
|
|
1565
1752
|
// and the status icons, and persist to config (so it survives a restart).
|
|
@@ -1598,11 +1785,13 @@ async function main() {
|
|
|
1598
1785
|
// Handle a `/goal` command shared by the main loop and the live interception
|
|
1599
1786
|
// (so a goal can be set even while the agent is busy — it only touches a
|
|
1600
1787
|
// variable and a file, never the chat). Output goes above the input line
|
|
1601
|
-
// when the editor is active, otherwise to stdout.
|
|
1788
|
+
// when the editor is active, otherwise to stdout. Returns which subcommand
|
|
1789
|
+
// ran so the main loop can START A TURN when a goal is freshly set (Claude
|
|
1790
|
+
// Code parity: setting a goal begins working toward it immediately).
|
|
1602
1791
|
async function handleGoal(input) {
|
|
1603
1792
|
const g = mod.commands.parseGoalCommand(input);
|
|
1604
1793
|
if (!g)
|
|
1605
|
-
return;
|
|
1794
|
+
return null;
|
|
1606
1795
|
const emit = (line) => {
|
|
1607
1796
|
if (editor)
|
|
1608
1797
|
editor.printAbove(line);
|
|
@@ -1613,16 +1802,19 @@ async function main() {
|
|
|
1613
1802
|
sessionGoal = null;
|
|
1614
1803
|
await saveGoal(currentWorkdir, null);
|
|
1615
1804
|
emit(theme.system(t('goal.cleared')));
|
|
1805
|
+
return 'cleared';
|
|
1616
1806
|
}
|
|
1617
1807
|
else if (g.sub === 'show') {
|
|
1618
1808
|
emit(sessionGoal
|
|
1619
1809
|
? theme.system(t('goal.current', { goal: sessionGoal }))
|
|
1620
1810
|
: theme.dim(t('goal.none')));
|
|
1811
|
+
return 'shown';
|
|
1621
1812
|
}
|
|
1622
1813
|
else {
|
|
1623
|
-
sessionGoal = g.goal;
|
|
1624
|
-
await saveGoal(currentWorkdir,
|
|
1625
|
-
emit(theme.system(t('goal.set', { goal:
|
|
1814
|
+
sessionGoal = clampGoalCondition(g.goal);
|
|
1815
|
+
await saveGoal(currentWorkdir, sessionGoal);
|
|
1816
|
+
emit(theme.system(t('goal.set', { goal: sessionGoal })));
|
|
1817
|
+
return 'set';
|
|
1626
1818
|
}
|
|
1627
1819
|
}
|
|
1628
1820
|
function configSetRaw(field, raw) {
|
|
@@ -1650,19 +1842,27 @@ async function main() {
|
|
|
1650
1842
|
return;
|
|
1651
1843
|
}
|
|
1652
1844
|
// The menu draws directly to stdout and reads keys itself. So that its
|
|
1653
|
-
// output doesn't overlap the
|
|
1845
|
+
// output doesn't overlap the editor's permanent input line, we "pause"
|
|
1654
1846
|
// the editor for the duration of the menu and restore it afterwards.
|
|
1655
1847
|
if (editor)
|
|
1656
1848
|
editor.pause();
|
|
1849
|
+
const menuOpts = {
|
|
1850
|
+
fields: CONFIG_SCHEMA,
|
|
1851
|
+
t,
|
|
1852
|
+
get: (path) => getByPath(config, path),
|
|
1853
|
+
getDefault: (path) => getByPath(DEFAULTS, path),
|
|
1854
|
+
set: (field, raw) => configSetRaw(field, raw),
|
|
1855
|
+
reset: (field) => configResetField(field),
|
|
1856
|
+
};
|
|
1657
1857
|
try {
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1858
|
+
// Prefer the Ink menu (nicer, resize-aware) when the TUI is enabled;
|
|
1859
|
+
// fall back to the legacy hand-rolled menu otherwise.
|
|
1860
|
+
if (config.ui.tui) {
|
|
1861
|
+
await runConfigMenuInk(menuOpts);
|
|
1862
|
+
}
|
|
1863
|
+
else {
|
|
1864
|
+
await runConfigMenu(menuOpts);
|
|
1865
|
+
}
|
|
1666
1866
|
}
|
|
1667
1867
|
catch (e) {
|
|
1668
1868
|
console.error(theme.error(String(e.message || e)));
|
|
@@ -1690,9 +1890,114 @@ async function main() {
|
|
|
1690
1890
|
projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
|
|
1691
1891
|
});
|
|
1692
1892
|
}
|
|
1893
|
+
// Open a chat by its ChatInfo and print the restored dialogue. Shared by
|
|
1894
|
+
// /resume and the interactive /chats list, so both paths behave identically
|
|
1895
|
+
// (todos reloaded, prompt-resend decision, transcript event).
|
|
1896
|
+
const openResumedChat = async (pick) => {
|
|
1897
|
+
if (editor)
|
|
1898
|
+
editor.lock(t('msg.input_locked'));
|
|
1899
|
+
try {
|
|
1900
|
+
await browser.openChat(pick.id);
|
|
1901
|
+
currentChatId = pick.id;
|
|
1902
|
+
freshChatNext = false;
|
|
1903
|
+
sendSystemPromptNext = promptOnResume();
|
|
1904
|
+
saveLastChat(pick.id, currentWorkdir, pick.title);
|
|
1905
|
+
transcript.log('resume_chat', { id: pick.id, title: pick.title });
|
|
1906
|
+
if (editor)
|
|
1907
|
+
editor.clearScreen();
|
|
1908
|
+
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1909
|
+
console.log(theme.assistant(t('msg.chat_opened')) +
|
|
1910
|
+
theme.system(sendSystemPromptNext
|
|
1911
|
+
? String.fromCharCode(10) + t('chats.prompt_will_resend')
|
|
1912
|
+
: String.fromCharCode(10) + t('chats.prompt_no_resend')));
|
|
1913
|
+
loadSessionTodos(pick.id);
|
|
1914
|
+
await printRestoredHistory(browser, editor, pick.id);
|
|
1915
|
+
}
|
|
1916
|
+
catch (e) {
|
|
1917
|
+
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
if (editor)
|
|
1921
|
+
editor.unlock();
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
// Show the recent chats in an Ink SelectList and open the chosen one.
|
|
1925
|
+
// Returns true when the operator picked a chat (the caller then skips the
|
|
1926
|
+
// text listing). Only called when the TUI owns the terminal.
|
|
1927
|
+
const pickChatInteractively = async (chats) => {
|
|
1928
|
+
if (!editor)
|
|
1929
|
+
return false;
|
|
1930
|
+
editor.pause();
|
|
1931
|
+
let picked = null;
|
|
1932
|
+
try {
|
|
1933
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
1934
|
+
picked = await selectListInk({
|
|
1935
|
+
title: t('chats.pick_title'),
|
|
1936
|
+
items: chats.map((c, i) => ({
|
|
1937
|
+
value: c.id,
|
|
1938
|
+
label: String(i + 1).padStart(2, ' ') + '. ' + c.title,
|
|
1939
|
+
detail: c.id.slice(0, 8) + '…',
|
|
1940
|
+
})),
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
catch {
|
|
1944
|
+
picked = null;
|
|
1945
|
+
}
|
|
1946
|
+
finally {
|
|
1947
|
+
editor.resume();
|
|
1948
|
+
}
|
|
1949
|
+
if (!picked)
|
|
1950
|
+
return false;
|
|
1951
|
+
const pick = chats.find((c) => c.id === picked);
|
|
1952
|
+
if (!pick)
|
|
1953
|
+
return false;
|
|
1954
|
+
await openResumedChat(pick);
|
|
1955
|
+
return true;
|
|
1956
|
+
};
|
|
1957
|
+
// Show the saved SESSIONS in an Ink SelectList and resume the chosen one.
|
|
1958
|
+
// Sessions carry the full chat id (unlike the /chats sidebar), so this is
|
|
1959
|
+
// the safe resume path; it reuses openResumedChat for identical behavior.
|
|
1960
|
+
const pickSessionInteractively = async (sessions) => {
|
|
1961
|
+
if (!editor)
|
|
1962
|
+
return false;
|
|
1963
|
+
editor.pause();
|
|
1964
|
+
let picked = null;
|
|
1965
|
+
try {
|
|
1966
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
1967
|
+
picked = await selectListInk({
|
|
1968
|
+
title: t('sessions.pick_title'),
|
|
1969
|
+
items: sessions.map((s, i) => ({
|
|
1970
|
+
value: s.id,
|
|
1971
|
+
label: String(i + 1).padStart(2, ' ') +
|
|
1972
|
+
'. ' +
|
|
1973
|
+
(s.title || s.id.slice(0, 8)),
|
|
1974
|
+
detail: s.id.slice(0, 8) +
|
|
1975
|
+
'…' +
|
|
1976
|
+
(s.workdir ? ' [' + dirLabel(s.workdir) + ']' : ''),
|
|
1977
|
+
})),
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
catch {
|
|
1981
|
+
picked = null;
|
|
1982
|
+
}
|
|
1983
|
+
finally {
|
|
1984
|
+
editor.resume();
|
|
1985
|
+
}
|
|
1986
|
+
if (!picked)
|
|
1987
|
+
return false;
|
|
1988
|
+
const pick = sessions.find((s) => s.id === picked);
|
|
1989
|
+
if (!pick)
|
|
1990
|
+
return false;
|
|
1991
|
+
await openResumedChat({ id: pick.id, title: pick.title || '' });
|
|
1992
|
+
return true;
|
|
1993
|
+
};
|
|
1693
1994
|
while (running) {
|
|
1694
1995
|
let input;
|
|
1695
1996
|
let inputAttachments = [];
|
|
1997
|
+
// Set by `/goal <condition>` to fall through into a task turn with this
|
|
1998
|
+
// text instead of returning to the prompt (Claude Code starts a turn the
|
|
1999
|
+
// moment a goal is set). Reset every iteration because it is declared here.
|
|
2000
|
+
let taskOverride = null;
|
|
1696
2001
|
try {
|
|
1697
2002
|
if (editor) {
|
|
1698
2003
|
editor.setPrompt(buildPrompt());
|
|
@@ -1898,7 +2203,7 @@ async function main() {
|
|
|
1898
2203
|
tools,
|
|
1899
2204
|
task: focus,
|
|
1900
2205
|
workdir: snapRoot,
|
|
1901
|
-
maxIterations:
|
|
2206
|
+
maxIterations: currentMaxIter(),
|
|
1902
2207
|
freshChat: false,
|
|
1903
2208
|
sendSystemPrompt: false,
|
|
1904
2209
|
transcript,
|
|
@@ -2003,6 +2308,15 @@ async function main() {
|
|
|
2003
2308
|
console.log(theme.system(t('chats.none_hint')));
|
|
2004
2309
|
}
|
|
2005
2310
|
else {
|
|
2311
|
+
// In the TUI, open a real SELECT LIST (like /model) instead of
|
|
2312
|
+
// dumping text: pick a chat and it opens directly. The text path
|
|
2313
|
+
// below stays for non-TTY / the legacy editor.
|
|
2314
|
+
const canPick = editor && 'isTui' in editor && config.ui.tui && !filter;
|
|
2315
|
+
if (canPick && editor) {
|
|
2316
|
+
const openChat = await pickChatInteractively(shown);
|
|
2317
|
+
if (openChat)
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2006
2320
|
console.log(theme.system(t('chats.recent')));
|
|
2007
2321
|
// The number is the index in the FULL list, so `/resume <n>` keeps
|
|
2008
2322
|
// pointing at the same chat even when a filter narrows the view.
|
|
@@ -2044,33 +2358,7 @@ async function main() {
|
|
|
2044
2358
|
console.error(theme.error(t('chats.no_n', { n, total: lastChats.length })));
|
|
2045
2359
|
continue;
|
|
2046
2360
|
}
|
|
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
|
-
}
|
|
2361
|
+
await openResumedChat(pick);
|
|
2074
2362
|
continue;
|
|
2075
2363
|
}
|
|
2076
2364
|
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
@@ -2195,6 +2483,9 @@ async function main() {
|
|
|
2195
2483
|
// launch failed, `browser` would point at a dead instance and every
|
|
2196
2484
|
// later command would break.
|
|
2197
2485
|
const nextBrowser = createProviderBrowser(arg);
|
|
2486
|
+
// Wire the Ink login BEFORE waitForLogin(), or the new backend would
|
|
2487
|
+
// fall back to readline and pause the TUI for its sign-in.
|
|
2488
|
+
wireLoginCallbacks(nextBrowser);
|
|
2198
2489
|
await nextBrowser.launch();
|
|
2199
2490
|
await nextBrowser.waitForLogin();
|
|
2200
2491
|
const oldBrowser = browser;
|
|
@@ -2248,11 +2539,46 @@ async function main() {
|
|
|
2248
2539
|
const models = qb.listModels ? await qb.listModels() : [];
|
|
2249
2540
|
if (!models.length) {
|
|
2250
2541
|
console.log(theme.system(t('qwen.model_list', { v: '(none)' })));
|
|
2542
|
+
continue;
|
|
2251
2543
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2544
|
+
// In the TUI, open a proper SELECT LIST instead of dumping text.
|
|
2545
|
+
if (editor && 'isTui' in editor && config.ui.tui) {
|
|
2546
|
+
if (editor.pause)
|
|
2547
|
+
editor.pause();
|
|
2548
|
+
let picked;
|
|
2549
|
+
try {
|
|
2550
|
+
const { selectListInk } = await import('./tui/run-select.js');
|
|
2551
|
+
picked = await selectListInk({
|
|
2552
|
+
title: t('qwen.model_pick_title'),
|
|
2553
|
+
initial: config.qwen.model || models[0].id,
|
|
2554
|
+
items: models.map((m) => ({
|
|
2555
|
+
value: m.id,
|
|
2556
|
+
label: m.name || m.id,
|
|
2557
|
+
detail: m.id === config.qwen.model ? '← текущая' : m.id,
|
|
2558
|
+
})),
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
catch {
|
|
2562
|
+
picked = null;
|
|
2563
|
+
}
|
|
2564
|
+
finally {
|
|
2565
|
+
if (editor.resume)
|
|
2566
|
+
editor.resume();
|
|
2567
|
+
}
|
|
2568
|
+
if (picked) {
|
|
2569
|
+
config.qwen.model = picked;
|
|
2570
|
+
if (typeof qb.model === 'string')
|
|
2571
|
+
qb.model = picked;
|
|
2572
|
+
try {
|
|
2573
|
+
writeConfigValue('home', 'qwen.model', picked);
|
|
2574
|
+
}
|
|
2575
|
+
catch { }
|
|
2576
|
+
console.log(theme.system(t('qwen.model_set', { v: picked })));
|
|
2577
|
+
}
|
|
2578
|
+
continue;
|
|
2255
2579
|
}
|
|
2580
|
+
const lines = models.map((m) => ` ${m.id} — ${m.name}`).join(NL);
|
|
2581
|
+
console.log(theme.system(t('qwen.model_list', { v: lines })));
|
|
2256
2582
|
continue;
|
|
2257
2583
|
}
|
|
2258
2584
|
config.qwen.model = arg;
|
|
@@ -2294,6 +2620,14 @@ async function main() {
|
|
|
2294
2620
|
console.log(theme.system(t('sessions.none')));
|
|
2295
2621
|
}
|
|
2296
2622
|
else {
|
|
2623
|
+
// In the TUI, open a SELECT LIST and resume the chosen session
|
|
2624
|
+
// directly (same behavior as /chats), instead of only listing ids.
|
|
2625
|
+
const canPick = editor && 'isTui' in editor && config.ui.tui && !filter;
|
|
2626
|
+
if (canPick && editor) {
|
|
2627
|
+
const opened = await pickSessionInteractively(all);
|
|
2628
|
+
if (opened)
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2297
2631
|
all.forEach((s, i) => {
|
|
2298
2632
|
const n = String(i + 1).padStart(2, ' ');
|
|
2299
2633
|
const mark = s.id === currentChatId ? theme.user(' *') : '';
|
|
@@ -2377,8 +2711,20 @@ async function main() {
|
|
|
2377
2711
|
continue;
|
|
2378
2712
|
}
|
|
2379
2713
|
if (lower === '/goal' || lower.startsWith('/goal ')) {
|
|
2380
|
-
await handleGoal(trimmed);
|
|
2381
|
-
|
|
2714
|
+
const sub = await handleGoal(trimmed);
|
|
2715
|
+
// Claude Code parity: setting a goal STARTS a turn immediately, with the
|
|
2716
|
+
// condition itself as the directive (no separate prompt needed). The
|
|
2717
|
+
// completion loop in runTask() then keeps working until the evaluator
|
|
2718
|
+
// reports MET/IMPOSSIBLE. We fall through to the regular task path with
|
|
2719
|
+
// an override task text instead of returning to the prompt.
|
|
2720
|
+
if (sub === 'set' && sessionGoal) {
|
|
2721
|
+
taskOverride =
|
|
2722
|
+
'Work toward this completion goal until it is satisfied. ' +
|
|
2723
|
+
'Do not call respond with a final summary until the condition is met.';
|
|
2724
|
+
}
|
|
2725
|
+
else {
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2382
2728
|
}
|
|
2383
2729
|
if (lower === '/loop' || lower.startsWith('/loop ')) {
|
|
2384
2730
|
const rest = trimmed.slice('/loop'.length).trim();
|
|
@@ -2662,7 +3008,7 @@ async function main() {
|
|
|
2662
3008
|
tools: initTools,
|
|
2663
3009
|
task: initTask,
|
|
2664
3010
|
workdir: currentWorkdir,
|
|
2665
|
-
maxIterations:
|
|
3011
|
+
maxIterations: currentMaxIter(),
|
|
2666
3012
|
freshChat: freshChatNext,
|
|
2667
3013
|
sendSystemPrompt: sendSystemPromptNext,
|
|
2668
3014
|
transcript,
|
|
@@ -2761,7 +3107,7 @@ async function main() {
|
|
|
2761
3107
|
console.log(theme.system(t('status.dev', {
|
|
2762
3108
|
v: devMode ? t('common.on') : t('common.off'),
|
|
2763
3109
|
})));
|
|
2764
|
-
console.log(theme.system(t('status.max_iter', { v:
|
|
3110
|
+
console.log(theme.system(t('status.max_iter', { v: currentMaxIter() })));
|
|
2765
3111
|
console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
|
|
2766
3112
|
console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
|
|
2767
3113
|
const statusToggles = browser.getToggleStatesSync();
|
|
@@ -2848,6 +3194,53 @@ async function main() {
|
|
|
2848
3194
|
}
|
|
2849
3195
|
continue;
|
|
2850
3196
|
}
|
|
3197
|
+
if (lower === '/copy-code' || lower.startsWith('/copy-code ')) {
|
|
3198
|
+
const arg = trimmed.slice('/copy-code'.length).trim();
|
|
3199
|
+
const blocks = extractCodeBlocks(lastAssistantMessage ?? '');
|
|
3200
|
+
if (!blocks.length) {
|
|
3201
|
+
console.log(theme.dim(t('copy.no_code')));
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
if (arg === 'list') {
|
|
3205
|
+
console.log(theme.system(t('copy_code.list')));
|
|
3206
|
+
for (const b of blocks) {
|
|
3207
|
+
console.log(theme.assistant(t('copy_code.item', {
|
|
3208
|
+
n: String(b.index),
|
|
3209
|
+
lang: b.lang || 'text',
|
|
3210
|
+
lines: String(b.text.split(String.fromCharCode(10)).length),
|
|
3211
|
+
})));
|
|
3212
|
+
}
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
// Prefer the TUI copy path (it reports into the log); fall back to a
|
|
3216
|
+
// direct clipboard write for the legacy editor / non-TTY.
|
|
3217
|
+
const n = arg ? Number(arg) : undefined;
|
|
3218
|
+
if (n !== undefined && (!Number.isInteger(n) || n < 1)) {
|
|
3219
|
+
console.log(theme.warn(t('copy.bad_index', { n: arg })));
|
|
3220
|
+
continue;
|
|
3221
|
+
}
|
|
3222
|
+
if (editor &&
|
|
3223
|
+
'copyCode' in editor &&
|
|
3224
|
+
typeof editor.copyCode === 'function') {
|
|
3225
|
+
editor.copyCode(n);
|
|
3226
|
+
continue;
|
|
3227
|
+
}
|
|
3228
|
+
const pick = n !== undefined
|
|
3229
|
+
? blocks.find((b) => b.index === n)
|
|
3230
|
+
: blocks[blocks.length - 1];
|
|
3231
|
+
if (!pick) {
|
|
3232
|
+
console.log(theme.warn(t('copy.bad_index', { n: String(n) })));
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3235
|
+
const res2 = writeClipboardText(pick.text);
|
|
3236
|
+
if (res2.ok) {
|
|
3237
|
+
console.log(theme.assistant(t('copy.done', { tool: res2.via })));
|
|
3238
|
+
}
|
|
3239
|
+
else {
|
|
3240
|
+
console.error(theme.warn(t('copy.failed', { tool: res2.via })));
|
|
3241
|
+
}
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
2851
3244
|
if (lower === '/retry') {
|
|
2852
3245
|
if (!lastTaskText) {
|
|
2853
3246
|
console.log(theme.dim(t('retry.none')));
|
|
@@ -3351,13 +3744,16 @@ async function main() {
|
|
|
3351
3744
|
}
|
|
3352
3745
|
continue;
|
|
3353
3746
|
}
|
|
3747
|
+
// A `/goal <condition>` turn is injected directly: skip the slash-command
|
|
3748
|
+
// resolution (the text is NOT a command) and use the override as the task.
|
|
3749
|
+
const isGoalOverride = taskOverride !== null;
|
|
3354
3750
|
// Custom command or skill invoked as a slash command? Expand it into a
|
|
3355
3751
|
// task. Skills are instructions the agent follows; custom commands are
|
|
3356
3752
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3357
3753
|
let expandedTask = null;
|
|
3358
3754
|
let missingArgs = null;
|
|
3359
3755
|
let skillAllowedTools = null;
|
|
3360
|
-
if (lower.startsWith('/')) {
|
|
3756
|
+
if (lower.startsWith('/') && !isGoalOverride) {
|
|
3361
3757
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3362
3758
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3363
3759
|
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
@@ -3376,12 +3772,16 @@ async function main() {
|
|
|
3376
3772
|
console.error(theme.error(t('msg.missing_args', { v: missingArgs.join(', ') })));
|
|
3377
3773
|
continue;
|
|
3378
3774
|
}
|
|
3379
|
-
if (lower.startsWith('/') && expandedTask === null) {
|
|
3775
|
+
if (lower.startsWith('/') && expandedTask === null && !isGoalOverride) {
|
|
3380
3776
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
3381
3777
|
continue;
|
|
3382
3778
|
}
|
|
3383
3779
|
// ---- Regular task (including in review mode) ----
|
|
3384
|
-
const taskText =
|
|
3780
|
+
const taskText = isGoalOverride
|
|
3781
|
+
? taskOverride
|
|
3782
|
+
: expandedTask !== null
|
|
3783
|
+
? expandedTask
|
|
3784
|
+
: trimmed;
|
|
3385
3785
|
// B5: inline any `@path` references into the task so the agent does not
|
|
3386
3786
|
// spend a round-trip reading a file the operator already pointed at. Only
|
|
3387
3787
|
// existing files are inlined; a bare `@name` in prose is left untouched.
|
|
@@ -3506,6 +3906,19 @@ async function main() {
|
|
|
3506
3906
|
contextLimit: config.ui.contextLimit,
|
|
3507
3907
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3508
3908
|
goal: sessionGoal,
|
|
3909
|
+
// Completion-goal mode (Claude Code parity): the goal is a CONDITION,
|
|
3910
|
+
// judged by a fresh model after each turn; while NOT met the loop
|
|
3911
|
+
// keeps working. goalQuery re-reads sessionGoal so a mid-run
|
|
3912
|
+
// `/goal clear` stops the auto-continuation immediately.
|
|
3913
|
+
goalIsCondition: true,
|
|
3914
|
+
goalQuery: () => sessionGoal,
|
|
3915
|
+
onGoalCleared: async () => {
|
|
3916
|
+
// The evaluator ended the goal (MET or IMPOSSIBLE). Clear the
|
|
3917
|
+
// persisted condition so it does not re-arm on the next task; the
|
|
3918
|
+
// run-task loop already surfaced the verdict line to the operator.
|
|
3919
|
+
sessionGoal = null;
|
|
3920
|
+
await saveGoal(currentWorkdir, null);
|
|
3921
|
+
},
|
|
3509
3922
|
todosQuery: () => todosSummary(),
|
|
3510
3923
|
selfImprovement: devMode,
|
|
3511
3924
|
onSubagent: makeSubagentRunner(),
|