Skip to content

feat: self-hosted token sync with Android support; Android IME polish and cursor context - #1133

Open
DepengWang wants to merge 13 commits into
Open-Less:betafrom
DepengWang:feature/custom-sync-server-token
Open

DepengWang wants to merge 13 commits into
Open-Less:betafrom
DepengWang:feature/custom-sync-server-token

Conversation

@DepengWang

@DepengWang DepengWang commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds a self-hosted alternative to the GitHub-OAuth-backed encrypted cloud sync (cloud_sync_e2ee): a user-configurable server origin + a static bearer token, stored in the same secure credential vault as the GitHub token. Motivated by GitHub connectivity instability for users in regions where github.com is unreliable — the sign-in/data path never touches github.com when a custom token is configured.
  • Reference self-hosted server implementation (not part of this repo): https://github.com/DepengWang/openless-sync-server — implements the /v1/auth/token, /v1/capabilities, and /v1/me/* endpoints this PR's client talks to.
  • Wires Android into the restore path for the first time. Three real, independent bugs blocked every Android restore before it ever inspected a document:
    • the mobile Tauri entry point never bound the restore runtime-effects handler that desktop always had, so runtime_effects() was permanently Unsupported during any restore;
    • the UI-preferences capture step required a slot that's only populated after a user manually touches locale/font-scale post-enable, so a fresh device/account could not create or restore a snapshot at all (not actually Android-specific, just never hit before);
    • a frozen isTauri constant computed once at module-eval time could get stuck false forever after a mid-session Android webview rebuild, silently routing every subsequent backend call through the browser-preview mock.
  • A new preference (syncCustomServerOrigin, the self-hosted server address) is explicitly registered as Excluded from sync — like the token itself, it's a manually-entered, per-device value and must never be classified as syncable in any form.
  • Fixes a real UX bug (present on both Windows and Android, same shared service.rs): saving a new custom server origin/token in Settings previously only took effect after a full app restart, because the live service cached the origin once at backend-construction time. The service now re-reads the preference on every connection attempt.
  • Minor UI: the encrypted-sync description now mentions token sign-in (zh-CN/zh-TW), and the account badge shows user@host instead of @user when signed in via a self-hosted server, to visually distinguish it from a GitHub account.

Android IME polish (Kotlin only, independent of the sync changes)

  • Consistent mode color through the thinking step. The three mic gestures already had their own accent while recording (Raw orange, Quick notes green, Cloud notes red), but everything fell back to gray/blue the moment recording stopped. The status text, the thinking dots, the hint row and the link indicator now all keep the armed mode's color from recording through thinking; an ordinary dictation looks exactly as before.
    • Cloud notes also keeps the red accent and the thinking dots during its webhook submit, which previously dropped back to the idle mic capsule with gray text. Tapping the mic during the submit still starts a new recording, as before.
    • Fixes the mode color being lost when the mic button is rebuilt mid-dictation (stopping a recording from the edit panel).
  • Keyboard height moved into a bottom sheet. The height/raise sliders and the 1:1 footprint used to be docked permanently at the bottom of the keyboard settings page; since the footprint is as tall as the keyboard itself, the rest of the settings were left with only a sliver to scroll in. They now live in a bottom sheet opened from a new "Keyboard appearance" row: two sliders with −/+ buttons for 1dp nudges, a reset, and the 1:1 footprint flush with the bottom of the screen, where the real keyboard appears.

Android cursor context (IME, opt-in)

  • The OpenLess keyboard now supports the existing Cursor context setting (previously macOS only). When recording starts it reads the text around the caret once through its own InputConnection and hands Core the two sides as structured text. Core applies the existing 600-character window and the shared <cursor_context> prompt, so there is no Android-specific prompt.
  • Dictation, quick notes and cloud notes all use the context for polish. It is never stored: not in notes, history, the cloud note webhook or logs (logs carry character counts and the package name only).
  • Off by default. Never read for password fields, TYPE_NULL, editors flagged IME_FLAG_NO_PERSONALIZED_LEARNING, or known password managers / Termux. A failed read degrades to no context and dictation continues.
  • Not included: the floating overlay path, and a debug probe UI.
  • Overlaps with feat(windows): UIA cursor context; pace SendInput keystrokes #1137 (Windows cursor context) on the settings toggle condition, the locale description and the README section. Whichever lands second needs those lines merged to "macOS / Windows / Android".

