Group the rail's open bot with its sessions

The active row and its session previews now share one highlighted card,
with a hairline thread from the bot's icon down to the last session.
This commit is contained in:
2026-10-19 18:33:46 +01:00
parent 4d5195447b
commit 0aad723a50
6 changed files with 65 additions and 17 deletions
+3 -2
View File
@@ -27,7 +27,8 @@ type Props = {
/**
* 48px rail row. Status is the face and the subtitle (the bot's status
* line while it has one): no badges, spinners or state dots.
* Selected is ink at 5%, never a hue.
* The selected background (ink at 5%, never a hue) is painted by
* RailBotCard, which also holds the sessions.
*/
export default function BotRow({ bot, href, selected, subtitle }: Props) {
const hue = (bot.hue || null) as BotHue | null;
@@ -48,7 +49,7 @@ export default function BotRow({ bot, href, selected, subtitle }: Props) {
"flex shrink-0 h-12 items-center gap-3 rounded-default px-1 @rail:px-3 border-0 leading-normal",
"hover:opacity-100 hover:bg-sunken transition-colors duration-[var(--m-fast)]",
"focus-visible:ring-2 focus-visible:ring-ink focus-visible:ring-offset-2 focus-visible:ring-offset-paper",
selected && "bg-ink/5 hover:bg-ink/5",
selected && "hover:bg-transparent hover:dark:bg-transparent",
)}
>
<BotAvatar image_media_id={bot.image_media_id} hue={hue} face={(bot.face || "idle") as BotFace} />
@@ -0,0 +1,25 @@
import Stack from "@moduletrace/twui/layout/Stack";
import type { ReactNode } from "react";
import { twMerge } from "tailwind-merge";
type Props = {
/**
* Groups the open bot's row with its sessions, so the active background
* covers both and the sessions read as belonging to that bot.
*/
selected?: boolean;
children: ReactNode;
};
export default function RailBotCard({ selected, children }: Props) {
return (
<Stack
className={twMerge(
"w-full shrink-0 flex-col gap-0! items-stretch rounded-default py-1",
selected && "bg-ink/5",
)}
>
{children}
</Stack>
);
}
+18 -3
View File
@@ -1,6 +1,8 @@
import type { BotWithLastMessage } from "@/src/types";
import BotRow from "./bot-row";
import RailBotCard from "./rail-bot-card";
import RailBotSessions from "./rail-bot-sessions";
import Divider from "@moduletrace/twui/layout/Divider";
type Props = {
bot: BotWithLastMessage;
@@ -12,11 +14,24 @@ type Props = {
/**
* A worker in the rail, and its sessions while it is the open conversation.
*/
export default function RailBotEntry({ bot, href, selected, workspace_id }: Props) {
export default function RailBotEntry({
bot,
href,
selected,
workspace_id,
}: Props) {
return (
<>
<RailBotCard selected={selected}>
<BotRow bot={bot} href={href} selected={selected} />
{selected ? <RailBotSessions key={bot.id} bot={bot} workspace_id={workspace_id} /> : null}
{selected ? (
<>
<RailBotSessions
key={bot.id}
bot={bot}
workspace_id={workspace_id}
/>
</>
) : null}
</RailBotCard>
);
}
@@ -32,7 +32,7 @@ export default function RailBotSessionRow({ session, loading, disabled, onOpen }
buttonContentProps={{ className: "w-full min-w-0 flex-nowrap" }}
className={twMerge(
"w-full h-8 shrink-0 justify-start! items-center! gap-2! rounded-default px-2! @rail:pl-14! text-left text-ink dark:text-ink hover:bg-ink/5 hover:dark:bg-ink/5 focus-visible:ring-2 focus-visible:ring-ink",
active && "bg-sunken hover:bg-sunken hover:dark:bg-sunken opacity-100!",
active && "bg-transparent hover:bg-transparent hover:dark:bg-transparent opacity-100!",
)}
>
<Span
@@ -30,6 +30,11 @@ export default function RailBotSessions({ bot, workspace_id }: Props) {
return (
<Stack className="hidden @rail:flex w-full shrink-0 flex-col gap-0! items-stretch pb-1">
<Stack className="relative w-full flex-col gap-0! items-stretch">
<Span
aria-hidden
className="pointer-events-none absolute left-7 -translate-x-1/2 -top-2 bottom-0 w-px bg-hairline"
/>
{shown.map((session) => (
<RailBotSessionRow
key={session.id}
@@ -39,6 +44,7 @@ export default function RailBotSessions({ bot, workspace_id }: Props) {
onOpen={() => sessions.reopenSession({ session_id: Number(session.id) })}
/>
))}
</Stack>
{sessions.sessions.length > AppData.RailRecentSessions ? (
<BotSessionsModal
bot={bot}
@@ -4,6 +4,7 @@ import { AppContext } from "@/src/pages/__root";
import canManageCrew from "@/src/utils/rbac/can-manage-crew";
import type { BotWithLastMessage, WorkspaceWithActivity } from "@/src/types";
import CoordinatorRow from "./coordinator-row";
import RailBotCard from "./rail-bot-card";
import RailBotSessions from "./rail-bot-sessions";
import RailBotsSection from "./rail-bots-section";
import RailSectionAddButton from "./rail-section-add-button";
@@ -28,7 +29,7 @@ export default function RailCrewList({ bots, workspaces, selected_id }: Props) {
return (
<>
{coordinator ? (
<>
<RailBotCard selected={coordinator.id === selected_id}>
<CoordinatorRow
bot={coordinator}
bots={bots}
@@ -37,7 +38,7 @@ export default function RailCrewList({ bots, workspaces, selected_id }: Props) {
{coordinator.id === selected_id ? (
<RailBotSessions key={coordinator.id} bot={coordinator} />
) : null}
</>
</RailBotCard>
) : null}
<RailBotsSection
action={<RailSectionAddButton title="Add a bot" onClick={() => openForm({ kind: "bot" })} />}