Skip to content

feat(agents-mobile): session sharing, access management & copy/share links (desktop parity)#4564

Open
msfstef wants to merge 2 commits into
mainfrom
msfstef/share-session-mobile-parity
Open

feat(agents-mobile): session sharing, access management & copy/share links (desktop parity)#4564
msfstef wants to merge 2 commits into
mainfrom
msfstef/share-session-mobile-parity

Conversation

@msfstef

@msfstef msfstef commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings session sharing to agents-mobile, closing the parity gap with the desktop ShareEntityDialog while using mobile-native UX paradigms. Mobile previously only consumed effective permissions to gate actions; it can now manage who has access to a session, copy the session id, and share a browser-openable session link.

Part of the agents-mobile ↔ agents-desktop parity track (follows #4553, #4546).

What's included

1. Share session screen (ShareSessionScreen + app/session-share.tsx modal route)

Opened from the session kebab menu's Share entry. Single-column layout, top to bottom:

  • Session title + copyable id (tap id → copy, copy→check icon swap).
  • Session link pill — abbreviated web URL (scheme stripped, middle-ellipsized) with a share glyph; one tap opens the native OS share sheet (which includes Copy, so there is no separate in-app "Copy link").
  • People with access — pinned, non-removable Owner row derived from entity.created_by (omitted when null), then one row per grantee (avatar/initials, name/email, role pill). Desktop doesn't show the owner; mobile adds it so the list is never confusingly empty.
  • General access — the workspace-wide All users grant (subject_kind: principal_kind / user) as its own section (Google-Drive style) rather than mixed into the people list like desktop.
  • Add people — search-first user list (synced users table), excluding self/owner/existing grantees.
  • Role picker — tapping any row opens a bottom sheet with View / Chat / Manage (same permission sets and eye/message/shield glyphs as desktop) plus a destructive Remove access (Alert-confirmed). Roles commit immediately per row — no desktop-style deferred Grant/Update button.

2. Copy session id

The session menu's status header now shows the id (in place of the agent type label) with a tap-to-copy affordance; same treatment on the long-press row sheet's id line. Mirrors the desktop EntityHeader copy-id pattern (1.2s icon swap, via the new useCopyFeedback hook + expo-clipboard).

3. Session web links

sessionWebUrl(serverUrl, entityUrl) builds {serverUrl}/__agent_ui/#/entity/{id}.

Key decisions & why

Decision Rationale
Grants come from REST GET /_electric/entities/:type/:id/grants, not sync The synced entity_effective_permissions shape is scoped to the current principal only (server-utils.ts buildCurrentPrincipalEntityEffectivePermissionsWhere), so it cannot list other people's access. Same approach as desktop's loadGrants(). Refetch-after-mutation (no optimistic grant state) because the server owns grant ids needed for subsequent diffs.
Web URL targets /__agent_ui/ directly, not the server root The root 302 redirect uses an absolute location: /__agent_ui/, which would drop a Cloud /t/<service-id>/v1 tenant prefix. The session id stays un-encoded to match the web UI's hash splat route (/entity/$).
Share screen reachable by everyone (no manage gate on the menu entry) Link sharing must work for non-managers. The grant sections self-gate: the manage-protected endpoint returns 401/403 → the screen shows a manage-required message (this also gracefully handles revoking your own manage access mid-session).
Single share affordance (link pill → native sheet), no in-app Copy link The native sheet includes Copy; two buttons for the same URL was duplicative. The pill shows the URL being shared (Meet/Zoom pattern). In-app clipboard copy remains only for the session id.
One Share menu item next to Pin Earlier iteration had Copy id / Copy link / Share… / Share & access — collapsed after review feedback.
userDisplay()/initials() extracted to agents-server-ui/src/lib/userDisplay.ts Mobile deep-imports shared lib code (existing pattern: principals, sharePermissions, entity-api, auth-fetch); extraction avoids drift vs. duplicating. Internal lib file only — no public API change, desktop dialog updated to import it.
Inbound links out of scope Universal links (https → app) need associated-domain setup; planned follow-up. The web URL format was chosen so those links can later deep-link into the app once a domain is verified.

Architecture notes (for future sessions on this PR)

  • src/lib/entityGrants.ts — all grant logic, pure where possible: REST wrappers (listEntityGrants/createEntityGrant/deleteEntityGrant, errors throw GrantsRequestError carrying status), diffGrantsForRole (set-based create/delete diff, ignores non-share custom permissions, handles duplicate rows), grantIdsForRemoval, buildShareAccessModel (groups grants → all-users + per-user entries with roles, drops current user / system principals / role-less subjects), setSubjectRole / removeSubjectAccess orchestration. Unit-tested with the repo's hoisted serverFetch mock pattern.
  • src/lib/sessionLinks.tssessionIdFromEntityUrl, sessionWebUrl (pure, unit-tested incl. Cloud prefix + malformed-URL fallback).
  • Role semantics live in shared agents-server-ui/src/lib/sharePermissions.ts: View=[read,fork], Chat=[read,write,signal,fork,schedule,spawn], Manage=[manage,delete]; roleFromGrants / rolePermissionsMatchGrants reused.
  • SessionMenu takes an optional onShare prop threaded from app/session.tsxSessionScreen (router push to /session-share?entityUrl=…).
  • BottomSheetItem always reserves a 22px icon slot — give items icons or they look indented (why the role options carry glyphs).
  • Search fields must mirror SearchBar metrics (fixed 36px height, zero-padding input) or the placeholder sits off-centre on Android.

Testing

  • ✅ 22 new vitest tests (sessionLinks.test.ts, entityGrants.test.ts) — written test-first; full mobile suite 83/83, agents-server-ui 88/88, both packages typecheck clean.
  • ⬜ Manual QA needed (see below).

Manual QA checklist

  • Rebuild dev clientsexpo-clipboard is a new native module; old dev binaries won't load this JS (the one build-impacting change).
  • Open a copied/shared link in a browser against a local server and a Cloud server (verifies /__agent_ui/#/entity/{id} resolves under the /t/<svc>/v1 prefix — the one assumption not verifiable statically).
  • iOS + Android: copy id from menu + long-press sheet; link pill opens native sheet; cancel is silent.
  • Manager flow: add user → change role View↔Chat↔Manage → remove → All-users grant add/change/remove; cross-check grants in the desktop ShareEntityDialog.
  • Non-manager: share screen shows link actions + manage-required message; no crash.
  • Self-revocation: remove the All-users Manage grant that granted you manage → screen degrades to access-denied state.

Out of scope / follow-ups

  • Universal links / inbound https session links opening the app (needs associated domains + per-deployment config).
  • Haptic feedback on copy (would add expo-haptics).
  • Sharing for non-session entity types (no mobile detail surfaces exist for them yet).

🤖 Generated with Claude Code

…links (desktop parity)

Ports the desktop ShareEntityDialog feature set to mobile with
mobile-native UX: a Share session modal (link pill -> native share
sheet, people-with-access list, General access section, search-first
add people, per-row role commits via bottom-sheet picker), tap-to-copy
session ids in the session menu and long-press sheet, and a
sessionWebUrl() helper that targets /__agent_ui/ directly so Cloud
tenant prefixes survive. Grant CRUD goes through the manage-protected
REST endpoints since the synced effective-permissions shape is
self-scoped. Extracts userDisplay/initials from the desktop dialog
into agents-server-ui lib for reuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@msfstef msfstef added the claude label Jun 11, 2026
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Desktop Builds

Build artifacts for commit 017f8ce.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.39216% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.46%. Comparing base (683cfae) to head (017f8ce).
⚠️ Report is 7 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/agents-server-ui/src/lib/userDisplay.ts 0.00% 15 Missing ⚠️
packages/agents-mobile/src/lib/entityGrants.ts 93.42% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4564      +/-   ##
==========================================
+ Coverage   58.06%   58.46%   +0.39%     
==========================================
  Files         369      373       +4     
  Lines       40459    40735     +276     
  Branches    11468    11567      +99     
==========================================
+ Hits        23494    23816     +322     
+ Misses      16890    16845      -45     
+ Partials       75       74       -1     
Flag Coverage Δ
packages/agents 71.37% <ø> (ø)
packages/agents-mcp 77.54% <ø> (ø)
packages/agents-mobile 78.81% <94.25%> (+3.32%) ⬆️
packages/agents-runtime 82.49% <ø> (+0.36%) ⬆️
packages/agents-server 75.06% <ø> (+0.29%) ⬆️
packages/agents-server-ui 6.25% <0.00%> (+<0.01%) ⬆️
packages/electric-ax 46.42% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.83% <ø> (+0.11%) ⬆️
packages/y-electric 56.05% <ø> (ø)
typescript 58.46% <80.39%> (+0.39%) ⬆️
unit-tests 58.46% <80.39%> (+0.39%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Mobile Build

Local mobile checks ran for commit 017f8ce.

The EAS Android preview build was skipped because the mobile-eas-build label is not present.
Add the mobile-eas-build label to this PR to produce an installable preview build.

Workflow run

@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown

Claude Code Review

Summary

Brings session sharing / access management to agents-mobile for desktop ShareEntityDialog parity, plus tap-to-copy session ids and a browser-openable session link. TypeScript/React-Native-only — no sync-service or HTTP-contract changes. The follow-up commit (017f8cec2) addresses the previous review's findings; the PR is in good shape.

What's Working Well

  • Test-first, pure-core design. entityGrants.ts and sessionLinks.ts keep diffing/grouping/URL logic side-effect-free and cover the tricky cases (disjoint role swaps, duplicate-permission rows, non-share custom permissions, Cloud /t/<svc>/v1 prefix, malformed-URL fallback).
  • Genuine de-duplication, not copy-paste. userDisplay/userSearchText live in one shared module; both clients import it.
  • Refetch-as-source-of-truth. Re-loading grants after every mutation keeps the UI consistent with server-owned grant ids and reconciles partial Promise.all failures.
  • Clean response to review. The follow-up commit fixes the substantive issues rather than papering over them, and adds a regression test.

Issues Found

Critical (Must Fix)

None.

Important (Should Fix)

None.

Suggestions (Nice to Have)

1. loadGrants success path still doesn't clear a stale error (carry-over from iteration 1, partially addressed)

File: packages/agents-mobile/src/screens/ShareSessionScreen.tsx:145-164

The follow-up correctly added setError(null) to the 401/403 branch (:158), so the access-denied transition now self-heals. But the success branch (:147-149) sets grants/accessDenied without clearing error. So a mutation that fails (sets error) and then triggers a loadGrants that succeeds in finally will refresh the data while leaving the stale error banner up until the next mutation calls setError(null). Clearing error alongside the success update would close this. Very minor — interactive flows always setError(null) before the next mutation.

const loaded = await listEntityGrants({ baseUrl: serverUrl, entityUrl })
setGrants(loaded)
setAccessDenied(false)
setError(null) // make the success path self-healing too

Issue Conformance

No linked issue (informational — the description references the parity track #4553/#4546 and is unusually thorough with an explicit manual-QA checklist). Changeset present, scoping both touched packages. The only assumption not statically verifiable (link resolves under a Cloud /t/<svc>/v1 prefix) is correctly called out for manual QA.

Previous Review Status

All three iteration-1 items resolved or down to a trace:

  • Owner double-renderaccessRows now filters out ownerUserId (:181); owner with an explicit grant renders only in the pinned Owner row.
  • Duplicate grant rows on re-add — role-less partial grants (e.g. a lone fork) are now exposed via ShareAccessModel.grantsByUserId and used as the diff base when re-granting, so no duplicate rows; covered by a new test. applyRole also switched to the exact rolePermissionsMatchGrants check (desktop Update parity) and both mutation paths now guard on savingKey !== null against cross-row clobbering.
  • 🟡 Stale error banner — access-denied case fixed; success-path clearing still open (Suggestion 1 above).

Verified the supporting refactors are safe: initials un-export is fine (only internal callers; the .initials references are struct fields), and ElectricUser (id/display_name/email) satisfies the new userSearchText(user: UserDisplayInfo & { id: string }) signature at both call sites.


Review iteration: 2 | 2026-06-11

- Diff re-added users against their actual grants (exposed via
  ShareAccessModel.grantsByUserId) so granting a role to a user with a
  partial, role-less grant set no longer creates duplicate grant rows.
- Use the exact rolePermissionsMatchGrants check in applyRole instead of
  the coarse role compare, so re-selecting a role normalizes partial
  grant sets (desktop Update parity).
- Guard applyRole/removeAccess while a save is in flight to prevent
  cross-row savingKey clobbering and racing grant refetches.
- Filter the owner out of the access rows (already pinned as a
  dedicated row) and reword the all-users remove confirmation.
- Dedupe userSearchText into agents-server-ui's userDisplay lib,
  un-export the internal initials helper, and trim unused optional
  fields from the mobile grant type.
- Minor polish: hitSlop consistency on the copy-id pill, accessibility
  label on the share-link pill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@msfstef

msfstef commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author
Simulator.Screen.Recording.-.iPhone.15.Pro.-.2026-06-11.at.16.44.01.mov

@msfstef msfstef marked this pull request as ready for review June 11, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant