clixad 0.0.1-beta.44 → 0.0.1-beta.45

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.
@@ -607,16 +607,16 @@ async function httpFailure(res, what) {
607
607
  return detail ? `${what} failed: ${res.status} \u2014 ${detail}` : `${what} failed: ${res.status}`;
608
608
  }
609
609
  function sleep(ms, signal) {
610
- return new Promise((resolve4) => {
610
+ return new Promise((resolve5) => {
611
611
  if (!signal) {
612
- setTimeout(resolve4, ms);
612
+ setTimeout(resolve5, ms);
613
613
  return;
614
614
  }
615
- if (signal.aborted) return resolve4();
615
+ if (signal.aborted) return resolve5();
616
616
  const done = () => {
617
617
  clearTimeout(timer);
618
618
  signal.removeEventListener("abort", done);
619
- resolve4();
619
+ resolve5();
620
620
  };
621
621
  const timer = setTimeout(done, ms);
622
622
  signal.addEventListener("abort", done, { once: true });
@@ -1939,16 +1939,16 @@ async function devLogin(client, config, email, invite) {
1939
1939
  };
1940
1940
  }
1941
1941
  function sleep2(ms, signal) {
1942
- return new Promise((resolve4) => {
1942
+ return new Promise((resolve5) => {
1943
1943
  if (!signal) {
1944
- setTimeout(resolve4, ms);
1944
+ setTimeout(resolve5, ms);
1945
1945
  return;
1946
1946
  }
1947
- if (signal.aborted) return resolve4();
1947
+ if (signal.aborted) return resolve5();
1948
1948
  const done = () => {
1949
1949
  clearTimeout(timer);
1950
1950
  signal.removeEventListener("abort", done);
1951
- resolve4();
1951
+ resolve5();
1952
1952
  };
1953
1953
  const timer = setTimeout(done, ms);
1954
1954
  signal.addEventListener("abort", done, { once: true });
@@ -3084,7 +3084,7 @@ var StdioTransport = class {
3084
3084
  if (this.exited) return Promise.reject(new McpError(this.exited));
3085
3085
  const id = ++this.seq;
3086
3086
  const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
3087
- return new Promise((resolve4, reject) => {
3087
+ return new Promise((resolve5, reject) => {
3088
3088
  const settle = (fn) => {
3089
3089
  clearTimeout(timer);
3090
3090
  signal?.removeEventListener("abort", onAbort);
@@ -3098,7 +3098,7 @@ var StdioTransport = class {
3098
3098
  const onAbort = () => settle(() => reject(new McpError("cancelled")));
3099
3099
  signal?.addEventListener("abort", onAbort, { once: true });
3100
3100
  this.pending.set(id, {
3101
- resolve: (v) => settle(() => resolve4(v)),
3101
+ resolve: (v) => settle(() => resolve5(v)),
3102
3102
  reject: (e) => settle(() => reject(e))
3103
3103
  });
3104
3104
  this.child.stdin?.write(payload, (err) => {
@@ -5972,12 +5972,20 @@ import {
5972
5972
  writeFileSync as writeFileSync3
5973
5973
  } from "node:fs";
5974
5974
  import { homedir as homedir5 } from "node:os";
5975
- import { join as join5 } from "node:path";
5975
+ import { join as join5, resolve as resolve4 } from "node:path";
5976
5976
  var ROOT = join5(homedir5(), ".clixad");
5977
5977
  var SESSION_DIR = join5(ROOT, "sessions");
5978
5978
  var HISTORY_FILE = join5(ROOT, "history");
5979
5979
  var MAX_HISTORY = 200;
5980
- var MAX_SESSIONS = 50;
5980
+ var MAX_SESSIONS = 200;
5981
+ var SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
5982
+ function isSessionId(id) {
5983
+ return SESSION_ID.test(id);
5984
+ }
5985
+ function sameDirectory(a, b) {
5986
+ const norm = (p) => process.platform === "win32" ? resolve4(p).toLowerCase() : resolve4(p);
5987
+ return norm(a) === norm(b);
5988
+ }
5981
5989
  function newSessionId() {
5982
5990
  const now = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "");
5983
5991
  return `${now}-${Math.random().toString(16).slice(2, 6)}`;
@@ -5992,12 +6000,25 @@ function saveSession(session) {
5992
6000
  }
5993
6001
  }
5994
6002
  function loadSession(id) {
6003
+ if (!isSessionId(id)) return void 0;
5995
6004
  try {
5996
- return JSON.parse(readFileSync5(join5(SESSION_DIR, `${id}.json`), "utf8"));
6005
+ const s = JSON.parse(readFileSync5(join5(SESSION_DIR, `${id}.json`), "utf8"));
6006
+ return s && Array.isArray(s.messages) && typeof s.id === "string" ? s : void 0;
5997
6007
  } catch {
5998
6008
  return void 0;
5999
6009
  }
6000
6010
  }
6011
+ function deleteSession(id) {
6012
+ if (!isSessionId(id)) return false;
6013
+ const path = join5(SESSION_DIR, `${id}.json`);
6014
+ try {
6015
+ if (!existsSync4(path)) return false;
6016
+ rmSync(path);
6017
+ return true;
6018
+ } catch {
6019
+ return false;
6020
+ }
6021
+ }
6001
6022
  function listSessions(cwd, limit = 20) {
6002
6023
  if (!existsSync4(SESSION_DIR)) return [];
6003
6024
  const metas = [];
@@ -6005,16 +6026,15 @@ function listSessions(cwd, limit = 20) {
6005
6026
  if (!name.endsWith(".json")) continue;
6006
6027
  const s = loadSession(name.slice(0, -5));
6007
6028
  if (!s) continue;
6008
- if (cwd && s.cwd !== cwd) continue;
6009
- const { messages: _messages, ...meta } = s;
6010
- metas.push(meta);
6029
+ if (cwd && !sameDirectory(s.cwd ?? "", cwd)) continue;
6030
+ const { messages, ...meta } = s;
6031
+ metas.push({ ...meta, title: meta.title ?? "", updated: meta.updated ?? "", messageCount: messages.length });
6011
6032
  }
6012
6033
  metas.sort((a, b) => b.updated.localeCompare(a.updated));
6013
6034
  return metas.slice(0, limit);
6014
6035
  }
6015
6036
  function hasSessionIn(cwd) {
6016
6037
  if (!existsSync4(SESSION_DIR)) return false;
6017
- const needle = `"cwd":${JSON.stringify(cwd)}`;
6018
6038
  try {
6019
6039
  for (const name of readdirSync3(SESSION_DIR)) {
6020
6040
  if (!name.endsWith(".json")) continue;
@@ -6027,8 +6047,14 @@ function hasSessionIn(cwd) {
6027
6047
  } finally {
6028
6048
  closeSync2(fd);
6029
6049
  }
6030
- if (head.includes(needle)) return true;
6031
- if (!head.includes('"cwd":') && loadSession(name.slice(0, -5))?.cwd === cwd) return true;
6050
+ const field = /"cwd":("(?:[^"\\]|\\.)*")/.exec(head)?.[1];
6051
+ let stored;
6052
+ try {
6053
+ stored = field ? JSON.parse(field) : loadSession(name.slice(0, -5))?.cwd;
6054
+ } catch {
6055
+ stored = void 0;
6056
+ }
6057
+ if (stored && sameDirectory(stored, cwd)) return true;
6032
6058
  }
6033
6059
  } catch {
6034
6060
  }
@@ -6073,6 +6099,7 @@ var CONTEXT_TOKENS = {
6073
6099
  "claude-haiku-5.5": 24e3,
6074
6100
  "deepseek-v4-flash": 1048576,
6075
6101
  "gemini-3.5-flash-lite": 1048576,
6102
+ "glm-5.3-flash": 1048576,
6076
6103
  "kimi-k2.5": 262144,
6077
6104
  "gemini-3.8-flash": 1048576,
6078
6105
  "kimi-k2.7-code": 262144,
@@ -6160,14 +6187,14 @@ function runKimiCode(config, task, opts = {}) {
6160
6187
  OPENAI_BASE_URL: `${config.gatewayUrl.replace(/\/+$/, "")}/v1`,
6161
6188
  OPENAI_API_KEY: config.token
6162
6189
  };
6163
- return new Promise((resolve4) => {
6190
+ return new Promise((resolve5) => {
6164
6191
  const child = process.platform === "win32" ? spawn5([bin, ...args].map(quoteWin).join(" "), { stdio: "inherit", env, shell: true }) : spawn5(bin, args, { stdio: "inherit", env });
6165
6192
  child.on("error", (err) => {
6166
6193
  if (err.code === "ENOENT") console.log("\n" + INSTALL_HELP);
6167
6194
  else console.error(`failed to launch Kimi CLI: ${err.message}`);
6168
- resolve4(127);
6195
+ resolve5(127);
6169
6196
  });
6170
- child.on("exit", (code) => resolve4(code ?? 0));
6197
+ child.on("exit", (code) => resolve5(code ?? 0));
6171
6198
  });
6172
6199
  }
6173
6200
  function quoteWin(s) {
@@ -6427,6 +6454,8 @@ function cutPoint(messages, keepTurns = 2) {
6427
6454
  }
6428
6455
  return 0;
6429
6456
  }
6457
+ var SUMMARY_LEAD = "Summary of the earlier conversation:\n\n";
6458
+ var SUMMARY_ACK = "Understood \u2014 continuing from that summary.";
6430
6459
  var SUMMARY_PROMPT = "Summarise the conversation above for your future self, which will continue the work without seeing it. Cover: what the user asked for, what was already changed (files and the reasoning), what was learned about the codebase, and what is still open. Be specific about file paths and names. No preamble.";
6431
6460
  async function compact(client, model, messages, opts = {}) {
6432
6461
  const cut2 = cutPoint(messages, opts.keepTurns ?? 2);
@@ -6442,10 +6471,8 @@ async function compact(client, model, messages, opts = {}) {
6442
6471
  const summary = messageText(turn.message).trim() || "(no summary returned)";
6443
6472
  const compacted = [
6444
6473
  messages[0],
6445
- { role: "user", content: `Summary of the earlier conversation:
6446
-
6447
- ${summary}` },
6448
- { role: "assistant", content: "Understood \u2014 continuing from that summary." },
6474
+ { role: "user", content: `${SUMMARY_LEAD}${summary}` },
6475
+ { role: "assistant", content: SUMMARY_ACK },
6449
6476
  ...messages.slice(cut2)
6450
6477
  ];
6451
6478
  return {
@@ -6561,6 +6588,7 @@ export {
6561
6588
  referLines,
6562
6589
  redeemMessage,
6563
6590
  ledgerReasonLabel,
6591
+ messageText,
6564
6592
  PaywallError,
6565
6593
  PremiumLockedError,
6566
6594
  LimitRefusalError,
@@ -6603,6 +6631,7 @@ export {
6603
6631
  NO_SEARCH_INSTRUCTION,
6604
6632
  runAgent,
6605
6633
  interruptedConversation,
6634
+ INTERRUPTED_CALL,
6606
6635
  collectContext,
6607
6636
  INIT_PROMPT,
6608
6637
  INIT_HINT,
@@ -6615,9 +6644,12 @@ export {
6615
6644
  switchMode,
6616
6645
  createPermit,
6617
6646
  denyAll,
6647
+ MAX_SESSIONS,
6648
+ sameDirectory,
6618
6649
  newSessionId,
6619
6650
  saveSession,
6620
6651
  loadSession,
6652
+ deleteSession,
6621
6653
  listSessions,
6622
6654
  hasSessionIn,
6623
6655
  latestSession,
@@ -6642,6 +6674,8 @@ export {
6642
6674
  estimateTokens,
6643
6675
  conversationTokens,
6644
6676
  shouldCompact,
6677
+ SUMMARY_LEAD,
6678
+ SUMMARY_ACK,
6645
6679
  compact,
6646
6680
  VERSION,
6647
6681
  VERSION_LABEL,
package/dist/clixad.mjs CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  rowTaskHint,
44
44
  runAgent,
45
45
  runKimiCode,
46
+ sameDirectory,
46
47
  saveConfig,
47
48
  savedEffort,
48
49
  shells,
@@ -53,7 +54,7 @@ import {
53
54
  takeConfigNotices,
54
55
  toolSchemaFor,
55
56
  usd
56
- } from "./chunk-JV44GYWT.mjs";
57
+ } from "./chunk-Y55DBUP6.mjs";
57
58
 
58
59
  // src/tui/load.ts
59
60
  import * as nodeModule from "node:module";
@@ -132,7 +133,7 @@ async function withProductionReact(load) {
132
133
  }
133
134
  function loadTui() {
134
135
  boundInkCaches();
135
- return withProductionReact(() => import("./tui-KA2P7O7W.mjs"));
136
+ return withProductionReact(() => import("./tui-KH7DNN65.mjs"));
136
137
  }
137
138
 
138
139
  // src/title.ts
@@ -615,13 +616,18 @@ async function codeCmd(client, config, rest) {
615
616
  }
616
617
  async function repl(client, config, opts = {}) {
617
618
  if (opts.resume === "pick") {
618
- const sessions = listSessions(void 0, 10);
619
+ const sessions = listSessions(void 0, 20);
619
620
  if (!sessions.length) return console.log(c.yellow("no saved sessions yet"));
620
- console.log(c.bold("Sessions:"));
621
+ const here = process.cwd();
622
+ console.log(c.bold("Sessions (newest first, * = this directory):"));
621
623
  for (const s of sessions) {
622
- console.log(` ${c.cyan(s.id)} ${s.updated.slice(0, 16).replace("T", " ")} ${s.title}`);
624
+ const mark = sameDirectory(s.cwd, here) ? "*" : " ";
625
+ console.log(
626
+ `${mark} ${c.cyan(s.id)} ${s.updated.slice(0, 16).replace("T", " ")} ${String(s.messageCount).padStart(4)} msgs ${s.title}`
627
+ );
628
+ if (mark === " ") console.log(c.dim(` in ${s.cwd}`));
623
629
  }
624
- console.log(c.dim("\n resume one with: clixad --resume <id>"));
630
+ console.log(c.dim("\n resume one with: clixad --resume <id> \xB7 delete one in the REPL: /resume, then d twice"));
625
631
  return;
626
632
  }
627
633
  if (!process.stdin.isTTY) {
@@ -639,9 +645,10 @@ async function repl(client, config, opts = {}) {
639
645
  if (!config.token) return;
640
646
  }
641
647
  let session;
648
+ const notices = [...opts.notices ?? []];
642
649
  if (opts.resume === "latest") {
643
650
  session = latestSession(process.cwd());
644
- if (!session) console.log(c.dim(" no previous session here \u2014 starting a new one"));
651
+ if (!session) notices.push("no saved session in this directory, so this is a new one (/resume all lists every directory's)");
645
652
  } else if (opts.resume) {
646
653
  session = loadSession(opts.resume);
647
654
  if (!session) return console.log(c.red(`no such session: ${opts.resume}`));
@@ -661,7 +668,7 @@ async function repl(client, config, opts = {}) {
661
668
  newAccount,
662
669
  preferences,
663
670
  initialTask: opts.task,
664
- ...opts.notices?.length ? { notices: opts.notices } : {},
671
+ ...notices.length ? { notices } : {},
665
672
  ...initHint ? { initHint } : {}
666
673
  });
667
674
  }
@@ -6,7 +6,9 @@ import {
6
6
  EXTRA_AD_FORMATS,
7
7
  INIT_HINT,
8
8
  INIT_PROMPT,
9
+ INTERRUPTED_CALL,
9
10
  LimitRefusalError,
11
+ MAX_SESSIONS,
10
12
  MIN_BANNER_WIDTH,
11
13
  MODES,
12
14
  MODE_LABEL,
@@ -17,6 +19,8 @@ import {
17
19
  PaywallError,
18
20
  PremiumLockedError,
19
21
  SPINNER_TICKS_PER_FRAME,
22
+ SUMMARY_ACK,
23
+ SUMMARY_LEAD,
20
24
  TERMS_NOTICE,
21
25
  TERMS_URL,
22
26
  TOOLS,
@@ -35,6 +39,7 @@ import {
35
39
  conversationTokens,
36
40
  createPermit,
37
41
  createState,
42
+ deleteSession,
38
43
  diffTint,
39
44
  effortLabel,
40
45
  effortRaised,
@@ -52,6 +57,7 @@ import {
52
57
  loadHistory,
53
58
  loadMcpConfig,
54
59
  loadSession,
60
+ messageText,
55
61
  modeInstruction,
56
62
  newSessionId,
57
63
  nextMode,
@@ -98,7 +104,7 @@ import {
98
104
  visibleLength,
99
105
  webSearchCostLine,
100
106
  welcomeBanner
101
- } from "./chunk-JV44GYWT.mjs";
107
+ } from "./chunk-Y55DBUP6.mjs";
102
108
 
103
109
  // src/tui/index.tsx
104
110
  import "react";
@@ -111,6 +117,7 @@ import { Box as Box2, Static, Text as Text2, useApp, useInput, usePaste, useStdo
111
117
  // src/mentions.ts
112
118
  var TRAILING = /[),;:]+$/;
113
119
  var MAX_MENTION_CHARS = 12e4;
120
+ var MENTIONS_HEADING = "\n\n# Files the user referenced\n";
114
121
  function findMentions(text) {
115
122
  const out = [];
116
123
  for (const match of text.matchAll(/(?:^|\s)@([^\s]+)/g)) {
@@ -145,10 +152,7 @@ ${clipped}`);
145
152
  }
146
153
  if (blocks.length === 0) return { text, attached: [], images: [] };
147
154
  return {
148
- text: `${text}
149
-
150
- # Files the user referenced
151
- Their contents are below, already read for you \u2014 do not call read_file on them again.
155
+ text: `${text}${MENTIONS_HEADING}Their contents are below, already read for you \u2014 do not call read_file on them again.
152
156
 
153
157
  ` + blocks.join("\n\n"),
154
158
  attached,
@@ -421,8 +425,8 @@ var COMMANDS = [
421
425
  { name: "clear", desc: "clear the screen and start a new session" },
422
426
  // Deliberately *no* `args` on /resume, for /login's reason: an arg hint makes
423
427
  // enter complete the command instead of running it, and the picker is the
424
- // point. `/resume <id>` still works when typed in full.
425
- { name: "resume", desc: "pick up an earlier session in this directory" },
428
+ // point. `/resume <id>`, `/resume all` and `/resume delete <id>` work typed in full.
429
+ { name: "resume", desc: "pick up or delete an earlier session (all: every directory)" },
426
430
  { name: "init", desc: "write a CLIXAD.md for this project" },
427
431
  // Deliberately *no* `args` on either, for /login's reason: an arg hint makes
428
432
  // enter complete the command instead of running it, and listing is what both
@@ -1462,9 +1466,9 @@ function presentResult(input, width, tint = diffTint()) {
1462
1466
  const [notes = "", ...rest] = result.split("\n\n");
1463
1467
  const out = nonEmpty(rest.join("\n\n")).filter((l) => l !== "(nothing new since the last read)");
1464
1468
  const { body, more } = rows(out);
1465
- const failed = /\] (exited with code (?!0\b)\d+|failed|killed)/.test(notes);
1469
+ const failed2 = /\] (exited with code (?!0\b)\d+|failed|killed)/.test(notes);
1466
1470
  return {
1467
- status: failed ? "error" : "ok",
1471
+ status: failed2 ? "error" : "ok",
1468
1472
  line: fit(nonEmpty(notes)[0] ?? "read"),
1469
1473
  body,
1470
1474
  more,
@@ -1604,6 +1608,69 @@ function askWords(req) {
1604
1608
  }
1605
1609
  }
1606
1610
 
1611
+ // src/tui/replay.ts
1612
+ function shellNote(command2, output) {
1613
+ return `I ran \`${command2}\` in the workspace myself. Its output:
1614
+
1615
+ ${output}`;
1616
+ }
1617
+ var SHELL_NOTE = /^I ran `([^\n]*)` in the workspace myself\. Its output:\n\n([\s\S]*)$/;
1618
+ var IMAGE_NOTE = /^Here (?:is the image|are the images) you read \(/;
1619
+ function failed(result) {
1620
+ return result.startsWith("error: ") || result === INTERRUPTED_CALL;
1621
+ }
1622
+ function parseArgs(raw) {
1623
+ if (!raw) return {};
1624
+ try {
1625
+ const parsed = JSON.parse(raw);
1626
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1627
+ } catch {
1628
+ return {};
1629
+ }
1630
+ }
1631
+ function replayItems(messages) {
1632
+ const answers = /* @__PURE__ */ new Map();
1633
+ for (const m of messages) {
1634
+ if (m.role === "tool" && m.tool_call_id) answers.set(m.tool_call_id, messageText(m));
1635
+ }
1636
+ const out = [];
1637
+ for (const m of messages) {
1638
+ if (m.role === "user") {
1639
+ const text = messageText(m);
1640
+ if (text.startsWith(SUMMARY_LEAD)) {
1641
+ out.push({ kind: "notice", text: "earlier messages were summarised to save context; the summary is what the model has" });
1642
+ continue;
1643
+ }
1644
+ if (IMAGE_NOTE.test(text)) continue;
1645
+ const shell = SHELL_NOTE.exec(text);
1646
+ if (shell) {
1647
+ out.push({ kind: "user", text: `!${shell[1]}` });
1648
+ out.push({ kind: "tool", name: "!", args: { command: shell[1] }, result: shell[2] ?? "", ok: true });
1649
+ continue;
1650
+ }
1651
+ const cut = text.indexOf(MENTIONS_HEADING);
1652
+ const typed = (cut >= 0 ? text.slice(0, cut) : text).trim();
1653
+ if (typed) out.push({ kind: "user", text: typed });
1654
+ continue;
1655
+ }
1656
+ if (m.role === "assistant") {
1657
+ const text = messageText(m).trim();
1658
+ if (text && text !== SUMMARY_ACK) out.push({ kind: "assistant", text });
1659
+ for (const call of m.tool_calls ?? []) {
1660
+ const result = answers.get(call.id) ?? INTERRUPTED_CALL;
1661
+ out.push({
1662
+ kind: "tool",
1663
+ name: call.function.name,
1664
+ args: parseArgs(call.function.arguments),
1665
+ result,
1666
+ ok: !failed(result)
1667
+ });
1668
+ }
1669
+ }
1670
+ }
1671
+ return out;
1672
+ }
1673
+
1607
1674
  // src/tui/app.tsx
1608
1675
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1609
1676
  var WORKING = "working\u2026";
@@ -1715,6 +1782,7 @@ var INIT_HINT_AFTER_TURNS = 3;
1715
1782
  var freshStream = () => ({ text: "", md: {}, committed: false, streamed: false });
1716
1783
  var EARN_ROW = "\0earn";
1717
1784
  var BUY_ROW = "\0buy";
1785
+ var PICKER_DELETE_CONFIRM = "Press d again to delete it for good \xB7 any other key keeps it";
1718
1786
  function App({
1719
1787
  client,
1720
1788
  config,
@@ -1805,6 +1873,8 @@ function App({
1805
1873
  const [plan, setPlan] = useState(null);
1806
1874
  const [picker, setPicker] = useState(null);
1807
1875
  const [pickerSel, setPickerSel] = useState(0);
1876
+ const [pickerArmed, setPickerArmed] = useState(null);
1877
+ const [pickerNote, setPickerNote] = useState(null);
1808
1878
  const [buy, setBuy] = useState(null);
1809
1879
  const [tick, setTick] = useState(0);
1810
1880
  const [startedAt, setStartedAt] = useState(0);
@@ -2113,9 +2183,6 @@ function App({
2113
2183
  if (freeLine) {
2114
2184
  push({ kind: "notice", tone: (wallet?.balance ?? 0) <= 0 ? "warn" : "good", text: ` ${freeLine}` });
2115
2185
  }
2116
- if (session?.messages.length) {
2117
- push({ kind: "notice", text: ` resumed session ${session.id} (${session.messages.length} messages)` });
2118
- }
2119
2186
  if (initHint?.returning) {
2120
2187
  initHintedRef.current = true;
2121
2188
  push({ kind: "notice", text: ` ${INIT_HINT}` });
@@ -2160,14 +2227,14 @@ function App({
2160
2227
  if (cancelled) return void hub.close();
2161
2228
  mcpRef.current = hub;
2162
2229
  const ready = hub.status().filter((s) => s.state === "ready");
2163
- const failed = hub.status().filter((s) => s.state === "failed");
2230
+ const failed2 = hub.status().filter((s) => s.state === "failed");
2164
2231
  if (ready.length) {
2165
2232
  push({
2166
2233
  kind: "notice",
2167
2234
  text: ` mcp: ${ready.map((s) => `${s.name} (${s.toolCount} tools)`).join(", ")}`
2168
2235
  });
2169
2236
  }
2170
- for (const server of failed) {
2237
+ for (const server of failed2) {
2171
2238
  push({ kind: "notice", tone: "warn", text: ` mcp: ${server.name} did not start \u2014 ${server.error}` });
2172
2239
  }
2173
2240
  });
@@ -2247,7 +2314,7 @@ function App({
2247
2314
  const fitting = AD_FORMATS.filter((format) => adFitsFormat(format, colsRef.current));
2248
2315
  void (async () => {
2249
2316
  let direct = [];
2250
- let failed = false;
2317
+ let failed2 = false;
2251
2318
  if (directSupportedRef.current && fitting.length > 0) {
2252
2319
  const ask2 = sponsorDirect ?? ((formats, cols2, signal) => client.directSponsor(formats, cols2, signal));
2253
2320
  let answer = { supported: true, lines: [] };
@@ -2257,9 +2324,9 @@ function App({
2257
2324
  answer = { supported: true, lines: [], failed: true };
2258
2325
  }
2259
2326
  if (!answer.supported) directSupportedRef.current = false;
2260
- failed = answer.failed === true;
2327
+ failed2 = answer.failed === true;
2261
2328
  direct = answer.lines;
2262
- if (!failed) knownBookedRef.current = new Set(direct.map((line2) => line2.format));
2329
+ if (!failed2) knownBookedRef.current = new Set(direct.map((line2) => line2.format));
2263
2330
  }
2264
2331
  if (call !== sponsorCallRef.current) return;
2265
2332
  for (const format of ["top_bar", ...EXTRA_AD_FORMATS]) {
@@ -2267,7 +2334,7 @@ function App({
2267
2334
  const booked = direct.find((line2) => line2.format === format);
2268
2335
  if (booked && "hold" in booked) continue;
2269
2336
  if (booked) paintDirect(booked);
2270
- else if (failed && knownBookedRef.current.has(format)) continue;
2337
+ else if (failed2 && knownBookedRef.current.has(format)) continue;
2271
2338
  else fetchNetwork(format);
2272
2339
  }
2273
2340
  })();
@@ -2364,6 +2431,26 @@ function App({
2364
2431
  },
2365
2432
  [cols, keepOutput, push]
2366
2433
  );
2434
+ const replayConversation = useCallback(
2435
+ (stored) => {
2436
+ const title = stored.title ? ` \xB7 ${stored.title}` : "";
2437
+ push({
2438
+ kind: "notice",
2439
+ tone: "good",
2440
+ text: ` resumed session ${stored.id} (${stored.messages.length} messages)${title}`
2441
+ });
2442
+ for (const item of replayItems(stored.messages)) {
2443
+ if (item.kind === "tool") commitTool(item.name, item.args, { result: item.result, ok: item.ok });
2444
+ else if (item.kind === "notice") push({ kind: "notice", text: ` ${item.text}` });
2445
+ else push(item);
2446
+ }
2447
+ push({ kind: "notice", text: " the agent has all of this conversation; carry on below" });
2448
+ },
2449
+ [commitTool, push]
2450
+ );
2451
+ useEffect(() => {
2452
+ if (session?.messages.length) replayConversation(session);
2453
+ }, []);
2367
2454
  const handleEvent = useCallback(
2368
2455
  (event) => {
2369
2456
  if (event.type === "delta") {
@@ -3349,11 +3436,23 @@ ${preferencesText}` : ""),
3349
3436
  // earlier conversation was to quit the one you were in.
3350
3437
  case "resume":
3351
3438
  case "sessions": {
3352
- const metas = listSessions(root, 10);
3353
- if (!metas.length) {
3354
- push({ kind: "notice", tone: "warn", text: " no saved sessions in this directory yet" });
3439
+ const [verb, target] = arg.split(/\s+/);
3440
+ if (verb === "delete" || verb === "rm") {
3441
+ if (!target) {
3442
+ push({ kind: "notice", tone: "warn", text: " /resume delete <id>, or press d twice on a row in /resume" });
3443
+ return;
3444
+ }
3445
+ if (target === sessionRef.current.id) {
3446
+ push({ kind: "notice", tone: "warn", text: ` ${target} is the conversation you are in. /clear starts a new one first.` });
3447
+ return;
3448
+ }
3449
+ push(
3450
+ deleteSession(target) ? { kind: "notice", tone: "good", text: ` deleted session ${target}` } : { kind: "notice", tone: "error", text: ` no such session: ${target}` }
3451
+ );
3355
3452
  return;
3356
3453
  }
3454
+ const everywhere = verb === "all";
3455
+ const metas = listSessions(everywhere ? void 0 : root, MAX_SESSIONS);
3357
3456
  const load = (id) => {
3358
3457
  const found = loadSession(id);
3359
3458
  if (!found) {
@@ -3362,26 +3461,42 @@ ${preferencesText}` : ""),
3362
3461
  }
3363
3462
  setMessages(found.messages);
3364
3463
  sessionRef.current = { id: found.id, started: found.started, title: found.title };
3464
+ todosRef.current = new TodoList();
3465
+ lastTurnRef.current = null;
3466
+ replayConversation(found);
3467
+ };
3468
+ if (arg && !everywhere) return load(arg);
3469
+ if (!metas.length) {
3470
+ const elsewhere = everywhere ? 0 : listSessions(void 0, MAX_SESSIONS).length;
3365
3471
  push({
3366
3472
  kind: "notice",
3367
- tone: "good",
3368
- text: ` resumed ${found.id} \u2014 ${found.messages.length} messages
3369
- ${found.title}`
3473
+ tone: "warn",
3474
+ text: elsewhere ? ` no saved sessions in this directory yet. ${elsewhere} in other directories: /resume all lists them.` : " no saved sessions yet"
3370
3475
  });
3371
- };
3372
- if (arg) return load(arg);
3476
+ return;
3477
+ }
3373
3478
  const items = metas.map((m) => ({
3374
3479
  value: m.id,
3375
3480
  label: m.updated.slice(0, 16).replace("T", " "),
3376
- hint: m.title.slice(0, 60),
3481
+ hint: `${m.title.slice(0, 60)} \xB7 ${m.messageCount} messages${everywhere ? ` \xB7 ${m.cwd}` : ""}`,
3377
3482
  current: m.id === sessionRef.current.id
3378
3483
  }));
3379
3484
  setPickerSel(0);
3485
+ setPickerArmed(null);
3486
+ setPickerNote(null);
3380
3487
  setPicker({
3381
- title: "Resume a session",
3382
- subtitle: "Replaces the current conversation; nothing on screen is lost.",
3488
+ title: everywhere ? "Resume a session from any directory" : "Resume a session",
3489
+ subtitle: "Replaces the current conversation; nothing on screen is lost. d twice deletes one.",
3383
3490
  items,
3384
- onPick: (choice) => load(choice.value)
3491
+ onPick: (choice) => load(choice.value),
3492
+ onDelete: (choice) => {
3493
+ if (choice.value === sessionRef.current.id) {
3494
+ return "That is the conversation you are in. /clear starts a new one first.";
3495
+ }
3496
+ if (!deleteSession(choice.value)) return "It could not be deleted.";
3497
+ push({ kind: "notice", tone: "good", text: ` deleted session ${choice.value}` });
3498
+ return void 0;
3499
+ }
3385
3500
  });
3386
3501
  return;
3387
3502
  }
@@ -3480,7 +3595,7 @@ ${preferencesText}` : ""),
3480
3595
  push({ kind: "notice", tone: "warn", text: ` unknown command: /${cmd} \u2014 try /help` });
3481
3596
  }
3482
3597
  },
3483
- [client, cols, config, cycleMode, exit, model, modelTaskHints, premium, push, root, runAdWall, runBusy, runBuy, runLogin, runTurn, setPremium, writeToStdout]
3598
+ [client, cols, config, cycleMode, exit, model, modelTaskHints, premium, push, replayConversation, root, runAdWall, runBusy, runBuy, runLogin, runTurn, setPremium, writeToStdout]
3484
3599
  );
3485
3600
  const runShell = useCallback(
3486
3601
  async (command2) => {
@@ -3507,9 +3622,7 @@ ${preferencesText}` : ""),
3507
3622
  if (output) {
3508
3623
  setMessages((prev) => [
3509
3624
  ...prev,
3510
- { role: "user", content: `I ran \`${command2}\` in the workspace myself. Its output:
3511
-
3512
- ${output}` }
3625
+ { role: "user", content: shellNote(command2, output) }
3513
3626
  ]);
3514
3627
  }
3515
3628
  },
@@ -3718,6 +3831,22 @@ ${output}` }
3718
3831
  }
3719
3832
  if (picker) {
3720
3833
  const n = picker.items.length;
3834
+ if (picker.onDelete && (ch === "d" || ch === "D") && !key.ctrl && !key.meta) {
3835
+ const item = picker.items[Math.min(pickerSel, n - 1)];
3836
+ if (!item) return;
3837
+ setPickerNote(null);
3838
+ if (pickerArmed !== item.value) return setPickerArmed(item.value);
3839
+ setPickerArmed(null);
3840
+ const refused = picker.onDelete(item);
3841
+ if (refused) return setPickerNote(refused);
3842
+ const rest = picker.items.filter((i) => i !== item);
3843
+ if (!rest.length) return setPicker(null);
3844
+ setPicker({ ...picker, items: rest });
3845
+ setPickerSel((sel2) => Math.min(sel2, rest.length - 1));
3846
+ return;
3847
+ }
3848
+ if (pickerArmed !== null) setPickerArmed(null);
3849
+ if (pickerNote !== null) setPickerNote(null);
3721
3850
  if (key.escape) {
3722
3851
  const cancel = picker.onCancel;
3723
3852
  setPicker(null);
@@ -3932,16 +4061,17 @@ ${dropped.map((l) => ` \xB7 ${l}`).join("\n")}`
3932
4061
  };
3933
4062
  });
3934
4063
  const pickerTitle = picker ? fitRow(picker.title, inner) : "";
3935
- const pickerSubtitle = picker ? fitRow(picker.subtitle, inner) : "";
4064
+ const pickerSubtitle = picker ? fitRow(pickerArmed !== null ? PICKER_DELETE_CONFIRM : pickerNote ?? picker.subtitle, inner) : "";
4065
+ const pickerDelete = picker?.onDelete ? " \xB7 d delete" : "";
3936
4066
  const pickerHint = picker ? pickHint(
3937
4067
  picker.items.length > pickerItems.length ? [
3938
- `\u2191\u2193 choose (${pickerFrom + 1}-${pickerFrom + pickerItems.length} of ${picker.items.length}) \xB7 1-${pickerItems.length} jump \xB7 \u23CE confirm \xB7 esc cancel`,
3939
- `\u2191\u2193 ${pickerFrom + 1}-${pickerFrom + pickerItems.length}/${picker.items.length} \xB7 \u23CE confirm \xB7 esc cancel`,
3940
- "\u2191\u2193 \xB7 \u23CE \xB7 esc"
4068
+ `\u2191\u2193 choose (${pickerFrom + 1}-${pickerFrom + pickerItems.length} of ${picker.items.length}) \xB7 1-${pickerItems.length} jump \xB7 \u23CE confirm${pickerDelete} \xB7 esc cancel`,
4069
+ `\u2191\u2193 ${pickerFrom + 1}-${pickerFrom + pickerItems.length}/${picker.items.length} \xB7 \u23CE confirm${pickerDelete} \xB7 esc cancel`,
4070
+ picker.onDelete ? "\u2191\u2193 \xB7 \u23CE \xB7 d \xB7 esc" : "\u2191\u2193 \xB7 \u23CE \xB7 esc"
3941
4071
  ] : [
3942
- `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump straight to a row \xB7 \u23CE confirm \xB7 esc cancel`,
3943
- `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump \xB7 \u23CE confirm \xB7 esc cancel`,
3944
- "\u2191\u2193 \xB7 \u23CE \xB7 esc"
4072
+ `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump straight to a row \xB7 \u23CE confirm${pickerDelete} \xB7 esc cancel`,
4073
+ `\u2191\u2193 choose \xB7 1-${Math.min(9, pickerItems.length)} jump \xB7 \u23CE confirm${pickerDelete} \xB7 esc cancel`,
4074
+ picker.onDelete ? "\u2191\u2193 \xB7 \u23CE \xB7 d \xB7 esc" : "\u2191\u2193 \xB7 \u23CE \xB7 esc"
3945
4075
  ],
3946
4076
  inner
3947
4077
  ) : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clixad",
3
- "version": "0.0.1-beta.44",
3
+ "version": "0.0.1-beta.45",
4
4
  "description": "Free AI coding agent in your terminal, funded by rewarded ads.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",