@visual-z/xeno 0.2.0 → 0.2.1
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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/install.md +5 -0
- package/package.json +1 -1
- package/src/apps/cli.ts +10 -11
- package/src/apps/project-trust.ts +2 -2
- package/src/apps/resource-browser.ts +2 -2
- package/src/apps/session-picker.ts +2 -2
- package/src/apps/startup-input.ts +94 -0
- package/src/apps/startup.ts +17 -31
- package/src/runtime/oauth.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# xeno changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 — 2026-09-30
|
|
4
|
+
|
|
5
|
+
- Handle Ctrl+C and termination signals while startup awaits resources or workspace snapshots, restoring the terminal before exit.
|
|
6
|
+
- Show the current phase during slow initialization and retain early input until Pi's editor or startup selector owns it.
|
|
7
|
+
- Preserve pasted control characters, Unicode input and terminal state across startup failures and selector transitions.
|
|
8
|
+
|
|
3
9
|
## 0.2.0 — 2026-09-30
|
|
4
10
|
|
|
5
11
|
- Restore saved model identity on session entry; cross-model history moves through a mechanical checkpoint.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ is ever summarized by a model, and nothing is ever rewritten in place.
|
|
|
17
17
|
messages[] frozen system prefix + handoff + this node's events
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Version **0.2.
|
|
20
|
+
Version **0.2.1**. The on-disk session format will still change; `0.x` is that
|
|
21
21
|
statement. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
|
|
22
22
|
|
|
23
23
|
## Install
|
package/docs/install.md
CHANGED
|
@@ -68,6 +68,11 @@ Each invocation starts a new session. `xeno -c` continues the newest one;
|
|
|
68
68
|
and nothing is deleted. Resume restores the saved model. An explicit
|
|
69
69
|
`--model` switches at a checkpoint while keeping the original log intact.
|
|
70
70
|
|
|
71
|
+
Startup loads resources and establishes a workspace snapshot before the editor
|
|
72
|
+
opens. Slow initialization shows its current stage; Ctrl+C cancels startup and
|
|
73
|
+
restores the terminal. Snapshots cover the selected workspace, so start inside
|
|
74
|
+
the intended project or pass `--workspace <path>`.
|
|
75
|
+
|
|
71
76
|
## Credentials
|
|
72
77
|
|
|
73
78
|
Resolution order, first match wins:
|
package/package.json
CHANGED
package/src/apps/cli.ts
CHANGED
|
@@ -13,18 +13,16 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
-
parseArgs, startup, readPipedStdin, mergePrintPrompt,
|
|
16
|
+
parseArgs, startup, readPipedStdin, mergePrintPrompt, isInteractiveSession, printCliHelp,
|
|
17
17
|
} from "./startup.ts";
|
|
18
|
+
import { createStartupInput } from "./startup-input.ts";
|
|
18
19
|
import { runPrompt, runRepl } from "./shell.ts";
|
|
19
20
|
import { runPackageCommand } from "../runtime/package-manager.ts";
|
|
20
21
|
import { exportHtmlFile } from "./export-cli.ts";
|
|
21
22
|
|
|
22
23
|
const args = parseArgs(process.argv.slice(2), { deferExtensions: true });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// straight into the banner.
|
|
26
|
-
const restoreTty = silenceTtyEarly({ interactive: !args.packageCommand && !args.help && !args.version && !args.exportFile && !args.print && !args.prompt });
|
|
27
|
-
process.once("exit", restoreTty);
|
|
24
|
+
const input = createStartupInput({ interactive: isInteractiveSession(args) && !args.packageCommand && !args.version && !args.exportFile && !args.listModels && !args.modelsCommand });
|
|
25
|
+
process.once("exit", input.restore);
|
|
28
26
|
try {
|
|
29
27
|
if (args.help && (args.packageCommand || args.configCommand || args.exportFile || args.version || args.listModels || args.modelsCommand)) {
|
|
30
28
|
printCliHelp();
|
|
@@ -44,22 +42,23 @@ try {
|
|
|
44
42
|
catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = controller.signal.aborted ? 130 : 1; }
|
|
45
43
|
finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
|
|
46
44
|
} else {
|
|
47
|
-
const app = await startup(args);
|
|
45
|
+
const app = await startup(args, input);
|
|
48
46
|
if (args.print) {
|
|
49
47
|
// With a prompt already given, stdin is optional context: wait briefly for it.
|
|
50
48
|
args.prompt = mergePrintPrompt(args.prompt,
|
|
51
49
|
await readPipedStdin(process.stdin, args.prompt.length > 0 ? { firstByteMs: 3_000 } : {}));
|
|
52
50
|
}
|
|
53
51
|
if (args.prompt) {
|
|
54
|
-
|
|
52
|
+
input.restore();
|
|
55
53
|
await runPrompt(app, args.prompt);
|
|
56
54
|
} else {
|
|
57
|
-
|
|
55
|
+
input.phase("Loading conversation");
|
|
56
|
+
await runRepl(app, input.handoff);
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
} catch (error) {
|
|
61
60
|
console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1;
|
|
62
61
|
} finally {
|
|
63
|
-
process.off("exit",
|
|
64
|
-
|
|
62
|
+
process.off("exit", input.restore);
|
|
63
|
+
input.restore();
|
|
65
64
|
}
|
|
@@ -33,7 +33,7 @@ export class TrustSelector extends SelectList {
|
|
|
33
33
|
.map(line => truncateToWidth(line, width, ""));
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
export async function pickProjectTrust(workspace: string): Promise<boolean> {
|
|
36
|
+
export async function pickProjectTrust(workspace: string, handoff?: () => void): Promise<boolean> {
|
|
37
37
|
const decision = await projectTrust(workspace);
|
|
38
38
|
installBindings((await loadUiResources({ workspace, trusted: false })).keymap.byAction);
|
|
39
39
|
const screen = createInteractiveTui();
|
|
@@ -42,7 +42,7 @@ export async function pickProjectTrust(workspace: string): Promise<boolean> {
|
|
|
42
42
|
try {
|
|
43
43
|
const choice = await new Promise<string | null>(resolve => {
|
|
44
44
|
select.onInput = data => { const state = select.acceptInput(data); if (state.cancelled) resolve(null); else if (state.confirmed) resolve(state.confirmed.value); screen.requestRender(); };
|
|
45
|
-
screen.start();
|
|
45
|
+
handoff?.(); screen.start();
|
|
46
46
|
});
|
|
47
47
|
return choice === null ? false : applyTrustChoice(workspace, choice as TrustChoice);
|
|
48
48
|
} finally { await screen.terminal.drainInput(); screen.stop(); }
|
|
@@ -54,7 +54,7 @@ export class ResourceBrowser extends SelectList {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** Standalone configuration does not allocate a conversation or require a model credential. */
|
|
57
|
-
export async function runResourceConfig(options: ResourceOptions): Promise<void> {
|
|
57
|
+
export async function runResourceConfig(options: ResourceOptions, handoff?: () => void): Promise<void> {
|
|
58
58
|
const index = await discoverResources(options);
|
|
59
59
|
const ui = await loadUiResources(options, index);
|
|
60
60
|
const screen = createInteractiveTui();
|
|
@@ -69,7 +69,7 @@ export async function runResourceConfig(options: ResourceOptions): Promise<void>
|
|
|
69
69
|
if (browser.acceptInput(data).cancelled) resolve();
|
|
70
70
|
screen.requestRender();
|
|
71
71
|
}).catch(error => { browser.notice = String(error); screen.requestRender(); }); };
|
|
72
|
-
screen.start();
|
|
72
|
+
handoff?.(); screen.start();
|
|
73
73
|
});
|
|
74
74
|
} finally { await screen.terminal.drainInput(); screen.stop(); }
|
|
75
75
|
}
|
|
@@ -22,7 +22,7 @@ export async function pickStartupSession(opts: {
|
|
|
22
22
|
style: Style;
|
|
23
23
|
say: (line: string) => void;
|
|
24
24
|
keymap?: Keymap;
|
|
25
|
-
}): Promise<string | null> {
|
|
25
|
+
}, handoff?: () => void): Promise<string | null> {
|
|
26
26
|
const root = { path: sessionsDir(opts.workspace, opts.sessionDir), workspace: opts.workspace };
|
|
27
27
|
await registerSessionRoot(root).catch(error => opts.say(String(error)));
|
|
28
28
|
const screen = createInteractiveTui();
|
|
@@ -40,7 +40,7 @@ export async function pickStartupSession(opts: {
|
|
|
40
40
|
else if (state.confirmed) resolve(state.confirmed.value);
|
|
41
41
|
else screen.requestRender();
|
|
42
42
|
}).catch(error => { opts.say(String(error)); }); };
|
|
43
|
-
screen.start();
|
|
43
|
+
handoff?.(); screen.start();
|
|
44
44
|
void select.load().then(async () => {
|
|
45
45
|
if (select.scope === "current" && !select.sessionCount && !select.notice && (await sessionRoots(root)).length === 1) {
|
|
46
46
|
opts.say("No sessions to resume"); resolve(null);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Startup owns raw input until a Pi screen can own it (D19). */
|
|
2
|
+
import { StdinBuffer } from "../vendor/pi-tui/stdin-buffer.ts";
|
|
3
|
+
import { isKeyRelease, matchesKey } from "../vendor/pi-tui/keys.ts";
|
|
4
|
+
import { resolveEscapeTimeoutMs } from "../vendor/pi-tui/terminal.ts";
|
|
5
|
+
|
|
6
|
+
type StartupStdin = Pick<NodeJS.ReadStream, "isTTY" | "isRaw" | "setRawMode" | "setEncoding" | "on" | "off" | "resume" | "pause" | "unshift">;
|
|
7
|
+
export interface StartupInput {
|
|
8
|
+
phase(name: string): void;
|
|
9
|
+
/** Pause, restore the shell's mode and replay held bytes to the next input owner. */
|
|
10
|
+
handoff(): void;
|
|
11
|
+
/** Final cleanup discards unsubmitted input instead of leaking it to the shell. */
|
|
12
|
+
restore(): void;
|
|
13
|
+
runWithTerminal<T>(run: (handoff: () => void) => Promise<T>): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createStartupInput(opts: {
|
|
17
|
+
interactive: boolean;
|
|
18
|
+
stdin?: StartupStdin;
|
|
19
|
+
signals?: NodeJS.EventEmitter;
|
|
20
|
+
report?: (message: string) => void;
|
|
21
|
+
exit?: (code: number) => void;
|
|
22
|
+
}): StartupInput {
|
|
23
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
24
|
+
if (!opts.interactive || stdin.isTTY !== true) return {
|
|
25
|
+
phase() {}, handoff() {}, restore() {}, runWithTerminal: run => run(() => {}),
|
|
26
|
+
};
|
|
27
|
+
const signals = opts.signals ?? process;
|
|
28
|
+
const report = opts.report ?? (message => console.error(message));
|
|
29
|
+
const exit = opts.exit ?? (code => process.exit(code));
|
|
30
|
+
const wasRaw = stdin.isRaw === true;
|
|
31
|
+
let active = false, closed = false, announced = false;
|
|
32
|
+
let phase = "Reading configuration";
|
|
33
|
+
let pending: string[] = [];
|
|
34
|
+
let parser: StdinBuffer | undefined;
|
|
35
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
36
|
+
|
|
37
|
+
const release = () => {
|
|
38
|
+
if (!active) return;
|
|
39
|
+
active = false;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
stdin.pause();
|
|
42
|
+
stdin.off("data", receive);
|
|
43
|
+
signals.off("SIGINT", interrupt); signals.off("SIGTERM", terminate); signals.off("SIGHUP", hangup);
|
|
44
|
+
parser?.destroy(); parser = undefined;
|
|
45
|
+
stdin.setRawMode(wasRaw);
|
|
46
|
+
};
|
|
47
|
+
const restore = () => { closed = true; release(); pending = []; };
|
|
48
|
+
const cancel = (code: number) => {
|
|
49
|
+
if (!active) return;
|
|
50
|
+
restore();
|
|
51
|
+
if (code === 130) report("Startup cancelled.");
|
|
52
|
+
exit(code);
|
|
53
|
+
};
|
|
54
|
+
const interrupt = () => cancel(130), terminate = () => cancel(143), hangup = () => cancel(129);
|
|
55
|
+
const receive = (data: string) => {
|
|
56
|
+
// Keep original chunks, including partial escapes/pastes, for Pi's own decoder.
|
|
57
|
+
// The temporary parser only recognizes cancellation outside bracketed paste.
|
|
58
|
+
pending.push(data);
|
|
59
|
+
parser?.process(data);
|
|
60
|
+
};
|
|
61
|
+
const announce = () => { if (active) { announced = true; report(` Starting xeno: ${phase} (Ctrl+C to cancel)`); } };
|
|
62
|
+
const capture = () => {
|
|
63
|
+
if (closed || active) return;
|
|
64
|
+
active = true; announced = false;
|
|
65
|
+
parser = new StdinBuffer({ escapeTimeout: resolveEscapeTimeoutMs() });
|
|
66
|
+
parser.on("data", (data: string) => { if (!isKeyRelease(data) && matchesKey(data, "ctrl+c")) interrupt(); });
|
|
67
|
+
stdin.setEncoding("utf8");
|
|
68
|
+
stdin.on("data", receive);
|
|
69
|
+
signals.on("SIGINT", interrupt); signals.on("SIGTERM", terminate); signals.on("SIGHUP", hangup);
|
|
70
|
+
stdin.setRawMode(true);
|
|
71
|
+
stdin.resume();
|
|
72
|
+
// Assumed UI grace period, not a work deadline: fast starts stay quiet;
|
|
73
|
+
// a slow stage remains visible without truncating or abandoning its work.
|
|
74
|
+
timer = setTimeout(announce, 750);
|
|
75
|
+
timer.unref();
|
|
76
|
+
};
|
|
77
|
+
const handoff = () => {
|
|
78
|
+
if (!active) return;
|
|
79
|
+
release();
|
|
80
|
+
const buffered = pending.join(""); pending = [];
|
|
81
|
+
if (buffered) stdin.unshift(buffered);
|
|
82
|
+
};
|
|
83
|
+
capture();
|
|
84
|
+
return {
|
|
85
|
+
phase(name) { if (name === phase) return; phase = name; if (announced) announce(); },
|
|
86
|
+
handoff, restore,
|
|
87
|
+
async runWithTerminal(run) {
|
|
88
|
+
// A selector can load settings/history before mounting. Keep owning input
|
|
89
|
+
// until its synchronous screen.start(), then reacquire only after stop().
|
|
90
|
+
try { return await run(handoff); }
|
|
91
|
+
finally { capture(); }
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
package/src/apps/startup.ts
CHANGED
|
@@ -57,6 +57,7 @@ import { DEFAULT_CONTEXT_POLICY, isContextPolicy, type ContextPolicy } from "../
|
|
|
57
57
|
import { windowBudget } from "../core/context-window.ts";
|
|
58
58
|
import { colorEnabled, createPalette, describeError, stdinIsTty, stdoutIsTty, type Palette } from "./render.ts";
|
|
59
59
|
import { type ActionId, type Keymap } from "../core/keymap.ts";
|
|
60
|
+
import type { StartupInput } from "./startup-input.ts";
|
|
60
61
|
|
|
61
62
|
export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
|
|
62
63
|
|
|
@@ -346,32 +347,6 @@ function rejectUnknownFlag(flag: string): never {
|
|
|
346
347
|
process.exit(1);
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
/**
|
|
350
|
-
* Stop the terminal echoing before startup does anything slow.
|
|
351
|
-
*
|
|
352
|
-
* Reading auth, scanning context files and printing the header all happen
|
|
353
|
-
* before the TUI attaches. In cooked mode the tty echoes whatever is typed
|
|
354
|
-
* during that window, so keystrokes land *between* the banner's own lines
|
|
355
|
-
* and look like output the program produced. Raw mode is the TUI's anyway;
|
|
356
|
-
* taking it at the first instruction just closes the gap.
|
|
357
|
-
*
|
|
358
|
-
* `stdin` is a parameter so the decision is testable without a real tty.
|
|
359
|
-
* Returns an undo so a caller that bails out can put the terminal back.
|
|
360
|
-
*/
|
|
361
|
-
export function silenceTtyEarly(opts: {
|
|
362
|
-
interactive: boolean;
|
|
363
|
-
stdin?: Pick<NodeJS.ReadStream, "isTTY" | "setRawMode" | "resume">;
|
|
364
|
-
}): () => void {
|
|
365
|
-
const stdin = opts.stdin ?? process.stdin;
|
|
366
|
-
if (!opts.interactive || stdin.isTTY !== true) return () => {};
|
|
367
|
-
stdin.setRawMode(true);
|
|
368
|
-
// Leave input buffered until ProcessTerminal installs its listener. Resuming
|
|
369
|
-
// here drops keystrokes typed while startup or transcript loading awaits I/O.
|
|
370
|
-
return () => {
|
|
371
|
-
if (stdin.isTTY === true) stdin.setRawMode(false);
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
350
|
export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
|
|
376
351
|
let selectedTheme: string | undefined;
|
|
377
352
|
const args: Args = {
|
|
@@ -961,6 +936,7 @@ export function sessionDirFor(args: Args, workspace: string): string | undefined
|
|
|
961
936
|
}
|
|
962
937
|
|
|
963
938
|
type OpenSessionOptions = Omit<Parameters<typeof createSessionFor>[0], "providerConfig" | "credential" | "oauth" | "system"> & {
|
|
939
|
+
startupPhase?: (name: string) => void;
|
|
964
940
|
catalog: readonly ModelSpec[];
|
|
965
941
|
auth: AuthFile;
|
|
966
942
|
credentialOf: (spec: ModelSpec) => Resolved;
|
|
@@ -1021,6 +997,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1021
997
|
session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
|
|
1022
998
|
workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
|
|
1023
999
|
}> {
|
|
1000
|
+
opts.startupPhase?.("Reading session history");
|
|
1024
1001
|
const recorded = await readCursor(opts.logDir);
|
|
1025
1002
|
const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
|
|
1026
1003
|
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
@@ -1029,6 +1006,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1029
1006
|
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
|
|
1030
1007
|
}
|
|
1031
1008
|
if (opts.preparedResources && opts.preparedResources.workspace !== workspace) await opts.preparedResources.value.extensions.dispose();
|
|
1009
|
+
opts.startupPhase?.("Loading session resources");
|
|
1032
1010
|
const resources = opts.preparedResources?.workspace === workspace ? opts.preparedResources.value
|
|
1033
1011
|
: await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
|
|
1034
1012
|
const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
|
|
@@ -1042,6 +1020,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1042
1020
|
const explicit = flags.model.length > 0 || flags.provider.length > 0;
|
|
1043
1021
|
const settings = opts.settings ?? {};
|
|
1044
1022
|
const preferred = saved ?? opts.fallback?.spec;
|
|
1023
|
+
opts.startupPhase?.("Resolving model credentials");
|
|
1045
1024
|
const selected = !explicit && preferred !== undefined
|
|
1046
1025
|
? { ok: true as const, spec: preferred }
|
|
1047
1026
|
: selectModel(settings, flags, (spec) => opts.credentialOf(spec).ok, opts.catalog);
|
|
@@ -1086,6 +1065,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1086
1065
|
credential: restoringCredential,
|
|
1087
1066
|
...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
|
|
1088
1067
|
});
|
|
1068
|
+
opts.startupPhase?.("Preparing workspace snapshot");
|
|
1089
1069
|
await session.start();
|
|
1090
1070
|
if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
|
|
1091
1071
|
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
|
|
@@ -1103,7 +1083,8 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1103
1083
|
blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
|
|
1104
1084
|
}
|
|
1105
1085
|
|
|
1106
|
-
export async function startup(args: Args): Promise<App> {
|
|
1086
|
+
export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
1087
|
+
const withTerminal = <T>(run: (handoff: () => void) => Promise<T>): Promise<T> => input ? input.runWithTerminal(run) : run(() => {});
|
|
1107
1088
|
if (args.extensionArgv && (args.listModels || args.modelsCommand || args.configCommand)) rejectUnknownFlag("--" + Object.keys(args.extensionInputs ?? {})[0]);
|
|
1108
1089
|
const C = createPalette(colorEnabled());
|
|
1109
1090
|
let printMode = isPrintMode(args);
|
|
@@ -1158,6 +1139,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1158
1139
|
}
|
|
1159
1140
|
|
|
1160
1141
|
// File only. Passing fetch here would make the test of D16 un-runnable.
|
|
1142
|
+
input?.phase("Loading model catalog");
|
|
1161
1143
|
const loaded = loadCatalog(await readModelSources());
|
|
1162
1144
|
for (const warning of loaded.warnings) io.say(C.yellow(` ${warning}`));
|
|
1163
1145
|
const catalog = loaded.models;
|
|
@@ -1187,6 +1169,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1187
1169
|
* It has to happen before model selection so a trusted `.xeno/settings.json`
|
|
1188
1170
|
* can override the default model. Trust does not affect tools.
|
|
1189
1171
|
*/
|
|
1172
|
+
input?.phase("Opening workspace");
|
|
1190
1173
|
let workspace: string;
|
|
1191
1174
|
try {
|
|
1192
1175
|
workspace = await workspaceRoot(args.workspace);
|
|
@@ -1206,6 +1189,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1206
1189
|
}
|
|
1207
1190
|
}
|
|
1208
1191
|
|
|
1192
|
+
input?.phase("Preparing extensions and project settings");
|
|
1209
1193
|
await prepareExtensionPackages(args,workspace);
|
|
1210
1194
|
const trust = await projectTrust(workspace, args.approve);
|
|
1211
1195
|
let loadProjectSettings = trust.trusted;
|
|
@@ -1213,11 +1197,11 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1213
1197
|
if (!stdinIsTty() || !stdoutIsTty()) {
|
|
1214
1198
|
const index = await discoverResources({ ...args, workspace, trusted: loadProjectSettings });
|
|
1215
1199
|
for (const entry of index.entries) io.say(`${entry.enabled ? "[x]" : "[ ]"} ${entry.kind} ${entry.scope}${entry.package?" · "+entry.package.source:""} ${entry.path}`);
|
|
1216
|
-
} else await runResourceConfig({ ...args, workspace, trusted: loadProjectSettings });
|
|
1200
|
+
} else await withTerminal(handoff => runResourceConfig({ ...args, workspace, trusted: loadProjectSettings }, handoff));
|
|
1217
1201
|
process.exit(0);
|
|
1218
1202
|
}
|
|
1219
1203
|
const hasProjectResources = (await Promise.all(["settings.json", "keybindings.json", "skills", "prompts", "themes", "extensions"].map(name => stat(join(workspace, ".xeno", name)).then(() => true, () => false)))).some(Boolean);
|
|
1220
|
-
if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await pickProjectTrust(workspace);
|
|
1204
|
+
if (!args.help && hasProjectResources && args.approve === undefined && !trust.saved && trust.policy === "ask" && isInteractiveSession(args)) loadProjectSettings = await withTerminal(handoff => pickProjectTrust(workspace, handoff));
|
|
1221
1205
|
if (hasProjectResources && !loadProjectSettings) io.say(C.dim(" Project resources not loaded (project is not trusted). /trust changes resource trust; tool permissions are separate."));
|
|
1222
1206
|
|
|
1223
1207
|
// Resolve the resource owner before factories run. Parsing flags must not load a different cwd first.
|
|
@@ -1231,6 +1215,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1231
1215
|
else if (args.continueLatest) resourceLog = (await listSessions(workspace, sessionDirFor(args, workspace), Infinity)).sessions.find(session => session.ok)?.path;
|
|
1232
1216
|
if (resourceLog) resourceWorkspace = (await readCursor(resourceLog))?.workspace ?? workspace;
|
|
1233
1217
|
}
|
|
1218
|
+
input?.phase("Loading session resources");
|
|
1234
1219
|
const preparedResources = prepareFlags
|
|
1235
1220
|
? { workspace: resourceWorkspace, value: await loadSessionResources(args, resourceWorkspace, io, resourceWorkspace === workspace ? loadProjectSettings : undefined) } : undefined;
|
|
1236
1221
|
if (args.help) { printCliHelp(preparedResources?.value.extensions.flags); await preparedResources?.value.extensions.dispose(); process.exit(0); }
|
|
@@ -1268,8 +1253,8 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1268
1253
|
if (printMode || jsonMode || !stdinIsTty() || !stdoutIsTty()) {
|
|
1269
1254
|
throw new Error("--resume needs an interactive terminal. Pass -r <id> or --session <id> instead");
|
|
1270
1255
|
}
|
|
1271
|
-
const picked = await pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
|
|
1272
|
-
...(sessionDir === undefined ? {} : { sessionDir }) });
|
|
1256
|
+
const picked = await withTerminal(handoff => pickStartupSession({ workspace, style: ui.style, say: io.say, keymap: ui.keymap,
|
|
1257
|
+
...(sessionDir === undefined ? {} : { sessionDir }) }, handoff));
|
|
1273
1258
|
if (picked === null) { io.say(C.dim("No session selected")); process.exit(0); }
|
|
1274
1259
|
sessionOpts = { explicit: picked };
|
|
1275
1260
|
}
|
|
@@ -1301,6 +1286,7 @@ export async function startup(args: Args): Promise<App> {
|
|
|
1301
1286
|
settings: mergedSettings, modelFlags: { model: args.model, provider: args.provider }, projectTrusted: loadProjectSettings,
|
|
1302
1287
|
...(preparedResources ? { preparedResources } : {}),
|
|
1303
1288
|
interactive: isInteractiveSession(args),
|
|
1289
|
+
...(input ? { startupPhase: input.phase } : {}),
|
|
1304
1290
|
});
|
|
1305
1291
|
} catch (err) {
|
|
1306
1292
|
const detail = err instanceof Error ? err.message : String(err);
|
package/src/runtime/oauth.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { createServer, type Server } from "node:http";
|
|
|
17
17
|
import { pollDeviceCode } from "./oauth-device.ts";
|
|
18
18
|
|
|
19
19
|
/** Matches package.json. Sent as User-Agent so the server can see who we are (D18). */
|
|
20
|
-
export const XENO_VERSION = "0.2.
|
|
20
|
+
export const XENO_VERSION = "0.2.1";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Minutes subtracted from server expiry so a request issued at the edge
|