@visual-z/xeno 0.3.0 → 0.4.0
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 +9 -0
- package/README.md +21 -9
- package/docs/app-server.md +84 -0
- package/docs/install.md +1 -1
- package/docs/providers.md +16 -6
- package/package.json +3 -2
- package/src/apps/app-server.ts +28 -0
- package/src/apps/cli.ts +25 -10
- package/src/apps/commands.ts +10 -6
- package/src/apps/completions.ts +15 -13
- package/src/apps/control-transport.ts +89 -0
- package/src/apps/model-catalog.ts +2 -1
- package/src/apps/provider-connect.ts +4 -4
- package/src/apps/shell.ts +39 -20
- package/src/apps/startup.ts +22 -9
- package/src/apps/ui/app.ts +196 -26
- package/src/apps/ui/control-model.ts +26 -0
- package/src/apps/ui/surface.ts +2 -0
- package/src/core/completion-action.ts +20 -0
- package/src/core/completion-review.ts +26 -26
- package/src/core/control-protocol.ts +62 -0
- package/src/core/control-view.ts +42 -0
- package/src/core/event-validation.ts +1 -1
- package/src/core/events.ts +2 -0
- package/src/core/harness-diagnostics.ts +206 -0
- package/src/core/oauth-fields.ts +44 -0
- package/src/core/program-status.ts +31 -0
- package/src/core/project.ts +25 -1
- package/src/core/request-progress.ts +87 -0
- package/src/core/request-state.ts +114 -0
- package/src/core/settings.ts +7 -0
- package/src/core/tool-input.ts +6 -0
- package/src/runtime/auth.ts +10 -3
- package/src/runtime/catalog.ts +13 -3
- package/src/runtime/control-connection.ts +104 -0
- package/src/runtime/harness-eval.ts +3 -1
- package/src/runtime/loop.ts +72 -19
- package/src/runtime/model.ts +9 -0
- package/src/runtime/native-eval.ts +6 -3
- package/src/runtime/oauth.ts +80 -3
- package/src/runtime/pi-provider.ts +169 -12
- package/src/runtime/program-status.ts +38 -0
- package/src/runtime/provider-connect.ts +2 -2
- package/src/runtime/provider-presets.ts +1 -1
- package/src/runtime/provider-request.ts +13 -2
- package/src/runtime/provider.ts +23 -9
- package/src/runtime/session-control.ts +198 -0
- package/src/tui/ui/composer.ts +3 -2
- package/src/tui/ui/control-panel.ts +23 -0
- package/src/tui/ui/mascot.ts +1 -1
- package/src/tui/ui/status.ts +8 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom endpoints and connected-provider management, reached from `/
|
|
2
|
+
* Custom endpoints and connected-provider management, reached from `/provider`.
|
|
3
3
|
*
|
|
4
4
|
* Flow (magpie's add form + OpenCode's /connect, see
|
|
5
5
|
* docs/tasks/feat-provider-connect.md): choose a preset or a custom endpoint
|
|
@@ -68,7 +68,7 @@ function connectedName(id: string): string {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Entry points from `/
|
|
71
|
+
* Entry points from `/provider`. API-key and subscription sign-in stay on the
|
|
72
72
|
* login picker itself (their masked editor and OAuth flows are fixed by
|
|
73
73
|
* tests); the wizard owns what login never did: custom endpoints and
|
|
74
74
|
* per-provider model choice.
|
|
@@ -86,7 +86,7 @@ export function startConnect(host: ConnectHost, entry: "custom" | "manage"): voi
|
|
|
86
86
|
}
|
|
87
87
|
const connected = [...new Set(app.catalog.map(model => model.provider))].filter(id => providerReady(app, id).ready > 0).sort();
|
|
88
88
|
if (connected.length === 0) {
|
|
89
|
-
wizard.push({ kind: "message", title: "No provider is connected yet", lines: ["Choose a subscription, an API-key provider or a custom endpoint from /
|
|
89
|
+
wizard.push({ kind: "message", title: "No provider is connected yet", lines: ["Choose a subscription, an API-key provider or a custom endpoint from /provider first."], onDone: () => host.close() });
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
wizard.push({
|
|
@@ -100,7 +100,7 @@ export function startConnect(host: ConnectHost, entry: "custom" | "manage"): voi
|
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
/** A provider configured by hand (or by an earlier /
|
|
103
|
+
/** A provider configured by hand (or by an earlier /provider) that is not a preset. */
|
|
104
104
|
function customFromCatalog(app: App, id: string): ProviderPreset | undefined {
|
|
105
105
|
const model = app.catalog.find(candidate => candidate.provider === id);
|
|
106
106
|
if (!model) return undefined;
|
package/src/apps/shell.ts
CHANGED
|
@@ -30,14 +30,14 @@ import {
|
|
|
30
30
|
} from "../core/context-policy.ts";
|
|
31
31
|
import { anchoredCount, estimateTokens, type Anchor, type TokenCounter } from "../core/tokens.ts";
|
|
32
32
|
import type { RestorePoint, RewindMode } from "../runtime/loop.ts";
|
|
33
|
-
import { createProvider, findModelByPattern, groupedModels, loadCatalog } from "../runtime/catalog.ts";
|
|
33
|
+
import { createProvider, providerSessionId, findModelByPattern, groupedModels, loadCatalog } from "../runtime/catalog.ts";
|
|
34
34
|
import { outputTokenBudget, type Effort, type ModelSpec } from "../runtime/model.ts";
|
|
35
35
|
import { scopedModels } from "../runtime/model-scope.ts";
|
|
36
36
|
import { equivalentCost, reportedCost } from "../runtime/usage.ts";
|
|
37
37
|
import type { ProviderConfig } from "../runtime/provider.ts";
|
|
38
38
|
import { describeMissingCredential, envLabel, ensureFreshCredential, storedIsOAuth } from "../runtime/auth.ts";
|
|
39
39
|
import {
|
|
40
|
-
SUBSCRIPTION_NOTICE, SUBSCRIPTION_OPTIONS,
|
|
40
|
+
SUBSCRIPTION_NOTICE, SUBSCRIPTION_OPTIONS, subscriptionById, loginGrok,
|
|
41
41
|
loginGitHubCopilot, generatePKCE, generateState, anthropicAuthorizeUrl, openaiCodexAuthorizeUrl,
|
|
42
42
|
startCallbackServer, exchangeAnthropicCode, exchangeOpenAICode, parseAuthorizationInput,
|
|
43
43
|
ANTHROPIC_CALLBACK_PORT, ANTHROPIC_CALLBACK_PATH, ANTHROPIC_REDIRECT_URI,
|
|
@@ -70,6 +70,7 @@ import { colorEnabled } from "./render.ts";
|
|
|
70
70
|
import { createStyle, IDENTITY } from "../tui/style.ts";
|
|
71
71
|
import { lastAssistantText, toHtml } from "../core/export.ts";
|
|
72
72
|
import { toJsonEvent } from "../core/json-event.ts";
|
|
73
|
+
import { sessionControl } from "../runtime/session-control.ts";
|
|
73
74
|
import { COMMANDS, parseCommand, type Command } from "./commands.ts";
|
|
74
75
|
import { expandInput } from "./input.ts";
|
|
75
76
|
import {
|
|
@@ -84,7 +85,7 @@ import type { TurnSurface, Ui } from "./ui/surface.ts";
|
|
|
84
85
|
* One line. Listing all ~40 commands here wrapped into a two-line wall that
|
|
85
86
|
* nobody reads; typing `/` already opens the searchable command menu.
|
|
86
87
|
*/
|
|
87
|
-
const HINT = `type a prompt · / for ${COMMANDS.length} commands · /
|
|
88
|
+
const HINT = `type a prompt · / for ${COMMANDS.length} commands · /provider connects a provider · /model switches · /help`;
|
|
88
89
|
const OAUTH_PASTE = "\0oauth";
|
|
89
90
|
|
|
90
91
|
const initializedExtensions = new WeakSet<ExtensionRegistry>();
|
|
@@ -344,7 +345,7 @@ async function runLines(app: App): Promise<void> {
|
|
|
344
345
|
await bindExtensions(app);
|
|
345
346
|
const C = app.io.palette;
|
|
346
347
|
if (app.ui?.settings.quietStartup !== true || app.args.verbose) app.io.say(C.dim(HINT));
|
|
347
|
-
/** Set by `/
|
|
348
|
+
/** Set by `/provider <provider>`; the next line typed is the key, not a prompt. */
|
|
348
349
|
let awaitingKeyFor: string | null = null;
|
|
349
350
|
for await (const line of console) {
|
|
350
351
|
const text = line.trim();
|
|
@@ -438,7 +439,11 @@ async function execute(app: App, command: Command, tui: Ui | null): Promise<Outc
|
|
|
438
439
|
printUsage(app);
|
|
439
440
|
return "continue";
|
|
440
441
|
case "state":
|
|
441
|
-
printState(app);
|
|
442
|
+
await printState(app);
|
|
443
|
+
return "continue";
|
|
444
|
+
case "control":
|
|
445
|
+
if (tui) await tui.openControl();
|
|
446
|
+
else app.io.say(JSON.stringify(await sessionControl(app.session).snapshot(), null, 2));
|
|
442
447
|
return "continue";
|
|
443
448
|
case "context":
|
|
444
449
|
printContext(app);
|
|
@@ -514,8 +519,8 @@ async function execute(app: App, command: Command, tui: Ui | null): Promise<Outc
|
|
|
514
519
|
if (command.provider === undefined) {
|
|
515
520
|
if (tui) { tui.openLogin(); return "continue"; }
|
|
516
521
|
const C = app.io.palette;
|
|
517
|
-
const providers = [...new Set(app.catalog.map((m) => m.provider))];
|
|
518
|
-
app.io.say(C.bold("\
|
|
522
|
+
const providers = [...new Set(app.catalog.map((m) => m.provider))].filter(provider => !subscriptionById(provider));
|
|
523
|
+
app.io.say(C.bold("\nproviders"));
|
|
519
524
|
app.io.say(C.bold(" API keys"));
|
|
520
525
|
for (const p of providers) {
|
|
521
526
|
app.io.say(` ${p.padEnd(12)}${C.dim(envLabel(p))}`);
|
|
@@ -524,13 +529,15 @@ async function execute(app: App, command: Command, tui: Ui | null): Promise<Outc
|
|
|
524
529
|
for (const s of SUBSCRIPTION_OPTIONS) {
|
|
525
530
|
app.io.say(` ${s.id.padEnd(16)}${C.dim(s.label)}`);
|
|
526
531
|
}
|
|
527
|
-
app.io.say(C.dim("\n /
|
|
528
|
-
app.io.say(C.dim(
|
|
532
|
+
app.io.say(C.dim("\n /provider <provider> then paste the key. Empty line cancels."));
|
|
533
|
+
app.io.say(C.dim(` /provider ${SUBSCRIPTION_OPTIONS.map(option => option.id).join("|")} starts an account login.\n`));
|
|
529
534
|
return "continue";
|
|
530
535
|
}
|
|
531
|
-
|
|
532
|
-
|
|
536
|
+
const subscription = subscriptionById(command.provider);
|
|
537
|
+
if (subscription) {
|
|
538
|
+
return startSubscriptionLogin(app, subscription.id, tui);
|
|
533
539
|
}
|
|
540
|
+
if (!app.catalog.some(model => model.provider === command.provider)) { app.io.say(app.io.palette.red(` unknown provider: ${command.provider}. Use /provider to choose one.`)); return "continue"; }
|
|
534
541
|
if (tui) {
|
|
535
542
|
const provider = command.provider;
|
|
536
543
|
const key = await tui.askSecret(`API key · ${provider}`, "Key", "paste the key · $VAR reads the environment · !cmd runs a command · enter save · esc cancel");
|
|
@@ -703,12 +710,21 @@ function printUsage(app: App): void {
|
|
|
703
710
|
app.io.say("");
|
|
704
711
|
}
|
|
705
712
|
|
|
706
|
-
function printState(app: App): void {
|
|
713
|
+
async function printState(app: App): Promise<void> {
|
|
707
714
|
const C = app.io.palette;
|
|
708
715
|
const s = app.session.state;
|
|
709
716
|
app.io.say(C.bold("\nstate"));
|
|
710
717
|
app.io.say(` node ${s.node} (depth ${s.depth})`);
|
|
711
718
|
app.io.say(` events in node ${s.events.length}`);
|
|
719
|
+
const request = await app.session.requestState();
|
|
720
|
+
if (request) {
|
|
721
|
+
app.io.say(` request ${request.request.event} · ${request.status}`);
|
|
722
|
+
app.io.say(` execution ${request.execution?.reason ?? "in progress or unknown"}`);
|
|
723
|
+
app.io.say(` command evidence ${request.commands.filter(command => command.status === "pass").length} current passes / ${request.commands.length} receipts`);
|
|
724
|
+
app.io.say(` observations ${request.progress.observations.length} new; artifact changes ${request.progress.artifact_changes.length}; acceptance advances ${request.progress.acceptance_advances.length}`);
|
|
725
|
+
app.io.say(` diagnostics ${request.progress.new_diagnostics.length} new; unchanged retries ${request.progress.unchanged_retries.length}; unknown ${request.progress.unknown_diagnostics.length}`);
|
|
726
|
+
if (!request.acceptance) app.io.say(" task verification no declared acceptance contract; command success alone does not verify the task");
|
|
727
|
+
}
|
|
712
728
|
if (s.handoff) {
|
|
713
729
|
const h = s.handoff;
|
|
714
730
|
app.io.say(` completed ${h.state.completed.length}`);
|
|
@@ -1244,7 +1260,7 @@ function providerOAuthOpts(app: App, spec: ModelSpec): { oauth: true } | undefin
|
|
|
1244
1260
|
}
|
|
1245
1261
|
|
|
1246
1262
|
function makeProvider(app: App, config: ProviderConfig, key: string) {
|
|
1247
|
-
return createProvider(config, key, retryHandler(app.io), { ...providerOAuthOpts(app, config.spec), effects: app.effects });
|
|
1263
|
+
return createProvider(config, key, retryHandler(app.io), { sessionId: providerSessionId(app.session.config.logDir), ...providerOAuthOpts(app, config.spec), effects: app.effects });
|
|
1248
1264
|
}
|
|
1249
1265
|
|
|
1250
1266
|
interface PendingOAuth {
|
|
@@ -1361,19 +1377,20 @@ async function startSubscriptionLogin(app: App, id: string, tui: Ui | null): Pro
|
|
|
1361
1377
|
await cancelPendingOAuth(app);
|
|
1362
1378
|
const effects = tui?.oauthEffects ?? loginEffects;
|
|
1363
1379
|
app.io.say(C.yellow(` ${SUBSCRIPTION_NOTICE}`));
|
|
1364
|
-
if (sub.id === "github-copilot") {
|
|
1380
|
+
if (sub.id === "github-copilot" || sub.id === "grok") {
|
|
1365
1381
|
const pending: { abort: AbortController; tui: Ui | null; close?: () => void } = { abort: new AbortController(), tui };
|
|
1366
1382
|
pendingDevices.set(app, pending);
|
|
1367
1383
|
const work = async () => { try {
|
|
1368
|
-
const credential = await effects.device({
|
|
1384
|
+
const credential = await (sub.id === "grok" ? loginGrok : effects.device)({
|
|
1369
1385
|
notify: (message) => {
|
|
1370
1386
|
if (pendingDevices.get(app) !== pending) return;
|
|
1371
|
-
if (tui) { pending.close?.(); pending.close = tui.waiting(
|
|
1387
|
+
if (tui) { pending.close?.(); pending.close = tui.waiting(sub.label, message.split("\n"), () => { void cancelPendingOAuth(app); }); }
|
|
1372
1388
|
else app.io.say(` ${message}`);
|
|
1373
1389
|
},
|
|
1374
1390
|
prompt: async () => "",
|
|
1375
1391
|
fetch: effects.fetch,
|
|
1376
1392
|
signal: pending.abort.signal,
|
|
1393
|
+
openBrowser: effects.openBrowser,
|
|
1377
1394
|
});
|
|
1378
1395
|
if (pending.abort.signal.aborted || pendingDevices.get(app) !== pending) return;
|
|
1379
1396
|
await storeOAuth(app, sub.provider, credential);
|
|
@@ -1387,7 +1404,7 @@ async function startSubscriptionLogin(app: App, id: string, tui: Ui | null): Pro
|
|
|
1387
1404
|
pending.close?.();
|
|
1388
1405
|
}
|
|
1389
1406
|
} };
|
|
1390
|
-
if (tui) { pending.close = tui.waiting(
|
|
1407
|
+
if (tui) { pending.close = tui.waiting(sub.label, ["Requesting a device code…"], () => { void cancelPendingOAuth(app); }); tui.runExtensionAction(work); return "continue"; }
|
|
1391
1408
|
await work();
|
|
1392
1409
|
return "continue";
|
|
1393
1410
|
}
|
|
@@ -1515,7 +1532,7 @@ export async function switchToModel(app: App, spec: ModelSpec, thinking?: Effort
|
|
|
1515
1532
|
const got = app.credentialOf(spec);
|
|
1516
1533
|
if (!got.ok) {
|
|
1517
1534
|
app.io.say(C.red(`\n ${got.reason}\n`));
|
|
1518
|
-
if (!got.reason.includes("/
|
|
1535
|
+
if (!got.reason.includes("/provider")) {
|
|
1519
1536
|
app.io.say(C.dim(` ${describeMissingCredential(spec.provider)}\n`));
|
|
1520
1537
|
}
|
|
1521
1538
|
return;
|
|
@@ -1750,6 +1767,7 @@ async function run(app: App, prompt: string, signal?: AbortSignal, tui?: Ui, ima
|
|
|
1750
1767
|
if (tui) tui.beginBash(prompt.slice(excluded ? 2 : 1));
|
|
1751
1768
|
const result = await app.session.runUserBash(prompt.slice(excluded ? 2 : 1), excluded, signal,
|
|
1752
1769
|
tui ? chunk => tui.pushBash(chunk) : undefined);
|
|
1770
|
+
tui?.observeEvent?.({ kind: "done", outcome: result.outcome });
|
|
1753
1771
|
if (tui) { await tui.syncTranscript(); tui.commitStream(); }
|
|
1754
1772
|
else app.io.say(`$ ${result.command}\n${result.content}`);
|
|
1755
1773
|
return true;
|
|
@@ -1760,7 +1778,7 @@ async function run(app: App, prompt: string, signal?: AbortSignal, tui?: Ui, ima
|
|
|
1760
1778
|
const C = app.io.palette;
|
|
1761
1779
|
app.io.say(C.yellow(" no credential yet -- this prompt was not sent."));
|
|
1762
1780
|
app.io.say(C.dim(` ${app.credential.reason}`));
|
|
1763
|
-
app.io.say(C.dim(" export an API key, or /
|
|
1781
|
+
app.io.say(C.dim(" export an API key, or /provider <provider>.\n"));
|
|
1764
1782
|
tui?.restoreDraft(prompt, images);
|
|
1765
1783
|
return false;
|
|
1766
1784
|
}
|
|
@@ -1855,7 +1873,8 @@ export async function runTurn(
|
|
|
1855
1873
|
}
|
|
1856
1874
|
await registry?.emit("agent_start");
|
|
1857
1875
|
await registry?.emit("turn_start", { turnIndex: 0 });
|
|
1858
|
-
try { for await (const e of skill ? app.session.sendSkill(skill, prompt, signal) : app.session.send(prompt, signal, pull)) {
|
|
1876
|
+
try { for await (const e of skill ? app.session.sendSkill(skill, prompt, signal) : sessionControl(app.session).send(prompt, signal, pull)) {
|
|
1877
|
+
tui?.observeEvent?.(e);
|
|
1859
1878
|
if (e.kind === "text_delta" || e.kind === "thinking_delta") await registry?.emit("message_update", { assistantMessageEvent: { type: e.kind, delta: e.text } });
|
|
1860
1879
|
if (e.kind === "assistant_text") await registry?.emit("message_end", { message: { role: "assistant", content: [{ type: "text", text: e.text }] } });
|
|
1861
1880
|
if (e.kind === "done") await registry?.emit("turn_end", { reason: e.reason });
|
package/src/apps/startup.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
13
13
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { Session, systemClock } from "../runtime/loop.ts";
|
|
15
15
|
import { DEFAULT_PROVIDER, type ProviderConfig } from "../runtime/provider.ts";
|
|
16
|
-
import { createProvider, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
16
|
+
import { createProvider, providerSessionId, selectModel, loadCatalog, groupedModels, parseUserCatalog, type CatalogFetch } from "../runtime/catalog.ts";
|
|
17
17
|
import { ExtensionRegistry } from "../runtime/extensions.ts";
|
|
18
18
|
import { DISCOVERY_TOOL_NAME } from "../runtime/tool-discovery.ts";
|
|
19
19
|
import { declaredToolNames } from "../core/tool-discovery.ts";
|
|
@@ -224,6 +224,8 @@ export interface Args {
|
|
|
224
224
|
sessionName?: string;
|
|
225
225
|
/** `--mode json`. Empty / omitted when the flag was not passed. */
|
|
226
226
|
mode?: string;
|
|
227
|
+
/** Explicit local adapter endpoint. An owned mode-0700 directory is required. */
|
|
228
|
+
controlSocket?: string;
|
|
227
229
|
/** `--verbose`. Expands the startup header's counts into lists. */
|
|
228
230
|
verbose?: boolean;
|
|
229
231
|
}
|
|
@@ -280,6 +282,7 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
280
282
|
"--no-prompt-templates",
|
|
281
283
|
"--extension", "-e", "--no-extensions", "-ne", "--extension-flag",
|
|
282
284
|
"--mode",
|
|
285
|
+
"--control-socket",
|
|
283
286
|
"--verbose",
|
|
284
287
|
"--print", "-p",
|
|
285
288
|
"--help", "-h",
|
|
@@ -451,6 +454,11 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
451
454
|
(args.extensionFlags ??= Object.create(null) as Record<string, string | boolean>)[value.slice(0, at)] = value.slice(at + 1) === "true" ? true : value.slice(at + 1) === "false" ? false : value.slice(at + 1);
|
|
452
455
|
}
|
|
453
456
|
else if (a === "--mode") args.mode = argv[++i] ?? "";
|
|
457
|
+
else if (a === "--control-socket") {
|
|
458
|
+
const value = argv[++i];
|
|
459
|
+
if (!value || value.startsWith("-")) throw Error("--control-socket requires a path");
|
|
460
|
+
args.controlSocket = resolve(value);
|
|
461
|
+
}
|
|
454
462
|
else if (a === "--verbose") args.verbose = true;
|
|
455
463
|
else if (a === "-p" || a === "--print") {
|
|
456
464
|
args.print = true;
|
|
@@ -713,7 +721,7 @@ export interface App {
|
|
|
713
721
|
blobDir: string;
|
|
714
722
|
system: string;
|
|
715
723
|
providerConfig: ProviderConfig;
|
|
716
|
-
/** May be `{ ok: false }` in an interactive session waiting on /
|
|
724
|
+
/** May be `{ ok: false }` in an interactive session waiting on /provider. */
|
|
717
725
|
credential: Resolved;
|
|
718
726
|
auth: AuthFile;
|
|
719
727
|
effects: ResolveEffects;
|
|
@@ -773,8 +781,8 @@ export function whyNot(spec: ModelSpec, got: Resolved, auth: AuthFile, C: Palett
|
|
|
773
781
|
}
|
|
774
782
|
|
|
775
783
|
/**
|
|
776
|
-
* /
|
|
777
|
-
* cannot answer it, so those runs must still refuse -- naming /
|
|
784
|
+
* /provider only exists inside a live session. A one-shot prompt and a pipe
|
|
785
|
+
* cannot answer it, so those runs must still refuse -- naming /provider there
|
|
778
786
|
* is how this file used to deadlock a first run.
|
|
779
787
|
*/
|
|
780
788
|
/**
|
|
@@ -792,7 +800,7 @@ export function isInteractiveSession(args: Args): boolean {
|
|
|
792
800
|
|
|
793
801
|
/**
|
|
794
802
|
* Script-facing missing-key text. Environment variables and --api-key are
|
|
795
|
-
* the routes that actually work without a tty; /
|
|
803
|
+
* the routes that actually work without a tty; /provider is not one of them.
|
|
796
804
|
*/
|
|
797
805
|
export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CATALOG): string {
|
|
798
806
|
const providers = [...new Set(catalog.map((m) => m.provider))];
|
|
@@ -809,7 +817,7 @@ export function describeUnauthenticatedScript(catalog: readonly ModelSpec[] = CA
|
|
|
809
817
|
* action that works from here; the interactive routes are named instead.
|
|
810
818
|
*/
|
|
811
819
|
export function describeUnauthenticatedSession(_catalog: readonly ModelSpec[] = CATALOG): string {
|
|
812
|
-
return "no provider connected yet. Type /
|
|
820
|
+
return "no provider connected yet. Type /provider to connect one — a Claude, ChatGPT, Copilot or Grok account, an API key, a gateway or a local server — " +
|
|
813
821
|
`or export a key such as ${["anthropic", "openai", "google", "deepseek"].map(provider => envLabel(provider).split(" or ")[0]).join(", ")}.`;
|
|
814
822
|
}
|
|
815
823
|
|
|
@@ -933,13 +941,13 @@ export function createSessionFor(opts: {
|
|
|
933
941
|
: {}),
|
|
934
942
|
},
|
|
935
943
|
// Empty key is a placeholder the shell must not send through. Interactive
|
|
936
|
-
// startup reaches this path so /
|
|
944
|
+
// startup reaches this path so /provider exists; run() refuses until a real
|
|
937
945
|
// credential is installed on the session.
|
|
938
946
|
createProvider(
|
|
939
947
|
providerConfig,
|
|
940
948
|
credential.ok ? credential.key : "",
|
|
941
949
|
retryHandler(io),
|
|
942
|
-
{ ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
950
|
+
{ sessionId: providerSessionId(logDir), ...(opts.oauth === true ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) },
|
|
943
951
|
),
|
|
944
952
|
systemClock(),
|
|
945
953
|
);
|
|
@@ -1095,7 +1103,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1095
1103
|
}
|
|
1096
1104
|
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1097
1105
|
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
1098
|
-
{ ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1106
|
+
{ sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
|
|
1099
1107
|
opts.io.say(opts.io.palette.yellow(` switched ${restoring.id} → ${spec.id} at a model checkpoint`));
|
|
1100
1108
|
}
|
|
1101
1109
|
resources.ui.settings = { ...resources.ui.settings, thinking: providerConfig.effort,
|
|
@@ -1445,6 +1453,7 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1445
1453
|
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1446
1454
|
" [--tool-profile coding|readonly|research|data|full]\n" +
|
|
1447
1455
|
" [--acceptance-file contract.json]\n" +
|
|
1456
|
+
" [--control-socket /private/directory/control.sock]\n" +
|
|
1448
1457
|
" [--no-builtin-tools]\n" +
|
|
1449
1458
|
" [--allow-tools names] [--approve|--no-approve]\n" +
|
|
1450
1459
|
" [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
|
|
@@ -1453,6 +1462,10 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1453
1462
|
" [--extension <path>] [--no-extensions] [--extension-flag name=value]\n" +
|
|
1454
1463
|
" [--mode text|json] [--tui-mode regular|fullscreen] [--verbose]\n" +
|
|
1455
1464
|
" [-p <prompt>] [prompt...]\n\n" +
|
|
1465
|
+
" xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
|
|
1466
|
+
" xeno app-server schema prints its versioned request schema without startup.\n" +
|
|
1467
|
+
" --control-socket attaches local adapters to the same live session as the TUI.\n" +
|
|
1468
|
+
" /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
|
|
1456
1469
|
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
1457
1470
|
" -c continues the newest (or starts one); -r opens the session picker.\n" +
|
|
1458
1471
|
" --session / -r <id> pick one (unique prefix only). Resume restores its model;\n" +
|