zames_pro 2.64.1 → 2.65.1
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 +58 -0
- package/README.md +7 -12
- package/dist/backlog.js +20 -0
- package/dist/browser.js +15 -0
- package/dist/commands.js +81 -0
- package/dist/config-menu.js +70 -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 +314 -0
- package/dist/input.js +88 -292
- package/dist/run-task.js +451 -0
- package/dist/spinner.js +5 -5
- package/package.json +2 -2
- package/scripts/render-demo.mjs +153 -0
- package/logo.jpg +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Terminal INPUT-LINE layout + formatting helpers — the PURE half of the
|
|
2
|
+
// LineEditor (src/input.ts). No terminal access, no state: given a prompt, a
|
|
3
|
+
// buffer, a cursor and a width these compute the visual rows, the cursor
|
|
4
|
+
// position, the visible width (wide chars count 2) and the status formatting.
|
|
5
|
+
// Split out (C3 step 3) so the geometry can be unit-tested without driving the
|
|
6
|
+
// real editor, and so the editor class keeps only render/key handling.
|
|
7
|
+
// All control characters are assembled from codes so the file has no
|
|
8
|
+
// "raw" ESC/CR/LF in string literals (see AGENTS.md).
|
|
9
|
+
const NL = String.fromCharCode(10);
|
|
10
|
+
const CR = String.fromCharCode(13);
|
|
11
|
+
const ESC = String.fromCharCode(27);
|
|
12
|
+
const PASTE_START = ESC + '[200~';
|
|
13
|
+
const PASTE_END = ESC + '[201~';
|
|
14
|
+
// A large paste (3+ lines) is collapsed in the input line into a short
|
|
15
|
+
// marker "[Pasted lines#N]" so the line stays readable; the original text is
|
|
16
|
+
// kept aside and expanded back on submit. Pastes of 1–2 lines are inserted
|
|
17
|
+
// as-is (small pastes are usually short and don't clutter the line).
|
|
18
|
+
export const PASTE_MIN_LINES = 3;
|
|
19
|
+
// How many slash-command hints are shown at once (the rest are paged with
|
|
20
|
+
// Ctrl+N/P). The list used to be hard-capped at 8 with a "...more" note and no
|
|
21
|
+
// way to reach the rest.
|
|
22
|
+
export const SUGGEST_PAGE = 8;
|
|
23
|
+
// True when `q` is a subsequence of `s` (e.g. "hst" in "/history"). Used for
|
|
24
|
+
// the fuzzy fallback in _suggestions().
|
|
25
|
+
export function isSubsequence(q, s) {
|
|
26
|
+
let i = 0;
|
|
27
|
+
for (let j = 0; j < s.length && i < q.length; j++) {
|
|
28
|
+
if (s[j] === q[i])
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
return i === q.length;
|
|
32
|
+
}
|
|
33
|
+
// Number of lines in a pasted block (the trailing newline doesn't add a line).
|
|
34
|
+
export function countPasteLines(raw) {
|
|
35
|
+
const normalized = String(raw)
|
|
36
|
+
.split(CR + NL)
|
|
37
|
+
.join(NL)
|
|
38
|
+
.split(CR)
|
|
39
|
+
.join(NL);
|
|
40
|
+
const trimmed = normalized.replace(/\n+$/, '');
|
|
41
|
+
if (trimmed === '')
|
|
42
|
+
return 1;
|
|
43
|
+
return trimmed.split(NL).length;
|
|
44
|
+
}
|
|
45
|
+
export function formatPasteMarker(lines) {
|
|
46
|
+
return '[Pasted lines#' + lines + ']';
|
|
47
|
+
}
|
|
48
|
+
// Decide how to represent a pasted block: null — insert as-is (small paste),
|
|
49
|
+
// otherwise { marker, text }: the marker goes into the input line, and the
|
|
50
|
+
// original text is expanded back on submit.
|
|
51
|
+
export function pasteReplacement(raw) {
|
|
52
|
+
const text = String(raw)
|
|
53
|
+
.split(CR + NL)
|
|
54
|
+
.join(NL)
|
|
55
|
+
.split(CR)
|
|
56
|
+
.join(NL);
|
|
57
|
+
const lines = countPasteLines(text);
|
|
58
|
+
if (lines < PASTE_MIN_LINES)
|
|
59
|
+
return null;
|
|
60
|
+
return { marker: formatPasteMarker(lines), text };
|
|
61
|
+
}
|
|
62
|
+
// Replace "[Pasted lines#N]" markers with the original pasted text (the first
|
|
63
|
+
// occurrence in order — matches the order in which the pastes were inserted).
|
|
64
|
+
export function expandPastes(pastes, text) {
|
|
65
|
+
let out = text;
|
|
66
|
+
for (const p of pastes) {
|
|
67
|
+
const idx = out.indexOf(p.marker);
|
|
68
|
+
if (idx !== -1) {
|
|
69
|
+
out = out.slice(0, idx) + p.text + out.slice(idx + p.marker.length);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function safeJson(v) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(v);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return String(v);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// How many visual lines the text occupies at the given terminal width
|
|
83
|
+
// (accounting for wrapping). Needed so the cursor/block erase don't drift
|
|
84
|
+
// when the status is longer than the terminal width and wraps onto 2+ lines.
|
|
85
|
+
export function visRows(s, cols) {
|
|
86
|
+
const width = Math.max(1, Number(cols) || 80);
|
|
87
|
+
const len = visLen(s);
|
|
88
|
+
return Math.max(1, Math.ceil(len / width));
|
|
89
|
+
}
|
|
90
|
+
// Visible length of a line without ANSI sequences. WIDE characters (emoji,
|
|
91
|
+
// CJK) count as 2 columns — a plain code-point count made the status-line
|
|
92
|
+
// arithmetic (right-align, wrapping, block erase) disagree with the real
|
|
93
|
+
// screen, so a status containing an emoji/CJK could wrap past the edge and
|
|
94
|
+
// make the block erase miss (status "stacking"). Combining marks and the
|
|
95
|
+
// zero-width joiner/space/variation selectors count as 0.
|
|
96
|
+
export function visLen(s) {
|
|
97
|
+
s = String(s);
|
|
98
|
+
let n = 0;
|
|
99
|
+
let i = 0;
|
|
100
|
+
while (i < s.length) {
|
|
101
|
+
if (s.charCodeAt(i) === 27) {
|
|
102
|
+
i++;
|
|
103
|
+
if (s[i] === '[') {
|
|
104
|
+
i++;
|
|
105
|
+
while (i < s.length && !/[A-Za-z]/.test(s[i]))
|
|
106
|
+
i++;
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const cp = s.codePointAt(i);
|
|
112
|
+
n += charWidth(cp);
|
|
113
|
+
i += cp > 0xffff ? 2 : 1;
|
|
114
|
+
}
|
|
115
|
+
return n;
|
|
116
|
+
}
|
|
117
|
+
// Display width of one code point in columns (0, 1 or 2). Based on the
|
|
118
|
+
// conventional wcwidth ranges — good enough for terminal layout, and it makes
|
|
119
|
+
// the emoji used in the status line (🧠🌐) count as 2, while math symbols like
|
|
120
|
+
// ⧗ (U+29D7) stay 1, matching a real terminal.
|
|
121
|
+
export function charWidth(cp) {
|
|
122
|
+
// Zero width: combining marks, ZWJ/ZWNJ, variation selectors.
|
|
123
|
+
if (cp === 0x200b ||
|
|
124
|
+
cp === 0x200c ||
|
|
125
|
+
cp === 0x200d ||
|
|
126
|
+
(cp >= 0x0300 && cp <= 0x036f) ||
|
|
127
|
+
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
|
128
|
+
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
|
129
|
+
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
|
130
|
+
(cp >= 0xfe00 && cp <= 0xfe0f) ||
|
|
131
|
+
(cp >= 0xfe20 && cp <= 0xfe2f)) {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
// Wide: CJK, Hangul, fullwidth forms, emoji.
|
|
135
|
+
if ((cp >= 0x1100 && cp <= 0x115f) ||
|
|
136
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
137
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
138
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
139
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
140
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
141
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
142
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
143
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
144
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
145
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
146
|
+
(cp >= 0x1f000 && cp <= 0x1f02f) ||
|
|
147
|
+
(cp >= 0x1f300 && cp <= 0x1faff) ||
|
|
148
|
+
(cp >= 0x20000 && cp <= 0x3fffd)) {
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
// Truncate a PLAIN (no-ANSI) string to at most `maxCols` visible columns,
|
|
154
|
+
// appending an ellipsis when it was cut. Used for one-line previews (tool call
|
|
155
|
+
// args, tool results) so a long argument cannot print a line wider than the
|
|
156
|
+
// terminal — which made some terminals show a horizontal scrollbar.
|
|
157
|
+
export function truncateToWidth(s, maxCols) {
|
|
158
|
+
const width = Math.max(1, Math.floor(maxCols));
|
|
159
|
+
const text = String(s ?? '');
|
|
160
|
+
if (visLen(text) <= width)
|
|
161
|
+
return text;
|
|
162
|
+
// Reserve one column for the ellipsis.
|
|
163
|
+
const budget = Math.max(0, width - 1);
|
|
164
|
+
let used = 0;
|
|
165
|
+
let out = '';
|
|
166
|
+
for (const ch of text) {
|
|
167
|
+
const w = charWidth(ch.codePointAt(0));
|
|
168
|
+
if (used + w > budget)
|
|
169
|
+
break;
|
|
170
|
+
out += ch;
|
|
171
|
+
used += w;
|
|
172
|
+
}
|
|
173
|
+
return out + '…';
|
|
174
|
+
}
|
|
175
|
+
export function layoutInput(promptStr, buf, cursor, cols) {
|
|
176
|
+
// Reserve the LAST column. If a printed row reaches exactly `cols` columns,
|
|
177
|
+
// terminals with autowrap (Tabby, iTerm, Windows Terminal, xterm's default
|
|
178
|
+
// deferred wrap) either wrap the cursor to the next line or leave it in the
|
|
179
|
+
// last cell with a "pending wrap". In both cases the editor's row count
|
|
180
|
+
// disagrees with the real screen by one, so the cursor/erase arithmetic in
|
|
181
|
+
// `_renderInputOnly`/`_writeBlock` walks one row too far and the input line
|
|
182
|
+
// climbs. This was most visible in an EMPTY chat with a single long,
|
|
183
|
+
// space-less line (which fills the row exactly) and when moving the cursor
|
|
184
|
+
// with the arrow keys.
|
|
185
|
+
const width = Math.max(2, (Number(cols) || 80) - 1);
|
|
186
|
+
const promptW = visLen(promptStr);
|
|
187
|
+
const pad = ' '.repeat(promptW);
|
|
188
|
+
const chars = Array.from(String(buf));
|
|
189
|
+
const cur = Math.max(0, Math.min(Number(cursor) || 0, chars.length));
|
|
190
|
+
const rows = [];
|
|
191
|
+
let i = 0;
|
|
192
|
+
while (true) {
|
|
193
|
+
const first = rows.length === 0;
|
|
194
|
+
const prefix = first ? promptStr : pad;
|
|
195
|
+
const avail = Math.max(1, width - promptW);
|
|
196
|
+
const start = i;
|
|
197
|
+
// Build the next visual line. First we take as many
|
|
198
|
+
// characters as fit (columns < avail). If we hit the width
|
|
199
|
+
// and the next character is not the end of the line, we roll back to the
|
|
200
|
+
// last space so we don't split a word in the middle (word-wrap).
|
|
201
|
+
// Wrapping and the cursor column are measured in COLUMNS (charWidth),
|
|
202
|
+
// not code points — otherwise a CJK/emoji input would overflow the row
|
|
203
|
+
// and put the cursor on the wrong cell.
|
|
204
|
+
let text = '';
|
|
205
|
+
let widthUsed = 0;
|
|
206
|
+
let charIdx = 0;
|
|
207
|
+
let lastSpace = -1; // char index of the space within text
|
|
208
|
+
while (i < chars.length && chars[i] !== NL) {
|
|
209
|
+
const w = charWidth(chars[i].codePointAt(0));
|
|
210
|
+
if (widthUsed + w > avail)
|
|
211
|
+
break;
|
|
212
|
+
if (chars[i] === ' ')
|
|
213
|
+
lastSpace = charIdx;
|
|
214
|
+
text += chars[i];
|
|
215
|
+
i++;
|
|
216
|
+
charIdx++;
|
|
217
|
+
widthUsed += w;
|
|
218
|
+
}
|
|
219
|
+
const atLineEnd = i >= chars.length || chars[i] === NL;
|
|
220
|
+
if (!atLineEnd && widthUsed >= avail && lastSpace > 0) {
|
|
221
|
+
// Move the "tail" of the line to the next visual line.
|
|
222
|
+
const textChars = Array.from(text);
|
|
223
|
+
const tailLen = textChars.length - lastSpace;
|
|
224
|
+
i -= tailLen;
|
|
225
|
+
text = textChars.slice(0, lastSpace).join('');
|
|
226
|
+
}
|
|
227
|
+
if (i < chars.length && chars[i] === NL) {
|
|
228
|
+
i++;
|
|
229
|
+
rows.push({ prefix, text, start });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
rows.push({ prefix, text, start });
|
|
233
|
+
if (i >= chars.length)
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
let cursorRow = 0;
|
|
237
|
+
let cursorCol = promptW;
|
|
238
|
+
for (let r = 0; r < rows.length; r++) {
|
|
239
|
+
const row = rows[r];
|
|
240
|
+
const rowChars = Array.from(row.text);
|
|
241
|
+
const len = rowChars.length;
|
|
242
|
+
const from = row.start;
|
|
243
|
+
const to = row.start + len;
|
|
244
|
+
if (cur >= from && cur <= to) {
|
|
245
|
+
const next = rows[r + 1];
|
|
246
|
+
if (cur === to && next && next.start === to)
|
|
247
|
+
continue;
|
|
248
|
+
cursorRow = r;
|
|
249
|
+
// Cursor column in COLUMNS (wide chars count as 2).
|
|
250
|
+
let w = 0;
|
|
251
|
+
for (let k = 0; k < cur - from; k++) {
|
|
252
|
+
w += charWidth(rowChars[k].codePointAt(0));
|
|
253
|
+
}
|
|
254
|
+
cursorCol = promptW + w;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { rows, cursorRow, cursorCol };
|
|
259
|
+
}
|
|
260
|
+
// Format a token count for the status line: compact (10k, 125k) plus the
|
|
261
|
+
// percentage of CONTEXT_LIMIT. Exported so it is unit-tested without a live
|
|
262
|
+
// editor. A null/undefined/NaN count renders an empty string (no status).
|
|
263
|
+
export const CONTEXT_LIMIT = 1_000_000;
|
|
264
|
+
// Thresholds for coloring the context counter. Below YELLOW the value is
|
|
265
|
+
// green (plenty of room), between YELLOW and RED it is yellow (getting full),
|
|
266
|
+
// above RED it is red (nearly exhausted). Percent of CONTEXT_LIMIT.
|
|
267
|
+
export const CONTEXT_YELLOW_PCT = 50;
|
|
268
|
+
export const CONTEXT_RED_PCT = 80;
|
|
269
|
+
// The color role for a context fill level, as a stable string so it can be
|
|
270
|
+
// unit-tested without a terminal: 'ok' | 'warn' | 'high'. Null tokens -> null.
|
|
271
|
+
export function tokenStatusLevel(tokens, limit = CONTEXT_LIMIT) {
|
|
272
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const pct = (tokens / limit) * 100;
|
|
276
|
+
if (pct >= CONTEXT_RED_PCT)
|
|
277
|
+
return 'high';
|
|
278
|
+
if (pct >= CONTEXT_YELLOW_PCT)
|
|
279
|
+
return 'warn';
|
|
280
|
+
return 'ok';
|
|
281
|
+
}
|
|
282
|
+
export function formatTokenStatus(tokens, limit = CONTEXT_LIMIT) {
|
|
283
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
284
|
+
return '';
|
|
285
|
+
}
|
|
286
|
+
const n = Math.round(tokens);
|
|
287
|
+
const compact = formatCompactTokens(n);
|
|
288
|
+
const pct = Math.max(0, (n / limit) * 100);
|
|
289
|
+
const pctStr = pct >= 10 ? String(Math.round(pct)) : pct.toFixed(1);
|
|
290
|
+
// "ctx:" marks this as the USED context size, so the operator does not have
|
|
291
|
+
// to guess what "302k · 30%" means.
|
|
292
|
+
return 'ctx: ' + compact + ' · ' + pctStr + '%';
|
|
293
|
+
}
|
|
294
|
+
// Compact token count (10k, 125k, 1.5M). Exported so the per-task summary
|
|
295
|
+
// reuses the EXACT same "k/M" formatting as the context counter. Invalid
|
|
296
|
+
// input (null/NaN/negative) renders an empty string.
|
|
297
|
+
export function formatCompactTokens(tokens) {
|
|
298
|
+
if (typeof tokens !== 'number' || !Number.isFinite(tokens) || tokens < 0) {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
const n = Math.round(tokens);
|
|
302
|
+
if (n >= 1_000_000) {
|
|
303
|
+
const m = n / 1_000_000;
|
|
304
|
+
return (Number.isInteger(m) ? String(m) : m.toFixed(1)) + 'M';
|
|
305
|
+
}
|
|
306
|
+
if (n >= 1_000) {
|
|
307
|
+
const k = n / 1_000;
|
|
308
|
+
return ((k >= 100 ? String(Math.round(k)) : k.toFixed(1).replace(/\.0$/, '')) +
|
|
309
|
+
'k');
|
|
310
|
+
}
|
|
311
|
+
return String(n);
|
|
312
|
+
}
|
|
313
|
+
// Re-exported for the editor's layout selftest (node src/input.js --selftest).
|
|
314
|
+
export { NL, CR, ESC, PASTE_START, PASTE_END, safeJson };
|