Skip to content

feat(lists): shared-with-me, contributors, add-watcher and invite landing (G23) - #68

Merged
Adron merged 2 commits into
devfrom
feat/lists-sharing-g23
Sep 13, 2026
Merged

Adron merged 2 commits into
devfrom
feat/lists-sharing-g23

Conversation

@Adron

@Adron Adron commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Builds the five live Lists routes the client never called — together the whole "lists other people gave me access to" story, a surface the macOS sidebar had no equivalent of. Closes #48 (work-consolidation.md G23).

A read-only recon pass (GET/OPTIONS only, zero writes to the shared test account) preceded the work and turned up four silent defects in the already-shipped watchers panel; all four are fixed here, because the issue asks for AddWatcherSheetView to be wired into a panel that could not load in the first place.

Changes

KitLists.watching, Lists.contributors, Lists.addWatcher, Lists.sharedRows (auth .none — the token is the capability), Lists.invite (auth .none). The /watching rows are a strict superset of the owned-list shape, so ListDTO grew optional role / user / parent / folderId / githubRepo* rather than forking a second list DTO; every other list route decodes unchanged.

DomainWatchedList (list + owner + the caller's role), WatchedListsPage, ListOwner, ListContributor, ResolvedListInvite + mappers. ListsService.watching|contributors|addWatcher|watcherCandidates; SharingService.sharedListRows|resolveListInvite. An absent or unrecognised role collapses to .watcher, the least privileged, so an unknown taxonomy can never unlock edit affordances.

App — a role-grouped "Shared with me" section in the Lists sidebar (title + owner + your role, mirroring the web's /lists datagrid), with its own view model so a failing watching call cannot blank the owned lists beside it. AddWatcherSheetView now grants access through the real add-watcher route, picking people via the purpose-built candidate search. A ranked contributors panel. A token-shared list renders its rows on the share landing. An invite landing reachable from interlinedlist://…/invite/{token}.

No changes to MainWindowView or AppEnvironment. The invite landing rides the existing .openShareLink notification — ParsedShare gained a mode with a defaulted init, and ResolveShareView branches internally — so the deep-link plumbing stayed a single routed sheet.

Four silent defects fixed in the existing watcher surface

Route What was wrong Effect
GET …/watchers Answers {watchers:[…],pagination}, builder declared a bare array Sharing panel could never list anyone
GET …/watchers/me Wire key is watching, not isWatching Flag decoded to nil on every call
GET …/watchers/users Is a candidate search returning people, not watcher rows Panel was calling it to populate itself
PUT …/watchers/{userId} WatcherRole.wireToken emitted owner/editor/viewer; API accepts only watcher/collaborator/manager Every role change was a 400

UI labels now follow the web's vocabulary (Read-only / Edit / Admin) on both WatcherRole and ShareRole, per the issue's role table. That changes copy in three already-shipped sharing panels — worth a look during review.

Deliberate limits

  • No native Accept on an invite. POST /api/lists/invite/{token} and POST /api/lists/shared/{token} are x-auth-type: session in the live OpenAPI spec, so a Bearer-only client cannot claim either. The landing ends in "Accept in Browser" rather than a button guaranteed to 401. InviteLandingViewModel.acceptInBrowserURL is the one seam a Bearer-reachable claim route would replace.
  • Entitlements consumed, not extended. Add-watcher and role changes are subscriber-gated server-side; the 403 is projected onto ListsError.subscriberRequired so the UI shows the same upsell Share Links and Invite by Email already use. The client-side pre-flight gate is left to bug(entitlements): subscriber gating covers 3 of ~10 documented features (G37) #40 (CapabilityGate), with a TODO naming it — all 21 ListsService writes route through one permissive canManageLists seam, so tightening it here would have blocked free users from reading their own lists.
  • Owner-only toolbar actions now gate on an owned selection rather than "something is selected", and a read-only share hides the row-write affordances instead of letting them 403.

Left unfixed, recorded instead

GET /api/lists/{id} answers the {"data":{…}} envelope while Lists.get(id:) decodes a bare ListDTO, so ListsService.detail(listId:) cannot decode a live response. No G23 path exercises it (the shared-with-me rows come from the watching payload itself). Noted under G23 in work-consolidation.md; probably deserves its own issue.

Verification

Run on the merged tree (this branch merged with origin/dev @ f00971d), with CODE_SIGNING_ALLOWED=NO -derivedDataPath DerivedData:

  • xcodebuild -scheme InterlinedList -destination 'platform=macOS' build** BUILD SUCCEEDED **
  • xcodebuild -scheme InterlinedList -destination 'platform=macOS' testExecuted 802 tests, with 0 failures (0 unexpected)** TEST SUCCEEDED **
  • swift test --package-path Packages/InterlinedKitExecuted 418 tests, with 0 failures (0 unexpected) (was 398)
  • swift test --package-path Packages/InterlinedDomainExecuted 803 tests, with 0 failures (0 unexpected) (was 772)
  • swift test --package-path Packages/InterlinedPersistenceExecuted 135 tests, with 0 failures (0 unexpected)
  • grep -rn "^import InterlinedKit" App/Features App/Navigation App/MenuCommandszero hits (Decision 0003)
  • Kit ContractTests ran live against the .env account: Executed 4 tests, with 0 failures

BDD quartets ship for every new behaviour across all three layers: ListsSharingEndpointTests (Kit), ListsSharingServiceTests (Domain), and WatchedListsViewModelTests / ContributorsViewModelTests / InviteLandingViewModelTests plus extensions to WatchersViewModelTests, ResolveShareViewModelTests and ShareURLParserTests (App).

Not covered by tests: the sidebar's visual grouping and the read-only rows pane need a hand-check in the running app.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QgRs8R6CcWHx8EBGP5cz7g

Adron and others added 2 commits September 9, 2026 10:08
…ding (G23)

Builds the five live Lists routes the client never called, which together are
the whole "lists other people gave me access to" story — a surface the macOS
sidebar had no equivalent of (GitHub issue #48, work-consolidation.md G23).

Kit
- `Lists.watching` (`GET /api/lists/watching`), `Lists.contributors`,
  `Lists.addWatcher` (`POST …/watchers`), `Lists.sharedRows`
  (`GET /api/lists/shared/{token}/data`, no auth — the token is the
  capability) and `Lists.invite` (`GET /api/lists/invite/{token}`, no auth).
- The watching rows are a superset of the owned-list shape, so `ListDTO` grew
  optional `role` / `user` / `parent` / `folderId` / `githubRepo*` rather than
  forking a second list DTO; every other route decodes unchanged.

Domain
- `WatchedList` (list + owner + the caller's role), `WatchedListsPage`,
  `ListOwner`, `ListContributor`, `ResolvedListInvite` + mappers. An absent or
  unrecognised role collapses to `.watcher`, the least privileged, so an
  unknown taxonomy can never unlock edit affordances.
- `ListsService.watching` / `contributors` / `addWatcher` / `watcherCandidates`;
  `SharingService.sharedListRows` / `resolveListInvite`.

App
- A "Shared with me" section in the Lists sidebar, grouped by the caller's
  role and showing title + owner, mirroring the web's /lists datagrid. Its own
  view model, so a failing `watching` call cannot blank the owned lists.
- `AddWatcherSheetView` now grants access through the real add-watcher route,
  picking people via the purpose-built candidate search.
- A contributors panel on the list toolbar.
- A token-shared list renders its rows on the share landing.
- An invite landing reachable from `interlinedlist://…/invite/{token}`, routed
  through the existing share-link sheet so MainWindowView is untouched.

Deliberate limits
- No native Accept on an invite: `POST /api/lists/invite/{token}` and
  `POST /api/lists/shared/{token}` are `x-auth-type: session` in the live
  spec, so a Bearer client cannot claim either. The landing ends in "Accept in
  Browser" rather than a button that would 401 every time.
- Add-watcher and role changes are subscriber-gated server-side; the 403 is
  projected onto `ListsError.subscriberRequired` so the UI shows the same
  upsell Share Links and Invite by Email already use. The client-side
  pre-flight gate is left to #40 (`CapabilityGate`) — all 21 ListsService
  writes route through one permissive seam, so tightening it here would have
  blocked free users from reading their own lists.
- Owner-only toolbar actions now gate on an *owned* selection, and a
  read-only share hides the row-write affordances instead of 403-ing.

Four defects the read-only recon pass found in the existing watcher surface,
all silent, all fixed
- `GET …/watchers` answers `{watchers:[…]}`, not a bare array — the sharing
  panel could never list anyone.
- `GET …/watchers/me` answers `watching`, not `isWatching` — the flag decoded
  to nil on every call.
- `GET …/watchers/users` is a candidate search returning people, not watcher
  rows; the panel was calling it to populate itself.
- `WatcherRole.wireToken` emitted owner/editor/viewer, none of which the API
  accepts, so every role change was a 400. Now watcher/collaborator/manager,
  with UI labels (Read-only / Edit / Admin) aligned across `WatcherRole` and
  `ShareRole` to match the web.

Still open, out of scope: `GET /api/lists/{id}` answers `{data:{…}}` while
`Lists.get` decodes a bare `ListDTO`. No G23 path exercises it; recorded in
work-consolidation.md.

Verification (all observed, CODE_SIGNING_ALLOWED=NO -derivedDataPath DerivedData)
- xcodebuild build → ** BUILD SUCCEEDED **
- xcodebuild test → 794 tests, 0 failures (** TEST SUCCEEDED **)
- swift test InterlinedKit → 418 tests, 0 failures (was 398)
- swift test InterlinedDomain → 803 tests, 0 failures (was 772)
- swift test InterlinedPersistence → 135 tests, 0 failures
- grep -rn "^import InterlinedKit" App/{Features,Navigation,MenuCommands} → 0 hits
- Kit ContractTests ran live against the .env account: 4 tests, 0 failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QgRs8R6CcWHx8EBGP5cz7g
@Adron
Adron merged commit 5d6fd79 into dev Sep 13, 2026
8 checks passed
@Adron
Adron deleted the feat/lists-sharing-g23 branch September 13, 2026 18:24
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