zames_pro 2.80.8 → 2.81.1
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 +50 -0
- package/README.md +71 -21
- package/dist/browser-login.js +179 -0
- package/dist/browser-pure.js +112 -8
- package/dist/browser.js +274 -16
- package/dist/commands.js +24 -8
- package/dist/config-command.js +5 -1
- package/dist/config.js +110 -2
- package/dist/glm-browser.js +1770 -0
- package/dist/glm-chats.js +170 -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 +332 -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,7 @@ ${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) ? '\n ' + t('help.cmd.model') : ''}
|
|
489
529
|
|
|
490
530
|
${theme.bold(t('help.sec.files'))}
|
|
491
531
|
${t('help.cmd.transcript')}
|
|
@@ -495,6 +535,7 @@ ${theme.bold(t('help.sec.files'))}
|
|
|
495
535
|
${t('help.cmd.rewind_list')}
|
|
496
536
|
${t('help.cmd.export')}
|
|
497
537
|
${t('help.cmd.cost')}
|
|
538
|
+
${t('help.cmd.limits')}
|
|
498
539
|
${t('help.cmd.copy')}
|
|
499
540
|
${t('help.cmd.doctor')}
|
|
500
541
|
${t('help.cmd.add_dir')}
|
|
@@ -660,90 +701,177 @@ async function resolveWorkdir() {
|
|
|
660
701
|
* config.browser.answerTimeoutMs there cut them off; pick the active
|
|
661
702
|
* provider's own value.
|
|
662
703
|
*/
|
|
704
|
+
/** True when the ACTIVE provider derives its pause adaptively (see part C). */
|
|
705
|
+
function providerAdaptiveThrottle(provider) {
|
|
706
|
+
const section = providerConfigSection(provider);
|
|
707
|
+
if (!section)
|
|
708
|
+
return false;
|
|
709
|
+
const sec = config[section];
|
|
710
|
+
return !!sec && sec.adaptiveThrottle === true;
|
|
711
|
+
}
|
|
712
|
+
/** The configured model id of a provider ('n/a' when it has no model picker). */
|
|
713
|
+
function providerModelOf(provider) {
|
|
714
|
+
const section = providerConfigSection(provider);
|
|
715
|
+
if (!section)
|
|
716
|
+
return 'n/a';
|
|
717
|
+
const sec = config[section];
|
|
718
|
+
const m = sec ? sec.model : '';
|
|
719
|
+
return typeof m === 'string' ? m : '';
|
|
720
|
+
}
|
|
663
721
|
function providerAnswerTimeoutMs() {
|
|
664
|
-
return config.
|
|
665
|
-
|
|
666
|
-
|
|
722
|
+
return providerSectionNumber('answerTimeoutMs', config.browser.answerTimeoutMs);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Read a number from the ACTIVE provider's own config section (`qwen.*`,
|
|
726
|
+
* `glm.*`), or the shared fallback for DeepSeek (whose section is `browser`).
|
|
727
|
+
* This replaced `provider === 'qwen' ? config.qwen.x : shared`, which silently
|
|
728
|
+
* gave every NEW provider the DeepSeek timing.
|
|
729
|
+
*/
|
|
730
|
+
function providerSectionNumber(field, fallback) {
|
|
731
|
+
const section = providerConfigSection(config.provider);
|
|
732
|
+
if (!section)
|
|
733
|
+
return fallback;
|
|
734
|
+
const s = config[section];
|
|
735
|
+
const v = s ? s[field] : undefined;
|
|
736
|
+
return typeof v === 'number' ? v : fallback;
|
|
737
|
+
}
|
|
738
|
+
/** The per-provider config section object, or null for DeepSeek. */
|
|
739
|
+
function activeProviderSection() {
|
|
740
|
+
const section = providerConfigSection(config.provider);
|
|
741
|
+
if (!section)
|
|
742
|
+
return null;
|
|
743
|
+
return (config[section] ||
|
|
744
|
+
null);
|
|
667
745
|
}
|
|
668
746
|
/** The provider's base chat URL, used by /chat to build the right link. */
|
|
669
747
|
function browserProviderUrl() {
|
|
670
748
|
return providerChatUrlPrefix(config.provider);
|
|
671
749
|
}
|
|
672
750
|
/**
|
|
673
|
-
* The context window (tokens) for the ACTIVE provider. The
|
|
674
|
-
*
|
|
675
|
-
* number would either overstate
|
|
676
|
-
*
|
|
751
|
+
* The context window (tokens) for the ACTIVE provider. The backends advertise
|
|
752
|
+
* different windows and a provider's window may be PER-MODEL (Qwen), so a
|
|
753
|
+
* single global number would either overstate a small model or understate a
|
|
754
|
+
* 1M build — and the number is the denominator of the auto-compact threshold,
|
|
677
755
|
* so an error directly delays (or prematurely fires) compaction.
|
|
678
756
|
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
757
|
+
* The provider's own `contextLimit` wins; DeepSeek (which has no section)
|
|
758
|
+
* falls back to `ui.contextLimit`.
|
|
681
759
|
*/
|
|
682
760
|
function effectiveContextLimit() {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
: config.ui.contextLimit;
|
|
687
|
-
}
|
|
761
|
+
const own = providerSectionNumber('contextLimit', 0);
|
|
762
|
+
if (own > 0)
|
|
763
|
+
return own;
|
|
688
764
|
return config.ui.contextLimit;
|
|
689
765
|
}
|
|
690
766
|
/**
|
|
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
|
|
767
|
+
* Re-derive the ACTIVE provider's `contextLimit` from a picked model id and
|
|
768
|
+
* persist it. Called by `/model` on an explicit pick (never on startup), so a
|
|
769
|
+
* manual `/config set <provider>.contextLimit` survives until the operator
|
|
770
|
+
* switches model again. An UNKNOWN model id leaves the limit alone: guessing
|
|
771
|
+
* would either fire auto-compact too early or too late. The live editor is
|
|
772
|
+
* repainted by the caller (it owns the `editor` reference); this only touches
|
|
773
|
+
* config.
|
|
697
774
|
*/
|
|
698
775
|
function applyModelContextLimit(model) {
|
|
699
|
-
const
|
|
776
|
+
const section = providerConfigSection(config.provider);
|
|
777
|
+
if (!section)
|
|
778
|
+
return;
|
|
779
|
+
const window = config.provider === 'qwen'
|
|
780
|
+
? qwenContextWindow(model)
|
|
781
|
+
: 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
|
+
// Dev runs (`npm run dev`, i.e. `--dev`) record the telemetry
|
|
855
|
+
// automatically: it is a diagnostic for the developer. For a regular
|
|
856
|
+
// user it stays OFF by default (see DEFAULTS in config.ts) and is
|
|
857
|
+
// opted into with `/config set browser.sendTelemetry true`.
|
|
858
|
+
sendTelemetry: config.browser.sendTelemetry || devMode,
|
|
859
|
+
logsDir: config.transcript.dir,
|
|
860
|
+
loginTimeoutMs: config.browser.loginTimeoutMs,
|
|
861
|
+
locale: currentLocale,
|
|
862
|
+
onAuthSave: persistAuth,
|
|
863
|
+
});
|
|
864
|
+
case 'deepseek':
|
|
865
|
+
default:
|
|
866
|
+
return new DeepSeekBrowser({
|
|
867
|
+
headless: headlessFor('deepseek'),
|
|
868
|
+
debug,
|
|
869
|
+
channel: config.browserChannel,
|
|
870
|
+
...config.browser,
|
|
871
|
+
locale: currentLocale,
|
|
872
|
+
onAuthSave: persistAuth,
|
|
873
|
+
});
|
|
874
|
+
}
|
|
747
875
|
}
|
|
748
876
|
/**
|
|
749
877
|
* Print the dialogue of the just-opened chat into the terminal. Called by
|
|
@@ -957,10 +1085,16 @@ async function main() {
|
|
|
957
1085
|
// never block startup.
|
|
958
1086
|
const warnUiHealth = (h) => {
|
|
959
1087
|
if (h.missingCritical.length) {
|
|
960
|
-
console.error(theme.warn(t('ui.probe_failed', {
|
|
1088
|
+
console.error(theme.warn(t('ui.probe_failed', {
|
|
1089
|
+
v: h.missingCritical.join(', '),
|
|
1090
|
+
provider: providerLabel(config.provider),
|
|
1091
|
+
})));
|
|
961
1092
|
}
|
|
962
1093
|
else if (h.missing.length) {
|
|
963
|
-
console.error(theme.warn(t('ui.probe_degraded', {
|
|
1094
|
+
console.error(theme.warn(t('ui.probe_degraded', {
|
|
1095
|
+
v: h.missing.join(', '),
|
|
1096
|
+
provider: providerLabel(config.provider),
|
|
1097
|
+
})));
|
|
964
1098
|
}
|
|
965
1099
|
};
|
|
966
1100
|
try {
|
|
@@ -1021,14 +1155,21 @@ async function main() {
|
|
|
1021
1155
|
await transcript.close();
|
|
1022
1156
|
return;
|
|
1023
1157
|
}
|
|
1158
|
+
if (providerNeedsVisibleBrowser(config.provider) &&
|
|
1159
|
+
headlessFor(config.provider)) {
|
|
1160
|
+
console.log(theme.warn(t('provider.needs_headed', { v: providerLabel(config.provider) })));
|
|
1161
|
+
}
|
|
1024
1162
|
console.log(theme.system(t('msg.interactive')));
|
|
1025
1163
|
console.log(theme.system(t('msg.queue_hint')));
|
|
1026
1164
|
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
|
|
1165
|
+
// No stored session yet (auth.json absent for THIS provider): the first
|
|
1166
|
+
// task will need a sign-in. The auto-login handles it, but a heads-up in
|
|
1167
|
+
// the banner saves the operator from wondering why the first task pauses.
|
|
1168
|
+
// The hint text describes the browser-based flow (see src/browser-login.ts);
|
|
1169
|
+
// the old wording promised a terminal prompt, which is no longer the main
|
|
1170
|
+
// path.
|
|
1030
1171
|
if (!authMarkerExists()) {
|
|
1031
|
-
console.log(theme.warn(t('msg.first_login_hint')));
|
|
1172
|
+
console.log(theme.warn(t('msg.first_login_hint', { provider: providerLabel(config.provider) })));
|
|
1032
1173
|
}
|
|
1033
1174
|
// Nudge when BACKLOG.md has grown large: /improve reads the whole file, so a
|
|
1034
1175
|
// bloated backlog costs tokens on every self-improvement run. Best-effort.
|
|
@@ -1390,8 +1531,10 @@ async function main() {
|
|
|
1390
1531
|
const { runLoginForm } = await import('./tui/run-login.js');
|
|
1391
1532
|
return await runLoginForm({
|
|
1392
1533
|
t,
|
|
1393
|
-
|
|
1394
|
-
|
|
1534
|
+
// The SAVED credentials of the ACTIVE provider (DeepSeek keeps
|
|
1535
|
+
// browser.auth; Qwen/GLM own their section).
|
|
1536
|
+
initialUsername: activeAuth().username,
|
|
1537
|
+
initialPassword: activeAuth().password,
|
|
1395
1538
|
note: info.hasSavedPassword || info.savedUsername
|
|
1396
1539
|
? t('auth.form.note_saved')
|
|
1397
1540
|
: undefined,
|
|
@@ -1737,10 +1880,11 @@ async function main() {
|
|
|
1737
1880
|
}
|
|
1738
1881
|
// The context window size feeds the status-bar percentage and its color.
|
|
1739
1882
|
// Apply it to the live editor immediately (the value is a plain number,
|
|
1740
|
-
// no restart needed).
|
|
1741
|
-
//
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1883
|
+
// no restart needed). ANY provider's limit matters: a /model pick rewrites
|
|
1884
|
+
// the active provider's own value, and a manual /config set on a non-active
|
|
1885
|
+
// provider must not leave the editor showing the old denominator when the
|
|
1886
|
+
// operator switches to it.
|
|
1887
|
+
if ((path === 'ui.contextLimit' || /\.contextLimit$/.test(path)) &&
|
|
1744
1888
|
typeof value === 'number') {
|
|
1745
1889
|
if (editor) {
|
|
1746
1890
|
editor.setContextLimit(effectiveContextLimit());
|
|
@@ -1751,9 +1895,16 @@ async function main() {
|
|
|
1751
1895
|
// by the browser at construction, so push them onto the live instance.
|
|
1752
1896
|
// (deepThinking/webSearch are handled above because they also repaint the
|
|
1753
1897
|
// status icons.)
|
|
1898
|
+
//
|
|
1899
|
+
// `browser.*` knobs belong to DEEPSEEK. Each other backend copies its
|
|
1900
|
+
// timings from its OWN section, so applying a `browser.*` value to the
|
|
1901
|
+
// live Qwen/GLM instance would clobber its interval with DeepSeek's
|
|
1902
|
+
// 15s (the real bug: /config set browser.minSendIntervalMs rewrote the
|
|
1903
|
+
// Qwen send interval).
|
|
1754
1904
|
if (path.startsWith('browser.')) {
|
|
1755
1905
|
const b = browser;
|
|
1756
|
-
if (
|
|
1906
|
+
if (config.provider === 'deepseek' &&
|
|
1907
|
+
applyBrowserLiveConfig(path, value, b)) {
|
|
1757
1908
|
// minSendIntervalMs feeds the send countdown the UI renders.
|
|
1758
1909
|
if (editor)
|
|
1759
1910
|
editor.refreshStatus();
|
|
@@ -1764,15 +1915,15 @@ async function main() {
|
|
|
1764
1915
|
undo: undo,
|
|
1765
1916
|
checkpoints: checkpoints,
|
|
1766
1917
|
});
|
|
1767
|
-
//
|
|
1768
|
-
// the SAME names, so push them there when
|
|
1769
|
-
// no-op
|
|
1770
|
-
|
|
1918
|
+
// A provider's OWN timing knobs feed its browser constructor. The LIVE
|
|
1919
|
+
// fields use the SAME names, so push them there when that backend is
|
|
1920
|
+
// active (a no-op otherwise). Generalized from the two hardcoded qwen.*
|
|
1921
|
+
// paths so a new backend needs no new branch here.
|
|
1922
|
+
const timingMatch = path.match(/^(qwen|glm)\.(minSendIntervalMs|answerTimeoutMs)$/);
|
|
1923
|
+
if (timingMatch) {
|
|
1771
1924
|
const b = browser;
|
|
1772
|
-
if (config.provider ===
|
|
1773
|
-
const field =
|
|
1774
|
-
? 'minSendIntervalMs'
|
|
1775
|
-
: 'answerTimeoutMs';
|
|
1925
|
+
if (config.provider === timingMatch[1]) {
|
|
1926
|
+
const field = timingMatch[2];
|
|
1776
1927
|
if (field in b) {
|
|
1777
1928
|
b[field] = value;
|
|
1778
1929
|
if (editor)
|
|
@@ -1810,6 +1961,20 @@ async function main() {
|
|
|
1810
1961
|
// The DeepSeek toggles themselves are clicked on the next send by
|
|
1811
1962
|
// _applyToggles(), so this is safe even while a generation is in flight.
|
|
1812
1963
|
function applyLiveToggle(cmd) {
|
|
1964
|
+
// GLM's flagship models (x-preview-l, glm-5.3, glm-4.6v) report
|
|
1965
|
+
// skip_think=false in the catalogue: thinking CANNOT be turned off, and
|
|
1966
|
+
// the site always sends enable_thinking=true. Flipping the switch would
|
|
1967
|
+
// only mislead the operator (and the icon would lie), so it is refused.
|
|
1968
|
+
if (cmd.kind === 'thinking' &&
|
|
1969
|
+
config.provider === 'glm' &&
|
|
1970
|
+
cmd.mode !== 'on') {
|
|
1971
|
+
const line = t('toggle.thinking_locked');
|
|
1972
|
+
if (editor)
|
|
1973
|
+
editor.printAbove(theme.warn(line));
|
|
1974
|
+
else
|
|
1975
|
+
console.log(theme.warn(line));
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1813
1978
|
const b = browser;
|
|
1814
1979
|
const cur = cmd.kind === 'thinking' ? b.deepThinking : b.webSearch;
|
|
1815
1980
|
const next = cmd.mode === 'toggle' ? !cur : cmd.mode === 'on';
|
|
@@ -2404,7 +2569,7 @@ async function main() {
|
|
|
2404
2569
|
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
2405
2570
|
const arg = trimmed.slice('/provider'.length).trim().toLowerCase();
|
|
2406
2571
|
if (!arg || arg === 'status') {
|
|
2407
|
-
const model = config.provider
|
|
2572
|
+
const model = providerModelOf(config.provider) || '(default)';
|
|
2408
2573
|
console.log(theme.system(t('provider.status', {
|
|
2409
2574
|
v: config.provider,
|
|
2410
2575
|
m: model,
|
|
@@ -2483,6 +2648,7 @@ async function main() {
|
|
|
2483
2648
|
if (summary) {
|
|
2484
2649
|
saveProviderHandoff({
|
|
2485
2650
|
provider: arg,
|
|
2651
|
+
from,
|
|
2486
2652
|
workdir: currentWorkdir,
|
|
2487
2653
|
summary,
|
|
2488
2654
|
task: lastTaskText || undefined,
|
|
@@ -2504,7 +2670,7 @@ async function main() {
|
|
|
2504
2670
|
saveLastChat(currentChatId, currentWorkdir);
|
|
2505
2671
|
// Persist the choice FIRST so a crash mid-switch still remembers it.
|
|
2506
2672
|
config.provider = arg;
|
|
2507
|
-
|
|
2673
|
+
setProviderEnabledFlag(arg);
|
|
2508
2674
|
try {
|
|
2509
2675
|
writeConfigValue('home', 'provider', arg);
|
|
2510
2676
|
}
|
|
@@ -2535,6 +2701,9 @@ async function main() {
|
|
|
2535
2701
|
currentChatId = await browser.getCurrentChatId().catch(() => null);
|
|
2536
2702
|
if (editor) {
|
|
2537
2703
|
editor.setProvider(providerLabel(arg));
|
|
2704
|
+
// The context window is provider-specific: without this the status
|
|
2705
|
+
// bar kept showing the OLD backend's denominator after a switch.
|
|
2706
|
+
editor.setContextLimit(effectiveContextLimit());
|
|
2538
2707
|
}
|
|
2539
2708
|
try {
|
|
2540
2709
|
warnUiHealth(await browser.probeUiHealth());
|
|
@@ -2546,7 +2715,7 @@ async function main() {
|
|
|
2546
2715
|
// The new backend did not come up: keep the OLD one active so the
|
|
2547
2716
|
// session keeps working, and roll the persisted choice back.
|
|
2548
2717
|
config.provider = from;
|
|
2549
|
-
|
|
2718
|
+
setProviderEnabledFlag(from);
|
|
2550
2719
|
try {
|
|
2551
2720
|
writeConfigValue('home', 'provider', from);
|
|
2552
2721
|
}
|
|
@@ -2562,23 +2731,30 @@ async function main() {
|
|
|
2562
2731
|
}
|
|
2563
2732
|
continue;
|
|
2564
2733
|
}
|
|
2565
|
-
console.error(theme.warn(t('provider.usage')));
|
|
2734
|
+
console.error(theme.warn(t('provider.usage', { providers: PROVIDER_IDS.join('|') })));
|
|
2566
2735
|
continue;
|
|
2567
2736
|
}
|
|
2568
2737
|
if (lower === '/model' || lower.startsWith('/model ')) {
|
|
2569
2738
|
const arg = trimmed.slice('/model'.length).trim();
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
console.log(theme.system(t('
|
|
2739
|
+
// `/model` is available to ANY provider that exposes a model picker
|
|
2740
|
+
// (Qwen, GLM). DeepSeek has none, so the command is hidden from the
|
|
2741
|
+
// hints there and only explains itself when typed by hand.
|
|
2742
|
+
if (!providerHasModelPicker(config.provider)) {
|
|
2743
|
+
console.log(theme.system(t('model.unsupported', {
|
|
2744
|
+
provider: providerLabel(config.provider),
|
|
2745
|
+
})));
|
|
2575
2746
|
continue;
|
|
2576
2747
|
}
|
|
2748
|
+
const section = providerConfigSection(config.provider);
|
|
2749
|
+
const sectionObj = activeProviderSection();
|
|
2750
|
+
if (!section || !sectionObj)
|
|
2751
|
+
continue;
|
|
2752
|
+
const curModel = String(sectionObj.model || '');
|
|
2577
2753
|
const qb = browser;
|
|
2578
2754
|
if (!arg) {
|
|
2579
2755
|
const models = qb.listModels ? await qb.listModels() : [];
|
|
2580
2756
|
if (!models.length) {
|
|
2581
|
-
console.log(theme.system(t('
|
|
2757
|
+
console.log(theme.system(t('model.list', { v: '(none)' })));
|
|
2582
2758
|
continue;
|
|
2583
2759
|
}
|
|
2584
2760
|
// In the TUI, open a proper SELECT LIST instead of dumping text.
|
|
@@ -2589,12 +2765,14 @@ async function main() {
|
|
|
2589
2765
|
try {
|
|
2590
2766
|
const { selectListInk } = await import('./tui/run-select.js');
|
|
2591
2767
|
picked = await selectListInk({
|
|
2592
|
-
title: t('
|
|
2593
|
-
|
|
2768
|
+
title: t('model.pick_title', {
|
|
2769
|
+
provider: providerLabel(config.provider),
|
|
2770
|
+
}),
|
|
2771
|
+
initial: curModel || models[0].id,
|
|
2594
2772
|
items: models.map((m) => ({
|
|
2595
2773
|
value: m.id,
|
|
2596
2774
|
label: m.name || m.id,
|
|
2597
|
-
detail: m.id ===
|
|
2775
|
+
detail: m.id === curModel ? t('tui.list.current') : m.id,
|
|
2598
2776
|
})),
|
|
2599
2777
|
locale: currentLocale,
|
|
2600
2778
|
});
|
|
@@ -2607,32 +2785,35 @@ async function main() {
|
|
|
2607
2785
|
editor.resume();
|
|
2608
2786
|
}
|
|
2609
2787
|
if (picked) {
|
|
2610
|
-
|
|
2788
|
+
sectionObj.model = picked;
|
|
2611
2789
|
if (typeof qb.model === 'string')
|
|
2612
2790
|
qb.model = picked;
|
|
2613
|
-
// Derive the context window from the picked model:
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2616
|
-
// `/config set
|
|
2617
|
-
// only runs on an explicit pick).
|
|
2791
|
+
// Derive the context window from the picked model: windows are
|
|
2792
|
+
// per-model, and this number is the denominator of the
|
|
2793
|
+
// auto-compact threshold. The operator's manual
|
|
2794
|
+
// `/config set <provider>.contextLimit` still wins afterwards
|
|
2795
|
+
// (this only runs on an explicit pick).
|
|
2618
2796
|
applyModelContextLimit(picked);
|
|
2619
2797
|
if (editor) {
|
|
2620
2798
|
editor.setContextLimit(effectiveContextLimit());
|
|
2621
2799
|
editor.refreshStatus();
|
|
2622
2800
|
}
|
|
2623
2801
|
try {
|
|
2624
|
-
writeConfigValue('home', '
|
|
2802
|
+
writeConfigValue('home', section + '.model', picked);
|
|
2625
2803
|
}
|
|
2626
2804
|
catch { }
|
|
2627
|
-
console.log(theme.system(t('
|
|
2805
|
+
console.log(theme.system(t('model.set', {
|
|
2806
|
+
provider: providerLabel(config.provider),
|
|
2807
|
+
v: picked,
|
|
2808
|
+
})));
|
|
2628
2809
|
}
|
|
2629
2810
|
continue;
|
|
2630
2811
|
}
|
|
2631
2812
|
const lines = models.map((m) => ` ${m.id} — ${m.name}`).join(NL);
|
|
2632
|
-
console.log(theme.system(t('
|
|
2813
|
+
console.log(theme.system(t('model.list', { v: lines })));
|
|
2633
2814
|
continue;
|
|
2634
2815
|
}
|
|
2635
|
-
|
|
2816
|
+
sectionObj.model = arg;
|
|
2636
2817
|
if (typeof qb.model === 'string')
|
|
2637
2818
|
qb.model = arg;
|
|
2638
2819
|
applyModelContextLimit(arg);
|
|
@@ -2641,10 +2822,10 @@ async function main() {
|
|
|
2641
2822
|
editor.refreshStatus();
|
|
2642
2823
|
}
|
|
2643
2824
|
try {
|
|
2644
|
-
writeConfigValue('home', '
|
|
2825
|
+
writeConfigValue('home', section + '.model', arg);
|
|
2645
2826
|
}
|
|
2646
2827
|
catch { }
|
|
2647
|
-
console.log(theme.system(t('
|
|
2828
|
+
console.log(theme.system(t('model.set', { provider: providerLabel(config.provider), v: arg })));
|
|
2648
2829
|
continue;
|
|
2649
2830
|
}
|
|
2650
2831
|
if (lower === '/chat') {
|
|
@@ -3328,6 +3509,37 @@ async function main() {
|
|
|
3328
3509
|
console.log(theme.system(mod.commands.formatDiffStat(out, { maxLines: 60 }, t)));
|
|
3329
3510
|
continue;
|
|
3330
3511
|
}
|
|
3512
|
+
if (lower === '/limits') {
|
|
3513
|
+
const { readSendEvents, summarizeSends, sendLogPath } = await import('./send-telemetry.js');
|
|
3514
|
+
const logsDir = config.transcript.dir;
|
|
3515
|
+
const events = readSendEvents(logsDir, config.provider);
|
|
3516
|
+
if (!events.length) {
|
|
3517
|
+
console.log(theme.system(t('limits.none', { path: sendLogPath(logsDir, config.provider) })));
|
|
3518
|
+
}
|
|
3519
|
+
else {
|
|
3520
|
+
const st = summarizeSends(events);
|
|
3521
|
+
console.log(theme.system(t('limits.title', { provider: providerLabel(config.provider) })));
|
|
3522
|
+
for (const [label, n] of [
|
|
3523
|
+
['10m', st.last10m],
|
|
3524
|
+
['30m', st.last30m],
|
|
3525
|
+
['60m', st.last60m],
|
|
3526
|
+
]) {
|
|
3527
|
+
console.log(' ' + t('limits.window', { label, n: String(n) }));
|
|
3528
|
+
}
|
|
3529
|
+
console.log(' ' +
|
|
3530
|
+
t('limits.rate', {
|
|
3531
|
+
n: String(st.rateLimits),
|
|
3532
|
+
sec: String(Math.round(st.avgBlockMs / 1000)),
|
|
3533
|
+
}));
|
|
3534
|
+
const adaptive = providerAdaptiveThrottle(config.provider);
|
|
3535
|
+
console.log(' ' +
|
|
3536
|
+
t('limits.interval', {
|
|
3537
|
+
sec: String(Math.round(browser.sendIntervalMs() / 1000)),
|
|
3538
|
+
adaptive: adaptive ? t('limits.adaptive_on') : '',
|
|
3539
|
+
}));
|
|
3540
|
+
}
|
|
3541
|
+
continue;
|
|
3542
|
+
}
|
|
3331
3543
|
if (lower === '/cost' || lower === '/usage') {
|
|
3332
3544
|
let stats = mod.commands.summarizeTranscript([]);
|
|
3333
3545
|
if (transcript.file) {
|
|
@@ -3413,7 +3625,11 @@ async function main() {
|
|
|
3413
3625
|
hasOrigin: gitOk
|
|
3414
3626
|
? (await runGitArgs(['remote'], currentWorkdir, 5000)).includes('origin')
|
|
3415
3627
|
: undefined,
|
|
3416
|
-
|
|
3628
|
+
// The ACTIVE browser's own interval: a per-provider timing
|
|
3629
|
+
// (Qwen 4s, GLM 0) used to be reported as DeepSeek's 15s.
|
|
3630
|
+
minSendIntervalMs: browser.sendIntervalMs(),
|
|
3631
|
+
adaptiveThrottle: providerAdaptiveThrottle(config.provider),
|
|
3632
|
+
displayAvailable: canShowWindow(process.env, process.platform),
|
|
3417
3633
|
sshRemote: !!(process.env.SSH_CONNECTION || process.env.SSH_TTY),
|
|
3418
3634
|
headless: !!headless,
|
|
3419
3635
|
headlessUaCached: await headlessUACacheReady().catch(() => false),
|
|
@@ -3421,7 +3637,9 @@ async function main() {
|
|
|
3421
3637
|
uiMissing: uiHealth?.missing,
|
|
3422
3638
|
uiMissingCritical: uiHealth?.missingCritical,
|
|
3423
3639
|
provider: config.provider,
|
|
3424
|
-
providerModel: config.provider
|
|
3640
|
+
providerModel: providerConfigSection(config.provider)
|
|
3641
|
+
? providerModelOf(config.provider)
|
|
3642
|
+
: undefined,
|
|
3425
3643
|
}, t)));
|
|
3426
3644
|
continue;
|
|
3427
3645
|
}
|
|
@@ -3828,7 +4046,7 @@ async function main() {
|
|
|
3828
4046
|
if (handoff) {
|
|
3829
4047
|
finalTaskText =
|
|
3830
4048
|
buildHandoffText({
|
|
3831
|
-
from: handoff
|
|
4049
|
+
from: handoffFromLabel(handoff),
|
|
3832
4050
|
to: providerLabel(config.provider),
|
|
3833
4051
|
summary: handoff.summary,
|
|
3834
4052
|
task: handoff.task,
|