Repository navigation
fix(app): land on the next session when the open one is deleted - #276
Merged
Merged
Conversation
quickbeard
force-pushed
the
fix/delete-session-jump
branch
from
August 19, 2026 11:24
b51358b to
815a414
Compare
Deleting the session on screen left the route pointing at it. The tab was spliced out of the store, nothing moved, and the window rendered "This session cannot be found" over a Close Tab button -- a dead end reached by the most ordinary way there is to delete a session, its own row in the sidebar. context/tabs.tsx already had a successor branch for this and it has been dead for a while: removeSessions() decides whether the deleted session is the one on screen from `params.dir && params.id`, and `dir` is a param of the legacy /:dir/session/:id route only. Sessions route through /server/:serverKey/session/:id now, so the condition is never true, the branch returns before navigating, and no amount of deleting moves the route. Left alone rather than revived: it is an upstream file, and two places choosing a destination would race. deleteSession (fork-only pages/home-model.tsx) picks the destination instead, from the list the user is actually looking at -- the sidebar's own rows, Pinned above Recents, snapshotted before the request because the delete refreshes the index they derive from. sessionAfterRemoval scans down from the deleted row and wraps, so the bottom row lands on the top one, and it skips every id the delete took: without that the wrap comes back around to the deleted session itself, which is the only candidate left when it was the only row. That case answers undefined and opens a new session in the same project instead. Indexed by the deleted session rather than by the session on screen, because those differ when a child session is open and its parent is deleted, and a child is not a row. Nothing happens unless the route is a session, on the server the sidebar is focused on, whose id the delete took -- so deleting a session you are not in still just removes it and its tab. Both sides get this: the list, the open path and the new-session path are all the ones the sidebar already uses, and none of them are Code-specific. The in-session header's own delete (message-timeline.tsx) is untouched. It already navigates -- parent, else the next session in the directory, else a new draft -- so it never produced the not-found screen, and giving it this rule would mean reaching the sidebar's list from a component that has no model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by deleting an open session in the real app: it lands on the next one now, but the session just deleted is still in the tab strip, titled "Unknown Session" and closable only by hand. The strip is kept in step with the route by an effect in titlebar.tsx: if the route names a session that has no tab, it adds one. Dropping the tabs before navigating put the app in exactly that state for an instant -- the route still on the deleted session, its tab already gone -- so the effect minted it a replacement. The replacement is titleless because the removal took the remembered title with it, and it survives the navigation that follows because nothing removes it a second time. Navigate first, then drop the tabs. The route never names a session with no tab, so there is no instant for the effect to act on. openSession and openProjectNewSession return their transitions so the delete can await the move, which also means the old session stays on screen, tab and all, until the next one is ready to replace it -- rather than the tab disappearing under a screen that has not changed yet. The e2e case that covers the landing now also asserts the strip holds exactly the tab it landed on. It fails without this: the mock answers for a deleted session, so the revived tab keeps its title there and the count is what gives it away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quickbeard
force-pushed
the
fix/delete-session-jump
branch
from
August 19, 2026 11:32
815a414 to
50f24e8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deleting the session on screen left the route pointing at it: the tab was spliced out of the store, nothing moved, and the window rendered "This session cannot be found" over a Close Tab button — reached by the most ordinary way there is to delete a session, its own row in the sidebar.
Two bugs, one flow
1. Why nothing moved
context/tabs.tsxalready has a successor branch for exactly this, and it has been dead for a while.removeSessions()decides whether the deleted session is the one on screen like this:diris a param of the legacy/:dir/session/:idroute only. Sessions route through/server/:serverKey/session/:idnow (app.tsx:662), so the condition is never true,removedCurrentis always false, and the function returns before it can navigate.I left that branch alone rather than reviving it — it is an upstream file, and two places choosing a destination would race.
What it does now
deleteSession(fork-onlypages/home-model.tsx) picks the destination, from the list the user is actually looking at: the sidebar's own rows, Pinned above Recents, snapshotted before the request because the delete refreshes the index they derive from.sessionAfterRemovalis the rule, exported besidedescendantIDsand unit-tested for the same reason. Two details worth knowing:undefinedand sends you to a new session.2. Why the tab came back
The strip is kept in step with the route by an effect in
titlebar.tsx: if the route names a session with no tab, it adds one. Dropping the tabs before navigating put the app in exactly that state for an instant — route still on the deleted session, its tab already gone — so the effect minted a replacement. Titleless, because the removal took the remembered title with it, and permanent, because nothing removes it a second time.The fix is ordering: navigate first, then drop the tabs, so the route never names a session with no tab.
openSessionandopenProjectNewSessionnow return their transitions so the delete can await the move. That has a second benefit — the old session stays on screen, tab and all, until the next one is ready to replace it, instead of the tab vanishing under a screen that has not changed yet.Deliberately out of scope
The in-session header's own delete (
message-timeline.tsx) is untouched. It already navigates — parent, else the next session in the directory, else a new draft — so it never produced the not-found screen. Giving it this rule would mean reaching the sidebar's list from a component that has no model;createHomeModel()is a factory each surface calls for itself, not a context. Happy to unify in a follow-up if you want one rule everywhere.Testing
sessionAfterRemovalintest-browser/home-session-records.test.ts— row below, crossing Pinned into Recents, wrap, skipping removed rows, wrapping past a removed run, empty list, a deleted session the list does not hold, and order preservation.e2e/regression/session-delete-navigation.spec.ts— row below, wrap from bottom to top, and the no-op when the deleted session is not on screen. Verified they bite: with thehome-model.tsxchange stashed, the two navigation cases fail and only the no-op passes.packages/apptypecheck clean, 702 unit + 110 browser passing, Playwright 140 passed / 2 failed (prompt-thinking-level,session-actions-menu:104) — both fail identically on the base commit and are unrelated.DIVERGENCES.mdrow:home-model.tsxis fork-only (added by feat(app): make the sidebar persistent and give it a Recents section #173) and the e2e spec is new.🤖 Generated with Claude Code