mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
185 lines
6.6 KiB
TypeScript
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,
|
|
});
|
|
}
|
|
}
|
|
}
|