Skip to content

feat(settings): add clean status bar, network conditions, and per-app language - #14

Merged
lauridskern merged 22 commits into
feat/device-app-toolsfrom
feat/device-conditions
Oct 15, 2026
Merged

lauridskern merged 22 commits into
feat/device-app-toolsfrom
feat/device-conditions

Conversation

@lauridskern

@lauridskern lauridskern commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements the "More device settings: clean status bar, language, slow network" card for iOS simulators and Android, in one PR.

  • Clean status bar (Device tab): iOS uses Baguette's existing /status-bar endpoint, which wraps simctl status_bar. Android uses a new serve-emu /api/status-bar that enters SystemUI demo mode (9:41, full battery and signal, no notification icons). Demo mode can't be queried, so serve-emu records the boot ID when it enters it, and after a reboot the setting reads back as live.

  • Network conditions (Device tab): Normal, LTE, 3G, Edge, and Offline on both platforms, with the same latency and bandwidth.

    • iOS uses Baguette's /network endpoint, which injects VirtualNetwork.dylib. Throttling reaches apps launched after it was turned on, so the panel's network card says so and has a relaunch button.
    • Android points the device's global HTTP proxy (settings put global http_proxy) at a pacing proxy the plugin runs on the Mac. src/server/network-throttle/proxy.ts paces traffic, and network-throttle/android.ts manages each device's proxy setting. Emulators reach it at 10.0.2.2, and physical phones reach it through adb reverse. Apps pick it up right away, with no root and no relaunch.
    • The emulator console (network speed/delay) was tried first and dropped: the current emulator accepts it but doesn't apply it.
    • Local destinations, such as Metro at 10.0.2.2:8081, localhost, and LAN or .local hosts, pass through at full speed.
    • If the user already has their own proxy (Charles, Proxyman), the panel shows Custom and won't replace it. Normal and Offline leave it alone.
    • A marker setting records which plugin process owns the proxy. The plugin clears it on Normal and on shutdown. If the owning process is gone, the plugin clears it the first time it starts that device's Android backend, or on the next settings read, so a crash can't leave the device without internet.
    • The proxy paces each direction through one ordered queue, and a tunnel finishes delivering paced bytes before it closes.
    • Changes to one device run one at a time, so two views can't start two proxies for it.
    • Apps that ignore the system proxy, or open raw sockets, aren't throttled.
    • A two-line card under the pills shows the simulated speed (for example "240 kbps · 800 ms") and where it applies, so switching conditions never moves the layout.
  • Per-app language (App tab, and the new mobile_set_app_language tool): sets one app's language and region, then relaunches it. The device language is unchanged. iOS writes AppleLanguages/AppleLocale into the app's own preferences domain, which is what Settings > Apps > Language does. Android 13+ uses cmd locale set-app-locales. The App tab has a Language section with common languages, including RTL ones (Arabic, Hebrew).

  • Why Baguette isn't patched: the binary we ship already has the status-bar and network endpoints, and language needs only simctl and defaults. serve-emu is our in-repo fork, so it's edited directly, and modified files carry the fork notice.

  • Smaller fixes along the way:

    • The plugin now shows serve-emu's rejection message, where it used to show only HTTP 400.
    • Toggling the network no longer fails on Wi-Fi-only devices, where svc data exits with "Can't find service: phone".
    • If the relaunch after a language change fails, the error says the language was still changed.
  • Structure: src/server/device-settings.ts has one class per platform. Android's plain settings share an endpoint table, and a single mapping turns the radios and throttle into a network condition for both reads and changes. The Device tab is split into network and location components that share a status card and pill row.

The final commit bumps the version to 0.1.156.

Stacked on #15 (feat/device-app-tools), which adds the App tab. Merge #15 first; GitHub retargets this PR to main when the base branch is deleted.

Telemetry

  • App control: mobile_app_language and mobile_set_app_language go through the existing app control wrapper as app_control.language / app_control.set_language. The App tab records ui.app_tools.language. Language tags are not sent.
  • Device settings: the tools had no error reporting until now. Unexpected Baguette or serve-emu failures now report a fixed message under device_settings.read / device_settings.<setting>. Android throttle adb failures report under device_settings.network. Throttling a device that already uses its own proxy is an expected outcome and counts through server.error.outcome. Recovering a throttle left by an exited plugin reports adb failures under device_settings.network_recovery. Proxy addresses and traffic are not sent.
  • Device tab: records ui.device_settings.change round trips and counts ui.device_settings.failure, with the same surface and device-change exclusions as the App tab.
  • Docs: docs/telemetry.md is updated.

