Skip to content

feat(agents-mobile): pin sessions via long-press context menu#4507

Open
msfstef wants to merge 3 commits into
mainfrom
worktree-pin-entity-in-mobile
Open

feat(agents-mobile): pin sessions via long-press context menu#4507
msfstef wants to merge 3 commits into
mainfrom
worktree-pin-entity-in-mobile

Conversation

@msfstef
Copy link
Copy Markdown
Contributor

@msfstef msfstef commented Jun 4, 2026

Intent

The web sidebar lets users pin sessions so frequently-used ones stay at the top regardless of grouping; mobile had no equivalent, so important sessions sink into the date buckets. This ports the pinning feature to the mobile app, adapted for touch — and folds the web's row hover info card into the same surface, since mobile had no way to see session metadata from the list either.

UX

  • Long-press a root session row — or any search result → bottom sheet with the entity info + a Pin/Unpin action.
    • Info is a field-for-field mirror of the web hover card (SidebarRowInfo): title, mono session id, type · status · N subagents, Runner (only when a runner is pinned), Sandbox (incl. the implicit "Local" default), Spawned (absolute), Last active (relative) — using the same shared formatters/helpers, so values match the web exactly.
  • In-session kebab menu also gets a Pin/Unpin item — the mirror of the desktop tile menu's Pin/Unpin (and the ⌘K "Pin current entity" action), so a session can be pinned from inside it. Together with search long-press this closes the "searched for a session, now want to pin it" gap.
  • Pinned section renders above the date/type/status groups; pinned roots are removed from the groups and from parent subtrees below (same de-dup as SidebarTree), ordered by recency like the web.
  • Pinned entities respect the type/status visibility filters (web parity — the pinned set derives from the filtered entities).
  • In tree mode pinning is root-only (web-sidebar parity); long-press on child subtree rows is a no-op. Search results accept long-press at any depth (desktop-tile-menu parity) — a pinned child hoists into the Pinned section and is filtered out of its parent's subtree.

Deliberate divergences from the web (touch context)

Web Mobile Why
Hover reveals pin icon + info card Long-press sheet combining both no hover on touch
Pinned section header is collapsible Not collapsible no mobile section is collapsible today
⌘K palette groups search results into Pinned/Sessions Search results stay a flat list (no pinned section/indicator) search targets a known session by name; can add later
"Last active" has absolute-time tooltip Relative only no tooltip affordance
Per-row pinned glyph (filled, rotated) None — section placement is the indicator rows stay clean

