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:
@@ -38,6 +38,8 @@ import HistoryErrorRow from "../(partials)/history-error-row";
|
||||
import useConversationHistory from "../(hooks)/use-conversation-history";
|
||||
import mergeLoadedHistory from "../(utils)/merge-loaded-history";
|
||||
import LoadingLine from "@/src/components/general/loading-line";
|
||||
import SessionMovedBar from "../(partials)/session-moved-bar";
|
||||
import syncSessionQuery from "../(utils)/sync-session-query";
|
||||
|
||||
/**
|
||||
* The conversation column: header, bottom-anchored thread, composer, the
|
||||
@@ -52,7 +54,9 @@ import LoadingLine from "@/src/components/general/loading-line";
|
||||
* the history (the session's latest messages and the bot's task there)
|
||||
* loads on the client with a loading line under the header, and again
|
||||
* when the back button restores the page. Older messages load as the
|
||||
* thread scrolls up.
|
||||
* thread scrolls up. `?session_id=` opens another session of the
|
||||
* conversation; a closed one shows, in the composer's place, a link to
|
||||
* where the conversation went on.
|
||||
*/
|
||||
export default function Conversation() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
@@ -73,6 +77,12 @@ export default function Conversation() {
|
||||
BotMessageWithAttachments[]
|
||||
>([]);
|
||||
const [session_id, setSessionId] = useState<number | null>(null);
|
||||
/**
|
||||
* Set when the session shown was carried over into a newer one
|
||||
* (opened by `?session_id=`, or carried on from while on screen): that
|
||||
* session, which the composer gives way to a link to.
|
||||
*/
|
||||
const [moved_to_session_id, setMovedToSessionId] = useState<number | null>(null);
|
||||
const [has_other_unread, setHasOtherUnread] = useState(false);
|
||||
const [reply_to, setReplyTo] = useState<BotMessageWithAttachments | null>(
|
||||
null,
|
||||
@@ -85,6 +95,12 @@ export default function Conversation() {
|
||||
* before the history landed stays.
|
||||
*/
|
||||
const known_session = useRef<number | null>(null);
|
||||
/**
|
||||
* Set when the user's own message carried the conversation into a new
|
||||
* session (the rollover at the session length): the thread follows
|
||||
* it, and what's on its way goes there, so it stays.
|
||||
*/
|
||||
const following_send = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const moved = Boolean(known_session.current);
|
||||
@@ -93,6 +109,12 @@ export default function Conversation() {
|
||||
|
||||
if (!moved) return;
|
||||
|
||||
if (following_send.current) {
|
||||
following_send.current = false;
|
||||
setReplyTo(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setReplyTo(null);
|
||||
setPendingMessages([]);
|
||||
}, [session_id]);
|
||||
@@ -148,8 +170,14 @@ export default function Conversation() {
|
||||
const loaded_session_id = Number(history.session.id);
|
||||
|
||||
setSessionId(loaded_session_id);
|
||||
setMovedToSessionId(history.moved_to_session_id || null);
|
||||
syncSessionQuery({ session: history.session });
|
||||
setMessages((prev) => mergeLoadedHistory({ shown: prev, history }));
|
||||
older.reset({ session_id: loaded_session_id, has_more: history.has_more });
|
||||
older.reset({
|
||||
session_id: loaded_session_id,
|
||||
has_more: history.has_more,
|
||||
previous_session_id: Number(history.session.previous_session_id) || null,
|
||||
});
|
||||
progress.reset({ task: history.task });
|
||||
},
|
||||
});
|
||||
@@ -161,7 +189,13 @@ export default function Conversation() {
|
||||
|
||||
const last_saved_id = messages[messages.length - 1]?.id || null;
|
||||
const last_thread_message = thread_messages[thread_messages.length - 1];
|
||||
const awaiting_reply = isAwaitingReply({ last_message: last_thread_message, face: bot?.face });
|
||||
/**
|
||||
* A session moved on from gets no reply here: the bot answered in the
|
||||
* new one.
|
||||
*/
|
||||
const awaiting_reply =
|
||||
!moved_to_session_id &&
|
||||
isAwaitingReply({ last_message: last_thread_message, face: bot?.face });
|
||||
|
||||
const reply_progress = useReplyProgress({
|
||||
bot_id: bot?.id || 0,
|
||||
@@ -208,9 +242,34 @@ export default function Conversation() {
|
||||
message.session_id &&
|
||||
message.session_id != session_id
|
||||
) {
|
||||
/**
|
||||
* This session's conversation was carried into a new one,
|
||||
* whose summary opens it. While the user's message is on
|
||||
* its way, it was that message (the rollover at the
|
||||
* session length): the thread follows into the new
|
||||
* session. Otherwise (the bot, Continue in new session in
|
||||
* another tab) the composer gives way to a link there.
|
||||
*/
|
||||
if (Number(message.carried_from_session_id) == session_id) {
|
||||
if (pending_messages.length) {
|
||||
following_send.current = true;
|
||||
history.retry();
|
||||
return;
|
||||
}
|
||||
|
||||
setMovedToSessionId(Number(message.session_id));
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user wrote in a newer session (another tab, or a
|
||||
* rollover whose summary push was missed): the thread
|
||||
* moves there, and loads it so its opening shows too.
|
||||
*/
|
||||
if (message.author == "user" && startsNewSession(message)) {
|
||||
setSessionId(Number(message.session_id));
|
||||
setMessages([message]);
|
||||
history.retry();
|
||||
} else if (message.author == "user") {
|
||||
window.location.reload();
|
||||
} else {
|
||||
@@ -341,11 +400,19 @@ export default function Conversation() {
|
||||
reply_progress={reply_progress}
|
||||
receiving={pending_messages.length > 0}
|
||||
has_task={Boolean(progress.task_id)}
|
||||
moved_on={Boolean(moved_to_session_id)}
|
||||
older={older}
|
||||
onQuote={({ text }) =>
|
||||
composer.current?.insertQuote({ text })
|
||||
}
|
||||
/>
|
||||
{moved_to_session_id ? (
|
||||
<SessionMovedBar
|
||||
bot_id={Number(bot.id)}
|
||||
workspace_id={workspace_id}
|
||||
moved_to_session_id={moved_to_session_id}
|
||||
/>
|
||||
) : (
|
||||
<Composer
|
||||
handle_ref={composer}
|
||||
bot={bot}
|
||||
@@ -381,10 +448,12 @@ export default function Conversation() {
|
||||
startsNewSession(message) &&
|
||||
message.session_id != session_id
|
||||
) {
|
||||
following_send.current = true;
|
||||
setSessionId(
|
||||
Number(message.session_id),
|
||||
);
|
||||
setMessages([message]);
|
||||
history.retry();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -395,6 +464,7 @@ export default function Conversation() {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</BotMessageActionsContext.Provider>
|
||||
<ComputerViewerModal bot={live_bot} />
|
||||
|
||||
Reference in New Issue
Block a user