Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,13 @@ describe("SidebarConversationCard sub-session chevron", () => {
expect(queryByLabelText("Expand sub-conversations")).toBeNull()
})

it("overlays the chevron on the rail axis (the agent-icon position)", () => {
it("anchors the chevron to the rail gutter", () => {
const { getByLabelText } = renderCard(conv(2), {
hasChildren: true,
expanded: false,
})
// The chevron now sits at the agent-icon's rail-axis x (revealed on hover),
// not in the right-hand time/action slot.
// The chevron stays in the rail gutter, separate from the agent icon
// and the right-hand time/action slot.
const chevron = getByLabelText("Expand sub-conversations")
expect(chevron.style.left).toContain("--conv-rail-axis")
})
Expand Down
77 changes: 46 additions & 31 deletions src/components/conversations/sidebar-conversation-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
CheckCircle2,
Info,
ChevronRight,
MessagesSquare,
SquareTerminal,
} from "lucide-react"
import { useTranslations } from "next-intl"
Expand Down Expand Up @@ -384,9 +385,8 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
"rounded-full",
"pr-[0.25rem]"
)}
// Rail-axis-relative left padding (was a fixed `pl-7`): at depth 0
// this resolves to 0.875rem + 0.875rem = 1.75rem (= pl-7), so root
// rows are pixel-identical; deeper rows inherit the shifted var.
// Keep agent icons and titles aligned for parents and leaves.
// The child chevron occupies the gutter before the rail.
style={{
paddingLeft:
"calc(var(--conv-rail-axis, 0.875rem) + 0.875rem)",
Expand All @@ -412,16 +412,7 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
}}
/>
<div
className={cn(
"pointer-events-none absolute top-1/2 z-10 flex items-center justify-center",
// With children, the row hover (or focus) swaps this agent
// icon out for the expand chevron at the same spot — fade it
// so the two cross-fade in place. On touch (no hover) the
// chevron is always shown, so the icon is always hidden.
hasChildren &&
onToggleExpand &&
"transition-opacity duration-150 group-hover:opacity-0 group-focus-within:opacity-0 [@media(hover:none)]:opacity-0"
)}
className="pointer-events-none absolute top-1/2 z-10 flex items-center justify-center"
style={{
left: "var(--conv-rail-axis, 0.875rem)",
width: "0.875rem",
Expand Down Expand Up @@ -495,13 +486,9 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
the hover bubble names the original path instead. */}
</button>

{/* Expand/collapse affordance for delegation children. It overlays
the agent icon at the rail axis: idle shows the icon, and
hovering (or focusing) the row swaps in this chevron at the very
same spot while the icon fades. A sibling of the row button (HTML
forbids nested buttons) with `stopPropagation` so a toggle never
selects the row; pointer events stay off until revealed so a
click on the icon area still selects the row when not hovering. */}
{/* Keep the child toggle visible in the gutter before the
agent icon. It is a sibling of the row button so expanding
a subtree never selects the conversation. */}
{hasChildren && onToggleExpand && (
<button
type="button"
Expand All @@ -520,31 +507,59 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
? t("collapseSubsessions")
: t("expandSubsessions")
}
className={cn(
"absolute top-0 bottom-0 z-20 flex items-center justify-center",
"cursor-pointer outline-none",
"opacity-0 pointer-events-none transition-opacity duration-150",
"group-hover:opacity-100 group-hover:pointer-events-auto",
"group-focus-within:opacity-100 group-focus-within:pointer-events-auto",
"[@media(hover:none)]:opacity-100 [@media(hover:none)]:pointer-events-auto"
)}
className="absolute top-0 bottom-0 z-20 flex cursor-pointer items-center justify-center rounded-sm text-sidebar-foreground outline-none hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-ring"
style={{
left: "var(--conv-rail-axis, 0.875rem)",
width: "0.875rem",
left: "calc(var(--conv-rail-axis, 0.875rem) - 0.75rem)",
width: "0.75rem",
transform: "translateX(-50%)",
}}
>
<ChevronRight
aria-hidden
className={cn(
"h-3 w-3 shrink-0 text-muted-foreground/70",
"h-3.5 w-3.5 shrink-0",
"transition-transform duration-200 ease-out",
expanded && "rotate-90"
)}
/>
</button>
)}

{hasChildren &&
onToggleExpand &&
conversation.child_count > 0 && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onToggleExpand(conversation.id)
}}
aria-label={`${
expanded
? t("collapseSubsessions")
: t("expandSubsessions")
} (${conversation.child_count})`}
aria-expanded={expanded}
title={
expanded
? t("collapseSubsessions")
: t("expandSubsessions")
}
className={cn(
"mr-1 inline-flex h-5 shrink-0 cursor-pointer items-center gap-1 rounded-full border border-sidebar-foreground/15 px-1.5 text-[0.6875rem] font-semibold text-sidebar-foreground outline-none transition-colors",
"hover:bg-sidebar-foreground/15 focus-visible:ring-2 focus-visible:ring-ring",
expanded
? "bg-sidebar-foreground/12"
: "bg-sidebar-foreground/6"
)}
>
<MessagesSquare className="h-3 w-3" aria-hidden />
<span className="tabular-nums">
{conversation.child_count}
</span>
</button>
)}

{/* Right slot: sizes to its content — the time / status badge
normally, the two quick-action buttons (pin, done) on hover —
so it never reserves more width than what is actually shown
Expand Down
Loading