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.
Files changed (2) hide show
  1. package/dist/clixad.mjs +546 -64
  2. 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 ToolError(res.reason);
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
- var ToolError, MAX_FILE_BYTES, MAX_OUTPUT_CHARS, MAX_GLOB_RESULTS, MAX_GREP_MATCHES, MAX_WALK_FILES, DEFAULT_COMMAND_TIMEOUT, SKIP_DIRS, TOOLS, TOOL_SCHEMA;
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 content = readFileSync2(abs, "utf8");
813
- if (content.length > MAX_FILE_BYTES) {
814
- return content.slice(0, MAX_FILE_BYTES) + `
815
- \u2026 [truncated at ${MAX_FILE_BYTES} chars]`;
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
- return content;
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) throw new ToolError(`old_string not found in ${args.path} \u2014 read the file and copy the exact text`);
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: { path: { type: "string" } },
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({ type: "tool_result", id, name, result: msg, ok: false });
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 changed and why.";
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 { allowed: false, reason: `the user denied ${req.summary}` };
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 reference a file (tab completes)",
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 last tool output \xB7 ctrl+c twice quit",
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 conversation context" },
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 lastOutputRef = useRef("");
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
- setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
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
- lastOutputRef.current = event.result;
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
- setLive((l) => l ? { text: l.text, tool: void 0 } : l);
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: runningToolRef.current?.summary ?? "",
2712
+ summary,
2470
2713
  output: shown.join("\n"),
2471
- outputMore: Math.max(0, lines.length - shown.length),
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
- systemPrompt: contextRef.current.systemPrompt,
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
- title: (session?.title || task).slice(0, 80),
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
- [client, contextWindow, dropDelta, handleEvent, model, nextSponsor, permit, push, root, session?.title]
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
- push({ kind: "notice", text: " (context cleared)" });
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
- push({ kind: "user", text: line2 });
2833
- if (line2.startsWith("/")) return runCommand(line2);
2834
- return runTurn2(line2);
3221
+ if (busyRef.current) return void enqueue(line2);
3222
+ return runSerially(line2);
2835
3223
  },
2836
- [push, runCommand, runTurn2]
3224
+ [enqueue, runSerially]
2837
3225
  );
2838
3226
  useEffect(() => {
2839
3227
  if (!initialTask) return;
2840
- push({ kind: "user", text: initialTask });
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 || busy || ask2 || picker ? [] : matches;
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
- const answer = ch === "y" || ch === "j" || key.return ? "once" : ch === "a" ? "always" : ch === "n" || key.escape ? "deny" : void 0;
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 choice = picker.items[Number(ch) - 1];
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
- if (key.escape) stopCurrent();
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 out = lastOutputRef.current;
2912
- if (out) push({ kind: "notice", text: out.split("\n").map((l) => ` ${l}`).join("\n") });
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 chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
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 cancels" })
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
- `${index + 1}. `,
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: "\u2191\u2193 choose \xB7 1-9 jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel" })
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 ? SLATE : MANGO3, paddingX: 1, children: [
3039
- /* @__PURE__ */ jsx2(Text2, { color: busy ? SLATE : MANGO_BRIGHT, children: "\u276F " }),
3040
- busy ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: toText(editor) }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
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
- console.error(c.red(`unknown command: ${cmd}`));
3299
- printHelp();
3300
- process.exitCode = 1;
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.7",
3
+ "version": "0.0.1-beta.8",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",