fix image stale-render on in-place overwrite (T-312)

Image cards, thumbnails, the lightbox, and `clide image show` rendered
via Image.file, whose FileImage keys Flutter's imageCache by (path,
scale) only — so overwriting a file at the same path handed back the
previously decoded frame (hit live re-exporting a wireframe PNG). Add
ClideFileImage, a FileImage that folds mtime + size into ==/hashCode so an
in-place change is a fresh cache key (miss → re-decode), and route the
five Image.file sites through it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:25:58 +02:00
co-authored by Claude Opus 4.8
parent 1194527ad5
commit a879238102
9 changed files with 103 additions and 16 deletions
@@ -3118,3 +3118,4 @@ Both panes use the existing `MultitabPane` (T-83) for tab management once they s
MERGED (2026-06-10) into the unified canvas epic T-317 (decision D-91). The canvas rendering work consolidates onto one clide-owned canvas renderer driven by JSON; .canvas becomes an import format converted into that JSON rather than a native schema. T-7''s scope is preserved as children of T-317: canvas pane -> T-322, graph view -> T-323. This epic is cancelled as superseded; track the work under T-317.', NULL, '2026-06-10 11:17:31', '2026-06-10 11:17:31', '2026-06-10 11:17:31', NULL, 'e7d70c5b631bd6109f1b83b55c857c20', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67JSC5RKS6M9182KG', 'parent_id', 'T-7', 'T-276', NULL, '2026-06-10 11:18:12', '2026-06-10 11:18:12', '2026-06-10 11:18:12', NULL, 'df23887d88f2a45e8ad12a72a4b27407', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7162Z26FDWXG35VH0', 'status', 'backlog', 'cancelled', NULL, '2026-06-10 11:18:15', '2026-06-10 11:18:15', '2026-06-10 11:18:15', NULL, '64906dcc15f8f1b32034640a7e9aab82', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'status', 'ready', 'done', NULL, '2026-06-10 11:25:58', '2026-06-10 11:25:58', '2026-06-10 11:25:58', NULL, '678a32ea96db32f51581309dc2af1b6d', 2) ON CONFLICT(hash) DO NOTHING;
+11
View File
@@ -2057,3 +2057,14 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
Both panes use the existing `MultitabPane` (T-83) for tab management once they ship. Each lives in its own slot per D-47 (canvas in workspace, graph in context panel).
MERGED (2026-06-10) into the unified canvas epic T-317 (decision D-91). The canvas rendering work consolidates onto one clide-owned canvas renderer driven by JSON; .canvas becomes an import format converted into that JSON rather than a native schema. T-7''s scope is preserved as children of T-317: canvas pane -> T-322, graph view -> T-323. This epic is cancelled as superseded; track the work under T-317.', 'cancelled', 'medium', NULL, NULL, 'D-17', '2026-04-22 11:45:41', '2026-06-10 11:18:15', NULL, '3e558a94afc3e675e673c6e9f9390855', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Image card caches by path — overwritten file shows stale render', 'clide image show (and the conversation image viewer card) appears to cache by file path: re-showing a file after its bytes change at the SAME path renders the previous image, not the new one. Hit live while iterating on a Frame0 wireframe (re-exported settings-modal.png in place; the pane kept showing the prior version until exported under a new filename).
Expected: showing a path always reflects the current file bytes.
Explore / options:
- Cache-key on mtime + size (or a content hash) instead of path alone; invalidate when they change.
- A force-refresh path for clide image show (re-decode, bypass cache).
- Check where the cache lives: the image card widget (Image.file / ImageProvider cache — Flutter''s imageCache keys by path+scale and won''t refresh on overwrite), the IPC image.show handler, or the pane render layer.
- Likely Flutter imageCache: evict the key (or use a FileImage with a key that includes mtime) before showing.
Note: surfaced via clide image show (T-249 / T-252 lightbox share the same provider). Repro: export an image, show it, overwrite the same path with different content, show again -> stale.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 10:04:53', '2026-06-10 11:25:58', NULL, 'e0db833047273042eaa96ba6b54bed8d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);