zames_pro 2.72.9 → 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 +73 -0
- package/README.md +111 -19
- package/dist/agent-loop.js +15 -5
- package/dist/browser-pure.js +6 -6
- package/dist/browser.js +5 -1
- package/dist/commands.js +7 -0
- package/dist/config.js +56 -1
- package/dist/i18n/config.js +14 -0
- package/dist/i18n/deepseek.js +26 -26
- package/dist/i18n/help.js +8 -0
- package/dist/i18n/qwen.js +85 -0
- package/dist/i18n.js +2 -0
- package/dist/index.js +226 -28
- 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 +15 -4
- package/dist/ui-health.js +27 -21
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +5 -1
package/dist/i18n/deepseek.js
CHANGED
|
@@ -5,28 +5,28 @@ export const deepseekMessages = {
|
|
|
5
5
|
// printed around the moment they happen (browser.ts / agent-loop.ts), not as
|
|
6
6
|
// a bare error at the end of the run.
|
|
7
7
|
'ds.rate_limit_wait': {
|
|
8
|
-
ru: '⏳
|
|
9
|
-
en: '⏳
|
|
8
|
+
ru: '⏳ {provider}: «слишком часто». Жду {min} мин ({attempt}/{max}) и повторю отправку...',
|
|
9
|
+
en: '⏳ {provider}: "too frequent". Waiting {min} min ({attempt}/{max}), then resending...',
|
|
10
10
|
},
|
|
11
11
|
'ds.rate_limit_give_up': {
|
|
12
|
-
ru: '✖
|
|
13
|
-
en: '✖
|
|
12
|
+
ru: '✖ {provider} не принял сообщение после {attempt} пауз по {min} мин. Подожди и попробуй снова.',
|
|
13
|
+
en: '✖ {provider} refused the message after {attempt} waits of {min} min. Wait and try again.',
|
|
14
14
|
},
|
|
15
15
|
'ds.server_busy_wait': {
|
|
16
|
-
ru: '⏳
|
|
17
|
-
en: '⏳
|
|
16
|
+
ru: '⏳ {provider}: сервер занят. Жду {sec}с ({attempt}/{max}) и повторю...',
|
|
17
|
+
en: '⏳ {provider}: server busy. Waiting {sec}s ({attempt}/{max}), then retrying...',
|
|
18
18
|
},
|
|
19
19
|
'ds.server_busy_give_up': {
|
|
20
|
-
ru: '✖
|
|
21
|
-
en: '✖
|
|
20
|
+
ru: '✖ {provider}: сервер не ответил после {attempt} повторов. Попробуй позже.',
|
|
21
|
+
en: '✖ {provider}: the server did not respond after {attempt} retries. Try again later.',
|
|
22
22
|
},
|
|
23
23
|
'ds.ask_retry': {
|
|
24
24
|
ru: '⚠ Отправка не удалась (попытка {attempt}/{max} на этот запрос): {error} — повторяю...',
|
|
25
25
|
en: '⚠ Send failed (attempt {attempt}/{max} for this request): {error} — retrying...',
|
|
26
26
|
},
|
|
27
27
|
'ds.incomplete_retry': {
|
|
28
|
-
ru: '⏳
|
|
29
|
-
en: '⏳
|
|
28
|
+
ru: '⏳ {provider} оборвал ответ и кнопки Continue нет — переотправляю задачу ({attempt}/{max})...',
|
|
29
|
+
en: '⏳ {provider} truncated the answer and there is no Continue button — resending the task ({attempt}/{max})...',
|
|
30
30
|
},
|
|
31
31
|
'ds.attach_read_failed': {
|
|
32
32
|
ru: '⚠ не удалось прочитать вложение {name}: {error}',
|
|
@@ -41,8 +41,8 @@ export const deepseekMessages = {
|
|
|
41
41
|
en: '⚠ could not attach the files: {error}',
|
|
42
42
|
},
|
|
43
43
|
'ds.incomplete_give_up': {
|
|
44
|
-
ru: '✖
|
|
45
|
-
en: '✖
|
|
44
|
+
ru: '✖ {provider} {attempt} раз подряд оборвал ответ, не дойдя до конца. Возможно, включён режим размышления — попробуй выключить его (browser.deepThinking) или повтори позже.',
|
|
45
|
+
en: '✖ {provider} truncated the answer {attempt} times in a row. The reasoning mode (deep thinking) may be on — try turning it off (browser.deepThinking) or retry later.',
|
|
46
46
|
},
|
|
47
47
|
'ds.ask_restart_browser': {
|
|
48
48
|
ru: '⚠ Браузер потерял страницу — перезапускаю и вхожу заново...',
|
|
@@ -53,36 +53,36 @@ export const deepseekMessages = {
|
|
|
53
53
|
en: '⚠ Could not restart the browser: {error}',
|
|
54
54
|
},
|
|
55
55
|
'ds.ask_failed': {
|
|
56
|
-
ru: '✖ Не удалось получить ответ от
|
|
57
|
-
en: '✖ Could not get an answer from
|
|
56
|
+
ru: '✖ Не удалось получить ответ от {provider} после {max} попыток: {error}',
|
|
57
|
+
en: '✖ Could not get an answer from {provider} after {max} attempts: {error}',
|
|
58
58
|
},
|
|
59
59
|
'ds.input_missing': {
|
|
60
|
-
ru: '✖ Не найдено поле ввода на странице
|
|
61
|
-
en: '✖ The
|
|
60
|
+
ru: '✖ Не найдено поле ввода на странице {provider}. Запусти /debug-dom и поправь INPUT_SELECTORS.',
|
|
61
|
+
en: '✖ The {provider} input field was not found. Run /debug-dom and fix INPUT_SELECTORS.',
|
|
62
62
|
},
|
|
63
63
|
'ds.input_partial': {
|
|
64
64
|
ru: '✖ Не удалось вставить текст в поле ввода целиком ({got} из {want} символов). Сообщение не отправлено, чтобы не отправить обрезанный текст. Попробуй ещё раз или разбей сообщение.',
|
|
65
65
|
en: '✖ Could not paste the whole text into the input ({got} of {want} chars). The message was not sent to avoid sending a truncated text. Retry or split the message.',
|
|
66
66
|
},
|
|
67
67
|
'ds.send_no_start': {
|
|
68
|
-
ru: '✖ Ответ не начал генерироваться за 35с даже после повторной отправки. Проверь чат
|
|
69
|
-
en: '✖ The answer did not start generating within 35s even after resending. Check the
|
|
68
|
+
ru: '✖ Ответ не начал генерироваться за 35с даже после повторной отправки. Проверь чат {provider} вручную (возможно, кнопка отправки не нажимается или сессия разлогинилась).',
|
|
69
|
+
en: '✖ The answer did not start generating within 35s even after resending. Check the {provider} chat manually (the send button may not be clickable or the session may have expired).',
|
|
70
70
|
},
|
|
71
71
|
'ds.send_no_new_answer': {
|
|
72
|
-
ru: '✖ Новый ответ не получен — на странице остался прежний текст. Возможно, сообщение не отправилось. Проверь чат
|
|
73
|
-
en: '✖ No new answer received — the page still shows the previous text. The message may not have been sent. Check the
|
|
72
|
+
ru: '✖ Новый ответ не получен — на странице остался прежний текст. Возможно, сообщение не отправилось. Проверь чат {provider} вручную.',
|
|
73
|
+
en: '✖ No new answer received — the page still shows the previous text. The message may not have been sent. Check the {provider} chat manually.',
|
|
74
74
|
},
|
|
75
75
|
'ds.continue_clicked': {
|
|
76
|
-
ru: '▶ Нажал «Continue» (
|
|
77
|
-
en: '▶ Clicked "Continue" (
|
|
76
|
+
ru: '▶ Нажал «Continue» ({provider} приостановил вывод — возобновляю).',
|
|
77
|
+
en: '▶ Clicked "Continue" ({provider} paused the output — resuming).',
|
|
78
78
|
},
|
|
79
79
|
'ds.answer_timeout': {
|
|
80
|
-
ru: '⏳
|
|
81
|
-
en: '⏳
|
|
80
|
+
ru: '⏳ {provider} не ответил за {sec}с ({attempt}/{max}) — повторяю запрос...',
|
|
81
|
+
en: '⏳ {provider} did not answer within {sec}s ({attempt}/{max}) — retrying the request...',
|
|
82
82
|
},
|
|
83
83
|
'ds.answer_timeout_give_up': {
|
|
84
|
-
ru: '✖
|
|
85
|
-
en: '✖
|
|
84
|
+
ru: '✖ {provider} перестал отвечать, лимит повторов исчерпан. Модель не дала ответа — проверь чат вручную.',
|
|
85
|
+
en: '✖ {provider} stopped responding, retry limit exhausted. The model gave no answer — check the chat manually.',
|
|
86
86
|
},
|
|
87
87
|
'ds.tool_retry': {
|
|
88
88
|
ru: '⏳ Агент не распознал ответ модели — прошу продолжить ({attempt}/{max})...',
|
package/dist/i18n/help.js
CHANGED
|
@@ -423,6 +423,14 @@ export const helpMessages = {
|
|
|
423
423
|
ru: '/config lang <ru|en> сменить язык интерфейса и агента',
|
|
424
424
|
en: '/config lang <ru|en> switch UI and agent language',
|
|
425
425
|
},
|
|
426
|
+
'help.cmd.provider': {
|
|
427
|
+
ru: '/provider <deepseek|qwen> переключить провайдера чата',
|
|
428
|
+
en: '/provider <deepseek|qwen> switch the chat provider',
|
|
429
|
+
},
|
|
430
|
+
'help.cmd.model': {
|
|
431
|
+
ru: '/model [id] показать/выбрать модель Qwen',
|
|
432
|
+
en: '/model [id] show/pick the Qwen model',
|
|
433
|
+
},
|
|
426
434
|
'help.cmd.debug_dom': {
|
|
427
435
|
ru: '/debug-dom сохранить HTML страницы (для отладки)',
|
|
428
436
|
en: '/debug-dom dump page HTML (debug)',
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Qwen (chat.qwen.ai) provider messages — everything the operator sees while
|
|
2
|
+
// the active backend is Qwen. Kept in its own section file (like deepseek.ts)
|
|
3
|
+
// so provider-specific wording has one home.
|
|
4
|
+
export const qwenMessages = {
|
|
5
|
+
'qwen.guest_mode': {
|
|
6
|
+
ru: 'Qwen: вход не выполнен, работаю в гостевом режиме (≈10 сообщений в сутки, без вложений). Войдите через /provider login или запустите с --headed.',
|
|
7
|
+
en: 'Qwen: not signed in, running in guest mode (~10 messages/day, no attachments). Log in via /provider login or launch with --headed.',
|
|
8
|
+
},
|
|
9
|
+
'qwen.login_email_prompt': {
|
|
10
|
+
ru: 'Email для входа в Qwen{def}: ',
|
|
11
|
+
en: 'Qwen login email{def}: ',
|
|
12
|
+
},
|
|
13
|
+
'qwen.login_code_prompt': {
|
|
14
|
+
ru: 'Код из письма Qwen: ',
|
|
15
|
+
en: 'Qwen email code: ',
|
|
16
|
+
},
|
|
17
|
+
'qwen.login_pass_prompt': {
|
|
18
|
+
ru: 'Пароль Qwen: ',
|
|
19
|
+
en: 'Qwen password: ',
|
|
20
|
+
},
|
|
21
|
+
'qwen.login_ok': {
|
|
22
|
+
ru: 'Qwen: вход выполнен.',
|
|
23
|
+
en: 'Qwen: signed in.',
|
|
24
|
+
},
|
|
25
|
+
'qwen.login_failed': {
|
|
26
|
+
ru: 'Qwen: войти не удалось. Войдите вручную в окне браузера (запустите с --headed) и повторите.',
|
|
27
|
+
en: 'Qwen: sign-in failed. Sign in manually in the browser window (launch with --headed) and retry.',
|
|
28
|
+
},
|
|
29
|
+
'qwen.attach_unsupported': {
|
|
30
|
+
ru: 'Qwen: вложения пока не поддерживаются — файлы не отправлены.',
|
|
31
|
+
en: 'Qwen: attachments are not supported yet — files were not sent.',
|
|
32
|
+
},
|
|
33
|
+
'qwen.guest_limit': {
|
|
34
|
+
ru: 'Qwen: исчерпан суточный лимит гостя. Войдите в аккаунт (email / Google / GitHub) командой /provider login или запустите с --headed.',
|
|
35
|
+
en: 'Qwen: the guest daily limit is exhausted. Sign in (email / Google / GitHub) with /provider login, or launch with --headed.',
|
|
36
|
+
},
|
|
37
|
+
'qwen.restart_browser': {
|
|
38
|
+
ru: '⚠ Qwen: страница браузера потеряна — перезапускаю и вхожу заново...',
|
|
39
|
+
en: '⚠ Qwen: the browser lost the page — restarting and signing in again...',
|
|
40
|
+
},
|
|
41
|
+
'qwen.restart_failed': {
|
|
42
|
+
ru: '⚠ Qwen: не удалось перезапустить браузер: {error}',
|
|
43
|
+
en: '⚠ Qwen: could not restart the browser: {error}',
|
|
44
|
+
},
|
|
45
|
+
'qwen.model_set': {
|
|
46
|
+
ru: 'Модель Qwen: {v}',
|
|
47
|
+
en: 'Qwen model: {v}',
|
|
48
|
+
},
|
|
49
|
+
'qwen.model_list': {
|
|
50
|
+
ru: 'Доступные модели Qwen:\n{v}',
|
|
51
|
+
en: 'Available Qwen models:\n{v}',
|
|
52
|
+
},
|
|
53
|
+
'qwen.model_error': {
|
|
54
|
+
ru: 'Не удалось получить список моделей Qwen: {v}',
|
|
55
|
+
en: 'Failed to fetch the Qwen model list: {v}',
|
|
56
|
+
},
|
|
57
|
+
'provider.switched': {
|
|
58
|
+
ru: 'Провайдер чата: {v}. Перезапустите zames, чтобы сменить браузер.',
|
|
59
|
+
en: 'Chat provider: {v}. Restart zames to switch the browser.',
|
|
60
|
+
},
|
|
61
|
+
'provider.usage': {
|
|
62
|
+
ru: 'Использование: /provider <deepseek|qwen> | /provider login | /provider status',
|
|
63
|
+
en: 'Usage: /provider <deepseek|qwen> | /provider login | /provider status',
|
|
64
|
+
},
|
|
65
|
+
'provider.handoff_saved': {
|
|
66
|
+
ru: 'Контекст текущего чата сжат и сохранён для {v}. Он подхватится при следующем запуске.',
|
|
67
|
+
en: 'The current chat was summarized and saved for {v}. It will be carried over on the next launch.',
|
|
68
|
+
},
|
|
69
|
+
'provider.handoff_used': {
|
|
70
|
+
ru: '📎 Подхватил контекст предыдущего провайдера (carried-over summary).',
|
|
71
|
+
en: '📎 Carried over the previous provider context (handoff summary).',
|
|
72
|
+
},
|
|
73
|
+
'provider.handoff_empty': {
|
|
74
|
+
ru: 'Не удалось сжать историю — контекст НЕ перенесён. Текущий чат останется у старого провайдера.',
|
|
75
|
+
en: 'Could not summarize the chat — context NOT carried over. The current chat stays with the old provider.',
|
|
76
|
+
},
|
|
77
|
+
'provider.login_done': {
|
|
78
|
+
ru: 'Проверка входа завершена.',
|
|
79
|
+
en: 'Login check complete.',
|
|
80
|
+
},
|
|
81
|
+
'provider.status': {
|
|
82
|
+
ru: 'Провайдер: {v}, модель: {m}, вход: {auth}',
|
|
83
|
+
en: 'Provider: {v}, model: {m}, login: {auth}',
|
|
84
|
+
},
|
|
85
|
+
};
|
package/dist/i18n.js
CHANGED
|
@@ -16,6 +16,7 @@ import { selfReviewMessages } from './i18n/self-review.js';
|
|
|
16
16
|
import { mcpMessages } from './i18n/mcp.js';
|
|
17
17
|
import { configMessages } from './i18n/config.js';
|
|
18
18
|
import { promptMessages } from './i18n/prompt.js';
|
|
19
|
+
import { qwenMessages } from './i18n/qwen.js';
|
|
19
20
|
export const DEFAULT_LOCALE = 'ru';
|
|
20
21
|
export const LOCALES = [
|
|
21
22
|
{ code: 'ru', name: 'Русский', englishName: 'Russian' },
|
|
@@ -47,6 +48,7 @@ export const CATALOG = {
|
|
|
47
48
|
...mcpMessages,
|
|
48
49
|
...configMessages,
|
|
49
50
|
...promptMessages,
|
|
51
|
+
...qwenMessages,
|
|
50
52
|
};
|
|
51
53
|
export function translate(locale) {
|
|
52
54
|
const loc = isLocale(locale) ? locale : DEFAULT_LOCALE;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ 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 { QwenBrowser } from './qwen-browser.js';
|
|
10
|
+
import { isProviderId, asProviderId, providerChatUrlPrefix, providerLabel, } from './providers.js';
|
|
9
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';
|
|
@@ -14,7 +16,7 @@ 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
21
|
import { formatConfigList } from './config-commands.js';
|
|
20
22
|
import { handleConfigCommand as runConfigCommand } from './config-command.js';
|
|
@@ -24,6 +26,7 @@ import { UndoStore } from './undo.js';
|
|
|
24
26
|
import { CheckpointStore, pickRecord as pickCheckpoint } from './checkpoint.js';
|
|
25
27
|
import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
26
28
|
import { performCompact } from './compact.js';
|
|
29
|
+
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
|
|
27
30
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
28
31
|
import { createCliArgs } from './cli-args.js';
|
|
29
32
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
@@ -121,10 +124,14 @@ if (jsonMode) {
|
|
|
121
124
|
function saveLastChat(id, workdir = '', title = '') {
|
|
122
125
|
if (!id)
|
|
123
126
|
return;
|
|
124
|
-
|
|
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 });
|
|
125
132
|
}
|
|
126
133
|
function loadLastChat(workdir = '') {
|
|
127
|
-
const s = loadLastSession(workdir);
|
|
134
|
+
const s = loadLastSession(workdir, config.provider);
|
|
128
135
|
return s ? s.id : null;
|
|
129
136
|
}
|
|
130
137
|
// Persistent per-project session goal (set by /goal). Kept OUT of the session
|
|
@@ -591,6 +598,21 @@ async function resolveWorkdir() {
|
|
|
591
598
|
return dir;
|
|
592
599
|
}
|
|
593
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
|
+
}
|
|
594
616
|
/**
|
|
595
617
|
* Print the dialogue of the just-opened chat into the terminal. Called by
|
|
596
618
|
* /resume, /resume-id and on startup when a previous session is restored, so
|
|
@@ -757,24 +779,52 @@ async function main() {
|
|
|
757
779
|
catch (e) {
|
|
758
780
|
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
759
781
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
config.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
+
});
|
|
778
828
|
const bootSpinner = mod.createSpinner(currentLocale);
|
|
779
829
|
bootSpinner.thinking();
|
|
780
830
|
try {
|
|
@@ -821,7 +871,7 @@ async function main() {
|
|
|
821
871
|
// explicitly: --chat <id> or --resume-last.
|
|
822
872
|
let resumeId = chatIdArg;
|
|
823
873
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
824
|
-
const last = loadLastSession(currentWorkdir);
|
|
874
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
825
875
|
if (last && last.id) {
|
|
826
876
|
resumeId = last.id;
|
|
827
877
|
console.log(theme.system(t('msg.resuming', { id: resumeId })));
|
|
@@ -937,7 +987,7 @@ async function main() {
|
|
|
937
987
|
},
|
|
938
988
|
transcript,
|
|
939
989
|
maxSubagents: config.browser.maxSubagents,
|
|
940
|
-
askDeadlineMs: config
|
|
990
|
+
askDeadlineMs: providerAskDeadlineMs(config),
|
|
941
991
|
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
942
992
|
// An `ask` permission rule must also gate the subagent: otherwise a
|
|
943
993
|
// guarded tool (e.g. a dangerous Bash command) would run inside the
|
|
@@ -1075,7 +1125,7 @@ async function main() {
|
|
|
1075
1125
|
// --new-chat kept for compatibility (this is the default behavior)
|
|
1076
1126
|
let resumeId = chatIdArg;
|
|
1077
1127
|
if (!resumeId && resumeLastFlag && !newChatFlag) {
|
|
1078
|
-
const last = loadLastSession(currentWorkdir);
|
|
1128
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
1079
1129
|
if (last && last.id) {
|
|
1080
1130
|
resumeId = last.id;
|
|
1081
1131
|
console.log(theme.system(t('msg.resuming', {
|
|
@@ -1137,6 +1187,8 @@ async function main() {
|
|
|
1137
1187
|
});
|
|
1138
1188
|
editor = ed;
|
|
1139
1189
|
ed.setTmpDir(TMP_DIR);
|
|
1190
|
+
// Provider badge: show which backend is answering ([Qwen]/[DeepSeek]).
|
|
1191
|
+
ed.setProvider(providerLabel(config.provider));
|
|
1140
1192
|
// Restore the input history from the previous runs and persist every new
|
|
1141
1193
|
// entry, so Ctrl+R / arrows can recall older messages after a restart.
|
|
1142
1194
|
ed.history = loadHistory();
|
|
@@ -1983,10 +2035,133 @@ async function main() {
|
|
|
1983
2035
|
}
|
|
1984
2036
|
continue;
|
|
1985
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
|
+
}
|
|
1986
2161
|
if (lower === '/chat') {
|
|
1987
2162
|
if (currentChatId) {
|
|
1988
2163
|
console.log(theme.system(t('chats.current_id', { v: currentChatId })));
|
|
1989
|
-
console.log(theme.system(`URL:
|
|
2164
|
+
console.log(theme.system(`URL: ${browserProviderUrl()}/${currentChatId}`));
|
|
1990
2165
|
}
|
|
1991
2166
|
else {
|
|
1992
2167
|
const id = await browser.getCurrentChatId();
|
|
@@ -2028,7 +2203,7 @@ async function main() {
|
|
|
2028
2203
|
if (lower === '/last') {
|
|
2029
2204
|
// Quick resume of the last chat for THIS working directory — otherwise
|
|
2030
2205
|
// finding it meant /chats (a network fetch) or copying the id.
|
|
2031
|
-
const last = loadLastSession(currentWorkdir);
|
|
2206
|
+
const last = loadLastSession(currentWorkdir, config.provider);
|
|
2032
2207
|
if (!last || !last.id) {
|
|
2033
2208
|
console.error(theme.error(t('sessions.none')));
|
|
2034
2209
|
continue;
|
|
@@ -2721,6 +2896,8 @@ async function main() {
|
|
|
2721
2896
|
uiPresent: uiHealth?.present,
|
|
2722
2897
|
uiMissing: uiHealth?.missing,
|
|
2723
2898
|
uiMissingCritical: uiHealth?.missingCritical,
|
|
2899
|
+
provider: config.provider,
|
|
2900
|
+
providerModel: config.provider === 'qwen' ? config.qwen.model : undefined,
|
|
2724
2901
|
}, t)));
|
|
2725
2902
|
continue;
|
|
2726
2903
|
}
|
|
@@ -2755,7 +2932,7 @@ async function main() {
|
|
|
2755
2932
|
ui: editor,
|
|
2756
2933
|
buildSystemPrompt: mod.buildSystemPrompt,
|
|
2757
2934
|
tools: mod.createTools(currentWorkdir, { undo, todos: todoStore }),
|
|
2758
|
-
answerTimeoutMs:
|
|
2935
|
+
answerTimeoutMs: providerAnswerTimeoutMs(),
|
|
2759
2936
|
fallbackLimit: COMPACT_FALLBACK_LIMIT,
|
|
2760
2937
|
selfImprovement: devMode,
|
|
2761
2938
|
});
|
|
@@ -3112,6 +3289,27 @@ async function main() {
|
|
|
3112
3289
|
}
|
|
3113
3290
|
}
|
|
3114
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
|
+
}
|
|
3115
3313
|
// Dev mode: pick up fresh logic modules before the task.
|
|
3116
3314
|
await autoReload();
|
|
3117
3315
|
// Re-scan skills/custom commands before every task: a SKILL.md or a
|
|
@@ -3185,7 +3383,7 @@ async function main() {
|
|
|
3185
3383
|
undo,
|
|
3186
3384
|
todos: todoStore,
|
|
3187
3385
|
}),
|
|
3188
|
-
answerTimeoutMs:
|
|
3386
|
+
answerTimeoutMs: providerAnswerTimeoutMs(),
|
|
3189
3387
|
quiet: true,
|
|
3190
3388
|
skipUiLock: true,
|
|
3191
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() +
|