fix(ask): resume the active thread after leaving the page

Navigating away from /dashboard/chat and back (e.g. via the sidebar
nav, which drops the ?thread= query) reset the conversation to a
blank composer even though the thread was still live and listed in
the sidebar. Persist the actor's active thread id in localStorage and
restore it when the URL has no ?thread=, so a conversation survives
leaving the page as the plan specifies. "New thread" clears the
stored id so it still starts blank.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DEls 2026-09-26 21:48:54 +02:00
parent fecc885d3e
commit 4c4f80cd8c

View file

@ -64,6 +64,10 @@ interface UiMemory {
}
const ACTOR_STORAGE_KEY = "mega-ask-user";
/** Per-actor: the thread they were last in, so leaving the page (e.g. via
the sidebar nav) and coming back resumes it instead of starting blank.
"New thread" clears this; picking a thread or sending a message sets it. */
const ACTIVE_THREAD_STORAGE_PREFIX = "mega-ask-active-thread:";
function loadStoredActor(): Actor | null {
if (typeof window === "undefined") return null;
@ -78,6 +82,25 @@ function loadStoredActor(): Actor | null {
return null;
}
function loadStoredThreadId(actorId: string): string | null {
if (typeof window === "undefined") return null;
try {
return window.localStorage.getItem(ACTIVE_THREAD_STORAGE_PREFIX + actorId);
} catch {
return null;
}
}
function saveStoredThreadId(actorId: string, threadId: string | null) {
if (typeof window === "undefined") return;
try {
if (threadId) window.localStorage.setItem(ACTIVE_THREAD_STORAGE_PREFIX + actorId, threadId);
else window.localStorage.removeItem(ACTIVE_THREAD_STORAGE_PREFIX + actorId);
} catch {
/* ignore */
}
}
/** Relative time for the thread list ("2h ago", "Just now"). */
function relativeTime(iso?: string): string {
if (!iso) return "";
@ -317,6 +340,7 @@ function ChatViewInner() {
const loadThread = React.useCallback(
async (threadId: string, forActor: Actor) => {
setActiveThreadId(threadId);
saveStoredThreadId(forActor.id, threadId);
setTurns([]);
setError(null);
try {
@ -361,20 +385,6 @@ function ChatViewInner() {
[],
);
// React to the URL's ?thread= — covers first load and back/forward.
React.useEffect(() => {
if (!actor) return;
const fromUrl = searchParams.get("thread");
if (fromUrl && fromUrl !== activeThreadIdRef.current) {
loadThread(fromUrl, actor);
} else if (!fromUrl && activeThreadIdRef.current) {
setActiveThreadId(null);
setTurns([]);
}
// Only re-run when the actor first resolves or the URL changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actor, searchParams]);
const goToThread = React.useCallback(
(threadId: string | null, mode: "push" | "replace" = "push") => {
const url = threadId ? `${pathname}?thread=${threadId}` : pathname;
@ -384,13 +394,35 @@ function ChatViewInner() {
[pathname, router],
);
// React to the URL's ?thread= — covers first load and back/forward.
React.useEffect(() => {
if (!actor) return;
const fromUrl = searchParams.get("thread");
if (fromUrl && fromUrl !== activeThreadIdRef.current) {
loadThread(fromUrl, actor);
} else if (!fromUrl && activeThreadIdRef.current) {
setActiveThreadId(null);
setTurns([]);
} else if (!fromUrl && !activeThreadIdRef.current) {
// No thread in the URL and nothing loaded yet — resume the last
// thread this actor was in (e.g. after leaving via the sidebar nav
// and coming back), rather than silently starting a blank one.
// "New thread" clears the stored id, so that still lands here empty.
const stored = loadStoredThreadId(actor.id);
if (stored) goToThread(stored, "replace");
}
// Only re-run when the actor first resolves or the URL changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actor, searchParams]);
const startNewThread = React.useCallback(() => {
if (actor) saveStoredThreadId(actor.id, null);
setActiveThreadId(null);
setTurns([]);
setError(null);
setSidebarOpen(false);
goToThread(null, "push");
}, [goToThread]);
}, [actor, goToThread]);
const selectThread = React.useCallback(
(t: ThreadSummary) => {
@ -614,6 +646,7 @@ function ChatViewInner() {
// reflect it in the URL and sidebar.
activeThreadIdRef.current = data.id;
setActiveThreadId(data.id);
saveStoredThreadId(actor.id, data.id);
setThreads((prev) => [
{ id: data.id, title: data.title, last_message_at: new Date().toISOString() },
...prev.filter((t) => t.id !== data.id),
@ -761,6 +794,7 @@ function ChatViewInner() {
if (!activeThreadIdRef.current && data.threadId) {
activeThreadIdRef.current = data.threadId;
setActiveThreadId(data.threadId);
saveStoredThreadId(actor.id, data.threadId);
setThreads((prev) => [
{ id: data.threadId, title: data.threadTitle ?? file.name, last_message_at: new Date().toISOString() },
...prev.filter((t) => t.id !== data.threadId),