mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 22:01:38 +00:00
feat: add configurable transport and codex websocket session caching
This commit is contained in:
parent
9537919a49
commit
a26a9cfabd
15 changed files with 580 additions and 4 deletions
47
.pi/extensions/tps.ts
Normal file
47
.pi/extensions/tps.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||||
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
||||||
|
|
||||||
|
function isAssistantMessage(message: unknown): message is AssistantMessage {
|
||||||
|
if (!message || typeof message !== "object") return false;
|
||||||
|
const role = (message as { role?: unknown }).role;
|
||||||
|
return role === "assistant";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
let agentStartMs: number | null = null;
|
||||||
|
|
||||||
|
pi.on("agent_start", () => {
|
||||||
|
agentStartMs = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.on("agent_end", (event, ctx) => {
|
||||||
|
if (!ctx.hasUI) return;
|
||||||
|
if (agentStartMs === null) return;
|
||||||
|
|
||||||
|
const elapsedMs = Date.now() - agentStartMs;
|
||||||
|
agentStartMs = null;
|
||||||
|
if (elapsedMs <= 0) return;
|
||||||
|
|
||||||
|
let input = 0;
|
||||||
|
let output = 0;
|
||||||
|
let cacheRead = 0;
|
||||||
|
let cacheWrite = 0;
|
||||||
|
let totalTokens = 0;
|
||||||
|
|
||||||
|
for (const message of event.messages) {
|
||||||
|
if (!isAssistantMessage(message)) continue;
|
||||||
|
input += message.usage.input || 0;
|
||||||
|
output += message.usage.output || 0;
|
||||||
|
cacheRead += message.usage.cacheRead || 0;
|
||||||
|
cacheWrite += message.usage.cacheWrite || 0;
|
||||||
|
totalTokens += message.usage.totalTokens || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output <= 0) return;
|
||||||
|
|
||||||
|
const elapsedSeconds = elapsedMs / 1000;
|
||||||
|
const tokensPerSecond = output / elapsedSeconds;
|
||||||
|
const message = `TPS ${tokensPerSecond.toFixed(1)} tok/s. out ${output.toLocaleString()}, in ${input.toLocaleString()}, cache r/w ${cacheRead.toLocaleString()}/${cacheWrite.toLocaleString()}, total ${totalTokens.toLocaleString()}, ${elapsedSeconds.toFixed(1)}s`;
|
||||||
|
ctx.ui.notify(message, "info");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `transport` to `AgentOptions` and `AgentLoopConfig` forwarding, allowing stream transport preference (`"sse"`, `"websocket"`, `"auto"`) to flow into provider calls.
|
||||||
|
|
||||||
## [0.52.11] - 2026-02-13
|
## [0.52.11] - 2026-02-13
|
||||||
|
|
||||||
## [0.52.10] - 2026-02-12
|
## [0.52.10] - 2026-02-12
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
streamSimple,
|
streamSimple,
|
||||||
type TextContent,
|
type TextContent,
|
||||||
type ThinkingBudgets,
|
type ThinkingBudgets,
|
||||||
|
type Transport,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
import { agentLoop, agentLoopContinue } from "./agent-loop.js";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -78,6 +79,11 @@ export interface AgentOptions {
|
||||||
*/
|
*/
|
||||||
thinkingBudgets?: ThinkingBudgets;
|
thinkingBudgets?: ThinkingBudgets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferred transport for providers that support multiple transports.
|
||||||
|
*/
|
||||||
|
transport?: Transport;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||||
* If the server's requested delay exceeds this value, the request fails immediately,
|
* If the server's requested delay exceeds this value, the request fails immediately,
|
||||||
|
|
@ -114,6 +120,7 @@ export class Agent {
|
||||||
private runningPrompt?: Promise<void>;
|
private runningPrompt?: Promise<void>;
|
||||||
private resolveRunningPrompt?: () => void;
|
private resolveRunningPrompt?: () => void;
|
||||||
private _thinkingBudgets?: ThinkingBudgets;
|
private _thinkingBudgets?: ThinkingBudgets;
|
||||||
|
private _transport: Transport;
|
||||||
private _maxRetryDelayMs?: number;
|
private _maxRetryDelayMs?: number;
|
||||||
|
|
||||||
constructor(opts: AgentOptions = {}) {
|
constructor(opts: AgentOptions = {}) {
|
||||||
|
|
@ -126,6 +133,7 @@ export class Agent {
|
||||||
this._sessionId = opts.sessionId;
|
this._sessionId = opts.sessionId;
|
||||||
this.getApiKey = opts.getApiKey;
|
this.getApiKey = opts.getApiKey;
|
||||||
this._thinkingBudgets = opts.thinkingBudgets;
|
this._thinkingBudgets = opts.thinkingBudgets;
|
||||||
|
this._transport = opts.transport ?? "sse";
|
||||||
this._maxRetryDelayMs = opts.maxRetryDelayMs;
|
this._maxRetryDelayMs = opts.maxRetryDelayMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,6 +166,20 @@ export class Agent {
|
||||||
this._thinkingBudgets = value;
|
this._thinkingBudgets = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current preferred transport.
|
||||||
|
*/
|
||||||
|
get transport(): Transport {
|
||||||
|
return this._transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the preferred transport.
|
||||||
|
*/
|
||||||
|
setTransport(value: Transport) {
|
||||||
|
this._transport = value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current max retry delay in milliseconds.
|
* Get the current max retry delay in milliseconds.
|
||||||
*/
|
*/
|
||||||
|
|
@ -407,6 +429,7 @@ export class Agent {
|
||||||
model,
|
model,
|
||||||
reasoning,
|
reasoning,
|
||||||
sessionId: this._sessionId,
|
sessionId: this._sessionId,
|
||||||
|
transport: this._transport,
|
||||||
thinkingBudgets: this._thinkingBudgets,
|
thinkingBudgets: this._thinkingBudgets,
|
||||||
maxRetryDelayMs: this._maxRetryDelayMs,
|
maxRetryDelayMs: this._maxRetryDelayMs,
|
||||||
convertToLlm: this.convertToLlm,
|
convertToLlm: this.convertToLlm,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,16 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `transport` to `StreamOptions` with values `"sse"`, `"websocket"`, and `"auto"` (currently supported by `openai-codex-responses`).
|
||||||
|
- Added WebSocket transport support for OpenAI Codex Responses (`openai-codex-responses`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- OpenAI Codex Responses now defaults to SSE transport unless `transport` is explicitly set.
|
||||||
|
- OpenAI Codex Responses WebSocket connections are cached per `sessionId` and expire after 5 minutes of inactivity.
|
||||||
|
|
||||||
## [0.52.11] - 2026-02-13
|
## [0.52.11] - 2026-02-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1086,7 +1086,7 @@ const response = await complete(model, {
|
||||||
|
|
||||||
### Provider Notes
|
### Provider Notes
|
||||||
|
|
||||||
**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options.
|
**OpenAI Codex**: Requires a ChatGPT Plus or Pro subscription. Provides access to GPT-5.x Codex models with extended context windows and reasoning capabilities. The library automatically handles session-based prompt caching when `sessionId` is provided in stream options. You can set `transport` in stream options to `"sse"`, `"websocket"`, or `"auto"` for Codex Responses transport selection. When using WebSocket with a `sessionId`, connections are reused per session and expire after 5 minutes of inactivity.
|
||||||
|
|
||||||
**Azure OpenAI (Responses)**: Uses the Responses API only. Set `AZURE_OPENAI_API_KEY` and either `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME`. Use `AZURE_OPENAI_API_VERSION` (defaults to `v1`) to override the API version if needed. Deployment names are treated as model IDs by default, override with `azureDeploymentName` or `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` using comma-separated `model-id=deployment` pairs (for example `gpt-4o-mini=my-deployment,gpt-4o=prod`). Legacy deployment-based URLs are intentionally unsupported.
|
**Azure OpenAI (Responses)**: Uses the Responses API only. Set `AZURE_OPENAI_API_KEY` and either `AZURE_OPENAI_BASE_URL` or `AZURE_OPENAI_RESOURCE_NAME`. Use `AZURE_OPENAI_API_VERSION` (defaults to `v1`) to override the API version if needed. Deployment names are treated as model IDs by default, override with `azureDeploymentName` or `AZURE_OPENAI_DEPLOYMENT_NAME_MAP` using comma-separated `model-id=deployment` pairs (for example `gpt-4o-mini=my-deployment,gpt-4o=prod`). Legacy deployment-based URLs are intentionally unsupported.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,40 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
||||||
options?.onPayload?.(body);
|
options?.onPayload?.(body);
|
||||||
const headers = buildHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
|
const headers = buildHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
|
||||||
const bodyJson = JSON.stringify(body);
|
const bodyJson = JSON.stringify(body);
|
||||||
|
const transport = options?.transport || "sse";
|
||||||
|
|
||||||
|
if (transport !== "sse") {
|
||||||
|
let websocketStarted = false;
|
||||||
|
try {
|
||||||
|
await processWebSocketStream(
|
||||||
|
resolveCodexWebSocketUrl(model.baseUrl),
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
output,
|
||||||
|
stream,
|
||||||
|
model,
|
||||||
|
() => {
|
||||||
|
websocketStarted = true;
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.signal?.aborted) {
|
||||||
|
throw new Error("Request was aborted");
|
||||||
|
}
|
||||||
|
stream.push({
|
||||||
|
type: "done",
|
||||||
|
reason: output.stopReason as "stop" | "length" | "toolUse",
|
||||||
|
message: output,
|
||||||
|
});
|
||||||
|
stream.end();
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (transport === "websocket" || websocketStarted) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch with retry logic for rate limits and transient errors
|
// Fetch with retry logic for rate limits and transient errors
|
||||||
let response: Response | undefined;
|
let response: Response | undefined;
|
||||||
|
|
@ -296,6 +330,13 @@ function resolveCodexUrl(baseUrl?: string): string {
|
||||||
return `${normalized}/codex/responses`;
|
return `${normalized}/codex/responses`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveCodexWebSocketUrl(baseUrl?: string): string {
|
||||||
|
const url = new URL(resolveCodexUrl(baseUrl));
|
||||||
|
if (url.protocol === "https:") url.protocol = "wss:";
|
||||||
|
if (url.protocol === "http:") url.protocol = "ws:";
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Response Processing
|
// Response Processing
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -381,6 +422,371 @@ async function* parseSSE(response: Response): AsyncGenerator<Record<string, unkn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// WebSocket Parsing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
|
||||||
|
const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
type WebSocketEventType = "open" | "message" | "error" | "close";
|
||||||
|
type WebSocketListener = (event: unknown) => void;
|
||||||
|
|
||||||
|
interface WebSocketLike {
|
||||||
|
close(code?: number, reason?: string): void;
|
||||||
|
send(data: string): void;
|
||||||
|
addEventListener(type: WebSocketEventType, listener: WebSocketListener): void;
|
||||||
|
removeEventListener(type: WebSocketEventType, listener: WebSocketListener): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CachedWebSocketConnection {
|
||||||
|
socket: WebSocketLike;
|
||||||
|
busy: boolean;
|
||||||
|
idleTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const websocketSessionCache = new Map<string, CachedWebSocketConnection>();
|
||||||
|
|
||||||
|
type WebSocketConstructor = new (
|
||||||
|
url: string,
|
||||||
|
protocols?: string | string[] | { headers?: Record<string, string> },
|
||||||
|
) => WebSocketLike;
|
||||||
|
|
||||||
|
function getWebSocketConstructor(): WebSocketConstructor | null {
|
||||||
|
const ctor = (globalThis as { WebSocket?: unknown }).WebSocket;
|
||||||
|
if (typeof ctor !== "function") return null;
|
||||||
|
return ctor as unknown as WebSocketConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headersToRecord(headers: Headers): Record<string, string> {
|
||||||
|
const out: Record<string, string> = {};
|
||||||
|
for (const [key, value] of headers.entries()) {
|
||||||
|
out[key] = value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebSocketReadyState(socket: WebSocketLike): number | undefined {
|
||||||
|
const readyState = (socket as { readyState?: unknown }).readyState;
|
||||||
|
return typeof readyState === "number" ? readyState : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWebSocketReusable(socket: WebSocketLike): boolean {
|
||||||
|
const readyState = getWebSocketReadyState(socket);
|
||||||
|
// If readyState is unavailable, assume the runtime keeps it open/reusable.
|
||||||
|
return readyState === undefined || readyState === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = "done"): void {
|
||||||
|
try {
|
||||||
|
socket.close(code, reason);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSessionWebSocketExpiry(sessionId: string, entry: CachedWebSocketConnection): void {
|
||||||
|
if (entry.idleTimer) {
|
||||||
|
clearTimeout(entry.idleTimer);
|
||||||
|
}
|
||||||
|
entry.idleTimer = setTimeout(() => {
|
||||||
|
if (entry.busy) return;
|
||||||
|
closeWebSocketSilently(entry.socket, 1000, "idle_timeout");
|
||||||
|
websocketSessionCache.delete(sessionId);
|
||||||
|
}, SESSION_WEBSOCKET_CACHE_TTL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWebSocket(url: string, headers: Headers, signal?: AbortSignal): Promise<WebSocketLike> {
|
||||||
|
const WebSocketCtor = getWebSocketConstructor();
|
||||||
|
if (!WebSocketCtor) {
|
||||||
|
throw new Error("WebSocket transport is not available in this runtime");
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsHeaders = headersToRecord(headers);
|
||||||
|
wsHeaders["OpenAI-Beta"] = OPENAI_BETA_RESPONSES_WEBSOCKETS;
|
||||||
|
|
||||||
|
return new Promise<WebSocketLike>((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
let socket: WebSocketLike;
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket = new WebSocketCtor(url, { headers: wsHeaders });
|
||||||
|
} catch (error) {
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOpen: WebSocketListener = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
resolve(socket);
|
||||||
|
};
|
||||||
|
const onError: WebSocketListener = (event) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(extractWebSocketError(event));
|
||||||
|
};
|
||||||
|
const onClose: WebSocketListener = (event) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(extractWebSocketCloseError(event));
|
||||||
|
};
|
||||||
|
const onAbort = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
socket.close(1000, "aborted");
|
||||||
|
reject(new Error("Request was aborted"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
socket.removeEventListener("open", onOpen);
|
||||||
|
socket.removeEventListener("error", onError);
|
||||||
|
socket.removeEventListener("close", onClose);
|
||||||
|
signal?.removeEventListener("abort", onAbort);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener("open", onOpen);
|
||||||
|
socket.addEventListener("error", onError);
|
||||||
|
socket.addEventListener("close", onClose);
|
||||||
|
signal?.addEventListener("abort", onAbort);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acquireWebSocket(
|
||||||
|
url: string,
|
||||||
|
headers: Headers,
|
||||||
|
sessionId: string | undefined,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<{ socket: WebSocketLike; release: (options?: { keep?: boolean }) => void }> {
|
||||||
|
if (!sessionId) {
|
||||||
|
const socket = await connectWebSocket(url, headers, signal);
|
||||||
|
return {
|
||||||
|
socket,
|
||||||
|
release: ({ keep } = {}) => {
|
||||||
|
if (keep === false) {
|
||||||
|
closeWebSocketSilently(socket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeWebSocketSilently(socket);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = websocketSessionCache.get(sessionId);
|
||||||
|
if (cached) {
|
||||||
|
if (cached.idleTimer) {
|
||||||
|
clearTimeout(cached.idleTimer);
|
||||||
|
cached.idleTimer = undefined;
|
||||||
|
}
|
||||||
|
if (!cached.busy && isWebSocketReusable(cached.socket)) {
|
||||||
|
cached.busy = true;
|
||||||
|
return {
|
||||||
|
socket: cached.socket,
|
||||||
|
release: ({ keep } = {}) => {
|
||||||
|
if (!keep || !isWebSocketReusable(cached.socket)) {
|
||||||
|
closeWebSocketSilently(cached.socket);
|
||||||
|
websocketSessionCache.delete(sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cached.busy = false;
|
||||||
|
scheduleSessionWebSocketExpiry(sessionId, cached);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cached.busy) {
|
||||||
|
const socket = await connectWebSocket(url, headers, signal);
|
||||||
|
return {
|
||||||
|
socket,
|
||||||
|
release: () => {
|
||||||
|
closeWebSocketSilently(socket);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!isWebSocketReusable(cached.socket)) {
|
||||||
|
closeWebSocketSilently(cached.socket);
|
||||||
|
websocketSessionCache.delete(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const socket = await connectWebSocket(url, headers, signal);
|
||||||
|
const entry: CachedWebSocketConnection = { socket, busy: true };
|
||||||
|
websocketSessionCache.set(sessionId, entry);
|
||||||
|
return {
|
||||||
|
socket,
|
||||||
|
release: ({ keep } = {}) => {
|
||||||
|
if (!keep || !isWebSocketReusable(entry.socket)) {
|
||||||
|
closeWebSocketSilently(entry.socket);
|
||||||
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
||||||
|
if (websocketSessionCache.get(sessionId) === entry) {
|
||||||
|
websocketSessionCache.delete(sessionId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entry.busy = false;
|
||||||
|
scheduleSessionWebSocketExpiry(sessionId, entry);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractWebSocketError(event: unknown): Error {
|
||||||
|
if (event && typeof event === "object" && "message" in event) {
|
||||||
|
const message = (event as { message?: unknown }).message;
|
||||||
|
if (typeof message === "string" && message.length > 0) {
|
||||||
|
return new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Error("WebSocket error");
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractWebSocketCloseError(event: unknown): Error {
|
||||||
|
if (event && typeof event === "object") {
|
||||||
|
const code = "code" in event ? (event as { code?: unknown }).code : undefined;
|
||||||
|
const reason = "reason" in event ? (event as { reason?: unknown }).reason : undefined;
|
||||||
|
const codeText = typeof code === "number" ? ` ${code}` : "";
|
||||||
|
const reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
|
||||||
|
return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
|
||||||
|
}
|
||||||
|
return new Error("WebSocket closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeWebSocketData(data: unknown): Promise<string | null> {
|
||||||
|
if (typeof data === "string") return data;
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
return new TextDecoder().decode(new Uint8Array(data));
|
||||||
|
}
|
||||||
|
if (ArrayBuffer.isView(data)) {
|
||||||
|
const view = data as ArrayBufferView;
|
||||||
|
return new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
|
||||||
|
}
|
||||||
|
if (data && typeof data === "object" && "arrayBuffer" in data) {
|
||||||
|
const blobLike = data as { arrayBuffer: () => Promise<ArrayBuffer> };
|
||||||
|
const arrayBuffer = await blobLike.arrayBuffer();
|
||||||
|
return new TextDecoder().decode(new Uint8Array(arrayBuffer));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function* parseWebSocket(socket: WebSocketLike, signal?: AbortSignal): AsyncGenerator<Record<string, unknown>> {
|
||||||
|
const queue: Record<string, unknown>[] = [];
|
||||||
|
let pending: (() => void) | null = null;
|
||||||
|
let done = false;
|
||||||
|
let failed: Error | null = null;
|
||||||
|
let sawCompletion = false;
|
||||||
|
|
||||||
|
const wake = () => {
|
||||||
|
if (!pending) return;
|
||||||
|
const resolve = pending;
|
||||||
|
pending = null;
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessage: WebSocketListener = (event) => {
|
||||||
|
void (async () => {
|
||||||
|
if (!event || typeof event !== "object" || !("data" in event)) return;
|
||||||
|
const text = await decodeWebSocketData((event as { data?: unknown }).data);
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text) as Record<string, unknown>;
|
||||||
|
const type = typeof parsed.type === "string" ? parsed.type : "";
|
||||||
|
if (type === "response.completed" || type === "response.done") {
|
||||||
|
sawCompletion = true;
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
queue.push(parsed);
|
||||||
|
wake();
|
||||||
|
} catch {}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onError: WebSocketListener = (event) => {
|
||||||
|
failed = extractWebSocketError(event);
|
||||||
|
done = true;
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose: WebSocketListener = (event) => {
|
||||||
|
if (sawCompletion) {
|
||||||
|
done = true;
|
||||||
|
wake();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!failed) {
|
||||||
|
failed = extractWebSocketCloseError(event);
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAbort = () => {
|
||||||
|
failed = new Error("Request was aborted");
|
||||||
|
done = true;
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener("message", onMessage);
|
||||||
|
socket.addEventListener("error", onError);
|
||||||
|
socket.addEventListener("close", onClose);
|
||||||
|
signal?.addEventListener("abort", onAbort);
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error("Request was aborted");
|
||||||
|
}
|
||||||
|
if (queue.length > 0) {
|
||||||
|
yield queue.shift()!;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (done) break;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
pending = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
throw failed;
|
||||||
|
}
|
||||||
|
if (!sawCompletion) {
|
||||||
|
throw new Error("WebSocket stream closed before response.completed");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
socket.removeEventListener("message", onMessage);
|
||||||
|
socket.removeEventListener("error", onError);
|
||||||
|
socket.removeEventListener("close", onClose);
|
||||||
|
signal?.removeEventListener("abort", onAbort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processWebSocketStream(
|
||||||
|
url: string,
|
||||||
|
body: RequestBody,
|
||||||
|
headers: Headers,
|
||||||
|
output: AssistantMessage,
|
||||||
|
stream: AssistantMessageEventStream,
|
||||||
|
model: Model<"openai-codex-responses">,
|
||||||
|
onStart: () => void,
|
||||||
|
options?: OpenAICodexResponsesOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
||||||
|
let keepConnection = true;
|
||||||
|
try {
|
||||||
|
socket.send(JSON.stringify({ type: "response.create", ...body }));
|
||||||
|
onStart();
|
||||||
|
stream.push({ type: "start", partial: output });
|
||||||
|
await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, model);
|
||||||
|
if (options?.signal?.aborted) {
|
||||||
|
keepConnection = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
keepConnection = false;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
release({ keep: keepConnection });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Error Handling
|
// Error Handling
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,18 @@ export interface ThinkingBudgets {
|
||||||
// Base options all providers share
|
// Base options all providers share
|
||||||
export type CacheRetention = "none" | "short" | "long";
|
export type CacheRetention = "none" | "short" | "long";
|
||||||
|
|
||||||
|
export type Transport = "sse" | "websocket" | "auto";
|
||||||
|
|
||||||
export interface StreamOptions {
|
export interface StreamOptions {
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
/**
|
||||||
|
* Preferred transport for providers that support multiple transports.
|
||||||
|
* Providers that do not support this option ignore it.
|
||||||
|
*/
|
||||||
|
transport?: Transport;
|
||||||
/**
|
/**
|
||||||
* Prompt cache retention preference. Providers map this to their supported values.
|
* Prompt cache retention preference. Providers map this to their supported values.
|
||||||
* Default: "short".
|
* Default: "short".
|
||||||
|
|
|
||||||
|
|
@ -1190,6 +1190,35 @@ describe("Generate E2E Tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("OpenAI Codex Provider (gpt-5.3-codex via WebSocket)", () => {
|
||||||
|
const llm = getModel("openai-codex", "gpt-5.3-codex");
|
||||||
|
const wsOptions = { apiKey: openaiCodexToken, transport: "websocket" as const };
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should complete basic text generation", { retry: 3 }, async () => {
|
||||||
|
await basicTextGeneration(llm, wsOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should handle tool calling", { retry: 3 }, async () => {
|
||||||
|
await handleToolCall(llm, wsOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should handle streaming", { retry: 3 }, async () => {
|
||||||
|
await handleStreaming(llm, wsOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should handle thinking with reasoningEffort high", { retry: 3 }, async () => {
|
||||||
|
await handleThinking(llm, { ...wsOptions, reasoningEffort: "high" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should handle multi-turn with thinking and tools", { retry: 3 }, async () => {
|
||||||
|
await multiTurn(llm, { ...wsOptions, reasoningEffort: "high" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(!openaiCodexToken)("should handle image input", { retry: 3 }, async () => {
|
||||||
|
await handleImage(llm, wsOptions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe.skipIf(!hasBedrockCredentials())("Amazon Bedrock Provider (claude-sonnet-4-5)", () => {
|
describe.skipIf(!hasBedrockCredentials())("Amazon Bedrock Provider (claude-sonnet-4-5)", () => {
|
||||||
const llm = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-5-20250929-v1:0");
|
const llm = getModel("amazon-bedrock", "global.anthropic.claude-sonnet-4-5-20250929-v1:0");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,15 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `transport` setting (`"sse"`, `"websocket"`, `"auto"`) to `/settings` and `settings.json` for providers that support multiple transports (currently `openai-codex` via OpenAI Codex Responses).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Interactive mode now applies transport changes immediately to the active agent session.
|
||||||
|
- Settings migration now maps legacy `websockets: boolean` to the new `transport` setting.
|
||||||
|
|
||||||
## [0.52.11] - 2026-02-13
|
## [0.52.11] - 2026-02-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ Type `/` in the editor to trigger commands. [Extensions](#extensions) can regist
|
||||||
| `/login`, `/logout` | OAuth authentication |
|
| `/login`, `/logout` | OAuth authentication |
|
||||||
| `/model` | Switch models |
|
| `/model` | Switch models |
|
||||||
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
|
| `/scoped-models` | Enable/disable models for Ctrl+P cycling |
|
||||||
| `/settings` | Thinking level, theme, message delivery |
|
| `/settings` | Thinking level, theme, message delivery, transport |
|
||||||
| `/resume` | Pick from previous sessions |
|
| `/resume` | Pick from previous sessions |
|
||||||
| `/new` | Start a new session |
|
| `/new` | Start a new session |
|
||||||
| `/name <name>` | Set session display name |
|
| `/name <name>` | Set session display name |
|
||||||
|
|
@ -193,7 +193,7 @@ Submit messages while the agent is working:
|
||||||
- **Escape** aborts and restores queued messages to editor
|
- **Escape** aborts and restores queued messages to editor
|
||||||
- **Alt+Up** retrieves queued messages back to editor
|
- **Alt+Up** retrieves queued messages back to editor
|
||||||
|
|
||||||
Configure delivery in [settings](docs/settings.md): `steeringMode` and `followUpMode` can be `"one-at-a-time"` (default, waits for response) or `"all"` (delivers all queued at once).
|
Configure delivery in [settings](docs/settings.md): `steeringMode` and `followUpMode` can be `"one-at-a-time"` (default, waits for response) or `"all"` (delivers all queued at once). `transport` selects provider transport preference (`"sse"`, `"websocket"`, or `"auto"`) for providers that support multiple transports.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ Edit directly or use `/settings` for common options.
|
||||||
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
|
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
|
||||||
| `quietStartup` | boolean | `false` | Hide startup header |
|
| `quietStartup` | boolean | `false` | Hide startup header |
|
||||||
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
|
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
|
||||||
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"` or `"fork"` |
|
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` |
|
||||||
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
|
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
|
||||||
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
||||||
| `showHardwareCursor` | boolean | `false` | Show terminal cursor |
|
| `showHardwareCursor` | boolean | `false` | Show terminal cursor |
|
||||||
|
|
@ -98,12 +98,14 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
|
||||||
|---------|------|---------|-------------|
|
|---------|------|---------|-------------|
|
||||||
| `steeringMode` | string | `"one-at-a-time"` | How steering messages are sent: `"all"` or `"one-at-a-time"` |
|
| `steeringMode` | string | `"one-at-a-time"` | How steering messages are sent: `"all"` or `"one-at-a-time"` |
|
||||||
| `followUpMode` | string | `"one-at-a-time"` | How follow-up messages are sent: `"all"` or `"one-at-a-time"` |
|
| `followUpMode` | string | `"one-at-a-time"` | How follow-up messages are sent: `"all"` or `"one-at-a-time"` |
|
||||||
|
| `transport` | string | `"sse"` | Preferred transport for providers that support multiple transports: `"sse"`, `"websocket"`, or `"auto"` |
|
||||||
|
|
||||||
### Terminal & Images
|
### Terminal & Images
|
||||||
|
|
||||||
| Setting | Type | Default | Description |
|
| Setting | Type | Default | Description |
|
||||||
|---------|------|---------|-------------|
|
|---------|------|---------|-------------|
|
||||||
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
|
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
|
||||||
|
| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) |
|
||||||
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
|
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
|
||||||
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |
|
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
},
|
},
|
||||||
steeringMode: settingsManager.getSteeringMode(),
|
steeringMode: settingsManager.getSteeringMode(),
|
||||||
followUpMode: settingsManager.getFollowUpMode(),
|
followUpMode: settingsManager.getFollowUpMode(),
|
||||||
|
transport: settingsManager.getTransport(),
|
||||||
thinkingBudgets: settingsManager.getThinkingBudgets(),
|
thinkingBudgets: settingsManager.getThinkingBudgets(),
|
||||||
maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,
|
maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,
|
||||||
getApiKey: async (provider) => {
|
getApiKey: async (provider) => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { Transport } from "@mariozechner/pi-ai";
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
||||||
|
|
@ -40,6 +41,8 @@ export interface MarkdownSettings {
|
||||||
codeBlockIndent?: string; // default: " "
|
codeBlockIndent?: string; // default: " "
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TransportSetting = Transport;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package source for npm/git packages.
|
* Package source for npm/git packages.
|
||||||
* - String form: load all resources from the package
|
* - String form: load all resources from the package
|
||||||
|
|
@ -60,6 +63,7 @@ export interface Settings {
|
||||||
defaultProvider?: string;
|
defaultProvider?: string;
|
||||||
defaultModel?: string;
|
defaultModel?: string;
|
||||||
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||||
|
transport?: TransportSetting; // default: "sse"
|
||||||
steeringMode?: "all" | "one-at-a-time";
|
steeringMode?: "all" | "one-at-a-time";
|
||||||
followUpMode?: "all" | "one-at-a-time";
|
followUpMode?: "all" | "one-at-a-time";
|
||||||
theme?: string;
|
theme?: string;
|
||||||
|
|
@ -188,6 +192,12 @@ export class SettingsManager {
|
||||||
delete settings.queueMode;
|
delete settings.queueMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate legacy websockets boolean -> transport enum
|
||||||
|
if (!("transport" in settings) && typeof settings.websockets === "boolean") {
|
||||||
|
settings.transport = settings.websockets ? "websocket" : "sse";
|
||||||
|
delete settings.websockets;
|
||||||
|
}
|
||||||
|
|
||||||
// Migrate old skills object format to new array format
|
// Migrate old skills object format to new array format
|
||||||
if (
|
if (
|
||||||
"skills" in settings &&
|
"skills" in settings &&
|
||||||
|
|
@ -433,6 +443,16 @@ export class SettingsManager {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTransport(): TransportSetting {
|
||||||
|
return this.settings.transport ?? "sse";
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransport(transport: TransportSetting): void {
|
||||||
|
this.globalSettings.transport = transport;
|
||||||
|
this.markModified("transport");
|
||||||
|
this.save();
|
||||||
|
}
|
||||||
|
|
||||||
getCompactionEnabled(): boolean {
|
getCompactionEnabled(): boolean {
|
||||||
return this.settings.compaction?.enabled ?? true;
|
return this.settings.compaction?.enabled ?? true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||||
|
import type { Transport } from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
getCapabilities,
|
getCapabilities,
|
||||||
|
|
@ -29,6 +30,7 @@ export interface SettingsConfig {
|
||||||
enableSkillCommands: boolean;
|
enableSkillCommands: boolean;
|
||||||
steeringMode: "all" | "one-at-a-time";
|
steeringMode: "all" | "one-at-a-time";
|
||||||
followUpMode: "all" | "one-at-a-time";
|
followUpMode: "all" | "one-at-a-time";
|
||||||
|
transport: Transport;
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
availableThinkingLevels: ThinkingLevel[];
|
availableThinkingLevels: ThinkingLevel[];
|
||||||
currentTheme: string;
|
currentTheme: string;
|
||||||
|
|
@ -51,6 +53,7 @@ export interface SettingsCallbacks {
|
||||||
onEnableSkillCommandsChange: (enabled: boolean) => void;
|
onEnableSkillCommandsChange: (enabled: boolean) => void;
|
||||||
onSteeringModeChange: (mode: "all" | "one-at-a-time") => void;
|
onSteeringModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||||
onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
|
onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void;
|
||||||
|
onTransportChange: (transport: Transport) => void;
|
||||||
onThinkingLevelChange: (level: ThinkingLevel) => void;
|
onThinkingLevelChange: (level: ThinkingLevel) => void;
|
||||||
onThemeChange: (theme: string) => void;
|
onThemeChange: (theme: string) => void;
|
||||||
onThemePreview?: (theme: string) => void;
|
onThemePreview?: (theme: string) => void;
|
||||||
|
|
@ -162,6 +165,13 @@ export class SettingsSelectorComponent extends Container {
|
||||||
currentValue: config.followUpMode,
|
currentValue: config.followUpMode,
|
||||||
values: ["one-at-a-time", "all"],
|
values: ["one-at-a-time", "all"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "transport",
|
||||||
|
label: "Transport",
|
||||||
|
description: "Preferred transport for providers that support multiple transports",
|
||||||
|
currentValue: config.transport,
|
||||||
|
values: ["sse", "websocket", "auto"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "hide-thinking",
|
id: "hide-thinking",
|
||||||
label: "Hide thinking",
|
label: "Hide thinking",
|
||||||
|
|
@ -354,6 +364,9 @@ export class SettingsSelectorComponent extends Container {
|
||||||
case "follow-up-mode":
|
case "follow-up-mode":
|
||||||
callbacks.onFollowUpModeChange(newValue as "all" | "one-at-a-time");
|
callbacks.onFollowUpModeChange(newValue as "all" | "one-at-a-time");
|
||||||
break;
|
break;
|
||||||
|
case "transport":
|
||||||
|
callbacks.onTransportChange(newValue as Transport);
|
||||||
|
break;
|
||||||
case "hide-thinking":
|
case "hide-thinking":
|
||||||
callbacks.onHideThinkingBlockChange(newValue === "true");
|
callbacks.onHideThinkingBlockChange(newValue === "true");
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -3034,6 +3034,7 @@ export class InteractiveMode {
|
||||||
enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
|
enableSkillCommands: this.settingsManager.getEnableSkillCommands(),
|
||||||
steeringMode: this.session.steeringMode,
|
steeringMode: this.session.steeringMode,
|
||||||
followUpMode: this.session.followUpMode,
|
followUpMode: this.session.followUpMode,
|
||||||
|
transport: this.settingsManager.getTransport(),
|
||||||
thinkingLevel: this.session.thinkingLevel,
|
thinkingLevel: this.session.thinkingLevel,
|
||||||
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
availableThinkingLevels: this.session.getAvailableThinkingLevels(),
|
||||||
currentTheme: this.settingsManager.getTheme() || "dark",
|
currentTheme: this.settingsManager.getTheme() || "dark",
|
||||||
|
|
@ -3076,6 +3077,10 @@ export class InteractiveMode {
|
||||||
onFollowUpModeChange: (mode) => {
|
onFollowUpModeChange: (mode) => {
|
||||||
this.session.setFollowUpMode(mode);
|
this.session.setFollowUpMode(mode);
|
||||||
},
|
},
|
||||||
|
onTransportChange: (transport) => {
|
||||||
|
this.settingsManager.setTransport(transport);
|
||||||
|
this.session.agent.setTransport(transport);
|
||||||
|
},
|
||||||
onThinkingLevelChange: (level) => {
|
onThinkingLevelChange: (level) => {
|
||||||
this.session.setThinkingLevel(level);
|
this.session.setThinkingLevel(level);
|
||||||
this.footer.invalidate();
|
this.footer.invalidate();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue