clixad 0.0.1-beta.6 → 0.0.1-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clixad.mjs +729 -117
- package/package.json +1 -1
package/dist/clixad.mjs
CHANGED
|
@@ -643,7 +643,7 @@ function rel(root, abs) {
|
|
|
643
643
|
async function gate(ctx, req) {
|
|
644
644
|
if (!ctx.permit) return;
|
|
645
645
|
const res = await ctx.permit(req);
|
|
646
|
-
if (!res.allowed) throw new
|
|
646
|
+
if (!res.allowed) throw new PermissionDeniedError(res.reason, res.notice);
|
|
647
647
|
}
|
|
648
648
|
function commandSignature(command) {
|
|
649
649
|
return `run:${command.replace(/\s+/g, " ").trim()}`;
|
|
@@ -779,14 +779,30 @@ function globToRegExp(pattern) {
|
|
|
779
779
|
const anchored = p.includes("/") ? `^${re}$` : `^(?:.*/)?${re}$`;
|
|
780
780
|
return new RegExp(anchored);
|
|
781
781
|
}
|
|
782
|
-
|
|
782
|
+
function toolSchemaFor(mode) {
|
|
783
|
+
if (mode !== "plan") return TOOL_SCHEMA;
|
|
784
|
+
return [
|
|
785
|
+
...TOOL_SCHEMA.filter((t) => READ_ONLY_TOOLS.includes(t.function.name)),
|
|
786
|
+
EXIT_PLAN_MODE_SCHEMA
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
var ToolError, PermissionDeniedError, MAX_FILE_BYTES, MAX_READ_LINES, MAX_READ_LINE_CHARS, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, SKIP_DIRS, TOOLS, READ_ONLY_TOOLS, EXIT_PLAN_MODE_SCHEMA, TOOL_SCHEMA;
|
|
783
790
|
var init_tools = __esm({
|
|
784
791
|
"src/tools.ts"() {
|
|
785
792
|
"use strict";
|
|
786
793
|
init_diff();
|
|
787
794
|
ToolError = class extends Error {
|
|
788
795
|
};
|
|
796
|
+
PermissionDeniedError = class extends ToolError {
|
|
797
|
+
notice;
|
|
798
|
+
constructor(message, notice) {
|
|
799
|
+
super(message);
|
|
800
|
+
this.notice = notice;
|
|
801
|
+
}
|
|
802
|
+
};
|
|
789
803
|
MAX_FILE_BYTES = 4e5;
|
|
804
|
+
MAX_READ_LINES = 2e3;
|
|
805
|
+
MAX_READ_LINE_CHARS = 2e3;
|
|
790
806
|
MAX_OUTPUT_CHARS = 3e4;
|
|
791
807
|
MAX_GLOB_RESULTS = 200;
|
|
792
808
|
MAX_GREP_MATCHES = 100;
|
|
@@ -805,16 +821,46 @@ var init_tools = __esm({
|
|
|
805
821
|
".venv"
|
|
806
822
|
]);
|
|
807
823
|
TOOLS = {
|
|
824
|
+
/**
|
|
825
|
+
* Read a file, numbered, and optionally only a window of it.
|
|
826
|
+
*
|
|
827
|
+
* The numbers are what let the model say `app.tsx:412` instead of "the bit
|
|
828
|
+
* near the top", and the window is what stops a 400 kB file being poured into
|
|
829
|
+
* a metered context to look at forty lines of it. Both are how Claude Code
|
|
830
|
+
* reads files, and the cost of neither is small on a product where the
|
|
831
|
+
* context is the bill.
|
|
832
|
+
*
|
|
833
|
+
* The obvious hazard is that `edit_file` matches text exactly, so a model
|
|
834
|
+
* that copies the numbers into `old_string` gets nothing. Two things guard
|
|
835
|
+
* it: the tool description says the numbers are display only, and
|
|
836
|
+
* `edit_file`'s not-found error names them, so a model that does it anyway
|
|
837
|
+
* is told precisely what went wrong instead of guessing.
|
|
838
|
+
*/
|
|
808
839
|
read_file(ctx, args) {
|
|
809
840
|
const abs = safeResolve(ctx.root, args.path);
|
|
810
841
|
if (!existsSync(abs)) throw new ToolError(`not found: ${args.path}`);
|
|
811
842
|
if (statSync(abs).isDirectory()) throw new ToolError(`${args.path} is a directory \u2014 use list_dir`);
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
843
|
+
const raw = readFileSync2(abs, "utf8");
|
|
844
|
+
const all = raw.split("\n");
|
|
845
|
+
const total = all.length > 1 && all[all.length - 1] === "" ? all.length - 1 : all.length;
|
|
846
|
+
const first = Math.max(1, Math.floor(args.offset ?? 1));
|
|
847
|
+
const want = Math.max(1, Math.floor(args.limit ?? MAX_READ_LINES));
|
|
848
|
+
const last = Math.min(total, first + want - 1);
|
|
849
|
+
if (first > total) {
|
|
850
|
+
return `${args.path} has ${total} lines; offset ${first} is past the end`;
|
|
816
851
|
}
|
|
817
|
-
|
|
852
|
+
const body = all.slice(first - 1, last).map((line2, i) => {
|
|
853
|
+
const n = String(first + i).padStart(5, " ");
|
|
854
|
+
const text = line2.length > MAX_READ_LINE_CHARS ? `${line2.slice(0, MAX_READ_LINE_CHARS)}\u2026 [line truncated]` : line2;
|
|
855
|
+
return `${n} ${text}`;
|
|
856
|
+
}).join("\n");
|
|
857
|
+
const note = last < total || first > 1 ? `
|
|
858
|
+
\u2026 showing lines ${first}-${last} of ${total}. Read on with offset: ${last + 1}.` : "";
|
|
859
|
+
if (body.length > MAX_FILE_BYTES) {
|
|
860
|
+
return `${body.slice(0, MAX_FILE_BYTES)}
|
|
861
|
+
\u2026 [truncated at ${MAX_FILE_BYTES} chars \u2014 read a smaller range with offset/limit]`;
|
|
862
|
+
}
|
|
863
|
+
return body + note;
|
|
818
864
|
},
|
|
819
865
|
list_dir(ctx, args) {
|
|
820
866
|
const abs = safeResolve(ctx.root, args.path ?? ".");
|
|
@@ -907,7 +953,11 @@ var init_tools = __esm({
|
|
|
907
953
|
if (args.old_string === args.new_string) throw new ToolError("old_string and new_string are identical");
|
|
908
954
|
const before = readFileSync2(abs, "utf8");
|
|
909
955
|
const count = occurrences(before, args.old_string);
|
|
910
|
-
if (count === 0)
|
|
956
|
+
if (count === 0) {
|
|
957
|
+
throw new ToolError(
|
|
958
|
+
`old_string not found in ${args.path} \u2014 read the file and copy the exact text, without the line numbers read_file puts in front of each line`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
911
961
|
if (count > 1 && !args.replace_all) {
|
|
912
962
|
throw new ToolError(
|
|
913
963
|
`old_string appears ${count} times in ${args.path} \u2014 add more surrounding context, or pass replace_all: true`
|
|
@@ -938,6 +988,47 @@ var init_tools = __esm({
|
|
|
938
988
|
preview: command
|
|
939
989
|
});
|
|
940
990
|
return execute(ctx, command, args.timeout ?? DEFAULT_COMMAND_TIMEOUT);
|
|
991
|
+
},
|
|
992
|
+
/**
|
|
993
|
+
* The way out of plan mode — the thing plan mode did not have.
|
|
994
|
+
*
|
|
995
|
+
* It changes no files. Its whole job is to put the plan in front of the user
|
|
996
|
+
* and report back what they chose, so the model can carry on **in the same
|
|
997
|
+
* turn** rather than being told to ask for an approval nothing could grant.
|
|
998
|
+
*
|
|
999
|
+
* The mode switch itself is not done here: `approvePlan` is wired to the
|
|
1000
|
+
* permission state in the TUI, which owns the mode, and this tool only
|
|
1001
|
+
* reports the answer. Keeping the two apart is what stops a tool from being
|
|
1002
|
+
* able to grant itself write access.
|
|
1003
|
+
*/
|
|
1004
|
+
async exit_plan_mode(ctx, args) {
|
|
1005
|
+
const plan = (args.plan ?? "").trim();
|
|
1006
|
+
if (!plan) throw new ToolError("exit_plan_mode needs a plan");
|
|
1007
|
+
if (!ctx.approvePlan) {
|
|
1008
|
+
throw new ToolError(
|
|
1009
|
+
"there is no one to approve a plan in this run \u2014 present the plan as your answer instead"
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
const decision = await ctx.approvePlan(plan);
|
|
1013
|
+
if (decision === "keepPlanning") {
|
|
1014
|
+
return "the user did not approve this plan. Stay read-only, ask what they would change, and call exit_plan_mode again when you have a plan they agree with.";
|
|
1015
|
+
}
|
|
1016
|
+
return `the user approved the plan. Permission mode is now "${decision}"` + (decision === "acceptEdits" ? " \u2014 file edits apply without asking, shell commands are still put to them." : " \u2014 each write and each command is put to them first.") + " Carry out the plan now.";
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
READ_ONLY_TOOLS = ["read_file", "list_dir", "glob", "grep"];
|
|
1020
|
+
EXIT_PLAN_MODE_SCHEMA = {
|
|
1021
|
+
type: "function",
|
|
1022
|
+
function: {
|
|
1023
|
+
name: "exit_plan_mode",
|
|
1024
|
+
description: "Present your plan to the user and ask them to approve leaving plan mode. Call this once you know what you intend to do. Writes and commands stay unavailable until they approve; on approval you continue in the same turn.",
|
|
1025
|
+
parameters: {
|
|
1026
|
+
type: "object",
|
|
1027
|
+
properties: {
|
|
1028
|
+
plan: { type: "string", description: "The plan, in a few short lines of markdown." }
|
|
1029
|
+
},
|
|
1030
|
+
required: ["plan"]
|
|
1031
|
+
}
|
|
941
1032
|
}
|
|
942
1033
|
};
|
|
943
1034
|
TOOL_SCHEMA = [
|
|
@@ -945,10 +1036,14 @@ var init_tools = __esm({
|
|
|
945
1036
|
type: "function",
|
|
946
1037
|
function: {
|
|
947
1038
|
name: "read_file",
|
|
948
|
-
description: "Read a UTF-8 text file from the workspace.",
|
|
1039
|
+
description: "Read a UTF-8 text file from the workspace. Lines come back as '<number>\\t<text>'. The numbers are for referring to places in the file and are NOT part of it \u2014 never include them in edit_file arguments. Long files are cut off; read on with offset.",
|
|
949
1040
|
parameters: {
|
|
950
1041
|
type: "object",
|
|
951
|
-
properties: {
|
|
1042
|
+
properties: {
|
|
1043
|
+
path: { type: "string" },
|
|
1044
|
+
offset: { type: "number", description: "First line to read, 1-based. Defaults to 1." },
|
|
1045
|
+
limit: { type: "number", description: "How many lines to read. Defaults to 2000." }
|
|
1046
|
+
},
|
|
952
1047
|
required: ["path"]
|
|
953
1048
|
}
|
|
954
1049
|
}
|
|
@@ -1047,7 +1142,8 @@ async function runAgent(client, task, opts) {
|
|
|
1047
1142
|
const ctx = {
|
|
1048
1143
|
root: opts.root,
|
|
1049
1144
|
permit: opts.permit,
|
|
1050
|
-
signal: opts.signal
|
|
1145
|
+
signal: opts.signal,
|
|
1146
|
+
approvePlan: opts.approvePlan
|
|
1051
1147
|
};
|
|
1052
1148
|
const messages = [
|
|
1053
1149
|
{ role: "system", content: opts.systemPrompt ?? SYSTEM_PROMPT },
|
|
@@ -1062,7 +1158,7 @@ async function runAgent(client, task, opts) {
|
|
|
1062
1158
|
let turn;
|
|
1063
1159
|
try {
|
|
1064
1160
|
turn = await client.chatStream(messages, opts.model, {
|
|
1065
|
-
tools: TOOL_SCHEMA,
|
|
1161
|
+
tools: opts.tools ? opts.tools() : TOOL_SCHEMA,
|
|
1066
1162
|
signal: opts.signal,
|
|
1067
1163
|
onDelta: (text) => emit({ type: "delta", text })
|
|
1068
1164
|
});
|
|
@@ -1129,7 +1225,14 @@ async function executeToolCall(ctx, call, emit) {
|
|
|
1129
1225
|
} catch (err) {
|
|
1130
1226
|
if (err instanceof ToolError) {
|
|
1131
1227
|
const msg = `error: ${err.message}`;
|
|
1132
|
-
emit({
|
|
1228
|
+
emit({
|
|
1229
|
+
type: "tool_result",
|
|
1230
|
+
id,
|
|
1231
|
+
name,
|
|
1232
|
+
result: msg,
|
|
1233
|
+
ok: false,
|
|
1234
|
+
...err instanceof PermissionDeniedError ? { display: err.notice } : {}
|
|
1235
|
+
});
|
|
1133
1236
|
return msg;
|
|
1134
1237
|
}
|
|
1135
1238
|
throw err;
|
|
@@ -1151,6 +1254,8 @@ async function dispatch(ctx, name, args) {
|
|
|
1151
1254
|
return TOOLS.write_file(ctx, args);
|
|
1152
1255
|
case "run_command":
|
|
1153
1256
|
return TOOLS.run_command(ctx, args);
|
|
1257
|
+
case "exit_plan_mode":
|
|
1258
|
+
return TOOLS.exit_plan_mode(ctx, args);
|
|
1154
1259
|
default:
|
|
1155
1260
|
return `error: unknown tool ${name}`;
|
|
1156
1261
|
}
|
|
@@ -1160,7 +1265,7 @@ var init_agent = __esm({
|
|
|
1160
1265
|
"src/agent.ts"() {
|
|
1161
1266
|
"use strict";
|
|
1162
1267
|
init_tools();
|
|
1163
|
-
SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you
|
|
1268
|
+
SYSTEM_PROMPT = "You are Clixad, a terminal coding agent. You can read, search, write and edit files and run shell commands, all confined to the user's workspace. Find your way around with glob and grep before reading whole files. Prefer edit_file over write_file for existing files. Make minimal correct edits, verify them when a test or build command is available, and stop when the task is complete. Keep your final message short: say what you did and why.\n\nNot every message is a task. If the user asks a question, or asks you to say something, answer in plain text and call no tools at all. Reach for a file or a command only when the request is actually about one, and when a message is ambiguous, prefer the reading of it that changes nothing.\n\nA tool call that was refused did not happen. Never describe it as done, and do not repeat it on a later message unless the user asks for it again.";
|
|
1164
1269
|
}
|
|
1165
1270
|
});
|
|
1166
1271
|
|
|
@@ -1240,6 +1345,30 @@ var init_context = __esm({
|
|
|
1240
1345
|
});
|
|
1241
1346
|
|
|
1242
1347
|
// src/permissions.ts
|
|
1348
|
+
function modeInstruction(mode) {
|
|
1349
|
+
switch (mode) {
|
|
1350
|
+
case "plan":
|
|
1351
|
+
return [
|
|
1352
|
+
"# Permission mode: plan (read-only)",
|
|
1353
|
+
"You cannot write files or run commands right now \u2014 those tools are not available to you",
|
|
1354
|
+
"in this mode. Investigate with read_file, list_dir, glob and grep, then call exit_plan_mode",
|
|
1355
|
+
"with a short plan. The user approves or rejects it there; on approval you carry straight on",
|
|
1356
|
+
"in the same turn with the write tools available. Do not ask for approval in prose \u2014 the user",
|
|
1357
|
+
"has no way to answer that."
|
|
1358
|
+
].join("\n");
|
|
1359
|
+
case "acceptEdits":
|
|
1360
|
+
return [
|
|
1361
|
+
"# Permission mode: auto-accept edits",
|
|
1362
|
+
"File edits apply without asking. Shell commands are still put to the user first."
|
|
1363
|
+
].join("\n");
|
|
1364
|
+
default:
|
|
1365
|
+
return [
|
|
1366
|
+
"# Permission mode: ask before edits",
|
|
1367
|
+
"Each write and each shell command is put to the user before it runs. A refusal is their",
|
|
1368
|
+
"decision: the call did not happen, so do not report it as done and do not retry it."
|
|
1369
|
+
].join("\n");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1243
1372
|
function isEdit(tool) {
|
|
1244
1373
|
return tool === "write_file" || tool === "edit_file";
|
|
1245
1374
|
}
|
|
@@ -1254,7 +1383,8 @@ function decide(state, req) {
|
|
|
1254
1383
|
if (state.mode === "plan") {
|
|
1255
1384
|
return {
|
|
1256
1385
|
kind: "deny",
|
|
1257
|
-
reason: `plan mode is on, so ${req.summary} was not executed. Do not attempt further writes or commands \u2014 describe the plan instead and let the user approve it
|
|
1386
|
+
reason: `plan mode is on, so ${req.summary} was not executed. Do not attempt further writes or commands \u2014 describe the plan instead and let the user approve it.`,
|
|
1387
|
+
notice: `plan mode \u2014 ${req.summary} was not run`
|
|
1258
1388
|
};
|
|
1259
1389
|
}
|
|
1260
1390
|
if (state.allowed.has(req.signature)) return { kind: "allow" };
|
|
@@ -1266,14 +1396,23 @@ function createPermit(getState, ask2) {
|
|
|
1266
1396
|
const state = getState();
|
|
1267
1397
|
const verdict = decide(state, req);
|
|
1268
1398
|
if (verdict.kind === "allow") return { allowed: true };
|
|
1269
|
-
if (verdict.kind === "deny") return { allowed: false, reason: verdict.reason };
|
|
1399
|
+
if (verdict.kind === "deny") return { allowed: false, reason: verdict.reason, notice: verdict.notice };
|
|
1270
1400
|
const answer = await ask2(req);
|
|
1271
1401
|
if (answer === "always") {
|
|
1272
1402
|
state.allowed.add(req.signature);
|
|
1273
1403
|
return { allowed: true };
|
|
1274
1404
|
}
|
|
1275
1405
|
if (answer === "once") return { allowed: true };
|
|
1276
|
-
return {
|
|
1406
|
+
return {
|
|
1407
|
+
allowed: false,
|
|
1408
|
+
// Stated as a fact about the world, not just as a verdict. A refused call
|
|
1409
|
+
// that reads like a pending one is how the model ends up describing work
|
|
1410
|
+
// it never did — or repeating the call on the next message.
|
|
1411
|
+
reason: `the user denied ${req.summary}. It did not happen. Do not retry it \u2014 ask what they would prefer, or carry on with the rest of the task.`,
|
|
1412
|
+
// The TUI has already said "denied: <summary>" the moment the key was
|
|
1413
|
+
// pressed, so repeating it in red under the tool call is noise.
|
|
1414
|
+
notice: `denied by you`
|
|
1415
|
+
};
|
|
1277
1416
|
};
|
|
1278
1417
|
}
|
|
1279
1418
|
var MODES, MODE_LABEL, denyAll;
|
|
@@ -1288,7 +1427,8 @@ var init_permissions = __esm({
|
|
|
1288
1427
|
};
|
|
1289
1428
|
denyAll = async (req) => ({
|
|
1290
1429
|
allowed: false,
|
|
1291
|
-
reason: `${req.summary} needs approval, but this run is non-interactive (use the REPL to approve it)
|
|
1430
|
+
reason: `${req.summary} needs approval, but this run is non-interactive (use the REPL to approve it)`,
|
|
1431
|
+
notice: `${req.summary} needs approval \u2014 this run is non-interactive`
|
|
1292
1432
|
});
|
|
1293
1433
|
}
|
|
1294
1434
|
});
|
|
@@ -1504,6 +1644,10 @@ function centered(rendered, visibleW, colW) {
|
|
|
1504
1644
|
const lead = Math.max(0, Math.floor((colW - visibleW) / 2));
|
|
1505
1645
|
return cell(" ".repeat(lead) + rendered, lead + visibleW);
|
|
1506
1646
|
}
|
|
1647
|
+
function bannerTip(index) {
|
|
1648
|
+
const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
|
|
1649
|
+
return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
|
|
1650
|
+
}
|
|
1507
1651
|
function earnedLabel(opts) {
|
|
1508
1652
|
return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
|
|
1509
1653
|
}
|
|
@@ -1515,14 +1659,19 @@ function welcomeBanner(opts) {
|
|
|
1515
1659
|
const cwd = opts.cwd.replace(homedir5(), "~").replace(/\\/g, "/");
|
|
1516
1660
|
const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
|
|
1517
1661
|
const duck = duckLines();
|
|
1518
|
-
const model = truncate(opts.model, leftW - 4);
|
|
1519
1662
|
const home = truncate(cwd, leftW);
|
|
1663
|
+
const hint = bannerTip(opts.tipIndex);
|
|
1664
|
+
const hintDesc = truncate(hint.desc, Math.max(1, leftW - 3 - hint.key.length));
|
|
1520
1665
|
const left = [
|
|
1521
1666
|
centered(BOLD + TEXT + welcome + R2, welcome.length, leftW),
|
|
1522
1667
|
cell("", 0),
|
|
1523
1668
|
...duck.map((d) => centered(d, DUCK_W, leftW)),
|
|
1524
1669
|
cell("", 0),
|
|
1525
|
-
centered(
|
|
1670
|
+
centered(
|
|
1671
|
+
MANGO + "\u25C6 " + R2 + BOLD + TEXT + hint.key + R2 + FAINT + " " + hintDesc + R2,
|
|
1672
|
+
2 + hint.key.length + 1 + hintDesc.length,
|
|
1673
|
+
leftW
|
|
1674
|
+
),
|
|
1526
1675
|
centered(FAINT + home + R2, home.length, leftW)
|
|
1527
1676
|
];
|
|
1528
1677
|
const tip = (k, d) => cell(MANGO + k + R2 + FAINT + " " + d + R2, k.length + 1 + d.length);
|
|
@@ -1553,10 +1702,11 @@ function welcomeBanner(opts) {
|
|
|
1553
1702
|
}
|
|
1554
1703
|
function compactBanner(opts) {
|
|
1555
1704
|
const welcome = opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!";
|
|
1705
|
+
const hint = bannerTip(opts.tipIndex);
|
|
1556
1706
|
return duckLines().map((d) => " " + d).join("\n") + `
|
|
1557
1707
|
|
|
1558
1708
|
${BOLD}${TEXT}${welcome}${R2}
|
|
1559
|
-
${MANGO}\u25C6${R2} ${
|
|
1709
|
+
${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${hint.desc}${R2} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
|
|
1560
1710
|
`;
|
|
1561
1711
|
}
|
|
1562
1712
|
function clearScreen() {
|
|
@@ -1568,7 +1718,7 @@ function clearScreen() {
|
|
|
1568
1718
|
function visibleLength(s) {
|
|
1569
1719
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1570
1720
|
}
|
|
1571
|
-
var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, MIN_BANNER_WIDTH, RIGHT_MAX;
|
|
1721
|
+
var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, BANNER_TIPS, MIN_BANNER_WIDTH, RIGHT_MAX;
|
|
1572
1722
|
var init_banner = __esm({
|
|
1573
1723
|
"src/banner.ts"() {
|
|
1574
1724
|
"use strict";
|
|
@@ -1610,6 +1760,15 @@ var init_banner = __esm({
|
|
|
1610
1760
|
cell = (text, w) => ({ text, w });
|
|
1611
1761
|
padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
|
|
1612
1762
|
truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1763
|
+
BANNER_TIPS = [
|
|
1764
|
+
{ key: "/init", desc: "make a CLIXAD.md" },
|
|
1765
|
+
{ key: "shift+tab", desc: "switch mode" },
|
|
1766
|
+
{ key: "@file", desc: "add a file to context" },
|
|
1767
|
+
{ key: "ctrl+o", desc: "expand tool output" },
|
|
1768
|
+
{ key: "esc", desc: "stop the current turn" },
|
|
1769
|
+
{ key: "/compact", desc: "free up context" },
|
|
1770
|
+
{ key: "/earn", desc: "top up credits" }
|
|
1771
|
+
];
|
|
1613
1772
|
MIN_BANNER_WIDTH = 66;
|
|
1614
1773
|
RIGHT_MAX = 34;
|
|
1615
1774
|
}
|
|
@@ -1635,6 +1794,59 @@ var init_version = __esm({
|
|
|
1635
1794
|
}
|
|
1636
1795
|
});
|
|
1637
1796
|
|
|
1797
|
+
// src/mentions.ts
|
|
1798
|
+
function findMentions(text) {
|
|
1799
|
+
const out = [];
|
|
1800
|
+
for (const match of text.matchAll(/(?:^|\s)@([^\s]+)/g)) {
|
|
1801
|
+
const path = match[1].replace(TRAILING, "");
|
|
1802
|
+
if (path && !out.includes(path)) out.push(path);
|
|
1803
|
+
}
|
|
1804
|
+
return out;
|
|
1805
|
+
}
|
|
1806
|
+
function expandMentions(text, root, limit = MAX_MENTION_CHARS) {
|
|
1807
|
+
const mentions = findMentions(text);
|
|
1808
|
+
if (mentions.length === 0) return { text, attached: [] };
|
|
1809
|
+
const blocks = [];
|
|
1810
|
+
const attached = [];
|
|
1811
|
+
let used = 0;
|
|
1812
|
+
for (const path of mentions) {
|
|
1813
|
+
if (used >= limit) break;
|
|
1814
|
+
let body;
|
|
1815
|
+
try {
|
|
1816
|
+
safeResolve(root, path);
|
|
1817
|
+
body = TOOLS.read_file({ root }, { path });
|
|
1818
|
+
} catch {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
const room = limit - used;
|
|
1822
|
+
const clipped = body.length > room ? `${body.slice(0, room)}
|
|
1823
|
+
\u2026 [attachment truncated \u2014 read the rest with read_file]` : body;
|
|
1824
|
+
used += clipped.length;
|
|
1825
|
+
attached.push(path);
|
|
1826
|
+
blocks.push(`--- ${path} ---
|
|
1827
|
+
${clipped}`);
|
|
1828
|
+
}
|
|
1829
|
+
if (blocks.length === 0) return { text, attached: [] };
|
|
1830
|
+
return {
|
|
1831
|
+
text: `${text}
|
|
1832
|
+
|
|
1833
|
+
# Files the user referenced
|
|
1834
|
+
Their contents are below, already read for you \u2014 do not call read_file on them again.
|
|
1835
|
+
|
|
1836
|
+
` + blocks.join("\n\n"),
|
|
1837
|
+
attached
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
var TRAILING, MAX_MENTION_CHARS;
|
|
1841
|
+
var init_mentions = __esm({
|
|
1842
|
+
"src/mentions.ts"() {
|
|
1843
|
+
"use strict";
|
|
1844
|
+
init_tools();
|
|
1845
|
+
TRAILING = /[),;:]+$/;
|
|
1846
|
+
MAX_MENTION_CHARS = 12e4;
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1638
1850
|
// src/compact.ts
|
|
1639
1851
|
function estimateTokens(text) {
|
|
1640
1852
|
return Math.ceil(text.length / 4);
|
|
@@ -1811,9 +2023,10 @@ function commandLabel(c2) {
|
|
|
1811
2023
|
function helpText() {
|
|
1812
2024
|
const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
|
|
1813
2025
|
return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
|
|
1814
|
-
" @path
|
|
2026
|
+
" @path attach a file to the message (tab completes)",
|
|
2027
|
+
" !command run a shell command yourself, no model call",
|
|
1815
2028
|
" shift+tab cycle permission mode \xB7 esc stop the current turn",
|
|
1816
|
-
" ctrl+o expand the
|
|
2029
|
+
" ctrl+o expand the newest collapsed tool output \xB7 ctrl+c twice quit",
|
|
1817
2030
|
" \\ + enter continue on a new line"
|
|
1818
2031
|
].join("\n");
|
|
1819
2032
|
}
|
|
@@ -1843,7 +2056,11 @@ var init_commands = __esm({
|
|
|
1843
2056
|
// somebody in a 401 loop needs.
|
|
1844
2057
|
{ name: "login", desc: "sign in, or switch account" },
|
|
1845
2058
|
{ name: "compact", desc: "summarise the conversation to free context" },
|
|
1846
|
-
{ name: "clear", desc: "clear the
|
|
2059
|
+
{ name: "clear", desc: "clear the screen and start a new session" },
|
|
2060
|
+
// Deliberately *no* `args` on /resume, for /login's reason: an arg hint makes
|
|
2061
|
+
// enter complete the command instead of running it, and the picker is the
|
|
2062
|
+
// point. `/resume <id>` still works when typed in full.
|
|
2063
|
+
{ name: "resume", desc: "pick up an earlier session in this directory" },
|
|
1847
2064
|
{ name: "init", desc: "write a CLIXAD.md for this project" },
|
|
1848
2065
|
{ name: "exit", desc: "quit clixad" }
|
|
1849
2066
|
];
|
|
@@ -2158,6 +2375,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2158
2375
|
function lineCount(text, cols) {
|
|
2159
2376
|
return text.split("\n").reduce((n, line2) => n + Math.max(1, Math.ceil(visibleLength(line2) / Math.max(1, cols))), 0);
|
|
2160
2377
|
}
|
|
2378
|
+
function tailRows(text, cols, max) {
|
|
2379
|
+
if (max <= 0) return "";
|
|
2380
|
+
const width = Math.max(1, cols);
|
|
2381
|
+
const lines = text.split("\n");
|
|
2382
|
+
const kept = [];
|
|
2383
|
+
let used = 0;
|
|
2384
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
2385
|
+
const line2 = lines[i];
|
|
2386
|
+
const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
|
|
2387
|
+
if (used + h <= max) {
|
|
2388
|
+
kept.unshift(line2);
|
|
2389
|
+
used += h;
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const room = max - used;
|
|
2393
|
+
if (room > 0) kept.unshift(line2.slice(-(room * width)));
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
return kept.join("\n");
|
|
2397
|
+
}
|
|
2398
|
+
function headRows(text, cols, max) {
|
|
2399
|
+
if (max <= 0) return "";
|
|
2400
|
+
const width = Math.max(1, cols);
|
|
2401
|
+
const lines = text.split("\n");
|
|
2402
|
+
const kept = [];
|
|
2403
|
+
let used = 0;
|
|
2404
|
+
for (const line2 of lines) {
|
|
2405
|
+
const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
|
|
2406
|
+
if (used + h <= max) {
|
|
2407
|
+
kept.push(line2);
|
|
2408
|
+
used += h;
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const room = max - used;
|
|
2412
|
+
if (room > 0) kept.push(line2.slice(0, room * width));
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
return kept.join("\n");
|
|
2416
|
+
}
|
|
2161
2417
|
function entryHeight(entry, cols) {
|
|
2162
2418
|
const margin = entry.kind === "banner" ? 0 : 1;
|
|
2163
2419
|
switch (entry.kind) {
|
|
@@ -2204,7 +2460,7 @@ function EntryView({ entry }) {
|
|
|
2204
2460
|
) });
|
|
2205
2461
|
}
|
|
2206
2462
|
}
|
|
2207
|
-
var MANGO3, MANGO_BRIGHT;
|
|
2463
|
+
var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE;
|
|
2208
2464
|
var init_views = __esm({
|
|
2209
2465
|
"src/tui/views.tsx"() {
|
|
2210
2466
|
"use strict";
|
|
@@ -2212,12 +2468,18 @@ var init_views = __esm({
|
|
|
2212
2468
|
init_markdown();
|
|
2213
2469
|
MANGO3 = "#f5b841";
|
|
2214
2470
|
MANGO_BRIGHT = "#ffcf6b";
|
|
2471
|
+
SLATE = "#8b98ae";
|
|
2472
|
+
MODE_STYLE = {
|
|
2473
|
+
normal: { color: SLATE, glyph: "\u23F5" },
|
|
2474
|
+
acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
|
|
2475
|
+
plan: { color: "#79c0ff", glyph: "\u23F8" }
|
|
2476
|
+
};
|
|
2215
2477
|
}
|
|
2216
2478
|
});
|
|
2217
2479
|
|
|
2218
2480
|
// src/tui/app.tsx
|
|
2219
2481
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2220
|
-
import { Box as Box2, Static, Text as Text2, useApp, useInput } from "ink";
|
|
2482
|
+
import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
|
|
2221
2483
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2222
2484
|
function useTerminalSize() {
|
|
2223
2485
|
const [size, setSize] = useState({
|
|
@@ -2235,6 +2497,7 @@ function useTerminalSize() {
|
|
|
2235
2497
|
}
|
|
2236
2498
|
function App({ client, config, wallet, session, initialTask }) {
|
|
2237
2499
|
const { exit } = useApp();
|
|
2500
|
+
const { write: writeToStdout } = useStdout();
|
|
2238
2501
|
const idRef = useRef(1);
|
|
2239
2502
|
const { rows, cols } = useTerminalSize();
|
|
2240
2503
|
const root = process.cwd();
|
|
@@ -2255,6 +2518,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2255
2518
|
})
|
|
2256
2519
|
}
|
|
2257
2520
|
]);
|
|
2521
|
+
const [staticEpoch, setStaticEpoch] = useState(0);
|
|
2258
2522
|
const [messages, setMessages] = useState(session?.messages ?? []);
|
|
2259
2523
|
const [editor, setEditor] = useState(EMPTY);
|
|
2260
2524
|
const [busy, setBusy] = useState(false);
|
|
@@ -2268,6 +2532,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2268
2532
|
const [sel, setSel] = useState(0);
|
|
2269
2533
|
const [menuOff, setMenuOff] = useState(false);
|
|
2270
2534
|
const [ask2, setAsk] = useState(null);
|
|
2535
|
+
const [plan, setPlan] = useState(null);
|
|
2271
2536
|
const [picker, setPicker] = useState(null);
|
|
2272
2537
|
const [pickerSel, setPickerSel] = useState(0);
|
|
2273
2538
|
const [tick, setTick] = useState(0);
|
|
@@ -2277,6 +2542,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2277
2542
|
const [sponsor, setSponsor] = useState(null);
|
|
2278
2543
|
const messagesRef = useRef(messages);
|
|
2279
2544
|
messagesRef.current = messages;
|
|
2545
|
+
const busyRef = useRef(false);
|
|
2546
|
+
busyRef.current = busy;
|
|
2547
|
+
const queueRef = useRef([]);
|
|
2548
|
+
const [queueView, setQueueView] = useState([]);
|
|
2280
2549
|
const permRef = useRef(createState("normal"));
|
|
2281
2550
|
const abortRef = useRef(null);
|
|
2282
2551
|
const busyAbortRef = useRef(null);
|
|
@@ -2286,16 +2555,54 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2286
2555
|
const runStartedAtRef = useRef(Date.now());
|
|
2287
2556
|
const contextRef = useRef(collectContext(root));
|
|
2288
2557
|
const catalogRef = useRef([]);
|
|
2289
|
-
const
|
|
2558
|
+
const outputsRef = useRef([]);
|
|
2559
|
+
const expandedRef = useRef(/* @__PURE__ */ new Set());
|
|
2560
|
+
const pickerWindowRef = useRef({ from: 0, count: 0 });
|
|
2290
2561
|
const runningToolRef = useRef(null);
|
|
2291
2562
|
const pendingTaskRef = useRef(null);
|
|
2563
|
+
const deltaBufRef = useRef("");
|
|
2564
|
+
const deltaTimerRef = useRef(null);
|
|
2292
2565
|
const sponsorRef = useRef(sponsorSource());
|
|
2293
2566
|
const sponsorIdxRef = useRef(0);
|
|
2294
2567
|
const sponsorPrevRef = useRef(void 0);
|
|
2295
2568
|
const tallyRef = useRef(createTally());
|
|
2569
|
+
const heightCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
2296
2570
|
const push = useCallback((e) => {
|
|
2297
|
-
|
|
2571
|
+
const id = idRef.current++;
|
|
2572
|
+
setEntries((prev) => [...prev, { ...e, id }]);
|
|
2573
|
+
return id;
|
|
2298
2574
|
}, []);
|
|
2575
|
+
const syncQueue = useCallback(() => setQueueView([...queueRef.current]), []);
|
|
2576
|
+
const enqueue = useCallback(
|
|
2577
|
+
(line2) => {
|
|
2578
|
+
queueRef.current.push(line2);
|
|
2579
|
+
syncQueue();
|
|
2580
|
+
},
|
|
2581
|
+
[syncQueue]
|
|
2582
|
+
);
|
|
2583
|
+
const dequeue = useCallback(() => {
|
|
2584
|
+
const next = queueRef.current.shift();
|
|
2585
|
+
if (next !== void 0) syncQueue();
|
|
2586
|
+
return next;
|
|
2587
|
+
}, [syncQueue]);
|
|
2588
|
+
const flushDelta = useCallback(() => {
|
|
2589
|
+
if (deltaTimerRef.current) {
|
|
2590
|
+
clearTimeout(deltaTimerRef.current);
|
|
2591
|
+
deltaTimerRef.current = null;
|
|
2592
|
+
}
|
|
2593
|
+
const buffered = deltaBufRef.current;
|
|
2594
|
+
if (!buffered) return;
|
|
2595
|
+
deltaBufRef.current = "";
|
|
2596
|
+
setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + buffered }));
|
|
2597
|
+
}, []);
|
|
2598
|
+
const dropDelta = useCallback(() => {
|
|
2599
|
+
if (deltaTimerRef.current) {
|
|
2600
|
+
clearTimeout(deltaTimerRef.current);
|
|
2601
|
+
deltaTimerRef.current = null;
|
|
2602
|
+
}
|
|
2603
|
+
deltaBufRef.current = "";
|
|
2604
|
+
}, []);
|
|
2605
|
+
useEffect(() => dropDelta, [dropDelta]);
|
|
2299
2606
|
useEffect(() => {
|
|
2300
2607
|
const files = contextRef.current.files;
|
|
2301
2608
|
if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
|
|
@@ -2344,6 +2651,21 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2344
2651
|
[]
|
|
2345
2652
|
);
|
|
2346
2653
|
const permit = useMemo(() => createPermit(() => permRef.current, askUser), [askUser]);
|
|
2654
|
+
const approvePlan = useCallback(
|
|
2655
|
+
(proposed) => new Promise((resolve2) => {
|
|
2656
|
+
setPlan({
|
|
2657
|
+
plan: proposed,
|
|
2658
|
+
resolve: (decision) => {
|
|
2659
|
+
if (decision !== "keepPlanning") {
|
|
2660
|
+
permRef.current.mode = decision;
|
|
2661
|
+
setMode(decision);
|
|
2662
|
+
}
|
|
2663
|
+
resolve2(decision);
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
}),
|
|
2667
|
+
[]
|
|
2668
|
+
);
|
|
2347
2669
|
const cycleMode = useCallback(() => {
|
|
2348
2670
|
const next = nextMode(permRef.current.mode);
|
|
2349
2671
|
permRef.current.mode = next;
|
|
@@ -2351,10 +2673,13 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2351
2673
|
}, []);
|
|
2352
2674
|
const handleEvent = useCallback(
|
|
2353
2675
|
(event) => {
|
|
2676
|
+
if (event.type === "delta") {
|
|
2677
|
+
deltaBufRef.current += event.text;
|
|
2678
|
+
if (!deltaTimerRef.current) deltaTimerRef.current = setTimeout(flushDelta, DELTA_FLUSH_MS);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
flushDelta();
|
|
2354
2682
|
switch (event.type) {
|
|
2355
|
-
case "delta":
|
|
2356
|
-
setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + event.text }));
|
|
2357
|
-
return;
|
|
2358
2683
|
case "message":
|
|
2359
2684
|
push({ kind: "assistant", text: event.content });
|
|
2360
2685
|
setLive((l) => ({ ...l ?? { text: "" }, text: "" }));
|
|
@@ -2371,18 +2696,27 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2371
2696
|
);
|
|
2372
2697
|
return;
|
|
2373
2698
|
case "tool_result": {
|
|
2374
|
-
|
|
2699
|
+
const summary = runningToolRef.current?.summary ?? "";
|
|
2700
|
+
setLive((l) => l ? { text: l.text, tool: void 0 } : l);
|
|
2701
|
+
if (event.display) {
|
|
2702
|
+
push({ kind: "tool", name: event.name, summary, output: ` ${event.display}`, ok: event.ok });
|
|
2703
|
+
runningToolRef.current = null;
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2375
2706
|
const lines = event.result.split("\n").filter((l) => l.trim() !== "");
|
|
2376
2707
|
const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
|
|
2377
|
-
|
|
2378
|
-
push({
|
|
2708
|
+
const more = Math.max(0, lines.length - shown.length);
|
|
2709
|
+
const id = push({
|
|
2379
2710
|
kind: "tool",
|
|
2380
2711
|
name: event.name,
|
|
2381
|
-
summary
|
|
2712
|
+
summary,
|
|
2382
2713
|
output: shown.join("\n"),
|
|
2383
|
-
outputMore:
|
|
2714
|
+
outputMore: more,
|
|
2384
2715
|
ok: event.ok
|
|
2385
2716
|
});
|
|
2717
|
+
if (more > 0) {
|
|
2718
|
+
outputsRef.current.push({ id, label: `${event.name} ${summary}`.trim(), output: event.result });
|
|
2719
|
+
}
|
|
2386
2720
|
runningToolRef.current = null;
|
|
2387
2721
|
return;
|
|
2388
2722
|
}
|
|
@@ -2394,7 +2728,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2394
2728
|
return;
|
|
2395
2729
|
}
|
|
2396
2730
|
},
|
|
2397
|
-
[cols, push]
|
|
2731
|
+
[cols, flushDelta, push]
|
|
2398
2732
|
);
|
|
2399
2733
|
const runTurn2 = useCallback(
|
|
2400
2734
|
async (task) => {
|
|
@@ -2427,8 +2761,18 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2427
2761
|
model,
|
|
2428
2762
|
root,
|
|
2429
2763
|
history,
|
|
2430
|
-
|
|
2764
|
+
// The mode is read at the moment the turn starts and appended to the
|
|
2765
|
+
// project context, so the model is told the rule instead of paying a
|
|
2766
|
+
// round-trip to discover it. Rebuilt per turn: shift+tab, `/mode` and
|
|
2767
|
+
// an approved plan all move it mid-conversation.
|
|
2768
|
+
systemPrompt: `${contextRef.current.systemPrompt}
|
|
2769
|
+
|
|
2770
|
+
${modeInstruction(permRef.current.mode)}`,
|
|
2431
2771
|
permit,
|
|
2772
|
+
approvePlan,
|
|
2773
|
+
// Asked per model call, not once: approving a plan restores the write
|
|
2774
|
+
// tools in the middle of a turn.
|
|
2775
|
+
tools: () => toolSchemaFor(permRef.current.mode),
|
|
2432
2776
|
signal: ac.signal,
|
|
2433
2777
|
onEvent: handleEvent
|
|
2434
2778
|
});
|
|
@@ -2440,7 +2784,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2440
2784
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2441
2785
|
cwd: root,
|
|
2442
2786
|
model,
|
|
2443
|
-
|
|
2787
|
+
// The first line only: an `@path` message carries the whole file after
|
|
2788
|
+
// a blank line, and a session titled with a file's contents is not a
|
|
2789
|
+
// title. What the user typed is always the first line.
|
|
2790
|
+
title: (session?.title || task.split("\n")[0] || task).slice(0, 80),
|
|
2444
2791
|
messages: next
|
|
2445
2792
|
});
|
|
2446
2793
|
if (result.stopped === "aborted") {
|
|
@@ -2474,12 +2821,25 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2474
2821
|
}
|
|
2475
2822
|
} finally {
|
|
2476
2823
|
abortRef.current = null;
|
|
2824
|
+
dropDelta();
|
|
2477
2825
|
setLive(null);
|
|
2478
2826
|
setBusy(false);
|
|
2479
2827
|
setSponsor(null);
|
|
2480
2828
|
}
|
|
2481
2829
|
},
|
|
2482
|
-
[
|
|
2830
|
+
[
|
|
2831
|
+
approvePlan,
|
|
2832
|
+
client,
|
|
2833
|
+
contextWindow,
|
|
2834
|
+
dropDelta,
|
|
2835
|
+
handleEvent,
|
|
2836
|
+
model,
|
|
2837
|
+
nextSponsor,
|
|
2838
|
+
permit,
|
|
2839
|
+
push,
|
|
2840
|
+
root,
|
|
2841
|
+
session?.title
|
|
2842
|
+
]
|
|
2483
2843
|
);
|
|
2484
2844
|
const stopCurrent = useCallback(() => {
|
|
2485
2845
|
abortRef.current?.abort();
|
|
@@ -2612,10 +2972,20 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2612
2972
|
case "help":
|
|
2613
2973
|
push({ kind: "notice", text: helpText() });
|
|
2614
2974
|
return;
|
|
2975
|
+
// Clears both halves of what "clear" means: the model's context *and*
|
|
2976
|
+
// the screen. It used to drop only the context, so nothing visibly
|
|
2977
|
+
// happened and the transcript of a conversation the agent could no
|
|
2978
|
+
// longer remember stayed sitting there.
|
|
2615
2979
|
case "clear":
|
|
2616
2980
|
setMessages([]);
|
|
2981
|
+
setEntries([]);
|
|
2982
|
+
setStaticEpoch((n) => n + 1);
|
|
2983
|
+
outputsRef.current = [];
|
|
2984
|
+
expandedRef.current = /* @__PURE__ */ new Set();
|
|
2985
|
+
heightCacheRef.current = /* @__PURE__ */ new Map();
|
|
2617
2986
|
sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2618
|
-
|
|
2987
|
+
writeToStdout("\x1B[2J\x1B[3J\x1B[H");
|
|
2988
|
+
push({ kind: "notice", text: " cleared \u2014 new session, empty context" });
|
|
2619
2989
|
return;
|
|
2620
2990
|
case "mode": {
|
|
2621
2991
|
const wanted = MODES.find((m) => m.toLowerCase() === arg.toLowerCase().replace(/[-\s]/g, ""));
|
|
@@ -2643,7 +3013,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2643
3013
|
[{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
|
|
2644
3014
|
{ signal }
|
|
2645
3015
|
);
|
|
2646
|
-
if (!res) return push({ kind: "notice", text: " nothing to compact yet" });
|
|
3016
|
+
if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
|
|
2647
3017
|
setMessages(res.messages.slice(1));
|
|
2648
3018
|
setBalance(res.balance);
|
|
2649
3019
|
setSpent((s) => s + res.creditsCharged);
|
|
@@ -2662,7 +3032,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2662
3032
|
await runBusy(LOADING, async (signal) => {
|
|
2663
3033
|
const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
|
|
2664
3034
|
catalogRef.current = models;
|
|
2665
|
-
if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
|
|
3035
|
+
if (!models.length) return void push({ kind: "notice", tone: "error", text: " could not load the model list" });
|
|
2666
3036
|
if (arg && !models.some((m) => m.id === arg)) {
|
|
2667
3037
|
push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
|
|
2668
3038
|
}
|
|
@@ -2716,6 +3086,47 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2716
3086
|
// The same browser handoff the paywall takes, rather than a second way of
|
|
2717
3087
|
// doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
|
|
2718
3088
|
// in production the slash command the paywall itself recommends was a 404.
|
|
3089
|
+
// Resuming without leaving the REPL. `--continue` and `--resume` have
|
|
3090
|
+
// always existed as flags, which means the only way to pick up an
|
|
3091
|
+
// earlier conversation was to quit the one you were in.
|
|
3092
|
+
case "resume":
|
|
3093
|
+
case "sessions": {
|
|
3094
|
+
const metas = listSessions(root, 10);
|
|
3095
|
+
if (!metas.length) {
|
|
3096
|
+
push({ kind: "notice", tone: "warn", text: " no saved sessions in this directory yet" });
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const load = (id) => {
|
|
3100
|
+
const found = loadSession(id);
|
|
3101
|
+
if (!found) {
|
|
3102
|
+
push({ kind: "notice", tone: "error", text: ` no such session: ${id}` });
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
setMessages(found.messages);
|
|
3106
|
+
sessionRef.current = { id: found.id, started: found.started };
|
|
3107
|
+
push({
|
|
3108
|
+
kind: "notice",
|
|
3109
|
+
tone: "good",
|
|
3110
|
+
text: ` resumed ${found.id} \u2014 ${found.messages.length} messages
|
|
3111
|
+
${found.title}`
|
|
3112
|
+
});
|
|
3113
|
+
};
|
|
3114
|
+
if (arg) return load(arg);
|
|
3115
|
+
const items = metas.map((m) => ({
|
|
3116
|
+
value: m.id,
|
|
3117
|
+
label: m.updated.slice(0, 16).replace("T", " "),
|
|
3118
|
+
hint: m.title.slice(0, 60),
|
|
3119
|
+
current: m.id === sessionRef.current.id
|
|
3120
|
+
}));
|
|
3121
|
+
setPickerSel(0);
|
|
3122
|
+
setPicker({
|
|
3123
|
+
title: "Resume a session",
|
|
3124
|
+
subtitle: "Replaces the current conversation; nothing on screen is lost.",
|
|
3125
|
+
items,
|
|
3126
|
+
onPick: (choice) => load(choice.value)
|
|
3127
|
+
});
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
2719
3130
|
case "earn":
|
|
2720
3131
|
await runAdWall();
|
|
2721
3132
|
return;
|
|
@@ -2726,7 +3137,74 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2726
3137
|
push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
|
|
2727
3138
|
}
|
|
2728
3139
|
},
|
|
2729
|
-
[client, config, cycleMode, exit, model, push, runAdWall, runBusy, runLogin, runTurn2]
|
|
3140
|
+
[client, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runLogin, runTurn2, writeToStdout]
|
|
3141
|
+
);
|
|
3142
|
+
const runShell = useCallback(
|
|
3143
|
+
async (command) => {
|
|
3144
|
+
push({ kind: "user", text: `!${command}` });
|
|
3145
|
+
let output = "";
|
|
3146
|
+
await runBusy(RUNNING, async (signal) => {
|
|
3147
|
+
output = await TOOLS.run_command(
|
|
3148
|
+
{
|
|
3149
|
+
root,
|
|
3150
|
+
signal,
|
|
3151
|
+
onOutput: (chunk) => setLive((l) => ({
|
|
3152
|
+
text: l?.text ?? "",
|
|
3153
|
+
tool: { name: "!", summary: command, output: tailLines((l?.tool?.output ?? "") + chunk, LIVE_OUTPUT_LINES) }
|
|
3154
|
+
}))
|
|
3155
|
+
},
|
|
3156
|
+
{ command }
|
|
3157
|
+
);
|
|
3158
|
+
const lines = output.split("\n").filter((l) => l.trim() !== "");
|
|
3159
|
+
const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
|
|
3160
|
+
const more = Math.max(0, lines.length - shown.length);
|
|
3161
|
+
const id = push({
|
|
3162
|
+
kind: "tool",
|
|
3163
|
+
name: "!",
|
|
3164
|
+
summary: command,
|
|
3165
|
+
output: shown.join("\n"),
|
|
3166
|
+
outputMore: more,
|
|
3167
|
+
ok: true
|
|
3168
|
+
});
|
|
3169
|
+
if (more > 0) outputsRef.current.push({ id, label: `! ${command}`, output });
|
|
3170
|
+
});
|
|
3171
|
+
setLive(null);
|
|
3172
|
+
if (output) {
|
|
3173
|
+
setMessages((prev) => [
|
|
3174
|
+
...prev,
|
|
3175
|
+
{ role: "user", content: `I ran \`${command}\` in the workspace myself. Its output:
|
|
3176
|
+
|
|
3177
|
+
${output}` }
|
|
3178
|
+
]);
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
[cols, push, root, runBusy]
|
|
3182
|
+
);
|
|
3183
|
+
const dispatchLine = useCallback(
|
|
3184
|
+
async (line2) => {
|
|
3185
|
+
if (line2.startsWith("!")) {
|
|
3186
|
+
const command = line2.slice(1).trim();
|
|
3187
|
+
if (!command) return;
|
|
3188
|
+
return runShell(command);
|
|
3189
|
+
}
|
|
3190
|
+
push({ kind: "user", text: line2 });
|
|
3191
|
+
if (line2.startsWith("/")) return runCommand(line2);
|
|
3192
|
+
const { text: task, attached } = expandMentions(line2, root);
|
|
3193
|
+
if (attached.length) {
|
|
3194
|
+
push({ kind: "notice", text: ` attached ${attached.join(", ")}` });
|
|
3195
|
+
}
|
|
3196
|
+
return runTurn2(task);
|
|
3197
|
+
},
|
|
3198
|
+
[push, root, runCommand, runShell, runTurn2]
|
|
3199
|
+
);
|
|
3200
|
+
const runSerially = useCallback(
|
|
3201
|
+
async (line2) => {
|
|
3202
|
+
await dispatchLine(line2);
|
|
3203
|
+
for (let next = dequeue(); next !== void 0; next = dequeue()) {
|
|
3204
|
+
await dispatchLine(next);
|
|
3205
|
+
}
|
|
3206
|
+
},
|
|
3207
|
+
[dequeue, dispatchLine]
|
|
2730
3208
|
);
|
|
2731
3209
|
const submit = useCallback(
|
|
2732
3210
|
async (raw) => {
|
|
@@ -2740,16 +3218,14 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2740
3218
|
saveHistory(next);
|
|
2741
3219
|
return next;
|
|
2742
3220
|
});
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
return runTurn2(line2);
|
|
3221
|
+
if (busyRef.current) return void enqueue(line2);
|
|
3222
|
+
return runSerially(line2);
|
|
2746
3223
|
},
|
|
2747
|
-
[
|
|
3224
|
+
[enqueue, runSerially]
|
|
2748
3225
|
);
|
|
2749
3226
|
useEffect(() => {
|
|
2750
3227
|
if (!initialTask) return;
|
|
2751
|
-
|
|
2752
|
-
void runTurn2(initialTask);
|
|
3228
|
+
void runSerially(initialTask);
|
|
2753
3229
|
}, []);
|
|
2754
3230
|
const text = toText(editor);
|
|
2755
3231
|
const cursor = offset(editor);
|
|
@@ -2760,7 +3236,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2760
3236
|
}) : [],
|
|
2761
3237
|
[query, root]
|
|
2762
3238
|
);
|
|
2763
|
-
const menu = menuOff ||
|
|
3239
|
+
const menu = menuOff || ask2 || picker || plan ? [] : matches;
|
|
2764
3240
|
useEffect(() => {
|
|
2765
3241
|
setSel(0);
|
|
2766
3242
|
setMenuOff(false);
|
|
@@ -2773,6 +3249,11 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2773
3249
|
},
|
|
2774
3250
|
[cursor, query, text]
|
|
2775
3251
|
);
|
|
3252
|
+
usePaste((text2) => {
|
|
3253
|
+
if (ask2 || picker || plan) return;
|
|
3254
|
+
setEditor((state) => insert(state, text2));
|
|
3255
|
+
setHistIdx(-1);
|
|
3256
|
+
});
|
|
2776
3257
|
useInput((ch, key) => {
|
|
2777
3258
|
if (key.ctrl && ch === "c") {
|
|
2778
3259
|
if (busy) return stopCurrent();
|
|
@@ -2783,8 +3264,32 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2783
3264
|
return;
|
|
2784
3265
|
}
|
|
2785
3266
|
if (quitHint) setQuitHint(false);
|
|
3267
|
+
if (plan) {
|
|
3268
|
+
const decision = ch === "y" || ch === "j" || key.return ? "normal" : ch === "a" ? "acceptEdits" : ch === "n" ? "keepPlanning" : void 0;
|
|
3269
|
+
if (key.escape) {
|
|
3270
|
+
setPlan(null);
|
|
3271
|
+
plan.resolve("keepPlanning");
|
|
3272
|
+
stopCurrent();
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (!decision) return;
|
|
3276
|
+
setPlan(null);
|
|
3277
|
+
plan.resolve(decision);
|
|
3278
|
+
push({
|
|
3279
|
+
kind: "notice",
|
|
3280
|
+
tone: decision === "keepPlanning" ? "warn" : "good",
|
|
3281
|
+
text: decision === "keepPlanning" ? " keeping plan mode on \u2014 say what you'd like changed" : ` plan approved \xB7 mode \u2192 ${MODE_LABEL[decision]}`
|
|
3282
|
+
});
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
2786
3285
|
if (ask2) {
|
|
2787
|
-
|
|
3286
|
+
if (key.escape) {
|
|
3287
|
+
setAsk(null);
|
|
3288
|
+
ask2.resolve("deny");
|
|
3289
|
+
stopCurrent();
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
const answer = ch === "y" || ch === "j" || key.return ? "once" : ch === "a" ? "always" : ch === "n" ? "deny" : void 0;
|
|
2788
3293
|
if (!answer) return;
|
|
2789
3294
|
setAsk(null);
|
|
2790
3295
|
ask2.resolve(answer);
|
|
@@ -2801,7 +3306,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2801
3306
|
if (key.upArrow) return setPickerSel((s) => (s - 1 + n) % n);
|
|
2802
3307
|
if (key.downArrow) return setPickerSel((s) => (s + 1) % n);
|
|
2803
3308
|
if (/^[1-9]$/.test(ch)) {
|
|
2804
|
-
const
|
|
3309
|
+
const { from, count } = pickerWindowRef.current;
|
|
3310
|
+
const row = Number(ch);
|
|
3311
|
+
if (row > count) return;
|
|
3312
|
+
const choice = picker.items[from + row - 1];
|
|
2805
3313
|
if (!choice) return;
|
|
2806
3314
|
setPicker(null);
|
|
2807
3315
|
return picker.onPick(choice);
|
|
@@ -2813,14 +3321,38 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2813
3321
|
}
|
|
2814
3322
|
return;
|
|
2815
3323
|
}
|
|
2816
|
-
if (busy) {
|
|
2817
|
-
|
|
3324
|
+
if (busy && key.escape) {
|
|
3325
|
+
const dropped = queueRef.current;
|
|
3326
|
+
if (dropped.length) {
|
|
3327
|
+
queueRef.current = [];
|
|
3328
|
+
syncQueue();
|
|
3329
|
+
push({
|
|
3330
|
+
kind: "notice",
|
|
3331
|
+
tone: "warn",
|
|
3332
|
+
text: ` not sent:
|
|
3333
|
+
${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
|
|
3334
|
+
});
|
|
3335
|
+
}
|
|
3336
|
+
stopCurrent();
|
|
2818
3337
|
return;
|
|
2819
3338
|
}
|
|
2820
3339
|
if (key.tab && key.shift) return cycleMode();
|
|
2821
3340
|
if (key.ctrl && ch === "o") {
|
|
2822
|
-
const
|
|
2823
|
-
if (
|
|
3341
|
+
const next = [...outputsRef.current].reverse().find((o) => !expandedRef.current.has(o.id));
|
|
3342
|
+
if (!next) {
|
|
3343
|
+
push({ kind: "notice", text: " nothing left to expand" });
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
expandedRef.current.add(next.id);
|
|
3347
|
+
const lines = next.output.split("\n");
|
|
3348
|
+
const shown = lines.slice(0, EXPAND_MAX_LINES);
|
|
3349
|
+
const dropped = lines.length - shown.length;
|
|
3350
|
+
push({
|
|
3351
|
+
kind: "notice",
|
|
3352
|
+
text: ` ${next.label}
|
|
3353
|
+
` + shown.map((l) => ` ${l}`).join("\n") + (dropped > 0 ? `
|
|
3354
|
+
\u2026 +${dropped} more lines (not shown)` : "")
|
|
3355
|
+
});
|
|
2824
3356
|
return;
|
|
2825
3357
|
}
|
|
2826
3358
|
if (menu.length > 0) {
|
|
@@ -2836,7 +3368,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2836
3368
|
if (key.return) {
|
|
2837
3369
|
if (key.meta || key.shift) return setEditor(newline(editor));
|
|
2838
3370
|
if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
|
|
2839
|
-
if (pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
|
|
3371
|
+
if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
|
|
2840
3372
|
return void submit(toText(editor));
|
|
2841
3373
|
}
|
|
2842
3374
|
if (key.ctrl && ch === "j") return setEditor(newline(editor));
|
|
@@ -2860,62 +3392,120 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2860
3392
|
const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
2861
3393
|
const spinner = SPINNER[tick % SPINNER.length];
|
|
2862
3394
|
const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
|
|
2863
|
-
const
|
|
2864
|
-
const
|
|
2865
|
-
|
|
3395
|
+
const viewport = Math.max(6, rows - 1);
|
|
3396
|
+
const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
|
|
3397
|
+
const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
|
|
3398
|
+
const inputBoxHeight = 2 + inputRows;
|
|
3399
|
+
const queueRows = [
|
|
3400
|
+
...queueView.slice(0, MAX_QUEUE_ROWS).map((line2) => line2.replace(/\s+/g, " ").slice(0, Math.max(10, cols - 6))),
|
|
3401
|
+
...queueView.length > MAX_QUEUE_ROWS ? [`+${queueView.length - MAX_QUEUE_ROWS} more queued`] : []
|
|
3402
|
+
];
|
|
3403
|
+
const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0) + queueRows.length;
|
|
3404
|
+
let budget = Math.max(0, viewport - chromeHeight - menu.length);
|
|
3405
|
+
const PICKER_CHROME = 8;
|
|
3406
|
+
const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - PICKER_CHROME)) : 0;
|
|
3407
|
+
const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
|
|
3408
|
+
const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
|
|
3409
|
+
const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
|
|
3410
|
+
pickerWindowRef.current = { from: pickerFrom, count: pickerItems.length };
|
|
3411
|
+
const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
|
|
3412
|
+
budget -= pickerHeight;
|
|
3413
|
+
const ASK_CHROME = 4;
|
|
3414
|
+
const askSummaryRows = ask2 ? lineCount(ask2.req.summary, cols) : 0;
|
|
3415
|
+
const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
|
|
3416
|
+
const askHeight = ask2 ? askSummaryRows + lineCount(askPreview, cols) * (askPreview ? 1 : 0) + ASK_CHROME : 0;
|
|
3417
|
+
budget -= askHeight;
|
|
3418
|
+
const PLAN_CHROME = 6;
|
|
3419
|
+
const planBlock = plan ? headRows(plan.plan, cols, Math.max(1, budget - PLAN_CHROME)) : "";
|
|
3420
|
+
const planHeight = plan ? lineCount(planBlock, cols) + PLAN_CHROME : 0;
|
|
3421
|
+
budget -= planHeight;
|
|
3422
|
+
const busyHeight = busy ? 2 : 0;
|
|
3423
|
+
budget -= busyHeight;
|
|
3424
|
+
const liveRaw = [
|
|
3425
|
+
// Trailing blank lines are rows the clamp would spend on nothing, and a
|
|
3426
|
+
// streamed answer ends on one more often than not.
|
|
3427
|
+
(live?.text ?? "").replace(/\n+$/, ""),
|
|
2866
3428
|
live?.tool ? `\u23FA ${live.tool.name} ${live.tool.summary}` : "",
|
|
2867
3429
|
live?.tool?.output ?? ""
|
|
2868
3430
|
].filter(Boolean).join("\n");
|
|
2869
|
-
const
|
|
2870
|
-
const pickerHeight = picker ? picker.items.length + 8 : 0;
|
|
2871
|
-
const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
|
|
2872
|
-
const inputBoxHeight = 2 + editor.lines.length;
|
|
2873
|
-
const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
|
|
3431
|
+
const liveBlock = tailRows(liveRaw, cols, Math.max(0, budget - 1));
|
|
2874
3432
|
const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
|
|
2875
|
-
const
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3433
|
+
const printed = useMemo(() => {
|
|
3434
|
+
const cache = heightCacheRef.current;
|
|
3435
|
+
return entries.reduce((n, e) => {
|
|
3436
|
+
const key = `${e.id}:${cols}`;
|
|
3437
|
+
let h = cache.get(key);
|
|
3438
|
+
if (h === void 0) {
|
|
3439
|
+
h = entryHeight(e, cols);
|
|
3440
|
+
cache.set(key, h);
|
|
3441
|
+
}
|
|
3442
|
+
return n + h;
|
|
3443
|
+
}, 0);
|
|
3444
|
+
}, [entries, cols]);
|
|
3445
|
+
const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
|
|
3446
|
+
const spacer = Math.max(0, viewport - printed - used);
|
|
2881
3447
|
const labelW = menu.reduce((w, c2) => Math.max(w, c2.label.length), 0);
|
|
3448
|
+
const contextPct = useMemo(() => {
|
|
3449
|
+
const window = contextWindow(model);
|
|
3450
|
+
if (!window) return 0;
|
|
3451
|
+
const used2 = conversationTokens([
|
|
3452
|
+
{ role: "system", content: contextRef.current.systemPrompt },
|
|
3453
|
+
...messages
|
|
3454
|
+
]);
|
|
3455
|
+
return used2 / window;
|
|
3456
|
+
}, [contextWindow, messages, model]);
|
|
2882
3457
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2883
|
-
/* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }),
|
|
3458
|
+
/* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
|
|
2884
3459
|
spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
|
|
2885
3460
|
liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
|
|
3461
|
+
busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
3462
|
+
/* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
|
|
3463
|
+
spinner,
|
|
3464
|
+
" "
|
|
3465
|
+
] }),
|
|
3466
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
3467
|
+
busyLabel,
|
|
3468
|
+
" ",
|
|
3469
|
+
elapsed,
|
|
3470
|
+
"s \xB7 esc to stop"
|
|
3471
|
+
] })
|
|
3472
|
+
] }) : null,
|
|
2886
3473
|
ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2887
3474
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
|
|
2888
|
-
|
|
2889
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc
|
|
3475
|
+
askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
|
|
3476
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc stops the turn" })
|
|
3477
|
+
] }) : null,
|
|
3478
|
+
plan ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MODE_STYLE.plan.color, paddingX: 1, children: [
|
|
3479
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: MODE_STYLE.plan.color, children: `${MODE_STYLE.plan.glyph} Ready to act on this plan?` }),
|
|
3480
|
+
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
3481
|
+
/* @__PURE__ */ jsx2(Text2, { children: renderMarkdown(planBlock) }),
|
|
3482
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] yes, ask before edits \xB7 [a] yes, auto-accept edits \xB7 [n] keep planning \xB7 esc stops the turn" })
|
|
2890
3483
|
] }) : null,
|
|
2891
3484
|
picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
|
|
2892
3485
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
|
|
2893
3486
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
|
|
2894
3487
|
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
3488
|
+
pickerItems.map((item, i) => {
|
|
3489
|
+
const index = pickerFrom + i;
|
|
3490
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3491
|
+
/* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
|
|
3492
|
+
index === pickerSel ? "\u276F " : " ",
|
|
3493
|
+
`${i + 1}. `,
|
|
3494
|
+
item.label.padEnd(pickerLabelW + 2)
|
|
3495
|
+
] }),
|
|
3496
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
|
|
3497
|
+
item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
|
|
3498
|
+
] }, item.value);
|
|
3499
|
+
}),
|
|
2904
3500
|
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
2905
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children:
|
|
3501
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.items.length > pickerItems.length ? `\u2191\u2193 choose (${pickerFrom + 1}-${pickerFrom + pickerItems.length} of ${picker.items.length}) \xB7 1-${pickerItems.length} jump \xB7 \u23CE confirm \xB7 esc cancel` : `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel` })
|
|
2906
3502
|
] }) : null,
|
|
2907
3503
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
3504
|
+
queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u23F3 ${line2}` }, i)),
|
|
2908
3505
|
sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
|
|
2909
|
-
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ?
|
|
3506
|
+
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
|
|
2910
3507
|
/* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
|
|
2911
|
-
|
|
2912
|
-
spinner,
|
|
2913
|
-
" ",
|
|
2914
|
-
busyLabel,
|
|
2915
|
-
" ",
|
|
2916
|
-
elapsed,
|
|
2917
|
-
"s \xB7 esc to stop"
|
|
2918
|
-
] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
|
|
3508
|
+
/* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
|
|
2919
3509
|
] }),
|
|
2920
3510
|
menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
2921
3511
|
/* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
|
|
@@ -2924,17 +3514,21 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2924
3514
|
] }),
|
|
2925
3515
|
/* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
|
|
2926
3516
|
] }, item.value)),
|
|
2927
|
-
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
3517
|
+
quitHint || menu.length > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2928
3518
|
" ",
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
3519
|
+
quitHint ? "press ctrl+c again to quit" : "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close"
|
|
3520
|
+
] }) : /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3521
|
+
/* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, children: ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}` }),
|
|
3522
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
3523
|
+
" ",
|
|
3524
|
+
statusLine({
|
|
3525
|
+
model,
|
|
3526
|
+
balance,
|
|
3527
|
+
spent,
|
|
3528
|
+
contextPct,
|
|
3529
|
+
minutes: (Date.now() - runStartedAtRef.current) / 6e4
|
|
3530
|
+
})
|
|
3531
|
+
] })
|
|
2938
3532
|
] })
|
|
2939
3533
|
] })
|
|
2940
3534
|
] });
|
|
@@ -2951,9 +3545,10 @@ function sleep2(ms, signal) {
|
|
|
2951
3545
|
signal.addEventListener("abort", done, { once: true });
|
|
2952
3546
|
});
|
|
2953
3547
|
}
|
|
2954
|
-
function renderInput(state) {
|
|
2955
|
-
return state.lines.map((line2,
|
|
2956
|
-
const
|
|
3548
|
+
function renderInput(state, from, rows) {
|
|
3549
|
+
return state.lines.slice(from, from + rows).map((line2, i) => {
|
|
3550
|
+
const row = from + i;
|
|
3551
|
+
const prefix = i === 0 ? "" : "\n";
|
|
2957
3552
|
if (row !== state.row) return /* @__PURE__ */ jsx2(Text2, { children: prefix + line2 }, row);
|
|
2958
3553
|
const before = line2.slice(0, state.col);
|
|
2959
3554
|
const at = line2.slice(state.col, state.col + 1) || " ";
|
|
@@ -2966,19 +3561,21 @@ function renderInput(state) {
|
|
|
2966
3561
|
});
|
|
2967
3562
|
}
|
|
2968
3563
|
function statusLine(o) {
|
|
2969
|
-
if (o.quitHint) return "press ctrl+c again to quit";
|
|
2970
|
-
if (o.menu) return "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close";
|
|
2971
3564
|
const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
|
|
2972
3565
|
const parts = [
|
|
2973
3566
|
o.model,
|
|
2974
3567
|
`${o.balance.toLocaleString("en-US")} cr`,
|
|
2975
|
-
o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}
|
|
3568
|
+
o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
|
|
2976
3569
|
burn,
|
|
2977
|
-
|
|
3570
|
+
o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
|
|
2978
3571
|
"/help"
|
|
2979
3572
|
].filter(Boolean);
|
|
2980
3573
|
return parts.join(" \xB7 ");
|
|
2981
3574
|
}
|
|
3575
|
+
function windowStart(sel, total, size) {
|
|
3576
|
+
if (total <= size) return 0;
|
|
3577
|
+
return Math.max(0, Math.min(total - size, sel - Math.floor(size / 2)));
|
|
3578
|
+
}
|
|
2982
3579
|
function toEditorKey(ch, key) {
|
|
2983
3580
|
const name = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : key.backspace ? "backspace" : key.delete ? "delete" : void 0;
|
|
2984
3581
|
return {
|
|
@@ -3011,7 +3608,7 @@ function tailLines(text, max) {
|
|
|
3011
3608
|
const lines = text.split("\n");
|
|
3012
3609
|
return lines.length <= max ? text : lines.slice(-max).join("\n");
|
|
3013
3610
|
}
|
|
3014
|
-
var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
|
|
3611
|
+
var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, EXPAND_MAX_LINES, CONTEXT_NOTICE_AT;
|
|
3015
3612
|
var init_app = __esm({
|
|
3016
3613
|
"src/tui/app.tsx"() {
|
|
3017
3614
|
"use strict";
|
|
@@ -3021,12 +3618,14 @@ var init_app = __esm({
|
|
|
3021
3618
|
init_banner();
|
|
3022
3619
|
init_agent();
|
|
3023
3620
|
init_context();
|
|
3621
|
+
init_mentions();
|
|
3024
3622
|
init_compact();
|
|
3025
3623
|
init_kimi();
|
|
3026
3624
|
init_browser();
|
|
3027
3625
|
init_version();
|
|
3028
3626
|
init_tools();
|
|
3029
3627
|
init_permissions();
|
|
3628
|
+
init_tools();
|
|
3030
3629
|
init_session();
|
|
3031
3630
|
init_counter();
|
|
3032
3631
|
init_provider();
|
|
@@ -3034,6 +3633,7 @@ var init_app = __esm({
|
|
|
3034
3633
|
init_commands();
|
|
3035
3634
|
init_editor();
|
|
3036
3635
|
init_suggest();
|
|
3636
|
+
init_markdown();
|
|
3037
3637
|
init_views();
|
|
3038
3638
|
SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3039
3639
|
WORKING = "working\u2026";
|
|
@@ -3041,9 +3641,16 @@ var init_app = __esm({
|
|
|
3041
3641
|
LOADING = "loading\u2026";
|
|
3042
3642
|
COMPACTING = "compacting\u2026";
|
|
3043
3643
|
SIGNING_IN = "signing in\u2026";
|
|
3644
|
+
RUNNING = "running\u2026";
|
|
3044
3645
|
NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
|
|
3045
3646
|
LIVE_OUTPUT_LINES = 5;
|
|
3046
3647
|
COMMITTED_OUTPUT_LINES = 4;
|
|
3648
|
+
DELTA_FLUSH_MS = 50;
|
|
3649
|
+
MAX_INPUT_ROWS = 10;
|
|
3650
|
+
MAX_PICKER_ROWS = 12;
|
|
3651
|
+
MAX_QUEUE_ROWS = 3;
|
|
3652
|
+
EXPAND_MAX_LINES = 400;
|
|
3653
|
+
CONTEXT_NOTICE_AT = 0.6;
|
|
3047
3654
|
}
|
|
3048
3655
|
});
|
|
3049
3656
|
|
|
@@ -3165,9 +3772,13 @@ async function main() {
|
|
|
3165
3772
|
case void 0:
|
|
3166
3773
|
return repl(client, config);
|
|
3167
3774
|
default:
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3775
|
+
if (cmd.startsWith("-")) {
|
|
3776
|
+
console.error(c.red(`unknown option: ${cmd}`));
|
|
3777
|
+
printHelp();
|
|
3778
|
+
process.exitCode = 1;
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
return repl(client, config, { task: [cmd, ...rest].join(" ").trim() });
|
|
3171
3782
|
}
|
|
3172
3783
|
}
|
|
3173
3784
|
async function login(client, config, args) {
|
|
@@ -3527,7 +4138,8 @@ function printHelp() {
|
|
|
3527
4138
|
${c.cyan("--continue")} resume the last session in this directory
|
|
3528
4139
|
${c.cyan("--resume")} [id] list saved sessions, or resume one
|
|
3529
4140
|
${c.cyan("--version")} print the version and exit
|
|
3530
|
-
(no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
|
|
4141
|
+
(no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
|
|
4142
|
+
"<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task`);
|
|
3531
4143
|
}
|
|
3532
4144
|
main().catch((err) => {
|
|
3533
4145
|
console.error(c.red(err.message));
|