Repository navigation
fix(native-chat): tell old mobile builds why a structured chat is missing - #18756
Conversation
…sing A structured native chat started on desktop was simply absent on a paired phone running any shipped App Store build. The host strips every `agent-session` tab from a client that does not advertise `agent-session.structured.v1`, and no released mobile build advertises it — so the chat had no representation at all and no way to explain itself. Keep the row and retitle it instead of deleting it. The shipped client does not filter unknown tab types and renders whatever title the host sends, so an old build now shows the chat's slot with a title naming the fix. Nothing is removed, so the tab order, groups and layout it belonged to are left intact. The prompt is keyed on the capability for that specific agent, not on the combined policy boolean: a capable phone whose desktop simply has the experiment off would otherwise be told to take an update that cannot help it. Claude rows are prompted too — mobile cannot render them yet and a later build can, so the message is true for that client as well. Restore is no longer gated on the caller's capability. It stayed gated on the host setting, which is what decides whether there is anything to reach at all, but gating on capability left an old client with nothing to project after a desktop restart: neither the chat nor the prompt. Tab titles are capped at 128px on one line in every shipped build, so the string is sized for ~15 characters rather than a sentence. Prompted rows are visible rows, so the host now permits all five session-tab mutations on them, close included. That is intended: a mobile close runs the same teardown as the desktop's own Close button.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (10)
🚧 Files skipped from review as they are similar to previous changes (4)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughStructured session-tab restoration now uses the native structured-chat setting for mobile clients and capability checks for other clients. Mobile projection preserves unsupported rows with agent-specific fallback titles when configured. Runtime clients continue to hide unsupported Claude rows. Destructive tab mutations now reject unsupported agent-session targets. Tests and comments reflect these behaviors. Merge Risk: ⚪ Minimal · up to Older mobile clients retain structured chat tab rows with appropriate fallback titles while unsupported destructive closes are refused; capable clients retain rendered chat behavior. The change is ready to merge. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Description checkExplanation The description explains the problem, behavior changes, mutation boundary, testing scope, and follow-ups. However, it omits the required linked issue, visual proof or an explicit N/A statement, and several template sections and checklist confirmations.
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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 |
There was a problem hiding this comment.
ℹ️ One rough edge worth a look — inline note below.
Reviewed changes
- Keep-and-retitle instead of delete:
projectSessionTabAgentStatusnow keeps a structured row a mobile client can't render and retitles itUpdate to view(resolveMobileUpdatePromptScope+promptAgentSessionTabsToUpdate), instead of pruning it and repairing the resulting group/layout holes. - Per-agent capability keying: the prompt scope keys on
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITYandCLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITYindependently, so a STRUCTURED-but-not-CLAUDE build is prompted only on claude rows. - Restore no longer gated on capability:
restoreStructuredTabsIfSupportedrestores for any mobile client when the host setting is on, so an old build has a row (and thus a prompt) to project after a desktop restart. - Mutation gates become visibility-derived: because prompted rows stay visible, all five
session.tabs.*mutations (close included) are now permitted on them, pinned by new tests for both codex and claude rows. - Test split: structured-restore tests moved out of
session-tabs.test.ts(which had crossed the 800-line cap) intosession-tabs-structured-restore.test.ts.
DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
| if (updatePromptScope) { | ||
| // Why: deleting the row left the user hunting for a chat the desktop says exists; the row | ||
| // survives with a title naming the fix. Nothing is removed, so no group/layout repair applies. | ||
| projected = promptAgentSessionTabsToUpdate(payload, updatePromptScope) |
There was a problem hiding this comment.
Keeping the row also un-blocks session.tabs.activate on it, since the mutation gate is visibility-derived (assertProjectedSessionTabVisible just checks membership in tabs). For a mobile build advertising STRUCTURED but not CLAUDE, tapping the prompted claude row selects into the blank pane the deleted comment right below warned about (resolveMobileNativeChat returns null for non-codex). The "Deliberate consequence" section defends close but not activate — worth confirming that blank-pane-on-tap is intended here, or refusing activate specifically on prompted rows.
Technical details
# Activate on a prompted row reintroduces the blank-pane selection the old code guarded against
## Affected sites
- src/main/runtime/rpc/methods/session-tab-agent-status-projection.ts:64 — row is kept (retitled) instead of pruned, so it stays in the projected `tabs` the mutation gates test against.
- src/main/runtime/rpc/methods/session-tab-mutation-methods.ts:17-25 — `session.tabs.activate` calls `assertProjectedSessionTabVisible`, which now passes for the prompted row.
- src/main/runtime/rpc/methods/session-tab-close-methods.ts:14-24 — same visibility-derived gate for close.
## Required outcome
- The author either accepts the blank-pane-on-tap consequence explicitly (and documents it), or `activate` on a prompted (unrenderable) row is refused rather than passed through.
## Open questions for the human (optional)
- Is the STRUCTURED-but-not-CLAUDE build a state that actually exists in the wild, or only a transient dev state? If the latter, the tap path may be unreachable and this is purely theoretical.There was a problem hiding this comment.
Reviewed changes
- Split the fallback title per agent: Claude rows an incapable mobile client cannot render now show
Open on desktopinstead ofUpdate to view, so the label no longer promises an update that may not reveal the chat. - Blocked destructive close from incapable clients: added
assertAgentSessionTabDestructiveMutationSupportedand wired it intosession.tabs.close/session.tabs.closeLifecycle, so a client that cannot drive a structured session cannot tear it down. - Shared the capability predicate: the old prompt-scope predicate became
clientCanRenderStructuredAgentSessionTab+resolveMobileStructuredChatFallbackTitle, reused by both the projection and the destructive guard. - Extended tests: capability-mutation and projection suites now pin the per-agent titles and the destructive-refusal behavior.
ℹ️ PR body still describes the reversed close behavior
The "One deliberate consequence" section says an old phone closing the tab "does exactly what the desktop's own Close button does, with the full teardown", but this commit now refuses session.tabs.close / closeLifecycle on unrenderable rows via assertAgentSessionTabDestructiveMutationSupported. The Before/After table likewise still lists Claude chats as titled "Update to view" when they now render "Open on desktop". Worth updating the description so it matches shipped behavior — and confirming the downstream consequence is intended: an old phone can see the placeholder rows but cannot dismiss them.
Technical details
# Update PR body to match the new destructive-close refusal and per-agent titles
## Affected sites
- src/main/runtime/rpc/methods/session-tab-agent-status-projection.ts:130 — `assertAgentSessionTabDestructiveMutationSupported` throws `structured_agent_session_unsupported` for agent-session tabs the client cannot render.
- src/main/runtime/rpc/methods/session-tab-close-methods.ts:29 — the guard is invoked for `session.tabs.close` (and `:108` for `closeLifecycle`).
- PR body "One deliberate consequence" + Before/After table — describe close working with full teardown and Claude = "Update to view".
## Required outcome
- PR description reflects that close/closeLifecycle are refused for incapable mobile clients, and that Claude rows show "Open on desktop".
- Confirm the UX is intended: incapable clients can see but not dismiss the placeholder rows.DeepSeek Pro (free via Pullfrog for OSS) | 𝕏
|
Review status: land after fixes at Meaningful fixes added during review:
Review and verification:
Rendered QA:
Residual QA limitation: the historical 0.0.47 JavaScript fixture ran in an iOS development-client shell whose native plist version is 0.0.44. This is rendered cross-version evidence, but it is not validation of the real App Store 0.0.47 binary on a physical device. The focused wire/projection/mutation/restore tests and green CI cover that remaining risk. |
…sing (stablyai#18756) * fix(native-chat): tell old mobile builds why a structured chat is missing A structured native chat started on desktop was simply absent on a paired phone running any shipped App Store build. The host strips every `agent-session` tab from a client that does not advertise `agent-session.structured.v1`, and no released mobile build advertises it — so the chat had no representation at all and no way to explain itself. Keep the row and retitle it instead of deleting it. The shipped client does not filter unknown tab types and renders whatever title the host sends, so an old build now shows the chat's slot with a title naming the fix. Nothing is removed, so the tab order, groups and layout it belonged to are left intact. The prompt is keyed on the capability for that specific agent, not on the combined policy boolean: a capable phone whose desktop simply has the experiment off would otherwise be told to take an update that cannot help it. Claude rows are prompted too — mobile cannot render them yet and a later build can, so the message is true for that client as well. Restore is no longer gated on the caller's capability. It stayed gated on the host setting, which is what decides whether there is anything to reach at all, but gating on capability left an old client with nothing to project after a desktop restart: neither the chat nor the prompt. Tab titles are capped at 128px on one line in every shipped build, so the string is sized for ~15 characters rather than a sentence. Prompted rows are visible rows, so the host now permits all five session-tab mutations on them, close included. That is intended: a mobile close runs the same teardown as the desktop's own Close button. * fix(native-chat): keep fallback tabs safe and truthful --------- Co-authored-by: Merge Sim <sim@local>



ELI5
You start a chat with Codex on your computer. You pick up your phone to check on it — and the chat
isn't there. No tab, no error, nothing. It looks like it never happened.
The reason is that your phone's app is older than the feature. The desktop knows an old app can't
display this kind of chat, so it hides it completely rather than showing something broken. The
problem is that "hide it completely" and "it doesn't exist" look identical from the couch.
This PR stops hiding it. The chat now appears on your phone as a tab that says "Update to view".
You still can't read the chat on an old app — that genuinely needs the update — but you can see it
exists and you're told what to do about it.
Before / After
The last two rows are the ones worth checking: an up-to-date phone must never be told to update.
What's in this PR
Three host-side changes. No mobile release needed — this reaches phones already in the App Store.
The chat's tab is kept and retitled instead of deleted. Old phones don't filter tab types
they don't recognise, so they render whatever title the desktop sends. Nothing is removed, so
the surrounding tabs, groups and layout are untouched.
The fallback is decided per agent. A phone that can show Codex chats but not Claude ones
gets the real Codex tab and "Open on desktop" for Claude. An old phone gets
"Update to view" for Codex. A phone whose desktop simply has the feature switched off gets
nothing at all, because updating would not help it.
The message survives a desktop restart. Previously the desktop only rebuilt this list for
phones that could already read it, so after a restart an old phone saw nothing — not even the
message.
Tab titles are capped at about 15 characters on a phone, which is why the label is short.
Mutation boundary
Because the tab is visible, an old phone can select and arrange the placeholder. Destructive close
and lifecycle-close require the relevant structured-chat capability and are refused for incapable
clients. This keeps the explanatory row without letting an old client tear down a chat it cannot
render. Tests pin the boundary for both Codex and Claude.
Testing
Focused projection, mutation, restore, and cross-version wire suites pass. The tests cover Codex
and Claude fallbacks, missing/base/agent-specific capability combinations, restart restoration,
non-destructive placeholder actions, destructive-close refusal for incapable clients, and unchanged
behavior for capable and desktop-remote clients.
Follow-ups, not in this PR
on a bad connection currently looks identical to an old one, and would be shown the message. Needs
to land before this feature becomes the default.
agent-session.structured.claude.v1and actually renders Claude structured chat. Only thenshould the host expose the real Claude row.
shipped app's source, not observed on a device.
Separately: a workspace running one of these chats shows no agent activity at all on mobile or in
orca worktree ps, even on an up-to-date phone. Different bug, planned separately.