prism branch T-1758/bugfix-non-file-image-detail-window-collision commits 2 files 7 touched (2 production, 4 test, 1 report) lines +155 / -26

Pre-push review: T-1758/bugfix-non-file-image-detail-window-collision

PR #399 — macOS image detail windows from clipboard/bundled/remote documents no longer collapse onto a shared nil identity. Diff reviewed: git diff origin/main...HEAD.

At a glance

  • ImageDetailIdentifier.documentURL: URL?ownerURL: URL; ImageDetailWindowData.documentURL deleted (it was derivable from sourceType + ownerURL).
  • MediaZoomPresenter.openImageWindow no longer passes documentFileURL; the diagram path still does (deliberately deferred, see findings).
  • Window restoration is safe: synthesised Decodable ignores a stale documentURL key and ownerURL already existed.
  • New ImageDetailWindowDataTests (4 tests) plus 3 existing suites migrated; targeted run of 5 suites passed, make lint 0 violations.
  • Sibling DiagramIdentifier keeps the same latent bug (also in its persistenceKey) — worth a follow-up ticket, not a blocker.

Verdict

Ready to push

The fix is minimal and correct: ImageDetailIdentifier now keys on the non-optional ownerURL (already used for lifecycle since T-1503), so the redundant, often-nil documentURL is removed rather than patched. Codable compatibility is preserved (synthesised conformance, no key added). Every constructor call site was migrated, targeted tests pass, lint is clean. All findings are minor or nits; none were fixed here because this review was run read-only, and none block the push.

Review findings

7 raised · 0 fixed · 7 skipped

Jump to findings →

Commits

Three-level explanation

What changed

On the Mac, double-clicking an image opens it in its own window. Prism remembers which windows are open so it does not open the same image twice; instead it brings the existing window forward. The 'which image is this' label was built from the document's file path plus the image name. Documents that are not files (pasted from the clipboard, the built-in welcome guide, or downloaded from a URL) have no file path, so their label was just the image name. Two such documents with an image called img.png looked identical, and opening the second one brought forward the first document's window.

Why it matters

Users saw the wrong picture. The fix uses a document key that every document has (the file path, or a synthetic per-document key for the others) so labels never collide.

Key concepts

  • Deduplication identity: a hashable value used as a dictionary key to find an already-open window.
  • Owner key: the same value was already used to close a document's windows when the document closes; now it also scopes identity.

Architecture

