fix(agent): 输入草稿按会话保留,切换会话、关抽屉不丢
会话面板与新对话输入框的草稿原来是组件内状态,切到别的会话或 Esc 关抽屉就没了。 改为按会话(以及列表页 / 抽屉两处新对话)分键记在模块里,发送成功后清掉。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
164a27fd80
commit
9f499ac7c2
5 files changed
+86
-6
No files matched your search
@@ -13,6 +13,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover
|
|||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
||||||
import { errorMessage } from "~/lib/api.ts";
|
import { errorMessage } from "~/lib/api.ts";
|
||||||
import { capabilityMeta, isDenied, RunStatusBadge } from "./agent-conversation.tsx";
|
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 { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx";
|
||||||
import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx";
|
import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx";
|
||||||
import {
|
import {
|
||||||
@@ -170,12 +171,16 @@ export function AgentDrawer() {
|
|||||||
context={name}
|
context={name}
|
||||||
onClearContext={clearAgentContext}
|
onClearContext={clearAgentContext}
|
||||||
defaultGoal={context?.goal ?? ""}
|
defaultGoal={context?.goal ?? ""}
|
||||||
|
draftKey={DRAFT_DRAWER_GOAL}
|
||||||
pending={create.isPending}
|
pending={create.isPending}
|
||||||
onSubmit={(goal, opts) =>
|
onSubmit={(goal, opts) =>
|
||||||
create.mutate(
|
create.mutate(
|
||||||
{ goal: `${prefix}${goal}`, ...opts },
|
{ goal: `${prefix}${goal}`, ...opts },
|
||||||
{
|
{
|
||||||
onSuccess: (run) => showAgentRunInDrawer(run.id),
|
onSuccess: (run) => {
|
||||||
|
writeDraft(DRAFT_DRAWER_GOAL, "");
|
||||||
|
showAgentRunInDrawer(run.id);
|
||||||
|
},
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,18 @@ import { createElement } from "react";
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { runGroup } from "~/api/agent.ts";
|
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 { CompactPager } from "./agent-session-list.tsx";
|
||||||
import { isAgentPage, outletKey } from "./agent-store.ts";
|
import { isAgentPage, outletKey } from "./agent-store.ts";
|
||||||
|
|
||||||
@@ -84,3 +95,27 @@ describe("会话列表分页(窄列表一行)", () => {
|
|||||||
expect(html).toContain("1 / 1+");
|
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("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -108,3 +108,20 @@ export function usePageListGroup(activeRun?: AgentRunGroup | null): [AgentRunGro
|
|||||||
);
|
);
|
||||||
return [group, change];
|
return [group, change];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 输入草稿:按会话(和两处新对话输入框)分开记,切换会话、关抽屉不丢;发送成功后清掉。模块级,刷新页面才清 ──
|
||||||
|
|
||||||
|
const drafts = new Map<string, string>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
runPlanIds,
|
runPlanIds,
|
||||||
} from "./agent-conversation.tsx";
|
} from "./agent-conversation.tsx";
|
||||||
import { RunPlan } from "./agent-interaction.tsx";
|
import { RunPlan } from "./agent-interaction.tsx";
|
||||||
|
import { readDraft, runDraftKey, writeDraft } from "./agent-nav.ts";
|
||||||
import { shortGoalTitle } from "./agent-store.ts";
|
import { shortGoalTitle } from "./agent-store.ts";
|
||||||
import { SubagentTree } from "./agent-subagent-tree.tsx";
|
import { SubagentTree } from "./agent-subagent-tree.tsx";
|
||||||
import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts";
|
import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts";
|
||||||
@@ -71,6 +72,7 @@ export function GoalComposer({
|
|||||||
onClearContext,
|
onClearContext,
|
||||||
defaultGoal = "",
|
defaultGoal = "",
|
||||||
suggestions,
|
suggestions,
|
||||||
|
draftKey,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (goal: string, opts: AgentSessionOptions) => void;
|
onSubmit: (goal: string, opts: AgentSessionOptions) => void;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
@@ -80,8 +82,14 @@ export function GoalComposer({
|
|||||||
defaultGoal?: string;
|
defaultGoal?: string;
|
||||||
/** 快捷目标:输入框为空时显示 */
|
/** 快捷目标:输入框为空时显示 */
|
||||||
suggestions?: 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<HTMLDivElement>(null);
|
const box = useRef<HTMLDivElement>(null);
|
||||||
const fill = (text: string) => {
|
const fill = (text: string) => {
|
||||||
setGoal(text);
|
setGoal(text);
|
||||||
@@ -295,7 +303,12 @@ export function AgentRunPanel({
|
|||||||
const append = useAppendAgentMessage();
|
const append = useAppendAgentMessage();
|
||||||
const create = useCreateAgentRun();
|
const create = useCreateAgentRun();
|
||||||
const navigate = useNavigate();
|
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 [confirmStop, setConfirmStop] = useState(false);
|
||||||
const sseStatus = useSseStatus();
|
const sseStatus = useSseStatus();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { FlaskConical } from "lucide-react";
|
|||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
|
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 { GoalComposer } from "~/components/agent/agent-run-panel.tsx";
|
||||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||||
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
||||||
@@ -43,11 +49,15 @@ export default function AgentPage() {
|
|||||||
<GoalComposer
|
<GoalComposer
|
||||||
pending={create.isPending}
|
pending={create.isPending}
|
||||||
suggestions={quickGoalsFor("/console/agent")}
|
suggestions={quickGoalsFor("/console/agent")}
|
||||||
|
draftKey={DRAFT_PAGE_GOAL}
|
||||||
onSubmit={(goal, opts) =>
|
onSubmit={(goal, opts) =>
|
||||||
create.mutate(
|
create.mutate(
|
||||||
{ goal, ...opts },
|
{ 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)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user