Skip to content

feat(mobile): follow phone appearance and add terminal theme modes - #19599

Open
kargnas wants to merge 7 commits into
stablyai:mainfrom
kargnas:feat/mobile-phone-terminal-appearance
Open

kargnas wants to merge 7 commits into
stablyai:mainfrom
kargnas:feat/mobile-phone-terminal-appearance

Conversation

@kargnas

@kargnas kargnas commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

Your phone's terminal now follows your phone's light/dark setting by default. You can override it from the session's ⋯ → Terminal appearance menu without reconnecting.

What Changed

  • Add Automatic (phone), Dark, Light, and Match desktop modes, saved per device.
  • Update already mounted and hidden terminals in place when the phone appearance or the saved mode changes. The saved mode is loaded before the native splash is dismissed, so the first frame already uses it.
  • Share the existing dark/light default palettes with desktop without changing their values (read from src/shared/terminal-themes/defaults.ts).
  • Show the session's ⋯ menu for every session, including folder and floating sessions, because the appearance entry applies to all of them.
  • The menu's labels are plain English, like the rest of mobile. There are no new dependencies, and mobile/package.json and the lockfile are unchanged.

Inside the hybrid-shell session page

Since #21977, the session route can also run as a page inside the hybrid shell. This applies to OTA and dev builds; release builds default to the native shell. That page's storage only holds keys listed in PAGE_STORAGE_EXACT_KEYS, and this PR does not add orca:terminalThemeMode to the list. Inside the page:

  • The page starts normally. The phone sends it the same init frame as on main.
  • The terminal starts in Automatic (phone), even if a different mode is saved in the app.
  • Choosing a mode applies it immediately and keeps it while that page stays open. The write is dropped with the bridge's usual storage-write-dropped log; no error is shown and nothing is thrown. The next page open starts in Automatic again.
  • The error alerts for a failed load or save appear only in the native app. On the page, react-native-web's Alert does nothing, and a refused write resolves instead of rejecting. QuickCommandsSheet has the same limitation.

Why the key is not granted here: a page bundle served by an older desktop checks init.storage against its own allowlist. When it finds a key it does not know, it refuses the whole frame, so the session page would never open for a phone with a saved appearance. Mixed versions are the normal case. How new keys should reach older page bundles (the shell filtering to what the served bundle knows, or the schema dropping unknown keys instead of refusing) looks like a maintainer decision, so I left it for a follow-up.

Rebased onto current main

This branch is two commits plus review follow-ups on fceca5cece:

  1. feat(mobile): follow phone appearance and add terminal theme modes: the previously reviewed change, squashed into one commit.
  2. test(mobile): adapt terminal appearance tests to current main:
    • Main now typechecks mobile tests (a ratchet that only lets the set of failing files shrink) and rejects type assertions in changed code. The new tests now query by the mocked component bindings instead of string element types, and the casts are gone.
    • A new test, terminal-theme-preference-page.test.ts, runs the preference against the page's real storage module. It checks the page behavior described above.
  3. Review follow-ups:
    • d7f6184780: the native splash waits at most 2 seconds for the saved appearance, so a storage read that stalls cannot hold the splash. The README points at src/shared/terminal-themes/defaults.ts. This commit also granted orca:terminalThemeMode to the page, which 012a2d32d8 reverts for the reason above.
    • 1a4076bd4b: tests only. The splash callback is written in plain JavaScript because terminal-theme-startup.test.ts evaluates its source, a new test covers the 2-second bound, and the theme test passes the onCellBoxChange prop that TerminalPaneView on main now requires.
    • d22e61f765: removes the i18next dependency and the 7 locale catalogs that commit 1 added. Mobile has no localized UI, so a first i18n setup does not belong in a feature PR. The menu labels are now English constants, the Agent History and Checks entries are back to main's strings, and mobile/package.json and the lockfile match main again.

Why

Mobile used the terminal palette provided by the desktop, so changing the phone's appearance could leave the console in the opposite mode. A device-local preference lets the phone choose its own appearance and still keeps an explicit option to follow the desktop.

Related implementation: #10421. This PR uses the same local-store and shared-palette approach, but makes the phone's system appearance the default and adds a four-mode session menu.

Linked Issue

Fixes #25499

Related: the terminal portion of #9959, which #22184 and #20382 also request for e-ink phones. The app chrome does not change, so those broader requests for a light mode on mobile stay open.

Visual Proof

Android emulator with the same mock host and session. These screenshots come from builds made before the rebase:

  • The before build is the pre-feature personal build (5f129d158c). It includes an unrelated keyboard toolbar button.
  • The after build is this PR's first revision (84dc7e29e4, based on bba68b1bdd).

