Skip to content

Show running and waiting agents on the Lock Screen and in the Dynamic Island, from a new Live Activity switch - #1071

Merged
sak0a merged 2 commits into
mainfrom
port/agent-live-activity
Oct 31, 2026
Merged

sak0a merged 2 commits into
mainfrom
port/agent-live-activity

Conversation

@sak0a

@sak0a sak0a commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

What Changed

Built on #1070 (the subscription usage widget, merged). Built on #1069 and #1055 (both merged; the two #1055 commits this branch used to carry are dropped). Stacking on #1070 resolves the keep-both overlaps in RycoWidgetsBundle.swift, widgetExtension.test.ts, withIosWidgetExtension.test.ts, AppAccessGate.tsx and mobileStartupImports.test.ts here. The shared Inbox hook (useInboxSource) now reads main's useListedEnvironmentsWorkspaceData, as Home does since #1025. After rebasing onto main with #1070 merged, all 2,863 mobile tests pass, and so does bun run check.

  • New Live Activity for agent work on the phone (iOS), off by default. Settings → Inbox → Alerts has a new "Live Activity" switch: "Show running and waiting tasks on the Lock Screen and in the Dynamic Island. It stays current only while Ryco runs, and task titles show on a locked phone." When iOS's own Live Activities switch for Ryco is off, the row says so and cannot be turned on.
  • One card at most:
    • Lock Screen: "2 running · 1 needs you", then up to 5 threads (title, project, phase).
    • Dynamic Island: the expanded view shows the top 3; compact shows a glyph (attention, failure, work, done) and a count; minimal shows the attention glyph or the count.
    • Each row opens its thread; the card opens the Inbox.
  • What it shows comes from the shared summarizeAgentActivity (Summarize running and waiting agents in one shared rule, for the phone's Live Activity #1055), fed by the Inbox as Home builds it: both read it through one hook (useInboxSource: machines, threads, messages queued on this phone, woken threads already seen), so the card never disagrees with the list it opens. Only machines that are connected with a current shell count.
  • When it changes (decideAgentActivity):
    • Start only while Ryco is in the foreground and something runs or waits. Never in the background. A send from the phone is such a moment, and so is coming back to Ryco with work running.
    • Update when what it shows changes; changes within a second become one update. Every update moves the stale date 10 minutes ahead, and a card close to it is refreshed while Ryco is in front.
    • End once nothing runs or waits, showing the results for 15 minutes. Sign-out, an account switch or the switch going off end every Ryco card at once, results cards included. A card also ends once no machine has been live for the 10-minute stale window while Ryco runs (coming back to the foreground starts that window again).
    • Never two: before a card starts, a card that ended and still shows its results is dismissed at once.
    • Leftovers: a card an earlier launch left is adopted, or ended once it is stale and nothing runs. Extra cards end.
  • Once Ryco is suspended the card stops changing. At its stale date, unfinished rows read "Out of date" and the header "Agent status out of date · open Ryco".
  • Nothing is sent anywhere: no push token (pushType: nil), no APNs, no Hub. Titles stay in iOS's activity store on the phone. The app keeps the content state's JSON within 3,584 bytes, and the module checks it again as Swift encodes it (4,096 bytes, ActivityKit's limit); rows are dropped before the counts.
  • Native: AgentActivityAttributes.swift and AgentActivityContentState.swift (Foundation only) are shared by the app module and the extension; the attributes carry only a version. The widgets module gets activitiesEnabled, startAgentActivity, updateAgentActivity, endAgentActivity, listAgentActivities and dismissAgentActivities. The extension renders the card (AgentActivityWidget.swift). The app's Info.plist gets NSSupportsLiveActivities.
  • Launch path: the bridge loads the runtime only after the app is idle, and only once the switch was ever turned on. The Inbox feed runs only while the switch is on. On the sign-in screen, once the switch was ever on, a small lazily loaded module ends every card.

Why

Phase 1 of the Live Activity port from T3 Code (one card, foreground-only start, 10 minute stale date, 15 minute results) and Synara (the card ends once everything has settled). Remote counts through the Hub (LA3) need Hub work, a paid Apple team and a device, so they are a later phase.

Validation

  • bun run check:changed: passes (format, lint, full bun typecheck, all 2,772 mobile tests, and the related client-runtime, desktop, web and ryco-cli tests).
  • New tests:
    • agentActivityModel.test.ts (11): starts only in the foreground when on, signed in and something runs; no update for the same state; the stale date is now + 600,000 ms; a nearly stale card is refreshed in front; the end on settle carries a 15 minute dismissal; sign-out or switching it off ends it at once; two cards become one; a stale leftover ends, a live one is adopted; 5 rows of the longest titles stay under 4 KB, and an oversized state drops rows, not counts; the Inbox rows carry state, request kind, resting, delegated and liveness.
    • liveActivityRuntime.test.ts (7, fake timers): nothing until the switch is on; changes within a second become one update; the end after settling; no start in the background, a start on coming back; sign-out and an account switch end every card; leftovers; a card iOS refused is not retried until the next foreground.
    • LiveActivitySettings.test.tsx (3): no row without the extension; off by default, with the required wording; the iOS Settings hint.
    • mobileStartupImports.test.ts: nothing under src/liveActivity but the bridge is on the launch path.
    • withIosWidgetExtension.test.ts: NSSupportsLiveActivities on the app, and the new sources in the extension.
    • The Mac-only Swift test typechecks the Live Activity widget with the rest of the extension against the iOS Simulator SDK.
  • Without the change check or the coalescing, those tests fail.
  • Built for the iOS Simulator: expo prebuild into a scratch folder, xcodebuild (Debug, ad-hoc signing). The build succeeds with the ActivityKit calls in the app's module; the appex contains AgentActivityWidget and links no React or Hermes; the app's Info.plist has NSSupportsLiveActivities => true.
  • Not verified: the card on a simulator or device. Starting one needs the app's JavaScript running against a connected machine with work, which I could not set up here, so there are no screenshots of the Lock Screen or the Dynamic Island. Also not verified: the frequent-updates budget, the Apple Watch Smart Stack, StandBy, and EAS signing.
  • Known limit: the phone keeps no record of which results were seen, so a finished thread counts as a result for its 15 minutes even after it was opened.

Review

An independent review found no privacy or network problem. Fixed in 5a8fe1cb0:

  • A second card appeared. An ended card shows its results for 15 minutes, the runtime forgot it and listAgentActivities leaves ended cards out, so work that started again within those minutes started a second card. The module gets dismissAgentActivities(endedOnly), and the runtime calls it with true right before every start, so an ended card is gone (dismissal .immediate) before the next one. Sign-out, an account switch and turning the switch off now call it with false: every Ryco card ends at once, also a results card the old code left for 15 minutes. Tests: liveActivityRuntime.test.ts ("removes a card still showing its results before work that starts again gets one"; fails without the call), and the sign-out, account-switch and switch-off tests now expect every card to go.
  • A card with titles could survive a sign-out. On a locked launch the workspace bridge never mounts. LockedLiveActivityBridge on the sign-in screen, when the switch was ever on, loads endLiveActivities.ts once the app is idle and ends every card. Tests: endLiveActivities.test.ts; mobileStartupImports.test.ts checks it stays off the launch path and loads neither the runtime nor the Inbox. Note: a phone that launches offline with only a Hub account is also locked for a moment, so it ends the card too; a new one starts once work runs and Ryco is in front.
  • An offline card sat for hours. With no live machine every decision was a no-op. The runtime now tracks since when nothing is live (unknownSinceMs), and the card ends at once after the 10-minute stale window. Coming back to the foreground starts the window again, so a resumed app gets time to reconnect instead of replacing its card. Tests: agentActivityModel.test.ts ("stays while machines may still answer…") and liveActivityRuntime.test.ts ("ends the card once no machine was live…", which also covers the foreground restart); both fail without the rule.
  • The 4 KB check measured JavaScript's JSON. Swift's JSONEncoder escapes every slash, and every row has a link. The content state moved to AgentActivityContentState.swift (Foundation only; AgentActivityAttributes keeps typealiases), with fitted(): rows go from the end until it encodes within 4,096 bytes, and a state that does not fit even without rows is refused. Start, update and end all go through it. The app's budget is now 3,584 bytes. Tests: agentActivityModel.test.ts ("keeps a margin under 4 KB…"; fails with the old 4,096 budget), and a Mac Swift harness (test/activityState/main.swift, run by widgetExtension.test.ts): the longest five rows the app sends stay under 4,096 bytes as Swift encodes them, and a 3.4 KB state of slashes (about 5.4 KB in Swift) is trimmed to fit with its counts. It fails if fitted() does not trim. ActivityKit's own encoding is private; JSONEncoder is the closest measure I have.
  • The feed was not exactly Home's Inbox. Home's Inbox input moved into useInboxSource (machines, workspace data filtered to Home's machines, messages queued on this phone, woken threads seen), used by HomeScreen and the feed, so the card counts a queued message and a seen woken thread as Home does. Only Home's search and machine filter stay out. Not covered by a new test: it is hook wiring, and the mobile runner cannot run effects; Home's own behavior is unchanged.
  • New overlap with the subscription usage widget PR: none beyond the five files above, but AppAccessGate.tsx now has both PRs adding a bridge on the sign-in screen as well (keep both).
  • Validation after the fixes: all 2,772 mobile tests, the plugin tests, the Mac Swift harness, the startup-import guard and bun run check:changed pass. Rebuilt once for the iOS Simulator (expo prebuild into a scratch folder, xcodebuild Debug, ad-hoc signing): the build succeeds, the app's module has dismissAgentActivities, the appex has AgentActivityContentState and links no React or Hermes; scratch deleted. Not verified on a simulator or device: that end(nil, dismissalPolicy: .immediate) on an already ended card removes it at once (Apple documents ending with an immediate policy; I could not run a card here), and ActivityKit's real encoded size.

🤖 Generated with Claude Code

@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Oct 31, 2026
@sak0a
sak0a force-pushed the port/ios-widget-extension branch from 0cdfbf0 to 4160521 Compare October 31, 2026 14:05
@sak0a
sak0a force-pushed the port/agent-live-activity branch from 5a8fe1c to 3bced6a Compare October 31, 2026 14:15
@sak0a
sak0a changed the base branch from port/ios-widget-extension to port/subscription-usage-widget October 31, 2026 14:15
@sak0a
sak0a force-pushed the port/subscription-usage-widget branch from 214c6ec to e7a2680 Compare October 31, 2026 14:42
@sak0a
sak0a force-pushed the port/agent-live-activity branch from 3bced6a to 5863ccb Compare October 31, 2026 14:48
@sak0a
sak0a changed the base branch from port/subscription-usage-widget to main October 31, 2026 14:48
sak0a and others added 2 commits October 31, 2026 15:56
… Island, from a new Live Activity switch

The phone app can now keep one Live Activity card for agent work. It is off
by default; Settings > Inbox > Alerts has a new Live Activity switch whose text
says the card stays current only while Ryco runs and that task titles show on
a locked phone.

The card counts what runs, what waits on you, and results from the last 15
minutes, with up to five threads on the Lock Screen and three in the expanded
Dynamic Island, each opening its thread. It uses the shared agent activity
summary, fed by the Inbox as Home builds it. It starts only in the foreground
with work running or waiting, updates when what it shows changes (a second of
changes becomes one update), ends once nothing runs or waits and stays 15
minutes with its results, and ends at once on sign-out, an account switch or
when the switch goes off. Once Ryco is suspended it reads "Out of date" after
10 minutes. Phase 1 sends nothing anywhere: no push token, no APNs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en no machine answers

- Before a card starts, any Ryco card that already ended and still shows its
  results is dismissed at once, so work that starts again within those 15
  minutes no longer puts a second card beside it. Sign-out, an account switch
  and turning the switch off now end every Ryco card, results cards included.
- The sign-in screen ends every card once the app is idle, when the switch
  was ever on, with a small lazily loaded module: a sign-out while Ryco was
  closed no longer leaves task titles on the Lock Screen.
- A card ends once no machine has been live for the 10-minute stale window
  while Ryco runs; coming back to the foreground gives the machines that
  window again.
- The content state is checked as Swift encodes it (JSONEncoder escapes every
  slash): the module trims rows above 4,096 bytes, and the app keeps its JSON
  within 3,584. The content state moved to a Foundation-only file so a Mac
  test can measure it.
- Home and the Live Activity read the Inbox through one hook
  (useInboxSource), so the card also counts messages queued on this phone and
  woken threads already seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sak0a
sak0a force-pushed the port/agent-live-activity branch from 5863ccb to 56441f2 Compare October 31, 2026 14:56
@sak0a
sak0a merged commit eea716b into main Oct 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant