diff --git a/.agents/docs/components-sidebar-session-tree.md b/.agents/docs/components-sidebar-session-tree.md index 0ec5ebfa39..ce4d33e565 100644 --- a/.agents/docs/components-sidebar-session-tree.md +++ b/.agents/docs/components-sidebar-session-tree.md @@ -48,14 +48,30 @@ opener is ranked by its freshest opened Session. Without that, nesting would bur just-updated row under a stale opener and silently break the ordering contract of Updated mode. -## The leading slot +## The leading slot, and why status left it The opener and unrelated top-level rows keep the exact flat-list alignment; only a child widens the shared leading slot from 14px to 26px, which produces the 12px title indent -without shifting the row background. Status outranks the tree because a node can show -only one thing at its centre: an active child drops its trunk and elbow, and an active -opener drops its disclosure. Gating the opener on the whole activity set rather than -`isWorking` alone matters because the disclosure branch replaces the indicator — an -unread opener would otherwise render a chevron and lose its unread dot. The context -menu's expand/collapse item is then the only way to fold a busy opener, so it must stay -wired. +without shifting the row background. + +That slot used to hold status as well, with status winning: an active child dropped its +trunk and elbow, and an active opener dropped its disclosure, because a node can show +only one thing at its centre. The cost of that rule was backwards. Working, unread, and +waiting rows are precisely the ones a user is tracking through a tree, and they were the +only rows whose nesting silently disappeared — a group of opened Sessions looked like a +group right up to the moment any of it started running, and then looked like a flat list +with a stray indent. + +So status moved to the row's END slot instead, where it replaces the resting metric +cluster (line diff, `Mergeable`, worktree glyph, PR icon, time) for as long as it lasts. +Both halves of that are deliberate. The leading slot now draws the tree unconditionally, +so nesting is a stable fact about the list rather than a function of activity. And the +end slot still shows one thing at a time: a running row reads `[├─ title ......... ◌]`, +with nothing competing for the right edge and more room for the title. Nothing is lost — +the diff, branch, and PR detail were already in the desktop hover info card, which is +where a user goes when they want numbers rather than a glance. + +The mobile chat rows still keep status on their leading node; their geometry (48px +indent, a selection checkbox in the same slot, a chevron that has to sit outside the row +button and lift above the edge-back swipe zone) is different enough that the change is a +separate one to make. diff --git a/packages/components/src/components/AGENTS.md b/packages/components/src/components/AGENTS.md index e7d44ded1d..05f33df8c0 100644 --- a/packages/components/src/components/AGENTS.md +++ b/packages/components/src/components/AGENTS.md @@ -36,15 +36,21 @@ Reasoning: [components-sidebar-session-tree.md](../../../../.agents/docs/compone "simplify" that by rewriting `openedBySessionId` to the root: "Go to Opener Session" and the conversation's "Opened by" entry must land on the exact Tab that created it. - The opener and unrelated top-level rows keep the exact flat-list alignment. The shared - leading slot owns the node-centre affordance: an idle opener shows its disclosure at - rest and swaps it for ⋯ on row hover; an idle child shows ├/└ and swaps those for ⋯ in - the SAME 7px-centred position. STATUS OUTRANKS THE TREE on both sides: an active - (working / unread / waiting) child drops the trunk and elbow, and an active opener - drops its disclosure — never both. Gate the opener on the whole activity set, not just - `isWorking`, and keep the context menu's expand/collapse item wired to the same toggle - callback so a busy opener stays foldable. Only a child widens that slot from 14px to - 26px, producing the 12px title indent without shifting the row background. Keep - connector geometry in `sidebar-row-shared.tsx`. + leading slot owns the node-centre affordance: an opener shows its disclosure at rest + and swaps it for ⋯ on row hover; a child shows ├/└ and swaps those for ⋯ in the SAME + 7px-centred position. It draws the tree UNCONDITIONALLY — a working / unread / waiting + row must never lose its nesting, which is the whole point of moving status out of it. + Only a child widens that slot from 14px to 26px, producing the 12px title indent + without shifting the row background. Keep connector geometry in + `sidebar-row-shared.tsx`, and keep the context menu's expand/collapse item wired to the + same toggle callback. +- Desktop row status (working / waiting / unread) belongs to the END slot + (`SessionRowStatusIndicator` inside `SidebarRowEndSlot`) and nowhere else. While a + status shows it REPLACES that slot's resting content — line diff, `Mergeable`, worktree + glyph, PR icon, mobile time — so the right edge is one 14px mark; the metrics stay one + hover away in the desktop info card. Pass the three flags to the end slot, never to the + leading slot. The mobile chat rows keep their own leading-node rule + ([mobile/AGENTS.md](mobile/AGENTS.md)); do not assume the two match. - The resolver needs `allActiveSessions`, so any new list must take it from the sidebar rather than re-deriving it from rows. - The tree never hides a Session: a missing, cross-section, cross-group, cycling, or diff --git a/packages/components/src/components/loro-app-sidebar.tsx b/packages/components/src/components/loro-app-sidebar.tsx index e10497de79..ee63a9b00f 100644 --- a/packages/components/src/components/loro-app-sidebar.tsx +++ b/packages/components/src/components/loro-app-sidebar.tsx @@ -698,9 +698,6 @@ const LocalProjectSessionItem = memo(function LocalProjectSessionItem({ >
diff --git a/packages/components/src/components/session-list.tsx b/packages/components/src/components/session-list.tsx index 8aaba32037..bc474b2132 100644 --- a/packages/components/src/components/session-list.tsx +++ b/packages/components/src/components/session-list.tsx @@ -952,9 +952,6 @@ const SessionGroupSection = memo(function SessionGroupSection({ ) : null}
{/* Keep PR at the right edge, with All Changes totals immediately before it. */} diff --git a/packages/components/src/components/sidebar-row-shared.tsx b/packages/components/src/components/sidebar-row-shared.tsx index 5a5cca6969..7f60fde7c4 100644 --- a/packages/components/src/components/sidebar-row-shared.tsx +++ b/packages/components/src/components/sidebar-row-shared.tsx @@ -31,7 +31,7 @@ import { getGitHubOwnerAvatarUrl } from '@/lib/github-avatar'; * flat Updated list in `sidebar-updated-task-list.tsx`) render the same anatomy, * so the pieces live here once instead of being copied three times. * - * Row anatomy: `[① status/tree affordance | more][② author avatar? + title][③ diff/mergeable?][worktree?][④ PR icon? | archive]`. + * Row anatomy: `[① tree affordance | more][② author avatar? + title][③ status | diff/mergeable? + worktree? + PR icon? | archive]`. * The author avatar (`SessionRowAuthorAvatar`) only appears in team ("All Tasks") scope on a * multi-member workspace; otherwise the title owns the leading edge of slot ②. The leading slot * stays reserved even when empty (the ⋯ menu button reveals there on hover). A local worktree @@ -39,10 +39,16 @@ import { getGitHubOwnerAvatarUrl } from '@/lib/github-avatar'; * between the line diff and the PR icon (taking the PR's right-edge spot when there is no PR); * GitHub sessions are always worktrees so they never show it. The full * repo / folder / worktree session-type detail still lives in the desktop hover info card - * (`session-info-hover-card.tsx`). At rest, PR status - * owns the right edge when present and line diff sits immediately before it. Without - * PR status, line diff owns the right edge. Archive replaces the trailing content on - * hover, so the row's rightmost metric never shifts. + * (`session-info-hover-card.tsx`). + * + * The END slot (③) is the row's single status channel: working / waiting / unread + * REPLACES the whole resting metric cluster there, so an active row reads + * `[title][status]` and nothing else competes with it. Only a resting row shows + * metrics, where PR status owns the right edge when present with the line diff + * immediately before it. Archive replaces the trailing content on hover, so the + * row's rightmost mark never shifts. The leading slot (①) is therefore free to + * ALWAYS draw the opened-by tree: a running or unread child keeps its ├/└ and an + * active opener keeps its disclosure. */ export type SidebarRowKind = 'github' | 'local' | 'chat'; @@ -114,13 +120,18 @@ function MaskedPrCiIcon({ } /** - * ① The 14px status slot at the row's leading edge. Single-slot priority: - * `waitingPermission > isWorking > hasUnread > empty`. The PR status now lives in - * the end slot (④), not here, so a resting GitHub row's leading slot is empty - * unless it is working / unread — but the slot is always reserved so the ⋯ menu - * button can reveal there on hover without shifting the row. + * ③ The 14px status mark, rendered at the row's TRAILING edge inside + * {@link SidebarRowEndSlot}. Single-mark priority: + * `waitingPermission > isWorking > hasUnread`. Returns `null` when the session is + * idle and read, which is what lets the end slot fall back to its resting metric + * cluster (diff / worktree / PR) — a row shows one or the other, never both. + * + * It lives at the end rather than at the leading edge so the opened-by tree can + * own the leading slot unconditionally: before this, an active child had to drop + * its ├/└ connectors and the nesting silently disappeared exactly on the rows a + * user watches most. */ -function SessionRowIndicator({ +function SessionRowStatusIndicator({ isWaitingPermission, isWorking, hasUnreadMessages, @@ -144,6 +155,8 @@ function SessionRowIndicator({ icon = ; } + if (!icon) return null; + return (
{openerTree ? ( @@ -473,7 +483,7 @@ export function SessionRowLeadingSlot({ aria-hidden="true" /> - ) : showChildConnectors ? ( + ) : childTree ? (
- ) : ( -
- -
- )} + ) : null} {showMenuButton ? (