fix(frontend): persistance immédiate de l'historique des prompts à chaque envoi — #323 (QA verte)

Symptôme: l'historique du composer CLI custom (flèche haut) n'était
écrit dans localStorage qu'via un effet sur l'état, sans garantie au
moment de l'envoi — une fermeture brutale/crash perdait la dernière
entrée.

- writeComposerHistory retourne désormais le succès de l'écriture; à
  chaque envoi, l'entrée est ajoutée et écrite AVANT l'invocation de
  l'agent (source de vérité par ref + état, réinit de l'index de
  navigation et du brouillon d'historique)
- l'effet d'écriture global est supprimé: l'historique n'est plus
  réécrit en boucle, seulement aux envois (et lectures au changement de
  scope projet/layout/node)
- si l'écriture échoue, l'envoi part quand même et un avertissement
  discret « Historique des prompts non sauvegardé. » (role=status)
  s'affiche au-dessus du composer
- tests: persistance vérifiée au moment de l'appel sendAgentChat,
  envoi conservé + avertissement affiché quand localStorage échoue

QA verte sur fix/batch-322-323-session-restore-prompt-history. Aucun push.
This commit is contained in:
2026-09-11 18:23:10 +02:00
parent b0d68c2d14
commit a3c2005938
2 changed files with 84 additions and 12 deletions

View File

@ -472,15 +472,16 @@ function readComposerHistory(storageKey: string): string[] {
}
}
function writeComposerHistory(storageKey: string, history: string[]): void {
if (typeof window === "undefined") return;
function writeComposerHistory(storageKey: string, history: string[]): boolean {
if (typeof window === "undefined") return true;
try {
window.localStorage.setItem(
storageKey,
JSON.stringify(history.slice(-COMPOSER_HISTORY_LIMIT)),
);
return true;
} catch {
/* local history only */
return false;
}
}
@ -1151,6 +1152,8 @@ export function CustomAgentChatView({
const [composerHistory, setComposerHistory] = useState<string[]>(() =>
readComposerHistory(composerHistoryKey),
);
const composerHistoryRef = useRef(composerHistory);
const [composerHistoryWarning, setComposerHistoryWarning] = useState<string | null>(null);
const [composerHistoryIndex, setComposerHistoryIndex] =
useState<number | null>(null);
const displayTurns = turns;
@ -1304,15 +1307,13 @@ export function CustomAgentChatView({
useEffect(() => {
const restored = readComposerHistory(composerHistoryKey);
composerHistoryRef.current = restored;
setComposerHistory(restored);
setComposerHistoryWarning(null);
setComposerHistoryIndex(null);
draftBeforeHistoryNavigationRef.current = "";
}, [composerHistoryKey]);
useEffect(() => {
writeComposerHistory(composerHistoryKey, composerHistory);
}, [composerHistory, composerHistoryKey]);
// Attachments can be large, so keep draft bytes only in this renderer's
// bounded lifetime instead of serialising base64 into localStorage. The
// project/layout/node scope mirrors the persisted text draft contract.
@ -2246,6 +2247,17 @@ export function CustomAgentChatView({
async function send() {
const text = draft.trim();
if (!canSend || !agent.sendAgentChat) return;
if (text) {
const nextHistory = appendComposerHistoryEntry(composerHistoryRef.current, text);
composerHistoryRef.current = nextHistory;
const persisted = writeComposerHistory(composerHistoryKey, nextHistory);
setComposerHistory(nextHistory);
setComposerHistoryWarning(
persisted ? null : "Historique des prompts non sauvegardé.",
);
setComposerHistoryIndex(null);
draftBeforeHistoryNavigationRef.current = "";
}
const slashInvocation = slashCommandInvocation(text);
if (slashInvocation) {
await executeSlashCommand(slashInvocation);
@ -2263,11 +2275,6 @@ export function CustomAgentChatView({
markProjectCliInteractionFinished(projectId, interactionId);
activeInteractionIdRef.current = null;
};
if (text) {
setComposerHistory((prev) => appendComposerHistoryEntry(prev, text));
setComposerHistoryIndex(null);
draftBeforeHistoryNavigationRef.current = "";
}
setDraft("");
setAttachments([]);
imageTokenSeqRef.current = 0;
@ -2705,6 +2712,11 @@ export function CustomAgentChatView({
})}
</div>
)}
{composerHistoryWarning && (
<p role="status" className="mb-1 text-[11px] text-warning">
{composerHistoryWarning}
</p>
)}
<div className="flex min-w-0 items-end gap-2">
<div className="relative flex min-w-0 flex-1">
<textarea