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({
>
{/* 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 (
- )}
+ ) : null}
{showMenuButton ? (
+ ) : (
+ restIcon
+ );
+ const hasRest = Boolean(restContent);
// Reserve the action hit target whenever something can occupy it; otherwise the
// slot sizes to its resting content (for example, a +/- line diff).
const reserve = hasRest || Boolean(archive);
@@ -632,6 +661,7 @@ export function SidebarRowEndSlot({
// pointer-events-none so the resting PR icon area still passes clicks through to
// the row's navigation; the Archive button re-enables pointer events on hover.
{/* Keep PR at the right edge, with All Changes totals immediately before it. */}
{/* Keep PR at the right edge, with All Changes totals immediately before it. */}
@@ -852,6 +849,9 @@ const TaskGroupSection = memo(function TaskGroupSection({
{/* Keep PR at the right edge, with All Changes totals immediately before it. */}
diff --git a/packages/components/src/stories/SessionList.stories.tsx b/packages/components/src/stories/SessionList.stories.tsx
index 21cf829bf1..1d67e88b86 100644
--- a/packages/components/src/stories/SessionList.stories.tsx
+++ b/packages/components/src/stories/SessionList.stories.tsx
@@ -662,7 +662,9 @@ export const AllDiffAndPrStates: Story = {
* Sessions — own machine, own project, own lifecycle — not `parentSessionId`
* child tabs. Covered here: an opener with several opened Sessions, a working
* one, an unread one, the active one, and an orphan whose opener is not in this
- * list (archived / another scope) and therefore stays top-level.
+ * list (archived / another scope) and therefore stays top-level. The working and
+ * unread children keep their ├/└ and carry their status at the row's right edge,
+ * where it stands in for the diff / PR cluster.
*/
const OPENED_SESSIONS_ARGS: SessionListProps = {
selectedSessionId: 'mcp-opened-2',
@@ -726,10 +728,10 @@ export const OpenedSessionsLight: Story = {
};
/**
- * The opener itself is WORKING. Status outranks the tree at that node, so the
- * opener shows its spinner instead of the disclosure — the same rule an active
- * opened Session follows when it drops its ├/└. Folding is still reachable
- * from the row's context menu, which carries the identical toggle.
+ * The opener itself is WORKING. Its disclosure stays put and the spinner shows at
+ * the row's END slot, replacing the diff / PR metrics there — the same rule an
+ * active opened Session follows while keeping its ├/└. Nothing about the tree
+ * depends on activity any more.
*/
export const OpenedSessionsActiveOpener: Story = {
name: 'Opened Sessions (MCP) · Active opener',
diff --git a/packages/components/tests/loro-app-sidebar-worktree.test.ts b/packages/components/tests/loro-app-sidebar-worktree.test.ts
index 5879c302d2..2232922e53 100644
--- a/packages/components/tests/loro-app-sidebar-worktree.test.ts
+++ b/packages/components/tests/loro-app-sidebar-worktree.test.ts
@@ -132,7 +132,7 @@ describe('LocalProjectItem session-type icon', () => {
expect(plainIcon).toBeNull();
});
- it('renders the PR status icon for a local session linked to a GitHub PR', () => {
+ it('renders the PR status icon for a resting local session linked to a GitHub PR', () => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
@@ -142,6 +142,9 @@ describe('LocalProjectItem session-type icon', () => {
...baseSession,
id: 'session-with-pr',
title: 'Local session with PR',
+ // Read, so the end slot shows its resting metrics: a status mark would
+ // take that slot for itself (`sidebar-row-shared.tsx`).
+ lastReadAt: Date.parse('2026-05-09T11:45:00.000Z'),
project: {
kind: 'local',
localProjectId,
@@ -154,6 +157,19 @@ describe('LocalProjectItem session-type icon', () => {
...baseSession,
id: 'session-without-pr',
title: 'Plain local session',
+ lastReadAt: Date.parse('2026-05-09T11:45:00.000Z'),
+ },
+ {
+ ...baseSession,
+ id: 'session-unread-with-pr',
+ title: 'Unread local session with PR',
+ project: {
+ kind: 'local',
+ localProjectId,
+ machineId,
+ githubRepoFullName: 'loro-dev/lody',
+ },
+ pullRequests: [{ url: 'https://github.com/loro-dev/lody/pull/43', status: 'open' }],
},
];
@@ -202,5 +218,11 @@ describe('LocalProjectItem session-type icon', () => {
const rowWithoutPr = container.querySelector('[data-sidebar-session-id="session-without-pr"]');
expect(rowWithPr?.querySelector('.lucide-git-pull-request')).not.toBeNull();
expect(rowWithoutPr?.querySelector('.lucide-git-pull-request')).toBeNull();
+
+ // An unread row spends its end slot on the status mark instead — the PR icon
+ // is one of the resting metrics that yields to it.
+ const unreadRow = container.querySelector('[data-sidebar-session-id="session-unread-with-pr"]');
+ expect(unreadRow?.querySelector('.lucide-git-pull-request')).toBeNull();
+ expect(unreadRow?.querySelector('[data-session-row-indicator]')).not.toBeNull();
});
});
diff --git a/packages/components/tests/session-list-opened-by.test.ts b/packages/components/tests/session-list-opened-by.test.ts
index 1c040b4513..8bc3c5e846 100644
--- a/packages/components/tests/session-list-opened-by.test.ts
+++ b/packages/components/tests/session-list-opened-by.test.ts
@@ -290,17 +290,20 @@ describe('SessionList opened-by rendering', () => {
).toBe('false');
});
- it('shows the working spinner instead of the disclosure on a working opener', () => {
+ it('keeps the disclosure on a working opener and marks status at the row end', () => {
renderList([
makeRow({ sessionId: 'opener', isWorking: true }),
makeRow({ sessionId: 'opened-1', openedBySessionId: 'opener' }),
]);
- const openerSlot = container
- ?.querySelector('[data-sidebar-session-id="opener"]')
- ?.querySelector('[data-session-row-leading-slot]');
- expect(openerSlot?.querySelector('[data-session-working-spinner]')).not.toBeNull();
- expect(openerSlot?.querySelector('[data-session-opened-by-toggle]')).toBeNull();
+ const opener = container?.querySelector('[data-sidebar-session-id="opener"]');
+ const openerSlot = opener?.querySelector('[data-session-row-leading-slot]');
+ // The leading slot is the tree's, unconditionally: status no longer evicts it.
+ expect(openerSlot?.querySelector('[data-session-opened-by-toggle]')).not.toBeNull();
+ expect(openerSlot?.querySelector('[data-session-row-indicator]')).toBeNull();
+
+ const openerEnd = opener?.querySelector('[data-session-row-end-slot]');
+ expect(openerEnd?.querySelector('[data-session-working-spinner]')).not.toBeNull();
});
it('keeps collapse reachable from the context menu while the opener is working', () => {
@@ -326,45 +329,46 @@ describe('SessionList opened-by rendering', () => {
expect(container?.querySelector('[data-sidebar-session-id="opened-1"]')).toBeNull();
});
- it('hides tree connectors on an active child and keeps them on an idle child', () => {
+ it('keeps tree connectors on an active child and moves its status to the row end', () => {
+ // The regression this locks: a working or unread child used to drop its ├/└,
+ // so the rows a user actually watches were the ones that lost their nesting.
renderList(makeOpenerGroupRows());
+ const rowOf = (sessionId: string) =>
+ container?.querySelector(`[data-sidebar-session-id="${sessionId}"]`);
const slotOf = (sessionId: string) =>
- container
- ?.querySelector(`[data-sidebar-session-id="${sessionId}"]`)
- ?.querySelector('[data-session-row-leading-slot]');
-
- const idleSlot = slotOf('opened-1');
- expect(idleSlot?.querySelector('[data-session-row-indicator]')).toBeNull();
- expect(idleSlot?.querySelectorAll('[data-session-tree-connector]')).toHaveLength(2);
-
- const workingSlot = slotOf('opened-2');
- expect(workingSlot?.querySelector('[data-session-tree-connector]')).toBeNull();
- expect(workingSlot?.querySelector('[data-session-working-spinner]')).not.toBeNull();
+ rowOf(sessionId)?.querySelector('[data-session-row-leading-slot]');
+ const endOf = (sessionId: string) =>
+ rowOf(sessionId)?.querySelector('[data-session-row-end-slot]');
+
+ for (const sessionId of ['opened-1', 'opened-2', 'opened-3']) {
+ const slot = slotOf(sessionId);
+ expect(slot?.querySelectorAll('[data-session-tree-connector]')).toHaveLength(2);
+ expect(slot?.querySelector('[data-session-row-indicator]')).toBeNull();
+ }
- const unreadSlot = slotOf('opened-3');
- expect(unreadSlot?.querySelector('[data-session-tree-connector]')).toBeNull();
- expect(unreadSlot?.querySelector('[data-session-row-indicator] span')).not.toBeNull();
+ expect(endOf('opened-1')?.querySelector('[data-session-row-indicator]')).toBeNull();
+ expect(endOf('opened-2')?.querySelector('[data-session-working-spinner]')).not.toBeNull();
+ expect(endOf('opened-3')?.querySelector('[data-session-row-indicator] span')).not.toBeNull();
});
- it('hides the opener disclosure for an unread or waiting opener, not just a working one', () => {
- // The disclosure branch REPLACES the indicator, so gating only on
- // `isWorking` makes an unread opener render a chevron and silently lose its
- // unread dot. Every status that would draw a mark has to take the node.
+ it('keeps the disclosure AND the status mark for an unread or waiting opener', () => {
+ // The two no longer compete for one node, so neither may be dropped: the
+ // disclosure stays at the leading edge and the status mark at the row end.
for (const status of [{ hasUnreadMessages: true }, { isWaitingPermission: true }] as const) {
renderList([
makeRow({ sessionId: 'opener', ...status }),
makeRow({ sessionId: 'opened-1', openedBySessionId: 'opener' }),
]);
- const openerSlot = container
- ?.querySelector('[data-sidebar-session-id="opener"]')
- ?.querySelector('[data-session-row-leading-slot]');
- expect(openerSlot?.querySelector('[data-session-opened-by-toggle]')).toBeNull();
- // The status mark it would otherwise have lost.
- expect(openerSlot?.querySelector('[data-session-row-indicator]')?.children.length).toBe(1);
+ const opener = container?.querySelector('[data-sidebar-session-id="opener"]');
+ const openerSlot = opener?.querySelector('[data-session-row-leading-slot]');
+ expect(openerSlot?.querySelector('[data-session-opened-by-toggle]')).not.toBeNull();
+ expect(
+ opener?.querySelector('[data-session-row-end-slot] [data-session-row-indicator]')?.children
+ .length
+ ).toBe(1);
- // The tree itself is untouched — only the affordance yields.
expect(
container
?.querySelector('[data-sidebar-session-id="opened-1"]')
@@ -373,6 +377,24 @@ describe('SessionList opened-by rendering', () => {
}
});
+ it('drops the resting metric cluster while a row carries a status', () => {
+ // "Only status at the right edge": the diff numbers a running row would
+ // otherwise show are what made the end slot compete with the status mark.
+ renderList([
+ makeRow({ sessionId: 'idle', addedLines: 12, deletedLines: 3 }),
+ makeRow({ sessionId: 'busy', addedLines: 12, deletedLines: 3, isWorking: true }),
+ ]);
+
+ const endOf = (sessionId: string) =>
+ container
+ ?.querySelector(`[data-sidebar-session-id="${sessionId}"]`)
+ ?.querySelector('[data-session-row-end-slot]');
+
+ expect(endOf('idle')?.textContent).toContain('+12');
+ expect(endOf('busy')?.textContent).not.toContain('+12');
+ expect(endOf('busy')?.querySelector('[data-session-working-spinner]')).not.toBeNull();
+ });
+
it('renders an active opened session as the selected row', () => {
const store = createStore();
container = document.createElement('div');
diff --git a/packages/components/tests/session-list-pr-badge.test.ts b/packages/components/tests/session-list-pr-badge.test.ts
index d1e0300e10..67aa4a53e2 100644
--- a/packages/components/tests/session-list-pr-badge.test.ts
+++ b/packages/components/tests/session-list-pr-badge.test.ts
@@ -6,7 +6,7 @@ import { flushSync } from 'react-dom';
import { createRoot, type Root } from 'react-dom/client';
import { Provider } from 'jotai';
import { SessionList } from '../src/components/session-list';
-import { SessionPrIcon, SessionRowLeadingSlot } from '../src/components/sidebar-row-shared';
+import { SessionPrIcon, SidebarRowEndSlot } from '../src/components/sidebar-row-shared';
import { initI18n } from '../src/i18n';
const PR_STATUS_CASES = [
@@ -357,9 +357,8 @@ describe('SessionList PR badge', () => {
flushSync(() => {
root?.render(
- React.createElement(SessionRowLeadingSlot, {
+ React.createElement(SidebarRowEndSlot, {
isWorking: true,
- menuLabel: 'More actions',
})
);
});
@@ -374,10 +373,9 @@ describe('SessionList PR badge', () => {
flushSync(() => {
root?.render(
- React.createElement(SessionRowLeadingSlot, {
+ React.createElement(SidebarRowEndSlot, {
isWorking: false,
hasUnreadMessages: true,
- menuLabel: 'More actions',
})
);
});
diff --git a/packages/components/tests/sidebar-updated-opened-by.test.tsx b/packages/components/tests/sidebar-updated-opened-by.test.tsx
index ca8756d74e..26a6579c28 100644
--- a/packages/components/tests/sidebar-updated-opened-by.test.tsx
+++ b/packages/components/tests/sidebar-updated-opened-by.test.tsx
@@ -225,6 +225,34 @@ describe('SidebarUpdatedSessionList opened-by rendering', () => {
expect(childSlot?.querySelector('button[aria-label="More actions"]')).not.toBeNull();
});
+ it('keeps the connectors on a working or unread child and puts status at the row end', () => {
+ // `opened-1` is working and `opened-2` is unread: the rows most likely to be
+ // watched are exactly the ones that used to lose their ├/└ to a status mark.
+ render(makeOpenerItems());
+
+ const rowOf = (id: string) => container?.querySelector(`[data-sidebar-updated-id="${id}"]`);
+
+ for (const id of ['opened-1', 'opened-2', 'opened-3']) {
+ expect(
+ rowOf(id)
+ ?.querySelector('[data-session-row-leading-slot]')
+ ?.querySelectorAll('[data-session-tree-connector]')
+ ).toHaveLength(2);
+ }
+
+ expect(
+ rowOf('opened-1')?.querySelector(
+ '[data-session-row-end-slot] [data-session-working-spinner]'
+ )
+ ).not.toBeNull();
+ expect(
+ rowOf('opened-2')?.querySelector('[data-session-row-end-slot] [data-session-row-indicator]')
+ ).not.toBeNull();
+ expect(
+ rowOf('opened-3')?.querySelector('[data-session-row-end-slot] [data-session-row-indicator]')
+ ).toBeNull();
+ });
+
it('offers collapse from the opener context menu', () => {
render(makeOpenerItems());
const opener = container?.querySelector('[data-sidebar-updated-id="opener"]');
diff --git a/packages/components/tests/spinner-rotates-in-place.test.tsx b/packages/components/tests/spinner-rotates-in-place.test.tsx
index d165152f36..a520e61ea0 100644
--- a/packages/components/tests/spinner-rotates-in-place.test.tsx
+++ b/packages/components/tests/spinner-rotates-in-place.test.tsx
@@ -26,7 +26,7 @@ import React from 'react';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { flushSync } from 'react-dom';
import { createRoot, type Root } from 'react-dom/client';
-import { SessionRowLeadingSlot } from '../src/components/sidebar-row-shared';
+import { SidebarRowEndSlot } from '../src/components/sidebar-row-shared';
import { SessionSyncingIndicator } from '../src/components/sessions/session-syncing-indicator';
import { MobileConnectionStatus } from '../src/components/mobile/mobile-connection-status';
import { initI18n } from '../src/i18n';
@@ -61,12 +61,9 @@ function render(node: React.ReactElement) {
describe('spinners rotate in place', () => {
it('session row working indicator: square SVG is centered without a vertical nudge', () => {
- render(
- React.createElement(SessionRowLeadingSlot, {
- isWorking: true,
- menuLabel: 'More actions',
- })
- );
+ // The status mark lives in the row's END slot (`sidebar-row-shared.tsx`),
+ // which is where the spinner has to keep its square, centered box.
+ render(React.createElement(SidebarRowEndSlot, { isWorking: true }));
const spinner = container.querySelector('[data-session-working-spinner]');
expect(spinner).not.toBeNull();