@alisio/alisio-code 0.1.0-alpha.20 → 0.1.0-alpha.21
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/dist/main.js +13 -0
- package/dist/serve.d.ts +15 -0
- package/dist/serve.js +99 -0
- package/dist/tui/agents.d.ts +3 -64
- package/dist/tui/agents.js +3 -111
- package/dist/tui/app.js +8 -29
- package/dist/tui/components.d.ts +5 -1
- package/dist/tui/components.js +144 -3
- package/dist/tui/state.d.ts +5 -5
- package/dist/tui/state.js +78 -53
- package/package.json +4 -3
package/dist/main.js
CHANGED
|
@@ -260,6 +260,19 @@ program
|
|
|
260
260
|
.argument("[prompt]")
|
|
261
261
|
.action((id, prompt, _opts, cmd) => run(cmd, prompt, id));
|
|
262
262
|
program.action((_opts, cmd) => run(cmd));
|
|
263
|
+
program
|
|
264
|
+
.command("serve")
|
|
265
|
+
.description("Start the local web UI server (HTTP + SSE)")
|
|
266
|
+
.option("--port <port>", "Port to listen on (0 picks a free one)", "4317")
|
|
267
|
+
.option("--host <address>", "Address to bind; non-loopback requires --allow-remote", "127.0.0.1")
|
|
268
|
+
.option("--allow-remote", "Allow a non-loopback --host (no TLS; prefer an SSH tunnel)")
|
|
269
|
+
.option("--no-open", "Do not open the browser")
|
|
270
|
+
.option("--max-workspaces <n>", "Maximum workspaces open at once", "4")
|
|
271
|
+
.option("--max-runs <n>", "Maximum concurrent runs across all sessions", "4")
|
|
272
|
+
.action(async (flags, cmd) => {
|
|
273
|
+
const { serve } = await import("./serve.js");
|
|
274
|
+
await serve(flags, { ...(await cliDefaults()), ...options(cmd) }, VERSION);
|
|
275
|
+
});
|
|
263
276
|
program
|
|
264
277
|
.command("setup")
|
|
265
278
|
.description("Write an example configuration without secrets (for AGENTS.md use /init)")
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AppOptions } from "@alisio/core";
|
|
2
|
+
export interface ServeFlags {
|
|
3
|
+
port: string;
|
|
4
|
+
host: string;
|
|
5
|
+
allowRemote?: boolean;
|
|
6
|
+
open: boolean;
|
|
7
|
+
maxWorkspaces: string;
|
|
8
|
+
maxRuns: string;
|
|
9
|
+
}
|
|
10
|
+
/** The operating system's URL opener for `platform`, as `[command, ...args]`. */
|
|
11
|
+
export declare function browserCommand(platform: NodeJS.Platform, url: string): string[];
|
|
12
|
+
/** Opens `url` in the default browser; failures are reported, never fatal. */
|
|
13
|
+
export declare function openBrowser(url: string): void;
|
|
14
|
+
/** Starts the server, prints the launch URL and stops cleanly on SIGINT/SIGTERM. */
|
|
15
|
+
export declare function serve(flags: ServeFlags, app: AppOptions & Record<string, unknown>, version: string): Promise<void>;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `alisio serve`: loaded only by the serve action, and it loads `@alisio/server` only then, so
|
|
3
|
+
* `alisio`, `alisio run` and the TUI never load the HTTP server (spec §3.4, T-12).
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
/** The operating system's URL opener for `platform`, as `[command, ...args]`. */
|
|
7
|
+
export function browserCommand(platform, url) {
|
|
8
|
+
if (platform === "darwin")
|
|
9
|
+
return ["open", url];
|
|
10
|
+
if (platform === "win32")
|
|
11
|
+
return ["cmd", "/c", "start", '""', url];
|
|
12
|
+
return ["xdg-open", url];
|
|
13
|
+
}
|
|
14
|
+
/** Opens `url` in the default browser; failures are reported, never fatal. */
|
|
15
|
+
export function openBrowser(url) {
|
|
16
|
+
const [command = "", ...args] = browserCommand(process.platform, url);
|
|
17
|
+
try {
|
|
18
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
19
|
+
child.on("error", () => process.stderr.write("Could not open a browser; open the URL above manually.\n"));
|
|
20
|
+
child.unref();
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
process.stderr.write("Could not open a browser; open the URL above manually.\n");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Parsed option keys that are not `AppOptions` (serve flags and UI-only globals). */
|
|
27
|
+
const SERVE_ONLY_KEYS = new Set([
|
|
28
|
+
"port",
|
|
29
|
+
"host",
|
|
30
|
+
"allowRemote",
|
|
31
|
+
"open",
|
|
32
|
+
"maxWorkspaces",
|
|
33
|
+
"maxRuns",
|
|
34
|
+
"json",
|
|
35
|
+
"quiet",
|
|
36
|
+
"banner",
|
|
37
|
+
"tui",
|
|
38
|
+
"baseUrl",
|
|
39
|
+
"herdr",
|
|
40
|
+
"disablePlugin",
|
|
41
|
+
"addDir",
|
|
42
|
+
"agents",
|
|
43
|
+
]);
|
|
44
|
+
function positiveInt(value, flag, min = 1) {
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
if (!Number.isInteger(parsed) || parsed < min)
|
|
47
|
+
throw new Error(`${flag} expects an integer >= ${min}, got "${value}"`);
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
/** Starts the server, prints the launch URL and stops cleanly on SIGINT/SIGTERM. */
|
|
51
|
+
export async function serve(flags, app, version) {
|
|
52
|
+
const port = positiveInt(flags.port, "--port", 0);
|
|
53
|
+
if (port > 65_535)
|
|
54
|
+
throw new Error(`--port expects a value up to 65535, got "${flags.port}"`);
|
|
55
|
+
const { startServer, isLoopbackHost } = await import("@alisio/server");
|
|
56
|
+
if (!isLoopbackHost(flags.host) && !flags.allowRemote)
|
|
57
|
+
throw new Error(`--host ${flags.host} is not a loopback address: add --allow-remote to expose the server on the network (no TLS; prefer an SSH tunnel).`);
|
|
58
|
+
// Global CLI options minus the serve flags and the UI-only ones: the rest are app options.
|
|
59
|
+
const { cwd, onEvent: _onEvent, approve: _approve, approveExternalDirectory: _approveDirectory, ...rest } = app;
|
|
60
|
+
const base = Object.fromEntries(Object.entries(rest).filter(([key]) => !SERVE_ONLY_KEYS.has(key)));
|
|
61
|
+
let server;
|
|
62
|
+
try {
|
|
63
|
+
server = await startServer({
|
|
64
|
+
host: flags.host,
|
|
65
|
+
port,
|
|
66
|
+
allowRemote: !!flags.allowRemote,
|
|
67
|
+
version,
|
|
68
|
+
maxOpenWorkspaces: positiveInt(flags.maxWorkspaces, "--max-workspaces"),
|
|
69
|
+
maxConcurrentRuns: positiveInt(flags.maxRuns, "--max-runs"),
|
|
70
|
+
defaultWorkspace: cwd ?? process.cwd(),
|
|
71
|
+
app: base,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === "EADDRINUSE")
|
|
76
|
+
throw new Error(`Port ${port} is already in use: choose another with --port <n> (0 picks a free one).`);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`Alisio web UI: ${server.launchUrl}\n`);
|
|
80
|
+
if (!isLoopbackHost(flags.host))
|
|
81
|
+
process.stderr.write(`Warning: listening on ${flags.host} (--allow-remote). Anyone who can reach this address and obtains the token can drive the agent; traffic is not encrypted.\n`);
|
|
82
|
+
if (!server.webInstalled)
|
|
83
|
+
process.stderr.write("The web UI assets are not installed in this build; only the HTTP API is served.\n");
|
|
84
|
+
process.stderr.write("Press Ctrl+C to stop.\n");
|
|
85
|
+
if (flags.open)
|
|
86
|
+
openBrowser(server.launchUrl);
|
|
87
|
+
let stopping = false;
|
|
88
|
+
const stop = () => {
|
|
89
|
+
if (stopping)
|
|
90
|
+
process.exit(130);
|
|
91
|
+
stopping = true;
|
|
92
|
+
process.stderr.write("Stopping Alisio server…\n");
|
|
93
|
+
// Safety net: shutdown is bounded internally; never hang the terminal.
|
|
94
|
+
setTimeout(() => process.exit(1), 8_000).unref();
|
|
95
|
+
void server.close().finally(() => process.exit(0));
|
|
96
|
+
};
|
|
97
|
+
process.on("SIGINT", stop);
|
|
98
|
+
process.on("SIGTERM", stop);
|
|
99
|
+
}
|
package/dist/tui/agents.d.ts
CHANGED
|
@@ -1,66 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* imports here — the Picker and status-line rendering live in the TUI shell.
|
|
5
|
-
*
|
|
6
|
-
* The active agent drives the MAIN session (its `instructions` are appended per run and an
|
|
7
|
-
* agent-specific `model` is offered on selection). This is deliberately separate from the
|
|
8
|
-
* delegated child-session agents of the subagents plugin: a subagent definition becomes a main
|
|
9
|
-
* agent only when it is marked main-capable (`mode: primary|all`), which the subagents plugin
|
|
10
|
-
* publishes for the host to read.
|
|
2
|
+
* The ACTIVE (main-session) agent logic lives in `@alisio/core` (`agents/active.ts`) so every
|
|
3
|
+
* surface (TUI, headless, server) resolves agents the same way. Re-exported for compatibility.
|
|
11
4
|
*/
|
|
12
|
-
export
|
|
13
|
-
/** Stable id, also the persisted `agents.active` value. */
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
description: string;
|
|
17
|
-
/** System prompt appended to the main session's instructions on every run. */
|
|
18
|
-
instructions?: string;
|
|
19
|
-
/** Optional default model selector (`provider/model` or an unambiguous model id). */
|
|
20
|
-
model?: string;
|
|
21
|
-
/** Read-only agents run with write/process/external tools disabled and approvals off. */
|
|
22
|
-
readOnly?: boolean;
|
|
23
|
-
/** Product built-ins are shown as built-in and never shadowed by contributed definitions. */
|
|
24
|
-
source: "builtin" | "user" | "plugin";
|
|
25
|
-
}
|
|
26
|
-
export declare const DEFAULT_AGENT_ID = "build";
|
|
27
|
-
/** Product built-ins. `build` keeps the CURRENT behavior exactly: no added persona, full power. */
|
|
28
|
-
export declare const BUILTIN_AGENTS: ActiveAgent[];
|
|
29
|
-
export interface MainCapableAgentRecord {
|
|
30
|
-
name: string;
|
|
31
|
-
description: string;
|
|
32
|
-
prompt: string;
|
|
33
|
-
model?: string;
|
|
34
|
-
readOnly?: boolean;
|
|
35
|
-
source: string;
|
|
36
|
-
path?: string;
|
|
37
|
-
}
|
|
38
|
-
/** Maps a main-capable definition published by the subagents plugin to an active agent. */
|
|
39
|
-
export declare function mainAgentFromRecord(record: MainCapableAgentRecord): ActiveAgent;
|
|
40
|
-
/** Builds the selectable catalog from the subagents plugin's published state (best-effort). */
|
|
41
|
-
export declare function agentCatalogFromState(untrusted: unknown): ActiveAgent[];
|
|
42
|
-
/**
|
|
43
|
-
* The full selectable catalog: built-ins first, then contributed main-capable definitions.
|
|
44
|
-
* A contributed definition may never shadow a built-in id; later duplicates of a contributed id
|
|
45
|
-
* are dropped (the subagents plugin already resolved precedence by its own discovery order).
|
|
46
|
-
*/
|
|
47
|
-
export declare function activeAgentCatalog(contributions?: ActiveAgent[]): ActiveAgent[];
|
|
48
|
-
/** Resolves the persisted agent id; unknown ids fall back to the built-in default (`build`). */
|
|
49
|
-
export declare function resolveActiveAgent(agents: ActiveAgent[], id: string | undefined): ActiveAgent;
|
|
50
|
-
/** The run options an active agent implies. An undefined agent yields the current behavior. */
|
|
51
|
-
export declare function agentRunOptions(agent: ActiveAgent | undefined): {
|
|
52
|
-
instructions?: string;
|
|
53
|
-
policy?: {
|
|
54
|
-
write: false;
|
|
55
|
-
process: false;
|
|
56
|
-
external: false;
|
|
57
|
-
};
|
|
58
|
-
approvals?: false;
|
|
59
|
-
};
|
|
60
|
-
export interface AgentPickerItem {
|
|
61
|
-
value: string;
|
|
62
|
-
label: string;
|
|
63
|
-
description: string;
|
|
64
|
-
}
|
|
65
|
-
/** Picker rows for `/agents`: name with (current), built-in/default and read-only markers. */
|
|
66
|
-
export declare function agentPickerItems(agents: ActiveAgent[], activeId: string): AgentPickerItem[];
|
|
5
|
+
export { type ActiveAgent, type AgentPickerItem, activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, type MainCapableAgentRecord, mainAgentFromRecord, resolveActiveAgent, } from "@alisio/core";
|
package/dist/tui/agents.js
CHANGED
|
@@ -1,113 +1,5 @@
|
|
|
1
|
-
export const DEFAULT_AGENT_ID = "build";
|
|
2
|
-
const PLAN_INSTRUCTIONS = `You are the "plan" agent: a read-only planning assistant for this main session.
|
|
3
|
-
|
|
4
|
-
Your job is analysis and planning only. Investigate the repository with read-only tools
|
|
5
|
-
(list_files, search_text, read_file, git_status, git_diff) and return a concrete, ordered plan:
|
|
6
|
-
files to change, functions to touch, risks and verification steps. You may run context and
|
|
7
|
-
inspection tools that do not modify anything.
|
|
8
|
-
|
|
9
|
-
You never edit, write or delete files, and you never start processes that change the workspace
|
|
10
|
-
or the network. If a task requires changes, hand back a precise plan (and the exact commands)
|
|
11
|
-
instead of making them.`;
|
|
12
|
-
/** Product built-ins. `build` keeps the CURRENT behavior exactly: no added persona, full power. */
|
|
13
|
-
export const BUILTIN_AGENTS = [
|
|
14
|
-
{
|
|
15
|
-
id: DEFAULT_AGENT_ID,
|
|
16
|
-
name: "build",
|
|
17
|
-
description: "Default full-power agent: edits code and files, runs processes and verifies its work through the normal permission flow.",
|
|
18
|
-
source: "builtin",
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
id: "plan",
|
|
22
|
-
name: "plan",
|
|
23
|
-
description: "Read-only planning agent: analyzes the codebase and returns an implementation plan without modifying anything.",
|
|
24
|
-
instructions: PLAN_INSTRUCTIONS,
|
|
25
|
-
readOnly: true,
|
|
26
|
-
source: "builtin",
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
/** Maps a main-capable definition published by the subagents plugin to an active agent. */
|
|
30
|
-
export function mainAgentFromRecord(record) {
|
|
31
|
-
const source = String(record.source ?? "");
|
|
32
|
-
return {
|
|
33
|
-
id: record.name,
|
|
34
|
-
name: record.name,
|
|
35
|
-
description: record.description,
|
|
36
|
-
...(record.prompt ? { instructions: record.prompt } : {}),
|
|
37
|
-
...(record.model ? { model: record.model } : {}),
|
|
38
|
-
...(record.readOnly ? { readOnly: true } : {}),
|
|
39
|
-
...(source.startsWith("plugin:")
|
|
40
|
-
? { source: "plugin" }
|
|
41
|
-
: { source: "user" }),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** Builds the selectable catalog from the subagents plugin's published state (best-effort). */
|
|
45
|
-
export function agentCatalogFromState(untrusted) {
|
|
46
|
-
if (!Array.isArray(untrusted))
|
|
47
|
-
return activeAgentCatalog([]);
|
|
48
|
-
const records = untrusted.filter((entry) => !!entry &&
|
|
49
|
-
typeof entry === "object" &&
|
|
50
|
-
typeof entry.name === "string" &&
|
|
51
|
-
typeof entry.description === "string" &&
|
|
52
|
-
entry.description.length > 0 &&
|
|
53
|
-
typeof entry.prompt === "string");
|
|
54
|
-
return activeAgentCatalog(records.map(mainAgentFromRecord));
|
|
55
|
-
}
|
|
56
1
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
* are dropped (the subagents plugin already resolved precedence by its own discovery order).
|
|
2
|
+
* The ACTIVE (main-session) agent logic lives in `@alisio/core` (`agents/active.ts`) so every
|
|
3
|
+
* surface (TUI, headless, server) resolves agents the same way. Re-exported for compatibility.
|
|
60
4
|
*/
|
|
61
|
-
export
|
|
62
|
-
const seen = new Set();
|
|
63
|
-
const out = [];
|
|
64
|
-
for (const agent of [...BUILTIN_AGENTS, ...contributions]) {
|
|
65
|
-
if (seen.has(agent.id))
|
|
66
|
-
continue;
|
|
67
|
-
seen.add(agent.id);
|
|
68
|
-
out.push(agent);
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
/** Resolves the persisted agent id; unknown ids fall back to the built-in default (`build`). */
|
|
73
|
-
export function resolveActiveAgent(agents, id) {
|
|
74
|
-
const found = id ? agents.find((a) => a.id === id) : undefined;
|
|
75
|
-
if (found)
|
|
76
|
-
return found;
|
|
77
|
-
return agents.find((a) => a.id === DEFAULT_AGENT_ID) ?? agents[0] ?? BUILTIN_AGENTS[0];
|
|
78
|
-
}
|
|
79
|
-
/** The run options an active agent implies. An undefined agent yields the current behavior. */
|
|
80
|
-
export function agentRunOptions(agent) {
|
|
81
|
-
if (!agent)
|
|
82
|
-
return {};
|
|
83
|
-
return {
|
|
84
|
-
...(agent.instructions ? { instructions: agent.instructions } : {}),
|
|
85
|
-
...(agent.readOnly
|
|
86
|
-
? {
|
|
87
|
-
policy: { write: false, process: false, external: false },
|
|
88
|
-
approvals: false,
|
|
89
|
-
}
|
|
90
|
-
: {}),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/** Picker rows for `/agents`: name with (current), built-in/default and read-only markers. */
|
|
94
|
-
export function agentPickerItems(agents, activeId) {
|
|
95
|
-
return agents.map((agent) => ({
|
|
96
|
-
value: agent.id,
|
|
97
|
-
label: [
|
|
98
|
-
agent.name,
|
|
99
|
-
agent.id === activeId ? "(current)" : undefined,
|
|
100
|
-
agent.id === DEFAULT_AGENT_ID ? "(default)" : undefined,
|
|
101
|
-
agent.readOnly ? "read-only" : undefined,
|
|
102
|
-
]
|
|
103
|
-
.filter(Boolean)
|
|
104
|
-
.join(" · "),
|
|
105
|
-
description: [
|
|
106
|
-
agent.description,
|
|
107
|
-
agent.model ? `default model: ${agent.model}` : undefined,
|
|
108
|
-
agent.instructions ? "injects its system prompt from the next prompt" : undefined,
|
|
109
|
-
]
|
|
110
|
-
.filter(Boolean)
|
|
111
|
-
.join(" · "),
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
5
|
+
export { activeAgentCatalog, agentCatalogFromState, agentPickerItems, agentRunOptions, BUILTIN_AGENTS, DEFAULT_AGENT_ID, mainAgentFromRecord, resolveActiveAgent, } from "@alisio/core";
|
package/dist/tui/app.js
CHANGED
|
@@ -136,7 +136,7 @@ export class Picker {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
export async function runTui(options) {
|
|
139
|
-
const { createApplication } = await import("@alisio/core");
|
|
139
|
+
const { createApplication, CommandCatalog } = await import("@alisio/core");
|
|
140
140
|
const { BUILTIN_PLUGINS } = await import("../builtin.js");
|
|
141
141
|
const { BUILTIN_PROMPTS } = await import("../prompts/index.js");
|
|
142
142
|
let dispatch = () => { };
|
|
@@ -473,7 +473,7 @@ export async function runTui(options) {
|
|
|
473
473
|
// pulls the persisted rich parts (ui/image) itself so native rendering survives resume.
|
|
474
474
|
if (event.type === "tool_completed") {
|
|
475
475
|
const data = (event.data ?? {});
|
|
476
|
-
const rich = richPartsOf(app.store.callResult(event.sessionId, String(data.id ?? "")));
|
|
476
|
+
const rich = richPartsOf(app.store.callResult(event.sessionId, String(data.id ?? "")), String(data.name ?? ""));
|
|
477
477
|
if (rich.ui !== undefined || rich.image !== undefined)
|
|
478
478
|
event = { ...event, data: { ...data, ...rich } };
|
|
479
479
|
}
|
|
@@ -1381,22 +1381,6 @@ export async function runTui(options) {
|
|
|
1381
1381
|
notice(`Resumed session ${found.id}`);
|
|
1382
1382
|
refreshEstimate();
|
|
1383
1383
|
};
|
|
1384
|
-
const toolsReport = () => {
|
|
1385
|
-
const policy = app.runner.policy;
|
|
1386
|
-
const rows = app.registry
|
|
1387
|
-
.list()
|
|
1388
|
-
.map((t) => {
|
|
1389
|
-
const effect = t.effect ?? "external";
|
|
1390
|
-
const state = effect === "read" || effect === "internal" || policy[effect]
|
|
1391
|
-
? "enabled"
|
|
1392
|
-
: app.runner.approvals && (effect === "write" || effect === "process")
|
|
1393
|
-
? "ask"
|
|
1394
|
-
: "disabled";
|
|
1395
|
-
return `| \`${t.name}\` | ${effect} | ${state} |`;
|
|
1396
|
-
})
|
|
1397
|
-
.join("\n");
|
|
1398
|
-
return `**Tools**\n\n| Tool | Effect | State |\n| --- | --- | --- |\n${rows}\n\n\`ask\` prompts before running (allow once / session / deny). Use --allow-write / --allow-process to pre-allow; --read-only disables them. Paths outside the workspace ask per directory; pre-allow with --add-dir or the \`additionalDirectories\` config key. \`internal\` tools (built-in plugins) only write Alisio's own state.`;
|
|
1399
|
-
};
|
|
1400
1384
|
const statsReport = () => {
|
|
1401
1385
|
const s = view.stats;
|
|
1402
1386
|
const budget = app.contextBudget(view.model);
|
|
@@ -1456,6 +1440,7 @@ export async function runTui(options) {
|
|
|
1456
1440
|
"**Keys**: Enter send · Shift+Enter / Alt+Enter / Ctrl+J newline · Tab complete · ↑↓ history · Esc interrupt · Ctrl+C clear input (twice to exit) · Ctrl+D exit on empty input · c / y copy last response (empty input) · x expand/collapse latest thought/tool batch/output (empty input) · click a collapsible header row to toggle it · PgUp/PgDn or mouse wheel scroll · Ctrl+X agent panel · Ctrl+B background running agents",
|
|
1457
1441
|
`**Paste**: multi-line text pastes as one block automatically · Ctrl+V attach a clipboard image (PNG/JPEG/GIF/WebP, up to ${(MAX_IMAGE_BYTES / (1024 * 1024)).toFixed(0)} MB, up to ${MAX_ATTACHMENTS_PER_MESSAGE} per message) · Ctrl+R remove the last attached image`,
|
|
1458
1442
|
].join("\n");
|
|
1443
|
+
const commandCatalog = new CommandCatalog(app);
|
|
1459
1444
|
const mutating = new Set([
|
|
1460
1445
|
"connect",
|
|
1461
1446
|
"model",
|
|
@@ -1508,7 +1493,11 @@ export async function runTui(options) {
|
|
|
1508
1493
|
case "stats":
|
|
1509
1494
|
return info(statsReport());
|
|
1510
1495
|
case "tools":
|
|
1511
|
-
|
|
1496
|
+
case "sessions": {
|
|
1497
|
+
// Text-identical core handlers (parity-tested): output comes from the CommandCatalog.
|
|
1498
|
+
const result = await commandCatalog.execute(name, parsed.args, { sessionId: session });
|
|
1499
|
+
return result.tone === "notice" ? notice(result.text ?? "") : info(result.text ?? "");
|
|
1500
|
+
}
|
|
1512
1501
|
case "connect":
|
|
1513
1502
|
return await connect();
|
|
1514
1503
|
case "model":
|
|
@@ -1575,16 +1564,6 @@ export async function runTui(options) {
|
|
|
1575
1564
|
void app.herdr.report("idle", session);
|
|
1576
1565
|
return refreshEstimate();
|
|
1577
1566
|
}
|
|
1578
|
-
case "sessions": {
|
|
1579
|
-
const list = workspaceSessions().slice(0, 20);
|
|
1580
|
-
if (!list.length)
|
|
1581
|
-
return notice("No sessions in this workspace");
|
|
1582
|
-
return info([
|
|
1583
|
-
"**Recent sessions** (use `/resume <id-prefix>`)",
|
|
1584
|
-
"",
|
|
1585
|
-
...list.map((s) => `- \`${s.id}\` ${s.id === session ? "**(current)** " : ""}${s.model} · ${app.store.messages(s.id).length} msgs · ${firstPrompt(s.id) || "_empty_"}`),
|
|
1586
|
-
].join("\n"));
|
|
1587
|
-
}
|
|
1588
1567
|
case "resume":
|
|
1589
1568
|
if (parsed.args)
|
|
1590
1569
|
return resume(parsed.args);
|
package/dist/tui/components.d.ts
CHANGED
|
@@ -21,7 +21,11 @@ export declare const exitCodeLine: (code: number | undefined, indent?: string) =
|
|
|
21
21
|
* `|-`/`` `- ``/`|` otherwise.
|
|
22
22
|
*/
|
|
23
23
|
export declare function renderTreeLines(nodes: TreeNode[], unicode: boolean): string[];
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Renders a `{type:"ui"}` block to styled lines for the given inner width. Blocks come from
|
|
26
|
+
* persisted transcripts too, so anything that does not validate (a kind added by a newer Alisio,
|
|
27
|
+
* or a malformed plugin block) falls back to labeled JSON instead of throwing.
|
|
28
|
+
*/
|
|
25
29
|
export declare function renderUiBlock(block: UiBlock, width: number, unicode: boolean): string[];
|
|
26
30
|
export type PermissionState = "on" | "ask" | "off";
|
|
27
31
|
export interface HeaderInfo {
|
package/dist/tui/components.js
CHANGED
|
@@ -3,8 +3,8 @@ import { terminalCapabilities } from "../banner.js";
|
|
|
3
3
|
import { attachmentCaption, MAX_ATTACHMENTS_PER_MESSAGE, } from "./attachments.js";
|
|
4
4
|
import { branchDisplay } from "./git-branch.js";
|
|
5
5
|
import { initialQuestionState, reduceQuestions, } from "./questions.js";
|
|
6
|
-
import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, previewLinesFor, previewRows, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, } from "./state.js";
|
|
7
|
-
import { highlightCode, imageTheme, levelColor, markdownDefaultTextStyle, markdownTheme, markdownTransform, style, } from "./theme.js";
|
|
6
|
+
import { contextLevel, contextPercent, editSummary, entryKeyOf, fitIdentityParts, fitSegments, foldCandidateOf, formatContext, formatDuration, formatTokens, groupIdentity, groupToolEntries, humanizeToolName, isUiBlock, previewLinesFor, previewRows, REASONING_EXPAND_MAX_LINES, reasoningDurationMs, } from "./state.js";
|
|
7
|
+
import { highlightCode, imageTheme, levelColor, markdownDefaultTextStyle, markdownTheme, markdownTransform, sanitizeCode, style, } from "./theme.js";
|
|
8
8
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
9
|
/** Shared animation clock advanced by the app while work is running. */
|
|
10
10
|
export const clock = { frame: 0, now: Date.now() };
|
|
@@ -169,8 +169,133 @@ function renderCodeBlock(block, _width) {
|
|
|
169
169
|
style.dim("```"),
|
|
170
170
|
];
|
|
171
171
|
}
|
|
172
|
-
/**
|
|
172
|
+
/** Longest JSON/unknown-block text shown inline before a `… truncated` marker. */
|
|
173
|
+
const MAX_BLOCK_LINES = 200;
|
|
174
|
+
const MAX_BLOCK_CHARS = 20_000;
|
|
175
|
+
/** Caps free-form text by lines and characters, appending a dim marker when anything is cut. */
|
|
176
|
+
function capText(text) {
|
|
177
|
+
let out = text.length > MAX_BLOCK_CHARS ? text.slice(0, MAX_BLOCK_CHARS) : text;
|
|
178
|
+
const lines = out.split("\n");
|
|
179
|
+
if (lines.length > MAX_BLOCK_LINES)
|
|
180
|
+
out = lines.slice(0, MAX_BLOCK_LINES).join("\n");
|
|
181
|
+
return { text: out, truncated: out.length < text.length };
|
|
182
|
+
}
|
|
183
|
+
/** `JSON.stringify` that never throws and never returns `undefined`. */
|
|
184
|
+
function jsonText(value) {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return String(value);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function renderCappedCode(code, lang, caption) {
|
|
193
|
+
const capped = capText(code);
|
|
194
|
+
return [
|
|
195
|
+
...renderCodeBlock({
|
|
196
|
+
kind: "code",
|
|
197
|
+
code: capped.text,
|
|
198
|
+
...(lang ? { lang } : {}),
|
|
199
|
+
...(caption ? { caption } : {}),
|
|
200
|
+
}, 0),
|
|
201
|
+
...(capped.truncated ? [style.gray("… truncated")] : []),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
function renderDiffBlock(block) {
|
|
205
|
+
const caption = [block.caption, block.path].filter(Boolean).join(" · ") || undefined;
|
|
206
|
+
if (block.patch !== undefined)
|
|
207
|
+
return renderCappedCode(block.patch, "diff", caption);
|
|
208
|
+
if (block.before === undefined && block.after === undefined)
|
|
209
|
+
return [...(caption ? [style.dim(caption)] : []), style.gray("(empty diff)")];
|
|
210
|
+
return [
|
|
211
|
+
...(caption ? [style.dim(caption)] : []),
|
|
212
|
+
...renderCappedCode(block.before ?? "", block.lang, "before"),
|
|
213
|
+
...renderCappedCode(block.after ?? "", block.lang, "after"),
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
function renderTerminalBlock(block) {
|
|
217
|
+
const head = block.command
|
|
218
|
+
? [style.dim(`$ ${block.command}${block.cwd ? ` (${block.cwd})` : ""}`)]
|
|
219
|
+
: [];
|
|
220
|
+
const status = [
|
|
221
|
+
block.exitCode !== undefined ? `exit ${block.exitCode}` : "",
|
|
222
|
+
block.durationMs !== undefined ? formatDuration(block.durationMs) : "",
|
|
223
|
+
]
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.join(" · ");
|
|
226
|
+
const paint = block.exitCode !== undefined && block.exitCode !== 0 ? style.red : style.gray;
|
|
227
|
+
return [
|
|
228
|
+
...head,
|
|
229
|
+
...renderCappedCode(sanitizeCode(block.output), undefined),
|
|
230
|
+
...(block.truncated ? [style.gray("… output truncated")] : []),
|
|
231
|
+
...(status ? [paint(status)] : []),
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
function renderTestResultsBlock(block, width, unicode) {
|
|
235
|
+
const cases = block.suites.flatMap((suite) => suite.cases.map((c) => ({ suite, c })));
|
|
236
|
+
const counts = new Map();
|
|
237
|
+
for (const { c } of cases)
|
|
238
|
+
counts.set(c.status, (counts.get(c.status) ?? 0) + 1);
|
|
239
|
+
const summary = [
|
|
240
|
+
block.framework,
|
|
241
|
+
["passed", "failed", "skipped", "todo"]
|
|
242
|
+
.filter((status) => counts.get(status))
|
|
243
|
+
.map((status) => `${counts.get(status)} ${status}`)
|
|
244
|
+
.join(", ") || "no tests",
|
|
245
|
+
block.durationMs !== undefined ? formatDuration(block.durationMs) : undefined,
|
|
246
|
+
]
|
|
247
|
+
.filter(Boolean)
|
|
248
|
+
.join(" · ");
|
|
249
|
+
const table = renderTableBlock({
|
|
250
|
+
kind: "table",
|
|
251
|
+
columns: ["suite", "case", "status", "time"],
|
|
252
|
+
rows: cases.map(({ suite, c }) => [
|
|
253
|
+
suite.name,
|
|
254
|
+
c.name,
|
|
255
|
+
c.status,
|
|
256
|
+
c.durationMs !== undefined ? formatDuration(c.durationMs) : "",
|
|
257
|
+
]),
|
|
258
|
+
caption: summary,
|
|
259
|
+
}, width);
|
|
260
|
+
const errors = cases
|
|
261
|
+
.filter(({ c }) => c.status === "failed" && c.error)
|
|
262
|
+
.map(({ suite, c }) => style.red(`${unicode ? "✗" : "x"} ${suite.name} ${unicode ? "›" : ">"} ${c.name}${suite.file ? ` (${suite.file}${c.line !== undefined ? `:${c.line}` : ""})` : ""}: ${c.error}`));
|
|
263
|
+
return [...table, ...errors];
|
|
264
|
+
}
|
|
265
|
+
const PROGRESS_GLYPHS = {
|
|
266
|
+
unicode: { completed: "✓", running: "●", pending: "○", failed: "✗", cancelled: "⊘" },
|
|
267
|
+
ascii: { completed: "[x]", running: "[>]", pending: "[ ]", failed: "[!]", cancelled: "[-]" },
|
|
268
|
+
};
|
|
269
|
+
function renderProgressBlock(block, unicode) {
|
|
270
|
+
const glyphs = PROGRESS_GLYPHS[unicode ? "unicode" : "ascii"];
|
|
271
|
+
const paint = {
|
|
272
|
+
completed: style.green,
|
|
273
|
+
running: style.brightCyan,
|
|
274
|
+
pending: style.gray,
|
|
275
|
+
failed: style.red,
|
|
276
|
+
cancelled: style.gray,
|
|
277
|
+
};
|
|
278
|
+
return [
|
|
279
|
+
...(block.title ? [style.bold(block.title)] : []),
|
|
280
|
+
...(block.steps.length ? [] : [style.gray("(no steps)")]),
|
|
281
|
+
...block.steps.map((step) => `${paint[step.status](glyphs[step.status])} ${step.label}${step.detail ? ` ${style.dim(step.detail)}` : ""}`),
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
/** Text fallback for a kind this build does not know (or a malformed block): never throws. */
|
|
285
|
+
function renderUnknownBlock(block) {
|
|
286
|
+
const kind = block && typeof block === "object" && typeof block.kind === "string"
|
|
287
|
+
? block.kind
|
|
288
|
+
: "unknown";
|
|
289
|
+
return [style.gray(`[ui block: ${kind}]`), ...renderCappedCode(jsonText(block), "json")];
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Renders a `{type:"ui"}` block to styled lines for the given inner width. Blocks come from
|
|
293
|
+
* persisted transcripts too, so anything that does not validate (a kind added by a newer Alisio,
|
|
294
|
+
* or a malformed plugin block) falls back to labeled JSON instead of throwing.
|
|
295
|
+
*/
|
|
173
296
|
export function renderUiBlock(block, width, unicode) {
|
|
297
|
+
if (!isUiBlock(block))
|
|
298
|
+
return renderUnknownBlock(block);
|
|
174
299
|
switch (block.kind) {
|
|
175
300
|
case "table":
|
|
176
301
|
return renderTableBlock(block, width);
|
|
@@ -186,6 +311,22 @@ export function renderUiBlock(block, width, unicode) {
|
|
|
186
311
|
});
|
|
187
312
|
return md.render(Math.max(1, width - 2));
|
|
188
313
|
}
|
|
314
|
+
case "diff":
|
|
315
|
+
return renderDiffBlock(block);
|
|
316
|
+
case "terminal":
|
|
317
|
+
return renderTerminalBlock(block);
|
|
318
|
+
case "mermaid":
|
|
319
|
+
return renderCappedCode(block.source, "mermaid", block.title);
|
|
320
|
+
case "math":
|
|
321
|
+
return block.latex.split("\n");
|
|
322
|
+
case "json":
|
|
323
|
+
return renderCappedCode(jsonText(block.value), "json", block.caption);
|
|
324
|
+
case "test-results":
|
|
325
|
+
return renderTestResultsBlock(block, width, unicode);
|
|
326
|
+
case "progress":
|
|
327
|
+
return renderProgressBlock(block, unicode);
|
|
328
|
+
default:
|
|
329
|
+
return renderUnknownBlock(block);
|
|
189
330
|
}
|
|
190
331
|
}
|
|
191
332
|
const permission = (label, state, priority) => ({
|
package/dist/tui/state.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure presentation logic for the TUI: formatting, command parsing and the reduction of
|
|
3
|
-
* versioned runner events into a view model. No terminal or pi-tui imports here.
|
|
4
|
-
*/
|
|
5
1
|
import type { Message, ModelInfo, RunEvent, ToolResult, TreeNode, UiBlock } from "@alisio/sdk";
|
|
6
2
|
export type Level = "ok" | "warn" | "danger";
|
|
7
3
|
export declare function formatTokens(n: number): string;
|
|
@@ -151,6 +147,10 @@ export interface CommandSpec {
|
|
|
151
147
|
argumentHint?: string;
|
|
152
148
|
aliases?: string[];
|
|
153
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Built-in slash commands the TUI offers, derived from the core `CommandCatalog` (the TUI
|
|
152
|
+
* surface of `BUILTIN_COMMANDS`). Kept as `COMMANDS` for compatibility.
|
|
153
|
+
*/
|
|
154
154
|
export declare const COMMANDS: CommandSpec[];
|
|
155
155
|
/** Every TUI slash name (commands, aliases and routing prefixes); templates cannot take them. */
|
|
156
156
|
export declare function reservedCommandNames(): string[];
|
|
@@ -382,7 +382,7 @@ export declare function isImagePart(value: unknown): value is {
|
|
|
382
382
|
data: string;
|
|
383
383
|
};
|
|
384
384
|
/** Rich (ui/image) parts of a persisted tool result, if any. */
|
|
385
|
-
export declare function richPartsOf(result: ToolResult | undefined): {
|
|
385
|
+
export declare function richPartsOf(result: ToolResult | undefined, toolName?: string): {
|
|
386
386
|
ui?: UiBlock;
|
|
387
387
|
image?: {
|
|
388
388
|
mimeType: string;
|
package/dist/tui/state.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure presentation logic for the TUI: formatting, command parsing and the reduction of
|
|
3
|
+
* versioned runner events into a view model. No terminal or pi-tui imports here.
|
|
4
|
+
*/
|
|
5
|
+
import { CommandCatalog } from "@alisio/core";
|
|
1
6
|
const trimZero = (value) => value.replace(/\.0$/, "");
|
|
2
7
|
export function formatTokens(n) {
|
|
3
8
|
if (!Number.isFinite(n) || n < 0)
|
|
@@ -251,61 +256,27 @@ export function fitSegments(segments, width, separator) {
|
|
|
251
256
|
kept[0] = { ...only, text: truncatePlain(only.text, width) };
|
|
252
257
|
return kept;
|
|
253
258
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
name: "plugins",
|
|
270
|
-
description: "Browse and manage project plugins",
|
|
271
|
-
aliases: ["plugin"],
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: "skills",
|
|
275
|
-
description: "Browse and manage effective skills",
|
|
276
|
-
aliases: ["skill"],
|
|
277
|
-
},
|
|
278
|
-
{ name: "mcp", description: "Browse and manage MCP servers" },
|
|
279
|
-
{
|
|
280
|
-
name: "settings",
|
|
281
|
-
description: "Open the settings menu",
|
|
282
|
-
aliases: ["prefs"],
|
|
283
|
-
},
|
|
284
|
-
{ name: "copy", description: "Copy the last assistant response to the clipboard" },
|
|
285
|
-
{
|
|
286
|
-
name: "agents",
|
|
287
|
-
description: "List and switch the active agent (applies from the next prompt)",
|
|
288
|
-
argumentHint: "[<verb>]",
|
|
289
|
-
},
|
|
290
|
-
{
|
|
291
|
-
name: "effort",
|
|
292
|
-
description: "Set the reasoning effort level for the active model",
|
|
293
|
-
argumentHint: "[level]",
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
name: "ask",
|
|
297
|
-
description: "Ask the agent to turn your question into a multiple-choice ask_user_question",
|
|
298
|
-
argumentHint: "<question>",
|
|
299
|
-
},
|
|
300
|
-
{ name: "exit", description: "Exit Alisio", aliases: ["quit"] },
|
|
301
|
-
];
|
|
259
|
+
/** Built-ins only: plugin, template and skill commands are resolved against the live app. */
|
|
260
|
+
const BUILTIN_CATALOG = new CommandCatalog();
|
|
261
|
+
/**
|
|
262
|
+
* Built-in slash commands the TUI offers, derived from the core `CommandCatalog` (the TUI
|
|
263
|
+
* surface of `BUILTIN_COMMANDS`). Kept as `COMMANDS` for compatibility.
|
|
264
|
+
*/
|
|
265
|
+
export const COMMANDS = BUILTIN_CATALOG.list("tui")
|
|
266
|
+
.filter((c) => c.source === "builtin")
|
|
267
|
+
.map((c) => ({
|
|
268
|
+
name: c.name,
|
|
269
|
+
description: c.description,
|
|
270
|
+
...(c.argumentHint ? { argumentHint: c.argumentHint } : {}),
|
|
271
|
+
...(c.aliases ? { aliases: c.aliases } : {}),
|
|
272
|
+
}));
|
|
302
273
|
/** Every TUI slash name (commands, aliases and routing prefixes); templates cannot take them. */
|
|
303
274
|
export function reservedCommandNames() {
|
|
304
275
|
return [...COMMANDS.flatMap((c) => [c.name, ...(c.aliases ?? [])]), "command"];
|
|
305
276
|
}
|
|
306
277
|
export function resolveCommand(name) {
|
|
307
|
-
const
|
|
308
|
-
return
|
|
278
|
+
const found = BUILTIN_CATALOG.resolve(name);
|
|
279
|
+
return found?.source === "builtin" && found.surfaces.includes("tui") ? found.name : undefined;
|
|
309
280
|
}
|
|
310
281
|
export function parseCommand(input) {
|
|
311
282
|
const match = /^\/([A-Za-z][\w:.-]*)(?:\s+([\s\S]*))?$/.exec(input.trim());
|
|
@@ -828,10 +799,51 @@ export function isUiBlock(value) {
|
|
|
828
799
|
(block.caption === undefined || typeof block.caption === "string"));
|
|
829
800
|
case "markdown":
|
|
830
801
|
return typeof block.text === "string";
|
|
802
|
+
case "diff":
|
|
803
|
+
return optionalStrings(block, ["path", "patch", "before", "after", "lang", "caption"]);
|
|
804
|
+
case "terminal":
|
|
805
|
+
return (typeof block.output === "string" &&
|
|
806
|
+
optionalStrings(block, ["command", "cwd"]) &&
|
|
807
|
+
optionalNumbers(block, ["exitCode", "durationMs"]) &&
|
|
808
|
+
(block.truncated === undefined || typeof block.truncated === "boolean"));
|
|
809
|
+
case "mermaid":
|
|
810
|
+
return typeof block.source === "string" && optionalStrings(block, ["title"]);
|
|
811
|
+
case "math":
|
|
812
|
+
return (typeof block.latex === "string" &&
|
|
813
|
+
(block.display === undefined || typeof block.display === "boolean"));
|
|
814
|
+
case "json":
|
|
815
|
+
return ("value" in block &&
|
|
816
|
+
optionalNumbers(block, ["collapsedDepth"]) &&
|
|
817
|
+
optionalStrings(block, ["caption"]));
|
|
818
|
+
case "test-results":
|
|
819
|
+
return (optionalStrings(block, ["framework"]) &&
|
|
820
|
+
optionalNumbers(block, ["durationMs"]) &&
|
|
821
|
+
Array.isArray(block.suites) &&
|
|
822
|
+
block.suites.every((suite) => isRecord(suite) &&
|
|
823
|
+
typeof suite.name === "string" &&
|
|
824
|
+
optionalStrings(suite, ["file"]) &&
|
|
825
|
+
Array.isArray(suite.cases) &&
|
|
826
|
+
suite.cases.every((c) => isRecord(c) &&
|
|
827
|
+
typeof c.name === "string" &&
|
|
828
|
+
TEST_STATUSES.has(c.status) &&
|
|
829
|
+
optionalStrings(c, ["error"]) &&
|
|
830
|
+
optionalNumbers(c, ["durationMs", "line"]))));
|
|
831
|
+
case "progress":
|
|
832
|
+
return (optionalStrings(block, ["title"]) &&
|
|
833
|
+
Array.isArray(block.steps) &&
|
|
834
|
+
block.steps.every((step) => isRecord(step) &&
|
|
835
|
+
typeof step.label === "string" &&
|
|
836
|
+
PROGRESS_STATUSES.has(step.status) &&
|
|
837
|
+
optionalStrings(step, ["detail"])));
|
|
831
838
|
default:
|
|
832
839
|
return false;
|
|
833
840
|
}
|
|
834
841
|
}
|
|
842
|
+
const TEST_STATUSES = new Set(["passed", "failed", "skipped", "todo"]);
|
|
843
|
+
const PROGRESS_STATUSES = new Set(["pending", "running", "completed", "failed", "cancelled"]);
|
|
844
|
+
const isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
845
|
+
const optionalStrings = (record, keys) => keys.every((key) => record[key] === undefined || typeof record[key] === "string");
|
|
846
|
+
const optionalNumbers = (record, keys) => keys.every((key) => record[key] === undefined || typeof record[key] === "number");
|
|
835
847
|
/** True when an unknown event value is a `{type:"image"}`-style part (mime + base64 data). */
|
|
836
848
|
export function isImagePart(value) {
|
|
837
849
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -839,11 +851,24 @@ export function isImagePart(value) {
|
|
|
839
851
|
const record = value;
|
|
840
852
|
return typeof record.mimeType === "string" && typeof record.data === "string";
|
|
841
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Display blocks the standard tools attach for the web UI only (spec §8.6): the TUI already
|
|
856
|
+
* shows the same content from the text (output rows, exit line), so rendering them would repeat it.
|
|
857
|
+
*/
|
|
858
|
+
const WEB_ONLY_UI = {
|
|
859
|
+
write_file: "diff",
|
|
860
|
+
edit_file: "diff",
|
|
861
|
+
shell: "terminal",
|
|
862
|
+
run_process: "terminal",
|
|
863
|
+
};
|
|
842
864
|
/** Rich (ui/image) parts of a persisted tool result, if any. */
|
|
843
|
-
export function richPartsOf(result) {
|
|
865
|
+
export function richPartsOf(result, toolName) {
|
|
844
866
|
if (!result)
|
|
845
867
|
return {};
|
|
846
|
-
const
|
|
868
|
+
const skip = toolName !== undefined && Object.hasOwn(WEB_ONLY_UI, toolName)
|
|
869
|
+
? WEB_ONLY_UI[toolName]
|
|
870
|
+
: undefined;
|
|
871
|
+
const ui = result.content.find((part) => part.type === "ui" && part.block.kind !== skip);
|
|
847
872
|
const image = result.content.find((part) => part.type === "image");
|
|
848
873
|
return {
|
|
849
874
|
...(ui?.type === "ui" ? { ui: ui.block } : {}),
|
|
@@ -1191,7 +1216,7 @@ export function itemsFromHistory(messages) {
|
|
|
1191
1216
|
preview,
|
|
1192
1217
|
toolKind: toolKindOf(c.name),
|
|
1193
1218
|
...exitCodePart(preview),
|
|
1194
|
-
...richPartsOf(r?.result),
|
|
1219
|
+
...richPartsOf(r?.result, c.name),
|
|
1195
1220
|
});
|
|
1196
1221
|
}
|
|
1197
1222
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/alisio-code",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
4
|
"description": "Alisio: an extensible, provider-agnostic coding-agent harness for your terminal. TUI, OpenAI-compatible providers, permissioned local tools, context compaction, persistent memory and a typed plugin SDK.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,11 +45,12 @@
|
|
|
45
45
|
"alisio": "./dist/main.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@alisio/core": "0.1.0-alpha.
|
|
48
|
+
"@alisio/core": "0.1.0-alpha.18",
|
|
49
49
|
"@alisio/plugin-memory": "0.1.0-alpha.9",
|
|
50
50
|
"@alisio/plugin-openai-compatible": "0.1.0-alpha.9",
|
|
51
51
|
"@alisio/plugin-subagents": "0.1.0-alpha.10",
|
|
52
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
52
|
+
"@alisio/sdk": "0.1.0-alpha.15",
|
|
53
|
+
"@alisio/server": "0.1.0-alpha.1",
|
|
53
54
|
"@earendil-works/pi-tui": "0.87.1",
|
|
54
55
|
"commander": "15.0.0"
|
|
55
56
|
},
|