From 0aad723a50363619de806d5caec7888c2b1b1f92 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Mon, 19 Oct 2026 18:33:46 +0100 Subject: [PATCH] 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. --- src/layouts/bots/(partials)/bot-row.tsx | 5 ++-- src/layouts/bots/(partials)/rail-bot-card.tsx | 25 +++++++++++++++++++ .../bots/(partials)/rail-bot-entry.tsx | 23 ++++++++++++++--- .../bots/(partials)/rail-bot-session-row.tsx | 2 +- .../bots/(partials)/rail-bot-sessions.tsx | 22 ++++++++++------ .../bots/(partials)/rail-crew-list.tsx | 5 ++-- 6 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 src/layouts/bots/(partials)/rail-bot-card.tsx diff --git a/src/layouts/bots/(partials)/bot-row.tsx b/src/layouts/bots/(partials)/bot-row.tsx index 478c01b..3f1e286 100644 --- a/src/layouts/bots/(partials)/bot-row.tsx +++ b/src/layouts/bots/(partials)/bot-row.tsx @@ -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", )} > diff --git a/src/layouts/bots/(partials)/rail-bot-card.tsx b/src/layouts/bots/(partials)/rail-bot-card.tsx new file mode 100644 index 0000000..47d1b0d --- /dev/null +++ b/src/layouts/bots/(partials)/rail-bot-card.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/layouts/bots/(partials)/rail-bot-entry.tsx b/src/layouts/bots/(partials)/rail-bot-entry.tsx index 4548a75..71bb9ad 100644 --- a/src/layouts/bots/(partials)/rail-bot-entry.tsx +++ b/src/layouts/bots/(partials)/rail-bot-entry.tsx @@ -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 ( - <> + - {selected ? : null} - + {selected ? ( + <> + + + ) : null} + ); } diff --git a/src/layouts/bots/(partials)/rail-bot-session-row.tsx b/src/layouts/bots/(partials)/rail-bot-session-row.tsx index 4fd708a..ae62446 100644 --- a/src/layouts/bots/(partials)/rail-bot-session-row.tsx +++ b/src/layouts/bots/(partials)/rail-bot-session-row.tsx @@ -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!", )} > - {shown.map((session) => ( - sessions.reopenSession({ session_id: Number(session.id) })} + + - ))} + {shown.map((session) => ( + sessions.reopenSession({ session_id: Number(session.id) })} + /> + ))} + {sessions.sessions.length > AppData.RailRecentSessions ? ( {coordinator ? ( - <> + ) : null} - + ) : null} openForm({ kind: "bot" })} />}