feat(lists): shared-with-me, contributors, add-watcher and invite landing (G23) - #68
Merged
Merged
Conversation
…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
This was referenced Sep 13, 2026
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.
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/OPTIONSonly, 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 forAddWatcherSheetViewto be wired into a panel that could not load in the first place.Changes
Kit —
Lists.watching,Lists.contributors,Lists.addWatcher,Lists.sharedRows(auth.none— the token is the capability),Lists.invite(auth.none). The/watchingrows are a strict superset of the owned-list shape, soListDTOgrew optionalrole/user/parent/folderId/githubRepo*rather than forking a second list DTO; every other list route decodes unchanged.Domain —
WatchedList(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
/listsdatagrid), with its own view model so a failingwatchingcall cannot blank the owned lists beside it.AddWatcherSheetViewnow 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 frominterlinedlist://…/invite/{token}.No changes to
MainWindowVieworAppEnvironment. The invite landing rides the existing.openShareLinknotification —ParsedSharegained amodewith a defaulted init, andResolveShareViewbranches internally — so the deep-link plumbing stayed a single routed sheet.Four silent defects fixed in the existing watcher surface
GET …/watchers{watchers:[…],pagination}, builder declared a bare arrayGET …/watchers/mewatching, notisWatchingnilon every callGET …/watchers/usersPUT …/watchers/{userId}WatcherRole.wireTokenemittedowner/editor/viewer; API accepts onlywatcher/collaborator/managerUI labels now follow the web's vocabulary (Read-only / Edit / Admin) on both
WatcherRoleandShareRole, per the issue's role table. That changes copy in three already-shipped sharing panels — worth a look during review.Deliberate limits
POST /api/lists/invite/{token}andPOST /api/lists/shared/{token}arex-auth-type: sessionin 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.acceptInBrowserURLis the one seam a Bearer-reachable claim route would replace.ListsError.subscriberRequiredso 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 21ListsServicewrites route through one permissivecanManageListsseam, so tightening it here would have blocked free users from reading their own lists.Left unfixed, recorded instead
GET /api/lists/{id}answers the{"data":{…}}envelope whileLists.get(id:)decodes a bareListDTO, soListsService.detail(listId:)cannot decode a live response. No G23 path exercises it (the shared-with-me rows come from thewatchingpayload itself). Noted under G23 inwork-consolidation.md; probably deserves its own issue.Verification
Run on the merged tree (this branch merged with
origin/dev@f00971d), withCODE_SIGNING_ALLOWED=NO -derivedDataPath DerivedData:xcodebuild -scheme InterlinedList -destination 'platform=macOS' build→** BUILD SUCCEEDED **xcodebuild -scheme InterlinedList -destination 'platform=macOS' test→Executed 802 tests, with 0 failures (0 unexpected)—** TEST SUCCEEDED **swift test --package-path Packages/InterlinedKit→Executed 418 tests, with 0 failures (0 unexpected)(was 398)swift test --package-path Packages/InterlinedDomain→Executed 803 tests, with 0 failures (0 unexpected)(was 772)swift test --package-path Packages/InterlinedPersistence→Executed 135 tests, with 0 failures (0 unexpected)grep -rn "^import InterlinedKit" App/Features App/Navigation App/MenuCommands→ zero hits (Decision 0003)ContractTestsran live against the.envaccount:Executed 4 tests, with 0 failuresBDD quartets ship for every new behaviour across all three layers:
ListsSharingEndpointTests(Kit),ListsSharingServiceTests(Domain), andWatchedListsViewModelTests/ContributorsViewModelTests/InviteLandingViewModelTestsplus extensions toWatchersViewModelTests,ResolveShareViewModelTestsandShareURLParserTests(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