Skip to content

feat(artifacts): All / Yours / Shared with you tabs on the library - #970

Merged
philmerrell merged 1 commit into
developfrom
feature/artifact-shared-with-you-tabs
Sep 5, 2026
Merged

philmerrell merged 1 commit into
developfrom
feature/artifact-shared-with-you-tabs

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

PR-2 of the share-inbox pair. #968 gave recipients somewhere to be listed; this is the surface that lists them. Frontend only.

Still dark: with ARTIFACT_SHARE_INBOX_ENABLED off the endpoint 404s, no tabs render, and the library is exactly what it is today.

One row shape, two records

A received artifact is not a LibraryArtifact with extra fields. It has no artifact id and no session to go back to — the share id is the only handle a recipient has on it — and no updatedAt that means anything to them, because that's the owner's clock. Modelling both as one type would invite a template to reach for a field that's structurally absent on half its rows.

So both lists normalize into a LibraryRow carrying a kind discriminator, and the template branches on that and nothing else:

Owned Received
Opens /artifacts/{id} /shared-artifact/{shareId}
Timestamp "Updated" (owner's clock) "Shared" (when you got it)
Actions Open, Conversation, Rename, Delete Open
Attribution — "Shared by ada@…"

ArtifactThumbnailComponent gains the same discrimination. A recipient can't mint against an artifact id — the owner endpoint builds its DynamoDB key from the session and would 404 — so the source says which credential it is rather than the component inferring it from which fields are populated. Two mint paths, one component, mirroring how ArtifactViewerComponent already serves the owner panel and the recipient page.

The flag reaches the SPA with no new plumbing

listSharedWithMe returns null on 404 — the backend saying the inbox doesn't exist here. Null hides the tabs; [] is a real, empty inbox and says "nothing yet". Collapsing those would either show a permanently empty tab wherever the feature is off, or hide a genuine empty state. Anything that isn't a 404 rethrows: "we couldn't load your inbox" is not "you don't have one".

The two lists load with allSettled, not all. Your own library is the page's reason to exist, so an inbox that 503s must not blank it — the tabs just don't appear, same as when the feature is off. The reverse isn't true, and isn't treated as such.

Smaller calls

  • "All" interleaves both lists newest-first. The one place this page sorts, and a deliberate exception to its own no-re-sorting rule: two independently server-ordered lists can't be shown as one without merging on the client. The single-list tabs don't sort at all.
  • Search matches the sender on a received row — "who sent me that thing" is at least as likely a starting point as remembering its title.
  • "Load more" is inbox-only (the owned endpoint is unpaginated) and stays visible on the All tab, since the merged list is only as complete as what's loaded and hiding the control there would present a partial merge as the whole thing.
  • Row keys are prefixed by provenance — an artifact id and a share id come from different key spaces and could otherwise collide into shared DOM.

A contrast defect caught by verifying

I first built the tabs as an underline in primary-accessible. That's a fixed brand colour with no dark variant, so a 2px #0033A0 rule on the #101828 dark surface is nearly invisible — and it passed every unit test. Switched to the filled pill that model-catalog.page.html and this page's own view toggle already use: 10.6:1 on the active pill, and one fewer idiom in the app.

Browser-verified against the compiled stylesheet at 1080px in both themes — tabs, received-card variant, no horizontal overflow.

Tests

2341 passed, +17 from this change (12 tab-behaviour, 3 service contract, 2 mint-path). tsc --noEmit clean.

The one failure in a full run — shared-view.page.spec.ts, a parallel-load timeout its own comment already documents — reproduces on clean develop with these changes stashed, so it's pre-existing and unrelated.

To turn it on

Set CDK_ARTIFACT_SHARE_INBOX_ENABLED=true for the environment and deploy platform.yml. Existing dev shares predate the fan-out and won't appear until re-created; prod has no shares at all, so it's correct from its first one.

🤖 Generated with Claude Code

PR-2 of the share-inbox pair. #968 gave recipients somewhere to be
listed; this is the surface that lists them.

## One row shape, two records

A received artifact is not a `LibraryArtifact` with extra fields. It has
no artifact id and no session to go back to — the share id is the only
handle a recipient has on it — and no `updatedAt` that means anything to
them, because that is the owner's clock. Modelling both as one type
would invite a template to reach for a field that is structurally absent
on half its rows.

So both lists are normalized into a `LibraryRow` with a `kind`
discriminator, and the template branches on that and nothing else. Owner
actions (rename, delete, jump to conversation) render only for `owned`;
a received card opens at /shared-artifact/{shareId} and stops there,
which is what the recipient endpoints enforce server-side anyway.

The thumbnail gains the same discrimination. A recipient cannot mint
against an artifact id — the owner endpoint builds its key from the
session and would 404 — so the source says which credential it is rather
than the component guessing from which fields are populated.

## The flag reaches the SPA without any new plumbing

`listSharedWithMe` returns null on 404, which is the backend saying the
inbox does not exist in this environment. Null hides the tabs entirely;
`[]` is a real, empty inbox and says "nothing yet". Collapsing the two
would either show a permanently empty tab wherever the feature is off or
hide a genuine empty state. Anything that is not a 404 rethrows — "we
could not load your inbox" is not "you do not have one".

The two lists load with `allSettled`, not `all`: your own library is the
page's reason to exist, so an inbox that 503s must not blank it. The
tabs simply do not appear, exactly as when the feature is off.

## Notes

"All" interleaves both lists newest-first. That is the one place this
page sorts, and it is a deliberate exception to its own no-re-sorting
rule: two independently server-ordered lists cannot be shown as one
without merging them on the client. The single-list tabs do not sort.

Search matches the sender on a received row — "who sent me that thing"
is at least as likely a starting point as remembering its title.

"Load more" is inbox-only (the owned library endpoint is unpaginated)
and stays visible on the All tab, because the merged list is only as
complete as what has been loaded and hiding the control there would
present a partial merge as the whole thing.

## Fixes a contrast defect found while verifying

The tabs were first built as an underline in `primary-accessible`, which
is a fixed brand colour with no dark variant: a 2px #0033A0 rule on the
#101828 dark surface is close to invisible. Switched to the filled pill
the model-catalog tabs and this page's own view toggle already use —
10.6:1 on the active pill, and one fewer idiom in the app.

Verified in the browser against the compiled stylesheet at 1080px in
both themes: tabs, received-card variant, and no horizontal overflow.

SPA suite: 2341 passed, +17 from this change. The one failure in a full
run (`shared-view.page.spec.ts`, a parallel-load timeout its own comment
documents) reproduces on clean develop with these changes stashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 2b2f918 into develop Sep 5, 2026
4 checks passed
@philmerrell
philmerrell deleted the feature/artifact-shared-with-you-tabs branch September 5, 2026 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant