feat(chat): persistance bornee du brouillon custom chat par projet/layout (#182)
This commit is contained in:
@ -28,7 +28,7 @@ import type {
|
||||
import { useGateways } from "@/app/di";
|
||||
import { usePluginRuntime } from "@/features/plugins/PluginRuntimeProvider";
|
||||
import { Button, Spinner, cn } from "@/shared";
|
||||
import type { ChatAttachmentInput } from "@/ports";
|
||||
import type { ChatAttachmentInput, ChatDraftScope } from "@/ports";
|
||||
|
||||
export interface CustomAgentChatViewProps {
|
||||
projectId: string;
|
||||
@ -36,6 +36,7 @@ export interface CustomAgentChatViewProps {
|
||||
agentName: string;
|
||||
profile: AgentProfile;
|
||||
cwd: string;
|
||||
layoutId?: string;
|
||||
nodeId: string;
|
||||
sessionId: string | null;
|
||||
conversationId: string | null;
|
||||
@ -464,6 +465,7 @@ export function CustomAgentChatView({
|
||||
agentName,
|
||||
profile,
|
||||
cwd,
|
||||
layoutId,
|
||||
nodeId,
|
||||
sessionId,
|
||||
conversationId,
|
||||
@ -490,6 +492,12 @@ export function CustomAgentChatView({
|
||||
const sessionRef = useRef<string | null>(sessionId);
|
||||
sessionRef.current = currentSession;
|
||||
const slashQuerySeqRef = useRef(0);
|
||||
const draftSaveSeqRef = useRef(0);
|
||||
const draftHydratedRef = useRef(false);
|
||||
const draftEditedBeforeHydrationRef = useRef(false);
|
||||
const lastPersistedDraftRef = useRef("");
|
||||
const draftRef = useRef("");
|
||||
draftRef.current = draft;
|
||||
const mountedRef = useRef(false);
|
||||
const openOrAttachCountRef = useRef(0);
|
||||
const selfEmittedSessionIdRef = useRef<string | null | undefined>(undefined);
|
||||
@ -507,6 +515,30 @@ export function CustomAgentChatView({
|
||||
agent.closeAgentChat,
|
||||
);
|
||||
const slashPrefix = slashCommandPrefix(draft);
|
||||
const draftScope = useMemo<ChatDraftScope | null>(
|
||||
() => (layoutId ? { projectId, layoutId, nodeId } : null),
|
||||
[layoutId, nodeId, projectId],
|
||||
);
|
||||
|
||||
const clearPersistedDraft = useCallback(async () => {
|
||||
if (!draftScope) return;
|
||||
draftSaveSeqRef.current += 1;
|
||||
lastPersistedDraftRef.current = "";
|
||||
try {
|
||||
if (agent.clearChatDraft) {
|
||||
await agent.clearChatDraft(draftScope);
|
||||
} else if (agent.saveChatDraft) {
|
||||
await agent.saveChatDraft(draftScope, "");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[chat-draft] clear failed", instrumentationError(e));
|
||||
}
|
||||
}, [agent, draftScope]);
|
||||
|
||||
const editDraft = useCallback((text: string) => {
|
||||
if (!draftHydratedRef.current) draftEditedBeforeHydrationRef.current = true;
|
||||
setDraft(text);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
@ -520,6 +552,79 @@ export function CustomAgentChatView({
|
||||
if (chunk.kind === "final" || chunk.kind === "error") setBusy(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
draftSaveSeqRef.current += 1;
|
||||
draftHydratedRef.current = false;
|
||||
draftEditedBeforeHydrationRef.current = false;
|
||||
lastPersistedDraftRef.current = "";
|
||||
setDraft("");
|
||||
if (!draftScope || !agent.readChatDraft) {
|
||||
draftHydratedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
agent
|
||||
.readChatDraft(draftScope)
|
||||
.then((snapshot) => {
|
||||
if (cancelled || !mountedRef.current) return;
|
||||
const restored = snapshot.text ?? "";
|
||||
lastPersistedDraftRef.current = restored;
|
||||
if (!draftEditedBeforeHydrationRef.current) {
|
||||
setDraft(restored);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (!cancelled) {
|
||||
console.warn("[chat-draft] read failed", instrumentationError(e));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
draftHydratedRef.current = true;
|
||||
if (
|
||||
draftEditedBeforeHydrationRef.current &&
|
||||
agent.saveChatDraft &&
|
||||
draftRef.current !== lastPersistedDraftRef.current
|
||||
) {
|
||||
const text = draftRef.current;
|
||||
void agent
|
||||
.saveChatDraft(draftScope, text)
|
||||
.then(() => {
|
||||
lastPersistedDraftRef.current = text;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("[chat-draft] save failed", instrumentationError(e));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [agent, draftScope]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draftScope || !agent.saveChatDraft || !draftHydratedRef.current) return;
|
||||
if (draft === lastPersistedDraftRef.current) return;
|
||||
|
||||
const seq = ++draftSaveSeqRef.current;
|
||||
const timeout = window.setTimeout(() => {
|
||||
if (draftSaveSeqRef.current !== seq) return;
|
||||
agent
|
||||
.saveChatDraft!(draftScope, draft)
|
||||
.then(() => {
|
||||
if (draftSaveSeqRef.current === seq) {
|
||||
lastPersistedDraftRef.current = draft;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn("[chat-draft] save failed", instrumentationError(e));
|
||||
});
|
||||
}, 250);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [agent, draft, draftScope]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!supported ||
|
||||
@ -554,12 +659,12 @@ export function CustomAgentChatView({
|
||||
|
||||
const selectSlashCommand = useCallback((command: SlashCommand) => {
|
||||
if (!isSlashCommandAvailable(command)) return;
|
||||
setDraft(`${command.name} `);
|
||||
editDraft(`${command.name} `);
|
||||
setSlashMenuOpen(false);
|
||||
setSlashCommands([]);
|
||||
setSlashActiveIndex(0);
|
||||
requestAnimationFrame(() => composerRef.current?.focus());
|
||||
}, []);
|
||||
}, [editDraft]);
|
||||
|
||||
const publishSessionId = useCallback((nextSessionId: string | null) => {
|
||||
selfEmittedSessionIdRef.current = nextSessionId;
|
||||
@ -887,6 +992,7 @@ export function CustomAgentChatView({
|
||||
return;
|
||||
}
|
||||
setDraft("");
|
||||
void clearPersistedDraft();
|
||||
setError(null);
|
||||
try {
|
||||
const sid =
|
||||
@ -979,6 +1085,7 @@ export function CustomAgentChatView({
|
||||
return unique.length > 0 ? [...prev, ...unique] : prev;
|
||||
});
|
||||
if (pastedText) {
|
||||
if (!draftHydratedRef.current) draftEditedBeforeHydrationRef.current = true;
|
||||
setDraft((prev) =>
|
||||
prev.slice(0, selectionStart) +
|
||||
pastedText +
|
||||
@ -1004,6 +1111,7 @@ export function CustomAgentChatView({
|
||||
const displayText = text || "Pièce jointe";
|
||||
setDraft("");
|
||||
setAttachments([]);
|
||||
void clearPersistedDraft();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setTurns((prev) => [
|
||||
@ -1305,7 +1413,7 @@ export function CustomAgentChatView({
|
||||
value={draft}
|
||||
disabled={!supported || opening || busy}
|
||||
placeholder="Message à l'agent…"
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onChange={(e) => editDraft(e.target.value)}
|
||||
onPaste={(e) => void pasteClipboardFiles(e)}
|
||||
onKeyDown={(e) => {
|
||||
if (slashMenuOpen && slashCommands.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user