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:
parent
fecc885d3e
commit
4c4f80cd8c
1 changed files with 49 additions and 15 deletions
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Reference in a new issue