The rebase does not change the native UI. The hybrid-shell page is not shown.

Before: phone light, console still dark After: phone light, Automatic (phone)

After switching that same open terminal to phone dark mode:

The appearance picker:

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Prebuilt APK for trying this on a phone: https://github.com/kargnas/orca/releases/tag/preview-mobile-20261005-f1d7719. It contains this PR and my other open mobile PRs on top of main 40f9e7a3fc, and installs next to the regular app as "Orca Preview". On an Android 13 emulator it launched, paired with mobile/scripts/mock-server.ts and painted the terminal.

On the current head (d22e61f765), merged with main 40f9e7a3fc:

  • Mobile: tsc --noEmit, check:tests-typecheck, oxlint and format:check passed. The full vitest run: 930 files passed, 2 skipped; 10,025 tests passed, 6 skipped. The only failures are two tests in src/tasks/mobile-agent-launch-architecture.test.ts, which fail the same way on plain main on my machine.
  • The four mobile web closure suites (mobile-web-app-session-terminal-closure, mobile-web-app-page-closure-families, mobile-web-terminal-engine-closure, mobile-web-app-push-subsystem-closure) passed.

On the earlier rebase onto 27b823f934, not repeated since:

  • Root pnpm tc passed. The desktop terminal theme, contrast, and settings tests that use the moved palette: 6 files, 59 passed.
  • Config tests: the session-closure, mirrored-storage write-path, page-routes, and session-render tests passed (4 files, 29 passed). The session-render test mounts the session page in headless Chromium and checks two things: the console stays clean, and the page writes no storage key it was not given. build:mobile-web (build + verify) passed. In the full config/scripts/mobile*.test.mjs run, 52 of 57 files pass. The other 5 are WebKit render suites that fail because Playwright WebKit is not installed here. They fail the same way on clean main locally; CI covers them.
  • Changed-file oxlint, oxfmt, and pnpm run check:code-quality:changed passed (zero new findings, including the casting gate and React Doctor).

From the pre-rebase revision (not repeated after the rebase):

  • The Android ARM64 release assembleRelease passed after the startup fix. On a headless Android 13 emulator I checked these cases: an existing terminal switching when the phone went from light to dark, manual Light overriding phone Dark, Match desktop, returning to Automatic, and the choice persisting after a restart.
  • A tester in this thread confirmed the behavior on an iOS simulator (2026-09-21), also on a pre-rebase build.

Not run:

  • No device test after the rebase, on Android or iOS.
  • The hybrid-shell session page on a phone. It was checked only by the unit test above and the Chromium render test. I also did not check whether useColorScheme inside the page's WebView follows the phone.
  • Linux and Windows desktop builds.
  • A real SSH session.
  • Full desktop build, test, and lint are left to CI.

There are no RPC, bridge, or init frame changes. The phone sends the session page exactly what it sends on main, so older desktop page bundles are unaffected. The session page that a new desktop serves carries the new menu, and it works against older phone shells because it asks the shell for nothing new. Host-provided palettes remain available unchanged in Match desktop.

AI Disclosure

Implemented and reviewed with OpenAI Codex (GPT-6), including delegated focused code review and merge-fixture validation. The rebase onto current main, the test follow-up and the i18n removal were developed with Claude Code (Claude Opus 5.5). The contributor requested the behavior and tested the earlier mobile behavior on their phone.

Review

Please review:

  • The default changing from following the desktop to following the phone's system setting.
  • The local preference lifecycle.
  • Whether orca:terminalThemeMode should reach the hybrid-shell page, and how, given the older page bundles described above.

The PR contains only the terminal appearance feature. It has no personal keyboard or gesture patches and no release-version changes.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Release note: Mobile terminals now follow the phone's light/dark appearance by default, including existing installations with no saved preference. Choose ⋯ → Terminal appearance → Match desktop to use the connected desktop's palette. Existing sessions stay connected.

The preference stays on the device. Desktop settings, paired-host capabilities, terminal transport, and SSH execution ownership do not change. There are no new environment variables.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred): pnpm tc, the full mobile suite, build:mobile-web, and the changed-code gates passed locally; full desktop lint, test, and build are left to CI

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

Adds mobile terminal appearance settings for system, dark, light, and desktop modes. The app stores the selection and applies the resolved theme to open terminal panes. A localized appearance picker is available in the session actions menu. Startup loads the saved mode before hiding the native splash screen, subject to a two-second timeout. Tests cover preference storage, theme updates, session controls, and startup behavior.

Merge Risk: Moderate · previous review d7f61

After a user saves a terminal appearance, sessions served by an older desktop app may fail to open. Raise the page compatibility version, or otherwise handle the version mismatch, before merging.

Security architecture risk: Moderate · up to d7f61

