fix: hold the session read-only while it moves into a fork

An edit in the session's preview during a move saves a draft in the
workspace being left, after the move listed what it brings, at any of
its awaits. The session now reads as read-only for the whole move, so
the preview shows its items without an editor until the move ends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-26 11:17:27 -07:00
co-authored by Claude Opus 5.5
parent cdd6fb2ca7
commit 6912a9546c
3 changed files with 10 additions and 3 deletions
@@ -785,6 +785,9 @@ export class AIChatManager implements ChatViewHost {
/** Use, Plan or Build. A session keeps its own: the runtime seeds it from the session record
* and stores each change through `onWorkModeChanged`. */
workMode = $state<AIWorkMode>(AIWorkMode.BUILD)
/** The session is moving into a fork. Its editors stay closed meanwhile: an edit would save a
* draft in the workspace being left, after the move listed what it brings along. */
moving = $state(false)
onWorkModeChanged: ((mode: AIWorkMode) => void) | undefined = undefined
workModesAvailable = $derived(this.isSessionChat && supportsPlanMode(this.mode))
/** Use is the only mode: the workspace is protected, or the user's role builds nothing in it. */
@@ -541,6 +541,7 @@
return
}
moving = true
if (runtime) runtime.manager.moving = true
const releaseAutoResume = runtime?.manager.holdAutoResume()
// Bracketed like a turn, so another tab on this session cannot send while its drafts
// move, then re-reads the session (its new workspace, the model note) once it ends.
@@ -614,6 +615,7 @@
sendUserToast(`Session moved to ${req.targetName}`)
} finally {
moving = false
if (runtime) runtime.manager.moving = false
releaseAutoResume?.()
if (chatId)
localRunEnded(session.id, runtime?.manager.historyManager.getCurrentChatId() ?? chatId)
@@ -42,7 +42,7 @@ export function useSessionReadOnly(workspace: () => string | undefined) {
}
/** The session chat whose work mode the session UI follows. */
type WorkModeHost = { readonly workMode: AIWorkMode }
type WorkModeHost = { readonly workMode: AIWorkMode; readonly moving?: boolean }
/**
* What the session UI may offer in `workspace`. Its editors save drafts, so it reads
@@ -51,7 +51,7 @@ type WorkModeHost = { readonly workMode: AIWorkMode }
* resolves: hiding every editor from a developer while `whoami` is in flight is the worse
* mistake, and the server refuses an operator anyway. A session held in Use opens no editor: one
* in a protected workspace, one whose role builds nothing there, or one whose chat is in Use
* mode — the chat `chat` returns, else the `aiChatManager` context's.
* mode — the chat `chat` returns, else the `aiChatManager` context's — and one moving into a fork.
* Reads context and registers an effect: call during component initialisation.
*/
export function useSessionPermissions(
@@ -62,10 +62,12 @@ export function useSessionPermissions(
const rights = useEditRights(workspace)
const user = useActingUser(workspace)
const protectedWorkspace = useSessionReadOnly(workspace)
const host = $derived(chat?.() ?? contextChat)
const readOnly = $derived(
protectedWorkspace.current ||
!buildsAnything(rights.capabilities) ||
(chat?.() ?? contextChat)?.workMode === AIWorkMode.USE
host?.workMode === AIWorkMode.USE ||
!!host?.moving
)
const workspaces = fromStore(userWorkspaces)
const entry = $derived(workspaces.current.find((w) => w.id === workspace()))