Platform support

  • Tested and working: Windows and Android. Both built in release mode and verified on real devices (a Windows desktop and a OnePlus phone), including a real cross-device restore between the two.
  • Not adapted/tested on other platforms (macOS, Linux). The changes live in the shared openless-core/service.rs layer with no platform-specific gating beyond the existing Android restore-path wiring above, so they likely build everywhere, but no macOS/Linux verification has been done in this PR.

Testing

  • Full create → delete round trip verified against a real self-hosted deployment (not just the in-process test mock): token auth, metadata read, real encrypted upload, and delete, with revision moving 0→1→2 as expected.
  • A first-ever cross-device restore (vault created on Windows, restored on a factory-fresh Android install) verified end to end after the fixes above: sign-in → unlock → review → confirm restore all complete successfully.
  • cargo test -p openless-core --lib: 1071 passed, 0 failed, 1 ignored (pre-existing, unrelated).
  • tsc --noEmit: clean.
  • Manually verified on a real Windows release build and a real Android device (OnePlus) after signing the release APK.
  • Android IME polish: :app:compileArmDebugKotlin clean, arm64 debug APK built and manually checked on a real device (Xiaomi M2011K2C).
  • Android cursor context: full openless-core lib tests pass, including a new test that runs dictation, quick note and cloud note sessions with a caret snapshot, checks the polisher saw it, and scans the data directory to confirm it was not persisted; 7 Kotlin unit tests for the privacy gate; a new contract test. On a Xiaomi phone (Android 14) in WeCom, from a local build that also includes feat(windows): UIA cursor context; pace SendInput keystrokes #1137: 62 characters captured in 8 ms and delivered to the polish request. Not yet verified on a device: caret in the middle of text, quick and cloud notes, password fields, other apps.

Not covered by this PR

  • A real multi-device concurrent-edit merge scenario (everything tested so far is sequential single-writer).
  • macOS and Linux builds/testing (see Platform support above).
  • The pre-existing MSI-bundling version-string issue (optional build metadata ... cannot be greater than 65535) — unrelated, worked around locally with --no-bundle, not fixed here.

Screenshot

Settings: cursor context toggle and encrypted sync signed in with a self-hosted token

Windows, Settings -> Permissions & data. The encrypted sync card is signed in with a static token against a self-hosted server: the account badge shows user@host instead of @user, and the vault is synced. Taken from a local build that also has #1137 merged, which is why the Cursor context toggle appears above it.

DepengWang and others added 7 commits October 1, 2026 16:21
Adds an alternative to GitHub OAuth for encrypted multi-device sync:
a user-configurable server origin plus a static bearer token, stored
securely via the existing CredentialStore (never in plain preferences).
A configured custom token always takes priority over a GitHub session
and bypasses the system-proxy preference, since a self-hosted server is
the user's own explicitly reachable endpoint. Verified end-to-end
(sign-in, vault read, create, enable) against a real self-hosted
deployment through the actual desktop UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three root causes blocked every Android restore attempt before it ever
inspected a document:

- mobile_runtime.rs never called bind_restore_runtime_effects() (desktop
  does), so runtime_effects() always returned Unsupported during restore.
- capture.rs required the UI-preferences extension slot to already be
  populated, which only happens after a user manually changes locale/font
  scale post-enable; a device that never touched that setting (true for a
  fresh install) could not create or restore any snapshot at all.
- isTauri was a frozen const computed once at module-eval time; a mid-session
  Android webview rebuild (ensure_main_webview_window) could permanently
  route all subsequent backend calls through the browser-preview mock.
  Switched the two gating call sites to a live isTauriNow() check.

Also registers sync_custom_server_origin (added for the self-hosted-server
feature) in the preference registry as Excluded — it's a manually-entered,
per-device value and must never be classified as syncable in any form.
…artup

