diff --git a/canvas-agent/src/agent/codex-protocol.ts b/canvas-agent/src/agent/codex-protocol.ts
index 6e68ecf..2cd210d 100644
--- a/canvas-agent/src/agent/codex-protocol.ts
+++ b/canvas-agent/src/agent/codex-protocol.ts
@@ -6,7 +6,7 @@ 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 CodexReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
export type CodexModel = JsonRecord & {
id: string;
model: string;
diff --git a/canvas-agent/src/server/http.ts b/canvas-agent/src/server/http.ts
index 51b53dc..5db6379 100644
--- a/canvas-agent/src/server/http.ts
+++ b/canvas-agent/src/server/http.ts
@@ -148,7 +148,9 @@ export function startHttpServer() {
const prompt = String(req.body?.prompt || "");
if (!prompt.trim()) return res.status(400).json({ ok: false, error: "请输入任务内容" });
const clientId = String(req.body?.clientId || "");
- logger.info("Codex turn accepted", { threadId: req.body?.threadId, promptLength: prompt.length, attachmentCount: attachments.length });
+ const model = String(req.body?.model || "") || undefined;
+ const effort = reasoningEffort(req.body?.effort);
+ logger.info("Codex turn accepted", { threadId: req.body?.threadId, model: model || "default", reasoningEffort: effort || "default", promptLength: prompt.length, attachmentCount: attachments.length });
session.setCodexState({ busy: true, threadId: String(req.body?.threadId || workspace.activeThreadId || ""), turnId: "" });
try {
let threadId = String(req.body?.threadId || workspace.activeThreadId || "");
@@ -176,8 +178,8 @@ export function startHttpServer() {
threadId,
cwd: workspace.workspacePath,
permissionMode: permissionMode(req.body?.permissionMode),
- model: String(req.body?.model || "") || undefined,
- effort: reasoningEffort(req.body?.effort),
+ model,
+ effort,
appEmit: emit,
onStart: clientId ? () => session.bindClient(clientId) : undefined,
onThread: (actualThreadId) => {
@@ -193,7 +195,7 @@ export function startHttpServer() {
},
onTurn: (actualTurnId) => {
turnId = actualTurnId;
- logger.info("Codex turn started", { threadId, turnId });
+ logger.info("Codex turn started", { threadId, turnId, model: model || "default", reasoningEffort: effort || "default" });
session.setCodexState({ busy: true, threadId, turnId });
},
onFinish: () => {
@@ -254,7 +256,7 @@ function permissionMode(value: unknown): AgentPermissionMode {
}
function reasoningEffort(value: unknown): CodexReasoningEffort | undefined {
- return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" ? value : undefined;
+ return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max" || value === "ultra" ? value : undefined;
}
/** 使用当前操作系统的文件管理器定位本地文件。 */
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index 1d846aa..c49576b 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -5,7 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
# 待测试
-- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型,切换模型后强度选项随模型能力更新,刷新页面后保留选择,发送任务时实际使用所选模型和强度。
+- 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 b47ab65..bffaaf5 100644
--- a/web/src/components/agent/agent-chat-composer.tsx
+++ b/web/src/components/agent/agent-chat-composer.tsx
@@ -1,7 +1,8 @@
import { useRef, type ReactNode } from "react";
import { Button, Dropdown, Tooltip } from "antd";
-import { ArrowUp, Check, ChevronUp, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Sparkles, Square, X } from "lucide-react";
+import { ArrowUp, Check, ChevronUp, Cpu, Hand, ImagePlus, LoaderCircle, RefreshCw, ShieldAlert, ShieldCheck, ShieldOff, Square, X } from "lucide-react";
+import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { canvasThemes } from "@/lib/canvas-theme";
import { isPlainEnterKey } from "@/lib/keyboard-event";
import type { AgentModel, AgentPermissionMode, AgentReasoningEffort } from "@/stores/use-agent-store";
@@ -105,7 +106,7 @@ export function AgentChatComposer({
) : null}
{onConfirmToolsChange ? : null}
{permissionMode && onPermissionModeChange ? : null}
- {models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? : null}
+ {models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? : null}
{left}
@@ -121,42 +122,28 @@ 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 }) {
+function AgentModelControls({ models, model, reasoningEffort, onModelChange, onReasoningEffortChange }: { models: AgentModel[]; model: string; reasoningEffort: AgentReasoningEffort; 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),
- })),
- },
- ],
- }}
- >
-
-
+
+
+
+
);
}
@@ -166,17 +153,10 @@ const effortLabels: Record
= {
medium: "中",
high: "高",
xhigh: "极高",
+ max: "最高",
+ ultra: "Ultra",
};
-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 6ee16e1..4399daa 100644
--- a/web/src/components/agent/local-agent-panel.tsx
+++ b/web/src/components/agent/local-agent-panel.tsx
@@ -61,6 +61,8 @@ import { AgentPanelTabs } from "./agent-panel-tabs";
const MAX_ATTACHMENTS = 6;
const MAX_ATTACHMENT_PAYLOAD_BYTES = 28 * 1024 * 1024;
const DEFAULT_AGENT_URL = "http://127.0.0.1:17371";
+const AGENT_REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
+const AGENT_REASONING_LABELS: Record = { minimal: "最低", low: "轻度", medium: "中", high: "高", xhigh: "极高", max: "最高", ultra: "Ultra" };
type AgentWorkspace = { workspacePath: string; activeThreadId?: string };
type AgentThreadsResponse = { ok?: boolean; workspace?: AgentWorkspace; data?: AgentThreadSummary[] };
@@ -285,15 +287,24 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
useEffect(() => {
if (!connected) return;
void fetchAgentJson(endpoint, token, "/agent/codex/models").then(({ data = [] }) => {
- if (!data.length) return;
+ const names = new Set();
+ const models = data.flatMap((item) => {
+ const name = item.displayName || item.model;
+ const efforts = item.supportedReasoningEfforts.filter(({ reasoningEffort }) => AGENT_REASONING_EFFORTS.has(reasoningEffort));
+ if (item.model === "codex-auto-review" || names.has(name) || !efforts.length) return [];
+ names.add(name);
+ const defaultReasoningEffort = efforts.some((effort) => effort.reasoningEffort === item.defaultReasoningEffort) ? item.defaultReasoningEffort : efforts[0].reasoningEffort;
+ return [{ ...item, supportedReasoningEfforts: efforts, defaultReasoningEffort }];
+ });
+ if (!models.length) return;
const savedModel = useAgentStore.getState().model;
- const current = data.find((item) => item.model === savedModel) || data.find((item) => item.isDefault) || data[0];
+ const current = models.find((item) => item.model === savedModel) || models.find((item) => item.isDefault) || models[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 });
+ setAgentState({ models, model: current.model, reasoningEffort: nextEffort });
}).catch((error) => addEventLog("读取模型列表失败", error));
}, [connected, endpoint, setAgentState, token]);
@@ -333,7 +344,9 @@ export function LocalAgentPanel({ embedded, headless, autoConnect }: { embedded?
setAgentState({ activeThreadId: threadId, tokenUsage: null });
}
if (files.length) void saveAgentUserMessage(threadId, { id: messageId, role: "user", text: userText, historyText: requestPrompt, attachments: files }).catch(() => undefined);
- addEventLog("发送任务", `${compactText(text) || "仅附件"}${files.length ? ` · 附件 ${files.length}` : ""}`);
+ const modelName = models.find((item) => item.model === model)?.displayName || model || "默认模型";
+ const effortName = reasoningEffort ? AGENT_REASONING_LABELS[reasoningEffort] : "默认强度";
+ addEventLog("发送任务", `${modelName} · ${effortName}${files.length ? ` · 附件 ${files.length}` : ""} · ${compactText(text) || "仅附件"}`);
const data = await fetchAgentJson<{ threadId?: string }>(endpoint, token, "/agent/codex/turn", {
method: "POST",
headers: { "content-type": "application/json" },
diff --git a/web/src/stores/use-agent-store.ts b/web/src/stores/use-agent-store.ts
index 4940eb7..b8e12fd 100644
--- a/web/src/stores/use-agent-store.ts
+++ b/web/src/stores/use-agent-store.ts
@@ -8,7 +8,7 @@ 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 AgentReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
export type AgentModel = {
id: string;
model: string;