Skip to content

refactor(app): merge sidebar pin and status slots (#150)#193

Merged
Astro-Han merged 2 commits into
devfrom
worktree-refactor-i150-merge-slots
Apr 23, 2026
Merged

refactor(app): merge sidebar pin and status slots (#150)#193
Astro-Han merged 2 commits into
devfrom
worktree-refactor-i150-merge-slots

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

Top-level session rows now share a single 24px leading slot instead of a pin-button column plus a separate status column. The slot priority is running > permission > error > unseen > pin > empty. When a status is active, the pin button is not rendered in the DOM; users unpin via the row context menu or the ... dots menu (both existing paths). Child sessions are unchanged.

Closes #150. The right-side action slot still transitions w-0 → w-6 on hover, which shrinks the title's truncation area — tracked separately in #192.

Measurement (Playwright, 4 states)

State Before After Δ
unpinned-idle left=81 width=236 left=53 width=264 -28 / +28
unpinned-hover left=81 width=212 left=53 width=240 -28 / +28
pinned-idle left=81 width=236 left=53 width=264 -28 / +28
pinned-hover left=81 width=212 left=53 width=240 -28 / +28

Title baseline shifts left by exactly 28px (24px freed slot + 4px gap) and stays stable across all four states, matching the spec in #150.

Design notes

The pin button is rendered as the indicator's fallback inside SessionRow, not as a sibling column. This technically nests a <button> inside <A>, which the existing InlineEditor already does when editing a title. The button's stopPropagation() on click blocks anchor navigation.

For the pinned + running (or permission / error / unseen) case, the pin button is hidden rather than stacked on top of the spinner. The issue body called out an "overlay" variant as a candidate but not a requirement; the context menu and dots menu both already offer unpin, so the simpler priority switch was chosen over absolute positioning.

Test plan

  • Typecheck across the workspace (bun turbo typecheck)
  • App unit tests on packages/app/src/pages/layout (34 pass)
  • Full sidebar e2e suite (bun run test:e2e:local -- sidebar/, 7 pass, 3 pre-existing skips)
  • New regression spec sidebar-leading-slot.spec.ts — asserts title baseline is stable across idle / hover / pinned states and the pin button sits inside the row's first 24px slot
  • Manual smoke on bun dev:desktop: idle unpinned row is visually clean, hover reveals outline pin, pinned row shows filled pin, child sessions have no pin button

Summary by CodeRabbit

  • Tests

    • Added E2E test validating sidebar pinning behavior and title position stability when hovering and pinning items.
  • Bug Fixes

    • Improved sidebar layout consistency for pinned items by refactoring indicator slot handling.

Top-level rows now share a single 24px leading slot with priority
running > permission > error > unseen > pin > empty, saving 28px
on the title's left baseline. When a status is active the pin button
is not rendered; users unpin via the row context menu or the ... dots
menu.
Locks in the title's left baseline across idle / hover / pinned states
and asserts the pin button sits inside the row's first 24px slot rather
than a separate column.
@Astro-Han Astro-Han added enhancement New feature or request P2 Medium priority ui Design system and user interface labels Apr 23, 2026
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements a unified left-side indicator slot for session rows. A new E2E test verifies that pinning a session merges the pin button into the status indicator slot while preserving title baseline stability. The SessionRow component now accepts a leadingSlot prop as a fallback when no status indicator is active, and SessionItem forwards the pin button only for top-level rows.

Changes

Cohort / File(s) Summary
E2E Test
packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
New test validating that pin button merges into status slot with stable title baseline; verifies pin visibility, positioning, and lack of title horizontal shift during hover and pin state transitions.
Sidebar Layout Components
packages/app/src/pages/layout/sidebar-items.tsx
SessionRow accepts leadingSlot prop and appends it as fallback in indicator() logic when no status indicator is active; SessionItem computes and forwards leadingSlot to SessionRow for top-level rows only, removing separate UI block.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels

app

Poem

🐰 A pin and a spinner, once side-by-side,
Now share one slot with unified pride!
The baseline holds steady through hover and click,
One slot for all states—efficient and slick! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: merging the sidebar pin and status slots into a unified indicator, directly matching the PR's core objective from issue #150.
Description check ✅ Passed The PR description is comprehensive and follows the template structure, covering summary, context, measurements, test results, and design rationale. All critical sections are complete with sufficient detail.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from #150: single 24px leading slot with priority hierarchy, leadingSlot prop on SessionRow, pin button as indicator fallback, stopPropagation() for click handling, and new E2E test validating baseline stability.
Out of Scope Changes check ✅ Passed All code changes are within scope—the E2E test and sidebar-items.tsx modifications directly address the slot unification and measurement requirements. No extraneous refactoring or unrelated changes detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-refactor-i150-merge-slots

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/app/src/pages/layout/sidebar-items.tsx (1)

110-126: ⚠️ Potential issue | 🟠 Major

Move the pin action outside the anchor element.

Nesting a button inside an <a> element is invalid HTML and creates accessibility issues with screen readers and keyboard navigation. The indicator() component (containing the pin button) should be rendered as a sibling of the link, not as its content.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/app/src/pages/layout/sidebar-items.tsx` around lines 110 - 126, The
anchor element A currently contains the indicator() (which renders the pin
button), causing invalid nested interactive controls; move the indicator()
render out of the A so it becomes a sibling element before (or after) the A,
preserve the styling container (the div with class "shrink-0 size-6...") and its
color from props.tint(), and ensure the pin button’s event handlers remain bound
to the indicator component while keeping the A’s href/navigation behavior and
its onClick logic (props.warmPress, props.warmFocus, sidebarOpened(),
clearHoverProjectSoon()) unchanged; update any layout or wrapping container to
maintain spacing and accessibility (keyboard focus order) after moving
indicator().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts`:
- Around line 5-55: Add a second regression scenario that pins a session which
has an active status indicator (e.g., running or permission-needed) and then
asserts the same layout/keyboard affordances as the idle case: create or update
the test session to a running/permission state (using the existing
withSession/sdk helper or an SDK call to set session status), navigate with
gotoSession, pin the row (reuse row.locator('[data-action="session-row-menu"]')
and menu click), find the pinnedRow and its pinnedTitle, and assert the title
baseline and hover behavior (reuse leftOf), plus verify the unpin control
remains keyboard-reachable and in the leading slot (reuse pinButton selector and
the getBoundingClientRect checks); ensure the new assertions mirror the idle
pinned assertions so the spec fails if the active-status indicator displaces or
hides the unpin control.

In `@packages/app/src/pages/layout/sidebar-items.tsx`:
- Around line 102-107: The indicator function currently returns
props.leadingSlot only as a fallback, removing the pin control when isWorking(),
hasPermissions(), hasError(), or unseenCount() > 0 are true; change indicator
(and its callers if necessary) to always render props.leadingSlot in the DOM
(e.g., render it alongside the status indicator rather than as a fallback) so
the pin control remains keyboard-focusable and reachable; keep the visual status
elements returned when props.isWorking(), props.hasPermissions(),
props.hasError(), or props.unseenCount() > 0, but ensure those status elements
do not remove or replace props.leadingSlot (use aria-hidden on decorative
indicators or separate DOM nodes) so pinned rows retain a direct keyboard unpin
path.

---

Outside diff comments:
In `@packages/app/src/pages/layout/sidebar-items.tsx`:
- Around line 110-126: The anchor element A currently contains the indicator()
(which renders the pin button), causing invalid nested interactive controls;
move the indicator() render out of the A so it becomes a sibling element before
(or after) the A, preserve the styling container (the div with class "shrink-0
size-6...") and its color from props.tint(), and ensure the pin button’s event
handlers remain bound to the indicator component while keeping the A’s
href/navigation behavior and its onClick logic (props.warmPress,
props.warmFocus, sidebarOpened(), clearHoverProjectSoon()) unchanged; update any
layout or wrapping container to maintain spacing and accessibility (keyboard
focus order) after moving indicator().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 34cf931d-e215-4e23-8f4d-3b9f3b028484

📥 Commits

Reviewing files that changed from the base of the PR and between 4204376 and 40fc418.

📒 Files selected for processing (2)
  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
  • packages/app/src/pages/layout/sidebar-items.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
packages/app/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (packages/app/AGENTS.md)

Always prefer createStore over multiple createSignal calls in SolidJS

Files:

  • packages/app/src/pages/layout/sidebar-items.tsx
  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
packages/app/e2e/**/*.spec.ts

📄 CodeRabbit inference engine (packages/app/e2e/AGENTS.md)

packages/app/e2e/**/*.spec.ts: Import test utilities from ../fixtures instead of @playwright/test
Test files should be named with the pattern feature-name.spec.ts
Use lowercase, descriptive test names (e.g., 'sidebar can be toggled')
Use camelCase for variable names in tests
Use SCREAMING_SNAKE_CASE for constants in tests
Use fixture-managed cleanup with withSession(sdk, title, callback) for temporary sessions instead of calling sdk.session.delete(...) directly
Prefer the project fixture for tests that need a dedicated project with LLM mocking
Use data-component, data-action, or semantic roles for selectors instead of CSS class names or IDs
Use modKey from utils for cross-platform keyboard shortcuts (Meta on Mac, Control on Linux/Windows)
In terminal tests, type through the browser using runTerminal() and waitTerminalReady() instead of writing to the PTY through the SDK
Never use wall-clock waits like page.waitForTimeout(...) to make a test pass
Wait on observable state with expect(...), expect.poll(...), or existing helpers instead of assuming work is finished after an action
Use locator assertions like toBeVisible(), toHaveCount(0), and toHaveAttribute(...) for normal UI state verification
Use expect.poll(...) for probing mock or backend state rather than transient DOM visibility
Prefer fluent helpers and drivers when they make intent obvious and reduce locator-heavy noise in tests
Use direct locators when the interaction is simple and a helper would not add clarity
When validating routing, assert against canonical or resolved workspace slugs using shared helpers from ../actions to account for Windows canonicalization
Test one feature per test file
Call project.trackSession(sessionID, directory?) and project.trackDirectory(directory) for any resources created outside the fixture so teardown can clean them up

Files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
🧠 Learnings (6)
📚 Learning: 2026-04-23T07:23:23.849Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 180
File: packages/app/src/components/session/session-new-view.tsx:13-18
Timestamp: 2026-04-23T07:23:23.849Z
Learning: In pawwork (Astro-Han/pawwork), prefer using `createStore` instead of multiple `createSignal` calls only when the signals represent **coupled** object state that is updated together (i.e., there is at least one shared batch-update site where the state is changed in the same transaction). If the state fields are **independent** and are mutated by separate handlers (e.g., one handler updates only `selectedSkill` while another updates only `mode`), keep them as individual `createSignal` calls—using `createStore` for truly independent fields adds boilerplate without behavioral benefit.

Applied to files:

  • packages/app/src/pages/layout/sidebar-items.tsx
  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
📚 Learning: 2026-04-20T14:36:04.099Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.099Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use lowercase, descriptive test names (e.g., 'sidebar can be toggled')

Applied to files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
📚 Learning: 2026-04-20T14:36:04.099Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.099Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test one feature per test file

Applied to files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
📚 Learning: 2026-04-20T14:36:04.099Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.099Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Use locator assertions like `toBeVisible()`, `toHaveCount(0)`, and `toHaveAttribute(...)` for normal UI state verification

Applied to files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
📚 Learning: 2026-04-22T08:49:44.563Z
Learnt from: Astro-Han
Repo: Astro-Han/pawwork PR: 126
File: packages/desktop-electron/src/main/index-sidecar-source.test.ts:3-11
Timestamp: 2026-04-22T08:49:44.563Z
Learning: In `packages/desktop-electron/src/main/index-sidecar-source.test.ts` (Astro-Han/pawwork), the test intentionally uses `expect(source).toContain` / `expect(source).not.toContain` string matching against the raw `index.ts` source text as a lightweight sidecar contract guard. The maintainer has explicitly chosen not to introduce an AST parser (e.g., `babel/parser` or acorn) for this purpose. Do not flag these string-based assertions as fragile or suggest converting them to AST-based matching.

Applied to files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
📚 Learning: 2026-04-20T14:36:04.099Z
Learnt from: CR
Repo: Astro-Han/pawwork PR: 0
File: packages/app/e2e/AGENTS.md:0-0
Timestamp: 2026-04-20T14:36:04.099Z
Learning: Applies to packages/app/e2e/**/*.spec.ts : Test files should be named with the pattern `feature-name.spec.ts`

Applied to files:

  • packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts

Comment thread packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
Comment thread packages/app/src/pages/layout/sidebar-items.tsx

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new E2E test to verify that pinning a session in the sidebar does not cause horizontal layout shifts. It refactors the SessionRow and SessionItem components to merge the leadingSlot into the status indicator area, ensuring that elements like the pin button occupy the same space as status indicators rather than creating a separate column. I have no feedback to provide.

@Astro-Han

Copy link
Copy Markdown
Owner Author

Addressing the "move the pin action outside the anchor" outside-diff note:

The codebase already nests interactive elements inside <A> for the same reason. InlineEditor renders an <input> inside the session link when editing a title (packages/app/src/pages/layout/inline-editor.tsx:87-110) and blocks anchor navigation via stopPropagation on pointerdown / click. The pin button follows the same pattern (pawwork-sidebar.tsx:140-145): click is preventDefault + stopPropagation, so the anchor's @solidjs/router click handler never fires and no navigation happens.

Moving the leading slot out as a sibling of <A> would revert the layout to two slots side-by-side, which is exactly what #150 removes. The alternative — absolute-positioned overlay for the pin only, conditional on !hasStatus — was considered during implementation and rejected as over-engineering for the corner case. The issue body for #150 mentions this variant as a candidate, not a requirement.

The HTML-spec concern is real (interactive inside interactive is technically not allowed). Modern browsers handle it, event propagation works, and accessibility trees tolerate it. If a real screen-reader regression surfaces, we'll revisit with the absolute-overlay approach. Not changing in this PR.

@Astro-Han Astro-Han added the app Application behavior and product flows label Apr 23, 2026
@Astro-Han Astro-Han merged commit d36c6ba into dev Apr 23, 2026
26 checks passed
@Astro-Han Astro-Han deleted the worktree-refactor-i150-merge-slots branch April 23, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows enhancement New feature or request P2 Medium priority ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] Merge pin button and status slot into a single left-side indicator

1 participant