* fix(app): bound the wake/quit paths implicated in the phone-session-ended freeze (#9447)
- relay-transport: waitForClose now times out (5s) so a half-open post-sleep
socket can't wedge runtimeRpc.stop()
- will-quit: race teardown against a 20s deadline so app.quit() always runs
(Force Quit was the only escape when any teardown member never settled)
- terminal-fit-restore: local restoreTerminalFit invoke gets the same 15s
bound as the remote path so the held-fit modal buttons can't pin disabled
* fix(app): close wake recovery timeout gaps
* fix(relay): drop late frames after forced teardown
* fix(relay): fence detached socket callbacks
* fix(app): close timeout resource gaps
* fix(relay): detach retired mobile transports
* fix(types): exclude absent stat overloads
* fix(runtime): expire wedged terminal restore dedupe
* fix(runtime): keep restore retries on one reclaim
* chore(skills): refresh bundled skill manifests
* fix(window): fence quit acknowledgements by request
* fix(relay): bound revoked device socket cleanup
* fix(terminal): let remote desktop viewers own the shared PTY width
A remote (relay/shared-control) desktop viewer resizes the host source PTY to
its own width, but was never registered as a width owner. So the host's own fit
cascade (window resize, split drag, tab reveal, "+"-new-tab re-render) freely
resized the viewed PTY back to the host-local width with no signal to the
viewer. The viewer kept its narrower grid while the host streamed wider
alt-screen frames -> cell-layout garble ("porridge") until a manual resize.
The mobile "presence lock" already solves this shape for phones by suppressing
the host's pty:resize while the phone drives. This does the same for remote
desktop viewers WITHOUT joining the mobile driver state machine (a viewer needs
only resize suppression, not input lock / phone-fit / driver banners), and it
routes every PTY geometry change through the existing enqueueLayout/applyLayout
serialization path rather than resizing the PTY ad hoc.
Design:
- Registry `remoteDesktopViewers: Map<ptyId, Map<subscriptionKey, viewport>>`,
keyed per SUBSCRIPTION so duplicate streams of one client cannot release each
other. isPtyResizeDrivenRemotely() = mobile driver OR any viewer present;
pty:resize (host fit cascade) bails on it. INPUT is never locked (shared
control: host and viewers can both type), unlike mobile.
- New internal layout target { kind: 'remote-desktop' } in applyLayout: clears
terminalFitOverrides like 'desktop', resizes only when dims changed, emits no
fit-override notifications, and does not call onExternalPtyResize (so a
viewer's width never pollutes desktop restore state). enqueueLayout remains
the sole serialized writer of PTY dimensions.
- Smallest client wins: applyRemoteDesktopLayout() sizes the PTY to the SMALLEST
attached viewer, so viewers with different screen sizes never overflow the
narrowest grid.
- No snapshot/replay race: a viewport that arrives while the initial scrollback
snapshot is being serialized is buffered and applied as ONE serialized resize
before serialization, so a mid-repaint alt screen is never baked into the
snapshot.
- Reclaim on detach: when the last viewer leaves, applyRemoteDesktopLayout
resizes the PTY back to the host's own width via enqueueLayout({kind:'desktop'}),
so the host reflows to full width without a manual window jiggle. The host
width is captured at the pty:resize suppression point (the host's own resize
attempts) — a source a remote viewport never pollutes.
- Mobile coexistence: a phone outranks a viewer; when the phone leaves, the
mobile-release paths call applyRemoteDesktopLayout so a surviving viewer keeps
the PTY, else the host reclaims.
Verified live (two local desktop instances): correct render on connect, no
garble on viewer-window resize (synchronous), and host reclaims full width on
disconnect. Unit tests cover the registry lifecycle, smallest-client-wins,
reclaim-via-layout, and mobile coexistence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
* fix(terminal): address remote-desktop PTY width review findings
Resolves four CodeRabbit findings on the remote-desktop viewer width
ownership change, plus a round-trip race surfaced while fixing the leak:
- Leak: the one-shot terminal.updateViewport RPC has no disconnect hook,
so it must never create a width floor (nothing releases it, pinning the
host at a stale width). Add refreshRemoteDesktopViewer, which only
refreshes floors the client already owns via its stream subscription
(matched by clientId), mirroring the mobile updateMobileViewport
no-op-without-subscription invariant. The one-shot handler now refreshes
instead of registering.
- Round-trip replay: with the one-shot fallback now refresh-only, a resize
landing during the subscribe round-trip (connected, stream not yet
current) was dropped. The transport now replays the latest viewport over
the stream once it becomes current.
- Reclaim: keep the host reclaim target unless the reclaim resize actually
landed (result.ok), so a failed reclaim can retry against true host
geometry instead of a stale remote width.
- Snapshot drain: drain pendingRemoteDesktopViewport in the
sendRequestedSnapshot finally block; a viewer resize parked during a
SnapshotRequest buffering window was otherwise dropped until the next
resize.
- Key scope: scope the multiplex width-floor key by connectionId so two
connections reusing the same client-local streamId can't overwrite or
release each other's floor.
Tests: refresh-never-creates-floor, reclaim-target-retained-on-failed-resize,
SnapshotRequest parked-resize drain, connection-scoped key assertions, and a
round-trip replay race test (verified to fail without the flush).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(terminal): arbitrate active desktop viewport ownership
Co-authored-by: Orca <help@stably.ai>
* test(terminal): expect viewport claim capability
Co-authored-by: Orca <help@stably.ai>
* fix(terminal): preserve input across ownership teardown
Co-authored-by: Orca <help@stably.ai>
* test(terminal): keep subscribe buffer coverage within lint limit
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: shady <shady2k@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Orca <help@stably.ai>
* Show worktree-attributed child agents inline
- Keep orchestration worker rows visible when hook status arrives before the
renderer has a tab for the pane.
- Show child agent lineage expanded by default with clearer parent/child styling.
- Tighten worktree list indentation so grouped and nested rows align correctly.
* Sync agent lineage from runtime graph
- Return main-owned orchestration metadata during window graph sync so live,
retained, and title-derived agent rows stay grouped under the right parent.
- Match lineage by terminal handle when pane keys are unavailable after
teardown, and ignore stale completed dispatches.
- Adjust sidebar indentation and compact agent row chrome/readability.
* Preserve agent lineage across status replay and worktree cleanup
- Enrich agent status snapshots with runtime terminal and orchestration metadata
- Share lineage tree building between dashboard and sidebar, including coordinator fallback
- Avoid applying stale runtime dispatch metadata to current hook statuses
- Use worktree attribution for PR refresh and live status removal before tabs mount