zames_pro 2.82.49 → 2.82.51
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 +60 -0
- package/README.md +159 -26
- package/SECURITY.md +50 -0
- package/dist/agent-loop.js +35 -5
- package/dist/answer-parse.js +1 -1
- package/dist/attach-flow.js +7 -3
- package/dist/attach-refs.js +30 -6
- package/dist/attachments.js +9 -0
- package/dist/browser-chats.js +9 -2
- package/dist/browser-dom.js +1 -1
- package/dist/browser-pure.js +9 -3
- package/dist/browser.js +1 -1
- package/dist/changelog.js +1 -1
- package/dist/checkpoint.js +1 -1
- package/dist/cli-args.js +1 -1
- package/dist/config-command.js +1 -1
- package/dist/config-commands.js +5 -2
- package/dist/config-live.js +10 -0
- package/dist/config.js +13 -1
- package/dist/context-command.js +6 -0
- package/dist/context.js +27 -4
- package/dist/diff.js +52 -9
- package/dist/extraTools.js +7 -0
- package/dist/glm-chats.js +18 -1
- package/dist/glm-net-capture.js +23 -5
- package/dist/goal.js +6 -1
- package/dist/i18n/deepseek.js +3 -3
- package/dist/i18n/self-review.js +4 -0
- package/dist/i18n.js +8 -2
- package/dist/index.js +90 -101
- package/dist/input/layout.js +33 -8
- package/dist/input/suggest.js +1 -1
- package/dist/input.js +1 -1
- package/dist/limits-command.js +42 -0
- package/dist/mcp.js +23 -4
- package/dist/model-command.js +21 -0
- package/dist/path-token.js +36 -9
- package/dist/permissions.js +32 -3
- package/dist/provider-command.js +29 -0
- package/dist/provider-handoff.js +18 -1
- package/dist/queue-command.js +15 -0
- package/dist/qwen-chats.js +13 -6
- package/dist/qwen-net-capture.js +13 -6
- package/dist/resume-command.js +38 -0
- package/dist/send-telemetry.js +15 -3
- package/dist/send-throttle.js +3 -1
- package/dist/status-command.js +68 -0
- package/dist/subagent.js +5 -12
- package/dist/tool-summary.js +8 -1
- package/dist/tools.js +13 -1
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/log-window.js +41 -4
- package/dist/undo.js +11 -1
- package/dist/web.js +16 -6
- package/llms-full.txt +679 -0
- package/llms.txt +102 -0
- package/package.json +84 -23
package/dist/index.js
CHANGED
|
@@ -47,6 +47,13 @@ import { askOperatorConfirm, } from './run-task.js';
|
|
|
47
47
|
import { closeWeb } from './web.js';
|
|
48
48
|
import { assertCommandInsideRoot, runShell, killAllBackground, } from './shell.js';
|
|
49
49
|
import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, isInternalChat, } from './sessions.js';
|
|
50
|
+
import { parseModelIntent, emptyModelListMessage } from './model-command.js';
|
|
51
|
+
import { parseResumeId, filterSessions, resumeProviderRefusal, } from './resume-command.js';
|
|
52
|
+
import { formatLimits, limitsEmptyMessage } from './limits-command.js';
|
|
53
|
+
import { renderContextReport } from './context-command.js';
|
|
54
|
+
import { renderQueueView } from './queue-command.js';
|
|
55
|
+
import { parseProviderIntent, providerUsageMessage, } from './provider-command.js';
|
|
56
|
+
import { formatStatus } from './status-command.js';
|
|
50
57
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
51
58
|
const NL = String.fromCharCode(10);
|
|
52
59
|
// The last final assistant message, for the /copy command. Module-level
|
|
@@ -70,7 +77,7 @@ function authMarkerExists(provider = config.provider) {
|
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
// ---------- CLI parsing ----------
|
|
73
|
-
// The pure helpers live in src/cli-args.ts
|
|
80
|
+
// The pure helpers live in src/cli-args.ts and are bound to this process's
|
|
74
81
|
// argv ONCE here. Call sites keep using getArg/hasFlag/getPositional.
|
|
75
82
|
const { getArg, hasFlag, getPositional } = createCliArgs(process.argv.slice(2));
|
|
76
83
|
// /compact asks the OLD chat for a summary. If browser.ask() exhausts its own
|
|
@@ -1267,7 +1274,7 @@ async function main() {
|
|
|
1267
1274
|
// Write the checklist whenever the agent changes it, so a restart/resume
|
|
1268
1275
|
// brings the tasks back (they used to be in-memory only).
|
|
1269
1276
|
todoStore.onChange = () => persistTodos();
|
|
1270
|
-
// Subagents
|
|
1277
|
+
// Subagents: the `Task` tool delegates a self-contained
|
|
1271
1278
|
// sub-task to a nested agent loop in its OWN chat (context isolation — in
|
|
1272
1279
|
// zames "context" IS the chat), then restores this chat and returns only
|
|
1273
1280
|
// the report. Sequential by design (one browser, one send slot). On by
|
|
@@ -1735,19 +1742,18 @@ async function main() {
|
|
|
1735
1742
|
if (ed.busy) {
|
|
1736
1743
|
const q = mod.commands.parseQueueCommand(text);
|
|
1737
1744
|
if (q) {
|
|
1738
|
-
|
|
1739
|
-
|
|
1745
|
+
const view = renderQueueView(q.sub, pendingQueue.length, pendingQueue);
|
|
1746
|
+
if (view.kind === 'cleared') {
|
|
1740
1747
|
pendingQueue.length = 0;
|
|
1741
|
-
ed.printAbove(theme.system(t('msg.queue_cleared') + ' (' + n + ')'));
|
|
1748
|
+
ed.printAbove(theme.system(t('msg.queue_cleared') + ' (' + view.n + ')'));
|
|
1742
1749
|
}
|
|
1743
|
-
else if (
|
|
1750
|
+
else if (view.kind === 'empty') {
|
|
1744
1751
|
ed.printAbove(theme.dim(t('msg.queue_empty')));
|
|
1745
1752
|
}
|
|
1746
|
-
else {
|
|
1753
|
+
else if (view.kind === 'list') {
|
|
1747
1754
|
ed.printAbove(theme.system(t('msg.queue_title')));
|
|
1748
|
-
for (const line of
|
|
1755
|
+
for (const line of view.lines)
|
|
1749
1756
|
ed.printAbove(theme.assistant(line));
|
|
1750
|
-
}
|
|
1751
1757
|
ed.printAbove(theme.dim(t('msg.queue_cleared_hint')));
|
|
1752
1758
|
}
|
|
1753
1759
|
return;
|
|
@@ -2616,8 +2622,8 @@ async function main() {
|
|
|
2616
2622
|
continue;
|
|
2617
2623
|
}
|
|
2618
2624
|
if (lower === '/provider' || lower.startsWith('/provider ')) {
|
|
2619
|
-
const
|
|
2620
|
-
if (
|
|
2625
|
+
const intent = parseProviderIntent(trimmed.slice('/provider'.length), config.provider);
|
|
2626
|
+
if (intent.kind === 'status') {
|
|
2621
2627
|
const model = providerModelOf(config.provider) || '(default)';
|
|
2622
2628
|
console.log(theme.system(t('provider.status', {
|
|
2623
2629
|
v: config.provider,
|
|
@@ -2626,7 +2632,7 @@ async function main() {
|
|
|
2626
2632
|
})));
|
|
2627
2633
|
continue;
|
|
2628
2634
|
}
|
|
2629
|
-
if (
|
|
2635
|
+
if (intent.kind === 'login') {
|
|
2630
2636
|
try {
|
|
2631
2637
|
await browser.waitForLogin();
|
|
2632
2638
|
console.log(theme.system(t('provider.login_done')));
|
|
@@ -2636,9 +2642,10 @@ async function main() {
|
|
|
2636
2642
|
}
|
|
2637
2643
|
continue;
|
|
2638
2644
|
}
|
|
2639
|
-
if (
|
|
2645
|
+
if (intent.kind === 'switch') {
|
|
2646
|
+
const arg = intent.provider;
|
|
2640
2647
|
const from = config.provider;
|
|
2641
|
-
if (
|
|
2648
|
+
if (intent.same) {
|
|
2642
2649
|
console.log(theme.system(t('provider.switched', { v: arg })));
|
|
2643
2650
|
continue;
|
|
2644
2651
|
}
|
|
@@ -2780,7 +2787,7 @@ async function main() {
|
|
|
2780
2787
|
}
|
|
2781
2788
|
continue;
|
|
2782
2789
|
}
|
|
2783
|
-
console.error(theme.warn(t
|
|
2790
|
+
console.error(theme.warn(providerUsageMessage(t)));
|
|
2784
2791
|
continue;
|
|
2785
2792
|
}
|
|
2786
2793
|
if (lower === '/model' || lower.startsWith('/model ')) {
|
|
@@ -2788,10 +2795,9 @@ async function main() {
|
|
|
2788
2795
|
// `/model` is available to ANY provider that exposes a model picker
|
|
2789
2796
|
// (Qwen, GLM). DeepSeek has none, so the command is hidden from the
|
|
2790
2797
|
// hints there and only explains itself when typed by hand.
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
})));
|
|
2798
|
+
const intent = parseModelIntent(config.provider, arg, t);
|
|
2799
|
+
if (intent.kind === 'unsupported') {
|
|
2800
|
+
console.log(theme.system(intent.message));
|
|
2795
2801
|
continue;
|
|
2796
2802
|
}
|
|
2797
2803
|
const section = providerConfigSection(config.provider);
|
|
@@ -2800,10 +2806,10 @@ async function main() {
|
|
|
2800
2806
|
continue;
|
|
2801
2807
|
const curModel = String(sectionObj.model || '');
|
|
2802
2808
|
const qb = browser;
|
|
2803
|
-
if (
|
|
2809
|
+
if (intent.kind === 'list') {
|
|
2804
2810
|
const models = qb.listModels ? await qb.listModels() : [];
|
|
2805
2811
|
if (!models.length) {
|
|
2806
|
-
console.log(theme.system(t
|
|
2812
|
+
console.log(theme.system(emptyModelListMessage(t)));
|
|
2807
2813
|
continue;
|
|
2808
2814
|
}
|
|
2809
2815
|
// In the TUI, open a proper SELECT LIST instead of dumping text.
|
|
@@ -2862,10 +2868,10 @@ async function main() {
|
|
|
2862
2868
|
console.log(theme.system(t('model.list', { v: lines })));
|
|
2863
2869
|
continue;
|
|
2864
2870
|
}
|
|
2865
|
-
sectionObj.model =
|
|
2871
|
+
sectionObj.model = intent.model;
|
|
2866
2872
|
if (typeof qb.model === 'string')
|
|
2867
|
-
qb.model =
|
|
2868
|
-
applyModelContextLimit(
|
|
2873
|
+
qb.model = intent.model;
|
|
2874
|
+
applyModelContextLimit(intent.model);
|
|
2869
2875
|
if (editor) {
|
|
2870
2876
|
editor.setContextLimit(effectiveContextLimit());
|
|
2871
2877
|
editor.refreshStatus();
|
|
@@ -2874,7 +2880,10 @@ async function main() {
|
|
|
2874
2880
|
writeConfigValue('home', section + '.model', arg);
|
|
2875
2881
|
}
|
|
2876
2882
|
catch { }
|
|
2877
|
-
console.log(theme.system(t('model.set', {
|
|
2883
|
+
console.log(theme.system(t('model.set', {
|
|
2884
|
+
provider: providerLabel(config.provider),
|
|
2885
|
+
v: intent.model,
|
|
2886
|
+
})));
|
|
2878
2887
|
continue;
|
|
2879
2888
|
}
|
|
2880
2889
|
if (lower === '/chat') {
|
|
@@ -2898,9 +2907,7 @@ async function main() {
|
|
|
2898
2907
|
.toLowerCase();
|
|
2899
2908
|
// /sessions <query> filters by title or workdir (substring). Sorted by
|
|
2900
2909
|
// recency already (listSessions).
|
|
2901
|
-
const all = listSessions()
|
|
2902
|
-
(s.title || '').toLowerCase().includes(filter) ||
|
|
2903
|
-
(s.workdir || '').toLowerCase().includes(filter));
|
|
2910
|
+
const all = filterSessions(listSessions(), filter);
|
|
2904
2911
|
console.log(theme.system(t('sessions.dir', { v: sessionsDir() })));
|
|
2905
2912
|
if (!all.length) {
|
|
2906
2913
|
console.log(theme.system(t('sessions.none')));
|
|
@@ -2961,11 +2968,19 @@ async function main() {
|
|
|
2961
2968
|
continue;
|
|
2962
2969
|
}
|
|
2963
2970
|
if (lower.startsWith('/resume-id ')) {
|
|
2964
|
-
const id = trimmed.slice('/resume-id'.length)
|
|
2971
|
+
const id = parseResumeId(trimmed.slice('/resume-id'.length));
|
|
2965
2972
|
if (!id) {
|
|
2966
2973
|
console.error(theme.error(t('sessions.resume_id_usage')));
|
|
2967
2974
|
continue;
|
|
2968
2975
|
}
|
|
2976
|
+
// A chat id only exists on the backend that created it, so opening a
|
|
2977
|
+
// session saved by ANOTHER provider fails deep inside the browser with a
|
|
2978
|
+
// confusing error. Refuse up front and name the right /provider command.
|
|
2979
|
+
const refusal = resumeProviderRefusal(readSession(id), config.provider, t);
|
|
2980
|
+
if (refusal) {
|
|
2981
|
+
console.error(theme.error(refusal));
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2969
2984
|
if (editor)
|
|
2970
2985
|
editor.lock(t('msg.input_locked'));
|
|
2971
2986
|
try {
|
|
@@ -3121,24 +3136,23 @@ async function main() {
|
|
|
3121
3136
|
}
|
|
3122
3137
|
if (lower === '/queue' || lower.startsWith('/queue ')) {
|
|
3123
3138
|
const q = mod.commands.parseQueueCommand(trimmed);
|
|
3124
|
-
|
|
3139
|
+
const view = renderQueueView(q ? q.sub : null, pendingQueue.length, pendingQueue);
|
|
3140
|
+
if (view.kind === 'usage') {
|
|
3125
3141
|
console.error(theme.error(t('msg.queue_usage')));
|
|
3126
3142
|
continue;
|
|
3127
3143
|
}
|
|
3128
|
-
if (
|
|
3129
|
-
const n = pendingQueue.length;
|
|
3144
|
+
if (view.kind === 'cleared') {
|
|
3130
3145
|
pendingQueue.length = 0;
|
|
3131
|
-
console.log(theme.system(t('msg.queue_cleared') + ' (' + n + ')'));
|
|
3146
|
+
console.log(theme.system(t('msg.queue_cleared') + ' (' + view.n + ')'));
|
|
3132
3147
|
continue;
|
|
3133
3148
|
}
|
|
3134
|
-
if (
|
|
3149
|
+
if (view.kind === 'empty') {
|
|
3135
3150
|
console.log(theme.dim(t('msg.queue_empty')));
|
|
3136
3151
|
continue;
|
|
3137
3152
|
}
|
|
3138
3153
|
console.log(theme.system(t('msg.queue_title')));
|
|
3139
|
-
for (const line of
|
|
3154
|
+
for (const line of view.lines)
|
|
3140
3155
|
console.log(theme.assistant(line));
|
|
3141
|
-
}
|
|
3142
3156
|
console.log(theme.dim(t('msg.queue_cleared_hint')));
|
|
3143
3157
|
continue;
|
|
3144
3158
|
}
|
|
@@ -3388,51 +3402,32 @@ async function main() {
|
|
|
3388
3402
|
continue;
|
|
3389
3403
|
}
|
|
3390
3404
|
if (lower === '/status') {
|
|
3391
|
-
const yes = t('common.yes');
|
|
3392
|
-
const no = t('common.no');
|
|
3393
|
-
console.log(theme.system(t('status.workdir', { v: currentWorkdir })));
|
|
3394
|
-
console.log(theme.system(t('status.review_mode', {
|
|
3395
|
-
v: reviewMode ? reviewMode.snapName : t('status.review_none'),
|
|
3396
|
-
})));
|
|
3397
|
-
if (reviewMode) {
|
|
3398
|
-
console.log(theme.system(t('status.orig_dir', { v: reviewMode.originalWorkdir })));
|
|
3399
|
-
}
|
|
3400
|
-
console.log(theme.system(t('status.chat', { v: currentChatId || t('common.none') })));
|
|
3401
|
-
console.log(theme.system(t('status.fresh_next', { v: freshChatNext ? yes : no })));
|
|
3402
|
-
console.log(theme.system(t('status.prompt_next', { v: sendSystemPromptNext ? yes : no })));
|
|
3403
|
-
console.log(theme.system(t('status.last_chat', {
|
|
3404
|
-
v: loadLastChat(currentWorkdir) || t('common.none'),
|
|
3405
|
-
})));
|
|
3406
|
-
console.log(theme.system(t('status.sessions', { v: sessionsDir() })));
|
|
3407
|
-
console.log(theme.system(t('status.dev', {
|
|
3408
|
-
v: devMode ? t('common.on') : t('common.off'),
|
|
3409
|
-
})));
|
|
3410
|
-
console.log(theme.system(t('status.max_iter', { v: currentMaxIter() })));
|
|
3411
|
-
console.log(theme.system(t('status.headless', { v: headless ? yes : no })));
|
|
3412
|
-
console.log(theme.system(t('status.debug', { v: debug ? yes : no })));
|
|
3413
3405
|
const statusToggles = browser.getToggleStatesSync();
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3406
|
+
const statusLines = formatStatus({
|
|
3407
|
+
workdir: currentWorkdir,
|
|
3408
|
+
reviewMode: reviewMode ? reviewMode.snapName : null,
|
|
3409
|
+
origDir: reviewMode ? reviewMode.originalWorkdir : null,
|
|
3410
|
+
chatId: currentChatId,
|
|
3411
|
+
freshNext: freshChatNext,
|
|
3412
|
+
promptNext: sendSystemPromptNext,
|
|
3413
|
+
lastChat: loadLastChat(currentWorkdir),
|
|
3414
|
+
sessionsDir: sessionsDir(),
|
|
3415
|
+
devMode,
|
|
3416
|
+
maxIterations: currentMaxIter(),
|
|
3417
|
+
headless,
|
|
3418
|
+
debug,
|
|
3419
|
+
deepThinking: statusToggles.deepThinking,
|
|
3420
|
+
webSearch: statusToggles.webSearch,
|
|
3421
|
+
undo: config.undo.enabled,
|
|
3422
|
+
checkpoint: config.checkpoint.enabled,
|
|
3423
|
+
transcriptFile: transcript.file,
|
|
3424
|
+
goal: sessionGoal,
|
|
3425
|
+
localeName: localeDisplayName(currentLocale),
|
|
3426
|
+
mcpToolCount: mcpPool ? mcpPool.status().toolCount : null,
|
|
3427
|
+
tokenUsage: browser.getLastTokenUsage(),
|
|
3428
|
+
}, t);
|
|
3429
|
+
for (const line of statusLines)
|
|
3430
|
+
console.log(theme.system(line));
|
|
3436
3431
|
continue;
|
|
3437
3432
|
}
|
|
3438
3433
|
if (lower === '/config' || lower.startsWith('/config ')) {
|
|
@@ -3456,7 +3451,12 @@ async function main() {
|
|
|
3456
3451
|
locale: currentLocale,
|
|
3457
3452
|
context: ctx,
|
|
3458
3453
|
}).length;
|
|
3459
|
-
console.log(theme.system(
|
|
3454
|
+
console.log(theme.system(renderContextReport({
|
|
3455
|
+
agents: ctx.agents,
|
|
3456
|
+
memory: ctx.memory,
|
|
3457
|
+
skills: ctx.skills,
|
|
3458
|
+
commands: ctx.commands,
|
|
3459
|
+
}, { systemPromptChars: sysLen }, t)));
|
|
3460
3460
|
}
|
|
3461
3461
|
catch (e) {
|
|
3462
3462
|
console.error(theme.error(e.message));
|
|
@@ -3563,29 +3563,18 @@ async function main() {
|
|
|
3563
3563
|
const logsDir = config.transcript.dir;
|
|
3564
3564
|
const events = readSendEvents(logsDir, config.provider);
|
|
3565
3565
|
if (!events.length) {
|
|
3566
|
-
console.log(theme.system(
|
|
3566
|
+
console.log(theme.system(limitsEmptyMessage(sendLogPath(logsDir, config.provider), t)));
|
|
3567
3567
|
}
|
|
3568
3568
|
else {
|
|
3569
3569
|
const st = summarizeSends(events);
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
console.log('
|
|
3570
|
+
for (const line of formatLimits(st, {
|
|
3571
|
+
providerLabel: providerLabel(config.provider),
|
|
3572
|
+
intervalMs: browser.sendIntervalMs(),
|
|
3573
|
+
adaptive: providerAdaptiveThrottle(config.provider),
|
|
3574
|
+
}, t)) {
|
|
3575
|
+
// The title is printed bare; the counters are indented under it.
|
|
3576
|
+
console.log(line.kind === 'title' ? theme.system(line.text) : ' ' + line.text);
|
|
3577
3577
|
}
|
|
3578
|
-
console.log(' ' +
|
|
3579
|
-
t('limits.rate', {
|
|
3580
|
-
n: String(st.rateLimits),
|
|
3581
|
-
sec: String(Math.round(st.avgBlockMs / 1000)),
|
|
3582
|
-
}));
|
|
3583
|
-
const adaptive = providerAdaptiveThrottle(config.provider);
|
|
3584
|
-
console.log(' ' +
|
|
3585
|
-
t('limits.interval', {
|
|
3586
|
-
sec: String(Math.round(browser.sendIntervalMs() / 1000)),
|
|
3587
|
-
adaptive: adaptive ? t('limits.adaptive_on') : '',
|
|
3588
|
-
}));
|
|
3589
3578
|
}
|
|
3590
3579
|
continue;
|
|
3591
3580
|
}
|
|
@@ -4120,7 +4109,7 @@ async function main() {
|
|
|
4120
4109
|
task: finalTaskText,
|
|
4121
4110
|
workdir: currentWorkdir,
|
|
4122
4111
|
});
|
|
4123
|
-
//
|
|
4112
|
+
// Snapshot the working tree BEFORE the task so /rewind can
|
|
4124
4113
|
// undo the whole task in one step. Best-effort: create() never throws, so
|
|
4125
4114
|
// a missing tar cannot abort the task it was meant to protect. Skipped
|
|
4126
4115
|
// while reviewing a snapshot (workdir is a throwaway copy).
|
package/dist/input/layout.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// LineEditor (src/input.ts). No terminal access, no state: given a prompt, a
|
|
3
3
|
// buffer, a cursor and a width these compute the visual rows, the cursor
|
|
4
4
|
// position, the visible width (wide chars count 2) and the status formatting.
|
|
5
|
-
// Split out
|
|
5
|
+
// Split out so the geometry can be unit-tested without driving the
|
|
6
6
|
// real editor, and so the editor class keeps only render/key handling.
|
|
7
7
|
// All control characters are assembled from codes so the file has no
|
|
8
8
|
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
@@ -37,7 +37,12 @@ export function countPasteLines(raw) {
|
|
|
37
37
|
.join(NL)
|
|
38
38
|
.split(CR)
|
|
39
39
|
.join(NL);
|
|
40
|
-
|
|
40
|
+
// Only the SINGLE last newline is a terminator (it does not open a new
|
|
41
|
+
// line); any newline before it ends a real - possibly BLANK - line. The
|
|
42
|
+
// old /\n+$/ stripped ALL trailing newlines, so a paste of "a\n\n\n"
|
|
43
|
+
// counted as ONE line: it was inserted verbatim instead of collapsing into
|
|
44
|
+
// a `[Pasted lines#N]` marker and flooded the input line.
|
|
45
|
+
const trimmed = normalized.endsWith(NL) ? normalized.slice(0, -1) : normalized;
|
|
41
46
|
if (trimmed === '')
|
|
42
47
|
return 1;
|
|
43
48
|
return trimmed.split(NL).length;
|
|
@@ -197,15 +202,23 @@ export function wrapToWidth(s, maxCols) {
|
|
|
197
202
|
// separator, so `'aaa bbb'` at width 7 became `'aaabbb'` and an indented
|
|
198
203
|
// warning lost its indent.
|
|
199
204
|
const words = raw.split(/ +/).filter((w) => w !== '');
|
|
200
|
-
|
|
205
|
+
// An INDENT deeper than the whole width can never be reproduced within
|
|
206
|
+
// the margin: keeping it on the first line glued `width` MORE columns to
|
|
207
|
+
// it (a pasted stack-trace line pushed the terminal into horizontal
|
|
208
|
+
// scroll). It is clipped so every produced line fits.
|
|
209
|
+
let indent = raw.slice(0, raw.length - raw.trimStart().length);
|
|
210
|
+
if (visLen(indent) > width)
|
|
211
|
+
indent = truncateToWidth(indent, width - 1);
|
|
212
|
+
const avail = Math.max(1, width - visLen(indent));
|
|
201
213
|
let line = indent;
|
|
202
214
|
let lineW = visLen(indent);
|
|
203
215
|
let first = true;
|
|
204
216
|
for (let i = 0; i < words.length; i++) {
|
|
205
217
|
const word = words[i];
|
|
206
218
|
const w = visLen(word);
|
|
207
|
-
// Hard-split a word
|
|
208
|
-
|
|
219
|
+
// Hard-split a word that does not fit the REMAINING columns on this
|
|
220
|
+
// line (the indent, plus any earlier word, consumes part of it).
|
|
221
|
+
if (w > avail) {
|
|
209
222
|
if (!first) {
|
|
210
223
|
out.push(line);
|
|
211
224
|
line = '';
|
|
@@ -213,12 +226,14 @@ export function wrapToWidth(s, maxCols) {
|
|
|
213
226
|
}
|
|
214
227
|
let piece = '';
|
|
215
228
|
let pieceW = 0;
|
|
229
|
+
let room = width - lineW;
|
|
216
230
|
for (const ch of word) {
|
|
217
231
|
const cw = charWidth(ch.codePointAt(0));
|
|
218
|
-
if (pieceW + cw >
|
|
232
|
+
if (pieceW + cw > room) {
|
|
219
233
|
out.push(line + piece);
|
|
220
234
|
line = '';
|
|
221
235
|
lineW = 0;
|
|
236
|
+
room = width;
|
|
222
237
|
piece = '';
|
|
223
238
|
pieceW = 0;
|
|
224
239
|
}
|
|
@@ -382,11 +397,21 @@ export const CONTEXT_YELLOW_PCT = 50;
|
|
|
382
397
|
export const CONTEXT_RED_PCT = 80;
|
|
383
398
|
// The color role for a context fill level, as a stable string so it can be
|
|
384
399
|
// unit-tested without a terminal: 'ok' | 'warn' | 'high'. Null tokens -> null.
|
|
400
|
+
// A usable context window: a finite POSITIVE number. The denominator comes
|
|
401
|
+
// from config (`ui.contextLimit` / a provider section), so a 0 / NaN /
|
|
402
|
+
// negative / Infinity value reaches these functions; dividing by it rendered
|
|
403
|
+
// `Infinity%` / `NaN%` (or a fake 0.0% and even 'high') on a real token
|
|
404
|
+
// count. An unusable window falls back to the default one.
|
|
405
|
+
function usableLimit(limit) {
|
|
406
|
+
return typeof limit === 'number' && Number.isFinite(limit) && limit > 0
|
|
407
|
+
? limit
|
|
408
|
+
: CONTEXT_LIMIT;
|
|
409
|
+
}
|
|
385
410
|
export function tokenStatusLevel(tokens, limit = CONTEXT_LIMIT) {
|
|
386
411
|
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
387
412
|
return null;
|
|
388
413
|
}
|
|
389
|
-
const pct = (tokens / limit) * 100;
|
|
414
|
+
const pct = (tokens / usableLimit(limit)) * 100;
|
|
390
415
|
if (pct >= CONTEXT_RED_PCT)
|
|
391
416
|
return 'high';
|
|
392
417
|
if (pct >= CONTEXT_YELLOW_PCT)
|
|
@@ -399,7 +424,7 @@ export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
|
399
424
|
}
|
|
400
425
|
const n = Math.round(tokens);
|
|
401
426
|
const compact = formatCompactTokens(n);
|
|
402
|
-
const pct = Math.max(0, (n / limit) * 100);
|
|
427
|
+
const pct = Math.max(0, (n / usableLimit(limit)) * 100);
|
|
403
428
|
const pctStr = pct >= 10 ? String(Math.round(pct)) : pct.toFixed(1);
|
|
404
429
|
// "ctx:" marks this as the USED context size, so the operator does not have
|
|
405
430
|
// to guess what "302k · 30%" means.
|
package/dist/input/suggest.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// of the LineEditor's «/» hint list (src/input.ts). No terminal access and no
|
|
3
3
|
// editor state: given the buffer text and the command list these decide which
|
|
4
4
|
// commands to show, the visible window, and the Tab completion. Split out
|
|
5
|
-
//
|
|
5
|
+
// so the fuzzy match can be unit-tested without driving the editor.
|
|
6
6
|
import { SUGGEST_PAGE, isSubsequence, visLen, } from './layout.js';
|
|
7
7
|
/**
|
|
8
8
|
* The commands matching the current buffer. Shown only when the line starts
|
package/dist/input.js
CHANGED
|
@@ -9,7 +9,7 @@ import { NL, CR, ESC, PASTE_START, PASTE_END, SUGGEST_PAGE, pasteReplacement, ex
|
|
|
9
9
|
// The pure layout/formatting helpers (visual rows, cursor position, visible
|
|
10
10
|
// width, paste markers, token formatting) live in ./input/layout.js. They are
|
|
11
11
|
// re-exported here so every existing import from './input.js' keeps working,
|
|
12
|
-
// and so tests can import them from either module
|
|
12
|
+
// and so tests can import them from either module.
|
|
13
13
|
export { PASTE_MIN_LINES, SUGGEST_PAGE, isSubsequence, countPasteLines, formatPasteMarker, pasteReplacement, expandPastes, visRows, visLen, charWidth, layoutInput, tokenStatusLevel, formatTokenStatus, formatCompactTokens, splitPasteTail, truncateToWidth, wrapToWidth, CONTEXT_LIMIT, CONTEXT_YELLOW_PCT, CONTEXT_RED_PCT, } from './input/layout.js';
|
|
14
14
|
// PURE slash-command suggestion logic (match / paging / completion), split out
|
|
15
15
|
// of the class so the fuzzy match is unit-tested without driving the editor.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { translate } from './i18n.js';
|
|
2
|
+
/**
|
|
3
|
+
* The lines of `/limits` for an aggregate. `intervalMs` is the ACTIVE browser's
|
|
4
|
+
* send interval and `adaptive` whether the pause is derived adaptively.
|
|
5
|
+
*/
|
|
6
|
+
export function formatLimits(stats, opts, t = translate('en')) {
|
|
7
|
+
const out = [];
|
|
8
|
+
out.push({
|
|
9
|
+
kind: 'title',
|
|
10
|
+
text: t('limits.title', { provider: opts.providerLabel }),
|
|
11
|
+
});
|
|
12
|
+
const windows = [
|
|
13
|
+
['10m', stats.last10m],
|
|
14
|
+
['30m', stats.last30m],
|
|
15
|
+
['60m', stats.last60m],
|
|
16
|
+
];
|
|
17
|
+
for (const [label, n] of windows) {
|
|
18
|
+
out.push({
|
|
19
|
+
kind: 'window',
|
|
20
|
+
text: t('limits.window', { label, n: String(n) }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
out.push({
|
|
24
|
+
kind: 'rate',
|
|
25
|
+
text: t('limits.rate', {
|
|
26
|
+
n: String(stats.rateLimits),
|
|
27
|
+
sec: String(Math.round(stats.avgBlockMs / 1000)),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
out.push({
|
|
31
|
+
kind: 'interval',
|
|
32
|
+
text: t('limits.interval', {
|
|
33
|
+
sec: String(Math.round(opts.intervalMs / 1000)),
|
|
34
|
+
adaptive: opts.adaptive ? t('limits.adaptive_on') : '',
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The text for an EMPTY telemetry file (nothing recorded yet). */
|
|
40
|
+
export function limitsEmptyMessage(path, t = translate('en')) {
|
|
41
|
+
return t('limits.none', { path });
|
|
42
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -44,7 +44,14 @@ export function hardenPlaywrightArgs(command, args) {
|
|
|
44
44
|
const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
|
|
45
45
|
if (!hay.includes('playwright') || !hay.includes('mcp'))
|
|
46
46
|
return args;
|
|
47
|
-
|
|
47
|
+
// The profile flag may sit in the COMMAND itself: a config like
|
|
48
|
+
// `{command: "npx --user-data-dir=/tmp/x", args: ["@playwright/mcp"]}` is
|
|
49
|
+
// legitimate, and checking only `args` appended `--isolated` next to the
|
|
50
|
+
// profile — the server then has BOTH an on-disk profile and an in-memory
|
|
51
|
+
// one and picks unpredictably (the live agent's profile is the very thing
|
|
52
|
+
// this guard exists to protect).
|
|
53
|
+
const all = [String(command || ''), ...args];
|
|
54
|
+
if (all.some((a) =>
|
|
48
55
|
// A PREFIX match treated `--isolated=false` as the isolated flag, so
|
|
49
56
|
// the hardener added nothing and the MCP browser opened the agent's
|
|
50
57
|
// live profile anyway. Only a flag EXPLICITLY ON, or a value-taking
|
|
@@ -53,10 +60,16 @@ export function hardenPlaywrightArgs(command, args) {
|
|
|
53
60
|
/^--isolated=(?!false$)/.test(a) ||
|
|
54
61
|
a === '--user-data-dir' ||
|
|
55
62
|
a.startsWith('--user-data-dir=') ||
|
|
63
|
+
a.endsWith('--user-data-dir') ||
|
|
64
|
+
a.includes('--user-data-dir=') ||
|
|
56
65
|
a === '--extension' ||
|
|
57
66
|
a.startsWith('--extension=') ||
|
|
67
|
+
a.endsWith('--extension') ||
|
|
68
|
+
a.includes('--extension=') ||
|
|
58
69
|
a === '--cdp-endpoint' ||
|
|
59
|
-
a.startsWith('--cdp-endpoint=')
|
|
70
|
+
a.startsWith('--cdp-endpoint=') ||
|
|
71
|
+
a.endsWith('--cdp-endpoint') ||
|
|
72
|
+
a.includes('--cdp-endpoint='))) {
|
|
60
73
|
return args;
|
|
61
74
|
}
|
|
62
75
|
return [...args, '--isolated'];
|
|
@@ -167,8 +180,14 @@ export function jsonSchemaToParams(schema) {
|
|
|
167
180
|
// descriptions and enums. The flat map alone cannot convey them, and MCP
|
|
168
181
|
// tools (browser_snapshot, browser_click, ...) rely on them heavily.
|
|
169
182
|
export function describeParamsExternally(description, schema) {
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
// A JSON Schema may spell the object type as a LIST (`type: ['object']`, a
|
|
184
|
+
// zod-derived or nullable wrapper). Rejecting anything but the exact string
|
|
185
|
+
// 'object' silently dropped the schema — field descriptions and enums — from
|
|
186
|
+
// the model-facing tool description while the flat param map still listed
|
|
187
|
+
// the fields.
|
|
188
|
+
const raw = schema?.type;
|
|
189
|
+
const isObject = raw === 'object' || (Array.isArray(raw) && raw.includes('object'));
|
|
190
|
+
if (!schema || !isObject)
|
|
172
191
|
return description;
|
|
173
192
|
try {
|
|
174
193
|
const json = JSON.stringify(schema);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { providerHasModelPicker } from './providers.js';
|
|
2
|
+
import { translate } from './i18n.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decide what /model should do from the command argument and the provider.
|
|
5
|
+
* The argument is the text after "/model" (trimmed here again); an empty
|
|
6
|
+
* argument means "list".
|
|
7
|
+
*/
|
|
8
|
+
export function parseModelIntent(provider, arg, t = translate('en')) {
|
|
9
|
+
if (!providerHasModelPicker(provider)) {
|
|
10
|
+
return {
|
|
11
|
+
kind: 'unsupported',
|
|
12
|
+
message: t('model.unsupported', { provider }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const model = String(arg ?? '').trim();
|
|
16
|
+
return model ? { kind: 'set', model } : { kind: 'list' };
|
|
17
|
+
}
|
|
18
|
+
/** The text for an EMPTY model list (a provider that answered with nothing). */
|
|
19
|
+
export function emptyModelListMessage(t = translate('en')) {
|
|
20
|
+
return t('model.list', { v: '(none)' });
|
|
21
|
+
}
|