@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +21 -9
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +1 -1
  5. package/docs/providers.md +16 -6
  6. package/package.json +3 -2
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/cli.ts +25 -10
  9. package/src/apps/commands.ts +10 -6
  10. package/src/apps/completions.ts +15 -13
  11. package/src/apps/control-transport.ts +89 -0
  12. package/src/apps/model-catalog.ts +2 -1
  13. package/src/apps/provider-connect.ts +4 -4
  14. package/src/apps/shell.ts +39 -20
  15. package/src/apps/startup.ts +22 -9
  16. package/src/apps/ui/app.ts +196 -26
  17. package/src/apps/ui/control-model.ts +26 -0
  18. package/src/apps/ui/surface.ts +2 -0
  19. package/src/core/completion-action.ts +20 -0
  20. package/src/core/completion-review.ts +26 -26
  21. package/src/core/control-protocol.ts +62 -0
  22. package/src/core/control-view.ts +42 -0
  23. package/src/core/event-validation.ts +1 -1
  24. package/src/core/events.ts +2 -0
  25. package/src/core/harness-diagnostics.ts +206 -0
  26. package/src/core/oauth-fields.ts +44 -0
  27. package/src/core/program-status.ts +31 -0
  28. package/src/core/project.ts +25 -1
  29. package/src/core/request-progress.ts +87 -0
  30. package/src/core/request-state.ts +114 -0
  31. package/src/core/settings.ts +7 -0
  32. package/src/core/tool-input.ts +6 -0
  33. package/src/runtime/auth.ts +10 -3
  34. package/src/runtime/catalog.ts +13 -3
  35. package/src/runtime/control-connection.ts +104 -0
  36. package/src/runtime/harness-eval.ts +3 -1
  37. package/src/runtime/loop.ts +72 -19
  38. package/src/runtime/model.ts +9 -0
  39. package/src/runtime/native-eval.ts +6 -3
  40. package/src/runtime/oauth.ts +80 -3
  41. package/src/runtime/pi-provider.ts +169 -12
  42. package/src/runtime/program-status.ts +38 -0
  43. package/src/runtime/provider-connect.ts +2 -2
  44. package/src/runtime/provider-presets.ts +1 -1
  45. package/src/runtime/provider-request.ts +13 -2
  46. package/src/runtime/provider.ts +23 -9
  47. package/src/runtime/session-control.ts +198 -0
  48. package/src/tui/ui/composer.ts +3 -2
  49. package/src/tui/ui/control-panel.ts +23 -0
  50. package/src/tui/ui/mascot.ts +1 -1
  51. package/src/tui/ui/status.ts +8 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Custom endpoints and connected-provider management, reached from `/login`.
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 `/login`. API-key and subscription sign-in stay on the
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 /login first."], onDone: () => host.close() });
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 /login) that is not a preset. */
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, isSubscriptionLogin, subscriptionById,
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 · /login connects a provider · /model switches · /help`;
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 `/login <provider>`; the next line typed is the key, not a prompt. */
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("\nlogin"));
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 /login <provider> then paste the key. Empty line cancels."));
528
- app.io.say(C.dim(" /login claude-pro|codex|github-copilot starts a subscription login.\n"));
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
- if (isSubscriptionLogin(command.provider)) {
532
- return startSubscriptionLogin(app, command.provider, tui);
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("GitHub Copilot", message.split("\n"), () => { void cancelPendingOAuth(app); }); }
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("GitHub Copilot", ["Requesting a device code…"], () => { void cancelPendingOAuth(app); }); tui.runExtensionAction(work); return "continue"; }
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("/login")) {
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 /login <provider>.\n"));
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 });
@@ -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 /login. */
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
- * /login only exists inside a live session. A one-shot prompt and a pipe
777
- * cannot answer it, so those runs must still refuse -- naming /login there
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; /login is not one of them.
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 /login to connect one — a Claude, ChatGPT or Copilot subscription, an API key, a gateway or a local server — " +
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 /login exists; run() refuses until a real
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" +