clixad 0.0.1-beta.7 → 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 +546 -64
- 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
|
});
|
|
@@ -1654,6 +1794,59 @@ var init_version = __esm({
|
|
|
1654
1794
|
}
|
|
1655
1795
|
});
|
|
1656
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
|
+
|
|
1657
1850
|
// src/compact.ts
|
|
1658
1851
|
function estimateTokens(text) {
|
|
1659
1852
|
return Math.ceil(text.length / 4);
|
|
@@ -1830,9 +2023,10 @@ function commandLabel(c2) {
|
|
|
1830
2023
|
function helpText() {
|
|
1831
2024
|
const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
|
|
1832
2025
|
return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
|
|
1833
|
-
" @path
|
|
2026
|
+
" @path attach a file to the message (tab completes)",
|
|
2027
|
+
" !command run a shell command yourself, no model call",
|
|
1834
2028
|
" shift+tab cycle permission mode \xB7 esc stop the current turn",
|
|
1835
|
-
" ctrl+o expand the
|
|
2029
|
+
" ctrl+o expand the newest collapsed tool output \xB7 ctrl+c twice quit",
|
|
1836
2030
|
" \\ + enter continue on a new line"
|
|
1837
2031
|
].join("\n");
|
|
1838
2032
|
}
|
|
@@ -1862,7 +2056,11 @@ var init_commands = __esm({
|
|
|
1862
2056
|
// somebody in a 401 loop needs.
|
|
1863
2057
|
{ name: "login", desc: "sign in, or switch account" },
|
|
1864
2058
|
{ name: "compact", desc: "summarise the conversation to free context" },
|
|
1865
|
-
{ 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" },
|
|
1866
2064
|
{ name: "init", desc: "write a CLIXAD.md for this project" },
|
|
1867
2065
|
{ name: "exit", desc: "quit clixad" }
|
|
1868
2066
|
];
|
|
@@ -2281,7 +2479,7 @@ var init_views = __esm({
|
|
|
2281
2479
|
|
|
2282
2480
|
// src/tui/app.tsx
|
|
2283
2481
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2284
|
-
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";
|
|
2285
2483
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2286
2484
|
function useTerminalSize() {
|
|
2287
2485
|
const [size, setSize] = useState({
|
|
@@ -2299,6 +2497,7 @@ function useTerminalSize() {
|
|
|
2299
2497
|
}
|
|
2300
2498
|
function App({ client, config, wallet, session, initialTask }) {
|
|
2301
2499
|
const { exit } = useApp();
|
|
2500
|
+
const { write: writeToStdout } = useStdout();
|
|
2302
2501
|
const idRef = useRef(1);
|
|
2303
2502
|
const { rows, cols } = useTerminalSize();
|
|
2304
2503
|
const root = process.cwd();
|
|
@@ -2319,6 +2518,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2319
2518
|
})
|
|
2320
2519
|
}
|
|
2321
2520
|
]);
|
|
2521
|
+
const [staticEpoch, setStaticEpoch] = useState(0);
|
|
2322
2522
|
const [messages, setMessages] = useState(session?.messages ?? []);
|
|
2323
2523
|
const [editor, setEditor] = useState(EMPTY);
|
|
2324
2524
|
const [busy, setBusy] = useState(false);
|
|
@@ -2332,6 +2532,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2332
2532
|
const [sel, setSel] = useState(0);
|
|
2333
2533
|
const [menuOff, setMenuOff] = useState(false);
|
|
2334
2534
|
const [ask2, setAsk] = useState(null);
|
|
2535
|
+
const [plan, setPlan] = useState(null);
|
|
2335
2536
|
const [picker, setPicker] = useState(null);
|
|
2336
2537
|
const [pickerSel, setPickerSel] = useState(0);
|
|
2337
2538
|
const [tick, setTick] = useState(0);
|
|
@@ -2341,6 +2542,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2341
2542
|
const [sponsor, setSponsor] = useState(null);
|
|
2342
2543
|
const messagesRef = useRef(messages);
|
|
2343
2544
|
messagesRef.current = messages;
|
|
2545
|
+
const busyRef = useRef(false);
|
|
2546
|
+
busyRef.current = busy;
|
|
2547
|
+
const queueRef = useRef([]);
|
|
2548
|
+
const [queueView, setQueueView] = useState([]);
|
|
2344
2549
|
const permRef = useRef(createState("normal"));
|
|
2345
2550
|
const abortRef = useRef(null);
|
|
2346
2551
|
const busyAbortRef = useRef(null);
|
|
@@ -2350,7 +2555,9 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2350
2555
|
const runStartedAtRef = useRef(Date.now());
|
|
2351
2556
|
const contextRef = useRef(collectContext(root));
|
|
2352
2557
|
const catalogRef = useRef([]);
|
|
2353
|
-
const
|
|
2558
|
+
const outputsRef = useRef([]);
|
|
2559
|
+
const expandedRef = useRef(/* @__PURE__ */ new Set());
|
|
2560
|
+
const pickerWindowRef = useRef({ from: 0, count: 0 });
|
|
2354
2561
|
const runningToolRef = useRef(null);
|
|
2355
2562
|
const pendingTaskRef = useRef(null);
|
|
2356
2563
|
const deltaBufRef = useRef("");
|
|
@@ -2361,8 +2568,23 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2361
2568
|
const tallyRef = useRef(createTally());
|
|
2362
2569
|
const heightCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
2363
2570
|
const push = useCallback((e) => {
|
|
2364
|
-
|
|
2571
|
+
const id = idRef.current++;
|
|
2572
|
+
setEntries((prev) => [...prev, { ...e, id }]);
|
|
2573
|
+
return id;
|
|
2365
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]);
|
|
2366
2588
|
const flushDelta = useCallback(() => {
|
|
2367
2589
|
if (deltaTimerRef.current) {
|
|
2368
2590
|
clearTimeout(deltaTimerRef.current);
|
|
@@ -2429,6 +2651,21 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2429
2651
|
[]
|
|
2430
2652
|
);
|
|
2431
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
|
+
);
|
|
2432
2669
|
const cycleMode = useCallback(() => {
|
|
2433
2670
|
const next = nextMode(permRef.current.mode);
|
|
2434
2671
|
permRef.current.mode = next;
|
|
@@ -2459,18 +2696,27 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2459
2696
|
);
|
|
2460
2697
|
return;
|
|
2461
2698
|
case "tool_result": {
|
|
2462
|
-
|
|
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
|
+
}
|
|
2463
2706
|
const lines = event.result.split("\n").filter((l) => l.trim() !== "");
|
|
2464
2707
|
const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
|
|
2465
|
-
|
|
2466
|
-
push({
|
|
2708
|
+
const more = Math.max(0, lines.length - shown.length);
|
|
2709
|
+
const id = push({
|
|
2467
2710
|
kind: "tool",
|
|
2468
2711
|
name: event.name,
|
|
2469
|
-
summary
|
|
2712
|
+
summary,
|
|
2470
2713
|
output: shown.join("\n"),
|
|
2471
|
-
outputMore:
|
|
2714
|
+
outputMore: more,
|
|
2472
2715
|
ok: event.ok
|
|
2473
2716
|
});
|
|
2717
|
+
if (more > 0) {
|
|
2718
|
+
outputsRef.current.push({ id, label: `${event.name} ${summary}`.trim(), output: event.result });
|
|
2719
|
+
}
|
|
2474
2720
|
runningToolRef.current = null;
|
|
2475
2721
|
return;
|
|
2476
2722
|
}
|
|
@@ -2515,8 +2761,18 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2515
2761
|
model,
|
|
2516
2762
|
root,
|
|
2517
2763
|
history,
|
|
2518
|
-
|
|
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)}`,
|
|
2519
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),
|
|
2520
2776
|
signal: ac.signal,
|
|
2521
2777
|
onEvent: handleEvent
|
|
2522
2778
|
});
|
|
@@ -2528,7 +2784,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2528
2784
|
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2529
2785
|
cwd: root,
|
|
2530
2786
|
model,
|
|
2531
|
-
|
|
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),
|
|
2532
2791
|
messages: next
|
|
2533
2792
|
});
|
|
2534
2793
|
if (result.stopped === "aborted") {
|
|
@@ -2568,7 +2827,19 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2568
2827
|
setSponsor(null);
|
|
2569
2828
|
}
|
|
2570
2829
|
},
|
|
2571
|
-
[
|
|
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
|
+
]
|
|
2572
2843
|
);
|
|
2573
2844
|
const stopCurrent = useCallback(() => {
|
|
2574
2845
|
abortRef.current?.abort();
|
|
@@ -2701,10 +2972,20 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2701
2972
|
case "help":
|
|
2702
2973
|
push({ kind: "notice", text: helpText() });
|
|
2703
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.
|
|
2704
2979
|
case "clear":
|
|
2705
2980
|
setMessages([]);
|
|
2981
|
+
setEntries([]);
|
|
2982
|
+
setStaticEpoch((n) => n + 1);
|
|
2983
|
+
outputsRef.current = [];
|
|
2984
|
+
expandedRef.current = /* @__PURE__ */ new Set();
|
|
2985
|
+
heightCacheRef.current = /* @__PURE__ */ new Map();
|
|
2706
2986
|
sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2707
|
-
|
|
2987
|
+
writeToStdout("\x1B[2J\x1B[3J\x1B[H");
|
|
2988
|
+
push({ kind: "notice", text: " cleared \u2014 new session, empty context" });
|
|
2708
2989
|
return;
|
|
2709
2990
|
case "mode": {
|
|
2710
2991
|
const wanted = MODES.find((m) => m.toLowerCase() === arg.toLowerCase().replace(/[-\s]/g, ""));
|
|
@@ -2732,7 +3013,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2732
3013
|
[{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
|
|
2733
3014
|
{ signal }
|
|
2734
3015
|
);
|
|
2735
|
-
if (!res) return push({ kind: "notice", text: " nothing to compact yet" });
|
|
3016
|
+
if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
|
|
2736
3017
|
setMessages(res.messages.slice(1));
|
|
2737
3018
|
setBalance(res.balance);
|
|
2738
3019
|
setSpent((s) => s + res.creditsCharged);
|
|
@@ -2751,7 +3032,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2751
3032
|
await runBusy(LOADING, async (signal) => {
|
|
2752
3033
|
const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
|
|
2753
3034
|
catalogRef.current = models;
|
|
2754
|
-
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" });
|
|
2755
3036
|
if (arg && !models.some((m) => m.id === arg)) {
|
|
2756
3037
|
push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
|
|
2757
3038
|
}
|
|
@@ -2805,6 +3086,47 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2805
3086
|
// The same browser handoff the paywall takes, rather than a second way of
|
|
2806
3087
|
// doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
|
|
2807
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
|
+
}
|
|
2808
3130
|
case "earn":
|
|
2809
3131
|
await runAdWall();
|
|
2810
3132
|
return;
|
|
@@ -2815,7 +3137,74 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2815
3137
|
push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
|
|
2816
3138
|
}
|
|
2817
3139
|
},
|
|
2818
|
-
[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]
|
|
2819
3208
|
);
|
|
2820
3209
|
const submit = useCallback(
|
|
2821
3210
|
async (raw) => {
|
|
@@ -2829,16 +3218,14 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2829
3218
|
saveHistory(next);
|
|
2830
3219
|
return next;
|
|
2831
3220
|
});
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
return runTurn2(line2);
|
|
3221
|
+
if (busyRef.current) return void enqueue(line2);
|
|
3222
|
+
return runSerially(line2);
|
|
2835
3223
|
},
|
|
2836
|
-
[
|
|
3224
|
+
[enqueue, runSerially]
|
|
2837
3225
|
);
|
|
2838
3226
|
useEffect(() => {
|
|
2839
3227
|
if (!initialTask) return;
|
|
2840
|
-
|
|
2841
|
-
void runTurn2(initialTask);
|
|
3228
|
+
void runSerially(initialTask);
|
|
2842
3229
|
}, []);
|
|
2843
3230
|
const text = toText(editor);
|
|
2844
3231
|
const cursor = offset(editor);
|
|
@@ -2849,7 +3236,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2849
3236
|
}) : [],
|
|
2850
3237
|
[query, root]
|
|
2851
3238
|
);
|
|
2852
|
-
const menu = menuOff ||
|
|
3239
|
+
const menu = menuOff || ask2 || picker || plan ? [] : matches;
|
|
2853
3240
|
useEffect(() => {
|
|
2854
3241
|
setSel(0);
|
|
2855
3242
|
setMenuOff(false);
|
|
@@ -2862,6 +3249,11 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2862
3249
|
},
|
|
2863
3250
|
[cursor, query, text]
|
|
2864
3251
|
);
|
|
3252
|
+
usePaste((text2) => {
|
|
3253
|
+
if (ask2 || picker || plan) return;
|
|
3254
|
+
setEditor((state) => insert(state, text2));
|
|
3255
|
+
setHistIdx(-1);
|
|
3256
|
+
});
|
|
2865
3257
|
useInput((ch, key) => {
|
|
2866
3258
|
if (key.ctrl && ch === "c") {
|
|
2867
3259
|
if (busy) return stopCurrent();
|
|
@@ -2872,8 +3264,32 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2872
3264
|
return;
|
|
2873
3265
|
}
|
|
2874
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
|
+
}
|
|
2875
3285
|
if (ask2) {
|
|
2876
|
-
|
|
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;
|
|
2877
3293
|
if (!answer) return;
|
|
2878
3294
|
setAsk(null);
|
|
2879
3295
|
ask2.resolve(answer);
|
|
@@ -2890,7 +3306,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2890
3306
|
if (key.upArrow) return setPickerSel((s) => (s - 1 + n) % n);
|
|
2891
3307
|
if (key.downArrow) return setPickerSel((s) => (s + 1) % n);
|
|
2892
3308
|
if (/^[1-9]$/.test(ch)) {
|
|
2893
|
-
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];
|
|
2894
3313
|
if (!choice) return;
|
|
2895
3314
|
setPicker(null);
|
|
2896
3315
|
return picker.onPick(choice);
|
|
@@ -2902,14 +3321,38 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2902
3321
|
}
|
|
2903
3322
|
return;
|
|
2904
3323
|
}
|
|
2905
|
-
if (busy) {
|
|
2906
|
-
|
|
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();
|
|
2907
3337
|
return;
|
|
2908
3338
|
}
|
|
2909
3339
|
if (key.tab && key.shift) return cycleMode();
|
|
2910
3340
|
if (key.ctrl && ch === "o") {
|
|
2911
|
-
const
|
|
2912
|
-
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
|
+
});
|
|
2913
3356
|
return;
|
|
2914
3357
|
}
|
|
2915
3358
|
if (menu.length > 0) {
|
|
@@ -2925,7 +3368,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2925
3368
|
if (key.return) {
|
|
2926
3369
|
if (key.meta || key.shift) return setEditor(newline(editor));
|
|
2927
3370
|
if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
|
|
2928
|
-
if (pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
|
|
3371
|
+
if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
|
|
2929
3372
|
return void submit(toText(editor));
|
|
2930
3373
|
}
|
|
2931
3374
|
if (key.ctrl && ch === "j") return setEditor(newline(editor));
|
|
@@ -2953,13 +3396,18 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2953
3396
|
const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
|
|
2954
3397
|
const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
|
|
2955
3398
|
const inputBoxHeight = 2 + inputRows;
|
|
2956
|
-
const
|
|
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;
|
|
2957
3404
|
let budget = Math.max(0, viewport - chromeHeight - menu.length);
|
|
2958
3405
|
const PICKER_CHROME = 8;
|
|
2959
3406
|
const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - PICKER_CHROME)) : 0;
|
|
2960
3407
|
const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
|
|
2961
3408
|
const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
|
|
2962
3409
|
const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
|
|
3410
|
+
pickerWindowRef.current = { from: pickerFrom, count: pickerItems.length };
|
|
2963
3411
|
const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
|
|
2964
3412
|
budget -= pickerHeight;
|
|
2965
3413
|
const ASK_CHROME = 4;
|
|
@@ -2967,6 +3415,10 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2967
3415
|
const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
|
|
2968
3416
|
const askHeight = ask2 ? askSummaryRows + lineCount(askPreview, cols) * (askPreview ? 1 : 0) + ASK_CHROME : 0;
|
|
2969
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;
|
|
2970
3422
|
const busyHeight = busy ? 2 : 0;
|
|
2971
3423
|
budget -= busyHeight;
|
|
2972
3424
|
const liveRaw = [
|
|
@@ -2990,11 +3442,20 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
2990
3442
|
return n + h;
|
|
2991
3443
|
}, 0);
|
|
2992
3444
|
}, [entries, cols]);
|
|
2993
|
-
const used = chromeHeight + menu.length + pickerHeight + askHeight + busyHeight + liveHeight;
|
|
3445
|
+
const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
|
|
2994
3446
|
const spacer = Math.max(0, viewport - printed - used);
|
|
2995
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]);
|
|
2996
3457
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2997
|
-
/* @__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),
|
|
2998
3459
|
spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
|
|
2999
3460
|
liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
|
|
3000
3461
|
busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
|
|
@@ -3012,7 +3473,13 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
3012
3473
|
ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3013
3474
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
|
|
3014
3475
|
askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
|
|
3015
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc
|
|
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" })
|
|
3016
3483
|
] }) : null,
|
|
3017
3484
|
picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
|
|
3018
3485
|
/* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
|
|
@@ -3023,7 +3490,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
3023
3490
|
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3024
3491
|
/* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
|
|
3025
3492
|
index === pickerSel ? "\u276F " : " ",
|
|
3026
|
-
`${
|
|
3493
|
+
`${i + 1}. `,
|
|
3027
3494
|
item.label.padEnd(pickerLabelW + 2)
|
|
3028
3495
|
] }),
|
|
3029
3496
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
|
|
@@ -3031,13 +3498,14 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
3031
3498
|
] }, item.value);
|
|
3032
3499
|
}),
|
|
3033
3500
|
/* @__PURE__ */ jsx2(Box2, { height: 1 }),
|
|
3034
|
-
/* @__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` })
|
|
3035
3502
|
] }) : null,
|
|
3036
3503
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
3504
|
+
queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u23F3 ${line2}` }, i)),
|
|
3037
3505
|
sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
|
|
3038
|
-
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ?
|
|
3039
|
-
/* @__PURE__ */ jsx2(Text2, { color:
|
|
3040
|
-
|
|
3506
|
+
/* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
|
|
3507
|
+
/* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
|
|
3508
|
+
/* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
|
|
3041
3509
|
] }),
|
|
3042
3510
|
menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
3043
3511
|
/* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
|
|
@@ -3057,6 +3525,7 @@ function App({ client, config, wallet, session, initialTask }) {
|
|
|
3057
3525
|
model,
|
|
3058
3526
|
balance,
|
|
3059
3527
|
spent,
|
|
3528
|
+
contextPct,
|
|
3060
3529
|
minutes: (Date.now() - runStartedAtRef.current) / 6e4
|
|
3061
3530
|
})
|
|
3062
3531
|
] })
|
|
@@ -3098,6 +3567,7 @@ function statusLine(o) {
|
|
|
3098
3567
|
`${o.balance.toLocaleString("en-US")} cr`,
|
|
3099
3568
|
o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
|
|
3100
3569
|
burn,
|
|
3570
|
+
o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
|
|
3101
3571
|
"/help"
|
|
3102
3572
|
].filter(Boolean);
|
|
3103
3573
|
return parts.join(" \xB7 ");
|
|
@@ -3138,7 +3608,7 @@ function tailLines(text, max) {
|
|
|
3138
3608
|
const lines = text.split("\n");
|
|
3139
3609
|
return lines.length <= max ? text : lines.slice(-max).join("\n");
|
|
3140
3610
|
}
|
|
3141
|
-
var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS;
|
|
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;
|
|
3142
3612
|
var init_app = __esm({
|
|
3143
3613
|
"src/tui/app.tsx"() {
|
|
3144
3614
|
"use strict";
|
|
@@ -3148,12 +3618,14 @@ var init_app = __esm({
|
|
|
3148
3618
|
init_banner();
|
|
3149
3619
|
init_agent();
|
|
3150
3620
|
init_context();
|
|
3621
|
+
init_mentions();
|
|
3151
3622
|
init_compact();
|
|
3152
3623
|
init_kimi();
|
|
3153
3624
|
init_browser();
|
|
3154
3625
|
init_version();
|
|
3155
3626
|
init_tools();
|
|
3156
3627
|
init_permissions();
|
|
3628
|
+
init_tools();
|
|
3157
3629
|
init_session();
|
|
3158
3630
|
init_counter();
|
|
3159
3631
|
init_provider();
|
|
@@ -3161,6 +3633,7 @@ var init_app = __esm({
|
|
|
3161
3633
|
init_commands();
|
|
3162
3634
|
init_editor();
|
|
3163
3635
|
init_suggest();
|
|
3636
|
+
init_markdown();
|
|
3164
3637
|
init_views();
|
|
3165
3638
|
SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3166
3639
|
WORKING = "working\u2026";
|
|
@@ -3168,12 +3641,16 @@ var init_app = __esm({
|
|
|
3168
3641
|
LOADING = "loading\u2026";
|
|
3169
3642
|
COMPACTING = "compacting\u2026";
|
|
3170
3643
|
SIGNING_IN = "signing in\u2026";
|
|
3644
|
+
RUNNING = "running\u2026";
|
|
3171
3645
|
NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
|
|
3172
3646
|
LIVE_OUTPUT_LINES = 5;
|
|
3173
3647
|
COMMITTED_OUTPUT_LINES = 4;
|
|
3174
3648
|
DELTA_FLUSH_MS = 50;
|
|
3175
3649
|
MAX_INPUT_ROWS = 10;
|
|
3176
3650
|
MAX_PICKER_ROWS = 12;
|
|
3651
|
+
MAX_QUEUE_ROWS = 3;
|
|
3652
|
+
EXPAND_MAX_LINES = 400;
|
|
3653
|
+
CONTEXT_NOTICE_AT = 0.6;
|
|
3177
3654
|
}
|
|
3178
3655
|
});
|
|
3179
3656
|
|
|
@@ -3295,9 +3772,13 @@ async function main() {
|
|
|
3295
3772
|
case void 0:
|
|
3296
3773
|
return repl(client, config);
|
|
3297
3774
|
default:
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
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() });
|
|
3301
3782
|
}
|
|
3302
3783
|
}
|
|
3303
3784
|
async function login(client, config, args) {
|
|
@@ -3657,7 +4138,8 @@ function printHelp() {
|
|
|
3657
4138
|
${c.cyan("--continue")} resume the last session in this directory
|
|
3658
4139
|
${c.cyan("--resume")} [id] list saved sessions, or resume one
|
|
3659
4140
|
${c.cyan("--version")} print the version and exit
|
|
3660
|
-
(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`);
|
|
3661
4143
|
}
|
|
3662
4144
|
main().catch((err) => {
|
|
3663
4145
|
console.error(c.red(err.message));
|