clixad 0.0.1-beta.6 → 0.0.1-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/clixad.mjs +729 -117
  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
  });
@@ -1504,6 +1644,10 @@ function centered(rendered, visibleW, colW) {
1504
1644
  const lead = Math.max(0, Math.floor((colW - visibleW) / 2));
1505
1645
  return cell(" ".repeat(lead) + rendered, lead + visibleW);
1506
1646
  }
1647
+ function bannerTip(index) {
1648
+ const i = index ?? Math.floor(Math.random() * BANNER_TIPS.length);
1649
+ return BANNER_TIPS[(i % BANNER_TIPS.length + BANNER_TIPS.length) % BANNER_TIPS.length];
1650
+ }
1507
1651
  function earnedLabel(opts) {
1508
1652
  return `$${opts.earnedUsdToday.toFixed(2)}/$${opts.maxRewardUsd.toFixed(2)} today`;
1509
1653
  }
@@ -1515,14 +1659,19 @@ function welcomeBanner(opts) {
1515
1659
  const cwd = opts.cwd.replace(homedir5(), "~").replace(/\\/g, "/");
1516
1660
  const welcome = opts.name ? `Welcome back, ${truncate(opts.name, 18)}!` : "Welcome to Clixad!";
1517
1661
  const duck = duckLines();
1518
- const model = truncate(opts.model, leftW - 4);
1519
1662
  const home = truncate(cwd, leftW);
1663
+ const hint = bannerTip(opts.tipIndex);
1664
+ const hintDesc = truncate(hint.desc, Math.max(1, leftW - 3 - hint.key.length));
1520
1665
  const left = [
1521
1666
  centered(BOLD + TEXT + welcome + R2, welcome.length, leftW),
1522
1667
  cell("", 0),
1523
1668
  ...duck.map((d) => centered(d, DUCK_W, leftW)),
1524
1669
  cell("", 0),
1525
- centered(MANGO + "\u25C6 " + R2 + TEXT + model + R2, 2 + model.length, leftW),
1670
+ centered(
1671
+ MANGO + "\u25C6 " + R2 + BOLD + TEXT + hint.key + R2 + FAINT + " " + hintDesc + R2,
1672
+ 2 + hint.key.length + 1 + hintDesc.length,
1673
+ leftW
1674
+ ),
1526
1675
  centered(FAINT + home + R2, home.length, leftW)
1527
1676
  ];
1528
1677
  const tip = (k, d) => cell(MANGO + k + R2 + FAINT + " " + d + R2, k.length + 1 + d.length);
@@ -1553,10 +1702,11 @@ function welcomeBanner(opts) {
1553
1702
  }
1554
1703
  function compactBanner(opts) {
1555
1704
  const welcome = opts.name ? `Welcome back, ${opts.name}!` : "Welcome to Clixad!";
1705
+ const hint = bannerTip(opts.tipIndex);
1556
1706
  return duckLines().map((d) => " " + d).join("\n") + `
1557
1707
 
1558
1708
  ${BOLD}${TEXT}${welcome}${R2}
1559
- ${MANGO}\u25C6${R2} ${opts.model} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
1709
+ ${MANGO}\u25C6${R2} ${BOLD}${TEXT}${hint.key}${R2} ${FAINT}${hint.desc}${R2} ${FAINT}\xB7${R2} ${opts.balance.toLocaleString("en-US")} credits
1560
1710
  `;
1561
1711
  }
1562
1712
  function clearScreen() {
@@ -1568,7 +1718,7 @@ function clearScreen() {
1568
1718
  function visibleLength(s) {
1569
1719
  return s.replace(/\x1b\[[0-9;]*m/g, "").length;
1570
1720
  }
1571
- var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, MIN_BANNER_WIDTH, RIGHT_MAX;
1721
+ var R2, BOLD, fg, bg, MANGO, TEXT, MUTED, FAINT, PAL, DUCK, DUCK_W, cell, padTo, truncate, BANNER_TIPS, MIN_BANNER_WIDTH, RIGHT_MAX;
1572
1722
  var init_banner = __esm({
1573
1723
  "src/banner.ts"() {
1574
1724
  "use strict";
@@ -1610,6 +1760,15 @@ var init_banner = __esm({
1610
1760
  cell = (text, w) => ({ text, w });
1611
1761
  padTo = (c2, width) => (c2?.text ?? "") + " ".repeat(Math.max(0, width - (c2?.w ?? 0)));
1612
1762
  truncate = (s, max) => s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1763
+ BANNER_TIPS = [
1764
+ { key: "/init", desc: "make a CLIXAD.md" },
1765
+ { key: "shift+tab", desc: "switch mode" },
1766
+ { key: "@file", desc: "add a file to context" },
1767
+ { key: "ctrl+o", desc: "expand tool output" },
1768
+ { key: "esc", desc: "stop the current turn" },
1769
+ { key: "/compact", desc: "free up context" },
1770
+ { key: "/earn", desc: "top up credits" }
1771
+ ];
1613
1772
  MIN_BANNER_WIDTH = 66;
1614
1773
  RIGHT_MAX = 34;
1615
1774
  }
@@ -1635,6 +1794,59 @@ var init_version = __esm({
1635
1794
  }
1636
1795
  });
1637
1796
 
1797
+ // src/mentions.ts
1798
+ function findMentions(text) {
1799
+ const out = [];
1800
+ for (const match of text.matchAll(/(?:^|\s)@([^\s]+)/g)) {
1801
+ const path = match[1].replace(TRAILING, "");
1802
+ if (path && !out.includes(path)) out.push(path);
1803
+ }
1804
+ return out;
1805
+ }
1806
+ function expandMentions(text, root, limit = MAX_MENTION_CHARS) {
1807
+ const mentions = findMentions(text);
1808
+ if (mentions.length === 0) return { text, attached: [] };
1809
+ const blocks = [];
1810
+ const attached = [];
1811
+ let used = 0;
1812
+ for (const path of mentions) {
1813
+ if (used >= limit) break;
1814
+ let body;
1815
+ try {
1816
+ safeResolve(root, path);
1817
+ body = TOOLS.read_file({ root }, { path });
1818
+ } catch {
1819
+ continue;
1820
+ }
1821
+ const room = limit - used;
1822
+ const clipped = body.length > room ? `${body.slice(0, room)}
1823
+ \u2026 [attachment truncated \u2014 read the rest with read_file]` : body;
1824
+ used += clipped.length;
1825
+ attached.push(path);
1826
+ blocks.push(`--- ${path} ---
1827
+ ${clipped}`);
1828
+ }
1829
+ if (blocks.length === 0) return { text, attached: [] };
1830
+ return {
1831
+ text: `${text}
1832
+
1833
+ # Files the user referenced
1834
+ Their contents are below, already read for you \u2014 do not call read_file on them again.
1835
+
1836
+ ` + blocks.join("\n\n"),
1837
+ attached
1838
+ };
1839
+ }
1840
+ var TRAILING, MAX_MENTION_CHARS;
1841
+ var init_mentions = __esm({
1842
+ "src/mentions.ts"() {
1843
+ "use strict";
1844
+ init_tools();
1845
+ TRAILING = /[),;:]+$/;
1846
+ MAX_MENTION_CHARS = 12e4;
1847
+ }
1848
+ });
1849
+
1638
1850
  // src/compact.ts
1639
1851
  function estimateTokens(text) {
1640
1852
  return Math.ceil(text.length / 4);
@@ -1811,9 +2023,10 @@ function commandLabel(c2) {
1811
2023
  function helpText() {
1812
2024
  const w = Math.max(...COMMANDS.map((c2) => commandLabel(c2).length));
1813
2025
  return COMMANDS.map((c2) => ` ${commandLabel(c2).padEnd(w + 2)}${c2.desc}`).join("\n") + "\n\n" + [
1814
- " @path 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",
1815
2028
  " shift+tab cycle permission mode \xB7 esc stop the current turn",
1816
- " 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",
1817
2030
  " \\ + enter continue on a new line"
1818
2031
  ].join("\n");
1819
2032
  }
@@ -1843,7 +2056,11 @@ var init_commands = __esm({
1843
2056
  // somebody in a 401 loop needs.
1844
2057
  { name: "login", desc: "sign in, or switch account" },
1845
2058
  { name: "compact", desc: "summarise the conversation to free context" },
1846
- { name: "clear", desc: "clear the 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" },
1847
2064
  { name: "init", desc: "write a CLIXAD.md for this project" },
1848
2065
  { name: "exit", desc: "quit clixad" }
1849
2066
  ];
@@ -2158,6 +2375,45 @@ import { jsx, jsxs } from "react/jsx-runtime";
2158
2375
  function lineCount(text, cols) {
2159
2376
  return text.split("\n").reduce((n, line2) => n + Math.max(1, Math.ceil(visibleLength(line2) / Math.max(1, cols))), 0);
2160
2377
  }
2378
+ function tailRows(text, cols, max) {
2379
+ if (max <= 0) return "";
2380
+ const width = Math.max(1, cols);
2381
+ const lines = text.split("\n");
2382
+ const kept = [];
2383
+ let used = 0;
2384
+ for (let i = lines.length - 1; i >= 0; i--) {
2385
+ const line2 = lines[i];
2386
+ const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
2387
+ if (used + h <= max) {
2388
+ kept.unshift(line2);
2389
+ used += h;
2390
+ continue;
2391
+ }
2392
+ const room = max - used;
2393
+ if (room > 0) kept.unshift(line2.slice(-(room * width)));
2394
+ break;
2395
+ }
2396
+ return kept.join("\n");
2397
+ }
2398
+ function headRows(text, cols, max) {
2399
+ if (max <= 0) return "";
2400
+ const width = Math.max(1, cols);
2401
+ const lines = text.split("\n");
2402
+ const kept = [];
2403
+ let used = 0;
2404
+ for (const line2 of lines) {
2405
+ const h = Math.max(1, Math.ceil(visibleLength(line2) / width));
2406
+ if (used + h <= max) {
2407
+ kept.push(line2);
2408
+ used += h;
2409
+ continue;
2410
+ }
2411
+ const room = max - used;
2412
+ if (room > 0) kept.push(line2.slice(0, room * width));
2413
+ break;
2414
+ }
2415
+ return kept.join("\n");
2416
+ }
2161
2417
  function entryHeight(entry, cols) {
2162
2418
  const margin = entry.kind === "banner" ? 0 : 1;
2163
2419
  switch (entry.kind) {
@@ -2204,7 +2460,7 @@ function EntryView({ entry }) {
2204
2460
  ) });
2205
2461
  }
2206
2462
  }
2207
- var MANGO3, MANGO_BRIGHT;
2463
+ var MANGO3, MANGO_BRIGHT, SLATE, MODE_STYLE;
2208
2464
  var init_views = __esm({
2209
2465
  "src/tui/views.tsx"() {
2210
2466
  "use strict";
@@ -2212,12 +2468,18 @@ var init_views = __esm({
2212
2468
  init_markdown();
2213
2469
  MANGO3 = "#f5b841";
2214
2470
  MANGO_BRIGHT = "#ffcf6b";
2471
+ SLATE = "#8b98ae";
2472
+ MODE_STYLE = {
2473
+ normal: { color: SLATE, glyph: "\u23F5" },
2474
+ acceptEdits: { color: "#7ee787", glyph: "\u23F5\u23F5" },
2475
+ plan: { color: "#79c0ff", glyph: "\u23F8" }
2476
+ };
2215
2477
  }
2216
2478
  });
2217
2479
 
2218
2480
  // src/tui/app.tsx
2219
2481
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2220
- import { Box as Box2, Static, Text as Text2, useApp, useInput } from "ink";
2482
+ import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdout } from "ink";
2221
2483
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2222
2484
  function useTerminalSize() {
2223
2485
  const [size, setSize] = useState({
@@ -2235,6 +2497,7 @@ function useTerminalSize() {
2235
2497
  }
2236
2498
  function App({ client, config, wallet, session, initialTask }) {
2237
2499
  const { exit } = useApp();
2500
+ const { write: writeToStdout } = useStdout();
2238
2501
  const idRef = useRef(1);
2239
2502
  const { rows, cols } = useTerminalSize();
2240
2503
  const root = process.cwd();
@@ -2255,6 +2518,7 @@ function App({ client, config, wallet, session, initialTask }) {
2255
2518
  })
2256
2519
  }
2257
2520
  ]);
2521
+ const [staticEpoch, setStaticEpoch] = useState(0);
2258
2522
  const [messages, setMessages] = useState(session?.messages ?? []);
2259
2523
  const [editor, setEditor] = useState(EMPTY);
2260
2524
  const [busy, setBusy] = useState(false);
@@ -2268,6 +2532,7 @@ function App({ client, config, wallet, session, initialTask }) {
2268
2532
  const [sel, setSel] = useState(0);
2269
2533
  const [menuOff, setMenuOff] = useState(false);
2270
2534
  const [ask2, setAsk] = useState(null);
2535
+ const [plan, setPlan] = useState(null);
2271
2536
  const [picker, setPicker] = useState(null);
2272
2537
  const [pickerSel, setPickerSel] = useState(0);
2273
2538
  const [tick, setTick] = useState(0);
@@ -2277,6 +2542,10 @@ function App({ client, config, wallet, session, initialTask }) {
2277
2542
  const [sponsor, setSponsor] = useState(null);
2278
2543
  const messagesRef = useRef(messages);
2279
2544
  messagesRef.current = messages;
2545
+ const busyRef = useRef(false);
2546
+ busyRef.current = busy;
2547
+ const queueRef = useRef([]);
2548
+ const [queueView, setQueueView] = useState([]);
2280
2549
  const permRef = useRef(createState("normal"));
2281
2550
  const abortRef = useRef(null);
2282
2551
  const busyAbortRef = useRef(null);
@@ -2286,16 +2555,54 @@ function App({ client, config, wallet, session, initialTask }) {
2286
2555
  const runStartedAtRef = useRef(Date.now());
2287
2556
  const contextRef = useRef(collectContext(root));
2288
2557
  const catalogRef = useRef([]);
2289
- const lastOutputRef = useRef("");
2558
+ const outputsRef = useRef([]);
2559
+ const expandedRef = useRef(/* @__PURE__ */ new Set());
2560
+ const pickerWindowRef = useRef({ from: 0, count: 0 });
2290
2561
  const runningToolRef = useRef(null);
2291
2562
  const pendingTaskRef = useRef(null);
2563
+ const deltaBufRef = useRef("");
2564
+ const deltaTimerRef = useRef(null);
2292
2565
  const sponsorRef = useRef(sponsorSource());
2293
2566
  const sponsorIdxRef = useRef(0);
2294
2567
  const sponsorPrevRef = useRef(void 0);
2295
2568
  const tallyRef = useRef(createTally());
2569
+ const heightCacheRef = useRef(/* @__PURE__ */ new Map());
2296
2570
  const push = useCallback((e) => {
2297
- setEntries((prev) => [...prev, { ...e, id: idRef.current++ }]);
2571
+ const id = idRef.current++;
2572
+ setEntries((prev) => [...prev, { ...e, id }]);
2573
+ return id;
2298
2574
  }, []);
2575
+ const syncQueue = useCallback(() => setQueueView([...queueRef.current]), []);
2576
+ const enqueue = useCallback(
2577
+ (line2) => {
2578
+ queueRef.current.push(line2);
2579
+ syncQueue();
2580
+ },
2581
+ [syncQueue]
2582
+ );
2583
+ const dequeue = useCallback(() => {
2584
+ const next = queueRef.current.shift();
2585
+ if (next !== void 0) syncQueue();
2586
+ return next;
2587
+ }, [syncQueue]);
2588
+ const flushDelta = useCallback(() => {
2589
+ if (deltaTimerRef.current) {
2590
+ clearTimeout(deltaTimerRef.current);
2591
+ deltaTimerRef.current = null;
2592
+ }
2593
+ const buffered = deltaBufRef.current;
2594
+ if (!buffered) return;
2595
+ deltaBufRef.current = "";
2596
+ setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + buffered }));
2597
+ }, []);
2598
+ const dropDelta = useCallback(() => {
2599
+ if (deltaTimerRef.current) {
2600
+ clearTimeout(deltaTimerRef.current);
2601
+ deltaTimerRef.current = null;
2602
+ }
2603
+ deltaBufRef.current = "";
2604
+ }, []);
2605
+ useEffect(() => dropDelta, [dropDelta]);
2299
2606
  useEffect(() => {
2300
2607
  const files = contextRef.current.files;
2301
2608
  if (files.length) push({ kind: "notice", text: ` context: ${files.join(", ")}` });
@@ -2344,6 +2651,21 @@ function App({ client, config, wallet, session, initialTask }) {
2344
2651
  []
2345
2652
  );
2346
2653
  const permit = useMemo(() => createPermit(() => permRef.current, askUser), [askUser]);
2654
+ const approvePlan = useCallback(
2655
+ (proposed) => new Promise((resolve2) => {
2656
+ setPlan({
2657
+ plan: proposed,
2658
+ resolve: (decision) => {
2659
+ if (decision !== "keepPlanning") {
2660
+ permRef.current.mode = decision;
2661
+ setMode(decision);
2662
+ }
2663
+ resolve2(decision);
2664
+ }
2665
+ });
2666
+ }),
2667
+ []
2668
+ );
2347
2669
  const cycleMode = useCallback(() => {
2348
2670
  const next = nextMode(permRef.current.mode);
2349
2671
  permRef.current.mode = next;
@@ -2351,10 +2673,13 @@ function App({ client, config, wallet, session, initialTask }) {
2351
2673
  }, []);
2352
2674
  const handleEvent = useCallback(
2353
2675
  (event) => {
2676
+ if (event.type === "delta") {
2677
+ deltaBufRef.current += event.text;
2678
+ if (!deltaTimerRef.current) deltaTimerRef.current = setTimeout(flushDelta, DELTA_FLUSH_MS);
2679
+ return;
2680
+ }
2681
+ flushDelta();
2354
2682
  switch (event.type) {
2355
- case "delta":
2356
- setLive((l) => ({ ...l ?? { text: "" }, text: (l?.text ?? "") + event.text }));
2357
- return;
2358
2683
  case "message":
2359
2684
  push({ kind: "assistant", text: event.content });
2360
2685
  setLive((l) => ({ ...l ?? { text: "" }, text: "" }));
@@ -2371,18 +2696,27 @@ function App({ client, config, wallet, session, initialTask }) {
2371
2696
  );
2372
2697
  return;
2373
2698
  case "tool_result": {
2374
- 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
+ }
2375
2706
  const lines = event.result.split("\n").filter((l) => l.trim() !== "");
2376
2707
  const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
2377
- setLive((l) => l ? { text: l.text, tool: void 0 } : l);
2378
- push({
2708
+ const more = Math.max(0, lines.length - shown.length);
2709
+ const id = push({
2379
2710
  kind: "tool",
2380
2711
  name: event.name,
2381
- summary: runningToolRef.current?.summary ?? "",
2712
+ summary,
2382
2713
  output: shown.join("\n"),
2383
- outputMore: Math.max(0, lines.length - shown.length),
2714
+ outputMore: more,
2384
2715
  ok: event.ok
2385
2716
  });
2717
+ if (more > 0) {
2718
+ outputsRef.current.push({ id, label: `${event.name} ${summary}`.trim(), output: event.result });
2719
+ }
2386
2720
  runningToolRef.current = null;
2387
2721
  return;
2388
2722
  }
@@ -2394,7 +2728,7 @@ function App({ client, config, wallet, session, initialTask }) {
2394
2728
  return;
2395
2729
  }
2396
2730
  },
2397
- [cols, push]
2731
+ [cols, flushDelta, push]
2398
2732
  );
2399
2733
  const runTurn2 = useCallback(
2400
2734
  async (task) => {
@@ -2427,8 +2761,18 @@ function App({ client, config, wallet, session, initialTask }) {
2427
2761
  model,
2428
2762
  root,
2429
2763
  history,
2430
- 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)}`,
2431
2771
  permit,
2772
+ approvePlan,
2773
+ // Asked per model call, not once: approving a plan restores the write
2774
+ // tools in the middle of a turn.
2775
+ tools: () => toolSchemaFor(permRef.current.mode),
2432
2776
  signal: ac.signal,
2433
2777
  onEvent: handleEvent
2434
2778
  });
@@ -2440,7 +2784,10 @@ function App({ client, config, wallet, session, initialTask }) {
2440
2784
  updated: (/* @__PURE__ */ new Date()).toISOString(),
2441
2785
  cwd: root,
2442
2786
  model,
2443
- 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),
2444
2791
  messages: next
2445
2792
  });
2446
2793
  if (result.stopped === "aborted") {
@@ -2474,12 +2821,25 @@ function App({ client, config, wallet, session, initialTask }) {
2474
2821
  }
2475
2822
  } finally {
2476
2823
  abortRef.current = null;
2824
+ dropDelta();
2477
2825
  setLive(null);
2478
2826
  setBusy(false);
2479
2827
  setSponsor(null);
2480
2828
  }
2481
2829
  },
2482
- [client, contextWindow, 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
+ ]
2483
2843
  );
2484
2844
  const stopCurrent = useCallback(() => {
2485
2845
  abortRef.current?.abort();
@@ -2612,10 +2972,20 @@ function App({ client, config, wallet, session, initialTask }) {
2612
2972
  case "help":
2613
2973
  push({ kind: "notice", text: helpText() });
2614
2974
  return;
2975
+ // Clears both halves of what "clear" means: the model's context *and*
2976
+ // the screen. It used to drop only the context, so nothing visibly
2977
+ // happened and the transcript of a conversation the agent could no
2978
+ // longer remember stayed sitting there.
2615
2979
  case "clear":
2616
2980
  setMessages([]);
2981
+ setEntries([]);
2982
+ setStaticEpoch((n) => n + 1);
2983
+ outputsRef.current = [];
2984
+ expandedRef.current = /* @__PURE__ */ new Set();
2985
+ heightCacheRef.current = /* @__PURE__ */ new Map();
2617
2986
  sessionRef.current = { id: newSessionId(), started: (/* @__PURE__ */ new Date()).toISOString() };
2618
- 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" });
2619
2989
  return;
2620
2990
  case "mode": {
2621
2991
  const wanted = MODES.find((m) => m.toLowerCase() === arg.toLowerCase().replace(/[-\s]/g, ""));
@@ -2643,7 +3013,7 @@ function App({ client, config, wallet, session, initialTask }) {
2643
3013
  [{ role: "system", content: contextRef.current.systemPrompt }, ...messagesRef.current],
2644
3014
  { signal }
2645
3015
  );
2646
- if (!res) return push({ kind: "notice", text: " nothing to compact yet" });
3016
+ if (!res) return void push({ kind: "notice", text: " nothing to compact yet" });
2647
3017
  setMessages(res.messages.slice(1));
2648
3018
  setBalance(res.balance);
2649
3019
  setSpent((s) => s + res.creditsCharged);
@@ -2662,7 +3032,7 @@ function App({ client, config, wallet, session, initialTask }) {
2662
3032
  await runBusy(LOADING, async (signal) => {
2663
3033
  const models = catalogRef.current.length ? catalogRef.current : await client.models(signal);
2664
3034
  catalogRef.current = models;
2665
- if (!models.length) return push({ kind: "notice", tone: "error", text: " could not load the model list" });
3035
+ if (!models.length) return void push({ kind: "notice", tone: "error", text: " could not load the model list" });
2666
3036
  if (arg && !models.some((m) => m.id === arg)) {
2667
3037
  push({ kind: "notice", tone: "warn", text: ` unknown model: ${arg}` });
2668
3038
  }
@@ -2716,6 +3086,47 @@ function App({ client, config, wallet, session, initialTask }) {
2716
3086
  // The same browser handoff the paywall takes, rather than a second way of
2717
3087
  // doing it: /earn used to call the dev-only /v1/ads/reward simulator, so
2718
3088
  // in production the slash command the paywall itself recommends was a 404.
3089
+ // Resuming without leaving the REPL. `--continue` and `--resume` have
3090
+ // always existed as flags, which means the only way to pick up an
3091
+ // earlier conversation was to quit the one you were in.
3092
+ case "resume":
3093
+ case "sessions": {
3094
+ const metas = listSessions(root, 10);
3095
+ if (!metas.length) {
3096
+ push({ kind: "notice", tone: "warn", text: " no saved sessions in this directory yet" });
3097
+ return;
3098
+ }
3099
+ const load = (id) => {
3100
+ const found = loadSession(id);
3101
+ if (!found) {
3102
+ push({ kind: "notice", tone: "error", text: ` no such session: ${id}` });
3103
+ return;
3104
+ }
3105
+ setMessages(found.messages);
3106
+ sessionRef.current = { id: found.id, started: found.started };
3107
+ push({
3108
+ kind: "notice",
3109
+ tone: "good",
3110
+ text: ` resumed ${found.id} \u2014 ${found.messages.length} messages
3111
+ ${found.title}`
3112
+ });
3113
+ };
3114
+ if (arg) return load(arg);
3115
+ const items = metas.map((m) => ({
3116
+ value: m.id,
3117
+ label: m.updated.slice(0, 16).replace("T", " "),
3118
+ hint: m.title.slice(0, 60),
3119
+ current: m.id === sessionRef.current.id
3120
+ }));
3121
+ setPickerSel(0);
3122
+ setPicker({
3123
+ title: "Resume a session",
3124
+ subtitle: "Replaces the current conversation; nothing on screen is lost.",
3125
+ items,
3126
+ onPick: (choice) => load(choice.value)
3127
+ });
3128
+ return;
3129
+ }
2719
3130
  case "earn":
2720
3131
  await runAdWall();
2721
3132
  return;
@@ -2726,7 +3137,74 @@ function App({ client, config, wallet, session, initialTask }) {
2726
3137
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
2727
3138
  }
2728
3139
  },
2729
- [client, config, cycleMode, exit, model, push, runAdWall, runBusy, runLogin, runTurn2]
3140
+ [client, config, cycleMode, exit, model, push, root, runAdWall, runBusy, runLogin, runTurn2, writeToStdout]
3141
+ );
3142
+ const runShell = useCallback(
3143
+ async (command) => {
3144
+ push({ kind: "user", text: `!${command}` });
3145
+ let output = "";
3146
+ await runBusy(RUNNING, async (signal) => {
3147
+ output = await TOOLS.run_command(
3148
+ {
3149
+ root,
3150
+ signal,
3151
+ onOutput: (chunk) => setLive((l) => ({
3152
+ text: l?.text ?? "",
3153
+ tool: { name: "!", summary: command, output: tailLines((l?.tool?.output ?? "") + chunk, LIVE_OUTPUT_LINES) }
3154
+ }))
3155
+ },
3156
+ { command }
3157
+ );
3158
+ const lines = output.split("\n").filter((l) => l.trim() !== "");
3159
+ const shown = lines.slice(0, COMMITTED_OUTPUT_LINES).map((l) => l.slice(0, Math.max(20, cols - 8)));
3160
+ const more = Math.max(0, lines.length - shown.length);
3161
+ const id = push({
3162
+ kind: "tool",
3163
+ name: "!",
3164
+ summary: command,
3165
+ output: shown.join("\n"),
3166
+ outputMore: more,
3167
+ ok: true
3168
+ });
3169
+ if (more > 0) outputsRef.current.push({ id, label: `! ${command}`, output });
3170
+ });
3171
+ setLive(null);
3172
+ if (output) {
3173
+ setMessages((prev) => [
3174
+ ...prev,
3175
+ { role: "user", content: `I ran \`${command}\` in the workspace myself. Its output:
3176
+
3177
+ ${output}` }
3178
+ ]);
3179
+ }
3180
+ },
3181
+ [cols, push, root, runBusy]
3182
+ );
3183
+ const dispatchLine = useCallback(
3184
+ async (line2) => {
3185
+ if (line2.startsWith("!")) {
3186
+ const command = line2.slice(1).trim();
3187
+ if (!command) return;
3188
+ return runShell(command);
3189
+ }
3190
+ push({ kind: "user", text: line2 });
3191
+ if (line2.startsWith("/")) return runCommand(line2);
3192
+ const { text: task, attached } = expandMentions(line2, root);
3193
+ if (attached.length) {
3194
+ push({ kind: "notice", text: ` attached ${attached.join(", ")}` });
3195
+ }
3196
+ return runTurn2(task);
3197
+ },
3198
+ [push, root, runCommand, runShell, runTurn2]
3199
+ );
3200
+ const runSerially = useCallback(
3201
+ async (line2) => {
3202
+ await dispatchLine(line2);
3203
+ for (let next = dequeue(); next !== void 0; next = dequeue()) {
3204
+ await dispatchLine(next);
3205
+ }
3206
+ },
3207
+ [dequeue, dispatchLine]
2730
3208
  );
2731
3209
  const submit = useCallback(
2732
3210
  async (raw) => {
@@ -2740,16 +3218,14 @@ function App({ client, config, wallet, session, initialTask }) {
2740
3218
  saveHistory(next);
2741
3219
  return next;
2742
3220
  });
2743
- push({ kind: "user", text: line2 });
2744
- if (line2.startsWith("/")) return runCommand(line2);
2745
- return runTurn2(line2);
3221
+ if (busyRef.current) return void enqueue(line2);
3222
+ return runSerially(line2);
2746
3223
  },
2747
- [push, runCommand, runTurn2]
3224
+ [enqueue, runSerially]
2748
3225
  );
2749
3226
  useEffect(() => {
2750
3227
  if (!initialTask) return;
2751
- push({ kind: "user", text: initialTask });
2752
- void runTurn2(initialTask);
3228
+ void runSerially(initialTask);
2753
3229
  }, []);
2754
3230
  const text = toText(editor);
2755
3231
  const cursor = offset(editor);
@@ -2760,7 +3236,7 @@ function App({ client, config, wallet, session, initialTask }) {
2760
3236
  }) : [],
2761
3237
  [query, root]
2762
3238
  );
2763
- const menu = menuOff || busy || ask2 || picker ? [] : matches;
3239
+ const menu = menuOff || ask2 || picker || plan ? [] : matches;
2764
3240
  useEffect(() => {
2765
3241
  setSel(0);
2766
3242
  setMenuOff(false);
@@ -2773,6 +3249,11 @@ function App({ client, config, wallet, session, initialTask }) {
2773
3249
  },
2774
3250
  [cursor, query, text]
2775
3251
  );
3252
+ usePaste((text2) => {
3253
+ if (ask2 || picker || plan) return;
3254
+ setEditor((state) => insert(state, text2));
3255
+ setHistIdx(-1);
3256
+ });
2776
3257
  useInput((ch, key) => {
2777
3258
  if (key.ctrl && ch === "c") {
2778
3259
  if (busy) return stopCurrent();
@@ -2783,8 +3264,32 @@ function App({ client, config, wallet, session, initialTask }) {
2783
3264
  return;
2784
3265
  }
2785
3266
  if (quitHint) setQuitHint(false);
3267
+ if (plan) {
3268
+ const decision = ch === "y" || ch === "j" || key.return ? "normal" : ch === "a" ? "acceptEdits" : ch === "n" ? "keepPlanning" : void 0;
3269
+ if (key.escape) {
3270
+ setPlan(null);
3271
+ plan.resolve("keepPlanning");
3272
+ stopCurrent();
3273
+ return;
3274
+ }
3275
+ if (!decision) return;
3276
+ setPlan(null);
3277
+ plan.resolve(decision);
3278
+ push({
3279
+ kind: "notice",
3280
+ tone: decision === "keepPlanning" ? "warn" : "good",
3281
+ text: decision === "keepPlanning" ? " keeping plan mode on \u2014 say what you'd like changed" : ` plan approved \xB7 mode \u2192 ${MODE_LABEL[decision]}`
3282
+ });
3283
+ return;
3284
+ }
2786
3285
  if (ask2) {
2787
- 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;
2788
3293
  if (!answer) return;
2789
3294
  setAsk(null);
2790
3295
  ask2.resolve(answer);
@@ -2801,7 +3306,10 @@ function App({ client, config, wallet, session, initialTask }) {
2801
3306
  if (key.upArrow) return setPickerSel((s) => (s - 1 + n) % n);
2802
3307
  if (key.downArrow) return setPickerSel((s) => (s + 1) % n);
2803
3308
  if (/^[1-9]$/.test(ch)) {
2804
- const 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];
2805
3313
  if (!choice) return;
2806
3314
  setPicker(null);
2807
3315
  return picker.onPick(choice);
@@ -2813,14 +3321,38 @@ function App({ client, config, wallet, session, initialTask }) {
2813
3321
  }
2814
3322
  return;
2815
3323
  }
2816
- if (busy) {
2817
- 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();
2818
3337
  return;
2819
3338
  }
2820
3339
  if (key.tab && key.shift) return cycleMode();
2821
3340
  if (key.ctrl && ch === "o") {
2822
- const out = lastOutputRef.current;
2823
- 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
+ });
2824
3356
  return;
2825
3357
  }
2826
3358
  if (menu.length > 0) {
@@ -2836,7 +3368,7 @@ function App({ client, config, wallet, session, initialTask }) {
2836
3368
  if (key.return) {
2837
3369
  if (key.meta || key.shift) return setEditor(newline(editor));
2838
3370
  if (endsWithContinuation(editor)) return setEditor(continueLine(editor));
2839
- if (pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
3371
+ if (!busy && pendingTaskRef.current && isEmpty(editor)) return void runAdWall();
2840
3372
  return void submit(toText(editor));
2841
3373
  }
2842
3374
  if (key.ctrl && ch === "j") return setEditor(newline(editor));
@@ -2860,62 +3392,120 @@ function App({ client, config, wallet, session, initialTask }) {
2860
3392
  const elapsed = busy && startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
2861
3393
  const spinner = SPINNER[tick % SPINNER.length];
2862
3394
  const sponsorLine = busy && sponsor ? sponsorText(sponsor, cols) : null;
2863
- const liveText = live?.text ? tailLines(live.text, Math.max(4, rows - 12)) : "";
2864
- const liveBlock = [
2865
- liveText,
3395
+ const viewport = Math.max(6, rows - 1);
3396
+ const inputRows = Math.max(1, Math.min(MAX_INPUT_ROWS, editor.lines.length));
3397
+ const inputFrom = windowStart(editor.row, editor.lines.length, inputRows);
3398
+ const inputBoxHeight = 2 + inputRows;
3399
+ const queueRows = [
3400
+ ...queueView.slice(0, MAX_QUEUE_ROWS).map((line2) => line2.replace(/\s+/g, " ").slice(0, Math.max(10, cols - 6))),
3401
+ ...queueView.length > MAX_QUEUE_ROWS ? [`+${queueView.length - MAX_QUEUE_ROWS} more queued`] : []
3402
+ ];
3403
+ const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0) + queueRows.length;
3404
+ let budget = Math.max(0, viewport - chromeHeight - menu.length);
3405
+ const PICKER_CHROME = 8;
3406
+ const pickerRows = picker ? Math.max(1, Math.min(MAX_PICKER_ROWS, picker.items.length, budget - PICKER_CHROME)) : 0;
3407
+ const pickerFrom = picker ? windowStart(pickerSel, picker.items.length, pickerRows) : 0;
3408
+ const pickerItems = picker ? picker.items.slice(pickerFrom, pickerFrom + pickerRows) : [];
3409
+ const pickerHeight = picker ? pickerItems.length + PICKER_CHROME : 0;
3410
+ pickerWindowRef.current = { from: pickerFrom, count: pickerItems.length };
3411
+ const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
3412
+ budget -= pickerHeight;
3413
+ const ASK_CHROME = 4;
3414
+ const askSummaryRows = ask2 ? lineCount(ask2.req.summary, cols) : 0;
3415
+ const askPreview = ask2?.req.preview ? headRows(ask2.req.preview, cols, Math.max(1, budget - ASK_CHROME - askSummaryRows)) : "";
3416
+ const askHeight = ask2 ? askSummaryRows + lineCount(askPreview, cols) * (askPreview ? 1 : 0) + ASK_CHROME : 0;
3417
+ budget -= askHeight;
3418
+ const PLAN_CHROME = 6;
3419
+ const planBlock = plan ? headRows(plan.plan, cols, Math.max(1, budget - PLAN_CHROME)) : "";
3420
+ const planHeight = plan ? lineCount(planBlock, cols) + PLAN_CHROME : 0;
3421
+ budget -= planHeight;
3422
+ const busyHeight = busy ? 2 : 0;
3423
+ budget -= busyHeight;
3424
+ const liveRaw = [
3425
+ // Trailing blank lines are rows the clamp would spend on nothing, and a
3426
+ // streamed answer ends on one more often than not.
3427
+ (live?.text ?? "").replace(/\n+$/, ""),
2866
3428
  live?.tool ? `\u23FA ${live.tool.name} ${live.tool.summary}` : "",
2867
3429
  live?.tool?.output ?? ""
2868
3430
  ].filter(Boolean).join("\n");
2869
- const askBlock = ask2 ? [ask2.req.summary, ask2.req.preview ?? ""].filter(Boolean).join("\n") : "";
2870
- const pickerHeight = picker ? picker.items.length + 8 : 0;
2871
- const pickerLabelW = picker ? picker.items.reduce((w, i) => Math.max(w, i.label.length), 0) : 0;
2872
- const inputBoxHeight = 2 + editor.lines.length;
2873
- const chromeHeight = inputBoxHeight + 2 + (sponsorLine ? 1 : 0);
3431
+ const liveBlock = tailRows(liveRaw, cols, Math.max(0, budget - 1));
2874
3432
  const liveHeight = liveBlock ? lineCount(liveBlock, cols) + 1 : 0;
2875
- const askHeight = ask2 ? lineCount(askBlock, cols) + 2 : 0;
2876
- const printed = useMemo(() => entries.reduce((n, e) => n + entryHeight(e, cols), 0), [entries, cols]);
2877
- const spacer = Math.max(
2878
- 0,
2879
- rows - 1 - printed - chromeHeight - menu.length - pickerHeight - liveHeight - askHeight
2880
- );
3433
+ const printed = useMemo(() => {
3434
+ const cache = heightCacheRef.current;
3435
+ return entries.reduce((n, e) => {
3436
+ const key = `${e.id}:${cols}`;
3437
+ let h = cache.get(key);
3438
+ if (h === void 0) {
3439
+ h = entryHeight(e, cols);
3440
+ cache.set(key, h);
3441
+ }
3442
+ return n + h;
3443
+ }, 0);
3444
+ }, [entries, cols]);
3445
+ const used = chromeHeight + menu.length + pickerHeight + askHeight + planHeight + busyHeight + liveHeight;
3446
+ const spacer = Math.max(0, viewport - printed - used);
2881
3447
  const labelW = menu.reduce((w, c2) => Math.max(w, c2.label.length), 0);
3448
+ const contextPct = useMemo(() => {
3449
+ const window = contextWindow(model);
3450
+ if (!window) return 0;
3451
+ const used2 = conversationTokens([
3452
+ { role: "system", content: contextRef.current.systemPrompt },
3453
+ ...messages
3454
+ ]);
3455
+ return used2 / window;
3456
+ }, [contextWindow, messages, model]);
2882
3457
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2883
- /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }),
3458
+ /* @__PURE__ */ jsx2(Static, { items: entries, children: (entry) => /* @__PURE__ */ jsx2(EntryView, { entry }, entry.id) }, staticEpoch),
2884
3459
  spacer > 0 ? /* @__PURE__ */ jsx2(Box2, { height: spacer }) : null,
2885
3460
  liveBlock ? /* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { children: liveBlock }) }) : null,
3461
+ busy ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
3462
+ /* @__PURE__ */ jsxs2(Text2, { color: MANGO3, children: [
3463
+ spinner,
3464
+ " "
3465
+ ] }),
3466
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3467
+ busyLabel,
3468
+ " ",
3469
+ elapsed,
3470
+ "s \xB7 esc to stop"
3471
+ ] })
3472
+ ] }) : null,
2886
3473
  ask2 ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2887
3474
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: "yellow", children: ask2.req.summary }),
2888
- ask2.req.preview ? /* @__PURE__ */ jsx2(Text2, { children: ask2.req.preview }) : null,
2889
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc cancels" })
3475
+ askPreview ? /* @__PURE__ */ jsx2(Text2, { children: askPreview }) : null,
3476
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] once \xB7 [a] always \xB7 [n] no \xB7 esc stops the turn" })
3477
+ ] }) : null,
3478
+ plan ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MODE_STYLE.plan.color, paddingX: 1, children: [
3479
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: MODE_STYLE.plan.color, children: `${MODE_STYLE.plan.glyph} Ready to act on this plan?` }),
3480
+ /* @__PURE__ */ jsx2(Box2, { height: 1 }),
3481
+ /* @__PURE__ */ jsx2(Text2, { children: renderMarkdown(planBlock) }),
3482
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "[y/\u23CE] yes, ask before edits \xB7 [a] yes, auto-accept edits \xB7 [n] keep planning \xB7 esc stops the turn" })
2890
3483
  ] }) : null,
