zames_pro 2.64.0 → 2.65.0
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 +46 -1
- package/README.md +16 -11
- package/dist/backlog.js +20 -0
- package/dist/browser.js +11 -0
- package/dist/commands.js +81 -0
- package/dist/config-menu.js +71 -33
- package/dist/i18n/common.js +362 -0
- package/dist/i18n/config.js +199 -0
- package/dist/i18n/deepseek.js +194 -0
- package/dist/i18n/doctor.js +127 -0
- package/dist/i18n/help.js +552 -0
- package/dist/i18n/mcp.js +41 -0
- package/dist/i18n/prompt.js +20 -0
- package/dist/i18n/self-review.js +198 -0
- package/dist/i18n/spinner.js +36 -0
- package/dist/i18n.js +21 -1724
- package/dist/index.js +39 -503
- package/dist/input/layout.js +292 -0
- package/dist/input.js +37 -290
- package/dist/run-task.js +451 -0
- package/package.json +2 -2
- package/scripts/render-demo.mjs +153 -0
- package/logo.jpg +0 -0
package/dist/run-task.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { theme } from './theme.js';
|
|
2
|
+
import { withGoal, mergeMessages, isSlashCommand, formatDuration, } from './commands.js';
|
|
3
|
+
import { formatCompactTokens, pasteReplacement, expandPastes } from './input.js';
|
|
4
|
+
// Ask the operator to approve a tool call flagged by an `ask` permission rule
|
|
5
|
+
// (C1). While the LineEditor owns the terminal it must be paused first, or the
|
|
6
|
+
// confirmation prompt and the input line fight for the same keys. A non-TTY run
|
|
7
|
+
// has no way to answer, so it DENIES (never silently allows a guarded call).
|
|
8
|
+
export async function askOperatorConfirm(editor, question) {
|
|
9
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
10
|
+
return false;
|
|
11
|
+
if (editor)
|
|
12
|
+
editor.pause();
|
|
13
|
+
try {
|
|
14
|
+
const a = await promptOnce(question + ' [y/N] ');
|
|
15
|
+
return /^(y|yes|\u0434|\u0434\u0430)$/i.test(a.trim());
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
if (editor)
|
|
22
|
+
editor.resume();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// A one-question raw-TTY line reader. It is deliberately SIMPLER than the
|
|
26
|
+
// full promptOnce the main loop uses for free-form input (no history, no
|
|
27
|
+
// editing, no multi-line paste): its only callers are y/N confirmations
|
|
28
|
+
// (permission ask, and the same shape used by /rewind in index.ts), so a
|
|
29
|
+
// single-line read is all that is needed. Lives here so runTask() and
|
|
30
|
+
// askOperatorConfirm() are self-contained.
|
|
31
|
+
async function promptOnce(question) {
|
|
32
|
+
const stdin = process.stdin;
|
|
33
|
+
const stdout = process.stdout;
|
|
34
|
+
// Non-TTY (pipe, redirect): read everything up to EOF as a single string.
|
|
35
|
+
if (!stdin.isTTY || !stdin.setRawMode) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
return await new Promise((resolve) => {
|
|
38
|
+
const onData = (b) => chunks.push(b);
|
|
39
|
+
const onEnd = () => {
|
|
40
|
+
stdin.removeListener('data', onData);
|
|
41
|
+
stdin.removeListener('end', onEnd);
|
|
42
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
43
|
+
};
|
|
44
|
+
stdin.on('data', onData);
|
|
45
|
+
stdin.on('end', onEnd);
|
|
46
|
+
stdin.resume();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const CR = String.fromCharCode(13);
|
|
50
|
+
const LF = String.fromCharCode(10);
|
|
51
|
+
const wasRaw = stdin.isRaw;
|
|
52
|
+
stdin.setRawMode(true);
|
|
53
|
+
stdin.resume();
|
|
54
|
+
stdout.write(question);
|
|
55
|
+
return await new Promise((resolve) => {
|
|
56
|
+
let line = '';
|
|
57
|
+
const onData = (buf) => {
|
|
58
|
+
const s = buf.toString('utf-8');
|
|
59
|
+
for (const ch of s) {
|
|
60
|
+
const code = ch.charCodeAt(0);
|
|
61
|
+
if (ch === CR || ch === LF) {
|
|
62
|
+
stdin.removeListener('data', onData);
|
|
63
|
+
if (stdin.setRawMode)
|
|
64
|
+
stdin.setRawMode(wasRaw || false);
|
|
65
|
+
stdout.write(String.fromCharCode(10));
|
|
66
|
+
resolve(line);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (code === 3) {
|
|
70
|
+
stdin.removeListener('data', onData);
|
|
71
|
+
if (stdin.setRawMode)
|
|
72
|
+
stdin.setRawMode(wasRaw || false);
|
|
73
|
+
resolve('');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (code === 127 || code === 8) {
|
|
77
|
+
if (line) {
|
|
78
|
+
line = line.slice(0, -1);
|
|
79
|
+
stdout.write(String.fromCharCode(8) + ' ' + String.fromCharCode(8));
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (code < 32)
|
|
84
|
+
continue;
|
|
85
|
+
line += ch;
|
|
86
|
+
stdout.write(ch);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
stdin.on('data', onData);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Watch the keyboard while the agent works in NON-TTY mode (pipes). In TTY the
|
|
93
|
+
// LineEditor owns this (index.ts). Input is buffered without a line editor: the
|
|
94
|
+
// typed text is shown in the spinner line via onChange -> ui.setPending().
|
|
95
|
+
// Enter sends the buffer to onQueue; an empty Enter is ignored. Backspace,
|
|
96
|
+
// Ctrl+U, Esc sequences (arrows/Home/End/Delete are ignored) and bracketed
|
|
97
|
+
// paste are supported. Returns a cleanup function.
|
|
98
|
+
export function watchInput({ onEscape, onChange, onQueue, } = {}) {
|
|
99
|
+
const stdin = process.stdin;
|
|
100
|
+
if (!stdin.isTTY || !stdin.setRawMode)
|
|
101
|
+
return () => { };
|
|
102
|
+
// Control characters are assembled from codes: this file must not contain
|
|
103
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
104
|
+
const ESC = String.fromCharCode(27);
|
|
105
|
+
const CSI = String.fromCharCode(91);
|
|
106
|
+
const CR = String.fromCharCode(13);
|
|
107
|
+
const LF = String.fromCharCode(10);
|
|
108
|
+
const wasRaw = stdin.isRaw;
|
|
109
|
+
stdin.setRawMode(true);
|
|
110
|
+
stdin.resume();
|
|
111
|
+
process.stdout.write(ESC + '[?2004h');
|
|
112
|
+
let buf = '';
|
|
113
|
+
let inPaste = false;
|
|
114
|
+
let pasteBuf = '';
|
|
115
|
+
const pastes = [];
|
|
116
|
+
const PASTE_START = ESC + '[200~';
|
|
117
|
+
const PASTE_END = ESC + '[201~';
|
|
118
|
+
const CSI_RE = new RegExp('^' + CSI + '[0-9;]*[A-Za-z~]');
|
|
119
|
+
const emitChange = () => {
|
|
120
|
+
if (onChange)
|
|
121
|
+
onChange(buf);
|
|
122
|
+
};
|
|
123
|
+
// Pasted text: small pastes (1-2 lines) are flattened into one line; large
|
|
124
|
+
// ones (3+ lines) are collapsed into "[Pasted lines#N]" (the original text is
|
|
125
|
+
// expanded back when the message is queued). pasteReplacement() owns the
|
|
126
|
+
// threshold, so the same rule the LineEditor uses applies here.
|
|
127
|
+
const insert = (text) => {
|
|
128
|
+
buf += text
|
|
129
|
+
.split(CR + LF)
|
|
130
|
+
.join(' ')
|
|
131
|
+
.split(CR)
|
|
132
|
+
.join(' ')
|
|
133
|
+
.split(LF)
|
|
134
|
+
.join(' ');
|
|
135
|
+
};
|
|
136
|
+
const insertPaste = (raw) => {
|
|
137
|
+
const rep = pasteReplacement(raw);
|
|
138
|
+
if (!rep) {
|
|
139
|
+
insert(raw);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
pastes.push(rep);
|
|
143
|
+
buf += rep.marker;
|
|
144
|
+
};
|
|
145
|
+
function onData(data) {
|
|
146
|
+
let s = data.toString('utf-8');
|
|
147
|
+
// A single Esc - abort generation. Arrows come as a whole chunk and do not
|
|
148
|
+
// reach here.
|
|
149
|
+
if (!inPaste && s === ESC) {
|
|
150
|
+
if (onEscape)
|
|
151
|
+
onEscape();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
while (s.length) {
|
|
155
|
+
if (inPaste) {
|
|
156
|
+
const end = s.indexOf(PASTE_END);
|
|
157
|
+
if (end === -1) {
|
|
158
|
+
pasteBuf += s;
|
|
159
|
+
s = '';
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
pasteBuf += s.slice(0, end);
|
|
163
|
+
s = s.slice(end + PASTE_END.length);
|
|
164
|
+
inPaste = false;
|
|
165
|
+
insertPaste(pasteBuf);
|
|
166
|
+
pasteBuf = '';
|
|
167
|
+
}
|
|
168
|
+
emitChange();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const start = s.indexOf(PASTE_START);
|
|
172
|
+
if (start !== -1) {
|
|
173
|
+
const before = s.slice(0, start);
|
|
174
|
+
s = s.slice(start + PASTE_START.length);
|
|
175
|
+
inPaste = true;
|
|
176
|
+
pasteBuf = '';
|
|
177
|
+
if (before) {
|
|
178
|
+
insert(before);
|
|
179
|
+
emitChange();
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const ch = s[0];
|
|
184
|
+
const code = s.charCodeAt(0);
|
|
185
|
+
s = s.slice(1);
|
|
186
|
+
if (ch === CR || ch === LF) {
|
|
187
|
+
const text = expandPastes(pastes, buf).trim();
|
|
188
|
+
buf = '';
|
|
189
|
+
pastes.length = 0;
|
|
190
|
+
emitChange();
|
|
191
|
+
if (text && onQueue)
|
|
192
|
+
onQueue(text);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (code === 3) {
|
|
196
|
+
// Ctrl+C - like Esc: abort generation.
|
|
197
|
+
if (onEscape)
|
|
198
|
+
onEscape();
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (code === 21) {
|
|
202
|
+
// Ctrl+U - clear what was typed.
|
|
203
|
+
buf = '';
|
|
204
|
+
pastes.length = 0;
|
|
205
|
+
emitChange();
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (code === 127 || code === 8) {
|
|
209
|
+
// Backspace.
|
|
210
|
+
if (buf) {
|
|
211
|
+
buf = buf.slice(0, -1);
|
|
212
|
+
emitChange();
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === ESC) {
|
|
217
|
+
// Escape sequence (arrows, Home/End, Delete) - skip.
|
|
218
|
+
const m = s.match(CSI_RE);
|
|
219
|
+
if (m)
|
|
220
|
+
s = s.slice(m[0].length);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (code < 32)
|
|
224
|
+
continue; // other control characters - ignore
|
|
225
|
+
buf += ch;
|
|
226
|
+
emitChange();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const handler = (data) => onData(data);
|
|
230
|
+
stdin.on('data', handler);
|
|
231
|
+
return () => {
|
|
232
|
+
stdin.removeListener('data', handler);
|
|
233
|
+
process.stdout.write(ESC + '[?2004l');
|
|
234
|
+
if (stdin.setRawMode)
|
|
235
|
+
stdin.setRawMode(wasRaw || false);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export async function runTask(deps, browser, tools, taskText, workdir, opts, attachments = []) {
|
|
239
|
+
const { t, locale, debug, maxIter, config, runAgentLoop, createSpinner } = deps;
|
|
240
|
+
const { transcript, freshChat, sendSystemPrompt, queue = [], ui: editor, onChatReady, onAutoCompact = null, autoCompactPct = 95, contextLimit = 1_000_000, getTokenUsage = null, goal = null, todosQuery = null, selfImprovement = false, } = opts;
|
|
241
|
+
// In TTY mode the UI is a LineEditor: it owns the input (queue, Esc,
|
|
242
|
+
// Ctrl+C) and draws the status ABOVE the permanent input line. In non-TTY
|
|
243
|
+
// mode (pipes) - a regular spinner + watchInput.
|
|
244
|
+
// A new task from the prompt - we reset the "stop" from the previous abort.
|
|
245
|
+
browser._stopped = false;
|
|
246
|
+
browser._abort = false;
|
|
247
|
+
const ui = editor || createSpinner(locale);
|
|
248
|
+
// Non-TTY parity: the ora spinner shows the same "tasks: 2/5" badge the
|
|
249
|
+
// LineEditor shows (the editor gets it via onTasksQuery in main()).
|
|
250
|
+
if (!editor && todosQuery && 'onTasksQuery' in ui) {
|
|
251
|
+
;
|
|
252
|
+
ui.onTasksQuery = todosQuery;
|
|
253
|
+
}
|
|
254
|
+
// Mark the editor busy for the WHOLE task, not only for /init and /self-fix.
|
|
255
|
+
// Esc / Ctrl+C abort the current generation only while busy; without this a
|
|
256
|
+
// long-running tool (Bash, npm, MCP) could not be interrupted - Esc did
|
|
257
|
+
// nothing. Cleared in the finally below.
|
|
258
|
+
if (editor)
|
|
259
|
+
editor.busy = true;
|
|
260
|
+
const stopWatching = editor
|
|
261
|
+
? () => { }
|
|
262
|
+
: watchInput({
|
|
263
|
+
onEscape: () => {
|
|
264
|
+
ui.stop();
|
|
265
|
+
console.error(theme.warn(t('msg.abort_gen_short')));
|
|
266
|
+
browser.stopGeneration().catch(() => { });
|
|
267
|
+
},
|
|
268
|
+
onChange: (text) => ui.setPending(text),
|
|
269
|
+
onQueue: (text) => {
|
|
270
|
+
queue.push({ text });
|
|
271
|
+
ui.setPending(null);
|
|
272
|
+
ui.stop();
|
|
273
|
+
console.log(theme.user(t('msg.queued', { n: queue.length })) +
|
|
274
|
+
theme.assistant(text));
|
|
275
|
+
ui.thinking();
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
try {
|
|
279
|
+
let next = {
|
|
280
|
+
text: taskText,
|
|
281
|
+
attachments,
|
|
282
|
+
freshChat,
|
|
283
|
+
sendSystemPrompt,
|
|
284
|
+
};
|
|
285
|
+
// Execute the task, then everything the user managed to type while it ran.
|
|
286
|
+
// The queue may be replenished right during draining.
|
|
287
|
+
//
|
|
288
|
+
// NOTE: we do NOT call ui.thinking() here. runAgentLoop() fires onThinking()
|
|
289
|
+
// right before the actual browser send (after the send pause, system-prompt,
|
|
290
|
+
// etc.), so the spinner only appears when a generation really starts.
|
|
291
|
+
// Calling it here made the spinner run for the whole pre-send phase (chat
|
|
292
|
+
// creation, throttle wait) with no generation in flight.
|
|
293
|
+
while (true) {
|
|
294
|
+
// T7: per-task summary (duration + number of tool calls + tokens).
|
|
295
|
+
const taskStart = Date.now();
|
|
296
|
+
// Token delta for THIS task: accumulated_token_usage is cumulative for
|
|
297
|
+
// the whole chat, so the per-task spend is after - before. Null when the
|
|
298
|
+
// counter is unknown (fresh chat / no answer yet).
|
|
299
|
+
const tokensBefore = getTokenUsage ? getTokenUsage() : null;
|
|
300
|
+
let taskTools = 0;
|
|
301
|
+
const outcome = await runAgentLoop({
|
|
302
|
+
browser,
|
|
303
|
+
tools,
|
|
304
|
+
task: withGoal(goal, next.text),
|
|
305
|
+
workdir,
|
|
306
|
+
maxIterations: maxIter,
|
|
307
|
+
freshChat: next.freshChat,
|
|
308
|
+
sendSystemPrompt: next.sendSystemPrompt,
|
|
309
|
+
attachments: next.attachments || [],
|
|
310
|
+
transcript,
|
|
311
|
+
onThinking: () => ui.thinking(),
|
|
312
|
+
onSendPause: (seconds) => ui.sendPause(seconds),
|
|
313
|
+
onSendState: (state) => {
|
|
314
|
+
if (editor)
|
|
315
|
+
editor.setSendState(state);
|
|
316
|
+
},
|
|
317
|
+
onNotice: (msg) => ui.warning(msg),
|
|
318
|
+
onToolCall: (name, toolArgs) => {
|
|
319
|
+
taskTools++;
|
|
320
|
+
ui.toolCall(name, toolArgs);
|
|
321
|
+
},
|
|
322
|
+
onToolResult: (result) => ui.toolResult(result),
|
|
323
|
+
onAssistantMessage: (msg) => {
|
|
324
|
+
// Remember the last answer so /copy can put it on the clipboard
|
|
325
|
+
// without re-reading the chat.
|
|
326
|
+
deps.onAssistantMessage(msg);
|
|
327
|
+
opts.onAssistantFinal?.(msg);
|
|
328
|
+
ui.assistant(msg);
|
|
329
|
+
},
|
|
330
|
+
onWarning: (msg) => ui.warning(msg),
|
|
331
|
+
onAskPermission: async (info) => {
|
|
332
|
+
// C1: a rule with action "ask". Show the reason and wait for the
|
|
333
|
+
// operator. In non-TTY this denies.
|
|
334
|
+
ui.warning(t('perm.ask', { tool: info.tool, reason: info.reason }));
|
|
335
|
+
const ok = await askOperatorConfirm(editor || null, t('perm.confirm'));
|
|
336
|
+
ui.warning(ok ? t('perm.allowed') : t('perm.denied'));
|
|
337
|
+
transcript?.log('permission_ask', {
|
|
338
|
+
tool: info.tool,
|
|
339
|
+
reason: info.reason,
|
|
340
|
+
allowed: ok,
|
|
341
|
+
});
|
|
342
|
+
return ok;
|
|
343
|
+
},
|
|
344
|
+
onChatReady,
|
|
345
|
+
debugLog: debug,
|
|
346
|
+
locale,
|
|
347
|
+
askDeadlineMs: config.browser.askDeadlineMs,
|
|
348
|
+
maxAfterToolRetries: config.browser.maxAfterToolRetries,
|
|
349
|
+
// Auto-compact between tool calls when the context nears the window
|
|
350
|
+
// limit. The callback is provided by the caller so it can refresh the
|
|
351
|
+
// outer currentChatId; it serializes with the throttle because the
|
|
352
|
+
// loop awaits it.
|
|
353
|
+
onAutoCompact,
|
|
354
|
+
autoCompactPct,
|
|
355
|
+
contextLimit,
|
|
356
|
+
getTokenUsage,
|
|
357
|
+
selfImprovement,
|
|
358
|
+
});
|
|
359
|
+
// The loop may end WITHOUT a model answer: an exhausted iteration limit
|
|
360
|
+
// or an ask() watchdog. Surface it instead of a silent stop.
|
|
361
|
+
if (outcome &&
|
|
362
|
+
(outcome.startsWith('Iteration limit reached') ||
|
|
363
|
+
outcome.startsWith('ask() watchdog'))) {
|
|
364
|
+
ui.warning(outcome);
|
|
365
|
+
transcript?.log('agent_no_answer', { outcome });
|
|
366
|
+
}
|
|
367
|
+
// T7: one summary line per task (duration + tool calls + tokens spent).
|
|
368
|
+
// Printed even on an abort, so the operator sees what happened.
|
|
369
|
+
const tokensAfter = getTokenUsage ? getTokenUsage() : null;
|
|
370
|
+
const tokenDelta = typeof tokensBefore === 'number' && typeof tokensAfter === 'number'
|
|
371
|
+
? Math.max(0, tokensAfter - tokensBefore)
|
|
372
|
+
: null;
|
|
373
|
+
const summaryParts = [
|
|
374
|
+
t('task_sum.dur', {
|
|
375
|
+
dur: formatDuration(Date.now() - taskStart, {
|
|
376
|
+
h: t('dur.h'),
|
|
377
|
+
m: t('dur.m'),
|
|
378
|
+
s: t('dur.s'),
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
t('task_sum.tools', { n: String(taskTools) }),
|
|
382
|
+
];
|
|
383
|
+
if (tokenDelta !== null) {
|
|
384
|
+
summaryParts.push(t('task_sum.tokens', { n: formatCompactTokens(tokenDelta) }));
|
|
385
|
+
}
|
|
386
|
+
const summary = theme.taskSummary('· ' + summaryParts.join(' · '));
|
|
387
|
+
if (editor)
|
|
388
|
+
editor.printAbove(summary);
|
|
389
|
+
else
|
|
390
|
+
console.log(summary);
|
|
391
|
+
// Aborted (Esc/Ctrl+C) - we don't start the next tasks from the queue and
|
|
392
|
+
// clear it, so "stop" really stops everything.
|
|
393
|
+
if (browser._stopped) {
|
|
394
|
+
queue.length = 0;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
if (!queue.length)
|
|
398
|
+
break;
|
|
399
|
+
// Merge the LEADING plain-text messages into ONE batch (fewer sends ->
|
|
400
|
+
// less rate-limit risk and the operator's thoughts arrive together). We
|
|
401
|
+
// stop at the first slash-command: it must NOT be sent as a task - it is
|
|
402
|
+
// handled by the main loop, so we leave it (and anything after it) in the
|
|
403
|
+
// queue and break out of this task's drain loop.
|
|
404
|
+
let batchLen = 0;
|
|
405
|
+
while (batchLen < queue.length && !isSlashCommand(queue[batchLen].text)) {
|
|
406
|
+
batchLen++;
|
|
407
|
+
}
|
|
408
|
+
if (batchLen === 0)
|
|
409
|
+
break;
|
|
410
|
+
const batch = queue.splice(0, batchLen);
|
|
411
|
+
const merged = mergeMessages(batch);
|
|
412
|
+
if (!merged.text.trim())
|
|
413
|
+
break;
|
|
414
|
+
ui.stop();
|
|
415
|
+
const banner = batch.length === 1
|
|
416
|
+
? theme.user(t('msg.from_queue')) + theme.assistant(batch[0].text)
|
|
417
|
+
: theme.user(t('msg.from_queue_batch', { n: batch.length })) +
|
|
418
|
+
theme.assistant(t('msg.batch_joined', { n: batch.length }));
|
|
419
|
+
if (editor)
|
|
420
|
+
editor.printAbove(banner);
|
|
421
|
+
else
|
|
422
|
+
console.log(banner);
|
|
423
|
+
transcript?.log('queued_task', { task: merged.text, count: batch.length });
|
|
424
|
+
next = {
|
|
425
|
+
text: merged.text,
|
|
426
|
+
attachments: merged.attachments,
|
|
427
|
+
freshChat: false,
|
|
428
|
+
sendSystemPrompt: false,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
ui.stop();
|
|
434
|
+
console.error(theme.error(String.fromCharCode(10) + t('msg.agent_error')), e.message);
|
|
435
|
+
if (debug)
|
|
436
|
+
console.error(e.stack);
|
|
437
|
+
transcript?.log('agent_error', { error: e.message });
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
stopWatching();
|
|
441
|
+
ui.stop();
|
|
442
|
+
if (editor)
|
|
443
|
+
editor.busy = false;
|
|
444
|
+
// Detach the send hooks so a later browser.ask() outside this task cannot
|
|
445
|
+
// start a stale spinner.
|
|
446
|
+
browser.onSendStart = null;
|
|
447
|
+
browser.onSendPause = null;
|
|
448
|
+
browser.onSendState = null;
|
|
449
|
+
browser.onNotice = null;
|
|
450
|
+
}
|
|
451
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.65.0",
|
|
4
4
|
"description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"README.md",
|
|
13
13
|
"CHANGELOG.md",
|
|
14
14
|
"LICENSE",
|
|
15
|
-
"logo.jpg",
|
|
16
15
|
"logo-small.jpg"
|
|
17
16
|
],
|
|
18
17
|
"scripts": {
|
|
@@ -31,6 +30,7 @@
|
|
|
31
30
|
"test": "tsx --test test/*.test.ts",
|
|
32
31
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
33
32
|
"self-smoke": "tsx scripts/self-smoke.mjs",
|
|
33
|
+
"render-demo": "node scripts/render-demo.mjs",
|
|
34
34
|
"postinstall": "node scripts/postinstall.mjs",
|
|
35
35
|
"prepare": "husky"
|
|
36
36
|
},
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Render docs/demo.png — the terminal screenshot used in README.md.
|
|
2
|
+
//
|
|
3
|
+
// Why a generator instead of a live screenshot: the demo must be
|
|
4
|
+
// REPRODUCIBLE and ENGLISH (the README is English). A live capture depends on
|
|
5
|
+
// the operator's locale/theme/font and would drift. This renders a fixed HTML
|
|
6
|
+
// page that mirrors the REAL editor layout (src/input.ts _writeBlock) and the
|
|
7
|
+
// theme palette (src/theme.ts) with the DejaVu Sans Mono font that the
|
|
8
|
+
// container actually ships, then screenshots it with headless Chromium.
|
|
9
|
+
//
|
|
10
|
+
// Run: node scripts/render-demo.mjs
|
|
11
|
+
//
|
|
12
|
+
// Isolation: chromium.launch() uses a THROWAWAY profile, never ~/.zames/profile
|
|
13
|
+
// (see AGENTS.md — never touch the live agent's browser/profile).
|
|
14
|
+
|
|
15
|
+
import { chromium } from 'playwright'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const out = path.join(root, 'docs', 'demo.png')
|
|
21
|
+
|
|
22
|
+
const C = {
|
|
23
|
+
bg: '#23272e',
|
|
24
|
+
user: '#b4b8d0',
|
|
25
|
+
prompt: '#c8b06a',
|
|
26
|
+
dir: '#7fc4f0',
|
|
27
|
+
assistant: '#cfc9b0',
|
|
28
|
+
tool: '#c6a97e',
|
|
29
|
+
toolResult: '#8a9bb5',
|
|
30
|
+
system: '#808896',
|
|
31
|
+
dim: '#5b616e',
|
|
32
|
+
taskSummary: '#8fa3c8',
|
|
33
|
+
toggleOn: '#6fd0b0',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const esc = (s) =>
|
|
37
|
+
String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
38
|
+
const span = (color, text, bold = false) =>
|
|
39
|
+
'<span style="color:' +
|
|
40
|
+
color +
|
|
41
|
+
(bold ? ';font-weight:700' : '') +
|
|
42
|
+
'">' +
|
|
43
|
+
esc(text) +
|
|
44
|
+
'</span>'
|
|
45
|
+
|
|
46
|
+
// One rendered session, line by line (mirrors the real banner + answer flow).
|
|
47
|
+
const lines = []
|
|
48
|
+
const push = (html) => lines.push('<div class="l">' + html + '</div>')
|
|
49
|
+
const blank = () => push(' ')
|
|
50
|
+
|
|
51
|
+
push(span(C.system, 'Working directory: ') + span(C.dir, '/home/dev/zames_pro'))
|
|
52
|
+
push(span(C.system, 'Version: ') + span(C.user, '2.64.1'))
|
|
53
|
+
push(span(C.system, 'Transcript: ') + span(C.dim, '~/.zames/logs/zames_pro-2026-10-07.log'))
|
|
54
|
+
blank()
|
|
55
|
+
push(
|
|
56
|
+
span(
|
|
57
|
+
C.system,
|
|
58
|
+
'Interactive mode. Enter a task. Commands — /help. Exit — /exit.',
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
push(
|
|
62
|
+
span(
|
|
63
|
+
C.system,
|
|
64
|
+
'While the agent works you can type the next message — it goes to the queue (Enter — send, Esc — abort).',
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
push(
|
|
68
|
+
span(
|
|
69
|
+
C.dim,
|
|
70
|
+
'Hint: "/" lists commands, Ctrl+R searches history, paste an image with Ctrl+Shift+V.',
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
blank()
|
|
74
|
+
push(
|
|
75
|
+
span(C.prompt, '❯ ', true) +
|
|
76
|
+
span(C.dir, 'zames_pro') +
|
|
77
|
+
span(C.dim, ' › ') +
|
|
78
|
+
span(C.user, 'Show the version from package.json.'),
|
|
79
|
+
)
|
|
80
|
+
blank()
|
|
81
|
+
push(
|
|
82
|
+
span(C.tool, '🔧 Read') +
|
|
83
|
+
' ' +
|
|
84
|
+
span(C.dim, '{"path":"package.json"}'),
|
|
85
|
+
)
|
|
86
|
+
push(
|
|
87
|
+
span(C.toolResult, ' → ') +
|
|
88
|
+
span(
|
|
89
|
+
C.toolResult,
|
|
90
|
+
'{\n "name": "zames_pro",\n "version": "2.64.1", …\n}',
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
blank()
|
|
94
|
+
push(span(C.assistant, '● Answer'))
|
|
95
|
+
push(
|
|
96
|
+
span(C.assistant, 'The version in ') +
|
|
97
|
+
'<span style="color:' +
|
|
98
|
+
C.assistant +
|
|
99
|
+
';background:#2c313a;padding:0 3px;border-radius:3px">package.json</span>' +
|
|
100
|
+
span(C.assistant, ' is ') +
|
|
101
|
+
span(C.assistant, '2.64.1', true) +
|
|
102
|
+
span(C.assistant, '.'),
|
|
103
|
+
)
|
|
104
|
+
blank()
|
|
105
|
+
push(span(C.taskSummary, '· duration: 6s · tools: 1 · tokens: 1.2k'))
|
|
106
|
+
push(span(C.dim, '─'.repeat(78)))
|
|
107
|
+
|
|
108
|
+
// Status line (right-aligned context + toggles) and the permanent input line.
|
|
109
|
+
const status =
|
|
110
|
+
'<div class="l status"><span class="grow"></span>' +
|
|
111
|
+
span(C.toggleOn, '🧠 ') +
|
|
112
|
+
span(C.toggleOn, '🌐 ') +
|
|
113
|
+
span(C.system, 'ctx: 12.4k · 1.2%') +
|
|
114
|
+
'</div>'
|
|
115
|
+
const input =
|
|
116
|
+
'<div class="l">' +
|
|
117
|
+
span(C.prompt, '❯ ', true) +
|
|
118
|
+
span(C.dir, 'zames_pro') +
|
|
119
|
+
span(C.dim, ' › ')
|
|
120
|
+
|
|
121
|
+
const html =
|
|
122
|
+
'<!doctype html><html><head><meta charset="utf-8"><style>' +
|
|
123
|
+
'@font-face{font-family:ZM;src:local("DejaVu Sans Mono")}' +
|
|
124
|
+
'html,body{margin:0;padding:0;background:' +
|
|
125
|
+
C.bg +
|
|
126
|
+
'}' +
|
|
127
|
+
'body{font-family:"DejaVu Sans Mono","Noto Color Emoji",monospace;font-size:15px;line-height:1.45;' +
|
|
128
|
+
'color:' +
|
|
129
|
+
C.system +
|
|
130
|
+
';-webkit-font-smoothing:antialiased}' +
|
|
131
|
+
'.screen{box-sizing:border-box;width:1197px;height:562px;padding:14px 18px;overflow:hidden;' +
|
|
132
|
+
'display:flex;flex-direction:column}' +
|
|
133
|
+
'.l{white-space:pre-wrap;word-break:break-word}' +
|
|
134
|
+
'.status{display:flex;align-items:center;margin-top:auto}' +
|
|
135
|
+
'.grow{flex:1}' +
|
|
136
|
+
'</style></head><body><div class="screen">' +
|
|
137
|
+
lines.join('') +
|
|
138
|
+
status +
|
|
139
|
+
input +
|
|
140
|
+
'</div></body></html>'
|
|
141
|
+
|
|
142
|
+
const browser = await chromium.launch({ headless: true })
|
|
143
|
+
try {
|
|
144
|
+
const page = await browser.newPage({
|
|
145
|
+
viewport: { width: 1197, height: 562 },
|
|
146
|
+
deviceScaleFactor: 2,
|
|
147
|
+
})
|
|
148
|
+
await page.setContent(html, { waitUntil: 'load' })
|
|
149
|
+
await page.screenshot({ path: out })
|
|
150
|
+
console.log('wrote', path.relative(root, out))
|
|
151
|
+
} finally {
|
|
152
|
+
await browser.close()
|
|
153
|
+
}
|
package/logo.jpg
DELETED
|
Binary file
|