Push & relay polish: actionable notifications, relay-everywhere, no-toggle UX#710
Conversation
…t-on relay, LA shutdown end, badges (brain/worker/desktop side) - pushPublisherService: approval alerts carry sessionId/itemId/category (ADE_APPROVAL); LA content-state waiting_for_approval rows gain optional itemId; aps.badge = awaiting-attention count with a silent badge-only sync item when the count drops; shutdown() publishes a bounded best-effort Live Activity end (dismissal +60s, active runs stamped stale) wired into the daemon dispose path. - push-relay worker: passes category/badge into aps, sessionId/itemId top-level, and accepts title-less badge-only items. - hello_ok + brain_status advertise cloudRelayWssUrl (optional, backward compatible) so already-paired phones learn the off-LAN route; toggle flips broadcast brain_status immediately; brain ingress fallback advertises it too. - syncCloudRelayStore: default enabled (missing field reads true; explicit false preserved as the kill-switch); desktop Settings control relabeled "ADE relay"; ade sync relay help updated. - docs: sync README security bullet + status table, push-notifications, ios-companion transport race. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… mute, badge clear - ADE_APPROVAL notification category with Approve/Deny actions routed through the intent command registry (works app-closed via the pending command queue); default tap keeps deep-link routing. - Live Activity: run rows decode optional itemId; lock-screen waiting_for_approval rows render Approve/Deny Button(intent:) capsules (Dynamic Island unchanged); AttentionActionIntents.swift added to the ADEWidgets target sources. - Relay reachability: hello_ok/brain_status cloudRelayWssUrl persists into HostConnectionProfile.savedRelayCandidates (fixes the hello_ok profile rebuild dropping saved relay routes); explicit null clears them. - Toggle removed: relay candidates always race strictly last; settings show a quiet "Using ADE relay — Tailscale gives a faster, private connection" line when the active route is the relay. - Per-session mute in the Work list row context menu + open-chat header menu, bell.slash glyph on muted rows. - App badge cleared on every foreground. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion marker, badge subset sync, shutdown guards - Approve/Deny intents conform to LiveActivityIntent so lock-screen Live Activity buttons run in the APP process (plain AppIntents execute in the widget extension where the command bridge is nil); pending intent queue now also drains on every foreground, not just cold launch. - syncCloudRelayStore: enabledSetByUser marker — pre-default-on builds implicitly persisted enabled:false on first run, which would have kept the relay off for every upgrader; an unmarked false now migrates to enabled while setEnabled() stamps the marker so a chosen kill-switch false survives. - Badge-only sync targets alert-enabled devices NOT covered by this flush's alerts (a device muted for the session still gets the count). - publisher.shutdown(): clears any scheduled flush timer before the LA end and holds the raced publish promise so a late rejection can't surface unhandled. - Judo moves: shared resolvePushRelayStateFile() (bootstrap + daemon shutdown key can't drift), closure-based dispose (no this-dependence), syncMergedRelayCandidates() unifies the hello_ok/brain_status merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- syncCloudRelayStore tests consolidated into their colocated file (moved store/url/signature describes out of syncTunnelClientService.test.ts, deduped 3 overlapping cases); added marker-persistence-through-setRelayUrl case. - iOS: testSyncMergedRelayCandidatesFoldsAdvertisedFreshestFirst pins the relay-fold contract (dedup, cap 3, freshest first, nil/invalid advertised preserves existing). - Docs: sync README/push-notifications/ios-companion refined for the enabledSetByUser migration, LiveActivityIntent flow + foreground drain, and subset-targeted badge sync; ade-cli README relay lines updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
📝 WalkthroughWalkthroughThis PR makes cloud relay default on with explicit user-disable state, advertises relay WebSocket URLs through sync payloads, and adds actionable approval notifications with badge-only updates, Live Activity actions, and per-session mute controls across the CLI, server, desktop, and iOS. ChangesCloud Relay Wiring and Client State
Actionable Push Notifications and Session Mute
Estimated code review effort: 4 (Complex) | ~75 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot review but do not make fixes |
Completed a focused review of the current PR changes. I did not find any significant correctness, logic, or security issues to fix. |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…ification actions) A waiting_for_approval row from a pre-itemId host would render buttons that dispatch an empty item id — a command that cannot target the pending approval. Gate the Live Activity buttons and the notification-action dispatch on a non-empty itemId; legacy payloads fall back to tap-to-open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30e8fdd1ff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
syncProfilesEquivalentForPublishedState omitted savedRelayCandidates, so a relay-only updateProfile (brain_status advertisement flip) persisted to disk while activeHostProfile stayed stale — and the next profile write, which starts from the in-memory copy, wrote the old relay list back. Include the field; only the high-churn cursor fields stay excluded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3730d12e80
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
"Mute pushes on a schedule" covers badge-only APNs sends too — exclude devices currently inside their quiet-hours window from the badge-sync target list. A stale badge self-heals on foreground (cleared) or the first post-window flush. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef191b049d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/ios/ADE/Views/Work/WorkRootComponents.swift (1)
619-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate mute-toggle logic; consider extracting a shared helper.
This block duplicates the read-state-and-toggle pattern already added in
WorkChatHeaderAndMessageViews.swift'ssessionItems(checkingPushNotificationService.shared.prefs.mutedSessionIds.contains(...)and callingsetMuted). Extracting a small helper (e.g.PushNotificationService.isMuted(sessionId:)plus a sharedMuteToggleMenuButtonview) would avoid the two call sites drifting.♻️ Proposed shared helper
extension PushNotificationService { func isMuted(sessionId: String) -> Bool { prefs.mutedSessionIds.contains(sessionId) } } struct MuteToggleMenuButton: View { let sessionId: String var body: some View { let muted = PushNotificationService.shared.isMuted(sessionId: sessionId) Button { PushNotificationService.shared.setMuted(!muted, sessionId: sessionId) } label: { Label(muted ? "Unmute notifications" : "Mute notifications", systemImage: muted ? "bell" : "bell.slash") } } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift` around lines 619 - 627, The mute-toggle logic in WorkRootComponents.swift is duplicated with the session menu code in WorkChatHeaderAndMessageViews.swift, so extract the shared read/toggle behavior into a reusable helper. Add a small PushNotificationService helper such as isMuted(sessionId:) and replace the inline muted check and setMuted call in the session button with a shared MuteToggleMenuButton view (or equivalent) so both call sites use the same implementation and stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/ade-cli/src/bootstrap.ts`:
- Around line 1226-1229: `createAdeRuntime` is creating a new
`syncTunnelClientService` for every runtime, which can duplicate relay clients
for the same machine when `ProjectScopeRegistry` builds multiple runtimes.
Update the runtime setup to deduplicate the tunnel client by `machineKey` using
the shared `phonePairingStateDir`/`secretsDir` store, so repeated
`createAdeRuntime` calls reuse the same `syncTunnelClientService` instead of
starting another client.
In `@apps/ade-cli/src/services/push/pushPublisherService.ts`:
- Around line 320-321: The badge sync gate in pushPublisherService is using a
single shared lastSentBadgeCount, which can skip sending the current badge to
devices that register later. Update the badge-only sync flow in
pushPublisherService to track badge state per device instead of globally, and
use that per-device state in the badge sync checks and updates around the
publish/send logic. Make sure the relevant device registration and badge
propagation paths consult the device-specific badge history so newly added
devices still receive the current badge even when the count has not changed.
In `@apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift`:
- Around line 253-262: The mute menu item in WorkChatHeaderMenu is reading
PushNotificationService.shared.prefs.mutedSessionIds directly, which can leave
the label/icon and action stale under the model’s .equatable() gating. Thread
mute state through WorkChatHeaderMenuModel the same way pinned state is handled
by adding a sessionMuted value and an onToggleMute callback, then update
WorkChatHeaderMenu to render from those model fields instead of querying
PushNotificationService.shared inline.
---
Nitpick comments:
In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift`:
- Around line 619-627: The mute-toggle logic in WorkRootComponents.swift is
duplicated with the session menu code in WorkChatHeaderAndMessageViews.swift, so
extract the shared read/toggle behavior into a reusable helper. Add a small
PushNotificationService helper such as isMuted(sessionId:) and replace the
inline muted check and setMuted call in the session button with a shared
MuteToggleMenuButton view (or equivalent) so both call sites use the same
implementation and stay in sync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: a06fe301-9f1c-4d11-8b94-f3241a6d3a20
⛔ Files ignored due to path filters (4)
apps/ios/ADE.xcodeproj/project.pbxprojis excluded by!**/*.xcodeproj/project.pbxprojdocs/features/sync-and-multi-device/README.mdis excluded by!docs/**docs/features/sync-and-multi-device/ios-companion.mdis excluded by!docs/**docs/features/sync-and-multi-device/push-notifications.mdis excluded by!docs/**
📒 Files selected for processing (33)
apps/ade-cli/README.mdapps/ade-cli/src/bootstrap.tsapps/ade-cli/src/cli.tsapps/ade-cli/src/services/push/pushPublisherService.test.tsapps/ade-cli/src/services/push/pushPublisherService.tsapps/ade-cli/src/services/push/pushRelayClient.tsapps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.tsapps/ade-cli/src/services/sync/syncCloudRelayStore.test.tsapps/ade-cli/src/services/sync/syncCloudRelayStore.tsapps/ade-cli/src/services/sync/syncHostService.test.tsapps/ade-cli/src/services/sync/syncHostService.tsapps/ade-cli/src/services/sync/syncService.tsapps/ade-cli/src/services/sync/syncTunnelClientService.test.tsapps/desktop/src/main/services/sync/syncService.test.tsapps/desktop/src/renderer/components/settings/SyncDevicesSection.tsxapps/desktop/src/shared/types/sync.tsapps/ios/ADE/App/ADEApp.swiftapps/ios/ADE/App/ADEAppDelegate.swiftapps/ios/ADE/Models/RemoteModels.swiftapps/ios/ADE/Services/PushNotificationService.swiftapps/ios/ADE/Services/SyncService.swiftapps/ios/ADE/Shared/ADEAgentActivityAttributes.swiftapps/ios/ADE/Shared/AttentionActionIntents.swiftapps/ios/ADE/Views/Settings/ConnectionSettingsView.swiftapps/ios/ADE/Views/Settings/SettingsConnectionHeader.swiftapps/ios/ADE/Views/Settings/SettingsPairingSection.swiftapps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swiftapps/ios/ADE/Views/Work/WorkRootComponents.swiftapps/ios/ADE/Views/Work/WorkSessionDestinationView.swiftapps/ios/ADETests/ADETests.swiftapps/ios/ADEWidgets/ADEAgentActivityWidget.swiftapps/push-relay/src/relay.tsapps/push-relay/test/relay.test.ts
💤 Files with no reviewable changes (1)
- apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
- buildSyncHostHelloOkPayload includes cloudRelayWssUrl: null when the caller supplied it (relay disabled) instead of omitting — the brain fallback handler never sends brain_status, so hello_ok is the only clear signal on that path; iOS clears saved relay routes on the explicit null (absent key still means "older host, keep them"). - Badge sync state is tracked per device instead of one global count: a flush that excludes a device (quiet hours, alert-covered subset) no longer marks its stale badge as synced; entries drop on unregister. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd0a63b6e2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… through the menu model - Badge-only items drop the relay dedupeKey (its suppression hash ignores deviceIds, so a shared key starved other devices of the same count); the publisher's per-device map is the sole dedupe. - lastSentBadgeByDevice commits from the relay's per-device alert outcomes — a transiently-failed target stays unsynced and retries on the next flush; legacy outcome-less responses commit all targeted. - Tunnel client is a machine-level shared singleton keyed by the relay config file: per-scope instances re-registered the same machineKey on every project open and churned the relay connection. Scope dispose no longer stops it; the daemon shutdown path does. - iOS: mute state rides WorkChatHeaderMenuModel (equatable menu re-renders on flips from anywhere) and both Work surfaces observe PushNotificationService so glyph/menu labels stay live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ce9a69836
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ttempts on reconnect - Only the runtime that owns the brain-level shared sync listener may register the relay tunnel: with default-on, a headless one-shot CLI runtime would otherwise claim the DO's single host socket (last-wins), stealing the relay from ade serve and failing every phone /connect. The enable-toggle handler gets the same gate. - iOS reconnect keeps relay wss URLs out of the TCP probe race and the port sweep — crossing one with the fallback port list retried the identical URL once per port. Each relay route is one attempt, strictly after the whole direct address x port sweep; relay-only profiles still connect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bce629a7f9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…intent dispatch - A new approval with identical rendered copy but a different itemId now re-publishes (fingerprint includes itemId/category) so the phone's Approve/Deny never targets an already-resolved item. - Notification actions / LA buttons can background-launch the app before the sync socket is up and chat.approve is not queueable — the intent bridge now treats the tap as a user-initiated reconnect and waits up to ~5s for the connection before sending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ios/ADE/App/ADEApp.swift (1)
90-109: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winSilent failure path: reconnect timeout and command delivery result are both dropped.
If the bounded reconnect wait (up to 5s) never reaches
.connected,sendRemoteCommandis still called and itsSyncRemoteCommandDelivery— including the.dropped(message)diagnostic case — is discarded. For a non-queueable, user-initiated approve/deny action, this means a tap can silently fail with no log and no user feedback. The same applies to the earlyguard let sync = SyncService.shared else { return }.At minimum, log the outcome (including the
.droppedmessage) so failed approvals are diagnosable.🔧 Proposed fix to surface delivery failures
- if sync.connectionState != .connected { + guard let sync = SyncService.shared else { + // TODO: log — approval/deny dropped, sync service unavailable + return + } + if sync.connectionState != .connected { await sync.reconnectIfPossible(userInitiated: true) for _ in 0..<20 where sync.connectionState != .connected { try? await Task.sleep(nanoseconds: 250_000_000) } } - await sync.sendRemoteCommand(mapped, payload: payload) + let delivery = await sync.sendRemoteCommand(mapped, payload: payload) + if case .dropped(let message) = delivery { + // TODO: log message — surfaces why an approval/deny action was lost + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ios/ADE/App/ADEApp.swift` around lines 90 - 109, In dispatch(_ kind:payload:) the reconnect timeout and sendRemoteCommand outcome are currently ignored, so failed user-initiated commands can fail silently. Update the ADEApp command path to capture the result from SyncService.shared and sendRemoteCommand(mapped,payload:), and log any non-connected timeout or .dropped(message) delivery so approve/deny actions are diagnosable even when the socket never reconnects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/ade-cli/src/bootstrap.ts`:
- Around line 1231-1252: Move the shared tunnel client setup in bootstrap behind
the canHostRelayTunnel check so only the runtime with sharedSyncListener creates
or reuses the singleton from getSharedSyncTunnelClientService. Right now
createSyncTunnelClientService can be cached with a getSyncPort() that always
returns null for non-host runtimes, causing later hosts to reuse a bad client.
Keep the existing disable/stop path using peekSharedSyncTunnelClientService so
an already-running host can still be torn down cleanly.
---
Outside diff comments:
In `@apps/ios/ADE/App/ADEApp.swift`:
- Around line 90-109: In dispatch(_ kind:payload:) the reconnect timeout and
sendRemoteCommand outcome are currently ignored, so failed user-initiated
commands can fail silently. Update the ADEApp command path to capture the result
from SyncService.shared and sendRemoteCommand(mapped,payload:), and log any
non-connected timeout or .dropped(message) delivery so approve/deny actions are
diagnosable even when the socket never reconnects.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e0fc95bf-e6e0-42e7-8a6b-b1e22ab03616
📒 Files selected for processing (12)
apps/ade-cli/src/bootstrap.tsapps/ade-cli/src/cli.tsapps/ade-cli/src/services/push/pushPublisherService.test.tsapps/ade-cli/src/services/push/pushPublisherService.tsapps/ade-cli/src/services/sync/syncHostService.test.tsapps/ade-cli/src/services/sync/syncHostService.tsapps/ade-cli/src/services/sync/syncTunnelClientService.tsapps/ios/ADE/App/ADEApp.swiftapps/ios/ADE/Services/SyncService.swiftapps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swiftapps/ios/ADE/Views/Work/WorkRootComponents.swiftapps/ios/ADE/Views/Work/WorkSessionDestinationView.swift
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/ade-cli/src/services/sync/syncHostService.test.ts
- apps/ade-cli/src/services/sync/syncHostService.ts
- apps/ios/ADE/Views/Work/WorkRootComponents.swift
- apps/ade-cli/src/cli.ts
- apps/ios/ADE/Views/Work/WorkChatHeaderAndMessageViews.swift
- apps/ios/ADE/Services/SyncService.swift
- apps/ade-cli/src/services/push/pushPublisherService.ts
| const { createSyncTunnelClientService, getSharedSyncTunnelClientService } = await import("./services/sync/syncTunnelClientService"); | ||
| const cloudRelayFilePath = path.join( | ||
| resolvedArgs.syncRuntime?.phonePairingStateDir ?? resolveMachineAdeLayout().secretsDir, | ||
| "sync-cloud-relay.json", | ||
| ); | ||
| const cloudRelayStore = createSyncCloudRelayStore({ filePath: cloudRelayFilePath }); | ||
| // ONE tunnel client per machine (keyed by the config file): per-scope | ||
| // instances would re-register the same machineKey with the relay on every | ||
| // project open and churn the connection paired phones dial through. | ||
| const syncTunnelClientService = getSharedSyncTunnelClientService(cloudRelayFilePath, () => | ||
| createSyncTunnelClientService({ | ||
| logger, | ||
| configStore: cloudRelayStore, | ||
| getSyncPort: () => resolvedArgs.syncRuntime?.sharedSyncListener?.getPort() ?? null, | ||
| })); | ||
| // Only the runtime that actually hosts phone sync (owns the brain-level | ||
| // shared listener) may register the relay tunnel. The relay DO keeps ONE | ||
| // host socket per machineKey (last wins), so a headless one-shot CLI | ||
| // runtime or embedded fallback starting the tunnel would steal the relay | ||
| // from `ade serve` and then fail every phone /connect (no sync port). | ||
| const canHostRelayTunnel = resolvedArgs.syncRuntime?.sharedSyncListener != null; | ||
| if (canHostRelayTunnel && cloudRelayStore.isEnabled()) { |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== bootstrap slice ==\n'
sed -n '1200,1335p' apps/ade-cli/src/bootstrap.ts
printf '\n== syncTunnelClientService file map ==\n'
ast-grep outline apps/ade-cli/src/services/sync/syncTunnelClientService.ts --view expanded
printf '\n== syncTunnelClientService source ==\n'
cat -n apps/ade-cli/src/services/sync/syncTunnelClientService.ts | sed -n '1,260p'
printf '\n== search shared tunnel helpers usages ==\n'
rg -n "getSharedSyncTunnelClientService|peekSharedSyncTunnelClientService|createSyncTunnelClientService|syncTunnelClientService" apps/ade-cli/src -g '!**/dist/**'Repository: arul28/ADE
Length of output: 19869
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== syncTunnelClientService tail ==\n'
sed -n '255,380p' apps/ade-cli/src/services/sync/syncTunnelClientService.ts
printf '\n== cli.ts usage around peekSharedSyncTunnelClientService ==\n'
sed -n '13545,13605p' apps/ade-cli/src/cli.ts
printf '\n== search for syncRuntime/sharedSyncListener in bootstrap and cli ==\n'
rg -n "syncRuntime|sharedSyncListener|phonePairingStateDir|initializeInBackground|runtimeKind" apps/ade-cli/src/bootstrap.ts apps/ade-cli/src/cli.tsRepository: arul28/ADE
Length of output: 9576
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== bootstrap call sites ==\n'
rg -n "bootstrap\(" apps/ade-cli/src
printf '\n== serve/runtime selection around syncRuntime ==\n'
rg -n "sharedSyncListener|initializeInBackground|runtimeKind|headless|desktop" apps/ade-cli/src/cli.ts apps/ade-cli/src/bootstrap.tsRepository: arul28/ADE
Length of output: 176
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== bootstrap file outline ==\n'
ast-grep outline apps/ade-cli/src/bootstrap.ts --view expanded | sed -n '1,220p'
printf '\n== top of bootstrap.ts ==\n'
sed -n '1,140p' apps/ade-cli/src/bootstrap.tsRepository: arul28/ADE
Length of output: 10847
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1330,1395p' apps/ade-cli/src/bootstrap.tsRepository: arul28/ADE
Length of output: 1845
Move shared tunnel creation behind canHostRelayTunnel. getSharedSyncTunnelClientService(...) runs unconditionally, so a non-host runtime can cache a client whose getSyncPort() is permanently null; a later host then reuses that singleton and brings up the relay without a sync port. Keep the disable path on peekSharedSyncTunnelClientService(...) so an existing host can still be stopped.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/ade-cli/src/bootstrap.ts` around lines 1231 - 1252, Move the shared
tunnel client setup in bootstrap behind the canHostRelayTunnel check so only the
runtime with sharedSyncListener creates or reuses the singleton from
getSharedSyncTunnelClientService. Right now createSyncTunnelClientService can be
cached with a getSyncPort() that always returns null for non-host runtimes,
causing later hosts to reuse a bad client. Keep the existing disable/stop path
using peekSharedSyncTunnelClientService so an already-running host can still be
torn down cleanly.
Source: Coding guidelines
Post-rebase reconciliation with #710: adapt publisher tests to the silent badge-sync item, and exclude CLI runs from the app-icon badge count — a CLI at its prompt reports waiting_for_input as its resting state (same reasoning as the no-alert rule), so counting it would pin the badge non-zero forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…capsules, lane PR badges, instant webhook PR freshness (#707) * push: surface tracked CLI session status in the Live Activity Wire ptyService.onSessionRuntimeSignal (OSC 133-derived running / waiting-input state, previously unconsumed by the brain) into the push publisher so tracked CLI sessions appear as run rows in the agent-runs Live Activity alongside chats. - Live Activity only, no alert pushes: a CLI agent returns to its prompt after every turn, so alerting on waiting-input would ping per turn. Failure alerts remain on the existing non-zero-exit path, which now also flips the run to its terminal phase. - Heartbeat re-fires of an unchanged state are ignored (no LA churn). - Chat-attached shells and unknown sessions are dropped via a resolveCliSession source (sessionService-backed) so a chat's shell never double-counts against its chat run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * push: idle CLI publishes as stale; ADE-branded pairing QR - Map the PTY `idle` runtime signal (12s quiet, no OSC prompt marker) to the `stale` Live Activity phase instead of `running` — a silent CLI no longer overstates as an active row. Stale rows are excluded from activeCount but hold the activity open (ending on a quiet spell would churn end -> push-to-start), prune after the 2h running TTL, and flip back to running when output resumes. - Tighten the shell/ghost exclusion test to assert no publish happens, and exercise exclusion against a real run's payload. - Restyle the mobile-pairing QR to match the welcome modal's ADE-branded TestFlight QR (accent-tinted tile, excavated ADE mark, level H). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * push: reconcile with relay-polish; CLI prompts don't badge the icon Post-rebase reconciliation with #710: adapt publisher tests to the silent badge-sync item, and exclude CLI runs from the app-icon badge count — a CLI at its prompt reports waiting_for_input as its resting state (same reasoning as the no-alert rule), so counting it would pin the badge non-zero forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * work: shared canonical session state + attention precedence fix One vocabulary for session status across the Work tab, Live Activity, and notifications: sessionCanonicalState maps session inputs to {phase, badge} with deterministic precedence — pendingInputItemId / waiting-input > failed > stale (running + silent >= 20 min) > running, with the preview-text heuristic consulted last (it can upgrade a plain running session, never outvote runtime state). Badges exist only for needs_you / failed / stale. sessionIndicatorState now honors pendingInputItemId (previously invisible to it) and gains sessionCapsuleBadge for the row capsules. 17 table-driven precedence + stale-boundary tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * work: status capsules + lane PR badges (desktop & iOS); webhook PRs poke the poller - Desktop SessionCard: one-word attention capsule (Needs you / Failed / Stale) from the shared canonical state; zero layout shift when calm; chat cards never double up with the existing Awaiting chip. - Desktop Work list lane headers: primary-open-PR badge (dot + #num + state) left of the session count, deep-linking to the PRs tab; ADE- mapped PRs only (GitHub-by-branch parity gap noted). - iOS: WorkSessionCanonicalState mirrors the vocabulary exactly (12 table-driven tests incl. the 20-min stale boundary); capsule renders on Work rows; the per-row WorkLanePrIndicator moves to the lane section header with the same cross-tab PR navigation. - PR freshness: automationIngressService gains onPrStateIngested; both the daemon and desktop-main wire it to prPollingService.poke(), so webhook-driven PR changes reach the panel immediately instead of waiting out the next scheduled poll tick. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: ship round 1 — prs-mock guard, spawn-failure capsule, iOS false-stale, frozen stale TTL, relay-poll poke - SessionListPane's PR-badge hook no-ops when window.ade.prs is absent (partial test mocks / web preview) instead of throwing in the effect. - Persisted status:"failed" with a null exit code (spawn/setup death) now yields the Failed capsule on both platforms. - iOS stale detection uses a stale-only activity timestamp (real lastActivityAt / chatIdleSinceAt, never the startedAt fallback), so a plain terminal open >20min no longer reads Stale. - A stale CLI row's lastActiveAt stays frozen through idle heartbeats, so it ages past the 2h TTL and releases the Live Activity (named regression added). - The relay poll loop's ingest now fires onPrStateIngested like the local-webhook path, so relay-delivered PR changes poke the poller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: ship round 2 — hoist PR badge out of lane-header button; real badge-count assertion - The lane header is now a flex row (collapse-toggle button + badge/count cluster as siblings) instead of nesting the interactive PR badge inside a native button — invalid HTML that broke focus order/assistive tech. - Replace the vacuous zero-iteration badge loop with direct countAwaitingAttentionRuns assertions (CLI waiting run counts 0, chat waiting run counts 1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…main The lane-branch-reset incident left three files rebuilt from a pre-#710 base, which read to git as intentional reverts of relay-everywhere code (Greptile P1s: relay URL missing from handshake, non-host runtime owning the relay). 3-way remerge (base=#709, ours=main, theirs=branch) so main's relay behavior returns while the deeplink additions stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* search: reject supplied-but-all-invalid kinds arrays instead of broadening A query whose kinds array contains only invalid entries (e.g. ["termnal"]) previously fell back to the default all-kind set — silently broadening the search. Mirror the inline kind:bogus reject: supplied + nothing valid left means empty results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * deeplinks: session anchors, file target, portable envelopes + resolution ladder (desktop) - Shared contract: session links carry event=<seq> (chat) / offset=<bytes> (terminal) anchors; new file target (repo-relative path + line + lane); new commit/artifact targets; portable envelope (repo/branch/pr/linear query params) on machine-local targets, parsed leniently. - Anchors consumed end-to-end: chat list scrolls to the anchored event (envelope.sequence match, ordinal fallback on full history, bounded older-history paging, highlight); terminal replay scrolls by byte fraction when the whole transcript is loaded; Files editor reveals the anchored line. Search emits envelope.sequence in chat links and canonical ade://file links (index schema 2->3, disposable rebuild). - Resolution ladder for session/lane links: local -> open exactly; repo matches another known project (new project.findForRepo IPC + repoProjectResolver over recents' git configs) -> switch-project card; foreign machine -> fallback-only card (create lane from branch, open PR on GitHub, open Linear issue). InboundDeeplinkModal generalized; no request-access path by design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * deeplinks(ios+web): router parity, Universal Links, envelope-aware Send-to-Mac, link minting - https /open session links navigate locally on the phone; file/commit/ artifact shapes validate then route to Send-to-Mac; session anchors carried on WorkSessionNavigationRequest (no scroll hook yet on iOS). - Universal Links: applinks:ade-app.dev entitlement, NSUserActivity routing, AASA served from apps/web/.well-known (appID VQ372F39G6.com.ade.ios, /open* and /pair* only) with JSON content-type. - Envelope-aware Swift link builder; lane/session/PR copy-link actions mint https links with envelopes; Send-to-Mac card offers GitHub PR / Linear fallbacks from the envelope. Web /open page describes the new target types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * deeplinks: commit/artifact dispatch, ade link targets, envelope auto-embed sweep - commit target dispatches to the owning lane (commitSha param) with the same resolution ladder as sessions/lanes; foreign fallback offers the GitHub commit URL. artifact links open the history surface (no focused- artifact mechanism exists yet). Palette commit results deep-route too. - ade link gains file/commit/artifact targets plus mint-time envelope enrichment (lane branch, PR number, Linear issue) with --no-envelope; round-trip re-emit handles all new forms. ade-deeplinks skill doc updated. - Auto-embed: Linear lane cards and desktop copy-link affordances (lane context menu, session copy, TUI Ctrl+Y) now mint envelope-carrying links. Search emits canonical commit/artifact links and repo/branch envelopes on session links via an optional repoSlug dep (index schema 3->4). - Recovered from an out-of-process lane-branch reset that wiped the working tree mid-task: restored A/B content (kinds guard, sequence anchors, canonical file links, palette anchor pass, regression tests) and re-added the named findForRepo types. Backup refs: backup/pre-incident, backup/c-snapshot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(deeplinks): new targets, anchors, portable envelopes, resolution ladder, iOS universal links Also restores the contract-file doc comments and the parser edge-case tests (traversal normalization, malformed anchors, envelope leniency). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * quality: traversal guard on RPC file targets, anchor re-apply, commit-sha retry, envelope/link hygiene Dual-review synthesis fixes: - app/navigate RPC file targets now validated with the shared repo-relative rules (exported isValidRepoRelativePath/isValidCommitSha) + defense-in-depth check in renderer dispatch — an RPC caller could previously smuggle ../../ paths past the URL parser into openExternalPath. - Work URL anchor apply-once guard keyed by session AND event/offset so re-opening the same session at a different anchor re-positions. - Commit deeplink keeps laneId+commitSha through switch-project retries instead of dropping to a bare /lanes. - Lane search results now carry the portable envelope (envelopeForLane). - ade link refuses to mint links the shared parser would reject (round-trip gate). - project.findForRepo IPC rewired through the tested mtime-cached resolver (was an inline reimplementation spawning git per recent project). - Parser branch/pr assembly unified into shared helpers across both URL forms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: regression pins, folder consolidation, parity residuals - Pin the quality gate: app/navigate file-traversal + malformed-sha rejection (adeRpcServer), ade link round-trip refusal (CLI), lane search envelope (searchService). - Consolidate services/deeplinks to one test file (dispatch contract merged into protocolHandler.test.ts). - Parity residuals: search README deepLink prose, ios-companion universal-link notes, ade-cli README CLI-surface inventory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ship: dedupe AASA header after main merge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ship: restore #710 relay code accidentally reverted in bootstrap/cli/main The lane-branch-reset incident left three files rebuilt from a pre-#710 base, which read to git as intentional reverts of relay-everywhere code (Greptile P1s: relay URL missing from handshake, non-host runtime owning the relay). 3-way remerge (base=#709, ours=main, theirs=branch) so main's relay behavior returns while the deeplink additions stay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ship: address Codex P2s — repo-qualified PR links hand off to Mac on iOS; validate ade link file paths before building - iOS: https pr links carrying a repo param no longer resolve a local PR by bare number (numbers are only unique per repo and the snapshot has no repo identity) — they route to Send-to-Mac where the desktop ladder verifies. - ade link file validates the input path with the shared repo-relative rule BEFORE building: the ade:// path form URL-normalizes dot segments, so the post-build round-trip gate alone would accept ../secret as a link to a different in-repo path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ship: sweep stale-state dispatch class — latest-dispatcher ref, no root fallback for lane file links - The switch-project card now re-dispatches through a ref that always points at the latest dispatcher, so post-switch retries see the new project's lanes/root instead of the closure captured before switchToPath. - laneById / active-repo / findForRepo comparisons read via refs (same class). - File deeplinks with an explicit lane never degrade to the project root: refresh + bounded wait for the lane list, then open the Files tab bare (or report unhandled during suppressed retries) instead of composing the same relative path in the wrong worktree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Push & relay polish — actionable notifications, relay-everywhere, no-toggle UX
Finishes the mobile push/relay platform (#705, #707) with six deliverables:
sessionId/itemId+aps.category: ADE_APPROVAL; iOS registers Approve/Deny notification actions routed through the intent command registry (chat.approve, works app-closed via the pending-command queue). Live Activitywaiting_for_approvalrows gain an optionalitemIdand render Approve/DenyButton(intent:)on the lock screen — the intents conform toLiveActivityIntentso they run in the app process, and the pending queue also drains on every foreground.hello_okandbrain_statusadvertise an optionalcloudRelayWssUrl; iOS folds it intoHostConnectionProfile.savedRelayCandidates(also fixes the hello_ok profile rebuild silently dropping saved relay routes). An explicitnullinbrain_status(kill-switch flipped) clears them; toggling broadcastsbrain_statusimmediately.bell.slashglyph on muted rows; backed by the existingpush.setPrefspipeline.enabledSetByUsermarker so a legacy implicitly-persistedfalsemigrates to on while a real kill-switch choice survives. Desktop Settings control relabeled "ADE relay" (the single quiet kill-switch);ade sync relay enable|disablekept.end(dismissal +60 s, active runs re-stampedstale) so dead agents don't linger on the lock screen.aps.badge= awaiting-attention count on every alert, plus a silent title-less badge-only item targeting devices not covered by the flush's alerts (tracks drops and muted sessions); iOS clears the badge on foreground.Worker redeployed (
ade-push-relay, health green). Wire changes are additive optional fields; the pinned HMAC canonical string and LA contract are untouched (golden-vector tests unchanged).Tests: ade-cli 1515+ green (incl. new publisher/store/hello suites), push-relay worker 12, desktop sharded green (one relay-ordering assertion updated for default-on), iOS app+widget builds + ADETests green (2 new contract tests).
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Greptile Summary
This PR polishes the mobile push and relay experience. The main changes are:
hello_okandbrain_status.Confidence Score: 5/5
This looks safe to merge.
No blocking issues found in the changed code.
What T-Rex did
Important Files Changed
Comments Outside Diff (1)
apps/ios/ADE/Services/SyncService.swift, line 7075-7078 (link)A
brain_statusthat only adds or clearscloudRelayWssUrlupdates storage throughupdateProfile, but this comparison ignoressavedRelayCandidates, soactiveHostProfileis not refreshed. Reconnects in the same app session can keep using the old in-memory profile and miss the newly learned relay route, or keep showing/racing a route that was just cleared.Artifacts
Repro: faithful executable harness for the Swift published-state comparator
Repro: command output showing unavailable native Swift tools and stale comparator result
Prompt To Fix With AI
Reviews (7): Last reviewed commit: "review: itemId in alert dedupe fingerpri..." | Re-trigger Greptile