2891
3484
  picker ? /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: MANGO3, paddingX: 1, children: [
2892
3485
  /* @__PURE__ */ jsx2(Text2, { bold: true, color: MANGO_BRIGHT, children: picker.title }),
2893
3486
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: picker.subtitle }),
2894
3487
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2895
- picker.items.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2896
- /* @__PURE__ */ jsxs2(Text2, { color: i === pickerSel ? MANGO_BRIGHT : void 0, bold: i === pickerSel, children: [
2897
- i === pickerSel ? "\u276F " : " ",
2898
- `${i + 1}. `,
2899
- item.label.padEnd(pickerLabelW + 2)
2900
- ] }),
2901
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
2902
- item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
2903
- ] }, item.value)),
3488
+ pickerItems.map((item, i) => {
3489
+ const index = pickerFrom + i;
3490
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
3491
+ /* @__PURE__ */ jsxs2(Text2, { color: index === pickerSel ? MANGO_BRIGHT : void 0, bold: index === pickerSel, children: [
3492
+ index === pickerSel ? "\u276F " : " ",
3493
+ `${i + 1}. `,
3494
+ item.label.padEnd(pickerLabelW + 2)
3495
+ ] }),
3496
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: item.hint }),
3497
+ item.current ? /* @__PURE__ */ jsx2(Text2, { color: "green", children: " \u2190 current" }) : null
3498
+ ] }, item.value);
3499
+ }),
2904
3500
  /* @__PURE__ */ jsx2(Box2, { height: 1 }),
