Skip to content

fix(web-ui): keep worktree sessions in their owning project - #3176

Merged
kev1n77 merged 6 commits into
GCWing:mainfrom
kev1n77:fmy/bugfix
Sep 21, 2026
Merged

kev1n77 merged 6 commits into
GCWing:mainfrom
kev1n77:fmy/bugfix

Conversation

@kev1n77

@kev1n77 kev1n77 commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

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:

  • Owning project ID (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.
  • Execution ID (the worktree workspace record) keeps driving where the work runs: session creation, turn dispatch, git state, the workspace strip, and terminal cwd.

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 - NavPanel sessions and workspaces sections, flow_chat session 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:

  • Creating a session from the composer's local menu with a new worktree made the session disappear from the current workspace and attached it to the worktree directory instead.
  • Re-entering that session reported "the workspace this session belongs to is not open yet; reopen that workspace before switching to this session".
  • The composer could not resolve modes, subagents, or external sources, and restore_session_view / load_session_turns / save_session_turn / delete_session failed for the session while the run itself worked.
  • Terminals opened for the session ran in the project directory, so a hand-started shell used the main working tree while the agent edited the worktree.
  • The session row's worktree marker carried a text label, the new menu entry copy was long enough to fade under the menu's overflow mask, and a linked worktree row showed a "show more +N" affordance for rows it could never reveal.

What changes for users:

  • A worktree-isolated session stays in the session list of the workspace it was created from, and is marked with an icon-only worktree badge whose tooltip carries the execution directory.
  • The worktree directory can be opened as its own workspace; it shows its own sessions and does not list the project's sessions.
  • Terminals created for that session start in the execution (worktree) root. An explicit directory, such as the file explorer's "open terminal here", still wins.
  • Session storage and configuration now resolve for these sessions instead of failing.

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 unused nav.sessions.worktreeBadge key removed from all three catalogs).
  • pnpm run theme:color-audit:all - pass, 23 surfaces.

Manual, Windows desktop (local workspace), run by the reporter:

  • Open a project folder, create a session, choose "New worktree" in the composer's local menu: the session stays in that project's list, with the worktree badge and the worktree path in its tooltip.
  • Run a turn: the agent works in the worktree directory; a new terminal for the session opens there.
  • Open the worktree directory as its own workspace: it lists its own sessions only.
  • Re-enter the worktree session after restarting: it activates its owning project and no longer reports an unopened workspace.
  • A linked worktree row no longer shows an empty "show more +N".

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):

  • A session whose config.executionTarget.kind !== 'local' is worktree-isolated and is owned by projectWorkspaceId, falling back to the execution ID.
  • Every other session is owned by the workspace it was created in - including a session created while browsing a linked worktree as its own workspace, which stays under that worktree row even though the worktree's project ID points at the main checkout.
  • Top-level IDs are authoritative; config copies 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-badge in favor of the trailing scheduledJobMark; the worktree badge was re-inserted between the dispatch and review badges accordingly.

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

…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.
@kev1n77
kev1n77 merged commit 0c447c3 into GCWing:main Sep 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant