Restore features lost during main merge and fix workspace setup flow
Restores all features dropped when merging main into dev: - Keyboard shortcuts (send, dictation-confirm) with full e2e chain - Feature toggles (plan/fast mode) in agent status bar - Sidebar kebab menu (Remove Project) on project rows - Combined model selector (sticky header, sizing, favorites sort, search) - Question form Enter key submit with guard - Input focus restoration (composer + AgentStatusBar onDropdownClose) - Draft-agent feature toggles and focus restoration - Provider diagnostics UI (settings section, diagnostic sheet, status badge) - Provider diagnostics server (snapshot manager, diagnostic utils, RPC) Fixes dev-branch regressions: - New workspace button now opens composer in setup dialog via beginWorkspaceSetup - Setup tab auto-opens when workspace setup is running
This commit is contained in:
@@ -42,6 +42,9 @@ import type {
|
||||
ListProviderModelsResponseMessage,
|
||||
ListProviderModesResponseMessage,
|
||||
ListAvailableProvidersResponse,
|
||||
GetProvidersSnapshotResponseMessage,
|
||||
RefreshProvidersSnapshotResponseMessage,
|
||||
ProviderDiagnosticResponseMessage,
|
||||
ListTerminalsResponse,
|
||||
CreateTerminalResponse,
|
||||
SubscribeTerminalResponse,
|
||||
@@ -152,6 +155,10 @@ export type DaemonEvent =
|
||||
requestId: string;
|
||||
resolution: AgentPermissionResponse;
|
||||
}
|
||||
| {
|
||||
type: "providers_snapshot_update";
|
||||
payload: Extract<SessionOutboundMessage, { type: "providers_snapshot_update" }>["payload"];
|
||||
}
|
||||
| { type: "error"; message: string };
|
||||
|
||||
export type DaemonEventHandler = (event: DaemonEvent) => void;
|
||||
@@ -228,6 +235,9 @@ type ListProviderFeaturesPayload = ListProviderFeaturesResponseMessage["payload"
|
||||
type ListProviderModelsPayload = ListProviderModelsResponseMessage["payload"];
|
||||
type ListProviderModesPayload = ListProviderModesResponseMessage["payload"];
|
||||
type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
|
||||
type GetProvidersSnapshotPayload = GetProvidersSnapshotResponseMessage["payload"];
|
||||
type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage["payload"];
|
||||
type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"];
|
||||
type ListCommandsPayload = ListCommandsResponse["payload"];
|
||||
type ListCommandsDraftConfig = Pick<
|
||||
AgentSessionConfig,
|
||||
@@ -2574,6 +2584,51 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async getProvidersSnapshot(options?: {
|
||||
cwd?: string;
|
||||
requestId?: string;
|
||||
}): Promise<GetProvidersSnapshotPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "get_providers_snapshot_request",
|
||||
cwd: options?.cwd,
|
||||
},
|
||||
responseType: "get_providers_snapshot_response",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
async refreshProvidersSnapshot(options?: {
|
||||
cwd?: string;
|
||||
requestId?: string;
|
||||
}): Promise<RefreshProvidersSnapshotPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "refresh_providers_snapshot_request",
|
||||
cwd: options?.cwd,
|
||||
},
|
||||
responseType: "refresh_providers_snapshot_response",
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
async getProviderDiagnostic(
|
||||
provider: AgentProvider,
|
||||
options?: { requestId?: string },
|
||||
): Promise<ProviderDiagnosticPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options?.requestId,
|
||||
message: {
|
||||
type: "provider_diagnostic_request",
|
||||
provider,
|
||||
},
|
||||
responseType: "provider_diagnostic_response",
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
async listCommands(agentId: string, requestId?: string): Promise<ListCommandsPayload>;
|
||||
async listCommands(agentId: string, options?: ListCommandsOptions): Promise<ListCommandsPayload>;
|
||||
async listCommands(
|
||||
@@ -3668,6 +3723,11 @@ export class DaemonClient {
|
||||
requestId: msg.payload.requestId,
|
||||
resolution: msg.payload.resolution,
|
||||
};
|
||||
case "providers_snapshot_update":
|
||||
return {
|
||||
type: "providers_snapshot_update",
|
||||
payload: msg.payload,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ export type AgentMode = {
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type ProviderStatus = "ready" | "loading" | "error" | "unavailable";
|
||||
|
||||
export type AgentModelDefinition = {
|
||||
provider: AgentProvider;
|
||||
id: string;
|
||||
@@ -64,6 +66,15 @@ export type AgentSelectOption = {
|
||||
metadata?: AgentMetadata;
|
||||
};
|
||||
|
||||
export interface ProviderSnapshotEntry {
|
||||
provider: AgentProvider;
|
||||
status: ProviderStatus;
|
||||
error?: string;
|
||||
models?: AgentModelDefinition[];
|
||||
modes?: AgentMode[];
|
||||
fetchedAt?: string;
|
||||
}
|
||||
|
||||
export type AgentFeatureToggle = {
|
||||
type: "toggle";
|
||||
id: string;
|
||||
@@ -469,4 +480,5 @@ export interface AgentClient {
|
||||
* Returns true if available, false otherwise.
|
||||
*/
|
||||
isAvailable(): Promise<boolean>;
|
||||
getDiagnostic?(): Promise<{ diagnostic: string }>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import type {
|
||||
AgentProvider,
|
||||
ProviderSnapshotEntry,
|
||||
} from "./agent-sdk-types.js";
|
||||
import type { ProviderDefinition } from "./provider-registry.js";
|
||||
import { AGENT_PROVIDER_IDS } from "./provider-manifest.js";
|
||||
|
||||
const DEFAULT_CWD_KEY = "__default__";
|
||||
|
||||
type ProviderSnapshotChangeListener = (
|
||||
entries: ProviderSnapshotEntry[],
|
||||
cwd?: string,
|
||||
) => void;
|
||||
|
||||
export class ProviderSnapshotManager {
|
||||
private readonly snapshots = new Map<string, Map<AgentProvider, ProviderSnapshotEntry>>();
|
||||
private readonly warmUps = new Map<string, Promise<void>>();
|
||||
private readonly events = new EventEmitter();
|
||||
private destroyed = false;
|
||||
|
||||
constructor(
|
||||
private readonly providerRegistry: Record<AgentProvider, ProviderDefinition>,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
getSnapshot(cwd?: string): ProviderSnapshotEntry[] {
|
||||
const cwdKey = normalizeCwdKey(cwd);
|
||||
const entries = this.snapshots.get(cwdKey);
|
||||
if (!entries) {
|
||||
const loadingEntries = this.createLoadingEntries();
|
||||
this.snapshots.set(cwdKey, loadingEntries);
|
||||
void this.warmUp(cwd);
|
||||
return entriesToArray(loadingEntries);
|
||||
}
|
||||
return entriesToArray(entries);
|
||||
}
|
||||
|
||||
refresh(cwd?: string): void {
|
||||
const cwdKey = normalizeCwdKey(cwd);
|
||||
this.snapshots.set(cwdKey, this.createLoadingEntries());
|
||||
void this.warmUp(cwd);
|
||||
}
|
||||
|
||||
on(event: "change", listener: ProviderSnapshotChangeListener): this {
|
||||
this.events.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
off(event: "change", listener: ProviderSnapshotChangeListener): this {
|
||||
this.events.off(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.destroyed = true;
|
||||
this.events.removeAllListeners();
|
||||
this.snapshots.clear();
|
||||
this.warmUps.clear();
|
||||
}
|
||||
|
||||
private createLoadingEntries(): Map<AgentProvider, ProviderSnapshotEntry> {
|
||||
const entries = new Map<AgentProvider, ProviderSnapshotEntry>();
|
||||
for (const provider of this.getProviderIds()) {
|
||||
entries.set(provider, {
|
||||
provider,
|
||||
status: "loading",
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private async warmUp(cwd?: string): Promise<void> {
|
||||
const cwdKey = normalizeCwdKey(cwd);
|
||||
const inFlight = this.warmUps.get(cwdKey);
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const warmUpPromise = Promise.allSettled(
|
||||
this.getProviderIds().map((provider) => this.refreshProvider(cwdKey, provider, cwd)),
|
||||
).then(() => undefined);
|
||||
|
||||
this.warmUps.set(cwdKey, warmUpPromise);
|
||||
|
||||
try {
|
||||
await warmUpPromise;
|
||||
} finally {
|
||||
if (this.warmUps.get(cwdKey) === warmUpPromise) {
|
||||
this.warmUps.delete(cwdKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshProvider(
|
||||
cwdKey: string,
|
||||
provider: AgentProvider,
|
||||
cwd?: string,
|
||||
): Promise<void> {
|
||||
const definition = this.providerRegistry[provider];
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = this.getOrCreateSnapshot(cwdKey);
|
||||
snapshot.set(provider, {
|
||||
provider,
|
||||
status: "loading",
|
||||
});
|
||||
|
||||
try {
|
||||
const client = definition.createClient(this.logger);
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
snapshot.set(provider, {
|
||||
provider,
|
||||
status: "unavailable",
|
||||
});
|
||||
this.emitChange(cwdKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const [models, modes] = await Promise.all([
|
||||
definition.fetchModels({ cwd }),
|
||||
definition.fetchModes({ cwd }),
|
||||
]);
|
||||
|
||||
snapshot.set(provider, {
|
||||
provider,
|
||||
status: "ready",
|
||||
models,
|
||||
modes,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
this.emitChange(cwdKey);
|
||||
} catch (error) {
|
||||
snapshot.set(provider, {
|
||||
provider,
|
||||
status: "error",
|
||||
error: toErrorMessage(error),
|
||||
});
|
||||
this.logger.warn({ err: error, provider, cwd: cwdKey }, "Failed to refresh provider snapshot");
|
||||
this.emitChange(cwdKey);
|
||||
}
|
||||
}
|
||||
|
||||
private emitChange(cwdKey: string): void {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
}
|
||||
const snapshot = this.snapshots.get(cwdKey);
|
||||
if (!snapshot) {
|
||||
return;
|
||||
}
|
||||
this.events.emit("change", entriesToArray(snapshot), denormalizeCwdKey(cwdKey));
|
||||
}
|
||||
|
||||
private getOrCreateSnapshot(cwdKey: string): Map<AgentProvider, ProviderSnapshotEntry> {
|
||||
const existing = this.snapshots.get(cwdKey);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const created = this.createLoadingEntries();
|
||||
this.snapshots.set(cwdKey, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private getProviderIds(): AgentProvider[] {
|
||||
return AGENT_PROVIDER_IDS.filter((provider) => this.providerRegistry[provider]);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCwdKey(cwd?: string): string {
|
||||
if (!cwd) {
|
||||
return DEFAULT_CWD_KEY;
|
||||
}
|
||||
|
||||
const trimmed = cwd.trim();
|
||||
if (!trimmed) {
|
||||
return DEFAULT_CWD_KEY;
|
||||
}
|
||||
|
||||
return resolve(trimmed);
|
||||
}
|
||||
|
||||
function denormalizeCwdKey(cwdKey: string): string | undefined {
|
||||
return cwdKey === DEFAULT_CWD_KEY ? undefined : cwdKey;
|
||||
}
|
||||
|
||||
function entriesToArray(
|
||||
entries: Map<AgentProvider, ProviderSnapshotEntry>,
|
||||
): ProviderSnapshotEntry[] {
|
||||
return Array.from(entries.values(), cloneEntry);
|
||||
}
|
||||
|
||||
function cloneEntry(entry: ProviderSnapshotEntry): ProviderSnapshotEntry {
|
||||
return {
|
||||
...entry,
|
||||
models: entry.models?.map((model) => ({ ...model })),
|
||||
modes: entry.modes?.map((mode) => ({ ...mode })),
|
||||
};
|
||||
}
|
||||
|
||||
function toErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string" && error) {
|
||||
return error;
|
||||
}
|
||||
return "Unknown error";
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
||||
|
||||
type DiagnosticEntry = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function formatProviderDiagnostic(
|
||||
providerName: string,
|
||||
entries: DiagnosticEntry[],
|
||||
): string {
|
||||
return [providerName, ...entries.map((entry) => ` ${entry.label}: ${entry.value}`)].join("\n");
|
||||
}
|
||||
|
||||
export function formatProviderDiagnosticError(
|
||||
providerName: string,
|
||||
error: unknown,
|
||||
): string {
|
||||
return formatProviderDiagnostic(providerName, [
|
||||
{
|
||||
label: "Error",
|
||||
value: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function formatAvailabilityStatus(available: boolean): string {
|
||||
return available ? "Available" : "Unavailable";
|
||||
}
|
||||
|
||||
export function formatDiagnosticStatus(
|
||||
available: boolean,
|
||||
error?: { source: string; cause: unknown },
|
||||
): string {
|
||||
if (error) {
|
||||
return `Error (${error.source} failed: ${toDiagnosticErrorMessage(error.cause)})`;
|
||||
}
|
||||
return formatAvailabilityStatus(available);
|
||||
}
|
||||
|
||||
export function toDiagnosticErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
if (typeof error === "string" && error.trim().length > 0) {
|
||||
return error;
|
||||
}
|
||||
return "Unknown error";
|
||||
}
|
||||
|
||||
export function resolveBinaryVersion(binaryPath: string): string {
|
||||
try {
|
||||
return (
|
||||
execFileSync(binaryPath, ["--version"], {
|
||||
encoding: "utf8",
|
||||
timeout: 5_000,
|
||||
}).trim() || "unknown"
|
||||
);
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatConfiguredCommand(
|
||||
defaultArgv: readonly string[],
|
||||
runtimeSettings?: ProviderRuntimeSettings,
|
||||
): string {
|
||||
const command = runtimeSettings?.command;
|
||||
if (!command || command.mode === "default") {
|
||||
return `${defaultArgv.join(" ")} (default)`;
|
||||
}
|
||||
|
||||
if (command.mode === "append") {
|
||||
return [defaultArgv[0], ...(command.args ?? []), ...defaultArgv.slice(1)].join(" ");
|
||||
}
|
||||
|
||||
return command.argv.join(" ");
|
||||
}
|
||||
@@ -68,6 +68,7 @@ export type AgentMcpTransportFactory = () => Promise<Transport>;
|
||||
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { AgentManager } from "./agent/agent-manager.js";
|
||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||
import type {
|
||||
AgentTimelineCursor,
|
||||
AgentTimelineFetchDirection,
|
||||
@@ -96,6 +97,7 @@ import type {
|
||||
AgentStreamEvent,
|
||||
AgentProvider,
|
||||
AgentPersistenceHandle,
|
||||
ProviderSnapshotEntry,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
||||
import type { AgentSnapshotStore } from "./agent/agent-snapshot-store.js";
|
||||
@@ -392,6 +394,7 @@ export type SessionOptions = {
|
||||
stt: Resolvable<SpeechToTextProvider | null>;
|
||||
tts: Resolvable<TextToSpeechProvider | null>;
|
||||
terminalManager: TerminalManager | null;
|
||||
providerSnapshotManager?: ProviderSnapshotManager;
|
||||
serviceRouteStore?: ServiceRouteStore;
|
||||
getDaemonTcpPort?: () => number | null;
|
||||
resolveServiceStatus?: (hostname: string) => "running" | "stopped" | null;
|
||||
@@ -568,6 +571,8 @@ export class Session {
|
||||
} | null = null;
|
||||
private readonly MOBILE_BACKGROUND_STREAM_GRACE_MS = 60_000;
|
||||
private readonly terminalManager: TerminalManager | null;
|
||||
private readonly providerSnapshotManager: ProviderSnapshotManager | null;
|
||||
private unsubscribeProviderSnapshotEvents: (() => void) | null = null;
|
||||
private readonly serviceRouteStore: ServiceRouteStore | null;
|
||||
private readonly getDaemonTcpPort: (() => number | null) | null;
|
||||
private readonly resolveServiceStatus: ((hostname: string) => "running" | "stopped" | null) | null;
|
||||
@@ -625,6 +630,7 @@ export class Session {
|
||||
stt,
|
||||
tts,
|
||||
terminalManager,
|
||||
providerSnapshotManager,
|
||||
serviceRouteStore,
|
||||
getDaemonTcpPort,
|
||||
resolveServiceStatus,
|
||||
@@ -665,6 +671,7 @@ export class Session {
|
||||
});
|
||||
this.createAgentMcpTransport = createAgentMcpTransport;
|
||||
this.terminalManager = terminalManager;
|
||||
this.providerSnapshotManager = providerSnapshotManager ?? null;
|
||||
this.serviceRouteStore = serviceRouteStore ?? null;
|
||||
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
|
||||
this.resolveServiceStatus = resolveServiceStatus ?? null;
|
||||
@@ -673,6 +680,25 @@ export class Session {
|
||||
this.handleTerminalsChanged(event),
|
||||
);
|
||||
}
|
||||
if (this.providerSnapshotManager) {
|
||||
const handleProviderSnapshotChange = (entries: ProviderSnapshotEntry[], cwd?: string) => {
|
||||
const visibleEntries = entries.filter((entry) =>
|
||||
this.isProviderVisibleToClient(entry.provider),
|
||||
);
|
||||
this.emit({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd,
|
||||
entries: visibleEntries,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
};
|
||||
this.providerSnapshotManager.on("change", handleProviderSnapshotChange);
|
||||
this.unsubscribeProviderSnapshotEvents = () => {
|
||||
this.providerSnapshotManager?.off("change", handleProviderSnapshotChange);
|
||||
};
|
||||
}
|
||||
this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
|
||||
this.resolveVoiceTurnDetection = toResolver(voice?.turnDetection ?? null);
|
||||
this.registerVoiceSpeakHandler = voiceBridge?.registerVoiceSpeakHandler;
|
||||
@@ -1624,6 +1650,18 @@ export class Session {
|
||||
await this.handleListAvailableProvidersRequest(msg);
|
||||
break;
|
||||
|
||||
case "get_providers_snapshot_request":
|
||||
await this.handleGetProvidersSnapshotRequest(msg);
|
||||
break;
|
||||
|
||||
case "refresh_providers_snapshot_request":
|
||||
await this.handleRefreshProvidersSnapshotRequest(msg);
|
||||
break;
|
||||
|
||||
case "provider_diagnostic_request":
|
||||
await this.handleProviderDiagnosticRequest(msg);
|
||||
break;
|
||||
|
||||
case "clear_agent_attention":
|
||||
await this.handleClearAgentAttention(msg.agentId);
|
||||
break;
|
||||
@@ -3099,6 +3137,72 @@ export class Session {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleGetProvidersSnapshotRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "get_providers_snapshot_request" }>,
|
||||
): Promise<void> {
|
||||
const entries = this.providerSnapshotManager
|
||||
? this.providerSnapshotManager
|
||||
.getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined)
|
||||
.filter((entry) => this.isProviderVisibleToClient(entry.provider))
|
||||
: [];
|
||||
|
||||
this.emit({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
entries,
|
||||
generatedAt: new Date().toISOString(),
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRefreshProvidersSnapshotRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "refresh_providers_snapshot_request" }>,
|
||||
): Promise<void> {
|
||||
this.providerSnapshotManager?.refresh(msg.cwd ? expandTilde(msg.cwd) : undefined);
|
||||
this.emit({
|
||||
type: "refresh_providers_snapshot_response",
|
||||
payload: {
|
||||
acknowledged: true,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async handleProviderDiagnosticRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "provider_diagnostic_request" }>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = this.providerRegistry[msg.provider].createClient(this.sessionLogger);
|
||||
const diagnostic = client.getDiagnostic
|
||||
? (await client.getDiagnostic()).diagnostic
|
||||
: "No diagnostic available for this provider.";
|
||||
this.emit({
|
||||
type: "provider_diagnostic_response",
|
||||
payload: {
|
||||
provider: msg.provider,
|
||||
diagnostic,
|
||||
requestId: msg.requestId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const err = error instanceof Error ? error : new Error(String(error));
|
||||
this.sessionLogger.error(
|
||||
{ err, provider: msg.provider },
|
||||
`Failed to get provider diagnostic for ${msg.provider}`,
|
||||
);
|
||||
this.emit({
|
||||
type: "rpc_error",
|
||||
payload: {
|
||||
requestId: msg.requestId,
|
||||
requestType: msg.type,
|
||||
error: `Failed to get provider diagnostic: ${err.message}`,
|
||||
code: "provider_diagnostic_failed",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private assertSafeGitRef(ref: string, label: string): void {
|
||||
if (!/^[A-Za-z0-9._/-]+$/.test(ref)) {
|
||||
throw new Error(`Invalid ${label}: ${ref}`);
|
||||
@@ -6913,6 +7017,10 @@ export class Session {
|
||||
this.unsubscribeTerminalsChanged();
|
||||
this.unsubscribeTerminalsChanged = null;
|
||||
}
|
||||
if (this.unsubscribeProviderSnapshotEvents) {
|
||||
this.unsubscribeProviderSnapshotEvents();
|
||||
this.unsubscribeProviderSnapshotEvents = null;
|
||||
}
|
||||
this.subscribedTerminalDirectories.clear();
|
||||
|
||||
for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
|
||||
|
||||
@@ -31,6 +31,8 @@ import { isHostAllowed } from "./allowed-hosts.js";
|
||||
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js";
|
||||
import type { AgentProvider } from "./agent/agent-sdk-types.js";
|
||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
||||
import { PushTokenStore } from "./push/token-store.js";
|
||||
import { PushService } from "./push/push-service.js";
|
||||
import type { ServiceRouteStore } from "./service-proxy.js";
|
||||
@@ -258,6 +260,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>();
|
||||
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
|
||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||
private readonly providerSnapshotManager: ProviderSnapshotManager;
|
||||
private readonly onLifecycleIntent: ((intent: SessionLifecycleIntent) => void) | null;
|
||||
private serverCapabilities: ServerCapabilities | undefined;
|
||||
private runtimeWindowStartedAt = Date.now();
|
||||
@@ -351,6 +354,13 @@ export class VoiceAssistantWebSocketServer {
|
||||
this.voice = voice ?? null;
|
||||
this.dictation = dictation ?? null;
|
||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||
const providerSnapshotLogger = this.logger.child({ module: "provider-snapshot-manager" });
|
||||
this.providerSnapshotManager = new ProviderSnapshotManager(
|
||||
buildProviderRegistry(providerSnapshotLogger, {
|
||||
runtimeSettings: this.agentProviderRuntimeSettings,
|
||||
}),
|
||||
providerSnapshotLogger,
|
||||
);
|
||||
this.onLifecycleIntent = onLifecycleIntent ?? null;
|
||||
this.serviceRouteStore = serviceRouteStore ?? null;
|
||||
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
|
||||
@@ -519,6 +529,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
}
|
||||
|
||||
await Promise.all(cleanupPromises);
|
||||
this.providerSnapshotManager.destroy();
|
||||
this.checkoutDiffManager.dispose();
|
||||
this.pendingConnections.clear();
|
||||
this.sessions.clear();
|
||||
@@ -669,6 +680,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
stt: () => this.speech?.resolveStt() ?? null,
|
||||
tts: () => this.speech?.resolveTts() ?? null,
|
||||
terminalManager: this.terminalManager,
|
||||
providerSnapshotManager: this.providerSnapshotManager,
|
||||
serviceRouteStore: this.serviceRouteStore ?? undefined,
|
||||
getDaemonTcpPort: this.getDaemonTcpPort ?? undefined,
|
||||
resolveServiceStatus: this.resolveServiceStatus ?? undefined,
|
||||
|
||||
@@ -54,6 +54,8 @@ import type {
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
ProviderSnapshotEntry,
|
||||
ProviderStatus,
|
||||
AgentRuntimeInfo,
|
||||
AgentTimelineItem,
|
||||
ToolCallDetail,
|
||||
@@ -69,6 +71,13 @@ const AgentModeSchema: z.ZodType<AgentMode> = z.object({
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
const ProviderStatusSchema: z.ZodType<ProviderStatus> = z.enum([
|
||||
"ready",
|
||||
"loading",
|
||||
"error",
|
||||
"unavailable",
|
||||
]);
|
||||
|
||||
const AgentSelectOptionSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
@@ -114,6 +123,15 @@ const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z.object({
|
||||
defaultThinkingOptionId: z.string().optional(),
|
||||
});
|
||||
|
||||
const ProviderSnapshotEntrySchema: z.ZodType<ProviderSnapshotEntry> = z.object({
|
||||
provider: AgentProviderSchema,
|
||||
status: ProviderStatusSchema,
|
||||
error: z.string().optional(),
|
||||
models: z.array(AgentModelDefinitionSchema).optional(),
|
||||
modes: z.array(AgentModeSchema).optional(),
|
||||
fetchedAt: z.string().optional(),
|
||||
});
|
||||
|
||||
const AgentCapabilityFlagsSchema: z.ZodType<AgentCapabilityFlags> = z.object({
|
||||
supportsStreaming: z.boolean(),
|
||||
supportsSessionPersistence: z.boolean(),
|
||||
@@ -776,6 +794,24 @@ export const ListAvailableProvidersRequestMessageSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const GetProvidersSnapshotRequestMessageSchema = z.object({
|
||||
type: z.literal("get_providers_snapshot_request"),
|
||||
cwd: z.string().optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const RefreshProvidersSnapshotRequestMessageSchema = z.object({
|
||||
type: z.literal("refresh_providers_snapshot_request"),
|
||||
cwd: z.string().optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const ProviderDiagnosticRequestMessageSchema = z.object({
|
||||
type: z.literal("provider_diagnostic_request"),
|
||||
provider: AgentProviderSchema,
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const ResumeAgentRequestMessageSchema = z.object({
|
||||
type: z.literal("resume_agent_request"),
|
||||
handle: AgentPersistenceHandleSchema,
|
||||
@@ -1289,6 +1325,9 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ListProviderModesRequestMessageSchema,
|
||||
ListProviderFeaturesRequestMessageSchema,
|
||||
ListAvailableProvidersRequestMessageSchema,
|
||||
GetProvidersSnapshotRequestMessageSchema,
|
||||
RefreshProvidersSnapshotRequestMessageSchema,
|
||||
ProviderDiagnosticRequestMessageSchema,
|
||||
ResumeAgentRequestMessageSchema,
|
||||
RefreshAgentRequestMessageSchema,
|
||||
CancelAgentRequestMessageSchema,
|
||||
@@ -2284,6 +2323,41 @@ export const ListAvailableProvidersResponseSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const GetProvidersSnapshotResponseMessageSchema = z.object({
|
||||
type: z.literal("get_providers_snapshot_response"),
|
||||
payload: z.object({
|
||||
entries: z.array(ProviderSnapshotEntrySchema),
|
||||
generatedAt: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProvidersSnapshotUpdateMessageSchema = z.object({
|
||||
type: z.literal("providers_snapshot_update"),
|
||||
payload: z.object({
|
||||
cwd: z.string().optional(),
|
||||
entries: z.array(ProviderSnapshotEntrySchema),
|
||||
generatedAt: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const RefreshProvidersSnapshotResponseMessageSchema = z.object({
|
||||
type: z.literal("refresh_providers_snapshot_response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
acknowledged: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProviderDiagnosticResponseMessageSchema = z.object({
|
||||
type: z.literal("provider_diagnostic_response"),
|
||||
payload: z.object({
|
||||
provider: AgentProviderSchema,
|
||||
diagnostic: z.string(),
|
||||
requestId: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const AgentSlashCommandSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
@@ -2482,6 +2556,10 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
ListProviderModesResponseMessageSchema,
|
||||
ListProviderFeaturesResponseMessageSchema,
|
||||
ListAvailableProvidersResponseSchema,
|
||||
GetProvidersSnapshotResponseMessageSchema,
|
||||
ProvidersSnapshotUpdateMessageSchema,
|
||||
RefreshProvidersSnapshotResponseMessageSchema,
|
||||
ProviderDiagnosticResponseMessageSchema,
|
||||
ListCommandsResponseSchema,
|
||||
ListTerminalsResponseSchema,
|
||||
TerminalsChangedSchema,
|
||||
@@ -2568,6 +2646,16 @@ export type ListProviderFeaturesResponseMessage = z.infer<
|
||||
typeof ListProviderFeaturesResponseMessageSchema
|
||||
>;
|
||||
export type ListAvailableProvidersResponse = z.infer<typeof ListAvailableProvidersResponseSchema>;
|
||||
export type GetProvidersSnapshotResponseMessage = z.infer<
|
||||
typeof GetProvidersSnapshotResponseMessageSchema
|
||||
>;
|
||||
export type ProvidersSnapshotUpdateMessage = z.infer<typeof ProvidersSnapshotUpdateMessageSchema>;
|
||||
export type RefreshProvidersSnapshotResponseMessage = z.infer<
|
||||
typeof RefreshProvidersSnapshotResponseMessageSchema
|
||||
>;
|
||||
export type ProviderDiagnosticResponseMessage = z.infer<
|
||||
typeof ProviderDiagnosticResponseMessageSchema
|
||||
>;
|
||||
export type ChatCreateResponse = z.infer<typeof ChatCreateResponseSchema>;
|
||||
export type ChatListResponse = z.infer<typeof ChatListResponseSchema>;
|
||||
export type ChatInspectResponse = z.infer<typeof ChatInspectResponseSchema>;
|
||||
@@ -2615,6 +2703,15 @@ export type ListProviderFeaturesRequestMessage = z.infer<
|
||||
export type ListAvailableProvidersRequestMessage = z.infer<
|
||||
typeof ListAvailableProvidersRequestMessageSchema
|
||||
>;
|
||||
export type GetProvidersSnapshotRequestMessage = z.infer<
|
||||
typeof GetProvidersSnapshotRequestMessageSchema
|
||||
>;
|
||||
export type RefreshProvidersSnapshotRequestMessage = z.infer<
|
||||
typeof RefreshProvidersSnapshotRequestMessageSchema
|
||||
>;
|
||||
export type ProviderDiagnosticRequestMessage = z.infer<
|
||||
typeof ProviderDiagnosticRequestMessageSchema
|
||||
>;
|
||||
export type ChatCreateRequest = z.infer<typeof ChatCreateRequestSchema>;
|
||||
export type ChatListRequest = z.infer<typeof ChatListRequestSchema>;
|
||||
export type ChatInspectRequest = z.infer<typeof ChatInspectRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user