2905
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\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` })
2906
3502
  ] }) : null,
2907
3503
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
3504
+ queueRows.map((line2, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ` \u23F3 ${line2}` }, i)),
2908
3505
  sponsorLine ? /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: sponsorLine }) : null,
2909
- /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO_BRIGHT : MANGO3, paddingX: 1, children: [
3506
+ /* @__PURE__ */ jsxs2(Box2, { borderStyle: "round", borderColor: busy ? MANGO3 : MANGO_BRIGHT, paddingX: 1, children: [
2910
3507
  /* @__PURE__ */ jsx2(Text2, { color: MANGO_BRIGHT, children: "\u276F " }),
2911
- busy ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2912
- spinner,
2913
- " ",
2914
- busyLabel,
2915
- " ",
2916
- elapsed,
2917
- "s \xB7 esc to stop"
2918
- ] }) : /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor) })
3508
+ /* @__PURE__ */ jsx2(Text2, { children: renderInput(editor, inputFrom, inputRows) })
2919
3509
  ] }),
2920
3510
  menu.map((item, i) => /* @__PURE__ */ jsxs2(Box2, { children: [
2921
3511
  /* @__PURE__ */ jsxs2(Text2, { color: i === sel ? MANGO_BRIGHT : MANGO3, bold: i === sel, children: [
@@ -2924,17 +3514,21 @@ function App({ client, config, wallet, session, initialTask }) {
2924
3514
  ] }),
2925
3515
  /* @__PURE__ */ jsx2(Text2, { dimColor: i !== sel, children: item.hint })
2926
3516
  ] }, item.value)),
2927
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3517
+ quitHint || menu.length > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2928
3518
  " ",
2929
- statusLine({
2930
- menu: menu.length > 0,
2931
- quitHint,
2932
- model,
2933
- balance,
2934
- spent,
2935
- mode,
2936
- minutes: (Date.now() - runStartedAtRef.current) / 6e4
2937
- })
3519
+ quitHint ? "press ctrl+c again to quit" : "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close"
3520
+ ] }) : /* @__PURE__ */ jsxs2(Box2, { children: [
3521
+ /* @__PURE__ */ jsx2(Text2, { color: MODE_STYLE[mode].color, bold: true, children: ` ${MODE_STYLE[mode].glyph} ${MODE_LABEL[mode]}` }),
3522
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3523
+ " ",
3524
+ statusLine({
3525
+ model,
3526
+ balance,
3527
+ spent,
3528
+ contextPct,
3529
+ minutes: (Date.now() - runStartedAtRef.current) / 6e4
3530
+ })
3531
+ ] })
2938
3532
  ] })
2939
3533
  ] })
2940
3534
  ] });
@@ -2951,9 +3545,10 @@ function sleep2(ms, signal) {
2951
3545
  signal.addEventListener("abort", done, { once: true });
2952
3546
  });
2953
3547
  }
2954
- function renderInput(state) {
2955
- return state.lines.map((line2, row) => {
2956
- const prefix = row === 0 ? "" : "\n";
3548
+ function renderInput(state, from, rows) {
3549
+ return state.lines.slice(from, from + rows).map((line2, i) => {
3550
+ const row = from + i;
3551
+ const prefix = i === 0 ? "" : "\n";
2957
3552
  if (row !== state.row) return /* @__PURE__ */ jsx2(Text2, { children: prefix + line2 }, row);
2958
3553
  const before = line2.slice(0, state.col);
2959
3554
  const at = line2.slice(state.col, state.col + 1) || " ";
@@ -2966,19 +3561,21 @@ function renderInput(state) {
2966
3561
  });
2967
3562
  }
2968
3563
  function statusLine(o) {
2969
- if (o.quitHint) return "press ctrl+c again to quit";
2970
- if (o.menu) return "\u2191\u2193 choose \xB7 \u23CE run \xB7 tab complete \xB7 esc close";
2971
3564
  const burn = o.spent > 0 && o.minutes >= 1 ? `${Math.round(o.spent / o.minutes).toLocaleString("en-US")} cr/min` : void 0;
2972
3565
  const parts = [
2973
3566
  o.model,
2974
3567
  `${o.balance.toLocaleString("en-US")} cr`,
2975
- o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")} this session` : void 0,
3568
+ o.spent > 0 ? `\u2212${o.spent.toLocaleString("en-US")}` : void 0,
2976
3569
  burn,
2977
- MODE_LABEL[o.mode],
3570
+ o.contextPct >= CONTEXT_NOTICE_AT ? `context ${Math.round(o.contextPct * 100)}%` : void 0,
2978
3571
  "/help"
2979
3572
  ].filter(Boolean);
2980
3573
  return parts.join(" \xB7 ");
2981
3574
  }
3575
+ function windowStart(sel, total, size) {
3576
+ if (total <= size) return 0;
3577
+ return Math.max(0, Math.min(total - size, sel - Math.floor(size / 2)));
3578
+ }
2982
3579
  function toEditorKey(ch, key) {
2983
3580
  const name = key.leftArrow ? "left" : key.rightArrow ? "right" : key.upArrow ? "up" : key.downArrow ? "down" : key.backspace ? "backspace" : key.delete ? "delete" : void 0;
2984
3581
  return {
@@ -3011,7 +3608,7 @@ function tailLines(text, max) {
3011
3608
  const lines = text.split("\n");
3012
3609
  return lines.length <= max ? text : lines.slice(-max).join("\n");
3013
3610
  }
3014
- var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES;
3611
+ var SPINNER, WORKING, WAITING_FOR_REWARD, LOADING, COMPACTING, SIGNING_IN, RUNNING, NOT_SIGNED_IN, LIVE_OUTPUT_LINES, COMMITTED_OUTPUT_LINES, DELTA_FLUSH_MS, MAX_INPUT_ROWS, MAX_PICKER_ROWS, MAX_QUEUE_ROWS, EXPAND_MAX_LINES, CONTEXT_NOTICE_AT;
3015
3612
  var init_app = __esm({
3016
3613
  "src/tui/app.tsx"() {
3017
3614
  "use strict";
@@ -3021,12 +3618,14 @@ var init_app = __esm({
3021
3618
  init_banner();
3022
3619
  init_agent();
3023
3620
  init_context();
3621
+ init_mentions();
3024
3622
  init_compact();
3025
3623
  init_kimi();
3026
3624
  init_browser();
3027
3625
  init_version();
3028
3626
  init_tools();
3029
3627
  init_permissions();
3628
+ init_tools();
3030
3629
  init_session();
3031
3630
  init_counter();
3032
3631
  init_provider();
@@ -3034,6 +3633,7 @@ var init_app = __esm({
3034
3633
  init_commands();
3035
3634
  init_editor();
3036
3635
  init_suggest();
3636
+ init_markdown();
3037
3637
  init_views();
3038
3638
  SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
3039
3639
  WORKING = "working\u2026";
@@ -3041,9 +3641,16 @@ var init_app = __esm({
3041
3641
  LOADING = "loading\u2026";
3042
3642
  COMPACTING = "compacting\u2026";
3043
3643
  SIGNING_IN = "signing in\u2026";
3644
+ RUNNING = "running\u2026";
3044
3645
  NOT_SIGNED_IN = " Not signed in, or the stored token isn't valid for this gateway.\n Run /login to sign in.";
3045
3646
  LIVE_OUTPUT_LINES = 5;
3046
3647
  COMMITTED_OUTPUT_LINES = 4;
3648
+ DELTA_FLUSH_MS = 50;
3649
+ MAX_INPUT_ROWS = 10;
3650
+ MAX_PICKER_ROWS = 12;
3651
+ MAX_QUEUE_ROWS = 3;
3652
+ EXPAND_MAX_LINES = 400;
3653
+ CONTEXT_NOTICE_AT = 0.6;
3047
3654
  }
3048
3655
  });
3049
3656
 
@@ -3165,9 +3772,13 @@ async function main() {
3165
3772
  case void 0:
3166
3773
  return repl(client, config);
3167
3774
  default:
3168
- console.error(c.red(`unknown command: ${cmd}`));
3169
- printHelp();
3170
- 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() });
3171
3782
  }
3172
3783
  }
3173
3784
  async function login(client, config, args) {
@@ -3527,7 +4138,8 @@ function printHelp() {
3527
4138
  ${c.cyan("--continue")} resume the last session in this directory
3528
4139
  ${c.cyan("--resume")} [id] list saved sessions, or resume one
3529
4140
  ${c.cyan("--version")} print the version and exit
3530
- (no command) interactive coding REPL ${c.dim("(signs you in if needed)")}`);
4141
+ (no command) interactive coding REPL ${c.dim("(signs you in if needed)")}
4142
+ "<task>" same as ${c.cyan("agent")} \u2014 anything that is not a command is a task`);
3531
4143
  }
3532
4144
  main().catch((err) => {
3533
4145
  console.error(c.red(err.message));