zames_pro 2.82.48 → 2.82.50
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 +45 -0
- package/dist/agent-loop.js +11 -1
- package/dist/attach-flow.js +7 -3
- package/dist/attach-refs.js +8 -0
- package/dist/attachments.js +9 -0
- package/dist/browser-chats.js +8 -1
- package/dist/browser-pure.js +8 -2
- package/dist/commands.js +5 -0
- package/dist/config-commands.js +4 -1
- package/dist/config-live.js +10 -0
- package/dist/config.js +12 -0
- package/dist/context-command.js +6 -0
- package/dist/context.js +8 -1
- package/dist/diff.js +52 -9
- package/dist/glm-chats.js +18 -1
- package/dist/glm-net-capture.js +23 -5
- package/dist/goal.js +6 -1
- package/dist/i18n/self-review.js +4 -0
- package/dist/i18n.js +7 -1
- package/dist/index.js +87 -98
- package/dist/input/layout.js +32 -7
- package/dist/limits-command.js +42 -0
- package/dist/mcp.js +15 -2
- package/dist/model-command.js +21 -0
- package/dist/path-token.js +35 -8
- 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/tool-summary.js +8 -1
- package/dist/tools.js +4 -1
- package/dist/tui/log-window.js +41 -4
- package/dist/undo.js +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { providerHasModelPicker, providerLabel, isProviderId, PROVIDER_IDS, } from './providers.js';
|
|
2
|
+
import { translate } from './i18n.js';
|
|
3
|
+
/** Parse the text after `/provider` into an intent. `current` is the ACTIVE id. */
|
|
4
|
+
export function parseProviderIntent(arg, current) {
|
|
5
|
+
const a = String(arg ?? '')
|
|
6
|
+
.trim()
|
|
7
|
+
.toLowerCase();
|
|
8
|
+
if (!a || a === 'status')
|
|
9
|
+
return { kind: 'status' };
|
|
10
|
+
if (a === 'login')
|
|
11
|
+
return { kind: 'login' };
|
|
12
|
+
if (isProviderId(a))
|
|
13
|
+
return { kind: 'switch', provider: a, same: a === current };
|
|
14
|
+
return { kind: 'usage' };
|
|
15
|
+
}
|
|
16
|
+
/** The usage line for an unknown `/provider` argument. */
|
|
17
|
+
export function providerUsageMessage(t = translate('en')) {
|
|
18
|
+
return t('provider.usage', { providers: PROVIDER_IDS.join('|') });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Why a provider with no model picker cannot be selected via `/model`.
|
|
22
|
+
* (Kept here as well as in model-command so the two commands share the
|
|
23
|
+
* capability check instead of each hardcoding a provider id.)
|
|
24
|
+
*/
|
|
25
|
+
export function providerWithoutPickerMessage(provider, t = translate('en')) {
|
|
26
|
+
if (providerHasModelPicker(provider))
|
|
27
|
+
return null;
|
|
28
|
+
return t('model.unsupported', { provider: providerLabel(provider) });
|
|
29
|
+
}
|
package/dist/provider-handoff.js
CHANGED
|
@@ -133,10 +133,27 @@ function sameDir(a, b) {
|
|
|
133
133
|
};
|
|
134
134
|
return real(x) === real(y);
|
|
135
135
|
}
|
|
136
|
-
/** Remove the handoff once it has been posted (or is stale). Best-effort.
|
|
136
|
+
/** Remove the handoff once it has been posted (or is stale). Best-effort.
|
|
137
|
+
*
|
|
138
|
+
* The GLOBAL file and every per-workdir SIDE file are removed. Clearing only
|
|
139
|
+
* the global one left a displaced record readable through its side file, so
|
|
140
|
+
* an already-consumed summary was posted again on a later switch in that
|
|
141
|
+
* project ("the summary came back from the dead"). Side files are small and
|
|
142
|
+
* their whole purpose is a pending handoff, so removing all of them is safe. */
|
|
137
143
|
export function clearProviderHandoff() {
|
|
138
144
|
try {
|
|
139
145
|
fs.unlinkSync(HANDOFF_FILE);
|
|
140
146
|
}
|
|
141
147
|
catch { }
|
|
148
|
+
try {
|
|
149
|
+
for (const f of fs.readdirSync(ZAMES_HOME)) {
|
|
150
|
+
if (!f.startsWith('provider-handoff-') || !f.endsWith('.json'))
|
|
151
|
+
continue;
|
|
152
|
+
try {
|
|
153
|
+
fs.unlinkSync(path.join(ZAMES_HOME, f));
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
142
159
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { formatQueueList } from './commands.js';
|
|
2
|
+
/**
|
|
3
|
+
* The full output of `/queue` (minus the `clear` SIDE EFFECT, which the caller
|
|
4
|
+
* performs on its own live queue). `sub` is the parsed subcommand or null when
|
|
5
|
+
* it was not recognized; `count` is the live queue length.
|
|
6
|
+
*/
|
|
7
|
+
export function renderQueueView(sub, count, queue) {
|
|
8
|
+
if (!sub)
|
|
9
|
+
return { kind: 'usage' };
|
|
10
|
+
if (sub === 'clear')
|
|
11
|
+
return { kind: 'cleared', n: count };
|
|
12
|
+
if (!count || !queue.length)
|
|
13
|
+
return { kind: 'empty' };
|
|
14
|
+
return { kind: 'list', lines: formatQueueList(queue) };
|
|
15
|
+
}
|
package/dist/qwen-chats.js
CHANGED
|
@@ -187,12 +187,19 @@ export function parseQwenHistory(json) {
|
|
|
187
187
|
arr = asArr(d.messages);
|
|
188
188
|
else if (d.chat && typeof d.chat === 'object') {
|
|
189
189
|
const chat = d.chat;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
// `history` may itself be the message LIST (an array), not only an
|
|
191
|
+
// object with a `messages` field: the array form was read as
|
|
192
|
+
// `history.messages` -> undefined and the whole restored dialogue came
|
|
193
|
+
// back EMPTY even though the payload carried it.
|
|
194
|
+
const hist = chat.history;
|
|
195
|
+
if (Array.isArray(hist))
|
|
196
|
+
arr = hist;
|
|
197
|
+
else if (hist && typeof hist === 'object') {
|
|
198
|
+
const h = hist;
|
|
199
|
+
if (h.messages)
|
|
200
|
+
arr = asArr(h.messages);
|
|
201
|
+
}
|
|
202
|
+
if (!arr && chat.messages)
|
|
196
203
|
arr = asArr(chat.messages);
|
|
197
204
|
}
|
|
198
205
|
}
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -97,6 +97,12 @@ function pickAnswer(node) {
|
|
|
97
97
|
if (typeof node !== 'object')
|
|
98
98
|
return '';
|
|
99
99
|
const obj = node;
|
|
100
|
+
// A plain-JSON reply can carry the SAME phase marker as an SSE delta
|
|
101
|
+
// (`thinking_summary`). The reasoning guard used to live only in the
|
|
102
|
+
// choices[] branch, so a top-level `{phase:"thinking_summary",content}`
|
|
103
|
+
// (or the same object one envelope deeper) was returned as the ANSWER.
|
|
104
|
+
if (isThinkingPhase(obj))
|
|
105
|
+
return '';
|
|
100
106
|
if (Array.isArray(obj.choices)) {
|
|
101
107
|
let out = '';
|
|
102
108
|
for (const ch of obj.choices) {
|
|
@@ -220,13 +226,14 @@ export function isRateLimitText(body) {
|
|
|
220
226
|
const b = String(body || '');
|
|
221
227
|
if (!b)
|
|
222
228
|
return false;
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
229
|
+
const errNotice = /event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b);
|
|
230
|
+
// The `quota_limit` CODE only means something in an ERROR payload: a normal
|
|
231
|
+
// ANSWER may MENTION `quota_limit` (the operator asked how to handle that
|
|
232
|
+
// API error), and this check used to run BEFORE the answer-content guard, so
|
|
233
|
+
// such an answer made ask() back off and re-send a good turn.
|
|
234
|
+
if (/quota_limit/i.test(b) && (errNotice || !hasAnswerContent(b)))
|
|
228
235
|
return true;
|
|
229
|
-
if (
|
|
236
|
+
if (errNotice) {
|
|
230
237
|
return /too many requests|rate limit|too frequent|please try again later|high demand|слишком часто/i.test(b);
|
|
231
238
|
}
|
|
232
239
|
// No structured error marker: only treat the body as a notice when it has
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { translate } from './i18n.js';
|
|
2
|
+
// Pure half of the session commands (/sessions, /resume-id). The resume itself
|
|
3
|
+
// drives a browser; deciding WHICH session may be opened — the filter, the id
|
|
4
|
+
// normalization, a foreign-provider refusal — is pure and lives here.
|
|
5
|
+
/** Parse the text after `/resume-id` into a chat id, or null when empty. */
|
|
6
|
+
export function parseResumeId(arg) {
|
|
7
|
+
const id = String(arg ?? '').trim();
|
|
8
|
+
return id ? id : null;
|
|
9
|
+
}
|
|
10
|
+
/** Keep the sessions matching a `/sessions <query>` filter (title or workdir). */
|
|
11
|
+
export function filterSessions(sessions, filter) {
|
|
12
|
+
const q = String(filter ?? '')
|
|
13
|
+
.trim()
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
if (!q)
|
|
16
|
+
return sessions.slice();
|
|
17
|
+
return sessions.filter((s) => (s.title || '').toLowerCase().includes(q) ||
|
|
18
|
+
(s.workdir || '').toLowerCase().includes(q));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A chat id only exists on the backend that created it, so opening a session
|
|
22
|
+
* saved by ANOTHER provider would fail inside the browser with a confusing
|
|
23
|
+
* error. Old files carry no `provider` field and belong to DeepSeek (the only
|
|
24
|
+
* backend that existed then).
|
|
25
|
+
*
|
|
26
|
+
* Returns a user-facing refusal, or null when the session may be opened.
|
|
27
|
+
*/
|
|
28
|
+
export function resumeProviderRefusal(session, activeProvider, t = translate('en')) {
|
|
29
|
+
if (!session)
|
|
30
|
+
return null;
|
|
31
|
+
const owner = session.provider || 'deepseek';
|
|
32
|
+
if (owner === activeProvider)
|
|
33
|
+
return null;
|
|
34
|
+
return t('sessions.provider_mismatch', {
|
|
35
|
+
owner,
|
|
36
|
+
active: activeProvider,
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/send-telemetry.js
CHANGED
|
@@ -43,7 +43,13 @@ export function parseSendEvents(raw) {
|
|
|
43
43
|
export function summarizeSends(events, now = Date.now()) {
|
|
44
44
|
const within = (ms) => events.filter((e) => {
|
|
45
45
|
const t = Date.parse(e.at);
|
|
46
|
-
|
|
46
|
+
// The event must be in the PAST: a future stamp (clock skew on the
|
|
47
|
+
// machine that wrote the log, a hand-edited line) satisfies
|
|
48
|
+
// `now - t <= ms` for EVERY window and inflated all three counters.
|
|
49
|
+
if (!Number.isFinite(t))
|
|
50
|
+
return false;
|
|
51
|
+
const age = now - t;
|
|
52
|
+
return age >= 0 && age <= ms;
|
|
47
53
|
}).length;
|
|
48
54
|
// `blockMs` may arrive as a NUMBER (the writer) or a string (an older
|
|
49
55
|
// build, a hand-edited file): a numeric string is accepted so the average
|
|
@@ -62,9 +68,15 @@ export function summarizeSends(events, now = Date.now()) {
|
|
|
62
68
|
avgBlockMs,
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
|
-
/** `~/.zames/logs/sends-<provider>.jsonl`.
|
|
71
|
+
/** `~/.zames/logs/sends-<provider>.jsonl`. The provider id comes from the
|
|
72
|
+
* CONFIG (a hand-edited file, `/provider`), so it must not be able to escape
|
|
73
|
+
* the log directory: a separator or `..` used to build a path OUTSIDE
|
|
74
|
+
* `~/.zames/logs` (a subdirectory at best, another file at worst — and the
|
|
75
|
+
* rotation DELETES files it finds there). Everything outside a plain file-name
|
|
76
|
+
* alphabet is replaced with `_`. */
|
|
66
77
|
export function sendLogPath(logsDir, provider) {
|
|
67
|
-
|
|
78
|
+
const safe = String(provider || 'unknown').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
79
|
+
return path.join(logsDir, 'sends-' + safe + '.jsonl');
|
|
68
80
|
}
|
|
69
81
|
/** Keep at most `keep` sends-*.jsonl files (rotate the oldest away). */
|
|
70
82
|
export function rotateSendLogs(logsDir, keep = 5) {
|
package/dist/send-throttle.js
CHANGED
|
@@ -87,7 +87,9 @@ export function applySendEvent(state, event, cfg, now = Date.now()) {
|
|
|
87
87
|
next.blockedAt = state.blocked ? state.blockedAt || stamp : stamp;
|
|
88
88
|
next.okStreak = 0;
|
|
89
89
|
next.forcedWaitMs =
|
|
90
|
-
typeof event.retryAfterMs === 'number' &&
|
|
90
|
+
typeof event.retryAfterMs === 'number' &&
|
|
91
|
+
Number.isFinite(event.retryAfterMs) &&
|
|
92
|
+
event.retryAfterMs > 0
|
|
91
93
|
? event.retryAfterMs
|
|
92
94
|
: cfg.rateLimitWaitMs;
|
|
93
95
|
next.forcedWaitAt = now;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { translate } from './i18n.js';
|
|
2
|
+
/** Render `/status` as an array of lines (already localized). */
|
|
3
|
+
export function formatStatus(s, t = translate('en')) {
|
|
4
|
+
const yes = t('common.yes');
|
|
5
|
+
const no = t('common.no');
|
|
6
|
+
const none = t('common.none');
|
|
7
|
+
const onoff = (b) => (b ? t('common.on') : t('common.off'));
|
|
8
|
+
const lines = [];
|
|
9
|
+
lines.push(t('status.workdir', { v: s.workdir }));
|
|
10
|
+
lines.push(t('status.review_mode', { v: s.reviewMode || t('status.review_none') }));
|
|
11
|
+
if (s.reviewMode && s.origDir) {
|
|
12
|
+
lines.push(t('status.orig_dir', { v: s.origDir }));
|
|
13
|
+
}
|
|
14
|
+
lines.push(t('status.chat', { v: s.chatId || none }));
|
|
15
|
+
lines.push(t('status.fresh_next', { v: s.freshNext ? yes : no }));
|
|
16
|
+
lines.push(t('status.prompt_next', { v: s.promptNext ? yes : no }));
|
|
17
|
+
lines.push(t('status.last_chat', { v: s.lastChat || none }));
|
|
18
|
+
lines.push(t('status.sessions', { v: s.sessionsDir }));
|
|
19
|
+
lines.push(t('status.dev', { v: s.devMode ? t('common.on') : t('common.off') }));
|
|
20
|
+
lines.push(t('status.max_iter', { v: s.maxIterations }));
|
|
21
|
+
lines.push(t('status.headless', { v: s.headless ? yes : no }));
|
|
22
|
+
lines.push(t('status.debug', { v: s.debug ? yes : no }));
|
|
23
|
+
lines.push(t('status.toggles', {
|
|
24
|
+
think: s.deepThinking ? yes : no,
|
|
25
|
+
search: s.webSearch ? yes : no,
|
|
26
|
+
}));
|
|
27
|
+
lines.push(t('status.undo', { v: onoff(s.undo) }));
|
|
28
|
+
lines.push(t('status.checkpoint', { v: onoff(s.checkpoint) }));
|
|
29
|
+
lines.push(t('status.transcript', { v: s.transcriptFile || t('common.off') }));
|
|
30
|
+
lines.push(t('status.goal', { v: s.goal || none }));
|
|
31
|
+
lines.push(t('status.locale', { v: s.localeName }));
|
|
32
|
+
lines.push(t('status.mcp', { v: s.mcpToolCount === null ? none : s.mcpToolCount }));
|
|
33
|
+
lines.push(t('status.tokens', {
|
|
34
|
+
v: s.tokenUsage === null ? t('common.unknown') : s.tokenUsage,
|
|
35
|
+
}));
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The `/doctor` rows that depend on a LIVE PROBE rather than on the config:
|
|
40
|
+
* git availability + branch, clipboard, MCP, the auth marker and the UI health
|
|
41
|
+
* result. `renderDoctor` (commands.ts) renders the full table from these.
|
|
42
|
+
*/
|
|
43
|
+
export function doctorProbeSummary(p) {
|
|
44
|
+
const rows = [];
|
|
45
|
+
rows.push({
|
|
46
|
+
status: p.gitOk ? 'ok' : 'warn',
|
|
47
|
+
label: p.gitOk ? 'git: ' + (p.gitBranch || 'detached') : 'git: absent',
|
|
48
|
+
});
|
|
49
|
+
rows.push({
|
|
50
|
+
status: p.authSaved ? 'ok' : 'warn',
|
|
51
|
+
label: p.authSaved ? 'auth: saved' : 'auth: not saved',
|
|
52
|
+
});
|
|
53
|
+
rows.push({
|
|
54
|
+
status: p.mcpTools > 0 ? 'ok' : 'warn',
|
|
55
|
+
label: 'mcp: ' +
|
|
56
|
+
String(p.mcpServers) +
|
|
57
|
+
' servers / ' +
|
|
58
|
+
String(p.mcpTools) +
|
|
59
|
+
' tools',
|
|
60
|
+
});
|
|
61
|
+
if (p.uiMissingCritical && p.uiMissingCritical.length) {
|
|
62
|
+
rows.push({
|
|
63
|
+
status: 'warn',
|
|
64
|
+
label: 'ui: missing ' + p.uiMissingCritical.join(', '),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return rows;
|
|
68
|
+
}
|
package/dist/tool-summary.js
CHANGED
|
@@ -101,5 +101,12 @@ export function looksLikeToolError(result) {
|
|
|
101
101
|
head.startsWith('subagent failed') ||
|
|
102
102
|
head.startsWith('subagent error') ||
|
|
103
103
|
head.includes('process killed') ||
|
|
104
|
-
head.startsWith('(aborted by the operator)')
|
|
104
|
+
head.startsWith('(aborted by the operator)') ||
|
|
105
|
+
// A SANDBOX refusal: `safePath` throws
|
|
106
|
+
// `Access outside the working directory is forbidden: …` and the shell
|
|
107
|
+
// guard throws `Sandbox: …` (eval+cd, an unresolvable cd target, chroot).
|
|
108
|
+
// Both mean the call did NOTHING, yet neither prefix matched and the
|
|
109
|
+
// operator saw a green check next to a refused call.
|
|
110
|
+
head.includes('outside the working directory is forbidden') ||
|
|
111
|
+
head.includes('sandbox:'));
|
|
105
112
|
}
|
package/dist/tools.js
CHANGED
|
@@ -417,7 +417,10 @@ export function filterToolsForReadOnly(tools) {
|
|
|
417
417
|
export function filterToolsByAllowList(tools, allowed) {
|
|
418
418
|
if (!allowed || !allowed.length)
|
|
419
419
|
return tools;
|
|
420
|
-
|
|
420
|
+
// `allowed-tools:` is human-written: an entry may carry surrounding spaces
|
|
421
|
+
// (a quoted value, a trailing comma). Lower-casing alone left `read `, which
|
|
422
|
+
// matched nothing and silently reduced the skill to `respond` only.
|
|
423
|
+
const allow = new Set(allowed.map((n) => String(n).trim().toLowerCase()).filter(Boolean));
|
|
421
424
|
allow.add('respond');
|
|
422
425
|
return tools.filter((t) => allow.has(t.name.toLowerCase()));
|
|
423
426
|
}
|
package/dist/tui/log-window.js
CHANGED
|
@@ -20,6 +20,26 @@ function visWidth(s) {
|
|
|
20
20
|
i++;
|
|
21
21
|
i++;
|
|
22
22
|
}
|
|
23
|
+
else if (s[i] === ']') {
|
|
24
|
+
// OSC sequence: `ESC ] ... (BEL | ESC \)`. OSC-8 hyperlinks
|
|
25
|
+
// (produced by renderMarkdown when the terminal supports them) wrap
|
|
26
|
+
// the LABEL in such a sequence, and the OLD parser did not skip it:
|
|
27
|
+
// every URL character counted as a visible column, so the viewport
|
|
28
|
+
// believed a short link line filled many rows and the log jumped.
|
|
29
|
+
i++;
|
|
30
|
+
while (i < s.length) {
|
|
31
|
+
const c = s.charCodeAt(i);
|
|
32
|
+
if (c === 7) {
|
|
33
|
+
i++;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (c === 27 && s[i + 1] === String.fromCharCode(92)) {
|
|
37
|
+
i += 2;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
i++;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
23
43
|
continue;
|
|
24
44
|
}
|
|
25
45
|
const cp = s.codePointAt(i);
|
|
@@ -73,21 +93,38 @@ export function windowByRows(entries, height, scroll, cols) {
|
|
|
73
93
|
let skip = Math.max(0, Math.floor(scroll));
|
|
74
94
|
const picked = [];
|
|
75
95
|
let used = 0;
|
|
96
|
+
// The pane above the panel is flexGrow:1 and overflow="hidden" with
|
|
97
|
+
// justifyContent="flex-end", so only the TAIL of a too-tall entry stays
|
|
98
|
+
// visible. `used` must therefore count the VISIBLE rows of the entry being
|
|
99
|
+
// kept, not its full height: counting the full height while scrolling
|
|
100
|
+
// INSIDE a tall entry exceeded the reserved height and pushed the status /
|
|
101
|
+
// input rows off a short terminal (the caret then drifted under the box).
|
|
76
102
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
77
103
|
const r = entryRows(entries[i], cols);
|
|
104
|
+
let visibleRows = r;
|
|
78
105
|
if (skip > 0) {
|
|
79
106
|
if (skip >= r) {
|
|
80
107
|
skip -= r;
|
|
81
108
|
continue;
|
|
82
109
|
}
|
|
83
|
-
// Partially scrolled into this entry:
|
|
110
|
+
// Partially scrolled into this entry: the rows above the fold are
|
|
111
|
+
// hidden and must not be counted against the viewport.
|
|
112
|
+
visibleRows = r - skip;
|
|
84
113
|
skip = 0;
|
|
85
114
|
}
|
|
86
|
-
|
|
115
|
+
// The pane is flexGrow:1 + overflow="hidden" + flex-end, so a tall entry
|
|
116
|
+
// is CLIPPED to its tail and never grows the window. A clipped entry is
|
|
117
|
+
// therefore always kept — dropping it blanked the pane while the operator
|
|
118
|
+
// scrolled inside a long answer (the old code required a free row for it).
|
|
119
|
+
const clipped = r > cap || visibleRows < r;
|
|
120
|
+
if (!clipped && used + visibleRows > cap)
|
|
87
121
|
break;
|
|
88
122
|
picked.push(entries[i]);
|
|
89
|
-
used +=
|
|
90
|
-
|
|
123
|
+
used += visibleRows;
|
|
124
|
+
// Only an EXACT fill stops the walk. A clipped entry already fills the
|
|
125
|
+
// viewport visually but leaves room in the budget for the shorter entries
|
|
126
|
+
// above it, which used to be dropped right after a tall one.
|
|
127
|
+
if (!clipped && used >= cap)
|
|
91
128
|
break;
|
|
92
129
|
}
|
|
93
130
|
picked.reverse();
|
package/dist/undo.js
CHANGED
|
@@ -17,7 +17,17 @@ export class UndoStore {
|
|
|
17
17
|
}
|
|
18
18
|
async _readIndex() {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const data = JSON.parse(await fs.readFile(INDEX, 'utf-8'));
|
|
21
|
+
// The index is a JSON FILE: a crash, a hand edit or an old format can
|
|
22
|
+
// leave VALID JSON that is not an array (`{}`, `null`, `"x"`, `42`),
|
|
23
|
+
// or an array with junk entries. Returning it as-is made /undo throw
|
|
24
|
+
// `history.pop is not a function` and /undo-list throw
|
|
25
|
+
// `history.slice is not a function` out of the command dispatcher.
|
|
26
|
+
if (!Array.isArray(data))
|
|
27
|
+
return [];
|
|
28
|
+
return data.filter((r) => !!r &&
|
|
29
|
+
typeof r === 'object' &&
|
|
30
|
+
typeof r.originalPath === 'string');
|
|
21
31
|
}
|
|
22
32
|
catch {
|
|
23
33
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.82.
|
|
3
|
+
"version": "2.82.50",
|
|
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": {
|