From d7d351eed4737846d9efbfe8f84e7998dcb43e15 Mon Sep 17 00:00:00 2001 From: xarmian Date: Wed, 22 Jul 2026 22:25:48 +0000 Subject: [PATCH] fix(web): make itemMatchesRef workspace-aware in ItemDetail (IDEA-2135) The no-{#key} switch-boundary predicate compared only ref/slug identity, never workspace. On a reused embedded ItemDetail instance, navigating ws1?item=TASK-1 -> ws2?item=TASK-1 (both workspaces owning TASK-1) kept the predicate true across the switch, leaving collabKey pinned to ws1's item.id and rawMode carried over until ws2's loadData resolved. Stamp the wsSlug each item is loaded under (loadedItemWsSlug, lock-stepped with item adoption inside the myItemGen===itemGen gate) and fold loadedItemWsSlug === wsSlug into itemMatchesRef. scrollReady, collabKey, resolvedIdentity, and the rawMode-reset gate all derive from it, so they tighten together and stay consistent. Single-workspace usage is unchanged (the arm is always true there). TASK-2283. Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra --- .../lib/components/items/ItemDetail.svelte | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/items/ItemDetail.svelte b/web/src/lib/components/items/ItemDetail.svelte index eb8d02a2..da35043a 100644 --- a/web/src/lib/components/items/ItemDetail.svelte +++ b/web/src/lib/components/items/ItemDetail.svelte @@ -178,6 +178,16 @@ (onNavigateAway ?? ((u: string) => goto(u, { replaceState: true })))(url); let item = $state(null); + // The wsSlug the currently-held `item` was actually loaded under. Stamped + // lock-stepped with every `item` adoption in loadData, so it can never + // disagree with `item`'s workspace. Folded into `itemMatchesRef` below so the + // switch boundary is workspace-aware: on a REUSED (no-{#key}) ItemDetail + // instance, a cross-workspace nav that preserves `?item=` for a ref both + // workspaces own (ws1?item=TASK-1 → ws2?item=TASK-1) would otherwise keep the + // predicate true across the switch — leaving `collabKey` pinned to ws1's + // item.id and rawMode carried over until ws2's load resolves (IDEA-2135). + // In single-workspace usage this always equals `wsSlug` → byte-identical. + let loadedItemWsSlug = $state(''); let collection = $state(null); let loading = $state(true); let error = $state(''); @@ -376,8 +386,15 @@ // stale provider for the previous item tears down the instant `ref` // changes — before the new fetch resolves — instead of lingering with a // destroyed editor (PLAN-2105 / TASK-2112 switch-safety; Codex). + // The workspace arm (`loadedItemWsSlug === wsSlug`) makes the switch boundary + // workspace-aware (IDEA-2135): a reused instance navigating ws1→ws2 with the + // same `?item=`, where both workspaces own that ref, flips the predicate + // false the instant `wsSlug` changes — before ws2's load resolves — so + // `collabKey`/`scrollReady`/`resolvedIdentity` and the rawMode-reset gate all + // tighten together instead of lingering on the previous workspace's item. let itemMatchesRef = $derived( item !== null && + loadedItemWsSlug === wsSlug && (item.id === itemSlug || item.slug === itemSlug || `${item.collection_prefix}-${item.item_number}` === itemSlug), @@ -1404,7 +1421,15 @@ // Gate the ITEM snapshot on itemGen too: a newer item write (e.g. the // migration refetch) that bumped itemGen but not loadGeneration must // not be reverted by this in-flight load (round 9). - if (myItemGen === itemGen) item = withInflightTags(itemData); + // Stamp the workspace this item was loaded under in lockstep with the + // item adoption (IDEA-2135) — same gate, so `loadedItemWsSlug` and + // `item` can never disagree about which workspace `item` belongs to. + // `reqWsSlug` (captured at load start) is authoritative here: `myGen` + // was already checked above, so no newer load has moved `wsSlug` on. + if (myItemGen === itemGen) { + item = withInflightTags(itemData); + loadedItemWsSlug = reqWsSlug; + } // Cross-collection `?item=` safety (PLAN-2105 / TASK-2112). The // Promise.all above optimistically fetched the collection by the // route's `collSlug` — correct for the common (row-click) case,