From 9f499ac7c24220c641c86f029f4c8929276c7dc9 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 6 Oct 2026 18:02:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E8=BE=93=E5=85=A5=E8=8D=89?= =?UTF-8?q?=E7=A8=BF=E6=8C=89=E4=BC=9A=E8=AF=9D=E4=BF=9D=E7=95=99,?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E4=BC=9A=E8=AF=9D=E3=80=81=E5=85=B3=E6=8A=BD?= =?UTF-8?q?=E5=B1=89=E4=B8=8D=E4=B8=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 会话面板与新对话输入框的草稿原来是组件内状态,切到别的会话或 Esc 关抽屉就没了。 改为按会话(以及列表页 / 抽屉两处新对话)分键记在模块里,发送成功后清掉。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../web/app/components/agent/agent-drawer.tsx | 7 +++- .../app/components/agent/agent-nav.test.ts | 37 ++++++++++++++++++- apps/web/app/components/agent/agent-nav.ts | 17 +++++++++ .../app/components/agent/agent-run-panel.tsx | 17 ++++++++- apps/web/app/routes/console/agent.tsx | 14 ++++++- 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/apps/web/app/components/agent/agent-drawer.tsx b/apps/web/app/components/agent/agent-drawer.tsx index ca64f89d..701512e5 100644 --- a/apps/web/app/components/agent/agent-drawer.tsx +++ b/apps/web/app/components/agent/agent-drawer.tsx @@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx"; import { errorMessage } from "~/lib/api.ts"; import { capabilityMeta, isDenied, RunStatusBadge } from "./agent-conversation.tsx"; +import { DRAFT_DRAWER_GOAL, writeDraft } from "./agent-nav.ts"; import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx"; import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx"; import { @@ -170,12 +171,16 @@ export function AgentDrawer() { context={name} onClearContext={clearAgentContext} defaultGoal={context?.goal ?? ""} + draftKey={DRAFT_DRAWER_GOAL} pending={create.isPending} onSubmit={(goal, opts) => create.mutate( { goal: `${prefix}${goal}`, ...opts }, { - onSuccess: (run) => showAgentRunInDrawer(run.id), + onSuccess: (run) => { + writeDraft(DRAFT_DRAWER_GOAL, ""); + showAgentRunInDrawer(run.id); + }, onError: (e) => toast.error(errorMessage(e)), }, ) diff --git a/apps/web/app/components/agent/agent-nav.test.ts b/apps/web/app/components/agent/agent-nav.test.ts index 55ec1d59..3e34ab87 100644 --- a/apps/web/app/components/agent/agent-nav.test.ts +++ b/apps/web/app/components/agent/agent-nav.test.ts @@ -2,7 +2,18 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { runGroup } from "~/api/agent.ts"; -import { agentListHref, agentRunHref, groupFromSearch, resolveGroup, revealScrollTop } from "./agent-nav.ts"; +import { + agentListHref, + agentRunHref, + DRAFT_DRAWER_GOAL, + DRAFT_PAGE_GOAL, + groupFromSearch, + readDraft, + resolveGroup, + revealScrollTop, + runDraftKey, + writeDraft, +} from "./agent-nav.ts"; import { CompactPager } from "./agent-session-list.tsx"; import { isAgentPage, outletKey } from "./agent-store.ts"; @@ -84,3 +95,27 @@ describe("会话列表分页(窄列表一行)", () => { expect(html).toContain("1 / 1+"); }); }); + +describe("输入草稿", () => { + it("按会话分开记,切走再回来还在,不串到别的会话", () => { + writeDraft(runDraftKey("a"), "A 的草稿"); + writeDraft(runDraftKey("b"), "B 的草稿"); + expect(readDraft(runDraftKey("a"))).toBe("A 的草稿"); + expect(readDraft(runDraftKey("b"))).toBe("B 的草稿"); + expect(readDraft(runDraftKey("c"))).toBe(""); + }); + it("清空(发送成功)后不再恢复;只有空白也算清空", () => { + writeDraft(runDraftKey("a"), ""); + writeDraft(runDraftKey("b"), " "); + expect(readDraft(runDraftKey("a"))).toBe(""); + expect(readDraft(runDraftKey("b"))).toBe(""); + }); + it("列表页与抽屉的新对话输入框各记各的;没有键时不记", () => { + writeDraft(DRAFT_PAGE_GOAL, "页面里写了一半"); + writeDraft(DRAFT_DRAWER_GOAL, "抽屉里写了一半"); + writeDraft(undefined, "不记"); + expect(readDraft(DRAFT_PAGE_GOAL)).toBe("页面里写了一半"); + expect(readDraft(DRAFT_DRAWER_GOAL)).toBe("抽屉里写了一半"); + expect(readDraft(undefined)).toBe(""); + }); +}); diff --git a/apps/web/app/components/agent/agent-nav.ts b/apps/web/app/components/agent/agent-nav.ts index afa9dfb7..98dcae72 100644 --- a/apps/web/app/components/agent/agent-nav.ts +++ b/apps/web/app/components/agent/agent-nav.ts @@ -108,3 +108,20 @@ export function usePageListGroup(activeRun?: AgentRunGroup | null): [AgentRunGro ); return [group, change]; } + +// ── 输入草稿:按会话(和两处新对话输入框)分开记,切换会话、关抽屉不丢;发送成功后清掉。模块级,刷新页面才清 ── + +const drafts = new Map(); +export const DRAFT_PAGE_GOAL = "goal:page"; +export const DRAFT_DRAWER_GOAL = "goal:drawer"; +export const runDraftKey = (runId: string) => `run:${runId}`; + +export function readDraft(key: string | undefined): string { + return key ? (drafts.get(key) ?? "") : ""; +} + +export function writeDraft(key: string | undefined, text: string): void { + if (!key) return; + if (text.trim()) drafts.set(key, text); + else drafts.delete(key); +} diff --git a/apps/web/app/components/agent/agent-run-panel.tsx b/apps/web/app/components/agent/agent-run-panel.tsx index 3d3c2d7c..d2b47745 100644 --- a/apps/web/app/components/agent/agent-run-panel.tsx +++ b/apps/web/app/components/agent/agent-run-panel.tsx @@ -40,6 +40,7 @@ import { runPlanIds, } from "./agent-conversation.tsx"; import { RunPlan } from "./agent-interaction.tsx"; +import { readDraft, runDraftKey, writeDraft } from "./agent-nav.ts"; import { shortGoalTitle } from "./agent-store.ts"; import { SubagentTree } from "./agent-subagent-tree.tsx"; import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts"; @@ -71,6 +72,7 @@ export function GoalComposer({ onClearContext, defaultGoal = "", suggestions, + draftKey, }: { onSubmit: (goal: string, opts: AgentSessionOptions) => void; pending: boolean; @@ -80,8 +82,14 @@ export function GoalComposer({ defaultGoal?: string; /** 快捷目标:输入框为空时显示 */ suggestions?: string[]; + /** 草稿记在这个键下(没有预填目标时恢复);发送成功后由调用方清掉 */ + draftKey?: string; }) { - const [goal, setGoal] = useState(defaultGoal); + const [goal, setGoalState] = useState(() => defaultGoal || readDraft(draftKey)); + const setGoal = (text: string) => { + setGoalState(text); + writeDraft(draftKey, text); + }; const box = useRef(null); const fill = (text: string) => { setGoal(text); @@ -295,7 +303,12 @@ export function AgentRunPanel({ const append = useAppendAgentMessage(); const create = useCreateAgentRun(); const navigate = useNavigate(); - const [msg, setMsg] = useState(""); + // 草稿按会话记:切到别的会话再回来还在,不会串到别的会话 + const [msg, setMsgState] = useState(() => readDraft(runDraftKey(runId))); + const setMsg = (text: string) => { + setMsgState(text); + writeDraft(runDraftKey(runId), text); + }; const [confirmStop, setConfirmStop] = useState(false); const sseStatus = useSseStatus(); const qc = useQueryClient(); diff --git a/apps/web/app/routes/console/agent.tsx b/apps/web/app/routes/console/agent.tsx index 495c0d78..db0fc3b6 100644 --- a/apps/web/app/routes/console/agent.tsx +++ b/apps/web/app/routes/console/agent.tsx @@ -2,7 +2,13 @@ import { FlaskConical } from "lucide-react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts"; -import { agentRunHref, pageListMemory, usePageListGroup } from "~/components/agent/agent-nav.ts"; +import { + agentRunHref, + DRAFT_PAGE_GOAL, + pageListMemory, + usePageListGroup, + writeDraft, +} from "~/components/agent/agent-nav.ts"; import { GoalComposer } from "~/components/agent/agent-run-panel.tsx"; import { AgentSessionList } from "~/components/agent/agent-session-list.tsx"; import { quickGoalsFor } from "~/components/agent/quick-goals.ts"; @@ -43,11 +49,15 @@ export default function AgentPage() { create.mutate( { goal, ...opts }, { - onSuccess: (run) => navigate(agentRunHref(run.id, mock)), + onSuccess: (run) => { + writeDraft(DRAFT_PAGE_GOAL, ""); + navigate(agentRunHref(run.id, mock)); + }, onError: (e) => toast.error(errorMessage(e)), }, )