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((
|
|
610
|
+
return new Promise((resolve5) => {
|
|
611
611
|
if (!signal) {
|
|
612
|
-
setTimeout(
|
|
612
|
+
setTimeout(resolve5, ms);
|
|
613
613
|
return;
|
|
614
614
|
}
|
|
615
|
-
if (signal.aborted) return
|
|
615
|
+
if (signal.aborted) return resolve5();
|
|
616
616
|
const done = () => {
|
|
617
617
|
clearTimeout(timer);
|
|
618
618
|
signal.removeEventListener("abort", done);
|
|
619
|
-
|
|
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((
|
|
1942
|
+
return new Promise((resolve5) => {
|
|
1943
1943
|
if (!signal) {
|
|
1944
|
-
setTimeout(
|
|
1944
|
+
setTimeout(resolve5, ms);
|
|
1945
1945
|
return;
|
|
1946
1946
|
}
|
|
1947
|
-
if (signal.aborted) return
|
|
1947
|
+
if (signal.aborted) return resolve5();
|
|
1948
1948
|
const done = () => {
|
|
1949
1949
|
clearTimeout(timer);
|
|
1950
1950
|
signal.removeEventListener("abort", done);
|
|
1951
|
-
|
|
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((
|
|
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(() =>
|
|
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 =
|
|
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
|
-
|
|
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
|
|
6009
|
-
const { messages
|
|
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
|
-
|
|
6031
|
-
|
|
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((
|
|
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
|
-
|
|
6195
|
+
resolve5(127);
|
|
6169
6196
|
});
|
|
6170
|
-
child.on("exit", (code) =>
|
|
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: `
|
|
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-
|
|
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-
|
|
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,
|
|
619
|
+
const sessions = listSessions(void 0, 20);
|
|
619
620
|
if (!sessions.length) return console.log(c.yellow("no saved sessions yet"));
|
|
620
|
-
|
|
621
|
+
const here = process.cwd();
|
|
622
|
+
console.log(c.bold("Sessions (newest first, * = this directory):"));
|
|
621
623
|
for (const s of sessions) {
|
|
622
|
-
|
|
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)
|
|
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
|
-
...
|
|
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-
|
|
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
|
|
425
|
-
{ name: "resume", desc: "pick up an earlier session
|
|
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
|
|
1469
|
+
const failed2 = /\] (exited with code (?!0\b)\d+|failed|killed)/.test(notes);
|
|
1466
1470
|
return {
|
|
1467
|
-
status:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2327
|
+
failed2 = answer.failed === true;
|
|
2261
2328
|
direct = answer.lines;
|
|
2262
|
-
if (!
|
|
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 (
|
|
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
|
|
3353
|
-
if (
|
|
3354
|
-
|
|
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: "
|
|
3368
|
-
text: `
|
|
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
|
-
|
|
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:
|
|
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
|
) : "";
|