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/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 { createTools, mergeTools, filterToolsForReadOnly } from './tools.js';
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 { configGetValue, configValueHint, formatConfigList, } from './config-commands.js';
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
- saveSession({ id, workdir, title });
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
- return { text: header + '\n\n' + body + extra };
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
- const browser = new DeepSeekBrowser({
752
- headless,
753
- debug,
754
- channel: config.browserChannel,
755
- ...config.browser,
756
- locale: currentLocale,
757
- // Persist credentials the operator typed in the terminal so the next
758
- // launch can re-login silently after a logout.
759
- onAuthSave: (username, password) => {
760
- config.browser.auth.username = username;
761
- config.browser.auth.password = password;
762
- try {
763
- writeConfigValue('home', 'browser.auth.username', username);
764
- writeConfigValue('home', 'browser.auth.password', password);
765
- }
766
- catch { }
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.browser.askDeadlineMs,
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
- const parts = input.trim().split(/\s+/);
1578
- const sub = (parts[1] || '').toLowerCase();
1579
- // With no subcommand — the menu (or a text list in non-TTY).
1580
- if (!sub || sub === 'menu' || sub === 'ui') {
1581
- await configOpenMenu();
1582
- return;
1583
- }
1584
- if (sub === 'list' || sub === 'show' || sub === 'ls') {
1585
- configShowList();
1586
- return;
1587
- }
1588
- if (sub === 'path' || sub === 'paths') {
1589
- console.log(theme.system(t('cfg.paths', {
1590
- global: CONFIG_PATHS.HOME_CONFIG,
1591
- project: CONFIG_PATHS.PROJECT_CONFIG,
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
- lastChats = await browser.listChats(30);
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: https://chat.deepseek.com/a/chat/s/${currentChatId}`));
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: config.browser.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
- const tools = mergeTools(baseTools, mcpPool?.tools, planMode);
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: config.browser.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() +