The appearance setting does not add host privileges. However, after a preference is saved, a newer phone app can fail to initialize pages supplied by an older desktop. This creates a meaningful compatibility risk during mixed-version upgrades and desktop rollback.

Retained concerns

  • Medium · reliability · inferred: An upgraded shell with a saved appearance preference can admit a base-era pageVersion 1 desktop bundle and send orca:terminalThemeMode in init. That older page rejects the entire frame because its storage allowlist lacks the key, preventing normal session bootstrap. The compatibility floor was not advanced and the inspected init path does not negotiate supported storage keys. Mixed-version deployment or desktop rollback can therefore strand affected hybrid pages; the base shell did not send this key.
Security review details

Security Blast Radius

  • inferred — The new preference is device-wide, not host- or workspace-scoped, and is included among exact keys for every page route. Its evidenced compatibility exposure can therefore extend across hybrid pages from older paired desktops on that phone. Direct native screens do not traverse the rejected page-init path.

Trust Boundaries and Controls

  • observed — The shell continues to restrict mirrored storage reads and writes to its route allowlist. The addition exposes one appearance key through that boundary; hydration accepts only system, dark, light, or desktop and rejects other stored values.

Resilience and Maintainability Implications

  • observed — Init parsing remains fail-closed: schema refusal is reported without dispatching acceptInit, and page bootstrap waits for an established shell session. This preserves the initialization boundary, but turns the new producer/older consumer mismatch into a whole-page startup failure rather than a single preference fallback.

