Carry conversations into new sessions with a visible summary

- Bots get continue_in_new_session: the bot writes a title and summary,
  the session closes for good (read-only) and the new one opens with the
  summary as a "Carried over from the last session" message that links
  back to the old session.
- Continue in new session (conversation menu, sessions modal) and the
  automatic rollover at the session length do the same; New session now
  starts a fresh, unlinked session.
- Session length is an app setting in Settings > General.
- ?session_id= opens a session; a carried-over one shows a bar linking to
  where the conversation went on instead of the composer.
- The thread can open the previous session above the current one (shown
  only, never in the prompt), and replies get search_past_sessions and
  read_past_session.
- Sessions are named from their opening messages, not their latest, and
  a carried summary is labelled as the earlier session's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 05:35:52 +01:00
co-authored by Claude Opus 5.5
parent 1e6833b5d8
commit cc109bc361
64 changed files with 2074 additions and 128 deletions
@@ -15,7 +15,8 @@ type Params = {
* Loads the open conversation's history once the page is on screen, and
* again whenever the browser restores the page from its back-forward
* cache (`pageshow` with `persisted`), so going back never shows an old
* thread. Everything else on the page (the header, the composer) comes
* thread. The page's `?session_id=` picks a session other than the
* active one, read on every load. Everything else on the page (the header, the composer) comes
* from the server and shows at once. `loaded` turns true with the first
* load; a conversation the user can no longer open sends them where the
* server says.
@@ -37,7 +38,10 @@ export default function useConversationHistory({ bot_id, workspace_id, onLoaded
setLoading(true);
setError(undefined);
const res = await fetchConversationHistory({ bot_id, workspace_id }).catch(() => null);
const session_id =
Number(new URLSearchParams(window.location.search).get("session_id")) || null;
const res = await fetchConversationHistory({ bot_id, workspace_id, session_id }).catch(() => null);
/** A newer load started meanwhile: it has the last word. */
if (id != request.current) return;
@@ -20,9 +20,12 @@ type Params = {
/**
* Loads the session's older messages a page at a time, when the thread
* is scrolled to its top. A session the thread moves onto starts with
* its first message, so it has none older; `reset` starts over from a
* loaded history instead.
* is scrolled to its top. Once the session's first message is in, the
* session it carries on from can be opened above it (`loadPrevious`, on
* the user's ask) and paged the same way, and so on back. Those messages
* are only shown: the bot's prompt still reads the session it answers in.
* A session the thread moves onto starts with its first message, so it
* has none older; `reset` starts over from a loaded history instead.
*/
export default function useOlderMessages({
session_id,
@@ -31,6 +34,11 @@ export default function useOlderMessages({
onLoaded,
}: Params) {
const [has_more, setHasMore] = useState(initial_has_more);
/**
* The session the thread's top belongs to, and the one before it.
*/
const [paging_session_id, setPagingSessionId] = useState<number | null>(session_id || null);
const [previous_session_id, setPreviousSessionId] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const base_session = useRef(session_id);
@@ -40,30 +48,41 @@ export default function useOlderMessages({
base_session.current = session_id;
setHasMore(false);
setPagingSessionId(session_id || null);
setPreviousSessionId(null);
setError(undefined);
}, [session_id]);
/**
* A history loaded for the session: whether it has older messages.
* A history loaded for the session: whether it has older messages,
* and the session it carries on from.
*/
const reset = useCallback(
({ session_id, has_more }: { session_id: number; has_more: boolean }) => {
({
session_id,
has_more,
previous_session_id,
}: {
session_id: number;
has_more: boolean;
previous_session_id?: number | null;
}) => {
base_session.current = session_id;
setHasMore(has_more);
setPagingSessionId(session_id);
setPreviousSessionId(previous_session_id || null);
setError(undefined);
},
[],
);
const loadOlder = async () => {
if (loading || !has_more || !session_id || !first_message_id) return;
const load = async ({ session_id, before_id }: { session_id: number; before_id?: number | null }) => {
setLoading(true);
setError(undefined);
const res = await postSessionApi<OlderBotMessagesData>({
path: "older-messages",
body: { session_id, before_id: first_message_id },
body: { session_id, before_id },
}).catch(() => null);
setLoading(false);
@@ -75,7 +94,29 @@ export default function useOlderMessages({
onLoaded({ messages: res.singleRes.messages });
setHasMore(res.singleRes.has_more);
setPagingSessionId(res.singleRes.session.id);
setPreviousSessionId(res.singleRes.session.previous_session_id);
};
return { has_more, loading, error, loadOlder, reset };
const loadOlder = async () => {
if (loading || !has_more || !paging_session_id || !first_message_id) return;
await load({ session_id: paging_session_id, before_id: first_message_id });
};
const loadPrevious = async () => {
if (loading || has_more || !previous_session_id) return;
await load({ session_id: previous_session_id });
};
return {
has_more,
has_previous: !has_more && Boolean(previous_session_id),
loading,
error,
loadOlder,
loadPrevious,
reset,
};
}