@thedevlabs-io/labd 0.3.1 → 0.6.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 +89 -30
- package/README.md +96 -81
- package/dist/labd.js +2608 -435
- package/dist/labd.js.map +4 -4
- package/package.json +1 -3
package/dist/labd.js
CHANGED
|
@@ -43,12 +43,12 @@ function scorePath(query, path) {
|
|
|
43
43
|
var FILENAME_BONUS = 20;
|
|
44
44
|
function scan(q, text) {
|
|
45
45
|
let score = 0;
|
|
46
|
-
let
|
|
46
|
+
let at2 = 0;
|
|
47
47
|
let run = 0;
|
|
48
48
|
for (const ch of q) {
|
|
49
|
-
const found = text.indexOf(ch,
|
|
49
|
+
const found = text.indexOf(ch, at2);
|
|
50
50
|
if (found === -1) return null;
|
|
51
|
-
if (found ===
|
|
51
|
+
if (found === at2 && at2 > 0) {
|
|
52
52
|
run += 1;
|
|
53
53
|
score += 4 + run * 2;
|
|
54
54
|
} else {
|
|
@@ -57,7 +57,7 @@ function scan(q, text) {
|
|
|
57
57
|
}
|
|
58
58
|
const prev = found === 0 ? "/" : text[found - 1];
|
|
59
59
|
if (prev === "/" || prev === "." || prev === "-" || prev === "_") score += 6;
|
|
60
|
-
|
|
60
|
+
at2 = found + 1;
|
|
61
61
|
}
|
|
62
62
|
return score;
|
|
63
63
|
}
|
|
@@ -433,6 +433,36 @@ var EXEC_TOOLS = ["run_command"];
|
|
|
433
433
|
function isExecTool(name) {
|
|
434
434
|
return EXEC_TOOLS.includes(name);
|
|
435
435
|
}
|
|
436
|
+
var AGENT_TOOLS = ["spawn_agent"];
|
|
437
|
+
function isAgentTool(name) {
|
|
438
|
+
return AGENT_TOOLS.includes(name);
|
|
439
|
+
}
|
|
440
|
+
var MAX_SUBAGENTS = 3;
|
|
441
|
+
var SUBAGENT_REPORT_CHARS = 8e3;
|
|
442
|
+
function subAgentCapabilities(parent) {
|
|
443
|
+
return parent.filter((c) => !isAgentTool(c));
|
|
444
|
+
}
|
|
445
|
+
function subAgentConversationId(parentId, callId) {
|
|
446
|
+
return parentId ? `${parentId}~helper~${callId}` : void 0;
|
|
447
|
+
}
|
|
448
|
+
function subAgentBrief(task) {
|
|
449
|
+
return `You are working as a helper for labd, the assistant the learner is talking to. It has handed you one piece of a larger job and is waiting on your report. The learner is the same person, in the same project, under the same rules: the same tools, the same approvals, and the same things you will not do.
|
|
450
|
+
|
|
451
|
+
Your task:
|
|
452
|
+
|
|
453
|
+
${task.trim()}
|
|
454
|
+
|
|
455
|
+
How to work:
|
|
456
|
+
- You start with none of the parent conversation, so the task above is all you know about it. If it is not enough to act on, say what is missing in your report rather than guessing at the intent.
|
|
457
|
+
- Do the task, and only the task. The parent decides what happens next, so do not widen the job, and do not ask the learner questions - your report goes to the parent, not to them.
|
|
458
|
+
- If a write or command you need is declined, do not look for another way round it. Stop and say so.
|
|
459
|
+
- End with a report the parent can act on without redoing your work: what you found or changed, file by file and with line numbers where they matter, and anything you could not finish and why. Keep it tight. Leave out how you went about it.`;
|
|
460
|
+
}
|
|
461
|
+
function subAgentReport(text) {
|
|
462
|
+
const clean = text.trim() || "The helper finished without a report.";
|
|
463
|
+
return clean.length <= SUBAGENT_REPORT_CHARS ? clean : `${clean.slice(0, SUBAGENT_REPORT_CHARS)}
|
|
464
|
+
... the helper's report was longer than this and was trimmed.`;
|
|
465
|
+
}
|
|
436
466
|
var AGENT_MODES = ["ask", "auto", "agent"];
|
|
437
467
|
var DEFAULT_AGENT_MODE = "auto";
|
|
438
468
|
function toAgentMode(raw) {
|
|
@@ -454,8 +484,8 @@ function danglingToolCallAt(messages) {
|
|
|
454
484
|
return null;
|
|
455
485
|
}
|
|
456
486
|
function sendableMessages(messages) {
|
|
457
|
-
const
|
|
458
|
-
return
|
|
487
|
+
const at2 = danglingToolCallAt(messages);
|
|
488
|
+
return at2 === null ? messages : messages.slice(0, at2);
|
|
459
489
|
}
|
|
460
490
|
function forExtraction(messages) {
|
|
461
491
|
return messages.map((m) => {
|
|
@@ -678,6 +708,11 @@ function attachOne(root, path, budget) {
|
|
|
678
708
|
import { appendFileSync, mkdirSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync3, writeFileSync } from "node:fs";
|
|
679
709
|
import { createHash, randomUUID } from "node:crypto";
|
|
680
710
|
import { basename as basename2, join as join2 } from "node:path";
|
|
711
|
+
function sessionTitle(summary, width = 60) {
|
|
712
|
+
const text = (summary.name ?? summary.preview ?? "").replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim();
|
|
713
|
+
if (!text) return "(no questions yet)";
|
|
714
|
+
return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
|
|
715
|
+
}
|
|
681
716
|
function bucketFor(stateDir2, root) {
|
|
682
717
|
const bucket = join2(stateDir2, "sessions", createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
683
718
|
mkdirSync(bucket, { recursive: true });
|
|
@@ -749,6 +784,7 @@ var Session = class _Session {
|
|
|
749
784
|
id: session.id,
|
|
750
785
|
file: join2(bucket, f),
|
|
751
786
|
name: session.name,
|
|
787
|
+
preview: session.firstQuestion(),
|
|
752
788
|
at: statSync3(join2(bucket, f)).mtimeMs,
|
|
753
789
|
messages: session.nodes.size
|
|
754
790
|
};
|
|
@@ -763,6 +799,14 @@ var Session = class _Session {
|
|
|
763
799
|
size() {
|
|
764
800
|
return this.nodes.size;
|
|
765
801
|
}
|
|
802
|
+
/** The earliest question in the file, on any branch. */
|
|
803
|
+
firstQuestion() {
|
|
804
|
+
let first;
|
|
805
|
+
for (const node of this.nodes.values()) {
|
|
806
|
+
if (node.body.kind === "user" && (!first || node.at < first.at)) first = node;
|
|
807
|
+
}
|
|
808
|
+
return first?.body.kind === "user" ? first.body.text : void 0;
|
|
809
|
+
}
|
|
766
810
|
rename(name) {
|
|
767
811
|
this.name = name;
|
|
768
812
|
this.append({ t: "meta", name });
|
|
@@ -802,15 +846,15 @@ var Session = class _Session {
|
|
|
802
846
|
/** Root-to-head, which is the conversation as the model will see it. */
|
|
803
847
|
path(from = this.head) {
|
|
804
848
|
const out = [];
|
|
805
|
-
let
|
|
849
|
+
let at2 = from;
|
|
806
850
|
const seen = /* @__PURE__ */ new Set();
|
|
807
|
-
while (
|
|
808
|
-
if (seen.has(
|
|
809
|
-
seen.add(
|
|
810
|
-
const node = this.nodes.get(
|
|
851
|
+
while (at2) {
|
|
852
|
+
if (seen.has(at2)) break;
|
|
853
|
+
seen.add(at2);
|
|
854
|
+
const node = this.nodes.get(at2);
|
|
811
855
|
if (!node) break;
|
|
812
856
|
out.push(node);
|
|
813
|
-
|
|
857
|
+
at2 = node.parentId;
|
|
814
858
|
}
|
|
815
859
|
return out.reverse();
|
|
816
860
|
}
|
|
@@ -1305,6 +1349,154 @@ function trim(output) {
|
|
|
1305
1349
|
].join("");
|
|
1306
1350
|
}
|
|
1307
1351
|
|
|
1352
|
+
// packages/core/src/calls.ts
|
|
1353
|
+
var TOOL_OUTPUT_KEPT = 8e3;
|
|
1354
|
+
function keepOutput(text) {
|
|
1355
|
+
const clean = text.replace(/\s+$/, "");
|
|
1356
|
+
return clean.length <= TOOL_OUTPUT_KEPT ? clean : `${clean.slice(0, TOOL_OUTPUT_KEPT)}
|
|
1357
|
+
\u2026 trimmed here.`;
|
|
1358
|
+
}
|
|
1359
|
+
async function executeCall(call, env) {
|
|
1360
|
+
let preview;
|
|
1361
|
+
if (isWriteTool(call.name) || isExecTool(call.name)) {
|
|
1362
|
+
try {
|
|
1363
|
+
preview = previewTool(call, env.toolCtx);
|
|
1364
|
+
env.step({ status: "running", preview });
|
|
1365
|
+
} catch (err) {
|
|
1366
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1367
|
+
env.step({ status: "error", detail: message });
|
|
1368
|
+
return { id: call.id, content: message, isError: true };
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
env.step({ status: "running" });
|
|
1372
|
+
}
|
|
1373
|
+
if (needsApproval(env.mode, call)) {
|
|
1374
|
+
env.step({ status: "awaiting", preview });
|
|
1375
|
+
const ok = env.approve ? await env.approve(call, preview) : false;
|
|
1376
|
+
if (env.signal.aborted) return "aborted";
|
|
1377
|
+
if (!ok) {
|
|
1378
|
+
env.step({ status: "skipped", preview });
|
|
1379
|
+
return {
|
|
1380
|
+
id: call.id,
|
|
1381
|
+
content: env.approve ? "The learner declined this, so nothing was done. Do not retry it. Explain what you would have done and why, and let them decide what to do next." : "This needed the learner to approve it and labd is running unattended, so nothing was done. Say what you would have done; do not try another way round it."
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (env.signal.aborted) return "aborted";
|
|
1386
|
+
try {
|
|
1387
|
+
const content = await runTool(call, env.toolCtx);
|
|
1388
|
+
if (isWriteTool(call.name)) {
|
|
1389
|
+
const input = call.input;
|
|
1390
|
+
env.wrote(String(input.path ?? input.from ?? ""));
|
|
1391
|
+
}
|
|
1392
|
+
if (isExecTool(call.name)) env.ran(String(call.input.command ?? ""));
|
|
1393
|
+
env.step({ status: isWriteTool(call.name) ? "applied" : "done", preview, output: keepOutput(content) });
|
|
1394
|
+
return { id: call.id, content };
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1397
|
+
env.step({ status: "error", preview, detail: message });
|
|
1398
|
+
return { id: call.id, content: message, isError: true };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// packages/core/src/helper.ts
|
|
1403
|
+
var STALL_ROUNDS = 3;
|
|
1404
|
+
var LABEL_MAX = 80;
|
|
1405
|
+
function helperLabel(call) {
|
|
1406
|
+
const description = call.input.description;
|
|
1407
|
+
const text = typeof description === "string" ? description.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim() : "";
|
|
1408
|
+
return text ? text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}\u2026` : text : "helper";
|
|
1409
|
+
}
|
|
1410
|
+
async function runHelper(call, env) {
|
|
1411
|
+
const task = call.input.task;
|
|
1412
|
+
if (typeof task !== "string" || !task.trim()) {
|
|
1413
|
+
return { id: call.id, content: "spawn_agent needs a `task`: the complete brief for the helper.", isError: true };
|
|
1414
|
+
}
|
|
1415
|
+
const label2 = helperLabel(call);
|
|
1416
|
+
const helper = { id: call.id, label: label2 };
|
|
1417
|
+
const ask = env.approve;
|
|
1418
|
+
const capabilities = subAgentCapabilities(env.capabilities);
|
|
1419
|
+
const conversationId = subAgentConversationId(env.sessionId, call.id);
|
|
1420
|
+
let wire = [{ role: "user", text: subAgentBrief(task) }];
|
|
1421
|
+
let report = "";
|
|
1422
|
+
const asked = /* @__PURE__ */ new Set();
|
|
1423
|
+
let stalled = 0;
|
|
1424
|
+
for (; ; ) {
|
|
1425
|
+
let text = "";
|
|
1426
|
+
const calls = [];
|
|
1427
|
+
let needTools = false;
|
|
1428
|
+
let failure = null;
|
|
1429
|
+
const stream = env.client.stream(
|
|
1430
|
+
{
|
|
1431
|
+
protocol: PROTOCOL_VERSION,
|
|
1432
|
+
messages: wire,
|
|
1433
|
+
context: buildContext(env.root),
|
|
1434
|
+
capabilities,
|
|
1435
|
+
...conversationId ? { conversationId } : {},
|
|
1436
|
+
...env.effort ? { effort: env.effort } : {}
|
|
1437
|
+
},
|
|
1438
|
+
env.signal
|
|
1439
|
+
);
|
|
1440
|
+
for await (const event of stream) {
|
|
1441
|
+
if (env.signal.aborted) break;
|
|
1442
|
+
if (event.type === "text") text += event.text;
|
|
1443
|
+
else if (event.type === "tool_call") calls.push(event.call);
|
|
1444
|
+
else if (event.type === "need_tools") needTools = true;
|
|
1445
|
+
else if (event.type === "error") failure = event.message;
|
|
1446
|
+
}
|
|
1447
|
+
if (env.signal.aborted) return "aborted";
|
|
1448
|
+
if (failure) {
|
|
1449
|
+
const sofar = report ? `
|
|
1450
|
+
|
|
1451
|
+
What it had reported before that:
|
|
1452
|
+
${subAgentReport(report)}` : "";
|
|
1453
|
+
return { id: call.id, content: `The helper stopped: ${failure}${sofar}`, isError: true };
|
|
1454
|
+
}
|
|
1455
|
+
wire = [...wire, { role: "assistant", text, ...calls.length ? { toolCalls: calls } : {} }];
|
|
1456
|
+
if (text.trim()) report = text;
|
|
1457
|
+
if (!needTools || !calls.length) break;
|
|
1458
|
+
const fresh = calls.some((c) => {
|
|
1459
|
+
if (isWriteTool(c.name) || isExecTool(c.name)) return true;
|
|
1460
|
+
const key = `${c.name}:${JSON.stringify(c.input)}`;
|
|
1461
|
+
if (asked.has(key)) return false;
|
|
1462
|
+
asked.add(key);
|
|
1463
|
+
return true;
|
|
1464
|
+
});
|
|
1465
|
+
stalled = fresh ? 0 : stalled + 1;
|
|
1466
|
+
if (stalled >= STALL_ROUNDS) {
|
|
1467
|
+
report = `${report}
|
|
1468
|
+
|
|
1469
|
+
(The helper kept asking for what it had already read, so it was stopped.)`.trim();
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
const results = [];
|
|
1473
|
+
for (const c of calls) {
|
|
1474
|
+
if (env.signal.aborted) return "aborted";
|
|
1475
|
+
if (isAgentTool(c.name)) {
|
|
1476
|
+
results.push({ id: c.id, content: "A helper cannot start helpers of its own. Do this part yourself.", isError: true });
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const outcome = await executeCall(c, {
|
|
1480
|
+
mode: env.mode(),
|
|
1481
|
+
// Routed to this call's row: two helpers can run commands at once.
|
|
1482
|
+
toolCtx: { ...env.toolCtx, onCommandOutput: (line) => env.toolCtx.onCommandOutput?.(line, `${call.id}/${c.id}`) },
|
|
1483
|
+
signal: env.signal,
|
|
1484
|
+
approve: ask ? (pending, preview) => ask(pending, preview, label2) : void 0,
|
|
1485
|
+
step: (s) => env.step({ ...s, call: c, helper }),
|
|
1486
|
+
wrote: env.wrote,
|
|
1487
|
+
ran: env.ran
|
|
1488
|
+
});
|
|
1489
|
+
if (outcome === "aborted") {
|
|
1490
|
+
env.step({ call: c, helper, status: "skipped", detail: "Stopped." });
|
|
1491
|
+
return "aborted";
|
|
1492
|
+
}
|
|
1493
|
+
results.push(outcome);
|
|
1494
|
+
}
|
|
1495
|
+
wire = [...wire, { role: "tool", results }];
|
|
1496
|
+
}
|
|
1497
|
+
return { id: call.id, content: subAgentReport(report) };
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1308
1500
|
// packages/core/src/agent.ts
|
|
1309
1501
|
var CLI_CAPABILITIES = [
|
|
1310
1502
|
"read_file",
|
|
@@ -1314,18 +1506,15 @@ var CLI_CAPABILITIES = [
|
|
|
1314
1506
|
"create_file",
|
|
1315
1507
|
"move_file",
|
|
1316
1508
|
"delete_file",
|
|
1317
|
-
"run_command"
|
|
1509
|
+
"run_command",
|
|
1510
|
+
// Helpers run here, in this process, on the same rules - see helper.ts.
|
|
1511
|
+
"spawn_agent"
|
|
1318
1512
|
];
|
|
1319
|
-
var
|
|
1320
|
-
function keepOutput(text) {
|
|
1321
|
-
const clean = text.replace(/\s+$/, "");
|
|
1322
|
-
return clean.length <= TOOL_OUTPUT_KEPT ? clean : `${clean.slice(0, TOOL_OUTPUT_KEPT)}
|
|
1323
|
-
\u2026 trimmed here.`;
|
|
1324
|
-
}
|
|
1325
|
-
var STALL_ROUNDS = 3;
|
|
1513
|
+
var STALL_ROUNDS2 = 3;
|
|
1326
1514
|
var COMPACT_AT = 75;
|
|
1327
1515
|
async function runTurn(question, opts) {
|
|
1328
|
-
const { session, client, ui
|
|
1516
|
+
const { session, client, ui } = opts;
|
|
1517
|
+
const modeNow = () => opts.currentMode?.() ?? opts.mode;
|
|
1329
1518
|
const turnId = randomUUID2();
|
|
1330
1519
|
const controller = new AbortController();
|
|
1331
1520
|
opts.signal?.addEventListener("abort", () => controller.abort(), { once: true });
|
|
@@ -1334,10 +1523,17 @@ async function runTurn(question, opts) {
|
|
|
1334
1523
|
checkpoints: opts.checkpoints,
|
|
1335
1524
|
turn: { sessionId: session.id, turnId },
|
|
1336
1525
|
commandTimeoutMs: opts.commandTimeoutMs ?? 12e4,
|
|
1337
|
-
onCommandOutput: (
|
|
1526
|
+
onCommandOutput: (line2, key) => ui.commandOutput?.(line2, key)
|
|
1338
1527
|
};
|
|
1339
1528
|
session.add({ kind: "user", text: question, ...opts.attachments?.length ? { attachments: opts.attachments.map((a) => ({ path: a.path })) } : {} }, turnId);
|
|
1340
1529
|
const result = { text: "", turnId, wrote: [], ran: [], aborted: false };
|
|
1530
|
+
const ask = ui.approve;
|
|
1531
|
+
let line = Promise.resolve();
|
|
1532
|
+
const approve = ask ? (call, preview, helper) => {
|
|
1533
|
+
const answer = line.then(() => controller.signal.aborted ? false : ask(call, preview, helper));
|
|
1534
|
+
line = answer.catch(() => void 0);
|
|
1535
|
+
return answer;
|
|
1536
|
+
} : void 0;
|
|
1341
1537
|
const asked = /* @__PURE__ */ new Set();
|
|
1342
1538
|
let stalled = 0;
|
|
1343
1539
|
for (let round = 0; ; round++) {
|
|
@@ -1416,20 +1612,79 @@ async function runTurn(question, opts) {
|
|
|
1416
1612
|
return true;
|
|
1417
1613
|
});
|
|
1418
1614
|
stalled = fresh ? 0 : stalled + 1;
|
|
1419
|
-
if (stalled >=
|
|
1615
|
+
if (stalled >= STALL_ROUNDS2) {
|
|
1420
1616
|
ui.error(
|
|
1421
1617
|
`labd asked for the same things ${stalled} rounds running, so it stopped rather than keep going round. Tell it which file to start in, or ask for a narrower piece of this.`
|
|
1422
1618
|
);
|
|
1423
1619
|
session.add({ kind: "tool", results: calls.map((c) => stopped(c, "error", "Stopped: labd was repeating itself.")) }, turnId);
|
|
1424
1620
|
return result;
|
|
1425
1621
|
}
|
|
1426
|
-
const results =
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1622
|
+
const results = new Array(calls.length);
|
|
1623
|
+
const working = [];
|
|
1624
|
+
try {
|
|
1625
|
+
let helpers = 0;
|
|
1626
|
+
calls.forEach((call, at2) => {
|
|
1627
|
+
if (!isAgentTool(call.name)) return;
|
|
1628
|
+
const label2 = helperLabel(call);
|
|
1629
|
+
if (helpers++ >= MAX_SUBAGENTS) {
|
|
1630
|
+
const why = `Only ${MAX_SUBAGENTS} helpers can run at once, so this one was not started. Do it yourself, or start it once the others have reported.`;
|
|
1631
|
+
ui.tool({ call, status: "error", detail: why });
|
|
1632
|
+
results[at2] = { id: call.id, content: why, isError: true };
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
ui.tool({ call, status: "running", detail: label2 });
|
|
1636
|
+
working.push(
|
|
1637
|
+
runHelper(call, {
|
|
1638
|
+
client,
|
|
1639
|
+
root: opts.root,
|
|
1640
|
+
mode: modeNow,
|
|
1641
|
+
effort: opts.effort,
|
|
1642
|
+
capabilities: CLI_CAPABILITIES,
|
|
1643
|
+
sessionId: session.id,
|
|
1644
|
+
toolCtx,
|
|
1645
|
+
signal: controller.signal,
|
|
1646
|
+
approve,
|
|
1647
|
+
step: (step) => ui.tool(step),
|
|
1648
|
+
wrote: (path) => result.wrote.push(path),
|
|
1649
|
+
ran: (command) => result.ran.push(command)
|
|
1650
|
+
}).catch((err) => ({
|
|
1651
|
+
id: call.id,
|
|
1652
|
+
content: `The helper failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1653
|
+
isError: true
|
|
1654
|
+
})).then((outcome) => {
|
|
1655
|
+
if (outcome === "aborted") {
|
|
1656
|
+
results[at2] = stopped(call);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
results[at2] = outcome;
|
|
1660
|
+
ui.tool({ call, status: outcome.isError ? "error" : "done", detail: label2, output: keepOutput(outcome.content) });
|
|
1661
|
+
})
|
|
1662
|
+
);
|
|
1663
|
+
});
|
|
1664
|
+
for (const [at2, call] of calls.entries()) {
|
|
1665
|
+
if (isAgentTool(call.name)) continue;
|
|
1666
|
+
if (controller.signal.aborted) {
|
|
1667
|
+
results[at2] = stopped(call);
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
const outcome = await executeCall(call, {
|
|
1671
|
+
mode: modeNow(),
|
|
1672
|
+
// Output names its row, so it cannot land in a helper's command
|
|
1673
|
+
// running at the same moment.
|
|
1674
|
+
toolCtx: { ...toolCtx, onCommandOutput: (line2) => ui.commandOutput?.(line2, call.id) },
|
|
1675
|
+
signal: controller.signal,
|
|
1676
|
+
approve,
|
|
1677
|
+
step: (step) => ui.tool({ ...step, call }),
|
|
1678
|
+
wrote: (path) => result.wrote.push(path),
|
|
1679
|
+
ran: (command) => result.ran.push(command)
|
|
1680
|
+
});
|
|
1681
|
+
results[at2] = outcome === "aborted" ? stopped(call) : outcome;
|
|
1431
1682
|
}
|
|
1432
|
-
|
|
1683
|
+
await Promise.all(working);
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
controller.abort();
|
|
1686
|
+
await Promise.allSettled(working);
|
|
1687
|
+
throw err;
|
|
1433
1688
|
}
|
|
1434
1689
|
session.add({ kind: "tool", results }, turnId);
|
|
1435
1690
|
if (controller.signal.aborted) {
|
|
@@ -1441,52 +1696,6 @@ async function runTurn(question, opts) {
|
|
|
1441
1696
|
ui.tool({ call, status, detail: why });
|
|
1442
1697
|
return { id: call.id, content: why, isError: true };
|
|
1443
1698
|
}
|
|
1444
|
-
async function runOne(call) {
|
|
1445
|
-
let preview;
|
|
1446
|
-
if (isWriteTool(call.name) || isExecTool(call.name)) {
|
|
1447
|
-
try {
|
|
1448
|
-
preview = previewTool(call, toolCtx);
|
|
1449
|
-
ui.tool({ call, status: "running", preview });
|
|
1450
|
-
} catch (err) {
|
|
1451
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1452
|
-
ui.tool({ call, status: "error", detail: message });
|
|
1453
|
-
return { id: call.id, content: message, isError: true };
|
|
1454
|
-
}
|
|
1455
|
-
} else {
|
|
1456
|
-
ui.tool({ call, status: "running" });
|
|
1457
|
-
}
|
|
1458
|
-
if (needsApproval(mode, call)) {
|
|
1459
|
-
ui.tool({ call, status: "awaiting", preview });
|
|
1460
|
-
const ok = ui.approve ? await ui.approve(call, preview) : false;
|
|
1461
|
-
if (controller.signal.aborted) return stopped(call);
|
|
1462
|
-
if (!ok) {
|
|
1463
|
-
ui.tool({ call, status: "skipped", preview });
|
|
1464
|
-
return {
|
|
1465
|
-
id: call.id,
|
|
1466
|
-
content: ui.approve ? "The learner declined this, so nothing was done. Do not retry it. Explain what you would have done and why, and let them decide what to do next." : "This needed the learner to approve it and labd is running unattended, so nothing was done. Say what you would have done; do not try another way round it."
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
try {
|
|
1471
|
-
const content = await runTool(call, toolCtx);
|
|
1472
|
-
if (isWriteTool(call.name)) {
|
|
1473
|
-
const input = call.input;
|
|
1474
|
-
result.wrote.push(String(input.path ?? input.from ?? ""));
|
|
1475
|
-
}
|
|
1476
|
-
if (isExecTool(call.name)) result.ran.push(String(call.input.command ?? ""));
|
|
1477
|
-
ui.tool({
|
|
1478
|
-
call,
|
|
1479
|
-
status: isWriteTool(call.name) ? "applied" : "done",
|
|
1480
|
-
preview,
|
|
1481
|
-
output: keepOutput(content)
|
|
1482
|
-
});
|
|
1483
|
-
return { id: call.id, content };
|
|
1484
|
-
} catch (err) {
|
|
1485
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1486
|
-
ui.tool({ call, status: "error", preview, detail: message });
|
|
1487
|
-
return { id: call.id, content: message, isError: true };
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
1699
|
}
|
|
1491
1700
|
async function compact(opts) {
|
|
1492
1701
|
const controller = new AbortController();
|
|
@@ -1884,9 +2093,16 @@ function setColor(value) {
|
|
|
1884
2093
|
on = value;
|
|
1885
2094
|
}
|
|
1886
2095
|
var wrap = (open, close = "0") => (text) => on ? `${ESC}[${open}m${text}${ESC}[${close}m` : text;
|
|
2096
|
+
function lightBackground() {
|
|
2097
|
+
const bg = Number(process.env.COLORFGBG?.split(";").at(-1));
|
|
2098
|
+
return bg === 7 || bg === 15;
|
|
2099
|
+
}
|
|
1887
2100
|
var paint = {
|
|
1888
|
-
/**
|
|
1889
|
-
|
|
2101
|
+
/**
|
|
2102
|
+
* labd's accent. 214 is the nearest xterm cube entry to the brand orange.
|
|
2103
|
+
* Closes the foreground only, so it can sit inside a `band` without ending it.
|
|
2104
|
+
*/
|
|
2105
|
+
accent: wrap("38;5;214", "39"),
|
|
1890
2106
|
bold: wrap("1", "22"),
|
|
1891
2107
|
dim: wrap("2", "22"),
|
|
1892
2108
|
muted: wrap("90"),
|
|
@@ -1898,6 +2114,11 @@ var paint = {
|
|
|
1898
2114
|
/** For the one place a background is worth it: the cursor cell in the box. */
|
|
1899
2115
|
invert: wrap("7", "27")
|
|
1900
2116
|
};
|
|
2117
|
+
function band(text, visible, width) {
|
|
2118
|
+
if (!on) return text;
|
|
2119
|
+
const bg = lightBackground() ? "48;5;254" : "48;5;236";
|
|
2120
|
+
return `${ESC}[${bg}m${text}${" ".repeat(Math.max(0, width - visible))}${ESC}[49m`;
|
|
2121
|
+
}
|
|
1901
2122
|
function stripColor(text) {
|
|
1902
2123
|
return text.replace(new RegExp(`${ESC}\\[[0-9;]*m`, "g"), "");
|
|
1903
2124
|
}
|
|
@@ -2059,7 +2280,8 @@ var LABELS = {
|
|
|
2059
2280
|
create_file: "Create",
|
|
2060
2281
|
move_file: "Move",
|
|
2061
2282
|
delete_file: "Delete",
|
|
2062
|
-
run_command: "Run"
|
|
2283
|
+
run_command: "Run",
|
|
2284
|
+
spawn_agent: "Helper"
|
|
2063
2285
|
};
|
|
2064
2286
|
function approvalCard(preview, call, mode, width) {
|
|
2065
2287
|
const out = [""];
|
|
@@ -2147,21 +2369,21 @@ function wrapCommand(command, width) {
|
|
|
2147
2369
|
for (let i = 0; i < command.length; i += width) out.push(command.slice(i, i + width));
|
|
2148
2370
|
return out;
|
|
2149
2371
|
}
|
|
2150
|
-
function statusLine(
|
|
2372
|
+
function statusLine(info2) {
|
|
2151
2373
|
const bits = [];
|
|
2152
|
-
if (
|
|
2153
|
-
const crowded =
|
|
2154
|
-
bits.push((crowded ? paint.yellow : paint.muted)(`${
|
|
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`));
|
|
2155
2377
|
}
|
|
2156
|
-
bits.push(paint.muted(
|
|
2157
|
-
if (
|
|
2158
|
-
if (
|
|
2159
|
-
bits.push(paint.muted(`${
|
|
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`));
|
|
2160
2382
|
}
|
|
2161
|
-
if (
|
|
2162
|
-
const mode =
|
|
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");
|
|
2163
2385
|
bits.push(mode);
|
|
2164
|
-
const left =
|
|
2386
|
+
const left = info2.busy ? `${paint.accent(FACE)} ${paint.dim(info2.busy)}` : paint.dim(FACE);
|
|
2165
2387
|
return `${left} ${paint.dim("|")} ${bits.join(paint.dim(" | "))}`;
|
|
2166
2388
|
}
|
|
2167
2389
|
function spinnerFrame(tick) {
|
|
@@ -2198,10 +2420,10 @@ function body(opts, inner) {
|
|
|
2198
2420
|
function withCursor(view, room, hidden) {
|
|
2199
2421
|
const text = view.text.slice(0, room);
|
|
2200
2422
|
if (hidden) return text;
|
|
2201
|
-
const
|
|
2202
|
-
const before = text.slice(0,
|
|
2203
|
-
const under = text.slice(
|
|
2204
|
-
const after = text.slice(
|
|
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);
|
|
2205
2427
|
return `${before}${paint.invert(under)}${after}`;
|
|
2206
2428
|
}
|
|
2207
2429
|
function suggestionLines(all, inner) {
|
|
@@ -2250,27 +2472,27 @@ function boundaries(text) {
|
|
|
2250
2472
|
for (const { index, segment } of segmenter.segment(text)) out.push(index + segment.length);
|
|
2251
2473
|
return out;
|
|
2252
2474
|
}
|
|
2253
|
-
function prevBoundary(text,
|
|
2475
|
+
function prevBoundary(text, at2) {
|
|
2254
2476
|
const marks = boundaries(text);
|
|
2255
2477
|
let last = 0;
|
|
2256
2478
|
for (const mark of marks) {
|
|
2257
|
-
if (mark >=
|
|
2479
|
+
if (mark >= at2) return last;
|
|
2258
2480
|
last = mark;
|
|
2259
2481
|
}
|
|
2260
2482
|
return last;
|
|
2261
2483
|
}
|
|
2262
|
-
function nextBoundary(text,
|
|
2263
|
-
for (const mark of boundaries(text)) if (mark >
|
|
2484
|
+
function nextBoundary(text, at2) {
|
|
2485
|
+
for (const mark of boundaries(text)) if (mark > at2) return mark;
|
|
2264
2486
|
return text.length;
|
|
2265
2487
|
}
|
|
2266
|
-
function wordStart(text,
|
|
2267
|
-
let i =
|
|
2488
|
+
function wordStart(text, at2) {
|
|
2489
|
+
let i = at2;
|
|
2268
2490
|
while (i > 0 && /\s/.test(text[i - 1])) i--;
|
|
2269
2491
|
while (i > 0 && !/\s/.test(text[i - 1])) i--;
|
|
2270
2492
|
return i;
|
|
2271
2493
|
}
|
|
2272
|
-
function wordEnd(text,
|
|
2273
|
-
let i =
|
|
2494
|
+
function wordEnd(text, at2) {
|
|
2495
|
+
let i = at2;
|
|
2274
2496
|
while (i < text.length && /\s/.test(text[i])) i++;
|
|
2275
2497
|
while (i < text.length && !/\s/.test(text[i])) i++;
|
|
2276
2498
|
return i;
|
|
@@ -2330,6 +2552,7 @@ var LineEditor = class {
|
|
|
2330
2552
|
if (!ctrl && !meta && key.sequence && PRINTABLE.test(key.sequence)) {
|
|
2331
2553
|
return this.insert(key.sequence);
|
|
2332
2554
|
}
|
|
2555
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
2333
2556
|
return "unhandled";
|
|
2334
2557
|
}
|
|
2335
2558
|
moveTo(next) {
|
|
@@ -2398,148 +2621,1056 @@ var History = class {
|
|
|
2398
2621
|
}
|
|
2399
2622
|
};
|
|
2400
2623
|
|
|
2401
|
-
// packages/tui/src/
|
|
2402
|
-
|
|
2403
|
-
" \
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
"
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
function noiseChar() {
|
|
2414
|
-
return NOISE[Math.floor(Math.random() * NOISE.length)];
|
|
2415
|
-
}
|
|
2416
|
-
function faceLines() {
|
|
2417
|
-
return FACE_ART.map((row) => paint.accent(row));
|
|
2418
|
-
}
|
|
2419
|
-
function decodeFrames() {
|
|
2420
|
-
const cells = FACE_ART.map((row) => [...row]);
|
|
2421
|
-
const lockAt = cells.map(
|
|
2422
|
-
(row, y) => row.map((_, x) => Math.round(x * 0.55 + y * 0.9 + Math.random() * 7))
|
|
2423
|
-
);
|
|
2424
|
-
const last = Math.max(...lockAt.flat());
|
|
2425
|
-
const frames = [];
|
|
2426
|
-
for (let f = 1; f <= last + 1; f++) {
|
|
2427
|
-
frames.push(
|
|
2428
|
-
cells.map(
|
|
2429
|
-
(row, y) => paint.accent(
|
|
2430
|
-
row.map((char, x) => {
|
|
2431
|
-
if (char === " ") return " ";
|
|
2432
|
-
return f >= lockAt[y][x] ? char : noiseChar();
|
|
2433
|
-
}).join("")
|
|
2434
|
-
)
|
|
2435
|
-
)
|
|
2436
|
-
);
|
|
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, "");
|
|
2627
|
+
}
|
|
2628
|
+
var TextEditor = class {
|
|
2629
|
+
lines = [""];
|
|
2630
|
+
row = 0;
|
|
2631
|
+
col = 0;
|
|
2632
|
+
/** First line shown in the last `view`, kept so the window scrolls rather than jumps. */
|
|
2633
|
+
top = 0;
|
|
2634
|
+
get text() {
|
|
2635
|
+
return this.lines.join("\n");
|
|
2437
2636
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2637
|
+
get cursor() {
|
|
2638
|
+
return { row: this.row, col: this.col };
|
|
2639
|
+
}
|
|
2640
|
+
get lineCount() {
|
|
2641
|
+
return this.lines.length;
|
|
2642
|
+
}
|
|
2643
|
+
get isEmpty() {
|
|
2644
|
+
return this.lines.length === 1 && this.lines[0] === "";
|
|
2645
|
+
}
|
|
2646
|
+
/** The line the caret is on. Used to work out whether a `/` or `@` is being typed. */
|
|
2647
|
+
get currentLine() {
|
|
2648
|
+
return this.lines[this.row];
|
|
2649
|
+
}
|
|
2650
|
+
/** Replace everything - from history, or a completion. The caret lands at the end. */
|
|
2651
|
+
setText(text) {
|
|
2652
|
+
this.lines = sanitizeMultiline(text).split("\n");
|
|
2653
|
+
this.row = this.lines.length - 1;
|
|
2654
|
+
this.col = this.lines[this.row].length;
|
|
2655
|
+
}
|
|
2656
|
+
/** Replace just the caret's line, keeping the rest. For a completion of `/cmd` or `@path`. */
|
|
2657
|
+
setCurrentLine(text) {
|
|
2658
|
+
this.lines[this.row] = sanitizeMultiline(text).replace(/\n/g, " ");
|
|
2659
|
+
this.col = this.lines[this.row].length;
|
|
2660
|
+
}
|
|
2661
|
+
reset() {
|
|
2662
|
+
this.lines = [""];
|
|
2663
|
+
this.row = 0;
|
|
2664
|
+
this.col = 0;
|
|
2665
|
+
this.top = 0;
|
|
2666
|
+
}
|
|
2667
|
+
/** Insert at the caret. Newlines in `text` become new lines. */
|
|
2668
|
+
insert(text) {
|
|
2669
|
+
const clean = sanitizeMultiline(text);
|
|
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";
|
|
2685
|
+
}
|
|
2686
|
+
newline() {
|
|
2687
|
+
return this.insert("\n");
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Apply one key. `unhandled` for a plain Enter (the caller sends), for Up on
|
|
2691
|
+
* the first line and Down on the last (the caller walks history), and for
|
|
2692
|
+
* anything else the editor does not claim.
|
|
2693
|
+
*/
|
|
2694
|
+
apply(key) {
|
|
2695
|
+
const { name, ctrl, meta, shift } = key;
|
|
2696
|
+
const line = this.lines[this.row];
|
|
2697
|
+
if (key.paste !== void 0) return this.insert(key.paste);
|
|
2698
|
+
if (name === "return") {
|
|
2699
|
+
if (shift || ctrl || meta) return this.newline();
|
|
2700
|
+
if (/(^|[^\\])\\$/.test(line)) {
|
|
2701
|
+
this.lines[this.row] = line.slice(0, -1);
|
|
2702
|
+
this.col = Math.min(this.col, this.lines[this.row].length);
|
|
2703
|
+
return this.newline();
|
|
2460
2704
|
}
|
|
2461
|
-
|
|
2705
|
+
return "unhandled";
|
|
2462
2706
|
}
|
|
2463
|
-
if (
|
|
2464
|
-
|
|
2465
|
-
|
|
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);
|
|
2466
2745
|
}
|
|
2467
|
-
|
|
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";
|
|
2468
2765
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
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 } };
|
|
2476
2789
|
}
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
return
|
|
2790
|
+
moveTo(next) {
|
|
2791
|
+
if (next === this.col) return "same";
|
|
2792
|
+
this.col = next;
|
|
2793
|
+
return "cursor";
|
|
2481
2794
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2795
|
+
cut(from, to) {
|
|
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";
|
|
2486
2801
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
const
|
|
2490
|
-
|
|
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";
|
|
2491
2809
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
function inline(text) {
|
|
2497
|
-
const code = [];
|
|
2498
|
-
let held = text.replace(/`([^`]+)`/g, (_, body2) => {
|
|
2499
|
-
code.push(body2);
|
|
2500
|
-
return `${HOLD}${code.length - 1}${HOLD}`;
|
|
2501
|
-
});
|
|
2502
|
-
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})`)}`);
|
|
2503
|
-
return held.replace(
|
|
2504
|
-
new RegExp(`${HOLD}(\\d+)${HOLD}`, "g"),
|
|
2505
|
-
(_, n) => paint.cyan(code[Number(n)] ?? "")
|
|
2506
|
-
);
|
|
2507
|
-
}
|
|
2508
|
-
function wrap2(text, width, first, rest) {
|
|
2509
|
-
const limit = Math.max(20, width - visibleWidth(first));
|
|
2510
|
-
if (visibleWidth(text) <= limit) return [`${first}${text}`];
|
|
2511
|
-
const out = [];
|
|
2512
|
-
let line = "";
|
|
2513
|
-
for (const word of text.split(" ")) {
|
|
2514
|
-
if (line && visibleWidth(line) + 1 + visibleWidth(word) > limit) {
|
|
2515
|
-
out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
2516
|
-
line = word;
|
|
2517
|
-
} else {
|
|
2518
|
-
line = line ? `${line} ${word}` : word;
|
|
2519
|
-
}
|
|
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";
|
|
2520
2814
|
}
|
|
2521
|
-
|
|
2522
|
-
return out;
|
|
2523
|
-
}
|
|
2815
|
+
};
|
|
2524
2816
|
|
|
2525
|
-
// packages/tui/src/
|
|
2526
|
-
import { emitKeypressEvents } from "node:readline";
|
|
2817
|
+
// packages/tui/src/keys.ts
|
|
2527
2818
|
var ESC3 = "\x1B";
|
|
2528
|
-
var PASTE_ON = `${ESC3}[?2004h`;
|
|
2529
|
-
var PASTE_OFF = `${ESC3}[?2004l`;
|
|
2530
2819
|
var PASTE_START = `${ESC3}[200~`;
|
|
2531
2820
|
var PASTE_END = `${ESC3}[201~`;
|
|
2532
|
-
var
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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;
|
|
2875
|
+
}
|
|
2876
|
+
var KeyDecoder = class {
|
|
2877
|
+
/** Bytes that could still turn out to be the start of something. */
|
|
2878
|
+
pending = "";
|
|
2879
|
+
/** Inside a bracketed paste: the text so far. Null outside one. */
|
|
2880
|
+
pasting = null;
|
|
2881
|
+
/**
|
|
2882
|
+
* Decode a chunk. Sequences split across two reads are held until the next
|
|
2883
|
+
* one; a lone ESC at the END of a chunk is reported as Escape at once, because
|
|
2884
|
+
* a terminal sends a whole sequence in one write and a person pressing the key
|
|
2885
|
+
* should not have to press another to be heard.
|
|
2886
|
+
*/
|
|
2887
|
+
feed(chunk) {
|
|
2888
|
+
const out = [];
|
|
2889
|
+
let buf = this.pending + chunk;
|
|
2890
|
+
this.pending = "";
|
|
2891
|
+
let i = 0;
|
|
2892
|
+
while (i < buf.length) {
|
|
2893
|
+
if (this.pasting !== null) {
|
|
2894
|
+
const end = buf.indexOf(PASTE_END, i);
|
|
2895
|
+
if (end === -1) {
|
|
2896
|
+
const hold = partialMarker(buf, PASTE_END);
|
|
2897
|
+
this.pasting += buf.slice(i, buf.length - hold);
|
|
2898
|
+
this.pending = buf.slice(buf.length - hold);
|
|
2899
|
+
return out;
|
|
2900
|
+
}
|
|
2901
|
+
this.pasting += buf.slice(i, end);
|
|
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
|
+
}
|
|
2990
|
+
return out;
|
|
2991
|
+
}
|
|
2992
|
+
/** Anything still held back - the tail of a paste, an escape that never finished. */
|
|
2993
|
+
flush() {
|
|
2994
|
+
const out = [];
|
|
2995
|
+
if (this.pasting !== null) {
|
|
2996
|
+
out.push({ name: "paste", paste: this.pasting + this.pending });
|
|
2997
|
+
this.pasting = null;
|
|
2998
|
+
} else if (this.pending === ESC3) {
|
|
2999
|
+
out.push({ name: "escape", sequence: ESC3 });
|
|
3000
|
+
}
|
|
3001
|
+
this.pending = "";
|
|
3002
|
+
return out;
|
|
3003
|
+
}
|
|
3004
|
+
};
|
|
3005
|
+
function decodeCsi(body2, final, sequence) {
|
|
3006
|
+
if (body2.startsWith("<") && (final === "M" || final === "m")) {
|
|
3007
|
+
const [b, x, y] = body2.slice(1).split(";").map(Number);
|
|
3008
|
+
if (final === "m") return null;
|
|
3009
|
+
if (b & 32) return null;
|
|
3010
|
+
const button = b & 3;
|
|
3011
|
+
if (b & 64) {
|
|
3012
|
+
return { name: "mouse", sequence, mouse: { kind: button === 0 ? "wheel-up" : "wheel-down", button: b, x, y } };
|
|
3013
|
+
}
|
|
3014
|
+
return { name: "mouse", sequence, mouse: { kind: "click", button, x, y } };
|
|
3015
|
+
}
|
|
3016
|
+
const params = body2.split(";");
|
|
3017
|
+
if (final === "~") {
|
|
3018
|
+
if (params[0] === "27") return coded(Number(params[2]), modifiers(params[1]), sequence);
|
|
3019
|
+
if (params[0] === "200" || params[0] === "201") return null;
|
|
3020
|
+
const name2 = TILDE[params[0]];
|
|
3021
|
+
return name2 ? { name: name2, ...modifiers(params[1]), sequence } : null;
|
|
3022
|
+
}
|
|
3023
|
+
if (final === "u") return coded(Number(params[0]), modifiers(params[1]), sequence);
|
|
3024
|
+
const name = CSI_FINAL[final];
|
|
3025
|
+
if (!name) return null;
|
|
3026
|
+
const mods = final === "Z" ? { shift: true } : modifiers(params[1]);
|
|
3027
|
+
return { name, ...mods, sequence };
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// packages/tui/src/layout.ts
|
|
3031
|
+
var MAX_COMPOSER_ROWS = 8;
|
|
3032
|
+
var SIDEBAR_MIN_COLUMNS = 120;
|
|
3033
|
+
var SIDEBAR_WIDTH = 32;
|
|
3034
|
+
function layout(opts) {
|
|
3035
|
+
const rows = Math.max(4, opts.rows);
|
|
3036
|
+
const columns = Math.max(20, opts.columns);
|
|
3037
|
+
const wanted = Math.min(MAX_COMPOSER_ROWS, Math.max(1, opts.editorLines) + 2);
|
|
3038
|
+
const composerHeight = Math.max(3, Math.min(wanted, rows - 3));
|
|
3039
|
+
const header = { top: 0, left: 0, width: columns, height: 1 };
|
|
3040
|
+
const footer = { top: rows - 1, left: 0, width: columns, height: 1 };
|
|
3041
|
+
const composer2 = { top: rows - 1 - composerHeight, left: 0, width: columns, height: composerHeight };
|
|
3042
|
+
const transcriptHeight = Math.max(0, composer2.top - 1);
|
|
3043
|
+
const withSidebar = Boolean(opts.sidebar) && columns >= SIDEBAR_MIN_COLUMNS;
|
|
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
|
+
}
|
|
3054
|
+
return out;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// packages/tui/src/popup.ts
|
|
3058
|
+
var MAX_POPUP_ROWS = 8;
|
|
3059
|
+
var Popup = class {
|
|
3060
|
+
constructor(kind, items) {
|
|
3061
|
+
this.kind = kind;
|
|
3062
|
+
this.items = items;
|
|
3063
|
+
}
|
|
3064
|
+
selected = 0;
|
|
3065
|
+
get current() {
|
|
3066
|
+
return this.items[this.selected] ?? null;
|
|
3067
|
+
}
|
|
3068
|
+
move(delta) {
|
|
3069
|
+
if (!this.items.length) return;
|
|
3070
|
+
this.selected = (this.selected + delta + this.items.length) % this.items.length;
|
|
3071
|
+
}
|
|
3072
|
+
/** Rows to draw, at most `MAX_POPUP_ROWS`, keeping the selection in view. */
|
|
3073
|
+
render(width, max = MAX_POPUP_ROWS) {
|
|
3074
|
+
if (!this.items.length) return [];
|
|
3075
|
+
const rows = Math.max(1, max);
|
|
3076
|
+
const start = Math.max(0, Math.min(this.selected - rows + 1, this.items.length - rows));
|
|
3077
|
+
const shown = this.items.slice(start, start + rows);
|
|
3078
|
+
const column = Math.min(Math.max(...shown.map((s) => s.label.length)), Math.max(10, Math.floor(width / 2)));
|
|
3079
|
+
return shown.map((item, i) => {
|
|
3080
|
+
const isSelected = start + i === this.selected;
|
|
3081
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
3082
|
+
const label2 = item.label.padEnd(column);
|
|
3083
|
+
const room = width - column - 6;
|
|
3084
|
+
const what = item.what ? paint.muted(truncate(item.what, Math.max(0, room))) : "";
|
|
3085
|
+
const line = ` ${mark} ${isSelected ? paint.bold(paint.accent(label2)) : paint.accent(label2)} ${what}`;
|
|
3086
|
+
return pad2(truncate(line, width), width);
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
};
|
|
3090
|
+
function pad2(text, width) {
|
|
3091
|
+
const short = width - visibleWidth(text);
|
|
3092
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// packages/tui/src/dialog.ts
|
|
3096
|
+
function pick(title, items, onSelect) {
|
|
3097
|
+
return { kind: "pick", title, items, query: "", selected: 0, onSelect };
|
|
3098
|
+
}
|
|
3099
|
+
function info(title, lines) {
|
|
3100
|
+
return { kind: "info", title, lines, scroll: 0 };
|
|
3101
|
+
}
|
|
3102
|
+
function filterItems(items, query) {
|
|
3103
|
+
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
3104
|
+
if (!words.length) return items;
|
|
3105
|
+
return items.filter((item) => {
|
|
3106
|
+
const hay = `${item.label} ${item.hint ?? ""}`.toLowerCase();
|
|
3107
|
+
return words.every((w) => hay.includes(w));
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
function dialogKey(dialog, key) {
|
|
3111
|
+
if (key.name === "escape" || key.ctrl && (key.name === "c" || key.name === "g")) return "close";
|
|
3112
|
+
if (dialog.kind === "info") {
|
|
3113
|
+
if (key.name === "up" || key.name === "k") dialog.scroll = Math.max(0, dialog.scroll - 1);
|
|
3114
|
+
else if (key.name === "down" || key.name === "j") dialog.scroll++;
|
|
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";
|
|
3123
|
+
}
|
|
3124
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
3125
|
+
if (key.name === "up" || key.mouse?.kind === "wheel-up") {
|
|
3126
|
+
dialog.selected = shown.length ? (dialog.selected - 1 + shown.length) % shown.length : 0;
|
|
3127
|
+
return "changed";
|
|
3128
|
+
}
|
|
3129
|
+
if (key.name === "down" || key.mouse?.kind === "wheel-down" || key.name === "tab") {
|
|
3130
|
+
dialog.selected = shown.length ? (dialog.selected + 1) % shown.length : 0;
|
|
3131
|
+
return "changed";
|
|
3132
|
+
}
|
|
3133
|
+
if (key.name === "return") {
|
|
3134
|
+
const item = shown[dialog.selected];
|
|
3135
|
+
if (!item) return "same";
|
|
3136
|
+
dialog.onSelect(item);
|
|
3137
|
+
return "select";
|
|
3138
|
+
}
|
|
3139
|
+
if (key.name === "backspace") {
|
|
3140
|
+
if (!dialog.query) return "same";
|
|
3141
|
+
dialog.query = dialog.query.slice(0, -1);
|
|
3142
|
+
dialog.selected = 0;
|
|
3143
|
+
return "changed";
|
|
3144
|
+
}
|
|
3145
|
+
if (!key.ctrl && !key.meta && key.sequence && /^[^\u0000-\u001f\u007f]+$/.test(key.sequence)) {
|
|
3146
|
+
dialog.query += key.sequence;
|
|
3147
|
+
dialog.selected = 0;
|
|
3148
|
+
return "changed";
|
|
3149
|
+
}
|
|
3150
|
+
return "same";
|
|
3151
|
+
}
|
|
3152
|
+
var BOX = { tl: "\u256D", tr: "\u256E", bl: "\u2570", br: "\u256F", h: "\u2500", v: "\u2502" };
|
|
3153
|
+
function renderDialog(dialog, rows, columns) {
|
|
3154
|
+
const width = Math.max(24, Math.min(columns - 4, 84));
|
|
3155
|
+
const inner = width - 4;
|
|
3156
|
+
const maxBody = Math.max(3, rows - 6);
|
|
3157
|
+
const body2 = [];
|
|
3158
|
+
if (dialog.kind === "pick") {
|
|
3159
|
+
const shown = filterItems(dialog.items, dialog.query);
|
|
3160
|
+
const listRows = Math.max(1, Math.min(shown.length, maxBody - 2));
|
|
3161
|
+
const start = Math.max(0, Math.min(dialog.selected - listRows + 1, shown.length - listRows));
|
|
3162
|
+
body2.push(`${paint.accent("search")} ${dialog.query}${paint.invert(" ")}`);
|
|
3163
|
+
body2.push("");
|
|
3164
|
+
if (!shown.length) body2.push(paint.muted("nothing matches"));
|
|
3165
|
+
for (const [i, item] of shown.slice(start, start + listRows).entries()) {
|
|
3166
|
+
const isSelected = start + i === dialog.selected;
|
|
3167
|
+
const mark = isSelected ? paint.accent("\u25B8") : " ";
|
|
3168
|
+
const label2 = isSelected ? paint.bold(item.label) : item.label;
|
|
3169
|
+
body2.push(truncate(`${mark} ${label2}${item.hint ? ` ${paint.muted(item.hint)}` : ""}`, inner));
|
|
3170
|
+
}
|
|
3171
|
+
if (shown.length > listRows) body2.push(paint.dim(`\u2026 ${shown.length - listRows} more; type to narrow`));
|
|
3172
|
+
} else {
|
|
3173
|
+
const visible = Math.min(dialog.lines.length, maxBody);
|
|
3174
|
+
dialog.scroll = Math.max(0, Math.min(dialog.scroll, dialog.lines.length - visible));
|
|
3175
|
+
for (const line of dialog.lines.slice(dialog.scroll, dialog.scroll + visible)) body2.push(truncate(line, inner));
|
|
3176
|
+
if (dialog.lines.length > visible) {
|
|
3177
|
+
body2.push(paint.dim(`\u2026 ${dialog.scroll + visible} of ${dialog.lines.length} lines \xB7 \u2191\u2193 to scroll`));
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
const title = ` ${dialog.title} `;
|
|
3181
|
+
const rule = BOX.h.repeat(Math.max(0, width - 2 - title.length - 1));
|
|
3182
|
+
const lines = [
|
|
3183
|
+
paint.dim(`${BOX.tl}${BOX.h}`) + paint.bold(title) + paint.dim(`${rule}${BOX.tr}`),
|
|
3184
|
+
...body2.map((line) => `${paint.dim(BOX.v)} ${pad3(truncate(line, inner), inner)} ${paint.dim(BOX.v)}`),
|
|
3185
|
+
paint.dim(`${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`)
|
|
3186
|
+
];
|
|
3187
|
+
const top = Math.max(0, Math.floor((rows - lines.length) / 2));
|
|
3188
|
+
const left = Math.max(0, Math.floor((columns - width) / 2));
|
|
3189
|
+
return { top, left, lines };
|
|
3190
|
+
}
|
|
3191
|
+
function pad3(text, width) {
|
|
3192
|
+
const short = width - visibleWidth(text);
|
|
3193
|
+
return short > 0 ? `${text}${" ".repeat(short)}` : text;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// packages/tui/src/screen.ts
|
|
3197
|
+
import { EventEmitter } from "node:events";
|
|
3198
|
+
var ESC4 = "\x1B";
|
|
3199
|
+
var ALT_SCREEN_ON = `${ESC4}[?1049h`;
|
|
3200
|
+
var ALT_SCREEN_OFF = `${ESC4}[?1049l`;
|
|
3201
|
+
var CURSOR_HIDE = `${ESC4}[?25l`;
|
|
3202
|
+
var CURSOR_SHOW = `${ESC4}[?25h`;
|
|
3203
|
+
var PASTE_ON = `${ESC4}[?2004h`;
|
|
3204
|
+
var PASTE_OFF = `${ESC4}[?2004l`;
|
|
3205
|
+
var MOUSE_ON = `${ESC4}[?1000h${ESC4}[?1006h`;
|
|
3206
|
+
var MOUSE_OFF = `${ESC4}[?1006l${ESC4}[?1000l`;
|
|
3207
|
+
var CLEAR = `${ESC4}[2J`;
|
|
3208
|
+
var RESET = `${ESC4}[0m`;
|
|
3209
|
+
var at = (row, col) => `${ESC4}[${row + 1};${col + 1}H`;
|
|
3210
|
+
var Screen = class extends EventEmitter {
|
|
3211
|
+
constructor(out = process.stdout, input = process.stdin, opts = {}) {
|
|
3212
|
+
super();
|
|
3213
|
+
this.out = out;
|
|
3214
|
+
this.input = input;
|
|
3215
|
+
this.opts = opts;
|
|
3216
|
+
}
|
|
3217
|
+
last = null;
|
|
3218
|
+
lastSize = "";
|
|
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;
|
|
3224
|
+
}
|
|
3225
|
+
get columns() {
|
|
3226
|
+
const n = this.opts.columns ?? this.out.columns;
|
|
3227
|
+
return typeof n === "number" && n > 0 ? n : 80;
|
|
3228
|
+
}
|
|
3229
|
+
/** Take the terminal: alternate screen, raw keys, bracketed paste, the mouse if asked. */
|
|
3230
|
+
enter() {
|
|
3231
|
+
if (this.entered) return;
|
|
3232
|
+
this.entered = true;
|
|
3233
|
+
this.out.write(`${ALT_SCREEN_ON}${CURSOR_HIDE}${CLEAR}${PASTE_ON}${this.opts.mouse ? MOUSE_ON : ""}`);
|
|
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);
|
|
3244
|
+
}
|
|
3245
|
+
/** Give the terminal back, in the reverse order. Safe to call more than once. */
|
|
3246
|
+
leave = () => {
|
|
3247
|
+
if (!this.entered) return;
|
|
3248
|
+
this.entered = false;
|
|
3249
|
+
process.off("exit", this.leave);
|
|
3250
|
+
this.out.off?.("resize", this.onResize);
|
|
3251
|
+
this.input.off("data", this.onData);
|
|
3252
|
+
this.input.off("end", this.onEnd);
|
|
3253
|
+
try {
|
|
3254
|
+
this.input.pause();
|
|
3255
|
+
this.input.setRawMode?.(false);
|
|
3256
|
+
} catch {
|
|
3257
|
+
}
|
|
3258
|
+
this.out.write(`${this.opts.mouse ? MOUSE_OFF : ""}${PASTE_OFF}${RESET}${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
|
|
3259
|
+
this.last = null;
|
|
3260
|
+
};
|
|
3261
|
+
/**
|
|
3262
|
+
* Draw a whole frame, writing only the rows that changed.
|
|
3263
|
+
*
|
|
3264
|
+
* `frame` is read as `rows` lines; missing ones are blank, extra ones are
|
|
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.
|
|
3267
|
+
*/
|
|
3268
|
+
paint(frame, cursor = null) {
|
|
3269
|
+
if (!this.entered) return;
|
|
3270
|
+
const rows = this.rows;
|
|
3271
|
+
const columns = this.columns;
|
|
3272
|
+
const size = `${rows}x${columns}`;
|
|
3273
|
+
let out = CURSOR_HIDE;
|
|
3274
|
+
if (!this.last || this.lastSize !== size) {
|
|
3275
|
+
out += CLEAR;
|
|
3276
|
+
this.last = new Array(rows).fill(null);
|
|
3277
|
+
this.lastSize = size;
|
|
3278
|
+
}
|
|
3279
|
+
for (let r = 0; r < rows; r++) {
|
|
3280
|
+
const line = fit(frame[r] ?? "", columns);
|
|
3281
|
+
if (this.last[r] === line) continue;
|
|
3282
|
+
this.last[r] = line;
|
|
3283
|
+
out += `${at(r, 0)}${line}${RESET}`;
|
|
3284
|
+
}
|
|
3285
|
+
if (cursor) out += `${at(Math.min(cursor.row, rows - 1), Math.min(cursor.col, columns - 1))}${CURSOR_SHOW}`;
|
|
3286
|
+
this.out.write(out);
|
|
3287
|
+
}
|
|
3288
|
+
/** Forget the last frame, so the next paint redraws every row. For ctrl-l. */
|
|
3289
|
+
invalidate() {
|
|
3290
|
+
this.last = null;
|
|
3291
|
+
}
|
|
3292
|
+
onData = (chunk) => {
|
|
3293
|
+
const keys = this.decoder.feed(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
3294
|
+
if (keys.length) this.emit("keys", keys);
|
|
3295
|
+
};
|
|
3296
|
+
onEnd = () => {
|
|
3297
|
+
this.emit("end");
|
|
3298
|
+
};
|
|
3299
|
+
onResize = () => {
|
|
3300
|
+
this.last = null;
|
|
3301
|
+
this.emit("resize");
|
|
3302
|
+
};
|
|
3303
|
+
};
|
|
3304
|
+
function fit(line, width) {
|
|
3305
|
+
const cut = truncate(line, width);
|
|
3306
|
+
const short = width - visibleWidth(cut);
|
|
3307
|
+
return short > 0 ? `${cut}${" ".repeat(short)}` : cut;
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// packages/tui/src/markdown.ts
|
|
3311
|
+
function renderMarkdown(text, opts) {
|
|
3312
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
3313
|
+
const pad4 = opts.indent ?? "";
|
|
3314
|
+
const out = [];
|
|
3315
|
+
let inFence = false;
|
|
3316
|
+
let fenceLang = "";
|
|
3317
|
+
for (const line of lines) {
|
|
3318
|
+
const fence = /^\s*```(\w*)/.exec(line);
|
|
3319
|
+
if (fence) {
|
|
3320
|
+
if (!inFence) {
|
|
3321
|
+
inFence = true;
|
|
3322
|
+
fenceLang = fence[1] ?? "";
|
|
3323
|
+
out.push(paint.dim(`${pad4}${fenceLang || "code"}`));
|
|
3324
|
+
} else {
|
|
3325
|
+
inFence = false;
|
|
3326
|
+
fenceLang = "";
|
|
3327
|
+
out.push("");
|
|
3328
|
+
}
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
if (inFence) {
|
|
3332
|
+
out.push(`${paint.dim(`${pad4}|`)} ${line}`);
|
|
3333
|
+
continue;
|
|
3334
|
+
}
|
|
3335
|
+
out.push(...prose(line, opts.width, pad4));
|
|
3336
|
+
}
|
|
3337
|
+
return out.join("\n");
|
|
3338
|
+
}
|
|
3339
|
+
function prose(line, width, pad4) {
|
|
3340
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
3341
|
+
if (heading) return [`${pad4}${paint.bold(paint.accent(heading[2]))}`];
|
|
3342
|
+
if (/^\s*([-*_])\s*\1\s*\1[\s-*_]*$/.test(line)) {
|
|
3343
|
+
return [`${pad4}${paint.dim("-".repeat(Math.min(width, 40)))}`];
|
|
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]} `);
|
|
3349
|
+
}
|
|
3350
|
+
const numbered = /^(\s*)(\d+)\.\s+(.*)$/.exec(line);
|
|
3351
|
+
if (numbered) {
|
|
3352
|
+
const indent2 = `${pad4}${numbered[1]}${paint.accent(`${numbered[2]}.`)} `;
|
|
3353
|
+
return wrap2(inline(numbered[3]), width, indent2, `${pad4}${numbered[1]} `);
|
|
3354
|
+
}
|
|
3355
|
+
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
3356
|
+
if (quote) {
|
|
3357
|
+
const gutter = `${pad4}${paint.dim("|")} `;
|
|
3358
|
+
return wrap2(paint.muted(inline(quote[1])), width, gutter, gutter);
|
|
3359
|
+
}
|
|
3360
|
+
if (!line.trim()) return [""];
|
|
3361
|
+
return wrap2(inline(line), width, pad4, pad4);
|
|
3362
|
+
}
|
|
3363
|
+
var HOLD = "\0";
|
|
3364
|
+
function inline(text) {
|
|
3365
|
+
const code = [];
|
|
3366
|
+
let held = text.replace(/`([^`]+)`/g, (_, body2) => {
|
|
3367
|
+
code.push(body2);
|
|
3368
|
+
return `${HOLD}${code.length - 1}${HOLD}`;
|
|
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;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
if (line) out.push(`${out.length === 0 ? first : rest}${line}`);
|
|
3390
|
+
return out;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// packages/tui/src/transcript.ts
|
|
3394
|
+
var MAX_PROSE_WIDTH = 100;
|
|
3395
|
+
var LIVE_LINES = 8;
|
|
3396
|
+
var Transcript = class {
|
|
3397
|
+
entries = [];
|
|
3398
|
+
nextId = 1;
|
|
3399
|
+
cache = /* @__PURE__ */ new Map();
|
|
3400
|
+
/** For a streaming answer: the rendered COMPLETE part, so only the tail is re-rendered per chunk. */
|
|
3401
|
+
prose = /* @__PURE__ */ new Map();
|
|
3402
|
+
get length() {
|
|
3403
|
+
return this.entries.length;
|
|
3404
|
+
}
|
|
3405
|
+
get blocks() {
|
|
3406
|
+
return this.entries;
|
|
3407
|
+
}
|
|
3408
|
+
/** Typed by kind rather than by the literal passed, so `done: false` stays a boolean the caller can flip. */
|
|
3409
|
+
add(block) {
|
|
3410
|
+
const entry = { id: this.nextId++, version: 0, block };
|
|
3411
|
+
this.entries.push(entry);
|
|
3412
|
+
return entry;
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Move a block to the bottom. For an approval card, which must stay the last
|
|
3416
|
+
* thing on screen while it waits: a helper that keeps working adds rows below
|
|
3417
|
+
* it, and a card pushed up past the fold is one approved without being read.
|
|
3418
|
+
*/
|
|
3419
|
+
toEnd(entry) {
|
|
3420
|
+
const at2 = this.entries.indexOf(entry);
|
|
3421
|
+
if (at2 === -1 || at2 === this.entries.length - 1) return;
|
|
3422
|
+
this.entries.splice(at2, 1);
|
|
3423
|
+
this.entries.push(entry);
|
|
3424
|
+
}
|
|
3425
|
+
/** Say a block changed, so its cached lines are dropped on the next render. */
|
|
3426
|
+
touch(entry) {
|
|
3427
|
+
entry.version++;
|
|
3428
|
+
}
|
|
3429
|
+
/** Forget everything shown. The session file is untouched; this is the view. */
|
|
3430
|
+
clear() {
|
|
3431
|
+
this.entries = [];
|
|
3432
|
+
this.cache.clear();
|
|
3433
|
+
this.prose.clear();
|
|
3434
|
+
}
|
|
3435
|
+
/** Every line, top to bottom, with one blank line between blocks. */
|
|
3436
|
+
render(width) {
|
|
3437
|
+
const out = [];
|
|
3438
|
+
const live = /* @__PURE__ */ new Set();
|
|
3439
|
+
for (const entry of this.entries) {
|
|
3440
|
+
live.add(entry.id);
|
|
3441
|
+
const lines = this.lines(entry, width);
|
|
3442
|
+
if (out.length && lines.length) out.push("");
|
|
3443
|
+
out.push(...lines);
|
|
3444
|
+
}
|
|
3445
|
+
for (const id of this.cache.keys()) if (!live.has(id)) this.cache.delete(id);
|
|
3446
|
+
for (const id of this.prose.keys()) if (!live.has(id)) this.prose.delete(id);
|
|
3447
|
+
return out;
|
|
3448
|
+
}
|
|
3449
|
+
lines(entry, width) {
|
|
3450
|
+
const hit = this.cache.get(entry.id);
|
|
3451
|
+
if (hit && hit.version === entry.version && hit.width === width) return hit.lines;
|
|
3452
|
+
const lines = entry.block.kind === "assistant" ? this.assistant(entry, width) : renderBlock(entry.block, width);
|
|
3453
|
+
this.cache.set(entry.id, { version: entry.version, width, lines });
|
|
3454
|
+
return lines;
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* An answer, streaming or finished.
|
|
3458
|
+
*
|
|
3459
|
+
* Complete markdown blocks are rendered; the unfinished tail - a line still
|
|
3460
|
+
* arriving, a fence not yet closed - is shown raw and dimmed, so the reader
|
|
3461
|
+
* sees the words land without a heading flickering between plain and bold.
|
|
3462
|
+
*/
|
|
3463
|
+
assistant(entry, width) {
|
|
3464
|
+
const inner = proseWidth(width);
|
|
3465
|
+
const { text, done } = entry.block;
|
|
3466
|
+
if (done) return text.trim() ? renderMarkdown(text.replace(/\s+$/, ""), { width: inner, indent: " " }).split("\n") : [];
|
|
3467
|
+
const { complete: complete2, tail } = splitStreaming(text);
|
|
3468
|
+
let head = this.prose.get(entry.id);
|
|
3469
|
+
if (!head || head.length !== complete2.length || head.width !== width) {
|
|
3470
|
+
head = {
|
|
3471
|
+
length: complete2.length,
|
|
3472
|
+
width,
|
|
3473
|
+
lines: complete2 ? renderMarkdown(complete2, { width: inner, indent: " " }).split("\n") : []
|
|
3474
|
+
};
|
|
3475
|
+
this.prose.set(entry.id, head);
|
|
3476
|
+
}
|
|
3477
|
+
const raw = tail ? tail.split("\n").flatMap((line) => wrapPlain(line, inner - 2)).map((line) => ` ${paint.dim(line)}`) : [];
|
|
3478
|
+
return [...head.lines, ...raw];
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
function splitStreaming(text) {
|
|
3482
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
3483
|
+
let tail = lines.pop() ?? "";
|
|
3484
|
+
let open = -1;
|
|
3485
|
+
lines.forEach((line, i) => {
|
|
3486
|
+
if (/^\s*```/.test(line)) open = open === -1 ? i : -1;
|
|
3487
|
+
});
|
|
3488
|
+
let complete2 = lines;
|
|
3489
|
+
if (open !== -1) {
|
|
3490
|
+
tail = [...lines.slice(open), tail].join("\n");
|
|
3491
|
+
complete2 = lines.slice(0, open);
|
|
3492
|
+
}
|
|
3493
|
+
return { complete: complete2.join("\n"), tail };
|
|
3494
|
+
}
|
|
3495
|
+
function proseWidth(width) {
|
|
3496
|
+
return Math.max(20, Math.min(width - 2, MAX_PROSE_WIDTH));
|
|
3497
|
+
}
|
|
3498
|
+
function renderBlock(block, width) {
|
|
3499
|
+
const inner = proseWidth(width);
|
|
3500
|
+
switch (block.kind) {
|
|
3501
|
+
case "user":
|
|
3502
|
+
return block.text.split("\n").flatMap(
|
|
3503
|
+
(line, i) => wrapPlain(line, inner - 2).map((piece, j) => {
|
|
3504
|
+
const text = i === 0 && j === 0 ? `${paint.accent(">")} ${piece}` : ` ${piece}`;
|
|
3505
|
+
return band(text, visibleWidth(text), inner);
|
|
3506
|
+
})
|
|
3507
|
+
);
|
|
3508
|
+
case "assistant":
|
|
3509
|
+
return [];
|
|
3510
|
+
case "tool": {
|
|
3511
|
+
const out = toolBlock(block.name, block.detail, block.status, { input: block.input, output: block.output }, inner);
|
|
3512
|
+
if (block.live.length && !block.output) {
|
|
3513
|
+
const shown = block.live.slice(-LIVE_LINES);
|
|
3514
|
+
if (block.live.length > LIVE_LINES) out.push(` ${paint.dim(`\u2026 ${block.live.length - LIVE_LINES} more lines`)}`);
|
|
3515
|
+
for (const line of shown) out.push(` ${paint.dim(line)}`);
|
|
3516
|
+
}
|
|
3517
|
+
return out;
|
|
3518
|
+
}
|
|
3519
|
+
case "approval": {
|
|
3520
|
+
const out = [
|
|
3521
|
+
...block.helper ? [` ${paint.accent("from a helper:")} ${paint.muted(block.helper)}`] : [],
|
|
3522
|
+
...approvalCard(block.preview, block.call, block.mode, inner)
|
|
3523
|
+
];
|
|
3524
|
+
out.push(
|
|
3525
|
+
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")}`
|
|
3526
|
+
);
|
|
3527
|
+
return out;
|
|
3528
|
+
}
|
|
3529
|
+
case "error":
|
|
3530
|
+
return block.text.split("\n").map((line, i) => i === 0 ? `${paint.red(" labd:")} ${line}` : ` ${line}`);
|
|
3531
|
+
case "note":
|
|
3532
|
+
return block.text.replace(/\n$/, "").split("\n");
|
|
3533
|
+
case "compaction": {
|
|
3534
|
+
const label2 = " summarised the conversation so far ";
|
|
3535
|
+
const room = Math.max(0, inner - label2.length);
|
|
3536
|
+
return [paint.dim(` ${"\u2500".repeat(Math.floor(room / 2))}${label2}${"\u2500".repeat(Math.ceil(room / 2))}`)];
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
function wrapPlain(text, width) {
|
|
3541
|
+
const limit = Math.max(10, width);
|
|
3542
|
+
if (visibleWidth(text) <= limit) return [text];
|
|
3543
|
+
const out = [];
|
|
3544
|
+
let line = "";
|
|
3545
|
+
for (const word of text.split(" ")) {
|
|
3546
|
+
if (line && line.length + 1 + word.length > limit) {
|
|
3547
|
+
out.push(line);
|
|
3548
|
+
line = word;
|
|
3549
|
+
} else {
|
|
3550
|
+
line = line ? `${line} ${word}` : word;
|
|
3551
|
+
}
|
|
3552
|
+
while (line.length > limit) {
|
|
3553
|
+
out.push(line.slice(0, limit));
|
|
3554
|
+
line = line.slice(limit);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (line || !out.length) out.push(line);
|
|
3558
|
+
return out;
|
|
3559
|
+
}
|
|
3560
|
+
var Viewport = class {
|
|
3561
|
+
constructor(height) {
|
|
3562
|
+
this.height = height;
|
|
3563
|
+
}
|
|
3564
|
+
top = 0;
|
|
3565
|
+
follow = true;
|
|
3566
|
+
total = 0;
|
|
3567
|
+
/** The line count when the reader stopped following, so `newLines` can be counted from it. */
|
|
3568
|
+
anchor = 0;
|
|
3569
|
+
/** Lines that arrived since the reader scrolled away. Zero while following. */
|
|
3570
|
+
get newLines() {
|
|
3571
|
+
return this.follow ? 0 : Math.max(0, this.total - this.anchor);
|
|
3572
|
+
}
|
|
3573
|
+
/** Tell the viewport how many lines there are now, and how tall it is. */
|
|
3574
|
+
update(total, height = this.height) {
|
|
3575
|
+
this.height = height;
|
|
3576
|
+
this.total = total;
|
|
3577
|
+
if (this.follow) this.top = Math.max(0, total - height);
|
|
3578
|
+
else this.top = Math.max(0, Math.min(this.top, total - height));
|
|
3579
|
+
}
|
|
3580
|
+
scroll(delta) {
|
|
3581
|
+
const max = Math.max(0, this.total - this.height);
|
|
3582
|
+
const next = Math.max(0, Math.min(max, this.top + delta));
|
|
3583
|
+
if (next === this.top && this.follow) return;
|
|
3584
|
+
this.top = next;
|
|
3585
|
+
if (next >= max) this.refollow();
|
|
3586
|
+
else this.unfollow();
|
|
3587
|
+
}
|
|
3588
|
+
pageUp() {
|
|
3589
|
+
this.scroll(-Math.max(1, this.height - 1));
|
|
3590
|
+
}
|
|
3591
|
+
pageDown() {
|
|
3592
|
+
this.scroll(Math.max(1, this.height - 1));
|
|
3593
|
+
}
|
|
3594
|
+
home() {
|
|
3595
|
+
this.top = 0;
|
|
3596
|
+
if (this.total > this.height) this.unfollow();
|
|
3597
|
+
}
|
|
3598
|
+
end() {
|
|
3599
|
+
this.refollow();
|
|
3600
|
+
this.top = Math.max(0, this.total - this.height);
|
|
3601
|
+
}
|
|
3602
|
+
/** The slice of `lines` on screen. */
|
|
3603
|
+
slice(lines) {
|
|
3604
|
+
return lines.slice(this.top, this.top + this.height);
|
|
3605
|
+
}
|
|
3606
|
+
unfollow() {
|
|
3607
|
+
if (this.follow) this.anchor = this.total;
|
|
3608
|
+
this.follow = false;
|
|
3609
|
+
}
|
|
3610
|
+
refollow() {
|
|
3611
|
+
this.follow = true;
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
|
|
3615
|
+
// packages/tui/src/face.ts
|
|
3616
|
+
var FACE_ART = [
|
|
3617
|
+
" \u250F\u2501 \u2501\u2513 ",
|
|
3618
|
+
" \u2503 \u2584\u2584\u2584\u2584\u2584 \u2584\u2584\u2584\u2584\u2584 \u2503 ",
|
|
3619
|
+
" \u2503 \u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2503 ",
|
|
3620
|
+
" \u2503 \u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580 \u2503 ",
|
|
3621
|
+
" \u2503 \u2503 ",
|
|
3622
|
+
" \u2503 \u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584\u2584 \u2503 ",
|
|
3623
|
+
" \u2517\u2501 \u2501\u251B "
|
|
3624
|
+
];
|
|
3625
|
+
var NOISE = "$#@%&*+=-:.<>/\\|_01x?";
|
|
3626
|
+
var FACE_FRAME_MS = 34;
|
|
3627
|
+
function noiseChar() {
|
|
3628
|
+
return NOISE[Math.floor(Math.random() * NOISE.length)];
|
|
3629
|
+
}
|
|
3630
|
+
function faceLines() {
|
|
3631
|
+
return FACE_ART.map((row) => paint.accent(row));
|
|
3632
|
+
}
|
|
3633
|
+
function decodeFrames() {
|
|
3634
|
+
const cells = FACE_ART.map((row) => [...row]);
|
|
3635
|
+
const lockAt = cells.map(
|
|
3636
|
+
(row, y) => row.map((_, x) => Math.round(x * 0.55 + y * 0.9 + Math.random() * 7))
|
|
3637
|
+
);
|
|
3638
|
+
const last = Math.max(...lockAt.flat());
|
|
3639
|
+
const frames = [];
|
|
3640
|
+
for (let f = 1; f <= last + 1; f++) {
|
|
3641
|
+
frames.push(
|
|
3642
|
+
cells.map(
|
|
3643
|
+
(row, y) => paint.accent(
|
|
3644
|
+
row.map((char, x) => {
|
|
3645
|
+
if (char === " ") return " ";
|
|
3646
|
+
return f >= lockAt[y][x] ? char : noiseChar();
|
|
3647
|
+
}).join("")
|
|
3648
|
+
)
|
|
3649
|
+
)
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
frames.push(faceLines());
|
|
3653
|
+
return frames;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// packages/tui/src/prompt.ts
|
|
3657
|
+
import { emitKeypressEvents } from "node:readline";
|
|
3658
|
+
var ESC5 = "\x1B";
|
|
3659
|
+
var PASTE_ON2 = `${ESC5}[?2004h`;
|
|
3660
|
+
var PASTE_OFF2 = `${ESC5}[?2004l`;
|
|
3661
|
+
var PASTE_START2 = `${ESC5}[200~`;
|
|
3662
|
+
var PASTE_END2 = `${ESC5}[201~`;
|
|
3663
|
+
var Prompt = class {
|
|
3664
|
+
constructor(renderer, hooks, input = process.stdin) {
|
|
3665
|
+
this.renderer = renderer;
|
|
3666
|
+
this.hooks = hooks;
|
|
2536
3667
|
this.input = input;
|
|
2537
3668
|
emitKeypressEvents(this.input);
|
|
2538
3669
|
if (this.input.isTTY) this.input.setRawMode(true);
|
|
2539
3670
|
this.input.on("keypress", this.onKey);
|
|
2540
3671
|
this.input.on("end", this.onEnd);
|
|
2541
3672
|
this.input.resume();
|
|
2542
|
-
if (this.renderer.isTty) process.stdout.write(
|
|
3673
|
+
if (this.renderer.isTty) process.stdout.write(PASTE_ON2);
|
|
2543
3674
|
this.renderer.hideCursor();
|
|
2544
3675
|
process.stdout.on("resize", this.onResize);
|
|
2545
3676
|
process.once("exit", this.restore);
|
|
@@ -2613,7 +3744,7 @@ var Prompt = class {
|
|
|
2613
3744
|
if (this.input.isTTY) this.input.setRawMode(false);
|
|
2614
3745
|
} catch {
|
|
2615
3746
|
}
|
|
2616
|
-
if (this.renderer.isTty) process.stdout.write(
|
|
3747
|
+
if (this.renderer.isTty) process.stdout.write(PASTE_OFF2);
|
|
2617
3748
|
this.renderer.showCursor();
|
|
2618
3749
|
};
|
|
2619
3750
|
onResize = () => {
|
|
@@ -2626,11 +3757,11 @@ var Prompt = class {
|
|
|
2626
3757
|
};
|
|
2627
3758
|
onKey = (str, key = {}) => {
|
|
2628
3759
|
const sequence = key.sequence ?? str ?? "";
|
|
2629
|
-
if (sequence.includes(
|
|
3760
|
+
if (sequence.includes(PASTE_START2)) {
|
|
2630
3761
|
this.pasting = true;
|
|
2631
3762
|
return;
|
|
2632
3763
|
}
|
|
2633
|
-
if (sequence.includes(
|
|
3764
|
+
if (sequence.includes(PASTE_END2)) {
|
|
2634
3765
|
this.pasting = false;
|
|
2635
3766
|
this.draw();
|
|
2636
3767
|
return;
|
|
@@ -2776,6 +3907,7 @@ import { chmodSync, existsSync, mkdirSync as mkdirSync4, readFileSync as readFil
|
|
|
2776
3907
|
import { hostname } from "node:os";
|
|
2777
3908
|
import { join as join5 } from "node:path";
|
|
2778
3909
|
var POLL_INTERVAL_MS = 2500;
|
|
3910
|
+
var REVOKE_TIMEOUT_MS = 3e3;
|
|
2779
3911
|
var Auth = class {
|
|
2780
3912
|
constructor(stateDir2, baseUrl) {
|
|
2781
3913
|
this.baseUrl = baseUrl;
|
|
@@ -2802,18 +3934,21 @@ var Auth = class {
|
|
|
2802
3934
|
isReady() {
|
|
2803
3935
|
return Boolean(this.token());
|
|
2804
3936
|
}
|
|
3937
|
+
/** True when there was a stored token to sign out; `LABD_TOKEN` is never stored, so never revoked. */
|
|
2805
3938
|
async signOut() {
|
|
2806
3939
|
const token = this.read().token;
|
|
2807
3940
|
if (token) {
|
|
2808
3941
|
await fetch(`${this.baseUrl}/v1/auth/device/revoke`, {
|
|
2809
3942
|
method: "POST",
|
|
2810
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3943
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
3944
|
+
signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS)
|
|
2811
3945
|
}).catch(() => void 0);
|
|
2812
3946
|
}
|
|
2813
3947
|
try {
|
|
2814
3948
|
rmSync3(this.file, { force: true });
|
|
2815
3949
|
} catch {
|
|
2816
3950
|
}
|
|
3951
|
+
return Boolean(token);
|
|
2817
3952
|
}
|
|
2818
3953
|
/**
|
|
2819
3954
|
* Pair this machine with the learner's account.
|
|
@@ -2894,6 +4029,17 @@ function sleep(ms, signal) {
|
|
|
2894
4029
|
}, { once: true });
|
|
2895
4030
|
});
|
|
2896
4031
|
}
|
|
4032
|
+
function signedOutMessage(hadStored, envToken) {
|
|
4033
|
+
const lines = [];
|
|
4034
|
+
if (hadStored) lines.push("signed out, and the stored token is gone. `labd login` pairs this machine again.");
|
|
4035
|
+
else if (!envToken) lines.push("this machine was not signed in.");
|
|
4036
|
+
if (envToken) {
|
|
4037
|
+
lines.push(
|
|
4038
|
+
hadStored ? "LABD_TOKEN is still set in this shell, so the next labd will be signed in with it." : "labd is signed in with LABD_TOKEN from this shell, which is never stored - unset it to sign out."
|
|
4039
|
+
);
|
|
4040
|
+
}
|
|
4041
|
+
return lines.join("\n");
|
|
4042
|
+
}
|
|
2897
4043
|
|
|
2898
4044
|
// packages/cli/src/config.ts
|
|
2899
4045
|
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
@@ -2904,7 +4050,9 @@ var DEFAULTS = {
|
|
|
2904
4050
|
modeChosen: false,
|
|
2905
4051
|
serverUrl: "https://agent.thedevlabs.io",
|
|
2906
4052
|
commandTimeoutSeconds: 120,
|
|
2907
|
-
checkpoints: true
|
|
4053
|
+
checkpoints: true,
|
|
4054
|
+
mouse: true,
|
|
4055
|
+
sidebar: false
|
|
2908
4056
|
};
|
|
2909
4057
|
function unattendedMode(settings) {
|
|
2910
4058
|
return settings.modeChosen ? settings.mode : "ask";
|
|
@@ -2940,7 +4088,11 @@ function loadSettings(root, overrides = {}, opts = {}) {
|
|
|
2940
4088
|
commandTimeoutSeconds: clampTimeout(
|
|
2941
4089
|
overrides.commandTimeoutSeconds ?? asNumber(project.commandTimeoutSeconds) ?? asNumber(global.commandTimeoutSeconds) ?? DEFAULTS.commandTimeoutSeconds
|
|
2942
4090
|
),
|
|
2943
|
-
checkpoints: overrides.checkpoints ?? asBoolean(project.checkpoints) ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints
|
|
4091
|
+
checkpoints: overrides.checkpoints ?? asBoolean(project.checkpoints) ?? asBoolean(global.checkpoints) ?? DEFAULTS.checkpoints,
|
|
4092
|
+
// Screen preferences. Harmless from either file: neither changes what labd
|
|
4093
|
+
// may do, only how the session is drawn.
|
|
4094
|
+
mouse: overrides.mouse ?? asBoolean(project.mouse) ?? asBoolean(global.mouse) ?? DEFAULTS.mouse,
|
|
4095
|
+
sidebar: overrides.sidebar ?? asBoolean(project.sidebar) ?? asBoolean(global.sidebar) ?? DEFAULTS.sidebar
|
|
2944
4096
|
};
|
|
2945
4097
|
}
|
|
2946
4098
|
function read(path) {
|
|
@@ -2977,10 +4129,14 @@ var BUILTIN_COMMANDS = [
|
|
|
2977
4129
|
{ name: "resume", args: "<id>", what: "continue from a point in /tree" },
|
|
2978
4130
|
{ name: "sessions", what: "earlier sessions in this project" },
|
|
2979
4131
|
{ name: "session", what: "where this one is stored" },
|
|
2980
|
-
{ name: "
|
|
4132
|
+
{ name: "rename", args: "<name>", what: "name this session, so /sessions and --resume show it" },
|
|
4133
|
+
{ name: "new", what: "start a fresh session; this one stays saved" },
|
|
2981
4134
|
{ name: "usage", what: "what's left of this month's allowance" },
|
|
2982
4135
|
{ name: "trust", what: "let this project load its own extensions" },
|
|
4136
|
+
{ name: "logout", what: "sign this machine out and leave" },
|
|
2983
4137
|
{ name: "reload", what: "reload extensions and the file list" },
|
|
4138
|
+
{ name: "sidebar", what: "show or hide the side panel (full-screen session only)" },
|
|
4139
|
+
{ name: "clear", what: "clear the screen; the session itself is kept" },
|
|
2984
4140
|
{ name: "quit", what: "leave" }
|
|
2985
4141
|
];
|
|
2986
4142
|
function classify(line, extra = []) {
|
|
@@ -3167,9 +4323,12 @@ function branchOf(root) {
|
|
|
3167
4323
|
if (abbrev === null) return void 0;
|
|
3168
4324
|
return describeBranch(abbrev, abbrev === "HEAD" ? git("rev-parse", "--short", "HEAD") : null);
|
|
3169
4325
|
}
|
|
4326
|
+
var closedAt = /* @__PURE__ */ new WeakMap();
|
|
3170
4327
|
async function closeConversation(runtime) {
|
|
3171
4328
|
const messages = runtime.session.wire();
|
|
3172
4329
|
if (!messages.some((m) => m.role === "user")) return;
|
|
4330
|
+
if (closedAt.get(runtime.session) === messages.length) return;
|
|
4331
|
+
closedAt.set(runtime.session, messages.length);
|
|
3173
4332
|
try {
|
|
3174
4333
|
await runtime.client.close({
|
|
3175
4334
|
protocol: PROTOCOL_VERSION,
|
|
@@ -3188,6 +4347,9 @@ async function closeConversation(runtime) {
|
|
|
3188
4347
|
// packages/cli/src/modes/interactive.ts
|
|
3189
4348
|
import { basename as basename4 } from "node:path";
|
|
3190
4349
|
|
|
4350
|
+
// packages/cli/src/commands.ts
|
|
4351
|
+
import { dirname as dirname3 } from "node:path";
|
|
4352
|
+
|
|
3191
4353
|
// packages/cli/src/files.ts
|
|
3192
4354
|
import { readdirSync as readdirSync6 } from "node:fs";
|
|
3193
4355
|
import { join as join8, relative as relative4 } from "node:path";
|
|
@@ -3227,7 +4389,6 @@ function walk2(root, dir, out, budget) {
|
|
|
3227
4389
|
}
|
|
3228
4390
|
|
|
3229
4391
|
// packages/cli/src/commands.ts
|
|
3230
|
-
import { dirname as dirname3 } from "node:path";
|
|
3231
4392
|
async function handleCommand(input, ctx) {
|
|
3232
4393
|
const [rawName = "", ...rest] = input.replace(/^\//, "").split(/\s+/);
|
|
3233
4394
|
const name = rawName === "?" || rawName === "" ? "help" : rawName.toLowerCase();
|
|
@@ -3328,9 +4489,9 @@ async function handleCommand(input, ctx) {
|
|
|
3328
4489
|
for (const s of all) {
|
|
3329
4490
|
const when = new Date(s.at).toLocaleString();
|
|
3330
4491
|
const mark = s.id === runtime.session.id ? paint.accent(">") : " ";
|
|
3331
|
-
print(` ${mark} ${paint.dim(s.id.slice(0, 8))} ${paint.muted(`${when} ${s.messages} nodes`)} ${s
|
|
4492
|
+
print(` ${mark} ${paint.dim(s.id.slice(0, 8))} ${paint.muted(`${when} ${s.messages} nodes`)} ${sessionTitle(s)}`);
|
|
3332
4493
|
}
|
|
3333
|
-
print(` ${paint.dim("labd --resume <id> opens one.")}`);
|
|
4494
|
+
print(` ${paint.dim("labd --resume <id> opens one; labd --resume on its own lists them to pick from.")}`);
|
|
3334
4495
|
return {};
|
|
3335
4496
|
}
|
|
3336
4497
|
case "undo": {
|
|
@@ -3427,6 +4588,27 @@ async function handleCommand(input, ctx) {
|
|
|
3427
4588
|
for (const skipped of next.skipped) print(` ${paint.yellow("!")} ${paint.muted(skipped.why)}`);
|
|
3428
4589
|
return {};
|
|
3429
4590
|
}
|
|
4591
|
+
case "sidebar":
|
|
4592
|
+
case "clear":
|
|
4593
|
+
print(` ${paint.muted(`/${name} works in the full-screen session; this is the plain one (--plain).`)}`);
|
|
4594
|
+
return {};
|
|
4595
|
+
case "rename": {
|
|
4596
|
+
const name2 = sessionName(args);
|
|
4597
|
+
if (!name2) {
|
|
4598
|
+
const current = sessionName(runtime.session.displayName ?? "");
|
|
4599
|
+
print(` ${current ? paint.bold(current) : paint.muted("this session has no name yet")} ${paint.dim("- /rename <name> sets one")}`);
|
|
4600
|
+
return {};
|
|
4601
|
+
}
|
|
4602
|
+
runtime.session.rename(name2);
|
|
4603
|
+
print(` ${paint.muted("named")} ${paint.bold(name2)}`);
|
|
4604
|
+
return {};
|
|
4605
|
+
}
|
|
4606
|
+
case "logout": {
|
|
4607
|
+
if (ctx.asked) await closeConversation(runtime);
|
|
4608
|
+
const hadStored = await runtime.auth.signOut();
|
|
4609
|
+
const farewell = signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n").map((line) => ` ${paint.muted(line)}`).join("\n");
|
|
4610
|
+
return { exit: true, farewell };
|
|
4611
|
+
}
|
|
3430
4612
|
case "session":
|
|
3431
4613
|
print(` ${paint.muted(runtime.session.file)}`);
|
|
3432
4614
|
print(` ${paint.muted(`${runtime.session.size()} nodes, head ${runtime.session.headId?.slice(0, 8) ?? "none"}`)}`);
|
|
@@ -3466,10 +4648,88 @@ function help(runtime) {
|
|
|
3466
4648
|
` ${paint.dim("@path tags a file into the question. ctrl-c stops an answer.")}`
|
|
3467
4649
|
].join("\n");
|
|
3468
4650
|
}
|
|
4651
|
+
var MAX_SESSION_NAME = 80;
|
|
4652
|
+
function sessionName(raw) {
|
|
4653
|
+
return raw.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_SESSION_NAME);
|
|
4654
|
+
}
|
|
3469
4655
|
function indent(text) {
|
|
3470
4656
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
3471
4657
|
}
|
|
3472
4658
|
|
|
4659
|
+
// packages/cli/src/modes/shared.ts
|
|
4660
|
+
function describe(line, runtime) {
|
|
4661
|
+
const extras = runtime.extensions.commands.keys();
|
|
4662
|
+
const what = classify(line, extras);
|
|
4663
|
+
if (what.state === "none") return { state: "idle" };
|
|
4664
|
+
if (what.state === "ok") {
|
|
4665
|
+
return { state: "ok", hint: what.info ? paint.muted(what.info.what) : null };
|
|
4666
|
+
}
|
|
4667
|
+
if (what.state === "partial") {
|
|
4668
|
+
return { state: "partial", suggestions: matching(line, extras) };
|
|
4669
|
+
}
|
|
4670
|
+
const typed = line.trimStart().slice(1).split(/\s+/)[0] ?? "";
|
|
4671
|
+
const meant = didYouMean(typed, extras);
|
|
4672
|
+
return {
|
|
4673
|
+
state: "unknown",
|
|
4674
|
+
hint: meant ? `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("did you mean")} ${paint.accent(`/${meant}`)}${paint.dim("?")}` : `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("/help lists them all.")}`
|
|
4675
|
+
};
|
|
4676
|
+
}
|
|
4677
|
+
function complete(line, runtime) {
|
|
4678
|
+
if (line.startsWith("/") && !/\s/.test(line)) {
|
|
4679
|
+
const hits2 = matching(line, runtime.extensions.commands.keys());
|
|
4680
|
+
return hits2.length === 1 ? `/${hits2[0].name} ` : null;
|
|
4681
|
+
}
|
|
4682
|
+
const at2 = line.lastIndexOf("@");
|
|
4683
|
+
if (at2 === -1) return null;
|
|
4684
|
+
const partial = line.slice(at2 + 1);
|
|
4685
|
+
const hits = rankPaths(partial, listFiles(runtime.root));
|
|
4686
|
+
return hits.length ? `${line.slice(0, at2)}@${hits[0]} ` : null;
|
|
4687
|
+
}
|
|
4688
|
+
function fileTag(line) {
|
|
4689
|
+
const at2 = line.lastIndexOf("@");
|
|
4690
|
+
if (at2 === -1) return null;
|
|
4691
|
+
if (at2 > 0 && !/\s/.test(line[at2 - 1])) return null;
|
|
4692
|
+
const partial = line.slice(at2 + 1);
|
|
4693
|
+
if (/\s/.test(partial)) return null;
|
|
4694
|
+
return { at: at2, partial };
|
|
4695
|
+
}
|
|
4696
|
+
function label(step) {
|
|
4697
|
+
if (step.call.name === "spawn_agent") return `working with a helper: ${detail(step)}`;
|
|
4698
|
+
const own = `${step.call.name.replace(/_/g, " ")} ${detail(step)}`;
|
|
4699
|
+
return step.helper ? `${step.helper.label}: ${own}` : own;
|
|
4700
|
+
}
|
|
4701
|
+
function stepKey(step) {
|
|
4702
|
+
return step.helper ? `${step.helper.id}/${step.call.id}` : step.call.id;
|
|
4703
|
+
}
|
|
4704
|
+
function detail(step) {
|
|
4705
|
+
const input = step.call.input;
|
|
4706
|
+
if (step.call.name === "spawn_agent") return String(input.description ?? "helper");
|
|
4707
|
+
const own = String(input.why ?? input.path ?? input.from ?? input.query ?? "");
|
|
4708
|
+
return step.helper ? `\u21B3 ${step.helper.label} \xB7 ${own}` : own;
|
|
4709
|
+
}
|
|
4710
|
+
function verbatim(call) {
|
|
4711
|
+
const i = call.input;
|
|
4712
|
+
const str = (k) => typeof i[k] === "string" ? i[k] : "";
|
|
4713
|
+
switch (call.name) {
|
|
4714
|
+
case "run_command":
|
|
4715
|
+
return str("command");
|
|
4716
|
+
case "search_workspace": {
|
|
4717
|
+
const include = str("include");
|
|
4718
|
+
return include ? `${str("query")} in ${include}` : str("query");
|
|
4719
|
+
}
|
|
4720
|
+
case "move_file":
|
|
4721
|
+
return `${str("from")} \u2192 ${str("to")}`;
|
|
4722
|
+
case "list_dir":
|
|
4723
|
+
return str("path") || "/";
|
|
4724
|
+
// The whole brief: what the helper was actually told is what a learner
|
|
4725
|
+
// wants when its report surprises them.
|
|
4726
|
+
case "spawn_agent":
|
|
4727
|
+
return str("task");
|
|
4728
|
+
default:
|
|
4729
|
+
return str("path");
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
|
|
3473
4733
|
// packages/cli/src/modes/interactive.ts
|
|
3474
4734
|
async function runInteractive(runtime) {
|
|
3475
4735
|
const renderer = new Renderer();
|
|
@@ -3524,98 +4784,936 @@ async function runInteractive(runtime) {
|
|
|
3524
4784
|
renderer.print(paint.dim(" bye."));
|
|
3525
4785
|
break;
|
|
3526
4786
|
}
|
|
3527
|
-
const input = result.text;
|
|
3528
|
-
if (!input) continue;
|
|
3529
|
-
renderer.print(`${paint.accent(">")} ${input}`);
|
|
3530
|
-
if (input.startsWith("/") || input === "?") {
|
|
3531
|
-
const outcome = await handleCommand(input, {
|
|
3532
|
-
runtime,
|
|
3533
|
-
mode: state.mode,
|
|
3534
|
-
effort: state.effort,
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
if (outcome.
|
|
3539
|
-
|
|
3540
|
-
|
|
4787
|
+
const input = result.text;
|
|
4788
|
+
if (!input) continue;
|
|
4789
|
+
renderer.print(`${paint.accent(">")} ${input}`);
|
|
4790
|
+
if (input.startsWith("/") || input === "?") {
|
|
4791
|
+
const outcome = await handleCommand(input, {
|
|
4792
|
+
runtime,
|
|
4793
|
+
mode: state.mode,
|
|
4794
|
+
effort: state.effort,
|
|
4795
|
+
asked: answered > 0,
|
|
4796
|
+
print: (text) => renderer.print(text)
|
|
4797
|
+
});
|
|
4798
|
+
if (outcome.farewell) process.stderr.write(`${outcome.farewell}
|
|
4799
|
+
`);
|
|
4800
|
+
if (outcome.exit) break;
|
|
4801
|
+
if (outcome.mode) state.mode = outcome.mode;
|
|
4802
|
+
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
4803
|
+
continue;
|
|
4804
|
+
}
|
|
4805
|
+
const tags = taggedPaths(input);
|
|
4806
|
+
const attachments = tags.length ? attachTagged(runtime.root, tags) : void 0;
|
|
4807
|
+
for (const refused of attachments?.filter((a) => a.omitted) ?? []) {
|
|
4808
|
+
renderer.print(` ${paint.yellow("!")} ${paint.muted(`${refused.path}: ${refused.omitted}`)}`);
|
|
4809
|
+
}
|
|
4810
|
+
for (const handler of runtime.extensions.onTurn) {
|
|
4811
|
+
try {
|
|
4812
|
+
handler({ question: input });
|
|
4813
|
+
} catch {
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
abort = new AbortController();
|
|
4817
|
+
const markdown = new MarkdownStream(renderer.contentWidth);
|
|
4818
|
+
let opened = false;
|
|
4819
|
+
let held = null;
|
|
4820
|
+
const say = (line) => held ? held.push(line) : renderer.print(line);
|
|
4821
|
+
const ui = {
|
|
4822
|
+
text(chunk) {
|
|
4823
|
+
const ready = markdown.push(chunk);
|
|
4824
|
+
if (!ready) return;
|
|
4825
|
+
if (!opened) {
|
|
4826
|
+
renderer.print("");
|
|
4827
|
+
opened = true;
|
|
4828
|
+
}
|
|
4829
|
+
renderer.print(ready.replace(/\n$/, ""));
|
|
4830
|
+
},
|
|
4831
|
+
status(what) {
|
|
4832
|
+
startSpinner(what);
|
|
4833
|
+
},
|
|
4834
|
+
tool(step) {
|
|
4835
|
+
for (const handler of runtime.extensions.onTool) {
|
|
4836
|
+
try {
|
|
4837
|
+
handler(step);
|
|
4838
|
+
} catch {
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
if (step.status === "running" || step.status === "awaiting") {
|
|
4842
|
+
if (!held) startSpinner(label(step));
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4845
|
+
for (const line of toolBlock(
|
|
4846
|
+
step.call.name,
|
|
4847
|
+
detail(step),
|
|
4848
|
+
step.status,
|
|
4849
|
+
{ input: verbatim(step.call), output: step.output ?? step.detail },
|
|
4850
|
+
renderer.contentWidth
|
|
4851
|
+
)) {
|
|
4852
|
+
say(line);
|
|
4853
|
+
}
|
|
4854
|
+
},
|
|
4855
|
+
async approve(call, preview, helper) {
|
|
4856
|
+
stopSpinner();
|
|
4857
|
+
if (helper) renderer.print(` ${paint.accent("from a helper:")} ${paint.muted(helper)}`);
|
|
4858
|
+
for (const line of approvalCard(preview, call, state.mode, renderer.contentWidth)) {
|
|
4859
|
+
renderer.print(line);
|
|
4860
|
+
}
|
|
4861
|
+
const verb = call.name === "run_command" ? "run this command" : "apply this change";
|
|
4862
|
+
held = [];
|
|
4863
|
+
try {
|
|
4864
|
+
const answer = await prompt.read(`${verb}? [y/N]`);
|
|
4865
|
+
const ok = answer.kind === "line" && /^y(es)?$/i.test(answer.text.trim());
|
|
4866
|
+
renderer.print(` ${ok ? paint.green("allowed") : paint.muted("declined")}`);
|
|
4867
|
+
return ok;
|
|
4868
|
+
} finally {
|
|
4869
|
+
const waiting = held;
|
|
4870
|
+
held = null;
|
|
4871
|
+
for (const line of waiting) renderer.print(line);
|
|
4872
|
+
}
|
|
4873
|
+
},
|
|
4874
|
+
error(message) {
|
|
4875
|
+
stopSpinner();
|
|
4876
|
+
renderer.print(`${paint.red(" labd:")} ${message}`);
|
|
4877
|
+
},
|
|
4878
|
+
done(info2) {
|
|
4879
|
+
if (info2.context) state.context = info2.context;
|
|
4880
|
+
if (info2.credits) state.credits = info2.credits;
|
|
4881
|
+
},
|
|
4882
|
+
commandOutput(line) {
|
|
4883
|
+
say(paint.dim(` | ${line.replace(/\n/g, "\n | ")}`));
|
|
4884
|
+
}
|
|
4885
|
+
};
|
|
4886
|
+
startSpinner("thinking");
|
|
4887
|
+
const turn = await runTurn(input, {
|
|
4888
|
+
session: runtime.session,
|
|
4889
|
+
client: runtime.client,
|
|
4890
|
+
root: runtime.root,
|
|
4891
|
+
mode: state.mode,
|
|
4892
|
+
checkpoints: runtime.checkpoints,
|
|
4893
|
+
effort: state.effort,
|
|
4894
|
+
attachments,
|
|
4895
|
+
commandTimeoutMs: runtime.settings.commandTimeoutSeconds * 1e3,
|
|
4896
|
+
ui,
|
|
4897
|
+
signal: abort.signal
|
|
4898
|
+
});
|
|
4899
|
+
abort = null;
|
|
4900
|
+
stopSpinner();
|
|
4901
|
+
const tail = markdown.flush();
|
|
4902
|
+
if (tail) {
|
|
4903
|
+
if (!opened) renderer.print("");
|
|
4904
|
+
renderer.print(tail.replace(/\n$/, ""));
|
|
4905
|
+
}
|
|
4906
|
+
if (turn.wrote.length && runtime.checkpoints) {
|
|
4907
|
+
renderer.print(
|
|
4908
|
+
paint.dim(
|
|
4909
|
+
` ${turn.wrote.length} file${turn.wrote.length === 1 ? "" : "s"} changed. /undo puts this answer back.`
|
|
4910
|
+
)
|
|
4911
|
+
);
|
|
4912
|
+
}
|
|
4913
|
+
answered++;
|
|
4914
|
+
if (state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
4915
|
+
startSpinner("summarising, to make room");
|
|
4916
|
+
const ok = await compact({ session: runtime.session, client: runtime.client, root: runtime.root });
|
|
4917
|
+
stopSpinner();
|
|
4918
|
+
renderer.print(paint.dim(ok ? " summarised the conversation so far." : " (could not summarise; carrying on)"));
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
} finally {
|
|
4922
|
+
stopSpinner();
|
|
4923
|
+
prompt.close();
|
|
4924
|
+
}
|
|
4925
|
+
if (answered) {
|
|
4926
|
+
process.stderr.write(paint.dim(`
|
|
4927
|
+
${FACE} session saved. Continue it with: labd --resume ${runtime.session.id.slice(0, 8)}
|
|
4928
|
+
`));
|
|
4929
|
+
await closeConversation(runtime);
|
|
4930
|
+
}
|
|
4931
|
+
return 0;
|
|
4932
|
+
}
|
|
4933
|
+
async function banner(renderer, runtime, mode) {
|
|
4934
|
+
const where = basename4(runtime.root);
|
|
4935
|
+
renderer.print("");
|
|
4936
|
+
const still = !renderer.isTty || Boolean(process.env.LABD_NO_MOTION);
|
|
4937
|
+
if (still) {
|
|
4938
|
+
for (const line of faceLines()) renderer.print(line);
|
|
4939
|
+
} else {
|
|
4940
|
+
renderer.hideCursor();
|
|
4941
|
+
for (const frame of decodeFrames()) {
|
|
4942
|
+
renderer.draw(frame);
|
|
4943
|
+
await sleep2(FACE_FRAME_MS);
|
|
4944
|
+
}
|
|
4945
|
+
renderer.clear();
|
|
4946
|
+
for (const line of faceLines()) renderer.print(line);
|
|
4947
|
+
}
|
|
4948
|
+
renderer.print("");
|
|
4949
|
+
renderer.print(`${paint.accent(FACE)} ${paint.bold("labd")} ${paint.dim(`in ${where}${runtime.branch ? ` on ${runtime.branch}` : ""}`)}`);
|
|
4950
|
+
renderer.print(` ${paint.dim(MODE_CAPTION[mode])}`);
|
|
4951
|
+
if (runtime.settings.ignoredProjectServerUrl) {
|
|
4952
|
+
renderer.print(
|
|
4953
|
+
` ${paint.yellow("!")} ${paint.muted(
|
|
4954
|
+
`ignoring serverUrl in .labd/settings.json (${runtime.settings.ignoredProjectServerUrl}) - this project isn't trusted; \`labd trust\` if you meant it`
|
|
4955
|
+
)}`
|
|
4956
|
+
);
|
|
4957
|
+
}
|
|
4958
|
+
for (const skipped of runtime.extensions.skipped) {
|
|
4959
|
+
renderer.print(` ${paint.yellow("!")} ${paint.muted(`extensions: ${skipped.why}`)}`);
|
|
4960
|
+
}
|
|
4961
|
+
renderer.print("");
|
|
4962
|
+
}
|
|
4963
|
+
function sleep2(ms) {
|
|
4964
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
// packages/cli/src/modes/screen.ts
|
|
4968
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
4969
|
+
import { basename as basename5, dirname as dirname4 } from "node:path";
|
|
4970
|
+
|
|
4971
|
+
// packages/cli/src/modes/serial.ts
|
|
4972
|
+
var Serial = class {
|
|
4973
|
+
constructor(opts) {
|
|
4974
|
+
this.opts = opts;
|
|
4975
|
+
}
|
|
4976
|
+
pending = [];
|
|
4977
|
+
current = null;
|
|
4978
|
+
get busy() {
|
|
4979
|
+
return this.current !== null;
|
|
4980
|
+
}
|
|
4981
|
+
/** What is waiting, oldest first. */
|
|
4982
|
+
get queued() {
|
|
4983
|
+
return this.pending;
|
|
4984
|
+
}
|
|
4985
|
+
/** Where the running work can be awaited, for a clean exit. */
|
|
4986
|
+
get running() {
|
|
4987
|
+
return this.current;
|
|
4988
|
+
}
|
|
4989
|
+
/** Send now if nothing is running and nothing holds the line, else wait behind it. */
|
|
4990
|
+
submit(text) {
|
|
4991
|
+
if (this.current || this.opts.held() || this.pending.length) {
|
|
4992
|
+
this.pending.push(text);
|
|
4993
|
+
this.opts.changed();
|
|
4994
|
+
return "queued";
|
|
4995
|
+
}
|
|
4996
|
+
this.start(() => this.opts.send(text));
|
|
4997
|
+
return "sent";
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Run something that is not a typed line - picking a session from a list, say
|
|
5001
|
+
* - under the same rule. Refused rather than queued when busy: the choice was
|
|
5002
|
+
* made against what was on screen, and by the time it ran that would be gone.
|
|
5003
|
+
*/
|
|
5004
|
+
run(work) {
|
|
5005
|
+
if (this.current) return false;
|
|
5006
|
+
this.start(work);
|
|
5007
|
+
return true;
|
|
5008
|
+
}
|
|
5009
|
+
/** Start the next line, if there is one and nothing holds it. Call when a hold lifts. */
|
|
5010
|
+
drain() {
|
|
5011
|
+
if (this.current || this.opts.held()) return;
|
|
5012
|
+
const next = this.pending.shift();
|
|
5013
|
+
if (next === void 0) return;
|
|
5014
|
+
this.start(() => this.opts.send(next));
|
|
5015
|
+
}
|
|
5016
|
+
/**
|
|
5017
|
+
* Stop waiting for what is running, without stopping it. For a command that
|
|
5018
|
+
* never finishes - an extension's, say - which would otherwise hold the line
|
|
5019
|
+
* for good. Whatever it does later lands late; nothing waits on it.
|
|
5020
|
+
*/
|
|
5021
|
+
release() {
|
|
5022
|
+
if (!this.current) return;
|
|
5023
|
+
this.current = null;
|
|
5024
|
+
this.opts.changed();
|
|
5025
|
+
this.drain();
|
|
5026
|
+
}
|
|
5027
|
+
/** Empty the line and hand back what was in it, so a stop does not lose it. */
|
|
5028
|
+
take() {
|
|
5029
|
+
const out = this.pending;
|
|
5030
|
+
this.pending = [];
|
|
5031
|
+
if (out.length) this.opts.changed();
|
|
5032
|
+
return out;
|
|
5033
|
+
}
|
|
5034
|
+
start(work) {
|
|
5035
|
+
const done = (async () => {
|
|
5036
|
+
try {
|
|
5037
|
+
await work();
|
|
5038
|
+
} catch (err) {
|
|
5039
|
+
this.opts.failed(err);
|
|
5040
|
+
}
|
|
5041
|
+
})();
|
|
5042
|
+
this.current = done;
|
|
5043
|
+
this.opts.changed();
|
|
5044
|
+
void done.then(() => {
|
|
5045
|
+
if (this.current === done) this.current = null;
|
|
5046
|
+
this.opts.changed();
|
|
5047
|
+
this.drain();
|
|
5048
|
+
});
|
|
5049
|
+
}
|
|
5050
|
+
};
|
|
5051
|
+
|
|
5052
|
+
// packages/cli/src/modes/screen.ts
|
|
5053
|
+
var FRAME_MS = 33;
|
|
5054
|
+
var QUIT_WINDOW_MS = 2e3;
|
|
5055
|
+
var WHEEL_LINES = 3;
|
|
5056
|
+
var MAX_QUEUED_SHOWN = 3;
|
|
5057
|
+
var ESC6 = "\x1B";
|
|
5058
|
+
async function runScreen(runtime, opts) {
|
|
5059
|
+
const screen = new Screen(process.stdout, process.stdin, { mouse: opts.mouse });
|
|
5060
|
+
const transcript = new Transcript();
|
|
5061
|
+
const viewport = new Viewport(10);
|
|
5062
|
+
const editor = new TextEditor();
|
|
5063
|
+
const history = new History();
|
|
5064
|
+
const state = {
|
|
5065
|
+
mode: runtime.settings.mode,
|
|
5066
|
+
/**
|
|
5067
|
+
* The mode the running turn is held to, when shift+tab has moved `mode` up
|
|
5068
|
+
* to `agent` mid-answer. Going up does not apply until the next question -
|
|
5069
|
+
* a habit keypress must not let the very next command run unasked - while
|
|
5070
|
+
* going down applies at once, being the safe direction. Null when the turn
|
|
5071
|
+
* simply follows `mode`.
|
|
5072
|
+
*/
|
|
5073
|
+
heldMode: null,
|
|
5074
|
+
effort: runtime.settings.effort,
|
|
5075
|
+
context: null,
|
|
5076
|
+
credits: null,
|
|
5077
|
+
sidebar: opts.sidebar,
|
|
5078
|
+
tick: 0,
|
|
5079
|
+
/** What labd is doing right now, for the footer. Null when idle. */
|
|
5080
|
+
busy: null,
|
|
5081
|
+
abort: null,
|
|
5082
|
+
popup: null,
|
|
5083
|
+
/** The line the popup was dismissed on, so Esc is not undone by the next frame. */
|
|
5084
|
+
popupDismissed: null,
|
|
5085
|
+
dialog: null,
|
|
5086
|
+
approval: null,
|
|
5087
|
+
/** The assistant block a streaming answer is going into. */
|
|
5088
|
+
answer: null,
|
|
5089
|
+
toolBlocks: /* @__PURE__ */ new Map(),
|
|
5090
|
+
lastSteps: [],
|
|
5091
|
+
ctrlCAt: 0,
|
|
5092
|
+
/** A one-line message in the footer, and when it expires. */
|
|
5093
|
+
notice: null,
|
|
5094
|
+
/** Printed to stderr once the screen is given back. */
|
|
5095
|
+
farewell: null,
|
|
5096
|
+
/** A question was asked in the CURRENT session, so leaving should say so and close it. */
|
|
5097
|
+
asked: false,
|
|
5098
|
+
quitting: false,
|
|
5099
|
+
faceIndex: 0,
|
|
5100
|
+
dirty: null,
|
|
5101
|
+
conventions: 0,
|
|
5102
|
+
skills: 0
|
|
5103
|
+
};
|
|
5104
|
+
const serial = new Serial({
|
|
5105
|
+
send: (text) => text.startsWith("/") || text === "?" ? command(text) : ask(text),
|
|
5106
|
+
held: () => state.dialog !== null || state.quitting,
|
|
5107
|
+
failed: (err) => transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) }),
|
|
5108
|
+
changed: () => schedule()
|
|
5109
|
+
});
|
|
5110
|
+
const faceFrames = process.env.LABD_NO_MOTION ? [faceLines()] : decodeFrames();
|
|
5111
|
+
let frameTimer = null;
|
|
5112
|
+
const schedule = () => {
|
|
5113
|
+
if (frameTimer) return;
|
|
5114
|
+
frameTimer = setTimeout(() => {
|
|
5115
|
+
frameTimer = null;
|
|
5116
|
+
draw();
|
|
5117
|
+
}, FRAME_MS);
|
|
5118
|
+
};
|
|
5119
|
+
let spinner = null;
|
|
5120
|
+
const setBusy = (what) => {
|
|
5121
|
+
state.busy = what;
|
|
5122
|
+
if (what && !spinner) {
|
|
5123
|
+
spinner = setInterval(() => {
|
|
5124
|
+
state.tick++;
|
|
5125
|
+
schedule();
|
|
5126
|
+
}, 400);
|
|
5127
|
+
}
|
|
5128
|
+
if (!what && spinner) {
|
|
5129
|
+
clearInterval(spinner);
|
|
5130
|
+
spinner = null;
|
|
5131
|
+
}
|
|
5132
|
+
schedule();
|
|
5133
|
+
};
|
|
5134
|
+
let resolveExit = null;
|
|
5135
|
+
const exited = new Promise((resolve2) => {
|
|
5136
|
+
resolveExit = resolve2;
|
|
5137
|
+
});
|
|
5138
|
+
const quit = () => {
|
|
5139
|
+
if (state.quitting) return;
|
|
5140
|
+
state.quitting = true;
|
|
5141
|
+
state.abort?.abort();
|
|
5142
|
+
resolveExit?.();
|
|
5143
|
+
};
|
|
5144
|
+
const note = (text) => {
|
|
5145
|
+
transcript.add({ kind: "note", text });
|
|
5146
|
+
schedule();
|
|
5147
|
+
};
|
|
5148
|
+
const notice = (text, ms = QUIT_WINDOW_MS) => {
|
|
5149
|
+
state.notice = { text, until: Date.now() + ms };
|
|
5150
|
+
schedule();
|
|
5151
|
+
setTimeout(schedule, ms + 10);
|
|
5152
|
+
};
|
|
5153
|
+
function draw() {
|
|
5154
|
+
if (state.quitting) return;
|
|
5155
|
+
const rows = screen.rows;
|
|
5156
|
+
const columns = screen.columns;
|
|
5157
|
+
const L = layout({ rows, columns, editorLines: editor.lineCount, sidebar: state.sidebar });
|
|
5158
|
+
const frame = [];
|
|
5159
|
+
let cursor = null;
|
|
5160
|
+
frame.push(header(columns));
|
|
5161
|
+
const width = L.transcript.width;
|
|
5162
|
+
const lines = transcript.length ? transcript.render(width) : emptyState(width, L.transcript.height);
|
|
5163
|
+
viewport.update(lines.length, L.transcript.height);
|
|
5164
|
+
const visible = viewport.slice(lines);
|
|
5165
|
+
while (visible.length < L.transcript.height) visible.push("");
|
|
5166
|
+
if (!viewport.follow && viewport.newLines > 0 && visible.length) {
|
|
5167
|
+
const n = viewport.newLines;
|
|
5168
|
+
const tag = paint.accent(`\u2193 ${n} new line${n === 1 ? "" : "s"} \xB7 end to follow`);
|
|
5169
|
+
visible[visible.length - 1] = fit(" ".repeat(Math.max(0, width - visibleWidth(tag) - 1)) + tag, width);
|
|
5170
|
+
}
|
|
5171
|
+
const overlays = [];
|
|
5172
|
+
if (serial.queued.length && !state.approval) overlays.push(queuedLines(width));
|
|
5173
|
+
if (state.popup && !state.dialog) overlays.push(state.popup.render(width));
|
|
5174
|
+
for (const rowsOut of overlays) {
|
|
5175
|
+
const start = visible.length - rowsOut.length;
|
|
5176
|
+
rowsOut.forEach((line, i) => {
|
|
5177
|
+
if (start + i >= 0) visible[start + i] = line;
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
const body2 = [...visible.map((l) => fit(l, width)), ...composer2(L.composer.width, L.composer.height)];
|
|
5181
|
+
if (L.sidebar) {
|
|
5182
|
+
const side = sidebar(L.sidebar.width);
|
|
5183
|
+
body2.forEach((line, i) => {
|
|
5184
|
+
body2[i] = `${line}${paint.dim("\u2502")}${fit(side[i] ?? "", L.sidebar.width)}`;
|
|
5185
|
+
});
|
|
5186
|
+
}
|
|
5187
|
+
frame.push(...body2);
|
|
5188
|
+
frame.push(footer(columns));
|
|
5189
|
+
if (!state.dialog && !state.approval && !state.quitting) {
|
|
5190
|
+
const view = editor.view(L.composer.width - 6, L.composer.height - 2);
|
|
5191
|
+
cursor = { row: L.composer.top + 1 + view.cursor.row, col: 4 + view.cursor.col };
|
|
5192
|
+
}
|
|
5193
|
+
if (state.dialog) {
|
|
5194
|
+
const placed = renderDialog(state.dialog, rows, columns);
|
|
5195
|
+
placed.lines.forEach((line, i) => {
|
|
5196
|
+
const row = placed.top + i;
|
|
5197
|
+
if (row < frame.length) frame[row] = overlay(frame[row] ?? "", placed.left, line, columns);
|
|
5198
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
screen.paint(frame, cursor);
|
|
5201
|
+
}
|
|
5202
|
+
function header(width) {
|
|
5203
|
+
const session = runtime.session.id.slice(0, 8);
|
|
5204
|
+
const left = [
|
|
5205
|
+
`${paint.accent(FACE)} ${paint.bold("labd")}`,
|
|
5206
|
+
`${paint.muted(basename5(runtime.root))}${runtime.branch ? ` ${paint.muted(runtime.branch)}` : ""}`,
|
|
5207
|
+
paint.dim(`session ${session}`)
|
|
5208
|
+
].join(paint.dim(" \xB7 "));
|
|
5209
|
+
const shown = state.mode === "agent" ? paint.red(paint.bold("agent")) : state.mode === "auto" ? paint.accent("auto") : paint.muted("ask");
|
|
5210
|
+
const mode = state.heldMode ? `${shown}${paint.dim(" (next question)")}` : shown;
|
|
5211
|
+
const right = `${mode}${state.effort ? paint.dim(` \xB7 think ${state.effort}`) : ""} `;
|
|
5212
|
+
return spread(` ${left}`, right, width);
|
|
5213
|
+
}
|
|
5214
|
+
function footer(width) {
|
|
5215
|
+
const d = paint.dim;
|
|
5216
|
+
let left;
|
|
5217
|
+
if (state.notice && state.notice.until > Date.now()) left = paint.yellow(state.notice.text);
|
|
5218
|
+
else if (state.dialog) left = d("\u2191\u2193 move \xB7 enter choose \xB7 type to search \xB7 esc close");
|
|
5219
|
+
else if (state.approval) left = `${paint.yellow("y")} ${d("allow \xB7")} ${paint.yellow("n")} ${d("decline \xB7 ctrl+c stop")}`;
|
|
5220
|
+
else if (state.busy) left = `${paint.accent(state.busy)}${spinnerFrame(state.tick)} ${d("enter queues \xB7 ctrl+c stop \xB7 pgup/pgdn scroll")}`;
|
|
5221
|
+
else if (state.popup) left = d("\u2191\u2193 move \xB7 tab/enter accept \xB7 esc close");
|
|
5222
|
+
else {
|
|
5223
|
+
const described = describe(editor.currentLine, runtime);
|
|
5224
|
+
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");
|
|
5225
|
+
}
|
|
5226
|
+
const bits = [];
|
|
5227
|
+
if (state.context) {
|
|
5228
|
+
const crowded = state.context.percentUsed >= 75;
|
|
5229
|
+
bits.push((crowded ? paint.yellow : paint.muted)(`context ${state.context.percentUsed}%`));
|
|
5230
|
+
}
|
|
5231
|
+
if (state.credits) bits.push(paint.muted(`credits ${state.credits.percentLeft}%`));
|
|
5232
|
+
return spread(` ${left}`, `${bits.join(paint.dim(" \xB7 "))} `, width);
|
|
5233
|
+
}
|
|
5234
|
+
function composer2(width, height) {
|
|
5235
|
+
const inner = width - 4;
|
|
5236
|
+
const textRows = Math.max(1, height - 2);
|
|
5237
|
+
const colour = state.approval ? paint.yellow : state.busy ? paint.accent : borderColour2(describe(editor.currentLine, runtime).state);
|
|
5238
|
+
const out = [];
|
|
5239
|
+
out.push(colour(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`));
|
|
5240
|
+
const view = editor.view(inner - 2, textRows);
|
|
5241
|
+
const lines = [...view.lines];
|
|
5242
|
+
while (lines.length < textRows) lines.push("");
|
|
5243
|
+
lines.forEach((line, i) => {
|
|
5244
|
+
const first = i === 0 && editor.cursor.row - view.cursor.row === 0;
|
|
5245
|
+
const sigil = first ? state.busy ? paint.dim(">") : paint.accent(">") : " ";
|
|
5246
|
+
let text = line;
|
|
5247
|
+
if (editor.isEmpty && i === 0) {
|
|
5248
|
+
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");
|
|
5249
|
+
}
|
|
5250
|
+
out.push(`${colour("\u2502")} ${sigil} ${fit(text, inner - 2)} ${colour("\u2502")}`);
|
|
5251
|
+
});
|
|
5252
|
+
out.push(colour(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`));
|
|
5253
|
+
return out;
|
|
5254
|
+
}
|
|
5255
|
+
function emptyState(width, height) {
|
|
5256
|
+
const face = faceFrames[Math.min(state.faceIndex, faceFrames.length - 1)];
|
|
5257
|
+
const hint = paint.dim("ask labd something - / for commands - @ to tag a file");
|
|
5258
|
+
const caption = wrapPlain(MODE_CAPTION[state.mode], Math.max(20, width - 4)).map((l) => paint.dim(l));
|
|
5259
|
+
const body2 = [...face, "", hint, "", ...caption];
|
|
5260
|
+
const top = Math.max(0, Math.floor((height - body2.length) / 2));
|
|
5261
|
+
return [...Array(top).fill(""), ...body2.map((line) => centre(line, width))];
|
|
5262
|
+
}
|
|
5263
|
+
function sidebar(width) {
|
|
5264
|
+
const row = (k, v) => `${paint.muted(k.padEnd(12))}${v}`;
|
|
5265
|
+
const lines = [
|
|
5266
|
+
` ${paint.bold("project")}`,
|
|
5267
|
+
` ${row("name", basename5(runtime.root))}`,
|
|
5268
|
+
` ${row("branch", runtime.branch ?? paint.dim("none"))}`,
|
|
5269
|
+
` ${row("changed", state.dirty === null ? paint.dim("-") : `${state.dirty} file${state.dirty === 1 ? "" : "s"}`)}`,
|
|
5270
|
+
` ${row("conventions", String(state.conventions))}`,
|
|
5271
|
+
` ${row("skills", String(state.skills))}`,
|
|
5272
|
+
"",
|
|
5273
|
+
` ${paint.bold("session")}`,
|
|
5274
|
+
` ${row("mode", state.mode)}`,
|
|
5275
|
+
` ${row("think", state.effort ?? paint.dim("default"))}`,
|
|
5276
|
+
` ${row("context", state.context ? `${state.context.percentUsed}%` : paint.dim("-"))}`,
|
|
5277
|
+
` ${row("credits", state.credits ? `${state.credits.percentLeft}% left` : paint.dim("-"))}`,
|
|
5278
|
+
"",
|
|
5279
|
+
` ${paint.bold("recent steps")}`,
|
|
5280
|
+
...state.lastSteps.length ? state.lastSteps.map((s) => ` ${paint.muted(s)}`) : [` ${paint.dim("none yet")}`]
|
|
5281
|
+
];
|
|
5282
|
+
return lines.map((l) => fit(l, width));
|
|
5283
|
+
}
|
|
5284
|
+
function onKeys(keys) {
|
|
5285
|
+
for (const key of keys) onKey(key);
|
|
5286
|
+
}
|
|
5287
|
+
function onKey(key) {
|
|
5288
|
+
if (key.ctrl && key.name === "c") return interrupt();
|
|
5289
|
+
if (state.dialog) {
|
|
5290
|
+
const r = dialogKey(state.dialog, key);
|
|
5291
|
+
if (r === "close" || r === "select") closeDialog();
|
|
5292
|
+
return schedule();
|
|
5293
|
+
}
|
|
5294
|
+
if (key.mouse) {
|
|
5295
|
+
if (key.mouse.kind === "wheel-up") viewport.scroll(-WHEEL_LINES);
|
|
5296
|
+
else if (key.mouse.kind === "wheel-down") viewport.scroll(WHEEL_LINES);
|
|
5297
|
+
return schedule();
|
|
5298
|
+
}
|
|
5299
|
+
if (key.name === "pageup") return viewport.pageUp(), schedule();
|
|
5300
|
+
if (key.name === "pagedown") return viewport.pageDown(), schedule();
|
|
5301
|
+
if (key.name === "home" && editor.isEmpty) return viewport.home(), schedule();
|
|
5302
|
+
if (key.name === "end" && editor.isEmpty) return viewport.end(), schedule();
|
|
5303
|
+
if (state.approval) {
|
|
5304
|
+
if (key.sequence === "y" || key.sequence === "Y") return answerApproval(true);
|
|
5305
|
+
if (key.sequence === "n" || key.sequence === "N" || key.name === "escape") return answerApproval(false);
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
if (key.name === "tab" && key.shift) return cycleMode();
|
|
5309
|
+
if (key.ctrl && key.name === "d") {
|
|
5310
|
+
if (editor.isEmpty) quit();
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5313
|
+
if (key.ctrl && key.name === "l") return screen.invalidate(), schedule();
|
|
5314
|
+
if (key.ctrl && key.name === "n") return void serial.submit("/new");
|
|
5315
|
+
if (key.ctrl && key.name === "o") return void serial.submit("/sessions");
|
|
5316
|
+
if (key.name === "escape") {
|
|
5317
|
+
if (state.popup) {
|
|
5318
|
+
state.popup = null;
|
|
5319
|
+
state.popupDismissed = editor.currentLine;
|
|
5320
|
+
} else if (!viewport.follow) viewport.end();
|
|
5321
|
+
return schedule();
|
|
5322
|
+
}
|
|
5323
|
+
if (state.popup) {
|
|
5324
|
+
if (key.name === "up") return state.popup.move(-1), schedule();
|
|
5325
|
+
if (key.name === "down") return state.popup.move(1), schedule();
|
|
5326
|
+
if (key.name === "tab" || key.name === "return" && !key.shift && !key.ctrl) return acceptPopup();
|
|
5327
|
+
}
|
|
5328
|
+
if (key.name === "tab") {
|
|
5329
|
+
const completed = complete(editor.currentLine, runtime);
|
|
5330
|
+
if (completed !== null) editor.setCurrentLine(completed);
|
|
5331
|
+
refreshPopup();
|
|
5332
|
+
return schedule();
|
|
5333
|
+
}
|
|
5334
|
+
const outcome = editor.apply(key);
|
|
5335
|
+
if (outcome === "unhandled") {
|
|
5336
|
+
if (key.name === "return") return submit();
|
|
5337
|
+
if (key.name === "up" || key.name === "down") {
|
|
5338
|
+
const line = key.name === "up" ? history.up(editor.text) : history.down();
|
|
5339
|
+
if (line !== null) {
|
|
5340
|
+
editor.setText(line);
|
|
5341
|
+
refreshPopup();
|
|
5342
|
+
schedule();
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (outcome === "text") refreshPopup();
|
|
5348
|
+
if (outcome !== "same") schedule();
|
|
5349
|
+
}
|
|
5350
|
+
function interrupt() {
|
|
5351
|
+
if (state.approval) {
|
|
5352
|
+
answerApproval(false);
|
|
5353
|
+
}
|
|
5354
|
+
if (state.abort) {
|
|
5355
|
+
state.abort.abort();
|
|
5356
|
+
state.abort = null;
|
|
5357
|
+
note(paint.dim(" stopped."));
|
|
5358
|
+
unqueue();
|
|
5359
|
+
return;
|
|
5360
|
+
}
|
|
5361
|
+
if (serial.busy && serial.queued.length) {
|
|
5362
|
+
unqueue();
|
|
5363
|
+
notice("the queue is back in the box; ctrl+c again stops waiting for the command", 2500);
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
if (serial.busy) {
|
|
5367
|
+
serial.release();
|
|
5368
|
+
setBusy(null);
|
|
5369
|
+
note(paint.dim(" stopped waiting. The command may still finish in the background."));
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
if (state.dialog) {
|
|
5373
|
+
closeDialog();
|
|
5374
|
+
return schedule();
|
|
5375
|
+
}
|
|
5376
|
+
if (!editor.isEmpty) {
|
|
5377
|
+
editor.reset();
|
|
5378
|
+
state.popup = null;
|
|
5379
|
+
return schedule();
|
|
5380
|
+
}
|
|
5381
|
+
const now = Date.now();
|
|
5382
|
+
if (now - state.ctrlCAt < QUIT_WINDOW_MS) return quit();
|
|
5383
|
+
state.ctrlCAt = now;
|
|
5384
|
+
notice("press ctrl+c again to quit");
|
|
5385
|
+
}
|
|
5386
|
+
function effectiveMode() {
|
|
5387
|
+
return state.heldMode ?? state.mode;
|
|
5388
|
+
}
|
|
5389
|
+
function cycleMode() {
|
|
5390
|
+
const previous = effectiveMode();
|
|
5391
|
+
const next = AGENT_MODES[(AGENT_MODES.indexOf(state.mode) + 1) % AGENT_MODES.length];
|
|
5392
|
+
state.mode = next;
|
|
5393
|
+
const running = state.abort !== null;
|
|
5394
|
+
state.heldMode = next === "agent" && running ? previous : null;
|
|
5395
|
+
notice(`${next}: ${MODE_CAPTION[next]}`, 4e3);
|
|
5396
|
+
if (next === "agent") {
|
|
5397
|
+
note(
|
|
5398
|
+
` ${paint.red(paint.bold("agent"))} ${paint.muted(
|
|
5399
|
+
running ? `- from your next question. This answer stays on ${previous}.` : "- commands now run without asking, and cannot be put back."
|
|
5400
|
+
)}
|
|
5401
|
+
${paint.muted("For this session only. shift+tab again to leave it.")}`
|
|
5402
|
+
);
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
function answerApproval(ok) {
|
|
5406
|
+
const approval = state.approval;
|
|
5407
|
+
if (!approval) return;
|
|
5408
|
+
state.approval = null;
|
|
5409
|
+
approval.entry.block.answer = ok ? "allowed" : "declined";
|
|
5410
|
+
transcript.touch(approval.entry);
|
|
5411
|
+
approval.resolve(ok);
|
|
5412
|
+
schedule();
|
|
5413
|
+
}
|
|
5414
|
+
function refreshPopup() {
|
|
5415
|
+
const line = editor.currentLine;
|
|
5416
|
+
if (state.popupDismissed !== null && state.popupDismissed === line) return;
|
|
5417
|
+
state.popupDismissed = null;
|
|
5418
|
+
if (line.startsWith("/") && !/\s/.test(line)) {
|
|
5419
|
+
if (classify(line, runtime.extensions.commands.keys()).state !== "partial") {
|
|
5420
|
+
state.popup = null;
|
|
5421
|
+
return;
|
|
5422
|
+
}
|
|
5423
|
+
const items = matching(line, runtime.extensions.commands.keys()).map((c) => ({
|
|
5424
|
+
label: `/${c.name}`,
|
|
5425
|
+
what: c.what,
|
|
5426
|
+
value: `/${c.name} `
|
|
5427
|
+
}));
|
|
5428
|
+
state.popup = items.length ? new Popup("command", items) : null;
|
|
5429
|
+
return;
|
|
5430
|
+
}
|
|
5431
|
+
const tag = fileTag(line);
|
|
5432
|
+
if (tag) {
|
|
5433
|
+
const hits = rankPaths(tag.partial, listFiles(runtime.root), 8);
|
|
5434
|
+
state.popup = hits.length ? new Popup(
|
|
5435
|
+
"file",
|
|
5436
|
+
hits.map((path) => ({ label: path, value: `${line.slice(0, tag.at)}@${path} ` }))
|
|
5437
|
+
) : null;
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
5440
|
+
state.popup = null;
|
|
5441
|
+
}
|
|
5442
|
+
function acceptPopup() {
|
|
5443
|
+
const item = state.popup?.current;
|
|
5444
|
+
state.popup = null;
|
|
5445
|
+
if (item) editor.setCurrentLine(item.value);
|
|
5446
|
+
schedule();
|
|
5447
|
+
}
|
|
5448
|
+
function submit() {
|
|
5449
|
+
const text = editor.text.trim();
|
|
5450
|
+
if (!text) return;
|
|
5451
|
+
history.add(text);
|
|
5452
|
+
editor.reset();
|
|
5453
|
+
state.popup = null;
|
|
5454
|
+
viewport.end();
|
|
5455
|
+
serial.submit(text);
|
|
5456
|
+
schedule();
|
|
5457
|
+
}
|
|
5458
|
+
async function command(text) {
|
|
5459
|
+
setBusy(`running ${text.split(/\s+/)[0]}`);
|
|
5460
|
+
try {
|
|
5461
|
+
await runCommand2(text);
|
|
5462
|
+
} finally {
|
|
5463
|
+
setBusy(null);
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
function closeDialog() {
|
|
5467
|
+
state.dialog = null;
|
|
5468
|
+
serial.drain();
|
|
5469
|
+
}
|
|
5470
|
+
function unqueue() {
|
|
5471
|
+
const queued = serial.take();
|
|
5472
|
+
if (!queued.length) return;
|
|
5473
|
+
editor.setText([...queued, editor.text].filter(Boolean).join("\n"));
|
|
5474
|
+
state.popup = null;
|
|
5475
|
+
}
|
|
5476
|
+
function queuedLines(width) {
|
|
5477
|
+
const shown = serial.queued.slice(0, MAX_QUEUED_SHOWN);
|
|
5478
|
+
const lines = shown.map((text) => fit(` ${paint.dim("queued")} ${paint.muted(text.split("\n")[0] ?? "")}`, width));
|
|
5479
|
+
const more = serial.queued.length - shown.length;
|
|
5480
|
+
if (more > 0) lines.push(paint.dim(` and ${more} more`));
|
|
5481
|
+
return lines;
|
|
5482
|
+
}
|
|
5483
|
+
async function runCommand2(input) {
|
|
5484
|
+
transcript.add({ kind: "user", text: input });
|
|
5485
|
+
const [rawName = ""] = input.replace(/^\//, "").split(/\s+/);
|
|
5486
|
+
const name = rawName === "?" || rawName === "" ? "help" : rawName.toLowerCase();
|
|
5487
|
+
switch (name) {
|
|
5488
|
+
case "help":
|
|
5489
|
+
state.dialog = info("help", helpLines());
|
|
5490
|
+
return schedule();
|
|
5491
|
+
case "sessions":
|
|
5492
|
+
return openSessions();
|
|
5493
|
+
case "tree":
|
|
5494
|
+
return openTree();
|
|
5495
|
+
case "sidebar":
|
|
5496
|
+
state.sidebar = !state.sidebar;
|
|
5497
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5498
|
+
note(paint.muted(state.sidebar ? " side panel on. It needs 120 columns to show." : " side panel off."));
|
|
5499
|
+
return;
|
|
5500
|
+
case "clear":
|
|
5501
|
+
transcript.clear();
|
|
5502
|
+
state.toolBlocks.clear();
|
|
5503
|
+
state.answer = null;
|
|
5504
|
+
return schedule();
|
|
5505
|
+
case "new":
|
|
5506
|
+
return newSession();
|
|
5507
|
+
default:
|
|
5508
|
+
break;
|
|
5509
|
+
}
|
|
5510
|
+
const outcome = await handleCommand(input, {
|
|
5511
|
+
runtime,
|
|
5512
|
+
mode: state.mode,
|
|
5513
|
+
effort: state.effort,
|
|
5514
|
+
asked: state.asked,
|
|
5515
|
+
print: (text) => note(text)
|
|
5516
|
+
});
|
|
5517
|
+
if (outcome.farewell) state.farewell = outcome.farewell;
|
|
5518
|
+
if (outcome.exit) return quit();
|
|
5519
|
+
if (outcome.mode) {
|
|
5520
|
+
state.mode = outcome.mode;
|
|
5521
|
+
state.heldMode = null;
|
|
5522
|
+
}
|
|
5523
|
+
if (outcome.effort !== void 0) state.effort = outcome.effort;
|
|
5524
|
+
schedule();
|
|
5525
|
+
}
|
|
5526
|
+
function helpLines() {
|
|
5527
|
+
const rows = BUILTIN_COMMANDS.map((c) => [`/${c.name}${c.args ? ` ${c.args}` : ""}`, c.what]);
|
|
5528
|
+
for (const [cmd, command2] of runtime.extensions.commands) rows.push([`/${cmd}`, `${command2.describe} ${paint.dim("(extension)")}`]);
|
|
5529
|
+
const width = Math.max(...rows.map(([n]) => n.length));
|
|
5530
|
+
const keys = [
|
|
5531
|
+
["enter", "send"],
|
|
5532
|
+
["shift+enter, ctrl+j, or a trailing \\", "new line"],
|
|
5533
|
+
["/ and @", "commands and files, with a list"],
|
|
5534
|
+
["tab", "complete"],
|
|
5535
|
+
["shift+tab", "next mode: ask, auto, agent"],
|
|
5536
|
+
["pgup / pgdn, wheel", "scroll the transcript"],
|
|
5537
|
+
["home / end", "top / bottom (with an empty box)"],
|
|
5538
|
+
["esc", "close a list or dialog; jump back to the bottom"],
|
|
5539
|
+
["ctrl+c", "stop the answer; clear the box; twice to quit"],
|
|
5540
|
+
["ctrl+d", "quit"],
|
|
5541
|
+
["ctrl+n / ctrl+o", "new session / earlier sessions"],
|
|
5542
|
+
["ctrl+l", "redraw"]
|
|
5543
|
+
];
|
|
5544
|
+
const kwidth = Math.max(...keys.map(([k]) => k.length));
|
|
5545
|
+
return [
|
|
5546
|
+
paint.bold("commands"),
|
|
5547
|
+
...rows.map(([n, w]) => `${paint.accent(n.padEnd(width))} ${paint.muted(w)}`),
|
|
5548
|
+
"",
|
|
5549
|
+
paint.bold("keys"),
|
|
5550
|
+
...keys.map(([k, w]) => `${paint.accent(k.padEnd(kwidth))} ${paint.muted(w)}`),
|
|
5551
|
+
"",
|
|
5552
|
+
paint.dim("@path tags a file into the question. --plain runs the older print-style session.")
|
|
5553
|
+
];
|
|
5554
|
+
}
|
|
5555
|
+
function openSessions() {
|
|
5556
|
+
const all = Session.list(dirname4(runtime.session.file));
|
|
5557
|
+
if (!all.length) return note(paint.muted(" no earlier sessions in this project"));
|
|
5558
|
+
state.dialog = pick(
|
|
5559
|
+
"sessions - enter resumes one",
|
|
5560
|
+
all.map((s) => ({
|
|
5561
|
+
id: s.file,
|
|
5562
|
+
label: `${s.id === runtime.session.id ? "> " : " "}${sessionTitle(s)}`,
|
|
5563
|
+
hint: `${s.id.slice(0, 8)} ${new Date(s.at).toLocaleString()} ${s.messages} nodes`
|
|
5564
|
+
})),
|
|
5565
|
+
(item) => {
|
|
5566
|
+
if (!serial.run(() => switchSession(item.id))) notice("labd is still working - pick again when it is done", 2500);
|
|
5567
|
+
}
|
|
5568
|
+
);
|
|
5569
|
+
schedule();
|
|
5570
|
+
}
|
|
5571
|
+
function openTree() {
|
|
5572
|
+
const nodes = runtime.session.all();
|
|
5573
|
+
if (!nodes.length) return note(paint.muted(" nothing in this session yet"));
|
|
5574
|
+
const head = runtime.session.headId;
|
|
5575
|
+
state.dialog = pick(
|
|
5576
|
+
"this session - enter continues from a point",
|
|
5577
|
+
nodes.map((n) => ({
|
|
5578
|
+
id: n.id,
|
|
5579
|
+
label: `${n.id === head ? "> " : " "}${n.id.slice(0, 8)} ${n.body.kind.padEnd(9)}`,
|
|
5580
|
+
hint: summarise2(n.body)
|
|
5581
|
+
})),
|
|
5582
|
+
(item) => {
|
|
5583
|
+
if (!serial.run(() => runCommand2(`/resume ${item.id.slice(0, 8)}`))) notice("labd is still working - pick again when it is done", 2500);
|
|
3541
5584
|
}
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
5585
|
+
);
|
|
5586
|
+
schedule();
|
|
5587
|
+
}
|
|
5588
|
+
async function switchSession(file) {
|
|
5589
|
+
const current = runtime.session;
|
|
5590
|
+
if (Session.list(dirname4(current.file)).find((s) => s.file === file)?.id === current.id) {
|
|
5591
|
+
return note(paint.muted(" that is this session."));
|
|
5592
|
+
}
|
|
5593
|
+
if (state.asked) await closeConversation(runtime);
|
|
5594
|
+
runtime.session = Session.open(file);
|
|
5595
|
+
state.asked = false;
|
|
5596
|
+
transcript.clear();
|
|
5597
|
+
state.toolBlocks.clear();
|
|
5598
|
+
state.answer = null;
|
|
5599
|
+
replay();
|
|
5600
|
+
note(paint.muted(` resumed ${runtime.session.id.slice(0, 8)}.`));
|
|
5601
|
+
}
|
|
5602
|
+
async function newSession() {
|
|
5603
|
+
if (state.asked) await closeConversation(runtime);
|
|
5604
|
+
runtime.session = Session.create(bucketFor(runtime.stateDir, runtime.root));
|
|
5605
|
+
state.asked = false;
|
|
5606
|
+
transcript.clear();
|
|
5607
|
+
state.toolBlocks.clear();
|
|
5608
|
+
state.answer = null;
|
|
5609
|
+
note(paint.muted(` new session ${runtime.session.id.slice(0, 8)}. The previous one is saved; /sessions lists it.`));
|
|
5610
|
+
}
|
|
5611
|
+
function replay() {
|
|
5612
|
+
for (const m of runtime.session.wire()) {
|
|
5613
|
+
if (m.role === "user" && m.text) transcript.add({ kind: "user", text: m.text });
|
|
5614
|
+
else if (m.role === "assistant" && m.text) transcript.add({ kind: "assistant", text: m.text, done: true });
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
async function ask(input) {
|
|
5618
|
+
state.heldMode = null;
|
|
5619
|
+
transcript.add({ kind: "user", text: input });
|
|
5620
|
+
state.asked = true;
|
|
5621
|
+
state.answer = null;
|
|
5622
|
+
const tags = taggedPaths(input);
|
|
5623
|
+
const attachments = tags.length ? attachTagged(runtime.root, tags) : void 0;
|
|
5624
|
+
for (const refused of attachments?.filter((a) => a.omitted) ?? []) {
|
|
5625
|
+
note(` ${paint.yellow("!")} ${paint.muted(`${refused.path}: ${refused.omitted}`)}`);
|
|
5626
|
+
}
|
|
5627
|
+
for (const handler of runtime.extensions.onTurn) {
|
|
5628
|
+
try {
|
|
5629
|
+
handler({ question: input });
|
|
5630
|
+
} catch {
|
|
3546
5631
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
5632
|
+
}
|
|
5633
|
+
const abort = new AbortController();
|
|
5634
|
+
state.abort = abort;
|
|
5635
|
+
const ui = {
|
|
5636
|
+
text(chunk) {
|
|
5637
|
+
if (!state.answer || state.answer.block.done) {
|
|
5638
|
+
state.answer = transcript.add({ kind: "assistant", text: "", done: false });
|
|
3551
5639
|
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
}
|
|
3564
|
-
renderer.print(ready.replace(/\n$/, ""));
|
|
3565
|
-
},
|
|
3566
|
-
status(what) {
|
|
3567
|
-
startSpinner(what);
|
|
3568
|
-
},
|
|
3569
|
-
tool(step) {
|
|
3570
|
-
for (const handler of runtime.extensions.onTool) {
|
|
3571
|
-
try {
|
|
3572
|
-
handler(step);
|
|
3573
|
-
} catch {
|
|
3574
|
-
}
|
|
3575
|
-
}
|
|
3576
|
-
if (step.status === "running" || step.status === "awaiting") {
|
|
3577
|
-
startSpinner(label(step));
|
|
3578
|
-
return;
|
|
3579
|
-
}
|
|
3580
|
-
for (const line of toolBlock(
|
|
3581
|
-
step.call.name,
|
|
3582
|
-
detail(step),
|
|
3583
|
-
step.status,
|
|
3584
|
-
{ input: verbatim(step.call), output: step.output ?? step.detail },
|
|
3585
|
-
renderer.contentWidth
|
|
3586
|
-
)) {
|
|
3587
|
-
renderer.print(line);
|
|
5640
|
+
state.answer.block.text += chunk;
|
|
5641
|
+
transcript.touch(state.answer);
|
|
5642
|
+
schedule();
|
|
5643
|
+
},
|
|
5644
|
+
status(what) {
|
|
5645
|
+
setBusy(what);
|
|
5646
|
+
},
|
|
5647
|
+
tool(step) {
|
|
5648
|
+
for (const handler of runtime.extensions.onTool) {
|
|
5649
|
+
try {
|
|
5650
|
+
handler(step);
|
|
5651
|
+
} catch {
|
|
3588
5652
|
}
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
5653
|
+
}
|
|
5654
|
+
closeAnswer();
|
|
5655
|
+
const key = stepKey(step);
|
|
5656
|
+
let entry = state.toolBlocks.get(key);
|
|
5657
|
+
if (!entry) {
|
|
5658
|
+
entry = transcript.add({
|
|
5659
|
+
kind: "tool",
|
|
5660
|
+
name: step.call.name,
|
|
5661
|
+
detail: detail(step),
|
|
5662
|
+
status: step.status,
|
|
5663
|
+
input: verbatim(step.call) || void 0,
|
|
5664
|
+
live: []
|
|
5665
|
+
});
|
|
5666
|
+
state.toolBlocks.set(key, entry);
|
|
5667
|
+
if (state.approval) {
|
|
5668
|
+
transcript.toEnd(state.approval.entry);
|
|
5669
|
+
if (viewport.follow) viewport.end();
|
|
3594
5670
|
}
|
|
3595
|
-
const verb = call.name === "run_command" ? "run this command" : "apply this change";
|
|
3596
|
-
const answer = await prompt.read(`${verb}? [y/N]`);
|
|
3597
|
-
const ok = answer.kind === "line" && /^y(es)?$/i.test(answer.text.trim());
|
|
3598
|
-
renderer.print(` ${ok ? paint.green("allowed") : paint.muted("declined")}`);
|
|
3599
|
-
return ok;
|
|
3600
|
-
},
|
|
3601
|
-
error(message) {
|
|
3602
|
-
stopSpinner();
|
|
3603
|
-
renderer.print(`${paint.red(" labd:")} ${message}`);
|
|
3604
|
-
},
|
|
3605
|
-
done(info) {
|
|
3606
|
-
if (info.context) state.context = info.context;
|
|
3607
|
-
if (info.credits) state.credits = info.credits;
|
|
3608
|
-
},
|
|
3609
|
-
commandOutput(line) {
|
|
3610
|
-
renderer.print(paint.dim(` | ${line.replace(/\n/g, "\n | ")}`));
|
|
3611
5671
|
}
|
|
3612
|
-
|
|
3613
|
-
|
|
5672
|
+
entry.block.status = step.status;
|
|
5673
|
+
if (step.status !== "running" && step.status !== "awaiting") {
|
|
5674
|
+
entry.block.output = step.output ?? step.detail;
|
|
5675
|
+
remember(`${label(step)}`.trim());
|
|
5676
|
+
}
|
|
5677
|
+
transcript.touch(entry);
|
|
5678
|
+
if (step.status === "running" || step.status === "awaiting") setBusy(label(step));
|
|
5679
|
+
schedule();
|
|
5680
|
+
},
|
|
5681
|
+
approve(call, preview, helper) {
|
|
5682
|
+
setBusy(null);
|
|
5683
|
+
closeAnswer();
|
|
5684
|
+
const entry = transcript.add({ kind: "approval", call, preview, helper, mode: effectiveMode(), answer: null });
|
|
5685
|
+
viewport.end();
|
|
5686
|
+
return new Promise((resolve2) => {
|
|
5687
|
+
state.approval = { entry, resolve: resolve2 };
|
|
5688
|
+
schedule();
|
|
5689
|
+
});
|
|
5690
|
+
},
|
|
5691
|
+
error(message) {
|
|
5692
|
+
setBusy(null);
|
|
5693
|
+
transcript.add({ kind: "error", text: message });
|
|
5694
|
+
schedule();
|
|
5695
|
+
},
|
|
5696
|
+
done(info2) {
|
|
5697
|
+
if (info2.context) state.context = info2.context;
|
|
5698
|
+
if (info2.credits) state.credits = info2.credits;
|
|
5699
|
+
schedule();
|
|
5700
|
+
},
|
|
5701
|
+
commandOutput(line, key) {
|
|
5702
|
+
const target = key ? state.toolBlocks.get(key) : void 0;
|
|
5703
|
+
if (!target) return;
|
|
5704
|
+
target.block.live.push(...line.replace(/\n$/, "").split("\n"));
|
|
5705
|
+
transcript.touch(target);
|
|
5706
|
+
schedule();
|
|
5707
|
+
}
|
|
5708
|
+
};
|
|
5709
|
+
setBusy("thinking");
|
|
5710
|
+
try {
|
|
3614
5711
|
const turn = await runTurn(input, {
|
|
3615
5712
|
session: runtime.session,
|
|
3616
5713
|
client: runtime.client,
|
|
3617
5714
|
root: runtime.root,
|
|
3618
5715
|
mode: state.mode,
|
|
5716
|
+
currentMode: effectiveMode,
|
|
3619
5717
|
checkpoints: runtime.checkpoints,
|
|
3620
5718
|
effort: state.effort,
|
|
3621
5719
|
attachments,
|
|
@@ -3623,126 +5721,171 @@ async function runInteractive(runtime) {
|
|
|
3623
5721
|
ui,
|
|
3624
5722
|
signal: abort.signal
|
|
3625
5723
|
});
|
|
3626
|
-
|
|
3627
|
-
stopSpinner();
|
|
3628
|
-
const tail = markdown.flush();
|
|
3629
|
-
if (tail) {
|
|
3630
|
-
if (!opened) renderer.print("");
|
|
3631
|
-
renderer.print(tail.replace(/\n$/, ""));
|
|
3632
|
-
}
|
|
5724
|
+
closeAnswer();
|
|
3633
5725
|
if (turn.wrote.length && runtime.checkpoints) {
|
|
3634
|
-
|
|
3635
|
-
paint.dim(
|
|
3636
|
-
` ${turn.wrote.length} file${turn.wrote.length === 1 ? "" : "s"} changed. /undo puts this answer back.`
|
|
3637
|
-
)
|
|
3638
|
-
);
|
|
5726
|
+
note(paint.dim(` ${turn.wrote.length} file${turn.wrote.length === 1 ? "" : "s"} changed. /undo puts this answer back.`));
|
|
3639
5727
|
}
|
|
3640
|
-
|
|
3641
|
-
if (state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
3642
|
-
|
|
5728
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5729
|
+
if (!abort.signal.aborted && state.context && state.context.percentUsed >= COMPACT_AT) {
|
|
5730
|
+
setBusy("summarising, to make room");
|
|
3643
5731
|
const ok = await compact({ session: runtime.session, client: runtime.client, root: runtime.root });
|
|
3644
|
-
|
|
3645
|
-
|
|
5732
|
+
if (ok) transcript.add({ kind: "compaction" });
|
|
5733
|
+
else note(paint.dim(" (could not summarise; carrying on)"));
|
|
3646
5734
|
}
|
|
5735
|
+
} catch (err) {
|
|
5736
|
+
transcript.add({ kind: "error", text: err instanceof Error ? err.message : String(err) });
|
|
5737
|
+
} finally {
|
|
5738
|
+
if (state.abort === abort) state.abort = null;
|
|
5739
|
+
state.heldMode = null;
|
|
5740
|
+
if (state.approval) answerApproval(false);
|
|
5741
|
+
setBusy(null);
|
|
3647
5742
|
}
|
|
3648
|
-
} finally {
|
|
3649
|
-
stopSpinner();
|
|
3650
|
-
prompt.close();
|
|
3651
5743
|
}
|
|
3652
|
-
|
|
5744
|
+
function closeAnswer() {
|
|
5745
|
+
const answer = state.answer;
|
|
5746
|
+
if (!answer || answer.block.done) return;
|
|
5747
|
+
answer.block.done = true;
|
|
5748
|
+
transcript.touch(answer);
|
|
5749
|
+
}
|
|
5750
|
+
function remember(step) {
|
|
5751
|
+
state.lastSteps.push(step);
|
|
5752
|
+
if (state.lastSteps.length > 5) state.lastSteps.shift();
|
|
5753
|
+
}
|
|
5754
|
+
function refreshProjectFacts() {
|
|
5755
|
+
try {
|
|
5756
|
+
const out = execFileSync3("git", ["status", "--porcelain"], {
|
|
5757
|
+
cwd: runtime.root,
|
|
5758
|
+
encoding: "utf8",
|
|
5759
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5760
|
+
timeout: 2e3
|
|
5761
|
+
});
|
|
5762
|
+
state.dirty = out.split("\n").filter(Boolean).length;
|
|
5763
|
+
} catch {
|
|
5764
|
+
state.dirty = null;
|
|
5765
|
+
}
|
|
5766
|
+
try {
|
|
5767
|
+
const ctx = buildContext(runtime.root).editor;
|
|
5768
|
+
state.conventions = ctx?.instructions?.length ?? 0;
|
|
5769
|
+
state.skills = ctx?.skills?.length ?? 0;
|
|
5770
|
+
} catch {
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
screen.on("keys", onKeys);
|
|
5774
|
+
screen.on("resize", schedule);
|
|
5775
|
+
screen.on("end", quit);
|
|
5776
|
+
const onSignal = (code) => () => {
|
|
5777
|
+
screen.leave();
|
|
5778
|
+
process.exit(code);
|
|
5779
|
+
};
|
|
5780
|
+
const onTerm = onSignal(143);
|
|
5781
|
+
const onInt = onSignal(130);
|
|
5782
|
+
const onHup = onSignal(129);
|
|
5783
|
+
process.on("SIGTERM", onTerm);
|
|
5784
|
+
process.on("SIGINT", onInt);
|
|
5785
|
+
process.on("SIGHUP", onHup);
|
|
5786
|
+
screen.enter();
|
|
5787
|
+
if (state.sidebar) refreshProjectFacts();
|
|
5788
|
+
if (runtime.settings.ignoredProjectServerUrl) {
|
|
5789
|
+
note(
|
|
5790
|
+
` ${paint.yellow("!")} ${paint.muted(
|
|
5791
|
+
`ignoring serverUrl in .labd/settings.json (${runtime.settings.ignoredProjectServerUrl}) - this project isn't trusted; \`labd trust\` if you meant it`
|
|
5792
|
+
)}`
|
|
5793
|
+
);
|
|
5794
|
+
}
|
|
5795
|
+
for (const skipped of runtime.extensions.skipped) note(` ${paint.yellow("!")} ${paint.muted(`extensions: ${skipped.why}`)}`);
|
|
5796
|
+
replay();
|
|
5797
|
+
let faceTimer = null;
|
|
5798
|
+
if (faceFrames.length > 1) {
|
|
5799
|
+
faceTimer = setInterval(() => {
|
|
5800
|
+
state.faceIndex++;
|
|
5801
|
+
if (state.faceIndex >= faceFrames.length - 1 && faceTimer) {
|
|
5802
|
+
clearInterval(faceTimer);
|
|
5803
|
+
faceTimer = null;
|
|
5804
|
+
}
|
|
5805
|
+
if (!transcript.length) schedule();
|
|
5806
|
+
}, FACE_FRAME_MS);
|
|
5807
|
+
}
|
|
5808
|
+
if (opts.pickSession) openSessions();
|
|
5809
|
+
draw();
|
|
5810
|
+
try {
|
|
5811
|
+
await exited;
|
|
5812
|
+
if (serial.running) await Promise.race([serial.running, sleep3(1500)]);
|
|
5813
|
+
} finally {
|
|
5814
|
+
if (faceTimer) clearInterval(faceTimer);
|
|
5815
|
+
if (frameTimer) clearTimeout(frameTimer);
|
|
5816
|
+
setBusy(null);
|
|
5817
|
+
process.off("SIGTERM", onTerm);
|
|
5818
|
+
process.off("SIGINT", onInt);
|
|
5819
|
+
process.off("SIGHUP", onHup);
|
|
5820
|
+
screen.leave();
|
|
5821
|
+
}
|
|
5822
|
+
if (state.asked) {
|
|
3653
5823
|
process.stderr.write(paint.dim(`
|
|
3654
5824
|
${FACE} session saved. Continue it with: labd --resume ${runtime.session.id.slice(0, 8)}
|
|
3655
5825
|
`));
|
|
3656
5826
|
await closeConversation(runtime);
|
|
3657
5827
|
}
|
|
5828
|
+
if (state.farewell) process.stderr.write(`${state.farewell}
|
|
5829
|
+
`);
|
|
3658
5830
|
return 0;
|
|
3659
5831
|
}
|
|
3660
|
-
function
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
5832
|
+
function borderColour2(s) {
|
|
5833
|
+
switch (s) {
|
|
5834
|
+
case "ok":
|
|
5835
|
+
return paint.green;
|
|
5836
|
+
case "unknown":
|
|
5837
|
+
return paint.red;
|
|
5838
|
+
case "partial":
|
|
5839
|
+
return paint.cyan;
|
|
5840
|
+
default:
|
|
5841
|
+
return paint.dim;
|
|
3669
5842
|
}
|
|
3670
|
-
const typed = line.trimStart().slice(1).split(/\s+/)[0] ?? "";
|
|
3671
|
-
const meant = didYouMean(typed, extras);
|
|
3672
|
-
return {
|
|
3673
|
-
state: "unknown",
|
|
3674
|
-
hint: meant ? `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("did you mean")} ${paint.accent(`/${meant}`)}${paint.dim("?")}` : `${paint.red("!")} ${paint.muted(`no /${typed}.`)} ${paint.dim("/help lists them all.")}`
|
|
3675
|
-
};
|
|
3676
5843
|
}
|
|
3677
|
-
function
|
|
3678
|
-
|
|
5844
|
+
function spread(left, right, width) {
|
|
5845
|
+
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
5846
|
+
if (gap >= 1) return `${left}${" ".repeat(gap)}${right}`;
|
|
5847
|
+
const room = width - visibleWidth(right) - 1;
|
|
5848
|
+
if (room < 8) return fit(left, width);
|
|
5849
|
+
return `${fit(left, room)} ${right}`;
|
|
3679
5850
|
}
|
|
3680
|
-
function
|
|
3681
|
-
const
|
|
3682
|
-
return
|
|
5851
|
+
function centre(line, width) {
|
|
5852
|
+
const pad4 = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
|
|
5853
|
+
return `${" ".repeat(pad4)}${line}`;
|
|
3683
5854
|
}
|
|
3684
|
-
function
|
|
3685
|
-
const
|
|
3686
|
-
const
|
|
3687
|
-
|
|
3688
|
-
case "run_command":
|
|
3689
|
-
return str("command");
|
|
3690
|
-
case "search_workspace": {
|
|
3691
|
-
const include = str("include");
|
|
3692
|
-
return include ? `${str("query")} in ${include}` : str("query");
|
|
3693
|
-
}
|
|
3694
|
-
case "move_file":
|
|
3695
|
-
return `${str("from")} \u2192 ${str("to")}`;
|
|
3696
|
-
case "list_dir":
|
|
3697
|
-
return str("path") || "/";
|
|
3698
|
-
default:
|
|
3699
|
-
return str("path");
|
|
3700
|
-
}
|
|
5855
|
+
function overlay(base, left, piece, width) {
|
|
5856
|
+
const before = fit(base, left);
|
|
5857
|
+
const after = sliceVisible(base, left + visibleWidth(piece));
|
|
5858
|
+
return fit(`${before}${ESC6}[0m${piece}${ESC6}[0m${after}`, width);
|
|
3701
5859
|
}
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
5860
|
+
function sliceVisible(line, from) {
|
|
5861
|
+
let visible = 0;
|
|
5862
|
+
let out = "";
|
|
5863
|
+
let i = 0;
|
|
5864
|
+
while (i < line.length) {
|
|
5865
|
+
if (line[i] === ESC6) {
|
|
5866
|
+
const end = line.indexOf("m", i);
|
|
5867
|
+
if (end === -1) break;
|
|
5868
|
+
out += line.slice(i, end + 1);
|
|
5869
|
+
i = end + 1;
|
|
5870
|
+
continue;
|
|
3713
5871
|
}
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
renderer.print("");
|
|
3718
|
-
renderer.print(`${paint.accent(FACE)} ${paint.bold("labd")} ${paint.dim(`in ${where}${runtime.branch ? ` on ${runtime.branch}` : ""}`)}`);
|
|
3719
|
-
renderer.print(` ${paint.dim(MODE_CAPTION[mode])}`);
|
|
3720
|
-
if (runtime.settings.ignoredProjectServerUrl) {
|
|
3721
|
-
renderer.print(
|
|
3722
|
-
` ${paint.yellow("!")} ${paint.muted(
|
|
3723
|
-
`ignoring serverUrl in .labd/settings.json (${runtime.settings.ignoredProjectServerUrl}) - this project isn't trusted; \`labd trust\` if you meant it`
|
|
3724
|
-
)}`
|
|
3725
|
-
);
|
|
5872
|
+
if (visible >= from) out += line[i];
|
|
5873
|
+
visible++;
|
|
5874
|
+
i++;
|
|
3726
5875
|
}
|
|
3727
|
-
|
|
3728
|
-
|
|
5876
|
+
return out;
|
|
5877
|
+
}
|
|
5878
|
+
function summarise2(body2) {
|
|
5879
|
+
const text = typeof body2.text === "string" ? body2.text : "";
|
|
5880
|
+
if (body2.kind === "tool") {
|
|
5881
|
+
const results = Array.isArray(body2.results) ? body2.results.length : 0;
|
|
5882
|
+
return `${results} result(s)`;
|
|
3729
5883
|
}
|
|
3730
|
-
|
|
5884
|
+
return text.replace(/\s+/g, " ").slice(0, 60);
|
|
3731
5885
|
}
|
|
3732
|
-
function
|
|
5886
|
+
function sleep3(ms) {
|
|
3733
5887
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3734
5888
|
}
|
|
3735
|
-
function complete(line, runtime) {
|
|
3736
|
-
if (line.startsWith("/") && !/\s/.test(line)) {
|
|
3737
|
-
const hits2 = matching(line, runtime.extensions.commands.keys());
|
|
3738
|
-
return hits2.length === 1 ? `/${hits2[0].name} ` : null;
|
|
3739
|
-
}
|
|
3740
|
-
const at = line.lastIndexOf("@");
|
|
3741
|
-
if (at === -1) return null;
|
|
3742
|
-
const partial = line.slice(at + 1);
|
|
3743
|
-
const hits = rankPaths(partial, listFiles(runtime.root));
|
|
3744
|
-
return hits.length ? `${line.slice(0, at)}@${hits[0]} ` : null;
|
|
3745
|
-
}
|
|
3746
5889
|
|
|
3747
5890
|
// packages/cli/src/modes/print.ts
|
|
3748
5891
|
function toStderr(text) {
|
|
@@ -3778,6 +5921,7 @@ async function runPrint(runtime, opts) {
|
|
|
3778
5921
|
emit({
|
|
3779
5922
|
type: "tool",
|
|
3780
5923
|
name: step.call.name,
|
|
5924
|
+
...step.helper ? { helper: step.helper } : {},
|
|
3781
5925
|
status: step.status,
|
|
3782
5926
|
input: step.call.input,
|
|
3783
5927
|
detail: step.detail,
|
|
@@ -3785,7 +5929,8 @@ async function runPrint(runtime, opts) {
|
|
|
3785
5929
|
output: step.output
|
|
3786
5930
|
});
|
|
3787
5931
|
} else if (step.status !== "running") {
|
|
3788
|
-
|
|
5932
|
+
const who = step.helper ? `${step.helper.label} > ` : "";
|
|
5933
|
+
toStderr(`${paint.dim(`${who}${step.call.name}: ${step.status}${step.detail ? ` - ${step.detail}` : ""}`)}
|
|
3789
5934
|
`);
|
|
3790
5935
|
}
|
|
3791
5936
|
if (step.status === "skipped") declined = true;
|
|
@@ -3796,8 +5941,8 @@ async function runPrint(runtime, opts) {
|
|
|
3796
5941
|
else toStderr(`${paint.red("labd:")} ${message}
|
|
3797
5942
|
`);
|
|
3798
5943
|
},
|
|
3799
|
-
done(
|
|
3800
|
-
if (opts.json) emit({ type: "done", credits:
|
|
5944
|
+
done(info2) {
|
|
5945
|
+
if (opts.json) emit({ type: "done", credits: info2.credits, context: info2.context });
|
|
3801
5946
|
},
|
|
3802
5947
|
commandOutput(line) {
|
|
3803
5948
|
if (opts.json) emit({ type: "command_output", line });
|
|
@@ -3854,10 +5999,10 @@ async function runRpc(runtime) {
|
|
|
3854
5999
|
process.stdin.setEncoding("utf8");
|
|
3855
6000
|
process.stdin.on("data", (chunk) => {
|
|
3856
6001
|
buffer += chunk;
|
|
3857
|
-
let
|
|
3858
|
-
while ((
|
|
3859
|
-
const line = buffer.slice(0,
|
|
3860
|
-
buffer = buffer.slice(
|
|
6002
|
+
let at2;
|
|
6003
|
+
while ((at2 = buffer.indexOf("\n")) !== -1) {
|
|
6004
|
+
const line = buffer.slice(0, at2);
|
|
6005
|
+
buffer = buffer.slice(at2 + 1);
|
|
3861
6006
|
if (line.trim()) void handle(line);
|
|
3862
6007
|
}
|
|
3863
6008
|
});
|
|
@@ -3944,6 +6089,8 @@ async function runRpc(runtime) {
|
|
|
3944
6089
|
tool(step) {
|
|
3945
6090
|
notify("labd/tool", {
|
|
3946
6091
|
name: step.call.name,
|
|
6092
|
+
// Which helper made the call, when one did: its spawn_agent call id and name.
|
|
6093
|
+
...step.helper ? { helper: step.helper } : {},
|
|
3947
6094
|
status: step.status,
|
|
3948
6095
|
input: step.call.input,
|
|
3949
6096
|
detail: step.detail,
|
|
@@ -3954,11 +6101,11 @@ async function runRpc(runtime) {
|
|
|
3954
6101
|
error(message) {
|
|
3955
6102
|
notify("labd/error", { message });
|
|
3956
6103
|
},
|
|
3957
|
-
done(
|
|
3958
|
-
notify("labd/done",
|
|
6104
|
+
done(info2) {
|
|
6105
|
+
notify("labd/done", info2);
|
|
3959
6106
|
},
|
|
3960
|
-
commandOutput(line) {
|
|
3961
|
-
notify("labd/command_output", { line });
|
|
6107
|
+
commandOutput(line, key) {
|
|
6108
|
+
notify("labd/command_output", { line, ...key ? { key } : {} });
|
|
3962
6109
|
},
|
|
3963
6110
|
/**
|
|
3964
6111
|
* Ask the host. A host that never answers gets a refusal after a timeout
|
|
@@ -3966,7 +6113,7 @@ async function runRpc(runtime) {
|
|
|
3966
6113
|
* alternative is writing to someone's files because their integration was
|
|
3967
6114
|
* asleep.
|
|
3968
6115
|
*/
|
|
3969
|
-
approve(call, preview) {
|
|
6116
|
+
approve(call, preview, helper) {
|
|
3970
6117
|
return new Promise((resolve2) => {
|
|
3971
6118
|
if (closed) return resolve2(false);
|
|
3972
6119
|
const id = nextId++;
|
|
@@ -3977,7 +6124,7 @@ async function runRpc(runtime) {
|
|
|
3977
6124
|
clearTimeout(timer);
|
|
3978
6125
|
resolve2(approved);
|
|
3979
6126
|
});
|
|
3980
|
-
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview } });
|
|
6127
|
+
out({ jsonrpc: "2.0", id, method: "labd/approve", params: { call, preview, ...helper ? { helper } : {} } });
|
|
3981
6128
|
});
|
|
3982
6129
|
}
|
|
3983
6130
|
};
|
|
@@ -4007,7 +6154,7 @@ async function runRpc(runtime) {
|
|
|
4007
6154
|
}
|
|
4008
6155
|
|
|
4009
6156
|
// packages/cli/src/index.ts
|
|
4010
|
-
var VERSION = true ? "0.
|
|
6157
|
+
var VERSION = true ? "0.6.0" : "0.0.0-dev";
|
|
4011
6158
|
async function main(argv) {
|
|
4012
6159
|
const args = parse(argv);
|
|
4013
6160
|
if (args.color !== void 0) setColor(args.color);
|
|
@@ -4033,11 +6180,16 @@ async function main(argv) {
|
|
|
4033
6180
|
default:
|
|
4034
6181
|
break;
|
|
4035
6182
|
}
|
|
6183
|
+
const screenCapable = !args.rpc && !args.print && !args.question && !args.plain && process.stdin.isTTY && process.stdout.isTTY;
|
|
6184
|
+
if (args.pickSession && !screenCapable) {
|
|
6185
|
+
throw new UsageError("--resume with no id opens a list, which needs the full-screen session. `labd sessions` prints the ids for `labd --resume <id>`.");
|
|
6186
|
+
}
|
|
4036
6187
|
const runtime = await createRuntime({
|
|
4037
6188
|
root,
|
|
4038
6189
|
overrides: {
|
|
4039
6190
|
...args.mode ? { mode: args.mode } : {},
|
|
4040
|
-
...args.effort ? { effort: args.effort } : {}
|
|
6191
|
+
...args.effort ? { effort: args.effort } : {},
|
|
6192
|
+
...args.mouse !== void 0 ? { mouse: args.mouse } : {}
|
|
4041
6193
|
},
|
|
4042
6194
|
resume: args.resume ? resolveSession(args.resume, root) : args.continue ? latestSession(root) : void 0,
|
|
4043
6195
|
extensions: { enabled: !args.noExtensions, extra: args.extensions }
|
|
@@ -4058,7 +6210,8 @@ async function main(argv) {
|
|
|
4058
6210
|
`);
|
|
4059
6211
|
return 1;
|
|
4060
6212
|
}
|
|
4061
|
-
return runInteractive(runtime);
|
|
6213
|
+
if (args.plain) return runInteractive(runtime);
|
|
6214
|
+
return runScreen(runtime, { mouse: runtime.settings.mouse, sidebar: runtime.settings.sidebar, pickSession: args.pickSession });
|
|
4062
6215
|
}
|
|
4063
6216
|
async function login(root) {
|
|
4064
6217
|
const settings = loadSettings(root, {}, { trusted: isTrusted(stateDir(), root) });
|
|
@@ -4093,9 +6246,11 @@ ${paint.red("labd:")} ${result.reason ?? "that did not complete"}.
|
|
|
4093
6246
|
}
|
|
4094
6247
|
async function logout(root) {
|
|
4095
6248
|
const settings = loadSettings(root, {}, { trusted: isTrusted(stateDir(), root) });
|
|
4096
|
-
await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
4097
|
-
process.
|
|
6249
|
+
const hadStored = await new Auth(stateDir(), settings.serverUrl).signOut();
|
|
6250
|
+
for (const line of signedOutMessage(hadStored, Boolean(process.env.LABD_TOKEN)).split("\n")) {
|
|
6251
|
+
process.stderr.write(` ${paint.muted(line)}
|
|
4098
6252
|
`);
|
|
6253
|
+
}
|
|
4099
6254
|
return 0;
|
|
4100
6255
|
}
|
|
4101
6256
|
function listSessions(root) {
|
|
@@ -4108,7 +6263,7 @@ function listSessions(root) {
|
|
|
4108
6263
|
}
|
|
4109
6264
|
for (const s of all.slice(0, 25)) {
|
|
4110
6265
|
process.stdout.write(
|
|
4111
|
-
` ${paint.dim(s.id.slice(0, 8))} ${paint.muted(new Date(s.at).toLocaleString())} ${paint.muted(`${s.messages} nodes`)} ${s
|
|
6266
|
+
` ${paint.dim(s.id.slice(0, 8))} ${paint.muted(new Date(s.at).toLocaleString())} ${paint.muted(`${s.messages} nodes`)} ${sessionTitle(s)}
|
|
4112
6267
|
`
|
|
4113
6268
|
);
|
|
4114
6269
|
}
|
|
@@ -4140,11 +6295,13 @@ function parse(argv) {
|
|
|
4140
6295
|
print: false,
|
|
4141
6296
|
json: false,
|
|
4142
6297
|
rpc: false,
|
|
6298
|
+
pickSession: false,
|
|
4143
6299
|
continue: false,
|
|
4144
6300
|
noExtensions: false,
|
|
4145
6301
|
extensions: [],
|
|
4146
6302
|
help: false,
|
|
4147
|
-
version: false
|
|
6303
|
+
version: false,
|
|
6304
|
+
plain: false
|
|
4148
6305
|
};
|
|
4149
6306
|
const words = [];
|
|
4150
6307
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -4170,10 +6327,14 @@ function parse(argv) {
|
|
|
4170
6327
|
out.effort = value;
|
|
4171
6328
|
break;
|
|
4172
6329
|
}
|
|
6330
|
+
case "-r":
|
|
4173
6331
|
case "--resume": {
|
|
4174
|
-
const value =
|
|
4175
|
-
if (!value
|
|
4176
|
-
|
|
6332
|
+
const value = argv[i + 1];
|
|
6333
|
+
if (!value || value.startsWith("-")) {
|
|
6334
|
+
out.pickSession = true;
|
|
6335
|
+
break;
|
|
6336
|
+
}
|
|
6337
|
+
out.resume = next();
|
|
4177
6338
|
break;
|
|
4178
6339
|
}
|
|
4179
6340
|
case "-c":
|
|
@@ -4203,6 +6364,12 @@ function parse(argv) {
|
|
|
4203
6364
|
case "--color":
|
|
4204
6365
|
out.color = true;
|
|
4205
6366
|
break;
|
|
6367
|
+
case "--plain":
|
|
6368
|
+
out.plain = true;
|
|
6369
|
+
break;
|
|
6370
|
+
case "--no-mouse":
|
|
6371
|
+
out.mouse = false;
|
|
6372
|
+
break;
|
|
4206
6373
|
case "-h":
|
|
4207
6374
|
case "--help":
|
|
4208
6375
|
out.help = true;
|
|
@@ -4227,6 +6394,9 @@ function parse(argv) {
|
|
|
4227
6394
|
} else {
|
|
4228
6395
|
out.question = words.join(" ");
|
|
4229
6396
|
}
|
|
6397
|
+
if ((out.resume || out.pickSession) && out.continue) {
|
|
6398
|
+
throw new UsageError("--resume and --continue both choose a session - pass one of them");
|
|
6399
|
+
}
|
|
4230
6400
|
return out;
|
|
4231
6401
|
}
|
|
4232
6402
|
function applyMode(out, value) {
|
|
@@ -4246,7 +6416,7 @@ function usage() {
|
|
|
4246
6416
|
${paint.accent(FACE)} ${paint.bold("labd")} - the Dev Labs assistant, in your terminal
|
|
4247
6417
|
|
|
4248
6418
|
${paint.bold("USAGE")}
|
|
4249
|
-
labd start a session in this project
|
|
6419
|
+
labd start a session in this project (full-screen)
|
|
4250
6420
|
labd "why is this failing" ask one question and exit
|
|
4251
6421
|
labd -p "..." | pbcopy print mode, for pipes and scripts
|
|
4252
6422
|
cat log | labd -p "why?" piped input is added to the question
|
|
@@ -4262,12 +6432,15 @@ ${paint.bold("OPTIONS")}
|
|
|
4262
6432
|
--json emit newline-delimited JSON events
|
|
4263
6433
|
--mode <m> ${AGENT_MODES.join(" | ")} (or rpc, for JSON-RPC over stdio)
|
|
4264
6434
|
--think <low|medium|high>
|
|
4265
|
-
|
|
6435
|
+
-r, --resume [id] continue an earlier session; with no id, pick one from a list
|
|
4266
6436
|
-c, --continue continue the most recent session here
|
|
4267
6437
|
-C, --cwd <dir> run as though started in <dir>
|
|
4268
6438
|
-e, --extension <path> load an extension file (repeatable)
|
|
4269
6439
|
--no-extensions load none
|
|
4270
6440
|
--no-color plain text (also honours NO_COLOR)
|
|
6441
|
+
--no-mouse leave the mouse to the terminal: the session takes it for
|
|
6442
|
+
wheel scrolling, and native text selection needs it released
|
|
6443
|
+
--plain the older print-style session (deprecated; removed in 0.5)
|
|
4271
6444
|
-h, --help -v, --version
|
|
4272
6445
|
|
|
4273
6446
|
${paint.bold("MODES")}
|