Hardening Proposals

  • proposed — Preserve strict init validation while making the storage-key expansion compatibility-aware: negotiate supported keys, or advance the page version and shell floor together so an incompatible desktop is explicitly blocked before page initialization.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 15 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description covers the required sections in detail, but it conflicts with the supplied change summary on material implementation details. It says there are no new dependencies and that `orca:termi… Update the description to match the pull request’s actual changes. Confirm whether i18next and the localization files are present, and whether orca:terminalThemeMode is included in PAGE_STORAGE_EXACT_KEYS; then correct the correspondi…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: mobile terminals follow the phone’s appearance and support additional theme modes.
Full details: Docstring Coverage

Explanation

Docstring coverage is 52.38% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 15 files. (1 skipped: 1 unsupported.)

Full details: Description check

Explanation

The description covers the required sections in detail, but it conflicts with the supplied change summary on material implementation details. It says there are no new dependencies and that orca:terminalThemeMode is not allowlisted, while the change summary reports adding i18next and adding that key to the page-storage allowlist.

Resolution

Update the description to match the pull request’s actual changes. Confirm whether i18next and the localization files are present, and whether orca:terminalThemeMode is included in PAGE_STORAGE_EXACT_KEYS; then correct the corresponding claims and page behavior description.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Share

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — minor suggestions inline.

Reviewed changes

  • Device-local theme preference store (mobile/src/storage/terminal-theme-preference.ts) — an AsyncStorage-backed system/dark/light/desktop mode with a serialized write queue, deduplicated hydration, and change-only publishing.
  • Theme resolution hook (mobile/src/terminal/use-mobile-terminal-theme.ts) — resolves the phone appearance or explicit mode against the shared default palettes, passing the host theme through only in desktop mode.
  • Session menu (MobileSessionHeaderMoreActionsSheet.tsx) — a ⋯ → Terminal appearance action sheet + picker, localized via i18next into 7 languages; the header more-button is now always shown.
  • Terminal pane (TerminalPaneView.tsx) — applies the resolved theme and a matching background color to mounted panes, including hidden ones.
  • Palette sharing — DEFAULT_TERMINAL_THEMES moved from terminal-themes/defaults.ts to src/shared/terminal-default-themes.ts (values unchanged, type narrowed to TerminalColorOverrides) so desktop and mobile share one source.

ℹ️ System-following default changes the appearance of existing terminals on upgrade

The default flip is well-motivated, and Match desktop faithfully preserves the pre-feature behavior (host palette, including a published minimumContrastRatio override, is passed through unchanged), so nothing is lost. The one consequence to be aware of is upgrade-time: users with no stored preference (none existed before) will see their already-open terminals switch from the desktop palette to the phone-system appearance on first launch, with no migration or notification. This is expected for a new v1 display-only preference — no session/reconnection impact — but it is a visible behavior change worth a release-note line rather than a code fix.

Technical details
# Default flip to phone-following

## Affected sites
- mobile/src/terminal/use-mobile-terminal-theme.ts:34 — `mode === 'system'` resolves to the phone appearance.
- mobile/src/storage/terminal-theme-preference.ts:7 — module default is `'system'`.

## Required outcome
- Confirm the intended upgrade behavior; document in release notes. No code change needed.

## Open questions for the human (optional)
- Should existing sessions show a one-time hint when the appearance first flips, or is a silent flip acceptable?

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread mobile/src/i18n/terminal-appearance.ts Resolved
@kargnas

kargnas commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai Addressed the documentation warning in 9f0f790b6d with concise JSDoc for the device preference lifecycle, storage ordering/error contract, theme precedence, and session/pane integration. Also removed two personal-fork-only callback fields from the upstream test fixture. Focused appearance regressions pass: 3 files, 15 tests; changed-file lint and formatting pass. Please recheck the updated head.

@kargnas

kargnas commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@pullfrog Please re-review the changes since 84dc7e29e4 on the current head f795819982. The startup-theme flash is fixed by waiting for saved appearance before dismissing the native splash, with deferred-read and read-failure regression tests. Both original threads have replies and are resolved. The intended upgrade default is now explicit in the PR release note.

@coderabbitai review

Current validation: 520 mobile test files, 4,224 passed / 3 skipped; mobile typecheck and changed-code quality passed. The preceding feature build passed Android release and desktop bundling, with native phone-theme/manual-override/persistence checks documented above.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta — documentation and a small test-fixture cleanup only.

Reviewed changes

  • terminal-theme-preference.ts, use-mobile-terminal-theme.ts, and the touch components — added JSDoc comments describing the synchronous snapshot, hydration precedence, and serialized-write semantics. All are accurate against the implementation.
  • use-mobile-terminal-theme.test.ts — removed onTerminalPlainTap and onTerminalPlainTapCancelled from the paneProps fixture. Neither prop exists on TerminalPaneViewProps, so this drops two excess-property errors from the typed fixture.

The two minor suggestions from the prior review (cold-start hydration flash, zh/zh-Hant locale granularity) are unaffected by this commit and still stand if you want to take them on.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== HEAD ==='
git rev-parse HEAD
git show -s --format='%H%n%P%n%s%n%b' 9f0f790b6d

echo '=== CHANGED PATHS ==='
git show --name-status --format='' 9f0f790b6d

echo '=== PATCH ==='
git show --format=fuller --find-renames --find-copies 9f0f790b6d -- \
  mobile/src/storage/terminal-theme-preference.ts \
  mobile/src/terminal/use-mobile-terminal-theme.ts \
  mobile/src/session/TerminalPaneView.tsx \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.tsx \
  mobile/src/storage/terminal-theme-preference.test.ts \
  mobile/src/terminal/use-mobile-terminal-theme.test.ts \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.test.tsx

echo '=== OUTLINES ==='
for file in \
  mobile/src/storage/terminal-theme-preference.ts \
  mobile/src/terminal/use-mobile-terminal-theme.ts \
  mobile/src/session/TerminalPaneView.tsx \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.tsx \
  mobile/src/storage/terminal-theme-preference.test.ts \
  mobile/src/terminal/use-mobile-terminal-theme.test.ts \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.test.tsx
do
  if [ -f "$file" ]; then
    echo "--- $file ---"
    ast-grep outline "$file" --items all || true
  fi
done

echo '=== CALLBACK-FIELD REFERENCES IN MOBILE TESTS ==='
rg -n -C 3 --glob '*.{test,spec}.{ts,tsx}' \
  '(on[A-Z][A-Za-z0-9]*(?:Callback|Change|Press|Select|Save|Load)|callback)' mobile/src || true

Length of output: 109155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== COMMITS AFTER DOCUMENTATION FIX ==='
git log --oneline 9f0f790b6d..HEAD

echo '=== CURRENT-HEAD DIFF AFTER DOCUMENTATION FIX ==='
git diff --stat 9f0f790b6d..HEAD
git diff --find-renames 9f0f790b6d..HEAD -- \
  mobile/src/storage/terminal-theme-preference.ts \
  mobile/src/terminal/use-mobile-terminal-theme.ts \
  mobile/src/session/TerminalPaneView.tsx \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.tsx \
  mobile/src/terminal/use-mobile-terminal-theme.test.ts

echo '=== CURRENT IMPLEMENTATIONS ==='
for file in \
  mobile/src/storage/terminal-theme-preference.ts \
  mobile/src/terminal/use-mobile-terminal-theme.ts \
  mobile/src/session/TerminalPaneView.tsx \
  mobile/src/session/MobileSessionHeaderMoreActionsSheet.tsx \
  mobile/src/terminal/use-mobile-terminal-theme.test.ts
do
  echo "--- $file ---"
  cat -n "$file"
done

echo '=== CURRENT FIXTURE CONTRACT ==='
rg -n -C 4 \
  'onTerminalPlainTap|onTerminalPlainTapCancelled|type TerminalPaneViewProps|interface TerminalPaneViewProps' \
  mobile/src/session/TerminalPaneView.tsx \
  mobile/src/terminal/use-mobile-terminal-theme.test.ts || true

Length of output: 23938


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '=== STARTUP IMPLEMENTATION OUTLINE ==='
ast-grep outline mobile/app/_layout.tsx --items all
echo '=== STARTUP IMPLEMENTATION ==='
cat -n mobile/app/_layout.tsx

echo '=== STARTUP REGRESSION TEST OUTLINE ==='
ast-grep outline mobile/src/storage/terminal-theme-startup.test.ts --items all
echo '=== STARTUP REGRESSION TEST ==='
cat -n mobile/src/storage/terminal-theme-startup.test.ts

echo '=== STARTUP IMPORT/CALL SITES ==='
rg -n -C 4 \
  '\b(loadMobileTerminalThemeMode|SplashScreen\.(preventAutoHideAsync|hideAsync)|onNavigatorLayout)\b' \
  mobile/app/_layout.tsx mobile/src/storage/terminal-theme-startup.test.ts

Length of output: 19983


@kargnas Recheck complete on f7958199.

The JSDoc matches the implemented preference lifecycle, storage ordering and error behavior, theme precedence, and pane integration. The test fixture now matches TerminalPaneViewProps and no longer contains the two personal-fork-only callback fields.

The later splash-screen change also waits for preference hydration before SplashScreen.hideAsync(). Its regression test covers deferred loading and storage failure.

No new issue found.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Startup flash fix — mobile/app/_layout.tsx now awaits loadMobileTerminalThemeMode() (error-swallowed via .catch) before SplashScreen.hideAsync(), so the native splash covers the first frame until the persisted appearance is published.
  • Splash-ordering regression tests — new mobile/src/storage/terminal-theme-startup.test.ts extracts the real onNavigatorLayout callback through the TypeScript API and executes it to assert hideAsync is deferred until hydration resolves, and that a rejected read still warns and dismisses the splash.

The prior cold-start hydration flash is correctly addressed. loadMobileTerminalThemeMode() publishes the hydrated mode synchronously inside its .then before the promise resolves, so by the time hideAsync runs, useSyncExternalStore subscribers have already repainted with the stored choice — and a pane mounted after hydration reads the same snapshot, so there is no ordering gap. The new tests can genuinely fail: deleting the await load... line changes the callback so hideAsync fires immediately, tripping the not.toHaveBeenCalled() assertion rather than passing as theatre.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Startup hydration fix looks right — one minor maintainability note inline.

Reviewed changes

  • mobile/app/_layout.tsx — the root onNavigatorLayout now awaits loadMobileTerminalThemeMode() (with a catch that logs to console.warn) before SplashScreen.hideAsync(), so the persisted appearance is published before the first visible frame. This resolves the cold-start flash from my earlier review.
  • mobile/src/storage/terminal-theme-startup.test.ts — new test asserting the splash stays up until the load settles, and still dismisses (after warning) on a storage failure.

Both splash assertions are real: they would fail if hideAsync were moved ahead of the load, or if the failure path skipped dismissal.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@coderabbitai

coderabbitai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

@kargnas I will review the current head, including the startup hydration sequence and its regression coverage.

⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@casualuser

Copy link
Copy Markdown

Tested this PR on an iOS simulator (iPhone 11, iOS 27.0):

  • Appearance Switching: Verified that useColorScheme() reacts properly with useMobileTerminalTheme. Toggling system appearance between light and dark modes (xcrun simctl ui ... appearance light/dark) updates the resolved theme mode dynamically as expected.
  • Hydration & Startup: Verified the startup sequence in mobile/app/_layout.tsx (onNavigatorLayout awaiting loadMobileTerminalThemeMode() before SplashScreen.hideAsync()). No theme flash or flicker observed during cold starts.
  • Tests & Quality:
    • Full mobile test suite passes cleanly: 520 test files, 4,224 passed tests.
    • pnpm --filter orca-mobile typecheck and oxlint pass with zero findings.

Since the initial PR description noted that iOS native testing hadn't been run yet, confirming here that iOS runtime behavior and preference hydration work cleanly!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the two commits that landed since the prior pullfrog review — a rebase onto current main plus a test-adaptation commit. The feature code is unchanged from the previously reviewed revision; the delta is test-only.

  • Rebased the branch onto current main (27b823f) — squashed into one feature commit (949bc801) plus one test commit (e9f8b974); the conflicts were confined to mobile/package.json and mobile/pnpm-lock.yaml, and the terminal-appearance behavior is unchanged.
  • Adapted the mobile tests to main's test typechecking — tests now query by mocked component bindings (ActionSheetModal, PickerModal, TerminalWebView) instead of string element types, and the type assertions are gone.
  • Pinned the menu test to English (terminalAppearanceI18n.changeLanguage('en') in beforeAll) so its label assertions no longer depend on the machine locale.
  • Added terminal-theme-preference-page.test.ts — runs the preference against the session page's real storage bridge and asserts the un-allowlisted orca:terminalThemeMode write is dropped with storage-write-dropped and the page still starts in Automatic (phone).

The adapted tests remain meaningful: the menu tests fail if the picker options or the save path change, and the new page test fails if the key were added to the page allowlist or if a refused write rejected instead of resolving. Prior review threads are all resolved; no new concerns.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: bb9e75d4-548c-4246-a5bf-1a0265207a8f

📥 Commits

Reviewing files that changed from the base of the PR and between f795819982b7da78b94585562ec049f294dba27b and e9f8b9742248855068fea47f16957d59b4774ee8.

⛔ Files ignored due to path filters (1)
  • mobile/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • mobile/README.md
  • mobile/app/_layout.tsx
  • mobile/package.json
  • mobile/src/session/MobileSessionHeader.tsx
  • mobile/src/session/MobileSessionHeaderMoreActionsSheet.test.tsx
  • mobile/src/storage/terminal-theme-preference-page.test.ts
  • mobile/src/storage/terminal-theme-startup.test.ts
  • mobile/src/terminal/use-mobile-terminal-theme.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/README.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Mobile terminals used the palette published by the connected desktop,
so switching the phone to light mode could leave the console dark.

Add a device-local terminal appearance preference (Automatic (phone),
Dark, Light, Match desktop) exposed from the session "More" menu.
Automatic follows the phone's color scheme; Match desktop keeps the
previous host-provided palette. Mounted and hidden panes update in
place, and the saved preference is hydrated before the native splash
is dismissed so the first frame uses the right palette.

The default dark/light palettes move to src/shared so mobile and
desktop reuse the same values; desktop catalog names and colors are
unchanged. Menu labels are translated with i18next.
Upstream now gates mobile tests with a tsc ratchet and rejects type
assertions in changed code. The ratchet refused the string element
types these tests passed to findByType, so query by the mocked
component bindings instead: they match the same host elements at
runtime and type as components. The test-state and AST casts become a
typed hoisted factory, a ts.isCallExpression guard and a declared
return type.

The menu labels resolve from the device locale, so the English
assertions failed on a non-English machine; pin the i18n instance to
English for the test.

Main can also serve the session route as a hybrid-shell page, whose
storage holds only allowlisted keys. Cover the preference there
through the page's real AsyncStorage: the page is not handed the saved
choice, so it follows the phone, and a choice made in the page applies
until the page closes while its write is dropped without rejecting.
@kargnas
kargnas force-pushed the feat/mobile-phone-terminal-appearance branch from e9f8b97 to 631eae4 Compare September 30, 2026 07:32
@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Adds terminal theme preference UI and storage to the mobile app.

The PR is not ready to merge while hybrid-shell sessions present a preference that they cannot save or restore.

Findings

  1. P1 Page appearance choices do not persist ▶
  2. P2 Storage can hold the splash ▶

Summary

The PR adds device-local terminal appearance modes, live recoloring of mounted panes, a session-menu picker, and localized labels.

  • Native sessions load the preference before dismissing the splash.
  • Hybrid-shell session pages cannot currently read or persist that preference.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Choice[Appearance selection] --> Store[Preference store]
  Store --> Native[Native session: device AsyncStorage]
  Store --> Page[Hybrid session: page storage bridge]
  Native --> Theme[Mounted terminal theme]
  Page --> Denied[Key not allowlisted: default on open; write dropped]
Loading

Reviews (1) · Last reviewed commit: "test(mobile): adapt terminal appearance ..."

Comment thread mobile/app/_layout.tsx Resolved

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one stale documentation reference inline.

Reviewed changes

This run reviewed the two commits that landed since the prior pullfrog review — a second rebase onto current main (fceca5c) plus the same test-adaptation commit. The feature code is otherwise unchanged from the previously reviewed revision; the material delta is where the shared palettes now resolve.

  • Rebased onto current main (fceca5c) — squashed into one feature commit (84110e9a56) plus one test commit (631eae4415).
  • Dropped the PR's own palette move — the previously added src/shared/terminal-default-themes.ts is gone, and use-mobile-terminal-theme.ts (plus its test) now imports DEFAULT_TERMINAL_THEMES from src/shared/terminal-themes/defaults.ts, which main already hosts. Same two palettes (Ghostty Default Style Dark, Builtin Tango Light), values unchanged; the PR is now mobile-only plus the i18n catalog.

All prior review threads are resolved, and the adapted tests remain meaningful.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread mobile/README.md Resolved
리뷰 반영 세 건. 페이지 allowlist에 orca:terminalThemeMode가 빠져 hybrid-shell
페이지에서 선택이 저장되지 않던 것을 allowlist 추가로 해소하고, 페이지 테스트를
새 계약(전달된 모드 적용, 브리지를 통한 저장)으로 갱신했다. 콜드 스타트에서
저장소 읽기가 거절 대신 멈추면 스플래시가 영구 유지되므로 2초 상한을 뒀다.
README의 팔레트 경로를 현재 위치로 고쳤다.

Constraint: 페이지 storage는 allowlist와 init 전달 값으로만 동작한다
Tested: vitest storage/page-bridge 6개 파일 54개, 품질 게이트 통과
Not-tested: 실기기 cold start에서의 스플래시 타이밍

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 45871bfd-204c-4a9c-a887-805ae0d1af4b
📥 Commits

Reviewing files that changed from the base of the PR and between 631eae4 and d7f6184.

📒 Files selected for processing (4)
  • mobile/README.md
  • mobile/app/_layout.tsx
  • mobile/src/mobile-web-shell/page-storage-keys.ts
  • mobile/src/storage/terminal-theme-preference-page.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/README.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Adding orca:terminalThemeMode to the page allowlist makes the shell send it in init.storage, and a page bundle built before this key refuses the whole init frame — so the hybrid-shell session page fails to open on a newer phone against an older desktop. The PR body already documents this exact hazard as the reason the key was withheld, and its "older desktop page bundles are unaffected" claim no longer holds. Details inline.

Reviewed changes

This run reviewed the single commit that landed since the prior pullfrog review (631eae4 → d7f6184): the hybrid-shell allowlist change, a bound on the cold-start appearance read, and a README path fix.

  • Granted orca:terminalThemeMode to the hybrid-shell page (mobile/src/mobile-web-shell/page-storage-keys.ts) — the session page can now read the saved mode from init and persist a selection through the bridge; the page test was rewritten to assert both crossing and persistence, and reworked its mock to survive vi.resetModules.
  • Bounded the cold-start appearance read (mobile/app/_layout.tsx) — a 2s Promise.race now caps how long a stalled storage read can hold the native splash before hideAsync.
  • Corrected the README's shared-palette path — mobile/README.md now points at src/shared/terminal-themes/defaults.ts, matching what use-mobile-terminal-theme.ts imports.

ℹ️ Nitpicks

  • mobile/app/_layout.tsx:196-199 — the new 2s splash cap has no test; terminal-theme-startup.test.ts still exercises only the awaited path, so removing or breaking the bound would pass CI.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

직전 커밋에서 orca:terminalThemeMode를 PAGE_STORAGE_EXACT_KEYS에 넣었지만,
구버전 데스크톱이 서빙한 페이지 번들은 BridgeInitStorageSchema의
refine(isPageStorageKey)로 init.storage를 검사해 모르는 키가 하나라도 있으면
init 프레임 전체를 거부한다. 그래서 새 앱과 구버전 데스크톱을 함께 쓰면서
테마를 저장한 사용자는 세션 페이지가 열리지 않게 되었다. PR 본문에 적어 둔
원래 설계(페이지에서는 Automatic으로 시작하고 쓰기는 드롭)로 되돌렸고,
페이지 테스트의 holder 하네스는 그대로 유지했다.

Constraint: 구버전 페이지 번들은 init.storage의 모르는 키 하나 때문에 프레임 전체를 거부한다
Rejected: init에서만 키를 빼고 쓰기는 허용 | isPageStorageKeyForRoute와 pageStorageKeysForRoute의 계약이 갈라지는 메인테이너 결정 영역
Directive: 페이지 allowlist에 키를 추가하려면 구버전 번들과의 호환 경로(셸 필터링이나 스키마의 unknown key drop)가 먼저 있어야 한다
Tested: vitest 모바일 전체 903개 파일 통과
Related: d7f6184
terminal-theme-startup.test.ts는 _layout.tsx의 onNavigatorLayout 콜백 소스를
new Function으로 실행하는데, 직전 커밋의 new Promise<null>이 JS에서는 비교식으로
파싱되어 두 테스트가 깨졌다. 콜백을 순수 JS 문법으로 바꾸고 그 이유를 주석으로
남겼으며, 저장소 읽기가 멈춰도 2초 뒤 스플래시가 내려가는지 확인하는 테스트를
추가했다. upstream이 TerminalPaneView에 필수 prop onCellBoxChange를 추가해
use-mobile-terminal-theme.test.ts가 tests-typecheck 래칫에서 실패하던 것도 고쳤다.

Tested: vitest 모바일 전체 903개 파일, check:tests-typecheck, tsc --noEmit, oxlint, oxfmt
Not-tested: 실기기 콜드 스타트의 스플래시 타이밍

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the two commits that landed since the prior pullfrog review (d7f6184 → 1a4076bd). Together they revert the one caution from that review and close its nitpick.

  • Withdrew orca:terminalThemeMode from the hybrid-shell page allowlist (012a2d32d8) — page-storage-keys.ts no longer grants the key, so the shell never puts it in init.storage and a page bundle built before the key keeps accepting the frame. This restores the mixed-version safety the previous review flagged as [!CAUTION], and the PR body's withhold-the-key description is accurate again.
  • Restored the page test to the withdraw behavior — terminal-theme-preference-page.test.ts now seeds the key and asserts the page still reports system, and that a selection applies for the page lifetime while the write is dropped with storage-write-dropped / refusal: 'not-allowed'. Both assertions would fail if the key were re-allowlisted or a refused write rejected.
  • Added a fake-timer case for the 2s splash bound (1a4076bd4b) — terminal-theme-startup.test.ts now advances 2000 ms against a never-settling read and asserts hideAsync runs, covering the previously untested timeout branch.
  • Fixed mobile test typecheck errors — dropped the new Promise<null> generic from the splash callback (it is plain JS on purpose, since the startup test evaluates its source via new Function) and added the onCellBoxChange noop that TerminalPaneViewProps requires.

The two open questions the caution left with the author were resolved by choosing the conservative path: the key stays withheld, and the hybrid-shell persistence limitation remains documented as a maintainer follow-up rather than shipped. The prior caution thread was replied to and resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

모바일의 다른 화면은 모두 영어 문자열을 쓰는데, 이 메뉴만 i18next와 7개 언어 카탈로그를 새로 들여와 리뷰 쟁점과 번들 크기를 늘리고 있었다. 메뉴 문구를 다른 화면처럼 영어 상수로 옮겼고, 함께 번역되던 Agent History와 Checks 항목은 main의 문구로 되돌렸다. 그 결과 package.json과 lockfile은 main과 같아졌다.

Rejected: i18next 유지 | 모바일 첫 i18n 도입을 기능 PR 안에서 하면 새 런타임 의존성과 카탈로그가 리뷰 범위를 키움
Tested: mobile tsc --noEmit, 시트·테마 관련 vitest 9개 파일 87개 통과, 변경 파일 oxlint·oxfmt --check
Not-tested: 기기 실행

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run reviewed the one commit that landed since the prior pullfrog review (1a4076bd → d22e61f765): removal of the i18next setup that the feature commit had introduced.

  • Removed the i18next dependency and locale catalogs (mobile/package.json, mobile/pnpm-lock.yaml, deleted mobile/src/i18n/terminal-appearance.ts and 7 *.json catalogs) — both dependency files are byte-identical to main again, so the PR is mobile-only.
  • Replaced the translated menu with English constants (MobileSessionHeaderMoreActionsSheet.tsx) — APPEARANCE_TITLE, MODE_LABELS, and MODE_HINTS now carry the same strings the catalog held, and the Agent History / Checks entries return to main's wording.
  • Dropped the locale pin from the menu test (MobileSessionHeaderMoreActionsSheet.test.tsx) — the beforeAll changeLanguage('en') call and the i18n import are gone; assertions compare against the literal constants.

This matches the repo convention that mobile has no localized UI, so the removal is the right call rather than a regression. No remaining reference to i18next, terminalAppearanceText, or the deleted catalog exists under mobile/. The menu tests still fail if the picker options or the save path change. All prior Pullfrog threads are resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@oscarmorrison

Copy link
Copy Markdown

Any update on this it's looking good

터미널 테마 선택을 현재 세션 메뉴에 통합하고, 저장된 테마를 앱 외관 및 언어와 함께 초기화했다. 분할 터미널과 숨겨진 탭의 현재 생명주기를 보존하면서 같은 WebView의 테마를 갱신하도록 맞췄다.

Constraint: 현재 세션 메뉴, 분할 화면, 입력 활성 상태와 탭 지연 마운트를 유지했다.
Tested: 핵심 테스트 36개와 연관 테스트 44개, 모바일 타입 및 테스트 타입 검사, 변경 파일 oxlint 검사를 통과했다.
Not-tested: 실제 Android 및 iOS 화면은 이번 변경에서 확인하지 않았다.

This branch has not been deployed

No deployments
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.

[Feature]: Mobile: terminal colors should follow the phone's light/dark setting

3 participants