@yanzc/claude-pro 0.5.6 → 0.5.8
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/README.md +3 -1
- package/package.json +1 -1
- package/src/agent.js +2 -0
- package/src/api.js +591 -591
- package/src/cache.js +17 -0
- package/src/commands.js +1 -1
- package/src/session.js +8 -0
- package/src/ui/ansi.js +2 -0
- package/src/ui/render.js +8 -3
- package/src/ui/repl.js +2327 -2205
- package/src/ui/screen.js +184 -7
package/src/ui/screen.js
CHANGED
|
@@ -43,6 +43,16 @@ const FULL_KEEP = 20_000; // fullscreen: lines of the conversation kept to scrol
|
|
|
43
43
|
const ALT_ON = '\x1b[?1049h\x1b[?1002h\x1b[?1006h';
|
|
44
44
|
const ALT_OFF = '\x1b[?1002l\x1b[?1006l\x1b[?1049l';
|
|
45
45
|
const MULTI_CLICK_MS = 450;
|
|
46
|
+
// A search (ctrl+f): the matches, the current one, and how many are looked for at most.
|
|
47
|
+
const FIND_MATCH = '\x1b[30;43m';
|
|
48
|
+
const FIND_CURRENT = '\x1b[30;48;5;208m';
|
|
49
|
+
const FIND_MAX = 10_000;
|
|
50
|
+
// A row that fills the rest of its width with its background (the user's messages, see renderUserMessage) ends in an
|
|
51
|
+
// erase to the end of the line made while that background is on. Nothing may erase after it: an erase does not move
|
|
52
|
+
// the cursor, so a second one, made after the colours are reset, would wipe the band out again from the same column.
|
|
53
|
+
const FILL = '\x1b[K';
|
|
54
|
+
const LEAD = /^(?:\x1b\[[0-9;]*m)+/; // the colours a row starts in
|
|
55
|
+
const endRow = (row) => row + (row.includes(FILL) ? '\x1b[0m' : '\x1b[0m\x1b[K');
|
|
46
56
|
|
|
47
57
|
// The part of `plain` (no escape codes) between columns `from` (included) and `to` (not).
|
|
48
58
|
function sliceCols(plain, from, to) {
|
|
@@ -146,6 +156,8 @@ export class Screen {
|
|
|
146
156
|
this.lastArea = 0; // fullscreen: rows the conversation had on screen at the last draw
|
|
147
157
|
this.viewMap = []; // fullscreen: what each row of the conversation area shows, {line, sub} (null: blank)
|
|
148
158
|
this.barRow = null; // fullscreen: the row of the "Jump to bottom" bar, when it is shown
|
|
159
|
+
this.finding = null; // fullscreen: a search in the conversation (ctrl+f), see openFind
|
|
160
|
+
this.dropped = 0; // fullscreen: lines dropped from the top of the history so far (the first one's number)
|
|
149
161
|
this.selection = null; // fullscreen: {anchor, focus}, points {line, sub, col} of the conversation (history line, wrapped row, column)
|
|
150
162
|
this.dragging = false;
|
|
151
163
|
this.lastClick = { at: 0, x: 0, y: 0, count: 0 };
|
|
@@ -223,7 +235,14 @@ export class Screen {
|
|
|
223
235
|
|
|
224
236
|
wrappedAt(i, cols) {
|
|
225
237
|
if (this.wrapCols !== cols) { this.wrapped = []; this.wrapCols = cols; this.selection = null; } // its rows are other rows now
|
|
226
|
-
|
|
238
|
+
if (this.wrapped[i]) return this.wrapped[i];
|
|
239
|
+
let rows = wrapLine(this.history[i], Math.max(1, cols - 1));
|
|
240
|
+
// a band now wider than the window: each of its rows starts in its colours and fills the rest of the row
|
|
241
|
+
if (rows.length > 1 && this.history[i].includes(FILL)) {
|
|
242
|
+
const lead = LEAD.exec(this.history[i])?.[0] || '';
|
|
243
|
+
rows = rows.map((r, k) => (k ? lead : '') + r.replaceAll(FILL, '') + lead + FILL);
|
|
244
|
+
}
|
|
245
|
+
return (this.wrapped[i] = rows);
|
|
227
246
|
}
|
|
228
247
|
|
|
229
248
|
// Rows of the whole conversation at this width (for the scrolling to stop at its top).
|
|
@@ -270,11 +289,166 @@ export class Screen {
|
|
|
270
289
|
return to > from ? [from, to] : null;
|
|
271
290
|
}
|
|
272
291
|
|
|
292
|
+
// A row of the conversation as drawn: the selection in reverse video, the matches of a search highlighted (the
|
|
293
|
+
// current one apart). Such a row loses its own colours, except the band of a row that fills its width.
|
|
273
294
|
paintRow(e) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
295
|
+
const sel = this.selectedCols(e);
|
|
296
|
+
const hits = this.finding?.byRow.get(`${e.line}:${e.sub}`);
|
|
297
|
+
if (!sel && !hits) return e.text;
|
|
298
|
+
const fill = e.text.includes(FILL);
|
|
299
|
+
const base = fill ? LEAD.exec(e.text)?.[0] || '' : '';
|
|
300
|
+
const style = (col) => {
|
|
301
|
+
if (sel && col >= sel[0] && col < sel[1]) return '\x1b[7m';
|
|
302
|
+
const hit = hits?.find((h) => col >= h.from && col < h.to);
|
|
303
|
+
return !hit ? base : hit.match === this.finding.matches[this.finding.index] ? FIND_CURRENT : FIND_MATCH;
|
|
304
|
+
};
|
|
305
|
+
let out = '';
|
|
306
|
+
let open = '';
|
|
307
|
+
let col = 0;
|
|
308
|
+
for (const g of graphemes(stripAnsi(e.text))) {
|
|
309
|
+
const s = style(col);
|
|
310
|
+
if (s !== open) { out += '\x1b[0m' + s; open = s; }
|
|
311
|
+
out += g;
|
|
312
|
+
col += graphemeWidth(g);
|
|
313
|
+
}
|
|
314
|
+
if (fill) return out + '\x1b[0m' + base + FILL;
|
|
315
|
+
return out + (open ? '\x1b[0m' : '');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ------------------------------------------------------------ finding text (fullscreen, ctrl+f)
|
|
319
|
+
//
|
|
320
|
+
// `finding`: {query, matches, byRow, index, anchor, key}. Matches are newest first (from the bottom of the
|
|
321
|
+
// conversation up): {abs, offset, line, sub, col, fromEnd}, `abs` the line counted from the first line ever
|
|
322
|
+
// printed (so that it survives lines dropped from the top), `offset` where it starts in the line's text, and
|
|
323
|
+
// `fromEnd` the rows from its first row to the end of the conversation. `byRow` maps "line:sub" to the column
|
|
324
|
+
// ranges to highlight, {from, to, match}. A match may go on over the rows a long line was wrapped onto.
|
|
325
|
+
|
|
326
|
+
// Start finding: the first match shown is the nearest one above the bottom of the view as it is now.
|
|
327
|
+
openFind() {
|
|
328
|
+
this.takeStatic();
|
|
329
|
+
this.finding = { query: '', matches: [], byRow: new Map(), index: -1, anchor: this.scroll, cols: 0, dropped: -1, scanned: -1 };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
closeFind() {
|
|
333
|
+
if (!this.finding) return;
|
|
334
|
+
this.finding = null;
|
|
335
|
+
this.draw();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Look for `query` as typed, in any case; shows the first match (see openFind). {index, total}: index is 0 for
|
|
339
|
+
// the newest match.
|
|
340
|
+
setFind(query) {
|
|
341
|
+
const f = this.finding;
|
|
342
|
+
if (!f) return null;
|
|
343
|
+
f.query = query;
|
|
344
|
+
f.cols = 0; // all of it again
|
|
345
|
+
f.index = -1;
|
|
346
|
+
this.refreshFind();
|
|
347
|
+
if (f.matches.length) {
|
|
348
|
+
const at = f.matches.findIndex((m) => m.fromEnd >= f.anchor);
|
|
349
|
+
f.index = at >= 0 ? at : f.matches.length - 1;
|
|
350
|
+
this.revealMatch();
|
|
351
|
+
}
|
|
352
|
+
this.draw();
|
|
353
|
+
return this.findStatus();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The next match up (`dir` 1: older) or down (-1: newer), round from either end.
|
|
357
|
+
findStep(dir) {
|
|
358
|
+
const f = this.finding;
|
|
359
|
+
if (!f) return null;
|
|
360
|
+
this.refreshFind();
|
|
361
|
+
const n = f.matches.length;
|
|
362
|
+
if (n) {
|
|
363
|
+
f.index = ((f.index < 0 ? 0 : f.index + dir) % n + n) % n;
|
|
364
|
+
this.revealMatch();
|
|
365
|
+
}
|
|
366
|
+
this.draw();
|
|
367
|
+
return this.findStatus();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
findStatus() {
|
|
371
|
+
const f = this.finding;
|
|
372
|
+
if (!f) return null;
|
|
373
|
+
this.refreshFind();
|
|
374
|
+
return { query: f.query, index: f.index, total: f.matches.length };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// The matches again when the conversation or the width changed since they were found; the current one stays.
|
|
378
|
+
// Lines added at the end (output while the search is open) are searched on their own.
|
|
379
|
+
refreshFind() {
|
|
380
|
+
const f = this.finding;
|
|
381
|
+
const cols = this.width;
|
|
382
|
+
if (!f || (f.cols === cols && f.dropped === this.dropped && f.scanned === this.history.length)) return;
|
|
383
|
+
if (!f.query) {
|
|
384
|
+
Object.assign(f, { matches: [], byRow: new Map(), index: -1, cols, dropped: this.dropped, scanned: this.history.length });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (f.cols === cols && f.dropped === this.dropped && f.scanned < this.history.length) {
|
|
388
|
+
const added = this.scanFind(f, f.scanned, this.history.length, cols);
|
|
389
|
+
for (const m of f.matches) m.fromEnd += added.rows;
|
|
390
|
+
f.matches = [...added.matches, ...f.matches];
|
|
391
|
+
if (f.index >= 0) f.index += added.matches.length;
|
|
392
|
+
f.scanned = this.history.length;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const current = f.matches[f.index];
|
|
396
|
+
f.byRow = new Map();
|
|
397
|
+
f.matches = this.scanFind(f, 0, this.history.length, cols).matches;
|
|
398
|
+
Object.assign(f, { cols, dropped: this.dropped, scanned: this.history.length });
|
|
399
|
+
const same = current ? f.matches.findIndex((m) => m.abs === current.abs && m.offset === current.offset) : -1;
|
|
400
|
+
f.index = same >= 0 ? same : Math.min(f.index, f.matches.length - 1);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// The matches of f.query in history lines [from, to), newest first, entered in f.byRow; `rows`: the rows those
|
|
404
|
+
// lines take (fromEnd counts from the end of line to - 1).
|
|
405
|
+
scanFind(f, from, to, cols) {
|
|
406
|
+
const re = new RegExp(f.query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'giu');
|
|
407
|
+
const matches = [];
|
|
408
|
+
let rowsAfter = 0;
|
|
409
|
+
for (let line = to - 1; line >= from; line--) {
|
|
410
|
+
const rows = this.wrappedAt(line, cols);
|
|
411
|
+
if (matches.length < FIND_MAX) {
|
|
412
|
+
const plains = rows.map((r) => stripAnsi(r));
|
|
413
|
+
const text = plains.join('');
|
|
414
|
+
const starts = [];
|
|
415
|
+
for (let k = 0, at = 0; k < plains.length; at += plains[k].length, k++) starts.push(at);
|
|
416
|
+
// offset -> {sub, col}: the row it falls on, and the column there (`end`: an end offset belongs to the row before)
|
|
417
|
+
const place = (offset, end = false) => {
|
|
418
|
+
let sub = plains.length - 1;
|
|
419
|
+
while (sub > 0 && (end ? starts[sub] >= offset : starts[sub] > offset)) sub--;
|
|
420
|
+
return { sub, col: stringWidth(plains[sub].slice(0, offset - starts[sub])) };
|
|
421
|
+
};
|
|
422
|
+
const found = [];
|
|
423
|
+
for (const m of text.matchAll(re)) {
|
|
424
|
+
if (!m[0]) continue;
|
|
425
|
+
const a = place(m.index);
|
|
426
|
+
const b = place(m.index + m[0].length, true);
|
|
427
|
+
const match = { abs: this.dropped + line, offset: m.index, line, sub: a.sub, col: a.col, fromEnd: rowsAfter + rows.length - 1 - a.sub };
|
|
428
|
+
for (let sub = a.sub; sub <= b.sub; sub++) {
|
|
429
|
+
const k = `${line}:${sub}`;
|
|
430
|
+
if (!f.byRow.has(k)) f.byRow.set(k, []);
|
|
431
|
+
f.byRow.get(k).push({ from: sub === a.sub ? a.col : 0, to: sub === b.sub ? b.col : Infinity, match });
|
|
432
|
+
}
|
|
433
|
+
found.push(match);
|
|
434
|
+
}
|
|
435
|
+
matches.push(...found.reverse());
|
|
436
|
+
}
|
|
437
|
+
rowsAfter += rows.length;
|
|
438
|
+
}
|
|
439
|
+
return { matches, rows: rowsAfter };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Scroll so that the current match is on screen: where it is if it is already in view, else in the middle.
|
|
443
|
+
revealMatch() {
|
|
444
|
+
const m = this.finding?.matches[this.finding.index];
|
|
445
|
+
if (!m || !this.full) return;
|
|
446
|
+
const area = Math.max(1, this.lastArea || this.height - 6);
|
|
447
|
+
const lowest = this.scroll + (this.scroll > 0 ? 1 : 0); // the "Jump to bottom" bar covers the last row
|
|
448
|
+
if (m.fromEnd >= lowest && m.fromEnd < this.scroll + area) return;
|
|
449
|
+
const max = Math.max(0, this.totalRows(this.width) - area);
|
|
450
|
+
this.scroll = Math.max(0, Math.min(max, m.fromEnd - Math.floor(area / 2)));
|
|
451
|
+
if (!this.scroll) this.unseen = 0;
|
|
278
452
|
}
|
|
279
453
|
|
|
280
454
|
// The text selected, as it was printed: rows a long line was wrapped onto are joined again.
|
|
@@ -407,6 +581,7 @@ export class Screen {
|
|
|
407
581
|
const layout = this.layoutLive(cols);
|
|
408
582
|
const area = Math.max(0, screenRows - layout.rows.length);
|
|
409
583
|
this.lastArea = area;
|
|
584
|
+
if (this.finding) this.refreshFind();
|
|
410
585
|
const shown = this.view(area, cols);
|
|
411
586
|
this.viewMap = shown;
|
|
412
587
|
const top = shown.map((e) => (e ? this.paintRow(e) : ''));
|
|
@@ -416,7 +591,7 @@ export class Screen {
|
|
|
416
591
|
let buf = SYNC_START + ESC.hideCursor + this.titleUpdate();
|
|
417
592
|
if (full) { buf += '\x1b[2J'; this.frame = []; }
|
|
418
593
|
frame.forEach((row, i) => {
|
|
419
|
-
if (this.frame[i] !== row) buf += `\x1b[${i + 1};1H` + row
|
|
594
|
+
if (this.frame[i] !== row) buf += `\x1b[${i + 1};1H` + endRow(row);
|
|
420
595
|
});
|
|
421
596
|
if (frame.length < this.frame.length) buf += `\x1b[${frame.length + 1};1H` + ESC.clearDown;
|
|
422
597
|
this.frame = frame;
|
|
@@ -474,6 +649,7 @@ export class Screen {
|
|
|
474
649
|
const drop = this.history.length - keep;
|
|
475
650
|
this.history.splice(0, drop);
|
|
476
651
|
this.wrapped.splice(0, drop);
|
|
652
|
+
this.dropped += drop;
|
|
477
653
|
if (this.selection) {
|
|
478
654
|
for (const p of [this.selection.anchor, this.selection.focus]) p.line -= drop;
|
|
479
655
|
if (this.selection.anchor.line < 0 || this.selection.focus.line < 0) this.selection = null;
|
|
@@ -585,7 +761,7 @@ export class Screen {
|
|
|
585
761
|
}
|
|
586
762
|
const all = [...tail, ...layout.rows];
|
|
587
763
|
let buf = SYNC_START + ESC.hideCursor + this.titleUpdate() + '\x1b[H';
|
|
588
|
-
buf += all.map(
|
|
764
|
+
buf += all.map(endRow).join('\r\n');
|
|
589
765
|
buf += ESC.clearDown;
|
|
590
766
|
buf += this.settleCursor(layout);
|
|
591
767
|
buf += SYNC_END;
|
|
@@ -630,6 +806,7 @@ export class Screen {
|
|
|
630
806
|
|
|
631
807
|
clearScreen() {
|
|
632
808
|
if (this.full) {
|
|
809
|
+
this.dropped += this.history.length;
|
|
633
810
|
this.history = [];
|
|
634
811
|
this.wrapped = [];
|
|
635
812
|
this.selection = null;
|