Files
cobble/src/components/general/bot-sessions-modal.tsx
T
tbenandClaude Opus 5.5 657a76d868 Mark the session on screen as Current in the session lists
- The conversation page tells the rail and the sessions modal which
  session it shows (announceShownSession, useShownSession); they reload,
  so a session reopened by ?session_id= shows at once.
- Only that session is Current and greyed out (the active one without
  ?session_id=); the active row then leads back to it.
- The rail lists the session on screen even when it's older than its
  latest sessions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 05:56:13 +01:00

171 lines
6.6 KiB
TypeScript

import ContinueSessionButton from "./continue-session-button";
import useShownSession from "@/src/hooks/use-shown-session";
import conversationHref from "@/src/utils/conversation-href";
import {
useEffect,
useState,
type ComponentProps,
type ReactNode,
} from "react";
import Input from "@moduletrace/twui/form/Input";
import Modal from "@moduletrace/twui/elements/Modal";
import Button from "@moduletrace/twui/layout/Button";
import H2 from "@moduletrace/twui/layout/H2";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { History, Plus, CircleAlert } from "lucide-react";
import type { BotWithLastMessage } from "@/src/types";
import useBotSessions from "@/src/hooks/use-bot-sessions";
import BotSessionRow from "./bot-session-row";
import HeaderIconButton from "./header-icon-button";
import LoadingBlock from "@moduletrace/twui/elements/LoadingBlock";
type Props = {
bot: BotWithLastMessage;
workspace_id?: number | null;
/**
* A bot message arrived in another session of this conversation.
*/
has_other_unread?: boolean;
/**
* What opens it; the conversation header's sessions icon by default.
*/
target?: ReactNode;
target_wrapper_props?: ComponentProps<typeof Modal>["targetWrapperProps"];
/**
* Runs as it opens, e.g. closing the menu it was opened from.
*/
on_open?: () => void;
};
/**
* The sessions icon (or another target) that opens a conversation's
* sessions. A conversation is split into sessions so a bot's context never
* grows without bound; this lists them, searches them, starts a fresh one,
* or reopens an old one to carry on.
*/
export default function BotSessionsModal({
bot,
workspace_id,
has_other_unread,
target,
target_wrapper_props,
on_open,
}: Props) {
const [open, setOpen] = useState(false);
const on_screen = useShownSession({ bot_id: Number(bot.id), workspace_id });
const sessions = useBotSessions({
bot_id: Number(bot.id),
workspace_id,
open,
refresh_key: on_screen ? `${on_screen.id}:${on_screen.status}` : null,
});
const name = bot.name || "the bot";
useEffect(() => {
if (open) {
on_open?.();
}
}, [open]);
return (
<Modal
target={
target ?? (
<HeaderIconButton
icon={History}
title={
has_other_unread
? "Sessions: new messages in another session"
: "Sessions"
}
/>
)
}
targetWrapperProps={
target_wrapper_props ?? {
className: target ? "w-full" : "shrink-0",
}
}
open={open}
setOpen={setOpen}
className="max-w-lg bg-paper p-6 xl:p-10 py-14!"
>
<Row className="w-full gap-3! flex-nowrap items-start justify-between">
<Stack className="min-w-0 gap-1! items-stretch">
<H2 className="text-lg! leading-6! tracking-normal! font-bold text-ink">
Sessions
</H2>
<Span className="text-[13px] leading-[18px] text-ink-secondary">
{`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>
<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"
title="Search sessions"
placeholder="Search titles, summaries and messages"
autoComplete="off"
defaultValue={sessions.query}
onChange={(e) => sessions.setQuery(e.target.value)}
wrapperWrapperProps={{ className: "mt-4" }}
/>
{sessions.error ? (
<Span
role="alert"
className="mt-2 flex items-center gap-1.5 text-[13px] leading-[18px] text-ink"
>
<CircleAlert size={14} aria-hidden className="shrink-0" />
{sessions.error}
</Span>
) : null}
<Stack className="w-full mt-2 max-h-[60vh] overflow-y-auto gap-0! items-stretch">
{sessions.sessions.map((session) => (
<BotSessionRow
key={session.id}
session={session}
loading={sessions.busy_id == session.id}
disabled={Boolean(sessions.busy_id)}
current={on_screen?.id ? on_screen.id == session.id : session.status == "active"}
onOpen={() =>
session.status == "active"
? window.location.assign(conversationHref({ bot_id: Number(bot.id), workspace_id }))
: sessions.reopenSession({
session_id: Number(session.id),
})
}
/>
))}
{sessions.loading ? (
<LoadingBlock className="w-full h-[300px]" />
) : sessions.sessions.length ? (
<Span className="px-3 py-6 text-[13px] leading-[18px] text-ink-secondary">
{sessions.query.trim()
? `No session matches "${sessions.query.trim()}".`
: "No sessions yet."}
</Span>
) : null}
</Stack>
</Modal>
);
}