fix(sidebar): put folder rows and session rows on one left edge - #3905
Conversation
UX Review (Fable 5) — ✅ PASSAdvisory UX-level review of UX-Verdict: PASS Alignment fix is real and visible in the screenshots; density cut was requested; the folder unread dot even gains an accessible name it never had. Watch
Suggestions
[UX-REVIEWED] 7db2aa7 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — ✅ PASSAdvisory design-level review of Design-Verdict: PASS Fixes the root cause — the hidden [DESIGN-REVIEWED] 7db2aa7 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSAdvisory premise-level review of I've read the contract, the intent file, and the patch, and checked the repo for sibling patterns (the First-Principles-Verdict: CONCERNS The alignment fix is cause-level and earns its place; a spinner-icon swap and a 6px row-density change ride along, and the spinner's own rationale indicts 21 untouched siblings. What this change shipsIntent: make folder rows and the sessions inside them share one left edge — a FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 7db2aa7 |
cc70f15 to
d2a0d3c
Compare
d2a0d3c to
6380d63
Compare
Dispositions
Rebased onto |
6380d63 to
5537051
Compare
Revert of
|
#3903 |
here | why |
|---|---|---|
folder header pl-[18px] pr-3 |
px-3.5 |
guide 3 requires header pad == session row pad; 18 breaks it |
unread dot absolute left-1.5 |
inline, right of the name | the left gutter is the only reason the pad had to be 18 |
body ml-4, no inner pad |
ml-3 pl-1 |
2 + 12 == 14, which is guide 1 |
row pl-1 pr-2 gap-1 |
pl-3.5 pr-3 |
the gutter is out of flow, so the pad is the whole content offset |
glyph→name gap-2 |
gap-[5px] |
at 8 the name overshoots the content column by 3px |
2px colour bar (index.css) |
kept | orthogonal to the guides |
Re-measured on the built SPA after the revert — all three guides hold at both depths and in the root lane: depth 1 connector/glyph 259, name/agent/title/subtitle 278; depth 2 connector/glyph 278 (== depth 1's content column), name/content 297; root-lane session content 259 (== the root folder's glyph). Gutter position: absolute, centre offset 0 on both a one-line and a three-line row.
Screenshots and the test header are updated to these numbers. tsc clean, eslint 0 errors, i18n green, 20445 frontend tests pass.
5537051 to
6f9e23e
Compare
A folder in the session list and the sessions filed under it did not share a left edge. #3766 added the row's status gutter as an in-flow flex child, so its 12px width plus a gap landed on the content column: the agent label, title and tool-call subtitle of every session inside a folder sat well right of that folder's own name. #3903 then tightened the same rows and got closer, but measured on a real render it still left the folder name 1px past its sessions' text and a nested folder's glyph 2px short of their column. Three guides now hold, in the root lane and at every nesting depth: 1. a folder glyph sits on its own border-l connector line 2. a folder name shares one edge with the agent label / title / tool-call subtitle of every session inside it 3. a NESTED folder's glyph sits on the content column of the sessions filed beside it, so a subfolder reads as their peer Solving all three simultaneously forces header pad == session row pad. With D = 2, a MEASURED container differential (the header's box starts 2px left of the nested body's box, and no class in the component expresses it): guide 1 P = D + M 14 = 2 + 12 guide 2 P + G + g = D + M + B + p + R 33 = 33 guide 3 P = R 14 = 14 The status gutter moves out of the content flow (absolutely positioned inside that pad) and is centred on the row rather than deriving the headline's y; in flow it is what broke guide 2, and no class-level assertion could see it. Because the algebra has no per-depth term it is scale-free: depth 3 nests as depth 2 does, and root-lane sessions land on the root folder's glyph, which restores the original pre-#1211 guide. Measured on the built SPA: depth 1 glyph/connector 259, name and all three text lines 278; depth 2 glyph/connector 278 (== depth 1's content column), name/content 297; root lane content 259. This reverts #3903's row and folder geometry, agreed with its author. Its collapsed-folder unread dot goes back inline on the right of the name rather than into an absolute left gutter: that gutter is what forced the header pad to 18px, and the pad is not free — it has to equal the session row's. The 2px colour bar from #3903 is kept, being orthogonal to the guides. Also drop the title's mt-0.5 (row height 49.6 -> 43.6px idle, 68.1 -> 62.1 running), use lucide Loader for the running spinner (at 12px the LoaderCircle arc reads as a broken ring where the spokes read as motion), and pull the divider inset and the empty-folder "New chat in X" affordance onto the same column as the sessions. The alignment invariant test has been rewritten by each of the last two PRs to accept the then-current misalignment. It now pins all three guides, adds guide 3 at depth 2 with a nested fixture, and the gutter test pins the gutter as out-of-flow and row-centred. Every number in the test header is a measurement with its frame named: two paper estimates during this fix were 2px and 3px out.
6f9e23e to
7db2aa7
Compare
Dispositions —
|
Dispositions —
|
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: cosmetic-UI (6 files). Criteria: no conflict, no requested changes, security path denylist clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: cosmetic-UI — sidebar folder/session row left-edge alignment fix (3 screenshots + 1 component + 2 tests).
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: cosmetic-UI (6 files). Criteria: no conflict, no requested changes, security path denylist clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: cosmetic-UI — folder rows and session rows aligned to one left edge (absolute-position status glyph, pl-3.5 unification).
The sidebar's three folder alignment guides have regressed four times (#1211, #3766, #3903, and twice on paper inside #3905), every time via a Tailwind-class derivation that omitted the 2px FOLDER_BODY_INSET_PX container differential between the folder header's box and the nested body's box. The jsdom test (ChatSidebar.folderAlignment.test.tsx) cannot catch that class of break: jsdom has no layout engine, so it asserts class tokens and an arithmetic identity between constants, never the rendered geometry. Add a Playwright spec to the `E2E (stub ACP backend, offline)` gate that seeds one folder, one subfolder, and sessions in both plus one ungrouped session via the harness gateway API, then asserts the guides on getBoundingClientRect().left of the real rendered elements: - guide 1: folder glyph on its border-l connector line (depth 1 and 2) - guide 2: folder name on the agent label / title of its sessions (depth 1 and 2) - guide 3: nested folder glyph on its sibling sessions' content column, and the root-lane identity (ungrouped content on the root glyph) - depth invariance: the depth-2 connector on depth 1's content column Falsified against the historical regression class: rebuilding with FOLDER_BODY_INSET_PX bumped 2 -> 4 fails the spec with "fGlyph=259 vs fConnector=261" while the jsdom test stays green. The missing-selector diagnostics were falsified the same way (a broken selector fails with "missing: [aAgent, ...]", not a bare timeout). Pre-push review (GPT + Opus model-pinned): no blocking. Fixed advisory findings: absolute sanity anchors so difference-only assertions cannot hold vacuously at left=0; seeding requests assert response.ok() so a 4xx fails as a seeding fault, not an alignment fault; the settle poll subsumes the render wait (budget fits the 30s test timeout) and names missing selectors; the serial-run dependency on the wipe discipline in session-tags-folders.spec.ts is documented in the header. The tool-call subtitle is not asserted: it needs a live agent turn to render, is a sibling of the title inside the same block container (equal left by construction), and its class parity is pinned by the jsdom test. MIN_EXECUTED_SPECS raised 219 -> 220 for the added spec. Closes #3914
The sidebar's three folder alignment guides have regressed four times (#1211, #3766, #3903, and twice on paper inside #3905), every time via a Tailwind-class derivation that omitted the 2px FOLDER_BODY_INSET_PX container differential between the folder header's box and the nested body's box. The jsdom test (ChatSidebar.folderAlignment.test.tsx) cannot catch that class of break: jsdom has no layout engine, so it asserts class tokens and an arithmetic identity between constants, never the rendered geometry. Add a Playwright spec to the `E2E (stub ACP backend, offline)` gate that seeds one folder, one subfolder, and sessions in both plus one ungrouped session via the harness gateway API, then asserts the guides on getBoundingClientRect().left of the real rendered elements: - guide 1: folder glyph on its border-l connector line (depth 1 and 2) - guide 2: folder name on the agent label / title of its sessions (depth 1 and 2) - guide 3: nested folder glyph on its sibling sessions' content column, and the root-lane identity (ungrouped content on the root glyph) - depth invariance: the depth-2 connector on depth 1's content column Falsified against the historical regression class: rebuilding with FOLDER_BODY_INSET_PX bumped 2 -> 4 fails the spec with "fGlyph=259 vs fConnector=261" while the jsdom test stays green. The missing-selector diagnostics were falsified the same way (a broken selector fails with "missing: [aAgent, ...]", not a bare timeout). Pre-push review (GPT + Opus model-pinned): no blocking. Fixed advisory findings: absolute coherence anchors so difference-only assertions cannot hold vacuously at left=0; seeding requests assert response.ok() so a 4xx fails as a seeding fault, not an alignment fault; the settle poll subsumes the render wait (budget fits the 30s test timeout) and names missing selectors; the serial-run dependency on the wipe discipline in session-tags-folders.spec.ts is documented in the header. The tool-call subtitle is not asserted: it needs a live agent turn to render, is a sibling of the title inside the same block container (equal left by construction), and its class parity is pinned by the jsdom test. MIN_EXECUTED_SPECS raised 219 -> 220 for the added spec. Closes #3914
… (#3942) The sidebar's three folder alignment guides have regressed four times (#1211, #3766, #3903, and twice on paper inside #3905), every time via a Tailwind-class derivation that omitted the 2px FOLDER_BODY_INSET_PX container differential between the folder header's box and the nested body's box. The jsdom test (ChatSidebar.folderAlignment.test.tsx) cannot catch that class of break: jsdom has no layout engine, so it asserts class tokens and an arithmetic identity between constants, never the rendered geometry. Add a Playwright spec to the `E2E (stub ACP backend, offline)` gate that seeds one folder, one subfolder, and sessions in both plus one ungrouped session via the harness gateway API, then asserts the guides on getBoundingClientRect().left of the real rendered elements: - guide 1: folder glyph on its border-l connector line (depth 1 and 2) - guide 2: folder name on the agent label / title of its sessions (depth 1 and 2) - guide 3: nested folder glyph on its sibling sessions' content column, and the root-lane identity (ungrouped content on the root glyph) - depth invariance: the depth-2 connector on depth 1's content column Falsified against the historical regression class: rebuilding with FOLDER_BODY_INSET_PX bumped 2 -> 4 fails the spec with "fGlyph=259 vs fConnector=261" while the jsdom test stays green. The missing-selector diagnostics were falsified the same way (a broken selector fails with "missing: [aAgent, ...]", not a bare timeout). Pre-push review (GPT + Opus model-pinned): no blocking. Fixed advisory findings: absolute coherence anchors so difference-only assertions cannot hold vacuously at left=0; seeding requests assert response.ok() so a 4xx fails as a seeding fault, not an alignment fault; the settle poll subsumes the render wait (budget fits the 30s test timeout) and names missing selectors; the serial-run dependency on the wipe discipline in session-tags-folders.spec.ts is documented in the header. The tool-call subtitle is not asserted: it needs a live agent turn to render, is a sibling of the title inside the same block container (equal left by construction), and its class parity is pinned by the jsdom test. MIN_EXECUTED_SPECS raised 219 -> 220 for the added spec. Closes #3914 Co-authored-by: Joe Guo <zejiangg@amazon.com>
) `LoaderCircle` (exported as both `Loader2` and `LoaderCircle`) draws a single 2px-stroke arc spanning ~270 degrees of a circle. At 10-14px that stroke is thick relative to the radius and the gap is roughly a sixth of the glyph, so it reads as a broken ring rather than as motion. kirodotdev#3905 swapped the one row it touched to lucide `Loader`, whose eight discrete spokes stay legible at that size, and its review made the sharp point: the stated cause is a property of the ICON at small sizes, not of that row, so it indicts every small spinner in the app. The rule, adopted from the issue's option 1 and written down in `website/docs/page-layout.md`: Below 15px use `Loader`. At 15px and above use `LoaderCircle`. Applied to every spinner with a DECLARED size below 15px (68 sites across 46 files), and in the other direction too: three `<Loader size={16|20}>` in ChatPage move to `LoaderCircle`, because a rule enforced one way only would let the spoke glyph spread upward and leave two spinners at one size again. `src/test/spinnerGlyphRule.test.ts` enforces both directions and pins the threshold to the doc, so the next call site has something to check against rather than re-litigating the choice. Two scope boundaries, both stated rather than left implicit: - A spinner sized by CSS is out of scope. `lucide-inline` is `width: 1em`, so it follows the surrounding font size and has no size to check statically; those stay on `LoaderCircle` rather than being guessed at. - The `Loader2` alias is left alone where it is not a small-size site. Retiring it in favour of the one canonical `LoaderCircle` name is worth doing, but it is a rename across ~40 unrelated sites and belongs in its own change. The third assertion exists because the size-keyed sweep MISSED a class: a spinner passed as a VALUE (`{ Icon: Loader2 }` in a phase/status map) has its size at the `<Icon size={13} />` render site instead, so four such maps went unconverted -- two of them breaking at runtime rather than in review. That case is now checked rather than remembered. - `spinnerGlyphRule`: 4 passed; all four fail on main. - Full `vitest run`: 20959 passed, 1328 of 1329 files. The one failure is `CronFolderHeader.cov80.test.tsx`, the known parallel-load flake. - `tsc --noEmit` clean; eslint 0 errors.
…dotdev#3905) A folder in the session list and the sessions filed under it did not share a left edge. kirodotdev#3766 added the row's status gutter as an in-flow flex child, so its 12px width plus a gap landed on the content column: the agent label, title and tool-call subtitle of every session inside a folder sat well right of that folder's own name. kirodotdev#3903 then tightened the same rows and got closer, but measured on a real render it still left the folder name 1px past its sessions' text and a nested folder's glyph 2px short of their column. Three guides now hold, in the root lane and at every nesting depth: 1. a folder glyph sits on its own border-l connector line 2. a folder name shares one edge with the agent label / title / tool-call subtitle of every session inside it 3. a NESTED folder's glyph sits on the content column of the sessions filed beside it, so a subfolder reads as their peer Solving all three simultaneously forces header pad == session row pad. With D = 2, a MEASURED container differential (the header's box starts 2px left of the nested body's box, and no class in the component expresses it): guide 1 P = D + M 14 = 2 + 12 guide 2 P + G + g = D + M + B + p + R 33 = 33 guide 3 P = R 14 = 14 The status gutter moves out of the content flow (absolutely positioned inside that pad) and is centred on the row rather than deriving the headline's y; in flow it is what broke guide 2, and no class-level assertion could see it. Because the algebra has no per-depth term it is scale-free: depth 3 nests as depth 2 does, and root-lane sessions land on the root folder's glyph, which restores the original pre-kirodotdev#1211 guide. Measured on the built SPA: depth 1 glyph/connector 259, name and all three text lines 278; depth 2 glyph/connector 278 (== depth 1's content column), name/content 297; root lane content 259. This reverts kirodotdev#3903's row and folder geometry, agreed with its author. Its collapsed-folder unread dot goes back inline on the right of the name rather than into an absolute left gutter: that gutter is what forced the header pad to 18px, and the pad is not free — it has to equal the session row's. The 2px colour bar from kirodotdev#3903 is kept, being orthogonal to the guides. Also drop the title's mt-0.5 (row height 49.6 -> 43.6px idle, 68.1 -> 62.1 running), use lucide Loader for the running spinner (at 12px the LoaderCircle arc reads as a broken ring where the spokes read as motion), and pull the divider inset and the empty-folder "New chat in X" affordance onto the same column as the sessions. The alignment invariant test has been rewritten by each of the last two PRs to accept the then-current misalignment. It now pins all three guides, adds guide 3 at depth 2 with a nested fixture, and the gutter test pins the gutter as out-of-flow and row-centred. Every number in the test header is a measurement with its frame named: two paper estimates during this fix were 2px and 3px out.
…dotdev#3914) (kirodotdev#3942) The sidebar's three folder alignment guides have regressed four times (kirodotdev#1211, kirodotdev#3766, kirodotdev#3903, and twice on paper inside kirodotdev#3905), every time via a Tailwind-class derivation that omitted the 2px FOLDER_BODY_INSET_PX container differential between the folder header's box and the nested body's box. The jsdom test (ChatSidebar.folderAlignment.test.tsx) cannot catch that class of break: jsdom has no layout engine, so it asserts class tokens and an arithmetic identity between constants, never the rendered geometry. Add a Playwright spec to the `E2E (stub ACP backend, offline)` gate that seeds one folder, one subfolder, and sessions in both plus one ungrouped session via the harness gateway API, then asserts the guides on getBoundingClientRect().left of the real rendered elements: - guide 1: folder glyph on its border-l connector line (depth 1 and 2) - guide 2: folder name on the agent label / title of its sessions (depth 1 and 2) - guide 3: nested folder glyph on its sibling sessions' content column, and the root-lane identity (ungrouped content on the root glyph) - depth invariance: the depth-2 connector on depth 1's content column Falsified against the historical regression class: rebuilding with FOLDER_BODY_INSET_PX bumped 2 -> 4 fails the spec with "fGlyph=259 vs fConnector=261" while the jsdom test stays green. The missing-selector diagnostics were falsified the same way (a broken selector fails with "missing: [aAgent, ...]", not a bare timeout). Pre-push review (GPT + Opus model-pinned): no blocking. Fixed advisory findings: absolute coherence anchors so difference-only assertions cannot hold vacuously at left=0; seeding requests assert response.ok() so a 4xx fails as a seeding fault, not an alignment fault; the settle poll subsumes the render wait (budget fits the 30s test timeout) and names missing selectors; the serial-run dependency on the wipe discipline in session-tags-folders.spec.ts is documented in the header. The tool-call subtitle is not asserted: it needs a live agent turn to render, is a sibling of the title inside the same block container (equal left by construction), and its class parity is pinned by the jsdom test. MIN_EXECUTED_SPECS raised 219 -> 220 for the added spec. Closes kirodotdev#3914 Co-authored-by: Joe Guo <zejiangg@amazon.com>
) `LoaderCircle` (exported as both `Loader2` and `LoaderCircle`) draws a single 2px-stroke arc spanning ~270 degrees of a circle. At 10-14px that stroke is thick relative to the radius and the gap is roughly a sixth of the glyph, so it reads as a broken ring rather than as motion. kirodotdev#3905 swapped the one row it touched to lucide `Loader`, whose eight discrete spokes stay legible at that size, and its review made the sharp point: the stated cause is a property of the ICON at small sizes, not of that row, so it indicts every small spinner in the app. The rule, adopted from the issue's option 1 and written down in `website/docs/page-layout.md`: Below 15px use `Loader`. At 15px and above use `LoaderCircle`. Applied to every spinner with a DECLARED size below 15px (68 sites across 46 files), and in the other direction too: three `<Loader size={16|20}>` in ChatPage move to `LoaderCircle`, because a rule enforced one way only would let the spoke glyph spread upward and leave two spinners at one size again. `src/test/spinnerGlyphRule.test.ts` enforces both directions and pins the threshold to the doc, so the next call site has something to check against rather than re-litigating the choice. Two scope boundaries, both stated rather than left implicit: - A spinner sized by CSS is out of scope. `lucide-inline` is `width: 1em`, so it follows the surrounding font size and has no size to check statically; those stay on `LoaderCircle` rather than being guessed at. - The `Loader2` alias is left alone where it is not a small-size site. Retiring it in favour of the one canonical `LoaderCircle` name is worth doing, but it is a rename across ~40 unrelated sites and belongs in its own change. The third assertion exists because the size-keyed sweep MISSED a class: a spinner passed as a VALUE (`{ Icon: Loader2 }` in a phase/status map) has its size at the `<Icon size={13} />` render site instead, so four such maps went unconverted -- two of them breaking at runtime rather than in review. That case is now checked rather than remembered. - `spinnerGlyphRule`: 4 passed; all four fail on main. - Full `vitest run`: 20959 passed, 1328 of 1329 files. The one failure is `CronFolderHeader.cov80.test.tsx`, the known parallel-load flake. - `tsc --noEmit` clean; eslint 0 errors.
) `LoaderCircle` (exported as both `Loader2` and `LoaderCircle`) draws a single 2px-stroke arc spanning ~270 degrees of a circle. At 10-14px that stroke is thick relative to the radius and the gap is roughly a sixth of the glyph, so it reads as a broken ring rather than as motion. kirodotdev#3905 swapped the one row it touched to lucide `Loader`, whose eight discrete spokes stay legible at that size, and its review made the sharp point: the stated cause is a property of the ICON at small sizes, not of that row, so it indicts every small spinner in the app. The rule, adopted from the issue's option 1 and written down in `website/docs/page-layout.md`: Below 15px use `Loader`. At 15px and above use `LoaderCircle`. Applied to every spinner with a DECLARED size below 15px (68 sites across 46 files), and in the other direction too: three `<Loader size={16|20}>` in ChatPage move to `LoaderCircle`, because a rule enforced one way only would let the spoke glyph spread upward and leave two spinners at one size again. `src/test/spinnerGlyphRule.test.ts` enforces both directions and pins the threshold to the doc, so the next call site has something to check against rather than re-litigating the choice. Two scope boundaries, both stated rather than left implicit: - A spinner sized by CSS is out of scope. `lucide-inline` is `width: 1em`, so it follows the surrounding font size and has no size to check statically; those stay on `LoaderCircle` rather than being guessed at. - The `Loader2` alias is left alone where it is not a small-size site. Retiring it in favour of the one canonical `LoaderCircle` name is worth doing, but it is a rename across ~40 unrelated sites and belongs in its own change. The third assertion exists because the size-keyed sweep MISSED a class: a spinner passed as a VALUE (`{ Icon: Loader2 }` in a phase/status map) has its size at the `<Icon size={13} />` render site instead, so four such maps went unconverted -- two of them breaking at runtime rather than in review. That case is now checked rather than remembered. - `spinnerGlyphRule`: 4 passed; all four fail on main. - Full `vitest run`: 20959 passed, 1328 of 1329 files. The one failure is `CronFolderHeader.cov80.test.tsx`, the known parallel-load flake. - `tsc --noEmit` clean; eslint 0 errors.
) `LoaderCircle` (exported as both `Loader2` and `LoaderCircle`) draws a single 2px-stroke arc spanning ~270 degrees of a circle. At 10-14px that stroke is thick relative to the radius and the gap is roughly a sixth of the glyph, so it reads as a broken ring rather than as motion. kirodotdev#3905 swapped the one row it touched to lucide `Loader`, whose eight discrete spokes stay legible at that size, and its review made the sharp point: the stated cause is a property of the ICON at small sizes, not of that row, so it indicts every small spinner in the app. The rule, adopted from the issue's option 1 and written down in `website/docs/page-layout.md`: Below 15px use `Loader`. At 15px and above use `LoaderCircle`. Applied to every spinner with a DECLARED size below 15px (68 sites across 46 files), and in the other direction too: three `<Loader size={16|20}>` in ChatPage move to `LoaderCircle`, because a rule enforced one way only would let the spoke glyph spread upward and leave two spinners at one size again. `src/test/spinnerGlyphRule.test.ts` enforces both directions and pins the threshold to the doc, so the next call site has something to check against rather than re-litigating the choice. Two scope boundaries, both stated rather than left implicit: - A spinner sized by CSS is out of scope. `lucide-inline` is `width: 1em`, so it follows the surrounding font size and has no size to check statically; those stay on `LoaderCircle` rather than being guessed at. - The `Loader2` alias is left alone where it is not a small-size site. Retiring it in favour of the one canonical `LoaderCircle` name is worth doing, but it is a rename across ~40 unrelated sites and belongs in its own change. The third assertion exists because the size-keyed sweep MISSED a class: a spinner passed as a VALUE (`{ Icon: Loader2 }` in a phase/status map) has its size at the `<Icon size={13} />` render site instead, so four such maps went unconverted -- two of them breaking at runtime rather than in review. That case is now checked rather than remembered. - `spinnerGlyphRule`: 4 passed; all four fail on main. - Full `vitest run`: 20959 passed, 1328 of 1329 files. The one failure is `CronFolderHeader.cov80.test.tsx`, the known parallel-load flake. - `tsc --noEmit` clean; eslint 0 errors.
Problem / Motivation
A folder in the session list and the sessions filed under it did not share a left edge.
#3766added the row's status gutter as an in-flow flex child, so its 12px width plus a gap landed on the content column: the agent label, title and tool-call subtitle of every session inside a folder sat well right of that folder's own name, and the sidebar read as two ragged columns.#3903then tightened the same rows and got closer, but measured on a real render it still leaves the folder name 1px past its sessions' text and a nested folder's glyph 2px short of its siblings' column.Why it matters
The session list is the most-looked-at surface in the dashboard. A ragged left edge is read as "this app is sloppy" long before anyone can name the cause — and the cause has been mis-derived four times now, so each attempt paid a full review cycle.
The report that opened this work asked for two things, and the second one is why row height changes here rather than in a separate PR: the rows were called out as both misaligned and bulky ("can we compress the left side"). So the 6px height cut is declared intent, not a ride-along — horizontal alignment and vertical density were one request.
What changed (motivation → approach → change)
Fixing one number was not enough: the three edges are one system. Solving them together forces the folder header's left pad to equal the session row's left pad, and everything else follows.
With
D = 2=FOLDER_BODY_INSET_PX, the nested body's own left inset.FolderBodyapplies it so its collapse animation (grid-template-rows+overflow: hidden) does not clip its children; it is invisible in the class list, which is why four revisions derived this geometry from Tailwind classes and each landed 2px out. It is now a named export thatFolderBodyuses, the alignment test imports asD, and that test asserts against the renderedpadding-left— so the algebra has no free empirical term and a change to it fails a unit test (verified: set it to 3 and guide 1 goes red withexpected 14 to be 15):border-lconnector lineP = D + M→14 = 2 + 12P + G + g = D + M + B + p + R→33 = 33P = R→14 = 14The algebra has no per-depth term, so it is scale-free: the guides hold in the root lane and at every nesting depth.
absolute left-px top-1/2 -translate-y-1/2, so it costs the content column nothing, and centred on the row instead of deriving the headline's y from repeated type classes (measured 10.25px above centre on a three-line row before)px-3.5, nested body →ml-3 pl-1, session row →pl-3.5 pr-3 py-1.5, folder glyph→name gapgap-2→gap-[5px](at 8 the name overshoots the content column by 3px)mt-0.5, rowpy-2→py-1.5— row height 49.6 → 43.6px idle, 68.1 → 62.1px running (the density half of the request)Loader2/LoaderCircle→Loader, requested by the repo owner for this row. Note the size argument for it (a 270° arc reads as a broken ring at 12px) would indict four other 12pxLoader2sites, so it is not offered as a general rule here — that rule is put to the maintainer in Decide one rule for small-size spinners: Loader vs LoaderCircle across 21 sites #3925 rather than applied to four unrelated files inside an alignment fixml-[20px]→ml-[14px], and the empty-folder "New chat in X" affordance onto the same column as the sessions it stands in forrole="img"+ anaria-label/titlereusing the session row's own i18n key. It had neither: a colour-only dot beside a count is invisible to a screen reader and indistinguishable from styling, while the session-row marker it mirrors has been labelled since feat(sessions): redesign the session list rows #3766. Raised by the UX Review gate on this PR; in scope because this PR is what moved that dotRelationship to
#3903#3903landed on these same rows 20 minutes after this PR opened. Its row and folder geometry is reverted here, agreed with its author. Concretely:pl-[18px], and the pad is not free — guide 3 requires it to equal the session row's pad. Putting the dot back is what lets the pad be 14.ml-4body indent,gap-2glyph→name gap andpl-1 pr-2 gap-1row padding are replaced by the values the three guides solve to.index.css), being orthogonal to the guides.Board view rows do change (content offset 20 → 14, height 49.6 → 43.6): the row component is shared, and the absolute gutter makes the left pad no longer free to differ per scope. Board's own folder header keeps its 6px pad, so the guides are a tree-layout property there as before.
Measured x (CSS px, real renders)
#3766(d926ca569^), pod#3766, pod#3903, built SPABaselines rendered on isolated pods; the last three rows on the built SPA behind
scripts/lib/serve-dist.mjs.Tests
ChatSidebar.folderAlignment.test.tsxhas been rewritten by each of the last two PRs to accept the then-current misalignment (#3766's header read "those guides no longer hold: … That is a decision, not drift"). It now pins all three guides, adds guide 3 asserted at depth 2 with a nested fixture, andChatSidebar.statusGutter.test.tsxpins the gutter as out-of-flow and row-centred — that being the thing that actually moved the content, and the thing no class-token assertion could see.Every number in the test header is a measurement, with the frame it was measured in named. Two paper estimates inside this PR were 2px and 3px out.
The one term that mattered most is no longer a number at all:
Dis imported from the component and checked against the rendered padding, so the offset behind all four historical breakages now fails a test when it moves.Gates:
tscclean,eslint0 errors,npm run i18n:checkall green, 20445 frontend tests pass.Manual verification
Rendered the built SPA and measured all four anchors with
getBoundingClientRectat depth 1, depth 2 and the root lane; screenshots below are from that same render.Screenshots / video
temp-screenshots/session-row-alignment/— the frame, and the same frame with guides 1 and 2 drawn on. Guide 3 is visible in the second image as the nestedSidebarfolder icon sitting on the red line.Known cosmetic limit
Element boxes align exactly, which is the only thing measurable and assertable. The lucide folder glyph's path starts at x=2 in a 24 viewBox, so its ink is inset ~0.6 CSS px from its box and a nested folder icon reads a hair right of the text column. Both depths are inset identically, so it is consistent rather than ragged; correcting the ink would need a negative offset that fights the algebra.
Issue link
no linked issue: this is a regression fix on
#3766/#3903, filed as neither. Follow-up filed for the missing E2E assertion: #3914