@thedevlabs-io/labd 0.7.0 → 0.8.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 +23 -8
- package/README.md +38 -11
- package/dist/labd.js +1716 -1311
- package/dist/labd.js.map +4 -4
- package/package.json +1 -1
package/dist/labd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/cli/src/index.ts
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { resolve as resolve2 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// vendor/labd/packages/protocol/src/activity.ts
|
|
7
7
|
var ACTIVITY_WEEKS = 53;
|
|
@@ -105,6 +105,8 @@ function surfaceLabel(surface) {
|
|
|
105
105
|
return "memory (background)";
|
|
106
106
|
case "api":
|
|
107
107
|
return "API";
|
|
108
|
+
case "help":
|
|
109
|
+
return "plans & billing help";
|
|
108
110
|
default:
|
|
109
111
|
return surface;
|
|
110
112
|
}
|
|
@@ -186,7 +188,7 @@ function rankPaths(query, paths, limit = 12) {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
// vendor/labd/packages/protocol/src/workspace.ts
|
|
189
|
-
var SECRETISH = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx)|id_(rsa|ed25519)|.*credentials.*|.*secrets?\.(json|ya?ml|toml))$/i;
|
|
191
|
+
var SECRETISH = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx)|id_(rsa|ed25519)|.*credentials.*|.*secrets?\.(json|ya?ml|toml)|\.dev\.vars|\.npmrc|\.netrc|\.pypirc|\.yarnrc\.yml|\.git\/config)$/i;
|
|
190
192
|
var EXCLUDED_DIRS = [
|
|
191
193
|
".git",
|
|
192
194
|
"node_modules",
|
|
@@ -201,6 +203,7 @@ var OFF_LIMITS = new RegExp(
|
|
|
201
203
|
`(^|/)(${EXCLUDED_DIRS.map((d) => d.replace(/\./g, "\\.")).join("|")})(/|$)`,
|
|
202
204
|
"i"
|
|
203
205
|
);
|
|
206
|
+
var EDITOR_SETTINGS = /(^|\/)\.vscode\/(settings|tasks|launch|mcp)\.json$|(^|\/)\.devcontainer(\/|$)|(^|\/)\.devcontainer\.json$|\.code-workspace$/i;
|
|
204
207
|
var EXCLUDED_GLOB = `**/{${EXCLUDED_DIRS.join(",")}}/**`;
|
|
205
208
|
var ASSIGNMENT = /^(\s*(?:export\s+)?["']?[\w.-]+["']?\s*[:=]\s*)(.+)$/;
|
|
206
209
|
function redactValues(text) {
|
|
@@ -293,8 +296,12 @@ function hasControlChars(command) {
|
|
|
293
296
|
}
|
|
294
297
|
var INTERPRETERS = /^(sh|bash|zsh|fish|dash|ksh|python[\d.]*|node|deno|bun|ruby|perl|php|osascript)$/;
|
|
295
298
|
var DOWNLOADERS = /^(curl|wget|fetch|httpie|http)$/;
|
|
296
|
-
var WRAPPERS = /^(command|exec|xargs|time|nice|builtin)$/;
|
|
299
|
+
var WRAPPERS = /^(command|exec|xargs|time|nice|builtin|env)$/;
|
|
300
|
+
var WRAPPER_VALUED = /^(-u|-C|-S|-I|-n|-P|-L|-d|-s|--unset|--chdir)$/;
|
|
301
|
+
var ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
297
302
|
var SHELLS_WITH_C = /^(sh|bash|zsh|dash|ksh|fish)$/;
|
|
303
|
+
var PROTECTED_REASON = "it would write a file labd never changes: the editor's settings, task or container files, or something inside .git. Those run commands or change rules outside any approval. Tell the learner what to change and let them do it.";
|
|
304
|
+
var SECRET_REASON = "it would read a file that holds credentials in the clear. labd only ever sees which keys are set, never their values, and a shell command is not a way around that - tell the learner what to change instead.";
|
|
298
305
|
var REFUSALS = [
|
|
299
306
|
{
|
|
300
307
|
// Everything after this runs as a different user, which puts it outside
|
|
@@ -362,9 +369,17 @@ var REFUSALS = [
|
|
|
362
369
|
test: (argv) => /^(vi|vim|nvim|nano|emacs|less|more|top|htop|watch|ssh|telnet|ftp|man)$/.test(argv[0] ?? "") || argv[0] === "tail" && argv.includes("-f"),
|
|
363
370
|
reason: "it waits for input or never exits, so there would be no output to read. Ask for a command that finishes."
|
|
364
371
|
},
|
|
372
|
+
{
|
|
373
|
+
test: (argv) => writesAProtectedFile(argv),
|
|
374
|
+
reason: PROTECTED_REASON
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
test: (argv) => changesGitExecution(argv),
|
|
378
|
+
reason: "it changes a git setting that decides which programs git runs (a hook path, pager, editor, alias, credential helper, include and the like), which would run code later outside any approval. Tell the learner what to set and why."
|
|
379
|
+
},
|
|
365
380
|
{
|
|
366
381
|
test: (argv) => namesASecret(argv),
|
|
367
|
-
reason:
|
|
382
|
+
reason: SECRET_REASON
|
|
368
383
|
},
|
|
369
384
|
{
|
|
370
385
|
test: (argv) => argv.length === 1 && /^(env|printenv|set)$/.test(argv[0] ?? ""),
|
|
@@ -385,9 +400,13 @@ function isBigTarget(t) {
|
|
|
385
400
|
}
|
|
386
401
|
function unwrap(argv) {
|
|
387
402
|
let out = argv;
|
|
388
|
-
|
|
403
|
+
for (; ; ) {
|
|
404
|
+
while (out.length > 1 && ASSIGNMENT_WORD.test(out[0])) out = out.slice(1);
|
|
405
|
+
if (!(out.length > 1 && WRAPPERS.test(bare(out[0])))) break;
|
|
389
406
|
out = out.slice(1);
|
|
390
|
-
while (out.length > 1 && (out[0].startsWith("-") || /^\d+$/.test(out[0])
|
|
407
|
+
while (out.length > 1 && (out[0].startsWith("-") || /^\d+$/.test(out[0]) || ASSIGNMENT_WORD.test(out[0]))) {
|
|
408
|
+
out = WRAPPER_VALUED.test(out[0]) && out.length > 2 ? out.slice(2) : out.slice(1);
|
|
409
|
+
}
|
|
391
410
|
}
|
|
392
411
|
return out.length ? [bare(out[0]), ...out.slice(1)] : out;
|
|
393
412
|
}
|
|
@@ -420,14 +439,115 @@ function pipesNetworkIntoShell(argv) {
|
|
|
420
439
|
return downloader && interpreter;
|
|
421
440
|
}
|
|
422
441
|
function namesASecret(argv) {
|
|
423
|
-
return argv.slice(1).some((a) => SECRETISH.test(bare(a)));
|
|
442
|
+
return argv.slice(1).some((a) => SECRETISH.test(bare(unredirect(a))) || argPaths(a).some((p) => SECRETISH.test(p)));
|
|
443
|
+
}
|
|
444
|
+
var REDIRECTION = /^\d*(<>|>\||[<>]+)&?/;
|
|
445
|
+
var unredirect = (arg) => arg.replace(/^["']|["']$/g, "").replace(REDIRECTION, "");
|
|
446
|
+
function argPath(arg) {
|
|
447
|
+
return unredirect(arg).replace(/\\/g, "/");
|
|
448
|
+
}
|
|
449
|
+
function argPaths(arg) {
|
|
450
|
+
const whole = argPath(arg);
|
|
451
|
+
const value = whole.includes("=") ? whole.slice(whole.indexOf("=") + 1) : null;
|
|
452
|
+
return [whole, ...value ? [value] : []].map((p) => {
|
|
453
|
+
const out = [];
|
|
454
|
+
for (const seg of p.split("/")) {
|
|
455
|
+
if (seg === "" || seg === ".") continue;
|
|
456
|
+
if (seg === ".." && out.length && out[out.length - 1] !== "..") out.pop();
|
|
457
|
+
else out.push(seg);
|
|
458
|
+
}
|
|
459
|
+
return out.join("/");
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var WRITERS = /^(rm|rmdir|mv|cp|tee|touch|mkdir|ln|chmod|chown|install|rsync|truncate|patch|unzip|dd|copy|xcopy|robocopy|move|del|erase|ren|rename|rd|md)$/i;
|
|
463
|
+
var EXCLUDING = /^(-path|-ipath|-prune|--exclude|--exclude-dir|--exclude-from)$/;
|
|
464
|
+
var PROTECTED_PATH = (p) => EDITOR_SETTINGS.test(p) || /(^|\/)\.vscode$/i.test(p) || /(^|\/)\.git(\/|$)/i.test(p);
|
|
465
|
+
function writtenPaths(argv) {
|
|
466
|
+
const out = [];
|
|
467
|
+
const writer = WRITERS.test(argv[0] ?? "") || argv[0] === "sed" && argv.some((a) => /^-i/.test(a) || a === "--in-place");
|
|
468
|
+
for (let i = 0; i < argv.length; i++) {
|
|
469
|
+
const a = argv[i].replace(/^["']|["']$/g, "");
|
|
470
|
+
if (EXCLUDING.test(a)) {
|
|
471
|
+
i++;
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (/^--exclude/.test(a)) continue;
|
|
475
|
+
const op = REDIRECT_OP.exec(a);
|
|
476
|
+
if (op) {
|
|
477
|
+
const writes = a.includes(">");
|
|
478
|
+
const duplicates = /&(\d+|-)$/.test(a);
|
|
479
|
+
if (!duplicates && i + 1 < argv.length) {
|
|
480
|
+
i++;
|
|
481
|
+
if (writes) out.push(...argPaths(argv[i]));
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (i > 0 && writer && !a.startsWith("-")) out.push(...argPaths(a));
|
|
486
|
+
}
|
|
487
|
+
return out;
|
|
488
|
+
}
|
|
489
|
+
var REDIRECT_OP = /^\d*(<>|>\||>>|>|<<<|<<|<)(&(\d+|-)?)?$/;
|
|
490
|
+
function writesAProtectedFile(argv) {
|
|
491
|
+
return writtenPaths(argv).some(PROTECTED_PATH);
|
|
492
|
+
}
|
|
493
|
+
var PROGRAM_KEYS = /^(core\.(hookspath|fsmonitor|pager|editor|sshcommand|askpass|gitproxy|alternaterefscommand)|diff\.external|credential\..*|filter\..*|include\..*|includeif\..*|alias\..*|sequence\.editor|gpg\..*program|pager\..*|diff\..*\.(command|textconv)|merge\..*\.driver|url\..*|hook\..*|submodule\..*\.update|remote\..*\.(uploadpack|receivepack)|mergetool\..*|difftool\..*)$/i;
|
|
494
|
+
var SAFE_CONFIG_KEYS = /^(user\.[\w.-]+|init\.defaultbranch|pull\.rebase|push\.default|color\.[\w.-]+|core\.autocrlf|core\.quotepath|merge\.conflictstyle)$/i;
|
|
495
|
+
var GIT_VALUED_OPTIONS = /^(-C|-c|--git-dir|--work-tree|--namespace|--config-env|--exec-path)$/;
|
|
496
|
+
var CONFIG_READ_FLAGS = /^(--get|--get-all|--get-regexp|--get-urlmatch|-l|--list)$/;
|
|
497
|
+
var CONFIG_WRITE_FLAGS = /^(--unset|--unset-all|--add|--replace-all|--rename-section|--remove-section|-e|--edit)$/;
|
|
498
|
+
function opensEditor(arg) {
|
|
499
|
+
if (/^--ed(i|it)?$/.test(arg)) return true;
|
|
500
|
+
return /^-[a-zA-Z]+$/.test(arg) && !arg.startsWith("-f") && arg.includes("e");
|
|
501
|
+
}
|
|
502
|
+
var CONFIG_READ_VERBS = /^(get|list)$/;
|
|
503
|
+
var CONFIG_VALUED = /^(-f|--file|--blob|--type|--default|--comment)$/;
|
|
504
|
+
function programSetting(pair) {
|
|
505
|
+
const eq = pair.indexOf("=");
|
|
506
|
+
const key = eq === -1 ? pair : pair.slice(0, eq);
|
|
507
|
+
const value = eq === -1 ? "" : pair.slice(eq + 1);
|
|
508
|
+
if (/^(core\.pager|pager\..*)$/i.test(key) && /^(cat)?$/.test(value)) return false;
|
|
509
|
+
return PROGRAM_KEYS.test(key);
|
|
510
|
+
}
|
|
511
|
+
function changesGitExecution(argv) {
|
|
512
|
+
if (bare(argv[0] ?? "") !== "git") return false;
|
|
513
|
+
const args = argv.slice(1).map((a) => a.replace(/^["']|["']$/g, ""));
|
|
514
|
+
let i = 0;
|
|
515
|
+
for (; i < args.length && args[i].startsWith("-"); i++) {
|
|
516
|
+
const a = args[i];
|
|
517
|
+
if (a === "-c" || a === "--config-env") {
|
|
518
|
+
if (programSetting(args[i + 1] ?? "")) return true;
|
|
519
|
+
i++;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (/^-c./.test(a) && programSetting(a.slice(2))) return true;
|
|
523
|
+
if (/^--config-env=/.test(a) && programSetting(a.slice("--config-env=".length))) return true;
|
|
524
|
+
if (GIT_VALUED_OPTIONS.test(a)) i++;
|
|
525
|
+
}
|
|
526
|
+
if (args[i] !== "config") return false;
|
|
527
|
+
const rest = args.slice(i + 1);
|
|
528
|
+
if (rest.some(opensEditor) || rest.find((a) => !a.startsWith("-")) === "edit") return true;
|
|
529
|
+
if (rest.some((a) => CONFIG_READ_FLAGS.test(a))) return false;
|
|
530
|
+
const words = [];
|
|
531
|
+
for (let j = 0; j < rest.length; j++) {
|
|
532
|
+
const a = rest[j];
|
|
533
|
+
if (CONFIG_VALUED.test(a)) {
|
|
534
|
+
j++;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (a.startsWith("-")) continue;
|
|
538
|
+
words.push(a);
|
|
539
|
+
}
|
|
540
|
+
if (CONFIG_READ_VERBS.test(words[0] ?? "")) return false;
|
|
541
|
+
if (words.length === 1 && !rest.some((a) => CONFIG_WRITE_FLAGS.test(a))) return false;
|
|
542
|
+
const key = /^(set|unset)$/.test(words[0] ?? "") ? words[1] : words[0];
|
|
543
|
+
return key !== void 0 && !SAFE_CONFIG_KEYS.test(key);
|
|
424
544
|
}
|
|
425
545
|
function bare(arg) {
|
|
426
546
|
const unquoted = arg.replace(/^["']|["']$/g, "");
|
|
427
547
|
const slash = Math.max(unquoted.lastIndexOf("/"), unquoted.lastIndexOf("\\"));
|
|
428
548
|
return slash === -1 ? unquoted : unquoted.slice(slash + 1);
|
|
429
549
|
}
|
|
430
|
-
function segments(command) {
|
|
550
|
+
function segments(command, literalBackslash = false) {
|
|
431
551
|
const out = [];
|
|
432
552
|
let argv = [];
|
|
433
553
|
let token = "";
|
|
@@ -452,7 +572,7 @@ function segments(command) {
|
|
|
452
572
|
quote = c;
|
|
453
573
|
continue;
|
|
454
574
|
}
|
|
455
|
-
if (c === "\\" && i + 1 < command.length) {
|
|
575
|
+
if (c === "\\" && i + 1 < command.length && !literalBackslash) {
|
|
456
576
|
token += command[++i];
|
|
457
577
|
continue;
|
|
458
578
|
}
|
|
@@ -470,6 +590,20 @@ function segments(command) {
|
|
|
470
590
|
argv.push(c);
|
|
471
591
|
continue;
|
|
472
592
|
}
|
|
593
|
+
if (c === "<" || c === ">") {
|
|
594
|
+
let op = /^\d+$/.test(token) ? token : "";
|
|
595
|
+
if (!op) endToken();
|
|
596
|
+
token = "";
|
|
597
|
+
op += c;
|
|
598
|
+
while (i + 1 < command.length && /[<>]/.test(command[i + 1])) op += command[++i];
|
|
599
|
+
if (c === ">" && command[i + 1] === "|") op += command[++i];
|
|
600
|
+
if (command[i + 1] === "&") {
|
|
601
|
+
op += command[++i];
|
|
602
|
+
while (i + 1 < command.length && /[\d-]/.test(command[i + 1])) op += command[++i];
|
|
603
|
+
}
|
|
604
|
+
argv.push(op);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
473
607
|
if (c === "&") {
|
|
474
608
|
const standalone = token === "" && (i === 0 || command[i - 1] === " " || command[i - 1] === " ");
|
|
475
609
|
if (standalone) argv.push("&");
|
|
@@ -502,6 +636,12 @@ function classifyCommand(command) {
|
|
|
502
636
|
}
|
|
503
637
|
for (const segment of segments(raw)) {
|
|
504
638
|
const argv = unwrap(segment);
|
|
639
|
+
if (segment.some((word) => /^(export\s+)?GIT_CONFIG\w*=/.test(word))) {
|
|
640
|
+
return {
|
|
641
|
+
ok: false,
|
|
642
|
+
reason: "it passes git configuration through the environment (GIT_CONFIG_*), which can make git run other programs. Tell the learner what to set and why."
|
|
643
|
+
};
|
|
644
|
+
}
|
|
505
645
|
if (argv.includes("&")) {
|
|
506
646
|
return {
|
|
507
647
|
ok: false,
|
|
@@ -519,6 +659,13 @@ function classifyCommand(command) {
|
|
|
519
659
|
}
|
|
520
660
|
}
|
|
521
661
|
}
|
|
662
|
+
for (const segment of segments(raw, true)) {
|
|
663
|
+
const argv = unwrap(segment);
|
|
664
|
+
for (const candidate of [argv, ...stages(argv)]) {
|
|
665
|
+
if (writesAProtectedFile(candidate)) return { ok: false, reason: PROTECTED_REASON };
|
|
666
|
+
if (namesASecret(candidate)) return { ok: false, reason: SECRET_REASON };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
522
669
|
return { ok: true };
|
|
523
670
|
}
|
|
524
671
|
|
|
@@ -613,6 +760,21 @@ function forExtraction(messages) {
|
|
|
613
760
|
};
|
|
614
761
|
});
|
|
615
762
|
}
|
|
763
|
+
var LOW_CREDITS_PERCENT = 15;
|
|
764
|
+
function safeTopUpUrl(url) {
|
|
765
|
+
const m = /^(https?):\/\/((?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)(:[1-9]\d{0,4})?(\/settings\/labd)(?:[?#]|$)/.exec(
|
|
766
|
+
(url ?? "").replace(/^[a-z]+:\/\/[^/?#]*/i, (origin) => origin.toLowerCase())
|
|
767
|
+
);
|
|
768
|
+
if (!m) return null;
|
|
769
|
+
const [, scheme, host, port = "", path] = m;
|
|
770
|
+
if (port && Number(port.slice(1)) > 65535) return null;
|
|
771
|
+
const local = host === "localhost" || host === "127.0.0.1";
|
|
772
|
+
const ours = host === "thedevlabs.io" || host.endsWith(".thedevlabs.io");
|
|
773
|
+
if (!(ours && scheme === "https" || local && scheme === "http")) return null;
|
|
774
|
+
return `${scheme}://${host}${port}${path}`;
|
|
775
|
+
}
|
|
776
|
+
var isOutOfCredits = (credits) => credits?.percentLeft === 0;
|
|
777
|
+
var isLowOnCredits = (credits) => credits != null && credits.percentLeft > 0 && credits.percentLeft <= LOW_CREDITS_PERCENT;
|
|
616
778
|
|
|
617
779
|
// packages/core/src/agent.ts
|
|
618
780
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -1381,7 +1543,7 @@ async function runCommand(input, ctx) {
|
|
|
1381
1543
|
].join("\n");
|
|
1382
1544
|
}
|
|
1383
1545
|
function execute(command, ctx) {
|
|
1384
|
-
return new Promise((
|
|
1546
|
+
return new Promise((resolve3) => {
|
|
1385
1547
|
const windows = process.platform === "win32";
|
|
1386
1548
|
const shell = windows ? process.env.ComSpec || "cmd.exe" : process.env.SHELL || "/bin/bash";
|
|
1387
1549
|
const args = windows ? ["/d", "/s", "/c", command] : ["-lc", command];
|
|
@@ -1436,7 +1598,7 @@ ${tail}` : head + tail;
|
|
|
1436
1598
|
if (settled) return;
|
|
1437
1599
|
settled = true;
|
|
1438
1600
|
clearTimeout(timer);
|
|
1439
|
-
|
|
1601
|
+
resolve3({ code, output: output(), timedOut });
|
|
1440
1602
|
};
|
|
1441
1603
|
child.on("close", done);
|
|
1442
1604
|
child.on("error", (err) => {
|
|
@@ -1666,11 +1828,14 @@ async function runTurn(question, opts) {
|
|
|
1666
1828
|
case "need_tools":
|
|
1667
1829
|
needTools = true;
|
|
1668
1830
|
return "go";
|
|
1831
|
+
case "ready":
|
|
1832
|
+
ui.ready?.({ credits: event.credits });
|
|
1833
|
+
return "go";
|
|
1669
1834
|
case "done":
|
|
1670
1835
|
ui.done?.({ credits: event.credits, context: event.context });
|
|
1671
1836
|
return "go";
|
|
1672
1837
|
case "error":
|
|
1673
|
-
ui.error(event.message);
|
|
1838
|
+
ui.error(event.message, { code: event.code, ...event.credits ? { credits: event.credits } : {} });
|
|
1674
1839
|
return "stop";
|
|
1675
1840
|
default:
|
|
1676
1841
|
return "go";
|
|
@@ -2100,9 +2265,14 @@ var AgentClient = class {
|
|
|
2100
2265
|
if (this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
|
|
2101
2266
|
return headers;
|
|
2102
2267
|
}
|
|
2103
|
-
|
|
2268
|
+
/**
|
|
2269
|
+
* What is left of this month's allowance, or null when the server cannot say.
|
|
2270
|
+
* Bounded, because the sessions ask for it as they open and a slow server
|
|
2271
|
+
* must not hold the first frame.
|
|
2272
|
+
*/
|
|
2273
|
+
async credits(timeoutMs = 5e3) {
|
|
2104
2274
|
try {
|
|
2105
|
-
const res = await this.fetch(`${this.opts.baseUrl}/v1/me`, { headers: this.headers() });
|
|
2275
|
+
const res = await this.fetch(`${this.opts.baseUrl}/v1/me`, { headers: this.headers(), signal: AbortSignal.timeout(timeoutMs) });
|
|
2106
2276
|
if (!res.ok) return null;
|
|
2107
2277
|
const body2 = await res.json();
|
|
2108
2278
|
return body2.credits ?? null;
|
|
@@ -2169,11 +2339,16 @@ var AgentClient = class {
|
|
|
2169
2339
|
}
|
|
2170
2340
|
if (!res.ok || !res.body) {
|
|
2171
2341
|
let message = `The assistant is unreachable (${res.status}).`;
|
|
2342
|
+
let body2 = {};
|
|
2172
2343
|
try {
|
|
2173
|
-
|
|
2344
|
+
body2 = await res.json();
|
|
2174
2345
|
if (body2.message) message = body2.message;
|
|
2175
2346
|
} catch {
|
|
2176
2347
|
}
|
|
2348
|
+
if (res.status === 402 && body2.code === "no_credits") {
|
|
2349
|
+
yield { type: "error", code: "no_credits", message, ...body2.credits ? { credits: body2.credits } : {} };
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2177
2352
|
if (res.status === 401) message = `${message} Run \`labd login\`.`;
|
|
2178
2353
|
yield { type: "error", code: res.status === 401 ? "unauthorized" : "upstream", message };
|
|
2179
2354
|
return;
|
|
@@ -2382,1369 +2557,1430 @@ function visibleWidth(line) {
|
|
|
2382
2557
|
return line.replace(new RegExp(`${ESC2}\\[[0-9;]*m`, "g"), "").length;
|
|
2383
2558
|
}
|
|
2384
2559
|
|
|
2385
|
-
// packages/tui/src/
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
const
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
const
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2560
|
+
// packages/tui/src/markdown.ts
|
|
2561
|
+
function renderMarkdown(text, opts) {
|
|
2562
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
2563
|
+
const pad4 = opts.indent ?? "";
|
|
2564
|
+
const out = [];
|
|
2565
|
+
let inFence = false;
|
|
2566
|
+
let fenceLang = "";
|
|
2567
|
+
for (const line of lines) {
|
|
2568
|
+
const fence = /^\s*```(\w*)/.exec(line);
|
|
2569
|
+
if (fence) {
|
|
2570
|
+
if (!inFence) {
|
|
2571
|
+
inFence = true;
|
|
2572
|
+
fenceLang = fence[1] ?? "";
|
|
2573
|
+
out.push(paint.dim(`${pad4}${fenceLang || "code"}`));
|
|
2574
|
+
} else {
|
|
2575
|
+
inFence = false;
|
|
2576
|
+
fenceLang = "";
|
|
2577
|
+
out.push("");
|
|
2578
|
+
}
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (inFence) {
|
|
2582
|
+
out.push(`${paint.dim(`${pad4}|`)} ${line}`);
|
|
2583
|
+
continue;
|
|
2407
2584
|
}
|
|
2585
|
+
out.push(...prose(line, opts.width, pad4));
|
|
2408
2586
|
}
|
|
2409
|
-
return out;
|
|
2587
|
+
return out.join("\n");
|
|
2410
2588
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
create_file: "Create",
|
|
2417
|
-
move_file: "Move",
|
|
2418
|
-
delete_file: "Delete",
|
|
2419
|
-
run_command: "Run",
|
|
2420
|
-
spawn_agent: "Helper"
|
|
2421
|
-
};
|
|
2422
|
-
function approvalCard(preview, call, mode, width) {
|
|
2423
|
-
const out = [""];
|
|
2424
|
-
if (call.name === "run_command" || preview?.kind === "run") {
|
|
2425
|
-
const command = preview?.kind === "run" ? preview.command : String(call.input.command ?? "");
|
|
2426
|
-
const why = preview?.kind === "run" ? preview.why : call.input.why;
|
|
2427
|
-
out.push(`${paint.yellow("?")} ${paint.bold("Run a command")}`);
|
|
2428
|
-
if (why) out.push(` ${paint.muted(why)}`);
|
|
2429
|
-
out.push("");
|
|
2430
|
-
for (const line of wrapCommand(command, width - 6)) out.push(` ${paint.cyan(line)}`);
|
|
2431
|
-
out.push("");
|
|
2432
|
-
out.push(
|
|
2433
|
-
` ${paint.dim(
|
|
2434
|
-
mode === "auto" ? "On auto, commands still ask - a command can do anything your shell can, and put back isn't one of them." : "Runs in this project. Unlike an edit, this can't be put back."
|
|
2435
|
-
)}`
|
|
2436
|
-
);
|
|
2437
|
-
return out;
|
|
2589
|
+
function prose(line, width, pad4) {
|
|
2590
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
2591
|
+
if (heading) return [`${pad4}${paint.bold(paint.accent(heading[2]))}`];
|
|
2592
|
+
if (/^\s*([-*_])\s*\1\s*\1[\s-*_]*$/.test(line)) {
|
|
2593
|
+
return [`${pad4}${paint.dim("-".repeat(Math.min(width, 40)))}`];
|
|
2438
2594
|
}
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
const destination = preview?.to ?? call.input.to;
|
|
2444
|
-
const title = moving ? "Move" : deleting ? "Delete" : creating ? "Create" : "Edit";
|
|
2445
|
-
out.push(
|
|
2446
|
-
`${paint.yellow("?")} ${paint.bold(title)} ${paint.accent(path)}${moving && destination ? paint.muted(" \u2192 ") + paint.accent(destination) : ""}${preview ? paint.muted(
|
|
2447
|
-
` ${counts(preview.added, preview.removed)}${creating || moving || deleting ? "" : ` at line ${preview.startLine}`}`
|
|
2448
|
-
) : ""}`
|
|
2449
|
-
);
|
|
2450
|
-
if (preview) {
|
|
2451
|
-
out.push("");
|
|
2452
|
-
for (const line of diffLines(preview.oldText, preview.newText, width)) out.push(` ${line}`);
|
|
2595
|
+
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
2596
|
+
if (bullet) {
|
|
2597
|
+
const indent2 = `${pad4}${bullet[1]}${paint.accent("-")} `;
|
|
2598
|
+
return wrap2(inline(bullet[2]), width, indent2, `${pad4}${bullet[1]} `);
|
|
2453
2599
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
);
|
|
2460
|
-
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
if (
|
|
2465
|
-
|
|
2466
|
-
return parts.join(" ");
|
|
2467
|
-
}
|
|
2468
|
-
function diffLines(oldText, newText, width, max = 12) {
|
|
2469
|
-
const out = [];
|
|
2470
|
-
const before = oldText ? oldText.split("\n") : [];
|
|
2471
|
-
const after = newText ? newText.split("\n") : [];
|
|
2472
|
-
for (const line of before.slice(0, max)) out.push(paint.red(`- ${clip(line, width - 4)}`));
|
|
2473
|
-
if (before.length > max) out.push(paint.dim(` ... ${before.length - max} more removed`));
|
|
2474
|
-
for (const line of after.slice(0, max)) out.push(paint.green(`+ ${clip(line, width - 4)}`));
|
|
2475
|
-
if (after.length > max) out.push(paint.dim(` ... ${after.length - max} more added`));
|
|
2476
|
-
return out;
|
|
2600
|
+
const numbered = /^(\s*)(\d+)\.\s+(.*)$/.exec(line);
|
|
2601
|
+
if (numbered) {
|
|
2602
|
+
const indent2 = `${pad4}${numbered[1]}${paint.accent(`${numbered[2]}.`)} `;
|
|
2603
|
+
return wrap2(inline(numbered[3]), width, indent2, `${pad4}${numbered[1]} `);
|
|
2604
|
+
}
|
|
2605
|
+
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
2606
|
+
if (quote) {
|
|
2607
|
+
const gutter = `${pad4}${paint.dim("|")} `;
|
|
2608
|
+
return wrap2(paint.muted(inline(quote[1])), width, gutter, gutter);
|
|
2609
|
+
}
|
|
2610
|
+
if (!line.trim()) return [""];
|
|
2611
|
+
return wrap2(inline(line), width, pad4, pad4);
|
|
2477
2612
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2613
|
+
var HOLD = "\0";
|
|
2614
|
+
function inline(text) {
|
|
2615
|
+
const code = [];
|
|
2616
|
+
let held = text.replace(/`([^`]+)`/g, (_, body2) => {
|
|
2617
|
+
code.push(body2);
|
|
2618
|
+
return `${HOLD}${code.length - 1}${HOLD}`;
|
|
2619
|
+
});
|
|
2620
|
+
held = held.replace(/\*\*([^*]+)\*\*/g, (_, body2) => paint.bold(body2)).replace(/(^|[\s(])\*([^*]+)\*/g, (_, pre, body2) => `${pre}${paint.bold(body2)}`).replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label2, href) => `${label2} ${paint.dim(`(${href})`)}`);
|
|
2621
|
+
return held.replace(
|
|
2622
|
+
new RegExp(`${HOLD}(\\d+)${HOLD}`, "g"),
|
|
2623
|
+
(_, n) => paint.cyan(code[Number(n)] ?? "")
|
|
2624
|
+
);
|
|
2480
2625
|
}
|
|
2481
|
-
function
|
|
2482
|
-
|
|
2626
|
+
function wrap2(text, width, first, rest) {
|
|
2627
|
+
const limit = Math.max(20, width - visibleWidth(first));
|
|
2628
|
+
if (visibleWidth(text) <= limit) return [`${first}${text}`];
|
|
2483
2629
|
const out = [];
|
|
2484
2630
|
let line = "";
|
|
2485
2631
|
for (const word of text.split(" ")) {
|
|
2486
|
-
if (
|
|
2632
|
+
if (line && visibleWidth(line) + 1 + visibleWidth(word) > limit) {
|
|
2633
|
+
out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
2487
2634
|
line = word;
|
|
2488
|
-
} else if (`${line} ${word}`.length <= width) {
|
|
2489
|
-
line = `${line} ${word}`;
|
|
2490
2635
|
} else {
|
|
2491
|
-
|
|
2492
|
-
line = word;
|
|
2493
|
-
}
|
|
2494
|
-
while (line.length > width) {
|
|
2495
|
-
out.push(line.slice(0, width));
|
|
2496
|
-
line = line.slice(width);
|
|
2636
|
+
line = line ? `${line} ${word}` : word;
|
|
2497
2637
|
}
|
|
2498
2638
|
}
|
|
2499
|
-
if (line) out.push(line);
|
|
2500
|
-
return out;
|
|
2501
|
-
}
|
|
2502
|
-
function wrapCommand(command, width) {
|
|
2503
|
-
if (command.length <= width) return [command];
|
|
2504
|
-
const out = [];
|
|
2505
|
-
for (let i = 0; i < command.length; i += width) out.push(command.slice(i, i + width));
|
|
2639
|
+
if (line) out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
2506
2640
|
return out;
|
|
2507
2641
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2642
|
+
|
|
2643
|
+
// packages/tui/src/transcript.ts
|
|
2644
|
+
var LIVE_LINES = 8;
|
|
2645
|
+
var Transcript = class {
|
|
2646
|
+
entries = [];
|
|
2647
|
+
nextId = 1;
|
|
2648
|
+
cache = /* @__PURE__ */ new Map();
|
|
2649
|
+
/** For a streaming answer: the rendered COMPLETE part, so only the tail is re-rendered per chunk. */
|
|
2650
|
+
prose = /* @__PURE__ */ new Map();
|
|
2651
|
+
get length() {
|
|
2652
|
+
return this.entries.length;
|
|
2513
2653
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
if (info2.creditsPercent !== null && info2.creditsPercent !== void 0) {
|
|
2517
|
-
bits.push(paint.muted(`${info2.creditsPercent}% left`));
|
|
2654
|
+
get blocks() {
|
|
2655
|
+
return this.entries;
|
|
2518
2656
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
function spinnerFrame(tick) {
|
|
2525
|
-
const dots = ".".repeat(tick % 3 + 1).padEnd(3);
|
|
2526
|
-
return paint.dim(dots);
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
|
-
// packages/tui/src/composer.ts
|
|
2530
|
-
var ROUND = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
|
|
2531
|
-
var MAX_SUGGESTIONS = 5;
|
|
2532
|
-
function composer(opts) {
|
|
2533
|
-
const width = Math.max(20, opts.width);
|
|
2534
|
-
const colour = borderColour(opts.state);
|
|
2535
|
-
const inner = width - 4;
|
|
2536
|
-
const out = [];
|
|
2537
|
-
for (const line of suggestionLines(opts.suggestions ?? [], inner)) out.push(line);
|
|
2538
|
-
out.push(colour(`${ROUND.tl}${ROUND.h.repeat(width - 2)}${ROUND.tr}`));
|
|
2539
|
-
out.push(`${colour(ROUND.v)} ${body(opts, inner)} ${colour(ROUND.v)}`);
|
|
2540
|
-
out.push(colour(`${ROUND.bl}${ROUND.h.repeat(width - 2)}${ROUND.br}`));
|
|
2541
|
-
if (opts.hint) out.push(` ${truncate(opts.hint, width - 2)}`);
|
|
2542
|
-
if (opts.status) out.push(opts.status);
|
|
2543
|
-
return out;
|
|
2544
|
-
}
|
|
2545
|
-
function body(opts, inner) {
|
|
2546
|
-
const sigil = opts.state === "busy" ? paint.dim(">") : paint.accent(">");
|
|
2547
|
-
const room = inner - 2;
|
|
2548
|
-
if (!opts.view.text && opts.placeholder && opts.state !== "busy") {
|
|
2549
|
-
const text2 = truncate(paint.dim(opts.placeholder), room);
|
|
2550
|
-
return `${sigil} ${pad(text2, room)}`;
|
|
2551
|
-
}
|
|
2552
|
-
const text = withCursor(opts.view, room, opts.state === "busy");
|
|
2553
|
-
return `${sigil} ${pad(text, room)}`;
|
|
2554
|
-
}
|
|
2555
|
-
function withCursor(view, room, hidden) {
|
|
2556
|
-
const text = view.text.slice(0, room);
|
|
2557
|
-
if (hidden) return text;
|
|
2558
|
-
const at2 = Math.min(view.column, room - 1 < 0 ? 0 : room);
|
|
2559
|
-
const before = text.slice(0, at2);
|
|
2560
|
-
const under = text.slice(at2, at2 + 1) || " ";
|
|
2561
|
-
const after = text.slice(at2 + 1);
|
|
2562
|
-
return `${before}${paint.invert(under)}${after}`;
|
|
2563
|
-
}
|
|
2564
|
-
function suggestionLines(all, inner) {
|
|
2565
|
-
if (!all.length) return [];
|
|
2566
|
-
const shown = all.slice(0, MAX_SUGGESTIONS);
|
|
2567
|
-
const column = Math.max(...shown.map((s) => s.name.length + 1));
|
|
2568
|
-
const out = shown.map((s, i) => {
|
|
2569
|
-
const name = `/${s.name}`.padEnd(column + 1);
|
|
2570
|
-
const mark = i === 0 ? paint.accent("-") : " ";
|
|
2571
|
-
return ` ${mark} ${paint.accent(name)} ${paint.muted(truncate(s.what, inner - column - 4))}`;
|
|
2572
|
-
});
|
|
2573
|
-
if (all.length > shown.length) {
|
|
2574
|
-
out.push(` ${paint.dim(`... ${all.length - shown.length} more`)}`);
|
|
2575
|
-
}
|
|
2576
|
-
return out;
|
|
2577
|
-
}
|
|
2578
|
-
function borderColour(state) {
|
|
2579
|
-
switch (state) {
|
|
2580
|
-
case "ok":
|
|
2581
|
-
return paint.green;
|
|
2582
|
-
case "unknown":
|
|
2583
|
-
return paint.red;
|
|
2584
|
-
case "partial":
|
|
2585
|
-
return paint.cyan;
|
|
2586
|
-
case "ask":
|
|
2587
|
-
return paint.yellow;
|
|
2588
|
-
case "busy":
|
|
2589
|
-
return paint.accent;
|
|
2590
|
-
default:
|
|
2591
|
-
return paint.dim;
|
|
2592
|
-
}
|
|
2593
|
-
}
|
|
2594
|
-
function pad(text, width) {
|
|
2595
|
-
const short = width - visibleWidth(text);
|
|
2596
|
-
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
// packages/tui/src/edit.ts
|
|
2600
|
-
function sanitize(text) {
|
|
2601
|
-
return text.replace(/\r\n?|\n|\t/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
2602
|
-
}
|
|
2603
|
-
var PRINTABLE = /^[^\u0000-\u001f\u007f]+$/;
|
|
2604
|
-
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2605
|
-
function boundaries(text) {
|
|
2606
|
-
const out = [0];
|
|
2607
|
-
for (const { index, segment } of segmenter.segment(text)) out.push(index + segment.length);
|
|
2608
|
-
return out;
|
|
2609
|
-
}
|
|
2610
|
-
function prevBoundary(text, at2) {
|
|
2611
|
-
const marks = boundaries(text);
|
|
2612
|
-
let last = 0;
|
|
2613
|
-
for (const mark of marks) {
|
|
2614
|
-
if (mark >= at2) return last;
|
|
2615
|
-
last = mark;
|
|
2616
|
-
}
|
|
2617
|
-
return last;
|
|
2618
|
-
}
|
|
2619
|
-
function nextBoundary(text, at2) {
|
|
2620
|
-
for (const mark of boundaries(text)) if (mark > at2) return mark;
|
|
2621
|
-
return text.length;
|
|
2622
|
-
}
|
|
2623
|
-
function wordStart(text, at2) {
|
|
2624
|
-
let i = at2;
|
|
2625
|
-
while (i > 0 && /\s/.test(text[i - 1])) i--;
|
|
2626
|
-
while (i > 0 && !/\s/.test(text[i - 1])) i--;
|
|
2627
|
-
return i;
|
|
2628
|
-
}
|
|
2629
|
-
function wordEnd(text, at2) {
|
|
2630
|
-
let i = at2;
|
|
2631
|
-
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2632
|
-
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
2633
|
-
return i;
|
|
2634
|
-
}
|
|
2635
|
-
var LineEditor = class {
|
|
2636
|
-
value = "";
|
|
2637
|
-
at = 0;
|
|
2638
|
-
get text() {
|
|
2639
|
-
return this.value;
|
|
2640
|
-
}
|
|
2641
|
-
/** Cursor position, as an offset into `text`, always on a grapheme boundary. */
|
|
2642
|
-
get cursor() {
|
|
2643
|
-
return this.at;
|
|
2657
|
+
/** Typed by kind rather than by the literal passed, so `done: false` stays a boolean the caller can flip. */
|
|
2658
|
+
add(block) {
|
|
2659
|
+
const entry = { id: this.nextId++, version: 0, block };
|
|
2660
|
+
this.entries.push(entry);
|
|
2661
|
+
return entry;
|
|
2644
2662
|
}
|
|
2645
|
-
/**
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2663
|
+
/**
|
|
2664
|
+
* Move a block to the bottom. For an approval card, which must stay the last
|
|
2665
|
+
* thing on screen while it waits: a helper that keeps working adds rows below
|
|
2666
|
+
* it, and a card pushed up past the fold is one approved without being read.
|
|
2667
|
+
*/
|
|
2668
|
+
toEnd(entry) {
|
|
2669
|
+
const at2 = this.entries.indexOf(entry);
|
|
2670
|
+
if (at2 === -1 || at2 === this.entries.length - 1) return;
|
|
2671
|
+
this.entries.splice(at2, 1);
|
|
2672
|
+
this.entries.push(entry);
|
|
2649
2673
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2674
|
+
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
2675
|
+
touch(entry) {
|
|
2676
|
+
entry.version++;
|
|
2653
2677
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
this.
|
|
2659
|
-
this.at += clean.length;
|
|
2660
|
-
return "text";
|
|
2678
|
+
/** Forget everything shown. The session file is untouched; this is the view. */
|
|
2679
|
+
clear() {
|
|
2680
|
+
this.entries = [];
|
|
2681
|
+
this.cache.clear();
|
|
2682
|
+
this.prose.clear();
|
|
2661
2683
|
}
|
|
2662
|
-
/**
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
if (name === "left" && !ctrl && !meta) return this.moveTo(prevBoundary(this.value, this.at));
|
|
2672
|
-
if (name === "right" && !ctrl && !meta) return this.moveTo(nextBoundary(this.value, this.at));
|
|
2673
|
-
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
2674
|
-
if (name === "end" || ctrl && name === "e") return this.moveTo(this.value.length);
|
|
2675
|
-
if ((meta || ctrl) && (name === "left" || name === "b")) {
|
|
2676
|
-
return this.moveTo(wordStart(this.value, this.at));
|
|
2677
|
-
}
|
|
2678
|
-
if ((meta || ctrl) && (name === "right" || name === "f")) {
|
|
2679
|
-
return this.moveTo(wordEnd(this.value, this.at));
|
|
2680
|
-
}
|
|
2681
|
-
if (name === "backspace" && (meta || ctrl)) return this.cut(wordStart(this.value, this.at), this.at);
|
|
2682
|
-
if (ctrl && name === "w") return this.cut(wordStart(this.value, this.at), this.at);
|
|
2683
|
-
if (name === "backspace") return this.cut(prevBoundary(this.value, this.at), this.at);
|
|
2684
|
-
if (name === "delete") return this.cut(this.at, nextBoundary(this.value, this.at));
|
|
2685
|
-
if (ctrl && name === "u") return this.cut(0, this.at);
|
|
2686
|
-
if (ctrl && name === "k") return this.cut(this.at, this.value.length);
|
|
2687
|
-
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) {
|
|
2688
|
-
return this.insert(key.sequence);
|
|
2684
|
+
/** Every line, top to bottom, with one blank line between blocks. */
|
|
2685
|
+
render(width) {
|
|
2686
|
+
const out = [];
|
|
2687
|
+
const live = /* @__PURE__ */ new Set();
|
|
2688
|
+
for (const entry of this.entries) {
|
|
2689
|
+
live.add(entry.id);
|
|
2690
|
+
const lines = this.lines(entry, width);
|
|
2691
|
+
if (out.length && lines.length) out.push("");
|
|
2692
|
+
out.push(...lines);
|
|
2689
2693
|
}
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
moveTo(next) {
|
|
2694
|
-
if (next === this.at) return "same";
|
|
2695
|
-
this.at = next;
|
|
2696
|
-
return "cursor";
|
|
2694
|
+
for (const id of this.cache.keys()) if (!live.has(id)) this.cache.delete(id);
|
|
2695
|
+
for (const id of this.prose.keys()) if (!live.has(id)) this.prose.delete(id);
|
|
2696
|
+
return out;
|
|
2697
2697
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
this.
|
|
2702
|
-
|
|
2698
|
+
lines(entry, width) {
|
|
2699
|
+
const hit = this.cache.get(entry.id);
|
|
2700
|
+
if (hit && hit.version === entry.version && hit.width === width) return hit.lines;
|
|
2701
|
+
const lines = entry.block.kind === "assistant" ? this.assistant(entry, width) : renderBlock(entry.block, width);
|
|
2702
|
+
this.cache.set(entry.id, { version: entry.version, width, lines });
|
|
2703
|
+
return lines;
|
|
2703
2704
|
}
|
|
2704
2705
|
/**
|
|
2705
|
-
*
|
|
2706
|
+
* An answer, streaming or finished.
|
|
2706
2707
|
*
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2709
|
-
*
|
|
2708
|
+
* Complete markdown blocks are rendered; the unfinished tail - a line still
|
|
2709
|
+
* arriving, a fence not yet closed - is shown raw and dimmed, so the reader
|
|
2710
|
+
* sees the words land without a heading flickering between plain and bold.
|
|
2710
2711
|
*/
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2712
|
+
assistant(entry, width) {
|
|
2713
|
+
const inner = proseWidth(width);
|
|
2714
|
+
const { text, done } = entry.block;
|
|
2715
|
+
if (done) return text.trim() ? renderMarkdown(text.replace(/\s+$/, ""), { width: inner, indent: " " }).split("\n") : [];
|
|
2716
|
+
const { complete: complete2, tail } = splitStreaming(text);
|
|
2717
|
+
let head = this.prose.get(entry.id);
|
|
2718
|
+
if (!head || head.length !== complete2.length || head.width !== width) {
|
|
2719
|
+
head = {
|
|
2720
|
+
length: complete2.length,
|
|
2721
|
+
width,
|
|
2722
|
+
lines: complete2 ? renderMarkdown(complete2, { width: inner, indent: " " }).split("\n") : []
|
|
2723
|
+
};
|
|
2724
|
+
this.prose.set(entry.id, head);
|
|
2725
|
+
}
|
|
2726
|
+
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
2727
|
+
return [...head.lines, ...raw];
|
|
2717
2728
|
}
|
|
2718
2729
|
};
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2730
|
+
function splitStreaming(text) {
|
|
2731
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
2732
|
+
let tail = lines.pop() ?? "";
|
|
2733
|
+
let open = -1;
|
|
2734
|
+
lines.forEach((line, i) => {
|
|
2735
|
+
if (/^\s*```/.test(line)) open = open === -1 ? i : -1;
|
|
2736
|
+
});
|
|
2737
|
+
let complete2 = lines;
|
|
2738
|
+
if (open !== -1) {
|
|
2739
|
+
tail = [...lines.slice(open), tail].join("\n");
|
|
2740
|
+
complete2 = lines.slice(0, open);
|
|
2722
2741
|
}
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2742
|
+
return { complete: complete2.join("\n"), tail };
|
|
2743
|
+
}
|
|
2744
|
+
function proseWidth(width) {
|
|
2745
|
+
return Math.max(20, width - 2);
|
|
2746
|
+
}
|
|
2747
|
+
function renderBlock(block, width) {
|
|
2748
|
+
const inner = proseWidth(width);
|
|
2749
|
+
switch (block.kind) {
|
|
2750
|
+
case "user":
|
|
2751
|
+
return block.text.split("\n").flatMap(
|
|
2752
|
+
(line, i) => wrapPlain(line, inner - 2).map((piece, j) => {
|
|
2753
|
+
const text = i === 0 && j === 0 ? `${paint.accent(">")} ${piece}` : ` ${piece}`;
|
|
2754
|
+
return band(text, visibleWidth(text), width);
|
|
2755
|
+
})
|
|
2756
|
+
);
|
|
2757
|
+
case "assistant":
|
|
2758
|
+
return [];
|
|
2759
|
+
case "tool": {
|
|
2760
|
+
const out = toolBlock(block.name, block.detail, block.status, { input: block.input, output: block.output }, inner);
|
|
2761
|
+
if (block.live.length && !block.output) {
|
|
2762
|
+
const shown = block.live.slice(-LIVE_LINES);
|
|
2763
|
+
if (block.live.length > LIVE_LINES) out.push(` ${paint.dim(`\u2026 ${block.live.length - LIVE_LINES} more lines`)}`);
|
|
2764
|
+
for (const line of shown) out.push(` ${paint.dim(line)}`);
|
|
2765
|
+
}
|
|
2766
|
+
return out;
|
|
2767
|
+
}
|
|
2768
|
+
case "approval": {
|
|
2769
|
+
const out = [
|
|
2770
|
+
...block.helper ? [` ${paint.accent("from a helper:")} ${paint.muted(block.helper)}`] : [],
|
|
2771
|
+
...approvalCard(block.preview, block.call, block.mode, inner)
|
|
2772
|
+
];
|
|
2773
|
+
out.push(
|
|
2774
|
+
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")}`
|
|
2775
|
+
);
|
|
2776
|
+
return out;
|
|
2777
|
+
}
|
|
2778
|
+
case "error":
|
|
2779
|
+
return block.text.split("\n").map((line, i) => i === 0 ? `${paint.red(" labd:")} ${line}` : ` ${line}`);
|
|
2780
|
+
case "note":
|
|
2781
|
+
return block.text.replace(/\n$/, "").split("\n");
|
|
2782
|
+
case "compaction": {
|
|
2783
|
+
const label2 = " summarised the conversation so far ";
|
|
2784
|
+
const room = Math.max(0, inner - label2.length);
|
|
2785
|
+
return [paint.dim(` ${"\u2500".repeat(Math.floor(room / 2))}${label2}${"\u2500".repeat(Math.ceil(room / 2))}`)];
|
|
2731
2786
|
}
|
|
2732
|
-
this.index = -1;
|
|
2733
|
-
this.draft = "";
|
|
2734
2787
|
}
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2788
|
+
}
|
|
2789
|
+
function wrapPlain(text, width) {
|
|
2790
|
+
const limit = Math.max(10, width);
|
|
2791
|
+
if (visibleWidth(text) <= limit) return [text];
|
|
2792
|
+
const out = [];
|
|
2793
|
+
let line = "";
|
|
2794
|
+
for (const word of text.split(" ")) {
|
|
2795
|
+
if (line && line.length + 1 + word.length > limit) {
|
|
2796
|
+
out.push(line);
|
|
2797
|
+
line = word;
|
|
2743
2798
|
} else {
|
|
2744
|
-
|
|
2799
|
+
line = line ? `${line} ${word}` : word;
|
|
2745
2800
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
if (this.index === -1) return null;
|
|
2750
|
-
if (this.index >= this.entries.length - 1) {
|
|
2751
|
-
this.index = -1;
|
|
2752
|
-
return this.draft;
|
|
2801
|
+
while (line.length > limit) {
|
|
2802
|
+
out.push(line.slice(0, limit));
|
|
2803
|
+
line = line.slice(limit);
|
|
2753
2804
|
}
|
|
2754
|
-
this.index++;
|
|
2755
|
-
return this.entries[this.index];
|
|
2756
2805
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
// packages/tui/src/editor.ts
|
|
2760
|
-
function sanitizeMultiline(text) {
|
|
2761
|
-
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
2806
|
+
if (line || !out.length) out.push(line);
|
|
2807
|
+
return out;
|
|
2762
2808
|
}
|
|
2763
|
-
var
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
/** First line shown in the last `view`, kept so the window scrolls rather than jumps. */
|
|
2809
|
+
var Viewport = class {
|
|
2810
|
+
constructor(height) {
|
|
2811
|
+
this.height = height;
|
|
2812
|
+
}
|
|
2768
2813
|
top = 0;
|
|
2769
|
-
|
|
2770
|
-
|
|
2814
|
+
follow = true;
|
|
2815
|
+
total = 0;
|
|
2816
|
+
/** The line count when the reader stopped following, so `newLines` can be counted from it. */
|
|
2817
|
+
anchor = 0;
|
|
2818
|
+
/** Lines that arrived since the reader scrolled away. Zero while following. */
|
|
2819
|
+
get newLines() {
|
|
2820
|
+
return this.follow ? 0 : Math.max(0, this.total - this.anchor);
|
|
2771
2821
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2822
|
+
/** Tell the viewport how many lines there are now, and how tall it is. */
|
|
2823
|
+
update(total, height = this.height) {
|
|
2824
|
+
this.height = height;
|
|
2825
|
+
this.total = total;
|
|
2826
|
+
if (this.follow) this.top = Math.max(0, total - height);
|
|
2827
|
+
else this.top = Math.max(0, Math.min(this.top, total - height));
|
|
2774
2828
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2829
|
+
scroll(delta) {
|
|
2830
|
+
const max = Math.max(0, this.total - this.height);
|
|
2831
|
+
const next = Math.max(0, Math.min(max, this.top + delta));
|
|
2832
|
+
if (next === this.top && this.follow) return;
|
|
2833
|
+
this.top = next;
|
|
2834
|
+
if (next >= max) this.refollow();
|
|
2835
|
+
else this.unfollow();
|
|
2777
2836
|
}
|
|
2778
|
-
|
|
2779
|
-
|
|
2837
|
+
pageUp() {
|
|
2838
|
+
this.scroll(-Math.max(1, this.height - 1));
|
|
2780
2839
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
return this.lines[this.row];
|
|
2840
|
+
pageDown() {
|
|
2841
|
+
this.scroll(Math.max(1, this.height - 1));
|
|
2784
2842
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
this.
|
|
2788
|
-
this.row = this.lines.length - 1;
|
|
2789
|
-
this.col = this.lines[this.row].length;
|
|
2843
|
+
home() {
|
|
2844
|
+
this.top = 0;
|
|
2845
|
+
if (this.total > this.height) this.unfollow();
|
|
2790
2846
|
}
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
this.
|
|
2794
|
-
this.col = this.lines[this.row].length;
|
|
2847
|
+
end() {
|
|
2848
|
+
this.refollow();
|
|
2849
|
+
this.top = Math.max(0, this.total - this.height);
|
|
2795
2850
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
this.
|
|
2799
|
-
this.col = 0;
|
|
2800
|
-
this.top = 0;
|
|
2851
|
+
/** The slice of `lines` on screen. */
|
|
2852
|
+
slice(lines) {
|
|
2853
|
+
return lines.slice(this.top, this.top + this.height);
|
|
2801
2854
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
if (!clean) return "same";
|
|
2806
|
-
const parts = clean.split("\n");
|
|
2807
|
-
const line = this.lines[this.row];
|
|
2808
|
-
const before = line.slice(0, this.col);
|
|
2809
|
-
const after = line.slice(this.col);
|
|
2810
|
-
if (parts.length === 1) {
|
|
2811
|
-
this.lines[this.row] = before + parts[0] + after;
|
|
2812
|
-
this.col += parts[0].length;
|
|
2813
|
-
return "text";
|
|
2814
|
-
}
|
|
2815
|
-
const inserted = [before + parts[0], ...parts.slice(1, -1), parts[parts.length - 1] + after];
|
|
2816
|
-
this.lines.splice(this.row, 1, ...inserted);
|
|
2817
|
-
this.row += parts.length - 1;
|
|
2818
|
-
this.col = parts[parts.length - 1].length;
|
|
2819
|
-
return "text";
|
|
2855
|
+
unfollow() {
|
|
2856
|
+
if (this.follow) this.anchor = this.total;
|
|
2857
|
+
this.follow = false;
|
|
2820
2858
|
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2859
|
+
refollow() {
|
|
2860
|
+
this.follow = true;
|
|
2823
2861
|
}
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2862
|
+
};
|
|
2863
|
+
|
|
2864
|
+
// packages/tui/src/credits.ts
|
|
2865
|
+
var OUT_OF_CREDITS_PLACEHOLDER = "Out of labd for this month. Press o to open the top-up page, or /topup";
|
|
2866
|
+
var CREDITS_REFRESH_MS = 1e4;
|
|
2867
|
+
function outOfCreditsAt(credits, now) {
|
|
2868
|
+
if (!isOutOfCredits(credits)) return false;
|
|
2869
|
+
return !(now >= Date.parse(credits?.resetsAt ?? ""));
|
|
2870
|
+
}
|
|
2871
|
+
function boxLocked(credits, now, refused) {
|
|
2872
|
+
return refused || outOfCreditsAt(credits, now);
|
|
2873
|
+
}
|
|
2874
|
+
function spentBeforeTurn(credits, now) {
|
|
2875
|
+
return isOutOfCredits(credits) && !outOfCreditsAt(credits, now);
|
|
2876
|
+
}
|
|
2877
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
2878
|
+
var AFTER_TURN_MS = 1e3;
|
|
2879
|
+
function untilReset(credits, now) {
|
|
2880
|
+
if (!outOfCreditsAt(credits, now)) return null;
|
|
2881
|
+
const at2 = Date.parse(credits?.resetsAt ?? "");
|
|
2882
|
+
if (Number.isNaN(at2)) return null;
|
|
2883
|
+
return Math.min(at2 - now + AFTER_TURN_MS, MAX_TIMER_MS);
|
|
2884
|
+
}
|
|
2885
|
+
function shouldRefreshCredits(lastAskedAt, now) {
|
|
2886
|
+
return lastAskedAt === null || now - lastAskedAt >= CREDITS_REFRESH_MS;
|
|
2887
|
+
}
|
|
2888
|
+
function keyWhileOut(key, at2) {
|
|
2889
|
+
if (!at2.out || !at2.boxEmpty || at2.popup || at2.approval || !typedText(key)) return "pass";
|
|
2890
|
+
if (key.sequence === "o" || key.sequence === "O") return "open";
|
|
2891
|
+
return key.sequence?.startsWith("/") ? "pass" : "drop";
|
|
2892
|
+
}
|
|
2893
|
+
function typedText(key) {
|
|
2894
|
+
return !key.name && !key.ctrl && !key.meta && Boolean(key.sequence);
|
|
2895
|
+
}
|
|
2896
|
+
function creditsReading(credits, usual, now = Date.now()) {
|
|
2897
|
+
if (outOfCreditsAt(credits, now)) return paint.red("out of labd \xB7 /topup");
|
|
2898
|
+
if (isLowOnCredits(credits)) return paint.yellow(`${credits.percentLeft}% left \xB7 /topup`);
|
|
2899
|
+
return paint.muted(usual);
|
|
2900
|
+
}
|
|
2901
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
2902
|
+
function resetDay(iso) {
|
|
2903
|
+
const at2 = new Date(iso ?? "");
|
|
2904
|
+
if (Number.isNaN(at2.getTime())) return null;
|
|
2905
|
+
return `${at2.getUTCDate()} ${MONTHS[at2.getUTCMonth()]}`;
|
|
2906
|
+
}
|
|
2907
|
+
function allowanceFacts(credits) {
|
|
2908
|
+
const saved = credits.resetsSaved ?? 0;
|
|
2909
|
+
const day = resetDay(credits.resetsAt);
|
|
2910
|
+
return [
|
|
2911
|
+
...saved > 0 ? [`${saved} saved ${saved === 1 ? "reset" : "resets"}`] : [],
|
|
2912
|
+
...day ? [`resets on ${day}`] : []
|
|
2913
|
+
].join(" \xB7 ");
|
|
2914
|
+
}
|
|
2915
|
+
function noCreditsLines(opts) {
|
|
2916
|
+
const inner = Math.max(20, opts.width - 10);
|
|
2917
|
+
const [first = "", ...rest] = wrapPlain(opts.message, inner);
|
|
2918
|
+
const facts = opts.credits ? allowanceFacts(opts.credits) : "";
|
|
2919
|
+
return [
|
|
2920
|
+
`${paint.red(" labd:")} ${first}`,
|
|
2921
|
+
...rest.map((line) => ` ${line}`),
|
|
2922
|
+
...facts ? [` ${paint.muted(facts)}`] : [],
|
|
2923
|
+
...opts.url ? [` ${paint.accent(opts.url)}`, ` ${paint.muted(opts.howToOpen)}`] : []
|
|
2924
|
+
];
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// packages/tui/src/cards.ts
|
|
2928
|
+
var OUTPUT_LINES = 8;
|
|
2929
|
+
function toolBlock(name, detail2, status, io, width = 100) {
|
|
2930
|
+
const label2 = LABELS[name] ?? name.replace(/_/g, " ");
|
|
2931
|
+
const dot = status === "error" ? paint.red("\u25CF") : status === "skipped" ? paint.muted("\u25CB") : status === "awaiting" ? paint.yellow("\u25CF") : status === "running" ? paint.dim("\u25CB") : paint.green("\u25CF");
|
|
2932
|
+
const out = [` ${dot} ${paint.bold(paint.accent(label2))} ${paint.muted(detail2)}`];
|
|
2933
|
+
const room = Math.max(20, width - 9);
|
|
2934
|
+
const row = (gutter, text, paintText, soft) => {
|
|
2935
|
+
const wrap3 = soft ? wrapWords : wrapCommand;
|
|
2936
|
+
const lines = text.split("\n").flatMap((line) => wrap3(line, room));
|
|
2937
|
+
lines.forEach((line, i) => {
|
|
2938
|
+
out.push(` ${i === 0 ? paint.dim(gutter.padEnd(4)) : " "} ${paintText(line)}`);
|
|
2939
|
+
});
|
|
2940
|
+
};
|
|
2941
|
+
const shell = name === "run_command";
|
|
2942
|
+
if (io.input) row(shell ? "$" : "IN", io.input, (t) => paint.cyan(t), false);
|
|
2943
|
+
if (io.output) {
|
|
2944
|
+
const lines = io.output.replace(/\s+$/, "").split("\n");
|
|
2945
|
+
const shown = lines.slice(0, OUTPUT_LINES).join("\n");
|
|
2946
|
+
row(shell ? "" : "OUT", shown, (t) => status === "error" ? paint.red(t) : paint.muted(t), true);
|
|
2947
|
+
if (lines.length > OUTPUT_LINES) {
|
|
2948
|
+
out.push(` ${"".padEnd(4)} ${paint.dim(`\u2026 ${lines.length - OUTPUT_LINES} more lines`)}`);
|
|
2863
2949
|
}
|
|
2864
|
-
if (name === "right" && !ctrl && !meta) {
|
|
2865
|
-
if (this.col >= line.length) {
|
|
2866
|
-
if (this.row === this.lines.length - 1) return "same";
|
|
2867
|
-
this.row++;
|
|
2868
|
-
this.col = 0;
|
|
2869
|
-
return "cursor";
|
|
2870
|
-
}
|
|
2871
|
-
return this.moveTo(nextBoundary(line, this.col));
|
|
2872
|
-
}
|
|
2873
|
-
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
2874
|
-
if (name === "end" || ctrl && name === "e") return this.moveTo(line.length);
|
|
2875
|
-
if ((meta || ctrl) && (name === "left" || name === "b")) return this.moveTo(wordStart(line, this.col));
|
|
2876
|
-
if ((meta || ctrl) && (name === "right" || name === "f")) return this.moveTo(wordEnd(line, this.col));
|
|
2877
|
-
if (name === "backspace" && (meta || ctrl)) {
|
|
2878
|
-
if (this.col === 0) return this.joinUp();
|
|
2879
|
-
return this.cut(wordStart(line, this.col), this.col);
|
|
2880
|
-
}
|
|
2881
|
-
if (ctrl && name === "w") {
|
|
2882
|
-
if (this.col === 0) return this.joinUp();
|
|
2883
|
-
return this.cut(wordStart(line, this.col), this.col);
|
|
2884
|
-
}
|
|
2885
|
-
if (name === "backspace") {
|
|
2886
|
-
if (this.col === 0) return this.joinUp();
|
|
2887
|
-
return this.cut(prevBoundary(line, this.col), this.col);
|
|
2888
|
-
}
|
|
2889
|
-
if (name === "delete") {
|
|
2890
|
-
if (this.col >= line.length) return this.joinDown();
|
|
2891
|
-
return this.cut(this.col, nextBoundary(line, this.col));
|
|
2892
|
-
}
|
|
2893
|
-
if (ctrl && name === "u") return this.cut(0, this.col);
|
|
2894
|
-
if (ctrl && name === "k") {
|
|
2895
|
-
if (this.col >= line.length) return this.joinDown();
|
|
2896
|
-
return this.cut(this.col, line.length);
|
|
2897
|
-
}
|
|
2898
|
-
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) return this.insert(key.sequence);
|
|
2899
|
-
return "unhandled";
|
|
2900
|
-
}
|
|
2901
|
-
/**
|
|
2902
|
-
* The lines to draw and where the caret is among them.
|
|
2903
|
-
*
|
|
2904
|
-
* At most `maxLines` rows, chosen so the caret's row is always one of them;
|
|
2905
|
-
* each row scrolls horizontally on its own so a long line never wraps into the
|
|
2906
|
-
* row below and the caret never leaves the box.
|
|
2907
|
-
*/
|
|
2908
|
-
view(width, maxLines) {
|
|
2909
|
-
const rows = Math.max(1, maxLines);
|
|
2910
|
-
if (this.row < this.top) this.top = this.row;
|
|
2911
|
-
if (this.row >= this.top + rows) this.top = this.row - rows + 1;
|
|
2912
|
-
this.top = Math.max(0, Math.min(this.top, this.lines.length - rows));
|
|
2913
|
-
const shown = this.lines.slice(this.top, this.top + rows);
|
|
2914
|
-
let cursorCol = this.col;
|
|
2915
|
-
const lines = shown.map((line, i) => {
|
|
2916
|
-
if (width <= 0) return "";
|
|
2917
|
-
if (line.length <= width) return line;
|
|
2918
|
-
if (this.top + i !== this.row) return line.slice(0, width);
|
|
2919
|
-
const start = Math.max(0, this.col - width + 1);
|
|
2920
|
-
cursorCol = this.col - start;
|
|
2921
|
-
return line.slice(start, start + width);
|
|
2922
|
-
});
|
|
2923
|
-
return { lines, cursor: { row: this.row - this.top, col: cursorCol } };
|
|
2924
|
-
}
|
|
2925
|
-
moveTo(next) {
|
|
2926
|
-
if (next === this.col) return "same";
|
|
2927
|
-
this.col = next;
|
|
2928
|
-
return "cursor";
|
|
2929
|
-
}
|
|
2930
|
-
cut(from, to) {
|
|
2931
|
-
if (from >= to) return "same";
|
|
2932
|
-
const line = this.lines[this.row];
|
|
2933
|
-
this.lines[this.row] = line.slice(0, from) + line.slice(to);
|
|
2934
|
-
this.col = from;
|
|
2935
|
-
return "text";
|
|
2936
2950
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2951
|
+
return out;
|
|
2952
|
+
}
|
|
2953
|
+
var LABELS = {
|
|
2954
|
+
read_file: "Read",
|
|
2955
|
+
list_dir: "List",
|
|
2956
|
+
search_workspace: "Search",
|
|
2957
|
+
edit_file: "Edit",
|
|
2958
|
+
create_file: "Create",
|
|
2959
|
+
move_file: "Move",
|
|
2960
|
+
delete_file: "Delete",
|
|
2961
|
+
run_command: "Run",
|
|
2962
|
+
spawn_agent: "Helper"
|
|
2963
|
+
};
|
|
2964
|
+
function approvalCard(preview, call, mode, width) {
|
|
2965
|
+
const out = [""];
|
|
2966
|
+
if (call.name === "run_command" || preview?.kind === "run") {
|
|
2967
|
+
const command = preview?.kind === "run" ? preview.command : String(call.input.command ?? "");
|
|
2968
|
+
const why = preview?.kind === "run" ? preview.why : call.input.why;
|
|
2969
|
+
out.push(`${paint.yellow("?")} ${paint.bold("Run a command")}`);
|
|
2970
|
+
if (why) out.push(` ${paint.muted(why)}`);
|
|
2971
|
+
out.push("");
|
|
2972
|
+
for (const line of wrapCommand(command, width - 6)) out.push(` ${paint.cyan(line)}`);
|
|
2973
|
+
out.push("");
|
|
2974
|
+
out.push(
|
|
2975
|
+
` ${paint.dim(
|
|
2976
|
+
mode === "auto" ? "On auto, commands still ask - a command can do anything your shell can, and put back isn't one of them." : "Runs in this project. Unlike an edit, this can't be put back."
|
|
2977
|
+
)}`
|
|
2978
|
+
);
|
|
2979
|
+
return out;
|
|
2944
2980
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2981
|
+
const creating = call.name === "create_file";
|
|
2982
|
+
const moving = call.name === "move_file";
|
|
2983
|
+
const deleting = call.name === "delete_file";
|
|
2984
|
+
const path = preview ? preview.path : String(call.input.path ?? call.input.from ?? "a file");
|
|
2985
|
+
const destination = preview?.to ?? call.input.to;
|
|
2986
|
+
const title = moving ? "Move" : deleting ? "Delete" : creating ? "Create" : "Edit";
|
|
2987
|
+
out.push(
|
|
2988
|
+
`${paint.yellow("?")} ${paint.bold(title)} ${paint.accent(path)}${moving && destination ? paint.muted(" \u2192 ") + paint.accent(destination) : ""}${preview ? paint.muted(
|
|
2989
|
+
` ${counts(preview.added, preview.removed)}${creating || moving || deleting ? "" : ` at line ${preview.startLine}`}`
|
|
2990
|
+
) : ""}`
|
|
2991
|
+
);
|
|
2992
|
+
if (preview) {
|
|
2993
|
+
out.push("");
|
|
2994
|
+
for (const line of diffLines(preview.oldText, preview.newText, width)) out.push(` ${line}`);
|
|
2949
2995
|
}
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
A: "up",
|
|
2958
|
-
B: "down",
|
|
2959
|
-
C: "right",
|
|
2960
|
-
D: "left",
|
|
2961
|
-
H: "home",
|
|
2962
|
-
F: "end",
|
|
2963
|
-
Z: "tab"
|
|
2964
|
-
};
|
|
2965
|
-
var TILDE = {
|
|
2966
|
-
"1": "home",
|
|
2967
|
-
"2": "insert",
|
|
2968
|
-
"3": "delete",
|
|
2969
|
-
"4": "end",
|
|
2970
|
-
"5": "pageup",
|
|
2971
|
-
"6": "pagedown",
|
|
2972
|
-
"7": "home",
|
|
2973
|
-
"8": "end"
|
|
2974
|
-
};
|
|
2975
|
-
function modifiers(param) {
|
|
2976
|
-
const n = (Number(param) || 1) - 1;
|
|
2977
|
-
return {
|
|
2978
|
-
...n & 1 ? { shift: true } : {},
|
|
2979
|
-
...n & 2 ? { meta: true } : {},
|
|
2980
|
-
...n & 4 ? { ctrl: true } : {}
|
|
2981
|
-
};
|
|
2996
|
+
out.push("");
|
|
2997
|
+
out.push(
|
|
2998
|
+
` ${paint.dim(
|
|
2999
|
+
moving || deleting ? "Changes the file on disk. You can put the whole answer back afterwards." : "Written to the file. You can put the whole answer back afterwards."
|
|
3000
|
+
)}`
|
|
3001
|
+
);
|
|
3002
|
+
return out;
|
|
2982
3003
|
}
|
|
2983
|
-
function
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3004
|
+
function counts(added, removed) {
|
|
3005
|
+
const parts = [];
|
|
3006
|
+
if (added) parts.push(`+${added}`);
|
|
3007
|
+
if (removed) parts.push(`-${removed}`);
|
|
3008
|
+
return parts.join(" ");
|
|
3009
|
+
}
|
|
3010
|
+
function diffLines(oldText, newText, width, max = 12) {
|
|
3011
|
+
const out = [];
|
|
3012
|
+
const before = oldText ? oldText.split("\n") : [];
|
|
3013
|
+
const after = newText ? newText.split("\n") : [];
|
|
3014
|
+
for (const line of before.slice(0, max)) out.push(paint.red(`- ${clip(line, width - 4)}`));
|
|
3015
|
+
if (before.length > max) out.push(paint.dim(` ... ${before.length - max} more removed`));
|
|
3016
|
+
for (const line of after.slice(0, max)) out.push(paint.green(`+ ${clip(line, width - 4)}`));
|
|
3017
|
+
if (after.length > max) out.push(paint.dim(` ... ${after.length - max} more added`));
|
|
3018
|
+
return out;
|
|
3019
|
+
}
|
|
3020
|
+
function clip(line, width) {
|
|
3021
|
+
return visibleWidth(line) <= width ? line : `${line.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
3022
|
+
}
|
|
3023
|
+
function wrapWords(text, width) {
|
|
3024
|
+
if (text.length <= width) return [text];
|
|
3025
|
+
const out = [];
|
|
3026
|
+
let line = "";
|
|
3027
|
+
for (const word of text.split(" ")) {
|
|
3028
|
+
if (!line) {
|
|
3029
|
+
line = word;
|
|
3030
|
+
} else if (`${line} ${word}`.length <= width) {
|
|
3031
|
+
line = `${line} ${word}`;
|
|
3032
|
+
} else {
|
|
3033
|
+
out.push(line);
|
|
3034
|
+
line = word;
|
|
3035
|
+
}
|
|
3036
|
+
while (line.length > width) {
|
|
3037
|
+
out.push(line.slice(0, width));
|
|
3038
|
+
line = line.slice(width);
|
|
3039
|
+
}
|
|
3000
3040
|
}
|
|
3041
|
+
if (line) out.push(line);
|
|
3042
|
+
return out;
|
|
3001
3043
|
}
|
|
3002
|
-
function
|
|
3003
|
-
|
|
3044
|
+
function wrapCommand(command, width) {
|
|
3045
|
+
if (command.length <= width) return [command];
|
|
3046
|
+
const out = [];
|
|
3047
|
+
for (let i = 0; i < command.length; i += width) out.push(command.slice(i, i + width));
|
|
3048
|
+
return out;
|
|
3004
3049
|
}
|
|
3005
|
-
function
|
|
3006
|
-
|
|
3007
|
-
|
|
3050
|
+
function statusLine(info2) {
|
|
3051
|
+
const bits = [];
|
|
3052
|
+
if (info2.contextPercent !== null && info2.contextPercent !== void 0) {
|
|
3053
|
+
const crowded = info2.contextPercent >= 75;
|
|
3054
|
+
bits.push((crowded ? paint.yellow : paint.muted)(`${info2.contextPercent}% full`));
|
|
3008
3055
|
}
|
|
3009
|
-
|
|
3056
|
+
bits.push(paint.muted(info2.project));
|
|
3057
|
+
if (info2.branch) bits.push(paint.muted(info2.branch));
|
|
3058
|
+
if (info2.credits) bits.push(creditsReading(info2.credits, `${info2.credits.percentLeft}% left`));
|
|
3059
|
+
if (info2.effort) bits.push(paint.muted(`think ${info2.effort}`));
|
|
3060
|
+
const mode = info2.mode === "agent" ? paint.red(paint.bold("agent")) : info2.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
3061
|
+
bits.push(mode);
|
|
3062
|
+
return `${paint.dim(FACE)} ${paint.dim("|")} ${bits.join(paint.dim(" | "))}`;
|
|
3010
3063
|
}
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
/** Inside a bracketed paste: the text so far. Null outside one. */
|
|
3015
|
-
pasting = null;
|
|
3016
|
-
/**
|
|
3017
|
-
* Decode a chunk. Sequences split across two reads are held until the next
|
|
3018
|
-
* one; a lone ESC at the END of a chunk is reported as Escape at once, because
|
|
3019
|
-
* a terminal sends a whole sequence in one write and a person pressing the key
|
|
3020
|
-
* should not have to press another to be heard.
|
|
3021
|
-
*/
|
|
3022
|
-
feed(chunk) {
|
|
3023
|
-
const out = [];
|
|
3024
|
-
let buf = this.pending + chunk;
|
|
3025
|
-
this.pending = "";
|
|
3026
|
-
let i = 0;
|
|
3027
|
-
while (i < buf.length) {
|
|
3028
|
-
if (this.pasting !== null) {
|
|
3029
|
-
const end = buf.indexOf(PASTE_END, i);
|
|
3030
|
-
if (end === -1) {
|
|
3031
|
-
const hold = partialMarker(buf, PASTE_END);
|
|
3032
|
-
this.pasting += buf.slice(i, buf.length - hold);
|
|
3033
|
-
this.pending = buf.slice(buf.length - hold);
|
|
3034
|
-
return out;
|
|
3035
|
-
}
|
|
3036
|
-
this.pasting += buf.slice(i, end);
|
|
3037
|
-
out.push({ name: "paste", paste: this.pasting.replace(/\r\n?/g, "\n") });
|
|
3038
|
-
this.pasting = null;
|
|
3039
|
-
i = end + PASTE_END.length;
|
|
3040
|
-
continue;
|
|
3041
|
-
}
|
|
3042
|
-
const ch = buf[i];
|
|
3043
|
-
if (ch !== ESC3) {
|
|
3044
|
-
const code2 = ch.charCodeAt(0);
|
|
3045
|
-
if (code2 === 13) out.push({ name: "return", sequence: ch });
|
|
3046
|
-
else if (code2 === 10) out.push({ name: "j", ctrl: true, sequence: ch });
|
|
3047
|
-
else if (code2 === 9) out.push({ name: "tab", sequence: ch });
|
|
3048
|
-
else if (code2 === 127 || code2 === 8) out.push({ name: "backspace", sequence: ch });
|
|
3049
|
-
else if (code2 < 32) {
|
|
3050
|
-
if (code2 > 0) out.push({ name: String.fromCharCode(96 + code2), ctrl: true, sequence: ch });
|
|
3051
|
-
} else {
|
|
3052
|
-
let j = i + 1;
|
|
3053
|
-
while (j < buf.length) {
|
|
3054
|
-
const c = buf.charCodeAt(j);
|
|
3055
|
-
if (c < 32 || c === 127 || buf[j] === ESC3) break;
|
|
3056
|
-
j++;
|
|
3057
|
-
}
|
|
3058
|
-
out.push({ sequence: buf.slice(i, j) });
|
|
3059
|
-
i = j;
|
|
3060
|
-
continue;
|
|
3061
|
-
}
|
|
3062
|
-
i++;
|
|
3063
|
-
continue;
|
|
3064
|
-
}
|
|
3065
|
-
if (i === buf.length - 1) {
|
|
3066
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
3067
|
-
i++;
|
|
3068
|
-
continue;
|
|
3069
|
-
}
|
|
3070
|
-
const next = buf[i + 1];
|
|
3071
|
-
if (next === "[") {
|
|
3072
|
-
let j = i + 2;
|
|
3073
|
-
while (j < buf.length && !(buf.charCodeAt(j) >= 64 && buf.charCodeAt(j) <= 126)) j++;
|
|
3074
|
-
if (j >= buf.length) {
|
|
3075
|
-
this.pending = buf.slice(i);
|
|
3076
|
-
return out;
|
|
3077
|
-
}
|
|
3078
|
-
const seq = buf.slice(i, j + 1);
|
|
3079
|
-
const body2 = buf.slice(i + 2, j);
|
|
3080
|
-
const final = buf[j];
|
|
3081
|
-
i = j + 1;
|
|
3082
|
-
if (seq === PASTE_START) {
|
|
3083
|
-
this.pasting = "";
|
|
3084
|
-
continue;
|
|
3085
|
-
}
|
|
3086
|
-
const key = decodeCsi(body2, final, seq);
|
|
3087
|
-
if (key) out.push(key);
|
|
3088
|
-
continue;
|
|
3089
|
-
}
|
|
3090
|
-
if (next === "O") {
|
|
3091
|
-
const final = buf[i + 2];
|
|
3092
|
-
if (final === void 0) {
|
|
3093
|
-
this.pending = buf.slice(i);
|
|
3094
|
-
return out;
|
|
3095
|
-
}
|
|
3096
|
-
const name = CSI_FINAL[final];
|
|
3097
|
-
if (name) out.push({ name, sequence: buf.slice(i, i + 3) });
|
|
3098
|
-
i += 3;
|
|
3099
|
-
continue;
|
|
3100
|
-
}
|
|
3101
|
-
if (next === "\x7F" || next === "\b") {
|
|
3102
|
-
out.push({ name: "backspace", meta: true, sequence: buf.slice(i, i + 2) });
|
|
3103
|
-
i += 2;
|
|
3104
|
-
continue;
|
|
3105
|
-
}
|
|
3106
|
-
if (next === ESC3) {
|
|
3107
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
3108
|
-
i++;
|
|
3109
|
-
continue;
|
|
3110
|
-
}
|
|
3111
|
-
const code = next.charCodeAt(0);
|
|
3112
|
-
if (code >= 32 && code !== 127) {
|
|
3113
|
-
out.push({
|
|
3114
|
-
name: next.toLowerCase(),
|
|
3115
|
-
meta: true,
|
|
3116
|
-
...next !== next.toLowerCase() ? { shift: true } : {},
|
|
3117
|
-
sequence: buf.slice(i, i + 2)
|
|
3118
|
-
});
|
|
3119
|
-
i += 2;
|
|
3120
|
-
continue;
|
|
3121
|
-
}
|
|
3122
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
3123
|
-
i++;
|
|
3124
|
-
}
|
|
3125
|
-
return out;
|
|
3126
|
-
}
|
|
3127
|
-
/** Anything still held back - the tail of a paste, an escape that never finished. */
|
|
3128
|
-
flush() {
|
|
3129
|
-
const out = [];
|
|
3130
|
-
if (this.pasting !== null) {
|
|
3131
|
-
out.push({ name: "paste", paste: this.pasting + this.pending });
|
|
3132
|
-
this.pasting = null;
|
|
3133
|
-
} else if (this.pending === ESC3) {
|
|
3134
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
3135
|
-
}
|
|
3136
|
-
this.pending = "";
|
|
3137
|
-
return out;
|
|
3138
|
-
}
|
|
3139
|
-
};
|
|
3140
|
-
function decodeCsi(body2, final, sequence) {
|
|
3141
|
-
if (body2.startsWith("<") && (final === "M" || final === "m")) {
|
|
3142
|
-
const [b, x, y] = body2.slice(1).split(";").map(Number);
|
|
3143
|
-
if (final === "m") return null;
|
|
3144
|
-
if (b & 32) return null;
|
|
3145
|
-
const button = b & 3;
|
|
3146
|
-
if (b & 64) {
|
|
3147
|
-
return { name: "mouse", sequence, mouse: { kind: button === 0 ? "wheel-up" : "wheel-down", button: b, x, y } };
|
|
3148
|
-
}
|
|
3149
|
-
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
3150
|
-
}
|
|
3151
|
-
const params = body2.split(";");
|
|
3152
|
-
if (final === "~") {
|
|
3153
|
-
if (params[0] === "27") return coded(Number(params[2]), modifiers(params[1]), sequence);
|
|
3154
|
-
if (params[0] === "200" || params[0] === "201") return null;
|
|
3155
|
-
const name2 = TILDE[params[0]];
|
|
3156
|
-
return name2 ? { name: name2, ...modifiers(params[1]), sequence } : null;
|
|
3157
|
-
}
|
|
3158
|
-
if (final === "u") return coded(Number(params[0]), modifiers(params[1]), sequence);
|
|
3159
|
-
const name = CSI_FINAL[final];
|
|
3160
|
-
if (!name) return null;
|
|
3161
|
-
const mods = final === "Z" ? { shift: true } : modifiers(params[1]);
|
|
3162
|
-
return { name, ...mods, sequence };
|
|
3064
|
+
function spinnerFrame(tick) {
|
|
3065
|
+
const dots = ".".repeat(tick % 3 + 1).padEnd(3);
|
|
3066
|
+
return paint.dim(dots);
|
|
3163
3067
|
}
|
|
3164
3068
|
|
|
3165
|
-
// packages/tui/src/
|
|
3166
|
-
var
|
|
3167
|
-
var
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
const
|
|
3171
|
-
const
|
|
3172
|
-
const
|
|
3173
|
-
const
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
const mainWidth = withSidebar ? columns - SIDEBAR_WIDTH - 1 : columns;
|
|
3180
|
-
const out = {
|
|
3181
|
-
header,
|
|
3182
|
-
transcript: { top: 1, left: 0, width: mainWidth, height: transcriptHeight },
|
|
3183
|
-
composer: { ...composer2, width: mainWidth },
|
|
3184
|
-
footer
|
|
3185
|
-
};
|
|
3186
|
-
if (withSidebar) {
|
|
3187
|
-
out.sidebar = { top: 1, left: mainWidth + 1, width: SIDEBAR_WIDTH, height: composer2.top + composerHeight - 1 };
|
|
3188
|
-
}
|
|
3069
|
+
// packages/tui/src/composer.ts
|
|
3070
|
+
var ROUND = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
|
|
3071
|
+
var MAX_SUGGESTIONS = 5;
|
|
3072
|
+
function composer(opts) {
|
|
3073
|
+
const width = Math.max(20, opts.width);
|
|
3074
|
+
const colour = borderColour(opts.state);
|
|
3075
|
+
const inner = width - 4;
|
|
3076
|
+
const out = [];
|
|
3077
|
+
for (const line of suggestionLines(opts.suggestions ?? [], inner)) out.push(line);
|
|
3078
|
+
out.push(colour(`${ROUND.tl}${ROUND.h.repeat(width - 2)}${ROUND.tr}`));
|
|
3079
|
+
out.push(`${colour(ROUND.v)} ${body(opts, inner)} ${colour(ROUND.v)}`);
|
|
3080
|
+
out.push(colour(`${ROUND.bl}${ROUND.h.repeat(width - 2)}${ROUND.br}`));
|
|
3081
|
+
if (opts.hint) out.push(` ${truncate(opts.hint, width - 2)}`);
|
|
3082
|
+
if (opts.status) out.push(opts.status);
|
|
3189
3083
|
return out;
|
|
3190
3084
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
this.items = items;
|
|
3198
|
-
}
|
|
3199
|
-
selected = 0;
|
|
3200
|
-
get current() {
|
|
3201
|
-
return this.items[this.selected] ?? null;
|
|
3085
|
+
function body(opts, inner) {
|
|
3086
|
+
const sigil = opts.state === "busy" ? paint.dim(">") : paint.accent(">");
|
|
3087
|
+
const room = inner - 2;
|
|
3088
|
+
if (!opts.view.text && opts.placeholder && opts.state !== "busy") {
|
|
3089
|
+
const text2 = truncate(paint.dim(opts.placeholder), room);
|
|
3090
|
+
return `${sigil} ${pad(text2, room)}`;
|
|
3202
3091
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3092
|
+
const text = withCursor(opts.view, room, opts.state === "busy");
|
|
3093
|
+
return `${sigil} ${pad(text, room)}`;
|
|
3094
|
+
}
|
|
3095
|
+
function withCursor(view, room, hidden) {
|
|
3096
|
+
const text = view.text.slice(0, room);
|
|
3097
|
+
if (hidden) return text;
|
|
3098
|
+
const at2 = Math.min(view.column, room - 1 < 0 ? 0 : room);
|
|
3099
|
+
const before = text.slice(0, at2);
|
|
3100
|
+
const under = text.slice(at2, at2 + 1) || " ";
|
|
3101
|
+
const after = text.slice(at2 + 1);
|
|
3102
|
+
return `${before}${paint.invert(under)}${after}`;
|
|
3103
|
+
}
|
|
3104
|
+
function suggestionLines(all, inner) {
|
|
3105
|
+
if (!all.length) return [];
|
|
3106
|
+
const shown = all.slice(0, MAX_SUGGESTIONS);
|
|
3107
|
+
const column = Math.max(...shown.map((s) => s.name.length + 1));
|
|
3108
|
+
const out = shown.map((s, i) => {
|
|
3109
|
+
const name = `/${s.name}`.padEnd(column + 1);
|
|
3110
|
+
const mark = i === 0 ? paint.accent("-") : " ";
|
|
3111
|
+
return ` ${mark} ${paint.accent(name)} ${paint.muted(truncate(s.what, inner - column - 4))}`;
|
|
3112
|
+
});
|
|
3113
|
+
if (all.length > shown.length) {
|
|
3114
|
+
out.push(` ${paint.dim(`... ${all.length - shown.length} more`)}`);
|
|
3206
3115
|
}
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3116
|
+
return out;
|
|
3117
|
+
}
|
|
3118
|
+
function borderColour(state) {
|
|
3119
|
+
switch (state) {
|
|
3120
|
+
case "ok":
|
|
3121
|
+
return paint.green;
|
|
3122
|
+
case "unknown":
|
|
3123
|
+
return paint.red;
|
|
3124
|
+
case "partial":
|
|
3125
|
+
return paint.cyan;
|
|
3126
|
+
case "ask":
|
|
3127
|
+
return paint.yellow;
|
|
3128
|
+
case "busy":
|
|
3129
|
+
return paint.accent;
|
|
3130
|
+
default:
|
|
3131
|
+
return paint.dim;
|
|
3223
3132
|
}
|
|
3224
|
-
}
|
|
3225
|
-
function
|
|
3133
|
+
}
|
|
3134
|
+
function pad(text, width) {
|
|
3226
3135
|
const short = width - visibleWidth(text);
|
|
3227
3136
|
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3228
3137
|
}
|
|
3229
3138
|
|
|
3230
|
-
// packages/tui/src/
|
|
3231
|
-
function
|
|
3232
|
-
return
|
|
3139
|
+
// packages/tui/src/edit.ts
|
|
3140
|
+
function sanitize(text) {
|
|
3141
|
+
return text.replace(/\r\n?|\n|\t/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
3233
3142
|
}
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3240
|
-
return items.filter((item) => {
|
|
3241
|
-
const hay = `${item.label} ${item.hint ?? ""}`.toLowerCase();
|
|
3242
|
-
return words.every((w) => hay.includes(w));
|
|
3243
|
-
});
|
|
3143
|
+
var PRINTABLE = /^[^\u0000-\u001f\u007f]+$/;
|
|
3144
|
+
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3145
|
+
function boundaries(text) {
|
|
3146
|
+
const out = [0];
|
|
3147
|
+
for (const { index, segment } of segmenter.segment(text)) out.push(index + segment.length);
|
|
3148
|
+
return out;
|
|
3244
3149
|
}
|
|
3245
|
-
function
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
else if (key.name === "pagedown") dialog.scroll += 10;
|
|
3252
|
-
else if (key.name === "home") dialog.scroll = 0;
|
|
3253
|
-
else if (key.name === "return" || key.name === "q") return "close";
|
|
3254
|
-
else if (key.mouse?.kind === "wheel-up") dialog.scroll = Math.max(0, dialog.scroll - 3);
|
|
3255
|
-
else if (key.mouse?.kind === "wheel-down") dialog.scroll += 3;
|
|
3256
|
-
else return "same";
|
|
3257
|
-
return "changed";
|
|
3258
|
-
}
|
|
3259
|
-
const shown = filterItems(dialog.items, dialog.query);
|
|
3260
|
-
if (key.name === "up" || key.mouse?.kind === "wheel-up") {
|
|
3261
|
-
dialog.selected = shown.length ? (dialog.selected - 1 + shown.length) % shown.length : 0;
|
|
3262
|
-
return "changed";
|
|
3263
|
-
}
|
|
3264
|
-
if (key.name === "down" || key.mouse?.kind === "wheel-down" || key.name === "tab") {
|
|
3265
|
-
dialog.selected = shown.length ? (dialog.selected + 1) % shown.length : 0;
|
|
3266
|
-
return "changed";
|
|
3267
|
-
}
|
|
3268
|
-
if (key.name === "return") {
|
|
3269
|
-
const item = shown[dialog.selected];
|
|
3270
|
-
if (!item) return "same";
|
|
3271
|
-
dialog.onSelect(item);
|
|
3272
|
-
return "select";
|
|
3273
|
-
}
|
|
3274
|
-
if (key.name === "backspace") {
|
|
3275
|
-
if (!dialog.query) return "same";
|
|
3276
|
-
dialog.query = dialog.query.slice(0, -1);
|
|
3277
|
-
dialog.selected = 0;
|
|
3278
|
-
return "changed";
|
|
3279
|
-
}
|
|
3280
|
-
if (!key.ctrl && !key.meta && key.sequence && /^[^\u0000-\u001f\u007f]+$/.test(key.sequence)) {
|
|
3281
|
-
dialog.query += key.sequence;
|
|
3282
|
-
dialog.selected = 0;
|
|
3283
|
-
return "changed";
|
|
3150
|
+
function prevBoundary(text, at2) {
|
|
3151
|
+
const marks = boundaries(text);
|
|
3152
|
+
let last = 0;
|
|
3153
|
+
for (const mark of marks) {
|
|
3154
|
+
if (mark >= at2) return last;
|
|
3155
|
+
last = mark;
|
|
3284
3156
|
}
|
|
3285
|
-
return
|
|
3157
|
+
return last;
|
|
3286
3158
|
}
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
const inner = width - 4;
|
|
3291
|
-
const maxBody = Math.max(3, rows - 6);
|
|
3292
|
-
const body2 = [];
|
|
3293
|
-
if (dialog.kind === "pick") {
|
|
3294
|
-
const shown = filterItems(dialog.items, dialog.query);
|
|
3295
|
-
const listRows = Math.max(1, Math.min(shown.length, maxBody - 2));
|
|
3296
|
-
const start = Math.max(0, Math.min(dialog.selected - listRows + 1, shown.length - listRows));
|
|
3297
|
-
body2.push(`${paint.accent("search")} ${dialog.query}${paint.invert(" ")}`);
|
|
3298
|
-
body2.push("");
|
|
3299
|
-
if (!shown.length) body2.push(paint.muted("nothing matches"));
|
|
3300
|
-
for (const [i, item] of shown.slice(start, start + listRows).entries()) {
|
|
3301
|
-
const isSelected = start + i === dialog.selected;
|
|
3302
|
-
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
3303
|
-
const label2 = isSelected ? paint.bold(item.label) : item.label;
|
|
3304
|
-
body2.push(truncate(`${mark} ${label2}${item.hint ? ` ${paint.muted(item.hint)}` : ""}`, inner));
|
|
3305
|
-
}
|
|
3306
|
-
if (shown.length > listRows) body2.push(paint.dim(`\u2026 ${shown.length - listRows} more; type to narrow`));
|
|
3307
|
-
} else {
|
|
3308
|
-
const visible = Math.min(dialog.lines.length, maxBody);
|
|
3309
|
-
dialog.scroll = Math.max(0, Math.min(dialog.scroll, dialog.lines.length - visible));
|
|
3310
|
-
for (const line of dialog.lines.slice(dialog.scroll, dialog.scroll + visible)) body2.push(truncate(line, inner));
|
|
3311
|
-
if (dialog.lines.length > visible) {
|
|
3312
|
-
body2.push(paint.dim(`\u2026 ${dialog.scroll + visible} of ${dialog.lines.length} lines \xB7 \u2191\u2193 to scroll`));
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
const title = ` ${dialog.title} `;
|
|
3316
|
-
const rule = BOX.h.repeat(Math.max(0, width - 2 - title.length - 1));
|
|
3317
|
-
const lines = [
|
|
3318
|
-
paint.dim(`${BOX.tl}${BOX.h}`) + paint.bold(title) + paint.dim(`${rule}${BOX.tr}`),
|
|
3319
|
-
...body2.map((line) => `${paint.dim(BOX.v)} ${pad3(truncate(line, inner), inner)} ${paint.dim(BOX.v)}`),
|
|
3320
|
-
paint.dim(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`)
|
|
3321
|
-
];
|
|
3322
|
-
const top = Math.max(0, Math.floor((rows - lines.length) / 2));
|
|
3323
|
-
const left = Math.max(0, Math.floor((columns - width) / 2));
|
|
3324
|
-
return { top, left, lines };
|
|
3159
|
+
function nextBoundary(text, at2) {
|
|
3160
|
+
for (const mark of boundaries(text)) if (mark > at2) return mark;
|
|
3161
|
+
return text.length;
|
|
3325
3162
|
}
|
|
3326
|
-
function
|
|
3327
|
-
|
|
3328
|
-
|
|
3163
|
+
function wordStart(text, at2) {
|
|
3164
|
+
let i = at2;
|
|
3165
|
+
while (i > 0 && /\s/.test(text[i - 1])) i--;
|
|
3166
|
+
while (i > 0 && !/\s/.test(text[i - 1])) i--;
|
|
3167
|
+
return i;
|
|
3329
3168
|
}
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
var
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
var MOUSE_OFF = `${ESC4}[?1006l${ESC4}[?1000l`;
|
|
3342
|
-
var CLEAR = `${ESC4}[2J`;
|
|
3343
|
-
var RESET = `${ESC4}[0m`;
|
|
3344
|
-
var at = (row, col) => `${ESC4}[${row + 1};${col + 1}H`;
|
|
3345
|
-
var Screen = class extends EventEmitter {
|
|
3346
|
-
constructor(out = process.stdout, input = process.stdin, opts = {}) {
|
|
3347
|
-
super();
|
|
3348
|
-
this.out = out;
|
|
3349
|
-
this.input = input;
|
|
3350
|
-
this.opts = opts;
|
|
3169
|
+
function wordEnd(text, at2) {
|
|
3170
|
+
let i = at2;
|
|
3171
|
+
while (i < text.length && /\s/.test(text[i])) i++;
|
|
3172
|
+
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
3173
|
+
return i;
|
|
3174
|
+
}
|
|
3175
|
+
var LineEditor = class {
|
|
3176
|
+
value = "";
|
|
3177
|
+
at = 0;
|
|
3178
|
+
get text() {
|
|
3179
|
+
return this.value;
|
|
3351
3180
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
decoder = new KeyDecoder();
|
|
3356
|
-
get rows() {
|
|
3357
|
-
const n = this.opts.rows ?? this.out.rows;
|
|
3358
|
-
return typeof n === "number" && n > 0 ? n : 24;
|
|
3181
|
+
/** Cursor position, as an offset into `text`, always on a grapheme boundary. */
|
|
3182
|
+
get cursor() {
|
|
3183
|
+
return this.at;
|
|
3359
3184
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3185
|
+
/** Set the whole line - from history, or a completion. Cursor lands at the end. */
|
|
3186
|
+
setText(text) {
|
|
3187
|
+
this.value = sanitize(text);
|
|
3188
|
+
this.at = this.value.length;
|
|
3363
3189
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
this.
|
|
3374
|
-
|
|
3375
|
-
this.input.on("end", this.onEnd);
|
|
3376
|
-
this.input.resume();
|
|
3377
|
-
this.out.on?.("resize", this.onResize);
|
|
3378
|
-
process.once("exit", this.leave);
|
|
3190
|
+
reset() {
|
|
3191
|
+
this.value = "";
|
|
3192
|
+
this.at = 0;
|
|
3193
|
+
}
|
|
3194
|
+
/** Insert at the cursor, newlines removed. Used for typing and for paste. */
|
|
3195
|
+
insert(text) {
|
|
3196
|
+
const clean = sanitize(text);
|
|
3197
|
+
if (!clean) return "same";
|
|
3198
|
+
this.value = this.value.slice(0, this.at) + clean + this.value.slice(this.at);
|
|
3199
|
+
this.at += clean.length;
|
|
3200
|
+
return "text";
|
|
3379
3201
|
}
|
|
3380
|
-
/** Give the terminal back, in the reverse order. Safe to call more than once. */
|
|
3381
|
-
leave = () => {
|
|
3382
|
-
if (!this.entered) return;
|
|
3383
|
-
this.entered = false;
|
|
3384
|
-
process.off("exit", this.leave);
|
|
3385
|
-
this.out.off?.("resize", this.onResize);
|
|
3386
|
-
this.input.off("data", this.onData);
|
|
3387
|
-
this.input.off("end", this.onEnd);
|
|
3388
|
-
try {
|
|
3389
|
-
this.input.pause();
|
|
3390
|
-
this.input.setRawMode?.(false);
|
|
3391
|
-
} catch {
|
|
3392
|
-
}
|
|
3393
|
-
this.out.write(`${this.opts.mouse ? MOUSE_OFF : ""}${PASTE_OFF}${RESET}${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
|
|
3394
|
-
this.last = null;
|
|
3395
|
-
};
|
|
3396
3202
|
/**
|
|
3397
|
-
*
|
|
3203
|
+
* Apply one key.
|
|
3398
3204
|
*
|
|
3399
|
-
* `
|
|
3400
|
-
*
|
|
3401
|
-
*
|
|
3205
|
+
* Returns `unhandled` for anything this does not claim - Enter, ctrl-c, Tab -
|
|
3206
|
+
* so the caller can decide what submitting, cancelling, and completing mean.
|
|
3207
|
+
* Those are session concerns, not editing ones.
|
|
3402
3208
|
*/
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
if (
|
|
3410
|
-
|
|
3411
|
-
this.last = new Array(rows).fill(null);
|
|
3412
|
-
this.lastSize = size;
|
|
3209
|
+
apply(key) {
|
|
3210
|
+
const { name, ctrl, meta } = key;
|
|
3211
|
+
if (name === "left" && !ctrl && !meta) return this.moveTo(prevBoundary(this.value, this.at));
|
|
3212
|
+
if (name === "right" && !ctrl && !meta) return this.moveTo(nextBoundary(this.value, this.at));
|
|
3213
|
+
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
3214
|
+
if (name === "end" || ctrl && name === "e") return this.moveTo(this.value.length);
|
|
3215
|
+
if ((meta || ctrl) && (name === "left" || name === "b")) {
|
|
3216
|
+
return this.moveTo(wordStart(this.value, this.at));
|
|
3413
3217
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
if (this.last[r] === line) continue;
|
|
3417
|
-
this.last[r] = line;
|
|
3418
|
-
out += `${at(r, 0)}${line}${RESET}`;
|
|
3218
|
+
if ((meta || ctrl) && (name === "right" || name === "f")) {
|
|
3219
|
+
return this.moveTo(wordEnd(this.value, this.at));
|
|
3419
3220
|
}
|
|
3420
|
-
if (
|
|
3421
|
-
this.
|
|
3221
|
+
if (name === "backspace" && (meta || ctrl)) return this.cut(wordStart(this.value, this.at), this.at);
|
|
3222
|
+
if (ctrl && name === "w") return this.cut(wordStart(this.value, this.at), this.at);
|
|
3223
|
+
if (name === "backspace") return this.cut(prevBoundary(this.value, this.at), this.at);
|
|
3224
|
+
if (name === "delete") return this.cut(this.at, nextBoundary(this.value, this.at));
|
|
3225
|
+
if (ctrl && name === "u") return this.cut(0, this.at);
|
|
3226
|
+
if (ctrl && name === "k") return this.cut(this.at, this.value.length);
|
|
3227
|
+
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) {
|
|
3228
|
+
return this.insert(key.sequence);
|
|
3229
|
+
}
|
|
3230
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
3231
|
+
return "unhandled";
|
|
3422
3232
|
}
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
this.
|
|
3233
|
+
moveTo(next) {
|
|
3234
|
+
if (next === this.at) return "same";
|
|
3235
|
+
this.at = next;
|
|
3236
|
+
return "cursor";
|
|
3237
|
+
}
|
|
3238
|
+
cut(from, to) {
|
|
3239
|
+
if (from >= to) return "same";
|
|
3240
|
+
this.value = this.value.slice(0, from) + this.value.slice(to);
|
|
3241
|
+
this.at = from;
|
|
3242
|
+
return "text";
|
|
3243
|
+
}
|
|
3244
|
+
/**
|
|
3245
|
+
* The slice of the line that fits in `width`, and where the cursor sits in it.
|
|
3246
|
+
*
|
|
3247
|
+
* A line longer than the box scrolls horizontally rather than wrapping, and
|
|
3248
|
+
* the window is chosen so the cursor is always inside it - typing past the
|
|
3249
|
+
* right edge must not take the cursor somewhere the reader cannot see it.
|
|
3250
|
+
*/
|
|
3251
|
+
viewport(width) {
|
|
3252
|
+
if (width <= 0) return { text: "", column: 0 };
|
|
3253
|
+
if (this.value.length <= width) return { text: this.value, column: this.at };
|
|
3254
|
+
let start = Math.max(0, this.at - width + 1);
|
|
3255
|
+
start = Math.min(start, this.value.length - width);
|
|
3256
|
+
return { text: this.value.slice(start, start + width), column: this.at - start };
|
|
3426
3257
|
}
|
|
3427
|
-
onData = (chunk) => {
|
|
3428
|
-
const keys = this.decoder.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
3429
|
-
if (keys.length) this.emit("keys", keys);
|
|
3430
|
-
};
|
|
3431
|
-
onEnd = () => {
|
|
3432
|
-
this.emit("end");
|
|
3433
|
-
};
|
|
3434
|
-
onResize = () => {
|
|
3435
|
-
this.last = null;
|
|
3436
|
-
this.emit("resize");
|
|
3437
|
-
};
|
|
3438
3258
|
};
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
let fenceLang = "";
|
|
3452
|
-
for (const line of lines) {
|
|
3453
|
-
const fence = /^\s*```(\w*)/.exec(line);
|
|
3454
|
-
if (fence) {
|
|
3455
|
-
if (!inFence) {
|
|
3456
|
-
inFence = true;
|
|
3457
|
-
fenceLang = fence[1] ?? "";
|
|
3458
|
-
out.push(paint.dim(`${pad4}${fenceLang || "code"}`));
|
|
3459
|
-
} else {
|
|
3460
|
-
inFence = false;
|
|
3461
|
-
fenceLang = "";
|
|
3462
|
-
out.push("");
|
|
3463
|
-
}
|
|
3464
|
-
continue;
|
|
3259
|
+
var History = class {
|
|
3260
|
+
constructor(limit = 200) {
|
|
3261
|
+
this.limit = limit;
|
|
3262
|
+
}
|
|
3263
|
+
entries = [];
|
|
3264
|
+
index = -1;
|
|
3265
|
+
draft = "";
|
|
3266
|
+
add(line) {
|
|
3267
|
+
const clean = sanitize(line).trim();
|
|
3268
|
+
if (clean && this.entries.at(-1) !== clean) {
|
|
3269
|
+
this.entries.push(clean);
|
|
3270
|
+
if (this.entries.length > this.limit) this.entries.shift();
|
|
3465
3271
|
}
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3272
|
+
this.index = -1;
|
|
3273
|
+
this.draft = "";
|
|
3274
|
+
}
|
|
3275
|
+
/** The line to show, or null when there is nowhere further to go. */
|
|
3276
|
+
up(current) {
|
|
3277
|
+
if (!this.entries.length) return null;
|
|
3278
|
+
if (this.index === -1) {
|
|
3279
|
+
this.draft = current;
|
|
3280
|
+
this.index = this.entries.length - 1;
|
|
3281
|
+
} else if (this.index > 0) {
|
|
3282
|
+
this.index--;
|
|
3283
|
+
} else {
|
|
3284
|
+
return null;
|
|
3469
3285
|
}
|
|
3470
|
-
|
|
3286
|
+
return this.entries[this.index];
|
|
3471
3287
|
}
|
|
3472
|
-
|
|
3288
|
+
down() {
|
|
3289
|
+
if (this.index === -1) return null;
|
|
3290
|
+
if (this.index >= this.entries.length - 1) {
|
|
3291
|
+
this.index = -1;
|
|
3292
|
+
return this.draft;
|
|
3293
|
+
}
|
|
3294
|
+
this.index++;
|
|
3295
|
+
return this.entries[this.index];
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
|
|
3299
|
+
// packages/tui/src/editor.ts
|
|
3300
|
+
function sanitizeMultiline(text) {
|
|
3301
|
+
return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
3473
3302
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3303
|
+
var TextEditor = class {
|
|
3304
|
+
lines = [""];
|
|
3305
|
+
row = 0;
|
|
3306
|
+
col = 0;
|
|
3307
|
+
/** First line shown in the last `view`, kept so the window scrolls rather than jumps. */
|
|
3308
|
+
top = 0;
|
|
3309
|
+
get text() {
|
|
3310
|
+
return this.lines.join("\n");
|
|
3479
3311
|
}
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
const indent2 = `${pad4}${bullet[1]}${paint.accent("-")} `;
|
|
3483
|
-
return wrap2(inline(bullet[2]), width, indent2, `${pad4}${bullet[1]} `);
|
|
3312
|
+
get cursor() {
|
|
3313
|
+
return { row: this.row, col: this.col };
|
|
3484
3314
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
const indent2 = `${pad4}${numbered[1]}${paint.accent(`${numbered[2]}.`)} `;
|
|
3488
|
-
return wrap2(inline(numbered[3]), width, indent2, `${pad4}${numbered[1]} `);
|
|
3315
|
+
get lineCount() {
|
|
3316
|
+
return this.lines.length;
|
|
3489
3317
|
}
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
const gutter = `${pad4}${paint.dim("|")} `;
|
|
3493
|
-
return wrap2(paint.muted(inline(quote[1])), width, gutter, gutter);
|
|
3318
|
+
get isEmpty() {
|
|
3319
|
+
return this.lines.length === 1 && this.lines[0] === "";
|
|
3494
3320
|
}
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3321
|
+
/** The line the caret is on. Used to work out whether a `/` or `@` is being typed. */
|
|
3322
|
+
get currentLine() {
|
|
3323
|
+
return this.lines[this.row];
|
|
3324
|
+
}
|
|
3325
|
+
/** Replace everything - from history, or a completion. The caret lands at the end. */
|
|
3326
|
+
setText(text) {
|
|
3327
|
+
this.lines = sanitizeMultiline(text).split("\n");
|
|
3328
|
+
this.row = this.lines.length - 1;
|
|
3329
|
+
this.col = this.lines[this.row].length;
|
|
3330
|
+
}
|
|
3331
|
+
/** Replace just the caret's line, keeping the rest. For a completion of `/cmd` or `@path`. */
|
|
3332
|
+
setCurrentLine(text) {
|
|
3333
|
+
this.lines[this.row] = sanitizeMultiline(text).replace(/\n/g, " ");
|
|
3334
|
+
this.col = this.lines[this.row].length;
|
|
3335
|
+
}
|
|
3336
|
+
reset() {
|
|
3337
|
+
this.lines = [""];
|
|
3338
|
+
this.row = 0;
|
|
3339
|
+
this.col = 0;
|
|
3340
|
+
this.top = 0;
|
|
3341
|
+
}
|
|
3342
|
+
/** Insert at the caret. Newlines in `text` become new lines. */
|
|
3343
|
+
insert(text) {
|
|
3344
|
+
const clean = sanitizeMultiline(text);
|
|
3345
|
+
if (!clean) return "same";
|
|
3346
|
+
const parts = clean.split("\n");
|
|
3347
|
+
const line = this.lines[this.row];
|
|
3348
|
+
const before = line.slice(0, this.col);
|
|
3349
|
+
const after = line.slice(this.col);
|
|
3350
|
+
if (parts.length === 1) {
|
|
3351
|
+
this.lines[this.row] = before + parts[0] + after;
|
|
3352
|
+
this.col += parts[0].length;
|
|
3353
|
+
return "text";
|
|
3354
|
+
}
|
|
3355
|
+
const inserted = [before + parts[0], ...parts.slice(1, -1), parts[parts.length - 1] + after];
|
|
3356
|
+
this.lines.splice(this.row, 1, ...inserted);
|
|
3357
|
+
this.row += parts.length - 1;
|
|
3358
|
+
this.col = parts[parts.length - 1].length;
|
|
3359
|
+
return "text";
|
|
3360
|
+
}
|
|
3361
|
+
newline() {
|
|
3362
|
+
return this.insert("\n");
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Apply one key. `unhandled` for a plain Enter (the caller sends), for Up on
|
|
3366
|
+
* the first line and Down on the last (the caller walks history), and for
|
|
3367
|
+
* anything else the editor does not claim.
|
|
3368
|
+
*/
|
|
3369
|
+
apply(key) {
|
|
3370
|
+
const { name, ctrl, meta, shift } = key;
|
|
3371
|
+
const line = this.lines[this.row];
|
|
3372
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
3373
|
+
if (name === "return") {
|
|
3374
|
+
if (shift || ctrl || meta) return this.newline();
|
|
3375
|
+
if (/(^|[^\\])\\$/.test(line)) {
|
|
3376
|
+
this.lines[this.row] = line.slice(0, -1);
|
|
3377
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
3378
|
+
return this.newline();
|
|
3379
|
+
}
|
|
3380
|
+
return "unhandled";
|
|
3381
|
+
}
|
|
3382
|
+
if (ctrl && name === "j") return this.newline();
|
|
3383
|
+
if (name === "up" && !ctrl && !meta) {
|
|
3384
|
+
if (this.row === 0) return "unhandled";
|
|
3385
|
+
this.row--;
|
|
3386
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
3387
|
+
return "cursor";
|
|
3388
|
+
}
|
|
3389
|
+
if (name === "down" && !ctrl && !meta) {
|
|
3390
|
+
if (this.row === this.lines.length - 1) return "unhandled";
|
|
3391
|
+
this.row++;
|
|
3392
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
3393
|
+
return "cursor";
|
|
3394
|
+
}
|
|
3395
|
+
if (name === "left" && !ctrl && !meta) {
|
|
3396
|
+
if (this.col === 0) {
|
|
3397
|
+
if (this.row === 0) return "same";
|
|
3398
|
+
this.row--;
|
|
3399
|
+
this.col = this.lines[this.row].length;
|
|
3400
|
+
return "cursor";
|
|
3401
|
+
}
|
|
3402
|
+
return this.moveTo(prevBoundary(line, this.col));
|
|
3403
|
+
}
|
|
3404
|
+
if (name === "right" && !ctrl && !meta) {
|
|
3405
|
+
if (this.col >= line.length) {
|
|
3406
|
+
if (this.row === this.lines.length - 1) return "same";
|
|
3407
|
+
this.row++;
|
|
3408
|
+
this.col = 0;
|
|
3409
|
+
return "cursor";
|
|
3410
|
+
}
|
|
3411
|
+
return this.moveTo(nextBoundary(line, this.col));
|
|
3412
|
+
}
|
|
3413
|
+
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
3414
|
+
if (name === "end" || ctrl && name === "e") return this.moveTo(line.length);
|
|
3415
|
+
if ((meta || ctrl) && (name === "left" || name === "b")) return this.moveTo(wordStart(line, this.col));
|
|
3416
|
+
if ((meta || ctrl) && (name === "right" || name === "f")) return this.moveTo(wordEnd(line, this.col));
|
|
3417
|
+
if (name === "backspace" && (meta || ctrl)) {
|
|
3418
|
+
if (this.col === 0) return this.joinUp();
|
|
3419
|
+
return this.cut(wordStart(line, this.col), this.col);
|
|
3420
|
+
}
|
|
3421
|
+
if (ctrl && name === "w") {
|
|
3422
|
+
if (this.col === 0) return this.joinUp();
|
|
3423
|
+
return this.cut(wordStart(line, this.col), this.col);
|
|
3424
|
+
}
|
|
3425
|
+
if (name === "backspace") {
|
|
3426
|
+
if (this.col === 0) return this.joinUp();
|
|
3427
|
+
return this.cut(prevBoundary(line, this.col), this.col);
|
|
3428
|
+
}
|
|
3429
|
+
if (name === "delete") {
|
|
3430
|
+
if (this.col >= line.length) return this.joinDown();
|
|
3431
|
+
return this.cut(this.col, nextBoundary(line, this.col));
|
|
3432
|
+
}
|
|
3433
|
+
if (ctrl && name === "u") return this.cut(0, this.col);
|
|
3434
|
+
if (ctrl && name === "k") {
|
|
3435
|
+
if (this.col >= line.length) return this.joinDown();
|
|
3436
|
+
return this.cut(this.col, line.length);
|
|
3437
|
+
}
|
|
3438
|
+
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) return this.insert(key.sequence);
|
|
3439
|
+
return "unhandled";
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* The lines to draw and where the caret is among them.
|
|
3443
|
+
*
|
|
3444
|
+
* At most `maxLines` rows, chosen so the caret's row is always one of them;
|
|
3445
|
+
* each row scrolls horizontally on its own so a long line never wraps into the
|
|
3446
|
+
* row below and the caret never leaves the box.
|
|
3447
|
+
*/
|
|
3448
|
+
view(width, maxLines) {
|
|
3449
|
+
const rows = Math.max(1, maxLines);
|
|
3450
|
+
if (this.row < this.top) this.top = this.row;
|
|
3451
|
+
if (this.row >= this.top + rows) this.top = this.row - rows + 1;
|
|
3452
|
+
this.top = Math.max(0, Math.min(this.top, this.lines.length - rows));
|
|
3453
|
+
const shown = this.lines.slice(this.top, this.top + rows);
|
|
3454
|
+
let cursorCol = this.col;
|
|
3455
|
+
const lines = shown.map((line, i) => {
|
|
3456
|
+
if (width <= 0) return "";
|
|
3457
|
+
if (line.length <= width) return line;
|
|
3458
|
+
if (this.top + i !== this.row) return line.slice(0, width);
|
|
3459
|
+
const start = Math.max(0, this.col - width + 1);
|
|
3460
|
+
cursorCol = this.col - start;
|
|
3461
|
+
return line.slice(start, start + width);
|
|
3462
|
+
});
|
|
3463
|
+
return { lines, cursor: { row: this.row - this.top, col: cursorCol } };
|
|
3464
|
+
}
|
|
3465
|
+
moveTo(next) {
|
|
3466
|
+
if (next === this.col) return "same";
|
|
3467
|
+
this.col = next;
|
|
3468
|
+
return "cursor";
|
|
3469
|
+
}
|
|
3470
|
+
cut(from, to) {
|
|
3471
|
+
if (from >= to) return "same";
|
|
3472
|
+
const line = this.lines[this.row];
|
|
3473
|
+
this.lines[this.row] = line.slice(0, from) + line.slice(to);
|
|
3474
|
+
this.col = from;
|
|
3475
|
+
return "text";
|
|
3476
|
+
}
|
|
3477
|
+
joinUp() {
|
|
3478
|
+
if (this.row === 0) return "same";
|
|
3479
|
+
const above = this.lines[this.row - 1];
|
|
3480
|
+
this.lines.splice(this.row - 1, 2, above + this.lines[this.row]);
|
|
3481
|
+
this.row--;
|
|
3482
|
+
this.col = above.length;
|
|
3483
|
+
return "text";
|
|
3484
|
+
}
|
|
3485
|
+
joinDown() {
|
|
3486
|
+
if (this.row >= this.lines.length - 1) return "same";
|
|
3487
|
+
this.lines.splice(this.row, 2, this.lines[this.row] + this.lines[this.row + 1]);
|
|
3488
|
+
return "text";
|
|
3489
|
+
}
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
// packages/tui/src/keys.ts
|
|
3493
|
+
var ESC3 = "\x1B";
|
|
3494
|
+
var PASTE_START = `${ESC3}[200~`;
|
|
3495
|
+
var PASTE_END = `${ESC3}[201~`;
|
|
3496
|
+
var CSI_FINAL = {
|
|
3497
|
+
A: "up",
|
|
3498
|
+
B: "down",
|
|
3499
|
+
C: "right",
|
|
3500
|
+
D: "left",
|
|
3501
|
+
H: "home",
|
|
3502
|
+
F: "end",
|
|
3503
|
+
Z: "tab"
|
|
3504
|
+
};
|
|
3505
|
+
var TILDE = {
|
|
3506
|
+
"1": "home",
|
|
3507
|
+
"2": "insert",
|
|
3508
|
+
"3": "delete",
|
|
3509
|
+
"4": "end",
|
|
3510
|
+
"5": "pageup",
|
|
3511
|
+
"6": "pagedown",
|
|
3512
|
+
"7": "home",
|
|
3513
|
+
"8": "end"
|
|
3514
|
+
};
|
|
3515
|
+
function modifiers(param) {
|
|
3516
|
+
const n = (Number(param) || 1) - 1;
|
|
3517
|
+
return {
|
|
3518
|
+
...n & 1 ? { shift: true } : {},
|
|
3519
|
+
...n & 2 ? { meta: true } : {},
|
|
3520
|
+
...n & 4 ? { ctrl: true } : {}
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
function coded(code, mods, sequence) {
|
|
3524
|
+
switch (code) {
|
|
3525
|
+
case 13:
|
|
3526
|
+
return { name: "return", ...mods, sequence };
|
|
3527
|
+
case 9:
|
|
3528
|
+
return { name: "tab", ...mods, sequence };
|
|
3529
|
+
case 27:
|
|
3530
|
+
return { name: "escape", ...mods, sequence };
|
|
3531
|
+
case 127:
|
|
3532
|
+
case 8:
|
|
3533
|
+
return { name: "backspace", ...mods, sequence };
|
|
3534
|
+
default:
|
|
3535
|
+
if (code >= 32) {
|
|
3536
|
+
const ch = String.fromCodePoint(code);
|
|
3537
|
+
return { name: ch.toLowerCase(), ...mods, sequence: mods.ctrl || mods.meta ? sequence : ch };
|
|
3538
|
+
}
|
|
3539
|
+
return null;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
function isPrefix(text, whole) {
|
|
3543
|
+
return text.length <= whole.length && whole.startsWith(text);
|
|
3544
|
+
}
|
|
3545
|
+
function partialMarker(buf, marker) {
|
|
3546
|
+
for (let n = Math.min(marker.length - 1, buf.length); n > 0; n--) {
|
|
3547
|
+
if (isPrefix(buf.slice(buf.length - n), marker)) return n;
|
|
3548
|
+
}
|
|
3549
|
+
return 0;
|
|
3550
|
+
}
|
|
3551
|
+
var KeyDecoder = class {
|
|
3552
|
+
/** Bytes that could still turn out to be the start of something. */
|
|
3553
|
+
pending = "";
|
|
3554
|
+
/** Inside a bracketed paste: the text so far. Null outside one. */
|
|
3555
|
+
pasting = null;
|
|
3556
|
+
/**
|
|
3557
|
+
* Decode a chunk. Sequences split across two reads are held until the next
|
|
3558
|
+
* one; a lone ESC at the END of a chunk is reported as Escape at once, because
|
|
3559
|
+
* a terminal sends a whole sequence in one write and a person pressing the key
|
|
3560
|
+
* should not have to press another to be heard.
|
|
3561
|
+
*/
|
|
3562
|
+
feed(chunk) {
|
|
3563
|
+
const out = [];
|
|
3564
|
+
let buf = this.pending + chunk;
|
|
3565
|
+
this.pending = "";
|
|
3566
|
+
let i = 0;
|
|
3567
|
+
while (i < buf.length) {
|
|
3568
|
+
if (this.pasting !== null) {
|
|
3569
|
+
const end = buf.indexOf(PASTE_END, i);
|
|
3570
|
+
if (end === -1) {
|
|
3571
|
+
const hold = partialMarker(buf, PASTE_END);
|
|
3572
|
+
this.pasting += buf.slice(i, buf.length - hold);
|
|
3573
|
+
this.pending = buf.slice(buf.length - hold);
|
|
3574
|
+
return out;
|
|
3575
|
+
}
|
|
3576
|
+
this.pasting += buf.slice(i, end);
|
|
3577
|
+
out.push({ name: "paste", paste: this.pasting.replace(/\r\n?/g, "\n") });
|
|
3578
|
+
this.pasting = null;
|
|
3579
|
+
i = end + PASTE_END.length;
|
|
3580
|
+
continue;
|
|
3581
|
+
}
|
|
3582
|
+
const ch = buf[i];
|
|
3583
|
+
if (ch !== ESC3) {
|
|
3584
|
+
const code2 = ch.charCodeAt(0);
|
|
3585
|
+
if (code2 === 13) out.push({ name: "return", sequence: ch });
|
|
3586
|
+
else if (code2 === 10) out.push({ name: "j", ctrl: true, sequence: ch });
|
|
3587
|
+
else if (code2 === 9) out.push({ name: "tab", sequence: ch });
|
|
3588
|
+
else if (code2 === 127 || code2 === 8) out.push({ name: "backspace", sequence: ch });
|
|
3589
|
+
else if (code2 < 32) {
|
|
3590
|
+
if (code2 > 0) out.push({ name: String.fromCharCode(96 + code2), ctrl: true, sequence: ch });
|
|
3591
|
+
} else {
|
|
3592
|
+
let j = i + 1;
|
|
3593
|
+
while (j < buf.length) {
|
|
3594
|
+
const c = buf.charCodeAt(j);
|
|
3595
|
+
if (c < 32 || c === 127 || buf[j] === ESC3) break;
|
|
3596
|
+
j++;
|
|
3597
|
+
}
|
|
3598
|
+
out.push({ sequence: buf.slice(i, j) });
|
|
3599
|
+
i = j;
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
i++;
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
if (i === buf.length - 1) {
|
|
3606
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
3607
|
+
i++;
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
const next = buf[i + 1];
|
|
3611
|
+
if (next === "[") {
|
|
3612
|
+
let j = i + 2;
|
|
3613
|
+
while (j < buf.length && !(buf.charCodeAt(j) >= 64 && buf.charCodeAt(j) <= 126)) j++;
|
|
3614
|
+
if (j >= buf.length) {
|
|
3615
|
+
this.pending = buf.slice(i);
|
|
3616
|
+
return out;
|
|
3617
|
+
}
|
|
3618
|
+
const seq = buf.slice(i, j + 1);
|
|
3619
|
+
const body2 = buf.slice(i + 2, j);
|
|
3620
|
+
const final = buf[j];
|
|
3621
|
+
i = j + 1;
|
|
3622
|
+
if (seq === PASTE_START) {
|
|
3623
|
+
this.pasting = "";
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
const key = decodeCsi(body2, final, seq);
|
|
3627
|
+
if (key) out.push(key);
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
if (next === "O") {
|
|
3631
|
+
const final = buf[i + 2];
|
|
3632
|
+
if (final === void 0) {
|
|
3633
|
+
this.pending = buf.slice(i);
|
|
3634
|
+
return out;
|
|
3635
|
+
}
|
|
3636
|
+
const name = CSI_FINAL[final];
|
|
3637
|
+
if (name) out.push({ name, sequence: buf.slice(i, i + 3) });
|
|
3638
|
+
i += 3;
|
|
3639
|
+
continue;
|
|
3640
|
+
}
|
|
3641
|
+
if (next === "\x7F" || next === "\b") {
|
|
3642
|
+
out.push({ name: "backspace", meta: true, sequence: buf.slice(i, i + 2) });
|
|
3643
|
+
i += 2;
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
if (next === ESC3) {
|
|
3647
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
3648
|
+
i++;
|
|
3649
|
+
continue;
|
|
3650
|
+
}
|
|
3651
|
+
const code = next.charCodeAt(0);
|
|
3652
|
+
if (code >= 32 && code !== 127) {
|
|
3653
|
+
out.push({
|
|
3654
|
+
name: next.toLowerCase(),
|
|
3655
|
+
meta: true,
|
|
3656
|
+
...next !== next.toLowerCase() ? { shift: true } : {},
|
|
3657
|
+
sequence: buf.slice(i, i + 2)
|
|
3658
|
+
});
|
|
3659
|
+
i += 2;
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
3663
|
+
i++;
|
|
3664
|
+
}
|
|
3665
|
+
return out;
|
|
3666
|
+
}
|
|
3667
|
+
/** Anything still held back - the tail of a paste, an escape that never finished. */
|
|
3668
|
+
flush() {
|
|
3669
|
+
const out = [];
|
|
3670
|
+
if (this.pasting !== null) {
|
|
3671
|
+
out.push({ name: "paste", paste: this.pasting + this.pending });
|
|
3672
|
+
this.pasting = null;
|
|
3673
|
+
} else if (this.pending === ESC3) {
|
|
3674
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
3675
|
+
}
|
|
3676
|
+
this.pending = "";
|
|
3677
|
+
return out;
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
function decodeCsi(body2, final, sequence) {
|
|
3681
|
+
if (body2.startsWith("<") && (final === "M" || final === "m")) {
|
|
3682
|
+
const [b, x, y] = body2.slice(1).split(";").map(Number);
|
|
3683
|
+
if (final === "m") return null;
|
|
3684
|
+
if (b & 32) return null;
|
|
3685
|
+
const button = b & 3;
|
|
3686
|
+
if (b & 64) {
|
|
3687
|
+
return { name: "mouse", sequence, mouse: { kind: button === 0 ? "wheel-up" : "wheel-down", button: b, x, y } };
|
|
3522
3688
|
}
|
|
3689
|
+
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
3523
3690
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3691
|
+
const params = body2.split(";");
|
|
3692
|
+
if (final === "~") {
|
|
3693
|
+
if (params[0] === "27") return coded(Number(params[2]), modifiers(params[1]), sequence);
|
|
3694
|
+
if (params[0] === "200" || params[0] === "201") return null;
|
|
3695
|
+
const name2 = TILDE[params[0]];
|
|
3696
|
+
return name2 ? { name: name2, ...modifiers(params[1]), sequence } : null;
|
|
3697
|
+
}
|
|
3698
|
+
if (final === "u") return coded(Number(params[0]), modifiers(params[1]), sequence);
|
|
3699
|
+
const name = CSI_FINAL[final];
|
|
3700
|
+
if (!name) return null;
|
|
3701
|
+
const mods = final === "Z" ? { shift: true } : modifiers(params[1]);
|
|
3702
|
+
return { name, ...mods, sequence };
|
|
3526
3703
|
}
|
|
3527
3704
|
|
|
3528
|
-
// packages/tui/src/
|
|
3529
|
-
var
|
|
3530
|
-
var
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
* it, and a card pushed up past the fold is one approved without being read.
|
|
3552
|
-
*/
|
|
3553
|
-
toEnd(entry) {
|
|
3554
|
-
const at2 = this.entries.indexOf(entry);
|
|
3555
|
-
if (at2 === -1 || at2 === this.entries.length - 1) return;
|
|
3556
|
-
this.entries.splice(at2, 1);
|
|
3557
|
-
this.entries.push(entry);
|
|
3558
|
-
}
|
|
3559
|
-
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
3560
|
-
touch(entry) {
|
|
3561
|
-
entry.version++;
|
|
3705
|
+
// packages/tui/src/layout.ts
|
|
3706
|
+
var MAX_COMPOSER_ROWS = 8;
|
|
3707
|
+
var SIDEBAR_MIN_COLUMNS = 120;
|
|
3708
|
+
var SIDEBAR_WIDTH = 32;
|
|
3709
|
+
function layout(opts) {
|
|
3710
|
+
const rows = Math.max(4, opts.rows);
|
|
3711
|
+
const columns = Math.max(20, opts.columns);
|
|
3712
|
+
const wanted = Math.min(MAX_COMPOSER_ROWS, Math.max(1, opts.editorLines) + 2);
|
|
3713
|
+
const composerHeight = Math.max(3, Math.min(wanted, rows - 3));
|
|
3714
|
+
const header = { top: 0, left: 0, width: columns, height: 1 };
|
|
3715
|
+
const footer = { top: rows - 1, left: 0, width: columns, height: 1 };
|
|
3716
|
+
const composer2 = { top: rows - 1 - composerHeight, left: 0, width: columns, height: composerHeight };
|
|
3717
|
+
const transcriptHeight = Math.max(0, composer2.top - 1);
|
|
3718
|
+
const withSidebar = Boolean(opts.sidebar) && columns >= SIDEBAR_MIN_COLUMNS;
|
|
3719
|
+
const mainWidth = withSidebar ? columns - SIDEBAR_WIDTH - 1 : columns;
|
|
3720
|
+
const out = {
|
|
3721
|
+
header,
|
|
3722
|
+
transcript: { top: 1, left: 0, width: mainWidth, height: transcriptHeight },
|
|
3723
|
+
composer: { ...composer2, width: mainWidth },
|
|
3724
|
+
footer
|
|
3725
|
+
};
|
|
3726
|
+
if (withSidebar) {
|
|
3727
|
+
out.sidebar = { top: 1, left: mainWidth + 1, width: SIDEBAR_WIDTH, height: composer2.top + composerHeight - 1 };
|
|
3562
3728
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3729
|
+
return out;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
// packages/tui/src/popup.ts
|
|
3733
|
+
var MAX_POPUP_ROWS = 8;
|
|
3734
|
+
var Popup = class {
|
|
3735
|
+
constructor(kind, items) {
|
|
3736
|
+
this.kind = kind;
|
|
3737
|
+
this.items = items;
|
|
3568
3738
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
const live = /* @__PURE__ */ new Set();
|
|
3573
|
-
for (const entry of this.entries) {
|
|
3574
|
-
live.add(entry.id);
|
|
3575
|
-
const lines = this.lines(entry, width);
|
|
3576
|
-
if (out.length && lines.length) out.push("");
|
|
3577
|
-
out.push(...lines);
|
|
3578
|
-
}
|
|
3579
|
-
for (const id of this.cache.keys()) if (!live.has(id)) this.cache.delete(id);
|
|
3580
|
-
for (const id of this.prose.keys()) if (!live.has(id)) this.prose.delete(id);
|
|
3581
|
-
return out;
|
|
3739
|
+
selected = 0;
|
|
3740
|
+
get current() {
|
|
3741
|
+
return this.items[this.selected] ?? null;
|
|
3582
3742
|
}
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
const lines = entry.block.kind === "assistant" ? this.assistant(entry, width) : renderBlock(entry.block, width);
|
|
3587
|
-
this.cache.set(entry.id, { version: entry.version, width, lines });
|
|
3588
|
-
return lines;
|
|
3743
|
+
move(delta) {
|
|
3744
|
+
if (!this.items.length) return;
|
|
3745
|
+
this.selected = (this.selected + delta + this.items.length) % this.items.length;
|
|
3589
3746
|
}
|
|
3590
|
-
/**
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
width,
|
|
3607
|
-
lines: complete2 ? renderMarkdown(complete2, { width: inner, indent: " " }).split("\n") : []
|
|
3608
|
-
};
|
|
3609
|
-
this.prose.set(entry.id, head);
|
|
3610
|
-
}
|
|
3611
|
-
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
3612
|
-
return [...head.lines, ...raw];
|
|
3747
|
+
/** Rows to draw, at most `MAX_POPUP_ROWS`, keeping the selection in view. */
|
|
3748
|
+
render(width, max = MAX_POPUP_ROWS) {
|
|
3749
|
+
if (!this.items.length) return [];
|
|
3750
|
+
const rows = Math.max(1, max);
|
|
3751
|
+
const start = Math.max(0, Math.min(this.selected - rows + 1, this.items.length - rows));
|
|
3752
|
+
const shown = this.items.slice(start, start + rows);
|
|
3753
|
+
const column = Math.min(Math.max(...shown.map((s) => s.label.length)), Math.max(10, Math.floor(width / 2)));
|
|
3754
|
+
return shown.map((item, i) => {
|
|
3755
|
+
const isSelected = start + i === this.selected;
|
|
3756
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
3757
|
+
const label2 = item.label.padEnd(column);
|
|
3758
|
+
const room = width - column - 6;
|
|
3759
|
+
const what = item.what ? paint.muted(truncate(item.what, Math.max(0, room))) : "";
|
|
3760
|
+
const line = ` ${mark} ${isSelected ? paint.bold(paint.accent(label2)) : paint.accent(label2)} ${what}`;
|
|
3761
|
+
return pad2(truncate(line, width), width);
|
|
3762
|
+
});
|
|
3613
3763
|
}
|
|
3614
3764
|
};
|
|
3615
|
-
function
|
|
3616
|
-
const
|
|
3617
|
-
|
|
3618
|
-
let open = -1;
|
|
3619
|
-
lines.forEach((line, i) => {
|
|
3620
|
-
if (/^\s*```/.test(line)) open = open === -1 ? i : -1;
|
|
3621
|
-
});
|
|
3622
|
-
let complete2 = lines;
|
|
3623
|
-
if (open !== -1) {
|
|
3624
|
-
tail = [...lines.slice(open), tail].join("\n");
|
|
3625
|
-
complete2 = lines.slice(0, open);
|
|
3626
|
-
}
|
|
3627
|
-
return { complete: complete2.join("\n"), tail };
|
|
3765
|
+
function pad2(text, width) {
|
|
3766
|
+
const short = width - visibleWidth(text);
|
|
3767
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3628
3768
|
}
|
|
3629
|
-
|
|
3630
|
-
|
|
3769
|
+
|
|
3770
|
+
// packages/tui/src/dialog.ts
|
|
3771
|
+
function pick(title, items, onSelect) {
|
|
3772
|
+
return { kind: "pick", title, items, query: "", selected: 0, onSelect };
|
|
3631
3773
|
}
|
|
3632
|
-
function
|
|
3633
|
-
|
|
3634
|
-
switch (block.kind) {
|
|
3635
|
-
case "user":
|
|
3636
|
-
return block.text.split("\n").flatMap(
|
|
3637
|
-
(line, i) => wrapPlain(line, inner - 2).map((piece, j) => {
|
|
3638
|
-
const text = i === 0 && j === 0 ? `${paint.accent(">")} ${piece}` : ` ${piece}`;
|
|
3639
|
-
return band(text, visibleWidth(text), width);
|
|
3640
|
-
})
|
|
3641
|
-
);
|
|
3642
|
-
case "assistant":
|
|
3643
|
-
return [];
|
|
3644
|
-
case "tool": {
|
|
3645
|
-
const out = toolBlock(block.name, block.detail, block.status, { input: block.input, output: block.output }, inner);
|
|
3646
|
-
if (block.live.length && !block.output) {
|
|
3647
|
-
const shown = block.live.slice(-LIVE_LINES);
|
|
3648
|
-
if (block.live.length > LIVE_LINES) out.push(` ${paint.dim(`\u2026 ${block.live.length - LIVE_LINES} more lines`)}`);
|
|
3649
|
-
for (const line of shown) out.push(` ${paint.dim(line)}`);
|
|
3650
|
-
}
|
|
3651
|
-
return out;
|
|
3652
|
-
}
|
|
3653
|
-
case "approval": {
|
|
3654
|
-
const out = [
|
|
3655
|
-
...block.helper ? [` ${paint.accent("from a helper:")} ${paint.muted(block.helper)}`] : [],
|
|
3656
|
-
...approvalCard(block.preview, block.call, block.mode, inner)
|
|
3657
|
-
];
|
|
3658
|
-
out.push(
|
|
3659
|
-
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")}`
|
|
3660
|
-
);
|
|
3661
|
-
return out;
|
|
3662
|
-
}
|
|
3663
|
-
case "error":
|
|
3664
|
-
return block.text.split("\n").map((line, i) => i === 0 ? `${paint.red(" labd:")} ${line}` : ` ${line}`);
|
|
3665
|
-
case "note":
|
|
3666
|
-
return block.text.replace(/\n$/, "").split("\n");
|
|
3667
|
-
case "compaction": {
|
|
3668
|
-
const label2 = " summarised the conversation so far ";
|
|
3669
|
-
const room = Math.max(0, inner - label2.length);
|
|
3670
|
-
return [paint.dim(` ${"\u2500".repeat(Math.floor(room / 2))}${label2}${"\u2500".repeat(Math.ceil(room / 2))}`)];
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3774
|
+
function info(title, lines) {
|
|
3775
|
+
return { kind: "info", title, lines, scroll: 0 };
|
|
3673
3776
|
}
|
|
3674
|
-
function
|
|
3675
|
-
const
|
|
3676
|
-
if (
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3777
|
+
function filterItems(items, query) {
|
|
3778
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
3779
|
+
if (!words.length) return items;
|
|
3780
|
+
return items.filter((item) => {
|
|
3781
|
+
const hay = `${item.label} ${item.hint ?? ""}`.toLowerCase();
|
|
3782
|
+
return words.every((w) => hay.includes(w));
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
function dialogKey(dialog, key) {
|
|
3786
|
+
if (key.name === "escape" || key.ctrl && (key.name === "c" || key.name === "g")) return "close";
|
|
3787
|
+
if (dialog.kind === "info") {
|
|
3788
|
+
if (key.name === "up" || key.name === "k") dialog.scroll = Math.max(0, dialog.scroll - 1);
|
|
3789
|
+
else if (key.name === "down" || key.name === "j") dialog.scroll++;
|
|
3790
|
+
else if (key.name === "pageup") dialog.scroll = Math.max(0, dialog.scroll - 10);
|
|
3791
|
+
else if (key.name === "pagedown") dialog.scroll += 10;
|
|
3792
|
+
else if (key.name === "home") dialog.scroll = 0;
|
|
3793
|
+
else if (key.name === "return" || key.name === "q") return "close";
|
|
3794
|
+
else if (key.mouse?.kind === "wheel-up") dialog.scroll = Math.max(0, dialog.scroll - 3);
|
|
3795
|
+
else if (key.mouse?.kind === "wheel-down") dialog.scroll += 3;
|
|
3796
|
+
else return "same";
|
|
3797
|
+
return "changed";
|
|
3798
|
+
}
|
|
3799
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
3800
|
+
if (key.name === "up" || key.mouse?.kind === "wheel-up") {
|
|
3801
|
+
dialog.selected = shown.length ? (dialog.selected - 1 + shown.length) % shown.length : 0;
|
|
3802
|
+
return "changed";
|
|
3690
3803
|
}
|
|
3691
|
-
if (
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
var Viewport = class {
|
|
3695
|
-
constructor(height) {
|
|
3696
|
-
this.height = height;
|
|
3804
|
+
if (key.name === "down" || key.mouse?.kind === "wheel-down" || key.name === "tab") {
|
|
3805
|
+
dialog.selected = shown.length ? (dialog.selected + 1) % shown.length : 0;
|
|
3806
|
+
return "changed";
|
|
3697
3807
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
/** Lines that arrived since the reader scrolled away. Zero while following. */
|
|
3704
|
-
get newLines() {
|
|
3705
|
-
return this.follow ? 0 : Math.max(0, this.total - this.anchor);
|
|
3808
|
+
if (key.name === "return") {
|
|
3809
|
+
const item = shown[dialog.selected];
|
|
3810
|
+
if (!item) return "same";
|
|
3811
|
+
dialog.onSelect(item);
|
|
3812
|
+
return "select";
|
|
3706
3813
|
}
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
else this.top = Math.max(0, Math.min(this.top, total - height));
|
|
3814
|
+
if (key.name === "backspace") {
|
|
3815
|
+
if (!dialog.query) return "same";
|
|
3816
|
+
dialog.query = dialog.query.slice(0, -1);
|
|
3817
|
+
dialog.selected = 0;
|
|
3818
|
+
return "changed";
|
|
3713
3819
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
this.top = next;
|
|
3719
|
-
if (next >= max) this.refollow();
|
|
3720
|
-
else this.unfollow();
|
|
3820
|
+
if (!key.ctrl && !key.meta && key.sequence && /^[^\u0000-\u001f\u007f]+$/.test(key.sequence)) {
|
|
3821
|
+
dialog.query += key.sequence;
|
|
3822
|
+
dialog.selected = 0;
|
|
3823
|
+
return "changed";
|
|
3721
3824
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3825
|
+
return "same";
|
|
3826
|
+
}
|
|
3827
|
+
var BOX = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
|
|
3828
|
+
function renderDialog(dialog, rows, columns) {
|
|
3829
|
+
const width = Math.max(24, Math.min(columns - 4, 84));
|
|
3830
|
+
const inner = width - 4;
|
|
3831
|
+
const maxBody = Math.max(3, rows - 6);
|
|
3832
|
+
const body2 = [];
|
|
3833
|
+
if (dialog.kind === "pick") {
|
|
3834
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
3835
|
+
const listRows = Math.max(1, Math.min(shown.length, maxBody - 2));
|
|
3836
|
+
const start = Math.max(0, Math.min(dialog.selected - listRows + 1, shown.length - listRows));
|
|
3837
|
+
body2.push(`${paint.accent("search")} ${dialog.query}${paint.invert(" ")}`);
|
|
3838
|
+
body2.push("");
|
|
3839
|
+
if (!shown.length) body2.push(paint.muted("nothing matches"));
|
|
3840
|
+
for (const [i, item] of shown.slice(start, start + listRows).entries()) {
|
|
3841
|
+
const isSelected = start + i === dialog.selected;
|
|
3842
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
3843
|
+
const label2 = isSelected ? paint.bold(item.label) : item.label;
|
|
3844
|
+
body2.push(truncate(`${mark} ${label2}${item.hint ? ` ${paint.muted(item.hint)}` : ""}`, inner));
|
|
3845
|
+
}
|
|
3846
|
+
if (shown.length > listRows) body2.push(paint.dim(`\u2026 ${shown.length - listRows} more; type to narrow`));
|
|
3847
|
+
} else {
|
|
3848
|
+
const visible = Math.min(dialog.lines.length, maxBody);
|
|
3849
|
+
dialog.scroll = Math.max(0, Math.min(dialog.scroll, dialog.lines.length - visible));
|
|
3850
|
+
for (const line of dialog.lines.slice(dialog.scroll, dialog.scroll + visible)) body2.push(truncate(line, inner));
|
|
3851
|
+
if (dialog.lines.length > visible) {
|
|
3852
|
+
body2.push(paint.dim(`\u2026 ${dialog.scroll + visible} of ${dialog.lines.length} lines \xB7 \u2191\u2193 to scroll`));
|
|
3853
|
+
}
|
|
3724
3854
|
}
|
|
3725
|
-
|
|
3726
|
-
|
|
3855
|
+
const title = ` ${dialog.title} `;
|
|
3856
|
+
const rule = BOX.h.repeat(Math.max(0, width - 2 - title.length - 1));
|
|
3857
|
+
const lines = [
|
|
3858
|
+
paint.dim(`${BOX.tl}${BOX.h}`) + paint.bold(title) + paint.dim(`${rule}${BOX.tr}`),
|
|
3859
|
+
...body2.map((line) => `${paint.dim(BOX.v)} ${pad3(truncate(line, inner), inner)} ${paint.dim(BOX.v)}`),
|
|
3860
|
+
paint.dim(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`)
|
|
3861
|
+
];
|
|
3862
|
+
const top = Math.max(0, Math.floor((rows - lines.length) / 2));
|
|
3863
|
+
const left = Math.max(0, Math.floor((columns - width) / 2));
|
|
3864
|
+
return { top, left, lines };
|
|
3865
|
+
}
|
|
3866
|
+
function pad3(text, width) {
|
|
3867
|
+
const short = width - visibleWidth(text);
|
|
3868
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// packages/tui/src/screen.ts
|
|
3872
|
+
import { EventEmitter } from "node:events";
|
|
3873
|
+
var ESC4 = "\x1B";
|
|
3874
|
+
var ALT_SCREEN_ON = `${ESC4}[?1049h`;
|
|
3875
|
+
var ALT_SCREEN_OFF = `${ESC4}[?1049l`;
|
|
3876
|
+
var CURSOR_HIDE = `${ESC4}[?25l`;
|
|
3877
|
+
var CURSOR_SHOW = `${ESC4}[?25h`;
|
|
3878
|
+
var PASTE_ON = `${ESC4}[?2004h`;
|
|
3879
|
+
var PASTE_OFF = `${ESC4}[?2004l`;
|
|
3880
|
+
var MOUSE_ON = `${ESC4}[?1000h${ESC4}[?1006h`;
|
|
3881
|
+
var MOUSE_OFF = `${ESC4}[?1006l${ESC4}[?1000l`;
|
|
3882
|
+
var CLEAR = `${ESC4}[2J`;
|
|
3883
|
+
var RESET = `${ESC4}[0m`;
|
|
3884
|
+
var at = (row, col) => `${ESC4}[${row + 1};${col + 1}H`;
|
|
3885
|
+
var Screen = class extends EventEmitter {
|
|
3886
|
+
constructor(out = process.stdout, input = process.stdin, opts = {}) {
|
|
3887
|
+
super();
|
|
3888
|
+
this.out = out;
|
|
3889
|
+
this.input = input;
|
|
3890
|
+
this.opts = opts;
|
|
3727
3891
|
}
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3892
|
+
last = null;
|
|
3893
|
+
lastSize = "";
|
|
3894
|
+
entered = false;
|
|
3895
|
+
decoder = new KeyDecoder();
|
|
3896
|
+
get rows() {
|
|
3897
|
+
const n = this.opts.rows ?? this.out.rows;
|
|
3898
|
+
return typeof n === "number" && n > 0 ? n : 24;
|
|
3731
3899
|
}
|
|
3732
|
-
|
|
3733
|
-
this.
|
|
3734
|
-
|
|
3900
|
+
get columns() {
|
|
3901
|
+
const n = this.opts.columns ?? this.out.columns;
|
|
3902
|
+
return typeof n === "number" && n > 0 ? n : 80;
|
|
3735
3903
|
}
|
|
3736
|
-
/**
|
|
3737
|
-
|
|
3738
|
-
|
|
3904
|
+
/** Take the terminal: alternate screen, raw keys, bracketed paste, the mouse if asked. */
|
|
3905
|
+
enter() {
|
|
3906
|
+
if (this.entered) return;
|
|
3907
|
+
this.entered = true;
|
|
3908
|
+
this.out.write(`${ALT_SCREEN_ON}${CURSOR_HIDE}${CLEAR}${PASTE_ON}${this.opts.mouse ? MOUSE_ON : ""}`);
|
|
3909
|
+
try {
|
|
3910
|
+
this.input.setRawMode?.(true);
|
|
3911
|
+
} catch {
|
|
3912
|
+
}
|
|
3913
|
+
this.input.setEncoding?.("utf8");
|
|
3914
|
+
this.input.on("data", this.onData);
|
|
3915
|
+
this.input.on("end", this.onEnd);
|
|
3916
|
+
this.input.resume();
|
|
3917
|
+
this.out.on?.("resize", this.onResize);
|
|
3918
|
+
process.once("exit", this.leave);
|
|
3739
3919
|
}
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
this.
|
|
3920
|
+
/** Give the terminal back, in the reverse order. Safe to call more than once. */
|
|
3921
|
+
leave = () => {
|
|
3922
|
+
if (!this.entered) return;
|
|
3923
|
+
this.entered = false;
|
|
3924
|
+
process.off("exit", this.leave);
|
|
3925
|
+
this.out.off?.("resize", this.onResize);
|
|
3926
|
+
this.input.off("data", this.onData);
|
|
3927
|
+
this.input.off("end", this.onEnd);
|
|
3928
|
+
try {
|
|
3929
|
+
this.input.pause();
|
|
3930
|
+
this.input.setRawMode?.(false);
|
|
3931
|
+
} catch {
|
|
3932
|
+
}
|
|
3933
|
+
this.out.write(`${this.opts.mouse ? MOUSE_OFF : ""}${PASTE_OFF}${RESET}${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
|
|
3934
|
+
this.last = null;
|
|
3935
|
+
};
|
|
3936
|
+
/**
|
|
3937
|
+
* Draw a whole frame, writing only the rows that changed.
|
|
3938
|
+
*
|
|
3939
|
+
* `frame` is read as `rows` lines; missing ones are blank, extra ones are
|
|
3940
|
+
* dropped. A size change since the last paint clears and redraws everything,
|
|
3941
|
+
* because a shorter line leaves the old one's tail where a diff cannot see it.
|
|
3942
|
+
*/
|
|
3943
|
+
paint(frame, cursor = null) {
|
|
3944
|
+
if (!this.entered) return;
|
|
3945
|
+
const rows = this.rows;
|
|
3946
|
+
const columns = this.columns;
|
|
3947
|
+
const size = `${rows}x${columns}`;
|
|
3948
|
+
let out = CURSOR_HIDE;
|
|
3949
|
+
if (!this.last || this.lastSize !== size) {
|
|
3950
|
+
out += CLEAR;
|
|
3951
|
+
this.last = new Array(rows).fill(null);
|
|
3952
|
+
this.lastSize = size;
|
|
3953
|
+
}
|
|
3954
|
+
for (let r = 0; r < rows; r++) {
|
|
3955
|
+
const line = fit(frame[r] ?? "", columns);
|
|
3956
|
+
if (this.last[r] === line) continue;
|
|
3957
|
+
this.last[r] = line;
|
|
3958
|
+
out += `${at(r, 0)}${line}${RESET}`;
|
|
3959
|
+
}
|
|
3960
|
+
if (cursor) out += `${at(Math.min(cursor.row, rows - 1), Math.min(cursor.col, columns - 1))}${CURSOR_SHOW}`;
|
|
3961
|
+
this.out.write(out);
|
|
3743
3962
|
}
|
|
3744
|
-
|
|
3745
|
-
|
|
3963
|
+
/** Forget the last frame, so the next paint redraws every row. For ctrl-l. */
|
|
3964
|
+
invalidate() {
|
|
3965
|
+
this.last = null;
|
|
3746
3966
|
}
|
|
3967
|
+
onData = (chunk) => {
|
|
3968
|
+
const keys = this.decoder.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
3969
|
+
if (keys.length) this.emit("keys", keys);
|
|
3970
|
+
};
|
|
3971
|
+
onEnd = () => {
|
|
3972
|
+
this.emit("end");
|
|
3973
|
+
};
|
|
3974
|
+
onResize = () => {
|
|
3975
|
+
this.last = null;
|
|
3976
|
+
this.emit("resize");
|
|
3977
|
+
};
|
|
3747
3978
|
};
|
|
3979
|
+
function fit(line, width) {
|
|
3980
|
+
const cut = truncate(line, width);
|
|
3981
|
+
const short = width - visibleWidth(cut);
|
|
3982
|
+
return short > 0 ? `${cut}${" ".repeat(short)}` : cut;
|
|
3983
|
+
}
|
|
3748
3984
|
|
|
3749
3985
|
// packages/tui/src/heatmap.ts
|
|
3750
3986
|
var INDENT = " ";
|
|
@@ -3903,8 +4139,8 @@ var Prompt = class {
|
|
|
3903
4139
|
this.busyLabel = null;
|
|
3904
4140
|
this.editor.reset();
|
|
3905
4141
|
this.draw();
|
|
3906
|
-
return new Promise((
|
|
3907
|
-
this.resolve =
|
|
4142
|
+
return new Promise((resolve3) => {
|
|
4143
|
+
this.resolve = resolve3;
|
|
3908
4144
|
});
|
|
3909
4145
|
}
|
|
3910
4146
|
/** Put the box in its working state, or take it out of one. */
|
|
@@ -4022,11 +4258,11 @@ var Prompt = class {
|
|
|
4022
4258
|
this.finish({ kind: "exit" });
|
|
4023
4259
|
}
|
|
4024
4260
|
finish(result) {
|
|
4025
|
-
const
|
|
4261
|
+
const resolve3 = this.resolve;
|
|
4026
4262
|
this.resolve = null;
|
|
4027
4263
|
this.label = null;
|
|
4028
4264
|
this.renderer.clear();
|
|
4029
|
-
|
|
4265
|
+
resolve3?.(result);
|
|
4030
4266
|
}
|
|
4031
4267
|
};
|
|
4032
4268
|
|
|
@@ -4227,11 +4463,11 @@ var Auth = class {
|
|
|
4227
4463
|
}
|
|
4228
4464
|
};
|
|
4229
4465
|
function sleep(ms, signal) {
|
|
4230
|
-
return new Promise((
|
|
4231
|
-
const timer = setTimeout(
|
|
4466
|
+
return new Promise((resolve3) => {
|
|
4467
|
+
const timer = setTimeout(resolve3, ms);
|
|
4232
4468
|
signal?.addEventListener("abort", () => {
|
|
4233
4469
|
clearTimeout(timer);
|
|
4234
|
-
|
|
4470
|
+
resolve3();
|
|
4235
4471
|
}, { once: true });
|
|
4236
4472
|
});
|
|
4237
4473
|
}
|
|
@@ -4271,11 +4507,18 @@ function stateDir() {
|
|
|
4271
4507
|
function projectDir(root) {
|
|
4272
4508
|
return join6(root, ".labd");
|
|
4273
4509
|
}
|
|
4274
|
-
|
|
4510
|
+
var GLOBAL_ONLY = ["serverUrl", "checkpoints", "effort"];
|
|
4511
|
+
function ignoredSettingsNote(keys) {
|
|
4512
|
+
return `ignoring ${keys.join(", ")} in .labd/settings.json - set ${keys.length > 1 ? "them" : "it"} in ~/.labd/settings.json instead`;
|
|
4513
|
+
}
|
|
4514
|
+
function loadSettings(root, overrides = {}) {
|
|
4275
4515
|
const global = read(join6(stateDir(), "settings.json"));
|
|
4276
4516
|
const project = read(join6(projectDir(root), "settings.json"));
|
|
4277
|
-
const
|
|
4278
|
-
const
|
|
4517
|
+
const settings = resolve(global, project, overrides);
|
|
4518
|
+
const ignored = GLOBAL_ONLY.filter((key) => project[key] !== void 0 && project[key] !== settings[key]);
|
|
4519
|
+
return ignored.length ? { ...settings, ignoredProjectKeys: [...ignored] } : settings;
|
|
4520
|
+
}
|
|
4521
|
+
function resolve(global, project, overrides) {
|
|
4279
4522
|
return {
|
|
4280
4523
|
// Global, then the command line. The project is deliberately not consulted:
|
|
4281
4524
|
// a checked-in file must not be able to turn approvals off for whoever runs
|
|
@@ -4286,15 +4529,14 @@ function loadSettings(root, overrides = {}, opts = {}) {
|
|
|
4286
4529
|
// settings file is not a choice, which is why this tests the parse rather
|
|
4287
4530
|
// than the presence of the key.
|
|
4288
4531
|
modeChosen: overrides.mode !== void 0 || AGENT_MODES.includes(global.mode) || global.mode === "approve",
|
|
4289
|
-
effort: overrides.effort ?? pickEffort(
|
|
4532
|
+
effort: overrides.effort ?? pickEffort(global.effort),
|
|
4290
4533
|
serverUrl: stripSlash(
|
|
4291
|
-
overrides.serverUrl ??
|
|
4534
|
+
overrides.serverUrl ?? asString(global.serverUrl) ?? process.env.LABD_SERVER_URL ?? DEFAULTS.serverUrl
|
|
4292
4535
|
),
|
|
4293
|
-
...projectServerUrl && !honourProjectUrl ? { ignoredProjectServerUrl: projectServerUrl } : {},
|
|
4294
4536
|
commandTimeoutSeconds: clampTimeout(
|
|
4295
4537
|
overrides.commandTimeoutSeconds ?? asNumber(project.commandTimeoutSeconds) ?? asNumber(global.commandTimeoutSeconds) ?? DEFAULTS.commandTimeoutSeconds
|
|
4296
4538
|
),
|
|
4297
|
-
checkpoints: overrides.checkpoints ?? asBoolean(
|
|
4539
|
+
checkpoints: overrides.checkpoints ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints,
|
|
4298
4540
|
// Screen preferences. Harmless from either file: neither changes what labd
|
|
4299
4541
|
// may do, only how the session is drawn.
|
|
4300
4542
|
mouse: overrides.mouse ?? asBoolean(project.mouse) ?? asBoolean(global.mouse) ?? DEFAULTS.mouse,
|
|
@@ -4338,6 +4580,7 @@ var BUILTIN_COMMANDS = [
|
|
|
4338
4580
|
{ name: "rename", args: "<name>", what: "name this session, so /sessions and --resume show it" },
|
|
4339
4581
|
{ name: "new", what: "start a fresh session; this one stays saved" },
|
|
4340
4582
|
{ name: "usage", what: "what's left of this month's allowance, and your activity" },
|
|
4583
|
+
{ name: "topup", what: "open the page to top up or use a saved reset" },
|
|
4341
4584
|
{ name: "trust", what: "let this project load its own extensions" },
|
|
4342
4585
|
{ name: "logout", what: "sign this machine out and leave" },
|
|
4343
4586
|
{ name: "reload", what: "reload extensions and the file list" },
|
|
@@ -4479,7 +4722,7 @@ async function createRuntime(opts) {
|
|
|
4479
4722
|
const root = opts.root;
|
|
4480
4723
|
const state = stateDir();
|
|
4481
4724
|
const trusted = isTrusted(state, root);
|
|
4482
|
-
const settings = loadSettings(root, opts.overrides
|
|
4725
|
+
const settings = loadSettings(root, opts.overrides);
|
|
4483
4726
|
const auth = new Auth(state, settings.serverUrl);
|
|
4484
4727
|
const client = new AgentClient({
|
|
4485
4728
|
baseUrl: settings.serverUrl,
|
|
@@ -4556,6 +4799,44 @@ import { basename as basename4 } from "node:path";
|
|
|
4556
4799
|
// packages/cli/src/commands.ts
|
|
4557
4800
|
import { dirname as dirname3 } from "node:path";
|
|
4558
4801
|
|
|
4802
|
+
// packages/cli/src/browser.ts
|
|
4803
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
4804
|
+
import { win32 } from "node:path";
|
|
4805
|
+
var PLAIN_URL = /^[a-z0-9.:/-]+$/;
|
|
4806
|
+
function launcherFor(platform, url, env = process.env) {
|
|
4807
|
+
const safe2 = safeTopUpUrl(url);
|
|
4808
|
+
if (!safe2 || !PLAIN_URL.test(safe2)) return null;
|
|
4809
|
+
if (platform === "darwin") return { command: "open", args: [safe2] };
|
|
4810
|
+
if (platform === "win32") {
|
|
4811
|
+
const cmd = env.ComSpec ?? win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe");
|
|
4812
|
+
return { command: cmd, args: ["/c", "start", '""', safe2] };
|
|
4813
|
+
}
|
|
4814
|
+
return { command: "xdg-open", args: [safe2] };
|
|
4815
|
+
}
|
|
4816
|
+
function openInBrowser(url) {
|
|
4817
|
+
const launcher = launcherFor(process.platform, url);
|
|
4818
|
+
return launcher ? launch(launcher) : Promise.resolve(false);
|
|
4819
|
+
}
|
|
4820
|
+
function launch(launcher) {
|
|
4821
|
+
return new Promise((resolve3) => {
|
|
4822
|
+
try {
|
|
4823
|
+
const child = spawn2(launcher.command, launcher.args, {
|
|
4824
|
+
detached: true,
|
|
4825
|
+
stdio: "ignore",
|
|
4826
|
+
// Node would quote the "" title into \"\"; cmd needs it as typed.
|
|
4827
|
+
windowsVerbatimArguments: process.platform === "win32"
|
|
4828
|
+
});
|
|
4829
|
+
child.once("error", () => resolve3(false));
|
|
4830
|
+
child.once("spawn", () => {
|
|
4831
|
+
child.unref();
|
|
4832
|
+
resolve3(true);
|
|
4833
|
+
});
|
|
4834
|
+
} catch {
|
|
4835
|
+
resolve3(false);
|
|
4836
|
+
}
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4559
4840
|
// packages/cli/src/files.ts
|
|
4560
4841
|
import { readdirSync as readdirSync6 } from "node:fs";
|
|
4561
4842
|
import { join as join8, relative as relative4 } from "node:path";
|
|
@@ -4655,6 +4936,7 @@ async function handleCommand(input, ctx) {
|
|
|
4655
4936
|
print(` ${paint.muted("Couldn't check that just now.")}`);
|
|
4656
4937
|
} else {
|
|
4657
4938
|
print(` ${paint.bold(`${credits.percentLeft}%`)} ${paint.muted("of this month left")}`);
|
|
4939
|
+
printAllowance(credits, print);
|
|
4658
4940
|
}
|
|
4659
4941
|
if (activity && !activity.days.length) {
|
|
4660
4942
|
print(` ${paint.muted("No activity yet. Ask labd something and it shows up here.")}`);
|
|
@@ -4664,7 +4946,23 @@ async function handleCommand(input, ctx) {
|
|
|
4664
4946
|
} else if (credits) {
|
|
4665
4947
|
print(` ${paint.muted("Couldn't read your activity just now.")}`);
|
|
4666
4948
|
}
|
|
4667
|
-
return {};
|
|
4949
|
+
return credits ? { credits } : {};
|
|
4950
|
+
}
|
|
4951
|
+
case "topup": {
|
|
4952
|
+
const credits = await runtime.client.credits();
|
|
4953
|
+
if (!credits) {
|
|
4954
|
+
print(` ${paint.muted("Couldn't check that just now.")}`);
|
|
4955
|
+
return {};
|
|
4956
|
+
}
|
|
4957
|
+
const url = safeTopUpUrl(credits.topUpUrl);
|
|
4958
|
+
if (!url) {
|
|
4959
|
+
print(` ${paint.muted("This server didn't send a top-up link. Top up from labd in your portal settings.")}`);
|
|
4960
|
+
return { credits };
|
|
4961
|
+
}
|
|
4962
|
+
printAllowance(credits, print);
|
|
4963
|
+
const opened = await (ctx.openUrl ?? openInBrowser)(url);
|
|
4964
|
+
print(` ${paint.muted(opened ? "Opening it in your browser." : "Open this link in your browser.")}`);
|
|
4965
|
+
return { credits };
|
|
4668
4966
|
}
|
|
4669
4967
|
case "tree": {
|
|
4670
4968
|
const nodes = runtime.session.all();
|
|
@@ -4843,6 +5141,12 @@ async function handleCommand(input, ctx) {
|
|
|
4843
5141
|
}
|
|
4844
5142
|
}
|
|
4845
5143
|
}
|
|
5144
|
+
function printAllowance(credits, print) {
|
|
5145
|
+
const facts = allowanceFacts(credits);
|
|
5146
|
+
if (facts) print(` ${paint.muted(facts)}`);
|
|
5147
|
+
const url = safeTopUpUrl(credits.topUpUrl);
|
|
5148
|
+
if (url) print(` ${paint.muted("top up or use a saved reset at")} ${paint.accent(url)}`);
|
|
5149
|
+
}
|
|
4846
5150
|
function summarise(body2) {
|
|
4847
5151
|
const text = typeof body2.text === "string" ? body2.text : "";
|
|
4848
5152
|
if (body2.kind === "tool") {
|
|
@@ -4960,7 +5264,15 @@ async function runInteractive(runtime) {
|
|
|
4960
5264
|
credits: null,
|
|
4961
5265
|
tick: 0
|
|
4962
5266
|
};
|
|
5267
|
+
const opening = runtime.client.credits();
|
|
4963
5268
|
await banner(renderer, runtime, state.mode);
|
|
5269
|
+
state.credits = await opening;
|
|
5270
|
+
if (isOutOfCredits(state.credits)) {
|
|
5271
|
+
const url = safeTopUpUrl(state.credits?.topUpUrl);
|
|
5272
|
+
renderer.print(` ${paint.red("Out of labd for this month.")}${url ? ` ${paint.muted("Top up or use a saved reset at")} ${paint.accent(url)}` : ""}`);
|
|
5273
|
+
renderer.print(` ${paint.dim("/topup opens it in your browser.")}`);
|
|
5274
|
+
renderer.print("");
|
|
5275
|
+
}
|
|
4964
5276
|
let abort = null;
|
|
4965
5277
|
const prompt = new Prompt(renderer, {
|
|
4966
5278
|
describe: (line) => describe(line, runtime),
|
|
@@ -4969,7 +5281,7 @@ async function runInteractive(runtime) {
|
|
|
4969
5281
|
project: basename4(runtime.root),
|
|
4970
5282
|
branch: runtime.branch,
|
|
4971
5283
|
contextPercent: state.context?.percentUsed ?? null,
|
|
4972
|
-
|
|
5284
|
+
credits: state.credits,
|
|
4973
5285
|
mode: state.mode,
|
|
4974
5286
|
effort: state.effort ?? null
|
|
4975
5287
|
}),
|
|
@@ -5022,6 +5334,7 @@ async function runInteractive(runtime) {
|
|
|
5022
5334
|
if (outcome.mode) state.mode = outcome.mode;
|
|
5023
5335
|
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
5024
5336
|
if (outcome.moved) state.context = null;
|
|
5337
|
+
if (outcome.credits) state.credits = outcome.credits;
|
|
5025
5338
|
continue;
|
|
5026
5339
|
}
|
|
5027
5340
|
const tags = taggedPaths(input);
|
|
@@ -5093,9 +5406,19 @@ async function runInteractive(runtime) {
|
|
|
5093
5406
|
for (const line of waiting) renderer.print(line);
|
|
5094
5407
|
}
|
|
5095
5408
|
},
|
|
5096
|
-
error(message) {
|
|
5409
|
+
error(message, info2) {
|
|
5097
5410
|
stopSpinner();
|
|
5098
|
-
|
|
5411
|
+
if (info2?.code !== "no_credits") {
|
|
5412
|
+
renderer.print(`${paint.red(" labd:")} ${message}`);
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
if (info2.credits) state.credits = info2.credits;
|
|
5416
|
+
const url = safeTopUpUrl(info2.credits?.topUpUrl);
|
|
5417
|
+
const lines = noCreditsLines({ message, credits: info2.credits, url, howToOpen: "/topup opens it in your browser.", width: renderer.contentWidth });
|
|
5418
|
+
for (const line of lines) renderer.print(line);
|
|
5419
|
+
},
|
|
5420
|
+
ready(info2) {
|
|
5421
|
+
if (info2.credits) state.credits = info2.credits;
|
|
5099
5422
|
},
|
|
5100
5423
|
done(info2) {
|
|
5101
5424
|
if (info2.context) state.context = info2.context;
|
|
@@ -5171,10 +5494,10 @@ async function banner(renderer, runtime, mode) {
|
|
|
5171
5494
|
renderer.print("");
|
|
5172
5495
|
renderer.print(`${paint.accent(FACE)} ${paint.bold("labd")} ${paint.dim(`in ${where}${runtime.branch ? ` on ${runtime.branch}` : ""}`)}`);
|
|
5173
5496
|
renderer.print(` ${paint.dim(MODE_CAPTION[mode])}`);
|
|
5174
|
-
if (runtime.settings.
|
|
5497
|
+
if (runtime.settings.ignoredProjectKeys) {
|
|
5175
5498
|
renderer.print(
|
|
5176
5499
|
` ${paint.yellow("!")} ${paint.muted(
|
|
5177
|
-
|
|
5500
|
+
ignoredSettingsNote(runtime.settings.ignoredProjectKeys)
|
|
5178
5501
|
)}`
|
|
5179
5502
|
);
|
|
5180
5503
|
}
|
|
@@ -5184,7 +5507,7 @@ async function banner(renderer, runtime, mode) {
|
|
|
5184
5507
|
renderer.print("");
|
|
5185
5508
|
}
|
|
5186
5509
|
function sleep2(ms) {
|
|
5187
|
-
return new Promise((
|
|
5510
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5188
5511
|
}
|
|
5189
5512
|
|
|
5190
5513
|
// packages/cli/src/modes/screen.ts
|
|
@@ -5297,6 +5620,14 @@ async function runScreen(runtime, opts) {
|
|
|
5297
5620
|
effort: runtime.settings.effort,
|
|
5298
5621
|
context: null,
|
|
5299
5622
|
credits: null,
|
|
5623
|
+
/** Bumped on every new reading, so a fetch that started before one does not overwrite it. */
|
|
5624
|
+
creditsVersion: 0,
|
|
5625
|
+
/** When a locked-out box last asked the server again. See shouldRefreshCredits. */
|
|
5626
|
+
creditsAskedAt: null,
|
|
5627
|
+
/** A turn was refused and no reading has arrived since: locked whatever the clock says. See boxLocked. */
|
|
5628
|
+
refusedLock: false,
|
|
5629
|
+
/** Fires when a spent allowance resets, so the box opens without a key. */
|
|
5630
|
+
resetTimer: null,
|
|
5300
5631
|
sidebar: opts.sidebar,
|
|
5301
5632
|
tick: 0,
|
|
5302
5633
|
/** What labd is doing right now, for the footer. Null when idle. */
|
|
@@ -5355,8 +5686,8 @@ async function runScreen(runtime, opts) {
|
|
|
5355
5686
|
schedule();
|
|
5356
5687
|
};
|
|
5357
5688
|
let resolveExit = null;
|
|
5358
|
-
const exited = new Promise((
|
|
5359
|
-
resolveExit =
|
|
5689
|
+
const exited = new Promise((resolve3) => {
|
|
5690
|
+
resolveExit = resolve3;
|
|
5360
5691
|
});
|
|
5361
5692
|
const quit = () => {
|
|
5362
5693
|
if (state.quitting) return;
|
|
@@ -5442,6 +5773,7 @@ async function runScreen(runtime, opts) {
|
|
|
5442
5773
|
else if (state.approval) left = `${paint.yellow("y")} ${d("allow \xB7")} ${paint.yellow("n")} ${d("decline \xB7 ctrl+c stop")}`;
|
|
5443
5774
|
else if (state.busy) left = `${paint.accent(state.busy)}${spinnerFrame(state.tick)} ${d("enter queues \xB7 ctrl+c stop \xB7 pgup/pgdn scroll")}`;
|
|
5444
5775
|
else if (state.popup) left = d("\u2191\u2193 move \xB7 tab/enter accept \xB7 esc close");
|
|
5776
|
+
else if (outOfCredits() && editor.isEmpty) left = `${paint.yellow("o")} ${d("open the top-up page \xB7 /topup \xB7 /usage \xB7 ctrl+d quit")}`;
|
|
5445
5777
|
else {
|
|
5446
5778
|
const described = describe(editor.currentLine, runtime);
|
|
5447
5779
|
left = described.hint ? described.hint : d("enter send \xB7 shift+tab mode \xB7 / commands \xB7 @ files \xB7 pgup/pgdn scroll \xB7 ctrl+c stop \xB7 ctrl+d quit");
|
|
@@ -5451,13 +5783,13 @@ async function runScreen(runtime, opts) {
|
|
|
5451
5783
|
const crowded = state.context.percentUsed >= 75;
|
|
5452
5784
|
bits.push((crowded ? paint.yellow : paint.muted)(`context ${state.context.percentUsed}%`));
|
|
5453
5785
|
}
|
|
5454
|
-
if (state.credits) bits.push(
|
|
5786
|
+
if (state.credits) bits.push(creditsReading(state.credits, `credits ${state.credits.percentLeft}%`));
|
|
5455
5787
|
return spread(` ${left}`, `${bits.join(paint.dim(" \xB7 "))} `, width);
|
|
5456
5788
|
}
|
|
5457
5789
|
function composer2(width, height) {
|
|
5458
5790
|
const inner = width - 4;
|
|
5459
5791
|
const textRows = Math.max(1, height - 2);
|
|
5460
|
-
const colour = state.approval ? paint.yellow : state.busy ? paint.accent : borderColour2(describe(editor.currentLine, runtime).state);
|
|
5792
|
+
const colour = state.approval ? paint.yellow : state.busy ? paint.accent : outOfCredits() && editor.isEmpty ? paint.red : borderColour2(describe(editor.currentLine, runtime).state);
|
|
5461
5793
|
const out = [];
|
|
5462
5794
|
out.push(colour(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`));
|
|
5463
5795
|
const view = editor.view(inner - 2, textRows);
|
|
@@ -5468,7 +5800,7 @@ async function runScreen(runtime, opts) {
|
|
|
5468
5800
|
const sigil = first ? state.busy ? paint.dim(">") : paint.accent(">") : " ";
|
|
5469
5801
|
let text = line;
|
|
5470
5802
|
if (editor.isEmpty && i === 0) {
|
|
5471
|
-
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");
|
|
5803
|
+
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") : outOfCredits() ? paint.red(OUT_OF_CREDITS_PLACEHOLDER) : paint.dim("ask labd something - / for commands - @ to tag a file");
|
|
5472
5804
|
}
|
|
5473
5805
|
out.push(`${colour("\u2502")} ${sigil} ${fit(text, inner - 2)} ${colour("\u2502")}`);
|
|
5474
5806
|
});
|
|
@@ -5554,6 +5886,10 @@ async function runScreen(runtime, opts) {
|
|
|
5554
5886
|
refreshPopup();
|
|
5555
5887
|
return schedule();
|
|
5556
5888
|
}
|
|
5889
|
+
if (spentBeforeTurn(state.credits, Date.now())) refreshWhileOut();
|
|
5890
|
+
const out = keyWhileOut(key, { out: outOfCredits(), boxEmpty: editor.isEmpty, popup: state.popup !== null, approval: state.approval !== null });
|
|
5891
|
+
if (out === "open") return void openTopUp();
|
|
5892
|
+
if (out === "drop") return refreshWhileOut();
|
|
5557
5893
|
const outcome = editor.apply(key);
|
|
5558
5894
|
if (outcome === "unhandled") {
|
|
5559
5895
|
if (key.name === "return") return submit();
|
|
@@ -5735,9 +6071,10 @@ async function runScreen(runtime, opts) {
|
|
|
5735
6071
|
mode: state.mode,
|
|
5736
6072
|
effort: state.effort,
|
|
5737
6073
|
asked: state.asked,
|
|
5738
|
-
width:
|
|
6074
|
+
width: transcriptWidth(),
|
|
5739
6075
|
print: (text) => note(text)
|
|
5740
6076
|
});
|
|
6077
|
+
if (outcome.credits) setCredits(outcome.credits);
|
|
5741
6078
|
if (outcome.farewell) state.farewell = outcome.farewell;
|
|
5742
6079
|
if (outcome.exit) return quit();
|
|
5743
6080
|
if (outcome.mode) {
|
|
@@ -5848,8 +6185,8 @@ async function runScreen(runtime, opts) {
|
|
|
5848
6185
|
state.answer = null;
|
|
5849
6186
|
const tags = taggedPaths(input);
|
|
5850
6187
|
const attachments = tags.length ? attachTagged(runtime.root, tags) : void 0;
|
|
5851
|
-
for (const
|
|
5852
|
-
note(` ${paint.yellow("!")} ${paint.muted(`${
|
|
6188
|
+
for (const refused2 of attachments?.filter((a) => a.omitted) ?? []) {
|
|
6189
|
+
note(` ${paint.yellow("!")} ${paint.muted(`${refused2.path}: ${refused2.omitted}`)}`);
|
|
5853
6190
|
}
|
|
5854
6191
|
for (const handler of runtime.extensions.onTurn) {
|
|
5855
6192
|
try {
|
|
@@ -5910,19 +6247,23 @@ async function runScreen(runtime, opts) {
|
|
|
5910
6247
|
closeAnswer();
|
|
5911
6248
|
const entry = transcript.add({ kind: "approval", call, preview, helper, mode: effectiveMode(), answer: null });
|
|
5912
6249
|
viewport.end();
|
|
5913
|
-
return new Promise((
|
|
5914
|
-
state.approval = { entry, resolve:
|
|
6250
|
+
return new Promise((resolve3) => {
|
|
6251
|
+
state.approval = { entry, resolve: resolve3 };
|
|
5915
6252
|
schedule();
|
|
5916
6253
|
});
|
|
5917
6254
|
},
|
|
5918
|
-
error(message) {
|
|
6255
|
+
error(message, info2) {
|
|
5919
6256
|
setBusy(null);
|
|
5920
|
-
|
|
6257
|
+
if (info2?.code === "no_credits") refused(message, info2);
|
|
6258
|
+
else transcript.add({ kind: "error", text: message });
|
|
5921
6259
|
schedule();
|
|
5922
6260
|
},
|
|
6261
|
+
ready(info2) {
|
|
6262
|
+
if (info2.credits) setCredits(info2.credits);
|
|
6263
|
+
},
|
|
5923
6264
|
done(info2) {
|
|
5924
6265
|
if (info2.context) state.context = info2.context;
|
|
5925
|
-
if (info2.credits)
|
|
6266
|
+
if (info2.credits) setCredits(info2.credits);
|
|
5926
6267
|
schedule();
|
|
5927
6268
|
},
|
|
5928
6269
|
commandOutput(line, key) {
|
|
@@ -5970,6 +6311,64 @@ async function runScreen(runtime, opts) {
|
|
|
5970
6311
|
setBusy(null);
|
|
5971
6312
|
}
|
|
5972
6313
|
}
|
|
6314
|
+
function outOfCredits() {
|
|
6315
|
+
return boxLocked(state.credits, Date.now(), state.refusedLock);
|
|
6316
|
+
}
|
|
6317
|
+
function setCredits(credits) {
|
|
6318
|
+
const wasSpent = state.refusedLock || isOutOfCredits(state.credits);
|
|
6319
|
+
state.credits = credits;
|
|
6320
|
+
state.creditsVersion++;
|
|
6321
|
+
state.refusedLock = false;
|
|
6322
|
+
if (wasSpent && !isOutOfCredits(credits)) note(` ${paint.green("labd is back:")} ${paint.muted(`${credits.percentLeft}% left`)}`);
|
|
6323
|
+
armResetTimer();
|
|
6324
|
+
schedule();
|
|
6325
|
+
}
|
|
6326
|
+
function armResetTimer() {
|
|
6327
|
+
if (state.resetTimer) clearTimeout(state.resetTimer);
|
|
6328
|
+
state.resetTimer = null;
|
|
6329
|
+
if (state.quitting) return;
|
|
6330
|
+
const wait = untilReset(state.credits, Date.now());
|
|
6331
|
+
if (wait === null) return;
|
|
6332
|
+
state.resetTimer = setTimeout(() => {
|
|
6333
|
+
state.resetTimer = null;
|
|
6334
|
+
fetchCredits();
|
|
6335
|
+
schedule();
|
|
6336
|
+
}, wait);
|
|
6337
|
+
state.resetTimer.unref();
|
|
6338
|
+
}
|
|
6339
|
+
function fetchCredits() {
|
|
6340
|
+
const version = state.creditsVersion;
|
|
6341
|
+
void runtime.client.credits().then((credits) => {
|
|
6342
|
+
if (credits && state.creditsVersion === version) setCredits(credits);
|
|
6343
|
+
});
|
|
6344
|
+
}
|
|
6345
|
+
function refreshWhileOut() {
|
|
6346
|
+
const now = Date.now();
|
|
6347
|
+
if (!shouldRefreshCredits(state.creditsAskedAt, now)) return;
|
|
6348
|
+
state.creditsAskedAt = now;
|
|
6349
|
+
fetchCredits();
|
|
6350
|
+
}
|
|
6351
|
+
function transcriptWidth() {
|
|
6352
|
+
return layout({ rows: screen.rows, columns: screen.columns, editorLines: editor.lineCount, sidebar: state.sidebar }).transcript.width;
|
|
6353
|
+
}
|
|
6354
|
+
function refused(message, info2) {
|
|
6355
|
+
if (info2.credits) setCredits(info2.credits);
|
|
6356
|
+
else {
|
|
6357
|
+
state.creditsVersion++;
|
|
6358
|
+
fetchCredits();
|
|
6359
|
+
}
|
|
6360
|
+
state.refusedLock = true;
|
|
6361
|
+
schedule();
|
|
6362
|
+
const url = safeTopUpUrl(state.credits?.topUpUrl);
|
|
6363
|
+
const lines = noCreditsLines({ message, credits: info2.credits, url, howToOpen: "Press o to open it in your browser, or /topup.", width: transcriptWidth() });
|
|
6364
|
+
note(lines.join("\n"));
|
|
6365
|
+
}
|
|
6366
|
+
async function openTopUp() {
|
|
6367
|
+
const url = safeTopUpUrl(state.credits?.topUpUrl);
|
|
6368
|
+
if (!url) return void serial.submit("/topup");
|
|
6369
|
+
const opened = await openInBrowser(url);
|
|
6370
|
+
note(` ${paint.muted(opened ? "Opening in your browser:" : "Open this link in your browser:")} ${paint.accent(url)}`);
|
|
6371
|
+
}
|
|
5973
6372
|
function closeAnswer() {
|
|
5974
6373
|
const answer = state.answer;
|
|
5975
6374
|
if (!answer || answer.block.done) return;
|
|
@@ -6014,10 +6413,10 @@ async function runScreen(runtime, opts) {
|
|
|
6014
6413
|
process.on("SIGHUP", onHup);
|
|
6015
6414
|
screen.enter();
|
|
6016
6415
|
if (state.sidebar) refreshProjectFacts();
|
|
6017
|
-
if (runtime.settings.
|
|
6416
|
+
if (runtime.settings.ignoredProjectKeys) {
|
|
6018
6417
|
note(
|
|
6019
6418
|
` ${paint.yellow("!")} ${paint.muted(
|
|
6020
|
-
|
|
6419
|
+
ignoredSettingsNote(runtime.settings.ignoredProjectKeys)
|
|
6021
6420
|
)}`
|
|
6022
6421
|
);
|
|
6023
6422
|
}
|
|
@@ -6036,11 +6435,13 @@ async function runScreen(runtime, opts) {
|
|
|
6036
6435
|
}
|
|
6037
6436
|
if (opts.pickSession) openSessions();
|
|
6038
6437
|
draw();
|
|
6438
|
+
fetchCredits();
|
|
6039
6439
|
try {
|
|
6040
6440
|
await exited;
|
|
6041
6441
|
if (serial.running) await Promise.race([serial.running, sleep3(1500)]);
|
|
6042
6442
|
} finally {
|
|
6043
6443
|
if (faceTimer) clearInterval(faceTimer);
|
|
6444
|
+
if (state.resetTimer) clearTimeout(state.resetTimer);
|
|
6044
6445
|
if (frameTimer) clearTimeout(frameTimer);
|
|
6045
6446
|
setBusy(null);
|
|
6046
6447
|
process.off("SIGTERM", onTerm);
|
|
@@ -6113,13 +6514,14 @@ function summarise2(body2) {
|
|
|
6113
6514
|
return text.replace(/\s+/g, " ").slice(0, 60);
|
|
6114
6515
|
}
|
|
6115
6516
|
function sleep3(ms) {
|
|
6116
|
-
return new Promise((
|
|
6517
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
6117
6518
|
}
|
|
6118
6519
|
|
|
6119
6520
|
// packages/cli/src/modes/print.ts
|
|
6120
6521
|
function toStderr(text) {
|
|
6121
6522
|
process.stderr.write(process.stderr.isTTY ? text : stripColor(text));
|
|
6122
6523
|
}
|
|
6524
|
+
var EXIT_NO_CREDITS = 3;
|
|
6123
6525
|
async function runPrint(runtime, opts) {
|
|
6124
6526
|
const question = await withStdin(opts.question);
|
|
6125
6527
|
if (!question.trim()) {
|
|
@@ -6129,6 +6531,7 @@ async function runPrint(runtime, opts) {
|
|
|
6129
6531
|
}
|
|
6130
6532
|
let failed = false;
|
|
6131
6533
|
let declined = false;
|
|
6534
|
+
let outOfCredits = false;
|
|
6132
6535
|
let answer = "";
|
|
6133
6536
|
const emit = (event) => {
|
|
6134
6537
|
if (opts.json) process.stdout.write(`${JSON.stringify(event)}
|
|
@@ -6164,9 +6567,10 @@ async function runPrint(runtime, opts) {
|
|
|
6164
6567
|
}
|
|
6165
6568
|
if (step.status === "skipped") declined = true;
|
|
6166
6569
|
},
|
|
6167
|
-
error(message) {
|
|
6570
|
+
error(message, info2) {
|
|
6168
6571
|
failed = true;
|
|
6169
|
-
if (
|
|
6572
|
+
if (info2?.code === "no_credits") outOfCredits = true;
|
|
6573
|
+
if (opts.json) emit({ type: "error", message, ...info2 });
|
|
6170
6574
|
else toStderr(`${paint.red("labd:")} ${message}
|
|
6171
6575
|
`);
|
|
6172
6576
|
},
|
|
@@ -6195,6 +6599,7 @@ async function runPrint(runtime, opts) {
|
|
|
6195
6599
|
});
|
|
6196
6600
|
if (!opts.json && answer && !answer.endsWith("\n")) process.stdout.write("\n");
|
|
6197
6601
|
await closeConversation(runtime);
|
|
6602
|
+
if (outOfCredits) return EXIT_NO_CREDITS;
|
|
6198
6603
|
if (failed) return 1;
|
|
6199
6604
|
if (declined) return 2;
|
|
6200
6605
|
return result.text.trim() ? 0 : 1;
|
|
@@ -6247,11 +6652,11 @@ async function runRpc(runtime) {
|
|
|
6247
6652
|
return;
|
|
6248
6653
|
}
|
|
6249
6654
|
if (req.method === void 0 && req.id !== void 0 && req.id !== null) {
|
|
6250
|
-
const
|
|
6251
|
-
if (
|
|
6655
|
+
const resolve3 = pending.get(Number(req.id));
|
|
6656
|
+
if (resolve3) {
|
|
6252
6657
|
pending.delete(Number(req.id));
|
|
6253
6658
|
const result = req.result;
|
|
6254
|
-
|
|
6659
|
+
resolve3(result?.approved === true);
|
|
6255
6660
|
}
|
|
6256
6661
|
return;
|
|
6257
6662
|
}
|
|
@@ -6327,8 +6732,8 @@ async function runRpc(runtime) {
|
|
|
6327
6732
|
output: step.output
|
|
6328
6733
|
});
|
|
6329
6734
|
},
|
|
6330
|
-
error(message) {
|
|
6331
|
-
notify("labd/error", { message });
|
|
6735
|
+
error(message, info2) {
|
|
6736
|
+
notify("labd/error", { message, ...info2 });
|
|
6332
6737
|
},
|
|
6333
6738
|
done(info2) {
|
|
6334
6739
|
notify("labd/done", info2);
|
|
@@ -6343,15 +6748,15 @@ async function runRpc(runtime) {
|
|
|
6343
6748
|
* asleep.
|
|
6344
6749
|
*/
|
|
6345
6750
|
approve(call, preview, helper) {
|
|
6346
|
-
return new Promise((
|
|
6347
|
-
if (closed) return
|
|
6751
|
+
return new Promise((resolve3) => {
|
|
6752
|
+
if (closed) return resolve3(false);
|
|
6348
6753
|
const id = nextId++;
|
|
6349
6754
|
const timer = setTimeout(() => {
|
|
6350
|
-
if (pending.delete(id))
|
|
6755
|
+
if (pending.delete(id)) resolve3(false);
|
|
6351
6756
|
}, APPROVAL_TIMEOUT_MS);
|
|
6352
6757
|
pending.set(id, (approved) => {
|
|
6353
6758
|
clearTimeout(timer);
|
|
6354
|
-
|
|
6759
|
+
resolve3(approved);
|
|
6355
6760
|
});
|
|
6356
6761
|
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview, ...helper ? { helper } : {} } });
|
|
6357
6762
|
});
|
|
@@ -6376,14 +6781,14 @@ async function runRpc(runtime) {
|
|
|
6376
6781
|
mode: unattendedMode(runtime.settings),
|
|
6377
6782
|
root: runtime.root
|
|
6378
6783
|
});
|
|
6379
|
-
await new Promise((
|
|
6784
|
+
await new Promise((resolve3) => process.stdin.on("end", resolve3));
|
|
6380
6785
|
await busy;
|
|
6381
6786
|
await closeConversation(runtime);
|
|
6382
6787
|
return 0;
|
|
6383
6788
|
}
|
|
6384
6789
|
|
|
6385
6790
|
// packages/cli/src/index.ts
|
|
6386
|
-
var VERSION = true ? "0.
|
|
6791
|
+
var VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
6387
6792
|
async function main(argv) {
|
|
6388
6793
|
const args = parse(argv);
|
|
6389
6794
|
if (args.color !== void 0) setColor(args.color);
|
|
@@ -6396,7 +6801,7 @@ async function main(argv) {
|
|
|
6396
6801
|
`);
|
|
6397
6802
|
return 0;
|
|
6398
6803
|
}
|
|
6399
|
-
const root =
|
|
6804
|
+
const root = resolve2(args.cwd ?? process.cwd());
|
|
6400
6805
|
switch (args.command) {
|
|
6401
6806
|
case "login":
|
|
6402
6807
|
return login(root);
|
|
@@ -6443,7 +6848,7 @@ async function main(argv) {
|
|
|
6443
6848
|
return runScreen(runtime, { mouse: runtime.settings.mouse, sidebar: runtime.settings.sidebar, pickSession: args.pickSession });
|
|
6444
6849
|
}
|
|
6445
6850
|
async function login(root) {
|
|
6446
|
-
const settings = loadSettings(root
|
|
6851
|
+
const settings = loadSettings(root);
|
|
6447
6852
|
const auth = new Auth(stateDir(), settings.serverUrl);
|
|
6448
6853
|
const say = (text) => process.stderr.write(text);
|
|
6449
6854
|
say(`
|
|
@@ -6474,7 +6879,7 @@ ${paint.red("labd:")} ${result.reason ?? "that did not complete"}.
|
|
|
6474
6879
|
return 0;
|
|
6475
6880
|
}
|
|
6476
6881
|
async function logout(root) {
|
|
6477
|
-
const settings = loadSettings(root
|
|
6882
|
+
const settings = loadSettings(root);
|
|
6478
6883
|
const hadStored = await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
6479
6884
|
for (const line of signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n")) {
|
|
6480
6885
|
process.stderr.write(` ${paint.muted(line)}
|
|
@@ -6504,7 +6909,7 @@ function trustHere(root) {
|
|
|
6504
6909
|
trust(stateDir(), root);
|
|
6505
6910
|
process.stdout.write(` ${paint.bold("trusted")} ${paint.muted(root)}
|
|
6506
6911
|
`);
|
|
6507
|
-
process.stdout.write(` ${paint.dim("Extensions in .labd/extensions
|
|
6912
|
+
process.stdout.write(` ${paint.dim("Extensions in .labd/extensions will now load here.")}
|
|
6508
6913
|
`);
|
|
6509
6914
|
return 0;
|
|
6510
6915
|
}
|