zames_pro 2.72.8 → 2.73.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 +84 -0
- package/README.md +111 -19
- package/dist/agent-loop.js +15 -5
- package/dist/browser-dom.js +21 -0
- package/dist/browser-pure.js +6 -6
- package/dist/browser.js +155 -12
- package/dist/commands.js +7 -0
- package/dist/config-command.js +118 -0
- package/dist/config.js +56 -1
- package/dist/context.js +47 -19
- package/dist/deepseek-ui.js +33 -0
- package/dist/i18n/common.js +0 -16
- package/dist/i18n/config.js +14 -18
- package/dist/i18n/deepseek.js +39 -39
- package/dist/i18n/doctor.js +0 -4
- package/dist/i18n/help.js +8 -8
- package/dist/i18n/prompt.js +0 -4
- package/dist/i18n/qwen.js +85 -0
- package/dist/i18n/self-review.js +0 -5
- package/dist/i18n.js +2 -0
- package/dist/index.js +273 -137
- package/dist/input.js +23 -0
- package/dist/provider-handoff.js +73 -0
- package/dist/providers.js +50 -0
- package/dist/qwen-browser.js +1668 -0
- package/dist/qwen-chats.js +183 -0
- package/dist/qwen-net-capture.js +221 -0
- package/dist/qwen-ui.js +160 -0
- package/dist/run-task.js +2 -1
- package/dist/sessions.js +56 -7
- package/dist/subagent.js +28 -3
- package/dist/system-prompt.js +15 -4
- package/dist/tools.js +19 -6
- package/dist/ui-health.js +27 -21
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +8 -1
- package/scripts/release.mts +9 -7
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@ import { existsSync, statSync } from 'fs';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { theme } from './theme.js';
|
|
8
8
|
import { DeepSeekBrowser, headlessUACacheReady } from './browser.js';
|
|
9
|
-
import {
|
|
9
|
+
import { QwenBrowser } from './qwen-browser.js';
|
|
10
|
+
import { isProviderId, asProviderId, providerChatUrlPrefix, providerLabel, } from './providers.js';
|
|
11
|
+
import { createTools, mergeTools, filterToolsForReadOnly, filterToolsByAllowList, } from './tools.js';
|
|
10
12
|
import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
|
|
11
13
|
import { runAgentLoop } from './agent-loop.js';
|
|
12
14
|
import { createSubagentRunner } from './subagent.js';
|
|
@@ -14,15 +16,17 @@ import { createSpinner } from './spinner.js';
|
|
|
14
16
|
import { LineEditor } from './input.js';
|
|
15
17
|
import { promptOnce } from './prompt.js';
|
|
16
18
|
import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
|
|
17
|
-
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
|
|
19
|
+
import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, providerAskDeadlineMs, } from './config.js';
|
|
18
20
|
import { runConfigMenu } from './config-menu.js';
|
|
19
|
-
import {
|
|
21
|
+
import { formatConfigList } from './config-commands.js';
|
|
22
|
+
import { handleConfigCommand as runConfigCommand } from './config-command.js';
|
|
20
23
|
import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
|
|
21
24
|
import { Transcript } from './transcript.js';
|
|
22
25
|
import { UndoStore } from './undo.js';
|
|
23
26
|
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
24
27
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
25
28
|
import { performCompact } from './compact.js';
|
|
29
|
+
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
|
|
26
30
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
27
31
|
import { createCliArgs } from './cli-args.js';
|
|
28
32
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
@@ -31,7 +35,7 @@ import { parseBacklogNote } from './backlog.js';
|
|
|
31
35
|
import { askOperatorConfirm, } from './run-task.js';
|
|
32
36
|
import { closeWeb } from './web.js';
|
|
33
37
|
import { assertCommandInsideRoot, runShell, killAllBackground, } from './shell.js';
|
|
34
|
-
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
|
|
38
|
+
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, isInternalChat, } from './sessions.js';
|
|
35
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
40
|
const NL = String.fromCharCode(10);
|
|
37
41
|
// The last final assistant message, for the /copy command. Module-level
|
|
@@ -120,10 +124,14 @@ if (jsonMode) {
|
|
|
120
124
|
function saveLastChat(id, workdir = '', title = '') {
|
|
121
125
|
if (!id)
|
|
122
126
|
return;
|
|
123
|
-
|
|
127
|
+
// Tag the session with the ACTIVE provider: a chat id is only meaningful to
|
|
128
|
+
// the backend that created it, so a later resume must open it in the same
|
|
129
|
+
// one (or refuse). Without this a /provider switch made /resume try to open a
|
|
130
|
+
// qwen id in DeepSeek (and vice versa).
|
|
131
|
+
saveSession({ id, workdir, title, provider: config.provider });
|
|
124
132
|
}
|
|
125
133
|
function loadLastChat(workdir = '') {
|
|
126
|
-
const s = loadLastSession(workdir);
|
|
134
|
+
const s = loadLastSession(workdir, config.provider);
|
|
127
135
|
return s ? s.id : null;
|
|
128
136
|
}
|
|
129
137
|
// Persistent per-project session goal (set by /goal). Kept OUT of the session
|
|
@@ -536,7 +544,15 @@ async function expandSlashTarget(workdir, name, rest) {
|
|
|
536
544
|
const extra = rest
|
|
537
545
|
? '\n\nAdditional instructions from the operator: ' + rest
|
|
538
546
|
: '';
|
|
539
|
-
|
|
547
|
+
// A skill may declare allowed-tools (Claude Code frontmatter): the
|
|
548
|
+
// operator expects the skill to run with ONLY those tools. Carry the
|
|
549
|
+
// list to the task so the main loop can narrow the tool set.
|
|
550
|
+
return {
|
|
551
|
+
text: header + '\n\n' + body + extra,
|
|
552
|
+
allowedTools: skill.allowedTools && skill.allowedTools.length
|
|
553
|
+
? skill.allowedTools
|
|
554
|
+
: undefined,
|
|
555
|
+
};
|
|
540
556
|
}
|
|
541
557
|
}
|
|
542
558
|
catch {
|
|
@@ -582,6 +598,21 @@ async function resolveWorkdir() {
|
|
|
582
598
|
return dir;
|
|
583
599
|
}
|
|
584
600
|
// ---------- restored dialogue ----------
|
|
601
|
+
/**
|
|
602
|
+
* The answer timeout for a one-off provider call (compact summary, etc.).
|
|
603
|
+
* Qwen's cold guest turns are far slower than DeepSeek's, so using
|
|
604
|
+
* config.browser.answerTimeoutMs there cut them off; pick the active
|
|
605
|
+
* provider's own value.
|
|
606
|
+
*/
|
|
607
|
+
function providerAnswerTimeoutMs() {
|
|
608
|
+
return config.provider === 'qwen'
|
|
609
|
+
? config.qwen.answerTimeoutMs
|
|
610
|
+
: config.browser.answerTimeoutMs;
|
|
611
|
+
}
|
|
612
|
+
/** The provider's base chat URL, used by /chat to build the right link. */
|
|
613
|
+
function browserProviderUrl() {
|
|
614
|
+
return providerChatUrlPrefix(config.provider);
|
|
615
|
+
}
|
|
585
616
|
/**
|
|
586
617
|
* Print the dialogue of the just-opened chat into the terminal. Called by
|
|
587
618
|
* /resume, /resume-id and on startup when a previous session is restored, so
|
|
@@ -748,24 +779,52 @@ async function main() {
|
|
|
748
779
|
catch (e) {
|
|
749
780
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
750
781
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
config.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
782
|
+
// Build the browser for the ACTIVE provider. The two backends differ in
|
|
783
|
+
// every layer (selectors, SSE format, chat-id scheme), so the switch is a
|
|
784
|
+
// class choice, not a flag. The shared timings live in config.browser; only
|
|
785
|
+
// the model is provider-specific. A provider switch takes effect on the
|
|
786
|
+
// next launch (the operator is told to restart).
|
|
787
|
+
const provider = asProviderId(config.provider);
|
|
788
|
+
const browser = provider === 'qwen'
|
|
789
|
+
? new QwenBrowser({
|
|
790
|
+
headless,
|
|
791
|
+
debug,
|
|
792
|
+
channel: config.browserChannel,
|
|
793
|
+
...config.browser,
|
|
794
|
+
// Qwen-specific overrides (see config.qwen): no 15s DeepSeek
|
|
795
|
+
// throttle, a longer cold-start answer timeout.
|
|
796
|
+
minSendIntervalMs: config.qwen.minSendIntervalMs,
|
|
797
|
+
answerTimeoutMs: config.qwen.answerTimeoutMs,
|
|
798
|
+
model: config.qwen.model,
|
|
799
|
+
locale: currentLocale,
|
|
800
|
+
onAuthSave: (username, password) => {
|
|
801
|
+
config.browser.auth.username = username;
|
|
802
|
+
config.browser.auth.password = password;
|
|
803
|
+
try {
|
|
804
|
+
writeConfigValue('home', 'browser.auth.username', username);
|
|
805
|
+
writeConfigValue('home', 'browser.auth.password', password);
|
|
806
|
+
}
|
|
807
|
+
catch { }
|
|
808
|
+
},
|
|
809
|
+
})
|
|
810
|
+
: new DeepSeekBrowser({
|
|
811
|
+
headless,
|
|
812
|
+
debug,
|
|
813
|
+
channel: config.browserChannel,
|
|
814
|
+
...config.browser,
|
|
815
|
+
locale: currentLocale,
|
|
816
|
+
// Persist credentials the operator typed in the terminal so the next
|
|
817
|
+
// launch can re-login silently after a logout.
|
|
818
|
+
onAuthSave: (username, password) => {
|
|
819
|
+
config.browser.auth.username = username;
|
|
820
|
+
config.browser.auth.password = password;
|
|
821
|
+
try {
|
|
822
|
+
writeConfigValue('home', 'browser.auth.username', username);
|
|
823
|
+
writeConfigValue('home', 'browser.auth.password', password);
|
|
824
|
+
}
|
|
825
|
+
catch { }
|
|
826
|
+
},
|
|
827
|
+
});
|
|
769
828
|
const bootSpinner = mod.createSpinner(currentLocale);
|
|
770
829
|
bootSpinner.thinking();
|
|
771
830
|
try {
|
|
@@ -812,7 +871,7 @@ async function main() {
|
|
|
812
871
|
// explicitly: --chat <id> or --resume-last.
|
|
813
872
|
let resumeId = chatIdArg;
|
|
814
873
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
815
|
-
const last = loadLastSession(currentWorkdir);
|
|
874
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
816
875
|
if (last && last.id) {
|
|
817
876
|
resumeId = last.id;
|
|
818
877
|
console.log(theme.system(t('msg.resuming', { id: resumeId })));
|
|
@@ -928,7 +987,7 @@ async function main() {
|
|
|
928
987
|
},
|
|
929
988
|
transcript,
|
|
930
989
|
maxSubagents: config.browser.maxSubagents,
|
|
931
|
-
askDeadlineMs: config
|
|
990
|
+
askDeadlineMs: providerAskDeadlineMs(config),
|
|
932
991
|
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
933
992
|
// An `ask` permission rule must also gate the subagent: otherwise a
|
|
934
993
|
// guarded tool (e.g. a dangerous Bash command) would run inside the
|
|
@@ -1066,7 +1125,7 @@ async function main() {
|
|
|
1066
1125
|
// --new-chat kept for compatibility (this is the default behavior)
|
|
1067
1126
|
let resumeId = chatIdArg;
|
|
1068
1127
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
1069
|
-
const last = loadLastSession(currentWorkdir);
|
|
1128
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
1070
1129
|
if (last && last.id) {
|
|
1071
1130
|
resumeId = last.id;
|
|
1072
1131
|
console.log(theme.system(t('msg.resuming', {
|
|
@@ -1128,6 +1187,8 @@ async function main() {
|
|
|
1128
1187
|
});
|
|
1129
1188
|
editor = ed;
|
|
1130
1189
|
ed.setTmpDir(TMP_DIR);
|
|
1190
|
+
// Provider badge: show which backend is answering ([Qwen]/[DeepSeek]).
|
|
1191
|
+
ed.setProvider(providerLabel(config.provider));
|
|
1131
1192
|
// Restore the input history from the previous runs and persist every new
|
|
1132
1193
|
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1133
1194
|
ed.history = loadHistory();
|
|
@@ -1574,108 +1635,22 @@ async function main() {
|
|
|
1574
1635
|
}
|
|
1575
1636
|
}
|
|
1576
1637
|
async function handleConfigCommand(input) {
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
// /config lang <ru|en> — quick access to ui.locale
|
|
1596
|
-
if (sub === 'lang' || sub === 'language' || sub === 'язык') {
|
|
1597
|
-
const val = parts[2];
|
|
1598
|
-
if (!val) {
|
|
1599
|
-
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1600
|
-
console.log(theme.dim(t('cfg.lang_usage')));
|
|
1601
|
-
return;
|
|
1602
|
-
}
|
|
1603
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === 'ui.locale');
|
|
1604
|
-
const loc = normalizeLocale(val);
|
|
1605
|
-
try {
|
|
1606
|
-
configSetRaw(field, loc);
|
|
1607
|
-
}
|
|
1608
|
-
catch (e) {
|
|
1609
|
-
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
console.log(theme.assistant(t('cfg.lang_set', { v: localeDisplayName(loc) })));
|
|
1613
|
-
return;
|
|
1614
|
-
}
|
|
1615
|
-
if (sub === 'get') {
|
|
1616
|
-
const key = parts[2];
|
|
1617
|
-
if (!key) {
|
|
1618
|
-
console.log(theme.dim(t('cfg.usage')));
|
|
1619
|
-
return;
|
|
1620
|
-
}
|
|
1621
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1622
|
-
if (!field) {
|
|
1623
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key })));
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
const cur = getByPath(config, key);
|
|
1627
|
-
console.log(theme.system(t('cfg.value', { v: key, value: configGetValue(key, cur, t) })));
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
if (sub === 'set') {
|
|
1631
|
-
const key = parts[2];
|
|
1632
|
-
const raw = parts.slice(3).join(' ');
|
|
1633
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1634
|
-
if (!field) {
|
|
1635
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
if (!raw) {
|
|
1639
|
-
console.log(theme.dim(t('cfg.usage')));
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
try {
|
|
1643
|
-
configSetRaw(field, raw);
|
|
1644
|
-
}
|
|
1645
|
-
catch {
|
|
1646
|
-
console.error(theme.error(t('cfg.bad_value', {
|
|
1647
|
-
v: key,
|
|
1648
|
-
type: configValueHint(field, t),
|
|
1649
|
-
})));
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
console.log(theme.assistant(t('cfg.saved', {
|
|
1653
|
-
v: key,
|
|
1654
|
-
value: JSON.stringify(getByPath(config, key)),
|
|
1655
|
-
file: CONFIG_PATHS.HOME_CONFIG,
|
|
1656
|
-
})));
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
if (sub === 'reset' || sub === 'unset') {
|
|
1660
|
-
const key = parts[2];
|
|
1661
|
-
const field = CONFIG_SCHEMA.find((f) => f.path === key);
|
|
1662
|
-
if (!field) {
|
|
1663
|
-
console.error(theme.error(t('cfg.unknown_key', { v: key || '' })));
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
try {
|
|
1667
|
-
configResetField(field);
|
|
1668
|
-
}
|
|
1669
|
-
catch (e) {
|
|
1670
|
-
console.error(theme.error(t('cfg.write_error', { v: e.message })));
|
|
1671
|
-
return;
|
|
1672
|
-
}
|
|
1673
|
-
console.log(theme.assistant(t('cfg.reset', { v: key })));
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
// Unknown subcommand — show the list.
|
|
1677
|
-
console.error(theme.error(t('cfg.unknown_key', { v: sub })));
|
|
1678
|
-
configShowList();
|
|
1638
|
+
await runConfigCommand(input, {
|
|
1639
|
+
t,
|
|
1640
|
+
print: (line) => console.log(line),
|
|
1641
|
+
printErr: (line) => console.error(line),
|
|
1642
|
+
getValue: (p) => getByPath(config, p),
|
|
1643
|
+
setValue: (field, raw) => configSetRaw(field, raw),
|
|
1644
|
+
resetField: (field) => configResetField(field),
|
|
1645
|
+
showList: () => configShowList(),
|
|
1646
|
+
openMenu: () => configOpenMenu(),
|
|
1647
|
+
currentLocale: () => currentLocale,
|
|
1648
|
+
normalizeLocale,
|
|
1649
|
+
localeDisplayName,
|
|
1650
|
+
schema: CONFIG_SCHEMA,
|
|
1651
|
+
homeConfigPath: CONFIG_PATHS.HOME_CONFIG,
|
|
1652
|
+
projectConfigPath: CONFIG_PATHS.PROJECT_CONFIG,
|
|
1653
|
+
});
|
|
1679
1654
|
}
|
|
1680
1655
|
while (running) {
|
|
1681
1656
|
let input;
|
|
@@ -1975,7 +1950,11 @@ async function main() {
|
|
|
1975
1950
|
editor.lock(t('msg.input_locked'));
|
|
1976
1951
|
spin.thinking();
|
|
1977
1952
|
try {
|
|
1978
|
-
|
|
1953
|
+
const all = await browser.listChats(30);
|
|
1954
|
+
// Hide subagent chats: they are real DeepSeek chats, but they are
|
|
1955
|
+
// internal context-isolation artifacts whose report already lives in
|
|
1956
|
+
// the parent chat, so resuming one is never what the operator wants.
|
|
1957
|
+
lastChats = all.filter((c) => !isInternalChat(c.id));
|
|
1979
1958
|
spin.stop();
|
|
1980
1959
|
// An optional query filters by title (substring, case-insensitive),
|
|
1981
1960
|
// so a long chat list does not have to be scanned by eye.
|
|
@@ -2056,10 +2035,133 @@ async function main() {
|
|
|
2056
2035
|
}
|
|
2057
2036
|
continue;
|
|
2058
2037
|
}
|
|
2038
|
+
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
2039
|
+
const arg = trimmed.slice('/provider'.length).trim().toLowerCase();
|
|
2040
|
+
if (!arg || arg === 'status') {
|
|
2041
|
+
const model = config.provider === 'qwen' ? config.qwen.model || '(default)' : 'n/a';
|
|
2042
|
+
console.log(theme.system(t('provider.status', {
|
|
2043
|
+
v: config.provider,
|
|
2044
|
+
m: model,
|
|
2045
|
+
auth: authMarkerExists() ? 'yes' : 'no',
|
|
2046
|
+
})));
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
if (arg === 'login') {
|
|
2050
|
+
try {
|
|
2051
|
+
await browser.waitForLogin();
|
|
2052
|
+
console.log(theme.system(t('provider.login_done')));
|
|
2053
|
+
}
|
|
2054
|
+
catch (e) {
|
|
2055
|
+
console.error(theme.warn(e.message));
|
|
2056
|
+
}
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
if (isProviderId(arg)) {
|
|
2060
|
+
const from = config.provider;
|
|
2061
|
+
if (arg === from) {
|
|
2062
|
+
console.log(theme.system(t('provider.switched', { v: arg })));
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
// CARRY-OVER: a chat id only exists on its own backend, so switching
|
|
2066
|
+
// providers loses the conversation. Summarize the CURRENT chat (still
|
|
2067
|
+
// on the OLD browser object) and persist a handoff for the target; on
|
|
2068
|
+
// the next launch the new provider posts it as the first message.
|
|
2069
|
+
if (editor)
|
|
2070
|
+
editor.lock(t('msg.input_locked'));
|
|
2071
|
+
try {
|
|
2072
|
+
const spin = editor || mod.createSpinner(currentLocale);
|
|
2073
|
+
spin.thinking();
|
|
2074
|
+
let summary = '';
|
|
2075
|
+
try {
|
|
2076
|
+
const answer = await browser.ask(mod.commands.buildCompactPrompt(currentLocale), {
|
|
2077
|
+
agent: true,
|
|
2078
|
+
timeout: providerAnswerTimeoutMs(),
|
|
2079
|
+
});
|
|
2080
|
+
const trimmed = String(answer || '').trim();
|
|
2081
|
+
if (mod.commands.isUsableCompactSummary(trimmed))
|
|
2082
|
+
summary = trimmed;
|
|
2083
|
+
}
|
|
2084
|
+
catch (e) {
|
|
2085
|
+
if (debug)
|
|
2086
|
+
console.error('handoff summary failed:', e.message);
|
|
2087
|
+
}
|
|
2088
|
+
// Fall back to the LOCAL history when the model summary is unusable.
|
|
2089
|
+
if (!summary && browser.fetchChatMessages) {
|
|
2090
|
+
const cid = await browser.getCurrentChatId().catch(() => null);
|
|
2091
|
+
const all = cid
|
|
2092
|
+
? await browser.fetchChatMessages(cid).catch(() => [])
|
|
2093
|
+
: [];
|
|
2094
|
+
const msgs = mod.commands.trimRestoredMessages(all, 0);
|
|
2095
|
+
if (msgs.length) {
|
|
2096
|
+
summary = mod.commands.formatRestoredHistory(msgs, { limit: 40 });
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
spin.stop();
|
|
2100
|
+
if (summary) {
|
|
2101
|
+
saveProviderHandoff({
|
|
2102
|
+
provider: arg,
|
|
2103
|
+
workdir: currentWorkdir,
|
|
2104
|
+
summary,
|
|
2105
|
+
task: lastTaskText || undefined,
|
|
2106
|
+
createdAt: new Date().toISOString(),
|
|
2107
|
+
});
|
|
2108
|
+
console.log(theme.system(t('provider.handoff_saved', { v: arg })));
|
|
2109
|
+
}
|
|
2110
|
+
else {
|
|
2111
|
+
console.log(theme.warn(t('provider.handoff_empty')));
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
finally {
|
|
2115
|
+
if (editor)
|
|
2116
|
+
editor.unlock();
|
|
2117
|
+
}
|
|
2118
|
+
// Tag the current session with the OLD provider before switching.
|
|
2119
|
+
if (currentChatId)
|
|
2120
|
+
saveLastChat(currentChatId, currentWorkdir);
|
|
2121
|
+
config.provider = arg;
|
|
2122
|
+
config.qwen.enabled = arg === 'qwen';
|
|
2123
|
+
try {
|
|
2124
|
+
writeConfigValue('home', 'provider', arg);
|
|
2125
|
+
}
|
|
2126
|
+
catch { }
|
|
2127
|
+
console.log(theme.system(t('provider.switched', { v: arg })));
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
console.error(theme.warn(t('provider.usage')));
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
if (lower === '/model' || lower.startsWith('/model ')) {
|
|
2134
|
+
const arg = trimmed.slice('/model'.length).trim();
|
|
2135
|
+
if (config.provider !== 'qwen') {
|
|
2136
|
+
console.log(theme.system(t('provider.usage')));
|
|
2137
|
+
continue;
|
|
2138
|
+
}
|
|
2139
|
+
const qb = browser;
|
|
2140
|
+
if (!arg) {
|
|
2141
|
+
const models = qb.listModels ? await qb.listModels() : [];
|
|
2142
|
+
if (!models.length) {
|
|
2143
|
+
console.log(theme.system(t('qwen.model_list', { v: '(none)' })));
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
const lines = models.map((m) => ` ${m.id} — ${m.name}`).join(NL);
|
|
2147
|
+
console.log(theme.system(t('qwen.model_list', { v: lines })));
|
|
2148
|
+
}
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
config.qwen.model = arg;
|
|
2152
|
+
if (typeof qb.model === 'string')
|
|
2153
|
+
qb.model = arg;
|
|
2154
|
+
try {
|
|
2155
|
+
writeConfigValue('home', 'qwen.model', arg);
|
|
2156
|
+
}
|
|
2157
|
+
catch { }
|
|
2158
|
+
console.log(theme.system(t('qwen.model_set', { v: arg })));
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2059
2161
|
if (lower === '/chat') {
|
|
2060
2162
|
if (currentChatId) {
|
|
2061
2163
|
console.log(theme.system(t('chats.current_id', { v: currentChatId })));
|
|
2062
|
-
console.log(theme.system(`URL:
|
|
2164
|
+
console.log(theme.system(`URL: ${browserProviderUrl()}/${currentChatId}`));
|
|
2063
2165
|
}
|
|
2064
2166
|
else {
|
|
2065
2167
|
const id = await browser.getCurrentChatId();
|
|
@@ -2101,7 +2203,7 @@ async function main() {
|
|
|
2101
2203
|
if (lower === '/last') {
|
|
2102
2204
|
// Quick resume of the last chat for THIS working directory — otherwise
|
|
2103
2205
|
// finding it meant /chats (a network fetch) or copying the id.
|
|
2104
|
-
const last = loadLastSession(currentWorkdir);
|
|
2206
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
2105
2207
|
if (!last || !last.id) {
|
|
2106
2208
|
console.error(theme.error(t('sessions.none')));
|
|
2107
2209
|
continue;
|
|
@@ -2794,6 +2896,8 @@ async function main() {
|
|
|
2794
2896
|
uiPresent: uiHealth?.present,
|
|
2795
2897
|
uiMissing: uiHealth?.missing,
|
|
2796
2898
|
uiMissingCritical: uiHealth?.missingCritical,
|
|
2899
|
+
provider: config.provider,
|
|
2900
|
+
providerModel: config.provider === 'qwen' ? config.qwen.model : undefined,
|
|
2797
2901
|
}, t)));
|
|
2798
2902
|
continue;
|
|
2799
2903
|
}
|
|
@@ -2828,7 +2932,7 @@ async function main() {
|
|
|
2828
2932
|
ui: editor,
|
|
2829
2933
|
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2830
2934
|
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
2831
|
-
answerTimeoutMs:
|
|
2935
|
+
answerTimeoutMs: providerAnswerTimeoutMs(),
|
|
2832
2936
|
fallbackLimit: COMPACT_FALLBACK_LIMIT,
|
|
2833
2937
|
selfImprovement: devMode,
|
|
2834
2938
|
});
|
|
@@ -3145,14 +3249,19 @@ async function main() {
|
|
|
3145
3249
|
// prompt templates with $ARGUMENTS / {{args}} placeholders.
|
|
3146
3250
|
let expandedTask = null;
|
|
3147
3251
|
let missingArgs = null;
|
|
3252
|
+
let skillAllowedTools = null;
|
|
3148
3253
|
if (lower.startsWith('/')) {
|
|
3149
3254
|
const name = trimmed.slice(1).split(/\s+/)[0];
|
|
3150
3255
|
const rest = trimmed.slice(1 + name.length).trim();
|
|
3151
3256
|
const res = await expandSlashTarget(currentWorkdir, name, rest);
|
|
3152
3257
|
if (res && 'missing' in res)
|
|
3153
3258
|
missingArgs = res.missing;
|
|
3154
|
-
else if (res)
|
|
3259
|
+
else if (res) {
|
|
3155
3260
|
expandedTask = res.text;
|
|
3261
|
+
if ('allowedTools' in res && res.allowedTools) {
|
|
3262
|
+
skillAllowedTools = res.allowedTools;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3156
3265
|
}
|
|
3157
3266
|
// B7: a custom command with mandatory `arguments:` refuses to run until the
|
|
3158
3267
|
// operator supplies them (by position).
|
|
@@ -3180,6 +3289,27 @@ async function main() {
|
|
|
3180
3289
|
}
|
|
3181
3290
|
}
|
|
3182
3291
|
lastTaskText = finalTaskText;
|
|
3292
|
+
// Provider handoff: if the operator switched providers, the new backend
|
|
3293
|
+
// has NO chat history. Post the carried-over summary as a preface so the
|
|
3294
|
+
// model continues instead of starting cold. Consumed once.
|
|
3295
|
+
{
|
|
3296
|
+
const handoff = readProviderHandoff(config.provider, currentWorkdir);
|
|
3297
|
+
if (handoff) {
|
|
3298
|
+
finalTaskText =
|
|
3299
|
+
buildHandoffText({
|
|
3300
|
+
from: handoff.provider === 'qwen' ? 'DeepSeek' : 'Qwen',
|
|
3301
|
+
to: providerLabel(config.provider),
|
|
3302
|
+
summary: handoff.summary,
|
|
3303
|
+
task: handoff.task,
|
|
3304
|
+
}) +
|
|
3305
|
+
String.fromCharCode(10, 10) +
|
|
3306
|
+
'Now continue with this task:' +
|
|
3307
|
+
String.fromCharCode(10) +
|
|
3308
|
+
finalTaskText;
|
|
3309
|
+
clearProviderHandoff();
|
|
3310
|
+
console.log(theme.system(t('provider.handoff_used')));
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3183
3313
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3184
3314
|
await autoReload();
|
|
3185
3315
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3209,7 +3339,13 @@ async function main() {
|
|
|
3209
3339
|
// plan (read-only) mode — a `general` subagent could write otherwise.
|
|
3210
3340
|
subagents: config.browser.subagents && !planMode,
|
|
3211
3341
|
});
|
|
3212
|
-
|
|
3342
|
+
let tools = mergeTools(baseTools, mcpPool?.tools, planMode);
|
|
3343
|
+
// A skill invoked via /skill with `allowed-tools:` narrows the tool set to
|
|
3344
|
+
// that allow-list (Claude Code contract). respond is always kept so the run
|
|
3345
|
+
// can still finish.
|
|
3346
|
+
if (skillAllowedTools) {
|
|
3347
|
+
tools = filterToolsByAllowList(tools, skillAllowedTools);
|
|
3348
|
+
}
|
|
3213
3349
|
if (editor)
|
|
3214
3350
|
editor.busy = true;
|
|
3215
3351
|
try {
|
|
@@ -3247,7 +3383,7 @@ async function main() {
|
|
|
3247
3383
|
undo,
|
|
3248
3384
|
todos: todoStore,
|
|
3249
3385
|
}),
|
|
3250
|
-
answerTimeoutMs:
|
|
3386
|
+
answerTimeoutMs: providerAnswerTimeoutMs(),
|
|
3251
3387
|
quiet: true,
|
|
3252
3388
|
skipUiLock: true,
|
|
3253
3389
|
selfImprovement: devMode,
|
package/dist/input.js
CHANGED
|
@@ -171,6 +171,10 @@ export class LineEditor {
|
|
|
171
171
|
// plain field because the CALLER runs the command on its own timer (a
|
|
172
172
|
// per-render spawn would block typing).
|
|
173
173
|
statusLine;
|
|
174
|
+
// Active chat provider label (e.g. "Qwen", "DeepSeek") shown as a small
|
|
175
|
+
// badge in the status line, so the operator always knows which backend is
|
|
176
|
+
// answering. Empty hides it. Set via setProvider().
|
|
177
|
+
providerBadge;
|
|
174
178
|
// Whether the DeepSeek "Deep thinking" / "Smart search" toggles are ON.
|
|
175
179
|
// Shown as two colored icons before the context counter: a dim gray icon
|
|
176
180
|
// means off, a teal-green icon means on.
|
|
@@ -265,6 +269,7 @@ export class LineEditor {
|
|
|
265
269
|
this.onContextQuery = null;
|
|
266
270
|
this.queueLength = 0;
|
|
267
271
|
this.statusLine = '';
|
|
272
|
+
this.providerBadge = '';
|
|
268
273
|
this.onQueueQuery = null;
|
|
269
274
|
this.onTasksQuery = null;
|
|
270
275
|
}
|
|
@@ -353,6 +358,14 @@ export class LineEditor {
|
|
|
353
358
|
_statusLineBadge() {
|
|
354
359
|
return this.statusLine ? theme.system(this.statusLine) + ' ' : '';
|
|
355
360
|
}
|
|
361
|
+
// A small badge with the ACTIVE provider (e.g. "Qwen"), shown before the
|
|
362
|
+
// toggles so the operator always knows which backend is answering (a
|
|
363
|
+
// multi-provider session can otherwise be ambiguous). Empty hides it.
|
|
364
|
+
_providerBadge() {
|
|
365
|
+
return this.providerBadge
|
|
366
|
+
? theme.system('[' + this.providerBadge + ']') + ' '
|
|
367
|
+
: '';
|
|
368
|
+
}
|
|
356
369
|
// Icons for the DeepSeek toggles: 🧠 "Deep thinking" and 🌐 "Smart search",
|
|
357
370
|
// shown before the context counter. Only the ENABLED toggles are shown, so
|
|
358
371
|
// the state is unambiguous WITHOUT relying on the terminal coloring the
|
|
@@ -593,6 +606,15 @@ export class LineEditor {
|
|
|
593
606
|
this.statusLine = t;
|
|
594
607
|
this._render();
|
|
595
608
|
}
|
|
609
|
+
// Set the provider badge (e.g. "Qwen"). An empty string hides it. Repaints
|
|
610
|
+
// only when the value actually changes.
|
|
611
|
+
setProvider(text) {
|
|
612
|
+
const t = String(text || '').trim();
|
|
613
|
+
if (t === this.providerBadge)
|
|
614
|
+
return;
|
|
615
|
+
this.providerBadge = t;
|
|
616
|
+
this._render();
|
|
617
|
+
}
|
|
596
618
|
setContextLimit(limit) {
|
|
597
619
|
if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0) {
|
|
598
620
|
this.contextLimit = limit;
|
|
@@ -692,6 +714,7 @@ export class LineEditor {
|
|
|
692
714
|
const ctxRaw = this._contextText();
|
|
693
715
|
this._refreshToggles();
|
|
694
716
|
const ctxText = this._statusLineBadge() +
|
|
717
|
+
this._providerBadge() +
|
|
695
718
|
this._queueBadge() +
|
|
696
719
|
this._tasksBadge() +
|
|
697
720
|
this._toggleIcons() +
|