murashit_codex-panel/src/panel/session-controller.ts
2026-05-17 15:28:06 +09:00

185 lines
6.6 KiB
TypeScript

import type { AppServerClient } from "../app-server/client";
import {
capabilityProbeError,
capabilityProbeOk,
upsertMcpServerDiagnostic,
type CapabilityProbeMethod,
} from "../app-server/compatibility";
import type { McpServerStatus } from "../generated/app-server/v2/McpServerStatus";
import { requestedOrConfiguredServiceTier, type RuntimeSnapshot } from "../runtime/state";
import type { PanelState } from "./state";
export interface PanelSessionControllerHost {
state: PanelState;
vaultPath: string;
currentClient: () => AppServerClient | null;
runtimeSnapshot: () => RuntimeSnapshot;
forceMessagesToBottom: () => void;
}
export class PanelSessionController {
constructor(private readonly host: PanelSessionControllerHost) {}
async refreshThreadList(): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
const response = await client.listThreads(this.host.vaultPath);
this.host.state.listedThreads = response.data;
this.host.state.threadsLoaded = true;
}
async refreshSessionMetadata(): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
this.host.state.effectiveConfig = await client.readEffectiveConfig(this.host.vaultPath);
await this.refreshModels();
await this.refreshSkills();
await this.refreshRateLimits();
}
async startThread(): Promise<Awaited<ReturnType<AppServerClient["startThread"]>> | null> {
const client = this.host.currentClient();
if (!client) return null;
const serviceTier = requestedOrConfiguredServiceTier(this.host.runtimeSnapshot());
const response = await client.startThread(this.host.vaultPath, serviceTier);
this.host.state.activeThreadId = response.thread.id;
this.host.state.activeThreadCwd = response.cwd ?? response.thread.cwd ?? this.host.vaultPath;
this.host.state.activeTurnId = null;
this.host.state.activeModel = response.model ?? null;
this.host.state.activeServiceTier = response.serviceTier ?? null;
this.host.state.activeThreadCliVersion = response.thread.cliVersion ?? null;
this.host.state.tokenUsage = null;
this.host.state.historyCursor = null;
this.host.state.turnDiffs.clear();
this.host.forceMessagesToBottom();
return response;
}
async refreshModels(): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
try {
const response = await client.listModels(false);
this.host.state.availableModels = response.data;
} catch {
this.host.state.availableModels = [];
}
}
async refreshSkills(forceReload = false): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
try {
const response = await client.listSkills(this.host.vaultPath, forceReload);
this.host.state.availableSkills = response.data.flatMap((entry) => entry.skills).filter((skill) => skill.enabled);
} catch {
this.host.state.availableSkills = [];
}
}
async refreshRateLimits(): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
try {
const response = await client.readAccountRateLimits();
this.host.state.rateLimit = response.rateLimitsByLimitId?.codex ?? response.rateLimits ?? null;
} catch {
this.host.state.rateLimit = null;
}
}
async refreshCapabilityDiagnostics(): Promise<void> {
const client = this.host.currentClient();
if (!client) return;
await Promise.all([
this.probeCapability(
"model/list",
() => client.listModels(false),
(response) => `${response.data.length} models`,
),
this.probeCapability(
"skills/list",
() => client.listSkills(this.host.vaultPath),
(response) => {
const count = response.data.reduce((total, entry) => total + entry.skills.length, 0);
return `${count} skills`;
},
),
this.probeCapability(
"hooks/list",
() => client.listHooks(this.host.vaultPath),
(response) => {
const count = response.data.reduce((total, entry) => total + entry.hooks.length, 0);
return `${count} hooks`;
},
),
this.probeCapability(
"account/rateLimits/read",
() => client.readAccountRateLimits(),
(response) => (response.rateLimitsByLimitId ? `${Object.keys(response.rateLimitsByLimitId).length} limits` : "available"),
),
this.probeCapability(
"mcpServerStatus/list",
() => client.listMcpServerStatus(),
(response) => {
this.recordMcpServerStatus(response.data);
const issueCount = response.data.filter((server) => server.authStatus === "notLoggedIn").length;
return issueCount > 0 ? `${response.data.length} servers, ${issueCount} auth issues` : `${response.data.length} servers`;
},
),
this.probeCapability(
"collaborationMode/list",
() => client.listCollaborationModes(),
(response) => `${response.data.length} modes`,
),
this.probeCapability(
"modelProvider/capabilities/read",
() => client.readModelProviderCapabilities(),
(response) =>
[
response.namespaceTools ? "namespace tools" : null,
response.imageGeneration ? "image generation" : null,
response.webSearch ? "web search" : null,
]
.filter(Boolean)
.join(", ") || "no optional capabilities",
),
]);
}
recordMcpStartupStatus(name: string, startupStatus: "starting" | "ready" | "failed" | "cancelled", message: string | null): void {
this.host.state.appServerDiagnostics = upsertMcpServerDiagnostic(this.host.state.appServerDiagnostics, {
name,
startupStatus,
authStatus: null,
toolCount: null,
message,
});
}
private async probeCapability<T>(
method: CapabilityProbeMethod,
request: () => Promise<T>,
summarize: (response: T) => string | null,
): Promise<void> {
try {
const response = await request();
this.host.state.appServerDiagnostics.probes[method] = capabilityProbeOk(method, summarize(response));
} catch (error) {
this.host.state.appServerDiagnostics.probes[method] = capabilityProbeError(method, error);
}
}
private recordMcpServerStatus(servers: McpServerStatus[]): void {
for (const server of servers) {
this.host.state.appServerDiagnostics = upsertMcpServerDiagnostic(this.host.state.appServerDiagnostics, {
name: server.name,
startupStatus: "unknown",
authStatus: server.authStatus,
toolCount: Object.keys(server.tools ?? {}).length,
message: null,
});
}
}
}