Testing

  • Automated:
    • npm test: 653 pass, 1 skipped.
    • New tests/network-throttle.test.ts covers:
      • pacing (timing and byte order);
      • the local-destination bypass;
      • emulator and phone (adb reverse) setup;
      • reusing the proxy across presets;
      • never replacing a user's proxy;
      • stale-marker cleanup after a plugin exit;
      • shutdown cleanup;
      • a real proxy forwarding and tunnelling to a local server;
      • a paced tunnel delivering a full payload when the server closes first (this test fails on the earlier closing code);
      • concurrent changes sharing one proxy, and recovery running once per device.
    • tests/serve-emu-device-conditions.test.ts covers the demo-mode boot marker, waiting for demo mode to be allowed, and devices without telephony.
    • device-settings, app-control, and the React UI test cover the new settings, language tools, relaunch, and validation. Invalid language tags never reach a device shell.
  • Builds and checks: npm run build succeeds. check:mcp-budget: 46 tools, 53,917 / 64,000 bytes. tsc reports only errors that already exist on the base branch, in files this PR doesn't touch.
  • iOS simulator (iOS 27), end to end through the built MCP server: the clean status bar is set (checked with a screenshot showing 9:41 and full battery) and cleared. Edge throttling reads back, the dylib is injected and removed, and Normal clears it. Setting Expo Go's language to de-AT reads back, relaunches the app, and resets.
  • Android emulator (API 36, emulator 36.4), end to end:
    • The clean status bar works after a fix in this PR (see the last commit), and cmd locale sets and reads per-app languages.

    • Throttling measured on device: a 100 KB HTTPS download from the internet, timed inside each device. iOS is a URLSession probe spawned in the simulator; Android is nc through the proxy at 10.0.2.2.

      Condition iOS simulator Android emulator
      Normal 0.15 s 0.23 s
      LTE 0.37 s 0.42 s
      3G 1.45 s 1.52 s
      Edge 4.43 s 4.65 s
    • After the review fixes: a 100 KB HTTPS download with Connection: close through the paced tunnel arrived complete at LTE (0.42 s), 3G (1.67 s), and Edge (5.25 s). The clean status bar applied on an emulator where demo mode started disallowed.

    • Local servers aren't slowed: at Edge, a 100 KB download from a server on the Mac took 0.06 s.

    • Chrome picks up the proxy live: with Edge set, loading example.com sent its tunnels through the proxy, with no relaunch. Normal left http_proxy and the marker unset.

    • Physical Android phones were covered only by unit tests; none was connected.

    • Offline uses the existing svc toggle and works on a healthy image.

  • Rotation: works for apps that allow it. Portrait-locked apps and the launcher stay portrait, which is how Android handles them.
  • UI: the Device and App tabs were rendered with the built CSS in a browser harness and checked visually.

…tus bar

/api/network now reads and sets the emulator console's latency and bandwidth alongside the
radio toggle, and rejects throttles for physical devices. The new /api/status-bar enters or
leaves SystemUI demo mode at 9:41 with full battery and signal. Demo mode cannot be queried, so
entering it records the boot ID and a reboot reads back as live. Wi-Fi-only devices without a
telephony service can still go offline.
The Device tab switches to a clean status bar and simulates LTE, 3G, Edge, or offline networks.
iOS uses Baguette's status-bar and network endpoints, and Android uses serve-emu, with the same
latency and bandwidth for each preset. Physical Android devices offer only offline. On iOS the
panel explains that throttling reaches apps launched afterwards and offers a relaunch. serve-emu
rejection messages now reach the panel instead of a bare HTTP status.
mobile_set_app_language sets one app's language and region, then relaunches it, leaving the
device language unchanged. iOS simulators write AppleLanguages and AppleLocale to the app's own
preferences, like Settings > Apps > Language; Android 13 and later use cmd locale. The App tab
gains a Language section with common languages, including right-to-left ones. Device setting
changes now report fixed-message errors and round-trip timings.
Android 16's demo mode ignores the mobile data type and signal level and draws an empty "3G"
icon, so the clean status bar hides the mobile icon and shows Wi-Fi as fully connected instead
of with a no-internet mark. Demo mode is re-entered first because mobile changes only apply in a
fresh demo session.
The ring sat 8px outside the screen and drifted off the Android frame's corners. The screen
still takes focus on click or drag, so typing reaches the device.
The emulator console accepts network speed and delay but no longer applies
them, so LTE, 3G and Edge did nothing. Android now points the device's
global proxy at a pacing proxy on the Mac: emulators reach it at 10.0.2.2
and phones through adb reverse, so throttling works on both without root
or an app relaunch.

Local dev servers such as Metro stay at full speed, a user's own proxy is
shown as Custom and never replaced, and the setting is cleared on Normal,
on shutdown, and on the next read if its plugin process exited. The
unused serve-emu console throttle is removed.
Comment thread tests/network-throttle.test.ts Fixed
The card always fills two lines with the simulated speed and where it applies, and holds the
iOS relaunch button, so switching conditions no longer shifts the panel.
SystemUI ignored the first demo broadcasts while its settings observer had not seen demo mode
allowed, yet the clean marker was still written.
…xies

- Pace each direction through one ordered queue and finish delivering before a tunnel closes.
- Clear a throttle left by an exited plugin when an Android backend first starts.
- Serialize throttle changes per device so concurrent views share one proxy.
- Report Normal over the user's own proxy as custom, and keep iOS controls on bad replies.
- Count expected outcomes, and clear throttles alongside the other shutdown steps.
# Conflicts:
#	src/server/app-control/tools.ts
#	src/ui/device-settings.ts
#	tests/app-control.test.ts
…e by role

- iOS and Android settings are separate classes; Android's plain settings share one endpoint table.
- One mapping turns radios and the throttle into a network condition for reads and changes.
- The pacing proxy and the device bookkeeping live in separate modules; local traffic skips pacing
  by having no pacer instead of separate branches.
- Throttle changes share one queue, and the Device tab splits into network and location parts.
- Drop networkConditions, which every device now reports in full.
# Conflicts:
#	src/server/app-control/tools.ts
#	src/ui/app-tools.ts
- Plain HTTP headers and stream ends share the paced queue, so requests take one round trip.
- A reply that breaks off closes the app's connection instead of crashing the plugin.
- Cancelled requests stop their download, so they no longer use the device's link.
- Android 12 and older report that per-app languages need Android 13.
- Expected recovery failures pass through, and every serve-emu rejection raises an error.
@lauridskern
lauridskern merged commit acd0768 into main Oct 15, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants