@f5-sales-demo/xcsh 21.2.0 → 21.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/package.json +8 -8
- package/src/browser/headless-bridge.ts +103 -71
- package/src/commands/worker.ts +94 -196
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/model-selector.ts +58 -11
- package/src/modes/controllers/login-model.ts +14 -10
- package/src/modes/controllers/selector-controller.ts +1 -3
- package/src/routing/subscription-profiles.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "21.2.
|
|
4
|
+
"version": "21.2.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
64
|
-
"@f5-sales-demo/pi-agent-core": "21.2.
|
|
65
|
-
"@f5-sales-demo/pi-ai": "21.2.
|
|
66
|
-
"@f5-sales-demo/pi-natives": "21.2.
|
|
67
|
-
"@f5-sales-demo/pi-resource-management": "21.2.
|
|
68
|
-
"@f5-sales-demo/pi-tui": "21.2.
|
|
69
|
-
"@f5-sales-demo/pi-utils": "21.2.
|
|
70
|
-
"@f5-sales-demo/xcsh-stats": "21.2.
|
|
64
|
+
"@f5-sales-demo/pi-agent-core": "21.2.1",
|
|
65
|
+
"@f5-sales-demo/pi-ai": "21.2.1",
|
|
66
|
+
"@f5-sales-demo/pi-natives": "21.2.1",
|
|
67
|
+
"@f5-sales-demo/pi-resource-management": "21.2.1",
|
|
68
|
+
"@f5-sales-demo/pi-tui": "21.2.1",
|
|
69
|
+
"@f5-sales-demo/pi-utils": "21.2.1",
|
|
70
|
+
"@f5-sales-demo/xcsh-stats": "21.2.1",
|
|
71
71
|
"@mozilla/readability": "^0.6",
|
|
72
72
|
"@sinclair/typebox": "^0.34",
|
|
73
73
|
"@xterm/headless": "^6.0",
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* create ONE agent session scoped to the browser tools, and attach the
|
|
9
|
-
* `ChatHandler`. Office document tools (Excel/Word/PPT) are advertised by the
|
|
10
|
-
* pane at runtime over the bridge (`set_host_tools`), so they need no scoping here.
|
|
2
|
+
* Shared no-TUI extension-bridge bootstrap for `xcsh office serve` and `xcsh
|
|
3
|
+
* worker`: initialize the browser-provider environment, Settings, Context, and
|
|
4
|
+
* discovery; resolve TLS; bind and publish the bridge; create one profile-scoped
|
|
5
|
+
* agent session; and attach its ChatHandler. Worker fleet behavior stays in
|
|
6
|
+
* `commands/worker.ts` and is connected through the lifecycle hooks below.
|
|
7
|
+
* Office document tools are advertised by the pane at runtime over the bridge.
|
|
11
8
|
*
|
|
12
9
|
* The heavy / socket / network calls are injected (defaulting to the real ones)
|
|
13
10
|
* so the wiring is unit-testable without opening real listeners or a session.
|
|
14
11
|
*
|
|
15
12
|
* NOT browser-safe (node/bun): runs inside the full xcsh binary, never the pane.
|
|
16
13
|
*/
|
|
17
|
-
import { getProjectDir, getXCSHConfigDir } from "@f5-sales-demo/pi-utils";
|
|
14
|
+
import { getProjectDir, getXCSHConfigDir, logger } from "@f5-sales-demo/pi-utils";
|
|
18
15
|
import { parseModelString } from "../config/model-resolver";
|
|
19
16
|
import { DEFAULT_MODEL_ROLE } from "../config/settings-schema";
|
|
20
17
|
import { createAgentSession } from "../sdk";
|
|
@@ -24,8 +21,13 @@ import { SessionManager } from "../session/session-manager";
|
|
|
24
21
|
import { resolveBridgeTls } from "./bridge-cert";
|
|
25
22
|
import { ChatHandler } from "./chat-handler";
|
|
26
23
|
import { isPickPath, type PathPicked } from "./chat-protocol";
|
|
27
|
-
import { type BridgeServer, OFFICE_PORT_RANGE, startBridgeServer } from "./extension-bridge";
|
|
28
|
-
import {
|
|
24
|
+
import { type BridgeServer, type BridgeSessionInfo, OFFICE_PORT_RANGE, startBridgeServer } from "./extension-bridge";
|
|
25
|
+
import {
|
|
26
|
+
BROWSER_TOOL_NAMES,
|
|
27
|
+
createExtensionBridgeTools,
|
|
28
|
+
EXTENSION_AGENT_TOOL_NAMES,
|
|
29
|
+
OFFICE_TOOL_NAMES,
|
|
30
|
+
} from "./extension-bridge-tools";
|
|
29
31
|
import { pickPathNative } from "./native-picker";
|
|
30
32
|
import { setSharedBridgeServer } from "./provider";
|
|
31
33
|
|
|
@@ -33,9 +35,34 @@ import { setSharedBridgeServer } from "./provider";
|
|
|
33
35
|
* closes the bridge (both ws + wss listeners). */
|
|
34
36
|
export interface HeadlessChatBridge {
|
|
35
37
|
bridge: BridgeServer;
|
|
38
|
+
handler: ChatHandler;
|
|
36
39
|
dispose: () => Promise<void>;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
type Session = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
43
|
+
|
|
44
|
+
export interface HeadlessBridgeLifecycleContext {
|
|
45
|
+
bridge: BridgeServer;
|
|
46
|
+
cwd: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HeadlessBridgeSessionContext extends HeadlessBridgeLifecycleContext {
|
|
50
|
+
session: Session;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type HeadlessBridgeOptions =
|
|
54
|
+
| {
|
|
55
|
+
kind?: "office";
|
|
56
|
+
afterBridgeBind?: (context: HeadlessBridgeLifecycleContext) => void | Promise<void>;
|
|
57
|
+
afterSessionCreate?: (context: HeadlessBridgeSessionContext) => void | Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
kind: "worker";
|
|
61
|
+
sessionInfo: () => BridgeSessionInfo;
|
|
62
|
+
afterBridgeBind?: (context: HeadlessBridgeLifecycleContext) => void | Promise<void>;
|
|
63
|
+
afterSessionCreate?: (context: HeadlessBridgeSessionContext) => void | Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
|
|
39
66
|
/**
|
|
40
67
|
* Tenant identity for the `hello` handshake, contextless-friendly: the active
|
|
41
68
|
* `/context` wins (its apiUrl + name), otherwise fall back to the `XCSH_API_URL`/
|
|
@@ -113,20 +140,25 @@ const defaultDeps: HeadlessBridgeDeps = {
|
|
|
113
140
|
* can connect and chat immediately (no warm-up race). A `configure`-less pane
|
|
114
141
|
* chats over xcsh's already-configured provider.
|
|
115
142
|
*/
|
|
116
|
-
export async function startHeadlessChatBridge(
|
|
143
|
+
export async function startHeadlessChatBridge(
|
|
144
|
+
deps: HeadlessBridgeDeps = defaultDeps,
|
|
145
|
+
options: HeadlessBridgeOptions = { kind: "office" },
|
|
146
|
+
): Promise<HeadlessChatBridge> {
|
|
117
147
|
const { cwd } = await deps.initEnv();
|
|
148
|
+
const kind = options.kind ?? "office";
|
|
118
149
|
|
|
119
150
|
// Provision the wss cert before binding (warm boot = on-disk cache hit);
|
|
120
151
|
// `undefined` (offline) → the bridge starts ws-only.
|
|
121
152
|
const tls = await deps.resolveBridgeTls();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
153
|
+
const start = () =>
|
|
154
|
+
deps.startBridgeServer(undefined, {
|
|
155
|
+
serveKind: kind === "worker" ? "browser" : "office",
|
|
156
|
+
sessionInfo: options.kind === "worker" ? options.sessionInfo : sessionInfoForOfficeServe,
|
|
157
|
+
...(tls ? { tls } : {}),
|
|
158
|
+
...(kind === "office" ? { range: OFFICE_PORT_RANGE } : {}),
|
|
159
|
+
});
|
|
160
|
+
const bridge = kind === "worker" ? await logger.time("session:bridgeListen", start) : await start();
|
|
161
|
+
|
|
130
162
|
// Reuse this bridge for any in-process selectProvider() (no conflicting second bridge).
|
|
131
163
|
deps.setSharedBridgeServer(bridge);
|
|
132
164
|
// Re-announce the tenant when the active context changes (best-effort).
|
|
@@ -136,72 +168,72 @@ export async function startHeadlessChatBridge(deps: HeadlessBridgeDeps = default
|
|
|
136
168
|
/* ContextService not initialized (tests) — the tenant is static. */
|
|
137
169
|
}
|
|
138
170
|
|
|
139
|
-
|
|
140
|
-
// provider, etc.). If it does, close the already-bound bridge and clear the
|
|
141
|
-
// shared-bridge global before rethrowing — otherwise the ws/wss listeners leak
|
|
142
|
-
// (keeping the event loop alive so Ctrl+C can't exit) and a later in-process
|
|
143
|
-
// selectProvider() reuses a dead bridge. The caller (startOfficeServe) treats
|
|
144
|
-
// the rethrow as a non-fatal "pane only" fallback.
|
|
171
|
+
let chatHandler: ChatHandler | undefined;
|
|
145
172
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
await options.afterBridgeBind?.({ bridge, cwd });
|
|
174
|
+
|
|
175
|
+
let session: Session;
|
|
176
|
+
if (kind === "worker") {
|
|
177
|
+
const create = () =>
|
|
178
|
+
deps.createAgentSession({
|
|
179
|
+
cwd,
|
|
180
|
+
hasUI: false,
|
|
181
|
+
toolNames: [...new Set([...BROWSER_TOOL_NAMES, ...EXTENSION_AGENT_TOOL_NAMES])],
|
|
182
|
+
customTools: createExtensionBridgeTools(bridge),
|
|
183
|
+
enableMCP: false,
|
|
184
|
+
enableLsp: false,
|
|
185
|
+
disableExtensionDiscovery: true,
|
|
186
|
+
...(process.env.XCSH_BENCH_EXTENSION
|
|
187
|
+
? { additionalExtensionPaths: [process.env.XCSH_BENCH_EXTENSION] }
|
|
188
|
+
: {}),
|
|
189
|
+
});
|
|
190
|
+
({ session } = await logger.time("session:createAgentSession", create));
|
|
191
|
+
} else {
|
|
192
|
+
const officeDefault = parseModelString(DEFAULT_MODEL_ROLE);
|
|
193
|
+
if (!officeDefault) {
|
|
194
|
+
throw new Error("Invalid baked Office default model selector");
|
|
195
|
+
}
|
|
196
|
+
({ session } = await deps.createAgentSession({
|
|
197
|
+
cwd,
|
|
198
|
+
hasUI: false,
|
|
199
|
+
modelPattern: DEFAULT_MODEL_ROLE,
|
|
200
|
+
thinkingLevel: officeDefault.thinkingLevel,
|
|
201
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
202
|
+
toolNames: [...OFFICE_TOOL_NAMES],
|
|
203
|
+
customTools: [],
|
|
204
|
+
enableMCP: false,
|
|
205
|
+
enableLsp: false,
|
|
206
|
+
disableExtensionDiscovery: true,
|
|
207
|
+
bundledExtensions: ["sandbox-guard"],
|
|
208
|
+
}));
|
|
149
209
|
}
|
|
150
|
-
// Create ONE headless Office session with the full CLI-parity builtin set
|
|
151
|
-
// (OFFICE_TOOL_NAMES: bash/read/write/edit/grep/inspect_image/… — NO browser tools, which
|
|
152
|
-
// would be hallucinated in a document task pane). The document's own tools
|
|
153
|
-
// (Excel/Word/PowerPoint) arrive at runtime via set_host_tools.
|
|
154
|
-
const { session } = await deps.createAgentSession({
|
|
155
|
-
cwd,
|
|
156
|
-
hasUI: false,
|
|
157
|
-
// Office must open on the production GPT profile regardless of a stale
|
|
158
|
-
// global/default role or resumable session. A saved pane model is applied
|
|
159
|
-
// afterward by its explicit configure frame and remains authoritative.
|
|
160
|
-
modelPattern: DEFAULT_MODEL_ROLE,
|
|
161
|
-
thinkingLevel: officeDefault.thinkingLevel,
|
|
162
|
-
// Office conversations can contain private workbook and working-directory
|
|
163
|
-
// data. Keep the entire headless session ephemeral instead of inheriting
|
|
164
|
-
// createAgentSession's file-backed default.
|
|
165
|
-
sessionManager: SessionManager.inMemory(cwd),
|
|
166
|
-
toolNames: [...OFFICE_TOOL_NAMES],
|
|
167
|
-
customTools: [],
|
|
168
|
-
// Headless: no MCP/LSP/extension discovery — lean, no network/blocking prompts.
|
|
169
|
-
enableMCP: false,
|
|
170
|
-
enableLsp: false,
|
|
171
|
-
disableExtensionDiscovery: true,
|
|
172
|
-
// …but DO load the bundled filesystem sandbox: the pane runs full CLI-parity
|
|
173
|
-
// tools (bash/read/write), so it needs the CLI's safety net confining file
|
|
174
|
-
// tools + the shell's cwd to the launch directory subtree (sandbox.enabled
|
|
175
|
-
// defaults true). Without this, a discovery-disabled session ran ungated.
|
|
176
|
-
bundledExtensions: ["sandbox-guard"],
|
|
177
|
-
});
|
|
178
210
|
|
|
179
|
-
|
|
211
|
+
await options.afterSessionCreate?.({ bridge, session, cwd });
|
|
212
|
+
chatHandler = new deps.ChatHandlerCtor(bridge, session);
|
|
180
213
|
chatHandler.attach();
|
|
181
214
|
|
|
182
|
-
// Second bridge subscriber (the bridge fans out to all): serve `pick_path` by
|
|
183
|
-
// opening a native OS picker and replying `path_picked`. Pure — it only returns
|
|
184
|
-
// the chosen path; the sandbox grant + prompt note happen in ChatHandler when the
|
|
185
|
-
// path rides the next `chat_request.contextPaths` (kept atomic there). `disposed`
|
|
186
|
-
// guards against a superseded serve firing the picker on a closed bridge.
|
|
187
215
|
let disposed = false;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
if (kind === "office") {
|
|
217
|
+
bridge.onMessage(async msg => {
|
|
218
|
+
if (disposed || !isPickPath(msg)) return;
|
|
219
|
+
const { path, canceled, unsupported } = await deps.pickPath((msg as { mode: "file" | "folder" }).mode);
|
|
220
|
+
if (disposed) return;
|
|
221
|
+
bridge.send({ type: "path_picked", path, canceled, unsupported } satisfies PathPicked);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
194
224
|
|
|
195
225
|
return {
|
|
196
226
|
bridge,
|
|
227
|
+
handler: chatHandler,
|
|
197
228
|
dispose: async () => {
|
|
198
229
|
disposed = true;
|
|
199
|
-
chatHandler
|
|
230
|
+
chatHandler?.dispose();
|
|
200
231
|
deps.setSharedBridgeServer(null);
|
|
201
232
|
await bridge.close();
|
|
202
233
|
},
|
|
203
234
|
};
|
|
204
235
|
} catch (err) {
|
|
236
|
+
chatHandler?.dispose();
|
|
205
237
|
deps.setSharedBridgeServer(null);
|
|
206
238
|
await bridge.close();
|
|
207
239
|
throw err;
|
package/src/commands/worker.ts
CHANGED
|
@@ -15,20 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import { logger } from "@f5-sales-demo/pi-utils";
|
|
19
19
|
import { Command } from "@f5-sales-demo/pi-utils/cli";
|
|
20
|
-
import { LOCALIP_HOST
|
|
21
|
-
import {
|
|
22
|
-
import { startBridgeServer } from "../browser/extension-bridge";
|
|
23
|
-
import {
|
|
24
|
-
BROWSER_TOOL_NAMES,
|
|
25
|
-
createExtensionBridgeTools,
|
|
26
|
-
EXTENSION_AGENT_TOOL_NAMES,
|
|
27
|
-
} from "../browser/extension-bridge-tools";
|
|
28
|
-
import { setSharedBridgeServer } from "../browser/provider";
|
|
20
|
+
import { LOCALIP_HOST } from "../browser/bridge-cert";
|
|
21
|
+
import { startHeadlessChatBridge } from "../browser/headless-bridge";
|
|
29
22
|
import { coldStartSpans, type SpanFrame, sessionBuildSpan } from "../browser/ttft-spans";
|
|
30
|
-
import { initializeWithSettings } from "../discovery";
|
|
31
|
-
import { createAgentSession } from "../sdk";
|
|
32
23
|
import { activateTenantContext } from "../services/session-context-binding";
|
|
33
24
|
import { ContextService } from "../services/xcsh-context";
|
|
34
25
|
import { deriveTenantEnv } from "../services/xcsh-env";
|
|
@@ -102,197 +93,113 @@ export default class Worker extends Command {
|
|
|
102
93
|
static description = "Run a headless extension-bridge worker (no TUI); blocks until SIGTERM";
|
|
103
94
|
|
|
104
95
|
async run(): Promise<void> {
|
|
105
|
-
// Record the per-tab session-boot timeline (parity with main.ts:runRootCommand).
|
|
106
|
-
// Spans only accumulate while recording; nothing prints unless PI_TIMING is set,
|
|
107
|
-
// and each logger.time() returns its wrapped value unchanged — so a normal
|
|
108
|
-
// `xcsh worker` run is behaviorally identical to before.
|
|
109
|
-
// TTFT Phase 2: the manager stamps XCSH_TTFT_SPAWN_AT at Bun.spawn for a cold
|
|
110
|
-
// spawn; worker_boot(cold) = bridge-listening instant - spawn instant (captures
|
|
111
|
-
// fork + runtime init, which logger.startTiming below misses).
|
|
112
96
|
const spawnAtEnv = Number(process.env.XCSH_TTFT_SPAWN_AT);
|
|
113
97
|
const coldSpawn = process.env.XCSH_TTFT_COLD === "1";
|
|
114
98
|
const managerProvisionMsEnv = Number(process.env.XCSH_TTFT_PROVISION_MS);
|
|
115
99
|
logger.startTiming();
|
|
116
100
|
|
|
117
|
-
process.env.XCSH_BROWSER_PROVIDER = "extension";
|
|
118
|
-
|
|
119
|
-
const cwd = getProjectDir();
|
|
120
|
-
const { Settings, settings } = await import("../config/settings");
|
|
121
|
-
await Settings.init({ cwd });
|
|
122
|
-
|
|
123
|
-
// Init the ContextService singleton so the session-context bootstrap (Task 3)
|
|
124
|
-
// can match XCSH_SESSION_TENANT to a stored context, and so sessionInfoForWorker
|
|
125
|
-
// can read the active apiUrl once bound.
|
|
126
|
-
try {
|
|
127
|
-
ContextService.init(getXCSHConfigDir());
|
|
128
|
-
} catch {
|
|
129
|
-
/* already initialized / unavailable — continue. */
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Provider persistence for model discovery (parity with main.ts).
|
|
133
|
-
initializeWithSettings(settings);
|
|
134
|
-
|
|
135
|
-
// Quiet startup: skip the welcome screen + blocking plugin "Fix now?" prompts.
|
|
136
|
-
settings.override("startup.quiet", true);
|
|
137
|
-
|
|
138
|
-
// Provision the wss cert BEFORE the session:bridgeListen span — its only
|
|
139
|
-
// network path (a cold/stale-cache fetch) must NOT inflate the measured
|
|
140
|
-
// bridge-ready time; a warm boot is a fast on-disk cache hit. `undefined`
|
|
141
|
-
// (offline / local-ip.sh unreachable) → the bridge starts ws-only (no crash).
|
|
142
|
-
const tls = await resolveBridgeTls();
|
|
143
|
-
|
|
144
|
-
// INSTANT-ON: start the bridge before the heavy session init so the extension
|
|
145
|
-
// can connect immediately. Honors XCSH_BRIDGE_PORT (forced) or auto-selects.
|
|
146
|
-
// session:bridgeListen — time-to-"bridge-ready": the extension can connect and
|
|
147
|
-
// complete the hello/hello_ack handshake once this resolves (INSTANT-ON path).
|
|
148
|
-
const bridge = await logger.time("session:bridgeListen", () =>
|
|
149
|
-
startBridgeServer(undefined, {
|
|
150
|
-
serveKind: "browser",
|
|
151
|
-
sessionInfo: sessionInfoForWorker,
|
|
152
|
-
...(tls ? { tls } : {}),
|
|
153
|
-
}),
|
|
154
|
-
);
|
|
155
|
-
console.error(
|
|
156
|
-
`[xcsh worker] extension bridge listening on ws://127.0.0.1:${bridge.port}` +
|
|
157
|
-
(bridge.wssPort ? ` + wss://${LOCALIP_HOST}:${bridge.wssPort}` : ""),
|
|
158
|
-
);
|
|
159
|
-
if (process.connected) {
|
|
160
|
-
process.send?.({ type: "ready", sessionId: sessionInfoForWorker().sessionId });
|
|
161
|
-
}
|
|
162
|
-
setSharedBridgeServer(bridge);
|
|
163
|
-
ContextService.onContextChange(() => bridge.broadcastTenantChanged());
|
|
164
|
-
|
|
165
|
-
// TTFT Phase 2: buffer the per-session cold-start spans and flush them once a
|
|
166
|
-
// client is actually connected. bridge.send() silently no-ops with no client, so
|
|
167
|
-
// the flush is gated on `clientConnected` — a cold-boot flush before the extension
|
|
168
|
-
// connects would otherwise burn the once-latch and lose the spans. Cold path is
|
|
169
|
-
// populated now (env); warm path by the {bind} handler below.
|
|
170
101
|
let coldStartBuffer: SpanFrame[] = [];
|
|
171
102
|
let coldStartSent = false;
|
|
172
103
|
let clientConnected = false;
|
|
173
|
-
const flushColdStart = (): void => {
|
|
174
|
-
if (coldStartSent || !clientConnected || coldStartBuffer.length === 0) return;
|
|
175
|
-
for (const s of coldStartBuffer) bridge.send(s);
|
|
176
|
-
coldStartSent = true;
|
|
177
|
-
};
|
|
178
|
-
// TTFT: the session_build span (createAgentSession seam) is computed after the
|
|
179
|
-
// bridge is listening but possibly before the extension connects — buffer it and
|
|
180
|
-
// flush on connect, same as the cold-start spans.
|
|
181
104
|
let sessionBuildFrame: SpanFrame | null = null;
|
|
182
105
|
let sessionBuildSent = false;
|
|
183
|
-
|
|
184
|
-
if (sessionBuildSent || !clientConnected || !sessionBuildFrame) return;
|
|
185
|
-
bridge.send(sessionBuildFrame);
|
|
186
|
-
sessionBuildSent = true;
|
|
187
|
-
};
|
|
188
|
-
// onConnected (raw WS open) is the deliberate flush trigger: no hello hook is exposed
|
|
189
|
-
// today, and the bridge's origin check already gates opens to the extension.
|
|
190
|
-
bridge.onConnected(() => {
|
|
191
|
-
clientConnected = true;
|
|
192
|
-
flushColdStart();
|
|
193
|
-
flushSessionBuild();
|
|
194
|
-
});
|
|
106
|
+
let sessionBuildStart = 0;
|
|
195
107
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
108
|
+
const running = await startHeadlessChatBridge(undefined, {
|
|
109
|
+
kind: "worker",
|
|
110
|
+
sessionInfo: sessionInfoForWorker,
|
|
111
|
+
afterBridgeBind: ({ bridge }) => {
|
|
112
|
+
console.error(
|
|
113
|
+
`[xcsh worker] extension bridge listening on ws://127.0.0.1:${bridge.port}` +
|
|
114
|
+
(bridge.wssPort ? ` + wss://${LOCALIP_HOST}:${bridge.wssPort}` : ""),
|
|
115
|
+
);
|
|
116
|
+
if (process.connected) {
|
|
117
|
+
process.send?.({ type: "ready", sessionId: sessionInfoForWorker().sessionId });
|
|
118
|
+
}
|
|
202
119
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} catch {
|
|
225
|
-
console.error("[xcsh worker] late tenant-bind failed");
|
|
120
|
+
const flushColdStart = (): void => {
|
|
121
|
+
if (coldStartSent || !clientConnected || coldStartBuffer.length === 0) return;
|
|
122
|
+
for (const span of coldStartBuffer) bridge.send(span);
|
|
123
|
+
coldStartSent = true;
|
|
124
|
+
};
|
|
125
|
+
const flushSessionBuild = (): void => {
|
|
126
|
+
if (sessionBuildSent || !clientConnected || !sessionBuildFrame) return;
|
|
127
|
+
bridge.send(sessionBuildFrame);
|
|
128
|
+
sessionBuildSent = true;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
bridge.onConnected(() => {
|
|
132
|
+
clientConnected = true;
|
|
133
|
+
flushColdStart();
|
|
134
|
+
flushSessionBuild();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (coldSpawn && process.env.XCSH_SESSION_ID && Number.isFinite(spawnAtEnv)) {
|
|
138
|
+
const workerBootMs = Date.now() - spawnAtEnv;
|
|
139
|
+
const managerMs = Number.isFinite(managerProvisionMsEnv) ? managerProvisionMsEnv : 0;
|
|
140
|
+
coldStartBuffer = coldStartSpans(process.env.XCSH_SESSION_ID, true, managerMs, workerBootMs);
|
|
226
141
|
}
|
|
227
|
-
bridge.broadcastTenantChanged();
|
|
228
|
-
// The standalone benchmark retains IPC to measure adoption latency. A real
|
|
229
|
-
// manager disconnects after binding so the worker survives manager handoff.
|
|
230
|
-
if (process.connected) process.send?.({ type: "bound", sessionId });
|
|
231
|
-
// TTFT Phase 2: warm adopt cold-start spans (worker_boot = bind -> bound).
|
|
232
|
-
coldStartBuffer = coldStartSpans(sessionId, false, relayedProvisionMs, Date.now() - bindAt);
|
|
233
|
-
flushColdStart();
|
|
234
|
-
})();
|
|
235
|
-
});
|
|
236
142
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
flushSessionBuild();
|
|
143
|
+
process.on("message", (raw: unknown) => {
|
|
144
|
+
const message = raw as {
|
|
145
|
+
type?: unknown;
|
|
146
|
+
sessionId?: unknown;
|
|
147
|
+
tenant?: unknown;
|
|
148
|
+
provisionMs?: unknown;
|
|
149
|
+
cold?: unknown;
|
|
150
|
+
};
|
|
151
|
+
if (
|
|
152
|
+
message?.type !== "bind" ||
|
|
153
|
+
typeof message.sessionId !== "string" ||
|
|
154
|
+
typeof message.tenant !== "string"
|
|
155
|
+
)
|
|
156
|
+
return;
|
|
157
|
+
const sessionId = message.sessionId;
|
|
158
|
+
const tenant = message.tenant;
|
|
159
|
+
const bindAt = Date.now();
|
|
160
|
+
const relayedProvisionMs = typeof message.provisionMs === "number" ? message.provisionMs : 0;
|
|
161
|
+
setWorkerIdentity(sessionId, tenant);
|
|
162
|
+
void (async () => {
|
|
163
|
+
try {
|
|
164
|
+
await activateTenantContext(tenant);
|
|
165
|
+
} catch {
|
|
166
|
+
console.error("[xcsh worker] late tenant-bind failed");
|
|
167
|
+
}
|
|
168
|
+
bridge.broadcastTenantChanged();
|
|
169
|
+
if (process.connected) process.send?.({ type: "bound", sessionId });
|
|
170
|
+
coldStartBuffer = coldStartSpans(sessionId, false, relayedProvisionMs, Date.now() - bindAt);
|
|
171
|
+
flushColdStart();
|
|
172
|
+
})();
|
|
173
|
+
});
|
|
269
174
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (benchModel) {
|
|
279
|
-
await session.setModel(benchModel);
|
|
280
|
-
} else {
|
|
281
|
-
console.error(
|
|
282
|
-
"[xcsh worker] BENCH ERROR: bench-instant model not registered — benchmark would measure a real provider",
|
|
175
|
+
// The shared bootstrap creates the session immediately after this hook.
|
|
176
|
+
sessionBuildStart = Date.now();
|
|
177
|
+
},
|
|
178
|
+
afterSessionCreate: async ({ bridge, session }) => {
|
|
179
|
+
sessionBuildFrame = sessionBuildSpan(
|
|
180
|
+
process.env.XCSH_SESSION_ID ?? "",
|
|
181
|
+
coldSpawn,
|
|
182
|
+
Date.now() - sessionBuildStart,
|
|
283
183
|
);
|
|
284
|
-
|
|
285
|
-
|
|
184
|
+
if (clientConnected && !sessionBuildSent) {
|
|
185
|
+
bridge.send(sessionBuildFrame);
|
|
186
|
+
sessionBuildSent = true;
|
|
187
|
+
}
|
|
286
188
|
|
|
287
|
-
|
|
288
|
-
|
|
189
|
+
if (process.env.XCSH_BENCH_EXTENSION) {
|
|
190
|
+
const benchModel = session.modelRegistry.find("bench-instant", "bench-instant");
|
|
191
|
+
if (benchModel) {
|
|
192
|
+
await session.setModel(benchModel);
|
|
193
|
+
} else {
|
|
194
|
+
console.error(
|
|
195
|
+
"[xcsh worker] BENCH ERROR: bench-instant model not registered — benchmark would measure a real provider",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const { handler: chatHandler } = running;
|
|
289
202
|
|
|
290
|
-
// Manager keepalive: while a turn is in flight (and at each turn start), ping
|
|
291
|
-
// the manager control socket so it refreshes this worker's lastSeen and its
|
|
292
|
-
// idle sweep does not reap an actively-used session mid-conversation. Chat
|
|
293
|
-
// traffic never reaches the manager, so this is the only liveness signal.
|
|
294
|
-
// Best-effort + self-reconnecting: a dropped socket (e.g. manager supersede)
|
|
295
|
-
// is re-opened on the next emit, re-targeting the successor manager.
|
|
296
203
|
const keepalive = new ManagerKeepalive({
|
|
297
204
|
sessionId: () => sessionInfoForWorker().sessionId ?? "spare",
|
|
298
205
|
busy: () => chatHandler.busy,
|
|
@@ -312,16 +219,13 @@ export default class Worker extends Command {
|
|
|
312
219
|
};
|
|
313
220
|
return transport;
|
|
314
221
|
} catch {
|
|
315
|
-
return null;
|
|
222
|
+
return null;
|
|
316
223
|
}
|
|
317
224
|
},
|
|
318
225
|
});
|
|
319
226
|
chatHandler.onTurnStart(() => keepalive.turnStart());
|
|
320
227
|
const keepaliveTimer = setInterval(() => keepalive.tick(), KEEPALIVE_MS);
|
|
321
228
|
|
|
322
|
-
// session-ready. Emit the per-tab boot breakdown when requested (parity with
|
|
323
|
-
// main.ts:1002-1009). PI_TIMING=x prints then exits — used by bench/extension-session.ts
|
|
324
|
-
// to measure total worker cold-start; otherwise this is a no-op.
|
|
325
229
|
if (process.env.PI_TIMING) {
|
|
326
230
|
logger.printTimings();
|
|
327
231
|
if (process.env.PI_TIMING === "x") {
|
|
@@ -334,16 +238,11 @@ export default class Worker extends Command {
|
|
|
334
238
|
const teardown = () => {
|
|
335
239
|
clearInterval(keepaliveTimer);
|
|
336
240
|
keepalive.stop();
|
|
337
|
-
|
|
338
|
-
void bridge.close().finally(() => process.exit(0));
|
|
241
|
+
void running.dispose().finally(() => process.exit(0));
|
|
339
242
|
};
|
|
340
243
|
const shutdown = () => {
|
|
341
244
|
if (shuttingDown) return;
|
|
342
245
|
shuttingDown = true;
|
|
343
|
-
// Bounded drain (#1874): if a chat turn is in flight (e.g. the manager is
|
|
344
|
-
// recycling for an upgrade), let it finish before teardown instead of
|
|
345
|
-
// aborting the running agent turn — with a hard ceiling so a hung turn can
|
|
346
|
-
// never wedge shutdown. An idle worker tears down immediately.
|
|
347
246
|
if (!chatHandler.busy) return teardown();
|
|
348
247
|
const deadline = Date.now() + WORKER_DRAIN_TIMEOUT_MS;
|
|
349
248
|
const tick = () => {
|
|
@@ -355,7 +254,6 @@ export default class Worker extends Command {
|
|
|
355
254
|
process.on("SIGTERM", shutdown);
|
|
356
255
|
process.on("SIGINT", shutdown);
|
|
357
256
|
|
|
358
|
-
// Block until a signal tears us down.
|
|
359
257
|
await Promise.withResolvers<never>().promise;
|
|
360
258
|
}
|
|
361
259
|
}
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "21.2.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "21.2.1",
|
|
21
|
+
"commit": "21fcf6e73313274c68dcb1db998bd12b75326489",
|
|
22
|
+
"shortCommit": "21fcf6e",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v21.2.
|
|
25
|
-
"commitDate": "2026-08-
|
|
26
|
-
"buildDate": "2026-08-
|
|
24
|
+
"tag": "v21.2.1",
|
|
25
|
+
"commitDate": "2026-08-31T13:35:12+00:00",
|
|
26
|
+
"buildDate": "2026-08-31T14:12:39.742Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.2.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/21fcf6e73313274c68dcb1db998bd12b75326489",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.2.1"
|
|
33
33
|
};
|
|
@@ -144,7 +144,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
|
|
|
144
144
|
"en/natives/porting-to-natives.md": "---\ntitle: Porting to pi-natives (Node-API) — Field notes\ndescription: Field notes for migrating Node.js child_process and shell code to the Rust N-API native layer.\nsidebar:\n order: 9\n label: Porting to pi-natives\n---\n\nThis guide describes how to migrate performance-critical execution paths to Rust in `crates/pi-natives` and expose them through TypeScript bindings in `@f5-sales-demo/pi-natives`.\n\n## When to port to native modules\n\nPort execution paths to Rust when:\n\n- Execution paths execute within high-frequency rendering loops or large batch data pipelines.\n- JavaScript memory allocations dominate execution time (excessive string allocations, regex backtracking, large array transformations).\n- The operation is CPU-bound or performs blocking I/O that can run on libuv worker threads (`task::blocking`).\n- The operation orchestrates asynchronous I/O that benefits from the Tokio runtime (`task::future`).\n\nAvoid porting operations that require access to JavaScript runtime state or dynamic module imports. Native exports must remain pure, data-in/data-out functions.\n\n## Native export architecture\n\n### Rust implementation layer (`crates/pi-natives/`)\n\n1. Implement core functionality in `crates/pi-natives/src/<MODULE>.rs`.\n2. Register the module in `crates/pi-natives/src/lib.rs`.\n3. Annotate functions with `#[napi]`. Rust `snake_case` function names convert to JavaScript `camelCase` identifiers automatically.\n4. Use `task::blocking` for CPU-bound computation and `task::future` for asynchronous operations. Pass `CancelToken` instances when supporting timeouts or `AbortSignal`.\n\n### TypeScript binding layer (`packages/natives/`)\n\n1. Define interface augmentations in `packages/natives/src/<MODULE>/types.ts` extending `NativeBindings` via declaration merging.\n2. Import `<MODULE>/types.ts` in `packages/natives/src/native.ts` to activate type definitions.\n3. Add validation entries in `validateNative` within `packages/natives/src/native.ts` to enforce required exports at startup.\n4. Implement ergonomic wrappers in `packages/natives/src/<MODULE>/index.ts` and re-export them from `packages/natives/src/index.ts`.\n\n## Step-by-step porting workflow\n\n1. **Implement Rust logic**: Create functions with owned types (`String`, `Vec<String>`, `Uint8Array`) and wrap long loops with `ct.heartbeat()?`.\n2. **Expose TypeScript bindings**: Add types in `<MODULE>/types.ts` and export wrapper functions.\n3. **Register native validation**: Add `checkFn(\"exportName\")` to `validateNative` in `packages/natives/src/native.ts`.\n4. **Create performance benchmarks**: Benchmark JavaScript and Rust implementations side-by-side using `Bun.nanoseconds()`.\n5. **Compile native binaries**: Run `bun --cwd=packages/natives run build`.\n6. **Validate performance**: Verify that the native path outperforms JavaScript before switching production call sites.\n\n## Troubleshooting common issues\n\n### Stale native binaries\n\nThe runtime loader prioritizes platform-tagged binaries (`pi_natives.<PLATFORM>-<ARCH>.node`). If exports fail to update:\n\n```bash\nrm packages/natives/native/pi_natives.*.node\nbun --cwd=packages/natives run build\n```\n\nIf testing against pre-compiled binaries, remove the extraction cache:\n\n```bash\nrm -rf ~/.xcsh/natives/\n```\n\n### Missing export validation errors\n\nIf `validateNative` raises an error indicating missing exports:\n\n- Verify that the Rust export name matches the expected JavaScript camelCase name.\n- Verify that the binary was rebuilt after adding the `#[napi]` attribute.\n- Never disable or weaken `validateNative` checks.\n",
|
|
145
145
|
"en/office-test-ownership.md": "# Office add-in test ownership\n\nThis document defines the canonical test ownership and execution responsibilities for the `office-pane` package and Office add-in task pane integration tests.\n\n## Test suites and domain ownership\n\n| Test suite / harness | Location | Canonical owner | Execution tier | Description |\n| --- | --- | --- | --- | --- |\n| **Office Task Pane UI Unit Tests** | `packages/office-pane/src/**/*.test.ts` | Frontend / UI Core Team | Local and PR CI | Core Office.js state management and task pane rendering |\n| **Office Host Integration Harness** | `packages/office-pane/test/integration/` | Ecosystem Integrations Team | PR CI Matrix | Simulated Office.js API host binding and event handling |\n| **Acceptance End-to-End Suite** | `packages/office-pane/test/e2e/` | QA and Release Engineering | Nightly Release CI | Headless browser execution against Word and Excel task pane mocks |\n\n## Maintenance responsibilities\n\n- **Frontend / UI Core Team**: Responsible for unit test coverage on UI components in `office-pane`.\n- **Ecosystem Integrations Team**: Responsible for host mock fidelity and API contract alignment.\n- **Release Engineering**: Responsible for end-to-end environment stability and CI matrix runtimes.\n",
|
|
146
146
|
"en/providers/models.md": "---\ntitle: Model and Provider Configuration\ndescription: Model registry and provider configuration via models.yml with routing, fallback, and pricing.\nsidebar:\n order: 1\n label: Models & providers\n---\n\nThis document describes how xcsh loads model registries, applies configuration overrides, resolves API credentials, and manages model selection at runtime.\n\n## Core implementation architecture\n\nModel configuration and runtime resolution are implemented across the following modules:\n\n- `src/config/model-registry.ts`: Loads built-in and custom models, manages provider overrides, discovers local models, and integrates authentication.\n- `src/config/model-resolver.ts`: Parses model patterns, handles canonical coalescing, and selects default, small, and reasoning models.\n- `src/config/settings-schema.ts`: Defines settings schemas for `modelRoles` and transport options.\n- `src/session/auth-storage.ts`: Resolves API keys and OAuth tokens across configuration sources.\n- `packages/ai/src/models.ts` and `packages/ai/src/types.ts`: Defines built-in models and compatibility contracts.\n\n## Configuration file path\n\nDefault configuration path:\n\n- `~/.xcsh/agent/models.yml`\n\n> [!NOTE]\n> If `models.yml` is missing and a legacy `models.json` file exists in the configuration directory, xcsh automatically migrates the settings to `models.yml`.\n\n## Schema structure\n\n```yaml\nconfigVersion: 1\nproviders:\n <PROVIDER_ID>:\n baseUrl: https://api.example.com/v1\n apiKey: MY_API_KEY_ENV_VAR\n api: openai-completions\n headers:\n X-Custom-Header: value\n auth: apiKey\n discovery:\n type: ollama\n modelOverrides:\n <MODEL_ID>:\n name: Custom Model Display Name\n models:\n - id: custom-model-id\n name: Custom Model Name\n api: openai-completions\n contextWindow: 128000\n maxTokens: 16384\n cost:\n input: 0\n output: 0\n cacheRead: 0\n cacheWrite: 0\nequivalence:\n overrides:\n <PROVIDER_ID>/<MODEL_ID>: <CANONICAL_MODEL_ID>\n exclude:\n - <PROVIDER_ID>/<MODEL_ID>\n```\n\n### Supported API protocol types\n\n- `openai-completions`\n- `openai-responses`\n- `openai-codex-responses`\n- `azure-openai-responses`\n- `anthropic-messages`\n- `google-generative-ai`\n- `google-vertex`\n\n## Validation rules\n\n### Custom provider definitions (`models` defined)\n\nThe following fields are required:\n\n- `baseUrl`: Base endpoint URL.\n- `apiKey`: Required unless `auth: none` is explicitly configured.\n- `api`: Protocol type specified at the provider or model level.\n\n### Override-only providers (`models` omitted)\n\nMust specify at least one of:\n\n- `baseUrl`\n- `modelOverrides`\n- `discovery`\n\n## Merge and override hierarchy\n\nWhen initializing or refreshing the model registry, xcsh applies configuration in the following order:\n\n1. **Built-in catalog**: Loads default providers and models from `@f5-sales-demo/pi-ai`.\n2. **Custom configuration**: Parses `~/.xcsh/agent/models.yml`.\n3. **Provider overrides**: Applies custom `baseUrl` and default `headers` to built-in models.\n4. **Model overrides**: Merges custom settings from `modelOverrides`.\n5. **Custom models**: Appends or replaces model definitions matching existing `provider/id` pairs.\n6. **Runtime discovery**: Queries active local endpoints (such as Ollama or LM Studio) and registers discovered models.\n\n## Canonical model equivalence and coalescing\n\nxcsh groups equivalent model checkpoints from different providers under canonical upstream identifiers (for example, `claude-sonnet-4`, `gpt-5.3-codex`):\n\n- **Overrides**: Maps specific provider identifiers to standard canonical names via `equivalence.overrides`.\n- **Exclusions**: Removes specific variants from automatic grouping via `equivalence.exclude`.\n- **Resolution priority**: Selects concrete providers based on credential availability and `modelProviderOrder` precedence.\n\n## Runtime model discovery\n\nxcsh detects local inference runtimes automatically:\n\n- **Ollama**: Probes `http://127.0.0.1:11434/api/tags` (or `OLLAMA_BASE_URL`).\n- **LM Studio**: Probes `http://127.0.0.1:1234/v1/models` (or `LM_STUDIO_BASE_URL`).\n- **llama.cpp**: Probes `http://127.0.0.1:8080/v1/models` (or `LLAMA_CPP_BASE_URL`).\n\n## Credential resolution hierarchy\n\nWhen resolving API keys for a provider, xcsh evaluates sources in the following priority order:\n\n1. CLI flag `--api-key`\n2. Stored API keys in `agent.db`\n3. Stored OAuth tokens in `agent.db`\n4. Standard environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`)\n5. Provider `apiKey` field in `models.yml` (evaluated first as an environment variable name, then as a literal token)\n\n## Model roles and selection\n\nxcsh supports logical role aliases configured via `settings.modelRoles`:\n\n- `default`: Primary interaction model.\n- `smol`: Fast, lightweight model for metadata tasks and short tool evaluations.\n- `slow`: High-reasoning model for complex multi-step planning and analysis.\n- `plan`: Model assigned to interactive planning modes.\n- `commit`: Model assigned to git commit generation.\n\nEach role definition can include thinking intensity suffixes (for example, `pi/smol:minimal`, `claude-sonnet-4:high`).\n\n## Context promotion and fallback\n\nWhen a model context window overflows during a conversation turn (`context_length_exceeded`), xcsh automatically promotes the session to a larger-context sibling model before initiating context compaction:\n\n1. Evaluates explicit `contextPromotionTarget` configurations.\n2. Identifies the smallest available model with a larger context window on the same provider.\n3. Switches the session model temporarily and retries the turn.\n",
|
|
147
|
-
"en/providers/openai-api-access.md": "---\ntitle: OpenAI access\ndescription: Choose ChatGPT subscription or usage-based OpenAI Platform access in xcsh.\nsidebar:\n order: 4\n label: OpenAI access\n---\n\nxcsh supports two distinct access methods for OpenAI services, depending on whether you authenticate with a ChatGPT subscription or an OpenAI Platform API key.\n\n## 1. ChatGPT subscription (OAuth)\n\nTo use your ChatGPT Plus or Pro subscription, run `/login openai-codex` or select **ChatGPT Plus/Pro (Codex Subscription)** in the `/login` interactive picker. xcsh stores the OAuth credentials securely in `agent.db` and discovers advertised subscription models.\n\n### Headless and SSH remote login\n\nWhen running inside an SSH session or headless terminal, xcsh automatically initiates device-code authentication:\n\n1. xcsh displays `https://auth.openai.com/codex/device` and a one-time verification code.\n2. Open the URL in any web browser and log in with your ChatGPT account credentials.\n3. Enter the one-time verification code.\n4. The remote xcsh process detects authorization automatically and completes authentication.\n\n> [!NOTE]\n> Device-code authentication requires enabling the beta feature in ChatGPT under **Settings → Security**, or via workspace administrator settings under **Workspace settings → Permissions & roles**.\n\n### Local desktop browser callback\n\nOn local workstations with graphical desktop environments,
|
|
147
|
+
"en/providers/openai-api-access.md": "---\ntitle: OpenAI access\ndescription: Choose ChatGPT subscription or usage-based OpenAI Platform access in xcsh.\nsidebar:\n order: 4\n label: OpenAI access\n---\n\nxcsh supports two distinct access methods for OpenAI services, depending on whether you authenticate with a ChatGPT subscription or an OpenAI Platform API key.\n\n## 1. ChatGPT subscription (OAuth)\n\nTo use your ChatGPT Plus or Pro subscription, run `/login openai-codex` or select **ChatGPT Plus/Pro (Codex Subscription)** in the `/login` interactive picker. xcsh stores the OAuth credentials securely in `agent.db` and discovers advertised subscription models.\n\n### Headless and SSH remote login\n\nWhen running inside an SSH session or headless terminal, xcsh automatically initiates device-code authentication:\n\n1. xcsh displays `https://auth.openai.com/codex/device` and a one-time verification code.\n2. Open the URL in any web browser and log in with your ChatGPT account credentials.\n3. Enter the one-time verification code.\n4. The remote xcsh process detects authorization automatically and completes authentication.\n\n> [!NOTE]\n> Device-code authentication requires enabling the beta feature in ChatGPT under **Settings → Security**, or via workspace administrator settings under **Workspace settings → Permissions & roles**.\n\n### Local desktop browser callback\n\nOn local workstations with graphical desktop environments, the same `/login openai-codex` command opens your default browser and receives tokens through a local loopback listener on port 1455 (`http://localhost:1455/auth/callback`).\n\nIf device authorization is unavailable in an SSH or headless session, the same login flow offers browser/manual redirect authentication. Open the displayed URL locally, then paste the redirect URL into `/login <redirect URL>` on the remote host.\n\n## 2. OpenAI Platform API (usage-based billing)\n\nTo use pay-as-you-go OpenAI Platform access, configure the `OPENAI_API_KEY` environment variable or provide it in `models.yml`:\n\n```bash\nexport OPENAI_API_KEY=\"<OPENAI_API_KEY>\"\nxcsh\n```\n\nAfter starting xcsh, select an OpenAI model using `/model`.\n",
|
|
148
148
|
"en/providers/provider-streaming-internals.md": "---\ntitle: Provider Streaming Internals\ndescription: Provider streaming implementation with SSE parsing, token counting, and backpressure handling.\nsidebar:\n order: 2\n label: Streaming internals\n---\n\nThis document describes how token and tool call streams from diverse LLM providers are normalized in `@f5-sales-demo/pi-ai` and propagated through `@f5-sales-demo/pi-agent-core` to `coding-agent` session events.\n\n## End-to-end streaming architecture\n\n1. **Stream dispatch**: `streamSimple()` in `packages/ai/src/stream.ts` maps provider-agnostic request options and dispatches them to the selected provider driver.\n2. **Provider normalization**: Provider stream drivers (`anthropic.ts`, `openai-responses.ts`, `google.ts`) translate vendor-specific Server-Sent Events (SSE) into a unified `AssistantMessageEvent` stream.\n3. **Event throttling**: `AssistantMessageEventStream` (`packages/ai/src/utils/event-stream.ts`) buffers and coalesces rapid delta events (~50ms cadence) to smooth UI rendering.\n4. **Agent loop consumption**: `agentLoop` (`packages/agent/src/agent-loop.ts`) processes events, updates in-flight message state, and emits `message_update` events.\n5. **Session event integration**: `AgentSession` (`packages/coding-agent/src/session/agent-session.ts`) handles user aborts, automated retries, context compaction, and tool call execution guards.\n\n## Unified stream contract (`AssistantMessageEvent`)\n\nAll provider drivers emit events conforming to the `AssistantMessageEvent` union:\n\n- `start`: Initiates stream processing.\n- Content block lifecycle triplets:\n - Text: `text_start` → `text_delta`* → `text_end`\n - Thinking / reasoning: `thinking_start` → `thinking_delta`* → `thinking_end`\n - Tool invocation: `toolcall_start` → `toolcall_delta`* → `toolcall_end`\n- Terminal events:\n - `done`: Emits termination reasons (`stop`, `length`, `toolUse`).\n - `error`: Emits failure reasons (`aborted`, `error`).\n\n## Provider-specific normalization logic\n\n### Anthropic (`anthropic-messages`)\n\n- Maps `message_start` to token usage metadata.\n- Maps `content_block_start` and `content_block_delta` to unified text, thinking, and tool invocation blocks.\n- Accumulates streamed JSON fragments in `partialJson` and reparses arguments incrementally via `parseStreamingJson()`.\n\n### OpenAI Responses (`openai-responses`)\n\n- Maps `response.output_item.added` to text or reasoning blocks.\n- Maps `response.reasoning_summary_text.delta` to `thinking_delta`.\n- Translates `response.function_call_arguments.delta` into `toolcall_delta`.\n- Normalizes tool call identifiers into `<CALL_ID>|<ITEM_ID>`.\n\n### Google Generative AI (`google-generative-ai`)\n\n- Parses `candidate.content.parts`, distinguishing thinking blocks via `isThinkingPart()`.\n- Emits synthetic `toolcall_delta` containing serialized JSON strings for structured tool call events.\n\n## Tool call JSON accumulation and error recovery\n\nIncremental tool arguments are parsed via `parseStreamingJson()` (`packages/ai/src/utils/json-parse.ts`):\n\n1. Attempts standard `JSON.parse`.\n2. Falls back to partial JSON parsing to evaluate incomplete streamed fragments.\n3. If partial parsing fails, returns `{}` temporarily until subsequent deltas provide valid JSON structures.\n4. Performs a final parse pass on `toolcall_end`.\n\n## Cancellation and lifecycle boundaries\n\n- **Provider HTTP request**: `options.signal` aborts the active HTTP transport connection.\n- **Agent loop**: Evaluates `signal.aborted` prior to processing each streamed event.\n- **Session abortion**: Calling `AgentSession.abort()` propagates cancellations to active tool subprocesses.\n- **Tool execution interrupts**: Tool runners listen to `AbortSignal.any([agentSignal, steeringAbortSignal])`, allowing users to interrupt long-running tools without discarding prior turns.\n\n## Related implementation files\n\n- `packages/ai/src/stream.ts`: Provider stream dispatcher and option normalizer.\n- `packages/ai/src/utils/event-stream.ts`: Stream queueing and delta event throttling.\n- `packages/ai/src/utils/json-parse.ts`: Incremental streaming JSON parser.\n- `packages/ai/src/providers/anthropic.ts`: Anthropic SSE event transformer.\n- `packages/ai/src/providers/openai-responses.ts`: OpenAI Responses event transformer.\n- `packages/ai/src/providers/google.ts`: Google Gemini event transformer.\n- `packages/agent/src/agent-loop.ts`: Agent event processing loop.\n- `packages/coding-agent/src/session/agent-session.ts`: Session lifecycle, retry policies, and persistence.\n",
|
|
149
149
|
"en/providers/python-repl.md": "---\ntitle: Python Tool and IPython Runtime\ndescription: Python REPL tool runtime with IPython kernel management, execution, and output capture.\nsidebar:\n order: 3\n label: Python & IPython\n---\n\nThis document describes the Python execution architecture in `packages/coding-agent`, covering tool parameters, Jupyter Kernel Gateway lifecycles, environment isolation, execution semantics, and troubleshooting procedures.\n\n## Architecture overview\n\nPython execution is managed across several core modules:\n\n- `src/tools/python.ts`: Tool interface definition and interactive cell renderers.\n- `src/ipy/executor.ts`: Session-level kernel orchestration and execution scheduling.\n- `src/ipy/kernel.ts`: Kernel lifecycle management and WebSocket communication protocol.\n- `src/ipy/gateway-coordinator.ts`: Shared local Jupyter Kernel Gateway process coordinator.\n- `src/ipy/runtime.ts`: Python environment discovery, virtualenv resolution, and environment variable filtering.\n\n## Tool parameter schema\n\n```typescript\ninterface PythonToolParams {\n cells: Array<{\n code: string;\n title?: string;\n }>;\n timeout?: number; // Execution timeout in seconds (1–600, default: 30)\n cwd?: string; // Working directory for execution\n reset?: boolean; // Reset kernel state prior to executing first cell\n}\n```\n\nThe tool executes with `concurrency: \"exclusive\"`, ensuring sequential execution per session.\n\n## Gateway lifecycle management\n\n### Gateway operational modes\n\n1. **Local shared gateway (default)**:\n - Coordinates a single shared process under `~/.xcsh/agent/python-gateway/`.\n - Synchronizes access via `gateway.lock` and tracks state in `gateway.json`.\n - Starts `python -m kernel_gateway` bound to an ephemeral port on `127.0.0.1`.\n2. **External gateway**:\n - Configured via `PI_PYTHON_GATEWAY_URL`.\n - Authenticates requests with `PI_PYTHON_GATEWAY_TOKEN` when required.\n - Bypasses local process management.\n\n### Kernel lifecycle\n\n1. **Creation**: Allocates a new kernel session via `POST /api/kernels`.\n2. **Connection**: Establishes a WebSocket channel (`/api/kernels/:id/channels`).\n3. **Initialization**: Configures `cwd`, applies sanitized environment variables, and executes runtime preludes.\n4. **Module loading**: Imports custom modules from `~/.xcsh/agent/modules/*.py` and project-specific `<CWD>/.xcsh/modules/*.py`.\n5. **Termination**: Issues `DELETE /api/kernels/:id` and closes WebSocket connections upon session cleanup.\n\n## Environment isolation and security\n\nBefore launching Python runtimes, xcsh filters environment variables:\n\n- **Retained variables**: Core system variables (`PATH`, `HOME`, `VIRTUAL_ENV`, `PYTHONPATH`) and whitelisted prefixes (`LC_*`, `XDG_*`, `PI_*`).\n- **Sanitized variables**: Strips sensitive LLM API keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) to prevent accidental leaks.\n\n### Python environment resolution order\n\n1. Active virtual environment (`VIRTUAL_ENV` or `<CWD>/.venv`)\n2. Managed xcsh virtual environment (`~/.xcsh/python-env`)\n3. System `python3` or `python` on `PATH`\n\n## Output capture and MIME rendering\n\nThe runtime captures structured output across multiple MIME types:\n\n- `text/markdown`: Rendered directly in interactive output panes.\n- `text/plain`: Standard console output and tracebacks.\n- `application/json`: Formatted JSON inspector trees.\n- `image/png`: Inline image payloads.\n- `application/x-xcsh-status`: Structured progress and execution status events.\n\n## Troubleshooting\n\n### Python tool unavailable\n\n- Verify that `jupyter_kernel_gateway` and `ipykernel` are installed in the resolved Python environment:\n\n ```bash\n python -m pip install jupyter_kernel_gateway ipykernel\n ```\n\n- Verify that `python.toolMode` or `PI_PY` is not set to `bash-only`.\n\n### Execution timeouts or hangs\n\n- Python does not support interactive standard input (`input()`). Avoid invoking interactive prompts.\n- To handle long-running workloads, increase the `timeout` parameter (up to 600 seconds).\n",
|
|
150
150
|
"en/runtime-tools/bash-tool-runtime.md": "---\ntitle: Bash Tool Runtime\ndescription: Bash tool runtime with shell process management, sandboxing, timeout, and output streaming.\nsidebar:\n order: 1\n label: Bash tool\n---\n\nThis document describes the execution pipeline of the `bash` tool in `packages/coding-agent`, covering command normalization, interception rules, process sandboxing, output truncation, and UI rendering across execution modes.\n\n## Execution entry points\n\nxcsh provides two distinct shell execution interfaces:\n\n1. **Agent tool interface (`bash`)**: Invoked by LLMs during conversation turns. Supports command normalization, security interception, PTY emulation, and structured output formatting.\n - Entry point: `BashTool.execute()`\n2. **User shell execution (`!cmd`)**: Direct shell execution triggered by user input in the TUI or RPC mode.\n - Entry point: `AgentSession.executeBash()`\n\nBoth execution paths utilize the underlying `executeBash()` engine in `src/exec/bash-executor.ts` for non-interactive execution.\n\n## Execution pipeline\n\n### 1. Command normalization and argument parsing\n\nWhen a tool call occurs, `BashTool.execute()` normalizes command strings via `normalizeBashCommand()`:\n\n- Extracts trailing pipe limits (`| head -n N`, `| tail -n N`) into structured pagination parameters.\n- Trims outer whitespace while preserving internal arguments and heredocs.\n- Merges extracted limits with explicit `head` or `tail` parameters (explicit arguments take precedence).\n\n### 2. Command interception and rule enforcement\n\nIf `bashInterceptor.enabled` is active, the tool checks the command against configured regex rules before spawning processes:\n\n- **Rule evaluation**: Blocks commands when patterns match (e.g., using `cat` or `grep` when specialized tools exist) and the suggested alternative tool is present in active context (`ctx.toolNames`).\n- **Rejection behavior**: Raises a `ToolError` containing the blocking reason and guidance toward preferred tools (`view_file`, `grep_search`, `list_dir`, `replace_file_content`).\n\n### 3. Working directory validation and timeouts\n\n- Resolves working directories relative to session root (`resolveToCwd`).\n- Validates that directory paths exist and are accessible directories prior to execution.\n- Clamps timeout durations to the range of 1 to 3600 seconds (default: 30 seconds).\n\n### 4. Interactive PTY vs. non-interactive execution\n\nxcsh chooses PTY execution (`runInteractiveBashPty`) when all of the following conditions are met:\n\n- `bash.virtualTerminal` is configured to `on`.\n- `PI_NO_PTY` is not set to `1`.\n- The session has an active graphical/TUI terminal context (`ctx.hasUI === true`).\n\nIn headless, print, or RPC modes, xcsh always uses non-interactive execution.\n\n## Output streaming, truncation, and artifact spill\n\nOutput is processed through `OutputSink` in `src/session/streaming-output.ts`:\n\n- **Memory buffer**: Maintains a UTF-8-safe tail buffer (default: 50 KB).\n- **Artifact spillover**: When output exceeds the buffer threshold, xcsh writes the full stream to disk in artifact storage (`artifact://<ID>`).\n- **Truncation notice**: Injects truncation summaries into tool results, including total byte/line counts and artifact links for full-output retrieval.\n\n## Filesystem sandboxing and containment\n\nxcsh enforces filesystem containment boundaries using platform-native security primitives:\n\n| Platform | Kernel version | Security backend | Boundary enforcement |\n| -------- | -------------- | ---------------- | -------------------- |\n| macOS | Any | `seatbelt` | OS-enforced kernel sandbox |\n| Linux (modern) | Kernel ≥ 6.1 (Debian 12, Ubuntu 24.04, Fedora) | `landlock` (ABI ≥ 2) | OS-enforced kernel sandbox |\n| Linux (legacy) | Kernel < 6.1 (RHEL 9, Ubuntu 22.04 GA) | `scanner-only` | Command-text static analysis |\n\n> [!IMPORTANT]\n> On legacy Linux kernels with Landlock ABI 1 (kernel < 6.1), xcsh operates in `scanner-only` mode because ABI 1 lacks `LANDLOCK_ACCESS_FS_REFER` (which prevents `git` and `mv` operations). For full OS-enforced isolation, deploy on Linux kernels 6.1 or newer.\n\n## Related implementation files\n\n- `src/tools/bash.ts`: Tool definition, normalization, and rendering logic.\n- `src/tools/bash-normalize.ts`: Command string normalization and line limit extraction.\n- `src/tools/bash-interceptor.ts`: Pattern matching rules for command redirection.\n- `src/exec/bash-executor.ts`: Process execution engine and shell session reuse.\n- `src/tools/bash-interactive.ts`: Virtual PTY runtime and terminal input handling.\n- `src/session/streaming-output.ts`: `OutputSink` buffer management and artifact spillover.\n",
|
|
@@ -67,6 +67,29 @@ interface ScopedModelItem {
|
|
|
67
67
|
thinkingLevel?: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export interface DefaultPickerModelPresentation {
|
|
71
|
+
model: Model;
|
|
72
|
+
displaySelector: string;
|
|
73
|
+
selector: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const OPENAI_CODEX_GPT56_TIERS = new Set(["gpt-5.6-luna", "gpt-5.6-terra", "gpt-5.6-sol"]);
|
|
77
|
+
|
|
78
|
+
/** Collapse subscription tiers only in the ordinary picker; explicit --models scopes keep raw access. */
|
|
79
|
+
export function presentModelsForDefaultPicker(
|
|
80
|
+
models: readonly Model[],
|
|
81
|
+
explicitlyScoped = false,
|
|
82
|
+
): DefaultPickerModelPresentation[] {
|
|
83
|
+
return models.flatMap(model => {
|
|
84
|
+
const selector = `${model.provider}/${model.id}`;
|
|
85
|
+
if (explicitlyScoped || model.provider !== "openai-codex" || !OPENAI_CODEX_GPT56_TIERS.has(model.id)) {
|
|
86
|
+
return [{ model, displaySelector: selector, selector }];
|
|
87
|
+
}
|
|
88
|
+
if (model.id !== "gpt-5.6-sol") return [];
|
|
89
|
+
return [{ model, displaySelector: "openai-codex/gpt-5.6", selector }];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
interface RoleAssignment {
|
|
71
94
|
model: Model;
|
|
72
95
|
thinkingLevel: ThinkingLevel;
|
|
@@ -359,12 +382,15 @@ export class ModelSelectorComponent extends Container {
|
|
|
359
382
|
|
|
360
383
|
// Use scoped models if provided via --models flag
|
|
361
384
|
if (this.#scopedModels.length > 0) {
|
|
362
|
-
models =
|
|
385
|
+
models = presentModelsForDefaultPicker(
|
|
386
|
+
this.#scopedModels.map(scoped => scoped.model),
|
|
387
|
+
true,
|
|
388
|
+
).map(item => ({
|
|
363
389
|
kind: "provider",
|
|
364
|
-
provider:
|
|
365
|
-
id:
|
|
366
|
-
model:
|
|
367
|
-
selector:
|
|
390
|
+
provider: item.model.provider,
|
|
391
|
+
id: item.displaySelector.slice(item.displaySelector.indexOf("/") + 1),
|
|
392
|
+
model: item.model,
|
|
393
|
+
selector: item.selector,
|
|
368
394
|
}));
|
|
369
395
|
} else {
|
|
370
396
|
// Reload config and cached discovery state without blocking on live provider refresh
|
|
@@ -381,12 +407,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
381
407
|
// Load available models (built-in models still work even if models.json failed)
|
|
382
408
|
try {
|
|
383
409
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
384
|
-
models = availableModels.map(
|
|
410
|
+
models = presentModelsForDefaultPicker(availableModels).map(item => ({
|
|
385
411
|
kind: "provider",
|
|
386
|
-
provider: model.provider,
|
|
387
|
-
id:
|
|
388
|
-
model,
|
|
389
|
-
selector:
|
|
412
|
+
provider: item.model.provider,
|
|
413
|
+
id: item.displaySelector.slice(item.displaySelector.indexOf("/") + 1),
|
|
414
|
+
model: item.model,
|
|
415
|
+
selector: item.selector,
|
|
390
416
|
}));
|
|
391
417
|
} catch (error) {
|
|
392
418
|
this.#allModels = [];
|
|
@@ -428,7 +454,28 @@ export class ModelSelectorComponent extends Container {
|
|
|
428
454
|
compactSearchText: compactSearchText(searchText),
|
|
429
455
|
};
|
|
430
456
|
})
|
|
431
|
-
.filter((item): item is CanonicalModelItem => item !== undefined)
|
|
457
|
+
.filter((item): item is CanonicalModelItem => item !== undefined)
|
|
458
|
+
.filter(
|
|
459
|
+
item =>
|
|
460
|
+
this.#scopedModels.length > 0 ||
|
|
461
|
+
item.model.provider !== "openai-codex" ||
|
|
462
|
+
!OPENAI_CODEX_GPT56_TIERS.has(item.model.id),
|
|
463
|
+
);
|
|
464
|
+
if (this.#scopedModels.length === 0) {
|
|
465
|
+
const friendly = models.find(item => item.provider === "openai-codex" && item.id === "gpt-5.6");
|
|
466
|
+
if (friendly) {
|
|
467
|
+
canonicalModels.push({
|
|
468
|
+
kind: "canonical",
|
|
469
|
+
id: "openai-codex/gpt-5.6",
|
|
470
|
+
model: friendly.model,
|
|
471
|
+
selector: friendly.selector,
|
|
472
|
+
variantCount: 1,
|
|
473
|
+
searchText: "openai-codex/gpt-5.6 GPT-5.6 Sol",
|
|
474
|
+
normalizedSearchText: normalizeSearchText("openai-codex/gpt-5.6 GPT-5.6 Sol"),
|
|
475
|
+
compactSearchText: compactSearchText("openai-codex/gpt-5.6 GPT-5.6 Sol"),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
432
479
|
|
|
433
480
|
this.#sortModels(models);
|
|
434
481
|
this.#sortCanonicalModels(canonicalModels);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ThinkingLevel } from "@f5-sales-demo/pi-agent-core";
|
|
2
2
|
import { canonicalizeOAuthProviderId, type Model } from "@f5-sales-demo/pi-ai";
|
|
3
|
+
import type { Settings } from "../../config/settings";
|
|
3
4
|
import type { VllmDiscoveredModel } from "../../config/vllm-config";
|
|
4
5
|
import { applySubscriptionProfileRoles, type SubscriptionProfileId } from "../../routing/subscription-profiles";
|
|
5
6
|
|
|
@@ -42,10 +43,10 @@ export const GOOGLE_ANTIGRAVITY_LOGIN_MODEL_CHOICE: LoginModelChoice = {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export const OPENAI_CODEX_LOGIN_MODEL_CHOICE: LoginModelChoice = {
|
|
45
|
-
label: "GPT-5.6
|
|
46
|
-
description: "
|
|
46
|
+
label: "GPT-5.6",
|
|
47
|
+
description: "OpenAI Codex subscription model with medium reasoning",
|
|
47
48
|
provider: "openai-codex",
|
|
48
|
-
modelId: "gpt-5.6-
|
|
49
|
+
modelId: "gpt-5.6-sol",
|
|
49
50
|
thinkingLevel: ThinkingLevel.Medium,
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -83,12 +84,7 @@ interface ModelApplicableSession extends BaseModelApplicableSession {
|
|
|
83
84
|
getAll(): Model[];
|
|
84
85
|
getProviderDiscoveryState?(provider: string): { status: string; stale: boolean } | undefined;
|
|
85
86
|
};
|
|
86
|
-
settings?:
|
|
87
|
-
getModelRoles(): Readonly<Record<string, string | undefined>>;
|
|
88
|
-
get?(key: "routing.profile"): "none" | SubscriptionProfileId;
|
|
89
|
-
set(key: "modelRoles", value: Record<string, string>): void;
|
|
90
|
-
set(key: "routing.profile", value: "none" | SubscriptionProfileId): void;
|
|
91
|
-
};
|
|
87
|
+
settings?: Pick<Settings, "getModelRoles" | "get" | "set">;
|
|
92
88
|
}
|
|
93
89
|
|
|
94
90
|
/**
|
|
@@ -138,7 +134,12 @@ export async function applyOAuthLoginModel(
|
|
|
138
134
|
}
|
|
139
135
|
|
|
140
136
|
const settings = session.settings;
|
|
141
|
-
const
|
|
137
|
+
const storedProfile = settings?.get("routing.profile");
|
|
138
|
+
const previousProfile: "none" | SubscriptionProfileId =
|
|
139
|
+
storedProfile === "google-antigravity" || storedProfile === "openai-codex" ? storedProfile : "none";
|
|
140
|
+
const storedRoutingMode = settings?.get("routing.mode");
|
|
141
|
+
const previousRoutingMode =
|
|
142
|
+
storedRoutingMode === "shadow" || storedRoutingMode === "auto" ? storedRoutingMode : "off";
|
|
142
143
|
const previousRoles = settings
|
|
143
144
|
? Object.fromEntries(
|
|
144
145
|
Object.entries(settings.getModelRoles()).filter(
|
|
@@ -155,6 +156,7 @@ export async function applyOAuthLoginModel(
|
|
|
155
156
|
);
|
|
156
157
|
if (!profile.applied) return undefined;
|
|
157
158
|
settings.set("modelRoles", profile.roles);
|
|
159
|
+
settings.set("routing.mode", "off");
|
|
158
160
|
settings.set("routing.profile", canonicalProvider as SubscriptionProfileId);
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -162,12 +164,14 @@ export async function applyOAuthLoginModel(
|
|
|
162
164
|
const applied = await applyModelAfterLogin(session, choice);
|
|
163
165
|
if (!applied && settings) {
|
|
164
166
|
settings.set("modelRoles", previousRoles);
|
|
167
|
+
settings.set("routing.mode", previousRoutingMode);
|
|
165
168
|
settings.set("routing.profile", previousProfile);
|
|
166
169
|
}
|
|
167
170
|
return applied ? choice : undefined;
|
|
168
171
|
} catch (error) {
|
|
169
172
|
if (settings) {
|
|
170
173
|
settings.set("modelRoles", previousRoles);
|
|
174
|
+
settings.set("routing.mode", previousRoutingMode);
|
|
171
175
|
settings.set("routing.profile", previousProfile);
|
|
172
176
|
}
|
|
173
177
|
throw error;
|
|
@@ -86,7 +86,6 @@ import { commitVllmLogin } from "./vllm-login-transaction";
|
|
|
86
86
|
|
|
87
87
|
const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
|
|
88
88
|
"anthropic",
|
|
89
|
-
"openai-codex-browser",
|
|
90
89
|
"gitlab-duo",
|
|
91
90
|
"google-gemini-cli",
|
|
92
91
|
"google-antigravity",
|
|
@@ -1225,8 +1224,7 @@ export class SelectorController {
|
|
|
1225
1224
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1226
1225
|
const manualInput = this.ctx.oauthManualInput;
|
|
1227
1226
|
const useManualInput =
|
|
1228
|
-
CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider) ||
|
|
1229
|
-
(providerId === "openai-codex" && resolveOpenAICodexLoginMethod() === "browser");
|
|
1227
|
+
CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider) || providerId === "openai-codex";
|
|
1230
1228
|
const shouldOpenBrowser = providerId !== "openai-codex" || resolveOpenAICodexLoginMethod() === "browser";
|
|
1231
1229
|
const loginCallbacks = {
|
|
1232
1230
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -39,7 +39,7 @@ export const SUBSCRIPTION_ROUTING_PROFILES: Readonly<Record<SubscriptionProfileI
|
|
|
39
39
|
provider: "openai-codex",
|
|
40
40
|
roles: {
|
|
41
41
|
smol: "openai-codex/gpt-5.6-luna:low",
|
|
42
|
-
default: "openai-codex/gpt-5.6-
|
|
42
|
+
default: "openai-codex/gpt-5.6-sol:medium",
|
|
43
43
|
slow: "openai-codex/gpt-5.6-sol:high",
|
|
44
44
|
plan: "openai-codex/gpt-5.6-sol:high",
|
|
45
45
|
},
|