feat: add Capacitor Android build with MobilePlatform and native MCP transport#3650
feat: add Capacitor Android build with MobilePlatform and native MCP transport#36503aKHP wants to merge 5 commits intochatboxai:mainfrom
Conversation
- Add translation.ts stub (no-op) so the OSS build compiles; the full implementation lives in the private edition only. - Add github-copilot provider stub so providers/index.ts side-effect import resolves without error in the open-source build. - Extend oauth/index.ts with OAuthCredentials / OAuthResult interfaces and OAuthIpcChannels constants required by the OAuth flow plumbing. - Add .erb/scripts/delete-source-maps-runner.js build helper. - Pin Node / pnpm versions via Volta in package.json.
- Add capacitor.config.ts (appId: app.chatboxai.chatbox, webDir points to the renderer dist output used by the mobile:sync:android script). - Add the generated android/ project (via `npx cap add android`). android/local.properties is excluded by android/.gitignore. - Add MobilePlatform class that extends WebPlatform with type = 'mobile', enabling all mobile UI branches (sidebar, SplashScreen, routing, etc.). Device metadata is resolved via @capacitor/app and @capacitor/device. - Update platform/index.ts to return MobilePlatform when Capacitor.isNativePlatform() is true (Android / iOS native runtime).
…obile On Android/iOS the WebView enforces CORS, so the standard browser fetch used by StreamableHTTPClientTransport is blocked for cross-origin MCP servers. MobileStreamableHTTPTransport routes all requests through @capacitor/http (OS-level networking, CORS-exempt): - POST and DELETE via CapacitorHttp.request - GET (SSE stream) via createNativeReadableStream, parsed with a line-by-line SSE decoder that handles chunked native responses Additional changes: - controller.ts: branch on Capacitor.isNativePlatform() to use the new transport; guard stdio path with a window.electronAPI check so an actionable error is thrown on mobile instead of a silent hang. - feature-flags.ts: enable MCP for 'mobile' and 'web' platforms, not just 'desktop'. - ConfigModal.tsx: hide the stdio radio button on non-desktop platforms to prevent users from configuring an unsupported transport type.
The previous code computed MUI's base font size as (userFontSize * 14) / 16 which yields 12.25px at the default setting of 14px — ~12.5% smaller than the MUI default of 14. Analysis of the official chatbox-1.20.1.apk bundle confirms it uses a fixed `fontSize: 14`, independent of the user-configurable message font size (which is correctly applied via the --chatbox-msg-font-size CSS variable). Changes: - getThemeDesign: remove fontSize parameter, hard-code typography.fontSize to 14 with an explanatory comment. - useAppTheme: drop the fontSize subscription from useSettingsStore and remove it from the useMemo dependency array. - Settings.tsx (navigateToSettings): update call site to the new 2-arg getThemeDesign signature.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR adds comprehensive Android/mobile platform support to a desktop Electron application using Capacitor. It includes Android build configuration, Gradle setup with dependency management, mobile platform detection, mobile-specific MCP HTTP transport via Capacitor, OAuth infrastructure, and adjusts platform-aware feature flags and theming logic. Changes
Sequence DiagramsequenceDiagram
participant Client as Renderer<br/>(MCP Client)
participant CapHttp as CapacitorHttp<br/>(Mobile Layer)
participant Native as Native<br/>iOS/Android
participant Server as MCP<br/>Server
rect rgba(100, 150, 200, 0.5)
Note over Client,Server: Mobile MCP HTTP Transport Initialization
Client->>CapHttp: POST /initialize (mcp-session-id)
CapHttp->>Native: HTTP POST request
Native->>Server: Forward HTTP request
Server->>Native: HTTP 202 Accepted + session-id header
Native->>CapHttp: Response with headers
CapHttp->>Client: Extract & store session-id
end
rect rgba(100, 150, 200, 0.5)
Note over Client,Server: Subsequent RPC Request
Client->>CapHttp: POST with mcp-session-id header
CapHttp->>Native: HTTP POST (auto-attach session-id)
Native->>Server: Forward with session context
Server->>Native: HTTP response
Native->>CapHttp: Response body
CapHttp->>Client: Parse JSON-RPC message
end
rect rgba(100, 150, 200, 0.5)
Note over Client,Server: SSE Streaming (notifications/initialized)
Client->>CapHttp: notifications/initialized POST
CapHttp->>Native: HTTP POST
Server->>Native: HTTP 202 + initiate SSE
Native->>CapHttp: Long-lived GET for SSE
loop SSE Event Stream
Server->>Native: data: {JSON-RPC}<br/>data: {JSON-RPC}
Native->>CapHttp: Buffer & parse chunks
CapHttp->>Client: Deliver JSON-RPC message
end
Client->>CapHttp: close()
CapHttp->>Native: DELETE session cleanup
Native->>Server: Terminate session
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (10)
.erb/scripts/delete-source-maps-runner.js (1)
5-7: Optional: deduplicate repeated cleanup calls.A small array + loop keeps this easier to extend if more output folders are added later.
Refactor sketch
-rimrafSync(path.join(webpackPaths.distMainPath, '*.js.map'), { glob: true }) -rimrafSync(path.join(webpackPaths.distRendererPath, '*.js.map'), { glob: true }) -rimrafSync(path.join(webpackPaths.distPath, 'preload', '*.js.map'), { glob: true }) +const sourceMapPatterns = [ + path.join(webpackPaths.distMainPath, '*.js.map'), + path.join(webpackPaths.distRendererPath, '*.js.map'), + path.join(webpackPaths.distPath, 'preload', '*.js.map'), +] + +for (const pattern of sourceMapPatterns) { + rimrafSync(pattern, { glob: true }) +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.erb/scripts/delete-source-maps-runner.js around lines 5 - 7, The three repeated calls to rimrafSync cleaning map files should be consolidated: build an array of the target directories (e.g., webpackPaths.distMainPath, webpackPaths.distRendererPath, path.join(webpackPaths.distPath, 'preload')) and iterate over it to call rimrafSync(path.join(dir, '*.js.map'), { glob: true }); this deduplicates the cleanup logic and makes adding new output folders easier while keeping the existing rimrafSync usage and glob option.src/renderer/hooks/useAppTheme.ts (1)
59-93: Consider extracting the base MUI font size into a shared constant.
fontSize: 14is now a product-level default; centralizing it helps prevent drift between theme logic and related settings/migration logic.♻️ Minimal refactor
+const BASE_MUI_FONT_SIZE = 14 export function getThemeDesign(realTheme: 'light' | 'dark', language: Language): ThemeOptions { return { @@ - fontSize: 14, + fontSize: BASE_MUI_FONT_SIZE,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/hooks/useAppTheme.ts` around lines 59 - 93, The theme hardcodes fontSize: 14 inside getThemeDesign; extract this into a shared constant (e.g., BASE_MUI_FONT_SIZE) and replace the literal in getThemeDesign with that constant, export it so other modules/migrations/settings can import the canonical value, and update any related places that reference the magic number to use the new constant to avoid drift.src/renderer/packages/translation.ts (1)
7-13: LGTM! Stub implementation is correct.The function correctly maintains the API contract by returning an array of nulls with the same length as the input. The
asynckeyword automatically wraps the return value in a Promise, and the caller inMessageErrTips.tsxalready handles null returns gracefully using the null-coalescing operator.💡 Optional: Consider adding a dev-mode warning
For improved developer experience, you could optionally add a console warning in development mode to alert developers that translation is unavailable in the OSS build:
export async function translateTexts( texts: string[], _targetLang: string, _options?: { sourceLang?: string } ): Promise<(string | null)[]> { + if (process.env.NODE_ENV === 'development') { + console.warn('Translation is not available in the OSS edition') + } return texts.map(() => null) }However, since the stub is well-documented and the feature is intentionally unavailable in OSS, this is purely optional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/packages/translation.ts` around lines 7 - 13, The translateTexts stub currently returns the correct Promise of nulls but optionally add a dev-only warning to make the lack of translation obvious during development: inside translateTexts, detect development mode (e.g., process.env.NODE_ENV === 'development' or an existing isDev flag) and call console.warn once (or for each call) with a clear message that translations are unavailable in the OSS build and translateTexts is a stub; keep the existing return behavior and do not alter the function signature or returned array length.src/shared/providers/definitions/github-copilot.ts (1)
9-19: Consider hiding this stub provider from OSS runtime selection.Registering this provider into the global registry with
models: []may expose a selectable but non-functional path to users. Consider gating registration or marking it hidden in OSS builds to avoid dead-end setup UX.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/providers/definitions/github-copilot.ts` around lines 9 - 19, The githubCopilotProvider entry currently registers a stub with defaultSettings.models = [] which can show a selectable but non-functional option; update the registration so the provider is not exposed in OSS runtimes by either adding a hidden flag or gating its registration behind a build/runtime check. Modify the call to defineProvider for githubCopilotProvider to include a property like hidden: true (or an isInternal/isExperimental flag) or wrap the export/registration logic with a condition (e.g., process.env.RUNTIME !== 'oss' or a feature-flag) so the provider is omitted from the global registry in OSS builds; ensure you adjust any consumer code that lists providers to respect the hidden/flag field and reference githubCopilotProvider, defineProvider, and defaultSettings.models while making the change.src/shared/oauth/index.ts (2)
21-41: Prefer discriminated unions for result contracts.Current shapes allow impossible states (
success: truewithout required payload,success: falsewithouterror). A discriminated union tightens correctness and simplifies consumers.Proposed refactor
-export interface OAuthResult { - success: boolean - error?: string - credentials?: OAuthCredentials -} +export type OAuthResult = + | { success: true; credentials: OAuthCredentials } + | { success: false; error: string } -export interface OAuthStartResult { - success: boolean - error?: string - authUrl?: string -} +export type OAuthStartResult = + | { success: true; authUrl: string } + | { success: false; error: string } -export interface DeviceFlowStartResult { - success: boolean - error?: string - deviceCode?: string - userCode?: string - verificationUri?: string - expiresIn?: number - interval?: number -} +export type DeviceFlowStartResult = + | { + success: true + deviceCode: string + userCode: string + verificationUri: string + expiresIn: number + interval?: number + } + | { success: false; error: string }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/oauth/index.ts` around lines 21 - 41, Replace the loose interfaces OAuthResult, OAuthStartResult, and DeviceFlowStartResult with discriminated union types so consumers cannot get impossible states; e.g., define each as a union of { success: true, /* required payload props */ } | { success: false, error: string }, ensuring for OAuthResult the success arm requires credentials: OAuthCredentials, for OAuthStartResult the success arm requires authUrl: string, and for DeviceFlowStartResult the success arm requires deviceCode/userCode/verificationUri/expiresIn/interval; update any call sites/types accordingly to use the new discriminated unions (refer to the OAuthResult, OAuthStartResult, and DeviceFlowStartResult symbols).
14-19: Clarify time-unit semantics on OAuth fields.
expiresAtandexpiresInuse different time models (absolute vs duration). Add explicit unit docs to prevent seconds-vs-milliseconds bugs at call sites.Proposed tweak
export interface OAuthCredentials { accessToken: string refreshToken?: string + /** Unix epoch in milliseconds */ expiresAt?: number tokenType?: string } @@ export interface DeviceFlowStartResult { success: boolean error?: string deviceCode?: string userCode?: string verificationUri?: string + /** Duration in seconds from RFC 8628 */ expiresIn?: number + /** Poll interval in seconds */ interval?: number }Also applies to: 33-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/oauth/index.ts` around lines 14 - 19, Update the OAuth types to explicitly document time-unit semantics: add JSDoc comments to OAuthCredentials.expiresAt and any expiresIn fields (e.g., other interfaces around lines 33–41) stating the exact unit used (for example: "expiresAt — milliseconds since UNIX epoch" and "expiresIn — lifetime in seconds"), or alternatively standardize both to the same unit and document that choice; ensure the comments are adjacent to the OAuthCredentials interface and the named expiresIn fields so callers know whether to use seconds or milliseconds.android/gradle/wrapper/gradle-wrapper.properties (1)
4-4: Consider increasing Gradle download timeout to reduce CI flakiness.
networkTimeout=10000is often too aggressive for wrapper downloads in shared CI runners.Proposed tweak
-networkTimeout=10000 +networkTimeout=60000🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/gradle/wrapper/gradle-wrapper.properties` at line 4, The Gradle wrapper timeout is set too low via the networkTimeout property; update the gradle-wrapper.properties entry networkTimeout from 10000 to a much larger value (e.g., 600000) to reduce CI download failures, save the file, and verify wrapper downloads succeed in CI; the key to change is the networkTimeout property in gradle-wrapper.properties.android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java (1)
14-17: Replace template arithmetic test with a project-relevant unit test.
addition_isCorrectis scaffold boilerplate and doesn’t validate app behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java` around lines 14 - 17, The current ExampleUnitTest.addition_isCorrect scaffold test should be replaced with a project-relevant unit test: open the ExampleUnitTest class and remove or replace the addition_isCorrect method with one or more `@Test` methods that exercise real app logic (for example, a pure function in AppUtils, a ViewModel method like MainViewModel.getX, or a utility such as DateFormatter.formatDate), import and instantiate the target class, provide any necessary test inputs or mocks, then use JUnit assertions (assertEquals/assertTrue/assertThrows) to verify the expected outputs/behavior; keep the test deterministic, name the new test to reflect the behavior being validated, and ensure it compiles in the current test configuration.android/app/build.gradle (1)
20-23: Consider enabling shrink/obfuscation for release builds.For a production-targeted Android build, Line 21 (
minifyEnabled false) is a weak default. Consider turning it on for size/security hardening before release distribution.Suggested hardening
release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@android/app/build.gradle` around lines 20 - 23, Change the Android release block to enable code shrinking/obfuscation by setting minifyEnabled true for the release build (the release {} block and its minifyEnabled setting), enable shrinkResources true if desired, and ensure proguardFiles (the existing getDefaultProguardFile('proguard-android.txt') and 'proguard-rules.pro') contain the necessary keep rules for your libraries/Reflection/serialization so the app still functions; run a release build and smoke-test features to validate and iterate on proguard-rules.pro to prevent runtime breakage.src/renderer/packages/mcp/controller.ts (1)
15-19: Reject unsupportedstdioconfigs before runtime.This guard is safe, but it fires only after the server is instantiated and started. Because persisted MCP configs still allow
type: 'stdio', mobile/web clients can keep reloading an enabled server that is guaranteed to fail on every launch. Validate or migrate unsupported transports when configs are loaded/saved instead of only throwing here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/packages/mcp/controller.ts` around lines 15 - 19, Persisted MCP configs may still contain transportConfig.type === 'stdio' which will always fail on non-desktop clients; instead of only throwing inside the runtime guard that checks window.electronAPI, add validation/migration when configs are loaded or saved. Implement a validateOrMigrateMcpConfig function (called from the existing config load/save paths) that inspects transportConfig.type, removes or replaces 'stdio' entries for non-desktop platforms, and persists the updated config; keep the runtime guard in the start/instantiate code (the block that checks transportConfig and window.electronAPI) as a last-resort safety net but ensure configs are fixed earlier to prevent repeated failing restarts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/app/build.gradle`:
- Around line 47-54: Replace the broad try/catch that swallows all errors around
reading google-services.json with an explicit existence/size check: test
file('google-services.json').exists() and .length() (or .size()) before
attempting to apply plugin 'com.google.gms.google-services', and only skip
applying the plugin when the file is genuinely absent/empty; do not catch
Exception around the read/apply logic—if an unexpected error occurs while
evaluating or applying the plugin (related to servicesJSON or apply plugin:
'com.google.gms.google-services'), let it surface or log it as an error instead
of silently logging info so real setup failures aren’t hidden.
In `@android/app/capacitor.build.gradle`:
- Around line 5-6: The build currently sets Java bytecode level via
sourceCompatibility and targetCompatibility to JavaVersion.VERSION_21, which is
undocumented and will break developers without JDK 21; either document the Java
21 requirement and set org.gradle.java.home in gradle.properties (and update
README/CONTRIBUTING with installation/path guidance) or change
sourceCompatibility/targetCompatibility from JavaVersion.VERSION_21 to
JavaVersion.VERSION_17 in capacitor.build.gradle if Java 21 language/bytecode
features are not required; update the project docs accordingly so contributors
know which JDK to install.
In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`:
- Line 24: Update the expected package name in the instrumented test: in
ExampleInstrumentedTest (the method using appContext.getPackageName() and
assertEquals), change the expected string from "com.getcapacitor.app" to the
actual app id "app.chatboxai.chatbox" so the assertEquals compares
appContext.getPackageName() against the correct package name.
In `@android/app/src/main/AndroidManifest.xml`:
- Line 5: Change the Android manifest backup settings to disable automatic
backups: locate the android:allowBackup attribute in the AndroidManifest
(attribute android:allowBackup currently set to "true") and set it to "false";
additionally, explicitly disable full-data backups by adding
android:fullBackupContent="false" (or point it to an empty backup rules XML) on
the same <application> element to ensure no user/chat data is included in system
backups.
In `@android/app/src/main/res/xml/file_paths.xml`:
- Around line 3-4: The FileProvider config in file_paths.xml currently uses
external-path and cache-path entries with path="." which exposes entire storage;
narrow these to explicit subdirectories by changing the external-path
name="my_images" path="." and cache-path name="my_cache_images" path="." entries
to point at dedicated folders (for example a fixed app images dir under external
storage and a specific cache subfolder) so only those subfolders are exposed;
update the external-path and cache-path entries (names: my_images,
my_cache_images) to use concrete subdirectory paths like an app-specific images
folder and cache/images folder and document the chosen folder names in the
manifest or README.
In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java`:
- Line 1: Update the test package declaration to match the app namespace and
move the test file into the corresponding directory: change the package line in
ExampleUnitTest (class ExampleUnitTest) from com.getcapacitor.myapp to
app.chatboxai.chatbox, then relocate the file to
android/app/src/test/java/app/chatboxai/chatbox/ and adjust any imports or
references if necessary so the class compiles under the new package.
In `@android/gradlew.bat`:
- Around line 1-94: The batch script (contains labels like :execute, :fail, and
header lines starting with `@rem` and `@if` "%DEBUG%") currently has LF-only
endings; convert the file to use CRLF line endings and commit the change so
cmd.exe parses it reliably. Also add or update a .gitattributes entry to enforce
CRLF for .bat files (e.g., ensure *.bat text eol=crlf) or set core.autocrlf
appropriately, then normalize the repository (re-save the gradlew.bat with CRLF
and recommit) to prevent future LF-only commits.
In `@src/renderer/packages/mcp/mobile-http-transport.ts`:
- Around line 151-165: The _extractSseData method currently only matches "data:
" (with a space) and concatenates lines without newlines, breaking JSON when
payloads span multiple data lines; update _extractSseData to accept the full
wire format (match "data:" with or without a following space and also bare
"data") and when accumulating multiple data lines insert the SSE-preserved
newline between them (i.e., join successive data line values with "\n" rather
than concatenating directly), ensuring the final assembled string follows the
SSE spec for data fields.
- Around line 190-196: The native stream creation and reader acquisition
(createNativeReadableStream and stream.getReader) must be moved inside the
existing guarded try/catch in _startGetSse so synchronous throws are caught and
will trigger the existing error handling (onerror or the catch block) instead of
being lost; update _startGetSse to perform const stream =
createNativeReadableStream({...}) and const reader = stream.getReader() inside
the try, use this._buildHeaders as before, and ensure any cleanup or
reader.release/reader.cancel remains in the finally or catch as appropriate so
errors during setup propagate to onerror.
In `@src/shared/oauth/index.ts`:
- Around line 46-54: Add ipcMain.handle registrations in the main process for
each channel defined on OAuthIpcChannels (LOGIN, START_LOGIN, EXCHANGE_CODE,
START_DEVICE_FLOW, WAIT_DEVICE_TOKEN, REFRESH, CANCEL). For each channel call
ipcMain.handle('oauth:xxx', ...) and delegate to the corresponding main-process
OAuth implementation (e.g., functions like handleLogin, startLogin,
exchangeCode, startDeviceFlow, waitDeviceToken, refreshToken, cancelOAuth)
ensuring you validate args and return results/errors to the renderer; register
these handlers during your app's main-process initialization so the renderer
invocations in useOAuth.ts have corresponding handlers.
---
Nitpick comments:
In @.erb/scripts/delete-source-maps-runner.js:
- Around line 5-7: The three repeated calls to rimrafSync cleaning map files
should be consolidated: build an array of the target directories (e.g.,
webpackPaths.distMainPath, webpackPaths.distRendererPath,
path.join(webpackPaths.distPath, 'preload')) and iterate over it to call
rimrafSync(path.join(dir, '*.js.map'), { glob: true }); this deduplicates the
cleanup logic and makes adding new output folders easier while keeping the
existing rimrafSync usage and glob option.
In `@android/app/build.gradle`:
- Around line 20-23: Change the Android release block to enable code
shrinking/obfuscation by setting minifyEnabled true for the release build (the
release {} block and its minifyEnabled setting), enable shrinkResources true if
desired, and ensure proguardFiles (the existing
getDefaultProguardFile('proguard-android.txt') and 'proguard-rules.pro') contain
the necessary keep rules for your libraries/Reflection/serialization so the app
still functions; run a release build and smoke-test features to validate and
iterate on proguard-rules.pro to prevent runtime breakage.
In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java`:
- Around line 14-17: The current ExampleUnitTest.addition_isCorrect scaffold
test should be replaced with a project-relevant unit test: open the
ExampleUnitTest class and remove or replace the addition_isCorrect method with
one or more `@Test` methods that exercise real app logic (for example, a pure
function in AppUtils, a ViewModel method like MainViewModel.getX, or a utility
such as DateFormatter.formatDate), import and instantiate the target class,
provide any necessary test inputs or mocks, then use JUnit assertions
(assertEquals/assertTrue/assertThrows) to verify the expected outputs/behavior;
keep the test deterministic, name the new test to reflect the behavior being
validated, and ensure it compiles in the current test configuration.
In `@android/gradle/wrapper/gradle-wrapper.properties`:
- Line 4: The Gradle wrapper timeout is set too low via the networkTimeout
property; update the gradle-wrapper.properties entry networkTimeout from 10000
to a much larger value (e.g., 600000) to reduce CI download failures, save the
file, and verify wrapper downloads succeed in CI; the key to change is the
networkTimeout property in gradle-wrapper.properties.
In `@src/renderer/hooks/useAppTheme.ts`:
- Around line 59-93: The theme hardcodes fontSize: 14 inside getThemeDesign;
extract this into a shared constant (e.g., BASE_MUI_FONT_SIZE) and replace the
literal in getThemeDesign with that constant, export it so other
modules/migrations/settings can import the canonical value, and update any
related places that reference the magic number to use the new constant to avoid
drift.
In `@src/renderer/packages/mcp/controller.ts`:
- Around line 15-19: Persisted MCP configs may still contain
transportConfig.type === 'stdio' which will always fail on non-desktop clients;
instead of only throwing inside the runtime guard that checks
window.electronAPI, add validation/migration when configs are loaded or saved.
Implement a validateOrMigrateMcpConfig function (called from the existing config
load/save paths) that inspects transportConfig.type, removes or replaces 'stdio'
entries for non-desktop platforms, and persists the updated config; keep the
runtime guard in the start/instantiate code (the block that checks
transportConfig and window.electronAPI) as a last-resort safety net but ensure
configs are fixed earlier to prevent repeated failing restarts.
In `@src/renderer/packages/translation.ts`:
- Around line 7-13: The translateTexts stub currently returns the correct
Promise of nulls but optionally add a dev-only warning to make the lack of
translation obvious during development: inside translateTexts, detect
development mode (e.g., process.env.NODE_ENV === 'development' or an existing
isDev flag) and call console.warn once (or for each call) with a clear message
that translations are unavailable in the OSS build and translateTexts is a stub;
keep the existing return behavior and do not alter the function signature or
returned array length.
In `@src/shared/oauth/index.ts`:
- Around line 21-41: Replace the loose interfaces OAuthResult, OAuthStartResult,
and DeviceFlowStartResult with discriminated union types so consumers cannot get
impossible states; e.g., define each as a union of { success: true, /* required
payload props */ } | { success: false, error: string }, ensuring for OAuthResult
the success arm requires credentials: OAuthCredentials, for OAuthStartResult the
success arm requires authUrl: string, and for DeviceFlowStartResult the success
arm requires deviceCode/userCode/verificationUri/expiresIn/interval; update any
call sites/types accordingly to use the new discriminated unions (refer to the
OAuthResult, OAuthStartResult, and DeviceFlowStartResult symbols).
- Around line 14-19: Update the OAuth types to explicitly document time-unit
semantics: add JSDoc comments to OAuthCredentials.expiresAt and any expiresIn
fields (e.g., other interfaces around lines 33–41) stating the exact unit used
(for example: "expiresAt — milliseconds since UNIX epoch" and "expiresIn —
lifetime in seconds"), or alternatively standardize both to the same unit and
document that choice; ensure the comments are adjacent to the OAuthCredentials
interface and the named expiresIn fields so callers know whether to use seconds
or milliseconds.
In `@src/shared/providers/definitions/github-copilot.ts`:
- Around line 9-19: The githubCopilotProvider entry currently registers a stub
with defaultSettings.models = [] which can show a selectable but non-functional
option; update the registration so the provider is not exposed in OSS runtimes
by either adding a hidden flag or gating its registration behind a build/runtime
check. Modify the call to defineProvider for githubCopilotProvider to include a
property like hidden: true (or an isInternal/isExperimental flag) or wrap the
export/registration logic with a condition (e.g., process.env.RUNTIME !== 'oss'
or a feature-flag) so the provider is omitted from the global registry in OSS
builds; ensure you adjust any consumer code that lists providers to respect the
hidden/flag field and reference githubCopilotProvider, defineProvider, and
defaultSettings.models while making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f59ec05b-ffac-4827-b513-a65675dd83e0
⛔ Files ignored due to path filters (27)
android/app/src/main/res/drawable-land-hdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-mdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-land-xxxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-hdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-mdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable-port-xxxhdpi/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/drawable/splash.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.pngis excluded by!**/*.pngandroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.pngis excluded by!**/*.pngandroid/gradle/wrapper/gradle-wrapper.jaris excluded by!**/*.jar
📒 Files selected for processing (40)
.erb/scripts/delete-source-maps-runner.jsandroid/.gitignoreandroid/app/.gitignoreandroid/app/build.gradleandroid/app/capacitor.build.gradleandroid/app/proguard-rules.proandroid/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.javaandroid/app/src/main/AndroidManifest.xmlandroid/app/src/main/java/app/chatboxai/chatbox/MainActivity.javaandroid/app/src/main/res/drawable-v24/ic_launcher_foreground.xmlandroid/app/src/main/res/drawable/ic_launcher_background.xmlandroid/app/src/main/res/layout/activity_main.xmlandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xmlandroid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xmlandroid/app/src/main/res/values/ic_launcher_background.xmlandroid/app/src/main/res/values/strings.xmlandroid/app/src/main/res/values/styles.xmlandroid/app/src/main/res/xml/file_paths.xmlandroid/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.javaandroid/build.gradleandroid/capacitor.settings.gradleandroid/gradle.propertiesandroid/gradle/wrapper/gradle-wrapper.propertiesandroid/gradlewandroid/gradlew.batandroid/settings.gradleandroid/variables.gradlecapacitor.config.tspackage.jsonsrc/renderer/components/settings/mcp/ConfigModal.tsxsrc/renderer/hooks/useAppTheme.tssrc/renderer/modals/Settings.tsxsrc/renderer/packages/mcp/controller.tssrc/renderer/packages/mcp/mobile-http-transport.tssrc/renderer/packages/translation.tssrc/renderer/platform/index.tssrc/renderer/platform/mobile_platform.tssrc/renderer/utils/feature-flags.tssrc/shared/oauth/index.tssrc/shared/providers/definitions/github-copilot.ts
| try { | ||
| def servicesJSON = file('google-services.json') | ||
| if (servicesJSON.text) { | ||
| apply plugin: 'com.google.gms.google-services' | ||
| } | ||
| } catch(Exception e) { | ||
| logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") | ||
| } |
There was a problem hiding this comment.
Don’t swallow all Google Services configuration errors.
Line 52 catches all exceptions, which can hide real setup failures and silently disable push support. Prefer explicit existence/size checks instead of reading servicesJSON.text as control flow.
Suggested fix
-try {
- def servicesJSON = file('google-services.json')
- if (servicesJSON.text) {
- apply plugin: 'com.google.gms.google-services'
- }
-} catch(Exception e) {
- logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
-}
+def servicesJSON = file('google-services.json')
+if (servicesJSON.exists() && servicesJSON.length() > 0) {
+ apply plugin: 'com.google.gms.google-services'
+} else {
+ logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}📝 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.
| try { | |
| def servicesJSON = file('google-services.json') | |
| if (servicesJSON.text) { | |
| apply plugin: 'com.google.gms.google-services' | |
| } | |
| } catch(Exception e) { | |
| logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") | |
| } | |
| def servicesJSON = file('google-services.json') | |
| if (servicesJSON.exists() && servicesJSON.length() > 0) { | |
| apply plugin: 'com.google.gms.google-services' | |
| } else { | |
| logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/build.gradle` around lines 47 - 54, Replace the broad try/catch
that swallows all errors around reading google-services.json with an explicit
existence/size check: test file('google-services.json').exists() and .length()
(or .size()) before attempting to apply plugin 'com.google.gms.google-services',
and only skip applying the plugin when the file is genuinely absent/empty; do
not catch Exception around the read/apply logic—if an unexpected error occurs
while evaluating or applying the plugin (related to servicesJSON or apply
plugin: 'com.google.gms.google-services'), let it surface or log it as an error
instead of silently logging info so real setup failures aren’t hidden.
| sourceCompatibility JavaVersion.VERSION_21 | ||
| targetCompatibility JavaVersion.VERSION_21 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Java/Gradle compatibility signals =="
rg -n --iglob '**/*.{gradle,gradle.kts,properties,md,yml,yaml}' \
'JavaVersion\.VERSION_21|sourceCompatibility|targetCompatibility|actions/setup-java|java-version|org\.gradle\.java\.home|com\.android\.tools\.build:gradle'
echo
echo "Expected: CI/workflow and contributor docs explicitly require Java 21 if VERSION_21 is kept."Repository: chatboxai/chatbox
Length of output: 447
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking CI Workflow Files ==="
find . -type f \( -name "*.yml" -o -name "*.yaml" \) -path "*/.github/workflows/*" -o -path "*/.gitlab-ci*" -o -path "*/bitbucket-pipelines*" -o -path "*/circleci*" 2>/dev/null | head -20
echo
echo "=== Looking for java-version in CI files ==="
find . -type f \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | xargs grep -l "java" 2>/dev/null | head -10
echo
echo "=== Checking gradle.properties ==="
find . -name "gradle.properties" -exec cat -n {} +
echo
echo "=== Checking README and docs for Java version mentions ==="
find . -type f \( -name "README*" -o -name "*.md" \) -exec grep -l -i "java\|jdk\|version" {} \; 2>/dev/null | head -5
echo
echo "=== Checking .java-version file (if exists) ==="
if [ -f .java-version ]; then cat .java-version; else echo "No .java-version file"; fi
echo
echo "=== Checking .jvmrc file (if exists) ==="
if [ -f .jvmrc ]; then cat .jvmrc; else echo "No .jvmrc file"; fiRepository: chatboxai/chatbox
Length of output: 1630
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== README.md Java version mentions ==="
grep -i "java\|jdk\|version" ./README.md | head -20
echo
echo "=== Checking docs/testing.md ==="
grep -i "java\|jdk\|setup\|environment" ./docs/testing.md | head -20
echo
echo "=== Checking docs/storage.md ==="
grep -i "java\|jdk" ./docs/storage.md || echo "No Java mentions found"
echo
echo "=== Checking .github/ISSUE_TEMPLATE/bug_report.md ==="
head -20 ./.github/ISSUE_TEMPLATE/bug_report.md
echo
echo "=== Checking ERROR_HANDLING.md ==="
grep -i "java\|jdk\|setup\|environment" ./ERROR_HANDLING.md || echo "No Java mentions found"
echo
echo "=== Checking for CONTRIBUTING.md or BUILD.md ==="
find . -type f \( -name "CONTRIBUTING*" -o -name "BUILD*" -o -name "INSTALL*" \) -exec echo "File: {}" \; -exec head -30 {} \;Repository: chatboxai/chatbox
Length of output: 1214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking gradle wrapper properties ==="
find . -name "gradle-wrapper.properties" -exec cat -n {} +
echo
echo "=== Looking for any .github directory structure ==="
ls -la .github/ 2>/dev/null || echo "No .github directory"
echo
echo "=== Looking for any workflows ==="
find . -type d -name "workflows" 2>/dev/null
echo
echo "=== Checking for package.json to understand Android build process ==="
cat -n package.json | head -50
echo
echo "=== Checking for android/package.json or android build scripts ==="
find android -name "package.json" -o -name "build.sh" -o -name "Makefile" 2>/dev/nullRepository: chatboxai/chatbox
Length of output: 4156
🌐 Web query:
Android Gradle Plugin 8.7.2 Java version requirements compatibility
💡 Result:
Android Gradle Plugin (AGP) 8.7.2 requires JDK 17 (minimum and default). This is the Java runtime version needed to execute the AGP and Gradle daemon for builds. Key compatibility details: - Minimum Gradle version: 8.9 - Default Gradle version: 8.9 (Gradle 8.9 requires JVM 8+, but AGP specifies JDK 17) - To configure: Set Gradle JDK to 17 in Android Studio (File > Settings > Build, Execution, Deployment > Build Tools > Gradle > Gradle JDK). Use GRADLE_LOCAL_JAVA_HOME or Android Studio's bundled JDK (JBR 17). - For project source compatibility: Set compileOptions { sourceCompatibility/targetCompatibility = JavaVersion.VERSION_17 } and kotlin { jvmToolchain(17) }. - Error if using lower: "Android Gradle plugin requires Java 17 to run. You are currently using Java 11." AGP 8.7.x patch releases (like 8.7.2) inherit the compatibility from 8.7.0 major release.
Citations:
- 1: https://developer.android.com/build/releases/agp-8-7-0-release-notes
- 2: https://developer.android.com/build/jdks
- 3: https://github.com/complycube/complycube-android-sdk/blob/main/docs/compatibility-matrix.md
- 4: https://developer.android.com/build/releases/about-agp
- 5: https://docs.gradle.org/8.7/userguide/compatibility.html
Document Java 21 requirement in gradle.properties and contributor docs, or adjust source/target compatibility.
The project sets sourceCompatibility and targetCompatibility to JavaVersion.VERSION_21, but this requirement is not documented anywhere. Developers without Java 21 available will encounter invalid source release: 21 errors during Android builds. AGP 8.7.2 requires JDK 17 minimum, but the project also targets Java 21 bytecode. Either:
-
Add to
gradle.properties:org.gradle.java.home=[path to Java 21]and document Java 21 as a requirement in README/CONTRIBUTING, or
-
Downgrade
sourceCompatibility/targetCompatibilitytoJavaVersion.VERSION_17if Java 21 features are not required.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/capacitor.build.gradle` around lines 5 - 6, The build currently
sets Java bytecode level via sourceCompatibility and targetCompatibility to
JavaVersion.VERSION_21, which is undocumented and will break developers without
JDK 21; either document the Java 21 requirement and set org.gradle.java.home in
gradle.properties (and update README/CONTRIBUTING with installation/path
guidance) or change sourceCompatibility/targetCompatibility from
JavaVersion.VERSION_21 to JavaVersion.VERSION_17 in capacitor.build.gradle if
Java 21 language/bytecode features are not required; update the project docs
accordingly so contributors know which JDK to install.
| // Context of the app under test. | ||
| Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | ||
|
|
||
| assertEquals("com.getcapacitor.app", appContext.getPackageName()); |
There was a problem hiding this comment.
Fix incorrect expected package in instrumented test.
At Line [24], the assertion expects com.getcapacitor.app, but this PR sets the app id to app.chatboxai.chatbox. This test will fail in CI/device runs.
✅ Proposed fix
- assertEquals("com.getcapacitor.app", appContext.getPackageName());
+ assertEquals("app.chatboxai.chatbox", appContext.getPackageName());📝 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.
| assertEquals("com.getcapacitor.app", appContext.getPackageName()); | |
| assertEquals("app.chatboxai.chatbox", appContext.getPackageName()); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java`
at line 24, Update the expected package name in the instrumented test: in
ExampleInstrumentedTest (the method using appContext.getPackageName() and
assertEquals), change the expected string from "com.getcapacitor.app" to the
actual app id "app.chatboxai.chatbox" so the assertEquals compares
appContext.getPackageName() against the correct package name.
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <application | ||
| android:allowBackup="true" |
There was a problem hiding this comment.
Harden backup settings for user data privacy.
At Line [5], android:allowBackup="true" enables backup/restore of app data, which is risky for a chat app handling potentially sensitive content.
🔐 Proposed fix
- android:allowBackup="true"
+ android:allowBackup="false"📝 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.
| android:allowBackup="true" | |
| android:allowBackup="false" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/AndroidManifest.xml` at line 5, Change the Android
manifest backup settings to disable automatic backups: locate the
android:allowBackup attribute in the AndroidManifest (attribute
android:allowBackup currently set to "true") and set it to "false";
additionally, explicitly disable full-data backups by adding
android:fullBackupContent="false" (or point it to an empty backup rules XML) on
the same <application> element to ensure no user/chat data is included in system
backups.
| <external-path name="my_images" path="." /> | ||
| <cache-path name="my_cache_images" path="." /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify FileProvider breadth + URI generation call sites (read-only)
fd -i "file_paths.xml" | xargs -r -I{} sh -c 'echo "== {} =="; sed -n "1,120p" "{}"'
fd -i "AndroidManifest.xml" | xargs -r -I{} sh -c 'echo "== {} =="; rg -n -C3 "FileProvider|FILE_PROVIDER_PATHS|grantUriPermissions|exported" "{}"'
rg -nP --type=java --type=kt -C3 '\bFileProvider\.getUriForFile\s*\('Repository: chatboxai/chatbox
Length of output: 1214
🏁 Script executed:
# Search for FileProvider.getUriForFile usage without type restrictions
rg -i "getUriForFile\|FileProvider" --glob="*.java" --glob="*.kt" -C2
# Also search for share intent handlers or uri generation patterns
rg -i "ACTION_SEND|ACTION_VIEW|intent.*uri|shareUri" --glob="*.java" --glob="*.kt" -C2Repository: chatboxai/chatbox
Length of output: 43
🏁 Script executed:
# Check if there are any calls to FileProvider in the entire codebase
find . -name "*.java" -o -name "*.kt" | head -20Repository: chatboxai/chatbox
Length of output: 287
🏁 Script executed:
cat -n android/app/src/main/java/app/chatboxai/chatbox/MainActivity.java | head -50Repository: chatboxai/chatbox
Length of output: 220
🏁 Script executed:
# Also check if there are any references to sharing or file URIs in the app
rg -i "file:|Uri\.parse|getUriForFile|shareFile|share.*intent" --glob="*.java" --glob="*.kt"Repository: chatboxai/chatbox
Length of output: 43
Narrow FileProvider exposure paths to follow principle of least privilege.
Lines 3-4 use path="." which exposes the entire external storage and cache directories. Although FileProvider.getUriForFile() is not currently called in the codebase, this overly broad configuration should be restricted to explicit subdirectories as a hardening measure.
Suggested fix
<paths xmlns:android="http://schemas.android.com/apk/res/android">
- <external-path name="my_images" path="." />
- <cache-path name="my_cache_images" path="." />
+ <external-files-path name="shared_images" path="images/" />
+ <cache-path name="shared_cache_images" path="images/" />
</paths>📝 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.
| <external-path name="my_images" path="." /> | |
| <cache-path name="my_cache_images" path="." /> | |
| <external-files-path name="shared_images" path="images/" /> | |
| <cache-path name="shared_cache_images" path="images/" /> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/main/res/xml/file_paths.xml` around lines 3 - 4, The
FileProvider config in file_paths.xml currently uses external-path and
cache-path entries with path="." which exposes entire storage; narrow these to
explicit subdirectories by changing the external-path name="my_images" path="."
and cache-path name="my_cache_images" path="." entries to point at dedicated
folders (for example a fixed app images dir under external storage and a
specific cache subfolder) so only those subfolders are exposed; update the
external-path and cache-path entries (names: my_images, my_cache_images) to use
concrete subdirectory paths like an app-specific images folder and cache/images
folder and document the chosen folder names in the manifest or README.
| @@ -0,0 +1,18 @@ | |||
| package com.getcapacitor.myapp; | |||
There was a problem hiding this comment.
Align test package with the app namespace.
The test package currently diverges from the app namespace (app.chatboxai.chatbox in android/app/build.gradle), which can cause confusion and harder future test integration.
Suggested package alignment
-package com.getcapacitor.myapp;
+package app.chatboxai.chatbox;📝 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.
| package com.getcapacitor.myapp; | |
| package app.chatboxai.chatbox; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java` at
line 1, Update the test package declaration to match the app namespace and move
the test file into the corresponding directory: change the package line in
ExampleUnitTest (class ExampleUnitTest) from com.getcapacitor.myapp to
app.chatboxai.chatbox, then relocate the file to
android/app/src/test/java/app/chatboxai/chatbox/ and adjust any imports or
references if necessary so the class compiles under the new package.
| @rem | ||
| @rem Copyright 2015 the original author or authors. | ||
| @rem | ||
| @rem Licensed under the Apache License, Version 2.0 (the "License"); | ||
| @rem you may not use this file except in compliance with the License. | ||
| @rem You may obtain a copy of the License at | ||
| @rem | ||
| @rem https://www.apache.org/licenses/LICENSE-2.0 | ||
| @rem | ||
| @rem Unless required by applicable law or agreed to in writing, software | ||
| @rem distributed under the License is distributed on an "AS IS" BASIS, | ||
| @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| @rem See the License for the specific language governing permissions and | ||
| @rem limitations under the License. | ||
| @rem | ||
| @rem SPDX-License-Identifier: Apache-2.0 | ||
| @rem | ||
|
|
||
| @if "%DEBUG%"=="" @echo off | ||
| @rem ########################################################################## | ||
| @rem | ||
| @rem Gradle startup script for Windows | ||
| @rem | ||
| @rem ########################################################################## | ||
|
|
||
| @rem Set local scope for the variables with windows NT shell | ||
| if "%OS%"=="Windows_NT" setlocal | ||
|
|
||
| set DIRNAME=%~dp0 | ||
| if "%DIRNAME%"=="" set DIRNAME=. | ||
| @rem This is normally unused | ||
| set APP_BASE_NAME=%~n0 | ||
| set APP_HOME=%DIRNAME% | ||
|
|
||
| @rem Resolve any "." and ".." in APP_HOME to make it shorter. | ||
| for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi | ||
|
|
||
| @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. | ||
| set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" | ||
|
|
||
| @rem Find java.exe | ||
| if defined JAVA_HOME goto findJavaFromJavaHome | ||
|
|
||
| set JAVA_EXE=java.exe | ||
| %JAVA_EXE% -version >NUL 2>&1 | ||
| if %ERRORLEVEL% equ 0 goto execute | ||
|
|
||
| echo. 1>&2 | ||
| echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 | ||
| echo. 1>&2 | ||
| echo Please set the JAVA_HOME variable in your environment to match the 1>&2 | ||
| echo location of your Java installation. 1>&2 | ||
|
|
||
| goto fail | ||
|
|
||
| :findJavaFromJavaHome | ||
| set JAVA_HOME=%JAVA_HOME:"=% | ||
| set JAVA_EXE=%JAVA_HOME%/bin/java.exe | ||
|
|
||
| if exist "%JAVA_EXE%" goto execute | ||
|
|
||
| echo. 1>&2 | ||
| echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 | ||
| echo. 1>&2 | ||
| echo Please set the JAVA_HOME variable in your environment to match the 1>&2 | ||
| echo location of your Java installation. 1>&2 | ||
|
|
||
| goto fail | ||
|
|
||
| :execute | ||
| @rem Setup the command line | ||
|
|
||
| set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar | ||
|
|
||
|
|
||
| @rem Execute Gradle | ||
| "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* | ||
|
|
||
| :end | ||
| @rem End local scope for the variables with windows NT shell | ||
| if %ERRORLEVEL% equ 0 goto mainEnd | ||
|
|
||
| :fail | ||
| rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of | ||
| rem the _cmd.exe /c_ return code! | ||
| set EXIT_CODE=%ERRORLEVEL% | ||
| if %EXIT_CODE% equ 0 set EXIT_CODE=1 | ||
| if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% | ||
| exit /b %EXIT_CODE% | ||
|
|
||
| :mainEnd | ||
| if "%OS%"=="Windows_NT" endlocal | ||
|
|
||
| :omega |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
from pathlib import Path
p = Path("android/gradlew.bat")
b = p.read_bytes()
crlf = b.count(b"\r\n")
lf = b.count(b"\n")
bare_lf = lf - crlf
print(f"file={p}")
print(f"CRLF={crlf}, LF={lf}, bare_LF={bare_lf}")
print("PASS" if bare_lf == 0 else "FAIL: contains LF-only line endings")
PYRepository: chatboxai/chatbox
Length of output: 147
Convert line endings to CRLF for Windows batch file compatibility.
The file currently uses LF-only line endings. Windows batch files require CRLF line endings for reliable parsing in cmd.exe; LF-only endings can cause intermittent parsing and runtime errors.
🧰 Tools
🪛 Blinter (1.0.112)
[error] 1-1: Unix line endings detected. Explanation: Batch file uses Unix line endings (LF-only) which can cause GOTO/CALL label parsing failures and script malfunction due to Windows batch parser 512-byte boundary bugs. Recommendation: Convert file to Windows line endings (CRLF). Use tools like dos2unix, notepad++, or configure git with 'git config core.autocrlf true'. Context: File uses Unix line endings (LF-only) - 94 LF sequences found
(E018)
[error] 88-88: Complex quote escaping error. Explanation: Complex quote patterns may not be handled correctly by the command interpreter. Recommendation: Use triple-quote pattern """text""" for quotes within quoted strings, or escape properly with variables. Context: Complex quote escaping detected
(E028)
[error] 29-29: Unsafe SET command usage. Explanation: SET commands without proper validation or quoting can cause security issues. Recommendation: Always quote SET values and validate input: SET "var=safe value". Context: SET command value should be quoted for safety
(SEC002)
[error] 45-45: Command injection via variable substitution. Explanation: Variables containing user input used in commands may allow code injection. Recommendation: Validate and sanitize variables before use in command execution. Context: Variable used with shell operators may allow injection
(SEC013)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@android/gradlew.bat` around lines 1 - 94, The batch script (contains labels
like :execute, :fail, and header lines starting with `@rem` and `@if` "%DEBUG%")
currently has LF-only endings; convert the file to use CRLF line endings and
commit the change so cmd.exe parses it reliably. Also add or update a
.gitattributes entry to enforce CRLF for .bat files (e.g., ensure *.bat text
eol=crlf) or set core.autocrlf appropriately, then normalize the repository
(re-save the gradlew.bat with CRLF and recommit) to prevent future LF-only
commits.
| /** | ||
| * Collect and concatenate all `data:` lines from one SSE event block. | ||
| * Multiple data lines are concatenated without a separator (SSE spec §9.2.6). | ||
| */ | ||
| private _extractSseData(eventBlock: string): string { | ||
| let data = '' | ||
| for (const line of eventBlock.split('\n')) { | ||
| if (line.startsWith('data: ')) { | ||
| data += line.slice(6) | ||
| } else if (line === 'data') { | ||
| // bare "data" field with no value — counts as empty string | ||
| data += '' | ||
| } | ||
| } | ||
| return data |
There was a problem hiding this comment.
Fix SSE data: parsing to accept the full wire format.
Line 158 only matches data: with a space, and the current concatenation drops the newline that SSE preserves between multiple data: fields. Servers that emit data:{"jsonrpc":...} or split payloads across multiple data: lines will produce malformed JSON and break MCP message delivery.
💡 Proposed fix
private _extractSseData(eventBlock: string): string {
- let data = ''
+ const chunks: string[] = []
for (const line of eventBlock.split('\n')) {
- if (line.startsWith('data: ')) {
- data += line.slice(6)
- } else if (line === 'data') {
- // bare "data" field with no value — counts as empty string
- data += ''
+ if (line.startsWith('data:')) {
+ const value = line.slice(5)
+ chunks.push(value.startsWith(' ') ? value.slice(1) : value)
+ } else if (line === 'data') {
+ chunks.push('')
}
}
- return data
+ return chunks.join('\n')
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/packages/mcp/mobile-http-transport.ts` around lines 151 - 165,
The _extractSseData method currently only matches "data: " (with a space) and
concatenates lines without newlines, breaking JSON when payloads span multiple
data lines; update _extractSseData to accept the full wire format (match "data:"
with or without a following space and also bare "data") and when accumulating
multiple data lines insert the SSE-preserved newline between them (i.e., join
successive data line values with "\n" rather than concatenating directly),
ensuring the final assembled string follows the SSE spec for data fields.
| const stream = createNativeReadableStream({ | ||
| url: this.url, | ||
| method: 'GET', | ||
| headers: this._buildHeaders({ Accept: 'text/event-stream' }), | ||
| }) | ||
|
|
||
| const reader = stream.getReader() |
There was a problem hiding this comment.
Move native stream setup inside the guarded error path.
createNativeReadableStream() and stream.getReader() run before the try. If either throws, _startGetSse() rejects and the void this._startGetSse() caller drops the rejection, so stream setup fails silently and onerror never fires.
💡 Proposed fix
- const stream = createNativeReadableStream({
- url: this.url,
- method: 'GET',
- headers: this._buildHeaders({ Accept: 'text/event-stream' }),
- })
-
- const reader = stream.getReader()
- const decoder = new TextDecoder()
- // Buffer for partial SSE events across chunk boundaries
- let buffer = ''
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined
try {
+ const stream = createNativeReadableStream({
+ url: this.url,
+ method: 'GET',
+ headers: this._buildHeaders({ Accept: 'text/event-stream' }),
+ })
+
+ reader = stream.getReader()
+ const decoder = new TextDecoder()
+ // Buffer for partial SSE events across chunk boundaries
+ let buffer = ''
+
while (!signal.aborted) {
const { done, value } = await reader.read()
if (done) break
@@
} finally {
try {
- reader.releaseLock()
+ reader?.releaseLock()
} catch {
// Ignore errors from releasing an already-released lock
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/packages/mcp/mobile-http-transport.ts` around lines 190 - 196,
The native stream creation and reader acquisition (createNativeReadableStream
and stream.getReader) must be moved inside the existing guarded try/catch in
_startGetSse so synchronous throws are caught and will trigger the existing
error handling (onerror or the catch block) instead of being lost; update
_startGetSse to perform const stream = createNativeReadableStream({...}) and
const reader = stream.getReader() inside the try, use this._buildHeaders as
before, and ensure any cleanup or reader.release/reader.cancel remains in the
finally or catch as appropriate so errors during setup propagate to onerror.
| export const OAuthIpcChannels = { | ||
| LOGIN: 'oauth:login', | ||
| START_LOGIN: 'oauth:start-login', | ||
| EXCHANGE_CODE: 'oauth:exchange-code', | ||
| START_DEVICE_FLOW: 'oauth:start-device-flow', | ||
| WAIT_DEVICE_TOKEN: 'oauth:wait-device-token', | ||
| REFRESH: 'oauth:refresh', | ||
| CANCEL: 'oauth:cancel', | ||
| } as const |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Declared OAuth channel constants:"
rg -n "^[[:space:]]+[A-Z_]+: 'oauth:" src/shared/oauth/index.ts
echo
echo "Renderer usage sites:"
rg -nP "OAuthIpcChannels\.[A-Z_]+" -C2 --type ts --type tsx
echo
echo "Main-process registrations:"
rg -nP "ipcMain\.(handle|on)\(\s*['\"]oauth:" -C2 --type ts --type tsx
echo
echo "Any direct oauth channel invocations without constants:"
rg -nP "['\"]oauth:(login|start-login|exchange-code|start-device-flow|wait-device-token|refresh|cancel)['\"]" -C2 --type ts --type tsxRepository: chatboxai/chatbox
Length of output: 433
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Declared OAuth channel constants ==="
rg -n "^[[:space:]]+[A-Z_]+: 'oauth:" src/shared/oauth/index.ts
echo
echo "=== Renderer usage of OAuthIpcChannels ==="
rg -n "OAuthIpcChannels\." --type ts --type js
echo
echo "=== Main-process ipcMain.handle/on registrations for oauth ==="
rg -n "ipcMain\.(handle|on)" --type ts --type js | grep -i oauth || echo "No matches found with grep"
echo
echo "=== All ipcMain.handle/on registrations ==="
rg -n "ipcMain\.(handle|on)" --type ts --type js -A1 | head -80
echo
echo "=== Direct oauth channel string literals (should ideally use constants) ==="
rg -n "'oauth:(login|start-login|exchange-code|start-device-flow|wait-device-token|refresh|cancel)'" --type ts --type jsRepository: chatboxai/chatbox
Length of output: 5783
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for 'oauth' in main directory ==="
rg -n "oauth" src/main/ --type ts --type js -i
echo
echo "=== Checking if OAuth constants are imported anywhere in main ==="
rg -n "OAuthIpcChannels" src/main/ --type ts --type js
echo
echo "=== List all files in src/main/ that might contain IPC ==="
find src/main -name "*.ts" -o -name "*.js" | grep -iE "(oauth|ipc|auth)" | head -20
echo
echo "=== Check preload file for oauth handling ==="
find src -name "*preload*" -o -name "*pre-load*" | while read file; do
echo "File: $file"
rg -n "oauth" "$file" -i || echo "No oauth found"
doneRepository: chatboxai/chatbox
Length of output: 108
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full useOAuth.ts file to check platform guards ==="
cat -n src/renderer/hooks/useOAuth.ts | head -150
echo
echo "=== Check for any oauth handler registration patterns ==="
rg -n "handle.*oauth|ipcMain.*oauth" --type ts --type js -i
echo
echo "=== Check if OAuth channels might be handled through different mechanism ==="
rg -n "oauth" --type ts --type js -i | grep -i "handler\|register\|setup" | head -20Repository: chatboxai/chatbox
Length of output: 8612
OAuth IPC handlers are missing from the main process.
The renderer correctly gates all invocations with if (!isDesktop) checks, ensuring non-desktop platforms are safe. However, verification reveals that none of the seven declared OAuth channels have corresponding ipcMain.handle registrations:
- LOGIN, START_LOGIN, EXCHANGE_CODE, START_DEVICE_FLOW, WAIT_DEVICE_TOKEN, REFRESH, CANCEL
All seven channels are invoked by src/renderer/hooks/useOAuth.ts but have no handlers in the main process. This would cause runtime failures if OAuth is invoked on desktop. The handlers must be implemented in the main process before these constants can be used.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/shared/oauth/index.ts` around lines 46 - 54, Add ipcMain.handle
registrations in the main process for each channel defined on OAuthIpcChannels
(LOGIN, START_LOGIN, EXCHANGE_CODE, START_DEVICE_FLOW, WAIT_DEVICE_TOKEN,
REFRESH, CANCEL). For each channel call ipcMain.handle('oauth:xxx', ...) and
delegate to the corresponding main-process OAuth implementation (e.g., functions
like handleLogin, startLogin, exchangeCode, startDeviceFlow, waitDeviceToken,
refreshToken, cancelOAuth) ensuring you validate args and return results/errors
to the renderer; register these handlers during your app's main-process
initialization so the renderer invocations in useOAuth.ts have corresponding
handlers.
Android / build: - build.gradle: replace try/catch on google-services.json with explicit file.exists() && file.length() > 0 check to avoid swallowing real errors - capacitor.build.gradle: downgrade Java source/target compatibility from VERSION_21 to VERSION_17 (AGP 8.7.2 requires JDK 17 minimum; VERSION_21 was undocumented and would break contributors without JDK 21) - AndroidManifest.xml: set android:allowBackup="false" to prevent system backup of potentially sensitive chat data and API keys - file_paths.xml: switch from external-path (entire external storage) to external-files-path (app-specific dir only) for FileProvider least-privilege - ExampleInstrumentedTest.java: fix package name and assertEquals expected value to match actual appId (app.chatboxai.chatbox) - ExampleUnitTest.java: align package declaration with app namespace MCP mobile transport: - _extractSseData: match 'data:' with or without trailing space per SSE wire spec; join multiple data lines with '\n' (W3C EventSource §9.2.6) - _startGetSse: move createNativeReadableStream / stream.getReader() inside the try block so synchronous setup errors surface via onerror instead of being silently swallowed by the void caller OAuth / tooling: - OAuthIpcChannels: expand comment to explain OSS design — ipcMain handlers exist only in the private edition; renderer already guards every invocation with isDesktop() checks so these channels are never invoked on web/mobile - .gitattributes: add '*.bat text eol=crlf' override so gradlew.bat retains CRLF line endings on Windows checkout despite the global eol=lf setting
|
All review findings have been addressed in commit
|
Closes #3639
Summary
This PR adds a production-ready Android/Capacitor build target to the open-source edition of Chatbox.
What's included
android/,capacitor.config.ts): generated bynpx cap add android, configured withappId: app.chatboxai.chatbox. Build withpnpm mobile:sync:android && cd android && ./gradlew assembleDebug.MobilePlatform(src/renderer/platform/mobile_platform.ts): extendsWebPlatformwithtype = 'mobile', activating all existing mobile UI branches (sidebar hide, SplashScreen, mobile routing history, etc.). Device metadata resolved via@capacitor/appand@capacitor/device.src/renderer/packages/mcp/mobile-http-transport.ts): on Capacitor the WebView enforces CORS, blocking standardfetch-based MCP connections.MobileStreamableHTTPTransportroutes POST/DELETE throughCapacitorHttp(OS networking, CORS-exempt) and GET/SSE throughcreateNativeReadableStreamwith a line-by-line SSE decoder.featureFlags.mcpenabled for'mobile'and'web'platform types.typography.fontSizefix: the previous formula(userFontSize * 14) / 16produced 12.25 px at default settings — ~12.5% smaller than the MUI default. Analysis of the officialchatbox-1.20.1.apkbundle confirms the correct value is a fixed14, matching MUI's default. The user's message font preference continues to be applied via--chatbox-msg-font-size.translation.tsandgithub-copilot.tsstubs so the open-source build compiles without the private-edition implementations (both files are already imported by the existing codebase but were missing from the OSS tree).Commits
a9bc6bbc718a87b1bc68e9198479Build instructions
android/local.properties(SDK path) is excluded byandroid/.gitignoreand must be created locally.Test plan
tsc --noEmit --project tsconfig.web.jsonpasses with no errorspnpm build)gradlew assembleDebug)platform.type === 'mobile')CapacitorHttp)chatbox-1.20.1.apkSummary by CodeRabbit
Release Notes
New Features
Chores
Tests