@alisio/alisio-code 0.1.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/banner.d.ts +56 -0
- package/dist/banner.js +69 -0
- package/dist/builtin.d.ts +8 -0
- package/dist/builtin.js +49 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +508 -0
- package/dist/prompts/index.d.ts +5 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/init.d.ts +2 -0
- package/dist/prompts/init.js +57 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +1412 -0
- package/dist/tui/attachments.d.ts +68 -0
- package/dist/tui/attachments.js +132 -0
- package/dist/tui/clipboard.d.ts +30 -0
- package/dist/tui/clipboard.js +57 -0
- package/dist/tui/components.d.ts +159 -0
- package/dist/tui/components.js +487 -0
- package/dist/tui/connect-input.d.ts +35 -0
- package/dist/tui/connect-input.js +104 -0
- package/dist/tui/panel.d.ts +54 -0
- package/dist/tui/panel.js +140 -0
- package/dist/tui/questions.d.ts +56 -0
- package/dist/tui/questions.js +113 -0
- package/dist/tui/queue.d.ts +35 -0
- package/dist/tui/queue.js +79 -0
- package/dist/tui/skills-manager.d.ts +67 -0
- package/dist/tui/skills-manager.js +200 -0
- package/dist/tui/state.d.ts +199 -0
- package/dist/tui/state.js +566 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +49 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +28 -0
- package/package.json +66 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
const trimZero = (value) => value.replace(/\.0$/, "");
|
|
2
|
+
export function formatTokens(n) {
|
|
3
|
+
if (!Number.isFinite(n) || n < 0)
|
|
4
|
+
return "0";
|
|
5
|
+
if (n < 1000)
|
|
6
|
+
return String(Math.round(n));
|
|
7
|
+
if (n < 1_000_000) {
|
|
8
|
+
const k = n / 1000;
|
|
9
|
+
return `${k < 100 ? trimZero(k.toFixed(1)) : Math.round(k)}k`;
|
|
10
|
+
}
|
|
11
|
+
return `${trimZero((n / 1_000_000).toFixed(1))}M`;
|
|
12
|
+
}
|
|
13
|
+
/** Prefixes every model with its owning provider so identical model ids are never ambiguous. */
|
|
14
|
+
export function providerModelItems(provider, models, current) {
|
|
15
|
+
return models.map((model) => ({
|
|
16
|
+
value: model.id,
|
|
17
|
+
label: `${provider} · ${model.name ?? model.id}${model.id === current ? " (current)" : ""}`,
|
|
18
|
+
...(model.contextWindow
|
|
19
|
+
? { description: `${model.id} · ${formatTokens(model.contextWindow)} context` }
|
|
20
|
+
: model.name
|
|
21
|
+
? { description: model.id }
|
|
22
|
+
: {}),
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
/** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
|
|
26
|
+
/** Group headings are derived from the primary category (or "General") of each plugin. */
|
|
27
|
+
export function pluginCatalogItems(entries) {
|
|
28
|
+
const marker = (entry) => entry.status === "active"
|
|
29
|
+
? "[x]"
|
|
30
|
+
: entry.status === "inactive"
|
|
31
|
+
? "[ ]"
|
|
32
|
+
: entry.status === "failed"
|
|
33
|
+
? "[!]"
|
|
34
|
+
: "[*]";
|
|
35
|
+
const grouped = new Map();
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const primary = entry.categories[0] ?? "General";
|
|
38
|
+
grouped.set(primary, [...(grouped.get(primary) ?? []), entry]);
|
|
39
|
+
}
|
|
40
|
+
return [...grouped].flatMap(([title, group]) => group.map((entry, index) => ({
|
|
41
|
+
value: entry.id,
|
|
42
|
+
label: `${index === 0 ? `${title} · ` : ""}${marker(entry)} ${entry.name} · ${entry.builtin ? "built-in" : entry.source}`,
|
|
43
|
+
description: `${entry.status}${entry.categories.length ? ` · ${entry.categories.join(", ")}` : ""} · ${entry.description}`,
|
|
44
|
+
})));
|
|
45
|
+
}
|
|
46
|
+
export const pluginToggleNeedsConfirmation = (entry) => !entry.builtin;
|
|
47
|
+
const mcpSourceTitle = (kind) => ({
|
|
48
|
+
global: "User",
|
|
49
|
+
project: "Project",
|
|
50
|
+
explicit: "Explicit",
|
|
51
|
+
builtin: "Built-in",
|
|
52
|
+
plugin: "Plugin",
|
|
53
|
+
})[kind];
|
|
54
|
+
/** Group headings are generated only for sources that actually registered servers. */
|
|
55
|
+
export function mcpServerItems(entries) {
|
|
56
|
+
const marker = (status) => ({
|
|
57
|
+
disabled: "[ ]",
|
|
58
|
+
disconnected: "[-]",
|
|
59
|
+
connecting: "[…]",
|
|
60
|
+
connected: "[x]",
|
|
61
|
+
failed: "[!]",
|
|
62
|
+
"needs-authentication": "[?]",
|
|
63
|
+
"restart-required": "[*]",
|
|
64
|
+
})[status];
|
|
65
|
+
const grouped = new Map();
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const title = mcpSourceTitle(entry.source.kind);
|
|
68
|
+
grouped.set(title, [...(grouped.get(title) ?? []), entry]);
|
|
69
|
+
}
|
|
70
|
+
return [...grouped].flatMap(([title, servers]) => servers.map((entry, index) => ({
|
|
71
|
+
value: entry.name,
|
|
72
|
+
label: `${index === 0 ? `${title} · ` : ""}${marker(entry.status)} ${entry.displayName}`,
|
|
73
|
+
description: `configured ${entry.enabled ? "enabled" : "disabled"} · permission ${entry.runtimePermission} · ${entry.status}${entry.status === "connected" ? ` · ${entry.counts.tools} tool${entry.counts.tools === 1 ? "" : "s"} loaded` : " · 0 tools loaded"}`,
|
|
74
|
+
})));
|
|
75
|
+
}
|
|
76
|
+
export function mcpToolItems(tools) {
|
|
77
|
+
return tools.map((tool) => {
|
|
78
|
+
const flags = [
|
|
79
|
+
tool.annotations?.readOnly === true ? "read-only" : undefined,
|
|
80
|
+
tool.annotations?.destructive === true ? "destructive" : undefined,
|
|
81
|
+
tool.annotations?.openWorld === true ? "open-world" : undefined,
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
return {
|
|
84
|
+
value: tool.effectiveName ?? tool.name,
|
|
85
|
+
label: tool.title
|
|
86
|
+
? `${tool.title} · ${tool.name}${tool.effectiveName && tool.effectiveName !== tool.name ? ` → ${tool.effectiveName}` : ""}`
|
|
87
|
+
: tool.effectiveName && tool.effectiveName !== tool.name
|
|
88
|
+
? `${tool.name} → ${tool.effectiveName}`
|
|
89
|
+
: tool.name,
|
|
90
|
+
description: [...flags, tool.description].filter(Boolean).join(" · ") || "No description",
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Builds one filterable list while retaining provider/profile ownership in each opaque value. */
|
|
95
|
+
export function configuredProviderModelItems(catalogs, current) {
|
|
96
|
+
const items = [];
|
|
97
|
+
for (const catalog of catalogs) {
|
|
98
|
+
if (catalog.unavailable) {
|
|
99
|
+
items.push({
|
|
100
|
+
value: JSON.stringify({ profile: catalog.profile }),
|
|
101
|
+
label: `${catalog.title} · unavailable`,
|
|
102
|
+
description: `${catalog.provider} · catalog refresh failed`,
|
|
103
|
+
unavailable: true,
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const models = catalog.models.length
|
|
108
|
+
? catalog.models
|
|
109
|
+
: catalog.configuredModel
|
|
110
|
+
? [{ id: catalog.configuredModel }]
|
|
111
|
+
: [];
|
|
112
|
+
for (const model of models)
|
|
113
|
+
items.push({
|
|
114
|
+
value: JSON.stringify({
|
|
115
|
+
profile: catalog.profile,
|
|
116
|
+
provider: catalog.provider,
|
|
117
|
+
model: model.id,
|
|
118
|
+
}),
|
|
119
|
+
label: `${catalog.title} · ${model.name ?? model.id}${catalog.provider === current?.provider && model.id === current.model ? " (current)" : ""}`,
|
|
120
|
+
description: `${model.id.startsWith(`${catalog.provider}/`) ? model.id : `${catalog.provider}/${model.id}`}${model.contextWindow ? ` · ${formatTokens(model.contextWindow)} context` : ""}`,
|
|
121
|
+
unavailable: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
export function contextLevel(pct, compactionAt = 85) {
|
|
127
|
+
// Warning band starts a quarter below the auto-compaction point; danger is exactly there.
|
|
128
|
+
const warn = Math.max(0, compactionAt - 25);
|
|
129
|
+
return pct < warn ? "ok" : pct < compactionAt ? "warn" : "danger";
|
|
130
|
+
}
|
|
131
|
+
export function contextPercent(used, total) {
|
|
132
|
+
return total && total > 0 ? (used / total) * 100 : undefined;
|
|
133
|
+
}
|
|
134
|
+
export function formatContext(used, total, estimated, basis) {
|
|
135
|
+
const prefix = `${estimated ? "~" : ""}${formatTokens(used)} / `;
|
|
136
|
+
// Honest unknown: the model window could not be known, so the bar shows `?` instead of a
|
|
137
|
+
// fabricated total or percentage.
|
|
138
|
+
if (basis === "unknown")
|
|
139
|
+
return `${prefix}?`;
|
|
140
|
+
const pct = contextPercent(used, total);
|
|
141
|
+
if (pct === undefined || !total)
|
|
142
|
+
return `${prefix}unknown`;
|
|
143
|
+
return `${prefix}${formatTokens(total)} (${Math.round(pct)}%)`;
|
|
144
|
+
}
|
|
145
|
+
export function formatDuration(ms) {
|
|
146
|
+
if (ms < 1000)
|
|
147
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
148
|
+
if (ms < 60_000)
|
|
149
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
150
|
+
const total = Math.floor(ms / 1000);
|
|
151
|
+
return `${Math.floor(total / 60)}m${String(total % 60).padStart(2, "0")}s`;
|
|
152
|
+
}
|
|
153
|
+
export const textWidth = (text) => [...text].length;
|
|
154
|
+
export function truncatePlain(text, width) {
|
|
155
|
+
if (width <= 0)
|
|
156
|
+
return "";
|
|
157
|
+
const chars = [...text];
|
|
158
|
+
return chars.length <= width ? text : `${chars.slice(0, width - 1).join("")}…`;
|
|
159
|
+
}
|
|
160
|
+
export function shortenPath(path, home, max = 40) {
|
|
161
|
+
let display = home && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
162
|
+
if (textWidth(display) <= max)
|
|
163
|
+
return display;
|
|
164
|
+
const parts = display.split("/").filter(Boolean);
|
|
165
|
+
display = "";
|
|
166
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
167
|
+
const next = `${parts[i]}${display ? `/${display}` : ""}`;
|
|
168
|
+
if (textWidth(`…/${next}`) > max && display)
|
|
169
|
+
break;
|
|
170
|
+
display = next;
|
|
171
|
+
}
|
|
172
|
+
return truncatePlain(`…/${display}`, max);
|
|
173
|
+
}
|
|
174
|
+
/** Host (and port) only: never path, query, user info or credentials. */
|
|
175
|
+
export function hostOf(baseURL) {
|
|
176
|
+
try {
|
|
177
|
+
return new URL(baseURL).host || "unknown";
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return "unknown";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export const shortId = (id) => id.slice(0, 8);
|
|
184
|
+
/** Keeps segments in order, dropping the lowest priority ones until the line fits. */
|
|
185
|
+
export function fitSegments(segments, width, separator) {
|
|
186
|
+
const kept = [...segments];
|
|
187
|
+
const total = () => kept.reduce((sum, s) => sum + textWidth(s.text), 0) +
|
|
188
|
+
Math.max(0, kept.length - 1) * textWidth(separator);
|
|
189
|
+
while (kept.length > 1 && total() > width) {
|
|
190
|
+
let lowest = 0;
|
|
191
|
+
for (let i = 1; i < kept.length; i++)
|
|
192
|
+
if ((kept[i]?.priority ?? 0) < (kept[lowest]?.priority ?? 0))
|
|
193
|
+
lowest = i;
|
|
194
|
+
kept.splice(lowest, 1);
|
|
195
|
+
}
|
|
196
|
+
const only = kept[0];
|
|
197
|
+
if (kept.length === 1 && only && textWidth(only.text) > width)
|
|
198
|
+
kept[0] = { ...only, text: truncatePlain(only.text, width) };
|
|
199
|
+
return kept;
|
|
200
|
+
}
|
|
201
|
+
export const COMMANDS = [
|
|
202
|
+
{ name: "help", description: "Show commands and keys" },
|
|
203
|
+
{ name: "connect", description: "Configure a provider and choose its active model" },
|
|
204
|
+
{
|
|
205
|
+
name: "model",
|
|
206
|
+
description: "Switch provider and model",
|
|
207
|
+
aliases: ["models"],
|
|
208
|
+
},
|
|
209
|
+
{ name: "compact", description: "Summarize older history", argumentHint: "[focus]" },
|
|
210
|
+
{ name: "stats", description: "Session statistics" },
|
|
211
|
+
{ name: "clear", description: "Start a new session", aliases: ["new"] },
|
|
212
|
+
{ name: "sessions", description: "List recent sessions" },
|
|
213
|
+
{ name: "resume", description: "Resume a session by ID or prefix", argumentHint: "<id>" },
|
|
214
|
+
{ name: "tools", description: "List tools and permission state" },
|
|
215
|
+
{
|
|
216
|
+
name: "plugins",
|
|
217
|
+
description: "Browse and manage project plugins",
|
|
218
|
+
aliases: ["plugin"],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "skills",
|
|
222
|
+
description: "Browse and manage effective skills",
|
|
223
|
+
aliases: ["skill"],
|
|
224
|
+
},
|
|
225
|
+
{ name: "mcp", description: "Browse and manage MCP servers" },
|
|
226
|
+
{ name: "copy", description: "Copy the last assistant response to the clipboard" },
|
|
227
|
+
{
|
|
228
|
+
name: "ask",
|
|
229
|
+
description: "Ask the agent to turn your question into a multiple-choice ask_user_question",
|
|
230
|
+
argumentHint: "<question>",
|
|
231
|
+
},
|
|
232
|
+
{ name: "exit", description: "Exit Alisio", aliases: ["quit"] },
|
|
233
|
+
];
|
|
234
|
+
/** Every TUI slash name (commands, aliases and routing prefixes); templates cannot take them. */
|
|
235
|
+
export function reservedCommandNames() {
|
|
236
|
+
return [...COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])]), "command"];
|
|
237
|
+
}
|
|
238
|
+
export function resolveCommand(name) {
|
|
239
|
+
const lower = name.toLowerCase();
|
|
240
|
+
return COMMANDS.find((c) => c.name === lower || c.aliases?.includes(lower))?.name;
|
|
241
|
+
}
|
|
242
|
+
export function parseCommand(input) {
|
|
243
|
+
const match = /^\/([A-Za-z][\w:.-]*)(?:\s+([\s\S]*))?$/.exec(input.trim());
|
|
244
|
+
if (!match?.[1])
|
|
245
|
+
return undefined;
|
|
246
|
+
return { name: match[1], args: (match[2] ?? "").trim() };
|
|
247
|
+
}
|
|
248
|
+
function parseArgs(args) {
|
|
249
|
+
try {
|
|
250
|
+
const value = JSON.parse(args);
|
|
251
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
252
|
+
? value
|
|
253
|
+
: undefined;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const oneLine = (text) => text.replace(/\s+/g, " ").trim();
|
|
260
|
+
export function summarizeToolArgs(name, args) {
|
|
261
|
+
const input = parseArgs(args);
|
|
262
|
+
if (!input)
|
|
263
|
+
return truncatePlain(oneLine(args), 120);
|
|
264
|
+
const str = (key) => (typeof input[key] === "string" ? input[key] : "");
|
|
265
|
+
let text;
|
|
266
|
+
if (name === "run_process") {
|
|
267
|
+
const rest = Array.isArray(input.args) ? input.args.map(String) : [];
|
|
268
|
+
text = [str("command"), ...rest].join(" ");
|
|
269
|
+
}
|
|
270
|
+
else if (name === "search_text")
|
|
271
|
+
text = `"${str("pattern")}"${str("path") ? ` in ${str("path")}` : ""}`;
|
|
272
|
+
else {
|
|
273
|
+
const key = ["path", "command", "name", "query", "pattern", "url", "title"].find((k) => str(k));
|
|
274
|
+
text = key ? str(key) : Object.keys(input).length ? JSON.stringify(input) : "";
|
|
275
|
+
}
|
|
276
|
+
return truncatePlain(oneLine(text), 120);
|
|
277
|
+
}
|
|
278
|
+
const splitLines = (text) => {
|
|
279
|
+
const lines = text.split(/\r?\n/);
|
|
280
|
+
if (lines.length > 1 && lines.at(-1) === "")
|
|
281
|
+
lines.pop();
|
|
282
|
+
return lines;
|
|
283
|
+
};
|
|
284
|
+
function diffLines(before, after) {
|
|
285
|
+
if (before.length * after.length > 250_000)
|
|
286
|
+
return [
|
|
287
|
+
...before.map((text) => ({ sign: "-", text })),
|
|
288
|
+
...after.map((text) => ({ sign: "+", text })),
|
|
289
|
+
];
|
|
290
|
+
const n = before.length, m = after.length;
|
|
291
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
292
|
+
for (let i = n - 1; i >= 0; i--)
|
|
293
|
+
for (let j = m - 1; j >= 0; j--)
|
|
294
|
+
lcs[i][j] =
|
|
295
|
+
before[i] === after[j]
|
|
296
|
+
? (lcs[i + 1]?.[j + 1] ?? 0) + 1
|
|
297
|
+
: Math.max(lcs[i + 1]?.[j] ?? 0, lcs[i]?.[j + 1] ?? 0);
|
|
298
|
+
const out = [];
|
|
299
|
+
let i = 0, j = 0;
|
|
300
|
+
while (i < n || j < m) {
|
|
301
|
+
if (i < n && j < m && before[i] === after[j]) {
|
|
302
|
+
i++;
|
|
303
|
+
j++;
|
|
304
|
+
}
|
|
305
|
+
else if (j >= m || (i < n && (lcs[i + 1]?.[j] ?? 0) >= (lcs[i]?.[j + 1] ?? 0)))
|
|
306
|
+
out.push({ sign: "-", text: before[i++] ?? "" });
|
|
307
|
+
else
|
|
308
|
+
out.push({ sign: "+", text: after[j++] ?? "" });
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
export function editSummary(name, args) {
|
|
313
|
+
if (name !== "edit_file" && name !== "write_file")
|
|
314
|
+
return undefined;
|
|
315
|
+
const input = parseArgs(args);
|
|
316
|
+
if (!input)
|
|
317
|
+
return undefined;
|
|
318
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
319
|
+
const lines = name === "write_file"
|
|
320
|
+
? splitLines(String(input.content ?? "")).map((text) => ({ sign: "+", text }))
|
|
321
|
+
: diffLines(splitLines(String(input.oldText ?? "")), splitLines(String(input.newText ?? "")));
|
|
322
|
+
return {
|
|
323
|
+
path,
|
|
324
|
+
added: lines.filter((l) => l.sign === "+").length,
|
|
325
|
+
removed: lines.filter((l) => l.sign === "-").length,
|
|
326
|
+
lines,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
export function initialViewState(model, now = Date.now()) {
|
|
330
|
+
return {
|
|
331
|
+
items: [],
|
|
332
|
+
streaming: false,
|
|
333
|
+
compacting: false,
|
|
334
|
+
model,
|
|
335
|
+
stats: {
|
|
336
|
+
input: 0,
|
|
337
|
+
output: 0,
|
|
338
|
+
cached: 0,
|
|
339
|
+
turns: 0,
|
|
340
|
+
runs: 0,
|
|
341
|
+
tools: {},
|
|
342
|
+
models: [model],
|
|
343
|
+
startedAt: now,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
export function addItem(state, item) {
|
|
348
|
+
return { ...state, items: [...state.items, item] };
|
|
349
|
+
}
|
|
350
|
+
const withModel = (stats, model) => typeof model === "string" && model && !stats.models.includes(model)
|
|
351
|
+
? { ...stats, models: [...stats.models, model] }
|
|
352
|
+
: stats;
|
|
353
|
+
function appendAssistant(state, field, delta) {
|
|
354
|
+
const last = state.items.at(-1);
|
|
355
|
+
if (last?.kind === "assistant" && !last.done)
|
|
356
|
+
return {
|
|
357
|
+
...state,
|
|
358
|
+
items: [...state.items.slice(0, -1), { ...last, [field]: last[field] + delta }],
|
|
359
|
+
};
|
|
360
|
+
return addItem(state, {
|
|
361
|
+
kind: "assistant",
|
|
362
|
+
text: field === "text" ? delta : "",
|
|
363
|
+
reasoning: field === "reasoning" ? delta : "",
|
|
364
|
+
done: false,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
function updateTool(state, id, update) {
|
|
368
|
+
const index = state.items.findLastIndex((i) => i.kind === "tool" && i.id === id);
|
|
369
|
+
const item = state.items[index];
|
|
370
|
+
if (index < 0 || item?.kind !== "tool")
|
|
371
|
+
return state;
|
|
372
|
+
const items = [...state.items];
|
|
373
|
+
items[index] = { ...item, ...update };
|
|
374
|
+
return { ...state, items };
|
|
375
|
+
}
|
|
376
|
+
export function reduceEvent(state, event) {
|
|
377
|
+
const d = (event.data ?? {});
|
|
378
|
+
const at = Date.parse(event.timestamp);
|
|
379
|
+
switch (event.type) {
|
|
380
|
+
case "run_started":
|
|
381
|
+
return {
|
|
382
|
+
...state,
|
|
383
|
+
streaming: true,
|
|
384
|
+
runStartedAt: at,
|
|
385
|
+
stats: withModel({ ...state.stats, runs: state.stats.runs + 1 }, d.model),
|
|
386
|
+
};
|
|
387
|
+
case "text_delta":
|
|
388
|
+
return appendAssistant(state, "text", String(d.delta ?? ""));
|
|
389
|
+
case "reasoning_delta":
|
|
390
|
+
return appendAssistant(state, "reasoning", String(d.delta ?? ""));
|
|
391
|
+
case "turn_completed": {
|
|
392
|
+
const usage = d.usage;
|
|
393
|
+
const last = state.items.at(-1);
|
|
394
|
+
const items = last?.kind === "assistant" && !last.done
|
|
395
|
+
? [...state.items.slice(0, -1), { ...last, done: true }]
|
|
396
|
+
: state.items;
|
|
397
|
+
const stats = withModel({ ...state.stats, turns: state.stats.turns + 1 }, d.model);
|
|
398
|
+
if (!usage)
|
|
399
|
+
return { ...state, items, stats };
|
|
400
|
+
return {
|
|
401
|
+
...state,
|
|
402
|
+
items,
|
|
403
|
+
context: { used: (usage.input ?? 0) + (usage.output ?? 0), estimated: false },
|
|
404
|
+
stats: {
|
|
405
|
+
...stats,
|
|
406
|
+
input: stats.input + (usage.input ?? 0),
|
|
407
|
+
output: stats.output + (usage.output ?? 0),
|
|
408
|
+
cached: stats.cached + (usage.cachedInput ?? 0),
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
case "tool_started":
|
|
413
|
+
return addItem(state, {
|
|
414
|
+
kind: "tool",
|
|
415
|
+
id: String(d.id ?? ""),
|
|
416
|
+
name: String(d.name ?? "tool"),
|
|
417
|
+
args: typeof d.arguments === "string" ? d.arguments : "",
|
|
418
|
+
summary: summarizeToolArgs(String(d.name ?? ""), typeof d.arguments === "string" ? d.arguments : ""),
|
|
419
|
+
status: "running",
|
|
420
|
+
});
|
|
421
|
+
case "approval_requested":
|
|
422
|
+
return updateTool(state, d.id, { status: "approval" });
|
|
423
|
+
case "approval_resolved":
|
|
424
|
+
return updateTool(state, d.id, { status: "running" });
|
|
425
|
+
case "tool_completed": {
|
|
426
|
+
const name = String(d.name ?? "tool");
|
|
427
|
+
const current = state.stats.tools[name] ?? { calls: 0, errors: 0 };
|
|
428
|
+
const next = updateTool(state, d.id, {
|
|
429
|
+
status: d.isError ? "error" : "ok",
|
|
430
|
+
...(typeof d.durationMs === "number" ? { durationMs: d.durationMs } : {}),
|
|
431
|
+
...(typeof d.preview === "string" ? { preview: d.preview } : {}),
|
|
432
|
+
});
|
|
433
|
+
return {
|
|
434
|
+
...next,
|
|
435
|
+
stats: {
|
|
436
|
+
...next.stats,
|
|
437
|
+
tools: {
|
|
438
|
+
...next.stats.tools,
|
|
439
|
+
[name]: { calls: current.calls + 1, errors: current.errors + (d.isError ? 1 : 0) },
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
case "run_completed":
|
|
445
|
+
case "run_failed":
|
|
446
|
+
case "run_cancelled": {
|
|
447
|
+
const ended = {
|
|
448
|
+
...state,
|
|
449
|
+
streaming: false,
|
|
450
|
+
compacting: false,
|
|
451
|
+
stats: {
|
|
452
|
+
...state.stats,
|
|
453
|
+
...(state.runStartedAt !== undefined ? { lastRunMs: at - state.runStartedAt } : {}),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
if (event.type === "run_failed")
|
|
457
|
+
return addItem(ended, { kind: "error", text: String(d.error ?? "Run failed") });
|
|
458
|
+
if (event.type === "run_cancelled") {
|
|
459
|
+
const reason = String(d.error ?? "cancelled");
|
|
460
|
+
return addItem(ended, {
|
|
461
|
+
kind: "notice",
|
|
462
|
+
text: reason.startsWith("Interrupted") ? reason : `Interrupted: ${reason}`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return ended;
|
|
466
|
+
}
|
|
467
|
+
case "compaction_started":
|
|
468
|
+
return { ...state, compacting: true };
|
|
469
|
+
case "compaction_completed": {
|
|
470
|
+
const reports = Object.values((d.plugins ?? {}))
|
|
471
|
+
.map((r) => (typeof r?.summary === "string" ? r.summary : ""))
|
|
472
|
+
.filter(Boolean);
|
|
473
|
+
const checkpoint = typeof d.summarizedTokens === "number" && typeof d.checkpointTokens === "number"
|
|
474
|
+
? ` · checkpoint ~${formatTokens(d.summarizedTokens)} → ~${formatTokens(d.checkpointTokens)} tokens`
|
|
475
|
+
: "";
|
|
476
|
+
const partial = d.partial
|
|
477
|
+
? " · partial: the summary was cut by max output tokens; consider raising compaction.maxOutputTokens"
|
|
478
|
+
: "";
|
|
479
|
+
return addItem({
|
|
480
|
+
...state,
|
|
481
|
+
compacting: false,
|
|
482
|
+
context: { used: Number(d.after ?? 0), estimated: true },
|
|
483
|
+
}, {
|
|
484
|
+
kind: "notice",
|
|
485
|
+
text: [
|
|
486
|
+
`Context compacted (${String(d.reason ?? "manual")}): ${String(d.replaced ?? 0)} messages summarized, ~${formatTokens(Number(d.before ?? 0))} → ~${formatTokens(Number(d.after ?? 0))} tokens${checkpoint}${partial}`,
|
|
487
|
+
...reports,
|
|
488
|
+
].join("\n"),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
case "plugin_hook_failed":
|
|
492
|
+
return addItem(state, {
|
|
493
|
+
kind: "notice",
|
|
494
|
+
text: `Plugin ${String(d.source ?? "?")} ${String(d.hook ?? "hook")} failed: ${String(d.error ?? "unknown error")} (continued without it)`,
|
|
495
|
+
});
|
|
496
|
+
case "session_context_injected":
|
|
497
|
+
return addItem(state, {
|
|
498
|
+
kind: "notice",
|
|
499
|
+
text: `Context injected by ${(d.sources ?? ["plugin"]).join(", ")} (~${formatTokens(Number(d.tokens ?? 0))} tokens)`,
|
|
500
|
+
});
|
|
501
|
+
case "compaction_skipped":
|
|
502
|
+
return addItem({ ...state, compacting: false }, { kind: "notice", text: `Compaction skipped: ${String(d.detail ?? "nothing to compact")}` });
|
|
503
|
+
case "compaction_failed":
|
|
504
|
+
return addItem({ ...state, compacting: false }, { kind: "error", text: `Compaction failed: ${String(d.error ?? "unknown error")}` });
|
|
505
|
+
case "response_truncated":
|
|
506
|
+
return addItem(state, {
|
|
507
|
+
kind: "notice",
|
|
508
|
+
text: "Response cut by max output tokens — the answer may be incomplete. Raise limits.maxOutputTokens to allow longer answers.",
|
|
509
|
+
});
|
|
510
|
+
case "model_changed":
|
|
511
|
+
return {
|
|
512
|
+
...state,
|
|
513
|
+
model: String(d.model ?? state.model),
|
|
514
|
+
stats: withModel(state.stats, d.model),
|
|
515
|
+
items: [
|
|
516
|
+
...state.items,
|
|
517
|
+
{
|
|
518
|
+
kind: "notice",
|
|
519
|
+
text: `Model: ${String(d.previous ?? "?")} → ${String(d.model ?? "?")}`,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
default:
|
|
524
|
+
return state;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
export function lastAssistantText(items) {
|
|
528
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
529
|
+
const item = items[i];
|
|
530
|
+
if (item?.kind === "assistant" && item.text.trim())
|
|
531
|
+
return item.text;
|
|
532
|
+
}
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
/** Rebuilds transcript items from persisted history (used by /resume). */
|
|
536
|
+
export function itemsFromHistory(messages) {
|
|
537
|
+
const items = [];
|
|
538
|
+
const results = new Map();
|
|
539
|
+
for (const m of messages)
|
|
540
|
+
if (m.role === "tool")
|
|
541
|
+
results.set(m.callId, m);
|
|
542
|
+
for (const m of messages) {
|
|
543
|
+
if (m.role === "user")
|
|
544
|
+
items.push(m.summary ? { kind: "info", text: m.text } : { kind: "user", text: m.display ?? m.text });
|
|
545
|
+
else if (m.role === "assistant") {
|
|
546
|
+
if (m.text)
|
|
547
|
+
items.push({ kind: "assistant", text: m.text, reasoning: "", done: true });
|
|
548
|
+
for (const c of m.calls) {
|
|
549
|
+
const r = results.get(c.id);
|
|
550
|
+
items.push({
|
|
551
|
+
kind: "tool",
|
|
552
|
+
id: c.id,
|
|
553
|
+
name: c.name,
|
|
554
|
+
args: c.arguments,
|
|
555
|
+
summary: summarizeToolArgs(c.name, c.arguments),
|
|
556
|
+
status: r?.result.isError ? "error" : "ok",
|
|
557
|
+
preview: r?.result.content
|
|
558
|
+
.map((x) => x.text)
|
|
559
|
+
.join("\n")
|
|
560
|
+
.slice(0, 2_000),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return items;
|
|
566
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Level } from "./state.ts";
|
|
3
|
+
export declare const style: {
|
|
4
|
+
bold: (text: string) => string;
|
|
5
|
+
dim: (text: string) => string;
|
|
6
|
+
italic: (text: string) => string;
|
|
7
|
+
underline: (text: string) => string;
|
|
8
|
+
strike: (text: string) => string;
|
|
9
|
+
red: (text: string) => string;
|
|
10
|
+
green: (text: string) => string;
|
|
11
|
+
yellow: (text: string) => string;
|
|
12
|
+
blue: (text: string) => string;
|
|
13
|
+
magenta: (text: string) => string;
|
|
14
|
+
cyan: (text: string) => string;
|
|
15
|
+
gray: (text: string) => string;
|
|
16
|
+
brightCyan: (text: string) => string;
|
|
17
|
+
userBg: (text: string) => string;
|
|
18
|
+
};
|
|
19
|
+
export declare const levelColor: (level: Level) => (text: string) => string;
|
|
20
|
+
export declare const markdownTheme: MarkdownTheme;
|
|
21
|
+
export declare const selectListTheme: SelectListTheme;
|
|
22
|
+
export declare const editorTheme: EditorTheme;
|
|
23
|
+
export declare const imageTheme: ImageTheme;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const enabled = !process.env.NO_COLOR;
|
|
2
|
+
const sgr = (open, close) => (text) => enabled ? `\x1b[${open}m${text}\x1b[${close}m` : text;
|
|
3
|
+
export const style = {
|
|
4
|
+
bold: sgr(1, 22),
|
|
5
|
+
dim: sgr(2, 22),
|
|
6
|
+
italic: sgr(3, 23),
|
|
7
|
+
underline: sgr(4, 24),
|
|
8
|
+
strike: sgr(9, 29),
|
|
9
|
+
red: sgr(31, 39),
|
|
10
|
+
green: sgr(32, 39),
|
|
11
|
+
yellow: sgr(33, 39),
|
|
12
|
+
blue: sgr(34, 39),
|
|
13
|
+
magenta: sgr(35, 39),
|
|
14
|
+
cyan: sgr(36, 39),
|
|
15
|
+
gray: sgr(90, 39),
|
|
16
|
+
brightCyan: sgr(96, 39),
|
|
17
|
+
userBg: sgr("48;5;236", 49),
|
|
18
|
+
};
|
|
19
|
+
export const levelColor = (level) => level === "ok" ? style.green : level === "warn" ? style.yellow : style.red;
|
|
20
|
+
export const markdownTheme = {
|
|
21
|
+
heading: (t) => style.bold(style.cyan(t)),
|
|
22
|
+
link: (t) => style.underline(style.blue(t)),
|
|
23
|
+
linkUrl: (t) => style.dim(t),
|
|
24
|
+
code: (t) => style.yellow(t),
|
|
25
|
+
codeBlock: (t) => style.green(t),
|
|
26
|
+
codeBlockBorder: (t) => style.dim(t),
|
|
27
|
+
quote: (t) => style.italic(style.gray(t)),
|
|
28
|
+
quoteBorder: (t) => style.dim(t),
|
|
29
|
+
hr: (t) => style.dim(t),
|
|
30
|
+
listBullet: (t) => style.cyan(t),
|
|
31
|
+
bold: (t) => style.bold(t),
|
|
32
|
+
italic: (t) => style.italic(t),
|
|
33
|
+
strikethrough: (t) => style.strike(t),
|
|
34
|
+
underline: (t) => style.underline(t),
|
|
35
|
+
};
|
|
36
|
+
export const selectListTheme = {
|
|
37
|
+
selectedPrefix: (t) => style.cyan(t),
|
|
38
|
+
selectedText: (t) => style.bold(style.cyan(t)),
|
|
39
|
+
description: (t) => style.gray(t),
|
|
40
|
+
scrollInfo: (t) => style.dim(t),
|
|
41
|
+
noMatch: (t) => style.yellow(t),
|
|
42
|
+
};
|
|
43
|
+
export const editorTheme = {
|
|
44
|
+
borderColor: (t) => style.gray(t),
|
|
45
|
+
selectList: selectListTheme,
|
|
46
|
+
};
|
|
47
|
+
export const imageTheme = {
|
|
48
|
+
fallbackColor: (t) => style.gray(t),
|
|
49
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the package version at runtime so --version, doctor, plugin metadata and MCP client
|
|
3
|
+
* metadata stay in sync with the published package without a build-time constant to update.
|
|
4
|
+
* The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
|
|
5
|
+
* source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
|
|
6
|
+
* binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadVersion(fromHere: string): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Fallback used when neither the build-time injection nor the package manifest is readable
|
|
6
|
+
* (e.g. an unpackaged embed). Keep it a development marker, never a publish literal, so it
|
|
7
|
+
* cannot silently desync from the published package.
|
|
8
|
+
*/
|
|
9
|
+
const FALLBACK = "dev";
|
|
10
|
+
/**
|
|
11
|
+
* Reads the package version at runtime so --version, doctor, plugin metadata and MCP client
|
|
12
|
+
* metadata stay in sync with the published package without a build-time constant to update.
|
|
13
|
+
* The manifest is resolved relative to the module (`src/version.ts` → `../package.json` in the
|
|
14
|
+
* source tree; `dist/version.js` → `../package.json` of the installed package). Standalone
|
|
15
|
+
* binaries inject the version at build time via ALISIO_PACKAGE_VERSION (scripts/binary-build.ts).
|
|
16
|
+
*/
|
|
17
|
+
export function loadVersion(fromHere) {
|
|
18
|
+
if (process.env.ALISIO_PACKAGE_VERSION)
|
|
19
|
+
return process.env.ALISIO_PACKAGE_VERSION;
|
|
20
|
+
try {
|
|
21
|
+
const manifest = join(dirname(fileURLToPath(fromHere)), "..", "package.json");
|
|
22
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
23
|
+
return typeof parsed.version === "string" && parsed.version ? parsed.version : FALLBACK;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return FALLBACK;
|
|
27
|
+
}
|
|
28
|
+
}
|