zames_pro 2.75.3 → 2.75.5
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 +16 -0
- package/README.md +4 -4
- package/dist/config.js +15 -0
- package/dist/i18n/config.js +6 -2
- package/dist/i18n/help.js +6 -6
- package/dist/i18n/spinner.js +9 -4
- package/dist/index.js +69 -11
- package/dist/qwen-ui.js +41 -0
- package/dist/tui/StatusBar.js +5 -1
- package/package.json +2 -1
- package/scripts/sched-smoke.mts +150 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.75.5] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **i18n:** clearer TUI key hints; localize the scrolled status; add sched-smoke
|
|
15
|
+
|
|
16
|
+
## [2.75.4] - 2026-10-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **qwen:** per-model context window for the status bar and auto-compact
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **test:** format qwen-context test with prettier
|
|
25
|
+
|
|
10
26
|
## [2.75.3] - 2026-10-15
|
|
11
27
|
|
|
12
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -259,8 +259,8 @@ Full-screen mode adds dialog navigation, so a long answer can be read and
|
|
|
259
259
|
selected without leaving the terminal:
|
|
260
260
|
|
|
261
261
|
- mouse wheel — scroll the dialog (never the input history; history is arrow
|
|
262
|
-
keys only). Selecting text needs `Shift`+
|
|
263
|
-
- `PgUp` / `PgDn` —
|
|
262
|
+
keys only). Selecting text needs `Shift`+click, the standard TUI convention.
|
|
263
|
+
- `PgUp` / `PgDn` — move up/down the dialog half a screen.
|
|
264
264
|
- `Home` / `End` — jump to the very top / bottom of the dialog (only while the
|
|
265
265
|
input line is empty; with text typed they move the caret as usual).
|
|
266
266
|
- To copy a LARGE block, use `/export` (writes the whole session as Markdown)
|
|
@@ -839,8 +839,8 @@ zames
|
|
|
839
839
|
можно было прочитать и выделить не покидая терминал:
|
|
840
840
|
|
|
841
841
|
- колесо мыши — скролл диалога (историю ввода колесо НЕ листает; история —
|
|
842
|
-
только стрелки). Выделение текста — `Shift
|
|
843
|
-
- `PgUp` / `PgDn` —
|
|
842
|
+
только стрелки). Выделение текста — `Shift`+клик, стандарт TUI.
|
|
843
|
+
- `PgUp` / `PgDn` — вверх/вниз по диалогу на пол-экрана.
|
|
844
844
|
- `Home` / `End` — прыжок в самый верх / низ диалога (только когда строка ввода
|
|
845
845
|
пуста; при набранном тексте они двигают курсор, как обычно).
|
|
846
846
|
- Чтобы скопировать БОЛЬШОЙ кусок, используйте `/export` (пишет всю сессию в
|
package/dist/config.js
CHANGED
|
@@ -21,6 +21,13 @@ export const DEFAULTS = {
|
|
|
21
21
|
qwen: {
|
|
22
22
|
model: '',
|
|
23
23
|
enabled: false,
|
|
24
|
+
// Qwen's context window is PER-MODEL, not a site constant: Qwen3-Max is
|
|
25
|
+
// 258K while the 3.7/3.8-Max and Coder builds are 1M. The default assumes
|
|
26
|
+
// the common Qwen3-Max; `/model` re-derives it from the picked id (see
|
|
27
|
+
// qwenContextWindow in src/qwen-ui.ts) and `/config set qwen.contextLimit`
|
|
28
|
+
// overrides it. Kept SEPARATE from ui.contextLimit (DeepSeek's 1M) so one
|
|
29
|
+
// backend's window never becomes the other's denominator.
|
|
30
|
+
contextLimit: 258_048,
|
|
24
31
|
// Qwen DOES rate-limit a rapid burst: measured live, the 5th back-to-back
|
|
25
32
|
// send at 0 ms gap hit "too frequent" and needed a 5-min back-off. A 1 s
|
|
26
33
|
// gap was too small for a real editing session, so the default is now 4 s
|
|
@@ -237,6 +244,14 @@ export const CONFIG_SCHEMA = [
|
|
|
237
244
|
labelKey: 'cfg.f.qwen_model',
|
|
238
245
|
groupKey: 'cfg.group.browser',
|
|
239
246
|
},
|
|
247
|
+
{
|
|
248
|
+
path: 'qwen.contextLimit',
|
|
249
|
+
type: 'number',
|
|
250
|
+
min: 10_000,
|
|
251
|
+
max: 10_000_000,
|
|
252
|
+
labelKey: 'cfg.f.qwen_contextLimit',
|
|
253
|
+
groupKey: 'cfg.group.browser',
|
|
254
|
+
},
|
|
240
255
|
{
|
|
241
256
|
path: 'qwen.minSendIntervalMs',
|
|
242
257
|
type: 'number',
|
package/dist/i18n/config.js
CHANGED
|
@@ -11,6 +11,10 @@ export const configMessages = {
|
|
|
11
11
|
ru: 'Модель Qwen (id)',
|
|
12
12
|
en: 'Qwen model (id)',
|
|
13
13
|
},
|
|
14
|
+
'cfg.f.qwen_contextLimit': {
|
|
15
|
+
ru: 'Qwen: размер контекста, токены (258K у Qwen3-Max, 1M у 3.7/3.8/Coder)',
|
|
16
|
+
en: 'Qwen: context window, tokens (258K on Qwen3-Max, 1M on 3.7/3.8/Coder)',
|
|
17
|
+
},
|
|
14
18
|
'cfg.f.qwen_minSendIntervalMs': {
|
|
15
19
|
ru: 'Qwen: пауза между отправками (мс)',
|
|
16
20
|
en: 'Qwen: pause between sends (ms)',
|
|
@@ -40,8 +44,8 @@ export const configMessages = {
|
|
|
40
44
|
en: 'Full-screen TUI mode (alternate screen)',
|
|
41
45
|
},
|
|
42
46
|
'cfg.f.ui_mouseScroll': {
|
|
43
|
-
ru: 'Прокрутка лога колесом мыши (выделение текста — Shift
|
|
44
|
-
en: 'Mouse-wheel log scrolling (select text with Shift+
|
|
47
|
+
ru: 'Прокрутка лога колесом мыши (выделение текста — Shift+клик)',
|
|
48
|
+
en: 'Mouse-wheel log scrolling (select text with Shift+click)',
|
|
45
49
|
},
|
|
46
50
|
'cfg.f.maxIterations': {
|
|
47
51
|
ru: 'Лимит итераций на задачу',
|
package/dist/i18n/help.js
CHANGED
|
@@ -78,8 +78,8 @@ export const helpMessages = {
|
|
|
78
78
|
en: 'Ctrl+← / Ctrl+→ move by words',
|
|
79
79
|
},
|
|
80
80
|
'help.key.slash': {
|
|
81
|
-
ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P —
|
|
82
|
-
en: '/ + Tab slash-command hints and completion (Ctrl+N/P
|
|
81
|
+
ru: '/ + Tab подсказка и автодополнение slash-команд (Ctrl+N/P — вверх/вниз)',
|
|
82
|
+
en: '/ + Tab slash-command hints and completion (Ctrl+N/P — up/down)',
|
|
83
83
|
},
|
|
84
84
|
'help.key.newline': {
|
|
85
85
|
ru: 'Ctrl+J / Ctrl+Enter новая строка (Shift+Enter в терминалах с поддержкой)',
|
|
@@ -98,12 +98,12 @@ export const helpMessages = {
|
|
|
98
98
|
en: 'Esc, Ctrl+C abort current generation',
|
|
99
99
|
},
|
|
100
100
|
'help.key.scroll': {
|
|
101
|
-
ru: 'PgUp / PgDn
|
|
102
|
-
en: 'PgUp / PgDn
|
|
101
|
+
ru: 'PgUp / PgDn вверх/вниз по диалогу (Home/End — в начало/конец) [full-screen]',
|
|
102
|
+
en: 'PgUp / PgDn up/down the dialog (Home/End — top/bottom) [full-screen]',
|
|
103
103
|
},
|
|
104
104
|
'help.key.wheel': {
|
|
105
|
-
ru: 'колесо мыши скролл диалога; выделение — Shift
|
|
106
|
-
en: 'mouse wheel scroll the dialog; select text with Shift+
|
|
105
|
+
ru: 'колесо мыши скролл диалога; выделение — Shift+клик',
|
|
106
|
+
en: 'mouse wheel scroll the dialog; select text with Shift+click',
|
|
107
107
|
},
|
|
108
108
|
'help.key.bang': {
|
|
109
109
|
ru: '!команда выполнить shell-команду напрямую (мимо модели)',
|
package/dist/i18n/spinner.js
CHANGED
|
@@ -18,10 +18,15 @@ export const spinnerMessages = {
|
|
|
18
18
|
// flight. 'paused'/'settled' have no label (see LineEditor._stateLabel).
|
|
19
19
|
'state.generating': { ru: '▶ генерация', en: '▶ generating' },
|
|
20
20
|
'state.settled': { ru: '✓ готово', en: '✓ done' },
|
|
21
|
+
// Shown in the status line while the full-screen log is scrolled up.
|
|
22
|
+
'status.scrolled': {
|
|
23
|
+
ru: '↑ прокручено на {n} стр. · PgDn/End',
|
|
24
|
+
en: '↑ scrolled {n} lines · PgDn/End',
|
|
25
|
+
},
|
|
21
26
|
'editor.more': { ru: '…ещё {n}', en: '…{n} more' },
|
|
22
27
|
'editor.page_hint': {
|
|
23
|
-
ru: '(Ctrl+N/Ctrl+P —
|
|
24
|
-
en: '(Ctrl+N/Ctrl+P
|
|
28
|
+
ru: '(Ctrl+N/Ctrl+P — вверх/вниз, Tab — вставить)',
|
|
29
|
+
en: '(Ctrl+N/Ctrl+P — up/down, Tab — insert)',
|
|
25
30
|
},
|
|
26
31
|
'editor.answer': { ru: '● Ответ', en: '● Answer' },
|
|
27
32
|
// Shown in the full-screen log viewport before the first turn, so a fresh
|
|
@@ -32,8 +37,8 @@ export const spinnerMessages = {
|
|
|
32
37
|
},
|
|
33
38
|
// One-line key hint UNDER the input box (full-screen mode).
|
|
34
39
|
'editor.keys_hint': {
|
|
35
|
-
ru: 'выделение — Shift
|
|
36
|
-
en: 'select — Shift+
|
|
40
|
+
ru: 'выделение — Shift+клик · колесо — скролл · PgUp/PgDn — вверх/вниз · Home/End — в начало/конец',
|
|
41
|
+
en: 'select — Shift+click · wheel — scroll · PgUp/PgDn — up/down · Home/End — top/bottom',
|
|
37
42
|
},
|
|
38
43
|
// Reverse search (Ctrl+R) status line. The query may be empty.
|
|
39
44
|
'editor.search_prompt': {
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { applyBrowserLiveConfig, applyStoreLiveConfig } from './config-live.js';
|
|
|
33
33
|
import { buildHandoffText, saveProviderHandoff, readProviderHandoff, clearProviderHandoff, } from './provider-handoff.js';
|
|
34
34
|
import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
|
|
35
35
|
import { createCliArgs } from './cli-args.js';
|
|
36
|
+
import { qwenContextWindow } from './qwen-ui.js';
|
|
36
37
|
import { renderMarkdown, setAnswerWidth } from './markdown.js';
|
|
37
38
|
import { extractCodeBlocks } from './code-blocks.js';
|
|
38
39
|
import { contentWidth } from './width.js';
|
|
@@ -660,6 +661,42 @@ function providerAnswerTimeoutMs() {
|
|
|
660
661
|
function browserProviderUrl() {
|
|
661
662
|
return providerChatUrlPrefix(config.provider);
|
|
662
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* The context window (tokens) for the ACTIVE provider. The two backends
|
|
666
|
+
* advertise different windows and Qwen's is PER-MODEL, so a single global
|
|
667
|
+
* number would either overstate Qwen's (258K models) or understate the 1M
|
|
668
|
+
* builds — and the number is the denominator of the auto-compact threshold,
|
|
669
|
+
* so an error directly delays (or prematurely fires) compaction.
|
|
670
|
+
*
|
|
671
|
+
* `config.qwen.contextLimit` / `ui.contextLimit` are the operator-tunable
|
|
672
|
+
* values; `/model` rewrites the Qwen one from the picked model id.
|
|
673
|
+
*/
|
|
674
|
+
function effectiveContextLimit() {
|
|
675
|
+
if (config.provider === 'qwen') {
|
|
676
|
+
return config.qwen.contextLimit > 0
|
|
677
|
+
? config.qwen.contextLimit
|
|
678
|
+
: config.ui.contextLimit;
|
|
679
|
+
}
|
|
680
|
+
return config.ui.contextLimit;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Re-derive `qwen.contextLimit` from a picked model id and persist it. Called
|
|
684
|
+
* by `/model` on an explicit pick (never on startup), so a manual
|
|
685
|
+
* `/config set qwen.contextLimit` survives until the operator switches model
|
|
686
|
+
* again. An UNKNOWN model id leaves the limit alone: guessing would either
|
|
687
|
+
* fire auto-compact too early or too late. The live editor is repainted by
|
|
688
|
+
* the caller (it owns the `editor` reference); this only touches config.
|
|
689
|
+
*/
|
|
690
|
+
function applyModelContextLimit(model) {
|
|
691
|
+
const window = qwenContextWindow(model);
|
|
692
|
+
if (window === null)
|
|
693
|
+
return;
|
|
694
|
+
config.qwen.contextLimit = window;
|
|
695
|
+
try {
|
|
696
|
+
writeConfigValue('home', 'qwen.contextLimit', String(window));
|
|
697
|
+
}
|
|
698
|
+
catch { }
|
|
699
|
+
}
|
|
663
700
|
/**
|
|
664
701
|
* Build the browser for a provider. Called at startup AND on a live
|
|
665
702
|
* `/provider` switch, so both paths construct the class the same way. The
|
|
@@ -756,8 +793,9 @@ async function printRestoredHistory(browser, ui, chatId = null) {
|
|
|
756
793
|
out(theme.dim(t('chats.history_tokens', { v: String(restoredTokens) })));
|
|
757
794
|
// Warn when the restored chat is near the context limit, and suggest
|
|
758
795
|
// /compact. The threshold mirrors the auto-compact one so the operator
|
|
759
|
-
// sees the same signal whether or not auto-compact is enabled.
|
|
760
|
-
|
|
796
|
+
// sees the same signal whether or not auto-compact is enabled. The limit
|
|
797
|
+
// is the ACTIVE provider's window (Qwen's is per-model).
|
|
798
|
+
const limit = effectiveContextLimit();
|
|
761
799
|
if (limit > 0) {
|
|
762
800
|
const pct = (restoredTokens / limit) * 100;
|
|
763
801
|
if (pct >= 80) {
|
|
@@ -1292,8 +1330,10 @@ async function main() {
|
|
|
1292
1330
|
// Task-list summary ("tasks: 2/5") before the context counter. Pulled on
|
|
1293
1331
|
// every render, like the queue badge.
|
|
1294
1332
|
ed.onTasksQuery = () => todosSummary();
|
|
1295
|
-
// Use the configured context window for the fill percentage/color.
|
|
1296
|
-
|
|
1333
|
+
// Use the configured context window for the fill percentage/color. The
|
|
1334
|
+
// window is PROVIDER-dependent: Qwen's is per-model (258K/1M, see
|
|
1335
|
+
// qwenContextWindow), DeepSeek's is the ui.contextLimit.
|
|
1336
|
+
ed.setContextLimit(effectiveContextLimit());
|
|
1297
1337
|
// Toggle icons (🧠 deep thinking, 🌐 web search) before the context
|
|
1298
1338
|
// counter. The editor pulls the live state on every render.
|
|
1299
1339
|
ed.onToggleQuery = () => browser.getToggleStatesSync();
|
|
@@ -1380,7 +1420,7 @@ async function main() {
|
|
|
1380
1420
|
chatId: currentChatId,
|
|
1381
1421
|
queueLength: pendingQueue.length,
|
|
1382
1422
|
tokens: browser.getLastTokenUsage(),
|
|
1383
|
-
contextLimit:
|
|
1423
|
+
contextLimit: effectiveContextLimit(),
|
|
1384
1424
|
tasks: todosSummary(),
|
|
1385
1425
|
locale: currentLocale,
|
|
1386
1426
|
});
|
|
@@ -1690,10 +1730,13 @@ async function main() {
|
|
|
1690
1730
|
}
|
|
1691
1731
|
// The context window size feeds the status-bar percentage and its color.
|
|
1692
1732
|
// Apply it to the live editor immediately (the value is a plain number,
|
|
1693
|
-
// no restart needed).
|
|
1694
|
-
|
|
1733
|
+
// no restart needed). A change to EITHER provider's limit matters: the
|
|
1734
|
+
// Qwen one is what a /model pick rewrites, and the active provider is
|
|
1735
|
+
// what decides which value the editor shows.
|
|
1736
|
+
if ((path === 'ui.contextLimit' || path === 'qwen.contextLimit') &&
|
|
1737
|
+
typeof value === 'number') {
|
|
1695
1738
|
if (editor) {
|
|
1696
|
-
editor.setContextLimit(
|
|
1739
|
+
editor.setContextLimit(effectiveContextLimit());
|
|
1697
1740
|
editor.refreshStatus();
|
|
1698
1741
|
}
|
|
1699
1742
|
}
|
|
@@ -2556,6 +2599,16 @@ async function main() {
|
|
|
2556
2599
|
config.qwen.model = picked;
|
|
2557
2600
|
if (typeof qb.model === 'string')
|
|
2558
2601
|
qb.model = picked;
|
|
2602
|
+
// Derive the context window from the picked model: Qwen windows
|
|
2603
|
+
// are per-model (258K vs 1M), and this number is the denominator
|
|
2604
|
+
// of the auto-compact threshold. The operator's manual
|
|
2605
|
+
// `/config set qwen.contextLimit` still wins afterwards (this
|
|
2606
|
+
// only runs on an explicit pick).
|
|
2607
|
+
applyModelContextLimit(picked);
|
|
2608
|
+
if (editor) {
|
|
2609
|
+
editor.setContextLimit(effectiveContextLimit());
|
|
2610
|
+
editor.refreshStatus();
|
|
2611
|
+
}
|
|
2559
2612
|
try {
|
|
2560
2613
|
writeConfigValue('home', 'qwen.model', picked);
|
|
2561
2614
|
}
|
|
@@ -2571,6 +2624,11 @@ async function main() {
|
|
|
2571
2624
|
config.qwen.model = arg;
|
|
2572
2625
|
if (typeof qb.model === 'string')
|
|
2573
2626
|
qb.model = arg;
|
|
2627
|
+
applyModelContextLimit(arg);
|
|
2628
|
+
if (editor) {
|
|
2629
|
+
editor.setContextLimit(effectiveContextLimit());
|
|
2630
|
+
editor.refreshStatus();
|
|
2631
|
+
}
|
|
2574
2632
|
try {
|
|
2575
2633
|
writeConfigValue('home', 'qwen.model', arg);
|
|
2576
2634
|
}
|
|
@@ -3257,7 +3315,7 @@ async function main() {
|
|
|
3257
3315
|
saveLastChat(chatId, currentWorkdir);
|
|
3258
3316
|
}
|
|
3259
3317
|
},
|
|
3260
|
-
contextLimit:
|
|
3318
|
+
contextLimit: effectiveContextLimit(),
|
|
3261
3319
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3262
3320
|
goal: sessionGoal,
|
|
3263
3321
|
});
|
|
@@ -3372,7 +3430,7 @@ async function main() {
|
|
|
3372
3430
|
mcpTools: mcpStatus.toolCount,
|
|
3373
3431
|
transcriptOk: !!transcript.file,
|
|
3374
3432
|
authSaved: authMarkerExists(),
|
|
3375
|
-
contextLimit:
|
|
3433
|
+
contextLimit: effectiveContextLimit(),
|
|
3376
3434
|
hasOrigin: gitOk
|
|
3377
3435
|
? (await runGitArgs(['remote'], currentWorkdir, 5000)).includes('origin')
|
|
3378
3436
|
: undefined,
|
|
@@ -3890,7 +3948,7 @@ async function main() {
|
|
|
3890
3948
|
}
|
|
3891
3949
|
: null,
|
|
3892
3950
|
autoCompactPct: config.browser.autoCompactPct,
|
|
3893
|
-
contextLimit:
|
|
3951
|
+
contextLimit: effectiveContextLimit(),
|
|
3894
3952
|
getTokenUsage: () => browser.getLastTokenUsage(),
|
|
3895
3953
|
goal: sessionGoal,
|
|
3896
3954
|
// Completion-goal mode (Claude Code parity): the goal is a CONDITION,
|
package/dist/qwen-ui.js
CHANGED
|
@@ -76,6 +76,47 @@ export const NEW_CHAT_SELECTOR = '.new-chat, button[aria-label="New Chat" i]';
|
|
|
76
76
|
// model itself is chosen via the config (sent with each completion request)
|
|
77
77
|
// unless a per-session override is set.
|
|
78
78
|
export const MODEL_TRIGGER_SELECTOR = '.wms-trigger';
|
|
79
|
+
// ---------- context windows ----------
|
|
80
|
+
// Qwen's per-model context window (tokens). The window is a property of the
|
|
81
|
+
// MODEL, not the site: Qwen3-Max is 258K while the 3.7/3.8-Max and Coder
|
|
82
|
+
// builds are 1M. zames uses the number ONLY as the denominator for the
|
|
83
|
+
// status-bar fill percentage and the auto-compact threshold, so a wrong value
|
|
84
|
+
// either fires compaction far too late (window overstated) or too early.
|
|
85
|
+
//
|
|
86
|
+
// Matching is done on a NORMALIZED id: lowercased, non-alphanumerics dropped,
|
|
87
|
+
// so `qwen3-max`, `Qwen3-Max` and `qwen3_max` all hit the same row. The list is
|
|
88
|
+
// ordered MOST SPECIFIC first: a `qwen3-coder-plus` id must not stop at a
|
|
89
|
+
// generic `qwen3` rule. Values verified against the public model cards
|
|
90
|
+
// (contextwindows.dev / haimaker.ai) in October 2026.
|
|
91
|
+
const CONTEXT_WINDOWS = [
|
|
92
|
+
// The 1M builds MUST be checked before the 258K `qwen3-max` family, or
|
|
93
|
+
// `qwen3.7-max` would match the shorter `max` rule first.
|
|
94
|
+
[/qwen3\.?7.?max/, 1_000_000],
|
|
95
|
+
[/qwen3\.?8.?max/, 1_000_000],
|
|
96
|
+
[/qwen3coder/, 1_000_000],
|
|
97
|
+
[/qwenlong/, 10_000_000],
|
|
98
|
+
[/qwen3max/, 258_048],
|
|
99
|
+
// Generic fallback: the OTHER qwen3 builds (Plus/Turbo/Flash) advertised 1M
|
|
100
|
+
// on their model cards, so an unknown qwen3 id gets 1M rather than the min.
|
|
101
|
+
[/qwen3/, 1_000_000],
|
|
102
|
+
];
|
|
103
|
+
/** Lowercase and drop every non-alphanumeric so `qwen3.7-max` -> `qwen37max`. */
|
|
104
|
+
export function normalizeModelId(model) {
|
|
105
|
+
return String(model || '')
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9]/g, '');
|
|
108
|
+
}
|
|
109
|
+
/** The context window (tokens) for a Qwen model id, or null when unknown. */
|
|
110
|
+
export function qwenContextWindow(model) {
|
|
111
|
+
const id = normalizeModelId(model);
|
|
112
|
+
if (!id)
|
|
113
|
+
return null;
|
|
114
|
+
for (const [re, window] of CONTEXT_WINDOWS) {
|
|
115
|
+
if (re.test(id))
|
|
116
|
+
return window;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
79
120
|
// ---------- age gate / cookies ----------
|
|
80
121
|
// A first-time visitor must confirm their birth year before chatting. The
|
|
81
122
|
// dialog is an ant-modal; the Continue button is the black footer button.
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -155,7 +155,11 @@ export function StatusBar({ state, divider = true, }) {
|
|
|
155
155
|
// When the log is scrolled up, show a marker in the status line (NOT as a
|
|
156
156
|
// separate row below the input — that pushed the panel up and made the input
|
|
157
157
|
// jump). It replaces the spinner text while scrolled, so it never adds a row.
|
|
158
|
-
const scrollNote = state.scroll > 0
|
|
158
|
+
const scrollNote = state.scroll > 0
|
|
159
|
+
? translate(state.locale)('status.scrolled', {
|
|
160
|
+
n: String(state.scroll),
|
|
161
|
+
})
|
|
162
|
+
: '';
|
|
159
163
|
const leftText = scrollNote || prefix + left;
|
|
160
164
|
const showLeft = !!leftText;
|
|
161
165
|
// Keep the row on ONE line. The left status gets at most a third of the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.75.
|
|
3
|
+
"version": "2.75.5",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
36
|
"qwen-smoke": "tsx scripts/qwen-smoke.mts",
|
|
37
37
|
"qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
|
|
38
|
+
"sched-smoke": "tsx scripts/sched-smoke.mts",
|
|
38
39
|
"render-demo": "tsx scripts/render-demo.mts",
|
|
39
40
|
"render-how": "tsx scripts/render-how-it-works.mts",
|
|
40
41
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// sched-smoke: ЖИВАЯ проверка планировщика (/loop, /cron, /jobs) в РЕАЛЬНОМ
|
|
2
|
+
// zames.
|
|
3
|
+
//
|
|
4
|
+
// Ключевая идея: TUI-ввод в песочнице проверить нельзя (Ink требует настоящий
|
|
5
|
+
// интерактивный терминал, а pty.fork/script в CI его не дают — панель ввода
|
|
6
|
+
// просто не рендерится). Поэтому проверяем ДВЕ вещи независимо и по-настоящему:
|
|
7
|
+
//
|
|
8
|
+
// 1) ЧИСТЫЕ модули планировщика (src/scheduler.ts) — экспортируемый код
|
|
9
|
+
// ядра, который вызывает main loop: parseInterval, parseCron, nextCronTime,
|
|
10
|
+
// Scheduler.addLoop/addCron/due/remove/clear, formatJobLine.
|
|
11
|
+
// 2) ФАКТИЧЕСКУЮ РЕАКЦИЮ main loop: планировщик реально enqueue'ит задачу в
|
|
12
|
+
// очередь, и она попадает в тот же pendingQueue, что и ввод оператора.
|
|
13
|
+
// Это воспроизводим без браузера: Scheduler + due() + push в очередь —
|
|
14
|
+
// ровно то, что делает 1-секундный tick в index.ts.
|
|
15
|
+
//
|
|
16
|
+
// Плюс: /loop, /cron, /jobs присутствуют в реальном списке команд (SLASH_COMMANDS)
|
|
17
|
+
// и их описания локализованы — чтобы оператор их видел в подсказке и /help.
|
|
18
|
+
//
|
|
19
|
+
// Изоляция: HOME -> временная папка ДО импорта модулей (как в self-smoke),
|
|
20
|
+
// живой ~/.zames не трогается. Флагами можно попросить --headed, но браузер
|
|
21
|
+
// здесь не поднимается вообще: сценарии не требуют сети.
|
|
22
|
+
import fs from 'fs'
|
|
23
|
+
import path from 'path'
|
|
24
|
+
import os from 'os'
|
|
25
|
+
|
|
26
|
+
const REAL_HOME = os.homedir()
|
|
27
|
+
if (!process.env.ZAMES_SCHED_SMOKE_HOME) {
|
|
28
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-sched-'))
|
|
29
|
+
process.env.HOME = tmp
|
|
30
|
+
process.env.ZAMES_SCHED_SMOKE_HOME = tmp
|
|
31
|
+
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
32
|
+
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
33
|
+
if (fs.existsSync(realCfg)) {
|
|
34
|
+
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const SMOKE_HOME = process.env.ZAMES_SCHED_SMOKE_HOME as string
|
|
38
|
+
console.log('sched-smoke: HOME=' + SMOKE_HOME)
|
|
39
|
+
|
|
40
|
+
const { Scheduler, parseInterval, parseCron, nextCronTime, formatJobLine } =
|
|
41
|
+
await import('../src/scheduler.ts')
|
|
42
|
+
const { SLASH_COMMANDS } = await import('../src/commands.ts')
|
|
43
|
+
const { translate } = await import('../src/i18n.ts')
|
|
44
|
+
|
|
45
|
+
let failures = 0
|
|
46
|
+
const check = (name: string, ok: boolean, note?: string): void => {
|
|
47
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
48
|
+
if (!ok) failures++
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 1. /loop: интервал -> задача
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
console.log('\n--- /loop ---')
|
|
55
|
+
check('parseInterval 10s = 10000ms', parseInterval('10s') === 10_000)
|
|
56
|
+
check('parseInterval 5m = 300000ms', parseInterval('5m') === 300_000)
|
|
57
|
+
check('parseInterval 2h = 7200000ms', parseInterval('2h') === 7_200_000)
|
|
58
|
+
check('parseInterval bare 30 = 30000ms', parseInterval('30') === 30_000)
|
|
59
|
+
check('parseInterval мусор = null', parseInterval('abc') === null)
|
|
60
|
+
|
|
61
|
+
const s = new Scheduler()
|
|
62
|
+
const loop = s.addLoop(parseInterval('20s')!, 'проверь состояние', 1_000_000)
|
|
63
|
+
check('addLoop создал job #' + loop.id, loop.kind === 'loop' && loop.id === 1)
|
|
64
|
+
check('nextAt = now + interval', loop.nextAt === 1_000_000 + 20_000)
|
|
65
|
+
console.log(' ' + formatJobLine(loop, 1_000_000))
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// 2. /cron: выражение -> задача
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
console.log('\n--- /cron ---')
|
|
71
|
+
check('parseCron */5 * * * *', parseCron('*/5 * * * *') !== null)
|
|
72
|
+
check('parseCron 0 9 * * 1-5', parseCron('0 9 * * 1-5') !== null)
|
|
73
|
+
check('parseCron отверг 4 поля', parseCron('* * * *') === null)
|
|
74
|
+
check('parseCron отверг имя дня MON', parseCron('0 9 * * MON') === null)
|
|
75
|
+
const nct = nextCronTime('*/2 * * * *', 1_000_000_000_000)
|
|
76
|
+
check(
|
|
77
|
+
'nextCronTime вернул будущее время',
|
|
78
|
+
typeof nct === 'number' && nct > 1_000_000_000_000,
|
|
79
|
+
)
|
|
80
|
+
const cron = s.addCron('*/2 * * * *', 'проверь сборку', 1_000_000)
|
|
81
|
+
check('addCron создал job #' + cron!.id, cron !== null && cron.kind === 'cron')
|
|
82
|
+
console.log(' ' + formatJobLine(cron!, 1_000_000))
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// 3. Главное: tick реально enqueue'ит задачу (то, что делает 1-сек таймер)
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
console.log('\n--- tick -> очередь (как в main loop) ---')
|
|
88
|
+
const pendingQueue: Array<{ text: string; jobId?: number }> = []
|
|
89
|
+
// Эмулируем 1-секундный tick из index.ts: due(now) -> push в очередь.
|
|
90
|
+
const tick = (now: number): number => {
|
|
91
|
+
const due = s.due(now)
|
|
92
|
+
for (const job of due) {
|
|
93
|
+
if (pendingQueue.some((m) => m.jobId === job.id)) continue
|
|
94
|
+
pendingQueue.push({ text: job.task, jobId: job.id })
|
|
95
|
+
}
|
|
96
|
+
return due.length
|
|
97
|
+
}
|
|
98
|
+
check('до срока tick ничего не даёт', tick(1_000_000 + 1_000) === 0)
|
|
99
|
+
check('cron */2 сработал по сроку', tick(cron!.nextAt) >= 1)
|
|
100
|
+
check(
|
|
101
|
+
'задача попала в очередь',
|
|
102
|
+
pendingQueue.some((m) => m.jobId === cron!.id),
|
|
103
|
+
)
|
|
104
|
+
check(
|
|
105
|
+
'в очереди текст задачи',
|
|
106
|
+
pendingQueue.some((m) => m.text === 'проверь сборку'),
|
|
107
|
+
)
|
|
108
|
+
// Даже если tick вызовут повторно, дубликата нет (та же логика дедупа).
|
|
109
|
+
const before = pendingQueue.length
|
|
110
|
+
tick(cron!.nextAt)
|
|
111
|
+
check('повторный tick не дублирует задачу', pendingQueue.length === before)
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// 4. /jobs: удаление и очистка
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
console.log('\n--- /jobs ---')
|
|
117
|
+
check('в списке 2 задачи', s.count() === 2)
|
|
118
|
+
check('remove(1) удалил loop', s.remove(1) === true)
|
|
119
|
+
check('count после rm = 1', s.count() === 1)
|
|
120
|
+
check('remove(999) несуществующей = false', s.remove(999) === false)
|
|
121
|
+
check('clear() вернул 1', s.clear() === 1)
|
|
122
|
+
check('count после clear = 0', s.count() === 0)
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// 5. /loop, /cron, /jobs есть в реальном списке команд и локализованы
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
console.log('\n--- список команд и локализация ---')
|
|
128
|
+
for (const name of ['/loop', '/cron', '/jobs']) {
|
|
129
|
+
const entry = SLASH_COMMANDS.find((c) => c.name === name)
|
|
130
|
+
check(name + ' в SLASH_COMMANDS', !!entry)
|
|
131
|
+
if (entry) {
|
|
132
|
+
const ru = translate('ru')(entry.key)
|
|
133
|
+
const en = translate('en')(entry.key)
|
|
134
|
+
check(
|
|
135
|
+
name + ' локализован (ru/en)',
|
|
136
|
+
!!ru && !!en && ru !== entry.key && en !== entry.key,
|
|
137
|
+
ru,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
'\nsched-smoke: ' + (failures === 0 ? 'ALL PASS' : failures + ' FAILED'),
|
|
144
|
+
)
|
|
145
|
+
if (!process.env.ZAMES_SCHED_SMOKE_KEEP) {
|
|
146
|
+
try {
|
|
147
|
+
fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|
|
150
|
+
process.exit(failures === 0 ? 0 : 1)
|