Files
cursor-byok/apps/desktop/src/shared/store/appStore.ts
T
2026-08-30 13:35:03 +08:00

248 lines
7.3 KiB
TypeScript

import { useSyncExternalStore } from "react";
import { api, type CursorHarnessStatus, type LlmCall, type Model, type ModelInput, type Overview, type PluginRuntimeStatus, type PortSettings } from "../api";
import { applyTheme, isThemeId, type ThemeId } from "../theme/theme";
export type AppSnapshot = {
models: Model[];
calls: LlmCall[];
overview: Overview;
detailed: boolean;
ports: PortSettings;
busy: boolean;
error: string | null;
theme: ThemeId;
cursorHarness: CursorHarnessStatus | null;
cursorBusy: boolean;
pluginRuntime: PluginRuntimeStatus | null;
};
const savedTheme = (): ThemeId => {
const saved = localStorage.getItem("cursor-byok.theme");
return isThemeId(saved) ? saved : "default-dark";
};
let snapshot: AppSnapshot = {
models: [],
calls: [],
overview: {
metrics: {
llm_calls: 0,
successful_calls: 0,
failed_calls: 0,
token_usage: 0,
prompt_tokens: 0,
input_tokens: 0,
cache_read_tokens: 0,
cache_write_tokens: 0,
output_tokens: 0,
},
token_usage_granularity: "day",
token_usage_series: [],
},
detailed: false,
ports: { proxy_port: 0, service_port: 0 },
busy: false,
error: null,
theme: savedTheme(),
cursorHarness: null,
cursorBusy: false,
pluginRuntime: null,
};
const listeners = new Set<() => void>();
function update(patch: Partial<AppSnapshot>) {
snapshot = { ...snapshot, ...patch };
listeners.forEach((listener) => listener());
}
async function perform(task: () => Promise<void>) {
update({ error: null });
try {
await task();
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
}
}
export const appStore = {
subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot: () => snapshot,
async refresh() {
update({ busy: true, error: null });
try {
const [models, calls, overview, settings, ports, cursorHarness, pluginRuntime] = await Promise.all([
api.models(),
api.calls(),
api.overview(),
api.observability(),
api.ports(),
api.cursorHarness(),
api.pluginRuntime(),
]);
update({ models, calls, overview, detailed: settings.detailed, ports, cursorHarness, pluginRuntime });
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
} finally {
update({ busy: false });
}
},
async deleteModel(modelHash: string) {
await perform(async () => {
await api.deleteModel(modelHash);
await appStore.refresh();
});
},
async initializeCursorCa() {
update({ cursorBusy: true, error: null });
try {
const status = await api.initializeCursorCa();
update({ cursorHarness: status });
return status;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
} finally { update({ cursorBusy: false }); }
},
async initializePluginRuntime() {
update({ error: null });
try {
const pluginRuntime = await api.initializePluginRuntime();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async refreshPluginRuntime() {
try {
const pluginRuntime = await api.pluginRuntime();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async cancelPluginRuntimeInitialization() {
try {
const pluginRuntime = await api.cancelPluginRuntimeInitialization();
update({ pluginRuntime });
return pluginRuntime;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
}
},
async setCursorEnabled(enabled: boolean) {
update({ cursorBusy: true, error: null });
try { update({ cursorHarness: await api.setCursorEnabled(enabled) }); }
catch (cause) { update({ error: cause instanceof Error ? cause.message : String(cause) }); }
finally { update({ cursorBusy: false }); }
},
async createModels(models: ModelInput[]) {
update({ cursorBusy: true, error: null });
try {
const created = await api.createModels(models);
await appStore.refresh();
return created;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
} finally { update({ cursorBusy: false }); }
},
async importV0049Models() {
update({ cursorBusy: true, error: null });
try {
const result = await api.importV0049Models();
await appStore.refresh();
return result;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
} finally { update({ cursorBusy: false }); }
},
async updateCursorModel(hash: string, model: ModelInput) {
update({ cursorBusy: true, error: null });
try {
const updated = await api.updateModel(hash, model);
await appStore.refresh();
return updated;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return null;
} finally { update({ cursorBusy: false }); }
},
async reorderCursorModels(modelHashes: string[]) {
const previous = snapshot.models;
const byHash = new Map(previous.map((model) => [model.model_hash, model]));
if (modelHashes.length !== previous.length || new Set(modelHashes).size !== previous.length) {
update({ error: t("模型配置已发生变化,请刷新后重试") });
return false;
}
const reordered: Model[] = [];
for (const [index, hash] of modelHashes.entries()) {
const model = byHash.get(hash);
if (!model) {
update({ error: t("模型配置已发生变化,请刷新后重试") });
return false;
}
reordered.push({ ...model, sort_order: index + 1 });
}
update({ models: reordered, cursorBusy: true, error: null });
try {
update({ models: await api.reorderModels(modelHashes) });
return true;
} catch (cause) {
update({
models: previous,
error: cause instanceof Error ? cause.message : String(cause),
});
return false;
} finally {
update({ cursorBusy: false });
}
},
async refreshCalls() {
try {
update({ calls: await api.calls() });
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
}
},
async openCallDetails(callId: string) {
await perform(() => api.openCallDetails(callId));
},
async updateDetailed(detailed: boolean) {
await perform(async () => update(await api.setObservability(detailed)));
},
async updatePorts(ports: PortSettings) {
try {
update({ error: null });
update({ ports: await api.setPorts(ports) });
return true;
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
return false;
}
},
selectTheme(theme: ThemeId) {
localStorage.setItem("cursor-byok.theme", theme);
applyTheme(theme);
update({ theme });
},
};
export function useAppStore() {
return useSyncExternalStore(appStore.subscribe, appStore.getSnapshot);
}