@thedevlabs-io/labd 0.6.1 → 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 +1948 -1313
- package/dist/labd.js.map +4 -4
- package/package.json +1 -1
package/dist/labd.js
CHANGED
|
@@ -1,7 +1,120 @@
|
|
|
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
|
+
|
|
6
|
+
// vendor/labd/packages/protocol/src/activity.ts
|
|
7
|
+
var ACTIVITY_WEEKS = 53;
|
|
8
|
+
function utcOffsetMinutes(at2 = /* @__PURE__ */ new Date()) {
|
|
9
|
+
return -at2.getTimezoneOffset();
|
|
10
|
+
}
|
|
11
|
+
var DAY_MS = 864e5;
|
|
12
|
+
function localDate(at2 = /* @__PURE__ */ new Date()) {
|
|
13
|
+
const pad4 = (n) => String(n).padStart(2, "0");
|
|
14
|
+
return `${at2.getFullYear()}-${pad4(at2.getMonth() + 1)}-${pad4(at2.getDate())}`;
|
|
15
|
+
}
|
|
16
|
+
function dayNumber(date) {
|
|
17
|
+
return Math.floor(Date.parse(`${date}T00:00:00Z`) / DAY_MS);
|
|
18
|
+
}
|
|
19
|
+
function dateOf(day) {
|
|
20
|
+
return new Date(day * DAY_MS).toISOString().slice(0, 10);
|
|
21
|
+
}
|
|
22
|
+
function summariseActivity(activity, weeks, today = localDate()) {
|
|
23
|
+
const span = Math.max(1, Math.min(ACTIVITY_WEEKS, Math.floor(weeks)));
|
|
24
|
+
const last = dayNumber(today);
|
|
25
|
+
const monday = last - (last + 3) % 7;
|
|
26
|
+
const first = monday - (span - 1) * 7;
|
|
27
|
+
const earliest = monday - (ACTIVITY_WEEKS - 1) * 7;
|
|
28
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
29
|
+
const perSurface = /* @__PURE__ */ new Map();
|
|
30
|
+
const totals = { turns: 0, input: 0, output: 0, activeDays: 0 };
|
|
31
|
+
for (const bucket of activity.days) {
|
|
32
|
+
const day = dayNumber(bucket.day);
|
|
33
|
+
if (!(day >= earliest && day <= last)) continue;
|
|
34
|
+
const tokens = bucket.input + bucket.output;
|
|
35
|
+
const d = perDay.get(day) ?? { turns: 0, tokens: 0 };
|
|
36
|
+
d.turns += bucket.turns;
|
|
37
|
+
d.tokens += tokens;
|
|
38
|
+
perDay.set(day, d);
|
|
39
|
+
if (day < first) continue;
|
|
40
|
+
const s = perSurface.get(bucket.surface) ?? { turns: 0, tokens: 0 };
|
|
41
|
+
s.turns += bucket.turns;
|
|
42
|
+
s.tokens += tokens;
|
|
43
|
+
perSurface.set(bucket.surface, s);
|
|
44
|
+
totals.turns += bucket.turns;
|
|
45
|
+
totals.input += bucket.input;
|
|
46
|
+
totals.output += bucket.output;
|
|
47
|
+
}
|
|
48
|
+
let run = 0;
|
|
49
|
+
let longest = 0;
|
|
50
|
+
for (let day = earliest; day <= last; day++) {
|
|
51
|
+
if ((perDay.get(day)?.turns ?? 0) > 0) {
|
|
52
|
+
run += 1;
|
|
53
|
+
longest = Math.max(longest, run);
|
|
54
|
+
} else if (day !== last) {
|
|
55
|
+
run = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const shown = [...perDay.entries()].filter(([day]) => day >= first).map(([, d]) => d.tokens);
|
|
59
|
+
const ranked = [...new Set(shown.filter((t) => t > 0))].sort((a, b) => a - b);
|
|
60
|
+
const days = [];
|
|
61
|
+
let busiest = null;
|
|
62
|
+
for (let day = first; day <= last; day++) {
|
|
63
|
+
const d = perDay.get(day) ?? { turns: 0, tokens: 0 };
|
|
64
|
+
const entry = { date: dateOf(day), turns: d.turns, tokens: d.tokens, level: levelFor(d.tokens, ranked) };
|
|
65
|
+
days.push(entry);
|
|
66
|
+
if (d.turns > 0) totals.activeDays += 1;
|
|
67
|
+
if (d.tokens > 0 && (!busiest || d.tokens > busiest.tokens)) busiest = entry;
|
|
68
|
+
}
|
|
69
|
+
const hours = activity.hours.length === 24 ? activity.hours : [];
|
|
70
|
+
const peak = Math.max(0, ...hours);
|
|
71
|
+
return {
|
|
72
|
+
days,
|
|
73
|
+
totals,
|
|
74
|
+
streak: { current: run, longest },
|
|
75
|
+
peakHour: peak > 0 ? hours.indexOf(peak) : null,
|
|
76
|
+
busiest,
|
|
77
|
+
surfaces: [...perSurface.entries()].map(([surface, s]) => ({ surface, ...s })).sort((a, b) => b.tokens - a.tokens || a.surface.localeCompare(b.surface))
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function levelFor(tokens, ranked) {
|
|
81
|
+
if (!(tokens > 0)) return 0;
|
|
82
|
+
if (ranked.length === 1) return 4;
|
|
83
|
+
return 1 + Math.round(3 * ranked.indexOf(tokens) / (ranked.length - 1));
|
|
84
|
+
}
|
|
85
|
+
function formatTokens(n) {
|
|
86
|
+
const units = ["", "k", "M", "B"];
|
|
87
|
+
let value = n;
|
|
88
|
+
let unit = 0;
|
|
89
|
+
while (value >= 999.5 && unit < units.length - 1) {
|
|
90
|
+
value /= 1e3;
|
|
91
|
+
unit += 1;
|
|
92
|
+
}
|
|
93
|
+
const shown = unit === 0 || value >= 10 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/, "");
|
|
94
|
+
return `${shown}${units[unit]}`;
|
|
95
|
+
}
|
|
96
|
+
function surfaceLabel(surface) {
|
|
97
|
+
switch (surface) {
|
|
98
|
+
case "web":
|
|
99
|
+
return "portal";
|
|
100
|
+
case "vscode":
|
|
101
|
+
return "editor";
|
|
102
|
+
case "cli":
|
|
103
|
+
return "terminal";
|
|
104
|
+
case "memory":
|
|
105
|
+
return "memory (background)";
|
|
106
|
+
case "api":
|
|
107
|
+
return "API";
|
|
108
|
+
case "help":
|
|
109
|
+
return "plans & billing help";
|
|
110
|
+
default:
|
|
111
|
+
return surface;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function formatHour(hour) {
|
|
115
|
+
const h = hour % 12 === 0 ? 12 : hour % 12;
|
|
116
|
+
return `${h}${hour < 12 ? "am" : "pm"}`;
|
|
117
|
+
}
|
|
5
118
|
|
|
6
119
|
// vendor/labd/packages/protocol/src/tags.ts
|
|
7
120
|
var TAG = /(^|[\s([])@(?:"([^"\n]+)"|([^\s"]+))/g;
|
|
@@ -75,7 +188,7 @@ function rankPaths(query, paths, limit = 12) {
|
|
|
75
188
|
}
|
|
76
189
|
|
|
77
190
|
// vendor/labd/packages/protocol/src/workspace.ts
|
|
78
|
-
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;
|
|
79
192
|
var EXCLUDED_DIRS = [
|
|
80
193
|
".git",
|
|
81
194
|
"node_modules",
|
|
@@ -90,6 +203,7 @@ var OFF_LIMITS = new RegExp(
|
|
|
90
203
|
`(^|/)(${EXCLUDED_DIRS.map((d) => d.replace(/\./g, "\\.")).join("|")})(/|$)`,
|
|
91
204
|
"i"
|
|
92
205
|
);
|
|
206
|
+
var EDITOR_SETTINGS = /(^|\/)\.vscode\/(settings|tasks|launch|mcp)\.json$|(^|\/)\.devcontainer(\/|$)|(^|\/)\.devcontainer\.json$|\.code-workspace$/i;
|
|
93
207
|
var EXCLUDED_GLOB = `**/{${EXCLUDED_DIRS.join(",")}}/**`;
|
|
94
208
|
var ASSIGNMENT = /^(\s*(?:export\s+)?["']?[\w.-]+["']?\s*[:=]\s*)(.+)$/;
|
|
95
209
|
function redactValues(text) {
|
|
@@ -182,8 +296,12 @@ function hasControlChars(command) {
|
|
|
182
296
|
}
|
|
183
297
|
var INTERPRETERS = /^(sh|bash|zsh|fish|dash|ksh|python[\d.]*|node|deno|bun|ruby|perl|php|osascript)$/;
|
|
184
298
|
var DOWNLOADERS = /^(curl|wget|fetch|httpie|http)$/;
|
|
185
|
-
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_]*=/;
|
|
186
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.";
|
|
187
305
|
var REFUSALS = [
|
|
188
306
|
{
|
|
189
307
|
// Everything after this runs as a different user, which puts it outside
|
|
@@ -251,9 +369,17 @@ var REFUSALS = [
|
|
|
251
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"),
|
|
252
370
|
reason: "it waits for input or never exits, so there would be no output to read. Ask for a command that finishes."
|
|
253
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
|
+
},
|
|
254
380
|
{
|
|
255
381
|
test: (argv) => namesASecret(argv),
|
|
256
|
-
reason:
|
|
382
|
+
reason: SECRET_REASON
|
|
257
383
|
},
|
|
258
384
|
{
|
|
259
385
|
test: (argv) => argv.length === 1 && /^(env|printenv|set)$/.test(argv[0] ?? ""),
|
|
@@ -274,9 +400,13 @@ function isBigTarget(t) {
|
|
|
274
400
|
}
|
|
275
401
|
function unwrap(argv) {
|
|
276
402
|
let out = argv;
|
|
277
|
-
|
|
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;
|
|
278
406
|
out = out.slice(1);
|
|
279
|
-
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
|
+
}
|
|
280
410
|
}
|
|
281
411
|
return out.length ? [bare(out[0]), ...out.slice(1)] : out;
|
|
282
412
|
}
|
|
@@ -309,14 +439,115 @@ function pipesNetworkIntoShell(argv) {
|
|
|
309
439
|
return downloader && interpreter;
|
|
310
440
|
}
|
|
311
441
|
function namesASecret(argv) {
|
|
312
|
-
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);
|
|
313
544
|
}
|
|
314
545
|
function bare(arg) {
|
|
315
546
|
const unquoted = arg.replace(/^["']|["']$/g, "");
|
|
316
547
|
const slash = Math.max(unquoted.lastIndexOf("/"), unquoted.lastIndexOf("\\"));
|
|
317
548
|
return slash === -1 ? unquoted : unquoted.slice(slash + 1);
|
|
318
549
|
}
|
|
319
|
-
function segments(command) {
|
|
550
|
+
function segments(command, literalBackslash = false) {
|
|
320
551
|
const out = [];
|
|
321
552
|
let argv = [];
|
|
322
553
|
let token = "";
|
|
@@ -341,7 +572,7 @@ function segments(command) {
|
|
|
341
572
|
quote = c;
|
|
342
573
|
continue;
|
|
343
574
|
}
|
|
344
|
-
if (c === "\\" && i + 1 < command.length) {
|
|
575
|
+
if (c === "\\" && i + 1 < command.length && !literalBackslash) {
|
|
345
576
|
token += command[++i];
|
|
346
577
|
continue;
|
|
347
578
|
}
|
|
@@ -359,6 +590,20 @@ function segments(command) {
|
|
|
359
590
|
argv.push(c);
|
|
360
591
|
continue;
|
|
361
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
|
+
}
|
|
362
607
|
if (c === "&") {
|
|
363
608
|
const standalone = token === "" && (i === 0 || command[i - 1] === " " || command[i - 1] === " ");
|
|
364
609
|
if (standalone) argv.push("&");
|
|
@@ -391,6 +636,12 @@ function classifyCommand(command) {
|
|
|
391
636
|
}
|
|
392
637
|
for (const segment of segments(raw)) {
|
|
393
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
|
+
}
|
|
394
645
|
if (argv.includes("&")) {
|
|
395
646
|
return {
|
|
396
647
|
ok: false,
|
|
@@ -408,6 +659,13 @@ function classifyCommand(command) {
|
|
|
408
659
|
}
|
|
409
660
|
}
|
|
410
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
|
+
}
|
|
411
669
|
return { ok: true };
|
|
412
670
|
}
|
|
413
671
|
|
|
@@ -502,6 +760,21 @@ function forExtraction(messages) {
|
|
|
502
760
|
};
|
|
503
761
|
});
|
|
504
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;
|
|
505
778
|
|
|
506
779
|
// packages/core/src/agent.ts
|
|
507
780
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -1270,7 +1543,7 @@ async function runCommand(input, ctx) {
|
|
|
1270
1543
|
].join("\n");
|
|
1271
1544
|
}
|
|
1272
1545
|
function execute(command, ctx) {
|
|
1273
|
-
return new Promise((
|
|
1546
|
+
return new Promise((resolve3) => {
|
|
1274
1547
|
const windows = process.platform === "win32";
|
|
1275
1548
|
const shell = windows ? process.env.ComSpec || "cmd.exe" : process.env.SHELL || "/bin/bash";
|
|
1276
1549
|
const args = windows ? ["/d", "/s", "/c", command] : ["-lc", command];
|
|
@@ -1325,7 +1598,7 @@ ${tail}` : head + tail;
|
|
|
1325
1598
|
if (settled) return;
|
|
1326
1599
|
settled = true;
|
|
1327
1600
|
clearTimeout(timer);
|
|
1328
|
-
|
|
1601
|
+
resolve3({ code, output: output(), timedOut });
|
|
1329
1602
|
};
|
|
1330
1603
|
child.on("close", done);
|
|
1331
1604
|
child.on("error", (err) => {
|
|
@@ -1555,13 +1828,14 @@ async function runTurn(question, opts) {
|
|
|
1555
1828
|
case "need_tools":
|
|
1556
1829
|
needTools = true;
|
|
1557
1830
|
return "go";
|
|
1831
|
+
case "ready":
|
|
1832
|
+
ui.ready?.({ credits: event.credits });
|
|
1833
|
+
return "go";
|
|
1558
1834
|
case "done":
|
|
1559
|
-
result.context = event.context;
|
|
1560
|
-
result.credits = event.credits;
|
|
1561
1835
|
ui.done?.({ credits: event.credits, context: event.context });
|
|
1562
1836
|
return "go";
|
|
1563
1837
|
case "error":
|
|
1564
|
-
ui.error(event.message);
|
|
1838
|
+
ui.error(event.message, { code: event.code, ...event.credits ? { credits: event.credits } : {} });
|
|
1565
1839
|
return "stop";
|
|
1566
1840
|
default:
|
|
1567
1841
|
return "go";
|
|
@@ -1991,9 +2265,14 @@ var AgentClient = class {
|
|
|
1991
2265
|
if (this.opts.token) headers.Authorization = `Bearer ${this.opts.token}`;
|
|
1992
2266
|
return headers;
|
|
1993
2267
|
}
|
|
1994
|
-
|
|
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) {
|
|
1995
2274
|
try {
|
|
1996
|
-
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) });
|
|
1997
2276
|
if (!res.ok) return null;
|
|
1998
2277
|
const body2 = await res.json();
|
|
1999
2278
|
return body2.credits ?? null;
|
|
@@ -2001,6 +2280,18 @@ var AgentClient = class {
|
|
|
2001
2280
|
return null;
|
|
2002
2281
|
}
|
|
2003
2282
|
}
|
|
2283
|
+
/** The learner's usage by local hour, for the heatmap under `/usage`. */
|
|
2284
|
+
async activity(offsetMinutes) {
|
|
2285
|
+
try {
|
|
2286
|
+
const res = await this.fetch(`${this.opts.baseUrl}/v1/usage/activity?offset=${offsetMinutes}`, { headers: this.headers() });
|
|
2287
|
+
if (!res.ok) return null;
|
|
2288
|
+
const body2 = await res.json();
|
|
2289
|
+
if (!Array.isArray(body2.days) || !Array.isArray(body2.hours)) return null;
|
|
2290
|
+
return { days: body2.days.filter(isBucket), hours: body2.hours.map((n) => isCount(n) ? n : 0) };
|
|
2291
|
+
} catch {
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2004
2295
|
/**
|
|
2005
2296
|
* Tell the server this conversation is over and can be distilled into memory.
|
|
2006
2297
|
*
|
|
@@ -2048,11 +2339,16 @@ var AgentClient = class {
|
|
|
2048
2339
|
}
|
|
2049
2340
|
if (!res.ok || !res.body) {
|
|
2050
2341
|
let message = `The assistant is unreachable (${res.status}).`;
|
|
2342
|
+
let body2 = {};
|
|
2051
2343
|
try {
|
|
2052
|
-
|
|
2344
|
+
body2 = await res.json();
|
|
2053
2345
|
if (body2.message) message = body2.message;
|
|
2054
2346
|
} catch {
|
|
2055
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
|
+
}
|
|
2056
2352
|
if (res.status === 401) message = `${message} Run \`labd login\`.`;
|
|
2057
2353
|
yield { type: "error", code: res.status === 401 ? "unauthorized" : "upstream", message };
|
|
2058
2354
|
return;
|
|
@@ -2078,6 +2374,13 @@ var AgentClient = class {
|
|
|
2078
2374
|
}
|
|
2079
2375
|
}
|
|
2080
2376
|
};
|
|
2377
|
+
function isBucket(value) {
|
|
2378
|
+
const b = value;
|
|
2379
|
+
return typeof b?.day === "string" && typeof b.surface === "string" && [b.turns, b.input, b.output].every(isCount);
|
|
2380
|
+
}
|
|
2381
|
+
function isCount(n) {
|
|
2382
|
+
return typeof n === "number" && Number.isFinite(n);
|
|
2383
|
+
}
|
|
2081
2384
|
|
|
2082
2385
|
// packages/tui/src/theme.ts
|
|
2083
2386
|
var ESC = "\x1B";
|
|
@@ -2103,6 +2406,14 @@ var paint = {
|
|
|
2103
2406
|
* Closes the foreground only, so it can sit inside a `band` without ending it.
|
|
2104
2407
|
*/
|
|
2105
2408
|
accent: wrap("38;5;214", "39"),
|
|
2409
|
+
/**
|
|
2410
|
+
* The heatmap's lower steps, walking the same orange down the cube towards
|
|
2411
|
+
* brown, with `accent` as the top one. The heatmap changes glyph with each
|
|
2412
|
+
* step too, so a light terminal where brown reads heavier is still legible.
|
|
2413
|
+
*/
|
|
2414
|
+
heat1: wrap("38;5;94", "39"),
|
|
2415
|
+
heat2: wrap("38;5;130", "39"),
|
|
2416
|
+
heat3: wrap("38;5;172", "39"),
|
|
2106
2417
|
bold: wrap("1", "22"),
|
|
2107
2418
|
dim: wrap("2", "22"),
|
|
2108
2419
|
muted: wrap("90"),
|
|
@@ -2246,1370 +2557,1502 @@ function visibleWidth(line) {
|
|
|
2246
2557
|
return line.replace(new RegExp(`${ESC2}\\[[0-9;]*m`, "g"), "").length;
|
|
2247
2558
|
}
|
|
2248
2559
|
|
|
2249
|
-
// packages/tui/src/
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
const
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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;
|
|
2271
2584
|
}
|
|
2585
|
+
out.push(...prose(line, opts.width, pad4));
|
|
2272
2586
|
}
|
|
2273
|
-
return out;
|
|
2587
|
+
return out.join("\n");
|
|
2274
2588
|
}
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
create_file: "Create",
|
|
2281
|
-
move_file: "Move",
|
|
2282
|
-
delete_file: "Delete",
|
|
2283
|
-
run_command: "Run",
|
|
2284
|
-
spawn_agent: "Helper"
|
|
2285
|
-
};
|
|
2286
|
-
function approvalCard(preview, call, mode, width) {
|
|
2287
|
-
const out = [""];
|
|
2288
|
-
if (call.name === "run_command" || preview?.kind === "run") {
|
|
2289
|
-
const command = preview?.kind === "run" ? preview.command : String(call.input.command ?? "");
|
|
2290
|
-
const why = preview?.kind === "run" ? preview.why : call.input.why;
|
|
2291
|
-
out.push(`${paint.yellow("?")} ${paint.bold("Run a command")}`);
|
|
2292
|
-
if (why) out.push(` ${paint.muted(why)}`);
|
|
2293
|
-
out.push("");
|
|
2294
|
-
for (const line of wrapCommand(command, width - 6)) out.push(` ${paint.cyan(line)}`);
|
|
2295
|
-
out.push("");
|
|
2296
|
-
out.push(
|
|
2297
|
-
` ${paint.dim(
|
|
2298
|
-
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."
|
|
2299
|
-
)}`
|
|
2300
|
-
);
|
|
2301
|
-
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)))}`];
|
|
2302
2594
|
}
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
const destination = preview?.to ?? call.input.to;
|
|
2308
|
-
const title = moving ? "Move" : deleting ? "Delete" : creating ? "Create" : "Edit";
|
|
2309
|
-
out.push(
|
|
2310
|
-
`${paint.yellow("?")} ${paint.bold(title)} ${paint.accent(path)}${moving && destination ? paint.muted(" \u2192 ") + paint.accent(destination) : ""}${preview ? paint.muted(
|
|
2311
|
-
` ${counts(preview.added, preview.removed)}${creating || moving || deleting ? "" : ` at line ${preview.startLine}`}`
|
|
2312
|
-
) : ""}`
|
|
2313
|
-
);
|
|
2314
|
-
if (preview) {
|
|
2315
|
-
out.push("");
|
|
2316
|
-
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]} `);
|
|
2317
2599
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
);
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
if (
|
|
2329
|
-
|
|
2330
|
-
return parts.join(" ");
|
|
2331
|
-
}
|
|
2332
|
-
function diffLines(oldText, newText, width, max = 12) {
|
|
2333
|
-
const out = [];
|
|
2334
|
-
const before = oldText ? oldText.split("\n") : [];
|
|
2335
|
-
const after = newText ? newText.split("\n") : [];
|
|
2336
|
-
for (const line of before.slice(0, max)) out.push(paint.red(`- ${clip(line, width - 4)}`));
|
|
2337
|
-
if (before.length > max) out.push(paint.dim(` ... ${before.length - max} more removed`));
|
|
2338
|
-
for (const line of after.slice(0, max)) out.push(paint.green(`+ ${clip(line, width - 4)}`));
|
|
2339
|
-
if (after.length > max) out.push(paint.dim(` ... ${after.length - max} more added`));
|
|
2340
|
-
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);
|
|
2341
2612
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
+
);
|
|
2344
2625
|
}
|
|
2345
|
-
function
|
|
2346
|
-
|
|
2626
|
+
function wrap2(text, width, first, rest) {
|
|
2627
|
+
const limit = Math.max(20, width - visibleWidth(first));
|
|
2628
|
+
if (visibleWidth(text) <= limit) return [`${first}${text}`];
|
|
2347
2629
|
const out = [];
|
|
2348
2630
|
let line = "";
|
|
2349
2631
|
for (const word of text.split(" ")) {
|
|
2350
|
-
if (
|
|
2632
|
+
if (line && visibleWidth(line) + 1 + visibleWidth(word) > limit) {
|
|
2633
|
+
out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
2351
2634
|
line = word;
|
|
2352
|
-
} else if (`${line} ${word}`.length <= width) {
|
|
2353
|
-
line = `${line} ${word}`;
|
|
2354
2635
|
} else {
|
|
2355
|
-
|
|
2356
|
-
line = word;
|
|
2357
|
-
}
|
|
2358
|
-
while (line.length > width) {
|
|
2359
|
-
out.push(line.slice(0, width));
|
|
2360
|
-
line = line.slice(width);
|
|
2636
|
+
line = line ? `${line} ${word}` : word;
|
|
2361
2637
|
}
|
|
2362
2638
|
}
|
|
2363
|
-
if (line) out.push(line);
|
|
2364
|
-
return out;
|
|
2365
|
-
}
|
|
2366
|
-
function wrapCommand(command, width) {
|
|
2367
|
-
if (command.length <= width) return [command];
|
|
2368
|
-
const out = [];
|
|
2369
|
-
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}`);
|
|
2370
2640
|
return out;
|
|
2371
2641
|
}
|
|
2372
|
-
function statusLine(info2) {
|
|
2373
|
-
const bits = [];
|
|
2374
|
-
if (info2.contextPercent !== null && info2.contextPercent !== void 0) {
|
|
2375
|
-
const crowded = info2.contextPercent >= 75;
|
|
2376
|
-
bits.push((crowded ? paint.yellow : paint.muted)(`${info2.contextPercent}% full`));
|
|
2377
|
-
}
|
|
2378
|
-
bits.push(paint.muted(info2.project));
|
|
2379
|
-
if (info2.branch) bits.push(paint.muted(info2.branch));
|
|
2380
|
-
if (info2.creditsPercent !== null && info2.creditsPercent !== void 0) {
|
|
2381
|
-
bits.push(paint.muted(`${info2.creditsPercent}% left`));
|
|
2382
|
-
}
|
|
2383
|
-
if (info2.effort) bits.push(paint.muted(`think ${info2.effort}`));
|
|
2384
|
-
const mode = info2.mode === "agent" ? paint.red(paint.bold("agent")) : info2.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
2385
|
-
bits.push(mode);
|
|
2386
|
-
const left = info2.busy ? `${paint.accent(FACE)} ${paint.dim(info2.busy)}` : paint.dim(FACE);
|
|
2387
|
-
return `${left} ${paint.dim("|")} ${bits.join(paint.dim(" | "))}`;
|
|
2388
|
-
}
|
|
2389
|
-
function spinnerFrame(tick) {
|
|
2390
|
-
const dots = ".".repeat(tick % 3 + 1).padEnd(3);
|
|
2391
|
-
return paint.dim(dots);
|
|
2392
|
-
}
|
|
2393
2642
|
|
|
2394
|
-
// packages/tui/src/
|
|
2395
|
-
var
|
|
2396
|
-
var
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
out.push(`${colour(ROUND.v)} ${body(opts, inner)} ${colour(ROUND.v)}`);
|
|
2405
|
-
out.push(colour(`${ROUND.bl}${ROUND.h.repeat(width - 2)}${ROUND.br}`));
|
|
2406
|
-
if (opts.hint) out.push(` ${truncate(opts.hint, width - 2)}`);
|
|
2407
|
-
if (opts.status) out.push(opts.status);
|
|
2408
|
-
return out;
|
|
2409
|
-
}
|
|
2410
|
-
function body(opts, inner) {
|
|
2411
|
-
const sigil = opts.state === "busy" ? paint.dim(">") : paint.accent(">");
|
|
2412
|
-
const room = inner - 2;
|
|
2413
|
-
if (!opts.view.text && opts.placeholder && opts.state !== "busy") {
|
|
2414
|
-
const text2 = truncate(paint.dim(opts.placeholder), room);
|
|
2415
|
-
return `${sigil} ${pad(text2, room)}`;
|
|
2416
|
-
}
|
|
2417
|
-
const text = withCursor(opts.view, room, opts.state === "busy");
|
|
2418
|
-
return `${sigil} ${pad(text, room)}`;
|
|
2419
|
-
}
|
|
2420
|
-
function withCursor(view, room, hidden) {
|
|
2421
|
-
const text = view.text.slice(0, room);
|
|
2422
|
-
if (hidden) return text;
|
|
2423
|
-
const at2 = Math.min(view.column, room - 1 < 0 ? 0 : room);
|
|
2424
|
-
const before = text.slice(0, at2);
|
|
2425
|
-
const under = text.slice(at2, at2 + 1) || " ";
|
|
2426
|
-
const after = text.slice(at2 + 1);
|
|
2427
|
-
return `${before}${paint.invert(under)}${after}`;
|
|
2428
|
-
}
|
|
2429
|
-
function suggestionLines(all, inner) {
|
|
2430
|
-
if (!all.length) return [];
|
|
2431
|
-
const shown = all.slice(0, MAX_SUGGESTIONS);
|
|
2432
|
-
const column = Math.max(...shown.map((s) => s.name.length + 1));
|
|
2433
|
-
const out = shown.map((s, i) => {
|
|
2434
|
-
const name = `/${s.name}`.padEnd(column + 1);
|
|
2435
|
-
const mark = i === 0 ? paint.accent("-") : " ";
|
|
2436
|
-
return ` ${mark} ${paint.accent(name)} ${paint.muted(truncate(s.what, inner - column - 4))}`;
|
|
2437
|
-
});
|
|
2438
|
-
if (all.length > shown.length) {
|
|
2439
|
-
out.push(` ${paint.dim(`... ${all.length - shown.length} more`)}`);
|
|
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;
|
|
2440
2653
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
function borderColour(state) {
|
|
2444
|
-
switch (state) {
|
|
2445
|
-
case "ok":
|
|
2446
|
-
return paint.green;
|
|
2447
|
-
case "unknown":
|
|
2448
|
-
return paint.red;
|
|
2449
|
-
case "partial":
|
|
2450
|
-
return paint.cyan;
|
|
2451
|
-
case "ask":
|
|
2452
|
-
return paint.yellow;
|
|
2453
|
-
case "busy":
|
|
2454
|
-
return paint.accent;
|
|
2455
|
-
default:
|
|
2456
|
-
return paint.dim;
|
|
2654
|
+
get blocks() {
|
|
2655
|
+
return this.entries;
|
|
2457
2656
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
// packages/tui/src/edit.ts
|
|
2465
|
-
function sanitize(text) {
|
|
2466
|
-
return text.replace(/\r\n?|\n|\t/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
2467
|
-
}
|
|
2468
|
-
var PRINTABLE = /^[^\u0000-\u001f\u007f]+$/;
|
|
2469
|
-
var segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2470
|
-
function boundaries(text) {
|
|
2471
|
-
const out = [0];
|
|
2472
|
-
for (const { index, segment } of segmenter.segment(text)) out.push(index + segment.length);
|
|
2473
|
-
return out;
|
|
2474
|
-
}
|
|
2475
|
-
function prevBoundary(text, at2) {
|
|
2476
|
-
const marks = boundaries(text);
|
|
2477
|
-
let last = 0;
|
|
2478
|
-
for (const mark of marks) {
|
|
2479
|
-
if (mark >= at2) return last;
|
|
2480
|
-
last = mark;
|
|
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;
|
|
2481
2662
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
return i;
|
|
2493
|
-
}
|
|
2494
|
-
function wordEnd(text, at2) {
|
|
2495
|
-
let i = at2;
|
|
2496
|
-
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2497
|
-
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
2498
|
-
return i;
|
|
2499
|
-
}
|
|
2500
|
-
var LineEditor = class {
|
|
2501
|
-
value = "";
|
|
2502
|
-
at = 0;
|
|
2503
|
-
get text() {
|
|
2504
|
-
return this.value;
|
|
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);
|
|
2505
2673
|
}
|
|
2506
|
-
/**
|
|
2507
|
-
|
|
2508
|
-
|
|
2674
|
+
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
2675
|
+
touch(entry) {
|
|
2676
|
+
entry.version++;
|
|
2509
2677
|
}
|
|
2510
|
-
/**
|
|
2511
|
-
|
|
2512
|
-
this.
|
|
2513
|
-
this.
|
|
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();
|
|
2514
2683
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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);
|
|
2693
|
+
}
|
|
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;
|
|
2518
2697
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
this.
|
|
2524
|
-
|
|
2525
|
-
return "text";
|
|
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;
|
|
2526
2704
|
}
|
|
2527
2705
|
/**
|
|
2528
|
-
*
|
|
2706
|
+
* An answer, streaming or finished.
|
|
2529
2707
|
*
|
|
2530
|
-
*
|
|
2531
|
-
*
|
|
2532
|
-
*
|
|
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.
|
|
2533
2711
|
*/
|
|
2534
|
-
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
if (
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
if (
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
if (ctrl && name === "w") return this.cut(wordStart(this.value, this.at), this.at);
|
|
2548
|
-
if (name === "backspace") return this.cut(prevBoundary(this.value, this.at), this.at);
|
|
2549
|
-
if (name === "delete") return this.cut(this.at, nextBoundary(this.value, this.at));
|
|
2550
|
-
if (ctrl && name === "u") return this.cut(0, this.at);
|
|
2551
|
-
if (ctrl && name === "k") return this.cut(this.at, this.value.length);
|
|
2552
|
-
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) {
|
|
2553
|
-
return this.insert(key.sequence);
|
|
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);
|
|
2554
2725
|
}
|
|
2555
|
-
|
|
2556
|
-
return
|
|
2726
|
+
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
2727
|
+
return [...head.lines, ...raw];
|
|
2557
2728
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2729
|
+
};
|
|
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);
|
|
2562
2741
|
}
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
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))}`)];
|
|
2596
2786
|
}
|
|
2597
|
-
this.index = -1;
|
|
2598
|
-
this.draft = "";
|
|
2599
2787
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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;
|
|
2608
2798
|
} else {
|
|
2609
|
-
|
|
2799
|
+
line = line ? `${line} ${word}` : word;
|
|
2610
2800
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
if (this.index === -1) return null;
|
|
2615
|
-
if (this.index >= this.entries.length - 1) {
|
|
2616
|
-
this.index = -1;
|
|
2617
|
-
return this.draft;
|
|
2801
|
+
while (line.length > limit) {
|
|
2802
|
+
out.push(line.slice(0, limit));
|
|
2803
|
+
line = line.slice(limit);
|
|
2618
2804
|
}
|
|
2619
|
-
this.index++;
|
|
2620
|
-
return this.entries[this.index];
|
|
2621
2805
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
// packages/tui/src/editor.ts
|
|
2625
|
-
function sanitizeMultiline(text) {
|
|
2626
|
-
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;
|
|
2627
2808
|
}
|
|
2628
|
-
var
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
/** 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
|
+
}
|
|
2633
2813
|
top = 0;
|
|
2634
|
-
|
|
2635
|
-
|
|
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);
|
|
2636
2821
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
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));
|
|
2639
2828
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
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();
|
|
2642
2836
|
}
|
|
2643
|
-
|
|
2644
|
-
|
|
2837
|
+
pageUp() {
|
|
2838
|
+
this.scroll(-Math.max(1, this.height - 1));
|
|
2645
2839
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
return this.lines[this.row];
|
|
2840
|
+
pageDown() {
|
|
2841
|
+
this.scroll(Math.max(1, this.height - 1));
|
|
2649
2842
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
this.
|
|
2653
|
-
this.row = this.lines.length - 1;
|
|
2654
|
-
this.col = this.lines[this.row].length;
|
|
2843
|
+
home() {
|
|
2844
|
+
this.top = 0;
|
|
2845
|
+
if (this.total > this.height) this.unfollow();
|
|
2655
2846
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
this.
|
|
2659
|
-
this.col = this.lines[this.row].length;
|
|
2847
|
+
end() {
|
|
2848
|
+
this.refollow();
|
|
2849
|
+
this.top = Math.max(0, this.total - this.height);
|
|
2660
2850
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
this.
|
|
2664
|
-
this.col = 0;
|
|
2665
|
-
this.top = 0;
|
|
2851
|
+
/** The slice of `lines` on screen. */
|
|
2852
|
+
slice(lines) {
|
|
2853
|
+
return lines.slice(this.top, this.top + this.height);
|
|
2666
2854
|
}
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
if (!clean) return "same";
|
|
2671
|
-
const parts = clean.split("\n");
|
|
2672
|
-
const line = this.lines[this.row];
|
|
2673
|
-
const before = line.slice(0, this.col);
|
|
2674
|
-
const after = line.slice(this.col);
|
|
2675
|
-
if (parts.length === 1) {
|
|
2676
|
-
this.lines[this.row] = before + parts[0] + after;
|
|
2677
|
-
this.col += parts[0].length;
|
|
2678
|
-
return "text";
|
|
2679
|
-
}
|
|
2680
|
-
const inserted = [before + parts[0], ...parts.slice(1, -1), parts[parts.length - 1] + after];
|
|
2681
|
-
this.lines.splice(this.row, 1, ...inserted);
|
|
2682
|
-
this.row += parts.length - 1;
|
|
2683
|
-
this.col = parts[parts.length - 1].length;
|
|
2684
|
-
return "text";
|
|
2855
|
+
unfollow() {
|
|
2856
|
+
if (this.follow) this.anchor = this.total;
|
|
2857
|
+
this.follow = false;
|
|
2685
2858
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2859
|
+
refollow() {
|
|
2860
|
+
this.follow = true;
|
|
2688
2861
|
}
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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`)}`);
|
|
2706
2949
|
}
|
|
2707
|
-
if (ctrl && name === "j") return this.newline();
|
|
2708
|
-
if (name === "up" && !ctrl && !meta) {
|
|
2709
|
-
if (this.row === 0) return "unhandled";
|
|
2710
|
-
this.row--;
|
|
2711
|
-
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2712
|
-
return "cursor";
|
|
2713
|
-
}
|
|
2714
|
-
if (name === "down" && !ctrl && !meta) {
|
|
2715
|
-
if (this.row === this.lines.length - 1) return "unhandled";
|
|
2716
|
-
this.row++;
|
|
2717
|
-
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2718
|
-
return "cursor";
|
|
2719
|
-
}
|
|
2720
|
-
if (name === "left" && !ctrl && !meta) {
|
|
2721
|
-
if (this.col === 0) {
|
|
2722
|
-
if (this.row === 0) return "same";
|
|
2723
|
-
this.row--;
|
|
2724
|
-
this.col = this.lines[this.row].length;
|
|
2725
|
-
return "cursor";
|
|
2726
|
-
}
|
|
2727
|
-
return this.moveTo(prevBoundary(line, this.col));
|
|
2728
|
-
}
|
|
2729
|
-
if (name === "right" && !ctrl && !meta) {
|
|
2730
|
-
if (this.col >= line.length) {
|
|
2731
|
-
if (this.row === this.lines.length - 1) return "same";
|
|
2732
|
-
this.row++;
|
|
2733
|
-
this.col = 0;
|
|
2734
|
-
return "cursor";
|
|
2735
|
-
}
|
|
2736
|
-
return this.moveTo(nextBoundary(line, this.col));
|
|
2737
|
-
}
|
|
2738
|
-
if (name === "home" || ctrl && name === "a") return this.moveTo(0);
|
|
2739
|
-
if (name === "end" || ctrl && name === "e") return this.moveTo(line.length);
|
|
2740
|
-
if ((meta || ctrl) && (name === "left" || name === "b")) return this.moveTo(wordStart(line, this.col));
|
|
2741
|
-
if ((meta || ctrl) && (name === "right" || name === "f")) return this.moveTo(wordEnd(line, this.col));
|
|
2742
|
-
if (name === "backspace" && (meta || ctrl)) {
|
|
2743
|
-
if (this.col === 0) return this.joinUp();
|
|
2744
|
-
return this.cut(wordStart(line, this.col), this.col);
|
|
2745
|
-
}
|
|
2746
|
-
if (ctrl && name === "w") {
|
|
2747
|
-
if (this.col === 0) return this.joinUp();
|
|
2748
|
-
return this.cut(wordStart(line, this.col), this.col);
|
|
2749
|
-
}
|
|
2750
|
-
if (name === "backspace") {
|
|
2751
|
-
if (this.col === 0) return this.joinUp();
|
|
2752
|
-
return this.cut(prevBoundary(line, this.col), this.col);
|
|
2753
|
-
}
|
|
2754
|
-
if (name === "delete") {
|
|
2755
|
-
if (this.col >= line.length) return this.joinDown();
|
|
2756
|
-
return this.cut(this.col, nextBoundary(line, this.col));
|
|
2757
|
-
}
|
|
2758
|
-
if (ctrl && name === "u") return this.cut(0, this.col);
|
|
2759
|
-
if (ctrl && name === "k") {
|
|
2760
|
-
if (this.col >= line.length) return this.joinDown();
|
|
2761
|
-
return this.cut(this.col, line.length);
|
|
2762
|
-
}
|
|
2763
|
-
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) return this.insert(key.sequence);
|
|
2764
|
-
return "unhandled";
|
|
2765
|
-
}
|
|
2766
|
-
/**
|
|
2767
|
-
* The lines to draw and where the caret is among them.
|
|
2768
|
-
*
|
|
2769
|
-
* At most `maxLines` rows, chosen so the caret's row is always one of them;
|
|
2770
|
-
* each row scrolls horizontally on its own so a long line never wraps into the
|
|
2771
|
-
* row below and the caret never leaves the box.
|
|
2772
|
-
*/
|
|
2773
|
-
view(width, maxLines) {
|
|
2774
|
-
const rows = Math.max(1, maxLines);
|
|
2775
|
-
if (this.row < this.top) this.top = this.row;
|
|
2776
|
-
if (this.row >= this.top + rows) this.top = this.row - rows + 1;
|
|
2777
|
-
this.top = Math.max(0, Math.min(this.top, this.lines.length - rows));
|
|
2778
|
-
const shown = this.lines.slice(this.top, this.top + rows);
|
|
2779
|
-
let cursorCol = this.col;
|
|
2780
|
-
const lines = shown.map((line, i) => {
|
|
2781
|
-
if (width <= 0) return "";
|
|
2782
|
-
if (line.length <= width) return line;
|
|
2783
|
-
if (this.top + i !== this.row) return line.slice(0, width);
|
|
2784
|
-
const start = Math.max(0, this.col - width + 1);
|
|
2785
|
-
cursorCol = this.col - start;
|
|
2786
|
-
return line.slice(start, start + width);
|
|
2787
|
-
});
|
|
2788
|
-
return { lines, cursor: { row: this.row - this.top, col: cursorCol } };
|
|
2789
|
-
}
|
|
2790
|
-
moveTo(next) {
|
|
2791
|
-
if (next === this.col) return "same";
|
|
2792
|
-
this.col = next;
|
|
2793
|
-
return "cursor";
|
|
2794
2950
|
}
|
|
2795
|
-
|
|
2796
|
-
if (from >= to) return "same";
|
|
2797
|
-
const line = this.lines[this.row];
|
|
2798
|
-
this.lines[this.row] = line.slice(0, from) + line.slice(to);
|
|
2799
|
-
this.col = from;
|
|
2800
|
-
return "text";
|
|
2801
|
-
}
|
|
2802
|
-
joinUp() {
|
|
2803
|
-
if (this.row === 0) return "same";
|
|
2804
|
-
const above = this.lines[this.row - 1];
|
|
2805
|
-
this.lines.splice(this.row - 1, 2, above + this.lines[this.row]);
|
|
2806
|
-
this.row--;
|
|
2807
|
-
this.col = above.length;
|
|
2808
|
-
return "text";
|
|
2809
|
-
}
|
|
2810
|
-
joinDown() {
|
|
2811
|
-
if (this.row >= this.lines.length - 1) return "same";
|
|
2812
|
-
this.lines.splice(this.row, 2, this.lines[this.row] + this.lines[this.row + 1]);
|
|
2813
|
-
return "text";
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
2816
|
-
|
|
2817
|
-
// packages/tui/src/keys.ts
|
|
2818
|
-
var ESC3 = "\x1B";
|
|
2819
|
-
var PASTE_START = `${ESC3}[200~`;
|
|
2820
|
-
var PASTE_END = `${ESC3}[201~`;
|
|
2821
|
-
var CSI_FINAL = {
|
|
2822
|
-
A: "up",
|
|
2823
|
-
B: "down",
|
|
2824
|
-
C: "right",
|
|
2825
|
-
D: "left",
|
|
2826
|
-
H: "home",
|
|
2827
|
-
F: "end",
|
|
2828
|
-
Z: "tab"
|
|
2829
|
-
};
|
|
2830
|
-
var TILDE = {
|
|
2831
|
-
"1": "home",
|
|
2832
|
-
"2": "insert",
|
|
2833
|
-
"3": "delete",
|
|
2834
|
-
"4": "end",
|
|
2835
|
-
"5": "pageup",
|
|
2836
|
-
"6": "pagedown",
|
|
2837
|
-
"7": "home",
|
|
2838
|
-
"8": "end"
|
|
2839
|
-
};
|
|
2840
|
-
function modifiers(param) {
|
|
2841
|
-
const n = (Number(param) || 1) - 1;
|
|
2842
|
-
return {
|
|
2843
|
-
...n & 1 ? { shift: true } : {},
|
|
2844
|
-
...n & 2 ? { meta: true } : {},
|
|
2845
|
-
...n & 4 ? { ctrl: true } : {}
|
|
2846
|
-
};
|
|
2847
|
-
}
|
|
2848
|
-
function coded(code, mods, sequence) {
|
|
2849
|
-
switch (code) {
|
|
2850
|
-
case 13:
|
|
2851
|
-
return { name: "return", ...mods, sequence };
|
|
2852
|
-
case 9:
|
|
2853
|
-
return { name: "tab", ...mods, sequence };
|
|
2854
|
-
case 27:
|
|
2855
|
-
return { name: "escape", ...mods, sequence };
|
|
2856
|
-
case 127:
|
|
2857
|
-
case 8:
|
|
2858
|
-
return { name: "backspace", ...mods, sequence };
|
|
2859
|
-
default:
|
|
2860
|
-
if (code >= 32) {
|
|
2861
|
-
const ch = String.fromCodePoint(code);
|
|
2862
|
-
return { name: ch.toLowerCase(), ...mods, sequence: mods.ctrl || mods.meta ? sequence : ch };
|
|
2863
|
-
}
|
|
2864
|
-
return null;
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
function isPrefix(text, whole) {
|
|
2868
|
-
return text.length <= whole.length && whole.startsWith(text);
|
|
2869
|
-
}
|
|
2870
|
-
function partialMarker(buf, marker) {
|
|
2871
|
-
for (let n = Math.min(marker.length - 1, buf.length); n > 0; n--) {
|
|
2872
|
-
if (isPrefix(buf.slice(buf.length - n), marker)) return n;
|
|
2873
|
-
}
|
|
2874
|
-
return 0;
|
|
2951
|
+
return out;
|
|
2875
2952
|
}
|
|
2876
|
-
var
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
out.push({ name: "paste", paste: this.pasting.replace(/\r\n?/g, "\n") });
|
|
2903
|
-
this.pasting = null;
|
|
2904
|
-
i = end + PASTE_END.length;
|
|
2905
|
-
continue;
|
|
2906
|
-
}
|
|
2907
|
-
const ch = buf[i];
|
|
2908
|
-
if (ch !== ESC3) {
|
|
2909
|
-
const code2 = ch.charCodeAt(0);
|
|
2910
|
-
if (code2 === 13) out.push({ name: "return", sequence: ch });
|
|
2911
|
-
else if (code2 === 10) out.push({ name: "j", ctrl: true, sequence: ch });
|
|
2912
|
-
else if (code2 === 9) out.push({ name: "tab", sequence: ch });
|
|
2913
|
-
else if (code2 === 127 || code2 === 8) out.push({ name: "backspace", sequence: ch });
|
|
2914
|
-
else if (code2 < 32) {
|
|
2915
|
-
if (code2 > 0) out.push({ name: String.fromCharCode(96 + code2), ctrl: true, sequence: ch });
|
|
2916
|
-
} else {
|
|
2917
|
-
let j = i + 1;
|
|
2918
|
-
while (j < buf.length) {
|
|
2919
|
-
const c = buf.charCodeAt(j);
|
|
2920
|
-
if (c < 32 || c === 127 || buf[j] === ESC3) break;
|
|
2921
|
-
j++;
|
|
2922
|
-
}
|
|
2923
|
-
out.push({ sequence: buf.slice(i, j) });
|
|
2924
|
-
i = j;
|
|
2925
|
-
continue;
|
|
2926
|
-
}
|
|
2927
|
-
i++;
|
|
2928
|
-
continue;
|
|
2929
|
-
}
|
|
2930
|
-
if (i === buf.length - 1) {
|
|
2931
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
2932
|
-
i++;
|
|
2933
|
-
continue;
|
|
2934
|
-
}
|
|
2935
|
-
const next = buf[i + 1];
|
|
2936
|
-
if (next === "[") {
|
|
2937
|
-
let j = i + 2;
|
|
2938
|
-
while (j < buf.length && !(buf.charCodeAt(j) >= 64 && buf.charCodeAt(j) <= 126)) j++;
|
|
2939
|
-
if (j >= buf.length) {
|
|
2940
|
-
this.pending = buf.slice(i);
|
|
2941
|
-
return out;
|
|
2942
|
-
}
|
|
2943
|
-
const seq = buf.slice(i, j + 1);
|
|
2944
|
-
const body2 = buf.slice(i + 2, j);
|
|
2945
|
-
const final = buf[j];
|
|
2946
|
-
i = j + 1;
|
|
2947
|
-
if (seq === PASTE_START) {
|
|
2948
|
-
this.pasting = "";
|
|
2949
|
-
continue;
|
|
2950
|
-
}
|
|
2951
|
-
const key = decodeCsi(body2, final, seq);
|
|
2952
|
-
if (key) out.push(key);
|
|
2953
|
-
continue;
|
|
2954
|
-
}
|
|
2955
|
-
if (next === "O") {
|
|
2956
|
-
const final = buf[i + 2];
|
|
2957
|
-
if (final === void 0) {
|
|
2958
|
-
this.pending = buf.slice(i);
|
|
2959
|
-
return out;
|
|
2960
|
-
}
|
|
2961
|
-
const name = CSI_FINAL[final];
|
|
2962
|
-
if (name) out.push({ name, sequence: buf.slice(i, i + 3) });
|
|
2963
|
-
i += 3;
|
|
2964
|
-
continue;
|
|
2965
|
-
}
|
|
2966
|
-
if (next === "\x7F" || next === "\b") {
|
|
2967
|
-
out.push({ name: "backspace", meta: true, sequence: buf.slice(i, i + 2) });
|
|
2968
|
-
i += 2;
|
|
2969
|
-
continue;
|
|
2970
|
-
}
|
|
2971
|
-
if (next === ESC3) {
|
|
2972
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
2973
|
-
i++;
|
|
2974
|
-
continue;
|
|
2975
|
-
}
|
|
2976
|
-
const code = next.charCodeAt(0);
|
|
2977
|
-
if (code >= 32 && code !== 127) {
|
|
2978
|
-
out.push({
|
|
2979
|
-
name: next.toLowerCase(),
|
|
2980
|
-
meta: true,
|
|
2981
|
-
...next !== next.toLowerCase() ? { shift: true } : {},
|
|
2982
|
-
sequence: buf.slice(i, i + 2)
|
|
2983
|
-
});
|
|
2984
|
-
i += 2;
|
|
2985
|
-
continue;
|
|
2986
|
-
}
|
|
2987
|
-
out.push({ name: "escape", sequence: ESC3 });
|
|
2988
|
-
i++;
|
|
2989
|
-
}
|
|
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
|
+
);
|
|
2990
2979
|
return out;
|
|
2991
2980
|
}
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
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}`);
|
|
3003
2995
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
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;
|
|
3003
|
+
}
|
|
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);
|
|
3013
3039
|
}
|
|
3014
|
-
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
3015
3040
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3041
|
+
if (line) out.push(line);
|
|
3042
|
+
return out;
|
|
3043
|
+
}
|
|
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;
|
|
3049
|
+
}
|
|
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`));
|
|
3022
3055
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
if (
|
|
3026
|
-
|
|
3027
|
-
|
|
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(" | "))}`;
|
|
3063
|
+
}
|
|
3064
|
+
function spinnerFrame(tick) {
|
|
3065
|
+
const dots = ".".repeat(tick % 3 + 1).padEnd(3);
|
|
3066
|
+
return paint.dim(dots);
|
|
3028
3067
|
}
|
|
3029
3068
|
|
|
3030
|
-
// packages/tui/src/
|
|
3031
|
-
var
|
|
3032
|
-
var
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
const
|
|
3036
|
-
const
|
|
3037
|
-
const
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
const mainWidth = withSidebar ? columns - SIDEBAR_WIDTH - 1 : columns;
|
|
3045
|
-
const out = {
|
|
3046
|
-
header,
|
|
3047
|
-
transcript: { top: 1, left: 0, width: mainWidth, height: transcriptHeight },
|
|
3048
|
-
composer: { ...composer2, width: mainWidth },
|
|
3049
|
-
footer
|
|
3050
|
-
};
|
|
3051
|
-
if (withSidebar) {
|
|
3052
|
-
out.sidebar = { top: 1, left: mainWidth + 1, width: SIDEBAR_WIDTH, height: composer2.top + composerHeight - 1 };
|
|
3053
|
-
}
|
|
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);
|
|
3054
3083
|
return out;
|
|
3055
3084
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
this.items = items;
|
|
3063
|
-
}
|
|
3064
|
-
selected = 0;
|
|
3065
|
-
get current() {
|
|
3066
|
-
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)}`;
|
|
3067
3091
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
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`)}`);
|
|
3071
3115
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
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;
|
|
3088
3132
|
}
|
|
3089
|
-
}
|
|
3090
|
-
function
|
|
3133
|
+
}
|
|
3134
|
+
function pad(text, width) {
|
|
3091
3135
|
const short = width - visibleWidth(text);
|
|
3092
3136
|
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3093
3137
|
}
|
|
3094
|
-
|
|
3095
|
-
// packages/tui/src/
|
|
3096
|
-
function
|
|
3097
|
-
return
|
|
3138
|
+
|
|
3139
|
+
// packages/tui/src/edit.ts
|
|
3140
|
+
function sanitize(text) {
|
|
3141
|
+
return text.replace(/\r\n?|\n|\t/g, " ").replace(/[\u0000-\u001f\u007f]/g, "");
|
|
3142
|
+
}
|
|
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;
|
|
3149
|
+
}
|
|
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;
|
|
3156
|
+
}
|
|
3157
|
+
return last;
|
|
3158
|
+
}
|
|
3159
|
+
function nextBoundary(text, at2) {
|
|
3160
|
+
for (const mark of boundaries(text)) if (mark > at2) return mark;
|
|
3161
|
+
return text.length;
|
|
3098
3162
|
}
|
|
3099
|
-
function
|
|
3100
|
-
|
|
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;
|
|
3101
3168
|
}
|
|
3102
|
-
function
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
return words.every((w) => hay.includes(w));
|
|
3108
|
-
});
|
|
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;
|
|
3109
3174
|
}
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
else if (key.name === "pageup") dialog.scroll = Math.max(0, dialog.scroll - 10);
|
|
3116
|
-
else if (key.name === "pagedown") dialog.scroll += 10;
|
|
3117
|
-
else if (key.name === "home") dialog.scroll = 0;
|
|
3118
|
-
else if (key.name === "return" || key.name === "q") return "close";
|
|
3119
|
-
else if (key.mouse?.kind === "wheel-up") dialog.scroll = Math.max(0, dialog.scroll - 3);
|
|
3120
|
-
else if (key.mouse?.kind === "wheel-down") dialog.scroll += 3;
|
|
3121
|
-
else return "same";
|
|
3122
|
-
return "changed";
|
|
3175
|
+
var LineEditor = class {
|
|
3176
|
+
value = "";
|
|
3177
|
+
at = 0;
|
|
3178
|
+
get text() {
|
|
3179
|
+
return this.value;
|
|
3123
3180
|
}
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
return "changed";
|
|
3181
|
+
/** Cursor position, as an offset into `text`, always on a grapheme boundary. */
|
|
3182
|
+
get cursor() {
|
|
3183
|
+
return this.at;
|
|
3128
3184
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
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;
|
|
3132
3189
|
}
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
dialog.onSelect(item);
|
|
3137
|
-
return "select";
|
|
3190
|
+
reset() {
|
|
3191
|
+
this.value = "";
|
|
3192
|
+
this.at = 0;
|
|
3138
3193
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
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";
|
|
3144
3201
|
}
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3202
|
+
/**
|
|
3203
|
+
* Apply one key.
|
|
3204
|
+
*
|
|
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.
|
|
3208
|
+
*/
|
|
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));
|
|
3217
|
+
}
|
|
3218
|
+
if ((meta || ctrl) && (name === "right" || name === "f")) {
|
|
3219
|
+
return this.moveTo(wordEnd(this.value, this.at));
|
|
3220
|
+
}
|
|
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";
|
|
3149
3232
|
}
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
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 };
|
|
3257
|
+
}
|
|
3258
|
+
};
|
|
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();
|
|
3170
3271
|
}
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
if (
|
|
3177
|
-
|
|
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;
|
|
3178
3285
|
}
|
|
3286
|
+
return this.entries[this.index];
|
|
3179
3287
|
}
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
}
|
|
3191
|
-
function pad3(text, width) {
|
|
3192
|
-
const short = width - visibleWidth(text);
|
|
3193
|
-
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3194
|
-
}
|
|
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
|
+
};
|
|
3195
3298
|
|
|
3196
|
-
// packages/tui/src/
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
var
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
this.
|
|
3214
|
-
this.input = input;
|
|
3215
|
-
this.opts = opts;
|
|
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, "");
|
|
3302
|
+
}
|
|
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");
|
|
3311
|
+
}
|
|
3312
|
+
get cursor() {
|
|
3313
|
+
return { row: this.row, col: this.col };
|
|
3314
|
+
}
|
|
3315
|
+
get lineCount() {
|
|
3316
|
+
return this.lines.length;
|
|
3216
3317
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
entered = false;
|
|
3220
|
-
decoder = new KeyDecoder();
|
|
3221
|
-
get rows() {
|
|
3222
|
-
const n = this.opts.rows ?? this.out.rows;
|
|
3223
|
-
return typeof n === "number" && n > 0 ? n : 24;
|
|
3318
|
+
get isEmpty() {
|
|
3319
|
+
return this.lines.length === 1 && this.lines[0] === "";
|
|
3224
3320
|
}
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
return
|
|
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];
|
|
3228
3324
|
}
|
|
3229
|
-
/**
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
this.
|
|
3233
|
-
this.
|
|
3234
|
-
try {
|
|
3235
|
-
this.input.setRawMode?.(true);
|
|
3236
|
-
} catch {
|
|
3237
|
-
}
|
|
3238
|
-
this.input.setEncoding?.("utf8");
|
|
3239
|
-
this.input.on("data", this.onData);
|
|
3240
|
-
this.input.on("end", this.onEnd);
|
|
3241
|
-
this.input.resume();
|
|
3242
|
-
this.out.on?.("resize", this.onResize);
|
|
3243
|
-
process.once("exit", this.leave);
|
|
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;
|
|
3244
3330
|
}
|
|
3245
|
-
/**
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
this.
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
this.
|
|
3252
|
-
this.
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
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";
|
|
3257
3354
|
}
|
|
3258
|
-
|
|
3259
|
-
this.
|
|
3260
|
-
|
|
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
|
+
}
|
|
3261
3364
|
/**
|
|
3262
|
-
*
|
|
3263
|
-
*
|
|
3264
|
-
*
|
|
3265
|
-
* dropped. A size change since the last paint clears and redraws everything,
|
|
3266
|
-
* because a shorter line leaves the old one's tail where a diff cannot see it.
|
|
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.
|
|
3267
3368
|
*/
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
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";
|
|
3278
3381
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
if (this.
|
|
3282
|
-
this.
|
|
3283
|
-
|
|
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";
|
|
3284
3388
|
}
|
|
3285
|
-
if (
|
|
3286
|
-
|
|
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";
|
|
3287
3440
|
}
|
|
3288
|
-
/**
|
|
3289
|
-
|
|
3290
|
-
|
|
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 } };
|
|
3291
3464
|
}
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
this.
|
|
3301
|
-
this.
|
|
3302
|
-
|
|
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"
|
|
3303
3514
|
};
|
|
3304
|
-
function
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
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
|
+
};
|
|
3308
3522
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
} else {
|
|
3325
|
-
inFence = false;
|
|
3326
|
-
fenceLang = "";
|
|
3327
|
-
out.push("");
|
|
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 };
|
|
3328
3538
|
}
|
|
3329
|
-
|
|
3330
|
-
}
|
|
3331
|
-
if (inFence) {
|
|
3332
|
-
out.push(`${paint.dim(`${pad4}|`)} ${line}`);
|
|
3333
|
-
continue;
|
|
3334
|
-
}
|
|
3335
|
-
out.push(...prose(line, opts.width, pad4));
|
|
3539
|
+
return null;
|
|
3336
3540
|
}
|
|
3337
|
-
return out.join("\n");
|
|
3338
3541
|
}
|
|
3339
|
-
function
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
3346
|
-
if (bullet) {
|
|
3347
|
-
const indent2 = `${pad4}${bullet[1]}${paint.accent("-")} `;
|
|
3348
|
-
return wrap2(inline(bullet[2]), width, indent2, `${pad4}${bullet[1]} `);
|
|
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;
|
|
3349
3548
|
}
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
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;
|
|
3354
3666
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
const
|
|
3358
|
-
|
|
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;
|
|
3359
3678
|
}
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
});
|
|
3370
|
-
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})`)}`);
|
|
3371
|
-
return held.replace(
|
|
3372
|
-
new RegExp(`${HOLD}(\\d+)${HOLD}`, "g"),
|
|
3373
|
-
(_, n) => paint.cyan(code[Number(n)] ?? "")
|
|
3374
|
-
);
|
|
3375
|
-
}
|
|
3376
|
-
function wrap2(text, width, first, rest) {
|
|
3377
|
-
const limit = Math.max(20, width - visibleWidth(first));
|
|
3378
|
-
if (visibleWidth(text) <= limit) return [`${first}${text}`];
|
|
3379
|
-
const out = [];
|
|
3380
|
-
let line = "";
|
|
3381
|
-
for (const word of text.split(" ")) {
|
|
3382
|
-
if (line && visibleWidth(line) + 1 + visibleWidth(word) > limit) {
|
|
3383
|
-
out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
3384
|
-
line = word;
|
|
3385
|
-
} else {
|
|
3386
|
-
line = line ? `${line} ${word}` : word;
|
|
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 } };
|
|
3387
3688
|
}
|
|
3689
|
+
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
3690
|
+
}
|
|
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 };
|
|
3703
|
+
}
|
|
3704
|
+
|
|
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 };
|
|
3388
3728
|
}
|
|
3389
|
-
if (line) out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
3390
3729
|
return out;
|
|
3391
3730
|
}
|
|
3392
3731
|
|
|
3393
|
-
// packages/tui/src/
|
|
3394
|
-
var
|
|
3395
|
-
var
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
/** For a streaming answer: the rendered COMPLETE part, so only the tail is re-rendered per chunk. */
|
|
3400
|
-
prose = /* @__PURE__ */ new Map();
|
|
3401
|
-
get length() {
|
|
3402
|
-
return this.entries.length;
|
|
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;
|
|
3403
3738
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
3739
|
+
selected = 0;
|
|
3740
|
+
get current() {
|
|
3741
|
+
return this.items[this.selected] ?? null;
|
|
3406
3742
|
}
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
this.entries.push(entry);
|
|
3411
|
-
return entry;
|
|
3743
|
+
move(delta) {
|
|
3744
|
+
if (!this.items.length) return;
|
|
3745
|
+
this.selected = (this.selected + delta + this.items.length) % this.items.length;
|
|
3412
3746
|
}
|
|
3413
|
-
/**
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
const
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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
|
+
});
|
|
3423
3763
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3764
|
+
};
|
|
3765
|
+
function pad2(text, width) {
|
|
3766
|
+
const short = width - visibleWidth(text);
|
|
3767
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// packages/tui/src/dialog.ts
|
|
3771
|
+
function pick(title, items, onSelect) {
|
|
3772
|
+
return { kind: "pick", title, items, query: "", selected: 0, onSelect };
|
|
3773
|
+
}
|
|
3774
|
+
function info(title, lines) {
|
|
3775
|
+
return { kind: "info", title, lines, scroll: 0 };
|
|
3776
|
+
}
|
|
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";
|
|
3427
3798
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
this.prose.clear();
|
|
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";
|
|
3433
3803
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
const live = /* @__PURE__ */ new Set();
|
|
3438
|
-
for (const entry of this.entries) {
|
|
3439
|
-
live.add(entry.id);
|
|
3440
|
-
const lines = this.lines(entry, width);
|
|
3441
|
-
if (out.length && lines.length) out.push("");
|
|
3442
|
-
out.push(...lines);
|
|
3443
|
-
}
|
|
3444
|
-
for (const id of this.cache.keys()) if (!live.has(id)) this.cache.delete(id);
|
|
3445
|
-
for (const id of this.prose.keys()) if (!live.has(id)) this.prose.delete(id);
|
|
3446
|
-
return out;
|
|
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";
|
|
3447
3807
|
}
|
|
3448
|
-
|
|
3449
|
-
const
|
|
3450
|
-
if (
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
return lines;
|
|
3808
|
+
if (key.name === "return") {
|
|
3809
|
+
const item = shown[dialog.selected];
|
|
3810
|
+
if (!item) return "same";
|
|
3811
|
+
dialog.onSelect(item);
|
|
3812
|
+
return "select";
|
|
3454
3813
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
* sees the words land without a heading flickering between plain and bold.
|
|
3461
|
-
*/
|
|
3462
|
-
assistant(entry, width) {
|
|
3463
|
-
const inner = proseWidth(width);
|
|
3464
|
-
const { text, done } = entry.block;
|
|
3465
|
-
if (done) return text.trim() ? renderMarkdown(text.replace(/\s+$/, ""), { width: inner, indent: " " }).split("\n") : [];
|
|
3466
|
-
const { complete: complete2, tail } = splitStreaming(text);
|
|
3467
|
-
let head = this.prose.get(entry.id);
|
|
3468
|
-
if (!head || head.length !== complete2.length || head.width !== width) {
|
|
3469
|
-
head = {
|
|
3470
|
-
length: complete2.length,
|
|
3471
|
-
width,
|
|
3472
|
-
lines: complete2 ? renderMarkdown(complete2, { width: inner, indent: " " }).split("\n") : []
|
|
3473
|
-
};
|
|
3474
|
-
this.prose.set(entry.id, head);
|
|
3475
|
-
}
|
|
3476
|
-
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
3477
|
-
return [...head.lines, ...raw];
|
|
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";
|
|
3478
3819
|
}
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
let open = -1;
|
|
3484
|
-
lines.forEach((line, i) => {
|
|
3485
|
-
if (/^\s*```/.test(line)) open = open === -1 ? i : -1;
|
|
3486
|
-
});
|
|
3487
|
-
let complete2 = lines;
|
|
3488
|
-
if (open !== -1) {
|
|
3489
|
-
tail = [...lines.slice(open), tail].join("\n");
|
|
3490
|
-
complete2 = lines.slice(0, open);
|
|
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";
|
|
3491
3824
|
}
|
|
3492
|
-
return
|
|
3493
|
-
}
|
|
3494
|
-
function proseWidth(width) {
|
|
3495
|
-
return Math.max(20, width - 2);
|
|
3825
|
+
return "same";
|
|
3496
3826
|
}
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
}
|
|
3516
|
-
return out;
|
|
3517
|
-
}
|
|
3518
|
-
case "approval": {
|
|
3519
|
-
const out = [
|
|
3520
|
-
...block.helper ? [` ${paint.accent("from a helper:")} ${paint.muted(block.helper)}`] : [],
|
|
3521
|
-
...approvalCard(block.preview, block.call, block.mode, inner)
|
|
3522
|
-
];
|
|
3523
|
-
out.push(
|
|
3524
|
-
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")}`
|
|
3525
|
-
);
|
|
3526
|
-
return out;
|
|
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));
|
|
3527
3845
|
}
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
return [paint.dim(` ${"\u2500".repeat(Math.floor(room / 2))}${label2}${"\u2500".repeat(Math.ceil(room / 2))}`)];
|
|
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`));
|
|
3536
3853
|
}
|
|
3537
3854
|
}
|
|
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 };
|
|
3538
3865
|
}
|
|
3539
|
-
function
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
const out = [];
|
|
3543
|
-
let line = "";
|
|
3544
|
-
for (const word of text.split(" ")) {
|
|
3545
|
-
if (line && line.length + 1 + word.length > limit) {
|
|
3546
|
-
out.push(line);
|
|
3547
|
-
line = word;
|
|
3548
|
-
} else {
|
|
3549
|
-
line = line ? `${line} ${word}` : word;
|
|
3550
|
-
}
|
|
3551
|
-
while (line.length > limit) {
|
|
3552
|
-
out.push(line.slice(0, limit));
|
|
3553
|
-
line = line.slice(limit);
|
|
3554
|
-
}
|
|
3555
|
-
}
|
|
3556
|
-
if (line || !out.length) out.push(line);
|
|
3557
|
-
return out;
|
|
3866
|
+
function pad3(text, width) {
|
|
3867
|
+
const short = width - visibleWidth(text);
|
|
3868
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3558
3869
|
}
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
const max = Math.max(0, this.total - this.height);
|
|
3581
|
-
const next = Math.max(0, Math.min(max, this.top + delta));
|
|
3582
|
-
if (next === this.top && this.follow) return;
|
|
3583
|
-
this.top = next;
|
|
3584
|
-
if (next >= max) this.refollow();
|
|
3585
|
-
else this.unfollow();
|
|
3586
|
-
}
|
|
3587
|
-
pageUp() {
|
|
3588
|
-
this.scroll(-Math.max(1, this.height - 1));
|
|
3589
|
-
}
|
|
3590
|
-
pageDown() {
|
|
3591
|
-
this.scroll(Math.max(1, this.height - 1));
|
|
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;
|
|
3592
3891
|
}
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
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;
|
|
3596
3899
|
}
|
|
3597
|
-
|
|
3598
|
-
this.
|
|
3599
|
-
|
|
3900
|
+
get columns() {
|
|
3901
|
+
const n = this.opts.columns ?? this.out.columns;
|
|
3902
|
+
return typeof n === "number" && n > 0 ? n : 80;
|
|
3600
3903
|
}
|
|
3601
|
-
/**
|
|
3602
|
-
|
|
3603
|
-
|
|
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);
|
|
3604
3919
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
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);
|
|
3608
3962
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
3963
|
+
/** Forget the last frame, so the next paint redraws every row. For ctrl-l. */
|
|
3964
|
+
invalidate() {
|
|
3965
|
+
this.last = null;
|
|
3611
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
|
+
};
|
|
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
|
+
}
|
|
3984
|
+
|
|
3985
|
+
// packages/tui/src/heatmap.ts
|
|
3986
|
+
var INDENT = " ";
|
|
3987
|
+
var DAY_LABEL = ["Mon", " ", "Wed", " ", "Fri", " ", "Sun"];
|
|
3988
|
+
var GUTTER = INDENT.length + 4;
|
|
3989
|
+
var MIN_WEEKS = 4;
|
|
3990
|
+
var GLYPH = { 0: "\xB7", 1: "\u2591", 2: "\u2592", 3: "\u2593", 4: "\u2588" };
|
|
3991
|
+
var SHADE = {
|
|
3992
|
+
0: paint.dim,
|
|
3993
|
+
1: paint.heat1,
|
|
3994
|
+
2: paint.heat2,
|
|
3995
|
+
3: paint.heat3,
|
|
3996
|
+
4: paint.accent
|
|
3612
3997
|
};
|
|
3998
|
+
function heatmapWeeks(width) {
|
|
3999
|
+
return Math.max(MIN_WEEKS, Math.min(ACTIVITY_WEEKS, Math.floor((width - GUTTER) / 2)));
|
|
4000
|
+
}
|
|
4001
|
+
function heatmapLines(summary, width) {
|
|
4002
|
+
const { days, totals, streak } = summary;
|
|
4003
|
+
const columns = Math.ceil(days.length / 7);
|
|
4004
|
+
const lines = [`${INDENT}${paint.muted(`last ${columns} weeks`)}`, `${" ".repeat(GUTTER)}${monthRow(days, columns)}`];
|
|
4005
|
+
for (let row2 = 0; row2 < 7; row2++) {
|
|
4006
|
+
let cells = "";
|
|
4007
|
+
for (let column = 0; column < columns; column++) {
|
|
4008
|
+
const day = days[column * 7 + row2];
|
|
4009
|
+
cells += day ? `${SHADE[day.level](GLYPH[day.level])} ` : " ";
|
|
4010
|
+
}
|
|
4011
|
+
lines.push(`${INDENT}${paint.dim(DAY_LABEL[row2])} ${cells.trimEnd()}`);
|
|
4012
|
+
}
|
|
4013
|
+
const legend = [0, 1, 2, 3, 4].map((l) => SHADE[l](GLYPH[l])).join(" ");
|
|
4014
|
+
lines.push(`${" ".repeat(GUTTER)}${paint.dim("less")} ${legend} ${paint.dim("more")}`, "");
|
|
4015
|
+
const row = (label2, value) => `${INDENT}${paint.muted(label2.padEnd(10))}${value}`;
|
|
4016
|
+
lines.push(
|
|
4017
|
+
row("tokens", `${paint.bold(formatTokens(totals.input + totals.output))} ${paint.dim(`${formatTokens(totals.input)} in \xB7 ${formatTokens(totals.output)} out`)}`),
|
|
4018
|
+
row("requests", `${paint.bold(String(totals.turns))} ${paint.dim(`on ${plural(totals.activeDays, "day")}`)}`),
|
|
4019
|
+
row("streak", `${paint.bold(plural(streak.current, "day"))} ${paint.dim(`longest ${plural(streak.longest, "day")}`)}`)
|
|
4020
|
+
);
|
|
4021
|
+
if (summary.busiest) {
|
|
4022
|
+
lines.push(row("busiest", `${paint.bold(dayName(summary.busiest.date))} ${paint.dim(`${formatTokens(summary.busiest.tokens)} tokens`)}`));
|
|
4023
|
+
}
|
|
4024
|
+
if (summary.peakHour !== null) lines.push(row("peak hour", paint.bold(formatHour(summary.peakHour))));
|
|
4025
|
+
const where = summary.surfaces.map((s) => `${surfaceLabel(s.surface)} ${paint.bold(formatTokens(s.tokens))}`);
|
|
4026
|
+
const joined = row("where", where.join(paint.dim(" \xB7 ")));
|
|
4027
|
+
if (!where.length) return lines;
|
|
4028
|
+
if (visibleWidth(joined) <= width) lines.push(joined);
|
|
4029
|
+
else where.forEach((w, i) => lines.push(row(i === 0 ? "where" : "", w)));
|
|
4030
|
+
return lines;
|
|
4031
|
+
}
|
|
4032
|
+
function monthRow(days, columns) {
|
|
4033
|
+
const starts = [];
|
|
4034
|
+
for (let column = 0; column < columns; column++) {
|
|
4035
|
+
const week = days.slice(column * 7, column * 7 + 7);
|
|
4036
|
+
const first = column === 0 ? week[0] : week.find((d) => d.date.endsWith("-01"));
|
|
4037
|
+
if (first && column + 1 < columns) starts.push({ column, date: first.date });
|
|
4038
|
+
}
|
|
4039
|
+
if (starts.length > 1 && starts[1].column < 2) starts.shift();
|
|
4040
|
+
let out = "";
|
|
4041
|
+
for (const { column, date } of starts) {
|
|
4042
|
+
out = out.padEnd(column * 2) + localDay(date).toLocaleDateString("en-GB", { month: "short" }).slice(0, 3);
|
|
4043
|
+
}
|
|
4044
|
+
return paint.dim(out);
|
|
4045
|
+
}
|
|
4046
|
+
function localDay(date) {
|
|
4047
|
+
const [y, m, d] = date.split("-").map(Number);
|
|
4048
|
+
return new Date(y, m - 1, d);
|
|
4049
|
+
}
|
|
4050
|
+
function dayName(date) {
|
|
4051
|
+
return localDay(date).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
4052
|
+
}
|
|
4053
|
+
function plural(n, word) {
|
|
4054
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
4055
|
+
}
|
|
3613
4056
|
|
|
3614
4057
|
// packages/tui/src/face.ts
|
|
3615
4058
|
var FACE_ART = [
|
|
@@ -3696,8 +4139,8 @@ var Prompt = class {
|
|
|
3696
4139
|
this.busyLabel = null;
|
|
3697
4140
|
this.editor.reset();
|
|
3698
4141
|
this.draw();
|
|
3699
|
-
return new Promise((
|
|
3700
|
-
this.resolve =
|
|
4142
|
+
return new Promise((resolve3) => {
|
|
4143
|
+
this.resolve = resolve3;
|
|
3701
4144
|
});
|
|
3702
4145
|
}
|
|
3703
4146
|
/** Put the box in its working state, or take it out of one. */
|
|
@@ -3815,11 +4258,11 @@ var Prompt = class {
|
|
|
3815
4258
|
this.finish({ kind: "exit" });
|
|
3816
4259
|
}
|
|
3817
4260
|
finish(result) {
|
|
3818
|
-
const
|
|
4261
|
+
const resolve3 = this.resolve;
|
|
3819
4262
|
this.resolve = null;
|
|
3820
4263
|
this.label = null;
|
|
3821
4264
|
this.renderer.clear();
|
|
3822
|
-
|
|
4265
|
+
resolve3?.(result);
|
|
3823
4266
|
}
|
|
3824
4267
|
};
|
|
3825
4268
|
|
|
@@ -4020,11 +4463,11 @@ var Auth = class {
|
|
|
4020
4463
|
}
|
|
4021
4464
|
};
|
|
4022
4465
|
function sleep(ms, signal) {
|
|
4023
|
-
return new Promise((
|
|
4024
|
-
const timer = setTimeout(
|
|
4466
|
+
return new Promise((resolve3) => {
|
|
4467
|
+
const timer = setTimeout(resolve3, ms);
|
|
4025
4468
|
signal?.addEventListener("abort", () => {
|
|
4026
4469
|
clearTimeout(timer);
|
|
4027
|
-
|
|
4470
|
+
resolve3();
|
|
4028
4471
|
}, { once: true });
|
|
4029
4472
|
});
|
|
4030
4473
|
}
|
|
@@ -4064,11 +4507,18 @@ function stateDir() {
|
|
|
4064
4507
|
function projectDir(root) {
|
|
4065
4508
|
return join6(root, ".labd");
|
|
4066
4509
|
}
|
|
4067
|
-
|
|
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 = {}) {
|
|
4068
4515
|
const global = read(join6(stateDir(), "settings.json"));
|
|
4069
4516
|
const project = read(join6(projectDir(root), "settings.json"));
|
|
4070
|
-
const
|
|
4071
|
-
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) {
|
|
4072
4522
|
return {
|
|
4073
4523
|
// Global, then the command line. The project is deliberately not consulted:
|
|
4074
4524
|
// a checked-in file must not be able to turn approvals off for whoever runs
|
|
@@ -4079,15 +4529,14 @@ function loadSettings(root, overrides = {}, opts = {}) {
|
|
|
4079
4529
|
// settings file is not a choice, which is why this tests the parse rather
|
|
4080
4530
|
// than the presence of the key.
|
|
4081
4531
|
modeChosen: overrides.mode !== void 0 || AGENT_MODES.includes(global.mode) || global.mode === "approve",
|
|
4082
|
-
effort: overrides.effort ?? pickEffort(
|
|
4532
|
+
effort: overrides.effort ?? pickEffort(global.effort),
|
|
4083
4533
|
serverUrl: stripSlash(
|
|
4084
|
-
overrides.serverUrl ??
|
|
4534
|
+
overrides.serverUrl ?? asString(global.serverUrl) ?? process.env.LABD_SERVER_URL ?? DEFAULTS.serverUrl
|
|
4085
4535
|
),
|
|
4086
|
-
...projectServerUrl && !honourProjectUrl ? { ignoredProjectServerUrl: projectServerUrl } : {},
|
|
4087
4536
|
commandTimeoutSeconds: clampTimeout(
|
|
4088
4537
|
overrides.commandTimeoutSeconds ?? asNumber(project.commandTimeoutSeconds) ?? asNumber(global.commandTimeoutSeconds) ?? DEFAULTS.commandTimeoutSeconds
|
|
4089
4538
|
),
|
|
4090
|
-
checkpoints: overrides.checkpoints ?? asBoolean(
|
|
4539
|
+
checkpoints: overrides.checkpoints ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints,
|
|
4091
4540
|
// Screen preferences. Harmless from either file: neither changes what labd
|
|
4092
4541
|
// may do, only how the session is drawn.
|
|
4093
4542
|
mouse: overrides.mouse ?? asBoolean(project.mouse) ?? asBoolean(global.mouse) ?? DEFAULTS.mouse,
|
|
@@ -4130,7 +4579,8 @@ var BUILTIN_COMMANDS = [
|
|
|
4130
4579
|
{ name: "session", what: "where this one is stored" },
|
|
4131
4580
|
{ name: "rename", args: "<name>", what: "name this session, so /sessions and --resume show it" },
|
|
4132
4581
|
{ name: "new", what: "start a fresh session; this one stays saved" },
|
|
4133
|
-
{ name: "usage", what: "what's left of this month's allowance" },
|
|
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" },
|
|
4134
4584
|
{ name: "trust", what: "let this project load its own extensions" },
|
|
4135
4585
|
{ name: "logout", what: "sign this machine out and leave" },
|
|
4136
4586
|
{ name: "reload", what: "reload extensions and the file list" },
|
|
@@ -4272,7 +4722,7 @@ async function createRuntime(opts) {
|
|
|
4272
4722
|
const root = opts.root;
|
|
4273
4723
|
const state = stateDir();
|
|
4274
4724
|
const trusted = isTrusted(state, root);
|
|
4275
|
-
const settings = loadSettings(root, opts.overrides
|
|
4725
|
+
const settings = loadSettings(root, opts.overrides);
|
|
4276
4726
|
const auth = new Auth(state, settings.serverUrl);
|
|
4277
4727
|
const client = new AgentClient({
|
|
4278
4728
|
baseUrl: settings.serverUrl,
|
|
@@ -4349,6 +4799,44 @@ import { basename as basename4 } from "node:path";
|
|
|
4349
4799
|
// packages/cli/src/commands.ts
|
|
4350
4800
|
import { dirname as dirname3 } from "node:path";
|
|
4351
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
|
+
|
|
4352
4840
|
// packages/cli/src/files.ts
|
|
4353
4841
|
import { readdirSync as readdirSync6 } from "node:fs";
|
|
4354
4842
|
import { join as join8, relative as relative4 } from "node:path";
|
|
@@ -4440,13 +4928,41 @@ async function handleCommand(input, ctx) {
|
|
|
4440
4928
|
return { effort: args };
|
|
4441
4929
|
}
|
|
4442
4930
|
case "usage": {
|
|
4443
|
-
const credits = await
|
|
4931
|
+
const [credits, activity] = await Promise.all([
|
|
4932
|
+
runtime.client.credits(),
|
|
4933
|
+
runtime.client.activity(utcOffsetMinutes())
|
|
4934
|
+
]);
|
|
4444
4935
|
if (!credits) {
|
|
4445
4936
|
print(` ${paint.muted("Couldn't check that just now.")}`);
|
|
4446
4937
|
} else {
|
|
4447
4938
|
print(` ${paint.bold(`${credits.percentLeft}%`)} ${paint.muted("of this month left")}`);
|
|
4939
|
+
printAllowance(credits, print);
|
|
4448
4940
|
}
|
|
4449
|
-
|
|
4941
|
+
if (activity && !activity.days.length) {
|
|
4942
|
+
print(` ${paint.muted("No activity yet. Ask labd something and it shows up here.")}`);
|
|
4943
|
+
} else if (activity) {
|
|
4944
|
+
print("");
|
|
4945
|
+
print(heatmapLines(summariseActivity(activity, heatmapWeeks(ctx.width)), ctx.width).join("\n"));
|
|
4946
|
+
} else if (credits) {
|
|
4947
|
+
print(` ${paint.muted("Couldn't read your activity just now.")}`);
|
|
4948
|
+
}
|
|
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 };
|
|
4450
4966
|
}
|
|
4451
4967
|
case "tree": {
|
|
4452
4968
|
const nodes = runtime.session.all();
|
|
@@ -4474,9 +4990,12 @@ async function handleCommand(input, ctx) {
|
|
|
4474
4990
|
print(` ${paint.yellow("!")} ${paint.muted(`no point here starts with "${args}"`)}`);
|
|
4475
4991
|
return {};
|
|
4476
4992
|
}
|
|
4993
|
+
const moved = match.id !== runtime.session.headId;
|
|
4477
4994
|
runtime.session.moveTo(match.id);
|
|
4478
|
-
print(
|
|
4479
|
-
|
|
4995
|
+
print(
|
|
4996
|
+
` ${paint.muted(moved ? `continuing from ${match.id.slice(0, 8)}. The other branch is still in the file.` : `already at ${match.id.slice(0, 8)}.`)}`
|
|
4997
|
+
);
|
|
4998
|
+
return { moved };
|
|
4480
4999
|
}
|
|
4481
5000
|
case "sessions": {
|
|
4482
5001
|
const bucket = dirname3(runtime.session.file);
|
|
@@ -4622,6 +5141,12 @@ async function handleCommand(input, ctx) {
|
|
|
4622
5141
|
}
|
|
4623
5142
|
}
|
|
4624
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
|
+
}
|
|
4625
5150
|
function summarise(body2) {
|
|
4626
5151
|
const text = typeof body2.text === "string" ? body2.text : "";
|
|
4627
5152
|
if (body2.kind === "tool") {
|
|
@@ -4739,7 +5264,15 @@ async function runInteractive(runtime) {
|
|
|
4739
5264
|
credits: null,
|
|
4740
5265
|
tick: 0
|
|
4741
5266
|
};
|
|
5267
|
+
const opening = runtime.client.credits();
|
|
4742
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
|
+
}
|
|
4743
5276
|
let abort = null;
|
|
4744
5277
|
const prompt = new Prompt(renderer, {
|
|
4745
5278
|
describe: (line) => describe(line, runtime),
|
|
@@ -4748,7 +5281,7 @@ async function runInteractive(runtime) {
|
|
|
4748
5281
|
project: basename4(runtime.root),
|
|
4749
5282
|
branch: runtime.branch,
|
|
4750
5283
|
contextPercent: state.context?.percentUsed ?? null,
|
|
4751
|
-
|
|
5284
|
+
credits: state.credits,
|
|
4752
5285
|
mode: state.mode,
|
|
4753
5286
|
effort: state.effort ?? null
|
|
4754
5287
|
}),
|
|
@@ -4792,6 +5325,7 @@ async function runInteractive(runtime) {
|
|
|
4792
5325
|
mode: state.mode,
|
|
4793
5326
|
effort: state.effort,
|
|
4794
5327
|
asked: answered > 0,
|
|
5328
|
+
width: process.stdout.columns ?? 80,
|
|
4795
5329
|
print: (text) => renderer.print(text)
|
|
4796
5330
|
});
|
|
4797
5331
|
if (outcome.farewell) process.stderr.write(`${outcome.farewell}
|
|
@@ -4799,6 +5333,8 @@ async function runInteractive(runtime) {
|
|
|
4799
5333
|
if (outcome.exit) break;
|
|
4800
5334
|
if (outcome.mode) state.mode = outcome.mode;
|
|
4801
5335
|
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
5336
|
+
if (outcome.moved) state.context = null;
|
|
5337
|
+
if (outcome.credits) state.credits = outcome.credits;
|
|
4802
5338
|
continue;
|
|
4803
5339
|
}
|
|
4804
5340
|
const tags = taggedPaths(input);
|
|
@@ -4870,9 +5406,19 @@ async function runInteractive(runtime) {
|
|
|
4870
5406
|
for (const line of waiting) renderer.print(line);
|
|
4871
5407
|
}
|
|
4872
5408
|
},
|
|
4873
|
-
error(message) {
|
|
5409
|
+
error(message, info2) {
|
|
4874
5410
|
stopSpinner();
|
|
4875
|
-
|
|
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;
|
|
4876
5422
|
},
|
|
4877
5423
|
done(info2) {
|
|
4878
5424
|
if (info2.context) state.context = info2.context;
|
|
@@ -4913,6 +5459,7 @@ async function runInteractive(runtime) {
|
|
|
4913
5459
|
if (state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
4914
5460
|
startSpinner("summarising, to make room");
|
|
4915
5461
|
const ok = await compact({ session: runtime.session, client: runtime.client, root: runtime.root });
|
|
5462
|
+
if (ok) state.context = null;
|
|
4916
5463
|
stopSpinner();
|
|
4917
5464
|
renderer.print(paint.dim(ok ? " summarised the conversation so far." : " (could not summarise; carrying on)"));
|
|
4918
5465
|
}
|
|
@@ -4947,10 +5494,10 @@ async function banner(renderer, runtime, mode) {
|
|
|
4947
5494
|
renderer.print("");
|
|
4948
5495
|
renderer.print(`${paint.accent(FACE)} ${paint.bold("labd")} ${paint.dim(`in ${where}${runtime.branch ? ` on ${runtime.branch}` : ""}`)}`);
|
|
4949
5496
|
renderer.print(` ${paint.dim(MODE_CAPTION[mode])}`);
|
|
4950
|
-
if (runtime.settings.
|
|
5497
|
+
if (runtime.settings.ignoredProjectKeys) {
|
|
4951
5498
|
renderer.print(
|
|
4952
5499
|
` ${paint.yellow("!")} ${paint.muted(
|
|
4953
|
-
|
|
5500
|
+
ignoredSettingsNote(runtime.settings.ignoredProjectKeys)
|
|
4954
5501
|
)}`
|
|
4955
5502
|
);
|
|
4956
5503
|
}
|
|
@@ -4960,7 +5507,7 @@ async function banner(renderer, runtime, mode) {
|
|
|
4960
5507
|
renderer.print("");
|
|
4961
5508
|
}
|
|
4962
5509
|
function sleep2(ms) {
|
|
4963
|
-
return new Promise((
|
|
5510
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4964
5511
|
}
|
|
4965
5512
|
|
|
4966
5513
|
// packages/cli/src/modes/screen.ts
|
|
@@ -5073,6 +5620,14 @@ async function runScreen(runtime, opts) {
|
|
|
5073
5620
|
effort: runtime.settings.effort,
|
|
5074
5621
|
context: null,
|
|
5075
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,
|
|
5076
5631
|
sidebar: opts.sidebar,
|
|
5077
5632
|
tick: 0,
|
|
5078
5633
|
/** What labd is doing right now, for the footer. Null when idle. */
|
|
@@ -5131,8 +5686,8 @@ async function runScreen(runtime, opts) {
|
|
|
5131
5686
|
schedule();
|
|
5132
5687
|
};
|
|
5133
5688
|
let resolveExit = null;
|
|
5134
|
-
const exited = new Promise((
|
|
5135
|
-
resolveExit =
|
|
5689
|
+
const exited = new Promise((resolve3) => {
|
|
5690
|
+
resolveExit = resolve3;
|
|
5136
5691
|
});
|
|
5137
5692
|
const quit = () => {
|
|
5138
5693
|
if (state.quitting) return;
|
|
@@ -5218,6 +5773,7 @@ async function runScreen(runtime, opts) {
|
|
|
5218
5773
|
else if (state.approval) left = `${paint.yellow("y")} ${d("allow \xB7")} ${paint.yellow("n")} ${d("decline \xB7 ctrl+c stop")}`;
|
|
5219
5774
|
else if (state.busy) left = `${paint.accent(state.busy)}${spinnerFrame(state.tick)} ${d("enter queues \xB7 ctrl+c stop \xB7 pgup/pgdn scroll")}`;
|
|
5220
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")}`;
|
|
5221
5777
|
else {
|
|
5222
5778
|
const described = describe(editor.currentLine, runtime);
|
|
5223
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");
|
|
@@ -5227,13 +5783,13 @@ async function runScreen(runtime, opts) {
|
|
|
5227
5783
|
const crowded = state.context.percentUsed >= 75;
|
|
5228
5784
|
bits.push((crowded ? paint.yellow : paint.muted)(`context ${state.context.percentUsed}%`));
|
|
5229
5785
|
}
|
|
5230
|
-
if (state.credits) bits.push(
|
|
5786
|
+
if (state.credits) bits.push(creditsReading(state.credits, `credits ${state.credits.percentLeft}%`));
|
|
5231
5787
|
return spread(` ${left}`, `${bits.join(paint.dim(" \xB7 "))} `, width);
|
|
5232
5788
|
}
|
|
5233
5789
|
function composer2(width, height) {
|
|
5234
5790
|
const inner = width - 4;
|
|
5235
5791
|
const textRows = Math.max(1, height - 2);
|
|
5236
|
-
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);
|
|
5237
5793
|
const out = [];
|
|
5238
5794
|
out.push(colour(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`));
|
|
5239
5795
|
const view = editor.view(inner - 2, textRows);
|
|
@@ -5244,7 +5800,7 @@ async function runScreen(runtime, opts) {
|
|
|
5244
5800
|
const sigil = first ? state.busy ? paint.dim(">") : paint.accent(">") : " ";
|
|
5245
5801
|
let text = line;
|
|
5246
5802
|
if (editor.isEmpty && i === 0) {
|
|
5247
|
-
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");
|
|
5248
5804
|
}
|
|
5249
5805
|
out.push(`${colour("\u2502")} ${sigil} ${fit(text, inner - 2)} ${colour("\u2502")}`);
|
|
5250
5806
|
});
|
|
@@ -5330,6 +5886,10 @@ async function runScreen(runtime, opts) {
|
|
|
5330
5886
|
refreshPopup();
|
|
5331
5887
|
return schedule();
|
|
5332
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();
|
|
5333
5893
|
const outcome = editor.apply(key);
|
|
5334
5894
|
if (outcome === "unhandled") {
|
|
5335
5895
|
if (key.name === "return") return submit();
|
|
@@ -5511,8 +6071,10 @@ async function runScreen(runtime, opts) {
|
|
|
5511
6071
|
mode: state.mode,
|
|
5512
6072
|
effort: state.effort,
|
|
5513
6073
|
asked: state.asked,
|
|
6074
|
+
width: transcriptWidth(),
|
|
5514
6075
|
print: (text) => note(text)
|
|
5515
6076
|
});
|
|
6077
|
+
if (outcome.credits) setCredits(outcome.credits);
|
|
5516
6078
|
if (outcome.farewell) state.farewell = outcome.farewell;
|
|
5517
6079
|
if (outcome.exit) return quit();
|
|
5518
6080
|
if (outcome.mode) {
|
|
@@ -5520,6 +6082,7 @@ async function runScreen(runtime, opts) {
|
|
|
5520
6082
|
state.heldMode = null;
|
|
5521
6083
|
}
|
|
5522
6084
|
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
6085
|
+
if (outcome.moved) state.context = null;
|
|
5523
6086
|
schedule();
|
|
5524
6087
|
}
|
|
5525
6088
|
function helpLines() {
|
|
@@ -5592,6 +6155,7 @@ async function runScreen(runtime, opts) {
|
|
|
5592
6155
|
if (state.asked) await closeConversation(runtime);
|
|
5593
6156
|
runtime.session = Session.open(file);
|
|
5594
6157
|
state.asked = false;
|
|
6158
|
+
state.context = null;
|
|
5595
6159
|
transcript.clear();
|
|
5596
6160
|
state.toolBlocks.clear();
|
|
5597
6161
|
state.answer = null;
|
|
@@ -5602,6 +6166,7 @@ async function runScreen(runtime, opts) {
|
|
|
5602
6166
|
if (state.asked) await closeConversation(runtime);
|
|
5603
6167
|
runtime.session = Session.create(bucketFor(runtime.stateDir, runtime.root));
|
|
5604
6168
|
state.asked = false;
|
|
6169
|
+
state.context = null;
|
|
5605
6170
|
transcript.clear();
|
|
5606
6171
|
state.toolBlocks.clear();
|
|
5607
6172
|
state.answer = null;
|
|
@@ -5620,8 +6185,8 @@ async function runScreen(runtime, opts) {
|
|
|
5620
6185
|
state.answer = null;
|
|
5621
6186
|
const tags = taggedPaths(input);
|
|
5622
6187
|
const attachments = tags.length ? attachTagged(runtime.root, tags) : void 0;
|
|
5623
|
-
for (const
|
|
5624
|
-
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}`)}`);
|
|
5625
6190
|
}
|
|
5626
6191
|
for (const handler of runtime.extensions.onTurn) {
|
|
5627
6192
|
try {
|
|
@@ -5682,19 +6247,23 @@ async function runScreen(runtime, opts) {
|
|
|
5682
6247
|
closeAnswer();
|
|
5683
6248
|
const entry = transcript.add({ kind: "approval", call, preview, helper, mode: effectiveMode(), answer: null });
|
|
5684
6249
|
viewport.end();
|
|
5685
|
-
return new Promise((
|
|
5686
|
-
state.approval = { entry, resolve:
|
|
6250
|
+
return new Promise((resolve3) => {
|
|
6251
|
+
state.approval = { entry, resolve: resolve3 };
|
|
5687
6252
|
schedule();
|
|
5688
6253
|
});
|
|
5689
6254
|
},
|
|
5690
|
-
error(message) {
|
|
6255
|
+
error(message, info2) {
|
|
5691
6256
|
setBusy(null);
|
|
5692
|
-
|
|
6257
|
+
if (info2?.code === "no_credits") refused(message, info2);
|
|
6258
|
+
else transcript.add({ kind: "error", text: message });
|
|
5693
6259
|
schedule();
|
|
5694
6260
|
},
|
|
6261
|
+
ready(info2) {
|
|
6262
|
+
if (info2.credits) setCredits(info2.credits);
|
|
6263
|
+
},
|
|
5695
6264
|
done(info2) {
|
|
5696
6265
|
if (info2.context) state.context = info2.context;
|
|
5697
|
-
if (info2.credits)
|
|
6266
|
+
if (info2.credits) setCredits(info2.credits);
|
|
5698
6267
|
schedule();
|
|
5699
6268
|
},
|
|
5700
6269
|
commandOutput(line, key) {
|
|
@@ -5728,8 +6297,10 @@ async function runScreen(runtime, opts) {
|
|
|
5728
6297
|
if (!abort.signal.aborted && state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
5729
6298
|
setBusy("summarising, to make room");
|
|
5730
6299
|
const ok = await compact({ session: runtime.session, client: runtime.client, root: runtime.root });
|
|
5731
|
-
if (ok)
|
|
5732
|
-
|
|
6300
|
+
if (ok) {
|
|
6301
|
+
state.context = null;
|
|
6302
|
+
transcript.add({ kind: "compaction" });
|
|
6303
|
+
} else note(paint.dim(" (could not summarise; carrying on)"));
|
|
5733
6304
|
}
|
|
5734
6305
|
} catch (err) {
|
|
5735
6306
|
transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) });
|
|
@@ -5740,6 +6311,64 @@ async function runScreen(runtime, opts) {
|
|
|
5740
6311
|
setBusy(null);
|
|
5741
6312
|
}
|
|
5742
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
|
+
}
|
|
5743
6372
|
function closeAnswer() {
|
|
5744
6373
|
const answer = state.answer;
|
|
5745
6374
|
if (!answer || answer.block.done) return;
|
|
@@ -5784,10 +6413,10 @@ async function runScreen(runtime, opts) {
|
|
|
5784
6413
|
process.on("SIGHUP", onHup);
|
|
5785
6414
|
screen.enter();
|
|
5786
6415
|
if (state.sidebar) refreshProjectFacts();
|
|
5787
|
-
if (runtime.settings.
|
|
6416
|
+
if (runtime.settings.ignoredProjectKeys) {
|
|
5788
6417
|
note(
|
|
5789
6418
|
` ${paint.yellow("!")} ${paint.muted(
|
|
5790
|
-
|
|
6419
|
+
ignoredSettingsNote(runtime.settings.ignoredProjectKeys)
|
|
5791
6420
|
)}`
|
|
5792
6421
|
);
|
|
5793
6422
|
}
|
|
@@ -5806,11 +6435,13 @@ async function runScreen(runtime, opts) {
|
|
|
5806
6435
|
}
|
|
5807
6436
|
if (opts.pickSession) openSessions();
|
|
5808
6437
|
draw();
|
|
6438
|
+
fetchCredits();
|
|
5809
6439
|
try {
|
|
5810
6440
|
await exited;
|
|
5811
6441
|
if (serial.running) await Promise.race([serial.running, sleep3(1500)]);
|
|
5812
6442
|
} finally {
|
|
5813
6443
|
if (faceTimer) clearInterval(faceTimer);
|
|
6444
|
+
if (state.resetTimer) clearTimeout(state.resetTimer);
|
|
5814
6445
|
if (frameTimer) clearTimeout(frameTimer);
|
|
5815
6446
|
setBusy(null);
|
|
5816
6447
|
process.off("SIGTERM", onTerm);
|
|
@@ -5883,13 +6514,14 @@ function summarise2(body2) {
|
|
|
5883
6514
|
return text.replace(/\s+/g, " ").slice(0, 60);
|
|
5884
6515
|
}
|
|
5885
6516
|
function sleep3(ms) {
|
|
5886
|
-
return new Promise((
|
|
6517
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
5887
6518
|
}
|
|
5888
6519
|
|
|
5889
6520
|
// packages/cli/src/modes/print.ts
|
|
5890
6521
|
function toStderr(text) {
|
|
5891
6522
|
process.stderr.write(process.stderr.isTTY ? text : stripColor(text));
|
|
5892
6523
|
}
|
|
6524
|
+
var EXIT_NO_CREDITS = 3;
|
|
5893
6525
|
async function runPrint(runtime, opts) {
|
|
5894
6526
|
const question = await withStdin(opts.question);
|
|
5895
6527
|
if (!question.trim()) {
|
|
@@ -5899,6 +6531,7 @@ async function runPrint(runtime, opts) {
|
|
|
5899
6531
|
}
|
|
5900
6532
|
let failed = false;
|
|
5901
6533
|
let declined = false;
|
|
6534
|
+
let outOfCredits = false;
|
|
5902
6535
|
let answer = "";
|
|
5903
6536
|
const emit = (event) => {
|
|
5904
6537
|
if (opts.json) process.stdout.write(`${JSON.stringify(event)}
|
|
@@ -5934,9 +6567,10 @@ async function runPrint(runtime, opts) {
|
|
|
5934
6567
|
}
|
|
5935
6568
|
if (step.status === "skipped") declined = true;
|
|
5936
6569
|
},
|
|
5937
|
-
error(message) {
|
|
6570
|
+
error(message, info2) {
|
|
5938
6571
|
failed = true;
|
|
5939
|
-
if (
|
|
6572
|
+
if (info2?.code === "no_credits") outOfCredits = true;
|
|
6573
|
+
if (opts.json) emit({ type: "error", message, ...info2 });
|
|
5940
6574
|
else toStderr(`${paint.red("labd:")} ${message}
|
|
5941
6575
|
`);
|
|
5942
6576
|
},
|
|
@@ -5965,6 +6599,7 @@ async function runPrint(runtime, opts) {
|
|
|
5965
6599
|
});
|
|
5966
6600
|
if (!opts.json && answer && !answer.endsWith("\n")) process.stdout.write("\n");
|
|
5967
6601
|
await closeConversation(runtime);
|
|
6602
|
+
if (outOfCredits) return EXIT_NO_CREDITS;
|
|
5968
6603
|
if (failed) return 1;
|
|
5969
6604
|
if (declined) return 2;
|
|
5970
6605
|
return result.text.trim() ? 0 : 1;
|
|
@@ -6017,11 +6652,11 @@ async function runRpc(runtime) {
|
|
|
6017
6652
|
return;
|
|
6018
6653
|
}
|
|
6019
6654
|
if (req.method === void 0 && req.id !== void 0 && req.id !== null) {
|
|
6020
|
-
const
|
|
6021
|
-
if (
|
|
6655
|
+
const resolve3 = pending.get(Number(req.id));
|
|
6656
|
+
if (resolve3) {
|
|
6022
6657
|
pending.delete(Number(req.id));
|
|
6023
6658
|
const result = req.result;
|
|
6024
|
-
|
|
6659
|
+
resolve3(result?.approved === true);
|
|
6025
6660
|
}
|
|
6026
6661
|
return;
|
|
6027
6662
|
}
|
|
@@ -6097,8 +6732,8 @@ async function runRpc(runtime) {
|
|
|
6097
6732
|
output: step.output
|
|
6098
6733
|
});
|
|
6099
6734
|
},
|
|
6100
|
-
error(message) {
|
|
6101
|
-
notify("labd/error", { message });
|
|
6735
|
+
error(message, info2) {
|
|
6736
|
+
notify("labd/error", { message, ...info2 });
|
|
6102
6737
|
},
|
|
6103
6738
|
done(info2) {
|
|
6104
6739
|
notify("labd/done", info2);
|
|
@@ -6113,15 +6748,15 @@ async function runRpc(runtime) {
|
|
|
6113
6748
|
* asleep.
|
|
6114
6749
|
*/
|
|
6115
6750
|
approve(call, preview, helper) {
|
|
6116
|
-
return new Promise((
|
|
6117
|
-
if (closed) return
|
|
6751
|
+
return new Promise((resolve3) => {
|
|
6752
|
+
if (closed) return resolve3(false);
|
|
6118
6753
|
const id = nextId++;
|
|
6119
6754
|
const timer = setTimeout(() => {
|
|
6120
|
-
if (pending.delete(id))
|
|
6755
|
+
if (pending.delete(id)) resolve3(false);
|
|
6121
6756
|
}, APPROVAL_TIMEOUT_MS);
|
|
6122
6757
|
pending.set(id, (approved) => {
|
|
6123
6758
|
clearTimeout(timer);
|
|
6124
|
-
|
|
6759
|
+
resolve3(approved);
|
|
6125
6760
|
});
|
|
6126
6761
|
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview, ...helper ? { helper } : {} } });
|
|
6127
6762
|
});
|
|
@@ -6146,14 +6781,14 @@ async function runRpc(runtime) {
|
|
|
6146
6781
|
mode: unattendedMode(runtime.settings),
|
|
6147
6782
|
root: runtime.root
|
|
6148
6783
|
});
|
|
6149
|
-
await new Promise((
|
|
6784
|
+
await new Promise((resolve3) => process.stdin.on("end", resolve3));
|
|
6150
6785
|
await busy;
|
|
6151
6786
|
await closeConversation(runtime);
|
|
6152
6787
|
return 0;
|
|
6153
6788
|
}
|
|
6154
6789
|
|
|
6155
6790
|
// packages/cli/src/index.ts
|
|
6156
|
-
var VERSION = true ? "0.
|
|
6791
|
+
var VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
6157
6792
|
async function main(argv) {
|
|
6158
6793
|
const args = parse(argv);
|
|
6159
6794
|
if (args.color !== void 0) setColor(args.color);
|
|
@@ -6166,7 +6801,7 @@ async function main(argv) {
|
|
|
6166
6801
|
`);
|
|
6167
6802
|
return 0;
|
|
6168
6803
|
}
|
|
6169
|
-
const root =
|
|
6804
|
+
const root = resolve2(args.cwd ?? process.cwd());
|
|
6170
6805
|
switch (args.command) {
|
|
6171
6806
|
case "login":
|
|
6172
6807
|
return login(root);
|
|
@@ -6213,7 +6848,7 @@ async function main(argv) {
|
|
|
6213
6848
|
return runScreen(runtime, { mouse: runtime.settings.mouse, sidebar: runtime.settings.sidebar, pickSession: args.pickSession });
|
|
6214
6849
|
}
|
|
6215
6850
|
async function login(root) {
|
|
6216
|
-
const settings = loadSettings(root
|
|
6851
|
+
const settings = loadSettings(root);
|
|
6217
6852
|
const auth = new Auth(stateDir(), settings.serverUrl);
|
|
6218
6853
|
const say = (text) => process.stderr.write(text);
|
|
6219
6854
|
say(`
|
|
@@ -6244,7 +6879,7 @@ ${paint.red("labd:")} ${result.reason ?? "that did not complete"}.
|
|
|
6244
6879
|
return 0;
|
|
6245
6880
|
}
|
|
6246
6881
|
async function logout(root) {
|
|
6247
|
-
const settings = loadSettings(root
|
|
6882
|
+
const settings = loadSettings(root);
|
|
6248
6883
|
const hadStored = await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
6249
6884
|
for (const line of signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n")) {
|
|
6250
6885
|
process.stderr.write(` ${paint.muted(line)}
|
|
@@ -6274,7 +6909,7 @@ function trustHere(root) {
|
|
|
6274
6909
|
trust(stateDir(), root);
|
|
6275
6910
|
process.stdout.write(` ${paint.bold("trusted")} ${paint.muted(root)}
|
|
6276
6911
|
`);
|
|
6277
|
-
process.stdout.write(` ${paint.dim("Extensions in .labd/extensions
|
|
6912
|
+
process.stdout.write(` ${paint.dim("Extensions in .labd/extensions will now load here.")}
|
|
6278
6913
|
`);
|
|
6279
6914
|
return 0;
|
|
6280
6915
|
}
|