@alisio/alisio-code 0.1.0-alpha.25 → 0.1.0-alpha.27
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/analysis.d.ts +27 -0
- package/dist/analysis.js +145 -0
- package/dist/banner.js +2 -1
- package/dist/main.js +63 -2
- package/dist/tui/app.js +203 -5
- package/dist/tui/artifact-preview.d.ts +52 -0
- package/dist/tui/artifact-preview.js +212 -0
- package/dist/tui/artifacts.d.ts +54 -0
- package/dist/tui/artifacts.js +207 -0
- package/dist/tui/components.d.ts +5 -0
- package/dist/tui/components.js +35 -3
- package/dist/tui/open-path.d.ts +38 -0
- package/dist/tui/open-path.js +80 -0
- package/dist/tui/settings.d.ts +11 -0
- package/dist/tui/settings.js +51 -0
- package/dist/tui/state.d.ts +20 -0
- package/dist/tui/state.js +97 -0
- package/package.json +4 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AnalysisStatus } from "@alisio/sdk";
|
|
2
|
+
type Common = {
|
|
3
|
+
db?: string;
|
|
4
|
+
python?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
config?: string;
|
|
7
|
+
trustProject?: boolean;
|
|
8
|
+
};
|
|
9
|
+
/** The unified runtime state and, when Python is missing, the install guidance for this system. */
|
|
10
|
+
export declare function analysisState(o: Common): Promise<{
|
|
11
|
+
status: AnalysisStatus;
|
|
12
|
+
guidance?: string;
|
|
13
|
+
ignored: string[];
|
|
14
|
+
}>;
|
|
15
|
+
/** Prints `alisio analysis status`. */
|
|
16
|
+
export declare function printAnalysisStatus(o: Common): Promise<void>;
|
|
17
|
+
/** `alisio analysis setup --extras analysis|science`: needs network; offline it fails cleanly. */
|
|
18
|
+
export declare function setupExtras(o: Common, extras: string | undefined): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* `alisio analysis setup --oci [--image <name>]`: pulls the image once and verifies it. With a
|
|
21
|
+
* digest-pinned reference the pull verifies the digest; with a tag it prints the digest to put in
|
|
22
|
+
* `analysis.oci.image` (Alisio never writes it: only you choose what runs your code).
|
|
23
|
+
*/
|
|
24
|
+
export declare function setupOci(o: Common, image: string | undefined): Promise<void>;
|
|
25
|
+
/** `alisio analysis sweep [--force]`: the retention sweep now (it also runs by itself, daily). */
|
|
26
|
+
export declare function sweep(o: Common, force: boolean): Promise<void>;
|
|
27
|
+
export {};
|
package/dist/analysis.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `alisio analysis …` and the analysis line of `alisio doctor`: the state of the Python runtime,
|
|
3
|
+
* the optional extras (`setup --extras`, hash-locked, needs network), the optional container
|
|
4
|
+
* runtime (`setup --oci`) and the retention sweep (`sweep`). None of these is needed to run
|
|
5
|
+
* `python_run`: the base runtime is the discovered Python with only the standard library.
|
|
6
|
+
*/
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
async function context(o) {
|
|
9
|
+
const core = await import("@alisio/core");
|
|
10
|
+
const workspace = await core.findWorkspace(o.cwd ?? process.cwd());
|
|
11
|
+
const { config, provenance } = await core.loadConfigWithProvenance(workspace, {
|
|
12
|
+
file: o.config,
|
|
13
|
+
trustProject: o.trustProject,
|
|
14
|
+
});
|
|
15
|
+
const stateDir = o.db && o.db !== ":memory:" ? dirname(o.db) : core.stateHome();
|
|
16
|
+
const manager = new core.AnalysisRuntimeManager({
|
|
17
|
+
stateDir,
|
|
18
|
+
...(o.python ? { python: o.python } : {}),
|
|
19
|
+
});
|
|
20
|
+
const oci = () => new core.OciRuntime({ ...config.analysis.oci });
|
|
21
|
+
return { core, config, provenance, stateDir, manager, oci };
|
|
22
|
+
}
|
|
23
|
+
/** The unified runtime state and, when Python is missing, the install guidance for this system. */
|
|
24
|
+
export async function analysisState(o) {
|
|
25
|
+
const { core, config, provenance, stateDir, manager, oci } = await context(o);
|
|
26
|
+
const janitor = {
|
|
27
|
+
lastSweep: () => import("node:fs/promises")
|
|
28
|
+
.then((fs) => fs.readFile(join(stateDir, "analysis", ".last-sweep"), "utf8"))
|
|
29
|
+
.then((text) => {
|
|
30
|
+
const value = Number(text.trim());
|
|
31
|
+
return Number.isFinite(value) ? value : undefined;
|
|
32
|
+
})
|
|
33
|
+
.catch(() => undefined),
|
|
34
|
+
};
|
|
35
|
+
const status = await core.analysisStatus({
|
|
36
|
+
config: config.analysis,
|
|
37
|
+
runtime: manager,
|
|
38
|
+
oci,
|
|
39
|
+
janitor,
|
|
40
|
+
enabled: config.analysis.enabled,
|
|
41
|
+
readOnly: false,
|
|
42
|
+
});
|
|
43
|
+
const guidance = status.python.found
|
|
44
|
+
? undefined
|
|
45
|
+
: (async () => {
|
|
46
|
+
const resolved = await manager.interpreter();
|
|
47
|
+
return resolved.ok
|
|
48
|
+
? undefined
|
|
49
|
+
: core.installHintsText(core.pythonInstallHints(await core.detectHintHost(resolved.found)));
|
|
50
|
+
})();
|
|
51
|
+
return {
|
|
52
|
+
status,
|
|
53
|
+
...(guidance ? { guidance: await guidance } : {}),
|
|
54
|
+
ignored: provenance.ignored ?? [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Prints `alisio analysis status`. */
|
|
58
|
+
export async function printAnalysisStatus(o) {
|
|
59
|
+
const { status, guidance, ignored } = await analysisState(o);
|
|
60
|
+
console.log(JSON.stringify(status, null, 2));
|
|
61
|
+
if (guidance)
|
|
62
|
+
process.stderr.write(`\n${guidance}\n`);
|
|
63
|
+
if (status.mode === "oci" && status.oci.available === false)
|
|
64
|
+
process.stderr.write(`\nThe container runtime is configured but not usable: ${status.oci.reason ?? "unknown reason"}\n`);
|
|
65
|
+
for (const key of ignored)
|
|
66
|
+
process.stderr.write(`\nIgnored ${key} from the project configuration: only your user configuration decides it.\n`);
|
|
67
|
+
}
|
|
68
|
+
/** `alisio analysis setup --extras analysis|science`: needs network; offline it fails cleanly. */
|
|
69
|
+
export async function setupExtras(o, extras) {
|
|
70
|
+
if (extras !== "analysis" && extras !== "science")
|
|
71
|
+
throw new Error("Choose the extras to install: --extras analysis or --extras science");
|
|
72
|
+
const { core, manager } = await context(o);
|
|
73
|
+
const preview = core.installPreview(extras);
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
process.on("SIGINT", () => controller.abort(new Error("Interrupted")));
|
|
76
|
+
process.stderr.write(`Installing the ${extras} extras: ${preview.packages.join(", ")} (+ dependencies, ${preview.packageCount} packages, about ${Math.round(preview.estimatedBytes / (1024 * 1024))} MB). ` +
|
|
77
|
+
"Wheels only, hashes required, needs network.\n");
|
|
78
|
+
try {
|
|
79
|
+
const state = await manager.installExtras(extras, {
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
onOutput: (text) => process.stderr.write(text),
|
|
82
|
+
});
|
|
83
|
+
console.log(JSON.stringify(state, null, 2));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// A clean message and a failing exit code; the base runtime is untouched.
|
|
87
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `alisio analysis setup --oci [--image <name>]`: pulls the image once and verifies it. With a
|
|
93
|
+
* digest-pinned reference the pull verifies the digest; with a tag it prints the digest to put in
|
|
94
|
+
* `analysis.oci.image` (Alisio never writes it: only you choose what runs your code).
|
|
95
|
+
*/
|
|
96
|
+
export async function setupOci(o, image) {
|
|
97
|
+
const { core, config } = await context(o);
|
|
98
|
+
const reference = image ?? config.analysis.oci.image;
|
|
99
|
+
if (!reference)
|
|
100
|
+
throw new Error("Pass --image <name[:tag]> (or set analysis.oci.image in your user configuration) to pull.");
|
|
101
|
+
const runtime = new core.OciRuntime({ ...config.analysis.oci, image: undefined });
|
|
102
|
+
const status = await runtime.status();
|
|
103
|
+
if (!status.ok)
|
|
104
|
+
throw new Error(`${status.reason}${status.alternative ? `; ${status.alternative} is installed (set analysis.oci.engine to "${status.alternative}")` : ""}. The container runtime is optional; python_run works without it.`);
|
|
105
|
+
process.stderr.write(`Pulling ${reference} with ${status.engine} ${status.version}…\n`);
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
process.on("SIGINT", () => controller.abort(new Error("Interrupted")));
|
|
108
|
+
const { digest } = await runtime.pull(reference, controller.signal);
|
|
109
|
+
const pinned = core.OCI_IMAGE_PATTERN.test(reference);
|
|
110
|
+
if (pinned) {
|
|
111
|
+
const wanted = reference.slice(reference.indexOf("@") + 1);
|
|
112
|
+
if (digest && !digest.endsWith(wanted))
|
|
113
|
+
throw new Error(`The pulled image reports ${digest}, not the pinned ${wanted}`);
|
|
114
|
+
console.log(`Verified ${reference}. Set analysis.runtime to "oci" to use it.`);
|
|
115
|
+
}
|
|
116
|
+
else if (digest)
|
|
117
|
+
console.log(`Pulled ${reference}. To use it, put this in your user configuration (${core.configHome()}):\n` +
|
|
118
|
+
` "analysis": { "runtime": "oci", "oci": { "engine": "${status.engine}", "image": "${digest}" } }`);
|
|
119
|
+
else
|
|
120
|
+
throw new Error(`${reference} has no repository digest (a locally built image?). Push it to a registry or pull a published image.`);
|
|
121
|
+
}
|
|
122
|
+
/** `alisio analysis sweep [--force]`: the retention sweep now (it also runs by itself, daily). */
|
|
123
|
+
export async function sweep(o, force) {
|
|
124
|
+
const { core, config, stateDir } = await context(o);
|
|
125
|
+
const store = new core.SQLiteStore(o.db ?? join(stateDir, "sessions.sqlite"));
|
|
126
|
+
try {
|
|
127
|
+
const artifacts = new core.ArtifactStore({ root: stateDir, db: store.db });
|
|
128
|
+
const datasets = new core.DatasetService({ root: stateDir, db: store.db });
|
|
129
|
+
const blobs = new core.BlobStore({ root: join(stateDir, "blobs"), db: store.db });
|
|
130
|
+
const janitor = new core.AnalysisJanitor({
|
|
131
|
+
root: stateDir,
|
|
132
|
+
db: store.db,
|
|
133
|
+
artifacts,
|
|
134
|
+
datasets,
|
|
135
|
+
blobs,
|
|
136
|
+
retention: () => config.analysis.retention,
|
|
137
|
+
});
|
|
138
|
+
const report = await janitor.sweep({ force });
|
|
139
|
+
console.log(JSON.stringify(report, null, 2));
|
|
140
|
+
datasets.close();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
store.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/banner.js
CHANGED
|
@@ -21,7 +21,8 @@ export function terminalCapabilities(input) {
|
|
|
21
21
|
const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
|
|
22
22
|
return {
|
|
23
23
|
color: input.tty && !dumb && env.NO_COLOR === undefined,
|
|
24
|
-
|
|
24
|
+
// `C.UTF-8` is a UTF-8 locale (the default on CI runners); only bare C/POSIX is ASCII.
|
|
25
|
+
unicode: !dumb && (/utf-?8/i.test(locale) || !/^(C|POSIX)(\.|$)/i.test(locale)),
|
|
25
26
|
columns: input.columns > 0 ? input.columns : 80,
|
|
26
27
|
interactive: input.tty,
|
|
27
28
|
};
|
package/dist/main.js
CHANGED
|
@@ -35,6 +35,8 @@ program
|
|
|
35
35
|
.option("--api-mode <mode>", "chat or responses")
|
|
36
36
|
.option("--allow-write", "Allow file writes")
|
|
37
37
|
.option("--allow-process", "Allow arbitrary subprocesses; not sandboxed")
|
|
38
|
+
.option("--allow-analysis", "Allow Python analysis (python_run) without asking; not sandboxed, never allows shell")
|
|
39
|
+
.option("--python <path>", "Python 3.10+ interpreter for python_run (default: discovered)")
|
|
38
40
|
.option("--allow-external", "Allow network tools: webfetch, websearch and provider-native search")
|
|
39
41
|
.option("--allow-mcp", "Allow configured MCP servers and remote tool calls")
|
|
40
42
|
.option("--allow-agents", "Allow messaging neighboring agents through Herdr")
|
|
@@ -116,7 +118,7 @@ async function run(cmd, prompt, sessionId) {
|
|
|
116
118
|
const trusted = await withProjectTrust(opts);
|
|
117
119
|
return runTui({ ...trusted, ...(sessionId ? { session: sessionId } : {}) });
|
|
118
120
|
}
|
|
119
|
-
const { createApplication } = await import("@alisio/core");
|
|
121
|
+
const { createApplication, formatBytes, KIND_LABELS } = await import("@alisio/core");
|
|
120
122
|
const app = await createApplication({
|
|
121
123
|
...(await cliDefaults()),
|
|
122
124
|
...opts,
|
|
@@ -125,10 +127,21 @@ async function run(cmd, prompt, sessionId) {
|
|
|
125
127
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
126
128
|
else if (event.type === "text_delta")
|
|
127
129
|
process.stdout.write(String(event.data.delta));
|
|
130
|
+
else if (event.type === "request_retry") {
|
|
131
|
+
const retry = event.data;
|
|
132
|
+
process.stderr.write(`\nThe model did not respond; retrying (${retry.attempt}/${retry.of})…\n`);
|
|
133
|
+
}
|
|
128
134
|
else if (event.type === "tool_started")
|
|
129
135
|
process.stderr.write(`\n→ ${event.data.name}\n`);
|
|
136
|
+
else if (event.type === "artifact_published")
|
|
137
|
+
process.stderr.write(artifactLine(event));
|
|
130
138
|
},
|
|
131
139
|
});
|
|
140
|
+
/** `artifact: Dashboard site.zip (48 KB) → /abs/path` (nothing is opened automatically). */
|
|
141
|
+
function artifactLine(event) {
|
|
142
|
+
const data = event.data;
|
|
143
|
+
return `artifact: ${KIND_LABELS[data.artifact.kind] ?? "File"} ${data.artifact.fileName} (${formatBytes(data.artifact.bytes)}) → ${data.path}\n`;
|
|
144
|
+
}
|
|
132
145
|
for (const failure of app.mcpStartupFailures())
|
|
133
146
|
process.stderr.write(`[startup] ${failure}\n`);
|
|
134
147
|
// The ACTIVE agent (built-in `build`/`plan` or a main-capable definition) drives the main
|
|
@@ -349,13 +362,61 @@ program.command("doctor").action(async (_opts, cmd) => {
|
|
|
349
362
|
: config.provider.auth === "none" || !!process.env[config.provider.apiKeyEnv],
|
|
350
363
|
},
|
|
351
364
|
};
|
|
352
|
-
|
|
365
|
+
const { analysisState } = await import("./analysis.js");
|
|
366
|
+
const analysisInfo = await analysisState(o);
|
|
367
|
+
const { hints: _hints, ...pythonInfo } = analysisInfo.status.python;
|
|
368
|
+
console.log(JSON.stringify({
|
|
369
|
+
...status,
|
|
370
|
+
python: { mode: analysisInfo.status.mode, ...pythonInfo },
|
|
371
|
+
analysis: {
|
|
372
|
+
mode: analysisInfo.status.mode,
|
|
373
|
+
oci: analysisInfo.status.oci,
|
|
374
|
+
limits: analysisInfo.status.limits,
|
|
375
|
+
retention: analysisInfo.status.retention,
|
|
376
|
+
...(analysisInfo.ignored.length ? { ignored: analysisInfo.ignored } : {}),
|
|
377
|
+
},
|
|
378
|
+
}, null, 2));
|
|
379
|
+
if (analysisInfo.guidance)
|
|
380
|
+
process.stderr.write(`\n${analysisInfo.guidance}\n`);
|
|
381
|
+
for (const key of provenance.ignored ?? [])
|
|
382
|
+
process.stderr.write(`\nIgnored ${key} from the project configuration: only your user configuration decides it.\n`);
|
|
353
383
|
if (!status.ripgrep)
|
|
354
384
|
process.stderr.write(`\nWarning: ripgrep (rg) is not installed; search_text and list_files will not work. ${RIPGREP_INSTALL_HINT}\n`);
|
|
355
385
|
if (!model || model === "YOUR_MODEL_ID")
|
|
356
386
|
console.error("\nNo model configured yet: set provider.model in your config, --model, or ALISIO_MODEL " +
|
|
357
387
|
"before starting a real conversation (it will otherwise fail on the first turn).");
|
|
358
388
|
});
|
|
389
|
+
const analysis = program
|
|
390
|
+
.command("analysis")
|
|
391
|
+
.description("Python analysis runtime (python_run): status, optional extras and containers");
|
|
392
|
+
analysis
|
|
393
|
+
.command("status")
|
|
394
|
+
.description("Show the runtime: Python, extras, container engine, limits and retention")
|
|
395
|
+
.action(async (_opts, cmd) => {
|
|
396
|
+
const { printAnalysisStatus } = await import("./analysis.js");
|
|
397
|
+
await printAnalysisStatus(options(cmd));
|
|
398
|
+
});
|
|
399
|
+
analysis
|
|
400
|
+
.command("setup")
|
|
401
|
+
.description("Optional: install hash-locked Python extras (--extras) or pull the container image (--oci). " +
|
|
402
|
+
"python_run never needs this for the standard library.")
|
|
403
|
+
.option("--extras <set>", "analysis (pandas, numpy, matplotlib…) or science (adds scipy…)")
|
|
404
|
+
.option("--oci", "Pull the container image once and verify its digest (Docker or Podman)")
|
|
405
|
+
.option("--image <name>", "With --oci: the image to pull (default: analysis.oci.image)")
|
|
406
|
+
.action(async (flags, cmd) => {
|
|
407
|
+
const { setupExtras, setupOci } = await import("./analysis.js");
|
|
408
|
+
if (flags.oci)
|
|
409
|
+
return setupOci(options(cmd), flags.image);
|
|
410
|
+
return setupExtras(options(cmd), flags.extras);
|
|
411
|
+
});
|
|
412
|
+
analysis
|
|
413
|
+
.command("sweep")
|
|
414
|
+
.description("Run the retention sweep now (it also runs by itself at most once a day)")
|
|
415
|
+
.option("--force", "Ignore the once-a-day mark")
|
|
416
|
+
.action(async (flags, cmd) => {
|
|
417
|
+
const { sweep } = await import("./analysis.js");
|
|
418
|
+
await sweep(options(cmd), !!flags.force);
|
|
419
|
+
});
|
|
359
420
|
const trust = program.command("trust").description("Inspect or revoke per-directory project trust");
|
|
360
421
|
trust.command("list").action(async () => {
|
|
361
422
|
const { listTrust } = await import("@alisio/core");
|
package/dist/tui/app.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { AGENT_TEMPLATES, agentAuthoringGuidance, agentCommandDescriptors, agentIdFromCommand, agentModelCapabilities, agentModelLabels, agentModelOptions, generateAgentDraft, NOT_SANDBOXED, toGrantWire, toRef, } from "@alisio/core";
|
|
3
4
|
import { CombinedAutocompleteProvider, Container, Editor, getImageDimensions, getNativeClipboard, Key, MouseRegion, matchesKey, ProcessTerminal, ScrollView, SelectList, TuiAltScreen, truncateToWidth, VStack, } from "@earendil-works/pi-tui";
|
|
4
5
|
import { loadVersion } from "../version.js";
|
|
5
6
|
import { AGENT_PICKER_ACTIONS, agentEditorRows, agentFieldChoices, agentFieldSupported, agentPickerRows, applyDraft, decodeMultiline, emptyAgentForm, encodeMultiline, filterAgentRows, formErrors, formFromDefinition, formFromTemplate, formToInput, parseAgentsArgs, } from "./agent-manager.js";
|
|
6
7
|
import { activeAgentCatalog, agentRunOptions, mainAgentFromRecord, resolveActiveAgent, } from "./agents.js";
|
|
8
|
+
import { ArtifactPreview, loadPreview } from "./artifact-preview.js";
|
|
9
|
+
import { ARTIFACTS_HINT, artifactActions, artifactRow, capabilityApprovalTitle, detailRows, installApprovalDetail, installApprovalTitle, matchArtifact, permissionRow, } from "./artifacts.js";
|
|
7
10
|
import { MAX_ATTACHMENTS_PER_MESSAGE, MAX_IMAGE_BYTES, pasteImageFromClipboard, removeLastAttachment, toApiAttachment, } from "./attachments.js";
|
|
8
11
|
import { BTW_DESCRIPTION, BTW_USAGE, btwBrowser, btwPending } from "./btw.js";
|
|
9
12
|
import { copyText, nodeSpawn } from "./clipboard.js";
|
|
10
|
-
import { AttachmentsBar, BannerBlock, BtwPanel, ContentInset, clock, contentInnerWidth, Footer, Header, QuestionPanel, Switch, TranscriptSync, TreePanel, } from "./components.js";
|
|
13
|
+
import { AttachmentsBar, BannerBlock, BtwPanel, ContentInset, clock, contentInnerWidth, Footer, Header, QuestionPanel, Switch, setArtifactPathResolver, TranscriptSync, TreePanel, } from "./components.js";
|
|
11
14
|
import { ConnectInputPrompt } from "./connect-input.js";
|
|
12
15
|
import { bounded, EXIT_PENDING_CAP_MS, EXIT_SESSION_END_CAP_MS } from "./exit.js";
|
|
13
16
|
import { BRANCH_REFRESH_MS, createBranchCache } from "./git-branch.js";
|
|
17
|
+
import { cannotOpenMessage, openPath, revealPath } from "./open-path.js";
|
|
14
18
|
import { initialPanelState, reducePanel, visibleRows } from "./panel.js";
|
|
15
19
|
import { summarizeAnswers } from "./questions.js";
|
|
16
20
|
import { InteractiveQueue } from "./queue.js";
|
|
@@ -479,6 +483,12 @@ export async function runTui(options) {
|
|
|
479
483
|
const error = (e) => push({ kind: "error", text: e instanceof Error ? e.message : String(e) });
|
|
480
484
|
const info = (text) => push({ kind: "info", text });
|
|
481
485
|
let terminalEvent = false;
|
|
486
|
+
let artifactsThisRun = 0;
|
|
487
|
+
// Artifact announcements show the real local file (resolved from the store, never the model).
|
|
488
|
+
setArtifactPathResolver((id) => {
|
|
489
|
+
const record = app.artifacts.get(id);
|
|
490
|
+
return record?.status === "ready" ? app.artifacts.localPath(record) : undefined;
|
|
491
|
+
});
|
|
482
492
|
dispatch = (event) => {
|
|
483
493
|
// The event stream stays text-only (headless/JSONL consumers see the projection); the TUI
|
|
484
494
|
// pulls the persisted rich parts (ui/image) itself so native rendering survives resume.
|
|
@@ -497,15 +507,22 @@ export async function runTui(options) {
|
|
|
497
507
|
tui.requestRender();
|
|
498
508
|
return;
|
|
499
509
|
}
|
|
500
|
-
if (
|
|
510
|
+
if (event.type === "artifact_published")
|
|
511
|
+
artifactsThisRun++;
|
|
512
|
+
const ended = [
|
|
501
513
|
"run_completed",
|
|
502
514
|
"run_failed",
|
|
503
515
|
"run_cancelled",
|
|
504
516
|
"run_turns_exceeded",
|
|
505
517
|
"compaction_failed",
|
|
506
|
-
].includes(event.type)
|
|
518
|
+
].includes(event.type);
|
|
519
|
+
if (ended)
|
|
507
520
|
terminalEvent = true;
|
|
508
521
|
view = reduceEvent(view, event);
|
|
522
|
+
if (ended && event.type !== "compaction_failed" && artifactsThisRun > 0) {
|
|
523
|
+
artifactsThisRun = 0;
|
|
524
|
+
view = addItem(view, { kind: "notice", text: ARTIFACTS_HINT });
|
|
525
|
+
}
|
|
509
526
|
sync();
|
|
510
527
|
};
|
|
511
528
|
const refreshEstimate = () => {
|
|
@@ -562,6 +579,31 @@ export async function runTui(options) {
|
|
|
562
579
|
};
|
|
563
580
|
const onAbort = () => finish("deny");
|
|
564
581
|
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
582
|
+
if (request.capability === "analysis.install" && request.install) {
|
|
583
|
+
// Installing the optional packages: only once or deny (never stored), with what is
|
|
584
|
+
// installed, the download estimate and that it needs the network.
|
|
585
|
+
showPicker(new Picker(`${request.label ? `[${request.label}] ` : ""}${installApprovalTitle(request.install)}`, [
|
|
586
|
+
{ value: "once", label: "Allow once" },
|
|
587
|
+
{ value: "deny", label: "Deny" },
|
|
588
|
+
], (item) => finish(item.value), () => finish("deny"), false, installApprovalDetail(request.install)));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (request.capability) {
|
|
592
|
+
// A capability (Python analysis): its own title, the script preview and the
|
|
593
|
+
// not-a-sandbox warning; "for this session" is persisted for the root session.
|
|
594
|
+
showPicker(new Picker(`${request.label ? `[${request.label}] ` : ""}${capabilityApprovalTitle(request.runtime)}`, [
|
|
595
|
+
{ value: "once", label: "Allow once" },
|
|
596
|
+
{ value: "session", label: "Allow for this session" },
|
|
597
|
+
{ value: "deny", label: "Deny" },
|
|
598
|
+
], (item) => finish(item.value), () => finish("deny"), false, [
|
|
599
|
+
request.preview ?? "",
|
|
600
|
+
"",
|
|
601
|
+
request.runtime === "oci"
|
|
602
|
+
? "Runs in a container without network access. Repository files are not modified by this step."
|
|
603
|
+
: `${NOT_SANDBOXED} It could change repository files; Alisio itself does not modify them in this step.`,
|
|
604
|
+
].join("\n")));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
565
607
|
showPicker(new Picker(`${request.label ? `[${request.label}] ` : ""}Allow ${request.call.name} (${request.effect}): ${summarizeToolArgs(request.call.name, request.call.arguments)}?`, [
|
|
566
608
|
{ value: "once", label: "Allow once" },
|
|
567
609
|
{ value: "session", label: `Always allow ${request.effect} in this session` },
|
|
@@ -684,6 +726,156 @@ export async function runTui(options) {
|
|
|
684
726
|
tui.requestRender();
|
|
685
727
|
}
|
|
686
728
|
};
|
|
729
|
+
/** `/artifacts [filter]`: the root session's artifacts, newest first, then their actions. */
|
|
730
|
+
const browseArtifacts = (filter) => {
|
|
731
|
+
const all = app.artifacts
|
|
732
|
+
.list(app.store.rootOf(session), { limit: 500, includeExpired: true })
|
|
733
|
+
.map(toRef);
|
|
734
|
+
const shown = filter ? all.filter((artifact) => matchArtifact(artifact, filter)) : all;
|
|
735
|
+
if (!shown.length)
|
|
736
|
+
return notice(all.length ? `No artifacts match "${filter}"` : "No artifacts in this session yet.");
|
|
737
|
+
const byId = new Map(all.map((artifact) => [artifact.id, artifact]));
|
|
738
|
+
const now = Date.now();
|
|
739
|
+
showPicker(new Picker(`Artifacts · ${shown.length}`, shown.map((artifact) => ({ value: artifact.id, label: artifactRow(artifact, now) })), (item) => artifactMenu(item.value, filter), () => closePicker(), true, undefined, (items, text) => items.filter((item) => {
|
|
740
|
+
const artifact = byId.get(item.value);
|
|
741
|
+
return !!artifact && matchArtifact(artifact, text);
|
|
742
|
+
})));
|
|
743
|
+
};
|
|
744
|
+
const artifactMenu = (id, filter) => {
|
|
745
|
+
const record = app.artifacts.get(id);
|
|
746
|
+
if (!record || (record.status !== "ready" && record.status !== "expired")) {
|
|
747
|
+
closePicker();
|
|
748
|
+
return notice("This artifact is no longer available.");
|
|
749
|
+
}
|
|
750
|
+
const artifact = toRef(record);
|
|
751
|
+
const path = app.artifacts.localPath(record);
|
|
752
|
+
const back = () => artifactMenu(id, filter);
|
|
753
|
+
const copyPath = async () => {
|
|
754
|
+
const result = await copy(path);
|
|
755
|
+
closePicker();
|
|
756
|
+
if (result.ok)
|
|
757
|
+
tui.flash("Path copied");
|
|
758
|
+
else
|
|
759
|
+
notice(`${copyMessage(result)}. Path: ${path}`);
|
|
760
|
+
};
|
|
761
|
+
/** Opens (or reveals) a store path; without a desktop, offers to copy it instead. */
|
|
762
|
+
const launch = async (target, reveal) => {
|
|
763
|
+
const outcome = await (reveal ? revealPath(target) : openPath(target));
|
|
764
|
+
if (outcome.ok)
|
|
765
|
+
return closePicker();
|
|
766
|
+
// No desktop (SSH, no display) or no opener: nothing was launched; offer the path.
|
|
767
|
+
return showPicker(new Picker(cannotOpenMessage(target, outcome), [
|
|
768
|
+
{ value: "copy", label: "Copy path" },
|
|
769
|
+
{ value: "close", label: "Close" },
|
|
770
|
+
], (choice) => choice.value === "copy"
|
|
771
|
+
? void copy(target).then((result) => {
|
|
772
|
+
closePicker();
|
|
773
|
+
if (result.ok)
|
|
774
|
+
tui.flash("Path copied");
|
|
775
|
+
else
|
|
776
|
+
notice(`${copyMessage(result)}. Path: ${target}`);
|
|
777
|
+
})
|
|
778
|
+
: closePicker(), () => closePicker()));
|
|
779
|
+
};
|
|
780
|
+
// `Copy to workspace…` needs artifact_export (absent under --read-only or with analysis off).
|
|
781
|
+
const canExport = !options.readOnly && app.registry.list().some((tool) => tool.name === "artifact_export");
|
|
782
|
+
const job = record.executionId ? app.analysis.jobs.get(record.executionId) : undefined;
|
|
783
|
+
// `Rerun` runs python_run { rerunOf } (absent under --read-only or with analysis disabled).
|
|
784
|
+
const canRerun = !options.readOnly && app.registry.list().some((tool) => tool.name === "python_run");
|
|
785
|
+
showPicker(new Picker(artifact.fileName, artifactActions(artifact, {
|
|
786
|
+
readOnly: !canExport,
|
|
787
|
+
sources: !!job,
|
|
788
|
+
rerun: !!job && canRerun,
|
|
789
|
+
}), (item) => void (async () => {
|
|
790
|
+
if (item.value === "copy")
|
|
791
|
+
return copyPath();
|
|
792
|
+
if (item.value === "open" || item.value === "reveal")
|
|
793
|
+
return launch(path, item.value === "reveal");
|
|
794
|
+
if (item.value === "preview") {
|
|
795
|
+
const preview = await loadPreview(artifact, path);
|
|
796
|
+
return showPicker(new ArtifactPreview(artifact, preview, {
|
|
797
|
+
height: () => Math.max(5, (process.stdout.rows ?? 24) - 12),
|
|
798
|
+
open: () => void launch(path, false).catch(error),
|
|
799
|
+
copy: () => void copyPath().catch(error),
|
|
800
|
+
close: back,
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
if (item.value === "sources" && job)
|
|
804
|
+
// An explicit user action: the job folder (script, logs, work) of this execution.
|
|
805
|
+
return launch(join(app.artifacts.root, ...job.relDir.split(/[\\/]/)), false);
|
|
806
|
+
if (item.value === "details") {
|
|
807
|
+
const provenance = record.provenance;
|
|
808
|
+
return showPicker(new Picker(`Details · ${artifact.fileName}`, [{ value: "back", label: "Back" }], back, back, false, detailRows(artifact, provenance)
|
|
809
|
+
.map(([label, value]) => `${label}: ${value}`)
|
|
810
|
+
.join("\n")));
|
|
811
|
+
}
|
|
812
|
+
if (item.value === "rerun") {
|
|
813
|
+
if (busy) {
|
|
814
|
+
closePicker();
|
|
815
|
+
return notice("Wait until the current run finishes, then run the analysis again.");
|
|
816
|
+
}
|
|
817
|
+
closePicker();
|
|
818
|
+
info(`Run the analysis of ${artifact.fileName} again`);
|
|
819
|
+
// A tool call without the model: the same capability gate as the first run.
|
|
820
|
+
void task((signal) => app.runner.runToolCall(session, "python_run", { rerunOf: id }, { signal, display: `Run the analysis of ${artifact.fileName} again` }));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (item.value === "export") {
|
|
824
|
+
if (busy) {
|
|
825
|
+
closePicker();
|
|
826
|
+
return notice("Wait until the current run finishes, then copy the artifact.");
|
|
827
|
+
}
|
|
828
|
+
return showPicker(new ConnectInputPrompt({
|
|
829
|
+
provider: "",
|
|
830
|
+
title: `Copy ${artifact.fileName} to the workspace`,
|
|
831
|
+
label: "Folder in the workspace",
|
|
832
|
+
initial: ".",
|
|
833
|
+
secret: false,
|
|
834
|
+
placeholder: ".",
|
|
835
|
+
hint: "Relative to the workspace. Writing asks first unless --allow-write.",
|
|
836
|
+
step: 1,
|
|
837
|
+
steps: 1,
|
|
838
|
+
onSubmit: (target) => {
|
|
839
|
+
closePicker();
|
|
840
|
+
info(`Copy ${artifact.fileName} to the workspace (${target.trim() || "."})`);
|
|
841
|
+
// A tool call without the model: the write gate (approval) applies as usual.
|
|
842
|
+
void task((signal) => app.runner.runToolCall(session, "artifact_export", { id, target: target.trim() || "." }, {
|
|
843
|
+
signal,
|
|
844
|
+
display: `Copy ${artifact.fileName} to the workspace (${target.trim() || "."})`,
|
|
845
|
+
}));
|
|
846
|
+
},
|
|
847
|
+
onCancel: back,
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
if (item.value === "delete")
|
|
851
|
+
return showPicker(new Picker(`Delete ${artifact.fileName}? This can't be undone.`, [
|
|
852
|
+
{ value: "delete", label: "Delete" },
|
|
853
|
+
{ value: "cancel", label: "Cancel" },
|
|
854
|
+
], (choice) => void (async () => {
|
|
855
|
+
if (choice.value !== "delete")
|
|
856
|
+
return back();
|
|
857
|
+
await app.artifacts.delete(id);
|
|
858
|
+
closePicker();
|
|
859
|
+
notice(`Deleted ${artifact.fileName}`);
|
|
860
|
+
})(), back));
|
|
861
|
+
})().catch(error), () => browseArtifacts(filter)));
|
|
862
|
+
};
|
|
863
|
+
/** `/permissions`: persisted grants of the root session; choosing one offers to revoke it. */
|
|
864
|
+
const managePermissions = () => {
|
|
865
|
+
const grants = app.capabilityGrants.live(session).map(toGrantWire);
|
|
866
|
+
if (!grants.length)
|
|
867
|
+
return notice("No saved permissions in this session.");
|
|
868
|
+
showPicker(new Picker("Session permissions", grants.map((grant) => ({ value: grant.id, label: permissionRow(grant) })), (item) => showPicker(new Picker(`Revoke "${item.label}"?`, [
|
|
869
|
+
{ value: "revoke", label: "Revoke" },
|
|
870
|
+
{ value: "cancel", label: "Cancel" },
|
|
871
|
+
], (choice) => {
|
|
872
|
+
if (choice.value !== "revoke")
|
|
873
|
+
return managePermissions();
|
|
874
|
+
app.capabilityGrants.revoke(session, item.value, "tui");
|
|
875
|
+
closePicker();
|
|
876
|
+
notice("Permission revoked: the next Python analysis asks again.");
|
|
877
|
+
}, () => managePermissions())), () => closePicker()));
|
|
878
|
+
};
|
|
687
879
|
const runPrompt = (display, prompt, persistDisplay) => {
|
|
688
880
|
// Any pending clipboard-pasted images ride along with the very next turn, then are cleared.
|
|
689
881
|
const attachments = pendingAttachments.map(toApiAttachment);
|
|
@@ -1740,7 +1932,9 @@ export async function runTui(options) {
|
|
|
1740
1932
|
.finally(settle);
|
|
1741
1933
|
return;
|
|
1742
1934
|
}
|
|
1743
|
-
const stored = row.id === "limits.timeoutMs"
|
|
1935
|
+
const stored = row.id === "limits.timeoutMs" || row.id === "analysis.limits.timeoutMs"
|
|
1936
|
+
? Number(value) * 1000
|
|
1937
|
+
: value;
|
|
1744
1938
|
void app
|
|
1745
1939
|
// Setting ids are the exact SettableSettingKey paths SETTINGS_DEFINITIONS is built from.
|
|
1746
1940
|
.updateSetting(row.id, stored)
|
|
@@ -2026,6 +2220,10 @@ export async function runTui(options) {
|
|
|
2026
2220
|
}
|
|
2027
2221
|
case "btw":
|
|
2028
2222
|
return await sideQuestion(parsed.args);
|
|
2223
|
+
case "artifacts":
|
|
2224
|
+
return browseArtifacts(parsed.args);
|
|
2225
|
+
case "permissions":
|
|
2226
|
+
return managePermissions();
|
|
2029
2227
|
case "ask": {
|
|
2030
2228
|
if (!parsed.args)
|
|
2031
2229
|
return notice("Usage: /ask <question>");
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ArtifactRef, UiBlock } from "@alisio/sdk";
|
|
2
|
+
import { type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
export declare const PREVIEW_MAX_BYTES: number;
|
|
4
|
+
export declare const PREVIEW_MAX_LINES = 2000;
|
|
5
|
+
export declare const PREVIEW_MAX_ROWS = 200;
|
|
6
|
+
export declare const PREVIEW_MAX_COLUMNS = 20;
|
|
7
|
+
export type PreviewKind = "markdown" | "table" | "json" | "text";
|
|
8
|
+
/** How an artifact previews in the terminal, or undefined when it only opens externally. */
|
|
9
|
+
export declare function previewKind(artifact: ArtifactRef): PreviewKind | undefined;
|
|
10
|
+
export interface Preview {
|
|
11
|
+
block: UiBlock;
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
/** Table legend: `showing 200 of 12,480 rows · 20 of 31 columns`. */
|
|
14
|
+
legend?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* RFC 4180 rows (quotes, doubled quotes, newlines inside quotes, CRLF/LF/CR), up to `limit`, with
|
|
18
|
+
* the same parser the dataset ingestion uses (so the preview and `data_inspect` agree).
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseDelimited(text: string, delimiter: string, limit?: number): {
|
|
21
|
+
rows: string[][];
|
|
22
|
+
total: number;
|
|
23
|
+
complete: boolean;
|
|
24
|
+
};
|
|
25
|
+
/** The preview of an artifact from its leading bytes (the caller reads them, bounded). */
|
|
26
|
+
export declare function buildPreview(artifact: ArtifactRef, bytes: Uint8Array, fileBytes?: number): Preview;
|
|
27
|
+
/** Reads the bytes a preview needs (never the whole of a large file). */
|
|
28
|
+
export declare function loadPreview(artifact: ArtifactRef, path: string): Promise<Preview>;
|
|
29
|
+
/**
|
|
30
|
+
* A bounded, scrollable preview panel: ↑/↓, PgUp/PgDn, Home/End scroll; `o` opens with the
|
|
31
|
+
* system app, `c` copies the path, `Esc` or `q` closes. The footer says `truncated` when cut.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ArtifactPreview implements Component {
|
|
34
|
+
private readonly artifact;
|
|
35
|
+
private readonly preview;
|
|
36
|
+
private readonly options;
|
|
37
|
+
private offset;
|
|
38
|
+
private lines;
|
|
39
|
+
private renderedWidth;
|
|
40
|
+
constructor(artifact: ArtifactRef, preview: Preview, options: {
|
|
41
|
+
/** Rows available for the body (the panel adds a title and a footer). */
|
|
42
|
+
height: () => number;
|
|
43
|
+
open: () => void;
|
|
44
|
+
copy: () => void;
|
|
45
|
+
close: () => void;
|
|
46
|
+
});
|
|
47
|
+
invalidate(): void;
|
|
48
|
+
private page;
|
|
49
|
+
private clamp;
|
|
50
|
+
handleInput(data: string): void;
|
|
51
|
+
render(width: number): string[];
|
|
52
|
+
}
|