fix(app): stabilize shell navigation state#424
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughCentralizes shell navigation into ChangesShell Navigation, Sidebar & Session Visibility
Sequence Diagram(s)sequenceDiagram
participant Layout as Layout Component
participant Shell as createShellNavigation
participant Navigator as Router/Navigator
participant Locks as TransientLocks
participant Choose as chooseProject
participant Settings as SettingsSurface
Layout->>Shell: openNewSession(optional dir)
Shell->>Locks: releaseTransientLocks("new-session" or "choose-project")
alt no project root
Shell->>Choose: chooseProject()
else has project root
Shell->>Navigator: navigate(newSessionRoute(root))
end
Layout->>Shell: openSession(session)
Shell->>Locks: releaseTransientLocks("session")
Shell->>Navigator: navigate(openSessionRoute(dir, id))
Layout->>Shell: openSettings()
Shell->>Locks: releaseTransientLocks("settings")
Shell->>Settings: openSettingsSurface()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 8/10 reviews remaining, refill in 8 minutes and 21 seconds. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a centralized shellNavigation utility to manage application navigation and transient locks, while also refactoring the session view logic. Key changes include removing the 'lookback' window for question fallback sessions and simplifying the SessionViewController by removing the logic that preserved the previous session's visibility during transitions. The review feedback suggests further streamlining the SessionViewController by inlining the now-trivial nextVisibleSessionID function and simplifying the transitioning and visibleReady logic to reflect the new immediate alignment with the target route.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/app/src/pages/layout/shell-navigation.test.ts (1)
54-67: ⚡ Quick winAdd the explicit-directory fallback case too.
This only exercises the
currentProjectRoot()failure path becauseopenNewSession()is called without an argument. The sibling branch forresolveProjectRoot(directory)is still unpinned when an explicit directory cannot be resolved.🧪 Suggested companion test
test("falls back to project chooser when no directory can be resolved for a new session", () => { const calls: string[] = [] const shell = createShellNavigation({ navigate: (route) => calls.push(`navigate:${route}`), releaseTransientLocks: (reason) => calls.push(`release:${reason}`), resolveProjectRoot: () => "", currentProjectRoot: () => undefined, chooseProject: () => calls.push("chooseProject"), openSettingsSurface: () => calls.push("settings"), }) shell.openNewSession() expect(calls).toEqual(["release:new-session", "chooseProject"]) }) + + test("falls back to project chooser when an explicit directory cannot be resolved", () => { + const calls: string[] = [] + const shell = createShellNavigation({ + navigate: (route) => calls.push(`navigate:${route}`), + releaseTransientLocks: (reason) => calls.push(`release:${reason}`), + resolveProjectRoot: () => undefined, + currentProjectRoot: () => "/current", + chooseProject: () => calls.push("chooseProject"), + openSettingsSurface: () => calls.push("settings"), + }) + + shell.openNewSession("/repo") + + expect(calls).toEqual(["release:new-session", "chooseProject"]) + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/pages/layout/shell-navigation.test.ts` around lines 54 - 67, Add a companion test that exercises the explicit-directory fallback path: createShellNavigation with resolveProjectRoot returning "" and currentProjectRoot returning undefined (or as appropriate), then call shell.openNewSession("/some/dir") (i.e., pass an explicit directory) and assert the calls include releasing transient locks and choosing project (e.g., ["release:new-session","chooseProject"]); this ensures the resolveProjectRoot(directory) branch is covered in addition to the existing currentProjectRoot() failure path. Use the same helpers (createShellNavigation, resolveProjectRoot, currentProjectRoot, chooseProject, openNewSession) and mirror the structure of the existing test.
🤖 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/src/pages/session/session-view-controller.ts`:
- Around line 23-36: In use-session-timeline-data.ts the various memos and
effects (sessionInfo, messages, diffs, revertMessageID,
historyMore/historyLoading and the createEffect that reads
lastUserMessage()?.id) dereference input.sync.session and input.sync.data.*
before the session messages are guaranteed to be loaded; update each to first
check messagesReady and if false return safe defaults (e.g., undefined for
sessionInfo/revertMessageID, [] for messages/diffs, false for historyLoading,
0/undefined for counts) and only access input.sync.session.get(id) or
input.sync.data.message[id] when messagesReady is true so no properties are read
from uninitialized state. Ensure the createEffect depends on messagesReady (or
guards lastUserMessage()) so it won’t run against unready data.
---
Nitpick comments:
In `@packages/app/src/pages/layout/shell-navigation.test.ts`:
- Around line 54-67: Add a companion test that exercises the explicit-directory
fallback path: createShellNavigation with resolveProjectRoot returning "" and
currentProjectRoot returning undefined (or as appropriate), then call
shell.openNewSession("/some/dir") (i.e., pass an explicit directory) and assert
the calls include releasing transient locks and choosing project (e.g.,
["release:new-session","chooseProject"]); this ensures the
resolveProjectRoot(directory) branch is covered in addition to the existing
currentProjectRoot() failure path. Use the same helpers (createShellNavigation,
resolveProjectRoot, currentProjectRoot, chooseProject, openNewSession) and
mirror the structure of the existing test.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7701fbec-d0c5-49f1-ac82-54b7886d8b78
📒 Files selected for processing (7)
packages/app/src/pages/layout.tsxpackages/app/src/pages/layout/shell-navigation.test.tspackages/app/src/pages/layout/shell-navigation.tspackages/app/src/pages/session/blockers/question-fallback.test.tspackages/app/src/pages/session/blockers/question-fallback.tspackages/app/src/pages/session/session-view-controller.test.tspackages/app/src/pages/session/session-view-controller.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/app/src/pages/layout/shell-navigation.test.ts (1)
54-66: ⚡ Quick win
resolveProjectRootmock in this case is not exercised.On Line 65,
openNewSession()is called without a directory, so Line 59’sresolveProjectRoot: () => ""never runs. This makes the setup misleading and leaves the explicit-directory + empty-string fallback path untested.♻️ Suggested test adjustment
- test("falls back to project chooser when no directory can be resolved for a new session", () => { + test("falls back to project chooser when explicit directory resolves to empty root", () => { const calls: string[] = [] const shell = createShellNavigation({ navigate: (route) => calls.push(`navigate:${route}`), releaseTransientLocks: (reason) => calls.push(`release:${reason}`), resolveProjectRoot: () => "", currentProjectRoot: () => undefined, chooseProject: () => calls.push("chooseProject"), openSettingsSurface: () => calls.push("settings"), }) - shell.openNewSession() + shell.openNewSession("/repo") expect(calls).toEqual(["release:choose-project", "chooseProject"]) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/pages/layout/shell-navigation.test.ts` around lines 54 - 66, The test's resolveProjectRoot mock is not actually exercised because openNewSession() is called with no directory; change the test to call shell.openNewSession with an explicit directory string (e.g. "/some/dir") so createShellNavigation's resolveProjectRoot mock (resolveProjectRoot: () => "") runs and returns the empty string, thereby exercising the explicit-directory + empty-string fallback path and still asserting that chooseProject is invoked; reference createShellNavigation, resolveProjectRoot, and openNewSession when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/app/src/pages/layout/shell-navigation.test.ts`:
- Around line 54-66: The test's resolveProjectRoot mock is not actually
exercised because openNewSession() is called with no directory; change the test
to call shell.openNewSession with an explicit directory string (e.g.
"/some/dir") so createShellNavigation's resolveProjectRoot mock
(resolveProjectRoot: () => "") runs and returns the empty string, thereby
exercising the explicit-directory + empty-string fallback path and still
asserting that chooseProject is invoked; reference createShellNavigation,
resolveProjectRoot, and openNewSession when making this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ef9091bf-cc04-4a35-8842-e601c0699b75
📒 Files selected for processing (2)
packages/app/src/pages/layout/shell-navigation.test.tspackages/app/src/pages/session/session-view-controller.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/app/src/pages/session/session-view-controller.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/app/src/pages/session/session-main-view.tsx (1)
114-127: 💤 Low valueConsider using the
Buttoncomponent for consistency.The retry and new session buttons use manual styling while the
Buttoncomponent is already imported and used elsewhere in the codebase. UsingButton variant="ghost"would ensure consistent styling, focus states, and accessibility behavior across the app.♻️ Suggested change
- <button - type="button" - class="rounded-md border border-border-subtle px-3 py-1 text-13-regular text-text-base transition-colors hover:bg-surface-raised-base-hover focus:outline-none focus-visible:bg-surface-raised-base-hover" - onClick={props.onRetryOpenSession} - > - {props.language.t("common.retry")} - </button> - <button - type="button" - class="rounded-md border border-border-subtle px-3 py-1 text-13-regular text-text-base transition-colors hover:bg-surface-raised-base-hover focus:outline-none focus-visible:bg-surface-raised-base-hover" - onClick={props.onOpenNewSession} - > - {props.language.t("command.session.new")} - </button> + <Button variant="ghost" size="small" onClick={props.onRetryOpenSession}> + {props.language.t("common.retry")} + </Button> + <Button variant="ghost" size="small" onClick={props.onOpenNewSession}> + {props.language.t("command.session.new")} + </Button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/app/src/pages/session/session-main-view.tsx` around lines 114 - 127, Replace the two plain <button> elements used for retry and new session with the shared Button component to ensure consistent styling and accessibility: locate the elements handling props.onRetryOpenSession and props.onOpenNewSession in SessionMainView (session-main-view.tsx) and swap them to use Button with variant="ghost" (or the matching ghost prop the project uses), forwarding the onClick handlers to Button and keeping the children as props.language.t("common.retry") and props.language.t("command.session.new"); ensure any class attributes are removed so the Button's built-in styles and focus behavior are used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/app/src/pages/session/session-main-view.tsx`:
- Around line 114-127: Replace the two plain <button> elements used for retry
and new session with the shared Button component to ensure consistent styling
and accessibility: locate the elements handling props.onRetryOpenSession and
props.onOpenNewSession in SessionMainView (session-main-view.tsx) and swap them
to use Button with variant="ghost" (or the matching ghost prop the project
uses), forwarding the onClick handlers to Button and keeping the children as
props.language.t("common.retry") and props.language.t("command.session.new");
ensure any class attributes are removed so the Button's built-in styles and
focus behavior are used.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ee163624-7c2a-4c3f-a92c-7af050f726b4
📒 Files selected for processing (11)
packages/app/src/pages/layout.tsxpackages/app/src/pages/layout/shell-navigation.tspackages/app/src/pages/layout/sidebar-item-navigation.test.tspackages/app/src/pages/layout/sidebar-item-navigation.tspackages/app/src/pages/layout/sidebar-items.tsxpackages/app/src/pages/layout/sidebar-workspace.tsxpackages/app/src/pages/session/session-main-view.tsxpackages/app/src/pages/session/session-view-controller.tspackages/app/src/pages/session/timeline-session-state.test.tspackages/app/src/pages/session/timeline-session-state.tspackages/app/src/shell-frame-contract.test.ts
💤 Files with no reviewable changes (1)
- packages/app/src/pages/session/timeline-session-state.ts
✅ Files skipped from review due to trivial changes (1)
- packages/app/src/pages/layout/shell-navigation.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/app/src/pages/layout/sidebar-item-navigation.test.ts
- packages/app/src/pages/layout/sidebar-items.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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-session-links.spec.ts`:
- Around line 138-149: The test registers the message route and may observe
targetMessageRequests > 0 before the sidebar click, so make the assertion verify
the fetch starts after the click: ensure you either reset or reinitialize the
targetMessageRequests counter (or re-register the page.route that increments it)
immediately before clicking the sidebar link, or assert await expect.poll(() =>
targetMessageRequests, { timeout: 0 }).toBe(0) right before
page.locator(`[data-session-id="${target.id}"] a`).first().click(), then click
and assert the counter increases (await expect.poll(() => targetMessageRequests,
{ timeout: 10_000 }).toBeGreaterThan(0)); keep the rest of the flow
(gotoSession, openSidebar, and checking
'[data-component="session-opening-state"]') unchanged.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9fc230ec-c98a-4924-aa0c-c12d9dde789f
📒 Files selected for processing (1)
packages/app/e2e/sidebar/sidebar-session-links.spec.ts
Summary
ShellSurfaceContextso the Opening recovery state also routes New session through the same owner instead of local route navigation.Opening session...recovery state with Retry and New session actions when the target session identity is selected but its messages are not ready.projectrelease reason andnextVisibleSessionID()wrapper.Why
Fixes #420.
Related: #419 for the stale running-question fallback path. This PR does not rewrite the full question recovery model.
The installed app could enter a shell-stuck state where global navigation looked clickable but did not change the visible page. The risky invariant was that the route could point at a new shell identity while the session view kept rendering the previous session as a loading bridge.
This PR favors trustworthy shell identity over visual continuity: when the route targets a session, the visible identity is that session; when the target data is not ready, the user sees a target loading/recovery state instead of stale content.
Related Issue
Fixes #420
Related: #419
Human Review Status
Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.
Review Focus
Please check:
createShellNavigationis the right narrow owner for shell actions without over-growinglayout.tsx.ShellSurfaceContextis an acceptable minimal bridge for session-page recovery actions before a fullShellRoute/ShellControllerrewrite.session-view-controllerno longer permits stale visible session identity while preserving loading readiness.SessionMainViewshows the target loading/recovery state only while route messages are not ready and keeps the composer hidden during that state.Risk Notes
Medium app-flow risk. This changes the session transition contract: users should see the target session loading state instead of the previous session during a not-ready route transition.
releaseTransientShellLocksis intentionally narrow in this PR: it clears the layout sizing lock and pending sizing timeout only. Modal, drag, permission, and question owners are not widened here; question and permission blockers remain session-local and must not block shell navigation.Settings still uses a local surface signal, now entered through the shell owner. A full
ShellRoute/ShellControllerstate machine, broad interaction-lock registry, durable blocker recovery source, visible question recovery UI, and soft timeout/error state for long Opening transitions are left as follow-up architecture work to avoid turning this P0 bugfix into a broad frontend rewrite.The broader settings E2E command also hit an existing
unknown theme ids migrate to pawwork and clear cached cssfailure unrelated to this change. The narrower shell navigation E2E checks passed.How To Verify
Screenshots or Recordings
Not applicable. This is shell navigation behavior with a small loading/recovery-state addition.
Checklist
dev, and my PR title and commit messages use Conventional Commits in EnglishSummary by CodeRabbit