mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-01 06:01:13 +08:00
feat(agent): add support for selecting Codex model and reasoning effort in the canvas agent, retaining choices for subsequent tasks
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
+ [新增] 画布右侧Agent支持按当前账号可用范围选择Codex 模型与推理强度。
|
||||
+ [优化] Canvas Agent 升级至最新 Codex,启动时检查版本更新,
|
||||
+ [优化] 统一通过公共日志输出 Info 以上信息,美化转发日志时间。
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, never>;
|
||||
};
|
||||
"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": {
|
||||
|
||||
@@ -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<unknown> = 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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项
|
||||
|
||||
# 待测试
|
||||
|
||||
- Agent 模型设置:连接 Canvas Agent 后,输入框左下方应显示当前 Codex 模型与推理强度;模型列表应来自当前账号实际可用模型,切换模型后强度选项随模型能力更新,刷新页面后保留选择,发送任务时实际使用所选模型和强度。
|
||||
- Agent 新对话响应:一轮对话完成后点击「新对话」,聊天内容应立即清空并进入空白对话,不出现等待或新建按钮卡顿;此时不应生成空历史记录,第一次发送消息时才创建线程;多个标签页应同步进入空白对话,点击后立即发送也不得误发到上一条会话。
|
||||
- Agent 读取画布卡片:读取当前画布完成后,卡片应按非零类型显示文本、图片、配置、视频、音频、分组、其他节点及连线数量,例如「3 个文本、5 张图片、2 个配置、4 条连线」;空画布应显示「当前画布为空」,执行失败时仍应显示错误信息,刷新恢复历史后统计保持一致。
|
||||
- Agent 首次发送响应:在空白新对话中输入内容并按回车后,输入框应立即清空、用户消息应立即出现在对话中,再显示「正在思考」;线程创建或发送失败时,原输入和附件应恢复;任务运行期间输入的新草稿不应在请求成功后被清空。
|
||||
|
||||
@@ -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<HTMLInputElement>(null);
|
||||
@@ -95,6 +105,7 @@ export function AgentChatComposer({
|
||||
) : null}
|
||||
{onConfirmToolsChange ? <ToolConfirmationMenu confirmTools={Boolean(confirmTools)} theme={theme} onChange={onConfirmToolsChange} /> : null}
|
||||
{permissionMode && onPermissionModeChange ? <PermissionModeMenu permissionMode={permissionMode} theme={theme} onChange={onPermissionModeChange} /> : null}
|
||||
{models?.length && model && reasoningEffort && onModelChange && onReasoningEffortChange ? <AgentModelMenu models={models} model={model} reasoningEffort={reasoningEffort} theme={theme} onModelChange={onModelChange} onReasoningEffortChange={onReasoningEffortChange} /> : null}
|
||||
{left}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
@@ -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 (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
placement="topLeft"
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: "model",
|
||||
label: "模型",
|
||||
children: models.map((item) => ({
|
||||
key: item.model,
|
||||
label: <PickerOption title={item.displayName || item.model} selected={item.model === model} />,
|
||||
onClick: () => onModelChange(item.model),
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: "effort",
|
||||
label: "推理强度",
|
||||
children: current.supportedReasoningEfforts.map((item) => ({
|
||||
key: item.reasoningEffort,
|
||||
label: <PickerOption title={effortLabels[item.reasoningEffort]} selected={item.reasoningEffort === reasoningEffort} />,
|
||||
onClick: () => onReasoningEffortChange(item.reasoningEffort),
|
||||
})),
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<button type="button" className="flex h-9 min-w-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition hover:bg-black/5 dark:hover:bg-white/10" style={{ color: theme.node.text }} aria-label="选择 Codex 模型和推理强度">
|
||||
<Sparkles className="size-3.5 shrink-0" />
|
||||
<span className="max-w-28 truncate">{current.displayName || current.model}</span>
|
||||
<span className="shrink-0 opacity-55">{effortLabels[reasoningEffort]}</span>
|
||||
<ChevronUp className="size-3 shrink-0 opacity-50" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const effortLabels: Record<AgentReasoningEffort, string> = {
|
||||
minimal: "最低",
|
||||
low: "轻度",
|
||||
medium: "中",
|
||||
high: "高",
|
||||
xhigh: "极高",
|
||||
};
|
||||
|
||||
function PickerOption({ title, selected }: { title: string; selected: boolean }) {
|
||||
return (
|
||||
<div className="flex min-w-48 items-center gap-3 py-0.5">
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">{title}</span>
|
||||
{selected ? <Check className="size-4 shrink-0" /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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<AgentModelsResponse>(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 ? (
|
||||
<span className="text-[11px]" style={{ color: theme.node.muted }}>
|
||||
|
||||
@@ -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<string, unknown> };
|
||||
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<AgentStore>((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,
|
||||
|
||||
Reference in New Issue
Block a user