Implementation choices

  • Storage: per-device AsyncStorage array of entity urls (electric-agents-mobile.pinned-entities) — the mobile mirror of the web's localStorage model; not synced to the server. Module-level store like sidebarPrefs.ts/savedServers.ts (whole-set subscription — a pin toggle changes list composition, so per-url listener buckets à la expandedTree.ts would buy nothing), incl. the dirty-guarded hydration merge so a toggle during the startup window isn't clobbered. Unit-tested (vitest, gated AsyncStorage mock that genuinely exercises the pre-hydration path).
  • Synced columns extended: entities now sync dispatch_policy + sandbox, runners sync sandbox_profiles (zod defs mirrored from ElectricAgentsProvider.tsx) so the menu can resolve runner/sandbox labels.
  • Shared-code reuse over reimplementation: the menu reuses formatTime + entityRuntime helpers from agents-server-ui. The only shared-file change is type-level — entityRuntime's runner params are loosened to a structural Pick subset (RunnerLike) since mobile's runner schema is narrower; entity params were left untouched (mobile's entity type is fully assignable).
  • Tree-mode root gating by construction: onLongPressRoot isn't forwarded into the SessionTree recursion, so child subtree rows can't get a menu; pinnedSet is forwarded so pinned entities are filtered out of subtrees before child-count/connector geometry is computed. Search rows wire the long-press directly (flat list, any depth) — safe because the subtree filtering above already handles pinned children.
  • The in-session menu reads/toggles the pin via the module store directly (no prop-drilling through SessionScreen), closing the sheet on toggle like the row menu does.
  • The subagent count in the info sheet is pinned-filtered, matching both the rendered subtree and the web hover card.
  • Review follow-ups (from the Claude review): the sheet re-resolves the long-pressed entity from the live query while open (snapshot fallback), so its header tracks status/title updates like the child count already did; the runners live query mounts only while the sheet has an entity instead of subscribing for the screen's lifetime; rows with a context menu expose an accessibilityHint so the long-press affordance is announced to screen readers.

Testing

  • pnpm --filter @electric-ax/agents-mobile typecheck && test — 26 passed (7 new for the pin store)
  • pnpm --filter @electric-ax/agents-server-ui typecheck && test — 66 passed (type-only change)
  • No RN component-test harness in the package, so the UI wiring was verified by review (independent parity + scope reviews) — worth a manual pass in Expo before merge: pin/unpin round-trip, persistence across relaunch, child-row no-op in tree mode, pin from a search result, pin/unpin from the in-session menu, filter interaction.

🤖 Generated with Claude Code

Port the web sidebar's session pinning to mobile. Long-pressing a root
row opens a bottom sheet with the entity info (the web hover card's
fields: title, session id, type/status, subagent count, runner,
sandbox, spawned, last active) and a Pin/Unpin action. Pinned sessions
render in a Pinned section above the date/type/status groups and are
removed from the groups and subtrees below, matching the web's
semantics; pin state is per-device (AsyncStorage array of urls, the
mobile mirror of the web's localStorage model).

To resolve runner/sandbox labels the mobile shapes now sync
dispatch_policy + sandbox (entities) and sandbox_profiles (runners),
and the entityRuntime helper runner params are loosened to structural
subsets so mobile reuses them without casts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Electric Agents Desktop Builds

Build artifacts for commit 9fef141.

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

Workflow run

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 4, 2026

Codecov Report

❌ Patch coverage is 78.26087% with 10 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@7709c9a). Learn more about missing BASE report.
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/agents-mobile/src/lib/pinnedEntities.ts 80.00% 8 Missing and 1 partial ⚠️
packages/agents-server-ui/src/lib/entityRuntime.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4507   +/-   ##
=======================================
  Coverage        ?   33.42%           
=======================================
  Files           ?      206           
  Lines           ?    16914           
  Branches        ?     5917           
=======================================
  Hits            ?     5653           
  Misses          ?    11242           
  Partials        ?       19           
Flag Coverage Δ
packages/agents-mobile 83.05% <80.00%> (?)
packages/agents-server 72.73% <ø> (?)
packages/agents-server-ui 5.79% <0.00%> (?)
typescript 33.42% <78.26%> (?)
unit-tests 33.42% <78.26%> (?)

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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 4, 2026

Electric Agents Mobile Build

Local mobile checks ran for commit 9fef141.

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

@msfstef msfstef requested review from kevin-dp and samwillis June 4, 2026 14:05
@msfstef
Copy link
Copy Markdown
Contributor Author

msfstef commented Jun 4, 2026

Simulator Screenshot - iPhone 15 Pro - 2026-06-04 at 16 55 12 Simulator Screenshot - iPhone 15 Pro - 2026-06-04 at 16 55 53
Simulator Screenshot - iPhone 15 Pro - 2026-06-04 at 17 38 30 Simulator Screenshot - iPhone 15 Pro - 2026-06-04 at 17 38 40

@msfstef msfstef marked this pull request as ready for review June 4, 2026 14:05
@msfstef msfstef added the claude label Jun 4, 2026
@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Claude Code Review

Summary

Iteration 2 review. Since the last review the author has (1) addressed all three suggestions from iteration 1 and (2) added two new entry points for pinning: long-press on search results at any depth, and a Pin/Unpin item in the in-session kebab menu (SessionMenu). The new code is small, faithful to the stated design, and clean. Still no critical or important issues.

Previous Review Status

All three iteration-1 suggestions are resolved:

  1. Stale entity snapshot — fixed. SessionListScreen now holds only a menuSnapshot (row identity) and re-resolves the rendered menuEntity from the live entities query by url, falling back to the snapshot if the entity leaves the collection (SessionListScreen.tsx:89-113). The sheet header now tracks status/title updates like menuChildCount already did, and the close handler still resets the snapshot. The fallback keeps open stable even if the entity disappears.
  2. Idle live query while closed — fixed. The runners useLiveQuery moved into a new EntityInfo child component that only mounts when the sheet has an entity (SessionRowMenu.tsx:82-104), so no subscription is held for the screen's lifetime when the sheet is closed. The non-null entity prop also let the helper calls drop their per-line null guards — a nice simplification.
  3. Undiscoverable long-press — fixed. SessionRow now exposes accessibilityHint="Long press for session options", gated on whether onLongPress is wired (SessionRow.tsx:96-98) — so it correctly announces on both tree roots and search rows, and stays absent on non-interactive rows.

What's Working Well (new in this iteration)

  • Search-result pinning is wired by construction, consistently with the tree-mode gating. The flat search list now passes onLongPress={() => openRowMenu(entity)} directly (SessionListScreen.tsx:325-327), while tree rows still route through onLongPressRoot which SessionTree deliberately doesn't forward into recursion. So search hits open the menu at any depth (desktop-tile parity) and tree mode stays root-only — both behaviors fall out of where the prop is attached, no runtime depth check. menuChildCount and pinned are both keyed off the resolved menuEntity.url, so a pinned search-result child reports its own subagent count and pin state correctly.
  • In-session SessionMenu pin reuses the module store directly. usePinnedUrls() + togglePin(entity.url) with handleClose() on tap (SessionMenu.tsx:130-131, 322-341) mirrors the row menu's toggle-then-close behavior and avoids prop-drilling through SessionScreen. The item is correctly guarded behind entity &&, so it's absent during the loading window.
  • Changeset updated to describe both new entry points — stays accurate.

Issues Found

Critical (Must Fix)

None.

Important (Should Fix)

None.

Suggestions (Nice to Have)

Nothing new. The remaining open item is unchanged from iteration 1 and already acknowledged by the author: there's no RN component-test harness, so the UI wiring — now including the two new entry points (pin from a search result, pin/unpin from the in-session menu) — is verified by review + screenshots rather than automated tests. Worth folding into the manual Expo pass before merge.

Issue Conformance

No linked issue (per linked_issues.json) — soft warning per convention, well compensated by the unusually thorough PR description, which the author kept in sync with the new scope (the divergences table, implementation-choices section, and review-follow-ups note all reflect the added entry points). Implementation matches the description; no scope creep beyond the pinning surface.


Review iteration: 2 | 2026-06-04

msfstef and others added 2 commits June 4, 2026 17:28
Two pin surfaces that were missing relative to desktop:

- Search results now support long-press, opening the same context
  sheet (info + pin) as tree roots — at any depth, like the desktop
  tile menu. Previously a searched-for session could not be pinned
  at all.
- The in-session kebab menu gains a Pin/Unpin item, mirroring the
  desktop tile menu's Pin/Unpin and the ⌘K "Pin current entity"
  action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Re-resolve the long-pressed entity from the live query while the
  sheet is open (snapshot fallback if it leaves the collection), so
  the header tracks status/title updates like the child count
  already did.
- Mount the runners live query only while the sheet has an entity,
  instead of subscribing for the screen's whole lifetime.
- Announce the long-press affordance to screen readers via an
  accessibilityHint on rows that have a context menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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