feat: enhance Codex agent connection handling with automatic thread recovery and improved error logging

This commit is contained in:
HouYunFei
2026-07-05 23:40:54 +08:00
parent 6113986637
commit 3080d6da10
9 changed files with 154 additions and 37 deletions
+27 -9
View File
@@ -35,8 +35,16 @@ async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: Age
try {
files = await writeAttachmentFiles(attachments);
codexApp ||= await CodexAppClient.start(emit);
const threadId = await ensureCodexThread(codexApp, options);
await codexApp.startTurn(threadId, prompt, files);
let threadId = await ensureCodexThread(codexApp, options, emit);
try {
await codexApp.startTurn(threadId, prompt, files);
} catch (error) {
if (!isRecoverableThreadError(error)) throw error;
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
codexThreadId = "";
threadId = await ensureCodexThread(codexApp, { cwd: options.cwd }, emit);
await codexApp.startTurn(threadId, prompt, files);
}
} catch (error) {
emit("agent_error", { message: errorMessage(error) });
} finally {
@@ -96,14 +104,20 @@ export function runClaudeTurn(prompt: string, emit: AgentEmit) {
pipeJsonLines(child, emit, "claude");
}
async function ensureCodexThread(app: CodexAppClient, options: CodexRunOptions) {
async function ensureCodexThread(app: CodexAppClient, options: CodexRunOptions, emit: AgentEmit) {
if (options.threadId) {
const result = await app.readThread(options.threadId, false);
assertThreadWorkspace(field(result, "thread") || {}, options.cwd);
const thread = await app.resumeThread(options.threadId, options.cwd);
assertThreadWorkspace(thread, options.cwd);
codexThreadId = String(field(thread, "id") || options.threadId);
return codexThreadId;
if (options.threadId === codexThreadId) return codexThreadId;
try {
const result = await app.readThread(options.threadId, false);
assertThreadWorkspace(field(result, "thread") || {}, options.cwd);
const thread = await app.resumeThread(options.threadId, options.cwd);
assertThreadWorkspace(thread, options.cwd);
codexThreadId = String(field(thread, "id") || options.threadId);
return codexThreadId;
} catch (error) {
if (!isRecoverableThreadError(error)) throw error;
emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` });
}
}
if (!codexThreadId) {
const thread = await app.startThread(options.cwd);
@@ -112,6 +126,10 @@ async function ensureCodexThread(app: CodexAppClient, options: CodexRunOptions)
return codexThreadId;
}
function isRecoverableThreadError(error: unknown) {
return /thread not loaded|no rollout found/i.test(errorMessage(error));
}
class CodexAppClient {
private nextId = 1;
private buffer = "";
+3 -2
View File
@@ -18,13 +18,14 @@ export class CanvasSession {
openEvents(url: URL, res: ServerResponse) {
const clientId = url.searchParams.get("clientId") || crypto.randomUUID();
const statusOnly = url.searchParams.get("role") === "status";
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
this.clients.set(clientId, res);
if (!statusOnly) this.clients.set(clientId, res);
sendEvent(res, "hello", { ok: true, clientId });
const timer = setInterval(() => sendEvent(res, "ping", { time: Date.now() }), 15000);
res.on("close", () => {
clearInterval(timer);
this.clients.delete(clientId);
if (!statusOnly) this.clients.delete(clientId);
if (this.canvasState?.clientId === clientId) this.canvasState = null;
});
}
@@ -10,6 +10,9 @@ description: 当前版本已实现但仍需人工验证的变更项
- Codex App 插件文档:新增插件安装教程,区分 AI 自动安装、手动安装和本仓库开发调试安装;同步优化插件 README 与技能说明。
- 顶部导航:缩小桌面顶部导航栏高度,导航项下划线和内容垂直对齐需确认。
- 配置与用户偏好:新增独立顶部导航页面,并在页面和弹窗中复用同一套配置面板;新增 Codex Tab,展示插件安装和本地 Agent 启动步骤,可配置本地 Agent 地址、Connect token 和工具执行确认开关。
- Codex 全局连接:配置页可直接连接本地 Canvas Agent,连接状态同步到画布和顶部 OpenAI 状态图标,刷新后会按本地保存的地址和 token 自动尝试重连;需验证已连接、连接中、连接失败、断开和刷新自动重连状态。
- 画布 AgentCodex 旧 thread 在重启后不可用时会自动新建会话并重试,需验证进入画布直接发送消息不会再出现 `thread not loaded` 或 `no rollout found`。
- 画布 Agent:切换不同画布时会清空并重新加载当前 canvasId 对应的 Codex 会话,需验证不会继续显示上一个画布的对话。
- 画布 Agent:右侧 Agent 面板移除网站 / 本机模式切换,固定使用本机 Agent。
- 配置与用户偏好:每个模型渠道新增 OpenAI / Gemini 调用格式选择,默认 OpenAI,切换默认格式时同步更新默认 Base URL;渠道 Tab 增加紧凑醒目的提醒,说明模型是否可选需要到“模型”Tab 选择。
- Gemini 调用格式:支持拉取 Gemini 模型列表,并用于文本问答、在线 Agent 工具调用、基础生图和图生图请求。
@@ -64,14 +64,14 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
setAgentState({ loadingThreads: true });
try {
const data = await fetchAgentJson<AgentThreadsResponse>(endpoint, token, `/agent/codex/threads?canvasId=${encodeURIComponent(projectId)}`);
const current = useCanvasAgentStore.getState();
const nextThreadId = data.workspace?.activeThreadId || "";
setAgentState({
threads: data.data || [],
workspacePath: data.workspace?.workspacePath || current.workspacePath,
activeThreadId: data.workspace?.activeThreadId || current.activeThreadId,
workspacePath: data.workspace?.workspacePath || "",
activeThreadId: nextThreadId,
messages: [],
});
const nextThreadId = data.workspace?.activeThreadId || current.activeThreadId;
if (nextThreadId && !current.messages.length) {
if (nextThreadId) {
const thread = await fetchAgentJson<AgentThreadResponse>(endpoint, token, `/agent/codex/threads/${encodeURIComponent(nextThreadId)}?canvasId=${encodeURIComponent(projectId)}`);
setAgentState({ messages: normalizeHistoryMessages(thread.messages || []) });
}
@@ -152,13 +152,17 @@ export function CanvasLocalAgentPanel({ snapshot, canUndoOps, collapsed, embedde
return () => {
source.close();
connectedRef.current = false;
setAgentState({ connected: false });
};
}, [enabled, endpoint, loadThreads, message, setAgentState, token]);
useEffect(() => {
if (connected) void loadThreads();
}, [connected, loadThreads, snapshot.projectId]);
}, [connected, loadThreads]);
useEffect(() => {
clearAgentSession({ activity: connected ? "切换画布" : activity });
if (connected) void loadThreads();
}, [snapshot.projectId]);
useEffect(() => {
if (!connected) return;
+21 -8
View File
@@ -1,6 +1,6 @@
import { App, Button, Form, Input, Modal, Progress, Select, Switch, Tabs } from "antd";
import { CircleAlert, Cloud, KeyRound, Link2, Plus, RefreshCw, ShieldCheck, Trash2, Wifi } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { ModelPicker } from "@/components/model-picker";
import { fetchChannelModels } from "@/services/api/image";
@@ -8,7 +8,7 @@ import { syncAppDataToWebdav, type AppSyncDomainKey, type AppSyncProgressEvent }
import { testWebdavConnection, WEBDAV_MANIFEST_FILE_NAME } from "@/services/webdav-sync";
import { audioFormatOptions, audioVoiceOptions, normalizeAudioSpeedValue } from "@/lib/audio-generation";
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
import { createModelChannel, defaultBaseUrlForApiFormat, filterModelsByCapability, modelOptionLabel, modelOptionsFromChannels, normalizeModelOptionValue, useConfigStore, type AiConfig, type ApiCallFormat, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
import { createModelChannel, defaultBaseUrlForApiFormat, filterModelsByCapability, modelOptionLabel, modelOptionsFromChannels, normalizeModelOptionValue, useConfigStore, type AiConfig, type ApiCallFormat, type ConfigTabKey, type ModelCapability, type ModelChannel } from "@/stores/use-config-store";
type ModelGroup = {
capability: ModelCapability;
@@ -47,7 +47,7 @@ const webdavDomainLabels: Record<AppSyncDomainKey, string> = {
};
const codexSetupSteps = [
{ title: "安装 Codex 插件", text: "先在 Codex App 安装 Infinite Canvas 插件,插件会注册 MCP 并尝试启动本地 Canvas Agent。" },
{ title: "打开画布 Agent", text: "回到画布右侧 Agent 面板,网页会自动读取 Local URL 和 Connect token。" },
{ title: "连接本地 Agent", text: "在本页填入 Local URL 和 Connect token 后点击连接。" },
{ title: "手动启动备用", text: "如果插件没有自动启动本地服务,在终端运行下面命令。", command: "npx -y @basketikun/canvas-agent" },
];
@@ -61,9 +61,9 @@ function createWebdavDomainProgress(): Record<AppSyncDomainKey, WebdavDomainProg
);
}
export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: boolean }) {
export function AppConfigPanel({ showDoneButton = false, initialTab = "channels" }: { showDoneButton?: boolean; initialTab?: ConfigTabKey }) {
const { message } = App.useApp();
const [activeTab, setActiveTab] = useState("channels");
const [activeTab, setActiveTab] = useState<ConfigTabKey>(initialTab);
const [loadingChannelId, setLoadingChannelId] = useState("");
const [testingWebdav, setTestingWebdav] = useState(false);
const [syncingWebdav, setSyncingWebdav] = useState(false);
@@ -81,10 +81,14 @@ export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: bo
const agentConnected = useCanvasAgentStore((state) => state.connected);
const agentEnabled = useCanvasAgentStore((state) => state.enabled);
const agentActivity = useCanvasAgentStore((state) => state.activity);
const agentConnectError = useCanvasAgentStore((state) => state.connectError);
const agentConfirmTools = useCanvasAgentStore((state) => state.confirmTools);
const setAgentState = useCanvasAgentStore((state) => state.setAgentState);
const connectAgent = useCanvasAgentStore((state) => state.connectAgent);
const disconnectAgent = useCanvasAgentStore((state) => state.disconnectAgent);
const modelOptions = config.models.map((model) => ({ label: modelOptionLabel(config, model), value: model }));
const webdavReady = Boolean(webdav.url.trim());
useEffect(() => setActiveTab(initialTab), [initialTab]);
const saveConfig = (nextConfig: AiConfig) => {
(Object.keys(nextConfig) as Array<keyof AiConfig>).forEach((key) => updateConfig(key, nextConfig[key]));
@@ -218,11 +222,13 @@ export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: bo
};
const updateAgentConfig = (patch: { url?: string; token?: string }) => {
setAgentState(patch);
setAgentState({ ...patch, connectError: "" });
if (patch.url !== undefined) localStorage.setItem("canvas-agent-url", patch.url.trim().replace(/\/$/, ""));
if (patch.token !== undefined) localStorage.setItem("canvas-agent-token", patch.token);
};
const toggleAgentConnection = () => (agentEnabled ? disconnectAgent({ connectError: "" }) : connectAgent());
return (
<>
<Tabs
@@ -430,7 +436,7 @@ export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: bo
</div>
<div className="mt-1 text-xs text-stone-500"> Agent Codex Canvas Agent</div>
</div>
<div className="text-xs text-stone-500">{agentConnected ? agentActivity || "已连接" : agentEnabled ? "连接中" : "未连接"}</div>
<div className={agentConnectError ? "text-xs text-red-600" : "text-xs text-stone-500"}>{agentConnectError ? "连接失败" : agentConnected ? agentActivity || "已连接" : agentEnabled ? "连接中" : "未连接"}</div>
</div>
<div className="mb-4 grid gap-2 md:grid-cols-3">
{codexSetupSteps.map((step, index) => (
@@ -450,6 +456,12 @@ export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: bo
<Input.Password prefix={<KeyRound className="mr-1 size-4 text-stone-400" />} value={agentToken} placeholder="自动发现,或手动填入 Connect token" onChange={(event) => updateAgentConfig({ token: event.target.value })} />
</Form.Item>
</div>
{agentConnectError ? <div className="mb-3 rounded-md border border-red-200 px-3 py-2 text-xs text-red-600 dark:border-red-900/60">{agentConnectError}</div> : null}
<div className="mb-3 flex justify-end">
<Button type={agentEnabled ? "default" : "primary"} icon={<Wifi className="size-4" />} onClick={toggleAgentConnection}>
{agentConnected ? "断开" : agentEnabled ? "取消连接" : "连接"}
</Button>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-stone-200 px-3 py-2 dark:border-stone-800">
<div className="flex min-w-0 items-center gap-2">
<ShieldCheck className="size-4 text-stone-500" />
@@ -479,6 +491,7 @@ export function AppConfigPanel({ showDoneButton = false }: { showDoneButton?: bo
export function AppConfigModal() {
const isConfigOpen = useConfigStore((state) => state.isConfigOpen);
const configTab = useConfigStore((state) => state.configTab);
const setConfigDialogOpen = useConfigStore((state) => state.setConfigDialogOpen);
return (
<Modal
@@ -495,7 +508,7 @@ export function AppConfigModal() {
styles={{ body: { maxHeight: "72vh", overflowY: "auto", paddingRight: 12 } }}
footer={null}
>
<AppConfigPanel showDoneButton />
<AppConfigPanel showDoneButton initialTab={configTab} />
</Modal>
);
}
+34 -1
View File
@@ -1,4 +1,5 @@
import { Menu } from "lucide-react";
import { Button, Tooltip } from "antd";
import { Link, useLocation } from "react-router-dom";
import { navigationTools, type NavigationToolSlug } from "@/constant/navigation-tools";
@@ -6,15 +7,28 @@ import { AppConfigModal } from "@/components/layout/app-config-modal";
import { MobileNavDrawer } from "@/components/layout/mobile-nav-drawer";
import { UserStatusActions } from "@/components/layout/user-status-actions";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
import { useConfigStore } from "@/stores/use-config-store";
export function AppTopNav() {
const { pathname } = useLocation();
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const autoConnectRef = useRef(false);
const agentToken = useCanvasAgentStore((state) => state.token);
const agentEnabled = useCanvasAgentStore((state) => state.enabled);
const agentConnected = useCanvasAgentStore((state) => state.connected);
const connectAgent = useCanvasAgentStore((state) => state.connectAgent);
const hideHeader = /^\/canvas\/[^/]+/.test(pathname);
const slug = pathname.split("/").filter(Boolean)[0];
const activeToolSlug = navigationTools.some((tool) => tool.slug === slug) ? (slug as NavigationToolSlug) : undefined;
useEffect(() => {
if (autoConnectRef.current || agentEnabled || agentConnected || !agentToken.trim()) return;
autoConnectRef.current = true;
connectAgent();
}, [agentConnected, agentEnabled, agentToken, connectAgent]);
return (
<>
{!hideHeader ? (
@@ -66,6 +80,7 @@ export function AppTopNav() {
</div>
<div className="my-auto flex h-9 min-w-0 items-center justify-end gap-2 justify-self-end whitespace-nowrap">
<CodexStatusButton />
<UserStatusActions />
</div>
</div>
@@ -77,3 +92,21 @@ export function AppTopNav() {
</>
);
}
function CodexStatusButton() {
const connected = useCanvasAgentStore((state) => state.connected);
const enabled = useCanvasAgentStore((state) => state.enabled);
const activity = useCanvasAgentStore((state) => state.activity);
const connectError = useCanvasAgentStore((state) => state.connectError);
const openConfigDialog = useConfigStore((state) => state.openConfigDialog);
const color = connectError ? "#dc2626" : connected ? "#16a34a" : enabled ? "#d97706" : "currentColor";
const title = connectError || (connected ? activity || "Codex 已连接" : enabled ? "Codex 连接中" : "Codex 未连接");
return (
<Tooltip title={title}>
<Button type="text" shape="circle" className="relative !h-8 !w-8 !min-w-8" onClick={() => openConfigDialog(false, "codex")} aria-label="Codex 连接状态">
<span className="mx-auto block size-4" style={{ background: color, WebkitMask: "url(/icons/openai.svg) center / contain no-repeat", mask: "url(/icons/openai.svg) center / contain no-repeat" }} />
<span className="absolute right-1 top-1 size-2 rounded-full border border-background" style={{ background: color }} />
</Button>
</Tooltip>
);
}
+6 -6
View File
@@ -2502,7 +2502,7 @@ function InfiniteCanvasPage() {
onUndo={undoCanvas}
onRedo={redoCanvas}
agentOpen={assistantOpen}
compactAgentStatus={codexCompactAgent ? { connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity } : undefined}
compactAgentStatus={{ connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity }}
onToggleAgent={() => (assistantOpen ? closeAgent() : openAgent())}
/>
@@ -2915,10 +2915,10 @@ function CanvasTopBar({
</button>
)}
</div>
<CompactAgentStatus status={compactAgentStatus} onClick={onToggleAgent} />
</div>
<div className="pointer-events-auto flex items-center gap-1.5">
{compactAgentStatus ? <CompactAgentStatus status={compactAgentStatus} onClick={onToggleAgent} /> : null}
<UserStatusActions
variant="canvas"
onOpenShortcuts={() => setShortcutsOpen(true)}
@@ -2968,18 +2968,18 @@ function MenuLabel({ text, shortcut }: { text: string; shortcut: string }) {
function CompactAgentStatus({ status, onClick }: { status: { connected: boolean; enabled: boolean; activity: string }; onClick: () => void }) {
const colorTheme = useThemeStore((state) => state.theme);
const theme = canvasThemes[colorTheme];
const label = status.connected ? "已连接到本地 Codex" : status.enabled ? status.activity || "连接中" : "正在连接本地 Codex";
const label = status.connected ? "Codex 已连接" : status.enabled ? `Codex ${status.activity || "连接中"}` : "Codex 未连接";
const dotColor = status.connected ? "#22c55e" : status.enabled ? "#f59e0b" : theme.node.muted;
return (
<button
type="button"
className="flex h-10 items-center gap-2 rounded-xl px-3 text-sm font-medium transition hover:opacity-85"
style={{ background: theme.toolbar.panel, color: theme.node.text, boxShadow: "0 10px 30px rgba(28,25,23,.10)" }}
className="flex h-8 items-center gap-1.5 text-xs transition hover:opacity-75"
style={{ color: status.connected ? "#16a34a" : status.enabled ? "#d97706" : theme.node.muted }}
onClick={onClick}
title="打开本地 Codex 面板"
>
<span className="size-2 rounded-full" style={{ background: dotColor }} />
<span className="max-w-[180px] truncate">{label}</span>
<span className="max-w-[140px] truncate">{label}</span>
</button>
);
}
@@ -10,6 +10,10 @@ export type AgentPendingToolCall = { requestId: string; name: string; input?: {
export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number };
export type AgentPanelTab = "chat" | "setup" | "history" | "log";
const CONNECT_TIMEOUT_MS = 6000;
let agentSource: EventSource | null = null;
let connectTimer: ReturnType<typeof setTimeout> | null = null;
type CanvasAgentStore = {
width: number;
url: string;
@@ -31,13 +35,15 @@ type CanvasAgentStore = {
activity: string;
connectError: string;
pendingTool: AgentPendingToolCall | null;
setAgentState: (patch: Partial<Omit<CanvasAgentStore, "setAgentState" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
setAgentState: (patch: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
connectAgent: () => void;
disconnectAgent: (patch?: Partial<Omit<CanvasAgentStore, "setAgentState" | "connectAgent" | "disconnectAgent" | "addMessage" | "addEventLog" | "clearEventLogs">>) => void;
addMessage: (item: AgentChatItem) => void;
addEventLog: (item: AgentEventLog) => void;
clearEventLogs: () => void;
};
export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
export const useCanvasAgentStore = create<CanvasAgentStore>((set, get) => ({
width: typeof window === "undefined" ? 440 : Number(localStorage.getItem("canvas-agent-panel-width")) || 440,
url: typeof window === "undefined" ? "http://127.0.0.1:17371" : localStorage.getItem("canvas-agent-url") || "http://127.0.0.1:17371",
token: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-token") || "",
@@ -59,6 +65,42 @@ export const useCanvasAgentStore = create<CanvasAgentStore>((set) => ({
connectError: "",
pendingTool: null,
setAgentState: (patch) => set(patch),
connectAgent: () => {
const endpoint = get().url.trim().replace(/\/$/, "");
const token = get().token.trim();
if (!endpoint || !token) return set({ connectError: "请填写 Local URL 和 Connect token" });
try {
const parsed = new URL(endpoint);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error();
} catch {
return set({ connectError: "Local URL 格式不正确" });
}
get().disconnectAgent({ url: endpoint, token, enabled: true, activity: "连接中", connectError: "" });
localStorage.setItem("canvas-agent-url", endpoint);
localStorage.setItem("canvas-agent-token", token);
const clientId = typeof crypto === "undefined" ? `${Date.now()}` : crypto.randomUUID();
const source = new EventSource(`${endpoint}/events?token=${encodeURIComponent(token)}&clientId=${encodeURIComponent(clientId)}&role=status`);
agentSource = source;
connectTimer = setTimeout(() => {
if (agentSource !== source) return;
get().disconnectAgent({ activity: "连接超时", connectError: "连接超时:请确认 Canvas Agent 正在运行,并使用当前启动输出的 Connect token" });
}, CONNECT_TIMEOUT_MS);
source.addEventListener("hello", () => {
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: true, connected: true, activity: "已连接", connectError: "" });
});
source.onerror = () => {
if (agentSource === source) get().disconnectAgent({ activity: "连接失败", connectError: "连接失败:请检查 Local URL、Connect token 或已绑定的网页 Origin" });
};
},
disconnectAgent: (patch = {}) => {
agentSource?.close();
agentSource = null;
if (connectTimer) clearTimeout(connectTimer);
connectTimer = null;
set({ enabled: false, connected: false, activity: "离线", ...patch });
},
addMessage: (item) => set((state) => ({ messages: [...state.messages.slice(-120), item] })),
addEventLog: (item) => set((state) => ({ eventLogs: [...state.eventLogs.slice(-160), item] })),
clearEventLogs: () => set({ eventLogs: [] }),
+5 -2
View File
@@ -52,6 +52,7 @@ export type WebdavSyncConfig = {
directory: string;
lastSyncedAt: string;
};
export type ConfigTabKey = "channels" | "models" | "preferences" | "webdav" | "codex";
export const CONFIG_STORE_KEY = "infinite-canvas:ai_config_store";
export type ModelCapability = "image" | "video" | "text" | "audio";
@@ -111,11 +112,12 @@ type ConfigStore = {
config: AiConfig;
webdav: WebdavSyncConfig;
isConfigOpen: boolean;
configTab: ConfigTabKey;
shouldPromptContinue: boolean;
updateConfig: <K extends keyof AiConfig>(key: K, value: AiConfig[K]) => void;
updateWebdavConfig: <K extends keyof WebdavSyncConfig>(key: K, value: WebdavSyncConfig[K]) => void;
isAiConfigReady: (config: AiConfig, model: string) => boolean;
openConfigDialog: (shouldPromptContinue?: boolean) => void;
openConfigDialog: (shouldPromptContinue?: boolean, tab?: ConfigTabKey) => void;
setConfigDialogOpen: (isOpen: boolean) => void;
clearPromptContinue: () => void;
};
@@ -171,6 +173,7 @@ export const useConfigStore = create<ConfigStore>()(
config: defaultConfig,
webdav: defaultWebdavSyncConfig,
isConfigOpen: false,
configTab: "channels",
shouldPromptContinue: false,
updateConfig: (key, value) =>
set((state) => ({
@@ -187,7 +190,7 @@ export const useConfigStore = create<ConfigStore>()(
},
})),
isAiConfigReady: (config, model) => isAiConfigReady(config, model),
openConfigDialog: (shouldPromptContinue = false) => set({ isConfigOpen: true, shouldPromptContinue }),
openConfigDialog: (shouldPromptContinue = false, configTab = "channels") => set({ isConfigOpen: true, shouldPromptContinue, configTab }),
setConfigDialogOpen: (isConfigOpen) => set({ isConfigOpen }),
clearPromptContinue: () => set({ shouldPromptContinue: false }),
}),