fix(web-ui): keep worktree sessions in their owning project - #3176
Merged
Merged
Conversation
…ls there A session that executes in a managed worktree is stamped with the worktree's own workspace record. That record is registered for execution but not opened, so grouping sessions by that raw workspace ID dropped the session out of every navigation group as soon as its first message created the worktree. Sessions are now attributed to their owning project workspace ID instead of a path, the row carries a worktree badge with the execution directory, and that directory can be opened as a workspace from the row menu. Terminals opened for such a session's project still belong to the project, but their cwd now follows the session's execution root, so a hand-started shell no longer runs git and build commands in the main working tree while the agent edits the worktree. An explicit directory, such as the file explorer's open terminal here, still wins.
Returning to a session that executes in a managed worktree reported that its workspace was not open and refused to switch to it. Activation resolved the session's execution workspace, which for such a session is the worktree record registered on demand during its first message and normally not open, while the session is listed under its project. Session ownership is now one shared identity, `sessionOwningWorkspaceId`, used by the navigation list, the workspace bootstrap, the scene tab key, session activation, the scene-active check, and the companion jump. None of them can derive a different owner any more. A worktree-isolated session is owned by its project, so the project is what gets activated and its scene counts as active. A session created while browsing a linked worktree as its own workspace is still owned by that workspace, even though the worktree's project ID points at the main checkout.
Every persisted-session command takes a workspace ID that the backend resolves into a session directory, and the backend refuses an ID whose workspace record is not an open workspace. A managed worktree record is created on demand for one session, is normally not open, and resolves to the same session directory as the project it belongs to, so addressing it made restore_session_view, load_session_turns, save_session_turn, and delete_session fail for a worktree session while the run itself worked. Route those calls through one owning-project selector so the shell never sends an execution identity for session storage, and keep the full-history hydration request key on the same identity the store already dedupes with.
A session isolated in a linked worktree carries that worktree's workspace ID as its execution identity. Persistence, session state, and the session's own configuration catalogs were addressed with the same ID, but a managed worktree record exists for execution and is normally not an open workspace, so the host rejected those requests outright and the composer could not resolve modes, subagents, or external sources for the session. Address session storage, activity, archive, rename, compact, fork, delete, export, and usage-report calls, plus the composer's workspace configuration and permission-mode requests, with the owning project ID. Requests that describe where the session executes - session creation, turn dispatch, git state, and the workspace strip - keep the execution ID, which the strip now receives explicitly.
A linked worktree row stores its sessions in the main workspace session root, so the metadata page total also counted the project sessions and the row offered "show more +N" for rows its own list can never reveal. Size the expand toggle from the rows the row actually owns when the scoped workspace is a linked worktree. The worktree marker beside a session title is now icon-only, and the menu entry that opens the worktree directory as a workspace uses the shorter "Open worktree workspace" copy that fits the menu width.
The icon-only worktree marker next to a session title used the 8px 2xs glyph inside a 14px circle; at that scale the worktree glyph was hard to read. Use the same 16px circle and 12px glyph as the neighbouring background-subagent badge.
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.
Summary
A session that runs in a managed Git worktree was addressed with the worktree's own workspace record everywhere. That record is created on demand for execution and is normally not an open workspace, so the session dropped out of the project's session list as soon as its first message materialized the worktree, returning to it reported that its workspace was not open, and session storage, session state and composer configuration requests were rejected by the host.
This PR gives session-to-workspace identity one shared owner and splits the two facts that were previously conflated:
sessionOwningWorkspaceId) drives navigation group membership, session storage, session state, archive/rename/compact/fork/delete/export, the usage report, and the session's own configuration catalogs.It also marks such a row with a worktree badge, lets the worktree directory be opened as its own workspace from the row menu, and keeps the two workspaces' session lists separate.
Type and Areas
Type: bug fix (session ownership/regression fix) with a small UI/UX change.
Areas: Web UI only -
NavPanelsessions and workspaces sections,flow_chatsession store/services/drivers/utilities, terminal and scene-activation hooks, shared i18n locales (en-US,zh-CN,zh-TW). No Rust change and no persisted-format change.Motivation / Impact
Reported behavior:
restore_session_view/load_session_turns/save_session_turn/delete_sessionfailed for the session while the run itself worked.What changes for users:
Verification
Automated:
pnpm --dir src/web-ui run type-check- pass.pnpm --dir src/web-ui exec eslint <changed source files>- pass (0 errors; test files are excluded by the eslint config).pnpm --dir src/web-ui run test:run src/app/components/NavPanel- 33 files / 198 tests pass.pnpm --dir src/web-ui run test:run src/app/components/NavPanel/sections/sessions src/app/hooks/useSessionTerminalDirectory.test.ts src/app/hooks/sessionTerminalDirectoryWiring.test.ts src/flow_chat/utils/sessionOrdering.test.ts src/flow_chat/utils/sessionWorktree.test.ts src/flow_chat/store/FlowChatStore.test.ts src/app/scenes/terminal/TerminalActionBridge.test.tsx- 16 files / 247 tests pass.pnpm run i18n:audit- pass, 0 warnings (locales updated; the now unusednav.sessions.worktreeBadgekey removed from all three catalogs).pnpm run theme:color-audit:all- pass, 23 surfaces.Manual, Windows desktop (local workspace), run by the reporter:
Not exercised: remote workspace, Remote Control, Peer Device Mode and Detached Dispatch paths. The change is frontend identity routing, and this branch was verified against a local workspace only.
Reviewer Notes
Ownership rules this PR relies on (see
src/web-ui/src/flow_chat/utils/sessionOrdering.ts):config.executionTarget.kind !== 'local'is worktree-isolated and is owned byprojectWorkspaceId, falling back to the execution ID.configcopies only serve legacy records persisted without them. Legacy records with no workspace identity stay unresolved rather than being grouped by a guessed folder.Trade-off accepted for the expand toggle: for a linked worktree row, the storage-level metadata total counts the project's sessions (both resolve to the same session directory), so the toggle is now sized from the rows that row actually owns. A worktree's own session that falls beyond the loaded page of that storage domain is therefore not revealed by "show more" - this is the pre-existing storage-domain paging versus navigation-domain filtering mismatch, and the row is now at least not offering rows it cannot show.
Compatibility: no persisted field was added, removed, or repurposed, and no migration or data repair is needed. Rollback is a plain revert of the branch.
Rebase note: this branch was rebased onto
3756f38c1, where upstream removed the in-row__inline-item-cron-badgein favor of the trailingscheduledJobMark; the worktree badge was re-inserted between the dispatch and review badges accordingly.Checklist