zames_pro 2.80.8 → 2.81.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 +36 -0
- package/README.md +71 -21
- package/dist/browser-login.js +177 -0
- package/dist/browser-pure.js +112 -8
- package/dist/browser.js +274 -16
- package/dist/commands.js +24 -8
- package/dist/config.js +106 -2
- package/dist/glm-browser.js +1766 -0
- package/dist/glm-chats.js +172 -0
- package/dist/glm-flags.js +75 -0
- package/dist/glm-net-capture.js +338 -0
- package/dist/glm-ui.js +171 -0
- package/dist/i18n/common.js +18 -2
- package/dist/i18n/config.js +41 -1
- package/dist/i18n/doctor.js +11 -6
- package/dist/i18n/glm.js +86 -0
- package/dist/i18n/help.js +14 -10
- package/dist/i18n/qwen.js +47 -15
- package/dist/i18n/self-review.js +2 -2
- package/dist/i18n.js +2 -0
- package/dist/index.js +325 -114
- package/dist/provider-handoff.js +11 -0
- package/dist/providers.js +65 -0
- package/dist/qwen-browser.js +274 -30
- package/dist/send-runtime.js +112 -0
- package/dist/send-telemetry.js +99 -0
- package/dist/send-throttle.js +108 -0
- package/dist/system-prompt.js +23 -2
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import './color.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs/promises';
|
|
5
|
-
import {
|
|
5
|
+
import { readFileSync, 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
9
|
import { QwenBrowser } from './qwen-browser.js';
|
|
10
|
-
import {
|
|
10
|
+
import { GlmBrowser } from './glm-browser.js';
|
|
11
|
+
import { authMarkerHas } from './browser-pure.js';
|
|
12
|
+
import { canShowWindow } from './browser-login.js';
|
|
13
|
+
import { isProviderId, asProviderId, providerChatUrlPrefix, providerLabel, providerHasModelPicker, providerNeedsVisibleBrowser, providerConfigSection, PROVIDER_IDS, } from './providers.js';
|
|
11
14
|
import { createTools, mergeTools, filterToolsForReadOnly, filterToolsByAllowList, } from './tools.js';
|
|
12
15
|
import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from './extraTools.js';
|
|
13
16
|
import { runAgentLoop } from './agent-loop.js';
|
|
@@ -30,10 +33,11 @@ import { selfReview, selfDiff, selfApply, selfList } from './self-review.js';
|
|
|
30
33
|
import { performCompact } from './compact.js';
|
|
31
34
|
import { clampGoalCondition } from './goal.js';
|
|
32
35
|
import { applyBrowserLiveConfig, applyStoreLiveConfig } from './config-live.js';
|
|
33
|
-
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
|
|
36
|
+
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, handoffFromLabel, clearProviderHandoff, } from './provider-handoff.js';
|
|
34
37
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
35
38
|
import { createCliArgs } from './cli-args.js';
|
|
36
39
|
import { qwenContextWindow } from './qwen-ui.js';
|
|
40
|
+
import { glmContextWindow } from './glm-ui.js';
|
|
37
41
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
38
42
|
import { contentWidth } from './width.js';
|
|
39
43
|
import { renderStartupBanner } from './banner.js';
|
|
@@ -53,11 +57,13 @@ let lastAssistantMessage = '';
|
|
|
53
57
|
// cannot do anything useful and would just hammer the rate limit, so we refuse
|
|
54
58
|
// it. It does NOT touch the throttle itself.
|
|
55
59
|
const MIN_LOOP_MS = 15_000;
|
|
56
|
-
// True when a
|
|
57
|
-
// Used by /doctor to report that auto
|
|
58
|
-
|
|
60
|
+
// True when a session/credentials for `provider` were stored earlier
|
|
61
|
+
// (auth.json). Used by /doctor and the startup banner to report that auto
|
|
62
|
+
// re-login is ready. The marker is PER PROVIDER (see authMarkerHas).
|
|
63
|
+
function authMarkerExists(provider = config.provider) {
|
|
59
64
|
try {
|
|
60
|
-
|
|
65
|
+
const raw = readFileSync(path.join(ZAMES_HOME, 'auth.json'), 'utf-8');
|
|
66
|
+
return authMarkerHas(raw, provider);
|
|
61
67
|
}
|
|
62
68
|
catch {
|
|
63
69
|
return false;
|
|
@@ -76,7 +82,16 @@ const { getArg, hasFlag, getPositional } = createCliArgs(process.argv.slice(2));
|
|
|
76
82
|
// compaction).
|
|
77
83
|
const COMPACT_FALLBACK_LIMIT = 40;
|
|
78
84
|
const config = loadConfig();
|
|
79
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Keep the legacy `qwen.enabled` mirror in sync with the ACTIVE provider. The
|
|
87
|
+
* field is written in one place (it used to be set from three call sites) and
|
|
88
|
+
* is no longer READ by the code — it survives only so an old config keeps its
|
|
89
|
+
* meaning. PURE state change on the live config object.
|
|
90
|
+
*/
|
|
91
|
+
function setProviderEnabledFlag(provider) {
|
|
92
|
+
config.qwen.enabled = provider === 'qwen';
|
|
93
|
+
}
|
|
94
|
+
// A `--provider <id>` CLI flag selects the backend for THIS run,
|
|
80
95
|
// mirroring the /provider command (which changes it live). Without this an
|
|
81
96
|
// operator who typed `--provider qwen` got the DEFAULT backend — the flag was
|
|
82
97
|
// silently treated as free-form task text and DeepSeek opened instead of Qwen.
|
|
@@ -84,7 +99,7 @@ const providerArg = getArg('--provider', null);
|
|
|
84
99
|
if (providerArg !== null) {
|
|
85
100
|
if (isProviderId(providerArg)) {
|
|
86
101
|
config.provider = providerArg;
|
|
87
|
-
|
|
102
|
+
setProviderEnabledFlag(providerArg);
|
|
88
103
|
}
|
|
89
104
|
else {
|
|
90
105
|
console.error(theme.warn('Unknown --provider "' +
|
|
@@ -107,9 +122,17 @@ let currentLocale = isLocale(config.ui?.locale)
|
|
|
107
122
|
const t = (key, params) => translate(currentLocale)(key, params);
|
|
108
123
|
// Headless is the default. --headed forces a visible window (useful for the
|
|
109
124
|
// first sign-in or debugging the DOM). --headless keeps it explicit.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
// DeepSeek/Qwen run headless by default; GLM CANNOT: z.ai answers a headless
|
|
126
|
+
// send with an Aliyun captcha (see PROVIDERS[id].needsVisibleBrowser). An
|
|
127
|
+
// explicit --headless still wins, and --headed forces a window everywhere.
|
|
128
|
+
function headlessFor(provider) {
|
|
129
|
+
if (hasFlag('--headed'))
|
|
130
|
+
return false;
|
|
131
|
+
if (hasFlag('--headless'))
|
|
132
|
+
return true;
|
|
133
|
+
return config.headless && !providerNeedsVisibleBrowser(provider);
|
|
134
|
+
}
|
|
135
|
+
const headless = headlessFor(config.provider);
|
|
113
136
|
const debug = hasFlag('--debug') || config.debug;
|
|
114
137
|
const maxIterArg = getArg('--max-iter', null);
|
|
115
138
|
// A non-numeric `--max-iter abc` yields NaN, which the loop treats as
|
|
@@ -314,6 +337,23 @@ await reloadModules();
|
|
|
314
337
|
// Enabled by the --dev flag (`npm run dev` sets it) or config.hotReload === true.
|
|
315
338
|
// In normal mode (npm start, global zames) auto-reload is off.
|
|
316
339
|
const devMode = hasFlag('--dev') || config.hotReload === true;
|
|
340
|
+
// The saved credentials of the ACTIVE provider: DeepSeek keeps the legacy
|
|
341
|
+
// `browser.auth` (so an old config works), every other backend owns its own
|
|
342
|
+
// section, so a DeepSeek login is never typed into a Qwen/GLM form.
|
|
343
|
+
function activeAuth() {
|
|
344
|
+
const section = providerConfigSection(config.provider);
|
|
345
|
+
if (!section) {
|
|
346
|
+
return {
|
|
347
|
+
username: config.browser.auth.username,
|
|
348
|
+
password: config.browser.auth.password,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const sec = config[section];
|
|
352
|
+
return {
|
|
353
|
+
username: (sec && sec.auth && sec.auth.username) || '',
|
|
354
|
+
password: (sec && sec.auth && sec.auth.password) || '',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
317
357
|
// Safe auto-reload: on a load error we keep the previous working modules.
|
|
318
358
|
async function autoReload() {
|
|
319
359
|
if (!devMode)
|
|
@@ -485,7 +525,9 @@ ${theme.bold(t('help.sec.context'))}
|
|
|
485
525
|
${t('help.cmd.lang')}
|
|
486
526
|
|
|
487
527
|
${theme.bold(t('help.sec.provider'))}
|
|
488
|
-
${t('help.cmd.provider')}${config.provider
|
|
528
|
+
${t('help.cmd.provider')}${providerHasModelPicker(config.provider)
|
|
529
|
+
? '\n ' + t('help.cmd.model')
|
|
530
|
+
: ''}
|
|
489
531
|
|
|
490
532
|
${theme.bold(t('help.sec.files'))}
|
|
491
533
|
${t('help.cmd.transcript')}
|
|
@@ -495,6 +537,7 @@ ${theme.bold(t('help.sec.files'))}
|
|
|
495
537
|
${t('help.cmd.rewind_list')}
|
|
496
538
|
${t('help.cmd.export')}
|
|
497
539
|
${t('help.cmd.cost')}
|
|
540
|
+
${t('help.cmd.limits')}
|
|
498
541
|
${t('help.cmd.copy')}
|
|
499
542
|
${t('help.cmd.doctor')}
|
|
500
543
|
${t('help.cmd.add_dir')}
|
|
@@ -660,90 +703,171 @@ async function resolveWorkdir() {
|
|
|
660
703
|
* config.browser.answerTimeoutMs there cut them off; pick the active
|
|
661
704
|
* provider's own value.
|
|
662
705
|
*/
|
|
706
|
+
/** True when the ACTIVE provider derives its pause adaptively (see part C). */
|
|
707
|
+
function providerAdaptiveThrottle(provider) {
|
|
708
|
+
const section = providerConfigSection(provider);
|
|
709
|
+
if (!section)
|
|
710
|
+
return false;
|
|
711
|
+
const sec = config[section];
|
|
712
|
+
return !!sec && sec.adaptiveThrottle === true;
|
|
713
|
+
}
|
|
714
|
+
/** The configured model id of a provider ('n/a' when it has no model picker). */
|
|
715
|
+
function providerModelOf(provider) {
|
|
716
|
+
const section = providerConfigSection(provider);
|
|
717
|
+
if (!section)
|
|
718
|
+
return 'n/a';
|
|
719
|
+
const sec = config[section];
|
|
720
|
+
const m = sec ? sec.model : '';
|
|
721
|
+
return typeof m === 'string' ? m : '';
|
|
722
|
+
}
|
|
663
723
|
function providerAnswerTimeoutMs() {
|
|
664
|
-
return config.
|
|
665
|
-
|
|
666
|
-
|
|
724
|
+
return providerSectionNumber('answerTimeoutMs', config.browser.answerTimeoutMs);
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Read a number from the ACTIVE provider's own config section (`qwen.*`,
|
|
728
|
+
* `glm.*`), or the shared fallback for DeepSeek (whose section is `browser`).
|
|
729
|
+
* This replaced `provider === 'qwen' ? config.qwen.x : shared`, which silently
|
|
730
|
+
* gave every NEW provider the DeepSeek timing.
|
|
731
|
+
*/
|
|
732
|
+
function providerSectionNumber(field, fallback) {
|
|
733
|
+
const section = providerConfigSection(config.provider);
|
|
734
|
+
if (!section)
|
|
735
|
+
return fallback;
|
|
736
|
+
const s = config[section];
|
|
737
|
+
const v = s ? s[field] : undefined;
|
|
738
|
+
return typeof v === 'number' ? v : fallback;
|
|
739
|
+
}
|
|
740
|
+
/** The per-provider config section object, or null for DeepSeek. */
|
|
741
|
+
function activeProviderSection() {
|
|
742
|
+
const section = providerConfigSection(config.provider);
|
|
743
|
+
if (!section)
|
|
744
|
+
return null;
|
|
745
|
+
return (config[section] ||
|
|
746
|
+
null);
|
|
667
747
|
}
|
|
668
748
|
/** The provider's base chat URL, used by /chat to build the right link. */
|
|
669
749
|
function browserProviderUrl() {
|
|
670
750
|
return providerChatUrlPrefix(config.provider);
|
|
671
751
|
}
|
|
672
752
|
/**
|
|
673
|
-
* The context window (tokens) for the ACTIVE provider. The
|
|
674
|
-
*
|
|
675
|
-
* number would either overstate
|
|
676
|
-
*
|
|
753
|
+
* The context window (tokens) for the ACTIVE provider. The backends advertise
|
|
754
|
+
* different windows and a provider's window may be PER-MODEL (Qwen), so a
|
|
755
|
+
* single global number would either overstate a small model or understate a
|
|
756
|
+
* 1M build — and the number is the denominator of the auto-compact threshold,
|
|
677
757
|
* so an error directly delays (or prematurely fires) compaction.
|
|
678
758
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
759
|
+
* The provider's own `contextLimit` wins; DeepSeek (which has no section)
|
|
760
|
+
* falls back to `ui.contextLimit`.
|
|
681
761
|
*/
|
|
682
762
|
function effectiveContextLimit() {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
: config.ui.contextLimit;
|
|
687
|
-
}
|
|
763
|
+
const own = providerSectionNumber('contextLimit', 0);
|
|
764
|
+
if (own > 0)
|
|
765
|
+
return own;
|
|
688
766
|
return config.ui.contextLimit;
|
|
689
767
|
}
|
|
690
768
|
/**
|
|
691
|
-
* Re-derive `
|
|
692
|
-
* by `/model` on an explicit pick (never on startup), so a
|
|
693
|
-
* `/config set
|
|
694
|
-
* again. An UNKNOWN model id leaves the limit alone: guessing
|
|
695
|
-
* fire auto-compact too early or too late. The live editor is
|
|
696
|
-
* the caller (it owns the `editor` reference); this only touches
|
|
769
|
+
* Re-derive the ACTIVE provider's `contextLimit` from a picked model id and
|
|
770
|
+
* persist it. Called by `/model` on an explicit pick (never on startup), so a
|
|
771
|
+
* manual `/config set <provider>.contextLimit` survives until the operator
|
|
772
|
+
* switches model again. An UNKNOWN model id leaves the limit alone: guessing
|
|
773
|
+
* would either fire auto-compact too early or too late. The live editor is
|
|
774
|
+
* repainted by the caller (it owns the `editor` reference); this only touches
|
|
775
|
+
* config.
|
|
697
776
|
*/
|
|
698
777
|
function applyModelContextLimit(model) {
|
|
699
|
-
const
|
|
778
|
+
const section = providerConfigSection(config.provider);
|
|
779
|
+
if (!section)
|
|
780
|
+
return;
|
|
781
|
+
const window = config.provider === 'qwen' ? qwenContextWindow(model) : glmContextWindow(model);
|
|
700
782
|
if (window === null)
|
|
701
783
|
return;
|
|
702
|
-
|
|
784
|
+
const sec = activeProviderSection();
|
|
785
|
+
if (!sec)
|
|
786
|
+
return;
|
|
787
|
+
sec.contextLimit = window;
|
|
703
788
|
try {
|
|
704
|
-
writeConfigValue('home',
|
|
789
|
+
writeConfigValue('home', `${section}.contextLimit`, String(window));
|
|
705
790
|
}
|
|
706
791
|
catch { }
|
|
707
792
|
}
|
|
708
793
|
/**
|
|
709
794
|
* Build the browser for a provider. Called at startup AND on a live
|
|
710
795
|
* `/provider` switch, so both paths construct the class the same way. The
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
* config.browser and only
|
|
796
|
+
* backends differ in every layer (selectors, SSE format, chat-id scheme), so
|
|
797
|
+
* the choice is a class, not a flag; the shared timings live in
|
|
798
|
+
* config.browser and only the provider's own section is overridden.
|
|
714
799
|
*/
|
|
715
800
|
function createProviderBrowser(provider) {
|
|
801
|
+
// Saved credentials go into the ACTIVE provider's section: browser.auth
|
|
802
|
+
// belongs to DeepSeek (so old configs keep working), qwen.auth / glm.auth
|
|
803
|
+
// are their own. Sharing one pair meant a DeepSeek login was typed into the
|
|
804
|
+
// Qwen form.
|
|
716
805
|
const persistAuth = (username, password) => {
|
|
717
|
-
|
|
718
|
-
|
|
806
|
+
const section = providerConfigSection(provider);
|
|
807
|
+
const prefix = section ? section + '.auth' : 'browser.auth';
|
|
808
|
+
const target = section
|
|
809
|
+
? config[section]
|
|
810
|
+
: null;
|
|
811
|
+
if (target) {
|
|
812
|
+
if (!target.auth)
|
|
813
|
+
target.auth = { username: '', password: '' };
|
|
814
|
+
target.auth.username = username;
|
|
815
|
+
target.auth.password = password;
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
config.browser.auth.username = username;
|
|
819
|
+
config.browser.auth.password = password;
|
|
820
|
+
}
|
|
719
821
|
try {
|
|
720
|
-
writeConfigValue('home', '
|
|
721
|
-
writeConfigValue('home', '
|
|
822
|
+
writeConfigValue('home', prefix + '.username', username);
|
|
823
|
+
writeConfigValue('home', prefix + '.password', password);
|
|
722
824
|
}
|
|
723
825
|
catch { }
|
|
724
826
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
827
|
+
switch (provider) {
|
|
828
|
+
case 'qwen':
|
|
829
|
+
return new QwenBrowser({
|
|
830
|
+
headless: headlessFor('qwen'),
|
|
831
|
+
debug,
|
|
832
|
+
channel: config.browserChannel,
|
|
833
|
+
...config.browser,
|
|
834
|
+
// Qwen-specific overrides (see config.qwen): no 15s DeepSeek
|
|
835
|
+
// throttle, a longer cold-start answer timeout.
|
|
836
|
+
minSendIntervalMs: config.qwen.minSendIntervalMs,
|
|
837
|
+
answerTimeoutMs: config.qwen.answerTimeoutMs,
|
|
838
|
+
model: config.qwen.model,
|
|
839
|
+
auth: config.qwen.auth,
|
|
840
|
+
loginTimeoutMs: config.browser.loginTimeoutMs,
|
|
841
|
+
locale: currentLocale,
|
|
842
|
+
onAuthSave: persistAuth,
|
|
843
|
+
});
|
|
844
|
+
case 'glm':
|
|
845
|
+
return new GlmBrowser({
|
|
846
|
+
headless: headlessFor('glm'),
|
|
847
|
+
debug,
|
|
848
|
+
channel: config.browserChannel,
|
|
849
|
+
...config.browser,
|
|
850
|
+
minSendIntervalMs: config.glm.minSendIntervalMs,
|
|
851
|
+
answerTimeoutMs: config.glm.answerTimeoutMs,
|
|
852
|
+
model: config.glm.model,
|
|
853
|
+
auth: config.glm.auth,
|
|
854
|
+
sendTelemetry: config.browser.sendTelemetry,
|
|
855
|
+
logsDir: config.transcript.dir,
|
|
856
|
+
loginTimeoutMs: config.browser.loginTimeoutMs,
|
|
857
|
+
locale: currentLocale,
|
|
858
|
+
onAuthSave: persistAuth,
|
|
859
|
+
});
|
|
860
|
+
case 'deepseek':
|
|
861
|
+
default:
|
|
862
|
+
return new DeepSeekBrowser({
|
|
863
|
+
headless: headlessFor('deepseek'),
|
|
864
|
+
debug,
|
|
865
|
+
channel: config.browserChannel,
|
|
866
|
+
...config.browser,
|
|
867
|
+
locale: currentLocale,
|
|
868
|
+
onAuthSave: persistAuth,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
747
871
|
}
|
|
748
872
|
/**
|
|
749
873
|
* Print the dialogue of the just-opened chat into the terminal. Called by
|
|
@@ -957,10 +1081,16 @@ async function main() {
|
|
|
957
1081
|
// never block startup.
|
|
958
1082
|
const warnUiHealth = (h) => {
|
|
959
1083
|
if (h.missingCritical.length) {
|
|
960
|
-
console.error(theme.warn(t('ui.probe_failed', {
|
|
1084
|
+
console.error(theme.warn(t('ui.probe_failed', {
|
|
1085
|
+
v: h.missingCritical.join(', '),
|
|
1086
|
+
provider: providerLabel(config.provider),
|
|
1087
|
+
})));
|
|
961
1088
|
}
|
|
962
1089
|
else if (h.missing.length) {
|
|
963
|
-
console.error(theme.warn(t('ui.probe_degraded', {
|
|
1090
|
+
console.error(theme.warn(t('ui.probe_degraded', {
|
|
1091
|
+
v: h.missing.join(', '),
|
|
1092
|
+
provider: providerLabel(config.provider),
|
|
1093
|
+
})));
|
|
964
1094
|
}
|
|
965
1095
|
};
|
|
966
1096
|
try {
|
|
@@ -1021,14 +1151,20 @@ async function main() {
|
|
|
1021
1151
|
await transcript.close();
|
|
1022
1152
|
return;
|
|
1023
1153
|
}
|
|
1154
|
+
if (providerNeedsVisibleBrowser(config.provider) && headlessFor(config.provider)) {
|
|
1155
|
+
console.log(theme.warn(t('provider.needs_headed', { v: providerLabel(config.provider) })));
|
|
1156
|
+
}
|
|
1024
1157
|
console.log(theme.system(t('msg.interactive')));
|
|
1025
1158
|
console.log(theme.system(t('msg.queue_hint')));
|
|
1026
1159
|
console.log(theme.dim(t('msg.first_hint')));
|
|
1027
|
-
// No stored session yet (auth.json absent
|
|
1028
|
-
// The auto-login handles it, but a heads-up in
|
|
1029
|
-
// from wondering why the first task pauses
|
|
1160
|
+
// No stored session yet (auth.json absent for THIS provider): the first
|
|
1161
|
+
// task will need a sign-in. The auto-login handles it, but a heads-up in
|
|
1162
|
+
// the banner saves the operator from wondering why the first task pauses.
|
|
1163
|
+
// The hint text describes the browser-based flow (see src/browser-login.ts);
|
|
1164
|
+
// the old wording promised a terminal prompt, which is no longer the main
|
|
1165
|
+
// path.
|
|
1030
1166
|
if (!authMarkerExists()) {
|
|
1031
|
-
console.log(theme.warn(t('msg.first_login_hint')));
|
|
1167
|
+
console.log(theme.warn(t('msg.first_login_hint', { provider: providerLabel(config.provider) })));
|
|
1032
1168
|
}
|
|
1033
1169
|
// Nudge when BACKLOG.md has grown large: /improve reads the whole file, so a
|
|
1034
1170
|
// bloated backlog costs tokens on every self-improvement run. Best-effort.
|
|
@@ -1390,8 +1526,10 @@ async function main() {
|
|
|
1390
1526
|
const { runLoginForm } = await import('./tui/run-login.js');
|
|
1391
1527
|
return await runLoginForm({
|
|
1392
1528
|
t,
|
|
1393
|
-
|
|
1394
|
-
|
|
1529
|
+
// The SAVED credentials of the ACTIVE provider (DeepSeek keeps
|
|
1530
|
+
// browser.auth; Qwen/GLM own their section).
|
|
1531
|
+
initialUsername: activeAuth().username,
|
|
1532
|
+
initialPassword: activeAuth().password,
|
|
1395
1533
|
note: info.hasSavedPassword || info.savedUsername
|
|
1396
1534
|
? t('auth.form.note_saved')
|
|
1397
1535
|
: undefined,
|
|
@@ -1737,10 +1875,11 @@ async function main() {
|
|
|
1737
1875
|
}
|
|
1738
1876
|
// The context window size feeds the status-bar percentage and its color.
|
|
1739
1877
|
// Apply it to the live editor immediately (the value is a plain number,
|
|
1740
|
-
// no restart needed).
|
|
1741
|
-
//
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1878
|
+
// no restart needed). ANY provider's limit matters: a /model pick rewrites
|
|
1879
|
+
// the active provider's own value, and a manual /config set on a non-active
|
|
1880
|
+
// provider must not leave the editor showing the old denominator when the
|
|
1881
|
+
// operator switches to it.
|
|
1882
|
+
if ((path === 'ui.contextLimit' || /\.contextLimit$/.test(path)) &&
|
|
1744
1883
|
typeof value === 'number') {
|
|
1745
1884
|
if (editor) {
|
|
1746
1885
|
editor.setContextLimit(effectiveContextLimit());
|
|
@@ -1751,9 +1890,16 @@ async function main() {
|
|
|
1751
1890
|
// by the browser at construction, so push them onto the live instance.
|
|
1752
1891
|
// (deepThinking/webSearch are handled above because they also repaint the
|
|
1753
1892
|
// status icons.)
|
|
1893
|
+
//
|
|
1894
|
+
// `browser.*` knobs belong to DEEPSEEK. Each other backend copies its
|
|
1895
|
+
// timings from its OWN section, so applying a `browser.*` value to the
|
|
1896
|
+
// live Qwen/GLM instance would clobber its interval with DeepSeek's
|
|
1897
|
+
// 15s (the real bug: /config set browser.minSendIntervalMs rewrote the
|
|
1898
|
+
// Qwen send interval).
|
|
1754
1899
|
if (path.startsWith('browser.')) {
|
|
1755
1900
|
const b = browser;
|
|
1756
|
-
if (
|
|
1901
|
+
if (config.provider === 'deepseek' &&
|
|
1902
|
+
applyBrowserLiveConfig(path, value, b)) {
|
|
1757
1903
|
// minSendIntervalMs feeds the send countdown the UI renders.
|
|
1758
1904
|
if (editor)
|
|
1759
1905
|
editor.refreshStatus();
|
|
@@ -1764,15 +1910,15 @@ async function main() {
|
|
|
1764
1910
|
undo: undo,
|
|
1765
1911
|
checkpoints: checkpoints,
|
|
1766
1912
|
});
|
|
1767
|
-
//
|
|
1768
|
-
// the SAME names, so push them there when
|
|
1769
|
-
// no-op
|
|
1770
|
-
|
|
1913
|
+
// A provider's OWN timing knobs feed its browser constructor. The LIVE
|
|
1914
|
+
// fields use the SAME names, so push them there when that backend is
|
|
1915
|
+
// active (a no-op otherwise). Generalized from the two hardcoded qwen.*
|
|
1916
|
+
// paths so a new backend needs no new branch here.
|
|
1917
|
+
const timingMatch = path.match(/^(qwen|glm)\.(minSendIntervalMs|answerTimeoutMs)$/);
|
|
1918
|
+
if (timingMatch) {
|
|
1771
1919
|
const b = browser;
|
|
1772
|
-
if (config.provider ===
|
|
1773
|
-
const field =
|
|
1774
|
-
? 'minSendIntervalMs'
|
|
1775
|
-
: 'answerTimeoutMs';
|
|
1920
|
+
if (config.provider === timingMatch[1]) {
|
|
1921
|
+
const field = timingMatch[2];
|
|
1776
1922
|
if (field in b) {
|
|
1777
1923
|
b[field] = value;
|
|
1778
1924
|
if (editor)
|
|
@@ -1810,6 +1956,20 @@ async function main() {
|
|
|
1810
1956
|
// The DeepSeek toggles themselves are clicked on the next send by
|
|
1811
1957
|
// _applyToggles(), so this is safe even while a generation is in flight.
|
|
1812
1958
|
function applyLiveToggle(cmd) {
|
|
1959
|
+
// GLM's flagship models (x-preview-l, glm-5.3, glm-4.6v) report
|
|
1960
|
+
// skip_think=false in the catalogue: thinking CANNOT be turned off, and
|
|
1961
|
+
// the site always sends enable_thinking=true. Flipping the switch would
|
|
1962
|
+
// only mislead the operator (and the icon would lie), so it is refused.
|
|
1963
|
+
if (cmd.kind === 'thinking' &&
|
|
1964
|
+
config.provider === 'glm' &&
|
|
1965
|
+
cmd.mode !== 'on') {
|
|
1966
|
+
const line = t('toggle.thinking_locked');
|
|
1967
|
+
if (editor)
|
|
1968
|
+
editor.printAbove(theme.warn(line));
|
|
1969
|
+
else
|
|
1970
|
+
console.log(theme.warn(line));
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1813
1973
|
const b = browser;
|
|
1814
1974
|
const cur = cmd.kind === 'thinking' ? b.deepThinking : b.webSearch;
|
|
1815
1975
|
const next = cmd.mode === 'toggle' ? !cur : cmd.mode === 'on';
|
|
@@ -2404,7 +2564,7 @@ async function main() {
|
|
|
2404
2564
|
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
2405
2565
|
const arg = trimmed.slice('/provider'.length).trim().toLowerCase();
|
|
2406
2566
|
if (!arg || arg === 'status') {
|
|
2407
|
-
const model = config.provider
|
|
2567
|
+
const model = providerModelOf(config.provider) || '(default)';
|
|
2408
2568
|
console.log(theme.system(t('provider.status', {
|
|
2409
2569
|
v: config.provider,
|
|
2410
2570
|
m: model,
|
|
@@ -2483,6 +2643,7 @@ async function main() {
|
|
|
2483
2643
|
if (summary) {
|
|
2484
2644
|
saveProviderHandoff({
|
|
2485
2645
|
provider: arg,
|
|
2646
|
+
from,
|
|
2486
2647
|
workdir: currentWorkdir,
|
|
2487
2648
|
summary,
|
|
2488
2649
|
task: lastTaskText || undefined,
|
|
@@ -2504,7 +2665,7 @@ async function main() {
|
|
|
2504
2665
|
saveLastChat(currentChatId, currentWorkdir);
|
|
2505
2666
|
// Persist the choice FIRST so a crash mid-switch still remembers it.
|
|
2506
2667
|
config.provider = arg;
|
|
2507
|
-
|
|
2668
|
+
setProviderEnabledFlag(arg);
|
|
2508
2669
|
try {
|
|
2509
2670
|
writeConfigValue('home', 'provider', arg);
|
|
2510
2671
|
}
|
|
@@ -2535,6 +2696,9 @@ async function main() {
|
|
|
2535
2696
|
currentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
2536
2697
|
if (editor) {
|
|
2537
2698
|
editor.setProvider(providerLabel(arg));
|
|
2699
|
+
// The context window is provider-specific: without this the status
|
|
2700
|
+
// bar kept showing the OLD backend's denominator after a switch.
|
|
2701
|
+
editor.setContextLimit(effectiveContextLimit());
|
|
2538
2702
|
}
|
|
2539
2703
|
try {
|
|
2540
2704
|
warnUiHealth(await browser.probeUiHealth());
|
|
@@ -2546,7 +2710,7 @@ async function main() {
|
|
|
2546
2710
|
// The new backend did not come up: keep the OLD one active so the
|
|
2547
2711
|
// session keeps working, and roll the persisted choice back.
|
|
2548
2712
|
config.provider = from;
|
|
2549
|
-
|
|
2713
|
+
setProviderEnabledFlag(from);
|
|
2550
2714
|
try {
|
|
2551
2715
|
writeConfigValue('home', 'provider', from);
|
|
2552
2716
|
}
|
|
@@ -2562,23 +2726,28 @@ async function main() {
|
|
|
2562
2726
|
}
|
|
2563
2727
|
continue;
|
|
2564
2728
|
}
|
|
2565
|
-
console.error(theme.warn(t('provider.usage')));
|
|
2729
|
+
console.error(theme.warn(t('provider.usage', { providers: PROVIDER_IDS.join('|') })));
|
|
2566
2730
|
continue;
|
|
2567
2731
|
}
|
|
2568
2732
|
if (lower === '/model' || lower.startsWith('/model ')) {
|
|
2569
2733
|
const arg = trimmed.slice('/model'.length).trim();
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
console.log(theme.system(t('
|
|
2734
|
+
// `/model` is available to ANY provider that exposes a model picker
|
|
2735
|
+
// (Qwen, GLM). DeepSeek has none, so the command is hidden from the
|
|
2736
|
+
// hints there and only explains itself when typed by hand.
|
|
2737
|
+
if (!providerHasModelPicker(config.provider)) {
|
|
2738
|
+
console.log(theme.system(t('model.unsupported', { provider: providerLabel(config.provider) })));
|
|
2575
2739
|
continue;
|
|
2576
2740
|
}
|
|
2741
|
+
const section = providerConfigSection(config.provider);
|
|
2742
|
+
const sectionObj = activeProviderSection();
|
|
2743
|
+
if (!section || !sectionObj)
|
|
2744
|
+
continue;
|
|
2745
|
+
const curModel = String(sectionObj.model || '');
|
|
2577
2746
|
const qb = browser;
|
|
2578
2747
|
if (!arg) {
|
|
2579
2748
|
const models = qb.listModels ? await qb.listModels() : [];
|
|
2580
2749
|
if (!models.length) {
|
|
2581
|
-
console.log(theme.system(t('
|
|
2750
|
+
console.log(theme.system(t('model.list', { v: '(none)' })));
|
|
2582
2751
|
continue;
|
|
2583
2752
|
}
|
|
2584
2753
|
// In the TUI, open a proper SELECT LIST instead of dumping text.
|
|
@@ -2589,12 +2758,14 @@ async function main() {
|
|
|
2589
2758
|
try {
|
|
2590
2759
|
const { selectListInk } = await import('./tui/run-select.js');
|
|
2591
2760
|
picked = await selectListInk({
|
|
2592
|
-
title: t('
|
|
2593
|
-
|
|
2761
|
+
title: t('model.pick_title', {
|
|
2762
|
+
provider: providerLabel(config.provider),
|
|
2763
|
+
}),
|
|
2764
|
+
initial: curModel || models[0].id,
|
|
2594
2765
|
items: models.map((m) => ({
|
|
2595
2766
|
value: m.id,
|
|
2596
2767
|
label: m.name || m.id,
|
|
2597
|
-
detail: m.id ===
|
|
2768
|
+
detail: m.id === curModel ? t('tui.list.current') : m.id,
|
|
2598
2769
|
})),
|
|
2599
2770
|
locale: currentLocale,
|
|
2600
2771
|
});
|
|
@@ -2607,32 +2778,35 @@ async function main() {
|
|
|
2607
2778
|
editor.resume();
|
|
2608
2779
|
}
|
|
2609
2780
|
if (picked) {
|
|
2610
|
-
|
|
2781
|
+
sectionObj.model = picked;
|
|
2611
2782
|
if (typeof qb.model === 'string')
|
|
2612
2783
|
qb.model = picked;
|
|
2613
|
-
// Derive the context window from the picked model:
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2616
|
-
// `/config set
|
|
2617
|
-
// only runs on an explicit pick).
|
|
2784
|
+
// Derive the context window from the picked model: windows are
|
|
2785
|
+
// per-model, and this number is the denominator of the
|
|
2786
|
+
// auto-compact threshold. The operator's manual
|
|
2787
|
+
// `/config set <provider>.contextLimit` still wins afterwards
|
|
2788
|
+
// (this only runs on an explicit pick).
|
|
2618
2789
|
applyModelContextLimit(picked);
|
|
2619
2790
|
if (editor) {
|
|
2620
2791
|
editor.setContextLimit(effectiveContextLimit());
|
|
2621
2792
|
editor.refreshStatus();
|
|
2622
2793
|
}
|
|
2623
2794
|
try {
|
|
2624
|
-
writeConfigValue('home', '
|
|
2795
|
+
writeConfigValue('home', section + '.model', picked);
|
|
2625
2796
|
}
|
|
2626
2797
|
catch { }
|
|
2627
|
-
console.log(theme.system(t('
|
|
2798
|
+
console.log(theme.system(t('model.set', {
|
|
2799
|
+
provider: providerLabel(config.provider),
|
|
2800
|
+
v: picked,
|
|
2801
|
+
})));
|
|
2628
2802
|
}
|
|
2629
2803
|
continue;
|
|
2630
2804
|
}
|
|
2631
2805
|
const lines = models.map((m) => ` ${m.id} — ${m.name}`).join(NL);
|
|
2632
|
-
console.log(theme.system(t('
|
|
2806
|
+
console.log(theme.system(t('model.list', { v: lines })));
|
|
2633
2807
|
continue;
|
|
2634
2808
|
}
|
|
2635
|
-
|
|
2809
|
+
sectionObj.model = arg;
|
|
2636
2810
|
if (typeof qb.model === 'string')
|
|
2637
2811
|
qb.model = arg;
|
|
2638
2812
|
applyModelContextLimit(arg);
|
|
@@ -2641,10 +2815,10 @@ async function main() {
|
|
|
2641
2815
|
editor.refreshStatus();
|
|
2642
2816
|
}
|
|
2643
2817
|
try {
|
|
2644
|
-
writeConfigValue('home', '
|
|
2818
|
+
writeConfigValue('home', section + '.model', arg);
|
|
2645
2819
|
}
|
|
2646
2820
|
catch { }
|
|
2647
|
-
console.log(theme.system(t('
|
|
2821
|
+
console.log(theme.system(t('model.set', { provider: providerLabel(config.provider), v: arg })));
|
|
2648
2822
|
continue;
|
|
2649
2823
|
}
|
|
2650
2824
|
if (lower === '/chat') {
|
|
@@ -3328,6 +3502,37 @@ async function main() {
|
|
|
3328
3502
|
console.log(theme.system(mod.commands.formatDiffStat(out, { maxLines: 60 }, t)));
|
|
3329
3503
|
continue;
|
|
3330
3504
|
}
|
|
3505
|
+
if (lower === '/limits') {
|
|
3506
|
+
const { readSendEvents, summarizeSends, sendLogPath } = await import('./send-telemetry.js');
|
|
3507
|
+
const logsDir = config.transcript.dir;
|
|
3508
|
+
const events = readSendEvents(logsDir, config.provider);
|
|
3509
|
+
if (!events.length) {
|
|
3510
|
+
console.log(theme.system(t('limits.none', { path: sendLogPath(logsDir, config.provider) })));
|
|
3511
|
+
}
|
|
3512
|
+
else {
|
|
3513
|
+
const st = summarizeSends(events);
|
|
3514
|
+
console.log(theme.system(t('limits.title', { provider: providerLabel(config.provider) })));
|
|
3515
|
+
for (const [label, n] of [
|
|
3516
|
+
['10m', st.last10m],
|
|
3517
|
+
['30m', st.last30m],
|
|
3518
|
+
['60m', st.last60m],
|
|
3519
|
+
]) {
|
|
3520
|
+
console.log(' ' + t('limits.window', { label, n: String(n) }));
|
|
3521
|
+
}
|
|
3522
|
+
console.log(' ' +
|
|
3523
|
+
t('limits.rate', {
|
|
3524
|
+
n: String(st.rateLimits),
|
|
3525
|
+
sec: String(Math.round(st.avgBlockMs / 1000)),
|
|
3526
|
+
}));
|
|
3527
|
+
const adaptive = providerAdaptiveThrottle(config.provider);
|
|
3528
|
+
console.log(' ' +
|
|
3529
|
+
t('limits.interval', {
|
|
3530
|
+
sec: String(Math.round(browser.sendIntervalMs() / 1000)),
|
|
3531
|
+
adaptive: adaptive ? t('limits.adaptive_on') : '',
|
|
3532
|
+
}));
|
|
3533
|
+
}
|
|
3534
|
+
continue;
|
|
3535
|
+
}
|
|
3331
3536
|
if (lower === '/cost' || lower === '/usage') {
|
|
3332
3537
|
let stats = mod.commands.summarizeTranscript([]);
|
|
3333
3538
|
if (transcript.file) {
|
|
@@ -3413,7 +3618,11 @@ async function main() {
|
|
|
3413
3618
|
hasOrigin: gitOk
|
|
3414
3619
|
? (await runGitArgs(['remote'], currentWorkdir, 5000)).includes('origin')
|
|
3415
3620
|
: undefined,
|
|
3416
|
-
|
|
3621
|
+
// The ACTIVE browser's own interval: a per-provider timing
|
|
3622
|
+
// (Qwen 4s, GLM 0) used to be reported as DeepSeek's 15s.
|
|
3623
|
+
minSendIntervalMs: browser.sendIntervalMs(),
|
|
3624
|
+
adaptiveThrottle: providerAdaptiveThrottle(config.provider),
|
|
3625
|
+
displayAvailable: canShowWindow(process.env, process.platform),
|
|
3417
3626
|
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
3418
3627
|
headless: !!headless,
|
|
3419
3628
|
headlessUaCached: await headlessUACacheReady().catch(() => false),
|
|
@@ -3421,7 +3630,9 @@ async function main() {
|
|
|
3421
3630
|
uiMissing: uiHealth?.missing,
|
|
3422
3631
|
uiMissingCritical: uiHealth?.missingCritical,
|
|
3423
3632
|
provider: config.provider,
|
|
3424
|
-
providerModel: config.provider
|
|
3633
|
+
providerModel: providerConfigSection(config.provider)
|
|
3634
|
+
? providerModelOf(config.provider)
|
|
3635
|
+
: undefined,
|
|
3425
3636
|
}, t)));
|
|
3426
3637
|
continue;
|
|
3427
3638
|
}
|
|
@@ -3828,7 +4039,7 @@ async function main() {
|
|
|
3828
4039
|
if (handoff) {
|
|
3829
4040
|
finalTaskText =
|
|
3830
4041
|
buildHandoffText({
|
|
3831
|
-
from: handoff
|
|
4042
|
+
from: handoffFromLabel(handoff),
|
|
3832
4043
|
to: providerLabel(config.provider),
|
|
3833
4044
|
summary: handoff.summary,
|
|
3834
4045
|
task: handoff.task,
|