ImageDetailWindowData is the Codable payload handed to WindowGroup("Image", id: "image-detail", for:). It exposes identifier, an ImageDetailIdentifier used by ImageDetailWindowManager for markPendingOpen/bringToFront/registerWindow. Previously the payload carried two keys: documentURL: URL? (file sessions only) for identity and ownerURL: URL (DocumentSession.diagramOwnerURL: source.url or prism://clipboard/{sessionID}) for lifecycle. The fix collapses both onto ownerURL.

Patterns

  • Remove redundant state instead of patching it: documentURL was exactly sourceType == "file" ? ownerURL : nil.
  • Identity keys must be non-optional; an optional folds every 'absent' case into one bucket.

Trade-offs

  • Two document windows on the same remote/bundled URL share one image window — identical to the pre-existing .file behaviour, so consistent.
  • The mirror-image DiagramIdentifier is left alone to avoid conflicts with T-2014/T-2175/T-2221; it is also Codable-persisted via persistenceKey, so changing it is a stored-state migration.

Deep dive

The change is confined to the identity struct and its single production constructor. ImageDetailIdentifier is nonisolated struct … Hashable, Sendable, not Codable, so no persisted shape changes there. ImageDetailWindowData uses synthesised Codable with no CodingKeys; dropping an optional stored property is decode-compatible with any window-restoration payload SwiftUI may replay, and WindowGroup(for:)'s value equality classes are unchanged since the dropped field was a pure function of the remaining ones.

Architecture impact

ImageDetailWindowManager.windowOwnership is now fully redundant: for every registration identifier.ownerURL == ownerURL. It is retained for API symmetry with DiagramWindowManager (whose identifier still has an optional URL) but is a second source of truth a caller could contradict.

Edge cases

  • URL == is exact: /tmp/x.md vs /private/tmp/x.md yields two windows; DocumentFlowCoordinator already documents this for the close path.
  • A restored .bundled payload holds a Bundle.main path; if the bundle relocates between launches, dedup and close both miss — pre-existing for lifecycle, now extended to identity. Negligible in practice.
  • The new tests build owner URLs from literals (prism://bundled/welcome.md is not a shape production ever produces) and cannot fail unless the optional is reintroduced; the wiring risk in MediaZoomPresenter is covered indirectly by DocumentFlowCoordinatorImageWindowTests.

Completeness assessment

Fully implemented: identity collision for image windows across clipboard/bundled/url documents; call-site migration; report. Partially: regression coverage (exercise real DocumentSessions; a legacy-payload decode test). Missing / deferred: DiagramIdentifier parity and its persistenceKey collision.

Important changes — detailed

ImageDetailIdentifier: optional documentURL → non-optional ownerURL

prism/Models/ImageDetailWindowData.swift

Why it matters. This is the fix. The optional key folded every non-file document into one (nil, imageSource) bucket; the non-optional owner key gives each document its own identity space.

What to look at. prism/Models/ImageDetailWindowData.swift:28-62 (ImageDetailWindowData.ownerURL, identifier, ImageDetailIdentifier.ownerURL)

Takeaway. Never build a hashable identity from an optional that is nil for whole categories of input — absent values collapse into one collision class. Reuse the lifecycle owner key as the identity key so there is one source of truth.
Rationale. ownerURL already existed for closeDocument targeting (T-1503) and is non-optional for every source; documentURL was derivable from it, so it was redundant state. Stated in the doc comments and the bugfix report.

MediaZoomPresenter.openImageWindow: stop passing documentFileURL

prism/Views/MediaZoomPresenter.swift

Why it matters. The only production constructor of the payload; the diagram path a few lines above still uses the optional file URL, which is the remaining instance of the same defect.

What to look at. prism/Views/MediaZoomPresenter.swift:91-110

Takeaway. When two sibling paths share a bug, fixing one while leaving the other should be an explicit, documented decision with a follow-up — the comment here and the report do that.
Rationale. DiagramIdentifier left untouched to avoid conflicts with T-2014, T-2175, T-2221 (bugfix report). The stronger reason — DiagramIdentifier.persistenceKey is persisted state, so changing it is a migration — is not stated.

ImageDetailWindowDataTests: new regression suite

prismTests/ImageDetailWindowDataTests.swift

Why it matters. Pins that distinct clipboard/bundled/remote owners produce distinct identifiers and that the same owner still dedups.

What to look at. prismTests/ImageDetailWindowDataTests.swift:28-90

Takeaway. Tests that only exercise the synthesised Hashable of a non-optional field cannot fail unless the type regresses; the wiring (session → payload) is where the risk lives. Prefer real DocumentSession fixtures, as the neighbouring coordinator tests do.
Rationale. Tests were written against literal owner URLs for simplicity; the coordinator suites were updated to use session.diagramOwnerURL. (inferred — not stated by the author)

Codable payload keeps window restoration compatible

prism/Models/ImageDetailWindowData.swift

Why it matters. ImageDetailWindowData is the value type SwiftUI persists for WindowGroup(for:) restoration; a shape change could break relaunch.

What to look at. prism/Models/ImageDetailWindowData.swift:18-46; prism/prismApp.swift:218

Takeaway. Removing a stored property from a synthesised-Codable type is decode-compatible (unknown keys are ignored); adding a non-optional one is not. Check which direction a payload change goes before shipping.
Open question. Rationale not stated by the author and not inferable from the diff.

Key decisions

Reuse <code>diagramOwnerURL</code> as the image identity key rather than adding a new key.

It is non-optional for all four sources and already governs closeWindows(ownedBy:); one key for both concerns removes the possibility of them disagreeing. Source: doc comments and specs/bugfixes/non-file-image-detail-window-collision/report.md.

Leave <code>DiagramIdentifier</code> untouched.

Report cites conflict avoidance with T-2014, T-2175, T-2221. Review note: DiagramIdentifier.persistenceKey is written to UserDefaults, so fixing it also needs a key migration — a stronger reason to defer, and a reason to file a follow-up ticket.

Delete <code>documentURL</code> instead of keeping it as informational.

It was exactly sourceType == "file" ? ownerURL : nil; keeping it would have preserved a second, contradictable source of truth.

(inferred — not stated by the author.)

Review findings

SeverityAreaFindingResolution
minorImageDetailWindowManager.windowOwnershipAfter this change identifier.ownerURL == the ownedBy: argument for every registration, so the windowOwnership dictionary and the ownedBy: parameter are redundant state; closeWindows/minimizeWindows/restoreWindows could filter openWindows.keys on $0.ownerURL.Not changed — review was read-only and the manager's shape mirrors DiagramWindowManager, which still needs its copy. Consider folding when DiagramIdentifier gets the same treatment.
minorprismTests/ImageDetailWindowDataTests.swiftTests build owner URLs from string literals (prism://bundled/welcome.md is not a shape production produces) and assert on synthesised Hashable of a non-optional field — they cannot fail unless the optional is reintroduced. No test exercises a real DocumentSession through the payload, and no test decodes a legacy payload containing documentURL.Skipped (no source edits). Suggest: build fixtures from DocumentSession(...).diagramOwnerURL / BundledDocument.url(for:), collapse the three near-identical tests into @Test(arguments:), and add a legacy-JSON decode test.
minorDiagramIdentifier (sibling, out of scope)DiagramIdentifier.documentURL is still optional and feeds persistenceKey, so clipboard/bundled/remote documents collide on both window identity and persisted zoom state (diagram-clipboard-{offset}). MediaZoomPresenter.documentFileURL also discards the non-nil URL that bundled/url sources have.Deferred per report. Recommend filing a follow-up ticket that also names the persistence-key migration.
nitDocumentSession.diagramOwnerURL namingThe property is now the general per-document window identity key for images too; its doc comment still says 'diagram window ownership tracking'.Skipped; rename/doc tweak when DiagramIdentifier is aligned.
nitURL equalityDedup now hinges on exact URL ==, so /tmp/x.md vs /private/tmp/x.md gives two windows. Already documented for the close path in DocumentFlowCoordinator but not on ImageDetailWindowData.Skipped; pre-existing trade-off, standardizedFileURL at the diagramOwnerURL source would address both.
nitDocumentFlowCoordinator.swift:657-658imageWindowOwnerURL and diagramOwnerURL bind the same expression and are unwrapped separately (pre-existing).Skipped; unrelated to this diff.
nitUncommitted doc-comment editMediaZoomPresenter.swift:63-65 has an unstaged doc-comment change in the worktree pointing at the diagram path's remaining optional.Left alone — another agent is concurrently committing that fix to this branch.

Per-file diffs

Click to expand.

prism/Models/ImageDetailWindowData.swift Modified +13 / -14
diff --git a/prism/Models/ImageDetailWindowData.swift b/prism/Models/ImageDetailWindowData.swiftindex e20526dc..2868a031 100644--- a/prism/Models/ImageDetailWindowData.swift+++ b/prism/Models/ImageDetailWindowData.swift@@ -25,17 +25,12 @@ struct ImageDetailWindowData: Codable, Hashable, Sendable {     /// Optional title/caption for the image.     let title: String? -    /// Owning document URL (for window grouping and dedup identity).-    ///-    /// Nil for clipboard/bundled/url sources. Use `ownerURL` (not this) for-    /// lifecycle ownership — `documentURL` only scopes the dedup identifier.-    let documentURL: URL?--    /// Stable owner key used to close this window when the parent document-    /// closes. For file sources this is the file URL; for clipboard sources it-    /// is the synthetic `prism://clipboard/{sessionID}` URL (`diagramOwnerURL`),-    /// so clipboard windows are still tracked under a per-document key rather-    /// than a shared fallback (T-1503).+    /// Stable owner key used both for window lifecycle (closing this window+    /// when the parent document closes) and for dedup identity scoping. For+    /// file sources this is the file URL; for clipboard sources it is the+    /// synthetic `prism://clipboard/{sessionID}` URL (`diagramOwnerURL`), so+    /// non-file windows are still tracked under a per-document key rather+    /// than collapsing to a shared fallback (T-1503, T-1758).     let ownerURL: URL      /// Base URL for resolving relative image paths on re-load.@@ -46,7 +41,7 @@ struct ImageDetailWindowData: Codable, Hashable, Sendable {      /// Derives the identifier used for window deduplication.     var identifier: ImageDetailIdentifier {-        ImageDetailIdentifier(documentURL: documentURL, imageSource: imageSource)+        ImageDetailIdentifier(ownerURL: ownerURL, imageSource: imageSource)     } } @@ -54,8 +49,12 @@ struct ImageDetailWindowData: Codable, Hashable, Sendable { /// /// Prevents opening multiple windows for the same image in the same document. nonisolated struct ImageDetailIdentifier: Hashable, Sendable {-    /// The document's file URL (nil for clipboard content).-    let documentURL: URL?+    /// The document's stable owner key (never nil — see `ImageDetailWindowData.ownerURL`).+    ///+    /// Using the non-optional owner key rather than the (often-nil) file URL+    /// keeps clipboard, bundled, and remote documents from collapsing to the+    /// same identity and colliding with each other's image windows (T-1758).+    let ownerURL: URL      /// The image source string.     let imageSource: String
prism/Views/MediaZoomPresenter.swift Modified +8 / -6
diff --git a/prism/Views/MediaZoomPresenter.swift b/prism/Views/MediaZoomPresenter.swiftindex e9bac053..fcee1f23 100644--- a/prism/Views/MediaZoomPresenter.swift+++ b/prism/Views/MediaZoomPresenter.swift@@ -60,8 +60,9 @@ struct MediaZoomPresenter: ViewModifier {     }      #if os(macOS)-    /// The owning file URL (nil for clipboard / bundled / url documents), used as the-    /// window identifier's document scope.+    /// The owning file URL (nil for clipboard / bundled / url documents). Only the+    /// diagram window path still keys on this optional (`DiagramIdentifier.documentURL`);+    /// image detail windows key on the session's non-optional owner URL instead (T-1758).     private var documentFileURL: URL? {         if case .file(let url) = session.source { return url }         return nil@@ -93,10 +94,11 @@ struct MediaZoomPresenter: ViewModifier {             imageSource: request.source,             alt: request.alt,             title: request.title,-            documentURL: documentFileURL,-            // Use the stable per-document owner key (synthetic for clipboard) so-            // closeDocument can target these windows even when there is no file-            // URL — mirrors the diagram window path (T-1503).+            // Use the stable per-document owner key (synthetic for clipboard,+            // resource-derived for bundled/url) both for closeDocument targeting+            // and for dedup identity scoping — mirrors the diagram window path+            // (T-1503) and prevents non-file documents from colliding on a+            // shared nil identity (T-1758).             ownerURL: session.diagramOwnerURL,             imageBaseURL: session.source.imageBaseURL,             sourceType: session.source.imageSourceType.rawValue
prismTests/ImageDetailWindowDataTests.swift Added +90 / -0
diff --git a/prismTests/ImageDetailWindowDataTests.swift b/prismTests/ImageDetailWindowDataTests.swiftnew file mode 100644index 00000000..7b65bf6e--- /dev/null+++ b/prismTests/ImageDetailWindowDataTests.swift@@ -0,0 +1,90 @@+//+//  ImageDetailWindowDataTests.swift+//  prismTests+//+//  T-1758: Non-file image detail windows collide across documents.+//+//  ImageDetailIdentifier used to scope its dedup identity on `documentURL`,+//  which is nil for clipboard, bundled, and remote documents (only `.file`+//  sessions have a non-nil file URL). Two windows for the SAME image source+//  string opened from two DIFFERENT non-file documents therefore reduced to+//  the identical (nil, imageSource) identity, so the second open was treated+//  as a duplicate of the first and brought the wrong document's window+//  forward instead of opening the requested image.+//+//  The fix scopes the identifier on `ownerURL` — the stable, non-optional+//  per-document key already used for window lifecycle ownership (file URL,+//  or a synthetic `prism://clipboard/{sessionID}` / resource URL for+//  clipboard/bundled/url sources) — so distinct documents never collapse to+//  the same identity even when they share an image source string.+//++#if os(macOS)+import Foundation+import Testing+@testable import prism++@Suite("ImageDetailWindowData identity (T-1758)")+struct ImageDetailWindowDataTests {++    private func makeData(ownerURL: URL, imageSource: String = "img.png") -> ImageDetailWindowData {+        ImageDetailWindowData(+            imageSource: imageSource,+            alt: "alt text",+            title: nil,+            ownerURL: ownerURL,+            imageBaseURL: nil,+            sourceType: "clipboard"+        )+    }++    @Test("Two clipboard sessions with the same image source get different identifiers")+    func distinctClipboardSessionsDoNotCollide() {+        // Mirrors two separate clipboard documents, each synthesising its own+        // diagramOwnerURL from its session UUID.+        let sessionAOwner = URL(string: "prism://clipboard/\(UUID().uuidString)")!+        let sessionBOwner = URL(string: "prism://clipboard/\(UUID().uuidString)")!++        let dataA = makeData(ownerURL: sessionAOwner)+        let dataB = makeData(ownerURL: sessionBOwner)++        #expect(dataA.identifier != dataB.identifier,+                "Two different clipboard documents opening the same image source must not collide")+    }++    @Test("Two different bundled documents with the same image source get different identifiers")+    func distinctBundledDocumentsDoNotCollide() {+        let welcomeOwner = URL(string: "prism://bundled/welcome.md")!+        let readmeOwner = URL(string: "prism://bundled/readme.md")!++        let dataA = makeData(ownerURL: welcomeOwner)+        let dataB = makeData(ownerURL: readmeOwner)++        #expect(dataA.identifier != dataB.identifier,+                "Two different bundled documents opening the same image source must not collide")+    }++    @Test("Two different remote documents with the same image source get different identifiers")+    func distinctRemoteDocumentsDoNotCollide() {+        let docAOwner = URL(string: "https://example.com/repo/a.md")!+        let docBOwner = URL(string: "https://example.com/repo/b.md")!++        let dataA = makeData(ownerURL: docAOwner)+        let dataB = makeData(ownerURL: docBOwner)++        #expect(dataA.identifier != dataB.identifier,+                "Two different remote documents opening the same image source must not collide")+    }++    @Test("Same owner URL and image source produce equal identifiers (dedup still works)")+    func sameOwnerAndSourceStillDedups() {+        let owner = URL(string: "prism://clipboard/\(UUID().uuidString)")!++        let dataA = makeData(ownerURL: owner)+        let dataB = makeData(ownerURL: owner)++        #expect(dataA.identifier == dataB.identifier,+                "Re-activating the same image in the same document must still dedup")+    }+}+#endif
prismTests/ImageDetailWindowManagerTests.swift Modified +1 / -1
diff --git a/prismTests/ImageDetailWindowManagerTests.swift b/prismTests/ImageDetailWindowManagerTests.swiftindex c9e25eaa..6eeac11d 100644--- a/prismTests/ImageDetailWindowManagerTests.swift+++ b/prismTests/ImageDetailWindowManagerTests.swift@@ -36,7 +36,7 @@ struct ImageDetailWindowManagerTests {         imageSource: String = "img.png"     ) -> ImageDetailIdentifier {         ImageDetailIdentifier(-            documentURL: URL(fileURLWithPath: path),+            ownerURL: URL(fileURLWithPath: path),             imageSource: imageSource         )     }
prismTests/DocumentFlowCoordinatorImageWindowTests.swift Modified +2 / -2
diff --git a/prismTests/DocumentFlowCoordinatorImageWindowTests.swift b/prismTests/DocumentFlowCoordinatorImageWindowTests.swiftindex a63346d9..545786b8 100644--- a/prismTests/DocumentFlowCoordinatorImageWindowTests.swift+++ b/prismTests/DocumentFlowCoordinatorImageWindowTests.swift@@ -74,7 +74,7 @@ struct DocumentFlowCoordinatorImageWindowTests {          // Simulate the open path: register an image window owned by the session's         // stable owner key (mirrors MediaZoomPresenter.openImageWindow after fix).-        let identifier = ImageDetailIdentifier(documentURL: nil, imageSource: "img.png")+        let identifier = ImageDetailIdentifier(ownerURL: session.diagramOwnerURL, imageSource: "img.png")         let window = makeWindow()         manager.registerWindow(window, for: identifier, ownedBy: session.diagramOwnerURL)         #expect(manager.openWindowCount == 1)@@ -99,7 +99,7 @@ struct DocumentFlowCoordinatorImageWindowTests {         let session = DocumentSession(url: fileURL, content: "![x](img.png)")         flow.currentSession = session -        let identifier = ImageDetailIdentifier(documentURL: fileURL, imageSource: "img.png")+        let identifier = ImageDetailIdentifier(ownerURL: session.diagramOwnerURL, imageSource: "img.png")         let window = makeWindow()         manager.registerWindow(window, for: identifier, ownedBy: session.diagramOwnerURL)         #expect(manager.openWindowCount == 1)
prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swift Modified +3 / -3
diff --git a/prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swift b/prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swiftindex 904ce865..0d9167c5 100644--- a/prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swift+++ b/prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swift@@ -87,7 +87,7 @@ struct DocumentFlowCoordinatorReplaceWindowCleanupTests {         let outgoingSession = DocumentSession(url: outgoingURL, content: "# Outgoing\n\n![x](img.png)")         flow.currentSession = outgoingSession -        let imageIdentifier = ImageDetailIdentifier(documentURL: outgoingURL, imageSource: "img.png")+        let imageIdentifier = ImageDetailIdentifier(ownerURL: outgoingSession.diagramOwnerURL, imageSource: "img.png")         let imageWindow = makeWindow()         imageManager.registerWindow(imageWindow, for: imageIdentifier, ownedBy: outgoingSession.diagramOwnerURL) @@ -138,7 +138,7 @@ struct DocumentFlowCoordinatorReplaceWindowCleanupTests {         let outgoingOwnerURL = outgoingSession.diagramOwnerURL         #expect(outgoingSession.source.url == nil) -        let imageIdentifier = ImageDetailIdentifier(documentURL: nil, imageSource: "img.png")+        let imageIdentifier = ImageDetailIdentifier(ownerURL: outgoingOwnerURL, imageSource: "img.png")         let imageWindow = makeWindow()         imageManager.registerWindow(imageWindow, for: imageIdentifier, ownedBy: outgoingOwnerURL) @@ -182,7 +182,7 @@ struct DocumentFlowCoordinatorReplaceWindowCleanupTests {         let outgoingSession = DocumentSession(url: sharedURL, content: "# Outgoing\n\n![x](img.png)")         flow.currentSession = outgoingSession -        let imageIdentifier = ImageDetailIdentifier(documentURL: sharedURL, imageSource: "img.png")+        let imageIdentifier = ImageDetailIdentifier(ownerURL: outgoingSession.diagramOwnerURL, imageSource: "img.png")         let imageWindow = makeWindow()         imageManager.registerWindow(imageWindow, for: imageIdentifier, ownedBy: outgoingSession.diagramOwnerURL) 
specs/bugfixes/non-file-image-detail-window-collision/report.md Added +38 / -0
diff --git a/specs/bugfixes/non-file-image-detail-window-collision/report.md b/specs/bugfixes/non-file-image-detail-window-collision/report.mdnew file mode 100644index 00000000..27864f46--- /dev/null+++ b/specs/bugfixes/non-file-image-detail-window-collision/report.md@@ -0,0 +1,38 @@+# Bugfix Report: Non-file image detail windows collide across documents (T-1758)++**Ticket**: T-1758+**PR**: #399+**Date**: 2026-08-29++## Description++On macOS, opening an image zoom window from a clipboard, bundled, or remote (URL) document could bring forward an unrelated document's image window instead of opening its own, whenever both documents referenced the same image source string (e.g. `img.png`).++## Root Cause++`ImageDetailIdentifier.documentURL` (`prism/Models/ImageDetailWindowData.swift`) was populated from `MediaZoomPresenter.documentFileURL`, which is only non-nil for `.file` sessions. Every non-file source produced `nil`, so two such documents sharing an image source collapsed to the identical `(nil, imageSource)` identity and deduplicated onto one window.++## Resolution++`ImageDetailIdentifier.documentURL` was replaced by a non-optional `ownerURL`, and `ImageDetailWindowData.identifier` now builds from the existing `ownerURL` field — the same per-document key (file URL, or the synthetic `prism://clipboard/{sessionID}` URL) that window-lifecycle ownership has used since T-1503. `MediaZoomPresenter.openImageWindow` no longer constructs the separate, often-nil `documentURL`.++`DiagramIdentifier`/`DiagramWindowManager` has the same latent shape but was deliberately left untouched to avoid conflicting with T-2014, T-2175 and T-2221.++## Affected Files++- `prism/Models/ImageDetailWindowData.swift`+- `prism/Views/MediaZoomPresenter.swift`+- `prismTests/ImageDetailWindowDataTests.swift` (new regression tests)+- `prismTests/ImageDetailWindowManagerTests.swift`+- `prismTests/DocumentFlowCoordinatorImageWindowTests.swift`+- `prismTests/DocumentFlowCoordinatorReplaceWindowCleanupTests.swift`++## Test Plan++- `ImageDetailWindowDataTests`: two non-file documents with the same image source yield distinct identifiers; the same document yields a stable identifier.+- Targeted suites (`ImageDetailWindowDataTests`, `ImageDetailWindowManagerTests`, `DocumentFlowCoordinatorImageWindowTests`, `DocumentFlowCoordinatorReplaceWindowCleanupTests`, `DiagramWindowDataTests`) pass 31/31; `make build-macos` and `make lint` pass.+- Full `make test-quick` hit the parallel-worktree host-launch cascade twice (other agents running concurrently); the only genuine failures among executed tests (`DocumentLayoutCoordinatorReloadTests`, `URLEncodingCorpusTests`) are unrelated and pass in isolation.++## Prevention++Identity keys for per-document windows must come from a non-optional owner key, never from a source-specific optional such as a file URL.

Things to double-check

Window restoration on relaunch.

Open an image window from a clipboard document, quit, relaunch. The restored payload (if any) must decode; synthesised Codable says yes, but it is the one runtime path no test covers.

Same remote URL in two document windows.

Both share one image window per image — intended and consistent with file sessions, but confirm it is the desired UX.