mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:40:50 +08:00
248 lines
7.3 KiB
TypeScript
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);
|
|
}
|