feat(harmonyos): show every account desktop in the sidebar - #2366
Merged
wgqqqqq merged 6 commits intoAug 18, 2026
Conversation
Creating a session in a workspace other than the open one first sent set_workspace, so the phone moved the desktop's own view before the create landed. CreateSession already binds whatever workspace_path it is handed, so that round trip bought nothing and cost the desktop its place — harmless enough with one machine in view, wrong once the sidebar lists several. Carrying the path on CreateSessionOptions collapses the three-branch dance around it, along with the recovery path that existed only to undo a host workspace this no longer touches. Co-authored-by: Cursor <cursoragent@cursor.com>
The remote surface only ever faced one machine: the workspace section drew the desktop the phone was currently driving, and reaching another one meant moving the global control target, which took the first one out of view. A console that can only see where it happens to be pointed is not a console. deviceRpc is already stateless — every call carries the relay URL, session and target device — so a read-only directory can fan out without touching RemoteSessionManager's single transport, which still owns the conversation. Expanding a device is what dials it; offline rows are never dialed at all, which is what keeps opening the drawer off a row of 130s timeouts. Each device gets its own SidebarDeviceGroup because SessionListProjectionCache holds a single slot keyed on the workspace, so one instance serving N devices would miss on every rebuild. Catalogs persist per device_key and the live device's own catalog is captured on publish, so a device keeps its rows after the control target moves on. Co-authored-by: Cursor <cursoragent@cursor.com>
Scoped to the HarmonyOS app: what was verified against main, why the observation surface fans out per device while the conversation surface stays on one, and why cross-device execution status is out of reach until the remote SessionInfo payload carries a status field at all.
…ooked at Device presence was fetched once at cold start and never again. The relay keeps presence in a live socket registry, so its answer is current whenever it is asked — the phone simply stopped asking, and a desktop that came online after launch stayed grey in the sidebar until the app was killed. Returning to the foreground and opening the sidebar each refetch, with a 15s freshness window so the two firing together on a cold start cost one request rather than three.
Tapping a session on another desktop tears the current link down before dialling the new one, so a desktop that has gone away used to cost the user both machines for two minutes: AccountDeviceCommandTransport dropped the caller's timeout on the floor and always took deviceRpc's hardcoded 130s, nothing on screen said a switch was underway, and when it finally failed the phone was left on no device at all with polling and the heartbeat stopped. The timeout now travels from the transport through to deviceRpc, and the connect handshake passes 15s of its own — a desktop that is running answers get_workspace_info from memory, so silence there means it is gone, not busy. The switch seam shows a toast, and a failed dial restores the device it started from, snapshotting it before the teardown makes it unreadable. Also from the same round on the device: the active desktop rendered grey because the ForEach key for the unpaired placeholder collided with the real row for that same machine, freezing the child on an object nothing updates; and a live link now outranks the relay's presence answer, which is hearsay from a socket registry that lags. Verified against two desktops, the failure path reproduced by freezing one so the relay kept reporting it online.
The phone decides; it never executes. Detached Dispatch already has the shape this needs — the phone remote-controls A, A is the controller that owns the Git baseline, and B is the target that runs the worker. What was missing was never authority, only an entry point on the phone. So the design is written as a checklist of decisions, not of features: for every decision the desktop can make about a job, the phone needs a verb, and every desktop command left out has to say why it is work rather than a decision. What cannot travel is the conversation — the submit request carries no history, on any client — and that boundary is stated rather than papered over.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The phone could only ever see one desktop at a time. To look at another
machine's workspaces you had to move the global control target, and the
machine you came from vanished from the sidebar. This makes the whole
account visible at once: the workspace section becomes `device > workspace
The fan-out needs no new transport.
deviceRpcwas already stateless —each call carries its own relay URL, session and target device id — so a
read-only directory service builds a transport per device and leaves
RemoteSessionManager's single live connection untouched.Four things this fixes along the way, each of which was a real defect
rather than a consequence of the new layer:
CreateSessionalready binds theworkspace_pathit is given, so theextra
set_workspacewas redundant — and under multi-device it waswrong: opening a session on B should not drag B's window elsewhere.
transport was dropping the caller's timeout and always taking the 130s
read timeout, so a failed switch hung for two minutes with no waiting
state and no way back. The handshake gets its own 15s budget, and a
failed switch restores the previous target.
link now overrides the relay's answer. The relay's
onlineflag is aprojection of a socket registry that lags and drops entries; when we are
pinging a desktop every fifteen seconds, that is first-hand proof.
forever.
ForEachbinds a child to the item it was first created with,so the placeholder used before the device list arrives has to key
differently from the real row, or the child is never rebuilt.
Verification:
harmony:architecture,assembleHap, LocalTest andtheme:color-audit:allall pass; the six manual checks in the design docwere walked on a physical phone against two live desktops, including the
two failure paths (a frozen desktop reproduced with
kill -STOP, and asecond-device session creation confirmed not to touch that desktop's
current_workspace_id).Design notes:
src/apps/mobile/harmonyos/docs/mobile-control-console-design.md.The last commit is documentation only — where the phone sits in Detached
Dispatch, which is not implemented here.