Skip to content

Maple 3.0: Native Rust/SwiftUI/Compose/iced app#462

Open
AnthonyRonning wants to merge 9 commits intomasterfrom
native-3.0
Open

Maple 3.0: Native Rust/SwiftUI/Compose/iced app#462
AnthonyRonning wants to merge 9 commits intomasterfrom
native-3.0

Conversation

@AnthonyRonning
Copy link
Contributor

@AnthonyRonning AnthonyRonning commented Mar 7, 2026

Maple 3.0 Native App

Introduces native/ directory alongside existing frontend/ (2.0 Tauri), enabling both to coexist during the transition period.

What's included

  • Rust core (maple_core) — all business logic: single-agent chat, SSE streaming, session persistence, pagination, settings/delete agent
  • iOS (SwiftUI, iOS 26) — native Liquid Glass design, .glassEffect() floating islands
  • Android (Jetpack Compose, Material 3) — simulated frosted glass, translucent floating pills
  • Desktop (iced 0.14) — simulated frosted glass, gradient backgrounds, floating pill containers
  • rmp-cli build tooling + Nix flake
  • Architecture bible and comprehensive README

Key decisions

  • Bundle IDs: cloud.opensecret.maple — matches 2.0's App Store/Play Store listings for upgrade continuity
  • Version: 3.0.0 (Android versionCode 3000000000, above 2.0's 2000019000)
  • SDK: opensecret = "3.0.0-alpha.0" from crates.io (no submodule)
  • Team ID: X773Y823TN (matching 2.0)

Design system

  • iOS: Native iOS 26 Liquid Glass (GlassEffectContainer, .glassEffect(), safeAreaInset)
  • Android/Desktop: Simulated frosted glass (translucent containers, subtle borders, scale animations)
  • Brand kit: Manrope/Array fonts, MPL icon, Maple/Pebble/Bark/Grove/Neutral color scales
  • Floating glass islands for header (centered wordmark + gear) and compose bar on all platforms

Relationship to 2.0

frontend/ (2.0) is completely untouched. Both coexist. When 3.0 is ready for production, it replaces 2.0 on the same store listings.

Build & run

just native-run-ios
just native-run-android
just native-run-desktop

Open with Devin

Summary by CodeRabbit

  • New Features

    • Native apps for Android, iOS, and desktop with full platform UIs
    • Email/password and OAuth (GitHub, Google, Apple) authentication
    • Chat with streaming responses, message history, auto-scroll, toasts, and compose bar
    • Agent management: settings, delete flow, secure token persistence
    • Polished design system and branding (wordmark, app icon, themed colors)
  • Documentation

    • Native README and example environment file added for local setup

AnthonyRonning and others added 3 commits March 6, 2026 20:12
Introduces native/ directory alongside existing frontend/ (2.0 Tauri).
All bundle IDs aligned to cloud.opensecret.maple for App Store continuity.
SDK dependency switched from submodule to crates.io opensecret 3.0.0-alpha.0.
Version set to 3.0.0 across all platforms.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

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

Walkthrough

Adds a full "native" Maple 3.0 platform: a Rust FFI core with UniFFI bindings, Android/iOS/desktop frontends and UIs, an rmp-cli for binding/build/run workflows, Nix dev shell and Justfile recipes, platform projects/resources, and CI packaging/build scripts.

Changes

Cohort / File(s) Summary
Repo tooling & workspace
\.gitignore, justfile, native/.env.example, native/.gitignore, native/rmp.toml, native/Cargo.toml
Add native ignore rules and example env; introduce workspace manifests and a comprehensive justfile with native build/run/bindings targets.
Nix dev environment
native/flake.nix
Add Nix flake defining rustToolchain (multi-target), Android SDK/NDK integration, rmp helper script, and an extensive dev shell hook.
Rust core (FFI) & UniFFI
native/rust/..., native/rust/uniffi.toml, native/uniffi-bindgen/*
Add FfiApp Rust core (state, actions, updates, auth, streaming messaging), uniffi configuration and a bindgen helper crate.
rmp-cli & orchestration
native/rmp-cli/Cargo.toml, native/rmp-cli/src/..., native/rmp-cli/README.md
Add rmp-cli binary with CLI, init scaffolding, doctor/devices/bindings/run flows, config loader, and binding/build orchestration.
Desktop (iced)
native/desktop/iced/Cargo.toml, native/desktop/iced/src/..., native/desktop/iced/src/theme.rs
Add iced-based desktop client, AppManager/reconciler, keyring persistence, full UI implementation and theming.
Android build infrastructure
native/android/build.gradle.kts, native/android/gradle.properties, native/android/settings.gradle.kts, native/android/app/build.gradle.kts
Add top-level and module Gradle Kotlin DSL files, properties, plugin declarations, build config, and a UniFFI generation check task.
Android app code & UI
native/android/app/src/main/java/.../AppManager.kt, .../MainActivity.kt, ui/.../MainApp.kt, ui/.../theme/*
Add AppManager bridging to Rust (EncryptedSharedPreferences), MainActivity, Jetpack Compose UI, theme and colors.
Android UniFFI bindings (autogen)
native/android/app/src/main/java/.../rust/maple_core.kt
Add very large autogenerated UniFFI Kotlin binding layer (converters, handles, callbacks, types) for Kotlin↔Rust interop.
Android resources
native/android/app/src/main/res/...
Add wordmark drawable, strings, and theme resources.
iOS project & UI
native/ios/project.yml, native/ios/Info.plist, native/ios/Sources/*, native/ios/Sources/Assets.xcassets/*
Add XcodeGen project, Info.plist, SwiftUI app entry, AppManager with Keychain persistence, ContentView and UI components, theme, wordmark, and assets.
Android/iOS binding & run integration
native/rmp-cli/src/bindings.rs, native/rmp-cli/src/run.rs, native/justfile
Add multi-target binding generation and run orchestration (host builds, xcframework, .so/.apk assembly, emulator/simulator management) and Justfile coordination.
CI workflows & packaging scripts
.github/workflows/*, native/scripts/ci/*
Add GitHub Actions for Android/iOS/desktop beta builds and many CI packaging scripts (Android release, iOS TestFlight, macOS/Linux packaging) plus shared CI helpers.
Project scaffolding templates
native/rmp-cli/src/init.rs
Add project initialization scaffolding to generate multi-platform template projects and many platform files under native/.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Native UI
    participant AM as AppManager (platform)
    participant Rust as Rust Core (FfiApp)
    participant API as OpenSecret API
    participant KS as Secure Storage

    rect rgba(100,150,200,0.5)
    UI->>AM: initialize / getInstance()
    AM->>KS: load persisted tokens
    AM->>Rust: FfiApp::new(api_url, client_id)
    Rust->>API: initialize client
    AM->>Rust: listen_for_updates(self)
    Rust-->>AM: AppUpdate events
    AM-->>UI: update state
    end

    rect rgba(150,100,200,0.5)
    UI->>AM: dispatch(LoginWithEmail)
    AM->>Rust: dispatch(LoginWithEmail)
    Rust->>API: auth.login_with_email()
    API-->>Rust: tokens
    Rust-->>AM: AppUpdate::SessionTokens
    AM->>KS: persist tokens
    AM-->>UI: state becomes LoggedIn
    end

    rect rgba(200,150,100,0.5)
    UI->>AM: dispatch(SendMessage)
    AM->>Rust: dispatch(SendMessage)
    Rust->>API: send message / receive stream
    API-->>Rust: AgentMessageChunk (stream)
    Rust-->>AM: AppUpdate::FullState (streaming updates)
    AM-->>UI: update messages / typing indicators
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐇 I hopped through crates and platform doors,

Rust in my pocket, bindings on my paws.
Simulators hum and SwiftUI glows,
Compose and Canvas where the maple grows.
Hooray — tiny rabbit, off to build we go!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch native-3.0

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +805 to +818
Ok(opensecret::types::AgentSseEvent::Done(_)) => {
InternalEvent::AgentDone
}
Ok(opensecret::types::AgentSseEvent::Error(err)) => {
InternalEvent::AgentError(err.error)
}
Err(e) => {
InternalEvent::AgentError(e.to_string())
}
};
let _ = tx.send(CoreMsg::Internal(Box::new(internal)));
}
// If stream ends without a Done event, finalize
let _ = tx.send(CoreMsg::Internal(Box::new(InternalEvent::AgentDone)));
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 7, 2026

Choose a reason for hiding this comment

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

🟡 Duplicate AgentDone event sent unconditionally after SSE stream ends

In the agent chat streaming task at native/rust/src/lib.rs:866-869, an InternalEvent::AgentDone is sent unconditionally after the while let loop over the SSE stream finishes. However, when the stream contains a Done SSE event (line 856-857), AgentDone is already sent inside the loop. When the stream then ends naturally, a second AgentDone is sent. This causes a redundant state emission cycle (rev increment + full state clone + FFI callback), and could confuse platform-side logic that tracks typing state transitions.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (24)
native/rmp-cli/README.md-7-9 (1)

7-9: ⚠️ Potential issue | 🟠 Major

The referenced documentation files are missing from the repository.

The README references three documentation files that do not exist:

  • docs/rmp.md
  • docs/rmp-ci.md
  • docs/architecture.md

Either include these documentation files in the PR or remove the references from the README.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/README.md` around lines 7 - 9, The README references missing
documentation files—update the PR so the links resolve: either add the three
files (`docs/rmp.md`, `docs/rmp-ci.md`, `docs/architecture.md`) with appropriate
content and commit them, or remove/update the three link entries in
native/rmp-cli/README.md that point to those filenames; ensure the README no
longer contains broken links to `docs/rmp.md`, `docs/rmp-ci.md`, or
`docs/architecture.md`.
native/android/app/src/main/AndroidManifest.xml-6-12 (1)

6-12: ⚠️ Potential issue | 🟠 Major

Disable Android backups for persisted chat data, or add explicit exclusions.

This app persists sessions/settings, so android:allowBackup="true" opts that data into device or cloud backup unless backup rules exclude it. For a chat client, that is an avoidable privacy leak.

Suggested change
-        android:allowBackup="true"
+        android:allowBackup="false"

If you need backup for non-sensitive preferences, add fullBackupContent / dataExtractionRules and explicitly exclude conversations, session state, and tokens.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/AndroidManifest.xml` around lines 6 - 12, The
AndroidManifest currently sets android:allowBackup="true" on the <application>
tag which allows app data (including persisted chat sessions/settings) to be
backed up; change this to disable backups or add explicit backup rules: either
set android:allowBackup="false" on the <application> element, or keep
allowBackup but add android:fullBackupContent or android:dataExtractionRules
pointing to an XML resource that explicitly excludes conversation storage,
session tokens, and other sensitive files; update/create the corresponding
full_backup_content or data_extraction_rules XML to list exclusions (e.g.,
exclude databases, prefs holding tokens/conversations).
native/.gitignore-15-17 (1)

15-17: ⚠️ Potential issue | 🟠 Major

Don’t ignore the Gradle wrapper.

For a new Android app, gradlew* and gradle/wrapper/* are what pin the Gradle distribution for every contributor and CI job. Keeping them ignored makes native Android builds depend on local setup.

Suggested change
-android/gradle/
-android/gradlew
-android/gradlew.bat
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/.gitignore` around lines 15 - 17, Remove the Gradle wrapper from being
ignored: delete the entries that ignore gradlew and the android/gradle/
directory in .gitignore, and instead ensure the Gradle wrapper files (gradlew,
gradlew.bat, and the gradle/wrapper/* files such as gradle-wrapper.properties
and gradle-wrapper.jar) are tracked in git so the project pins the Gradle
distribution for all contributors and CI; update .gitignore to only ignore build
artifacts, not the wrapper files.
native/android/build.gradle.kts-1-4 (1)

1-4: ⚠️ Potential issue | 🟠 Major

Update to latest stable plugin versions.

The code uses AGP 8.5.1 and Kotlin 1.9.24, which are outdated. As of March 2026, the latest stable versions are AGP 9.1.0 (released this month) and Kotlin 2.3.10 (released February 2026). Update both plugins to maintain compatibility with current tooling and benefit from recent fixes and features:

plugins {
    id("com.android.application") version "9.1.0" apply false
    id("org.jetbrains.kotlin.android") version "2.3.10" apply false
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/build.gradle.kts` around lines 1 - 4, Update the plugin
version numbers in the plugins block: change the com.android.application plugin
version used with id("com.android.application") to "9.1.0" and change the Kotlin
Android plugin version used with id("org.jetbrains.kotlin.android") to "2.3.10";
ensure both apply false remain unchanged and run a Gradle sync/build to verify
compatibility after the update.
native/ios/Sources/ContentView.swift-122-127 (1)

122-127: ⚠️ Potential issue | 🟠 Major

Guard submit() against repeat keyboard submits.

The button is disabled while isLoading, but Line 127 still calls submit() from the password field. Repeated Return presses can dispatch duplicate auth requests unless submit() enforces the same guard itself.

🔐 Keep the handler consistent with the button state
     private func submit() {
+        guard !isLoading, !email.isEmpty, !password.isEmpty else { return }
         if isSignUp {
             manager.dispatch(.signUpWithEmail(email: email, password: password, name: name))
         } else {

Also applies to: 134-145, 200-205

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/ContentView.swift` around lines 122 - 127, The submit()
handler must early-return when an auth request is already in progress; add a
guard at the top of submit() that checks the same isLoading flag used to disable
the button and returns immediately if true, so keyboard Return presses cannot
dispatch duplicate requests. Update the submit() method referenced by the
SecureField .onSubmit(submit) (and other .onSubmit usages) to atomically set
isLoading before starting async work and clear it on all exit paths (success,
failure, cancellation) to match the button state.
native/rmp-cli/src/init.rs-593-653 (1)

593-653: ⚠️ Potential issue | 🟠 Major

The generated iOS recipes assume Xcode lives at one fixed path.

Hard-coding /Applications/Xcode.app/Contents/Developer breaks just ios-* on common setups like Xcode-beta.app, renamed installs, or xcode-select/DEVELOPER_DIR overrides. The scaffold should discover or inherit the active developer dir instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/init.rs` around lines 593 - 653, The template hard-codes
XCODE_DEV and TOOLCHAIN_BIN; change it to detect or inherit the active developer
dir (use DEVELOPER_DIR if set, otherwise fall back to xcode-select -p) and
derive TOOLCHAIN_BIN from that so ios-rust, ios-build, ios-xcframework and
ios-full work for Xcode-beta/renamed installs and xcode-select overrides; update
the XCODE_DEV/TOOLCHAIN_BIN definitions in the generated string (and any uses
like DEVELOPER_DIR="{{XCODE_DEV}}" and SDK paths) to compute the path
dynamically from DEVELOPER_DIR or xcode-select.
native/ios/Sources/ContentView.swift-332-347 (1)

332-347: ⚠️ Potential issue | 🟠 Major

Loading older messages snaps the list back to the bottom.

Pagination from Line 333 and Line 347 prepends older history, but this hook still sees a higher message count and scrolls to last.id. That makes upward history browsing effectively unusable.

🧭 Auto-scroll on tail changes, not any count increase
-            .onChange(of: manager.state.messages.count) { oldCount, newCount in
-                if newCount > oldCount {
-                    if manager.state.isAgentTyping {
-                        withAnimation { proxy.scrollTo("typing-indicator", anchor: .bottom) }
-                    } else if let last = manager.state.messages.last {
-                        withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
-                    }
-                }
+            .onChange(of: manager.state.messages.last?.id) { oldLastId, newLastId in
+                guard newLastId != oldLastId else { return }
+                if manager.state.isAgentTyping {
+                    withAnimation { proxy.scrollTo("typing-indicator", anchor: .bottom) }
+                } else if let last = manager.state.messages.last {
+                    withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
+                }
             }

Also applies to: 365-380

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/ContentView.swift` around lines 332 - 347, The list is
auto-scrolling whenever the message array length changes (including when older
messages are prepended), so stop treating any count increase as a tail append:
only trigger the scroll-to-tail behavior when the last message at the end
actually changes. Locate the logic that scrolls to last.id (the code observing
manager.state.messages and calling scrollTo(last.id)), and change it to compare
the previous last message id with the new last message id (or track a boolean
like isLoadingHistory) and only call scrollTo(last.id) when the last message id
differs (i.e., new messages appended at the tail), not when older messages are
loaded via manager.dispatch(.loadOlderMessages) / the prefetched branch in
MessageBubble onAppear.
native/rmp-cli/src/util.rs-33-65 (1)

33-65: ⚠️ Potential issue | 🟠 Major

Honor xcode-select -p before scanning /Applications.

Many valid setups point xcode-select at Xcode-beta.app or a non-default install location. This helper skips that source entirely, so doctor and run can fail even though /usr/bin/xcrun would work.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/util.rs` around lines 33 - 65, discover_xcode_dev_dir
currently ignores the system xcode-select setting; update it to call
xcode-select -p (e.g., via std::process::Command) and, if that returns a valid
path, construct a PathBuf pointing at its Developer directory and validate it
the same way as DEVELOPER_DIR by checking for usr/bin/xcrun or usr/bin/simctl
and by calling developer_dir_supports_iphoneos(&path); only fall back to
scanning /Applications if xcode-select either fails or yields a path that does
not pass those checks. Ensure you reuse the existing
developer_dir_supports_iphoneos function and the same validation logic used for
DEVELOPER_DIR to keep behavior consistent.
native/desktop/iced/src/main.rs-334-362 (1)

334-362: ⚠️ Potential issue | 🟠 Major

Mirror the disabled-state checks in update().

Line 618 and Line 793 still submit on Enter, but Message::SubmitAuth and Message::SendMessage only trust the button state. That lets keyboard submit send duplicate auth requests or another chat turn while state.is_agent_typing is true.

🛡️ Re-check the same guards in the handler
             Message::SubmitAuth => {
+                if matches!(state.auth, AuthState::LoggingIn | AuthState::SigningUp) {
+                    return Task::none();
+                }
                 if let ScreenState::Login(s) = screen {
                     if s.is_sign_up {
                         manager.dispatch(AppAction::SignUpWithEmail {
@@
             Message::SendMessage => {
                 if let ScreenState::Chat { compose } = screen {
-                    if !compose.trim().is_empty() {
+                    if !state.is_agent_typing && !compose.trim().is_empty() {
                         manager.dispatch(AppAction::SendMessage { content: compose.clone() });
                         *compose = String::new();
                     }
                 }
             }

Also applies to: 615-633, 782-795

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/desktop/iced/src/main.rs` around lines 334 - 362, The handlers for
Message::SubmitAuth and Message::SendMessage must mirror the UI disabled-state
checks currently used for the buttons: before calling manager.dispatch in the
Message::SubmitAuth branch (when matching ScreenState::Login(s)), check the same
"disabled" condition you use for the submit button (e.g., s.is_disabled or
validation flags) and early-return if disabled; similarly, in
Message::SendMessage (when matching ScreenState::Chat { compose }), ensure you
check the agent-typing guard (e.g., state.is_agent_typing or the same flag used
to disable the send button) and that compose.trim().is_empty() is still checked,
and only call manager.dispatch(AppAction::SendMessage { .. }) when those guards
allow it; update both branches where manager.dispatch is invoked so keyboard
Enter submits cannot bypass the button-enabled logic.
native/rmp-cli/src/doctor.rs-41-63 (1)

41-63: ⚠️ Potential issue | 🟠 Major

Do not hard-fail iOS checks on non-macOS hosts.

init() enables iOS by default, so a fresh scaffold on Linux/Windows will fail rmp doctor here trying to discover Xcode under /Applications. That blocks health checks for projects that can still build Android or desktop on those hosts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/doctor.rs` around lines 41 - 63, The iOS check currently
hard-fails by calling discover_xcode_dev_dir() and running xcrun on non-macOS
hosts; wrap the whole iOS block with a runtime host check (e.g. use
cfg!(target_os = "macos") or std::env::consts::OS == "macos") so that if the
host is not macOS you skip the Xcode/simctl checks (or return a non-fatal
warning) instead of returning CliError; specifically modify the block guarded by
cfg.ios.is_some() to first verify the host is macOS before calling
discover_xcode_dev_dir(), run_capture, or setting xcode_developer_dir, and
ensure discover_xcode_dev_dir() errors are treated as non-fatal on non-macOS
hosts.
native/rmp-cli/src/init.rs-63-64 (1)

63-64: ⚠️ Potential issue | 🟠 Major

Validate Android app_id with package-name rules, not bundle-id rules.

The validate_bundle_like function accepts hyphens in package segments, but the generated Android templates emit app_id directly into namespace and package declarations. Kotlin package names and Android's namespace value require valid Java identifiers—hyphens are not permitted. An app_id like com.my-app.foo will pass validation but produce uncompilable Kotlin sources.

Use Java identifier rules (alphanumeric, _, $; no hyphens) for Android package validation, or transform hyphens to underscores before using app_id in Android templates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/init.rs` around lines 63 - 64, The current flow uses
validate_bundle_like (which allows hyphens) and then writes app_id into
Kotlin/Android namespace/package via kotlin_pkg and kotlin_pkg_path, producing
invalid Java/Kotlin identifiers; either update validation to enforce Java
identifier rules (no hyphens, only letters/digits/underscore/$ per segment) or,
if you want to accept hyphens in input, normalize app_id before templating by
converting hyphens to underscores (e.g., replace '-' with '_' when computing
kotlin_pkg / kotlin_pkg_path) so the values used in namespace/package and Kotlin
sources are valid identifiers; apply the change where app_id is read/validated
and where kotlin_pkg/kotlin_pkg_path are derived.
native/ios/Sources/AppManager.swift-10-21 (1)

10-21: ⚠️ Potential issue | 🟠 Major

Handle keychain write/delete failures instead of dropping them.

SecItemAdd and SecItemDelete return OSStatus codes, but both calls ignore them. This causes silent failures—if Keychain access fails, login appears to work and session restore simply stops working on next launch. Apple's own documentation recommends checking these return values.

Affects both save() (lines 10–21) and delete() (lines 39–45).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/AppManager.swift` around lines 10 - 21, In
save(key:value:) and delete(key:) capture the OSStatus return values from
SecItemAdd and SecItemDelete instead of ignoring them, check for non-success
statuses, and handle failures (e.g., log the error with the status code and a
clear message or propagate/throw an error) so Keychain write/delete failures
aren’t silent; specifically modify the SecItemDelete(query as CFDictionary) and
SecItemAdd(item as CFDictionary, nil) calls to assign their results to
variables, compare against errSecSuccess, and handle/log non-success using your
existing logging/error propagation patterns.
native/rmp-cli/src/devices.rs-233-239 (1)

233-239: ⚠️ Potential issue | 🟠 Major

devices list should still work when Xcode is unavailable.

iOS discovery is unconditional here, so Linux/Windows hosts—or Macs without Xcode—fail before adb enumeration runs. That makes Android-only setups unusable even though android_targets() is independent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/devices.rs` around lines 233 - 239, The devices_list
function currently fails early because discover_xcode_dev_dir() is called
unconditionally; change the code to attempt Xcode/device discovery only if
discover_xcode_dev_dir() succeeds and otherwise continue to enumerate Android
targets. Specifically, call discover_xcode_dev_dir() and match on its Result (or
convert to Option) so that ios_connected_devices(&dev_dir) and
ios_simulators(&dev_dir) are only invoked on Ok(dev_dir); on Err just skip/empty
the iOS lists (optionally log/debug the skip) and then call android_targets() as
before so Android-only environments still work. Ensure you update any variable
bindings (ios_devs, ios_sims) accordingly to valid empty defaults when Xcode is
unavailable.
native/flake.nix-97-103 (1)

97-103: ⚠️ Potential issue | 🟠 Major

Resolve the native workspace before using relative paths in shellHook.

This assumes the shell was entered from the native/ directory. With nix develop ./native from the repo root, RMP_REPO stays unset and android/local.properties is created in the wrong place. Resolve the native workspace first, then write through that absolute path.

💡 Suggested hardening
-            # Auto-detect rmp-cli in parent directory
-            if [ -z "''${RMP_REPO:-}" ]; then
-              _parent="$(cd .. 2>/dev/null && pwd)"
-              if [ -f "$_parent/rmp-cli/Cargo.toml" ]; then
-                export RMP_REPO="$_parent"
-              fi
-            fi
+            # Resolve the writable native workspace independently of the entry cwd.
+            if [ -z "''${RMP_REPO:-}" ]; then
+              _repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
+              if [ -n "$_repo_root" ] && [ -f "$_repo_root/native/Cargo.toml" ] && [ -d "$_repo_root/native/rmp-cli" ]; then
+                export RMP_REPO="$_repo_root/native"
+              elif [ -f "$PWD/Cargo.toml" ] && [ -d "$PWD/rmp-cli" ]; then
+                export RMP_REPO="$PWD"
+              fi
+            fi
@@
-            mkdir -p android
-            cat > android/local.properties <<EOF
-            sdk.dir=$ANDROID_HOME
-EOF
+            if [ -n "''${RMP_REPO:-}" ]; then
+              mkdir -p "$RMP_REPO/android"
+              cat > "$RMP_REPO/android/local.properties" <<EOF
+            sdk.dir=$ANDROID_HOME
+EOF
+            fi

Also applies to: 121-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/flake.nix` around lines 97 - 103, The shellHook currently uses a
relative parent lookup to set RMP_REPO which fails when nix develop is run from
the repo root; replace the relative lookup with code that resolves the absolute
path of the native workspace (compute the repository root first and then
resolve/realpath the native directory) and export that absolute path into
RMP_REPO before any writes to android/local.properties in shellHook; make the
same change to the other occurrence mentioned (the block around the second if at
lines 121-124) so both use the resolved native workspace instead of a relative
parent path.
native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt-359-367 (1)

359-367: ⚠️ Potential issue | 🟠 Major

Gate auto-scroll on whether the user is already near the bottom.

This effect fires for any message-list growth, so loading older pages or scrolling up while the last assistant message is streaming immediately snaps the list back to the bottom. That makes pagination and reading history effectively unusable.

💡 Suggested guard
+    val isNearBottom by remember {
+        derivedStateOf {
+            val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
+            lastVisible >= listState.layoutInfo.totalItemsCount - 3
+        }
+    }
+
     var prevMessageCount by remember { mutableIntStateOf(state.messages.size) }
     LaunchedEffect(state.messages.size, state.messages.lastOrNull()?.content, state.isAgentTyping) {
         val totalItems = listState.layoutInfo.totalItemsCount
-        if (totalItems > 0 && state.messages.size >= prevMessageCount) {
+        if (isNearBottom && totalItems > 0 && state.messages.size >= prevMessageCount) {
             listState.animateScrollToItem(totalItems - 1)
         }
         prevMessageCount = state.messages.size
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt` around
lines 359 - 367, The auto-scroll LaunchedEffect should only run when the user is
already near the bottom; modify the effect that uses prevMessageCount,
LaunchedEffect, and listState.animateScrollToItem so it checks the current last
visible item before animating. Specifically, inside the LaunchedEffect that
watches state.messages.size/state.isAgentTyping, get the last visible index from
listState.layoutInfo.visibleItemsInfo (or compute from
listState.firstVisibleItemIndex + visibleCount) and only call
listState.animateScrollToItem(totalItems - 1) if totalItems > 0 AND
state.messages.size >= prevMessageCount AND (totalItems - 1 - lastVisible) <= N
(e.g., N = 1 or 2) to treat “near bottom” as true; still update prevMessageCount
= state.messages.size after the guard. This keeps existing symbols
(prevMessageCount, LaunchedEffect, listState.animateScrollToItem,
state.messages, state.isAgentTyping) but prevents jumping when the user is
reading history or paginating.
native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt-84-101 (1)

84-101: ⚠️ Potential issue | 🟠 Major

Align SessionTokens handling with the architecture bible's safe pattern for side-effect updates.

The code persists credentials correctly before checking the revision, but then omits the early return guard that the bible prescribes. Line 94 should check if (update.rev <= lastRevApplied) return@post after persistence, matching the pattern shown in Bible 6.6 for side-effect variants like AccountCreated.

Current pattern (unsafe assumption):

// persist
if (update.rev > lastRevApplied) {
    lastRevApplied = update.rev
}

Safe pattern (per bible):

// persist
if (update.rev <= lastRevApplied) return@post
lastRevApplied = update.rev

While the current producer increments rev independently for each event (so SessionTokens and FullState never share a revision), the code should be defensive. If the producer's semantics change or if SessionTokens and FullState ever share a revision from the same logical transition, the current code would silently drop the full-state update and leave Compose on stale auth/router state. Align Android with the documented safe pattern used on iOS.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt` around
lines 84 - 101, The SessionTokens branch persists credentials to securePrefs but
lacks the early-return rev guard; change the handler for AppUpdate.SessionTokens
so that after persisting (in the block handling update.accessToken) you check if
(update.rev <= lastRevApplied) return@post and only then set lastRevApplied =
update.rev, mirroring the safe pattern used in AppUpdate.FullState; update the
logic around lastRevApplied, AppUpdate.SessionTokens, update.rev and the
return@post guard to be defensive against replayed/duplicate revisions.
native/rust/src/lib.rs-305-312 (1)

305-312: ⚠️ Potential issue | 🟠 Major

The core loop never sees channel shutdown.

core_tx_async stays alive inside the worker thread for the lifetime of the loop. After the last FfiApp handle is dropped, self.core_tx is gone but core_rx.recv() at Line 391 still blocks forever, so the Tokio runtime and update pipeline leak. Add an explicit shutdown message in Drop, or drop the long-lived sender and keep only short-lived clones for spawned tasks.

Also applies to: 391-391

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 305 - 312, The core loop blocks forever
because a long-lived sender (core_tx_async / self.core_tx) stays alive
preventing core_rx.recv() from returning; modify FfiApp so that Drop either
sends an explicit shutdown message into core_tx (a dedicated CoreMsg::Shutdown)
or ensure the persistent sender is removed and only short-lived clones are
created for tasks (so when all FfiApp handles are dropped receivers observe
closed channel), update the core loop to handle the shutdown variant and exit
cleanly; search for core_tx_async, core_rx.recv(), self.core_tx, and the FfiApp
Drop impl to make these changes.
native/rust/src/lib.rs-313-325 (1)

313-325: ⚠️ Potential issue | 🟠 Major

Don't panic on bad bootstrap config.

These expects kill the core thread on an invalid client_id or api_url before it can emit any recoverable state. Because dispatch() later ignores send failures, the app just silently stops responding. Return an error from new or translate bootstrap failures into an initial error state instead.

Also applies to: 884-886

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 313 - 325, Replace the panics in the
bootstrap path by returning an error or setting an initial error state instead
of calling expect: change the tokio runtime creation,
uuid::Uuid::parse_str(&client_id), and
opensecret::OpenSecretClient::new(&api_url) calls (seen around rt, client_uuid,
and os_client) to propagate failures (e.g., return Result::Err from new) or map
them into an initial error message that the rest of the system can handle (so
dispatch() won’t silently stop); ensure the constructor/new function signature
returns a Result or stores a recoverable error state and use ? or map_err to
forward detailed errors rather than panicking.
native/rmp-cli/src/bindings.rs-233-253 (1)

233-253: ⚠️ Potential issue | 🟠 Major

Hash the generator, not just the Rust inputs.

The cache key only covers rust/src, uniffi.toml, and a few manifests, but generation is executed via cargo run -p uniffi-bindgen. Source edits under the local uniffi-bindgen crate can change emitted Swift/Kotlin while this hash still returns a hit, so stale bindings get reused. Include the generator crate sources/manifests in the cache inputs or key off the built binary fingerprint.

Also applies to: 281-302, 905-943

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/bindings.rs` around lines 233 - 253, The cache key
currently omits the uniffi-bindgen generator sources so edits under the local
uniffi-bindgen crate produce stale hits; update the cache input computation to
include the generator crate (e.g., its Cargo.toml and src tree) or, better, key
off the actual built generator binary fingerprint. Concretely, when computing
the hash used before running Command::new("cargo") ... arg("run") ...
arg("-p").arg("uniffi-bindgen"), include the uniffi-bindgen crate
files/manifests or the built artifact's checksum (target/.../uniffi-bindgen) in
the inputs (also apply same change to the other occurrences noted around the
other blocks at 281-302 and 905-943) so generator changes invalidate the cache.
native/rmp-cli/src/bindings.rs-82-161 (1)

82-161: ⚠️ Potential issue | 🟠 Major

Keep --json stdout clean.

native/rmp-cli/src/cli.rs, Lines 294-296 prints the success payload to stdout, but this flow still delegates to helpers that inherit child stdout. Any output from cargo, uniffi-bindgen, or xcodebuild will corrupt the JSON stream for callers. Pass the json flag through and redirect child stdout to stderr/capture when JSON mode is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/bindings.rs` around lines 82 - 161, The bindings function
currently ignores the json flag when invoking child-work helpers, so external
tool stdout (cargo/uniffi/xcodebuild) can mix into the JSON output; update each
helper invocation from bindings (clean_ios, check_ios_sources,
generate_ios_sources, build_ios_xcframework, clean_android,
check_android_sources, generate_android_sources, build_android_so) to accept and
receive the json boolean, and change those helper implementations to, when json
== true, redirect or capture child process stdout (write child stdout to stderr
or suppress it) so only the structured JSON from json_print is emitted to
stdout; ensure function signatures (e.g., generate_ios_sources(...),
build_ios_xcframework(...), generate_android_sources(...),
build_android_so(...)) are adjusted consistently to take the json flag and
branch their child-process spawning to use stderr/capture in JSON mode.
native/rmp-cli/src/bindings.rs-769-773 (1)

769-773: ⚠️ Potential issue | 🟠 Major

Avoid deleting the entire shared jniLibs tree.

android/app/src/main/jniLibs is the shared packaging location for all native libraries, not just Maple's generated .sos. Removing the whole directory here will silently wipe unrelated JNI dependencies the app may already ship. Delete only Maple's generated slices, or write into a dedicated generated location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/bindings.rs` around lines 769 - 773, The code currently
removes the entire shared jniLibs tree via std::fs::remove_dir_all(&out_dir)
which can wipe unrelated JNI artifacts; change the strategy to either (A) write
Maple outputs into a dedicated subdirectory (e.g., set out_dir =
root.join("android/app/src/main/jniLibs/maple")) and call
remove_dir_all/create_dir_all on that subdirectory only, or (B) if keeping the
top-level jniLibs, replace the blanket std::fs::remove_dir_all(&out_dir) with
logic that iterates the entries in out_dir and only deletes Maple-generated
files/directories (detect by a stable marker such as a "maple" subfolder name or
filename pattern like libmaple*.so) before calling create_dir_all; update
references to out_dir and the std::fs::remove_dir_all/std::fs::create_dir_all
calls accordingly.
native/rust/src/lib.rs-494-499 (1)

494-499: ⚠️ Potential issue | 🟠 Major

Remove raw conversation content from stderr logs.

Line 498 prints every streamed agent chunk, including message text. That leaks conversation content into logcat/console output and is hard to scrub later. Drop this log or reduce it to non-content metadata behind a debug-only path.

🔇 Minimal safe change
-                            eprintln!("[agent] step={step} messages={messages:?}");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 494 - 499, The eprintln! in the
InternalEvent::AgentMessageChunk arm currently prints raw message content
(messages) and must be removed or silenced; update the handler for
InternalEvent::AgentMessageChunk to stop logging message text — either delete
the eprintln! call or replace it with a debug-only log that emits only
non-sensitive metadata (e.g., step or a boolean has_messages) and does not
include messages' contents, ensuring you modify the match arm handling
InternalEvent::AgentMessageChunk (look for variables step and messages)
accordingly.
native/rust/src/lib.rs-458-464 (1)

458-464: ⚠️ Potential issue | 🟠 Major

Emit a token-clear update on restore failure.

native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt, Lines 80-104 only removes persisted credentials when it receives AppUpdate.SessionTokens with empty values. This branch only routes back to Login, so invalid tokens stay on disk and the app will retry them on every launch. Mirror the logout path here.

🧹 Mirror the logout-side credential clearing
                         InternalEvent::SessionRestoreFailed => {
+                            rev += 1;
+                            state.rev = rev;
+                            let _ = update_tx.send(AppUpdate::SessionTokens {
+                                rev,
+                                access_token: String::new(),
+                                refresh_token: String::new(),
+                            });
                             state.auth = AuthState::Ready;
                             state.router = Router {
                                 default_screen: Screen::Login,
                                 screen_stack: vec![],
                             };
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 458 - 464, When handling
InternalEvent::SessionRestoreFailed, in addition to setting state.auth =
AuthState::Ready and resetting state.router to Router { default_screen:
Screen::Login, screen_stack: vec![] }, also emit the same AppUpdate used by the
logout path that clears persisted credentials (i.e., send an
AppUpdate::SessionTokens with empty token/user values) so the Android side sees
empty session tokens and removes stored credentials; update the handler around
InternalEvent::SessionRestoreFailed to mirror the logout logic that emits the
empty SessionTokens update.
native/rmp-cli/src/bindings.rs-757-767 (1)

757-767: ⚠️ Potential issue | 🟠 Major

Don't overwrite android/local.properties.

This replaces the whole file with a single sdk.dir line and ignores write failures. Existing entries like ndk.dir or other local overrides get dropped after one run. Update just the sdk.dir entry and surface I/O errors instead.

🛠️ Safer update pattern
     if !sdk.is_empty() {
         let lp = root.join("android/local.properties");
-        let _ = std::fs::create_dir_all(root.join("android"));
-        let contents = format!("sdk.dir={}\n", sdk);
-        let _ = std::fs::write(lp, contents);
+        std::fs::create_dir_all(root.join("android")).map_err(|e| {
+            CliError::operational(format!("failed to create android dir: {e}"))
+        })?;
+        let existing = match std::fs::read_to_string(&lp) {
+            Ok(s) => s,
+            Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
+            Err(e) => {
+                return Err(CliError::operational(format!(
+                    "failed to read {}: {e}",
+                    lp.display()
+                )));
+            }
+        };
+        let mut lines: Vec<String> = existing
+            .lines()
+            .filter(|line| !line.trim_start().starts_with("sdk.dir="))
+            .map(|line| line.to_string())
+            .collect();
+        lines.push(format!("sdk.dir={sdk}"));
+        std::fs::write(&lp, lines.join("\n") + "\n").map_err(|e| {
+            CliError::operational(format!("failed to write {}: {e}", lp.display()))
+        })?;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/bindings.rs` around lines 757 - 767, The current block in
bindings.rs blindly overwrites android/local.properties (using create_dir_all
and std::fs::write) which loses other keys and ignores I/O errors; instead, read
the existing file at lp if it exists, update or insert the sdk.dir line while
preserving other lines (edit the line that starts with "sdk.dir=" or append if
missing), create the android directory as needed with create_dir_all, and write
the updated contents back, returning or propagating any std::io::Error rather
than swallowing it so callers can handle failures; reference the sdk variable,
lp path, and the current use of std::fs::create_dir_all and std::fs::write when
locating where to change the logic.
🟡 Minor comments (2)
native/android/app/src/main/res/values/strings.xml-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Rename the Android app name resource to Maple.

app_name still says Hello Chat, which conflicts with the Maple branding in this PR. If anything starts referencing this resource, the launcher name will regress.

Suggested change
-    <string name="app_name">Hello Chat</string>
+    <string name="app_name">Maple</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/res/values/strings.xml` at line 2, The app name
resource currently defines <string name="app_name">Hello Chat</string>, which
conflicts with Maple branding; update the value of the resource named app_name
to "Maple" (replace the current "Hello Chat" value) so the launcher and any
references use the correct brand name; ensure the string resource entry with
name="app_name" is the only change and keep the XML well-formed.
native/rmp-cli/src/run.rs-465-469 (1)

465-469: ⚠️ Potential issue | 🟡 Minor

Return the real Android target kind in JSON.

ensure_android_emulator() can return a physical device when --serial points at one, but this payload always says "kind":"emulator". That breaks callers consuming the structured output.

🛠️ Suggested fix
     if json {
         json_print(&JsonOk {
             ok: true,
-            data: serde_json::json!({"platform":"android","kind":"emulator","serial":serial,"app_id":pkg}),
+            data: serde_json::json!({
+                "platform":"android",
+                "kind": if serial.starts_with("emulator-") { "emulator" } else { "device" },
+                "serial": serial,
+                "app_id": pkg
+            }),
         });
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/run.rs` around lines 465 - 469, The JSON output hardcodes
"kind":"emulator" even though ensure_android_emulator() may return a physical
device; update the JSON payload to use the actual kind from the value returned
by ensure_android_emulator() (e.g., use the returned target's kind/variant or an
is_emulator() check) instead of the literal "emulator" so the JSON reflects
platform:"android", kind:<actual_kind>, serial:serial, app_id:pkg; locate the
JSON emission near json_print(&JsonOk { ... }) and replace the hardcoded string
with the target's real kind from the function's return.
🧹 Nitpick comments (8)
native/rust/build.rs (1)

1-1: Consider removing the empty build script or documenting its purpose.

An empty build.rs adds compilation overhead without providing any functionality. If this is a placeholder for future UniFFI bindings or other build-time tasks, consider adding a comment explaining the intent. Otherwise, remove it to speed up builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/build.rs` at line 1, The build script contains an empty main
function (fn main() {}) which adds unnecessary build overhead; either remove the
build.rs file entirely or replace the empty fn main with a short explanatory
comment indicating it is intentionally left blank as a placeholder for future
UniFFI bindings or other build-time tasks so reviewers and CI understand the
intent; update any project docs or Cargo config if you remove it to ensure no
build expectations remain.
native/.gitignore (1)

7-7: Commit flake.lock with the flake.

Ignoring the lock file makes the native toolchain drift across contributors and CI. If the flake is part of the supported workflow, its inputs should be pinned in version control.

Suggested change
-flake.lock
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/.gitignore` at line 7, Remove the flake.lock entry from .gitignore and
commit the actual flake.lock file into the repository so the native toolchain
inputs are pinned; specifically edit the .gitignore to delete the line
"flake.lock", add the generated flake.lock to source control, and create a
commit that includes the tracked flake.lock (so functions/tools that depend on
the flake use the pinned inputs).
native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt (1)

63-112: Consider defining remaining Material 3 typography slots.

The typography only defines 8 of the 15 Material 3 text styles (missing displayLarge, displayMedium, displaySmall, headlineSmall, titleSmall, bodySmall, labelSmall). Material 3 will fall back to defaults for undefined styles, which may use system fonts instead of Manrope.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt`
around lines 63 - 112, MapleTypography currently sets only 8 Material3 styles
and omits displayLarge/Medium/Small, headlineSmall, titleSmall, bodySmall, and
labelSmall, causing fallbacks to system fonts; update the MapleTypography
definition to include those missing TextStyle entries (displayLarge,
displayMedium, displaySmall, headlineSmall, titleSmall, bodySmall, labelSmall),
setting fontFamily = ManropeFamily and appropriate FontWeight, fontSize and
lineHeight values consistent with the existing styles so all Material3 slots use
ManropeFamily.
native/justfile (2)

6-8: Hardcoded user-specific path in comment.

The comment contains a developer-specific path (/Users/tony/Dev/OpenSecret/maple/native/justfile). Consider using a placeholder or relative reference:

 # │  From macOS:     just <recipe>                                      │
-# │  From OrbStack:  mac just -f /Users/tony/Dev/OpenSecret/maple/native/justfile <recipe> │
+# │  From OrbStack:  mac just -f /path/to/maple/native/justfile <recipe> │
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/justfile` around lines 6 - 8, Replace the hardcoded developer-specific
path "/Users/tony/Dev/OpenSecret/maple/native/justfile" in the commented example
line with a neutral placeholder or relative reference (e.g.,
"<path-to-repo>/native/justfile" or "mac just -f ./native/justfile <recipe>") so
the example is portable; update the comment in the justfile that currently
contains the mac/OrbStack example line to use the placeholder instead of the
user-specific path.

15-16: Same ossl helper limitation as root justfile.

This helper assumes macOS with Homebrew. See the comment on the root justfile for a potential fallback approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/justfile` around lines 15 - 16, The ossl helper in the justfile
currently hardcodes Homebrew paths for OpenSSL (the ossl variable) which only
works on macOS; update ossl to detect platform or provide fallbacks: if on macOS
keep the Homebrew-derived PKG_CONFIG_PATH/OPENSSL_DIR, otherwise fall back to
system pkg-config/OPENSSL_DIR or allow an environment override (e.g., use
existing OPENSSL_DIR if set) so builds on Linux/CI don't break; modify the ossl
definition in the native/justfile accordingly and ensure referenced targets that
use ossl still consume the environment variables.
native/ios/Sources/MapleWordmark.swift (2)

101-104: Force unwrap on CGPath.copy(using:).

While unlikely to fail with a valid transform, the force unwrap could theoretically crash. Consider using optional binding or guard let:

-                let transformed = cgPath.copy(using: [transform])!
-                let path = Path(transformed)
-                context.fill(path, with: .color(color))
+                if let transformed = cgPath.copy(using: [transform]) {
+                    let path = Path(transformed)
+                    context.fill(path, with: .color(color))
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/MapleWordmark.swift` around lines 101 - 104, The loop in
MapleWordmark.swift force-unwraps cgPath.copy(using:) which can crash; replace
the force unwrap in the for cgPath in paths loop by using optional binding
(guard let transformed = cgPath.copy(using: [transform]) else { continue } or if
let) and only create Path(transformed) and call context.fill(path, with:
.color(color)) when transformed is non-nil, optionally logging or skipping when
copy returns nil; update references to cgPath.copy(using:), Path(...), and
context.fill(...) accordingly.

10-11: Unused GeometryReader parameter.

The geometry proxy is captured with _ but unused. Since the Canvas already receives size, and the frame is explicitly set on line 108, you could simplify by removing GeometryReader:

     var body: some View {
-        GeometryReader { _ in
-            Canvas { context, size in
+        Canvas { context, size in

Then close the Canvas before .frame(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/MapleWordmark.swift` around lines 10 - 11, Remove the
unused GeometryReader wrapper around Canvas in MapleWordmark.swift: delete the
GeometryReader { _ in ... } and move the Canvas block to the top-level view,
ensuring you close the Canvas before applying the existing .frame(...) modifier;
since Canvas already receives the size parameter, drop the unused geometry proxy
and keep Canvas(size-aware) with the same drawing code and frame call.
justfile (1)

215-217: native_ossl assumes Homebrew on macOS.

The OpenSSL configuration uses brew --prefix openssl@3, which only works on macOS with Homebrew installed. Consider adding a fallback or documenting this requirement for contributors on other platforms.

-native_ossl := 'PKG_CONFIG_PATH="$(brew --prefix openssl@3)/lib/pkgconfig" OPENSSL_DIR="$(brew --prefix openssl@3)"'
+# macOS with Homebrew - Linux users may need to adjust paths
+native_ossl := 'PKG_CONFIG_PATH="${OPENSSL_PKG_CONFIG:-$(brew --prefix openssl@3 2>/dev/null || echo /usr/lib/pkgconfig)/lib/pkgconfig}" OPENSSL_DIR="${OPENSSL_DIR:-$(brew --prefix openssl@3 2>/dev/null || echo /usr)}"'

Alternatively, document the macOS-only assumption in the comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@justfile` around lines 215 - 217, The justfile sets native_ossl to use brew
--prefix openssl@3 which only works on macOS/Homebrew; update the definition of
native_ossl (the variable named native_ossl) to either detect platform and
choose a fallback (e.g., use pkg-config or OPENSSL_DIR from environment when not
on macOS) or add a clear comment above native_ossl documenting the
macOS/Homebrew requirement and how contributors on Linux/Windows should set
PKG_CONFIG_PATH/OPENSSL_DIR manually; reference and change the native_ossl
variable so the behavior is explicit for non-macOS systems.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 06b6e9f4-a5d1-4762-8388-cef07ee9aaab

📥 Commits

Reviewing files that changed from the base of the PR and between 7dda1fe and 7dedfa2.

⛔ Files ignored due to path filters (37)
  • native/Cargo.lock is excluded by !**/*.lock
  • native/android/app/src/main/res/font/array_bold.ttf is excluded by !**/*.ttf
  • native/android/app/src/main/res/font/manrope_bold.ttf is excluded by !**/*.ttf
  • native/android/app/src/main/res/font/manrope_medium.ttf is excluded by !**/*.ttf
  • native/android/app/src/main/res/font/manrope_regular.ttf is excluded by !**/*.ttf
  • native/android/app/src/main/res/font/manrope_semibold.ttf is excluded by !**/*.ttf
  • native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • native/desktop/iced/Array-Bold.ttf is excluded by !**/*.ttf
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_1024.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_120.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_152.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_167.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_180.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_20.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_29.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_40.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_58.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_60.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_76.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_80.png is excluded by !**/*.png
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon_87.png is excluded by !**/*.png
  • native/ios/Sources/Fonts/Array-Bold.ttf is excluded by !**/*.ttf
  • native/ios/Sources/Fonts/Manrope-Bold.ttf is excluded by !**/*.ttf
  • native/ios/Sources/Fonts/Manrope-Medium.ttf is excluded by !**/*.ttf
  • native/ios/Sources/Fonts/Manrope-Regular.ttf is excluded by !**/*.ttf
  • native/ios/Sources/Fonts/Manrope-SemiBold.ttf is excluded by !**/*.ttf
  • native/ios/Sources/maple-wordmark.svg is excluded by !**/*.svg
  • native/rmp-cli/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (53)
  • .gitignore
  • justfile
  • native/.env.example
  • native/.gitignore
  • native/Cargo.toml
  • native/README.md
  • native/android/app/build.gradle.kts
  • native/android/app/src/main/AndroidManifest.xml
  • native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt
  • native/android/app/src/main/java/cloud/opensecret/maple/MainActivity.kt
  • native/android/app/src/main/java/cloud/opensecret/maple/rust/maple_core.kt
  • native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt
  • native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Color.kt
  • native/android/app/src/main/java/cloud/opensecret/maple/ui/theme/Theme.kt
  • native/android/app/src/main/res/drawable/maple_wordmark.xml
  • native/android/app/src/main/res/values/strings.xml
  • native/android/app/src/main/res/values/themes.xml
  • native/android/build.gradle.kts
  • native/android/gradle.properties
  • native/android/settings.gradle.kts
  • native/desktop/iced/Cargo.toml
  • native/desktop/iced/src/main.rs
  • native/desktop/iced/src/theme.rs
  • native/flake.nix
  • native/ios/Info.plist
  • native/ios/Sources/App.swift
  • native/ios/Sources/AppManager.swift
  • native/ios/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json
  • native/ios/Sources/Assets.xcassets/Contents.json
  • native/ios/Sources/ContentView.swift
  • native/ios/Sources/MapleTheme.swift
  • native/ios/Sources/MapleWordmark.swift
  • native/ios/project.yml
  • native/justfile
  • native/rmp-architecture-bible.md
  • native/rmp-cli/Cargo.toml
  • native/rmp-cli/README.md
  • native/rmp-cli/src/bindings.rs
  • native/rmp-cli/src/cli.rs
  • native/rmp-cli/src/config.rs
  • native/rmp-cli/src/devices.rs
  • native/rmp-cli/src/doctor.rs
  • native/rmp-cli/src/init.rs
  • native/rmp-cli/src/main.rs
  • native/rmp-cli/src/run.rs
  • native/rmp-cli/src/util.rs
  • native/rmp.toml
  • native/rust/Cargo.toml
  • native/rust/build.rs
  • native/rust/src/lib.rs
  • native/rust/uniffi.toml
  • native/uniffi-bindgen/Cargo.toml
  • native/uniffi-bindgen/src/main.rs

Comment on lines +327 to +338
// Start attestation handshake
{
let os = os_client.clone();
let tx = core_tx_async.clone();
rt.spawn(async move {
let event = match os.perform_attestation_handshake().await {
Ok(()) => InternalEvent::AttestationComplete,
Err(e) => InternalEvent::AttestationFailed(e.to_string()),
};
let _ = tx.send(CoreMsg::Internal(Box::new(event)));
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Startup attestation can clobber session restore.

The constructor always spawns an initial attestation task, and these handlers unconditionally switch the app to Ready/Login. If RestoreSession finishes first, a late AttestationComplete or AttestationFailed will overwrite the restored LoggedIn state and bounce the user back to the login screen. Guard startup events with an initialization generation/token, or ignore them once restore has begun.

Also applies to: 394-408

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 327 - 338, The attestation spawn (using
os_client.clone(), core_tx_async.clone(), rt.spawn and
perform_attestation_handshake) can race with session restore and unconditionally
send InternalEvent::AttestationComplete/AttestationFailed wrapped in
CoreMsg::Internal, overwriting a restored LoggedIn state; fix by adding an
initialization generation/token or a guard flag in the core state that marks
"restore in progress" and have the spawned attestation task check that
token/flag before sending events (or include the generation with the
InternalEvent and have the core ignore events whose generation is older than the
current one); update both the startup attestation blocks (the one shown and the
similar block around lines 394-408) to attach/check this generation so late
attestation results are ignored once RestoreSession begins.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 7, 2026

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: f95cf40
Status: ✅  Deploy successful!
Preview URL: https://c239f0ff.maple-ca8.pages.dev
Branch Preview URL: https://native-3-0.maple-ca8.pages.dev

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@justfile`:
- Around line 254-259: The `native-run-all` just recipe runs `native-run-ios`,
`native-run-android`, and `native-run-desktop` sequentially (blocking) which
prevents concurrent launches; update the Justfile to make the behavior explicit
by either renaming the target (e.g., `native-run-ios-then-android-then-desktop`)
or adding a clear comment/docstring above `native-run-all` stating it runs tasks
sequentially and users should open separate terminals for concurrent runs;
alternatively, if true concurrent launches are desired, add a new target
`native-run-concurrent` that launches `native-run-ios`, `native-run-android`,
and `native-run-desktop` in the background (with proper signal handling) rather
than changing the existing `native-run-all` behavior—refer to the
`native-run-all`, `native-run-ios`, `native-run-android`, and
`native-run-desktop` task names when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e18da7c4-ffda-4c4e-8bd0-61521ea53ab9

📥 Commits

Reviewing files that changed from the base of the PR and between 7dedfa2 and 6792c82.

📒 Files selected for processing (1)
  • justfile

justfile Outdated
Comment on lines +254 to +259
# Build all native platforms
native-build-all: native-build-desktop native-build-android
@echo "Native desktop and Android built. iOS is build+run only via rmp."

# Run all native platforms
native-run-all: native-bindings native-run-ios native-run-android native-run-desktop
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

native-run-all will not run concurrently.

native-run-ios, native-run-android, and native-run-desktop are blocking interactive processes (simulator/emulator/app). When specified as dependencies, just executes them sequentially—meaning only iOS will run, and Android/Desktop won't start until you manually terminate iOS.

If the intent is a convenience alias to "run everything at once," consider either:

  1. Documenting that users should run these in separate terminals
  2. Using background execution (though that complicates signal handling)
  3. Renaming to clarify behavior (e.g., native-run-ios-then-android-then-desktop)
📝 Suggested documentation clarification
 # Run all native platforms
-native-run-all: native-bindings native-run-ios native-run-android native-run-desktop
+# NOTE: These are blocking processes - run in separate terminals instead
+# native-run-all: native-bindings native-run-ios native-run-android native-run-desktop

Or keep as-is if sequential execution is intentional for testing purposes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@justfile` around lines 254 - 259, The `native-run-all` just recipe runs
`native-run-ios`, `native-run-android`, and `native-run-desktop` sequentially
(blocking) which prevents concurrent launches; update the Justfile to make the
behavior explicit by either renaming the target (e.g.,
`native-run-ios-then-android-then-desktop`) or adding a clear comment/docstring
above `native-run-all` stating it runs tasks sequentially and users should open
separate terminals for concurrent runs; alternatively, if true concurrent
launches are desired, add a new target `native-run-concurrent` that launches
`native-run-ios`, `native-run-android`, and `native-run-desktop` in the
background (with proper signal handling) rather than changing the existing
`native-run-all` behavior—refer to the `native-run-all`, `native-run-ios`,
`native-run-android`, and `native-run-desktop` task names when making the
change.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

return None;
}
let s = String::from_utf8_lossy(&out.stdout);
let avd = s.lines().last()?.trim();
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 7, 2026

Choose a reason for hiding this comment

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

🔴 emulator_avd_name reads last line instead of first line, always returning None

The adb emu avd name command outputs the AVD name on the first line followed by "OK" on the last line. At native/rmp-cli/src/run.rs:703, s.lines().last() retrieves "OK", which is then filtered out by the check on line 704 (avd.eq_ignore_ascii_case("ok")), causing the function to always return None. This means the headless emulator detection logic (emulator_is_headless_only) that depends on the AVD name will never work, and the restart-headless-emulator-with-GUI feature in ensure_android_emulator is silently broken.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

AnthonyRonning and others added 2 commits March 7, 2026 10:36
Let native local, dev, and prod builds switch endpoints without baking the API base URL into Rust.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Keep the iced desktop app on the shared default API URL path and record the related lockfile and formatting updates.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment on lines +401 to +409
Message::SendMessage => {
if let ScreenState::Chat { compose } = screen {
if !compose.trim().is_empty() {
manager.dispatch(AppAction::SendMessage {
content: compose.clone(),
});
*compose = String::new();
}
}
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 7, 2026

Choose a reason for hiding this comment

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

🔴 Desktop iced client clears compose text on Enter even when agent is typing, causing data loss

In the desktop iced SendMessage handler (native/desktop/iced/src/main.rs:407-415), pressing Enter dispatches AppAction::SendMessage and clears the compose text without checking state.is_agent_typing. The Rust core at native/rust/src/lib.rs:817 guards with if content.is_empty() || state.is_agent_typing { continue; }, silently dropping the message. But the desktop has already cleared *compose = String::new() on line 413, so the user's typed text is lost. Both iOS (native/ios/Sources/ContentView.swift:419) and Android (native/android/app/src/main/java/cloud/opensecret/maple/ui/MainApp.kt:519) correctly guard against this by checking isAgentTyping before sending.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (5)
native/rust/src/lib.rs (1)

372-382: ⚠️ Potential issue | 🔴 Critical

Startup attestation can still clobber session restore.

This task can still deliver AttestationComplete / AttestationFailed after RestoreSession has already moved the app to LoggedIn, and the handlers below still overwrite that state back to Ready/Login. Guard startup attestation with an init generation/flag and ignore stale results once restore begins.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 372 - 382, The attestation task spawned
from perform_attestation_handshake() can send
InternalEvent::AttestationComplete/AttestationFailed after RestoreSession has
already transitioned the app to LoggedIn and thereby overwrite it; add an
init-generation or boolean flag on the startup sequence (e.g., attach a
startup_id or startup_in_progress flag to the struct that owns
core_tx_async/os_client) and capture that id/flag in the spawned closure, then
before sending CoreMsg::Internal ignore results whose captured id/flag does not
match the current startup generation or where startup_in_progress has been
cleared by RestoreSession; update RestoreSession to bump/clear that
generation/flag when it begins so stale attestation results are dropped and
cannot transition state back to Ready/Login.
native/ios/Sources/AppManager.swift (1)

58-72: ⚠️ Potential issue | 🟠 Major

Do not fall back to defaultApiUrl() for shipped iOS builds.

When OPEN_SECRET_API_URL is missing, this still resolves to the Rust default (http://0.0.0.0:3000). That leaves packaged builds pointed at localhost instead of a real backend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/ios/Sources/AppManager.swift` around lines 58 - 72, The
configuredApiUrl() function currently falls back to defaultApiUrl(), which
causes shipped iOS builds to point at the Rust dev default; change
configuredApiUrl() so it does NOT return defaultApiUrl() when
OPEN_SECRET_API_URL is absent—remove the final return defaultApiUrl() and
instead return an explicit empty value (e.g., empty string or nil) or surface an
error so production builds cannot silently point to localhost; update callers
that expect a value to handle the empty/nil case accordingly (refer to
configuredApiUrl() and defaultApiUrl()).
native/desktop/iced/src/main.rs (1)

53-56: ⚠️ Potential issue | 🟠 Major

Do not ship the desktop app with a localhost fallback.

If OPEN_SECRET_API_URL is missing, this still falls back to maple_core::default_api_url() (http://0.0.0.0:3000). Packaged builds then point at the user's machine instead of a real backend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/desktop/iced/src/main.rs` around lines 53 - 56, The code currently
falls back to maple_core::default_api_url() when OPEN_SECRET_API_URL is missing,
which causes packaged desktop builds to point at localhost; change the api_url
retrieval so it does not use maple_core::default_api_url()—instead require
OPEN_SECRET_API_URL to be present and fail fast (e.g., replace the
unwrap_or_else fallback with an expect/propagate error) so main.rs's api_url
variable cannot silently default to http://0.0.0.0:3000; keep the environment
key name OPEN_SECRET_API_URL in the error message for clarity.
native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt (1)

58-63: ⚠️ Potential issue | 🟠 Major

The Android fallback is still emulator-only.

If BuildConfig.OPEN_SECRET_API_URL is empty, this maps the Rust localhost default to http://10.0.2.2:3000. That only works in the emulator; physical devices and release builds will still point at a local machine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt` around
lines 58 - 63, configuredApiUrl() currently rewrites the Rust default
"http://0.0.0.0:3000" to the emulator-only "http://10.0.2.2:3000"
unconditionally; change it to only perform that translation when running on an
Android emulator (so physical devices and release builds keep the
original/default host). Update configuredApiUrl() to detect emulator status
(e.g., using android.os.Build properties like Build.FINGERPRINT, Build.MODEL or
Build.PRODUCT) and only return "http://10.0.2.2:3000" when an emulator is
detected; otherwise return apiUrl (or the value from defaultApiUrl()) unchanged.
Ensure you reference and preserve BuildConfig.OPEN_SECRET_API_URL and
defaultApiUrl() when implementing this conditional logic.
native/android/app/build.gradle.kts (1)

18-24: ⚠️ Potential issue | 🔴 Critical

versionCode is still outside Android's supported range.

defaultConfig.versionCode is an Int, so 3000000000 will not compile here, and it also exceeds Google Play's maximum allowed value. This needs to be reduced to an in-range Int.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/build.gradle.kts` around lines 18 - 24,
defaultConfig.versionCode is set to 3000000000 which is too large for an Int and
exceeds Google Play's max; change the literal to a valid 32-bit Int within
Play's allowed range (e.g. <= 2100000000) or compute a safe int from versionName
(e.g. derive from 3.0.0 -> 300000000) and replace the value on the versionCode
assignment so the build compiles and adheres to Play limits.
🧹 Nitpick comments (4)
native/justfile (1)

57-68: Consider extracting duplicated API URL logic.

The API URL determination pattern (api_env="{{api_env}}"; while [...]; api_url=...) is repeated across six recipes. Extracting this to a helper function or a shell script would reduce duplication and simplify maintenance.

Example: Define a reusable function at the top
+# Helper function to resolve API URL
+resolve_api_url := '''
+  api_env="$1"
+  while [[ "$api_env" == api_env=* ]]; do api_env="${api_env#api_env=}"; done
+  case "$api_env" in
+    dev)  echo "https://enclave.secretgpt.ai" ;;
+    prod) echo "https://enclave.trymaple.ai" ;;
+    *)    echo "http://0.0.0.0:3000" ;;
+  esac
+'''
+
 # build desktop app only (api_env: local|dev|prod)
 build-desktop api_env="local":
-  api_env="{{api_env}}"; while [[ "$api_env" == api_env=* ]]; do api_env="${api_env#api_env=}"; done; \
-  api_url="http://0.0.0.0:3000"; \
-  if [ "$api_env" = "dev" ]; then api_url="https://enclave.secretgpt.ai"; elif [ "$api_env" = "prod" ]; then api_url="https://enclave.trymaple.ai"; fi; \
+  api_url=$({{resolve_api_url}} "{{api_env}}"); \
   {{ossl}} OPEN_SECRET_API_URL="$api_url" cargo build -p maple_desktop_iced
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/justfile` around lines 57 - 68, Extract the duplicated API URL
resolution into a single helper in the justfile (e.g., define a get_api_url()
recipe or a shell function at the top) and call it from the recipes that
currently contain the repeated block (notably build-desktop and run-desktop);
replace the repeated lines starting with api_env="{{api_env}}"; while ... and
the if/elif branch that sets api_url with a call to the helper so each recipe
simply invokes the helper to set OPEN_SECRET_API_URL before running cargo
build/run (keep function name like get_api_url or resolve_api_url to match
references).
native/desktop/iced/src/theme.rs (3)

249-259: Same border configuration issue: width is 0.0 with non-default color.

Similar to branded_header_style, this sets color: NEUTRAL_200 but width: 0.0 renders the color ineffective.

♻️ Simplify if no border is intended
 pub fn header_style(_theme: &iced::Theme) -> iced::widget::container::Style {
     iced::widget::container::Style {
         background: Some(iced::Background::Color(NEUTRAL_0)),
-        border: Border {
-            radius: 0.0.into(),
-            width: 0.0,
-            color: NEUTRAL_200,
-        },
+        border: Border::default(),
         ..Default::default()
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/desktop/iced/src/theme.rs` around lines 249 - 259, The header_style
currently sets Border { radius: 0.0.into(), width: 0.0, color: NEUTRAL_200 }
which makes the color ineffective; update the function (header_style) to either
remove the explicit Border entirely if no border is intended or set a visible
width (e.g., width: 1.0) along with color: NEUTRAL_200 so the border shows,
mirroring the same fix applied to branded_header_style; ensure you only change
header_style's Border configuration and keep other fields as-is.

261-277: Border color is set but border width is 0.0, making the color ineffective.

In branded_header_style, the border color is configured but width: 0.0 means the border won't render. If this is intentional (no border desired), the color assignment is dead code. If a subtle border is intended, set width to a positive value.

♻️ Option A: Remove unused border color
 pub fn branded_header_style(_theme: &iced::Theme) -> iced::widget::container::Style {
     iced::widget::container::Style {
         background: Some(iced::Background::Color(Color {
             a: 0.88,
             ..PEBBLE_800
         })),
-        border: Border {
-            width: 0.0,
-            color: Color {
-                a: 0.1,
-                ..PEBBLE_600
-            },
-            ..Default::default()
-        },
+        border: Border::default(),
         ..Default::default()
     }
 }
♻️ Option B: Add border width if subtle border is desired
         border: Border {
-            width: 0.0,
+            width: 1.0,
             color: Color {
                 a: 0.1,
                 ..PEBBLE_600
             },
             ..Default::default()
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/desktop/iced/src/theme.rs` around lines 261 - 277, In
branded_header_style the container sets a Border color but uses width: 0.0 so
the color never renders; either remove the Border color assignment (dead code)
or set a small positive width (e.g., 1.0 or a fractional value) so the border
becomes visible — update the Border struct in branded_header_style (and keep
references to PEBBLE_600 if using the color) within the
iced::widget::container::Style to reflect the chosen option.

168-184: Consider applying opacity to disabled ghost button for consistency.

The ghost_button_style disabled state uses NEUTRAL_400 directly, while primary_button_style and secondary_button_style apply a 0.5 opacity to their disabled states. This creates a subtle visual inconsistency across button variants.

♻️ Optional: Apply opacity for consistency
     let text_color = match status {
         iced::widget::button::Status::Active => PEBBLE_500,
         iced::widget::button::Status::Hovered => PEBBLE_700,
         iced::widget::button::Status::Pressed => PEBBLE_800,
-        iced::widget::button::Status::Disabled => NEUTRAL_400,
+        iced::widget::button::Status::Disabled => Color { a: 0.5, ..NEUTRAL_400 },
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/desktop/iced/src/theme.rs` around lines 168 - 184, In
ghost_button_style, make the disabled text color match the other variants by
applying 0.5 opacity to NEUTRAL_400 (instead of using NEUTRAL_400 directly);
update the disabled branch inside the ghost_button_style function so it returns
the same semi-transparent neutral color used by primary_button_style and
secondary_button_style for their disabled states.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@justfile`:
- Around line 222-232: The three recipes declare an unused parameter api_env
(native-bindings, native-bindings-swift, native-bindings-kotlin); either drop
the parameter declarations or wire api_env through to the commands they run. Fix
by propagating api_env into the invocation of {{native_nix}} and/or the rmp
command (so the underlying native build gets the selected environment) or remove
api_env from the recipe signatures if not needed; update the recipes
native-bindings, native-bindings-swift, and native-bindings-kotlin accordingly.

In `@native/justfile`:
- Around line 30-36: The bindings recipes declare an unused parameter api_env
(seen on recipes bindings-swift, bindings-kotlin, and bindings) — either remove
the api_env parameter from each recipe signature to avoid confusion, or wire it
through like other recipes by exporting OPEN_SECRET_API_URL (or the appropriate
env var) based on api_env and passing it into the rmp/cargo run command; update
the recipe bodies for bindings-swift, bindings-kotlin, and bindings to either
drop api_env or to set OPEN_SECRET_API_URL (e.g., conditional on api_env values
local|dev|prod) before invoking rmp/cargo so the parameter is actually used.

In `@native/rmp-cli/src/bindings.rs`:
- Around line 96-150: The check path currently still invokes
build_ios_xcframework and build_android_so after running
check_ios_sources/check_android_sources; change the control flow so that when
args.check is true no builds are performed — either return early after the check
calls or move the build_ios_xcframework and build_android_so invocations into
the else branch that runs when !args.check. Apply this to the Swift branch
(match arm handling crate::cli::BindingsTarget::Swift, functions
check_ios_sources and build_ios_xcframework), the Kotlin branch
(crate::cli::BindingsTarget::Kotlin, functions check_android_sources and
build_android_so), and the All branch (both
check_ios_sources/check_android_sources and
build_ios_xcframework/build_android_so).

In `@native/rmp-cli/src/run.rs`:
- Around line 417-448: The Android path always builds the debug variant; update
the logic in run.rs where you create the gradle Command and set apk/pkg
(currently using ":app:assembleDebug", apk =
"android/app/build/outputs/apk/debug/app-debug.apk", and pkg =
format!("{app_id}.dev")) to branch on the release flag: when release is true,
run ":app:assembleRelease", point apk to
"android/app/build/outputs/apk/release/app-release.apk", and set pkg to the
production package id (e.g., app_id without the ".dev" suffix); otherwise keep
the existing debug assemble, apk path, and pkg naming. Ensure the Status check
and file-exists error messages still apply to the selected apk.

In `@native/rmp-cli/src/util.rs`:
- Around line 33-64: discover_xcode_dev_dir currently only checks DEVELOPER_DIR
and /Applications; add a step to run and read the output of "xcode-select -p"
(e.g., via std::process::Command) when DEVELOPER_DIR is unset, convert that path
to a PathBuf, and apply the same validation used elsewhere (check presence of
"usr/bin/xcrun" or "usr/bin/simctl" and call developer_dir_supports_iphoneos)
before falling back to scanning /Applications; update discover_xcode_dev_dir to
return that validated path if valid and handle/propagate any command errors as a
CliError similar to existing error handling.

In `@native/rust/src/lib.rs`:
- Around line 545-550: The stderr logging in the
InternalEvent::AgentMessageChunk match arm currently prints full user messages
via eprintln! with "messages:?"; remove or redact that sensitive payload and
instead log only non-sensitive metadata (e.g., step and messages.len() or a
redacted placeholder) in the AgentMessageChunk branch so conversation content is
not written to stderr/logcat/Xcode; update the eprintln! call in the
InternalEvent::AgentMessageChunk handler to omit messages:? and use a safe
alternative like step and message count or a fixed string.
- Around line 505-511: In the InternalEvent::SessionRestoreFailed handler, in
addition to setting state.auth = AuthState::Ready and resetting state.router,
clear any in-memory and persisted session tokens so stale credentials aren't
retried: set the relevant field (e.g., state.session_tokens or state.tokens) to
an empty/default SessionTokens (SessionTokens::default() or
SessionTokens::empty()) and call your persistence/flush routine (or emit the
existing SessionTokens update event) to overwrite storage so the native launch
code stops attempting the broken restore path; update the
InternalEvent::SessionRestoreFailed branch in lib.rs accordingly.

---

Duplicate comments:
In `@native/android/app/build.gradle.kts`:
- Around line 18-24: defaultConfig.versionCode is set to 3000000000 which is too
large for an Int and exceeds Google Play's max; change the literal to a valid
32-bit Int within Play's allowed range (e.g. <= 2100000000) or compute a safe
int from versionName (e.g. derive from 3.0.0 -> 300000000) and replace the value
on the versionCode assignment so the build compiles and adheres to Play limits.

In `@native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt`:
- Around line 58-63: configuredApiUrl() currently rewrites the Rust default
"http://0.0.0.0:3000" to the emulator-only "http://10.0.2.2:3000"
unconditionally; change it to only perform that translation when running on an
Android emulator (so physical devices and release builds keep the
original/default host). Update configuredApiUrl() to detect emulator status
(e.g., using android.os.Build properties like Build.FINGERPRINT, Build.MODEL or
Build.PRODUCT) and only return "http://10.0.2.2:3000" when an emulator is
detected; otherwise return apiUrl (or the value from defaultApiUrl()) unchanged.
Ensure you reference and preserve BuildConfig.OPEN_SECRET_API_URL and
defaultApiUrl() when implementing this conditional logic.

In `@native/desktop/iced/src/main.rs`:
- Around line 53-56: The code currently falls back to
maple_core::default_api_url() when OPEN_SECRET_API_URL is missing, which causes
packaged desktop builds to point at localhost; change the api_url retrieval so
it does not use maple_core::default_api_url()—instead require
OPEN_SECRET_API_URL to be present and fail fast (e.g., replace the
unwrap_or_else fallback with an expect/propagate error) so main.rs's api_url
variable cannot silently default to http://0.0.0.0:3000; keep the environment
key name OPEN_SECRET_API_URL in the error message for clarity.

In `@native/ios/Sources/AppManager.swift`:
- Around line 58-72: The configuredApiUrl() function currently falls back to
defaultApiUrl(), which causes shipped iOS builds to point at the Rust dev
default; change configuredApiUrl() so it does NOT return defaultApiUrl() when
OPEN_SECRET_API_URL is absent—remove the final return defaultApiUrl() and
instead return an explicit empty value (e.g., empty string or nil) or surface an
error so production builds cannot silently point to localhost; update callers
that expect a value to handle the empty/nil case accordingly (refer to
configuredApiUrl() and defaultApiUrl()).

In `@native/rust/src/lib.rs`:
- Around line 372-382: The attestation task spawned from
perform_attestation_handshake() can send
InternalEvent::AttestationComplete/AttestationFailed after RestoreSession has
already transitioned the app to LoggedIn and thereby overwrite it; add an
init-generation or boolean flag on the startup sequence (e.g., attach a
startup_id or startup_in_progress flag to the struct that owns
core_tx_async/os_client) and capture that id/flag in the spawned closure, then
before sending CoreMsg::Internal ignore results whose captured id/flag does not
match the current startup generation or where startup_in_progress has been
cleared by RestoreSession; update RestoreSession to bump/clear that
generation/flag when it begins so stale attestation results are dropped and
cannot transition state back to Ready/Login.

---

Nitpick comments:
In `@native/desktop/iced/src/theme.rs`:
- Around line 249-259: The header_style currently sets Border { radius:
0.0.into(), width: 0.0, color: NEUTRAL_200 } which makes the color ineffective;
update the function (header_style) to either remove the explicit Border entirely
if no border is intended or set a visible width (e.g., width: 1.0) along with
color: NEUTRAL_200 so the border shows, mirroring the same fix applied to
branded_header_style; ensure you only change header_style's Border configuration
and keep other fields as-is.
- Around line 261-277: In branded_header_style the container sets a Border color
but uses width: 0.0 so the color never renders; either remove the Border color
assignment (dead code) or set a small positive width (e.g., 1.0 or a fractional
value) so the border becomes visible — update the Border struct in
branded_header_style (and keep references to PEBBLE_600 if using the color)
within the iced::widget::container::Style to reflect the chosen option.
- Around line 168-184: In ghost_button_style, make the disabled text color match
the other variants by applying 0.5 opacity to NEUTRAL_400 (instead of using
NEUTRAL_400 directly); update the disabled branch inside the ghost_button_style
function so it returns the same semi-transparent neutral color used by
primary_button_style and secondary_button_style for their disabled states.

In `@native/justfile`:
- Around line 57-68: Extract the duplicated API URL resolution into a single
helper in the justfile (e.g., define a get_api_url() recipe or a shell function
at the top) and call it from the recipes that currently contain the repeated
block (notably build-desktop and run-desktop); replace the repeated lines
starting with api_env="{{api_env}}"; while ... and the if/elif branch that sets
api_url with a call to the helper so each recipe simply invokes the helper to
set OPEN_SECRET_API_URL before running cargo build/run (keep function name like
get_api_url or resolve_api_url to match references).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b539e0f6-fc76-478a-863f-2816aa2ac340

📥 Commits

Reviewing files that changed from the base of the PR and between 6792c82 and 5255fa5.

⛔ Files ignored due to path filters (1)
  • native/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • justfile
  • native/Cargo.toml
  • native/android/app/build.gradle.kts
  • native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt
  • native/desktop/iced/src/main.rs
  • native/desktop/iced/src/theme.rs
  • native/ios/Info.plist
  • native/ios/Sources/AppManager.swift
  • native/justfile
  • native/rmp-cli/src/bindings.rs
  • native/rmp-cli/src/run.rs
  • native/rmp-cli/src/util.rs
  • native/rust/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • native/ios/Info.plist

Comment on lines +222 to +232
# Regenerate native FFI bindings (Swift + Kotlin) (api_env: local|dev|prod)
native-bindings api_env="local":
{{native_nix}} "unset CC CXX AR RANLIB && rmp bindings all"

# Regenerate native Swift FFI bindings (api_env: local|dev|prod)
native-bindings-swift api_env="local":
{{native_nix}} "unset CC CXX AR RANLIB && rmp bindings swift"

# Regenerate native Kotlin FFI bindings (api_env: local|dev|prod)
native-bindings-kotlin api_env="local":
{{native_nix}} "unset CC CXX AR RANLIB && rmp bindings kotlin"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused api_env parameter in native-bindings recipes.

Same issue as native/justfile: the api_env parameter is declared but the bindings commands don't use it. Either remove the parameter or wire it through to the underlying command.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@justfile` around lines 222 - 232, The three recipes declare an unused
parameter api_env (native-bindings, native-bindings-swift,
native-bindings-kotlin); either drop the parameter declarations or wire api_env
through to the commands they run. Fix by propagating api_env into the invocation
of {{native_nix}} and/or the rmp command (so the underlying native build gets
the selected environment) or remove api_env from the recipe signatures if not
needed; update the recipes native-bindings, native-bindings-swift, and
native-bindings-kotlin accordingly.

Comment on lines +30 to +36
# regenerate Swift FFI bindings (api_env: local|dev|prod)
bindings-swift api_env="local":
if command -v nix >/dev/null 2>&1; then \
nix develop --command bash -c "unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi"; \
else \
unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi; \
fi
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused api_env parameter in bindings recipes.

The api_env parameter is declared on lines 31, 39, and 47 but is never used in the recipe bodies. The bindings commands don't pass api_env or set OPEN_SECRET_API_URL. If bindings generation doesn't require API configuration, consider removing the parameter to avoid confusion; otherwise, wire it through like the other recipes.

Suggested fix (if parameter is not needed)
-# regenerate Swift FFI bindings (api_env: local|dev|prod)
-bindings-swift api_env="local":
+# regenerate Swift FFI bindings
+bindings-swift:
   if command -v nix >/dev/null 2>&1; then \

Apply similar changes to bindings-kotlin (line 39) and bindings (line 47).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# regenerate Swift FFI bindings (api_env: local|dev|prod)
bindings-swift api_env="local":
if command -v nix >/dev/null 2>&1; then \
nix develop --command bash -c "unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi"; \
else \
unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi; \
fi
# regenerate Swift FFI bindings
bindings-swift:
if command -v nix >/dev/null 2>&1; then \
nix develop --command bash -c "unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi"; \
else \
unset CC CXX AR RANLIB && if command -v rmp >/dev/null 2>&1; then rmp bindings swift; else cargo run --manifest-path rmp-cli/Cargo.toml -- bindings swift; fi; \
fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/justfile` around lines 30 - 36, The bindings recipes declare an unused
parameter api_env (seen on recipes bindings-swift, bindings-kotlin, and
bindings) — either remove the api_env parameter from each recipe signature to
avoid confusion, or wire it through like other recipes by exporting
OPEN_SECRET_API_URL (or the appropriate env var) based on api_env and passing it
into the rmp/cargo run command; update the recipe bodies for bindings-swift,
bindings-kotlin, and bindings to either drop api_env or to set
OPEN_SECRET_API_URL (e.g., conditional on api_env values local|dev|prod) before
invoking rmp/cargo so the parameter is actually used.

Comment on lines +96 to +150
match args.target {
crate::cli::BindingsTarget::Swift => {
let _ = ios.ok_or_else(|| CliError::user("rmp.toml missing [ios] section"))?;
if args.clean {
clean_ios(root, verbose)?;
}
if args.check {
check_ios_sources(root, &cfg, verbose)?;
} else {
generate_ios_sources(root, &cfg, profile, false, verbose)?;
}
build_ios_xcframework(
root,
&core_lib,
&core_pkg,
&default_ios_targets(),
profile,
verbose,
)?;
}
crate::cli::BindingsTarget::Kotlin => {
let _ = android.ok_or_else(|| CliError::user("rmp.toml missing [android] section"))?;
if args.clean {
clean_android(root, verbose)?;
}
if args.check {
check_android_sources(root, &cfg, verbose)?;
} else {
generate_android_sources(root, &cfg, profile, false, verbose)?;
}
build_android_so(root, &core_pkg, &default_android_abis(), profile, verbose)?;
}
crate::cli::BindingsTarget::All => {
let _ = ios.ok_or_else(|| CliError::user("rmp.toml missing [ios] section"))?;
let _ = android.ok_or_else(|| CliError::user("rmp.toml missing [android] section"))?;
if args.clean {
clean_ios(root, verbose)?;
clean_android(root, verbose)?;
}
if args.check {
check_ios_sources(root, &cfg, verbose)?;
check_android_sources(root, &cfg, verbose)?;
} else {
generate_ios_sources(root, &cfg, profile, false, verbose)?;
generate_android_sources(root, &cfg, profile, false, verbose)?;
}
build_ios_xcframework(
root,
&core_lib,
&core_pkg,
&default_ios_targets(),
profile,
verbose,
)?;
build_android_so(root, &core_pkg, &default_android_abis(), profile, verbose)?;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--check still performs full platform builds.

In the args.check branches, the source diff runs first, but the code then immediately builds the XCFramework and Android .so outputs anyway. That makes bindings --check mutate build artifacts and require Xcode/NDK even when the generated sources already match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/bindings.rs` around lines 96 - 150, The check path
currently still invokes build_ios_xcframework and build_android_so after running
check_ios_sources/check_android_sources; change the control flow so that when
args.check is true no builds are performed — either return early after the check
calls or move the build_ios_xcframework and build_android_so invocations into
the else branch that runs when !args.check. Apply this to the Swift branch
(match arm handling crate::cli::BindingsTarget::Swift, functions
check_ios_sources and build_ios_xcframework), the Kotlin branch
(crate::cli::BindingsTarget::Kotlin, functions check_android_sources and
build_android_so), and the All branch (both
check_ios_sources/check_android_sources and
build_ios_xcframework/build_android_so).

Comment on lines +417 to +448
// Assemble debug APK.
human_log(verbose, "gradle assembleDebug");
let ci = is_ci();
let mut cmd = Command::new("./gradlew");
cmd.current_dir(root.join("android"))
.arg(":app:assembleDebug");
if ci {
// CI stability + debuggability.
cmd.arg("--no-daemon")
.arg("--console=plain")
.arg("--stacktrace");
} else if verbose {
cmd.arg("--console=plain");
}
let status = cmd
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| CliError::operational(format!("failed to run gradlew: {e}")))?;
if !status.success() {
return Err(CliError::operational("gradle assembleDebug failed"));
}

let apk = root.join("android/app/build/outputs/apk/debug/app-debug.apk");
if !apk.is_file() {
return Err(CliError::operational(format!(
"expected apk not found: {}",
apk.to_string_lossy()
)));
}

let pkg = format!("{app_id}.dev");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

--release is ignored in the Android run path.

This branch always runs :app:assembleDebug, reads app-debug.apk, and launches "{appId}.dev", even when release is true. rmp run android --release therefore still builds and installs the debug variant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/run.rs` around lines 417 - 448, The Android path always
builds the debug variant; update the logic in run.rs where you create the gradle
Command and set apk/pkg (currently using ":app:assembleDebug", apk =
"android/app/build/outputs/apk/debug/app-debug.apk", and pkg =
format!("{app_id}.dev")) to branch on the release flag: when release is true,
run ":app:assembleRelease", point apk to
"android/app/build/outputs/apk/release/app-release.apk", and set pkg to the
production package id (e.g., app_id without the ".dev" suffix); otherwise keep
the existing debug assemble, apk path, and pkg naming. Ensure the Status check
and file-exists error messages still apply to the selected apk.

Comment on lines +33 to +64
pub fn discover_xcode_dev_dir() -> Result<PathBuf, CliError> {
if let Ok(v) = std::env::var("DEVELOPER_DIR") {
let p = PathBuf::from(v);
if (p.join("usr/bin/xcrun").exists() || p.join("usr/bin/simctl").exists())
&& developer_dir_supports_iphoneos(&p)
{
return Ok(p);
}
}
let apps = std::fs::read_dir("/Applications")
.map_err(|e| CliError::operational(format!("failed to read /Applications: {e}")))?;
let mut candidates: Vec<PathBuf> = vec![];
for ent in apps.flatten() {
let name = ent.file_name();
let name = name.to_string_lossy();
if !name.starts_with("Xcode") || !name.ends_with(".app") {
continue;
}
let dev = ent.path().join("Contents/Developer");
if dev.is_dir() {
candidates.push(dev);
}
}
candidates.sort();
for dev in candidates.into_iter().rev() {
if developer_dir_supports_iphoneos(&dev) {
return Ok(dev);
}
}
Err(CliError::operational(
"Xcode with iPhoneOS SDK not found under /Applications",
))
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Check xcode-select -p before scanning /Applications.

With DEVELOPER_DIR unset, this ignores the active developer directory entirely. A valid Xcode selected via xcode-select but installed outside /Applications will be treated as missing, so iOS commands fail on correctly configured machines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rmp-cli/src/util.rs` around lines 33 - 64, discover_xcode_dev_dir
currently only checks DEVELOPER_DIR and /Applications; add a step to run and
read the output of "xcode-select -p" (e.g., via std::process::Command) when
DEVELOPER_DIR is unset, convert that path to a PathBuf, and apply the same
validation used elsewhere (check presence of "usr/bin/xcrun" or "usr/bin/simctl"
and call developer_dir_supports_iphoneos) before falling back to scanning
/Applications; update discover_xcode_dev_dir to return that validated path if
valid and handle/propagate any command errors as a CliError similar to existing
error handling.

Comment on lines +505 to +511
InternalEvent::SessionRestoreFailed => {
state.auth = AuthState::Ready;
state.router = Router {
default_screen: Screen::Login,
screen_stack: vec![],
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear persisted tokens when session restore fails.

A restore failure only sends the UI back to login. It never emits empty SessionTokens, so the stale tokens remain in storage and every launch retries the same broken restore path from native/ios/Sources/AppManager.swift, native/android/app/src/main/java/cloud/opensecret/maple/AppManager.kt, and native/desktop/iced/src/main.rs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 505 - 511, In the
InternalEvent::SessionRestoreFailed handler, in addition to setting state.auth =
AuthState::Ready and resetting state.router, clear any in-memory and persisted
session tokens so stale credentials aren't retried: set the relevant field
(e.g., state.session_tokens or state.tokens) to an empty/default SessionTokens
(SessionTokens::default() or SessionTokens::empty()) and call your
persistence/flush routine (or emit the existing SessionTokens update event) to
overwrite storage so the native launch code stops attempting the broken restore
path; update the InternalEvent::SessionRestoreFailed branch in lib.rs
accordingly.

Comment on lines +545 to +550
InternalEvent::AgentMessageChunk { messages, step } => {
// Each event carries new messages for this step.
// Finalize any currently-streaming message, then
// append all new messages (last one stays streaming).
eprintln!("[agent] step={step} messages={messages:?}");

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove chat content from stderr logging.

messages:? dumps full streamed agent payloads into stderr/logcat/Xcode logs. That's user conversation data and should not be logged in normal builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/rust/src/lib.rs` around lines 545 - 550, The stderr logging in the
InternalEvent::AgentMessageChunk match arm currently prints full user messages
via eprintln! with "messages:?"; remove or redact that sensitive payload and
instead log only non-sensitive metadata (e.g., step and messages.len() or a
redacted placeholder) in the AgentMessageChunk branch so conversation content is
not written to stderr/logcat/Xcode; update the eprintln! call in the
InternalEvent::AgentMessageChunk handler to omit messages:? and use a safe
alternative like step and message count or a fixed string.

Ship signed dev builds for iOS, Android, macOS, and Linux on every PR so we can validate native 3.0 releases without changing the existing 2.0 automation.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
native/scripts/ci/package-linux.sh (1)

84-85: Consider pinning linuxdeploy to a specific release for reproducible builds.

The script downloads from the continuous release, which changes over time and could introduce unexpected behavior or break builds. Pinning to a specific version ensures reproducibility.

💡 Example with pinned version
-curl -fsSL "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -o "$linuxdeploy_path"
+# Pin to a specific release for reproducibility
+LINUXDEPLOY_VERSION="1-alpha-20240109-1"
+curl -fsSL "https://github.com/linuxdeploy/linuxdeploy/releases/download/${LINUXDEPLOY_VERSION}/linuxdeploy-x86_64.AppImage" -o "$linuxdeploy_path"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/package-linux.sh` around lines 84 - 85, The script
currently downloads linuxdeploy from the "continuous" channel which makes builds
non-reproducible; change it to use a pinned release by adding a
LINUXDEPLOY_VERSION variable and substitute that into the download URL (replace
"continuous" with the specific tag) where linuxdeploy_path is used, e.g.,
construct the URL using LINUXDEPLOY_VERSION and keep the chmod +x
"$linuxdeploy_path" step as-is so the script always retrieves a fixed,
reproducible linuxdeploy-x86_64.AppImage.
native/android/app/build.gradle.kts (1)

133-135: Update to stable version 1.1.0.

security-crypto:1.1.0-alpha06 is an alpha release. A stable 1.1.0 is available and should be used for production builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/android/app/build.gradle.kts` around lines 133 - 135, Replace the
alpha dependency declaration
implementation("androidx.security:security-crypto:1.1.0-alpha06") with the
stable release by updating it to
implementation("androidx.security:security-crypto:1.1.0"); ensure this change is
made in the dependency block where the security-crypto implementation is
declared so production builds use the stable 1.1.0 artifact.
native/scripts/ci/build-android-release.sh (1)

6-6: Consider adding require_env validation for sourced variables.

The script relies on variables from common.sh (native_root, ci_build_root, maple_version_value, maple_build_number, open_secret_api_url) but doesn't explicitly validate they're set. Adding require_env after sourcing would provide clearer error messages if common.sh fails to compute expected values.

🛡️ Suggested validation
 source "$(cd "$(dirname "$0")" && pwd)/common.sh"
 
+# Validate variables exported by common.sh
+require_env native_root ci_build_root maple_version_value maple_build_number open_secret_api_url
+
 require_command cargo
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/build-android-release.sh` at line 6, Add explicit
environment validation after sourcing common.sh by calling require_env for each
variable the script depends on: native_root, ci_build_root, maple_version_value,
maple_build_number, and open_secret_api_url. Locate the top of
build-android-release.sh where require_command is invoked (and where common.sh
is sourced) and add require_env calls for those variable names so the script
fails fast with clear messages if any sourced value is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/native-beta-android.yml:
- Around line 64-70: The heredoc writing native/android/keystore.properties
currently includes leading spaces so keys become " keyAlias" etc and
Properties.load() calls (e.g., keystoreProperties.getProperty("keyAlias") in
build.gradle.kts) fail; update the workflow to write the properties without
leading whitespace (for example use an unindented heredoc or a dedenting form)
so lines are exactly "keyAlias=...", "password=...", "storeFile=..." and
reference RUNNER_TEMP/maple-upload.jks and native/android/keystore.properties
when making the change.

In `@native/scripts/ci/common.sh`:
- Around line 55-79: The tauri_sign_artifact function writes the private key to
a temp file (key_file) but doesn’t guarantee cleanup if signing fails or the
script is interrupted; add a trap immediately after creating key_file (after
key_file="$(mktemp)") to remove "$key_file" on EXIT and ERR, then clear that
trap after the explicit rm -f "$key_file" so the temp is always cleaned up even
on failure or signals; keep the existing artifact signature checks and exit
behavior unchanged.

---

Nitpick comments:
In `@native/android/app/build.gradle.kts`:
- Around line 133-135: Replace the alpha dependency declaration
implementation("androidx.security:security-crypto:1.1.0-alpha06") with the
stable release by updating it to
implementation("androidx.security:security-crypto:1.1.0"); ensure this change is
made in the dependency block where the security-crypto implementation is
declared so production builds use the stable 1.1.0 artifact.

In `@native/scripts/ci/build-android-release.sh`:
- Line 6: Add explicit environment validation after sourcing common.sh by
calling require_env for each variable the script depends on: native_root,
ci_build_root, maple_version_value, maple_build_number, and open_secret_api_url.
Locate the top of build-android-release.sh where require_command is invoked (and
where common.sh is sourced) and add require_env calls for those variable names
so the script fails fast with clear messages if any sourced value is missing.

In `@native/scripts/ci/package-linux.sh`:
- Around line 84-85: The script currently downloads linuxdeploy from the
"continuous" channel which makes builds non-reproducible; change it to use a
pinned release by adding a LINUXDEPLOY_VERSION variable and substitute that into
the download URL (replace "continuous" with the specific tag) where
linuxdeploy_path is used, e.g., construct the URL using LINUXDEPLOY_VERSION and
keep the chmod +x "$linuxdeploy_path" step as-is so the script always retrieves
a fixed, reproducible linuxdeploy-x86_64.AppImage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0a2f84e-23da-485e-bafc-b6f339e256fb

📥 Commits

Reviewing files that changed from the base of the PR and between 5255fa5 and 337154a.

📒 Files selected for processing (9)
  • .github/workflows/native-beta-android.yml
  • .github/workflows/native-beta-desktop.yml
  • .github/workflows/native-beta-ios.yml
  • native/android/app/build.gradle.kts
  • native/scripts/ci/build-android-release.sh
  • native/scripts/ci/build-ios-testflight.sh
  • native/scripts/ci/common.sh
  • native/scripts/ci/package-linux.sh
  • native/scripts/ci/package-macos.sh

Comment on lines +64 to +70
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/maple-upload.jks"
cat > native/android/keystore.properties <<EOF
keyAlias=$ANDROID_KEY_ALIAS
password=$ANDROID_KEY_PASSWORD
storeFile=$RUNNER_TEMP/maple-upload.jks
EOF
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Leading whitespace in keystore.properties may cause parsing failures.

The heredoc content has leading spaces before each property key. Java's Properties.load() will include these spaces as part of the key names, causing lookups like keystoreProperties.getProperty("keyAlias") in build.gradle.kts to fail.

🐛 Proposed fix
         run: |
           echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/maple-upload.jks"
           cat > native/android/keystore.properties <<EOF
-          keyAlias=$ANDROID_KEY_ALIAS
-          password=$ANDROID_KEY_PASSWORD
-          storeFile=$RUNNER_TEMP/maple-upload.jks
-          EOF
+keyAlias=$ANDROID_KEY_ALIAS
+password=$ANDROID_KEY_PASSWORD
+storeFile=$RUNNER_TEMP/maple-upload.jks
+EOF
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/native-beta-android.yml around lines 64 - 70, The heredoc
writing native/android/keystore.properties currently includes leading spaces so
keys become " keyAlias" etc and Properties.load() calls (e.g.,
keystoreProperties.getProperty("keyAlias") in build.gradle.kts) fail; update the
workflow to write the properties without leading whitespace (for example use an
unindented heredoc or a dedenting form) so lines are exactly "keyAlias=...",
"password=...", "storeFile=..." and reference RUNNER_TEMP/maple-upload.jks and
native/android/keystore.properties when making the change.

Comment on lines +55 to +79
function tauri_sign_artifact() {
local artifact="$1"

if [[ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" || -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
return 0
fi

require_command cargo-tauri

local key_file
key_file="$(mktemp)"
printf '%s' "$TAURI_SIGNING_PRIVATE_KEY" > "$key_file"

cargo-tauri signer sign \
--private-key-path "$key_file" \
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"$artifact"

rm -f "$key_file"

if [[ ! -f "${artifact}.sig" ]]; then
printf 'Expected detached signature at %s.sig\n' "$artifact" >&2
exit 1
fi
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Private key may be left on disk if signing fails or script is interrupted.

If cargo-tauri signer sign fails (e.g., due to set -e in the sourcing script) or the process is interrupted, the temp file containing the private key won't be cleaned up.

🛡️ Proposed fix using trap for guaranteed cleanup
 function tauri_sign_artifact() {
   local artifact="$1"

   if [[ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" || -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
     return 0
   fi

   require_command cargo-tauri

   local key_file
   key_file="$(mktemp)"
+  trap 'rm -f "$key_file"' EXIT
   printf '%s' "$TAURI_SIGNING_PRIVATE_KEY" > "$key_file"

   cargo-tauri signer sign \
     --private-key-path "$key_file" \
     --password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
     "$artifact"

   rm -f "$key_file"
+  trap - EXIT

   if [[ ! -f "${artifact}.sig" ]]; then
     printf 'Expected detached signature at %s.sig\n' "$artifact" >&2
     exit 1
   fi
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function tauri_sign_artifact() {
local artifact="$1"
if [[ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" || -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
return 0
fi
require_command cargo-tauri
local key_file
key_file="$(mktemp)"
printf '%s' "$TAURI_SIGNING_PRIVATE_KEY" > "$key_file"
cargo-tauri signer sign \
--private-key-path "$key_file" \
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"$artifact"
rm -f "$key_file"
if [[ ! -f "${artifact}.sig" ]]; then
printf 'Expected detached signature at %s.sig\n' "$artifact" >&2
exit 1
fi
}
function tauri_sign_artifact() {
local artifact="$1"
if [[ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" || -z "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" ]]; then
return 0
fi
require_command cargo-tauri
local key_file
key_file="$(mktemp)"
trap 'rm -f "$key_file"' EXIT
printf '%s' "$TAURI_SIGNING_PRIVATE_KEY" > "$key_file"
cargo-tauri signer sign \
--private-key-path "$key_file" \
--password "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" \
"$artifact"
rm -f "$key_file"
trap - EXIT
if [[ ! -f "${artifact}.sig" ]]; then
printf 'Expected detached signature at %s.sig\n' "$artifact" >&2
exit 1
fi
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/common.sh` around lines 55 - 79, The tauri_sign_artifact
function writes the private key to a temp file (key_file) but doesn’t guarantee
cleanup if signing fails or the script is interrupted; add a trap immediately
after creating key_file (after key_file="$(mktemp)") to remove "$key_file" on
EXIT and ERR, then clear that trap after the explicit rm -f "$key_file" so the
temp is always cleaned up even on failure or signals; keep the existing artifact
signature checks and exit behavior unchanged.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 17 additional findings in Devin Review.

Open in Devin Review

Comment on lines +441 to +446
InternalEvent::AttestationComplete => {
state.auth = AuthState::Ready;
state.router = Router {
default_screen: Screen::Login,
screen_stack: vec![],
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Race condition: initial attestation can override successful session restore, kicking user to login

When the app starts, FfiApp::new spawns an initial attestation handshake asynchronously (native/rust/src/lib.rs:376). Immediately after, the platform layer dispatches RestoreSession which spawns its own attestation + get_user call (native/rust/src/lib.rs:745-770). Both async tasks send results to the same core_rx channel. If the restore succeeds first (SessionRestored at line 491 sets Screen::Chat), the initial attestation may complete later and its AttestationComplete handler at line 441 unconditionally resets state.auth = AuthState::Ready and state.router.default_screen = Screen::Login, kicking the already-authenticated user back to the login screen and losing their session state.

Race timeline that triggers the bug
  1. FfiApp::new spawns initial attestation (T=0ms)
  2. Platform dispatches RestoreSession → core thread sets tokens, spawns restore attestation (T≈2ms)
  3. Restore attestation + get_user completes → SessionRestored processed → user sees Chat (T≈200ms)
  4. Initial attestation completes (delayed by network) → AttestationComplete processed → state reset to Login (T≈250ms)
Prompt for agents
In native/rust/src/lib.rs, the AttestationComplete handler at lines 441-446 unconditionally sets state.auth = AuthState::Ready and state.router to Screen::Login. This creates a race condition with RestoreSession which can complete and set the user to Screen::Chat before the initial attestation finishes.

Fix: Guard the AttestationComplete handler to only transition to Ready/Login if the user is not already logged in. For example:

InternalEvent::AttestationComplete => {
    if matches!(state.auth, AuthState::Initializing) {
        state.auth = AuthState::Ready;
        state.router = Router {
            default_screen: Screen::Login,
            screen_stack: vec![],
        };
    }
}

Apply the same guard to AttestationFailed at lines 448-455.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Use the correct Android NDK setup, run native binding generation from the project root, and embed the desktop beta API URL at build time so packaged apps stop falling back to localhost.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 19 additional findings in Devin Review.

Open in Devin Review

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Hello Chat</string>
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Android app_name string resource is 'Hello Chat' instead of 'Maple' (template leftover)

The string resource app_name in native/android/app/src/main/res/values/strings.xml is set to "Hello Chat" — a leftover from template scaffolding. While AndroidManifest.xml directly sets android:label="Maple", the app_name string resource may be referenced by the theme or other system components (e.g., the recent apps list, settings). The same leftover exists in native/android/settings.gradle.kts:15 where rootProject.name = "Hello Chat".

Suggested change
<string name="app_name">Hello Chat</string>
<string name="app_name">Maple</string>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
native/scripts/ci/package-linux.sh (3)

73-73: Consider adding email to Maintainer field.

Debian packaging best practices recommend the Maintainer field include an email address in the format Name <email@example.com>. This helps users report issues.

📝 Proposed fix
-Maintainer: OpenSecret
+Maintainer: OpenSecret <support@opensecret.cloud>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/package-linux.sh` at line 73, The Maintainer field
currently reads "Maintainer: OpenSecret" and should include an email address;
update the string "Maintainer: OpenSecret" in package-linux.sh to the Debian
recommended format, e.g. "Maintainer: OpenSecret <maintainer@example.com>",
ensuring the angle-bracketed email is present and properly quoted/escaped if
used inside a shell variable or here-doc.

8-11: Consider adding shellcheck source directive for clarity.

The variables ci_build_root, maple_version_value, maple_build_number, native_root, and open_secret_api_url are referenced but defined in common.sh. Adding a shellcheck directive suppresses the warnings and documents the dependency.

📝 Proposed fix
 source "$(cd "$(dirname "$0")" && pwd)/common.sh"
+# shellcheck source=common.sh
 
 require_command cargo cargo-tauri curl dpkg-deb dpkg-shlibdeps

Alternatively, add at the top of the variable block:

+# Variables ci_build_root, maple_version_value, maple_build_number, native_root,
+# and open_secret_api_url are exported by common.sh
 linux_build_root="$ci_build_root/desktop-linux"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/package-linux.sh` around lines 8 - 11, The script
references variables defined in common.sh (ci_build_root, maple_version_value,
maple_build_number, native_root, open_secret_api_url) but lacks a ShellCheck
source directive; add a top-of-file ShellCheck directive like "shellcheck
source=common.sh" immediately before the existing source/include of common.sh in
package-linux.sh so ShellCheck recognizes those external vars and suppresses
warnings while documenting the dependency.

87-88: Consider pinning linuxdeploy version and verifying checksum.

Downloading from the continuous release tag means the binary can change between CI runs, affecting reproducibility. For supply chain security, consider:

  1. Pinning to a specific release version (most recent is 1-alpha-20251107-1, though linuxdeploy releases are all labeled "alpha")
  2. Verifying the SHA256 checksum after download
🔒 Proposed fix with checksum verification
-curl -fsSL "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -o "$linuxdeploy_path"
-chmod +x "$linuxdeploy_path"
+LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20251107-1/linuxdeploy-x86_64.AppImage"
+LINUXDEPLOY_SHA256="<insert-expected-sha256-here>"
+
+curl -fsSL "$LINUXDEPLOY_URL" -o "$linuxdeploy_path"
+echo "$LINUXDEPLOY_SHA256  $linuxdeploy_path" | sha256sum -c -
+chmod +x "$linuxdeploy_path"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/package-linux.sh` around lines 87 - 88, Change the curl
download to use a pinned linuxdeploy release instead of the "continuous" tag and
add SHA256 checksum verification after download: update the URL that writes to
"$linuxdeploy_path" to reference a specific release artifact (the exact release
string for linuxdeploy-x86_64.AppImage), compute the downloaded file's SHA256
(e.g., via sha256sum) and compare it against the known expected checksum
constant in the script, fail/exit non‑zero if the checksums do not match, and
only then run chmod +x on "$linuxdeploy_path"; reference the variables and file
name used in the script (linuxdeploy_path and linuxdeploy-x86_64.AppImage) when
adding the expected checksum and verification step.
native/scripts/ci/build-ios-testflight.sh (1)

40-43: Lock Cargo resolution in CI.

This step should use --locked so binding generation cannot silently pick up a different dependency graph than the checked-in lockfile.

Proposed change
 (
   cd "$native_root"
-  cargo run --manifest-path "$native_root/rmp-cli/Cargo.toml" -- bindings swift
+  cargo run --locked --manifest-path "$native_root/rmp-cli/Cargo.toml" -- bindings swift
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/build-ios-testflight.sh` around lines 40 - 43, The cargo
invocation that generates Swift bindings (the subshell running cargo run
--manifest-path "$native_root/rmp-cli/Cargo.toml" -- bindings swift) should be
locked to the checked-in Cargo.lock; update that command to pass the --locked
flag so dependency resolution cannot change in CI. Modify the subshell
invocation that runs cargo run in build-ios-testflight.sh (the line invoking
rmp-cli with -- bindings swift) to include --locked immediately after cargo run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@native/scripts/ci/build-android-release.sh`:
- Line 15: The android_version_code computation uses the wrong base constant;
update the base from 300000000 to 3000000000 so that
android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((3000000000 +
maple_build_number))}" produces values like 3000000001; locate the expression
that sets android_version_code (and references MAPLE_ANDROID_VERSION_CODE and
maple_build_number) and replace the base constant accordingly to match the PR
objective of 3,000,000,000.

In `@native/scripts/ci/build-ios-testflight.sh`:
- Around line 88-91: The altool upload step (xcrun altool --upload-app ...)
doesn't pass the key file location so it fails when the private key is outside
~/.private_keys; update the xcrun altool invocation in build-ios-testflight.sh
to include the APPLE_API_KEY_PATH variable (e.g. add --apiKeyPath
"$APPLE_API_KEY_PATH") alongside --apiKey "$APPLE_API_KEY" and --apiIssuer
"$APPLE_API_ISSUER" so altool can locate the private key when it's placed in
$RUNNER_TEMP/app-store-connect/ or other custom paths.

---

Nitpick comments:
In `@native/scripts/ci/build-ios-testflight.sh`:
- Around line 40-43: The cargo invocation that generates Swift bindings (the
subshell running cargo run --manifest-path "$native_root/rmp-cli/Cargo.toml" --
bindings swift) should be locked to the checked-in Cargo.lock; update that
command to pass the --locked flag so dependency resolution cannot change in CI.
Modify the subshell invocation that runs cargo run in build-ios-testflight.sh
(the line invoking rmp-cli with -- bindings swift) to include --locked
immediately after cargo run.

In `@native/scripts/ci/package-linux.sh`:
- Line 73: The Maintainer field currently reads "Maintainer: OpenSecret" and
should include an email address; update the string "Maintainer: OpenSecret" in
package-linux.sh to the Debian recommended format, e.g. "Maintainer: OpenSecret
<maintainer@example.com>", ensuring the angle-bracketed email is present and
properly quoted/escaped if used inside a shell variable or here-doc.
- Around line 8-11: The script references variables defined in common.sh
(ci_build_root, maple_version_value, maple_build_number, native_root,
open_secret_api_url) but lacks a ShellCheck source directive; add a top-of-file
ShellCheck directive like "shellcheck source=common.sh" immediately before the
existing source/include of common.sh in package-linux.sh so ShellCheck
recognizes those external vars and suppresses warnings while documenting the
dependency.
- Around line 87-88: Change the curl download to use a pinned linuxdeploy
release instead of the "continuous" tag and add SHA256 checksum verification
after download: update the URL that writes to "$linuxdeploy_path" to reference a
specific release artifact (the exact release string for
linuxdeploy-x86_64.AppImage), compute the downloaded file's SHA256 (e.g., via
sha256sum) and compare it against the known expected checksum constant in the
script, fail/exit non‑zero if the checksums do not match, and only then run
chmod +x on "$linuxdeploy_path"; reference the variables and file name used in
the script (linuxdeploy_path and linuxdeploy-x86_64.AppImage) when adding the
expected checksum and verification step.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 055669a4-e7de-4d27-8f28-1b0a71eeb91c

📥 Commits

Reviewing files that changed from the base of the PR and between 337154a and a557ae7.

📒 Files selected for processing (5)
  • .github/workflows/native-beta-android.yml
  • native/desktop/iced/src/main.rs
  • native/scripts/ci/build-android-release.sh
  • native/scripts/ci/build-ios-testflight.sh
  • native/scripts/ci/package-linux.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/native-beta-android.yml
  • native/desktop/iced/src/main.rs


android_build_root="$ci_build_root/android"
android_version_name="${MAPLE_ANDROID_VERSION_NAME:-${maple_version_value}-beta.${maple_build_number}}"
android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((300000000 + maple_build_number))}"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Version code base appears incorrect.

The PR objectives state the Android versionCode should be 3000000000 (3 billion), but this line uses 300000000 (300 million) as the base—off by a factor of 10. This would result in version codes like 300000001 instead of 3000000001 for the first beta build.

🐛 Proposed fix
-android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((300000000 + maple_build_number))}"
+android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((3000000000 + maple_build_number))}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((300000000 + maple_build_number))}"
android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((3000000000 + maple_build_number))}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/build-android-release.sh` at line 15, The
android_version_code computation uses the wrong base constant; update the base
from 300000000 to 3000000000 so that
android_version_code="${MAPLE_ANDROID_VERSION_CODE:-$((3000000000 +
maple_build_number))}" produces values like 3000000001; locate the expression
that sets android_version_code (and references MAPLE_ANDROID_VERSION_CODE and
maple_build_number) and replace the base constant accordingly to match the PR
objective of 3,000,000,000.

Comment on lines +88 to +91
xcrun altool --upload-app --type ios \
--file "$ipa_path" \
--apiKey "$APPLE_API_KEY" \
--apiIssuer "$APPLE_API_ISSUER"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 'API_PRIVATE_KEYS_DIR|AuthKey_|APPLE_API_KEY_PATH|altool' \
  .github/workflows native/scripts/ci

Repository: OpenSecretCloud/Maple

Length of output: 8545


altool will not locate the private key file in workflows placing it outside ~/.private_keys/.

The script correctly passes $APPLE_API_KEY_PATH to xcodebuild (lines 58, 77) but the subsequent altool upload step (lines 88-91) only provides the key ID and issuer. While workflows like testflight-on-comment.yml and mobile-build.yml place the key in ~/.private_keys/ (where altool searches by default), others like native-beta-ios.yml and native-beta-desktop.yml place it in $RUNNER_TEMP/app-store-connect/. In those cases, altool fails at upload time because it cannot find the key.

Proposed change
 if [[ -z "$ipa_path" ]]; then
   echo "Failed to locate exported IPA" >&2
   exit 1
 fi

+altool_keys_dir="$ios_build_root/private_keys"
+ensure_dir "$altool_keys_dir"
+cp "$APPLE_API_KEY_PATH" "$altool_keys_dir/AuthKey_${APPLE_API_KEY}.p8"
+export API_PRIVATE_KEYS_DIR="$altool_keys_dir"
+
 xcrun altool --upload-app --type ios \
   --file "$ipa_path" \
   --apiKey "$APPLE_API_KEY" \
   --apiIssuer "$APPLE_API_ISSUER"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@native/scripts/ci/build-ios-testflight.sh` around lines 88 - 91, The altool
upload step (xcrun altool --upload-app ...) doesn't pass the key file location
so it fails when the private key is outside ~/.private_keys; update the xcrun
altool invocation in build-ios-testflight.sh to include the APPLE_API_KEY_PATH
variable (e.g. add --apiKeyPath "$APPLE_API_KEY_PATH") alongside --apiKey
"$APPLE_API_KEY" and --apiIssuer "$APPLE_API_ISSUER" so altool can locate the
private key when it's placed in $RUNNER_TEMP/app-store-connect/ or other custom
paths.

Restore the native Android Gradle wrapper in version control so CI can build the app, and place the App Store Connect key where altool expects it so TestFlight uploads complete.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
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.

1 participant