zames_pro 2.90.16 → 2.90.18
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 +28 -0
- package/dist/browser-chats.js +105 -64
- package/dist/browser-pure.js +12 -0
- package/dist/browser.js +115 -25
- package/dist/commands.js +1 -178
- package/dist/compact.js +42 -4
- package/dist/deepseek-ui.js +43 -7
- package/dist/glm-browser.js +14 -0
- package/dist/glm-chats.js +13 -1
- package/dist/glm-ui.js +4 -1
- package/dist/index.js +10 -6
- package/dist/net-capture.js +65 -7
- package/dist/qwen-browser.js +11 -0
- package/dist/qwen-chats.js +13 -1
- package/dist/qwen-net-capture.js +7 -1
- package/dist/qwen-ui.js +8 -1
- package/dist/restore-history.js +304 -0
- package/dist/tui/TuiEditor.js +8 -0
- package/package.json +1 -1
package/dist/qwen-browser.js
CHANGED
|
@@ -57,6 +57,12 @@ function capNetBody(body) {
|
|
|
57
57
|
return body.slice(0, head) + '\n...[truncated]...\n' + body.slice(-1024);
|
|
58
58
|
}
|
|
59
59
|
export class QwenBrowser {
|
|
60
|
+
/**
|
|
61
|
+
* True while a CONTEXT-LIMIT compaction runs in a fresh chat: that chat
|
|
62
|
+
* starts EMPTY (no system prompt yet), so a COMPACT prompt is tiny and must
|
|
63
|
+
* NOT be refused with a rate limit by the adaptive throttle.
|
|
64
|
+
*/
|
|
65
|
+
compactInProgress = false;
|
|
60
66
|
providerName = 'qwen';
|
|
61
67
|
/** The chat a send must join (set by the agent loop; see types.ts). */
|
|
62
68
|
resumeChatId = null;
|
|
@@ -122,6 +128,7 @@ export class QwenBrowser {
|
|
|
122
128
|
_appliedThinking;
|
|
123
129
|
_appliedWebSearch;
|
|
124
130
|
_apiAuth;
|
|
131
|
+
/** Last error from fetchChatMessages ('' on success). */
|
|
125
132
|
_lastHistoryError;
|
|
126
133
|
_netBody;
|
|
127
134
|
_netHookInstalled;
|
|
@@ -1650,6 +1657,10 @@ export class QwenBrowser {
|
|
|
1650
1657
|
async _waitForSendSlot(agent) {
|
|
1651
1658
|
if (!agent)
|
|
1652
1659
|
return;
|
|
1660
|
+
// A COMPACTION send goes into a FRESH chat with a tiny prompt; the
|
|
1661
|
+
// fixed gap learned in the OLD chat must not delay the recovery.
|
|
1662
|
+
if (this.compactInProgress)
|
|
1663
|
+
return;
|
|
1653
1664
|
if (!this._lastSentAt)
|
|
1654
1665
|
return;
|
|
1655
1666
|
const gap = this.sendIntervalMs() - (Date.now() - this._lastSentAt);
|
package/dist/qwen-chats.js
CHANGED
|
@@ -71,13 +71,25 @@ export function scrapeQwenChatMessages(opts) {
|
|
|
71
71
|
const uniq = Array.from(new Set(containers));
|
|
72
72
|
// Keep only the OUTERMOST block of a nested pair.
|
|
73
73
|
const blocks = uniq.filter((b) => !uniq.some((o) => o !== b && o.contains(b)));
|
|
74
|
+
// A REASONING block is UI state, not the answer. It is not always a PARENT
|
|
75
|
+
// of the text (a provider may render it as a sibling of the answer), so the
|
|
76
|
+
// message text is read from a CLONE with the reasoning subtrees removed —
|
|
77
|
+
// otherwise the restored dialogue printed the whole THINK phase.
|
|
78
|
+
const contentOf = (e) => {
|
|
79
|
+
const clone = e.cloneNode(true);
|
|
80
|
+
for (const t of Array.from(clone.querySelectorAll('*'))) {
|
|
81
|
+
if (thinkRe.test((t.className || '').toString()))
|
|
82
|
+
t.remove();
|
|
83
|
+
}
|
|
84
|
+
return (clone.textContent || '').trim();
|
|
85
|
+
};
|
|
74
86
|
const out = [];
|
|
75
87
|
for (const c of blocks) {
|
|
76
88
|
let node = c.querySelector('[class*="markdown"], [class*="answer"]');
|
|
77
89
|
const isAssistant = assistantRe.test(c.className || '') || !!node;
|
|
78
90
|
if (!node)
|
|
79
91
|
node = c;
|
|
80
|
-
const text = (node
|
|
92
|
+
const text = contentOf(node);
|
|
81
93
|
if (!text)
|
|
82
94
|
continue;
|
|
83
95
|
out.push({ role: isAssistant ? 'assistant' : 'user', text });
|
package/dist/qwen-net-capture.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SERVER_BUSY_RE } from './qwen-ui.js';
|
|
1
2
|
// Extraction of the RAW model answer from Qwen Chat's (chat.qwen.ai) network
|
|
2
3
|
// data. The DOM distorts the answer (markdown/LaTeX/link auto-detection), so
|
|
3
4
|
// — exactly like the DeepSeek path (net-capture.ts) — the answer the agent
|
|
@@ -316,7 +317,12 @@ export function isServerBusyText(body) {
|
|
|
316
317
|
const errNotice = /event:\s*error|"type"\s*:\s*"error"|"error"\s*:/i.test(b);
|
|
317
318
|
if (!errNotice && hasAnswerContent(b))
|
|
318
319
|
return false;
|
|
319
|
-
|
|
320
|
+
// A bare notice without JSON framing (`Service unavailable`) is still a
|
|
321
|
+
// notice; a sentence that merely mentions one is not (SERVER_BUSY_RE needs
|
|
322
|
+
// the clause to start at the beginning / after punctuation / after a JSON
|
|
323
|
+
// key).
|
|
324
|
+
return (SERVER_BUSY_RE.test(b) ||
|
|
325
|
+
/^(?:please )?try again later[.!]?$/i.test(b.trim()));
|
|
320
326
|
}
|
|
321
327
|
// The chat's CONTEXT WINDOW is exhausted. Qwen refuses the turn and no retry
|
|
322
328
|
// in the same chat can succeed - the only way out is a compaction into a
|
package/dist/qwen-ui.js
CHANGED
|
@@ -171,7 +171,14 @@ export const LOGIN_SELECTORS = [
|
|
|
171
171
|
// ---------- service statuses / errors ----------
|
|
172
172
|
// Qwen's rate-limit / busy notices (best-effort; the toast may be short-lived).
|
|
173
173
|
export const RATE_LIMIT_RE = /too many requests|rate limit|too frequent|please try again later|слишком часто/i;
|
|
174
|
-
|
|
174
|
+
// A notice, not a phrase inside the answer (same lesson as the DeepSeek
|
|
175
|
+
// pattern): the classifier also requires an error payload or an answer-less
|
|
176
|
+
// body, and the phrase must stand alone.
|
|
177
|
+
// A NOTICE, not a phrase inside a reply: the phrase must start a clause
|
|
178
|
+
// (start of the text, after a newline/punctuation/colon, or right after a
|
|
179
|
+
// `"content":"` / `"error":"` JSON key) and end one. A normal answer that
|
|
180
|
+
// says "try again later" therefore does not look like a dead server.
|
|
181
|
+
export const SERVER_BUSY_RE = /(?:^|[\n!?:]\s*|\.\s+|"(?:content|error)"\s*:\s*")(?:server is busy|service unavailable|try again later)(?=[.!,]|"|$)/im;
|
|
175
182
|
// ---------- toggles (thinking mode / web search) ----------
|
|
176
183
|
// Qwen exposes thinking as a MODE dropdown (`.qwen-thinking-selector`, label
|
|
177
184
|
// "Auto" by default) with options Auto / Thinking / Fast — NOT a boolean
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// The RESTORED-DIALOGUE layer: what the operator sees when a chat is
|
|
2
|
+
// reopened (/resume, /resume-id, startup). Every provider's stored history is
|
|
3
|
+
// raw protocol text — the system prompt, tool results, tool-call JSON, the
|
|
4
|
+
// model's safety block — so a restore must KEEP only the operator's own turns
|
|
5
|
+
// and the agent's `respond` message. The formatting used to live inside the
|
|
6
|
+
// DeepSeek DOM scraper, which is why only DeepSeek produced a readable
|
|
7
|
+
// dialogue: Qwen/GLM store the SAME raw text through the same endpoints, so
|
|
8
|
+
// normalization and truncation moved HERE, where every provider shares them.
|
|
9
|
+
import { parseToolCall } from './answer-parse.js';
|
|
10
|
+
const NL = String.fromCharCode(10);
|
|
11
|
+
/** How many messages of the dialogue are printed by default. */
|
|
12
|
+
export const RESTORED_HISTORY_LIMIT = 20;
|
|
13
|
+
/**
|
|
14
|
+
* Longest restored message KEPT for display. The system prompt (leaked into
|
|
15
|
+
* history) is far bigger and is filtered by the noise checks below; a real
|
|
16
|
+
* answer above this size is shortened instead of being dropped.
|
|
17
|
+
*/
|
|
18
|
+
const RESTORED_MESSAGE_MAX = 100_000;
|
|
19
|
+
/**
|
|
20
|
+
* DeepSeek stores the assistant turn VERBATIM: either a raw tool-call JSON
|
|
21
|
+
* ({"tool":"Read","args":{...}}, frequently dirty/truncated) or a `respond`
|
|
22
|
+
* call that carries the real message for the operator
|
|
23
|
+
* ({"tool":"respond","args":{"message":"..."}}). The operator only wants the
|
|
24
|
+
* `respond` message (and plain-text answers) — a tool-call is protocol noise.
|
|
25
|
+
*
|
|
26
|
+
* Returns the tool name and, for `respond`, the unwrapped message. Tolerant on
|
|
27
|
+
* purpose: the stored JSON is often dirty (a stray `}}`, a missing "args"
|
|
28
|
+
* wrapper), so a bare JSON.parse is not enough.
|
|
29
|
+
*/
|
|
30
|
+
export function parseAssistantToolCall(text) {
|
|
31
|
+
const t = String(text || '').trim();
|
|
32
|
+
if (!t)
|
|
33
|
+
return null;
|
|
34
|
+
// Broken heads like `<|tool": ...` or `tool": "Bash"` are tolerated.
|
|
35
|
+
const m = t.match(/tool"?\s*:\s*"([^"\s]+)"/);
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
const tool = m[1];
|
|
39
|
+
if (tool !== 'respond')
|
|
40
|
+
return { tool };
|
|
41
|
+
const greedy = t.match(/"message"\s*:\s*"([\s\S]*)"\s*\}*\s*$/);
|
|
42
|
+
let raw = greedy ? greedy[1] : '';
|
|
43
|
+
if (!greedy) {
|
|
44
|
+
// The value is not a quoted string (a number, a boolean, an array, a
|
|
45
|
+
// nested object): there is no operator-facing MESSAGE here. The old
|
|
46
|
+
// fallback sliced the raw text from the key and the restored turn printed
|
|
47
|
+
// `:42}}` / `:[1,2]}}` as if it were the model's answer.
|
|
48
|
+
const value = t.slice(t.indexOf('"message"') + 9).replace(/^\s*:\s*/, '');
|
|
49
|
+
if (!value.startsWith('"'))
|
|
50
|
+
return { tool, message: '' };
|
|
51
|
+
raw = value.replace(/^"/, '');
|
|
52
|
+
}
|
|
53
|
+
return { tool, message: unescapeJsonString(raw) };
|
|
54
|
+
}
|
|
55
|
+
/** Decode the JSON escape sequences that appear inside a captured string. */
|
|
56
|
+
export function unescapeJsonString(s) {
|
|
57
|
+
// A `\uD800`-style escape that is NOT followed by a low surrogate (a
|
|
58
|
+
// truncated or hand-written payload) would become a LONE surrogate - a
|
|
59
|
+
// broken code point the restored dialogue prints as `?`. Decode a valid PAIR
|
|
60
|
+
// as one code point and drop an unpaired half.
|
|
61
|
+
let pendingHigh = '';
|
|
62
|
+
return String(s || '')
|
|
63
|
+
.replace(/\\u([0-9a-fA-F]{4})/g, (_x, h) => {
|
|
64
|
+
const cp = parseInt(h, 16);
|
|
65
|
+
if (cp >= 0xd800 && cp <= 0xdbff) {
|
|
66
|
+
pendingHigh = String.fromCharCode(cp);
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
if (cp >= 0xdc00 && cp <= 0xdfff) {
|
|
70
|
+
const high = pendingHigh;
|
|
71
|
+
pendingHigh = '';
|
|
72
|
+
return high ? high + String.fromCharCode(cp) : '';
|
|
73
|
+
}
|
|
74
|
+
pendingHigh = '';
|
|
75
|
+
return String.fromCharCode(cp);
|
|
76
|
+
})
|
|
77
|
+
.replace(/\\n/g, NL)
|
|
78
|
+
.replace(/\\r/g, String.fromCharCode(13))
|
|
79
|
+
.replace(/\\t/g, String.fromCharCode(9))
|
|
80
|
+
.replace(/\\"/g, '"')
|
|
81
|
+
.replace(/\\\\/g, '\\');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalize one restored message for display:
|
|
85
|
+
* - USER: drop the protocol noise (system-prompt, tool results, nudges) — it
|
|
86
|
+
* is not the operator's words.
|
|
87
|
+
* - ASSISTANT: unwrap a `respond` call to its message; drop any other
|
|
88
|
+
* tool-call (protocol noise); keep plain-text answers.
|
|
89
|
+
* Returns null when the message must not be shown.
|
|
90
|
+
*/
|
|
91
|
+
export function normalizeRestoredMessage(m) {
|
|
92
|
+
let text = String((m && m.text) || '').trim();
|
|
93
|
+
if (!text)
|
|
94
|
+
return null;
|
|
95
|
+
// The size cap exists to keep a leaked SYSTEM PROMPT (or another giant
|
|
96
|
+
// protocol blob) out of the restored dialogue. It must NOT drop a real
|
|
97
|
+
// answer: a long report/diff/document easily exceeds the cap, and dropping
|
|
98
|
+
// it silently left the operator with a dialogue where the agent's own reply
|
|
99
|
+
// was missing. Keep such a message but shorten it for display.
|
|
100
|
+
if (text.length > RESTORED_MESSAGE_MAX) {
|
|
101
|
+
// Never split a surrogate PAIR: the cut is in UTF-16 units, so a high half
|
|
102
|
+
// alone would reach the terminal as a broken code point (same rule as
|
|
103
|
+
// truncateToolResult / capReport / cutHookText).
|
|
104
|
+
let cut = RESTORED_MESSAGE_MAX;
|
|
105
|
+
const last = text.charCodeAt(cut - 1);
|
|
106
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
107
|
+
cut--;
|
|
108
|
+
text = text.slice(0, cut) + '…';
|
|
109
|
+
}
|
|
110
|
+
if (m.role === 'user') {
|
|
111
|
+
if (/^Tool result for /.test(text))
|
|
112
|
+
return null;
|
|
113
|
+
if (/^You are a coding agent running in a terminal/.test(text))
|
|
114
|
+
return null;
|
|
115
|
+
if (/^You stopped after a tool result/.test(text))
|
|
116
|
+
return null;
|
|
117
|
+
// The agent loop's own watchdog nudge is protocol noise written by US, not
|
|
118
|
+
// by the operator: when an answer timed out the loop resends this text as
|
|
119
|
+
// the "user" message. It used to leak into the restored dialogue (a dozen
|
|
120
|
+
// copies after a flaky run), so drop it like the other nudges.
|
|
121
|
+
if (/^The previous turn was cut off/.test(text))
|
|
122
|
+
return null;
|
|
123
|
+
if (/^\[system\]/.test(text))
|
|
124
|
+
return null;
|
|
125
|
+
if (/^The user ran \//.test(text))
|
|
126
|
+
return null;
|
|
127
|
+
// `/compact` posts its summary as a USER turn, prefixed with a marker the
|
|
128
|
+
// loop writes (not the operator): a restored dialogue should show the
|
|
129
|
+
// TASK, not the mechanical handover.
|
|
130
|
+
if (/^Compact the conversation so far/.test(text))
|
|
131
|
+
return null;
|
|
132
|
+
if (/^This is the handover summary/.test(text))
|
|
133
|
+
return null;
|
|
134
|
+
const stripped = stripUserFraming(text);
|
|
135
|
+
if (!stripped)
|
|
136
|
+
return null;
|
|
137
|
+
return { role: 'user', text: stripped };
|
|
138
|
+
}
|
|
139
|
+
// Reasoning is UI state, not the answer.
|
|
140
|
+
text = stripThinkBlocks(text);
|
|
141
|
+
if (!text)
|
|
142
|
+
return null;
|
|
143
|
+
// Only a real DSML MARKER is protocol noise: a bare mention of the feature
|
|
144
|
+
// (an answer explaining the DSML parser) is a normal reply.
|
|
145
|
+
if (/[<|]\s*DSML\s*[>|]/i.test(text))
|
|
146
|
+
return null;
|
|
147
|
+
if (/^<system>/i.test(text))
|
|
148
|
+
return null;
|
|
149
|
+
// The model sometimes emits its own safety-classification block as a plain
|
|
150
|
+
// "answer"; it is not for the operator.
|
|
151
|
+
if (/<ds_safety>/i.test(text))
|
|
152
|
+
return null;
|
|
153
|
+
if (/^Safe$/i.test(text))
|
|
154
|
+
return null;
|
|
155
|
+
// EVERY format the parser knows, not just the legacy `{"tool":...}` head.
|
|
156
|
+
const generic = toolCallOf(text);
|
|
157
|
+
if (generic) {
|
|
158
|
+
if (generic.tool !== 'respond')
|
|
159
|
+
return null;
|
|
160
|
+
const msg = (generic.message || '').trim();
|
|
161
|
+
if (!msg)
|
|
162
|
+
return null;
|
|
163
|
+
if (/<\/?ds_safety\b/i.test(msg))
|
|
164
|
+
return null;
|
|
165
|
+
return { role: 'assistant', text: msg };
|
|
166
|
+
}
|
|
167
|
+
const legacy = parseAssistantToolCall(text);
|
|
168
|
+
if (legacy) {
|
|
169
|
+
if (legacy.tool !== 'respond')
|
|
170
|
+
return null;
|
|
171
|
+
const msg = (legacy.message || '').trim();
|
|
172
|
+
if (!msg)
|
|
173
|
+
return null;
|
|
174
|
+
if (/<\/?ds_safety\b/i.test(msg))
|
|
175
|
+
return null;
|
|
176
|
+
return { role: 'assistant', text: msg };
|
|
177
|
+
}
|
|
178
|
+
// A plain-text answer (written without the respond wrapper) — show as-is.
|
|
179
|
+
return { role: 'assistant', text };
|
|
180
|
+
}
|
|
181
|
+
/** Back-compat predicate: is the message worth showing at all? */
|
|
182
|
+
export function isDisplayableMessage(m) {
|
|
183
|
+
return normalizeRestoredMessage(m) !== null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Keep only the last `limit` messages, dropping protocol noise and UNWRAPPING
|
|
187
|
+
* the assistant `respond` calls to their operator-facing text.
|
|
188
|
+
*/
|
|
189
|
+
export function trimRestoredMessages(messages, limit = RESTORED_HISTORY_LIMIT) {
|
|
190
|
+
const clean = [];
|
|
191
|
+
for (const m of messages || []) {
|
|
192
|
+
if (!m || typeof m.text !== 'string')
|
|
193
|
+
continue;
|
|
194
|
+
const n = normalizeRestoredMessage({
|
|
195
|
+
role: m.role === 'user' ? 'user' : 'assistant',
|
|
196
|
+
text: String(m.text),
|
|
197
|
+
});
|
|
198
|
+
if (n)
|
|
199
|
+
clean.push(n);
|
|
200
|
+
}
|
|
201
|
+
if (limit > 0 && clean.length > limit)
|
|
202
|
+
return clean.slice(clean.length - limit);
|
|
203
|
+
return clean;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Render the restored dialogue as plain text lines: "❯ ..." for the operator,
|
|
207
|
+
* "● ..." for the agent. Markdown rendering is done by the caller (it needs
|
|
208
|
+
* the terminal width); this helper is pure and unit-tested.
|
|
209
|
+
*/
|
|
210
|
+
export function formatRestoredHistory(messages, opts = {}) {
|
|
211
|
+
const list = trimRestoredMessages(messages, opts.limit ?? RESTORED_HISTORY_LIMIT);
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const m of list) {
|
|
214
|
+
const marker = m.role === 'user' ? '❯ ' : '● ';
|
|
215
|
+
out.push(marker + m.text.trim());
|
|
216
|
+
}
|
|
217
|
+
return out.join(NL + NL);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Remove the reasoning a provider renders INTO the message text.
|
|
221
|
+
*
|
|
222
|
+
* Qwen/GLM expose the THINK phase as part of the turn, and a DeepSeek DOM
|
|
223
|
+
* scrape keeps the collapsible "Thought for N seconds" header after the
|
|
224
|
+
* reasoning subtree is stripped. Reasoning is NOT the answer, so a restored
|
|
225
|
+
* dialogue must not show it.
|
|
226
|
+
*/
|
|
227
|
+
export function stripThinkBlocks(text) {
|
|
228
|
+
let t = String(text || '');
|
|
229
|
+
// A fenced or tagged reasoning block, the shape every provider uses when it
|
|
230
|
+
// stores the phase inline.
|
|
231
|
+
t = t.replace(/<(think|thinking|reasoning)>[\s\S]*?<\/\1>\s*/gi, '');
|
|
232
|
+
t = t.replace(/```(?:think|thinking|reasoning)\s*[\s\S]*?```\s*/gi, '');
|
|
233
|
+
// "Thought for 1 second" / "Think for 12s" — a header LINE. Live DOM
|
|
234
|
+
// scrape: the header, then the answer ("Thought for 1 second" + NEWLINE +
|
|
235
|
+
// "OK2"); the reasoning BODY was already removed with its subtree.
|
|
236
|
+
// A single newline: with the blank-line form the `[\s\S]*?` below ate
|
|
237
|
+
// the whole answer and the restored turn vanished.
|
|
238
|
+
t = t.replace(/^\s*(?:thought|thinking|think)\s+for\s+[^\n]*\n?/i, '');
|
|
239
|
+
// The DOM may hand the header WITHOUT its newline ("Thought for 1
|
|
240
|
+
// secondOK2"): drop the header up to the first non-space run only when a
|
|
241
|
+
// line break or the end of the text follows it.
|
|
242
|
+
return t.trim();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Drop a provider's OWN framing from a user turn so the restored dialogue
|
|
246
|
+
* shows the operator's words: DeepSeek stores "You are a coding agent ..." as a
|
|
247
|
+
* USER message and prefixes a task with `<system>...</system>`.
|
|
248
|
+
*/
|
|
249
|
+
export function stripUserFraming(text) {
|
|
250
|
+
let t = String(text || '');
|
|
251
|
+
for (let i = 0; i < 3; i++) {
|
|
252
|
+
const next = t.replace(/^\s*<system>[\s\S]*?<\/system>\s*/i, '');
|
|
253
|
+
if (next === t)
|
|
254
|
+
break;
|
|
255
|
+
t = next;
|
|
256
|
+
}
|
|
257
|
+
return t.trim();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A turn that is a TOOL CALL (any provider, any format the parser knows) is
|
|
261
|
+
* protocol noise in the restored dialogue, EXCEPT a `respond` call, whose
|
|
262
|
+
* message is the agent's real answer.
|
|
263
|
+
*
|
|
264
|
+
* Returns the call when it is one, `null` otherwise, and the operator-facing
|
|
265
|
+
* message for a `respond`.
|
|
266
|
+
*/
|
|
267
|
+
export function toolCallOf(text) {
|
|
268
|
+
const t = String(text || '').trim();
|
|
269
|
+
if (!t)
|
|
270
|
+
return null;
|
|
271
|
+
// A real call starts with a bracket, a marker or the `tool` key; the strict
|
|
272
|
+
// parser is only consulted then, so ordinary prose is never re-parsed (and a
|
|
273
|
+
// bare "tool:" inside a report does not turn the report into a call).
|
|
274
|
+
if (!/^[\s\[\{<]/.test(t) && !/tool"?\s*:/.test(t.slice(0, 200))) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
let calls = [];
|
|
278
|
+
try {
|
|
279
|
+
const parsed = parseToolCall(t);
|
|
280
|
+
calls = (Array.isArray(parsed) ? parsed : parsed ? [parsed] : []);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (!calls.length)
|
|
286
|
+
return null;
|
|
287
|
+
const first = calls[0];
|
|
288
|
+
if (first.tool === 'respond') {
|
|
289
|
+
const msg = first.args?.message;
|
|
290
|
+
return { tool: 'respond', message: typeof msg === 'string' ? msg : '' };
|
|
291
|
+
}
|
|
292
|
+
return { tool: first.tool };
|
|
293
|
+
}
|
|
294
|
+
/** Convert a provider history to the shared restored-message shape. */
|
|
295
|
+
export function toRestoredMessages(messages) {
|
|
296
|
+
const out = [];
|
|
297
|
+
for (const m of messages || []) {
|
|
298
|
+
if (!m || typeof m.text !== 'string')
|
|
299
|
+
continue;
|
|
300
|
+
const role = m.role === 'user' ? 'user' : 'assistant';
|
|
301
|
+
out.push({ role, text: m.text });
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
package/dist/tui/TuiEditor.js
CHANGED
|
@@ -252,6 +252,14 @@ export class TuiEditor {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
// ---------- submission ----------
|
|
255
|
+
/**
|
|
256
|
+
* Submit a message as if the operator pressed Enter. The demo recorder
|
|
257
|
+
* (scripts/demo-replay.mts) needs the REAL entry path — the turn rule and
|
|
258
|
+
* the bar-marked rows — without a keypress.
|
|
259
|
+
*/
|
|
260
|
+
submitQuestion(text) {
|
|
261
|
+
this.handleSubmit(text, []);
|
|
262
|
+
}
|
|
255
263
|
handleSubmit(display, atts) {
|
|
256
264
|
const text = expandPastes(this.store.getSnapshot().pastes, display);
|
|
257
265
|
this.attachments.reset();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.90.
|
|
3
|
+
"version": "2.90.18",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
|