feat(artifacts): render a shared conversation's artifacts for its recipient - #973
Merged
Merged
Conversation
…ipient PR-2 of the pair. #971 made the artifacts reachable; this makes them visible. A recipient now sees artifact cards anchored under the same turns the owner sees them under, and opens them read-only. ## A separate recipient component, not a mode of the owner's `ArtifactCardComponent` opens the docked panel and carries download, share, rename and delete; `ArtifactPanelComponent` adds a version picker and a code view. Every one of those is keyed on something a conversation-share recipient does not have — an owned artifact, or an artifact share id. Bending either into a "read-only mode" would mean a component whose every action is conditional on a flag, and the failure mode of a missed condition is a visible button that 403s. So the card and dialog are their own thing. What IS shared is the layer that should be: `ArtifactViewerComponent`, which is purely presentational and already served the owner panel and the standalone recipient page through two mint endpoints. This is the third, and it needed no change to that component — which is the sign the split was drawn in the right place. ## Artifacts arrive as an input, not through the state service `MessageListComponent` reads artifacts from `ArtifactStateService`, which is the OWNER's live session state: populated by SSE events and owner-scoped hydration, neither of which a recipient has. Feeding it recipient rows would put another user's artifacts into the signal the real session view reads. So the shared view passes them down, and the list groups them with the same index-anchoring logic — including the orphan fallback, so an artifact with no usable anchor lands in the end strip rather than disappearing, which is the exact failure this whole feature exists to fix. ## No code view The source endpoint is keyed on an artifact share id, which a conversation share does not have. The toggle is therefore absent rather than present and permanently failing. Adding it is a backend change, not a UI one. A refused artifact and a missing one get one message: "not part of this share" and "you may not open this share" are the same fact to a recipient, and telling them apart would describe what the owner has. SPA suite: 2367 passed (+26). 20 new tests: card, dialog (including the sandbox isolation the third mint path must not weaken, and the superseded-mint race), and the message-list anchoring — which had no component spec at all before, so the orphan-fallback branch the owner path also relies on is now covered. Verified in the browser against the compiled stylesheet at 1080px in both themes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR-2 of the pair. #971 made the artifacts reachable; this makes them visible. A recipient now sees artifact cards anchored under the same turns the owner sees them under, and opens them read-only.
Frontend only. Depends on #971 for the endpoint — until that merges, the payload carries no
artifactsand this renders nothing, which is the current behaviour.A separate recipient component, not a mode of the owner's
ArtifactCardComponentopens the docked panel and carries download, share, rename and delete.ArtifactPanelComponentadds a version picker and a code view. Every one of those is keyed on something a conversation-share recipient does not have — an owned artifact, or an artifact share id. Bending either into a "read-only mode" means a component whose every action is conditional on a flag, and the failure mode of a missed condition is a visible button that 403s.So the card and dialog are their own components. What is shared is the layer that should be:
ArtifactViewerComponent, which is purely presentational and already served the owner panel and the standalone recipient page through two different mint endpoints. This is the third, and it needed no change to that component — which is the sign the split was drawn in the right place.Artifacts arrive as an input, not through the state service
MessageListComponentreads artifacts fromArtifactStateService— the owner's live session state, populated by SSE events and owner-scoped hydration, neither of which a recipient has. Feeding it recipient rows would put another user's artifacts into the signal the real session view reads.So the shared view passes them down, and the list groups them with the same index-anchoring the owner path uses, including the orphan fallback: an artifact with no usable anchor lands in the end strip rather than disappearing. That matters here more than anywhere, since silently disappearing artifacts is the exact failure this whole feature exists to fix.
No code view, deliberately
The source endpoint is keyed on an artifact share id, which a conversation share doesn't have. The toggle is absent rather than present and permanently failing — adding it is a backend change, not a UI one. I built it with the toggle first, then removed it rather than ship a control that can only ever error.
A refused artifact and a missing one get one message: "not part of this share" and "you may not open this share" are the same fact to a recipient, and distinguishing them would describe what the owner has.
Tests
2367 passed (+26). 20 new: the card, the dialog (including the sandbox isolation this third mint path must not weaken, and the superseded-mint race), and the message-list anchoring.
That last one is worth calling out —
MessageListComponenthad no component spec at all, so the orphan-fallback branch the owner path also depends on was untested until now. It's covered for both.A verification correction worth recording
Browser-verified at 1080px in both themes — but getting light mode right took two attempts. This app drives dark mode with a
.darkclass on<html>and honoursprefers-color-scheme. The Browser pane'scolorSchemeemulation only moves the second, so with the class still set the "light" screenshot was the app still fully in dark mode. Cards looked broken; they weren't.Correct check is both levers: clear the class and emulate light. Once done, cards are white with dark text as intended (verified against computed styles, not just the screenshot).
🤖 Generated with Claude Code