Repository navigation
feat(mobile): sleep and wake a desktop from the Android home screen - #4
Merged
Merged
Conversation
A power-managed environment shows a status chip above the thread list. Tapping it opens a sheet with one Awake switch. Wake and sleep commands run in a short-lived terminal on another connected environment on the same LAN (for example `pc-power wake` on an always-on box), so the server is unchanged. Status comes from reachability: when a power-managed environment fails to connect it is treated as asleep and its reconnect loop is paused, without switching the environment off, so its threads stay listed. The app checks again on foreground and retries every few seconds after a wake. Sleeping asks for confirmation only when threads are running on that machine. Configured per environment in Settings -> Environments (Android only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx
…f outage A single failed reconnect marked a power-managed machine asleep and paused its connection, so a dropped socket on app resume or a server restart left the chip on Asleep until the next tap. A machine that was awake now counts as asleep only after 20 s of continuous failures, and while the app is open an asleep machine gets one connection attempt a minute. Sleeping now probes the live session first. A machine that fell asleep keeps reading as connected until its socket times out, so a tap on a stale Awake switch rechecks the connection instead of sending a sleep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…op-power # Conflicts: # apps/mobile/src/Stack.tsx # apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx # apps/mobile/src/state/workspace.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wake and sleep defaults called `pc-power`, a script that only exists on one machine. Wake now sends a Wake-on-LAN broadcast from the chosen relay (python3 on Linux/macOS, PowerShell on Windows) to a MAC address set in Settings, and sleep runs on the machine itself over its own connection (systemctl, pmset or SetSuspendState, started after a short delay so the exit status arrives first). Custom commands still replace either one and run on the relay. Review fixes: - Wake retries only cut a backoff wait short; they no longer interrupt an attempt that is still connecting. - A pause that lands after a late connected update is undone instead of leaving the chip on Checking with reconnects stopped. - Rechecks and cleanup never reconnect a switched-off or removed environment. - The terminal runner uses PowerShell on Windows, reattaches after a dropped connection, reads the exit from the reattach snapshot, and bounds terminal cleanup. - A missed pre-sleep probe no longer skips the 20 s grace period later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The built-in sleep runs the OS call in the foreground, so a refusal comes back as a failed exit. A call still running 5 s after its input reached the shell counts as sent. - A sent sleep, built-in or custom, marks the machine asleep only once its connection fails or a live probe goes unanswered. After 45 s of answering it shows "It didn't go to sleep." and keeps reconnecting. - Sleep resolves its target before the pre-sleep probe and aborts if the settings or the environment's enabled state changed meanwhile. sleepTarget without settings is an error, never the built-in sleep. - A queued resume checks the registry entry's enabled flag before and after connecting, so it can't reconnect a switched-off environment. - A lost terminal-write reply no longer fails the action. The reattach snapshot rewrites the input only when the shell never echoed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sleeping machine took 20-35 s to show as asleep after the app started: its first connection attempt has to time out, and then the 20 s grace period for brief outages runs. With an optional local address in Power settings, the "Wake through" machine checks that address once per launch, through the same throwaway terminal the wake uses. It connects to the machine's T3 port with a 1 s timeout and confirms the environment id from the public descriptor. On silence the chip shows Asleep right away while the reconnect loop keeps running: one more failure pauses it, and connecting makes it awake. A refused connection, another server, or a relay without python3 changes nothing. Later outages keep the grace period, since the check can't tell a sleeping machine from a restarting server. Test fixtures no longer name a specific wrapper script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…connections - A blocked connection (rejected credentials, missing permission) no longer counts as sleep; it shows "Can't connect" and clears any earlier failures or asleep guess, since the machine answered. - Wake, sleep and probe results that land after the machine connected, or after power control was turned off, are ignored. Turning power control off always resumes the reconnect loop, including mid-wake. - Throwaway terminal ids get a random suffix, so commands started in the same millisecond on one relay don't share a terminal. - A launch check result is dropped if the settings changed while it ran, and a settings change allows a new check. A silent check after a failed connection pauses at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…minals - A wake whose machine answers with a blocked connection ends there and shows "Can't connect", instead of timing out into Asleep. - A launch check result applies only while the machine's own connection is still trying, and never after it connected once this launch. - Each built-in sleep first closes the power terminals and history that earlier sleeps on that machine couldn't close after it stopped answering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Built-in sleep terminals use their own terminal thread, so clearing the ones earlier sleeps left can't close other power commands running on that machine. - A live probe that fails while the phone is offline no longer confirms a sleep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Home's empty states skipped the thread list header that holds the chips, so with no threads, or an empty filter on Android, a sleeping machine had no way to be woken from Home. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 25, 2026
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.
Waking a machine that sleeps most of the time meant leaving the app. Its environment also kept showing "Reconnecting" while the app retried against a machine that was asleep.
What changed
Home chip (Android only). Environments with sleep/wake turned on get a status chip above the thread list, also shown when Home has no threads to list: Awake, Asleep, Waking…, Going to sleep…, Checking… or Can't connect. Tapping it opens a sheet with one Awake switch. The chip is hidden when no environment is set up.
Settings. Settings → Environments → environment → Sleep and wake has an on/off switch, a "Wake through" dropdown listing your other connected environments, the machine's MAC address, an optional local address, and optional custom wake and sleep commands.
Built-in wake. The phone opens a short-lived terminal on the chosen environment, which must be on the same LAN. That terminal sends a Wake-on-LAN broadcast to the MAC address, using
python3on Linux and macOS or PowerShell on Windows.Built-in sleep. Runs on the machine itself over its own connection, in the foreground:
systemctl suspend,pmset sleepnow, orSetSuspendStateon Windows. A non-zero exit is reported as a failure, and on Windows so isSetSuspendStatereturning false. A call still running 5 s after its input reached the shell counts as sent, since some OS sleep calls return only after the machine wakes.Launch check. With a local address set (
hostorhost:port, port 3773 by default), the "Wake through" environment checks the machine once per app launch: a 1 s connection to its T3 port, then the environment id from/.well-known/t3/environment. If nothing answers, the chip shows Asleep within a few seconds instead of after the 20–35 s it takes the phone's own connection to time out and the grace period to run. The phone keeps trying in the meantime: its next failure pauses the loop, and connecting overrules the check. A refused connection, another server, or a relay withoutpython3changes nothing. Later outages keep the grace period, because the relay can't tell a sleeping machine from a restarting server. The machine needs T3's Network access on and its port open to the relay.Custom commands. Either custom command replaces the built-in one and runs on the "Wake through" environment, for example a lease-aware wrapper script.
How commands run. Through the existing terminal RPCs, with the command passed in an environment variable. The runner:
exec sh -celsewhere;The server is unchanged.
Asleep detection. Status comes from reachability. A machine that was awake counts as asleep after about 20 s of continuous connection failures. Only retryable failures count: a blocked connection (rejected credentials, missing permission) shows "Can't connect" instead. Its reconnect loop is then paused without switching the environment off, so its threads stay listed. While the app is open, it rechecks once on foreground and then once a minute. After a wake it shortens backoff waits for up to 2.5 min, but never interrupts an attempt that is still connecting. Switched-off or removed environments are never reconnected by this code: a queued resume checks the environment's enabled flag right before and right after connecting, and disconnects again if it was switched off in between. A pause left over from a race is undone.
Sleep confirmation. A sent sleep, built-in or custom, doesn't mark the machine Asleep straight away. It shows "Going to sleep…" until the connection fails or a live-session probe (every 5 s) goes unanswered. If the machine is still answering after 45 s, the sheet shows "It didn't go to sleep." and reconnects keep running. A failed probe while the phone itself is offline doesn't count.
Sleep safety. Sleeping first probes the live session. A machine that fell asleep keeps reading as connected until its socket times out, so a tap on a stale Awake switch rechecks instead of sending a sleep. The command is chosen before the probe; if the power settings or the environment's on/off state changed during it, nothing is sent. It asks for confirmation only when threads are running on that machine.
Header. A power-managed environment that isn't connected is left out of the header's "Reconnecting" status.
The code is in
apps/mobile/src/features/power/. Outside that folder,state/workspaceModel.tsgets the header change andpersistence/mobile-preferences.tsgets the newenvironmentPowerpreference. iOS, web and desktop are unchanged.Testing
sh -cpath against a local UDP listener. It sends a correct 102-byte magic packet.Built with Claude Opus 5.5 in Claude Code, running inside T3 Code.
🤖 Generated with Claude Code