From 9330bdb318f43bf819c611be27626710401b5840 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Fri, 31 Jul 2026 11:03:19 +0800 Subject: [PATCH] feat(agent): add support for selecting Codex model and reasoning effort in the canvas agent, retaining choices for subsequent tasks --- CHANGELOG.md | 1 + canvas-agent/src/agent/codex-client.ts | 11 ++- canvas-agent/src/agent/codex-protocol.ts | 15 +++- canvas-agent/src/agent/codex.ts | 12 +++- canvas-agent/src/server/http.ts | 10 ++- docs/content/docs/progress/pending-test.mdx | 1 + .../components/agent/agent-chat-composer.tsx | 71 ++++++++++++++++++- .../components/agent/local-agent-panel.tsx | 40 ++++++++++- web/src/stores/use-agent-store.ts | 15 ++++ 9 files changed, 164 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 083499a..0a6ee28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ++ [新增] 画布右侧Agent支持按当前账号可用范围选择Codex 模型与推理强度。 + [优化] Canvas Agent 升级至最新 Codex,启动时检查版本更新, + [优化] 统一通过公共日志输出 Info 以上信息,美化转发日志时间。 diff --git a/canvas-agent/src/agent/codex-client.ts b/canvas-agent/src/agent/codex-client.ts index a47cc00..3153b8d 100644 --- a/canvas-agent/src/agent/codex-client.ts +++ b/canvas-agent/src/agent/codex-client.ts @@ -7,7 +7,7 @@ import stripAnsi from "strip-ansi"; import { VERSION } from "../config.js"; import { logger } from "../utils/logger.js"; import { field, type JsonRecord } from "../utils/value.js"; -import type { CodexNotificationParams, CodexPlanUpdate, CodexRequestMethod, CodexRequestParams, CodexRequestResult, CodexTurnInput } from "./codex-protocol.js"; +import type { CodexNotificationParams, CodexPlanUpdate, CodexReasoningEffort, CodexRequestMethod, CodexRequestParams, CodexRequestResult, CodexTurnInput } from "./codex-protocol.js"; import type { AgentEmit, AgentPermissionMode } from "./types.js"; type AgentEvent = JsonRecord & { type: string; usage?: unknown }; @@ -92,6 +92,11 @@ export class CodexAppClient { return this.request("thread/archive", { threadId }); } + /** 查询当前账号可用的 Codex 模型。 */ + listModels() { + return this.request("model/list", { limit: 100, includeHidden: false }); + } + /** 返回指定线程在当前进程中收到的最新任务计划。 */ planUpdates(threadId: string) { return [...this.plansByTurn.values()].filter((item) => item.threadId === threadId); @@ -105,9 +110,9 @@ export class CodexAppClient { } /** 启动一个 Codex turn 并等待完成通知。 */ - async startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, onTurn?: (turnId: string) => void) { + async startTurn(threadId: string, prompt: string, images: string[], permissionMode: AgentPermissionMode, model?: string, effort?: CodexReasoningEffort, onTurn?: (turnId: string) => void) { this.currentThreadId = threadId; - const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode) }); + const { turn } = await this.request("turn/start", { threadId, input: codexInput(prompt, images), ...turnSettings(permissionMode), ...(model ? { model } : {}), ...(effort ? { effort } : {}) }); const turnId = turn.id; if (!turnId) throw new Error("Codex app-server 没有返回 turn id"); this.currentTurnId = turnId; diff --git a/canvas-agent/src/agent/codex-protocol.ts b/canvas-agent/src/agent/codex-protocol.ts index b40e61b..6e68ecf 100644 --- a/canvas-agent/src/agent/codex-protocol.ts +++ b/canvas-agent/src/agent/codex-protocol.ts @@ -6,6 +6,15 @@ export type CodexTurnError = JsonRecord & { message: string }; export type CodexItem = JsonRecord & { id: string; type: string; text?: string }; export type CodexPlanStep = { step: string; status: "pending" | "inProgress" | "completed" }; export type CodexPlanUpdate = { threadId: string; turnId: string; explanation?: string | null; plan: CodexPlanStep[]; turnStatus?: string }; +export type CodexReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; +export type CodexModel = JsonRecord & { + id: string; + model: string; + displayName: string; + defaultReasoningEffort: CodexReasoningEffort; + supportedReasoningEfforts: Array<{ reasoningEffort: CodexReasoningEffort; description?: string }>; + isDefault?: boolean; +}; export type CodexTurnInput = | { type: "text"; text: string; text_elements: [] } @@ -53,8 +62,12 @@ type CodexRequestSpec = { params: { threadId: string }; result: Record; }; + "model/list": { + params: { limit: number; includeHidden: boolean }; + result: { data: CodexModel[]; nextCursor: string | null }; + }; "turn/start": { - params: { threadId: string; input: CodexTurnInput[]; approvalPolicy: "never" | "on-request"; sandboxPolicy: { type: "workspaceWrite"; networkAccess: boolean } | { type: "dangerFullAccess" } }; + params: { threadId: string; input: CodexTurnInput[]; approvalPolicy: "never" | "on-request"; sandboxPolicy: { type: "workspaceWrite"; networkAccess: boolean } | { type: "dangerFullAccess" }; model?: string; effort?: CodexReasoningEffort }; result: { turn: CodexTurn }; }; "turn/interrupt": { diff --git a/canvas-agent/src/agent/codex.ts b/canvas-agent/src/agent/codex.ts index 499d38d..5a56d6a 100644 --- a/canvas-agent/src/agent/codex.ts +++ b/canvas-agent/src/agent/codex.ts @@ -6,9 +6,10 @@ import { logger } from "../utils/logger.js"; import { errorMessage, field } from "../utils/value.js"; import { CodexAppClient } from "./codex-client.js"; import { summarizeCodexThread, threadMessages } from "./codex-history.js"; +import type { CodexReasoningEffort } from "./codex-protocol.js"; import type { AgentAttachment, AgentEmit, AgentPermissionMode } from "./types.js"; -type CodexRunOptions = { threadId?: string; cwd?: string; permissionMode?: AgentPermissionMode; appEmit?: AgentEmit; onStart?: () => void; onThread?: (threadId: string) => void; onTurn?: (turnId: string) => void; onFinish?: () => void }; +type CodexRunOptions = { threadId?: string; cwd?: string; permissionMode?: AgentPermissionMode; model?: string; effort?: CodexReasoningEffort; appEmit?: AgentEmit; onStart?: () => void; onThread?: (threadId: string) => void; onTurn?: (turnId: string) => void; onFinish?: () => void }; let codexQueue: Promise = Promise.resolve(); let codexApp: CodexAppClient | null = null; @@ -71,6 +72,11 @@ export async function listCodexThreads(emit: AgentEmit, options: { cwd: string; return { data, nextCursor: field(result, "nextCursor") || null, backwardsCursor: field(result, "backwardsCursor") || null }; } +/** 查询当前账号可用于新任务的 Codex 模型。 */ +export async function listCodexModels(emit: AgentEmit) { + return await (await getCodexApp(emit)).listModels(); +} + /** 读取指定 Codex 线程及其聊天历史。 */ export async function readCodexThread(emit: AgentEmit, threadId: string, cwd?: string) { const app = await getCodexApp(emit); @@ -115,7 +121,7 @@ async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: Age options.onThread?.(threadId); unmaterializedThreadIds.delete(threadId); try { - await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn); + await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.model, options.effort, options.onTurn); } catch (error) { if (!isRecoverableThreadError(error)) throw error; emit("agent_log", { text: `Codex thread unavailable, starting a new thread: ${errorMessage(error)}` }); @@ -123,7 +129,7 @@ async function runCodexTurnNow(prompt: string, emit: AgentEmit, attachments: Age threadId = await ensureCodexThread(app, { cwd: options.cwd }, emit); options.onThread?.(threadId); unmaterializedThreadIds.delete(threadId); - await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.onTurn); + await app.startTurn(threadId, prompt, files, options.permissionMode || "request", options.model, options.effort, options.onTurn); } } catch (error) { logger.error("Codex turn failed", error); diff --git a/canvas-agent/src/server/http.ts b/canvas-agent/src/server/http.ts index b510fe9..51b53dc 100644 --- a/canvas-agent/src/server/http.ts +++ b/canvas-agent/src/server/http.ts @@ -4,7 +4,8 @@ import path from "node:path"; import express, { type NextFunction, type Request, type Response } from "express"; import { runClaudeTurn } from "../agent/claude.js"; -import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js"; +import { archiveCodexThread, interruptCodexTurn, isRecoverableThreadError, listCodexModels, listCodexThreads, readCodexThread, resolveCodexApproval, resumeCodexThread, runCodexTurn, startCodexThread, summarizeCodexThread, verifyCodexThreadWorkspace } from "../agent/codex.js"; +import type { CodexReasoningEffort } from "../agent/codex-protocol.js"; import type { AgentAttachment, AgentPermissionMode } from "../agent/types.js"; import { CanvasSession } from "../canvas/session.js"; import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWorkspace, type CanvasAgentConfig } from "../config.js"; @@ -96,6 +97,7 @@ export function startHttpServer() { const workspace = ensureSiteWorkspace(config); res.json({ ok: true, workspace }); }); + app.get("/agent/codex/models", route(async (_req, res) => res.json({ ok: true, ...(await listCodexModels(emit)) }))); app.get("/agent/codex/threads", route(async (req, res) => { const workspace = ensureSiteWorkspace(config); const result = await listCodexThreads(emit, { cwd: workspace.workspacePath, searchTerm: String(req.query.searchTerm || "") }); @@ -174,6 +176,8 @@ export function startHttpServer() { threadId, cwd: workspace.workspacePath, permissionMode: permissionMode(req.body?.permissionMode), + model: String(req.body?.model || "") || undefined, + effort: reasoningEffort(req.body?.effort), appEmit: emit, onStart: clientId ? () => session.bindClient(clientId) : undefined, onThread: (actualThreadId) => { @@ -249,6 +253,10 @@ function permissionMode(value: unknown): AgentPermissionMode { return value === "automatic" || value === "full" ? value : "request"; } +function reasoningEffort(value: unknown): CodexReasoningEffort | undefined { + return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" ? value : undefined; +} + /** 使用当前操作系统的文件管理器定位本地文件。 */ function revealLocalFile(filePath: string, isDirectory: boolean) { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open"; diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 521aba5..1d846aa 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型,切换模型后强度选项随模型能力更新,刷新页面后保留选择,发送任务时实际使用所选模型和强度。 - Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。 - Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。 - Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。 diff --git a/web/src/components/agent/agent-chat-composer.tsx b/web/src/components/agent/agent-chat-composer.tsx index cb3e8bf..b47ab65 100644 --- a/web/src/components/agent/agent-chat-composer.tsx +++ b/web/src/components/agent/agent-chat-composer.tsx @@ -1,10 +1,10 @@ import { useRef, type ReactNode } from "react"; import { Button, Dropdown, Tooltip } from "antd"; -import { ArrowUp, Check, ChevronUp, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Square, X } from "lucide-react"; +import { ArrowUp, Check, ChevronUp, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Sparkles, Square, X } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { isPlainEnterKey } from "@/lib/keyboard-event"; -import type { AgentPermissionMode } from "@/stores/use-agent-store"; +import type { AgentModel, AgentPermissionMode, AgentReasoningEffort } from "@/stores/use-agent-store"; import type { AgentChatAttachment } from "./agent-chat-message"; export function AgentChatComposer({ @@ -23,6 +23,11 @@ export function AgentChatComposer({ onConfirmToolsChange, permissionMode, onPermissionModeChange, + models, + model, + reasoningEffort, + onModelChange, + onReasoningEffortChange, left, }: { prompt: string; @@ -40,6 +45,11 @@ export function AgentChatComposer({ onConfirmToolsChange?: (confirmTools: boolean) => void; permissionMode?: AgentPermissionMode; onPermissionModeChange?: (permissionMode: AgentPermissionMode) => void; + models?: AgentModel[]; + model?: string; + reasoningEffort?: AgentReasoningEffort | ""; + onModelChange?: (model: string) => void; + onReasoningEffortChange?: (effort: AgentReasoningEffort) => void; left?: ReactNode; }) { const fileInputRef = useRef(null); @@ -95,6 +105,7 @@ export function AgentChatComposer({ ) : null} {onConfirmToolsChange ? : null} {permissionMode && onPermissionModeChange ? : null} + {models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? : null} {left}
@@ -110,6 +121,62 @@ export function AgentChatComposer({ ); } +function AgentModelMenu({ models, model, reasoningEffort, theme, onModelChange, onReasoningEffortChange }: { models: AgentModel[]; model: string; reasoningEffort: AgentReasoningEffort; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onModelChange: (model: string) => void; onReasoningEffortChange: (effort: AgentReasoningEffort) => void }) { + const current = models.find((item) => item.model === model) || models[0]; + return ( + ({ + key: item.model, + label: , + onClick: () => onModelChange(item.model), + })), + }, + { + key: "effort", + label: "推理强度", + children: current.supportedReasoningEfforts.map((item) => ({ + key: item.reasoningEffort, + label: , + onClick: () => onReasoningEffortChange(item.reasoningEffort), + })), + }, + ], + }} + > + + + ); +} + +const effortLabels: Record = { + minimal: "最低", + low: "轻度", + medium: "中", + high: "高", + xhigh: "极高", +}; + +function PickerOption({ title, selected }: { title: string; selected: boolean }) { + return ( +
+ {title} + {selected ? : null} +
+ ); +} + function PermissionModeMenu({ permissionMode, theme, onChange }: { permissionMode: AgentPermissionMode; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onChange: (permissionMode: AgentPermissionMode) => void }) { const current = permissionOptions.find((item) => item.key === permissionMode) || permissionOptions[0]; return ( diff --git a/web/src/components/agent/local-agent-panel.tsx b/web/src/components/agent/local-agent-panel.tsx index 0af6aa8..6ee16e1 100644 --- a/web/src/components/agent/local-agent-panel.tsx +++ b/web/src/components/agent/local-agent-panel.tsx @@ -13,7 +13,7 @@ import { uploadImage } from "@/services/image-storage"; import { deleteAgentThreadMessages, readAgentUserMessages, saveAgentUserMessage } from "@/services/agent-chat-storage"; import { useThemeStore } from "@/stores/use-theme-store"; import { useShallow } from "zustand/react/shallow"; -import { useAgentStore, type AgentCanvasContext, type AgentChatItem, type AgentPendingApproval, type AgentPendingToolCall, type AgentPermissionMode, type AgentThreadSummary } from "@/stores/use-agent-store"; +import { useAgentStore, type AgentCanvasContext, type AgentChatItem, type AgentModel, type AgentPendingApproval, type AgentPendingToolCall, type AgentPermissionMode, type AgentReasoningEffort, type AgentThreadSummary } from "@/stores/use-agent-store"; import { summarizeCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops"; import { isSiteTool, runSiteTool } from "@/lib/agent/agent-site-tools"; import { activateAgentClient, discoverAgentConfig, fetchAgentJson, postCodexApproval, postState, postToolResult } from "./agent-api"; @@ -65,6 +65,7 @@ const DEFAULT_AGENT_URL = "http://127.0.0.1:17371"; type AgentWorkspace = { workspacePath: string; activeThreadId?: string }; type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] }; type AgentThreadResponse = { ok?: boolean; workspace?: AgentWorkspace; thread?: AgentThreadSummary; messages?: AgentChatItem[] }; +type AgentModelsResponse = { ok?: boolean; data?: AgentModel[] }; type AgentCodexState = { busy?: boolean; threadId?: string; turnId?: string }; type AgentHelloEvent = { ok?: boolean; clientId?: string; codex?: AgentCodexState }; type AgentWorkspaceEvent = { activeThreadId?: string; threadId?: string; emptyThread?: boolean; draftThread?: boolean }; @@ -79,7 +80,7 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? // 注意:canvasContext 不在此订阅内 —— 它在拖拽/resize 时会被 project 每帧写入, // 但面板只在 ref 同步与防抖 postState 中用到它、渲染层从不读它。若把它放进订阅, // 面板会随画布每帧重渲染(性能问题,也是 #185 崩溃的放大器)。改为下方 subscribe 命令式监听。 - const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, tokenUsage, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, permissionMode, activity, connectError, pendingTool, pendingApprovals } = useAgentStore( + const { width, url, token, connected, enabled, prompt, attachments, sending, waiting, tokenUsage, eventLogs, threads, activeThreadId, workspacePath, loadingThreads, activeTab, confirmTools, permissionMode, models, model, reasoningEffort, activity, connectError, pendingTool, pendingApprovals } = useAgentStore( useShallow((state) => ({ width: state.width, url: state.url, @@ -99,6 +100,9 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? activeTab: state.activeTab, confirmTools: state.confirmTools, permissionMode: state.permissionMode, + models: state.models, + model: state.model, + reasoningEffort: state.reasoningEffort, activity: state.activity, connectError: state.connectError, pendingTool: state.pendingTool, @@ -278,6 +282,21 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? if (connected) void loadThreads(); }, [connected, loadThreads]); + useEffect(() => { + if (!connected) return; + void fetchAgentJson(endpoint, token, "/agent/codex/models").then(({ data = [] }) => { + if (!data.length) return; + const savedModel = useAgentStore.getState().model; + const current = data.find((item) => item.model === savedModel) || data.find((item) => item.isDefault) || data[0]; + const savedEffort = useAgentStore.getState().reasoningEffort; + const efforts = current.supportedReasoningEfforts.map((item) => item.reasoningEffort); + const nextEffort = efforts.includes(savedEffort as AgentReasoningEffort) ? savedEffort as AgentReasoningEffort : current.defaultReasoningEffort || efforts[0]; + localStorage.setItem("canvas-agent-model", current.model); + localStorage.setItem("canvas-agent-reasoning-effort", nextEffort); + setAgentState({ models: data, model: current.model, reasoningEffort: nextEffort }); + }).catch((error) => addEventLog("读取模型列表失败", error)); + }, [connected, endpoint, setAgentState, token]); + useEffect(() => { if (!connected) return; const activate = () => void activateAgentClient(endpoint, token, clientIdRef.current); @@ -325,6 +344,8 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? clientId: clientIdRef.current, threadId, permissionMode, + model, + effort: reasoningEffort, attachments: files.map(({ id, name, type, size, width, height, dataUrl }) => ({ id, name, type, size, width, height, dataUrl })), }), }); @@ -904,6 +925,21 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded? onConfirmToolsChange={(confirmTools) => setAgentState({ confirmTools })} permissionMode={permissionMode} onPermissionModeChange={changePermissionMode} + models={models} + model={model} + reasoningEffort={reasoningEffort} + onModelChange={(model) => { + const selected = models.find((item) => item.model === model); + if (!selected) return; + const effort = selected.defaultReasoningEffort || selected.supportedReasoningEfforts[0]?.reasoningEffort; + localStorage.setItem("canvas-agent-model", model); + if (effort) localStorage.setItem("canvas-agent-reasoning-effort", effort); + setAgentState({ model, ...(effort ? { reasoningEffort: effort } : {}) }); + }} + onReasoningEffortChange={(reasoningEffort) => { + localStorage.setItem("canvas-agent-reasoning-effort", reasoningEffort); + setAgentState({ reasoningEffort }); + }} left={ attachments.length ? ( diff --git a/web/src/stores/use-agent-store.ts b/web/src/stores/use-agent-store.ts index 446ce86..4940eb7 100644 --- a/web/src/stores/use-agent-store.ts +++ b/web/src/stores/use-agent-store.ts @@ -8,6 +8,15 @@ export type AgentChatItem = { id: string; role: AgentChatRole; title?: string; t export type AgentEventLog = { id: string; time: string; title: string; text: string; raw?: unknown }; export type AgentPendingToolCall = { requestId: string; name: string; input?: { ops?: CanvasAgentOp[]; path?: string } & Record }; export type AgentPermissionMode = "request" | "automatic" | "full"; +export type AgentReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh"; +export type AgentModel = { + id: string; + model: string; + displayName: string; + defaultReasoningEffort: AgentReasoningEffort; + supportedReasoningEfforts: Array<{ reasoningEffort: AgentReasoningEffort; description?: string }>; + isDefault?: boolean; +}; export type AgentPendingApproval = { requestId: string; method: string; threadId?: string; turnId?: string; itemId?: string; reason?: string; command?: unknown; cwd?: string; grantRoot?: string; networkApprovalContext?: unknown; permissions?: unknown }; export type AgentCanvasContext = { snapshot: CanvasAgentSnapshot; applyOps: (ops?: CanvasAgentOp[]) => CanvasAgentSnapshot; undoOps: () => CanvasAgentSnapshot | null; canUndo: boolean }; export type AgentThreadSummary = { id: string; preview: string; name?: string | null; cwd?: string; status?: string; source?: unknown; createdAt?: number; updatedAt?: number }; @@ -43,6 +52,9 @@ type AgentStore = { activeTab: AgentPanelTab; confirmTools: boolean; permissionMode: AgentPermissionMode; + models: AgentModel[]; + model: string; + reasoningEffort: AgentReasoningEffort | ""; activity: string; connectError: string; pendingTool: AgentPendingToolCall | null; @@ -86,6 +98,9 @@ export const useAgentStore = create((set, get) => ({ activeTab: "setup", confirmTools: false, permissionMode: typeof window === "undefined" ? "request" : (localStorage.getItem("canvas-agent-permission-mode") as AgentPermissionMode) || "request", + models: [], + model: typeof window === "undefined" ? "" : localStorage.getItem("canvas-agent-model") || "", + reasoningEffort: typeof window === "undefined" ? "" : (localStorage.getItem("canvas-agent-reasoning-effort") as AgentReasoningEffort) || "", activity: "就绪", connectError: "", pendingTool: null,