fix(cli): restaure la conversation au retour de layout via cache conversationId+sessionId — #217

This commit is contained in:
2026-08-15 19:32:56 +02:00
parent 02715a00b8
commit 581063cf96
3 changed files with 236 additions and 12 deletions

View File

@ -67,15 +67,18 @@ interface CachedChatView {
turns: ChatTurn[];
progressIndex: Map<string, ProgressIndexEntry>;
chunkCount: number;
sessionId?: string;
}
const chatSessionViewCache = new Map<string, CachedChatView>();
const chatConversationViewCache = new Map<string, CachedChatView>();
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
const COMPOSER_RESIZE_STEP = 12;
const FALLBACK_COMPOSER_HEIGHT = 64;
export function __clearCustomAgentChatViewCacheForTests() {
chatSessionViewCache.clear();
chatConversationViewCache.clear();
}
interface AttachmentDraft {
@ -310,6 +313,14 @@ function emptyChatViewState(): ChatViewState {
return { turns: [], progressIndex: new Map() };
}
function conversationCacheKey(
projectId: string,
agentId: string,
conversationId: string | null,
): string | null {
return conversationId ? `${projectId}::${agentId}::${conversationId}` : null;
}
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
const key = progressUpsertKey(progress);
const existingIndex = state.turns.findIndex(
@ -711,10 +722,27 @@ export function CustomAgentChatView({
}: CustomAgentChatViewProps) {
const { agent, profile: profileGateway, system } = useGateways();
const pluginRuntime = usePluginRuntime();
const [turns, setTurns] = useState<ChatTurn[]>(
() => (sessionId ? (chatSessionViewCache.get(sessionId)?.turns ?? []) : []),
const initialConversationCacheKey = conversationCacheKey(
projectId,
agentId,
conversationId,
);
const [turns, setTurns] = useState<ChatTurn[]>(
() =>
sessionId
? (chatSessionViewCache.get(sessionId)?.turns ?? [])
: initialConversationCacheKey
? (chatConversationViewCache.get(initialConversationCacheKey)?.turns ?? [])
: [],
);
const [progressIndex, setProgressIndex] = useState<Map<string, ProgressIndexEntry>>(
() =>
sessionId
? (chatSessionViewCache.get(sessionId)?.progressIndex ?? new Map())
: initialConversationCacheKey
? (chatConversationViewCache.get(initialConversationCacheKey)?.progressIndex ?? new Map())
: new Map(),
);
const [progressIndex, setProgressIndex] = useState<Map<string, ProgressIndexEntry>>(new Map());
const [currentSession, setCurrentSession] = useState(sessionId);
const [externalSessionId, setExternalSessionId] = useState(sessionId);
const [draft, setDraft] = useState("");
@ -724,7 +752,7 @@ export function CustomAgentChatView({
const [slashActiveIndex, setSlashActiveIndex] = useState(0);
const [profileDialog, setProfileDialog] =
useState<ProfileCommandDialogState | null>(null);
const [opening, setOpening] = useState(Boolean(sessionId));
const [opening, setOpening] = useState(Boolean(sessionId || conversationId));
const [busy, setBusy] = useState(false);
const [stopping, setStopping] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -849,6 +877,10 @@ export function CustomAgentChatView({
() => (layoutId ? { projectId, layoutId, nodeId } : null),
[layoutId, nodeId, projectId],
);
const currentConversationCacheKey = useMemo(
() => conversationCacheKey(projectId, agentId, conversationId),
[agentId, conversationId, projectId],
);
const displayTurns = turns;
const clearPersistedDraft = useCallback(async () => {
@ -918,13 +950,27 @@ export function CustomAgentChatView({
);
useEffect(() => {
if (!currentSession) return;
chatSessionViewCache.set(currentSession, {
turns,
progressIndex,
chunkCount: scrollbackChunkCountRef.current,
});
}, [currentSession, progressIndex, turns]);
if (currentSession) {
chatSessionViewCache.set(currentSession, {
turns,
progressIndex,
chunkCount: scrollbackChunkCountRef.current,
sessionId: currentSession,
});
}
if (currentConversationCacheKey) {
const previous = chatConversationViewCache.get(currentConversationCacheKey);
const cachedSessionId = currentSession ?? previous?.sessionId;
if (cachedSessionId || turns.length > 0 || progressIndex.size > 0) {
chatConversationViewCache.set(currentConversationCacheKey, {
turns,
progressIndex,
chunkCount: scrollbackChunkCountRef.current,
...(cachedSessionId ? { sessionId: cachedSessionId } : {}),
});
}
}
}, [currentConversationCacheKey, currentSession, progressIndex, turns]);
useEffect(() => {
draftSaveSeqRef.current += 1;
@ -1216,6 +1262,15 @@ export function CustomAgentChatView({
setOpening(true);
setError(null);
try {
const localSession = sessionRef.current;
if (!externalSessionId && localSession) {
console.debug("[ticket149] openOrAttach:local-session-present:skip", {
timestamp: new Date().toISOString(),
execution,
currentSession: localSession,
});
return;
}
if (externalSessionId) {
const cached = chatSessionViewCache.get(externalSessionId);
if (cached) {
@ -1250,9 +1305,32 @@ export function CustomAgentChatView({
error: instrumentationError(e),
});
}
} else if (currentConversationCacheKey) {
const cached = chatConversationViewCache.get(currentConversationCacheKey);
if (cached) {
scrollbackChunkCountRef.current = cached.chunkCount;
applyChatViewState({
turns: cached.turns,
progressIndex: cached.progressIndex ?? new Map(),
});
if (cached.sessionId) {
const reattachedSessionId = await reattachStructuredSession(cached.sessionId, {
applyScrollback: true,
});
if (cancelled) return;
publishSessionId(reattachedSessionId);
console.debug("[ticket149] openOrAttach:reattach-conversation-cache:success", {
timestamp: new Date().toISOString(),
execution,
sessionId: reattachedSessionId,
});
return;
}
return;
}
}
await recoverStructuredSession({
applyScrollback: false,
applyScrollback: Boolean(conversationId),
retryAttachNotFound: true,
});
if (cancelled) return;
@ -1285,6 +1363,8 @@ export function CustomAgentChatView({
recoverStructuredSession,
publishSessionId,
applyChatViewState,
currentConversationCacheKey,
conversationId,
]);
const canSend = useMemo(