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
+20 -13
View File
@@ -1,3 +1,4 @@
import ContinueSessionButton from "./continue-session-button";
import {
useEffect,
useState,
@@ -93,21 +94,27 @@ export default function BotSessionsModal({
Sessions
</H2>
<Span className="text-[13px] leading-[18px] text-ink-secondary">
{`Each session keeps ${name}'s context short. A new one starts with a summary of the last; reopen any to pick up where you left off.`}
{`Each session keeps ${name}'s context short. New starts fresh; Continue carries a summary of this one into the next. Reopen any to pick up where you left off.`}
</Span>
</Stack>
<Button
title="Start a new session"
type="button"
size="small"
loading={sessions.busy_id == "new"}
disabled={Boolean(sessions.busy_id)}
onClick={sessions.startSession}
className="shrink-0 gap-1!"
>
<Plus size={14} aria-hidden />
New
</Button>
<Row className="shrink-0 gap-2! flex-nowrap items-center">
<ContinueSessionButton
bot_id={Number(bot.id)}
workspace_id={workspace_id}
/>
<Button
title="Start a fresh session, without carrying this one over"
type="button"
size="small"
loading={sessions.busy_id == "new"}
disabled={Boolean(sessions.busy_id)}
onClick={sessions.startSession}
className="shrink-0 gap-1!"
>
<Plus size={14} aria-hidden />
New
</Button>
</Row>
</Row>
<Input
id="bot-sessions-search"
@@ -0,0 +1,66 @@
import { useState } from "react";
import { MessageSquareShare } from "lucide-react";
import Button from "@moduletrace/twui/layout/Button";
import postSessionApi from "@/src/functions/frontend/post-session-api";
import conversationHref from "@/src/utils/conversation-href";
type Props = {
bot_id: number;
workspace_id?: number | null;
/**
* Renders the trigger instead of the default button (the conversation
* menu's row), given whether it's working and the click.
*/
render?: (params: { loading: boolean; onClick: () => void }) => React.ReactNode;
};
/**
* Continue in new session: carries the conversation into a new session
* with a summary of this one, as the bot does when asked
* (`api/admin/bots/continue-session`). Unlike New session, which starts
* fresh. Asks first, since this session then stays closed for good. It
* stays on screen (a menu stays open) while the summary is written, which
* takes a model call.
*/
export default function ContinueSessionButton({ bot_id, workspace_id, render }: Props) {
const [loading, setLoading] = useState(false);
const start = async () => {
if (loading) return;
if (!window.confirm("Continue in a new session? A summary of this session carries over, and this one is closed for good (you can still read it).")) return;
setLoading(true);
const res = await postSessionApi({
path: "continue-session",
body: { bot_id, workspace_id },
}).catch(() => null);
if (res?.success) {
window.location.assign(conversationHref({ bot_id, workspace_id }));
return;
}
setLoading(false);
window.alert(res?.msg || "Couldn't continue in a new session.");
};
if (render) return <>{render({ loading, onClick: start })}</>;
return (
<Button
title="Continue in a new session, with a summary of this one"
type="button"
size="small"
variant="outlined"
color="gray"
loading={loading}
disabled={loading}
onClick={start}
className="shrink-0 gap-1!"
>
<MessageSquareShare size={14} aria-hidden />
Continue
</Button>
);
}