feat(mobile): Android mobile app (v1) - #86
Open
thangnm93 wants to merge 96 commits into
Open
Conversation
_doSave() now returns Future<String> (the saved host's id). For new hosts the id comes from the Host object built before addHost is called; for edits it is existing.id. _saveAndConnect uses the returned id to call byId() instead of the fragile allHosts.last assumption.
- _importKey now shows _ImportPassphraseSheet after file pick; calls savePassphrase(entry.id, passphrase) when non-empty, with context.mounted guards across every await. - New app/lib/mobile/util/ssh_fingerprint.dart: sha256Fingerprint() parses the OpenSSH public key line, SHA256-hashes the raw blob via the crypto package, and returns the canonical 'SHA256:<base64>' form (no '=' padding); returns null for unparseable input. - KeyCard._fingerprintLine replaced truncated-base64 display with the real fingerprint; falls back to 'SHA256:(pending)' for null. - New test/mobile/util/ssh_fingerprint_test.dart: 7 cases covering the known vector, no-comment variant, empty/single-token/invalid-base64 inputs, and extra-whitespace handling. - keys_screen_test.dart: seed publicKey values updated to realistic OpenSSH key lines; comment typo '1 keys' → '1 key' fixed.
Rewrites MobileSettingsScreen with ListGroup/SettingsRow primitives: TERMINAL (TerminalAppearanceControls embedded), SECURITY (biometric toggle on kAppLockPrefKey, auto-lock chevron), KEYBOARD & SYNC (shortcut key bar toggle, Supabase sync sheet, P2P QR pair). Sync banner card reflects isSupabaseConfigured. Footer shows version via PackageInfo. Preserves full Supabase config + P2P QR import logic via bottom sheet. Wires Settings tab in MobileHomeShell.
- Introduce kAccessoryBarPrefKey ('mobile_accessory_bar_enabled') in
app/lib/mobile/util/mobile_prefs.dart; wire the Shortcut key bar
toggle to read/write this key via SharedPreferences instead of the
desktop-only shellIntegrationEnabled flag
- Fix sync-sheet pull spinner: wrap _SupabaseSyncSheet in StatefulBuilder
so setSheetState drives the in-sheet spinner/disabled state during an
in-flight pull (parent setState never reached the modal builder context)
- Upgrade test pump: register SettingsProvider, HostProvider, SyncService
so the screen never throws ProviderNotFoundException; add two pref-key
persistence tests (biometric → kAppLockPrefKey, shortcut bar →
kAccessoryBarPrefKey)
Both home-shell test helpers were missing SnippetProvider, KeyProvider, KeyGenService, SettingsProvider, SyncProvider, SyncService, and SshService. MobileHomeShell uses an IndexedStack so all 4 tab screens build eagerly; each screen's provider dependencies must be satisfied at the shell level. Also switches the 'Settings tab is reachable' assertion from tester.widget to tester.firstWidget: TerminalAppearanceControls renders a DropdownButton which Flutter implements via IndexedStack internally, causing find.byType(IndexedStack) to match multiple widgets once the Settings screen is fully rendered.
Replace MobileSessionsScreen with MobileTerminalScreen: adds amber header bar with back/split/⋮ buttons, session-tab strip with active/inactive states and a "+" tile, accessory-bar pref wiring (kAccessoryBarPrefKey), and stub ⋮ menu items for Files and Port forwarding. Preserves pinch-zoom, CursorDragMapper long-press drag, and showTerminalSidePanel.
…ive picker list - pop-on-empty: re-check sshSessions.isEmpty at postFrameCallback fire time and reset _popScheduled so a later empty→fill→empty cycle still triggers a pop - re-entry path: use existing.last.id (matches new-connect path which uses after.last.id) - host picker: read HostProvider.allHosts inside the sheet builder so the list is live while the sheet is open, not a stale snapshot - tests: add T5 (pop-on-empty) and T6 (no-spurious-pop) to terminal_nav_test.dart; fix pre-existing __ lint in probe connector lambda
MobileSftpScreen is now contextual: accepts a required Host param instead of reading SessionProvider.activeSshSession. Design 04 applied: header with back + "Files / <host>" + upload + ⋮; breadcrumb row (home icon, mono segments, current bold); meta row with item count, total size, and cycling sort control (Name/Size ↑↓); list rows with typed icons (folder=blue, dotfiles/secrets=lock yellow, archives/images/code), mono names for dotfiles, file sizes in mono, chevrons for dirs, hairline dividers inset 47px. SFTP browsing logic (single reusable SftpClient, load-token, listdir, chunked download, SftpTransferService upload) preserved unchanged. Terminal ⋮ "Files" entry now calls onOpenFiles(host) with the active session's host; MobileHostsScreen passes a callback that pushes MobileSftpScreen(host).
Add PortForwardProvider + PortForwardService to MobileBootstrap, mirroring the desktop wiring in main.dart: onStatus/onConnections callbacks feed the provider, autoStartAll fires after provider.ready, and onHostDeleted tears down tunnels. SnippetProvider and KeyGenService were already present. Providers list grows from 11 → 13 entries.
Add a testWidgets test that pumps the real provider tree and calls Provider.of<T> from a child BuildContext, catching type-param or Provider.value vs ChangeNotifierProvider.value mismatches that field-access checks cannot detect. Remove the misleading comment that discouraged the real test in favour of the indirect list-length check.
Introduces MobileSyncScreen (Screen 09): heading, P2P QR export card (reuses the desktop P2P start-server + base64-key JSON format), E2E Supabase status card, and "Scan QR code" button. Settings "Supabase sync" and "Pair new device" rows now push MobileSyncScreen. Scan header reskinned to MobileColors/mobileHeading.
Reject empty URL, non-http/https URL, and empty anon key in _saveConfig instead of writing broken state to SyncProvider. Shows an inline error (keyed sync-config-error) and keeps the form open. Sync code remains optional (unchanged). 4 new widget tests cover the three rejection paths and the valid round-trip (including setSyncCode exercised with a Crockford-safe code).
Fix 8 lint issues in mobile test files: - unnecessary_underscores in 3 files (use named params instead of _/__) - unused_local_variable noSession in snippets test (use it as provider value) - prefer_function_declarations_over_variables in probe test (clock fn) Close tracked minor gaps: - Replace raw SizedBox(height:4) with MobileTokens.space1 in snippets screen - Add probeAll debounce unit test (concurrent duplicate ids not double-probed) - Add widget test asserting _EntryRow shows Icons.lock_outline for .env file Regression confirmed: AppLockGate wraps app in mobile_app.dart, TofuWatcher in mobile_home_shell.dart, pinch-zoom + CursorDrag in terminal screen, P2P QR import reachable via mobile_qr_scan_screen / mobile_sync_screen, no dangling refs to deleted mobile_sessions_screen.dart.
…assphrase, remove dead-end controls
Finding 1: add KeyGenService to MobileBootstrap providers list — opening
the Generate sheet no longer throws ProviderNotFoundException at runtime.
Finding 2: capture addKeyFromFile return value in _GenerateSheetState._generate
and call keyProv.savePassphrase when passphrase is non-empty — mirrors the
Import flow; generated passphrase-protected keys are now usable after restart.
Finding 3: remove all no-op / misleading interactive controls —
- port_forward_screen: replace empty PopupMenuButton with SizedBox
- sync_screen: remove placeholder more_vert IconButton from AppBar
- terminal_screen: remove dead Split IconButton (mobile has no split)
- hosts_screen: replace no-op _CircleButton header items with non-interactive
_DecorativeCircle; remove now-dead _CircleButton class
- settings_screen: remove no-op tune AppBar action; render Auto-lock row as
display-only (drop onTap/chevron since the feature is not built)
Finding 4: add MobileColors.fieldFill (0xFF1C1C1E) and MobileColors.tabInactive
(0xFF6D6D72) tokens; replace all hardcoded occurrences in mobile_hosts_screen,
mobile_keys_screen, and mobile_tab_bar with the tokens.
Tests: extend mobile_bootstrap_test to assert KeyGenService resolves from the
provider tree; add two unit tests in keys_screen_test asserting savePassphrase
is called iff passphrase is non-empty.
…, safe to delete later)
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.
Summary
Adds Android as a target of the existing
app/Flutter project — a focused mobile SSH client (v1) that reuses the platform-agnostic core and adds a mobile UI layer. Built as 5 milestones (M1–M5). Closes #85.Changes
android/target;main()branches onisMobilePlatform(newlib/platform/runtime_platform.dart) to mountYourSSHMobileAppand skip the desktop-only bootstrap.compileSdk 36for transitive plugins; build-artifact gitignores.MobileBootstrap(reuses existing providers/services); hosts list + search + connect, minimal add-host form;TerminalViewwith an accessory key bar (sticky Ctrl/Alt, ^C/^D, arrows, chars) and pinch-to-zoom; multi-session strip.mobile_scanner, camera permission); transfer-code parser.file_picker); snippets quick-insert sheet.local_auth), TOFU host-key-mismatch dialog, terminal appearance settings, release signing viakey.properties(debug fallback).docs/superpowers/; roadmap + README note Android v1 (in development).Desktop (macOS/Windows/Linux) code paths are untouched — the entire widget tree branches at the root, and only the shared
main()bootstrap is platform-gated.Type of change
feat— new featureHow was this tested?
cd app && flutter analyze— no new warnings (2 pre-existingintegration_testprobe warnings remain, unrelated)cd app && flutter test— all 1543 tests pass (33 new mobile widget/unit tests, TDD)cd app && flutter build apk --debug— builds greenScreenshots
N/A this pass (no on-device run yet); mobile UI is widget-test-covered.
Checklist
Notes for reviewers
mobile_scanner+local_authreintroduce native plugins into desktop bundles (single shared pubspec) — accepted v1 tradeoff.Update — Claude Design import redesign
The mobile UI was reworked to match the imported Claude Design "SSH Client" (dark, Apple-inspired, amber accent), on top of the v1 milestones above:
PortForwardServicewired into the mobile bootstrap.KeyGenServiceinto the bootstrap (Generate no longer crashes), persist generated-key passphrase, real OpenSSH SHA256 fingerprints, removed dead-end controls, balanced filter chips.Spec & plan:
docs/superpowers/specs/2026-06-26-mobile-design-import-design.md,docs/superpowers/plans/2026-06-26-mobile-design-import.md.Verified on the Android emulator (API 36): Hosts, Snippets, a live SSH session, Settings, and the launcher icon.
flutter analyzeclean (lib/mobile+test/mobile); fullflutter testgreen (168 mobile tests).