SyncServiceConfig.origin was a fixed snapshot taken once at backend
construction; saving a new self-hosted server address in Settings updated
the preferences file but never reached the already-running sync service,
so the new address only took effect after a full app restart (affects
both Windows and Android, same shared service.rs).

Added SyncServiceData::custom_server_origin() (reads the live preferences
store) and a private EncryptedSyncService::origin() helper that every
connection attempt now calls instead of trusting config.origin directly.
zh-CN/zh-TW only — the description under "加密云同步" said sync happened
via GitHub account only, no longer true now that a self-hosted server's
static token is a supported sign-in path.
The account badge always showed "@login", which reads like a GitHub handle.
When a custom server origin is configured, show "login@host" instead so a
self-hosted account is visually distinct from a GitHub one.
The component imports both from '../../lib/ipc', but the test harness's
dependency map only spread '../../lib/ipc/cloud-sync-e2ee', leaving the
two functions undefined and crashing render with "readCredential is not
a function". This broke Windows and macOS CI checks on PR Open-Less#1133.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h9QUkoFtCoDLsJjRDB6dX
Raw / Quick notes / Cloud notes now keep their accent color from
recording through the thinking step: status text, the thinking dots,
the hint row and the link indicator all follow the armed mode instead
of dropping back to gray/blue. Cloud notes also keeps it (and the
dots) up during its webhook submit.

Keyboard height and raise move out of the permanently docked panel at
the bottom of keyboard settings into a bottom sheet with stepper
sliders, a reset, and the 1:1 footprint flush with the screen bottom.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DepengWang DepengWang changed the title feat(sync): self-hosted cloud sync via static token, with Android support feat(sync): self-hosted cloud sync via static token, with Android support; Android IME mode colors and keyboard height sheet Oct 2, 2026
DepengWang and others added 5 commits October 2, 2026 22:40
Adds ImePrivacyPolicy: the gate (TYPE_NULL, password fields,
IME_FLAG_NO_PERSONALIZED_LEARNING, sensitive packages) and the single,
no-retry read of the text around the caret. The readers are never invoked
while the cursorContextEnabled switch is off or a gate rejects the editor,
and a null or throwing InputConnection degrades to no context. A surrogate
pair cut by the read window is trimmed so no half character crosses JNI.

Kept separate from ImeLearningPolicy: learning decides what stays on the
device, cursor context may be sent to the polish LLM provider. The
accessibility vocabulary observer now shares the same sensitive package
list instead of carrying its own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The IME snapshots the text around the caret through its own
InputConnection when a recording starts and sends it with the "start"
command as structured frontApp / cursorBefore / cursorAfter fields. Stop,
cancel and cloud commands never resend it.

native_bridge trims the two sides with Core's existing window budget
(new window_from_split helper, same 80/20 rule as the desktop readers) and
builds the envelope input with the shared cursor_context_input, so Android
has no prompt format of its own. Logs carry character counts and the
package name only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quick notes and cloud notes already keep polish.cursor_context when the
output target changes. This locks that in, together with the other half of
the rule: the context informs polish but never becomes stored content.

The Core test runs dictation, quick note and cloud note sessions with a
caret snapshot, checks the polisher saw it each time, and scans the whole
data directory for the snapshot text afterwards. The contract test covers
the Kotlin and JNI side: InputConnection source, gate order, start-only
payload, metadata-only logs, webhook body and history schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shows the Cursor context toggle on Android as well as macOS. The
description in all 8 locales now names Android, says dictation, quick
notes and cloud notes use the context without storing it, and states that
a cloud polish model receives it with the polish request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the Android read path, what the context is and is not used for,
the extra Android gates, and that the floating overlay does not read
cursor context yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DepengWang DepengWang changed the title feat(sync): self-hosted cloud sync via static token, with Android support; Android IME mode colors and keyboard height sheet feat: self-hosted token sync with Android support; Android IME polish and cursor context Oct 2, 2026
The macOS jobs on the previous commit were cancelled before running
because no hosted runner was acquired. No code change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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