@thedevlabs-io/labd 0.3.1 → 0.5.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/AGENTS.md +54 -18
- package/README.md +76 -66
- package/dist/labd.js +2098 -213
- package/dist/labd.js.map +4 -4
- package/package.json +1 -3
package/dist/labd.js
CHANGED
|
@@ -43,12 +43,12 @@ function scorePath(query, path) {
|
|
|
43
43
|
var FILENAME_BONUS = 20;
|
|
44
44
|
function scan(q, text) {
|
|
45
45
|
let score = 0;
|
|
46
|
-
let
|
|
46
|
+
let at2 = 0;
|
|
47
47
|
let run = 0;
|
|
48
48
|
for (const ch of q) {
|
|
49
|
-
const found = text.indexOf(ch,
|
|
49
|
+
const found = text.indexOf(ch, at2);
|
|
50
50
|
if (found === -1) return null;
|
|
51
|
-
if (found ===
|
|
51
|
+
if (found === at2 && at2 > 0) {
|
|
52
52
|
run += 1;
|
|
53
53
|
score += 4 + run * 2;
|
|
54
54
|
} else {
|
|
@@ -57,7 +57,7 @@ function scan(q, text) {
|
|
|
57
57
|
}
|
|
58
58
|
const prev = found === 0 ? "/" : text[found - 1];
|
|
59
59
|
if (prev === "/" || prev === "." || prev === "-" || prev === "_") score += 6;
|
|
60
|
-
|
|
60
|
+
at2 = found + 1;
|
|
61
61
|
}
|
|
62
62
|
return score;
|
|
63
63
|
}
|
|
@@ -454,8 +454,8 @@ function danglingToolCallAt(messages) {
|
|
|
454
454
|
return null;
|
|
455
455
|
}
|
|
456
456
|
function sendableMessages(messages) {
|
|
457
|
-
const
|
|
458
|
-
return
|
|
457
|
+
const at2 = danglingToolCallAt(messages);
|
|
458
|
+
return at2 === null ? messages : messages.slice(0, at2);
|
|
459
459
|
}
|
|
460
460
|
function forExtraction(messages) {
|
|
461
461
|
return messages.map((m) => {
|
|
@@ -678,6 +678,11 @@ function attachOne(root, path, budget) {
|
|
|
678
678
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3, writeFileSync } from "node:fs";
|
|
679
679
|
import { createHash, randomUUID } from "node:crypto";
|
|
680
680
|
import { basename as basename2, join as join2 } from "node:path";
|
|
681
|
+
function sessionTitle(summary, width = 60) {
|
|
682
|
+
const text = (summary.name ?? summary.preview ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
683
|
+
if (!text) return "(no questions yet)";
|
|
684
|
+
return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
|
|
685
|
+
}
|
|
681
686
|
function bucketFor(stateDir2, root) {
|
|
682
687
|
const bucket = join2(stateDir2, "sessions", createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
683
688
|
mkdirSync(bucket, { recursive: true });
|
|
@@ -749,6 +754,7 @@ var Session = class _Session {
|
|
|
749
754
|
id: session.id,
|
|
750
755
|
file: join2(bucket, f),
|
|
751
756
|
name: session.name,
|
|
757
|
+
preview: session.firstQuestion(),
|
|
752
758
|
at: statSync3(join2(bucket, f)).mtimeMs,
|
|
753
759
|
messages: session.nodes.size
|
|
754
760
|
};
|
|
@@ -763,6 +769,14 @@ var Session = class _Session {
|
|
|
763
769
|
size() {
|
|
764
770
|
return this.nodes.size;
|
|
765
771
|
}
|
|
772
|
+
/** The earliest question in the file, on any branch. */
|
|
773
|
+
firstQuestion() {
|
|
774
|
+
let first;
|
|
775
|
+
for (const node of this.nodes.values()) {
|
|
776
|
+
if (node.body.kind === "user" && (!first || node.at < first.at)) first = node;
|
|
777
|
+
}
|
|
778
|
+
return first?.body.kind === "user" ? first.body.text : void 0;
|
|
779
|
+
}
|
|
766
780
|
rename(name) {
|
|
767
781
|
this.name = name;
|
|
768
782
|
this.append({ t: "meta", name });
|
|
@@ -802,15 +816,15 @@ var Session = class _Session {
|
|
|
802
816
|
/** Root-to-head, which is the conversation as the model will see it. */
|
|
803
817
|
path(from = this.head) {
|
|
804
818
|
const out = [];
|
|
805
|
-
let
|
|
819
|
+
let at2 = from;
|
|
806
820
|
const seen = /* @__PURE__ */ new Set();
|
|
807
|
-
while (
|
|
808
|
-
if (seen.has(
|
|
809
|
-
seen.add(
|
|
810
|
-
const node = this.nodes.get(
|
|
821
|
+
while (at2) {
|
|
822
|
+
if (seen.has(at2)) break;
|
|
823
|
+
seen.add(at2);
|
|
824
|
+
const node = this.nodes.get(at2);
|
|
811
825
|
if (!node) break;
|
|
812
826
|
out.push(node);
|
|
813
|
-
|
|
827
|
+
at2 = node.parentId;
|
|
814
828
|
}
|
|
815
829
|
return out.reverse();
|
|
816
830
|
}
|
|
@@ -2147,21 +2161,21 @@ function wrapCommand(command, width) {
|
|
|
2147
2161
|
for (let i = 0; i < command.length; i += width) out.push(command.slice(i, i + width));
|
|
2148
2162
|
return out;
|
|
2149
2163
|
}
|
|
2150
|
-
function statusLine(
|
|
2164
|
+
function statusLine(info2) {
|
|
2151
2165
|
const bits = [];
|
|
2152
|
-
if (
|
|
2153
|
-
const crowded =
|
|
2154
|
-
bits.push((crowded ? paint.yellow : paint.muted)(`${
|
|
2166
|
+
if (info2.contextPercent !== null && info2.contextPercent !== void 0) {
|
|
2167
|
+
const crowded = info2.contextPercent >= 75;
|
|
2168
|
+
bits.push((crowded ? paint.yellow : paint.muted)(`${info2.contextPercent}% full`));
|
|
2155
2169
|
}
|
|
2156
|
-
bits.push(paint.muted(
|
|
2157
|
-
if (
|
|
2158
|
-
if (
|
|
2159
|
-
bits.push(paint.muted(`${
|
|
2170
|
+
bits.push(paint.muted(info2.project));
|
|
2171
|
+
if (info2.branch) bits.push(paint.muted(info2.branch));
|
|
2172
|
+
if (info2.creditsPercent !== null && info2.creditsPercent !== void 0) {
|
|
2173
|
+
bits.push(paint.muted(`${info2.creditsPercent}% left`));
|
|
2160
2174
|
}
|
|
2161
|
-
if (
|
|
2162
|
-
const mode =
|
|
2175
|
+
if (info2.effort) bits.push(paint.muted(`think ${info2.effort}`));
|
|
2176
|
+
const mode = info2.mode === "agent" ? paint.red(paint.bold("agent")) : info2.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
2163
2177
|
bits.push(mode);
|
|
2164
|
-
const left =
|
|
2178
|
+
const left = info2.busy ? `${paint.accent(FACE)} ${paint.dim(info2.busy)}` : paint.dim(FACE);
|
|
2165
2179
|
return `${left} ${paint.dim("|")} ${bits.join(paint.dim(" | "))}`;
|
|
2166
2180
|
}
|
|
2167
2181
|
function spinnerFrame(tick) {
|
|
@@ -2198,10 +2212,10 @@ function body(opts, inner) {
|
|
|
2198
2212
|
function withCursor(view, room, hidden) {
|
|
2199
2213
|
const text = view.text.slice(0, room);
|
|
2200
2214
|
if (hidden) return text;
|
|
2201
|
-
const
|
|
2202
|
-
const before = text.slice(0,
|
|
2203
|
-
const under = text.slice(
|
|
2204
|
-
const after = text.slice(
|
|
2215
|
+
const at2 = Math.min(view.column, room - 1 < 0 ? 0 : room);
|
|
2216
|
+
const before = text.slice(0, at2);
|
|
2217
|
+
const under = text.slice(at2, at2 + 1) || " ";
|
|
2218
|
+
const after = text.slice(at2 + 1);
|
|
2205
2219
|
return `${before}${paint.invert(under)}${after}`;
|
|
2206
2220
|
}
|
|
2207
2221
|
function suggestionLines(all, inner) {
|
|
@@ -2250,27 +2264,27 @@ function boundaries(text) {
|
|
|
2250
2264
|
for (const { index, segment } of segmenter.segment(text)) out.push(index + segment.length);
|
|
2251
2265
|
return out;
|
|
2252
2266
|
}
|
|
2253
|
-
function prevBoundary(text,
|
|
2267
|
+
function prevBoundary(text, at2) {
|
|
2254
2268
|
const marks = boundaries(text);
|
|
2255
2269
|
let last = 0;
|
|
2256
2270
|
for (const mark of marks) {
|
|
2257
|
-
if (mark >=
|
|
2271
|
+
if (mark >= at2) return last;
|
|
2258
2272
|
last = mark;
|
|
2259
2273
|
}
|
|
2260
2274
|
return last;
|
|
2261
2275
|
}
|
|
2262
|
-
function nextBoundary(text,
|
|
2263
|
-
for (const mark of boundaries(text)) if (mark >
|
|
2276
|
+
function nextBoundary(text, at2) {
|
|
2277
|
+
for (const mark of boundaries(text)) if (mark > at2) return mark;
|
|
2264
2278
|
return text.length;
|
|
2265
2279
|
}
|
|
2266
|
-
function wordStart(text,
|
|
2267
|
-
let i =
|
|
2280
|
+
function wordStart(text, at2) {
|
|
2281
|
+
let i = at2;
|
|
2268
2282
|
while (i > 0 && /\s/.test(text[i - 1])) i--;
|
|
2269
2283
|
while (i > 0 && !/\s/.test(text[i - 1])) i--;
|
|
2270
2284
|
return i;
|
|
2271
2285
|
}
|
|
2272
|
-
function wordEnd(text,
|
|
2273
|
-
let i =
|
|
2286
|
+
function wordEnd(text, at2) {
|
|
2287
|
+
let i = at2;
|
|
2274
2288
|
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2275
2289
|
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
2276
2290
|
return i;
|
|
@@ -2330,6 +2344,7 @@ var LineEditor = class {
|
|
|
2330
2344
|
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) {
|
|
2331
2345
|
return this.insert(key.sequence);
|
|
2332
2346
|
}
|
|
2347
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
2333
2348
|
return "unhandled";
|
|
2334
2349
|
}
|
|
2335
2350
|
moveTo(next) {
|
|
@@ -2398,51 +2413,696 @@ var History = class {
|
|
|
2398
2413
|
}
|
|
2399
2414
|
};
|
|
2400
2415
|
|
|
2401
|
-
// packages/tui/src/
|
|
2402
|
-
|
|
2403
|
-
" \
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
"
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2416
|
+
// packages/tui/src/editor.ts
|
|
2417
|
+
function sanitizeMultiline(text) {
|
|
2418
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
2419
|
+
}
|
|
2420
|
+
var TextEditor = class {
|
|
2421
|
+
lines = [""];
|
|
2422
|
+
row = 0;
|
|
2423
|
+
col = 0;
|
|
2424
|
+
/** First line shown in the last `view`, kept so the window scrolls rather than jumps. */
|
|
2425
|
+
top = 0;
|
|
2426
|
+
get text() {
|
|
2427
|
+
return this.lines.join("\n");
|
|
2428
|
+
}
|
|
2429
|
+
get cursor() {
|
|
2430
|
+
return { row: this.row, col: this.col };
|
|
2431
|
+
}
|
|
2432
|
+
get lineCount() {
|
|
2433
|
+
return this.lines.length;
|
|
2434
|
+
}
|
|
2435
|
+
get isEmpty() {
|
|
2436
|
+
return this.lines.length === 1 && this.lines[0] === "";
|
|
2437
|
+
}
|
|
2438
|
+
/** The line the caret is on. Used to work out whether a `/` or `@` is being typed. */
|
|
2439
|
+
get currentLine() {
|
|
2440
|
+
return this.lines[this.row];
|
|
2441
|
+
}
|
|
2442
|
+
/** Replace everything - from history, or a completion. The caret lands at the end. */
|
|
2443
|
+
setText(text) {
|
|
2444
|
+
this.lines = sanitizeMultiline(text).split("\n");
|
|
2445
|
+
this.row = this.lines.length - 1;
|
|
2446
|
+
this.col = this.lines[this.row].length;
|
|
2447
|
+
}
|
|
2448
|
+
/** Replace just the caret's line, keeping the rest. For a completion of `/cmd` or `@path`. */
|
|
2449
|
+
setCurrentLine(text) {
|
|
2450
|
+
this.lines[this.row] = sanitizeMultiline(text).replace(/\n/g, " ");
|
|
2451
|
+
this.col = this.lines[this.row].length;
|
|
2452
|
+
}
|
|
2453
|
+
reset() {
|
|
2454
|
+
this.lines = [""];
|
|
2455
|
+
this.row = 0;
|
|
2456
|
+
this.col = 0;
|
|
2457
|
+
this.top = 0;
|
|
2458
|
+
}
|
|
2459
|
+
/** Insert at the caret. Newlines in `text` become new lines. */
|
|
2460
|
+
insert(text) {
|
|
2461
|
+
const clean = sanitizeMultiline(text);
|
|
2462
|
+
if (!clean) return "same";
|
|
2463
|
+
const parts = clean.split("\n");
|
|
2464
|
+
const line = this.lines[this.row];
|
|
2465
|
+
const before = line.slice(0, this.col);
|
|
2466
|
+
const after = line.slice(this.col);
|
|
2467
|
+
if (parts.length === 1) {
|
|
2468
|
+
this.lines[this.row] = before + parts[0] + after;
|
|
2469
|
+
this.col += parts[0].length;
|
|
2470
|
+
return "text";
|
|
2471
|
+
}
|
|
2472
|
+
const inserted = [before + parts[0], ...parts.slice(1, -1), parts[parts.length - 1] + after];
|
|
2473
|
+
this.lines.splice(this.row, 1, ...inserted);
|
|
2474
|
+
this.row += parts.length - 1;
|
|
2475
|
+
this.col = parts[parts.length - 1].length;
|
|
2476
|
+
return "text";
|
|
2477
|
+
}
|
|
2478
|
+
newline() {
|
|
2479
|
+
return this.insert("\n");
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Apply one key. `unhandled` for a plain Enter (the caller sends), for Up on
|
|
2483
|
+
* the first line and Down on the last (the caller walks history), and for
|
|
2484
|
+
* anything else the editor does not claim.
|
|
2485
|
+
*/
|
|
2486
|
+
apply(key) {
|
|
2487
|
+
const { name, ctrl, meta, shift } = key;
|
|
2488
|
+
const line = this.lines[this.row];
|
|
2489
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
2490
|
+
if (name === "return") {
|
|
2491
|
+
if (shift || ctrl || meta) return this.newline();
|
|
2492
|
+
if (/(^|[^\\])\\$/.test(line)) {
|
|
2493
|
+
this.lines[this.row] = line.slice(0, -1);
|
|
2494
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2495
|
+
return this.newline();
|
|
2496
|
+
}
|
|
2497
|
+
return "unhandled";
|
|
2498
|
+
}
|
|
2499
|
+
if (ctrl && name === "j") return this.newline();
|
|
2500
|
+
if (name === "up" && !ctrl && !meta) {
|
|
2501
|
+
if (this.row === 0) return "unhandled";
|
|
2502
|
+
this.row--;
|
|
2503
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2504
|
+
return "cursor";
|
|
2505
|
+
}
|
|
2506
|
+
if (name === "down" && !ctrl && !meta) {
|
|
2507
|
+
if (this.row === this.lines.length - 1) return "unhandled";
|
|
2508
|
+
this.row++;
|
|
2509
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2510
|
+
return "cursor";
|
|
2511
|
+
}
|
|
2512
|
+
if (name === "left" && !ctrl && !meta) {
|
|
2513
|
+
if (this.col === 0) {
|
|
2514
|
+
if (this.row === 0) return "same";
|
|
2515
|
+
this.row--;
|
|
2516
|
+
this.col = this.lines[this.row].length;
|
|
2517
|
+
return "cursor";
|
|
2518
|
+
}
|
|
2519
|
+
return this.moveTo(prevBoundary(line, this.col));
|
|
2520
|
+
}
|
|
2521
|
+
if (name === "right" && !ctrl && !meta) {
|
|
2522
|
+
if (this.col >= line.length) {
|
|
2523
|
+
if (this.row === this.lines.length - 1) return "same";
|
|
2524
|
+
this.row++;
|
|
2525
|
+
this.col = 0;
|
|
2526
|
+
return "cursor";
|
|
2527
|
+
}
|
|
2528
|
+
return this.moveTo(nextBoundary(line, this.col));
|
|
2529
|
+
}
|
|
2530
|
+
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
2531
|
+
if (name === "end" || ctrl && name === "e") return this.moveTo(line.length);
|
|
2532
|
+
if ((meta || ctrl) && (name === "left" || name === "b")) return this.moveTo(wordStart(line, this.col));
|
|
2533
|
+
if ((meta || ctrl) && (name === "right" || name === "f")) return this.moveTo(wordEnd(line, this.col));
|
|
2534
|
+
if (name === "backspace" && (meta || ctrl)) {
|
|
2535
|
+
if (this.col === 0) return this.joinUp();
|
|
2536
|
+
return this.cut(wordStart(line, this.col), this.col);
|
|
2537
|
+
}
|
|
2538
|
+
if (ctrl && name === "w") {
|
|
2539
|
+
if (this.col === 0) return this.joinUp();
|
|
2540
|
+
return this.cut(wordStart(line, this.col), this.col);
|
|
2541
|
+
}
|
|
2542
|
+
if (name === "backspace") {
|
|
2543
|
+
if (this.col === 0) return this.joinUp();
|
|
2544
|
+
return this.cut(prevBoundary(line, this.col), this.col);
|
|
2545
|
+
}
|
|
2546
|
+
if (name === "delete") {
|
|
2547
|
+
if (this.col >= line.length) return this.joinDown();
|
|
2548
|
+
return this.cut(this.col, nextBoundary(line, this.col));
|
|
2549
|
+
}
|
|
2550
|
+
if (ctrl && name === "u") return this.cut(0, this.col);
|
|
2551
|
+
if (ctrl && name === "k") {
|
|
2552
|
+
if (this.col >= line.length) return this.joinDown();
|
|
2553
|
+
return this.cut(this.col, line.length);
|
|
2554
|
+
}
|
|
2555
|
+
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) return this.insert(key.sequence);
|
|
2556
|
+
return "unhandled";
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* The lines to draw and where the caret is among them.
|
|
2560
|
+
*
|
|
2561
|
+
* At most `maxLines` rows, chosen so the caret's row is always one of them;
|
|
2562
|
+
* each row scrolls horizontally on its own so a long line never wraps into the
|
|
2563
|
+
* row below and the caret never leaves the box.
|
|
2564
|
+
*/
|
|
2565
|
+
view(width, maxLines) {
|
|
2566
|
+
const rows = Math.max(1, maxLines);
|
|
2567
|
+
if (this.row < this.top) this.top = this.row;
|
|
2568
|
+
if (this.row >= this.top + rows) this.top = this.row - rows + 1;
|
|
2569
|
+
this.top = Math.max(0, Math.min(this.top, this.lines.length - rows));
|
|
2570
|
+
const shown = this.lines.slice(this.top, this.top + rows);
|
|
2571
|
+
let cursorCol = this.col;
|
|
2572
|
+
const lines = shown.map((line, i) => {
|
|
2573
|
+
if (width <= 0) return "";
|
|
2574
|
+
if (line.length <= width) return line;
|
|
2575
|
+
if (this.top + i !== this.row) return line.slice(0, width);
|
|
2576
|
+
const start = Math.max(0, this.col - width + 1);
|
|
2577
|
+
cursorCol = this.col - start;
|
|
2578
|
+
return line.slice(start, start + width);
|
|
2579
|
+
});
|
|
2580
|
+
return { lines, cursor: { row: this.row - this.top, col: cursorCol } };
|
|
2581
|
+
}
|
|
2582
|
+
moveTo(next) {
|
|
2583
|
+
if (next === this.col) return "same";
|
|
2584
|
+
this.col = next;
|
|
2585
|
+
return "cursor";
|
|
2586
|
+
}
|
|
2587
|
+
cut(from, to) {
|
|
2588
|
+
if (from >= to) return "same";
|
|
2589
|
+
const line = this.lines[this.row];
|
|
2590
|
+
this.lines[this.row] = line.slice(0, from) + line.slice(to);
|
|
2591
|
+
this.col = from;
|
|
2592
|
+
return "text";
|
|
2593
|
+
}
|
|
2594
|
+
joinUp() {
|
|
2595
|
+
if (this.row === 0) return "same";
|
|
2596
|
+
const above = this.lines[this.row - 1];
|
|
2597
|
+
this.lines.splice(this.row - 1, 2, above + this.lines[this.row]);
|
|
2598
|
+
this.row--;
|
|
2599
|
+
this.col = above.length;
|
|
2600
|
+
return "text";
|
|
2601
|
+
}
|
|
2602
|
+
joinDown() {
|
|
2603
|
+
if (this.row >= this.lines.length - 1) return "same";
|
|
2604
|
+
this.lines.splice(this.row, 2, this.lines[this.row] + this.lines[this.row + 1]);
|
|
2605
|
+
return "text";
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
// packages/tui/src/keys.ts
|
|
2610
|
+
var ESC3 = "\x1B";
|
|
2611
|
+
var PASTE_START = `${ESC3}[200~`;
|
|
2612
|
+
var PASTE_END = `${ESC3}[201~`;
|
|
2613
|
+
var CSI_FINAL = {
|
|
2614
|
+
A: "up",
|
|
2615
|
+
B: "down",
|
|
2616
|
+
C: "right",
|
|
2617
|
+
D: "left",
|
|
2618
|
+
H: "home",
|
|
2619
|
+
F: "end",
|
|
2620
|
+
Z: "tab"
|
|
2621
|
+
};
|
|
2622
|
+
var TILDE = {
|
|
2623
|
+
"1": "home",
|
|
2624
|
+
"2": "insert",
|
|
2625
|
+
"3": "delete",
|
|
2626
|
+
"4": "end",
|
|
2627
|
+
"5": "pageup",
|
|
2628
|
+
"6": "pagedown",
|
|
2629
|
+
"7": "home",
|
|
2630
|
+
"8": "end"
|
|
2631
|
+
};
|
|
2632
|
+
function modifiers(param) {
|
|
2633
|
+
const n = (Number(param) || 1) - 1;
|
|
2634
|
+
return {
|
|
2635
|
+
...n & 1 ? { shift: true } : {},
|
|
2636
|
+
...n & 2 ? { meta: true } : {},
|
|
2637
|
+
...n & 4 ? { ctrl: true } : {}
|
|
2638
|
+
};
|
|
2415
2639
|
}
|
|
2416
|
-
function
|
|
2417
|
-
|
|
2640
|
+
function coded(code, mods, sequence) {
|
|
2641
|
+
switch (code) {
|
|
2642
|
+
case 13:
|
|
2643
|
+
return { name: "return", ...mods, sequence };
|
|
2644
|
+
case 9:
|
|
2645
|
+
return { name: "tab", ...mods, sequence };
|
|
2646
|
+
case 27:
|
|
2647
|
+
return { name: "escape", ...mods, sequence };
|
|
2648
|
+
case 127:
|
|
2649
|
+
case 8:
|
|
2650
|
+
return { name: "backspace", ...mods, sequence };
|
|
2651
|
+
default:
|
|
2652
|
+
if (code >= 32) {
|
|
2653
|
+
const ch = String.fromCodePoint(code);
|
|
2654
|
+
return { name: ch.toLowerCase(), ...mods, sequence: mods.ctrl || mods.meta ? sequence : ch };
|
|
2655
|
+
}
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2418
2658
|
}
|
|
2419
|
-
function
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
);
|
|
2424
|
-
|
|
2425
|
-
const frames = [];
|
|
2426
|
-
for (let f = 1; f <= last + 1; f++) {
|
|
2427
|
-
frames.push(
|
|
2428
|
-
cells.map(
|
|
2429
|
-
(row, y) => paint.accent(
|
|
2430
|
-
row.map((char, x) => {
|
|
2431
|
-
if (char === " ") return " ";
|
|
2432
|
-
return f >= lockAt[y][x] ? char : noiseChar();
|
|
2433
|
-
}).join("")
|
|
2434
|
-
)
|
|
2435
|
-
)
|
|
2436
|
-
);
|
|
2659
|
+
function isPrefix(text, whole) {
|
|
2660
|
+
return text.length <= whole.length && whole.startsWith(text);
|
|
2661
|
+
}
|
|
2662
|
+
function partialMarker(buf, marker) {
|
|
2663
|
+
for (let n = Math.min(marker.length - 1, buf.length); n > 0; n--) {
|
|
2664
|
+
if (isPrefix(buf.slice(buf.length - n), marker)) return n;
|
|
2437
2665
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2666
|
+
return 0;
|
|
2667
|
+
}
|
|
2668
|
+
var KeyDecoder = class {
|
|
2669
|
+
/** Bytes that could still turn out to be the start of something. */
|
|
2670
|
+
pending = "";
|
|
2671
|
+
/** Inside a bracketed paste: the text so far. Null outside one. */
|
|
2672
|
+
pasting = null;
|
|
2673
|
+
/**
|
|
2674
|
+
* Decode a chunk. Sequences split across two reads are held until the next
|
|
2675
|
+
* one; a lone ESC at the END of a chunk is reported as Escape at once, because
|
|
2676
|
+
* a terminal sends a whole sequence in one write and a person pressing the key
|
|
2677
|
+
* should not have to press another to be heard.
|
|
2678
|
+
*/
|
|
2679
|
+
feed(chunk) {
|
|
2680
|
+
const out = [];
|
|
2681
|
+
let buf = this.pending + chunk;
|
|
2682
|
+
this.pending = "";
|
|
2683
|
+
let i = 0;
|
|
2684
|
+
while (i < buf.length) {
|
|
2685
|
+
if (this.pasting !== null) {
|
|
2686
|
+
const end = buf.indexOf(PASTE_END, i);
|
|
2687
|
+
if (end === -1) {
|
|
2688
|
+
const hold = partialMarker(buf, PASTE_END);
|
|
2689
|
+
this.pasting += buf.slice(i, buf.length - hold);
|
|
2690
|
+
this.pending = buf.slice(buf.length - hold);
|
|
2691
|
+
return out;
|
|
2692
|
+
}
|
|
2693
|
+
this.pasting += buf.slice(i, end);
|
|
2694
|
+
out.push({ name: "paste", paste: this.pasting.replace(/\r\n?/g, "\n") });
|
|
2695
|
+
this.pasting = null;
|
|
2696
|
+
i = end + PASTE_END.length;
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
const ch = buf[i];
|
|
2700
|
+
if (ch !== ESC3) {
|
|
2701
|
+
const code2 = ch.charCodeAt(0);
|
|
2702
|
+
if (code2 === 13) out.push({ name: "return", sequence: ch });
|
|
2703
|
+
else if (code2 === 10) out.push({ name: "j", ctrl: true, sequence: ch });
|
|
2704
|
+
else if (code2 === 9) out.push({ name: "tab", sequence: ch });
|
|
2705
|
+
else if (code2 === 127 || code2 === 8) out.push({ name: "backspace", sequence: ch });
|
|
2706
|
+
else if (code2 < 32) {
|
|
2707
|
+
if (code2 > 0) out.push({ name: String.fromCharCode(96 + code2), ctrl: true, sequence: ch });
|
|
2708
|
+
} else {
|
|
2709
|
+
let j = i + 1;
|
|
2710
|
+
while (j < buf.length) {
|
|
2711
|
+
const c = buf.charCodeAt(j);
|
|
2712
|
+
if (c < 32 || c === 127 || buf[j] === ESC3) break;
|
|
2713
|
+
j++;
|
|
2714
|
+
}
|
|
2715
|
+
out.push({ sequence: buf.slice(i, j) });
|
|
2716
|
+
i = j;
|
|
2717
|
+
continue;
|
|
2718
|
+
}
|
|
2719
|
+
i++;
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
if (i === buf.length - 1) {
|
|
2723
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
2724
|
+
i++;
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
const next = buf[i + 1];
|
|
2728
|
+
if (next === "[") {
|
|
2729
|
+
let j = i + 2;
|
|
2730
|
+
while (j < buf.length && !(buf.charCodeAt(j) >= 64 && buf.charCodeAt(j) <= 126)) j++;
|
|
2731
|
+
if (j >= buf.length) {
|
|
2732
|
+
this.pending = buf.slice(i);
|
|
2733
|
+
return out;
|
|
2734
|
+
}
|
|
2735
|
+
const seq = buf.slice(i, j + 1);
|
|
2736
|
+
const body2 = buf.slice(i + 2, j);
|
|
2737
|
+
const final = buf[j];
|
|
2738
|
+
i = j + 1;
|
|
2739
|
+
if (seq === PASTE_START) {
|
|
2740
|
+
this.pasting = "";
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
const key = decodeCsi(body2, final, seq);
|
|
2744
|
+
if (key) out.push(key);
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (next === "O") {
|
|
2748
|
+
const final = buf[i + 2];
|
|
2749
|
+
if (final === void 0) {
|
|
2750
|
+
this.pending = buf.slice(i);
|
|
2751
|
+
return out;
|
|
2752
|
+
}
|
|
2753
|
+
const name = CSI_FINAL[final];
|
|
2754
|
+
if (name) out.push({ name, sequence: buf.slice(i, i + 3) });
|
|
2755
|
+
i += 3;
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
if (next === "\x7F" || next === "\b") {
|
|
2759
|
+
out.push({ name: "backspace", meta: true, sequence: buf.slice(i, i + 2) });
|
|
2760
|
+
i += 2;
|
|
2761
|
+
continue;
|
|
2762
|
+
}
|
|
2763
|
+
if (next === ESC3) {
|
|
2764
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
2765
|
+
i++;
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
const code = next.charCodeAt(0);
|
|
2769
|
+
if (code >= 32 && code !== 127) {
|
|
2770
|
+
out.push({
|
|
2771
|
+
name: next.toLowerCase(),
|
|
2772
|
+
meta: true,
|
|
2773
|
+
...next !== next.toLowerCase() ? { shift: true } : {},
|
|
2774
|
+
sequence: buf.slice(i, i + 2)
|
|
2775
|
+
});
|
|
2776
|
+
i += 2;
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
2780
|
+
i++;
|
|
2781
|
+
}
|
|
2782
|
+
return out;
|
|
2783
|
+
}
|
|
2784
|
+
/** Anything still held back - the tail of a paste, an escape that never finished. */
|
|
2785
|
+
flush() {
|
|
2786
|
+
const out = [];
|
|
2787
|
+
if (this.pasting !== null) {
|
|
2788
|
+
out.push({ name: "paste", paste: this.pasting + this.pending });
|
|
2789
|
+
this.pasting = null;
|
|
2790
|
+
} else if (this.pending === ESC3) {
|
|
2791
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
2792
|
+
}
|
|
2793
|
+
this.pending = "";
|
|
2794
|
+
return out;
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
function decodeCsi(body2, final, sequence) {
|
|
2798
|
+
if (body2.startsWith("<") && (final === "M" || final === "m")) {
|
|
2799
|
+
const [b, x, y] = body2.slice(1).split(";").map(Number);
|
|
2800
|
+
if (final === "m") return null;
|
|
2801
|
+
if (b & 32) return null;
|
|
2802
|
+
const button = b & 3;
|
|
2803
|
+
if (b & 64) {
|
|
2804
|
+
return { name: "mouse", sequence, mouse: { kind: button === 0 ? "wheel-up" : "wheel-down", button: b, x, y } };
|
|
2805
|
+
}
|
|
2806
|
+
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
2807
|
+
}
|
|
2808
|
+
const params = body2.split(";");
|
|
2809
|
+
if (final === "~") {
|
|
2810
|
+
if (params[0] === "27") return coded(Number(params[2]), modifiers(params[1]), sequence);
|
|
2811
|
+
if (params[0] === "200" || params[0] === "201") return null;
|
|
2812
|
+
const name2 = TILDE[params[0]];
|
|
2813
|
+
return name2 ? { name: name2, ...modifiers(params[1]), sequence } : null;
|
|
2814
|
+
}
|
|
2815
|
+
if (final === "u") return coded(Number(params[0]), modifiers(params[1]), sequence);
|
|
2816
|
+
const name = CSI_FINAL[final];
|
|
2817
|
+
if (!name) return null;
|
|
2818
|
+
const mods = final === "Z" ? { shift: true } : modifiers(params[1]);
|
|
2819
|
+
return { name, ...mods, sequence };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// packages/tui/src/layout.ts
|
|
2823
|
+
var MAX_COMPOSER_ROWS = 8;
|
|
2824
|
+
var SIDEBAR_MIN_COLUMNS = 120;
|
|
2825
|
+
var SIDEBAR_WIDTH = 32;
|
|
2826
|
+
function layout(opts) {
|
|
2827
|
+
const rows = Math.max(4, opts.rows);
|
|
2828
|
+
const columns = Math.max(20, opts.columns);
|
|
2829
|
+
const wanted = Math.min(MAX_COMPOSER_ROWS, Math.max(1, opts.editorLines) + 2);
|
|
2830
|
+
const composerHeight = Math.max(3, Math.min(wanted, rows - 3));
|
|
2831
|
+
const header = { top: 0, left: 0, width: columns, height: 1 };
|
|
2832
|
+
const footer = { top: rows - 1, left: 0, width: columns, height: 1 };
|
|
2833
|
+
const composer2 = { top: rows - 1 - composerHeight, left: 0, width: columns, height: composerHeight };
|
|
2834
|
+
const transcriptHeight = Math.max(0, composer2.top - 1);
|
|
2835
|
+
const withSidebar = Boolean(opts.sidebar) && columns >= SIDEBAR_MIN_COLUMNS;
|
|
2836
|
+
const mainWidth = withSidebar ? columns - SIDEBAR_WIDTH - 1 : columns;
|
|
2837
|
+
const out = {
|
|
2838
|
+
header,
|
|
2839
|
+
transcript: { top: 1, left: 0, width: mainWidth, height: transcriptHeight },
|
|
2840
|
+
composer: { ...composer2, width: mainWidth },
|
|
2841
|
+
footer
|
|
2842
|
+
};
|
|
2843
|
+
if (withSidebar) {
|
|
2844
|
+
out.sidebar = { top: 1, left: mainWidth + 1, width: SIDEBAR_WIDTH, height: composer2.top + composerHeight - 1 };
|
|
2845
|
+
}
|
|
2846
|
+
return out;
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
// packages/tui/src/popup.ts
|
|
2850
|
+
var MAX_POPUP_ROWS = 8;
|
|
2851
|
+
var Popup = class {
|
|
2852
|
+
constructor(kind, items) {
|
|
2853
|
+
this.kind = kind;
|
|
2854
|
+
this.items = items;
|
|
2855
|
+
}
|
|
2856
|
+
selected = 0;
|
|
2857
|
+
get current() {
|
|
2858
|
+
return this.items[this.selected] ?? null;
|
|
2859
|
+
}
|
|
2860
|
+
move(delta) {
|
|
2861
|
+
if (!this.items.length) return;
|
|
2862
|
+
this.selected = (this.selected + delta + this.items.length) % this.items.length;
|
|
2863
|
+
}
|
|
2864
|
+
/** Rows to draw, at most `MAX_POPUP_ROWS`, keeping the selection in view. */
|
|
2865
|
+
render(width, max = MAX_POPUP_ROWS) {
|
|
2866
|
+
if (!this.items.length) return [];
|
|
2867
|
+
const rows = Math.max(1, max);
|
|
2868
|
+
const start = Math.max(0, Math.min(this.selected - rows + 1, this.items.length - rows));
|
|
2869
|
+
const shown = this.items.slice(start, start + rows);
|
|
2870
|
+
const column = Math.min(Math.max(...shown.map((s) => s.label.length)), Math.max(10, Math.floor(width / 2)));
|
|
2871
|
+
return shown.map((item, i) => {
|
|
2872
|
+
const isSelected = start + i === this.selected;
|
|
2873
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
2874
|
+
const label2 = item.label.padEnd(column);
|
|
2875
|
+
const room = width - column - 6;
|
|
2876
|
+
const what = item.what ? paint.muted(truncate(item.what, Math.max(0, room))) : "";
|
|
2877
|
+
const line = ` ${mark} ${isSelected ? paint.bold(paint.accent(label2)) : paint.accent(label2)} ${what}`;
|
|
2878
|
+
return pad2(truncate(line, width), width);
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2882
|
+
function pad2(text, width) {
|
|
2883
|
+
const short = width - visibleWidth(text);
|
|
2884
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// packages/tui/src/dialog.ts
|
|
2888
|
+
function pick(title, items, onSelect) {
|
|
2889
|
+
return { kind: "pick", title, items, query: "", selected: 0, onSelect };
|
|
2890
|
+
}
|
|
2891
|
+
function info(title, lines) {
|
|
2892
|
+
return { kind: "info", title, lines, scroll: 0 };
|
|
2893
|
+
}
|
|
2894
|
+
function filterItems(items, query) {
|
|
2895
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
2896
|
+
if (!words.length) return items;
|
|
2897
|
+
return items.filter((item) => {
|
|
2898
|
+
const hay = `${item.label} ${item.hint ?? ""}`.toLowerCase();
|
|
2899
|
+
return words.every((w) => hay.includes(w));
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
function dialogKey(dialog, key) {
|
|
2903
|
+
if (key.name === "escape" || key.ctrl && (key.name === "c" || key.name === "g")) return "close";
|
|
2904
|
+
if (dialog.kind === "info") {
|
|
2905
|
+
if (key.name === "up" || key.name === "k") dialog.scroll = Math.max(0, dialog.scroll - 1);
|
|
2906
|
+
else if (key.name === "down" || key.name === "j") dialog.scroll++;
|
|
2907
|
+
else if (key.name === "pageup") dialog.scroll = Math.max(0, dialog.scroll - 10);
|
|
2908
|
+
else if (key.name === "pagedown") dialog.scroll += 10;
|
|
2909
|
+
else if (key.name === "home") dialog.scroll = 0;
|
|
2910
|
+
else if (key.name === "return" || key.name === "q") return "close";
|
|
2911
|
+
else if (key.mouse?.kind === "wheel-up") dialog.scroll = Math.max(0, dialog.scroll - 3);
|
|
2912
|
+
else if (key.mouse?.kind === "wheel-down") dialog.scroll += 3;
|
|
2913
|
+
else return "same";
|
|
2914
|
+
return "changed";
|
|
2915
|
+
}
|
|
2916
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
2917
|
+
if (key.name === "up" || key.mouse?.kind === "wheel-up") {
|
|
2918
|
+
dialog.selected = shown.length ? (dialog.selected - 1 + shown.length) % shown.length : 0;
|
|
2919
|
+
return "changed";
|
|
2920
|
+
}
|
|
2921
|
+
if (key.name === "down" || key.mouse?.kind === "wheel-down" || key.name === "tab") {
|
|
2922
|
+
dialog.selected = shown.length ? (dialog.selected + 1) % shown.length : 0;
|
|
2923
|
+
return "changed";
|
|
2924
|
+
}
|
|
2925
|
+
if (key.name === "return") {
|
|
2926
|
+
const item = shown[dialog.selected];
|
|
2927
|
+
if (!item) return "same";
|
|
2928
|
+
dialog.onSelect(item);
|
|
2929
|
+
return "select";
|
|
2930
|
+
}
|
|
2931
|
+
if (key.name === "backspace") {
|
|
2932
|
+
if (!dialog.query) return "same";
|
|
2933
|
+
dialog.query = dialog.query.slice(0, -1);
|
|
2934
|
+
dialog.selected = 0;
|
|
2935
|
+
return "changed";
|
|
2936
|
+
}
|
|
2937
|
+
if (!key.ctrl && !key.meta && key.sequence && /^[^\u0000-\u001f\u007f]+$/.test(key.sequence)) {
|
|
2938
|
+
dialog.query += key.sequence;
|
|
2939
|
+
dialog.selected = 0;
|
|
2940
|
+
return "changed";
|
|
2941
|
+
}
|
|
2942
|
+
return "same";
|
|
2943
|
+
}
|
|
2944
|
+
var BOX = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
|
|
2945
|
+
function renderDialog(dialog, rows, columns) {
|
|
2946
|
+
const width = Math.max(24, Math.min(columns - 4, 84));
|
|
2947
|
+
const inner = width - 4;
|
|
2948
|
+
const maxBody = Math.max(3, rows - 6);
|
|
2949
|
+
const body2 = [];
|
|
2950
|
+
if (dialog.kind === "pick") {
|
|
2951
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
2952
|
+
const listRows = Math.max(1, Math.min(shown.length, maxBody - 2));
|
|
2953
|
+
const start = Math.max(0, Math.min(dialog.selected - listRows + 1, shown.length - listRows));
|
|
2954
|
+
body2.push(`${paint.accent("search")} ${dialog.query}${paint.invert(" ")}`);
|
|
2955
|
+
body2.push("");
|
|
2956
|
+
if (!shown.length) body2.push(paint.muted("nothing matches"));
|
|
2957
|
+
for (const [i, item] of shown.slice(start, start + listRows).entries()) {
|
|
2958
|
+
const isSelected = start + i === dialog.selected;
|
|
2959
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
2960
|
+
const label2 = isSelected ? paint.bold(item.label) : item.label;
|
|
2961
|
+
body2.push(truncate(`${mark} ${label2}${item.hint ? ` ${paint.muted(item.hint)}` : ""}`, inner));
|
|
2962
|
+
}
|
|
2963
|
+
if (shown.length > listRows) body2.push(paint.dim(`\u2026 ${shown.length - listRows} more; type to narrow`));
|
|
2964
|
+
} else {
|
|
2965
|
+
const visible = Math.min(dialog.lines.length, maxBody);
|
|
2966
|
+
dialog.scroll = Math.max(0, Math.min(dialog.scroll, dialog.lines.length - visible));
|
|
2967
|
+
for (const line of dialog.lines.slice(dialog.scroll, dialog.scroll + visible)) body2.push(truncate(line, inner));
|
|
2968
|
+
if (dialog.lines.length > visible) {
|
|
2969
|
+
body2.push(paint.dim(`\u2026 ${dialog.scroll + visible} of ${dialog.lines.length} lines \xB7 \u2191\u2193 to scroll`));
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
const title = ` ${dialog.title} `;
|
|
2973
|
+
const rule = BOX.h.repeat(Math.max(0, width - 2 - title.length - 1));
|
|
2974
|
+
const lines = [
|
|
2975
|
+
paint.dim(`${BOX.tl}${BOX.h}`) + paint.bold(title) + paint.dim(`${rule}${BOX.tr}`),
|
|
2976
|
+
...body2.map((line) => `${paint.dim(BOX.v)} ${pad3(truncate(line, inner), inner)} ${paint.dim(BOX.v)}`),
|
|
2977
|
+
paint.dim(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`)
|
|
2978
|
+
];
|
|
2979
|
+
const top = Math.max(0, Math.floor((rows - lines.length) / 2));
|
|
2980
|
+
const left = Math.max(0, Math.floor((columns - width) / 2));
|
|
2981
|
+
return { top, left, lines };
|
|
2982
|
+
}
|
|
2983
|
+
function pad3(text, width) {
|
|
2984
|
+
const short = width - visibleWidth(text);
|
|
2985
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// packages/tui/src/screen.ts
|
|
2989
|
+
import { EventEmitter } from "node:events";
|
|
2990
|
+
var ESC4 = "\x1B";
|
|
2991
|
+
var ALT_SCREEN_ON = `${ESC4}[?1049h`;
|
|
2992
|
+
var ALT_SCREEN_OFF = `${ESC4}[?1049l`;
|
|
2993
|
+
var CURSOR_HIDE = `${ESC4}[?25l`;
|
|
2994
|
+
var CURSOR_SHOW = `${ESC4}[?25h`;
|
|
2995
|
+
var PASTE_ON = `${ESC4}[?2004h`;
|
|
2996
|
+
var PASTE_OFF = `${ESC4}[?2004l`;
|
|
2997
|
+
var MOUSE_ON = `${ESC4}[?1000h${ESC4}[?1006h`;
|
|
2998
|
+
var MOUSE_OFF = `${ESC4}[?1006l${ESC4}[?1000l`;
|
|
2999
|
+
var CLEAR = `${ESC4}[2J`;
|
|
3000
|
+
var RESET = `${ESC4}[0m`;
|
|
3001
|
+
var at = (row, col) => `${ESC4}[${row + 1};${col + 1}H`;
|
|
3002
|
+
var Screen = class extends EventEmitter {
|
|
3003
|
+
constructor(out = process.stdout, input = process.stdin, opts = {}) {
|
|
3004
|
+
super();
|
|
3005
|
+
this.out = out;
|
|
3006
|
+
this.input = input;
|
|
3007
|
+
this.opts = opts;
|
|
3008
|
+
}
|
|
3009
|
+
last = null;
|
|
3010
|
+
lastSize = "";
|
|
3011
|
+
entered = false;
|
|
3012
|
+
decoder = new KeyDecoder();
|
|
3013
|
+
get rows() {
|
|
3014
|
+
const n = this.opts.rows ?? this.out.rows;
|
|
3015
|
+
return typeof n === "number" && n > 0 ? n : 24;
|
|
3016
|
+
}
|
|
3017
|
+
get columns() {
|
|
3018
|
+
const n = this.opts.columns ?? this.out.columns;
|
|
3019
|
+
return typeof n === "number" && n > 0 ? n : 80;
|
|
3020
|
+
}
|
|
3021
|
+
/** Take the terminal: alternate screen, raw keys, bracketed paste, the mouse if asked. */
|
|
3022
|
+
enter() {
|
|
3023
|
+
if (this.entered) return;
|
|
3024
|
+
this.entered = true;
|
|
3025
|
+
this.out.write(`${ALT_SCREEN_ON}${CURSOR_HIDE}${CLEAR}${PASTE_ON}${this.opts.mouse ? MOUSE_ON : ""}`);
|
|
3026
|
+
try {
|
|
3027
|
+
this.input.setRawMode?.(true);
|
|
3028
|
+
} catch {
|
|
3029
|
+
}
|
|
3030
|
+
this.input.setEncoding?.("utf8");
|
|
3031
|
+
this.input.on("data", this.onData);
|
|
3032
|
+
this.input.on("end", this.onEnd);
|
|
3033
|
+
this.input.resume();
|
|
3034
|
+
this.out.on?.("resize", this.onResize);
|
|
3035
|
+
process.once("exit", this.leave);
|
|
3036
|
+
}
|
|
3037
|
+
/** Give the terminal back, in the reverse order. Safe to call more than once. */
|
|
3038
|
+
leave = () => {
|
|
3039
|
+
if (!this.entered) return;
|
|
3040
|
+
this.entered = false;
|
|
3041
|
+
process.off("exit", this.leave);
|
|
3042
|
+
this.out.off?.("resize", this.onResize);
|
|
3043
|
+
this.input.off("data", this.onData);
|
|
3044
|
+
this.input.off("end", this.onEnd);
|
|
3045
|
+
try {
|
|
3046
|
+
this.input.pause();
|
|
3047
|
+
this.input.setRawMode?.(false);
|
|
3048
|
+
} catch {
|
|
3049
|
+
}
|
|
3050
|
+
this.out.write(`${this.opts.mouse ? MOUSE_OFF : ""}${PASTE_OFF}${RESET}${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
|
|
3051
|
+
this.last = null;
|
|
3052
|
+
};
|
|
3053
|
+
/**
|
|
3054
|
+
* Draw a whole frame, writing only the rows that changed.
|
|
3055
|
+
*
|
|
3056
|
+
* `frame` is read as `rows` lines; missing ones are blank, extra ones are
|
|
3057
|
+
* dropped. A size change since the last paint clears and redraws everything,
|
|
3058
|
+
* because a shorter line leaves the old one's tail where a diff cannot see it.
|
|
3059
|
+
*/
|
|
3060
|
+
paint(frame, cursor = null) {
|
|
3061
|
+
if (!this.entered) return;
|
|
3062
|
+
const rows = this.rows;
|
|
3063
|
+
const columns = this.columns;
|
|
3064
|
+
const size = `${rows}x${columns}`;
|
|
3065
|
+
let out = CURSOR_HIDE;
|
|
3066
|
+
if (!this.last || this.lastSize !== size) {
|
|
3067
|
+
out += CLEAR;
|
|
3068
|
+
this.last = new Array(rows).fill(null);
|
|
3069
|
+
this.lastSize = size;
|
|
3070
|
+
}
|
|
3071
|
+
for (let r = 0; r < rows; r++) {
|
|
3072
|
+
const line = fit(frame[r] ?? "", columns);
|
|
3073
|
+
if (this.last[r] === line) continue;
|
|
3074
|
+
this.last[r] = line;
|
|
3075
|
+
out += `${at(r, 0)}${line}${RESET}`;
|
|
3076
|
+
}
|
|
3077
|
+
if (cursor) out += `${at(Math.min(cursor.row, rows - 1), Math.min(cursor.col, columns - 1))}${CURSOR_SHOW}`;
|
|
3078
|
+
this.out.write(out);
|
|
3079
|
+
}
|
|
3080
|
+
/** Forget the last frame, so the next paint redraws every row. For ctrl-l. */
|
|
3081
|
+
invalidate() {
|
|
3082
|
+
this.last = null;
|
|
3083
|
+
}
|
|
3084
|
+
onData = (chunk) => {
|
|
3085
|
+
const keys = this.decoder.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
3086
|
+
if (keys.length) this.emit("keys", keys);
|
|
3087
|
+
};
|
|
3088
|
+
onEnd = () => {
|
|
3089
|
+
this.emit("end");
|
|
3090
|
+
};
|
|
3091
|
+
onResize = () => {
|
|
3092
|
+
this.last = null;
|
|
3093
|
+
this.emit("resize");
|
|
3094
|
+
};
|
|
3095
|
+
};
|
|
3096
|
+
function fit(line, width) {
|
|
3097
|
+
const cut = truncate(line, width);
|
|
3098
|
+
const short = width - visibleWidth(cut);
|
|
3099
|
+
return short > 0 ? `${cut}${" ".repeat(short)}` : cut;
|
|
2440
3100
|
}
|
|
2441
3101
|
|
|
2442
3102
|
// packages/tui/src/markdown.ts
|
|
2443
3103
|
function renderMarkdown(text, opts) {
|
|
2444
3104
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
2445
|
-
const
|
|
3105
|
+
const pad4 = opts.indent ?? "";
|
|
2446
3106
|
const out = [];
|
|
2447
3107
|
let inFence = false;
|
|
2448
3108
|
let fenceLang = "";
|
|
@@ -2452,7 +3112,7 @@ function renderMarkdown(text, opts) {
|
|
|
2452
3112
|
if (!inFence) {
|
|
2453
3113
|
inFence = true;
|
|
2454
3114
|
fenceLang = fence[1] ?? "";
|
|
2455
|
-
out.push(paint.dim(`${
|
|
3115
|
+
out.push(paint.dim(`${pad4}${fenceLang || "code"}`));
|
|
2456
3116
|
} else {
|
|
2457
3117
|
inFence = false;
|
|
2458
3118
|
fenceLang = "";
|
|
@@ -2461,36 +3121,36 @@ function renderMarkdown(text, opts) {
|
|
|
2461
3121
|
continue;
|
|
2462
3122
|
}
|
|
2463
3123
|
if (inFence) {
|
|
2464
|
-
out.push(`${paint.dim(`${
|
|
3124
|
+
out.push(`${paint.dim(`${pad4}|`)} ${line}`);
|
|
2465
3125
|
continue;
|
|
2466
3126
|
}
|
|
2467
|
-
out.push(...prose(line, opts.width,
|
|
3127
|
+
out.push(...prose(line, opts.width, pad4));
|
|
2468
3128
|
}
|
|
2469
3129
|
return out.join("\n");
|
|
2470
3130
|
}
|
|
2471
|
-
function prose(line, width,
|
|
3131
|
+
function prose(line, width, pad4) {
|
|
2472
3132
|
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
2473
|
-
if (heading) return [`${
|
|
3133
|
+
if (heading) return [`${pad4}${paint.bold(paint.accent(heading[2]))}`];
|
|
2474
3134
|
if (/^\s*([-*_])\s*\1\s*\1[\s-*_]*$/.test(line)) {
|
|
2475
|
-
return [`${
|
|
3135
|
+
return [`${pad4}${paint.dim("-".repeat(Math.min(width, 40)))}`];
|
|
2476
3136
|
}
|
|
2477
3137
|
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
2478
3138
|
if (bullet) {
|
|
2479
|
-
const indent2 = `${
|
|
2480
|
-
return wrap2(inline(bullet[2]), width, indent2, `${
|
|
3139
|
+
const indent2 = `${pad4}${bullet[1]}${paint.accent("-")} `;
|
|
3140
|
+
return wrap2(inline(bullet[2]), width, indent2, `${pad4}${bullet[1]} `);
|
|
2481
3141
|
}
|
|
2482
3142
|
const numbered = /^(\s*)(\d+)\.\s+(.*)$/.exec(line);
|
|
2483
3143
|
if (numbered) {
|
|
2484
|
-
const indent2 = `${
|
|
2485
|
-
return wrap2(inline(numbered[3]), width, indent2, `${
|
|
3144
|
+
const indent2 = `${pad4}${numbered[1]}${paint.accent(`${numbered[2]}.`)} `;
|
|
3145
|
+
return wrap2(inline(numbered[3]), width, indent2, `${pad4}${numbered[1]} `);
|
|
2486
3146
|
}
|
|
2487
3147
|
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
2488
3148
|
if (quote) {
|
|
2489
|
-
const gutter = `${
|
|
3149
|
+
const gutter = `${pad4}${paint.dim("|")} `;
|
|
2490
3150
|
return wrap2(paint.muted(inline(quote[1])), width, gutter, gutter);
|
|
2491
3151
|
}
|
|
2492
3152
|
if (!line.trim()) return [""];
|
|
2493
|
-
return wrap2(inline(line), width,
|
|
3153
|
+
return wrap2(inline(line), width, pad4, pad4);
|
|
2494
3154
|
}
|
|
2495
3155
|
var HOLD = "\0";
|
|
2496
3156
|
function inline(text) {
|
|
@@ -2522,55 +3182,301 @@ function wrap2(text, width, first, rest) {
|
|
|
2522
3182
|
return out;
|
|
2523
3183
|
}
|
|
2524
3184
|
|
|
2525
|
-
// packages/tui/src/
|
|
2526
|
-
|
|
2527
|
-
var
|
|
2528
|
-
var
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
this.
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
this.
|
|
2544
|
-
|
|
2545
|
-
|
|
3185
|
+
// packages/tui/src/transcript.ts
|
|
3186
|
+
var MAX_PROSE_WIDTH = 100;
|
|
3187
|
+
var LIVE_LINES = 8;
|
|
3188
|
+
var Transcript = class {
|
|
3189
|
+
entries = [];
|
|
3190
|
+
nextId = 1;
|
|
3191
|
+
cache = /* @__PURE__ */ new Map();
|
|
3192
|
+
/** For a streaming answer: the rendered COMPLETE part, so only the tail is re-rendered per chunk. */
|
|
3193
|
+
prose = /* @__PURE__ */ new Map();
|
|
3194
|
+
get length() {
|
|
3195
|
+
return this.entries.length;
|
|
3196
|
+
}
|
|
3197
|
+
get blocks() {
|
|
3198
|
+
return this.entries;
|
|
3199
|
+
}
|
|
3200
|
+
/** Typed by kind rather than by the literal passed, so `done: false` stays a boolean the caller can flip. */
|
|
3201
|
+
add(block) {
|
|
3202
|
+
const entry = { id: this.nextId++, version: 0, block };
|
|
3203
|
+
this.entries.push(entry);
|
|
3204
|
+
return entry;
|
|
3205
|
+
}
|
|
3206
|
+
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
3207
|
+
touch(entry) {
|
|
3208
|
+
entry.version++;
|
|
3209
|
+
}
|
|
3210
|
+
/** Forget everything shown. The session file is untouched; this is the view. */
|
|
3211
|
+
clear() {
|
|
3212
|
+
this.entries = [];
|
|
3213
|
+
this.cache.clear();
|
|
3214
|
+
this.prose.clear();
|
|
3215
|
+
}
|
|
3216
|
+
/** Every line, top to bottom, with one blank line between blocks. */
|
|
3217
|
+
render(width) {
|
|
3218
|
+
const out = [];
|
|
3219
|
+
const live = /* @__PURE__ */ new Set();
|
|
3220
|
+
for (const entry of this.entries) {
|
|
3221
|
+
live.add(entry.id);
|
|
3222
|
+
const lines = this.lines(entry, width);
|
|
3223
|
+
if (out.length && lines.length) out.push("");
|
|
3224
|
+
out.push(...lines);
|
|
3225
|
+
}
|
|
3226
|
+
for (const id of this.cache.keys()) if (!live.has(id)) this.cache.delete(id);
|
|
3227
|
+
for (const id of this.prose.keys()) if (!live.has(id)) this.prose.delete(id);
|
|
3228
|
+
return out;
|
|
3229
|
+
}
|
|
3230
|
+
lines(entry, width) {
|
|
3231
|
+
const hit = this.cache.get(entry.id);
|
|
3232
|
+
if (hit && hit.version === entry.version && hit.width === width) return hit.lines;
|
|
3233
|
+
const lines = entry.block.kind === "assistant" ? this.assistant(entry, width) : renderBlock(entry.block, width);
|
|
3234
|
+
this.cache.set(entry.id, { version: entry.version, width, lines });
|
|
3235
|
+
return lines;
|
|
2546
3236
|
}
|
|
2547
|
-
editor = new LineEditor();
|
|
2548
|
-
history = new History();
|
|
2549
|
-
resolve = null;
|
|
2550
|
-
busyLabel = null;
|
|
2551
|
-
label = null;
|
|
2552
|
-
pasting = false;
|
|
2553
|
-
/** stdin has closed. Every later `read` resolves `exit` at once. */
|
|
2554
|
-
ended = false;
|
|
2555
|
-
closed = false;
|
|
2556
3237
|
/**
|
|
2557
|
-
*
|
|
3238
|
+
* An answer, streaming or finished.
|
|
2558
3239
|
*
|
|
2559
|
-
*
|
|
2560
|
-
*
|
|
2561
|
-
*
|
|
3240
|
+
* Complete markdown blocks are rendered; the unfinished tail - a line still
|
|
3241
|
+
* arriving, a fence not yet closed - is shown raw and dimmed, so the reader
|
|
3242
|
+
* sees the words land without a heading flickering between plain and bold.
|
|
2562
3243
|
*/
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
this.
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
3244
|
+
assistant(entry, width) {
|
|
3245
|
+
const inner = proseWidth(width);
|
|
3246
|
+
const { text, done } = entry.block;
|
|
3247
|
+
if (done) return text.trim() ? renderMarkdown(text.replace(/\s+$/, ""), { width: inner, indent: " " }).split("\n") : [];
|
|
3248
|
+
const { complete: complete2, tail } = splitStreaming(text);
|
|
3249
|
+
let head = this.prose.get(entry.id);
|
|
3250
|
+
if (!head || head.length !== complete2.length || head.width !== width) {
|
|
3251
|
+
head = {
|
|
3252
|
+
length: complete2.length,
|
|
3253
|
+
width,
|
|
3254
|
+
lines: complete2 ? renderMarkdown(complete2, { width: inner, indent: " " }).split("\n") : []
|
|
3255
|
+
};
|
|
3256
|
+
this.prose.set(entry.id, head);
|
|
3257
|
+
}
|
|
3258
|
+
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
3259
|
+
return [...head.lines, ...raw];
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
function splitStreaming(text) {
|
|
3263
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
3264
|
+
let tail = lines.pop() ?? "";
|
|
3265
|
+
let open = -1;
|
|
3266
|
+
lines.forEach((line, i) => {
|
|
3267
|
+
if (/^\s*```/.test(line)) open = open === -1 ? i : -1;
|
|
3268
|
+
});
|
|
3269
|
+
let complete2 = lines;
|
|
3270
|
+
if (open !== -1) {
|
|
3271
|
+
tail = [...lines.slice(open), tail].join("\n");
|
|
3272
|
+
complete2 = lines.slice(0, open);
|
|
3273
|
+
}
|
|
3274
|
+
return { complete: complete2.join("\n"), tail };
|
|
3275
|
+
}
|
|
3276
|
+
function proseWidth(width) {
|
|
3277
|
+
return Math.max(20, Math.min(width - 2, MAX_PROSE_WIDTH));
|
|
3278
|
+
}
|
|
3279
|
+
function renderBlock(block, width) {
|
|
3280
|
+
const inner = proseWidth(width);
|
|
3281
|
+
switch (block.kind) {
|
|
3282
|
+
case "user":
|
|
3283
|
+
return block.text.split("\n").flatMap(
|
|
3284
|
+
(line, i) => wrapPlain(line, inner - 2).map((piece, j) => i === 0 && j === 0 ? `${paint.accent(">")} ${piece}` : ` ${piece}`)
|
|
3285
|
+
);
|
|
3286
|
+
case "assistant":
|
|
3287
|
+
return [];
|
|
3288
|
+
case "tool": {
|
|
3289
|
+
const out = toolBlock(block.name, block.detail, block.status, { input: block.input, output: block.output }, inner);
|
|
3290
|
+
if (block.live.length && !block.output) {
|
|
3291
|
+
const shown = block.live.slice(-LIVE_LINES);
|
|
3292
|
+
if (block.live.length > LIVE_LINES) out.push(` ${paint.dim(`\u2026 ${block.live.length - LIVE_LINES} more lines`)}`);
|
|
3293
|
+
for (const line of shown) out.push(` ${paint.dim(line)}`);
|
|
3294
|
+
}
|
|
3295
|
+
return out;
|
|
3296
|
+
}
|
|
3297
|
+
case "approval": {
|
|
3298
|
+
const out = approvalCard(block.preview, block.call, block.mode, inner);
|
|
3299
|
+
out.push(
|
|
3300
|
+
block.answer === null ? ` ${paint.yellow("y")} ${paint.muted("to allow \xB7")} ${paint.yellow("n")} ${paint.muted("or esc to decline")}` : ` ${block.answer === "allowed" ? paint.green("allowed") : paint.muted("declined")}`
|
|
3301
|
+
);
|
|
3302
|
+
return out;
|
|
3303
|
+
}
|
|
3304
|
+
case "error":
|
|
3305
|
+
return block.text.split("\n").map((line, i) => i === 0 ? `${paint.red(" labd:")} ${line}` : ` ${line}`);
|
|
3306
|
+
case "note":
|
|
3307
|
+
return block.text.replace(/\n$/, "").split("\n");
|
|
3308
|
+
case "compaction": {
|
|
3309
|
+
const label2 = " summarised the conversation so far ";
|
|
3310
|
+
const room = Math.max(0, inner - label2.length);
|
|
3311
|
+
return [paint.dim(` ${"\u2500".repeat(Math.floor(room / 2))}${label2}${"\u2500".repeat(Math.ceil(room / 2))}`)];
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function wrapPlain(text, width) {
|
|
3316
|
+
const limit = Math.max(10, width);
|
|
3317
|
+
if (visibleWidth(text) <= limit) return [text];
|
|
3318
|
+
const out = [];
|
|
3319
|
+
let line = "";
|
|
3320
|
+
for (const word of text.split(" ")) {
|
|
3321
|
+
if (line && line.length + 1 + word.length > limit) {
|
|
3322
|
+
out.push(line);
|
|
3323
|
+
line = word;
|
|
3324
|
+
} else {
|
|
3325
|
+
line = line ? `${line} ${word}` : word;
|
|
3326
|
+
}
|
|
3327
|
+
while (line.length > limit) {
|
|
3328
|
+
out.push(line.slice(0, limit));
|
|
3329
|
+
line = line.slice(limit);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (line || !out.length) out.push(line);
|
|
3333
|
+
return out;
|
|
3334
|
+
}
|
|
3335
|
+
var Viewport = class {
|
|
3336
|
+
constructor(height) {
|
|
3337
|
+
this.height = height;
|
|
3338
|
+
}
|
|
3339
|
+
top = 0;
|
|
3340
|
+
follow = true;
|
|
3341
|
+
total = 0;
|
|
3342
|
+
/** The line count when the reader stopped following, so `newLines` can be counted from it. */
|
|
3343
|
+
anchor = 0;
|
|
3344
|
+
/** Lines that arrived since the reader scrolled away. Zero while following. */
|
|
3345
|
+
get newLines() {
|
|
3346
|
+
return this.follow ? 0 : Math.max(0, this.total - this.anchor);
|
|
3347
|
+
}
|
|
3348
|
+
/** Tell the viewport how many lines there are now, and how tall it is. */
|
|
3349
|
+
update(total, height = this.height) {
|
|
3350
|
+
this.height = height;
|
|
3351
|
+
this.total = total;
|
|
3352
|
+
if (this.follow) this.top = Math.max(0, total - height);
|
|
3353
|
+
else this.top = Math.max(0, Math.min(this.top, total - height));
|
|
3354
|
+
}
|
|
3355
|
+
scroll(delta) {
|
|
3356
|
+
const max = Math.max(0, this.total - this.height);
|
|
3357
|
+
const next = Math.max(0, Math.min(max, this.top + delta));
|
|
3358
|
+
if (next === this.top && this.follow) return;
|
|
3359
|
+
this.top = next;
|
|
3360
|
+
if (next >= max) this.refollow();
|
|
3361
|
+
else this.unfollow();
|
|
3362
|
+
}
|
|
3363
|
+
pageUp() {
|
|
3364
|
+
this.scroll(-Math.max(1, this.height - 1));
|
|
3365
|
+
}
|
|
3366
|
+
pageDown() {
|
|
3367
|
+
this.scroll(Math.max(1, this.height - 1));
|
|
3368
|
+
}
|
|
3369
|
+
home() {
|
|
3370
|
+
this.top = 0;
|
|
3371
|
+
if (this.total > this.height) this.unfollow();
|
|
3372
|
+
}
|
|
3373
|
+
end() {
|
|
3374
|
+
this.refollow();
|
|
3375
|
+
this.top = Math.max(0, this.total - this.height);
|
|
3376
|
+
}
|
|
3377
|
+
/** The slice of `lines` on screen. */
|
|
3378
|
+
slice(lines) {
|
|
3379
|
+
return lines.slice(this.top, this.top + this.height);
|
|
3380
|
+
}
|
|
3381
|
+
unfollow() {
|
|
3382
|
+
if (this.follow) this.anchor = this.total;
|
|
3383
|
+
this.follow = false;
|
|
3384
|
+
}
|
|
3385
|
+
refollow() {
|
|
3386
|
+
this.follow = true;
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
// packages/tui/src/face.ts
|
|
3391
|
+
var FACE_ART = [
|
|
3392
|
+
" \u250F\u2501 \u2501\u2513 ",
|
|
3393
|
+
" \u2503 \u2584\u2584\u2584\u2584\u2584 \u2584\u2584\u2584\u2584\u2584 \u2503 ",
|
|
3394
|
+
" \u2503 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2503 ",
|
|
3395
|
+
" \u2503 \u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580 \u2503 ",
|
|
3396
|
+
" \u2503 \u2503 ",
|
|
3397
|
+
" \u2503 \u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584 \u2503 ",
|
|
3398
|
+
" \u2517\u2501 \u2501\u251B "
|
|
3399
|
+
];
|
|
3400
|
+
var NOISE = "$#@%&*+=-:.<>/\\|_01x?";
|
|
3401
|
+
var FACE_FRAME_MS = 34;
|
|
3402
|
+
function noiseChar() {
|
|
3403
|
+
return NOISE[Math.floor(Math.random() * NOISE.length)];
|
|
3404
|
+
}
|
|
3405
|
+
function faceLines() {
|
|
3406
|
+
return FACE_ART.map((row) => paint.accent(row));
|
|
3407
|
+
}
|
|
3408
|
+
function decodeFrames() {
|
|
3409
|
+
const cells = FACE_ART.map((row) => [...row]);
|
|
3410
|
+
const lockAt = cells.map(
|
|
3411
|
+
(row, y) => row.map((_, x) => Math.round(x * 0.55 + y * 0.9 + Math.random() * 7))
|
|
3412
|
+
);
|
|
3413
|
+
const last = Math.max(...lockAt.flat());
|
|
3414
|
+
const frames = [];
|
|
3415
|
+
for (let f = 1; f <= last + 1; f++) {
|
|
3416
|
+
frames.push(
|
|
3417
|
+
cells.map(
|
|
3418
|
+
(row, y) => paint.accent(
|
|
3419
|
+
row.map((char, x) => {
|
|
3420
|
+
if (char === " ") return " ";
|
|
3421
|
+
return f >= lockAt[y][x] ? char : noiseChar();
|
|
3422
|
+
}).join("")
|
|
3423
|
+
)
|
|
3424
|
+
)
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
frames.push(faceLines());
|
|
3428
|
+
return frames;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// packages/tui/src/prompt.ts
|
|
3432
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3433
|
+
var ESC5 = "\x1B";
|
|
3434
|
+
var PASTE_ON2 = `${ESC5}[?2004h`;
|
|
3435
|
+
var PASTE_OFF2 = `${ESC5}[?2004l`;
|
|
3436
|
+
var PASTE_START2 = `${ESC5}[200~`;
|
|
3437
|
+
var PASTE_END2 = `${ESC5}[201~`;
|
|
3438
|
+
var Prompt = class {
|
|
3439
|
+
constructor(renderer, hooks, input = process.stdin) {
|
|
3440
|
+
this.renderer = renderer;
|
|
3441
|
+
this.hooks = hooks;
|
|
3442
|
+
this.input = input;
|
|
3443
|
+
emitKeypressEvents(this.input);
|
|
3444
|
+
if (this.input.isTTY) this.input.setRawMode(true);
|
|
3445
|
+
this.input.on("keypress", this.onKey);
|
|
3446
|
+
this.input.on("end", this.onEnd);
|
|
3447
|
+
this.input.resume();
|
|
3448
|
+
if (this.renderer.isTty) process.stdout.write(PASTE_ON2);
|
|
3449
|
+
this.renderer.hideCursor();
|
|
3450
|
+
process.stdout.on("resize", this.onResize);
|
|
3451
|
+
process.once("exit", this.restore);
|
|
3452
|
+
}
|
|
3453
|
+
editor = new LineEditor();
|
|
3454
|
+
history = new History();
|
|
3455
|
+
resolve = null;
|
|
3456
|
+
busyLabel = null;
|
|
3457
|
+
label = null;
|
|
3458
|
+
pasting = false;
|
|
3459
|
+
/** stdin has closed. Every later `read` resolves `exit` at once. */
|
|
3460
|
+
ended = false;
|
|
3461
|
+
closed = false;
|
|
3462
|
+
/**
|
|
3463
|
+
* Wait for a line.
|
|
3464
|
+
*
|
|
3465
|
+
* `label` replaces the placeholder and turns the border yellow - used for the
|
|
3466
|
+
* approval question, which is a different kind of asking and should not look
|
|
3467
|
+
* like the ordinary prompt.
|
|
3468
|
+
*/
|
|
3469
|
+
read(label2) {
|
|
3470
|
+
if (this.ended) return Promise.resolve({ kind: "exit" });
|
|
3471
|
+
this.label = label2 ?? null;
|
|
3472
|
+
this.busyLabel = null;
|
|
3473
|
+
this.editor.reset();
|
|
3474
|
+
this.draw();
|
|
3475
|
+
return new Promise((resolve2) => {
|
|
3476
|
+
this.resolve = resolve2;
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
/** Put the box in its working state, or take it out of one. */
|
|
2574
3480
|
busy(what) {
|
|
2575
3481
|
this.busyLabel = what;
|
|
2576
3482
|
this.draw();
|
|
@@ -2613,7 +3519,7 @@ var Prompt = class {
|
|
|
2613
3519
|
if (this.input.isTTY) this.input.setRawMode(false);
|
|
2614
3520
|
} catch {
|
|
2615
3521
|
}
|
|
2616
|
-
if (this.renderer.isTty) process.stdout.write(
|
|
3522
|
+
if (this.renderer.isTty) process.stdout.write(PASTE_OFF2);
|
|
2617
3523
|
this.renderer.showCursor();
|
|
2618
3524
|
};
|
|
2619
3525
|
onResize = () => {
|
|
@@ -2626,11 +3532,11 @@ var Prompt = class {
|
|
|
2626
3532
|
};
|
|
2627
3533
|
onKey = (str, key = {}) => {
|
|
2628
3534
|
const sequence = key.sequence ?? str ?? "";
|
|
2629
|
-
if (sequence.includes(
|
|
3535
|
+
if (sequence.includes(PASTE_START2)) {
|
|
2630
3536
|
this.pasting = true;
|
|
2631
3537
|
return;
|
|
2632
3538
|
}
|
|
2633
|
-
if (sequence.includes(
|
|
3539
|
+
if (sequence.includes(PASTE_END2)) {
|
|
2634
3540
|
this.pasting = false;
|
|
2635
3541
|
this.draw();
|
|
2636
3542
|
return;
|
|
@@ -2776,6 +3682,7 @@ import { chmodSync, existsSync, mkdirSync as mkdirSync4, readFileSync as readFil
|
|
|
2776
3682
|
import { hostname } from "node:os";
|
|
2777
3683
|
import { join as join5 } from "node:path";
|
|
2778
3684
|
var POLL_INTERVAL_MS = 2500;
|
|
3685
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
2779
3686
|
var Auth = class {
|
|
2780
3687
|
constructor(stateDir2, baseUrl) {
|
|
2781
3688
|
this.baseUrl = baseUrl;
|
|
@@ -2802,18 +3709,21 @@ var Auth = class {
|
|
|
2802
3709
|
isReady() {
|
|
2803
3710
|
return Boolean(this.token());
|
|
2804
3711
|
}
|
|
3712
|
+
/** True when there was a stored token to sign out; `LABD_TOKEN` is never stored, so never revoked. */
|
|
2805
3713
|
async signOut() {
|
|
2806
3714
|
const token = this.read().token;
|
|
2807
3715
|
if (token) {
|
|
2808
3716
|
await fetch(`${this.baseUrl}/v1/auth/device/revoke`, {
|
|
2809
3717
|
method: "POST",
|
|
2810
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3718
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
3719
|
+
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
2811
3720
|
}).catch(() => void 0);
|
|
2812
3721
|
}
|
|
2813
3722
|
try {
|
|
2814
3723
|
rmSync3(this.file, { force: true });
|
|
2815
3724
|
} catch {
|
|
2816
3725
|
}
|
|
3726
|
+
return Boolean(token);
|
|
2817
3727
|
}
|
|
2818
3728
|
/**
|
|
2819
3729
|
* Pair this machine with the learner's account.
|
|
@@ -2894,6 +3804,17 @@ function sleep(ms, signal) {
|
|
|
2894
3804
|
}, { once: true });
|
|
2895
3805
|
});
|
|
2896
3806
|
}
|
|
3807
|
+
function signedOutMessage(hadStored, envToken) {
|
|
3808
|
+
const lines = [];
|
|
3809
|
+
if (hadStored) lines.push("signed out, and the stored token is gone. `labd login` pairs this machine again.");
|
|
3810
|
+
else if (!envToken) lines.push("this machine was not signed in.");
|
|
3811
|
+
if (envToken) {
|
|
3812
|
+
lines.push(
|
|
3813
|
+
hadStored ? "LABD_TOKEN is still set in this shell, so the next labd will be signed in with it." : "labd is signed in with LABD_TOKEN from this shell, which is never stored - unset it to sign out."
|
|
3814
|
+
);
|
|
3815
|
+
}
|
|
3816
|
+
return lines.join("\n");
|
|
3817
|
+
}
|
|
2897
3818
|
|
|
2898
3819
|
// packages/cli/src/config.ts
|
|
2899
3820
|
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
@@ -2904,7 +3825,9 @@ var DEFAULTS = {
|
|
|
2904
3825
|
modeChosen: false,
|
|
2905
3826
|
serverUrl: "https://agent.thedevlabs.io",
|
|
2906
3827
|
commandTimeoutSeconds: 120,
|
|
2907
|
-
checkpoints: true
|
|
3828
|
+
checkpoints: true,
|
|
3829
|
+
mouse: true,
|
|
3830
|
+
sidebar: false
|
|
2908
3831
|
};
|
|
2909
3832
|
function unattendedMode(settings) {
|
|
2910
3833
|
return settings.modeChosen ? settings.mode : "ask";
|
|
@@ -2940,7 +3863,11 @@ function loadSettings(root, overrides = {}, opts = {}) {
|
|
|
2940
3863
|
commandTimeoutSeconds: clampTimeout(
|
|
2941
3864
|
overrides.commandTimeoutSeconds ?? asNumber(project.commandTimeoutSeconds) ?? asNumber(global.commandTimeoutSeconds) ?? DEFAULTS.commandTimeoutSeconds
|
|
2942
3865
|
),
|
|
2943
|
-
checkpoints: overrides.checkpoints ?? asBoolean(project.checkpoints) ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints
|
|
3866
|
+
checkpoints: overrides.checkpoints ?? asBoolean(project.checkpoints) ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints,
|
|
3867
|
+
// Screen preferences. Harmless from either file: neither changes what labd
|
|
3868
|
+
// may do, only how the session is drawn.
|
|
3869
|
+
mouse: overrides.mouse ?? asBoolean(project.mouse) ?? asBoolean(global.mouse) ?? DEFAULTS.mouse,
|
|
3870
|
+
sidebar: overrides.sidebar ?? asBoolean(project.sidebar) ?? asBoolean(global.sidebar) ?? DEFAULTS.sidebar
|
|
2944
3871
|
};
|
|
2945
3872
|
}
|
|
2946
3873
|
function read(path) {
|
|
@@ -2977,10 +3904,14 @@ var BUILTIN_COMMANDS = [
|
|
|
2977
3904
|
{ name: "resume", args: "<id>", what: "continue from a point in /tree" },
|
|
2978
3905
|
{ name: "sessions", what: "earlier sessions in this project" },
|
|
2979
3906
|
{ name: "session", what: "where this one is stored" },
|
|
2980
|
-
{ name: "
|
|
3907
|
+
{ name: "rename", args: "<name>", what: "name this session, so /sessions and --resume show it" },
|
|
3908
|
+
{ name: "new", what: "start a fresh session; this one stays saved" },
|
|
2981
3909
|
{ name: "usage", what: "what's left of this month's allowance" },
|
|
2982
3910
|
{ name: "trust", what: "let this project load its own extensions" },
|
|
3911
|
+
{ name: "logout", what: "sign this machine out and leave" },
|
|
2983
3912
|
{ name: "reload", what: "reload extensions and the file list" },
|
|
3913
|
+
{ name: "sidebar", what: "show or hide the side panel (full-screen session only)" },
|
|
3914
|
+
{ name: "clear", what: "clear the screen; the session itself is kept" },
|
|
2984
3915
|
{ name: "quit", what: "leave" }
|
|
2985
3916
|
];
|
|
2986
3917
|
function classify(line, extra = []) {
|
|
@@ -3167,9 +4098,12 @@ function branchOf(root) {
|
|
|
3167
4098
|
if (abbrev === null) return void 0;
|
|
3168
4099
|
return describeBranch(abbrev, abbrev === "HEAD" ? git("rev-parse", "--short", "HEAD") : null);
|
|
3169
4100
|
}
|
|
4101
|
+
var closedAt = /* @__PURE__ */ new WeakMap();
|
|
3170
4102
|
async function closeConversation(runtime) {
|
|
3171
4103
|
const messages = runtime.session.wire();
|
|
3172
4104
|
if (!messages.some((m) => m.role === "user")) return;
|
|
4105
|
+
if (closedAt.get(runtime.session) === messages.length) return;
|
|
4106
|
+
closedAt.set(runtime.session, messages.length);
|
|
3173
4107
|
try {
|
|
3174
4108
|
await runtime.client.close({
|
|
3175
4109
|
protocol: PROTOCOL_VERSION,
|
|
@@ -3188,6 +4122,9 @@ async function closeConversation(runtime) {
|
|
|
3188
4122
|
// packages/cli/src/modes/interactive.ts
|
|
3189
4123
|
import { basename as basename4 } from "node:path";
|
|
3190
4124
|
|
|
4125
|
+
// packages/cli/src/commands.ts
|
|
4126
|
+
import { dirname as dirname3 } from "node:path";
|
|
4127
|
+
|
|
3191
4128
|
// packages/cli/src/files.ts
|
|
3192
4129
|
import { readdirSync as readdirSync6 } from "node:fs";
|
|
3193
4130
|
import { join as join8, relative as relative4 } from "node:path";
|
|
@@ -3227,7 +4164,6 @@ function walk2(root, dir, out, budget) {
|
|
|
3227
4164
|
}
|
|
3228
4165
|
|
|
3229
4166
|
// packages/cli/src/commands.ts
|
|
3230
|
-
import { dirname as dirname3 } from "node:path";
|
|
3231
4167
|
async function handleCommand(input, ctx) {
|
|
3232
4168
|
const [rawName = "", ...rest] = input.replace(/^\//, "").split(/\s+/);
|
|
3233
4169
|
const name = rawName === "?" || rawName === "" ? "help" : rawName.toLowerCase();
|
|
@@ -3328,9 +4264,9 @@ async function handleCommand(input, ctx) {
|
|
|
3328
4264
|
for (const s of all) {
|
|
3329
4265
|
const when = new Date(s.at).toLocaleString();
|
|
3330
4266
|
const mark = s.id === runtime.session.id ? paint.accent(">") : " ";
|
|
3331
|
-
print(` ${mark} ${paint.dim(s.id.slice(0, 8))} ${paint.muted(`${when} ${s.messages} nodes`)} ${s
|
|
4267
|
+
print(` ${mark} ${paint.dim(s.id.slice(0, 8))} ${paint.muted(`${when} ${s.messages} nodes`)} ${sessionTitle(s)}`);
|
|
3332
4268
|
}
|
|
3333
|
-
print(` ${paint.dim("labd --resume <id> opens one.")}`);
|
|
4269
|
+
print(` ${paint.dim("labd --resume <id> opens one; labd --resume on its own lists them to pick from.")}`);
|
|
3334
4270
|
return {};
|
|
3335
4271
|
}
|
|
3336
4272
|
case "undo": {
|
|
@@ -3427,6 +4363,27 @@ async function handleCommand(input, ctx) {
|
|
|
3427
4363
|
for (const skipped of next.skipped) print(` ${paint.yellow("!")} ${paint.muted(skipped.why)}`);
|
|
3428
4364
|
return {};
|
|
3429
4365
|
}
|
|
4366
|
+
case "sidebar":
|
|
4367
|
+
case "clear":
|
|
4368
|
+
print(` ${paint.muted(`/${name} works in the full-screen session; this is the plain one (--plain).`)}`);
|
|
4369
|
+
return {};
|
|
4370
|
+
case "rename": {
|
|
4371
|
+
const name2 = sessionName(args);
|
|
4372
|
+
if (!name2) {
|
|
4373
|
+
const current = sessionName(runtime.session.displayName ?? "");
|
|
4374
|
+
print(` ${current ? paint.bold(current) : paint.muted("this session has no name yet")} ${paint.dim("- /rename <name> sets one")}`);
|
|
4375
|
+
return {};
|
|
4376
|
+
}
|
|
4377
|
+
runtime.session.rename(name2);
|
|
4378
|
+
print(` ${paint.muted("named")} ${paint.bold(name2)}`);
|
|
4379
|
+
return {};
|
|
4380
|
+
}
|
|
4381
|
+
case "logout": {
|
|
4382
|
+
if (ctx.asked) await closeConversation(runtime);
|
|
4383
|
+
const hadStored = await runtime.auth.signOut();
|
|
4384
|
+
const farewell = signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n").map((line) => ` ${paint.muted(line)}`).join("\n");
|
|
4385
|
+
return { exit: true, farewell };
|
|
4386
|
+
}
|
|
3430
4387
|
case "session":
|
|
3431
4388
|
print(` ${paint.muted(runtime.session.file)}`);
|
|
3432
4389
|
print(` ${paint.muted(`${runtime.session.size()} nodes, head ${runtime.session.headId?.slice(0, 8) ?? "none"}`)}`);
|
|
@@ -3466,10 +4423,77 @@ function help(runtime) {
|
|
|
3466
4423
|
` ${paint.dim("@path tags a file into the question. ctrl-c stops an answer.")}`
|
|
3467
4424
|
].join("\n");
|
|
3468
4425
|
}
|
|
4426
|
+
var MAX_SESSION_NAME = 80;
|
|
4427
|
+
function sessionName(raw) {
|
|
4428
|
+
return raw.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_SESSION_NAME);
|
|
4429
|
+
}
|
|
3469
4430
|
function indent(text) {
|
|
3470
4431
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
3471
4432
|
}
|
|
3472
4433
|
|
|
4434
|
+
// packages/cli/src/modes/shared.ts
|
|
4435
|
+
function describe(line, runtime) {
|
|
4436
|
+
const extras = runtime.extensions.commands.keys();
|
|
4437
|
+
const what = classify(line, extras);
|
|
4438
|
+
if (what.state === "none") return { state: "idle" };
|
|
4439
|
+
if (what.state === "ok") {
|
|
4440
|
+
return { state: "ok", hint: what.info ? paint.muted(what.info.what) : null };
|
|
4441
|
+
}
|
|
4442
|
+
if (what.state === "partial") {
|
|
4443
|
+
return { state: "partial", suggestions: matching(line, extras) };
|
|
4444
|
+
}
|
|
4445
|
+
const typed = line.trimStart().slice(1).split(/\s+/)[0] ?? "";
|
|
4446
|
+
const meant = didYouMean(typed, extras);
|
|
4447
|
+
return {
|
|
4448
|
+
state: "unknown",
|
|
4449
|
+
hint: meant ? `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("did you mean")} ${paint.accent(`/${meant}`)}${paint.dim("?")}` : `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("/help lists them all.")}`
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
function complete(line, runtime) {
|
|
4453
|
+
if (line.startsWith("/") && !/\s/.test(line)) {
|
|
4454
|
+
const hits2 = matching(line, runtime.extensions.commands.keys());
|
|
4455
|
+
return hits2.length === 1 ? `/${hits2[0].name} ` : null;
|
|
4456
|
+
}
|
|
4457
|
+
const at2 = line.lastIndexOf("@");
|
|
4458
|
+
if (at2 === -1) return null;
|
|
4459
|
+
const partial = line.slice(at2 + 1);
|
|
4460
|
+
const hits = rankPaths(partial, listFiles(runtime.root));
|
|
4461
|
+
return hits.length ? `${line.slice(0, at2)}@${hits[0]} ` : null;
|
|
4462
|
+
}
|
|
4463
|
+
function fileTag(line) {
|
|
4464
|
+
const at2 = line.lastIndexOf("@");
|
|
4465
|
+
if (at2 === -1) return null;
|
|
4466
|
+
if (at2 > 0 && !/\s/.test(line[at2 - 1])) return null;
|
|
4467
|
+
const partial = line.slice(at2 + 1);
|
|
4468
|
+
if (/\s/.test(partial)) return null;
|
|
4469
|
+
return { at: at2, partial };
|
|
4470
|
+
}
|
|
4471
|
+
function label(step) {
|
|
4472
|
+
return `${step.call.name.replace(/_/g, " ")} ${detail(step)}`;
|
|
4473
|
+
}
|
|
4474
|
+
function detail(step) {
|
|
4475
|
+
const input = step.call.input;
|
|
4476
|
+
return String(input.why ?? input.path ?? input.from ?? input.query ?? "");
|
|
4477
|
+
}
|
|
4478
|
+
function verbatim(call) {
|
|
4479
|
+
const i = call.input;
|
|
4480
|
+
const str = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
4481
|
+
switch (call.name) {
|
|
4482
|
+
case "run_command":
|
|
4483
|
+
return str("command");
|
|
4484
|
+
case "search_workspace": {
|
|
4485
|
+
const include = str("include");
|
|
4486
|
+
return include ? `${str("query")} in ${include}` : str("query");
|
|
4487
|
+
}
|
|
4488
|
+
case "move_file":
|
|
4489
|
+
return `${str("from")} \u2192 ${str("to")}`;
|
|
4490
|
+
case "list_dir":
|
|
4491
|
+
return str("path") || "/";
|
|
4492
|
+
default:
|
|
4493
|
+
return str("path");
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
|
|
3473
4497
|
// packages/cli/src/modes/interactive.ts
|
|
3474
4498
|
async function runInteractive(runtime) {
|
|
3475
4499
|
const renderer = new Renderer();
|
|
@@ -3532,8 +4556,11 @@ async function runInteractive(runtime) {
|
|
|
3532
4556
|
runtime,
|
|
3533
4557
|
mode: state.mode,
|
|
3534
4558
|
effort: state.effort,
|
|
4559
|
+
asked: answered > 0,
|
|
3535
4560
|
print: (text) => renderer.print(text)
|
|
3536
4561
|
});
|
|
4562
|
+
if (outcome.farewell) process.stderr.write(`${outcome.farewell}
|
|
4563
|
+
`);
|
|
3537
4564
|
if (outcome.exit) break;
|
|
3538
4565
|
if (outcome.mode) state.mode = outcome.mode;
|
|
3539
4566
|
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
@@ -3602,9 +4629,9 @@ async function runInteractive(runtime) {
|
|
|
3602
4629
|
stopSpinner();
|
|
3603
4630
|
renderer.print(`${paint.red(" labd:")} ${message}`);
|
|
3604
4631
|
},
|
|
3605
|
-
done(
|
|
3606
|
-
if (
|
|
3607
|
-
if (
|
|
4632
|
+
done(info2) {
|
|
4633
|
+
if (info2.context) state.context = info2.context;
|
|
4634
|
+
if (info2.credits) state.credits = info2.credits;
|
|
3608
4635
|
},
|
|
3609
4636
|
commandOutput(line) {
|
|
3610
4637
|
renderer.print(paint.dim(` | ${line.replace(/\n/g, "\n | ")}`));
|
|
@@ -3657,48 +4684,6 @@ ${FACE} session saved. Continue it with: labd --resume ${runtime.session.id.slic
|
|
|
3657
4684
|
}
|
|
3658
4685
|
return 0;
|
|
3659
4686
|
}
|
|
3660
|
-
function describe(line, runtime) {
|
|
3661
|
-
const extras = runtime.extensions.commands.keys();
|
|
3662
|
-
const what = classify(line, extras);
|
|
3663
|
-
if (what.state === "none") return { state: "idle" };
|
|
3664
|
-
if (what.state === "ok") {
|
|
3665
|
-
return { state: "ok", hint: what.info ? paint.muted(what.info.what) : null };
|
|
3666
|
-
}
|
|
3667
|
-
if (what.state === "partial") {
|
|
3668
|
-
return { state: "partial", suggestions: matching(line, extras) };
|
|
3669
|
-
}
|
|
3670
|
-
const typed = line.trimStart().slice(1).split(/\s+/)[0] ?? "";
|
|
3671
|
-
const meant = didYouMean(typed, extras);
|
|
3672
|
-
return {
|
|
3673
|
-
state: "unknown",
|
|
3674
|
-
hint: meant ? `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("did you mean")} ${paint.accent(`/${meant}`)}${paint.dim("?")}` : `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("/help lists them all.")}`
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
function label(step) {
|
|
3678
|
-
return `${step.call.name.replace(/_/g, " ")} ${detail(step)}`;
|
|
3679
|
-
}
|
|
3680
|
-
function detail(step) {
|
|
3681
|
-
const input = step.call.input;
|
|
3682
|
-
return String(input.why ?? input.path ?? input.from ?? input.query ?? "");
|
|
3683
|
-
}
|
|
3684
|
-
function verbatim(call) {
|
|
3685
|
-
const i = call.input;
|
|
3686
|
-
const str = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
3687
|
-
switch (call.name) {
|
|
3688
|
-
case "run_command":
|
|
3689
|
-
return str("command");
|
|
3690
|
-
case "search_workspace": {
|
|
3691
|
-
const include = str("include");
|
|
3692
|
-
return include ? `${str("query")} in ${include}` : str("query");
|
|
3693
|
-
}
|
|
3694
|
-
case "move_file":
|
|
3695
|
-
return `${str("from")} \u2192 ${str("to")}`;
|
|
3696
|
-
case "list_dir":
|
|
3697
|
-
return str("path") || "/";
|
|
3698
|
-
default:
|
|
3699
|
-
return str("path");
|
|
3700
|
-
}
|
|
3701
|
-
}
|
|
3702
4687
|
async function banner(renderer, runtime, mode) {
|
|
3703
4688
|
const where = basename4(runtime.root);
|
|
3704
4689
|
renderer.print("");
|
|
@@ -3732,16 +4717,890 @@ async function banner(renderer, runtime, mode) {
|
|
|
3732
4717
|
function sleep2(ms) {
|
|
3733
4718
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3734
4719
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
4720
|
+
|
|
4721
|
+
// packages/cli/src/modes/screen.ts
|
|
4722
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4723
|
+
import { basename as basename5, dirname as dirname4 } from "node:path";
|
|
4724
|
+
|
|
4725
|
+
// packages/cli/src/modes/serial.ts
|
|
4726
|
+
var Serial = class {
|
|
4727
|
+
constructor(opts) {
|
|
4728
|
+
this.opts = opts;
|
|
3739
4729
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
4730
|
+
pending = [];
|
|
4731
|
+
current = null;
|
|
4732
|
+
get busy() {
|
|
4733
|
+
return this.current !== null;
|
|
4734
|
+
}
|
|
4735
|
+
/** What is waiting, oldest first. */
|
|
4736
|
+
get queued() {
|
|
4737
|
+
return this.pending;
|
|
4738
|
+
}
|
|
4739
|
+
/** Where the running work can be awaited, for a clean exit. */
|
|
4740
|
+
get running() {
|
|
4741
|
+
return this.current;
|
|
4742
|
+
}
|
|
4743
|
+
/** Send now if nothing is running and nothing holds the line, else wait behind it. */
|
|
4744
|
+
submit(text) {
|
|
4745
|
+
if (this.current || this.opts.held() || this.pending.length) {
|
|
4746
|
+
this.pending.push(text);
|
|
4747
|
+
this.opts.changed();
|
|
4748
|
+
return "queued";
|
|
4749
|
+
}
|
|
4750
|
+
this.start(() => this.opts.send(text));
|
|
4751
|
+
return "sent";
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* Run something that is not a typed line - picking a session from a list, say
|
|
4755
|
+
* - under the same rule. Refused rather than queued when busy: the choice was
|
|
4756
|
+
* made against what was on screen, and by the time it ran that would be gone.
|
|
4757
|
+
*/
|
|
4758
|
+
run(work) {
|
|
4759
|
+
if (this.current) return false;
|
|
4760
|
+
this.start(work);
|
|
4761
|
+
return true;
|
|
4762
|
+
}
|
|
4763
|
+
/** Start the next line, if there is one and nothing holds it. Call when a hold lifts. */
|
|
4764
|
+
drain() {
|
|
4765
|
+
if (this.current || this.opts.held()) return;
|
|
4766
|
+
const next = this.pending.shift();
|
|
4767
|
+
if (next === void 0) return;
|
|
4768
|
+
this.start(() => this.opts.send(next));
|
|
4769
|
+
}
|
|
4770
|
+
/**
|
|
4771
|
+
* Stop waiting for what is running, without stopping it. For a command that
|
|
4772
|
+
* never finishes - an extension's, say - which would otherwise hold the line
|
|
4773
|
+
* for good. Whatever it does later lands late; nothing waits on it.
|
|
4774
|
+
*/
|
|
4775
|
+
release() {
|
|
4776
|
+
if (!this.current) return;
|
|
4777
|
+
this.current = null;
|
|
4778
|
+
this.opts.changed();
|
|
4779
|
+
this.drain();
|
|
4780
|
+
}
|
|
4781
|
+
/** Empty the line and hand back what was in it, so a stop does not lose it. */
|
|
4782
|
+
take() {
|
|
4783
|
+
const out = this.pending;
|
|
4784
|
+
this.pending = [];
|
|
4785
|
+
if (out.length) this.opts.changed();
|
|
4786
|
+
return out;
|
|
4787
|
+
}
|
|
4788
|
+
start(work) {
|
|
4789
|
+
const done = (async () => {
|
|
4790
|
+
try {
|
|
4791
|
+
await work();
|
|
4792
|
+
} catch (err) {
|
|
4793
|
+
this.opts.failed(err);
|
|
4794
|
+
}
|
|
4795
|
+
})();
|
|
4796
|
+
this.current = done;
|
|
4797
|
+
this.opts.changed();
|
|
4798
|
+
void done.then(() => {
|
|
4799
|
+
if (this.current === done) this.current = null;
|
|
4800
|
+
this.opts.changed();
|
|
4801
|
+
this.drain();
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
};
|
|
4805
|
+
|
|
4806
|
+
// packages/cli/src/modes/screen.ts
|
|
4807
|
+
var FRAME_MS = 33;
|
|
4808
|
+
var QUIT_WINDOW_MS = 2e3;
|
|
4809
|
+
var WHEEL_LINES = 3;
|
|
4810
|
+
var MAX_QUEUED_SHOWN = 3;
|
|
4811
|
+
var ESC6 = "\x1B";
|
|
4812
|
+
async function runScreen(runtime, opts) {
|
|
4813
|
+
const screen = new Screen(process.stdout, process.stdin, { mouse: opts.mouse });
|
|
4814
|
+
const transcript = new Transcript();
|
|
4815
|
+
const viewport = new Viewport(10);
|
|
4816
|
+
const editor = new TextEditor();
|
|
4817
|
+
const history = new History();
|
|
4818
|
+
const state = {
|
|
4819
|
+
mode: runtime.settings.mode,
|
|
4820
|
+
effort: runtime.settings.effort,
|
|
4821
|
+
context: null,
|
|
4822
|
+
credits: null,
|
|
4823
|
+
sidebar: opts.sidebar,
|
|
4824
|
+
tick: 0,
|
|
4825
|
+
/** What labd is doing right now, for the footer. Null when idle. */
|
|
4826
|
+
busy: null,
|
|
4827
|
+
abort: null,
|
|
4828
|
+
popup: null,
|
|
4829
|
+
/** The line the popup was dismissed on, so Esc is not undone by the next frame. */
|
|
4830
|
+
popupDismissed: null,
|
|
4831
|
+
dialog: null,
|
|
4832
|
+
approval: null,
|
|
4833
|
+
/** The assistant block a streaming answer is going into. */
|
|
4834
|
+
answer: null,
|
|
4835
|
+
toolBlocks: /* @__PURE__ */ new Map(),
|
|
4836
|
+
lastSteps: [],
|
|
4837
|
+
ctrlCAt: 0,
|
|
4838
|
+
/** A one-line message in the footer, and when it expires. */
|
|
4839
|
+
notice: null,
|
|
4840
|
+
/** Printed to stderr once the screen is given back. */
|
|
4841
|
+
farewell: null,
|
|
4842
|
+
/** A question was asked in the CURRENT session, so leaving should say so and close it. */
|
|
4843
|
+
asked: false,
|
|
4844
|
+
quitting: false,
|
|
4845
|
+
faceIndex: 0,
|
|
4846
|
+
dirty: null,
|
|
4847
|
+
conventions: 0,
|
|
4848
|
+
skills: 0
|
|
4849
|
+
};
|
|
4850
|
+
const serial = new Serial({
|
|
4851
|
+
send: (text) => text.startsWith("/") || text === "?" ? command(text) : ask(text),
|
|
4852
|
+
held: () => state.dialog !== null || state.quitting,
|
|
4853
|
+
failed: (err) => transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) }),
|
|
4854
|
+
changed: () => schedule()
|
|
4855
|
+
});
|
|
4856
|
+
const faceFrames = process.env.LABD_NO_MOTION ? [faceLines()] : decodeFrames();
|
|
4857
|
+
let frameTimer = null;
|
|
4858
|
+
const schedule = () => {
|
|
4859
|
+
if (frameTimer) return;
|
|
4860
|
+
frameTimer = setTimeout(() => {
|
|
4861
|
+
frameTimer = null;
|
|
4862
|
+
draw();
|
|
4863
|
+
}, FRAME_MS);
|
|
4864
|
+
};
|
|
4865
|
+
let spinner = null;
|
|
4866
|
+
const setBusy = (what) => {
|
|
4867
|
+
state.busy = what;
|
|
4868
|
+
if (what && !spinner) {
|
|
4869
|
+
spinner = setInterval(() => {
|
|
4870
|
+
state.tick++;
|
|
4871
|
+
schedule();
|
|
4872
|
+
}, 400);
|
|
4873
|
+
}
|
|
4874
|
+
if (!what && spinner) {
|
|
4875
|
+
clearInterval(spinner);
|
|
4876
|
+
spinner = null;
|
|
4877
|
+
}
|
|
4878
|
+
schedule();
|
|
4879
|
+
};
|
|
4880
|
+
let resolveExit = null;
|
|
4881
|
+
const exited = new Promise((resolve2) => {
|
|
4882
|
+
resolveExit = resolve2;
|
|
4883
|
+
});
|
|
4884
|
+
const quit = () => {
|
|
4885
|
+
if (state.quitting) return;
|
|
4886
|
+
state.quitting = true;
|
|
4887
|
+
state.abort?.abort();
|
|
4888
|
+
resolveExit?.();
|
|
4889
|
+
};
|
|
4890
|
+
const note = (text) => {
|
|
4891
|
+
transcript.add({ kind: "note", text });
|
|
4892
|
+
schedule();
|
|
4893
|
+
};
|
|
4894
|
+
const notice = (text, ms = QUIT_WINDOW_MS) => {
|
|
4895
|
+
state.notice = { text, until: Date.now() + ms };
|
|
4896
|
+
schedule();
|
|
4897
|
+
setTimeout(schedule, ms + 10);
|
|
4898
|
+
};
|
|
4899
|
+
function draw() {
|
|
4900
|
+
if (state.quitting) return;
|
|
4901
|
+
const rows = screen.rows;
|
|
4902
|
+
const columns = screen.columns;
|
|
4903
|
+
const L = layout({ rows, columns, editorLines: editor.lineCount, sidebar: state.sidebar });
|
|
4904
|
+
const frame = [];
|
|
4905
|
+
let cursor = null;
|
|
4906
|
+
frame.push(header(columns));
|
|
4907
|
+
const width = L.transcript.width;
|
|
4908
|
+
const lines = transcript.length ? transcript.render(width) : emptyState(width, L.transcript.height);
|
|
4909
|
+
viewport.update(lines.length, L.transcript.height);
|
|
4910
|
+
const visible = viewport.slice(lines);
|
|
4911
|
+
while (visible.length < L.transcript.height) visible.push("");
|
|
4912
|
+
if (!viewport.follow && viewport.newLines > 0 && visible.length) {
|
|
4913
|
+
const n = viewport.newLines;
|
|
4914
|
+
const tag = paint.accent(`\u2193 ${n} new line${n === 1 ? "" : "s"} \xB7 end to follow`);
|
|
4915
|
+
visible[visible.length - 1] = fit(" ".repeat(Math.max(0, width - visibleWidth(tag) - 1)) + tag, width);
|
|
4916
|
+
}
|
|
4917
|
+
const overlays = [];
|
|
4918
|
+
if (serial.queued.length && !state.approval) overlays.push(queuedLines(width));
|
|
4919
|
+
if (state.popup && !state.dialog) overlays.push(state.popup.render(width));
|
|
4920
|
+
for (const rowsOut of overlays) {
|
|
4921
|
+
const start = visible.length - rowsOut.length;
|
|
4922
|
+
rowsOut.forEach((line, i) => {
|
|
4923
|
+
if (start + i >= 0) visible[start + i] = line;
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
const body2 = [...visible.map((l) => fit(l, width)), ...composer2(L.composer.width, L.composer.height)];
|
|
4927
|
+
if (L.sidebar) {
|
|
4928
|
+
const side = sidebar(L.sidebar.width);
|
|
4929
|
+
body2.forEach((line, i) => {
|
|
4930
|
+
body2[i] = `${line}${paint.dim("\u2502")}${fit(side[i] ?? "", L.sidebar.width)}`;
|
|
4931
|
+
});
|
|
4932
|
+
}
|
|
4933
|
+
frame.push(...body2);
|
|
4934
|
+
frame.push(footer(columns));
|
|
4935
|
+
if (!state.dialog && !state.approval && !state.quitting) {
|
|
4936
|
+
const view = editor.view(L.composer.width - 6, L.composer.height - 2);
|
|
4937
|
+
cursor = { row: L.composer.top + 1 + view.cursor.row, col: 4 + view.cursor.col };
|
|
4938
|
+
}
|
|
4939
|
+
if (state.dialog) {
|
|
4940
|
+
const placed = renderDialog(state.dialog, rows, columns);
|
|
4941
|
+
placed.lines.forEach((line, i) => {
|
|
4942
|
+
const row = placed.top + i;
|
|
4943
|
+
if (row < frame.length) frame[row] = overlay(frame[row] ?? "", placed.left, line, columns);
|
|
4944
|
+
});
|
|
4945
|
+
}
|
|
4946
|
+
screen.paint(frame, cursor);
|
|
4947
|
+
}
|
|
4948
|
+
function header(width) {
|
|
4949
|
+
const session = runtime.session.id.slice(0, 8);
|
|
4950
|
+
const left = [
|
|
4951
|
+
`${paint.accent(FACE)} ${paint.bold("labd")}`,
|
|
4952
|
+
`${paint.muted(basename5(runtime.root))}${runtime.branch ? ` ${paint.muted(runtime.branch)}` : ""}`,
|
|
4953
|
+
paint.dim(`session ${session}`)
|
|
4954
|
+
].join(paint.dim(" \xB7 "));
|
|
4955
|
+
const mode = state.mode === "agent" ? paint.red(paint.bold("agent")) : state.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
4956
|
+
const right = `${mode}${state.effort ? paint.dim(` \xB7 think ${state.effort}`) : ""} `;
|
|
4957
|
+
return spread(` ${left}`, right, width);
|
|
4958
|
+
}
|
|
4959
|
+
function footer(width) {
|
|
4960
|
+
const d = paint.dim;
|
|
4961
|
+
let left;
|
|
4962
|
+
if (state.notice && state.notice.until > Date.now()) left = paint.yellow(state.notice.text);
|
|
4963
|
+
else if (state.dialog) left = d("\u2191\u2193 move \xB7 enter choose \xB7 type to search \xB7 esc close");
|
|
4964
|
+
else if (state.approval) left = `${paint.yellow("y")} ${d("allow \xB7")} ${paint.yellow("n")} ${d("decline \xB7 ctrl+c stop")}`;
|
|
4965
|
+
else if (state.busy) left = `${paint.accent(state.busy)}${spinnerFrame(state.tick)} ${d("enter queues \xB7 ctrl+c stop \xB7 pgup/pgdn scroll")}`;
|
|
4966
|
+
else if (state.popup) left = d("\u2191\u2193 move \xB7 tab/enter accept \xB7 esc close");
|
|
4967
|
+
else {
|
|
4968
|
+
const described = describe(editor.currentLine, runtime);
|
|
4969
|
+
left = described.hint ? described.hint : d("enter send \xB7 shift+enter newline \xB7 / commands \xB7 @ files \xB7 pgup/pgdn scroll \xB7 ctrl+c stop \xB7 ctrl+d quit");
|
|
4970
|
+
}
|
|
4971
|
+
const bits = [];
|
|
4972
|
+
if (state.context) {
|
|
4973
|
+
const crowded = state.context.percentUsed >= 75;
|
|
4974
|
+
bits.push((crowded ? paint.yellow : paint.muted)(`context ${state.context.percentUsed}%`));
|
|
4975
|
+
}
|
|
4976
|
+
if (state.credits) bits.push(paint.muted(`credits ${state.credits.percentLeft}%`));
|
|
4977
|
+
return spread(` ${left}`, `${bits.join(paint.dim(" \xB7 "))} `, width);
|
|
4978
|
+
}
|
|
4979
|
+
function composer2(width, height) {
|
|
4980
|
+
const inner = width - 4;
|
|
4981
|
+
const textRows = Math.max(1, height - 2);
|
|
4982
|
+
const colour = state.approval ? paint.yellow : state.busy ? paint.accent : borderColour2(describe(editor.currentLine, runtime).state);
|
|
4983
|
+
const out = [];
|
|
4984
|
+
out.push(colour(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`));
|
|
4985
|
+
const view = editor.view(inner - 2, textRows);
|
|
4986
|
+
const lines = [...view.lines];
|
|
4987
|
+
while (lines.length < textRows) lines.push("");
|
|
4988
|
+
lines.forEach((line, i) => {
|
|
4989
|
+
const first = i === 0 && editor.cursor.row - view.cursor.row === 0;
|
|
4990
|
+
const sigil = first ? state.busy ? paint.dim(">") : paint.accent(">") : " ";
|
|
4991
|
+
let text = line;
|
|
4992
|
+
if (editor.isEmpty && i === 0) {
|
|
4993
|
+
text = state.approval ? paint.yellow("y to allow, n or esc to decline") : state.busy ? paint.dim("labd is working - type to queue a message, ctrl+c stops it") : paint.dim("ask labd something - / for commands - @ to tag a file");
|
|
4994
|
+
}
|
|
4995
|
+
out.push(`${colour("\u2502")} ${sigil} ${fit(text, inner - 2)} ${colour("\u2502")}`);
|
|
4996
|
+
});
|
|
4997
|
+
out.push(colour(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`));
|
|
4998
|
+
return out;
|
|
4999
|
+
}
|
|
5000
|
+
function emptyState(width, height) {
|
|
5001
|
+
const face = faceFrames[Math.min(state.faceIndex, faceFrames.length - 1)];
|
|
5002
|
+
const hint = paint.dim("ask labd something - / for commands - @ to tag a file");
|
|
5003
|
+
const caption = wrapPlain(MODE_CAPTION[state.mode], Math.max(20, width - 4)).map((l) => paint.dim(l));
|
|
5004
|
+
const body2 = [...face, "", hint, "", ...caption];
|
|
5005
|
+
const top = Math.max(0, Math.floor((height - body2.length) / 2));
|
|
5006
|
+
return [...Array(top).fill(""), ...body2.map((line) => centre(line, width))];
|
|
5007
|
+
}
|
|
5008
|
+
function sidebar(width) {
|
|
5009
|
+
const row = (k, v) => `${paint.muted(k.padEnd(12))}${v}`;
|
|
5010
|
+
const lines = [
|
|
5011
|
+
` ${paint.bold("project")}`,
|
|
5012
|
+
` ${row("name", basename5(runtime.root))}`,
|
|
5013
|
+
` ${row("branch", runtime.branch ?? paint.dim("none"))}`,
|
|
5014
|
+
` ${row("changed", state.dirty === null ? paint.dim("-") : `${state.dirty} file${state.dirty === 1 ? "" : "s"}`)}`,
|
|
5015
|
+
` ${row("conventions", String(state.conventions))}`,
|
|
5016
|
+
` ${row("skills", String(state.skills))}`,
|
|
5017
|
+
"",
|
|
5018
|
+
` ${paint.bold("session")}`,
|
|
5019
|
+
` ${row("mode", state.mode)}`,
|
|
5020
|
+
` ${row("think", state.effort ?? paint.dim("default"))}`,
|
|
5021
|
+
` ${row("context", state.context ? `${state.context.percentUsed}%` : paint.dim("-"))}`,
|
|
5022
|
+
` ${row("credits", state.credits ? `${state.credits.percentLeft}% left` : paint.dim("-"))}`,
|
|
5023
|
+
"",
|
|
5024
|
+
` ${paint.bold("recent steps")}`,
|
|
5025
|
+
...state.lastSteps.length ? state.lastSteps.map((s) => ` ${paint.muted(s)}`) : [` ${paint.dim("none yet")}`]
|
|
5026
|
+
];
|
|
5027
|
+
return lines.map((l) => fit(l, width));
|
|
5028
|
+
}
|
|
5029
|
+
function onKeys(keys) {
|
|
5030
|
+
for (const key of keys) onKey(key);
|
|
5031
|
+
}
|
|
5032
|
+
function onKey(key) {
|
|
5033
|
+
if (key.ctrl && key.name === "c") return interrupt();
|
|
5034
|
+
if (state.dialog) {
|
|
5035
|
+
const r = dialogKey(state.dialog, key);
|
|
5036
|
+
if (r === "close" || r === "select") closeDialog();
|
|
5037
|
+
return schedule();
|
|
5038
|
+
}
|
|
5039
|
+
if (key.mouse) {
|
|
5040
|
+
if (key.mouse.kind === "wheel-up") viewport.scroll(-WHEEL_LINES);
|
|
5041
|
+
else if (key.mouse.kind === "wheel-down") viewport.scroll(WHEEL_LINES);
|
|
5042
|
+
return schedule();
|
|
5043
|
+
}
|
|
5044
|
+
if (key.name === "pageup") return viewport.pageUp(), schedule();
|
|
5045
|
+
if (key.name === "pagedown") return viewport.pageDown(), schedule();
|
|
5046
|
+
if (key.name === "home" && editor.isEmpty) return viewport.home(), schedule();
|
|
5047
|
+
if (key.name === "end" && editor.isEmpty) return viewport.end(), schedule();
|
|
5048
|
+
if (state.approval) {
|
|
5049
|
+
if (key.sequence === "y" || key.sequence === "Y") return answerApproval(true);
|
|
5050
|
+
if (key.sequence === "n" || key.sequence === "N" || key.name === "escape") return answerApproval(false);
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
5053
|
+
if (key.ctrl && key.name === "d") {
|
|
5054
|
+
if (editor.isEmpty) quit();
|
|
5055
|
+
return;
|
|
5056
|
+
}
|
|
5057
|
+
if (key.ctrl && key.name === "l") return screen.invalidate(), schedule();
|
|
5058
|
+
if (key.ctrl && key.name === "n") return void serial.submit("/new");
|
|
5059
|
+
if (key.ctrl && key.name === "o") return void serial.submit("/sessions");
|
|
5060
|
+
if (key.name === "escape") {
|
|
5061
|
+
if (state.popup) {
|
|
5062
|
+
state.popup = null;
|
|
5063
|
+
state.popupDismissed = editor.currentLine;
|
|
5064
|
+
} else if (!viewport.follow) viewport.end();
|
|
5065
|
+
return schedule();
|
|
5066
|
+
}
|
|
5067
|
+
if (state.popup) {
|
|
5068
|
+
if (key.name === "up") return state.popup.move(-1), schedule();
|
|
5069
|
+
if (key.name === "down") return state.popup.move(1), schedule();
|
|
5070
|
+
if (key.name === "tab" || key.name === "return" && !key.shift && !key.ctrl) return acceptPopup();
|
|
5071
|
+
}
|
|
5072
|
+
if (key.name === "tab") {
|
|
5073
|
+
const completed = complete(editor.currentLine, runtime);
|
|
5074
|
+
if (completed !== null) editor.setCurrentLine(completed);
|
|
5075
|
+
refreshPopup();
|
|
5076
|
+
return schedule();
|
|
5077
|
+
}
|
|
5078
|
+
const outcome = editor.apply(key);
|
|
5079
|
+
if (outcome === "unhandled") {
|
|
5080
|
+
if (key.name === "return") return submit();
|
|
5081
|
+
if (key.name === "up" || key.name === "down") {
|
|
5082
|
+
const line = key.name === "up" ? history.up(editor.text) : history.down();
|
|
5083
|
+
if (line !== null) {
|
|
5084
|
+
editor.setText(line);
|
|
5085
|
+
refreshPopup();
|
|
5086
|
+
schedule();
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
if (outcome === "text") refreshPopup();
|
|
5092
|
+
if (outcome !== "same") schedule();
|
|
5093
|
+
}
|
|
5094
|
+
function interrupt() {
|
|
5095
|
+
if (state.approval) {
|
|
5096
|
+
answerApproval(false);
|
|
5097
|
+
}
|
|
5098
|
+
if (state.abort) {
|
|
5099
|
+
state.abort.abort();
|
|
5100
|
+
state.abort = null;
|
|
5101
|
+
note(paint.dim(" stopped."));
|
|
5102
|
+
unqueue();
|
|
5103
|
+
return;
|
|
5104
|
+
}
|
|
5105
|
+
if (serial.busy && serial.queued.length) {
|
|
5106
|
+
unqueue();
|
|
5107
|
+
notice("the queue is back in the box; ctrl+c again stops waiting for the command", 2500);
|
|
5108
|
+
return;
|
|
5109
|
+
}
|
|
5110
|
+
if (serial.busy) {
|
|
5111
|
+
serial.release();
|
|
5112
|
+
setBusy(null);
|
|
5113
|
+
note(paint.dim(" stopped waiting. The command may still finish in the background."));
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
if (state.dialog) {
|
|
5117
|
+
closeDialog();
|
|
5118
|
+
return schedule();
|
|
5119
|
+
}
|
|
5120
|
+
if (!editor.isEmpty) {
|
|
5121
|
+
editor.reset();
|
|
5122
|
+
state.popup = null;
|
|
5123
|
+
return schedule();
|
|
5124
|
+
}
|
|
5125
|
+
const now = Date.now();
|
|
5126
|
+
if (now - state.ctrlCAt < QUIT_WINDOW_MS) return quit();
|
|
5127
|
+
state.ctrlCAt = now;
|
|
5128
|
+
notice("press ctrl+c again to quit");
|
|
5129
|
+
}
|
|
5130
|
+
function answerApproval(ok) {
|
|
5131
|
+
const approval = state.approval;
|
|
5132
|
+
if (!approval) return;
|
|
5133
|
+
state.approval = null;
|
|
5134
|
+
approval.entry.block.answer = ok ? "allowed" : "declined";
|
|
5135
|
+
transcript.touch(approval.entry);
|
|
5136
|
+
approval.resolve(ok);
|
|
5137
|
+
schedule();
|
|
5138
|
+
}
|
|
5139
|
+
function refreshPopup() {
|
|
5140
|
+
const line = editor.currentLine;
|
|
5141
|
+
if (state.popupDismissed !== null && state.popupDismissed === line) return;
|
|
5142
|
+
state.popupDismissed = null;
|
|
5143
|
+
if (line.startsWith("/") && !/\s/.test(line)) {
|
|
5144
|
+
if (classify(line, runtime.extensions.commands.keys()).state !== "partial") {
|
|
5145
|
+
state.popup = null;
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
5148
|
+
const items = matching(line, runtime.extensions.commands.keys()).map((c) => ({
|
|
5149
|
+
label: `/${c.name}`,
|
|
5150
|
+
what: c.what,
|
|
5151
|
+
value: `/${c.name} `
|
|
5152
|
+
}));
|
|
5153
|
+
state.popup = items.length ? new Popup("command", items) : null;
|
|
5154
|
+
return;
|
|
5155
|
+
}
|
|
5156
|
+
const tag = fileTag(line);
|
|
5157
|
+
if (tag) {
|
|
5158
|
+
const hits = rankPaths(tag.partial, listFiles(runtime.root), 8);
|
|
5159
|
+
state.popup = hits.length ? new Popup(
|
|
5160
|
+
"file",
|
|
5161
|
+
hits.map((path) => ({ label: path, value: `${line.slice(0, tag.at)}@${path} ` }))
|
|
5162
|
+
) : null;
|
|
5163
|
+
return;
|
|
5164
|
+
}
|
|
5165
|
+
state.popup = null;
|
|
5166
|
+
}
|
|
5167
|
+
function acceptPopup() {
|
|
5168
|
+
const item = state.popup?.current;
|
|
5169
|
+
state.popup = null;
|
|
5170
|
+
if (item) editor.setCurrentLine(item.value);
|
|
5171
|
+
schedule();
|
|
5172
|
+
}
|
|
5173
|
+
function submit() {
|
|
5174
|
+
const text = editor.text.trim();
|
|
5175
|
+
if (!text) return;
|
|
5176
|
+
history.add(text);
|
|
5177
|
+
editor.reset();
|
|
5178
|
+
state.popup = null;
|
|
5179
|
+
viewport.end();
|
|
5180
|
+
serial.submit(text);
|
|
5181
|
+
schedule();
|
|
5182
|
+
}
|
|
5183
|
+
async function command(text) {
|
|
5184
|
+
setBusy(`running ${text.split(/\s+/)[0]}`);
|
|
5185
|
+
try {
|
|
5186
|
+
await runCommand2(text);
|
|
5187
|
+
} finally {
|
|
5188
|
+
setBusy(null);
|
|
5189
|
+
}
|
|
5190
|
+
}
|
|
5191
|
+
function closeDialog() {
|
|
5192
|
+
state.dialog = null;
|
|
5193
|
+
serial.drain();
|
|
5194
|
+
}
|
|
5195
|
+
function unqueue() {
|
|
5196
|
+
const queued = serial.take();
|
|
5197
|
+
if (!queued.length) return;
|
|
5198
|
+
editor.setText([...queued, editor.text].filter(Boolean).join("\n"));
|
|
5199
|
+
state.popup = null;
|
|
5200
|
+
}
|
|
5201
|
+
function queuedLines(width) {
|
|
5202
|
+
const shown = serial.queued.slice(0, MAX_QUEUED_SHOWN);
|
|
5203
|
+
const lines = shown.map((text) => fit(` ${paint.dim("queued")} ${paint.muted(text.split("\n")[0] ?? "")}`, width));
|
|
5204
|
+
const more = serial.queued.length - shown.length;
|
|
5205
|
+
if (more > 0) lines.push(paint.dim(` and ${more} more`));
|
|
5206
|
+
return lines;
|
|
5207
|
+
}
|
|
5208
|
+
async function runCommand2(input) {
|
|
5209
|
+
transcript.add({ kind: "user", text: input });
|
|
5210
|
+
const [rawName = ""] = input.replace(/^\//, "").split(/\s+/);
|
|
5211
|
+
const name = rawName === "?" || rawName === "" ? "help" : rawName.toLowerCase();
|
|
5212
|
+
switch (name) {
|
|
5213
|
+
case "help":
|
|
5214
|
+
state.dialog = info("help", helpLines());
|
|
5215
|
+
return schedule();
|
|
5216
|
+
case "sessions":
|
|
5217
|
+
return openSessions();
|
|
5218
|
+
case "tree":
|
|
5219
|
+
return openTree();
|
|
5220
|
+
case "sidebar":
|
|
5221
|
+
state.sidebar = !state.sidebar;
|
|
5222
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5223
|
+
note(paint.muted(state.sidebar ? " side panel on. It needs 120 columns to show." : " side panel off."));
|
|
5224
|
+
return;
|
|
5225
|
+
case "clear":
|
|
5226
|
+
transcript.clear();
|
|
5227
|
+
state.toolBlocks.clear();
|
|
5228
|
+
state.answer = null;
|
|
5229
|
+
return schedule();
|
|
5230
|
+
case "new":
|
|
5231
|
+
return newSession();
|
|
5232
|
+
default:
|
|
5233
|
+
break;
|
|
5234
|
+
}
|
|
5235
|
+
const outcome = await handleCommand(input, {
|
|
5236
|
+
runtime,
|
|
5237
|
+
mode: state.mode,
|
|
5238
|
+
effort: state.effort,
|
|
5239
|
+
asked: state.asked,
|
|
5240
|
+
print: (text) => note(text)
|
|
5241
|
+
});
|
|
5242
|
+
if (outcome.farewell) state.farewell = outcome.farewell;
|
|
5243
|
+
if (outcome.exit) return quit();
|
|
5244
|
+
if (outcome.mode) state.mode = outcome.mode;
|
|
5245
|
+
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
5246
|
+
schedule();
|
|
5247
|
+
}
|
|
5248
|
+
function helpLines() {
|
|
5249
|
+
const rows = BUILTIN_COMMANDS.map((c) => [`/${c.name}${c.args ? ` ${c.args}` : ""}`, c.what]);
|
|
5250
|
+
for (const [cmd, command2] of runtime.extensions.commands) rows.push([`/${cmd}`, `${command2.describe} ${paint.dim("(extension)")}`]);
|
|
5251
|
+
const width = Math.max(...rows.map(([n]) => n.length));
|
|
5252
|
+
const keys = [
|
|
5253
|
+
["enter", "send"],
|
|
5254
|
+
["shift+enter, ctrl+j, or a trailing \\", "new line"],
|
|
5255
|
+
["/ and @", "commands and files, with a list"],
|
|
5256
|
+
["tab", "complete"],
|
|
5257
|
+
["pgup / pgdn, wheel", "scroll the transcript"],
|
|
5258
|
+
["home / end", "top / bottom (with an empty box)"],
|
|
5259
|
+
["esc", "close a list or dialog; jump back to the bottom"],
|
|
5260
|
+
["ctrl+c", "stop the answer; clear the box; twice to quit"],
|
|
5261
|
+
["ctrl+d", "quit"],
|
|
5262
|
+
["ctrl+n / ctrl+o", "new session / earlier sessions"],
|
|
5263
|
+
["ctrl+l", "redraw"]
|
|
5264
|
+
];
|
|
5265
|
+
const kwidth = Math.max(...keys.map(([k]) => k.length));
|
|
5266
|
+
return [
|
|
5267
|
+
paint.bold("commands"),
|
|
5268
|
+
...rows.map(([n, w]) => `${paint.accent(n.padEnd(width))} ${paint.muted(w)}`),
|
|
5269
|
+
"",
|
|
5270
|
+
paint.bold("keys"),
|
|
5271
|
+
...keys.map(([k, w]) => `${paint.accent(k.padEnd(kwidth))} ${paint.muted(w)}`),
|
|
5272
|
+
"",
|
|
5273
|
+
paint.dim("@path tags a file into the question. --plain runs the older print-style session.")
|
|
5274
|
+
];
|
|
5275
|
+
}
|
|
5276
|
+
function openSessions() {
|
|
5277
|
+
const all = Session.list(dirname4(runtime.session.file));
|
|
5278
|
+
if (!all.length) return note(paint.muted(" no earlier sessions in this project"));
|
|
5279
|
+
state.dialog = pick(
|
|
5280
|
+
"sessions - enter resumes one",
|
|
5281
|
+
all.map((s) => ({
|
|
5282
|
+
id: s.file,
|
|
5283
|
+
label: `${s.id === runtime.session.id ? "> " : " "}${sessionTitle(s)}`,
|
|
5284
|
+
hint: `${s.id.slice(0, 8)} ${new Date(s.at).toLocaleString()} ${s.messages} nodes`
|
|
5285
|
+
})),
|
|
5286
|
+
(item) => {
|
|
5287
|
+
if (!serial.run(() => switchSession(item.id))) notice("labd is still working - pick again when it is done", 2500);
|
|
5288
|
+
}
|
|
5289
|
+
);
|
|
5290
|
+
schedule();
|
|
5291
|
+
}
|
|
5292
|
+
function openTree() {
|
|
5293
|
+
const nodes = runtime.session.all();
|
|
5294
|
+
if (!nodes.length) return note(paint.muted(" nothing in this session yet"));
|
|
5295
|
+
const head = runtime.session.headId;
|
|
5296
|
+
state.dialog = pick(
|
|
5297
|
+
"this session - enter continues from a point",
|
|
5298
|
+
nodes.map((n) => ({
|
|
5299
|
+
id: n.id,
|
|
5300
|
+
label: `${n.id === head ? "> " : " "}${n.id.slice(0, 8)} ${n.body.kind.padEnd(9)}`,
|
|
5301
|
+
hint: summarise2(n.body)
|
|
5302
|
+
})),
|
|
5303
|
+
(item) => {
|
|
5304
|
+
if (!serial.run(() => runCommand2(`/resume ${item.id.slice(0, 8)}`))) notice("labd is still working - pick again when it is done", 2500);
|
|
5305
|
+
}
|
|
5306
|
+
);
|
|
5307
|
+
schedule();
|
|
5308
|
+
}
|
|
5309
|
+
async function switchSession(file) {
|
|
5310
|
+
const current = runtime.session;
|
|
5311
|
+
if (Session.list(dirname4(current.file)).find((s) => s.file === file)?.id === current.id) {
|
|
5312
|
+
return note(paint.muted(" that is this session."));
|
|
5313
|
+
}
|
|
5314
|
+
if (state.asked) await closeConversation(runtime);
|
|
5315
|
+
runtime.session = Session.open(file);
|
|
5316
|
+
state.asked = false;
|
|
5317
|
+
transcript.clear();
|
|
5318
|
+
state.toolBlocks.clear();
|
|
5319
|
+
state.answer = null;
|
|
5320
|
+
replay();
|
|
5321
|
+
note(paint.muted(` resumed ${runtime.session.id.slice(0, 8)}.`));
|
|
5322
|
+
}
|
|
5323
|
+
async function newSession() {
|
|
5324
|
+
if (state.asked) await closeConversation(runtime);
|
|
5325
|
+
runtime.session = Session.create(bucketFor(runtime.stateDir, runtime.root));
|
|
5326
|
+
state.asked = false;
|
|
5327
|
+
transcript.clear();
|
|
5328
|
+
state.toolBlocks.clear();
|
|
5329
|
+
state.answer = null;
|
|
5330
|
+
note(paint.muted(` new session ${runtime.session.id.slice(0, 8)}. The previous one is saved; /sessions lists it.`));
|
|
5331
|
+
}
|
|
5332
|
+
function replay() {
|
|
5333
|
+
for (const m of runtime.session.wire()) {
|
|
5334
|
+
if (m.role === "user" && m.text) transcript.add({ kind: "user", text: m.text });
|
|
5335
|
+
else if (m.role === "assistant" && m.text) transcript.add({ kind: "assistant", text: m.text, done: true });
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
async function ask(input) {
|
|
5339
|
+
transcript.add({ kind: "user", text: input });
|
|
5340
|
+
state.asked = true;
|
|
5341
|
+
state.answer = null;
|
|
5342
|
+
const tags = taggedPaths(input);
|
|
5343
|
+
const attachments = tags.length ? attachTagged(runtime.root, tags) : void 0;
|
|
5344
|
+
for (const refused of attachments?.filter((a) => a.omitted) ?? []) {
|
|
5345
|
+
note(` ${paint.yellow("!")} ${paint.muted(`${refused.path}: ${refused.omitted}`)}`);
|
|
5346
|
+
}
|
|
5347
|
+
for (const handler of runtime.extensions.onTurn) {
|
|
5348
|
+
try {
|
|
5349
|
+
handler({ question: input });
|
|
5350
|
+
} catch {
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
const abort = new AbortController();
|
|
5354
|
+
state.abort = abort;
|
|
5355
|
+
const ui = {
|
|
5356
|
+
text(chunk) {
|
|
5357
|
+
if (!state.answer || state.answer.block.done) {
|
|
5358
|
+
state.answer = transcript.add({ kind: "assistant", text: "", done: false });
|
|
5359
|
+
}
|
|
5360
|
+
state.answer.block.text += chunk;
|
|
5361
|
+
transcript.touch(state.answer);
|
|
5362
|
+
schedule();
|
|
5363
|
+
},
|
|
5364
|
+
status(what) {
|
|
5365
|
+
setBusy(what);
|
|
5366
|
+
},
|
|
5367
|
+
tool(step) {
|
|
5368
|
+
for (const handler of runtime.extensions.onTool) {
|
|
5369
|
+
try {
|
|
5370
|
+
handler(step);
|
|
5371
|
+
} catch {
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
closeAnswer();
|
|
5375
|
+
let entry = state.toolBlocks.get(step.call.id);
|
|
5376
|
+
if (!entry) {
|
|
5377
|
+
entry = transcript.add({
|
|
5378
|
+
kind: "tool",
|
|
5379
|
+
name: step.call.name,
|
|
5380
|
+
detail: detail(step),
|
|
5381
|
+
status: step.status,
|
|
5382
|
+
input: verbatim(step.call) || void 0,
|
|
5383
|
+
live: []
|
|
5384
|
+
});
|
|
5385
|
+
state.toolBlocks.set(step.call.id, entry);
|
|
5386
|
+
}
|
|
5387
|
+
entry.block.status = step.status;
|
|
5388
|
+
if (step.status !== "running" && step.status !== "awaiting") {
|
|
5389
|
+
entry.block.output = step.output ?? step.detail;
|
|
5390
|
+
remember(`${label(step)}`.trim());
|
|
5391
|
+
}
|
|
5392
|
+
transcript.touch(entry);
|
|
5393
|
+
if (step.status === "running" || step.status === "awaiting") setBusy(label(step));
|
|
5394
|
+
schedule();
|
|
5395
|
+
},
|
|
5396
|
+
approve(call, preview) {
|
|
5397
|
+
setBusy(null);
|
|
5398
|
+
closeAnswer();
|
|
5399
|
+
const entry = transcript.add({ kind: "approval", call, preview, mode: state.mode, answer: null });
|
|
5400
|
+
viewport.end();
|
|
5401
|
+
return new Promise((resolve2) => {
|
|
5402
|
+
state.approval = { entry, resolve: resolve2 };
|
|
5403
|
+
schedule();
|
|
5404
|
+
});
|
|
5405
|
+
},
|
|
5406
|
+
error(message) {
|
|
5407
|
+
setBusy(null);
|
|
5408
|
+
transcript.add({ kind: "error", text: message });
|
|
5409
|
+
schedule();
|
|
5410
|
+
},
|
|
5411
|
+
done(info2) {
|
|
5412
|
+
if (info2.context) state.context = info2.context;
|
|
5413
|
+
if (info2.credits) state.credits = info2.credits;
|
|
5414
|
+
schedule();
|
|
5415
|
+
},
|
|
5416
|
+
commandOutput(line) {
|
|
5417
|
+
let target;
|
|
5418
|
+
for (const entry of state.toolBlocks.values()) {
|
|
5419
|
+
if (entry.block.name === "run_command" && entry.block.status === "running") target = entry;
|
|
5420
|
+
}
|
|
5421
|
+
if (!target) return;
|
|
5422
|
+
target.block.live.push(...line.replace(/\n$/, "").split("\n"));
|
|
5423
|
+
transcript.touch(target);
|
|
5424
|
+
schedule();
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
setBusy("thinking");
|
|
5428
|
+
try {
|
|
5429
|
+
const turn = await runTurn(input, {
|
|
5430
|
+
session: runtime.session,
|
|
5431
|
+
client: runtime.client,
|
|
5432
|
+
root: runtime.root,
|
|
5433
|
+
mode: state.mode,
|
|
5434
|
+
checkpoints: runtime.checkpoints,
|
|
5435
|
+
effort: state.effort,
|
|
5436
|
+
attachments,
|
|
5437
|
+
commandTimeoutMs: runtime.settings.commandTimeoutSeconds * 1e3,
|
|
5438
|
+
ui,
|
|
5439
|
+
signal: abort.signal
|
|
5440
|
+
});
|
|
5441
|
+
closeAnswer();
|
|
5442
|
+
if (turn.wrote.length && runtime.checkpoints) {
|
|
5443
|
+
note(paint.dim(` ${turn.wrote.length} file${turn.wrote.length === 1 ? "" : "s"} changed. /undo puts this answer back.`));
|
|
5444
|
+
}
|
|
5445
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5446
|
+
if (!abort.signal.aborted && state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
5447
|
+
setBusy("summarising, to make room");
|
|
5448
|
+
const ok = await compact({ session: runtime.session, client: runtime.client, root: runtime.root });
|
|
5449
|
+
if (ok) transcript.add({ kind: "compaction" });
|
|
5450
|
+
else note(paint.dim(" (could not summarise; carrying on)"));
|
|
5451
|
+
}
|
|
5452
|
+
} catch (err) {
|
|
5453
|
+
transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) });
|
|
5454
|
+
} finally {
|
|
5455
|
+
if (state.abort === abort) state.abort = null;
|
|
5456
|
+
if (state.approval) answerApproval(false);
|
|
5457
|
+
setBusy(null);
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
function closeAnswer() {
|
|
5461
|
+
const answer = state.answer;
|
|
5462
|
+
if (!answer || answer.block.done) return;
|
|
5463
|
+
answer.block.done = true;
|
|
5464
|
+
transcript.touch(answer);
|
|
5465
|
+
}
|
|
5466
|
+
function remember(step) {
|
|
5467
|
+
state.lastSteps.push(step);
|
|
5468
|
+
if (state.lastSteps.length > 5) state.lastSteps.shift();
|
|
5469
|
+
}
|
|
5470
|
+
function refreshProjectFacts() {
|
|
5471
|
+
try {
|
|
5472
|
+
const out = execFileSync3("git", ["status", "--porcelain"], {
|
|
5473
|
+
cwd: runtime.root,
|
|
5474
|
+
encoding: "utf8",
|
|
5475
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5476
|
+
timeout: 2e3
|
|
5477
|
+
});
|
|
5478
|
+
state.dirty = out.split("\n").filter(Boolean).length;
|
|
5479
|
+
} catch {
|
|
5480
|
+
state.dirty = null;
|
|
5481
|
+
}
|
|
5482
|
+
try {
|
|
5483
|
+
const ctx = buildContext(runtime.root).editor;
|
|
5484
|
+
state.conventions = ctx?.instructions?.length ?? 0;
|
|
5485
|
+
state.skills = ctx?.skills?.length ?? 0;
|
|
5486
|
+
} catch {
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
screen.on("keys", onKeys);
|
|
5490
|
+
screen.on("resize", schedule);
|
|
5491
|
+
screen.on("end", quit);
|
|
5492
|
+
const onSignal = (code) => () => {
|
|
5493
|
+
screen.leave();
|
|
5494
|
+
process.exit(code);
|
|
5495
|
+
};
|
|
5496
|
+
const onTerm = onSignal(143);
|
|
5497
|
+
const onInt = onSignal(130);
|
|
5498
|
+
const onHup = onSignal(129);
|
|
5499
|
+
process.on("SIGTERM", onTerm);
|
|
5500
|
+
process.on("SIGINT", onInt);
|
|
5501
|
+
process.on("SIGHUP", onHup);
|
|
5502
|
+
screen.enter();
|
|
5503
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5504
|
+
if (runtime.settings.ignoredProjectServerUrl) {
|
|
5505
|
+
note(
|
|
5506
|
+
` ${paint.yellow("!")} ${paint.muted(
|
|
5507
|
+
`ignoring serverUrl in .labd/settings.json (${runtime.settings.ignoredProjectServerUrl}) - this project isn't trusted; \`labd trust\` if you meant it`
|
|
5508
|
+
)}`
|
|
5509
|
+
);
|
|
5510
|
+
}
|
|
5511
|
+
for (const skipped of runtime.extensions.skipped) note(` ${paint.yellow("!")} ${paint.muted(`extensions: ${skipped.why}`)}`);
|
|
5512
|
+
replay();
|
|
5513
|
+
let faceTimer = null;
|
|
5514
|
+
if (faceFrames.length > 1) {
|
|
5515
|
+
faceTimer = setInterval(() => {
|
|
5516
|
+
state.faceIndex++;
|
|
5517
|
+
if (state.faceIndex >= faceFrames.length - 1 && faceTimer) {
|
|
5518
|
+
clearInterval(faceTimer);
|
|
5519
|
+
faceTimer = null;
|
|
5520
|
+
}
|
|
5521
|
+
if (!transcript.length) schedule();
|
|
5522
|
+
}, FACE_FRAME_MS);
|
|
5523
|
+
}
|
|
5524
|
+
if (opts.pickSession) openSessions();
|
|
5525
|
+
draw();
|
|
5526
|
+
try {
|
|
5527
|
+
await exited;
|
|
5528
|
+
if (serial.running) await Promise.race([serial.running, sleep3(1500)]);
|
|
5529
|
+
} finally {
|
|
5530
|
+
if (faceTimer) clearInterval(faceTimer);
|
|
5531
|
+
if (frameTimer) clearTimeout(frameTimer);
|
|
5532
|
+
setBusy(null);
|
|
5533
|
+
process.off("SIGTERM", onTerm);
|
|
5534
|
+
process.off("SIGINT", onInt);
|
|
5535
|
+
process.off("SIGHUP", onHup);
|
|
5536
|
+
screen.leave();
|
|
5537
|
+
}
|
|
5538
|
+
if (state.asked) {
|
|
5539
|
+
process.stderr.write(paint.dim(`
|
|
5540
|
+
${FACE} session saved. Continue it with: labd --resume ${runtime.session.id.slice(0, 8)}
|
|
5541
|
+
`));
|
|
5542
|
+
await closeConversation(runtime);
|
|
5543
|
+
}
|
|
5544
|
+
if (state.farewell) process.stderr.write(`${state.farewell}
|
|
5545
|
+
`);
|
|
5546
|
+
return 0;
|
|
5547
|
+
}
|
|
5548
|
+
function borderColour2(s) {
|
|
5549
|
+
switch (s) {
|
|
5550
|
+
case "ok":
|
|
5551
|
+
return paint.green;
|
|
5552
|
+
case "unknown":
|
|
5553
|
+
return paint.red;
|
|
5554
|
+
case "partial":
|
|
5555
|
+
return paint.cyan;
|
|
5556
|
+
default:
|
|
5557
|
+
return paint.dim;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
function spread(left, right, width) {
|
|
5561
|
+
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
5562
|
+
if (gap >= 1) return `${left}${" ".repeat(gap)}${right}`;
|
|
5563
|
+
const room = width - visibleWidth(right) - 1;
|
|
5564
|
+
if (room < 8) return fit(left, width);
|
|
5565
|
+
return `${fit(left, room)} ${right}`;
|
|
5566
|
+
}
|
|
5567
|
+
function centre(line, width) {
|
|
5568
|
+
const pad4 = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
|
|
5569
|
+
return `${" ".repeat(pad4)}${line}`;
|
|
5570
|
+
}
|
|
5571
|
+
function overlay(base, left, piece, width) {
|
|
5572
|
+
const before = fit(base, left);
|
|
5573
|
+
const after = sliceVisible(base, left + visibleWidth(piece));
|
|
5574
|
+
return fit(`${before}${ESC6}[0m${piece}${ESC6}[0m${after}`, width);
|
|
5575
|
+
}
|
|
5576
|
+
function sliceVisible(line, from) {
|
|
5577
|
+
let visible = 0;
|
|
5578
|
+
let out = "";
|
|
5579
|
+
let i = 0;
|
|
5580
|
+
while (i < line.length) {
|
|
5581
|
+
if (line[i] === ESC6) {
|
|
5582
|
+
const end = line.indexOf("m", i);
|
|
5583
|
+
if (end === -1) break;
|
|
5584
|
+
out += line.slice(i, end + 1);
|
|
5585
|
+
i = end + 1;
|
|
5586
|
+
continue;
|
|
5587
|
+
}
|
|
5588
|
+
if (visible >= from) out += line[i];
|
|
5589
|
+
visible++;
|
|
5590
|
+
i++;
|
|
5591
|
+
}
|
|
5592
|
+
return out;
|
|
5593
|
+
}
|
|
5594
|
+
function summarise2(body2) {
|
|
5595
|
+
const text = typeof body2.text === "string" ? body2.text : "";
|
|
5596
|
+
if (body2.kind === "tool") {
|
|
5597
|
+
const results = Array.isArray(body2.results) ? body2.results.length : 0;
|
|
5598
|
+
return `${results} result(s)`;
|
|
5599
|
+
}
|
|
5600
|
+
return text.replace(/\s+/g, " ").slice(0, 60);
|
|
5601
|
+
}
|
|
5602
|
+
function sleep3(ms) {
|
|
5603
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3745
5604
|
}
|
|
3746
5605
|
|
|
3747
5606
|
// packages/cli/src/modes/print.ts
|
|
@@ -3796,8 +5655,8 @@ async function runPrint(runtime, opts) {
|
|
|
3796
5655
|
else toStderr(`${paint.red("labd:")} ${message}
|
|
3797
5656
|
`);
|
|
3798
5657
|
},
|
|
3799
|
-
done(
|
|
3800
|
-
if (opts.json) emit({ type: "done", credits:
|
|
5658
|
+
done(info2) {
|
|
5659
|
+
if (opts.json) emit({ type: "done", credits: info2.credits, context: info2.context });
|
|
3801
5660
|
},
|
|
3802
5661
|
commandOutput(line) {
|
|
3803
5662
|
if (opts.json) emit({ type: "command_output", line });
|
|
@@ -3854,10 +5713,10 @@ async function runRpc(runtime) {
|
|
|
3854
5713
|
process.stdin.setEncoding("utf8");
|
|
3855
5714
|
process.stdin.on("data", (chunk) => {
|
|
3856
5715
|
buffer += chunk;
|
|
3857
|
-
let
|
|
3858
|
-
while ((
|
|
3859
|
-
const line = buffer.slice(0,
|
|
3860
|
-
buffer = buffer.slice(
|
|
5716
|
+
let at2;
|
|
5717
|
+
while ((at2 = buffer.indexOf("\n")) !== -1) {
|
|
5718
|
+
const line = buffer.slice(0, at2);
|
|
5719
|
+
buffer = buffer.slice(at2 + 1);
|
|
3861
5720
|
if (line.trim()) void handle(line);
|
|
3862
5721
|
}
|
|
3863
5722
|
});
|
|
@@ -3954,8 +5813,8 @@ async function runRpc(runtime) {
|
|
|
3954
5813
|
error(message) {
|
|
3955
5814
|
notify("labd/error", { message });
|
|
3956
5815
|
},
|
|
3957
|
-
done(
|
|
3958
|
-
notify("labd/done",
|
|
5816
|
+
done(info2) {
|
|
5817
|
+
notify("labd/done", info2);
|
|
3959
5818
|
},
|
|
3960
5819
|
commandOutput(line) {
|
|
3961
5820
|
notify("labd/command_output", { line });
|
|
@@ -4007,7 +5866,7 @@ async function runRpc(runtime) {
|
|
|
4007
5866
|
}
|
|
4008
5867
|
|
|
4009
5868
|
// packages/cli/src/index.ts
|
|
4010
|
-
var VERSION = true ? "0.
|
|
5869
|
+
var VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
4011
5870
|
async function main(argv) {
|
|
4012
5871
|
const args = parse(argv);
|
|
4013
5872
|
if (args.color !== void 0) setColor(args.color);
|
|
@@ -4033,11 +5892,16 @@ async function main(argv) {
|
|
|
4033
5892
|
default:
|
|
4034
5893
|
break;
|
|
4035
5894
|
}
|
|
5895
|
+
const screenCapable = !args.rpc && !args.print && !args.question && !args.plain && process.stdin.isTTY && process.stdout.isTTY;
|
|
5896
|
+
if (args.pickSession && !screenCapable) {
|
|
5897
|
+
throw new UsageError("--resume with no id opens a list, which needs the full-screen session. `labd sessions` prints the ids for `labd --resume <id>`.");
|
|
5898
|
+
}
|
|
4036
5899
|
const runtime = await createRuntime({
|
|
4037
5900
|
root,
|
|
4038
5901
|
overrides: {
|
|
4039
5902
|
...args.mode ? { mode: args.mode } : {},
|
|
4040
|
-
...args.effort ? { effort: args.effort } : {}
|
|
5903
|
+
...args.effort ? { effort: args.effort } : {},
|
|
5904
|
+
...args.mouse !== void 0 ? { mouse: args.mouse } : {}
|
|
4041
5905
|
},
|
|
4042
5906
|
resume: args.resume ? resolveSession(args.resume, root) : args.continue ? latestSession(root) : void 0,
|
|
4043
5907
|
extensions: { enabled: !args.noExtensions, extra: args.extensions }
|
|
@@ -4058,7 +5922,8 @@ async function main(argv) {
|
|
|
4058
5922
|
`);
|
|
4059
5923
|
return 1;
|
|
4060
5924
|
}
|
|
4061
|
-
return runInteractive(runtime);
|
|
5925
|
+
if (args.plain) return runInteractive(runtime);
|
|
5926
|
+
return runScreen(runtime, { mouse: runtime.settings.mouse, sidebar: runtime.settings.sidebar, pickSession: args.pickSession });
|
|
4062
5927
|
}
|
|
4063
5928
|
async function login(root) {
|
|
4064
5929
|
const settings = loadSettings(root, {}, { trusted: isTrusted(stateDir(), root) });
|
|
@@ -4093,9 +5958,11 @@ ${paint.red("labd:")} ${result.reason ?? "that did not complete"}.
|
|
|
4093
5958
|
}
|
|
4094
5959
|
async function logout(root) {
|
|
4095
5960
|
const settings = loadSettings(root, {}, { trusted: isTrusted(stateDir(), root) });
|
|
4096
|
-
await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
4097
|
-
process.
|
|
5961
|
+
const hadStored = await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
5962
|
+
for (const line of signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n")) {
|
|
5963
|
+
process.stderr.write(` ${paint.muted(line)}
|
|
4098
5964
|
`);
|
|
5965
|
+
}
|
|
4099
5966
|
return 0;
|
|
4100
5967
|
}
|
|
4101
5968
|
function listSessions(root) {
|
|
@@ -4108,7 +5975,7 @@ function listSessions(root) {
|
|
|
4108
5975
|
}
|
|
4109
5976
|
for (const s of all.slice(0, 25)) {
|
|
4110
5977
|
process.stdout.write(
|
|
4111
|
-
` ${paint.dim(s.id.slice(0, 8))} ${paint.muted(new Date(s.at).toLocaleString())} ${paint.muted(`${s.messages} nodes`)} ${s
|
|
5978
|
+
` ${paint.dim(s.id.slice(0, 8))} ${paint.muted(new Date(s.at).toLocaleString())} ${paint.muted(`${s.messages} nodes`)} ${sessionTitle(s)}
|
|
4112
5979
|
`
|
|
4113
5980
|
);
|
|
4114
5981
|
}
|
|
@@ -4140,11 +6007,13 @@ function parse(argv) {
|
|
|
4140
6007
|
print: false,
|
|
4141
6008
|
json: false,
|
|
4142
6009
|
rpc: false,
|
|
6010
|
+
pickSession: false,
|
|
4143
6011
|
continue: false,
|
|
4144
6012
|
noExtensions: false,
|
|
4145
6013
|
extensions: [],
|
|
4146
6014
|
help: false,
|
|
4147
|
-
version: false
|
|
6015
|
+
version: false,
|
|
6016
|
+
plain: false
|
|
4148
6017
|
};
|
|
4149
6018
|
const words = [];
|
|
4150
6019
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -4170,10 +6039,14 @@ function parse(argv) {
|
|
|
4170
6039
|
out.effort = value;
|
|
4171
6040
|
break;
|
|
4172
6041
|
}
|
|
6042
|
+
case "-r":
|
|
4173
6043
|
case "--resume": {
|
|
4174
|
-
const value =
|
|
4175
|
-
if (!value
|
|
4176
|
-
|
|
6044
|
+
const value = argv[i + 1];
|
|
6045
|
+
if (!value || value.startsWith("-")) {
|
|
6046
|
+
out.pickSession = true;
|
|
6047
|
+
break;
|
|
6048
|
+
}
|
|
6049
|
+
out.resume = next();
|
|
4177
6050
|
break;
|
|
4178
6051
|
}
|
|
4179
6052
|
case "-c":
|
|
@@ -4203,6 +6076,12 @@ function parse(argv) {
|
|
|
4203
6076
|
case "--color":
|
|
4204
6077
|
out.color = true;
|
|
4205
6078
|
break;
|
|
6079
|
+
case "--plain":
|
|
6080
|
+
out.plain = true;
|
|
6081
|
+
break;
|
|
6082
|
+
case "--no-mouse":
|
|
6083
|
+
out.mouse = false;
|
|
6084
|
+
break;
|
|
4206
6085
|
case "-h":
|
|
4207
6086
|
case "--help":
|
|
4208
6087
|
out.help = true;
|
|
@@ -4227,6 +6106,9 @@ function parse(argv) {
|
|
|
4227
6106
|
} else {
|
|
4228
6107
|
out.question = words.join(" ");
|
|
4229
6108
|
}
|
|
6109
|
+
if ((out.resume || out.pickSession) && out.continue) {
|
|
6110
|
+
throw new UsageError("--resume and --continue both choose a session - pass one of them");
|
|
6111
|
+
}
|
|
4230
6112
|
return out;
|
|
4231
6113
|
}
|
|
4232
6114
|
function applyMode(out, value) {
|
|
@@ -4246,7 +6128,7 @@ function usage() {
|
|
|
4246
6128
|
${paint.accent(FACE)} ${paint.bold("labd")} - the Dev Labs assistant, in your terminal
|
|
4247
6129
|
|
|
4248
6130
|
${paint.bold("USAGE")}
|
|
4249
|
-
labd start a session in this project
|
|
6131
|
+
labd start a session in this project (full-screen)
|
|
4250
6132
|
labd "why is this failing" ask one question and exit
|
|
4251
6133
|
labd -p "..." | pbcopy print mode, for pipes and scripts
|
|
4252
6134
|
cat log | labd -p "why?" piped input is added to the question
|
|
@@ -4262,12 +6144,15 @@ ${paint.bold("OPTIONS")}
|
|
|
4262
6144
|
--json emit newline-delimited JSON events
|
|
4263
6145
|
--mode <m> ${AGENT_MODES.join(" | ")} (or rpc, for JSON-RPC over stdio)
|
|
4264
6146
|
--think <low|medium|high>
|
|
4265
|
-
|
|
6147
|
+
-r, --resume [id] continue an earlier session; with no id, pick one from a list
|
|
4266
6148
|
-c, --continue continue the most recent session here
|
|
4267
6149
|
-C, --cwd <dir> run as though started in <dir>
|
|
4268
6150
|
-e, --extension <path> load an extension file (repeatable)
|
|
4269
6151
|
--no-extensions load none
|
|
4270
6152
|
--no-color plain text (also honours NO_COLOR)
|
|
6153
|
+
--no-mouse leave the mouse to the terminal: the session takes it for
|
|
6154
|
+
wheel scrolling, and native text selection needs it released
|
|
6155
|
+
--plain the older print-style session (deprecated; removed in 0.5)
|
|
4271
6156
|
-h, --help -v, --version
|
|
4272
6157
|
|
|
4273
6158
|
${paint.bold("MODES")}
|