From 6a17f9159647ee5bccba739a31ac0bb5e661d44f Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 26 Sep 2026 23:22:22 -0700 Subject: [PATCH 1/9] fix: package the pinned Octos runtime with standalone Rinx --- .github/workflows/builds.yml | 12 +- .github/workflows/main.yml | 1 + .github/workflows/release.yml | 12 ++ Cargo.lock | 38 ++-- Cargo.toml | 8 +- docs/adr/0007-host-owned-octos-app-peers.md | 42 +++- packaging/README-octos.md | 81 ++++++++ packaging/build-macos-dmg.sh | 1 + packaging/octos.lock.json | 6 + packaging/run-macos.sh | 7 + tests/standalone_local.rs | 8 +- tools/package-octos.py | 204 ++++++++++++++++++++ tools/test_package_octos.py | 60 ++++++ 13 files changed, 452 insertions(+), 28 deletions(-) create mode 100644 packaging/README-octos.md create mode 100644 packaging/octos.lock.json create mode 100644 tools/package-octos.py create mode 100644 tools/test_package_octos.py diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index ffb81a85..e24bd536 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -191,6 +191,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -202,7 +203,8 @@ jobs: RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ @@ -224,6 +226,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -235,7 +238,8 @@ jobs: RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ @@ -265,6 +269,7 @@ jobs: - name: Install Android toolchain run: | cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android - name: Install Ninja run: choco install ninja -y @@ -278,7 +283,8 @@ jobs: run: | ## Note: we can't use the profile here because cargo-makepad doesn't support custom profiles. # cargo makepad android build -p rinx --profile fast - cargo makepad android build -p rinx \ + python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d153945..8b0df3cd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,7 @@ jobs: toolchain: 1.98.0 - uses: Swatinem/rust-cache@v2 - run: brew install cmake + - run: python3 tools/test_package_octos.py - run: cargo test --locked --features agent_chat --lib - run: cargo build --locked --features agent_chat - run: cargo check --locked --no-default-features --features agent_chat diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30df53a6..209657ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -565,6 +565,18 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable + - name: Setup pinned cargo-makepad + uses: ./.github/actions/setup-cargo-makepad + + - name: Package the standalone assistant runtime + env: + AWS_LC_SYS_CMAKE_BUILDER: 1 + run: | + cargo makepad android install-toolchain --full-ndk + rustup target add aarch64-linux-android + python3 tools/package-octos.py android + echo "MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=$PWD/dist/octos-aarch64-linux-android" >> "$GITHUB_ENV" + - name: Package (android) uses: project-robius/makepad-packaging-action@v1.8.0 env: diff --git a/Cargo.lock b/Cargo.lock index 344fc495..92f01ceb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7466,7 +7466,7 @@ dependencies = [ [[package]] name = "octos-agent" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "async-trait", "base64 0.22.1", @@ -7527,7 +7527,7 @@ dependencies = [ [[package]] name = "octos-bus" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "aes 0.8.4", "async-trait", @@ -7556,7 +7556,7 @@ dependencies = [ [[package]] name = "octos-cli" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "agent-client-protocol", "argon2", @@ -7636,7 +7636,7 @@ dependencies = [ [[package]] name = "octos-core" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "chrono", "eyre", @@ -7650,7 +7650,7 @@ dependencies = [ [[package]] name = "octos-diagnostics" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "eyre", "octos-core", @@ -7662,7 +7662,7 @@ dependencies = [ [[package]] name = "octos-fleet" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "eyre", "octos-core", @@ -7678,7 +7678,7 @@ dependencies = [ [[package]] name = "octos-fleet-worker" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "async-trait", "eyre", @@ -7695,7 +7695,7 @@ dependencies = [ [[package]] name = "octos-llm" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "async-trait", "base64 0.22.1", @@ -7719,7 +7719,7 @@ dependencies = [ [[package]] name = "octos-memory" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "bincode", "chrono", @@ -7738,7 +7738,7 @@ dependencies = [ [[package]] name = "octos-pipeline" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "async-trait", "chrono", @@ -7760,7 +7760,7 @@ dependencies = [ [[package]] name = "octos-plugin" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "eyre", "metrics", @@ -7774,7 +7774,7 @@ dependencies = [ [[package]] name = "octos-sandbox" version = "0.1.0" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "clap", "eyre", @@ -7789,7 +7789,7 @@ dependencies = [ [[package]] name = "octos-services" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "chrono", "dirs", @@ -7810,7 +7810,7 @@ dependencies = [ [[package]] name = "octos-store" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "base64 0.22.1", "chrono", @@ -7830,7 +7830,7 @@ dependencies = [ [[package]] name = "octos-swarm" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "async-trait", "chrono", @@ -7849,7 +7849,7 @@ dependencies = [ [[package]] name = "octos-workflows" version = "2.0.3-rc.13" -source = "git+https://github.com/octos-org/octos.git?rev=6ad76e5c1e659bdf10ec05ae869428b48edccf7f#6ad76e5c1e659bdf10ec05ae869428b48edccf7f" +source = "git+https://github.com/octos-org/octos.git?rev=552767dd50e16e08c6225a0cb252fe884e794084#552767dd50e16e08c6225a0cb252fe884e794084" dependencies = [ "chrono", "eyre", @@ -7908,7 +7908,7 @@ dependencies = [ [[package]] name = "octosense-app-peers" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=4fa0f122ab01d58e468d339f1920ec2758741012#4fa0f122ab01d58e468d339f1920ec2758741012" +source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=cc1cee3ee308843aac569111cf2eb31f995646cd#cc1cee3ee308843aac569111cf2eb31f995646cd" dependencies = [ "futures-util", "log", @@ -7934,7 +7934,7 @@ dependencies = [ [[package]] name = "octosense-llm-config" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=4fa0f122ab01d58e468d339f1920ec2758741012#4fa0f122ab01d58e468d339f1920ec2758741012" +source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=cc1cee3ee308843aac569111cf2eb31f995646cd#cc1cee3ee308843aac569111cf2eb31f995646cd" dependencies = [ "argon2", "chacha20poly1305 0.10.1", @@ -7950,7 +7950,7 @@ dependencies = [ [[package]] name = "octosense-octos-core" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=4fa0f122ab01d58e468d339f1920ec2758741012#4fa0f122ab01d58e468d339f1920ec2758741012" +source = "git+https://github.com/OctoSense-org/OctoSense-System-Apps.git?rev=cc1cee3ee308843aac569111cf2eb31f995646cd#cc1cee3ee308843aac569111cf2eb31f995646cd" dependencies = [ "log", "octos-cli", diff --git a/Cargo.toml b/Cargo.toml index d7cfd4e7..29b3a25e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ uuid = { version = "1", features = ["v4"] } ## The assistant (ADR 0007): the scoped service contract a host injects. The ## local-runtime and remote connectors are optional (`octos-local`, ## `octos-remote`), so a hosted build links no runtime of its own. -octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense-System-Apps.git", rev = "4fa0f122ab01d58e468d339f1920ec2758741012" } +octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense-System-Apps.git", rev = "cc1cee3ee308843aac569111cf2eb31f995646cd" } octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "68f6a9df55692b5d8ef8873a12721e279a3f40d6" } octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "c3d53ba8bdbc219478b98b34fc73066228d9dfb3" } octoscript-widgets = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "c3d53ba8bdbc219478b98b34fc73066228d9dfb3" } @@ -328,6 +328,8 @@ icons = [ "./resources/icon.ico", ] out_dir = "./dist" +# One pinned local assistant runtime; installed beside Rinx by the packager. +external-binaries = ["./dist/octos"] ## Here, we define the list of resource directories for both Makepad and Rinx. ## The `src` directory is based on `./dist/` as the `out_dir` (above), which is where the @@ -345,6 +347,7 @@ resources = [ { src = "./LICENSE", target = "licenses/LICENSE" }, { src = "./LICENSE-MIT", target = "licenses/LICENSE-MIT" }, { src = "./NOTICE", target = "licenses/NOTICE" }, + { src = "./dist/licenses/octos-LICENSE", target = "licenses/octos-LICENSE" }, { src = "./dist/resources/rinx", target = "rinx" }, ] @@ -358,7 +361,8 @@ robius-packaging-commands before-each-package \ --force-makepad \ --binary-name rinx \ --path-to-binary ./target/release/rinx \ - --add-deb-dep geoclue-2.0 + --add-deb-dep geoclue-2.0 && +python3 tools/package-octos.py desktop """ ## This is not yet implemented, so remove it. diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index e8e59f53..3ef3133b 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -488,9 +488,12 @@ Deployment completion criteria: Known limits and follow-ups: -- In one scripted run the kernel accepted a second `peer_send_input` to a peer - that had already answered once, but did not run it within two minutes. The - first exchange is covered. This is a kernel follow-up. +- The earlier repeated-input failure was traced to the scripted model reusing + the same tool-call ID (`call_1`). The kernel correctly deduplicated that + occurrence. System Apps #18 gives each call a fresh ID and adds a regression + that completes a question/answer exchange, then executes another input on + the same peer. Both exchanges pass against `552767dd`; no production kernel + scheduling change was needed. - Rinx's `ServiceExecutor` (the system agent operating Rinx's UI or Matrix actions) still returns unavailable. Peer messaging is available, but no authorized app operations are defined yet. @@ -499,3 +502,36 @@ Known limits and follow-ups: request. - Per-app token/tool budgets and fair scheduling beyond the kernel's existing limits are not part of this change. + +### Deployment validation follow-up (2026-09-26) + +Standalone Rinx now pins System Apps `cc1cee3` and kernel `552767dd`, matching +the merged shell contracts. [Runtime packaging](../../packaging/README-octos.md) +builds the pinned executable for desktop and Android and includes its license. +Hosted builds continue to use only the injected service. + +Rechecked in isolated worktrees and disposable data directories: + +- All 20 app-peers tests pass, including five tests using the real pinned + kernel and a scripted model. The repeated-input regression is in + [System Apps #18](https://github.com/OctoSense-org/OctoSense-System-Apps/pull/18). +- Standalone local and authenticated remote process tests pass with the + aligned dependency. The local test also passes from a private bundle with + `RINX_OCTOS_BIN` removed, finding the adjacent packaged executable. +- Four packaging checks pass. cargo-packager 0.10.1 produces a macOS bundle + with `Contents/MacOS/octos` and its license. This inclusion check reuses the + existing fast-build Rinx binary; it is not a notarized distribution test. +- Native Makepad instrumentation verifies the standalone development bundle's + login screen and hosted Rinx's launch, close and reopen in the rebuilt + Desktop #41 shell. Both native test processes exited after capture. The + hosted launch starts no child kernel before an authorized request. +- The pinned Android kernel cross-builds as an aarch64 ELF PIE with 16 KB + load-segment alignment and only Android system-library dependencies. +- Standalone Android builds as the separate `dev.makepad.rinx.validation` + package. APK v2/v3 signature verification passes and the bundled + `lib/arm64-v8a/liboctos.so` matches the staged kernel's SHA-256 exactly. + This is a development APK; no device installation is claimed. + +Signed-in native mini-app AI/approval flows still require a disposable Matrix +login. The OnePlus 6 is absent from ADB; the four visible emulators are not +substitutes for its device test and were left untouched. diff --git a/packaging/README-octos.md b/packaging/README-octos.md new file mode 100644 index 00000000..28bba55f --- /dev/null +++ b/packaging/README-octos.md @@ -0,0 +1,81 @@ +# Standalone assistant runtime + +Standalone desktop and Android packages ship one Octos executable owned by +Rinx. `packaging/octos.lock.json` pins its source and features. The helper +refuses to package a revision different from `octos-cli` in `Cargo.lock`. +Update the System Apps dependency, lockfile and packaging pin together. + +OctoSense module builds do not use this packaging step. They link Rinx with +`default-features = false, features = ["octosense-module"]` and inject the +shell's scoped app-peer service. Mini apps open contexts on that peer; they +do not receive kernels of their own. + +## Desktop + +The existing cargo-packager hook builds Rinx, then runs: + +```sh +python3 tools/package-octos.py desktop +``` + +This builds the pinned kernel with `--locked --no-default-features --features +api,git,ast`, stages `dist/octos-`, its SHA-256 receipt and license, +and cargo-packager installs it beside Rinx. macOS signs the nested kernel +before signing and notarizing the app. For cross-packaging, set +`RINX_KERNEL_TARGET` to the same target passed to cargo-packager; the Rust +target and platform linker must already be installed. + +For a development build: + +```sh +cargo build --profile fast +python3 tools/package-octos.py desktop --app-binary target/fast/rinx +cargo run --profile fast +``` + +The macOS development runner copies and signs the adjacent kernel inside +its development bundle. `--kernel ` can reuse a native executable +whose version reports the pinned revision. It is not a cross-build option. + +## Android + +Use the pinned cargo-makepad and a **full** NDK (its minimal installation +does not contain the CMake metadata needed by Rinx's native dependencies). +CMake and a working native build tool must be on `PATH`. + +```sh +cargo makepad android install-toolchain --full-ndk +rustup target add aarch64-linux-android +python3 tools/package-octos.py android -- \ + cargo makepad android --abi=aarch64 build -p rinx --release --locked +``` + +With an explicitly installed SDK, pass its path both to the helper's +`--sdk` and cargo-makepad's `--sdk-path`. Otherwise the helper resolves +cargo-makepad's default SDK from the pinned Makepad checkout. + +The helper checks that the kernel is an aarch64 ELF PIE executable and +passes it as `MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=`. Android executes +it from the APK's native library directory, rather than Rinx's writable +data directory. Android release CI stages the same binary before packaging. + +These packaging steps cover desktop and Android. They do not provide a +local subprocess kernel on iOS. + +## Validation + +```sh +python3 tools/test_package_octos.py +RINX_TEST_OCTOS_BIN=/absolute/path/to/octos cargo test --locked \ + --test standalone_local --test standalone_remote -- --test-threads=1 +``` + +To verify production discovery, copy the `standalone_local` test executable +and the packaged kernel into a private directory beside one another (kernel +name `octos`, or `octos.exe` on Windows), then run the test with +`RINX_TEST_PACKAGED_OCTOS=1` as well. This clears `RINX_OCTOS_BIN` and exercises +Rinx's normal adjacent-executable lookup. The process tests currently use +Unix process inspection and are run on macOS; this is not a Windows test claim. + +Keep real-device installation and signed-in native UI evidence separate from +the process tests. A successful cross-build alone does not verify phone behavior. diff --git a/packaging/build-macos-dmg.sh b/packaging/build-macos-dmg.sh index 11c29c41..6e660e28 100755 --- a/packaging/build-macos-dmg.sh +++ b/packaging/build-macos-dmg.sh @@ -197,6 +197,7 @@ echo "==> Found unsigned DMG: $DMG_FILE" echo "==> Codesigning $APP_PATH..." xattr -cr "$APP_PATH" +codesign_with_retry "$APP_PATH/Contents/MacOS/octos" app codesign_with_retry "$APP_PATH/Contents/MacOS/rinx" app codesign_with_retry "$APP_PATH" app codesign --verify --verbose=2 "$APP_PATH" diff --git a/packaging/octos.lock.json b/packaging/octos.lock.json new file mode 100644 index 00000000..f5b1adb4 --- /dev/null +++ b/packaging/octos.lock.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "url": "https://github.com/octos-org/octos.git", + "revision": "552767dd50e16e08c6225a0cb252fe884e794084", + "features": ["api", "git", "ast"] +} diff --git a/packaging/run-macos.sh b/packaging/run-macos.sh index 31a7c532..8c0030e0 100755 --- a/packaging/run-macos.sh +++ b/packaging/run-macos.sh @@ -65,6 +65,9 @@ trap 'forward_signal TERM 143' TERM trap 'forward_signal HUP 129' HUP inputs=$(/usr/bin/shasum -a 256 "$binary" "$packaging_dir/macos/Info.plist" "${BASH_SOURCE[0]}") +if [[ -f $binary_dir/octos ]]; then + inputs+=$'\n'$(/usr/bin/shasum -a 256 "$binary_dir/octos") +fi previous_inputs= if [[ -f $bundle_dir/inputs ]]; then previous_inputs=$(< "$bundle_dir/inputs"); fi if [[ $inputs != "$previous_inputs" || ! -f $bundle/Contents/MacOS/rinx ]]; then @@ -79,6 +82,10 @@ if [[ $inputs != "$previous_inputs" || ! -f $bundle/Contents/MacOS/rinx ]]; then # Copy before signing: a symlink or hard link would mutate Cargo's executable. mkdir -p "$staging/Rinx.app/Contents/MacOS" cp "$binary" "$staging/Rinx.app/Contents/MacOS/rinx" + if [[ -f $binary_dir/octos ]]; then + cp "$binary_dir/octos" "$staging/Rinx.app/Contents/MacOS/octos" + /usr/bin/codesign --force --sign - "$staging/Rinx.app/Contents/MacOS/octos" + fi cp "$packaging_dir/macos/Info.plist" "$staging/Rinx.app/Contents/Info.plist" plist="$staging/Rinx.app/Contents/Info.plist" /usr/libexec/PlistBuddy -c 'Set :CFBundleIdentifier org.octosense.rinx.development' "$plist" diff --git a/tests/standalone_local.rs b/tests/standalone_local.rs index 4991755b..8bc46e06 100644 --- a/tests/standalone_local.rs +++ b/tests/standalone_local.rs @@ -72,7 +72,13 @@ fn standalone_rinx_owns_one_local_runtime_and_stops_only_it() { // Rinx's own data root and the packaged kernel, set explicitly (never PATH). unsafe { std::env::set_var("RINX_DATA_DIR", &data); - std::env::set_var("RINX_OCTOS_BIN", &kernel); + // Release validation runs a copy of this executable beside the + // packaged kernel, exercising production discovery without overrides. + if std::env::var_os("RINX_TEST_PACKAGED_OCTOS").is_some() { + std::env::remove_var("RINX_OCTOS_BIN"); + } else { + std::env::set_var("RINX_OCTOS_BIN", &kernel); + } } let model = model(); rinx::octos_service::write_local_provider("local", "mock-model", &format!("http://127.0.0.1:{}/v1", model.1), "").unwrap(); diff --git a/tools/package-octos.py b/tools/package-octos.py new file mode 100644 index 00000000..70d5881b --- /dev/null +++ b/tools/package-octos.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Build and package Rinx's pinned, independently owned Octos runtime. + +Desktop: python3 tools/package-octos.py desktop [--target ] +Android: python3 tools/package-octos.py android --sdk -- \ + cargo makepad android --sdk-path= build -p rinx --release + +Desktop output uses cargo-packager's external-binary naming convention. +Android commands receive MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=. +The checkout and build outputs stay under this repository's target directory. +No installed kernel, provider settings, or app data is read or modified. +""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import struct +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +ANDROID = "aarch64-linux-android" + + +def read_lock(root=ROOT): + lock = json.loads((root / "packaging/octos.lock.json").read_text()) + if (lock.get("schema_version") != 1 + or lock.get("url") != "https://github.com/octos-org/octos.git" + or not re.fullmatch(r"[0-9a-f]{40}", lock.get("revision", "")) + or lock.get("features") != ["api", "git", "ast"]): + raise ValueError("Invalid pinned Octos runtime lock") + revisions = set(re.findall( + r'git\+https://github\.com/octos-org/octos\.git\?rev=([0-9a-f]{40})#', + (root / "Cargo.lock").read_text())) + # nix may come from a separate historical vendor revision. Only the + # kernel crate's source determines the runtime contract. + cli = re.search(r'name = "octos-cli"\nversion = "[^\"]+"\nsource = "git\+https://github\.com/octos-org/octos\.git\?rev=([0-9a-f]{40})#', + (root / "Cargo.lock").read_text()) + if not cli or cli[1] != lock["revision"] or lock["revision"] not in revisions: + raise ValueError("Octos packaging pin differs from Cargo.lock; update them together") + return lock + + +def host_target(): + text = subprocess.check_output(["rustc", "-vV"], text=True) + return next(line.split(": ", 1)[1] for line in text.splitlines() if line.startswith("host: ")) + + +def prepare_source(lock, work, offline=False): + source = work / "src" + if not (source / ".git").exists(): + source.mkdir(parents=True, exist_ok=True) + subprocess.run(["git", "init", "--quiet", str(source)], check=True) + def git(*args, **kwargs): + return subprocess.run(["git", "-C", str(source), *args], **kwargs) + dirty = git("status", "--porcelain", capture_output=True, text=True, check=True).stdout + if dirty: + raise RuntimeError("Preserving modified kernel checkout: " + str(source)) + have = git("cat-file", "-e", lock["revision"] + "^{commit}", capture_output=True) + if have.returncode: + if offline: + raise RuntimeError("Pinned kernel is not cached; prepare it once without --offline") + git("fetch", "--quiet", "--depth=1", "--no-tags", lock["url"], lock["revision"], check=True) + git("checkout", "--quiet", "--detach", lock["revision"], check=True) + return source + + +def android_env(sdk): + bins = list(sdk.glob("ndk/*/toolchains/llvm/prebuilt/*/bin")) + if not bins: + raise ValueError("No NDK found under " + str(sdk / "ndk")) + bins.sort(key=lambda p: tuple(int(n) for n in re.findall(r"\d+", str(p.relative_to(sdk))))) + ndk = bins[-1] + clang = ndk / (ANDROID + "33-clang") + if os.name == "nt": + clang = clang.with_suffix(".cmd") + suffix = ".exe" if os.name == "nt" else "" + if not clang.is_file(): + raise ValueError("Missing Android API 33 compiler: " + str(clang)) + return { + "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER": str(clang), + "CARGO_TARGET_AARCH64_LINUX_ANDROID_AR": str(ndk / ("llvm-ar" + suffix)), + "CC_aarch64_linux_android": str(clang), + "CXX_aarch64_linux_android": str(clang).replace("-clang", "-clang++"), + "AR_aarch64_linux_android": str(ndk / ("llvm-ar" + suffix)), + "RANLIB_aarch64_linux_android": str(ndk / ("llvm-ranlib" + suffix)), + } + + +def default_android_sdk(): + """Find cargo-makepad's default SDK in the pinned Makepad checkout.""" + metadata = json.loads(subprocess.check_output( + ["cargo", "metadata", "--locked", "--format-version=1"], cwd=ROOT, text=True)) + platform = next(p for p in metadata["packages"] if p["name"] == "makepad-platform") + makepad = Path(platform["manifest_path"]).parent.parent + host = host_target() + if "apple" in host: + name = "macos_aarch64" if host.startswith("aarch64") else "macos_x64" + elif "windows" in host: + name = "windows_x64" + else: + name = "linux_x64" + return makepad / "tools/cargo_makepad" / ("android_33_" + name) + + +def build_kernel(source, lock, work, target, sdk=None, offline=False): + env = os.environ.copy() + # Rinx's package/resource flags must not leak into the independent kernel. + env.pop("MAKEPAD", None) + env.pop("MAKEPAD_PACKAGE_DIR", None) + env.pop("RUSTFLAGS", None) + env.pop("CARGO_ENCODED_RUSTFLAGS", None) + env["CARGO_TARGET_DIR"] = str(work / "build") + if target == ANDROID: + if sdk is None: + sdk = default_android_sdk() + env.update(android_env(sdk)) + args = ["cargo", "build", "--locked", "--release", "--target", target, + "-p", "octos-cli", "--bin", "octos", "--no-default-features", + "--features", ",".join(lock["features"])] + if offline: + args.append("--offline") + subprocess.run(args, cwd=source, env=env, check=True) + return work / "build" / target / "release" / ("octos.exe" if "windows" in target else "octos") + + +def stage(kernel, lock, target, source, out=ROOT / "dist", prebuilt=False): + # Android executes this PIE binary out of nativeLibraryDir. Packaging a + # host binary under liboctos.so would install successfully but fail at run. + if target == ANDROID: + with kernel.open("rb") as stream: + header = stream.read(20) + if (len(header) != 20 or header[:6] != b"\x7fELF\x02\x01" + or struct.unpack_from(" Date: Sat, 26 Sep 2026 23:26:15 -0700 Subject: [PATCH 2/9] fix: keep runtime executables out of release installer uploads --- .github/workflows/release.yml | 2 +- Cargo.toml | 4 ++-- docs/adr/0007-host-owned-octos-app-peers.md | 4 ++++ packaging/README-octos.md | 6 ++++-- tools/package-octos.py | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 209657ca..8138c3ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -575,7 +575,7 @@ jobs: cargo makepad android install-toolchain --full-ndk rustup target add aarch64-linux-android python3 tools/package-octos.py android - echo "MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=$PWD/dist/octos-aarch64-linux-android" >> "$GITHUB_ENV" + echo "MAKEPAD_ANDROID_EXTRA_LIBS=liboctos.so=$PWD/dist/runtime/octos-aarch64-linux-android" >> "$GITHUB_ENV" - name: Package (android) uses: project-robius/makepad-packaging-action@v1.8.0 diff --git a/Cargo.toml b/Cargo.toml index 29b3a25e..1f70ccd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -329,7 +329,7 @@ icons = [ ] out_dir = "./dist" # One pinned local assistant runtime; installed beside Rinx by the packager. -external-binaries = ["./dist/octos"] +external-binaries = ["./dist/runtime/octos"] ## Here, we define the list of resource directories for both Makepad and Rinx. ## The `src` directory is based on `./dist/` as the `out_dir` (above), which is where the @@ -347,7 +347,7 @@ resources = [ { src = "./LICENSE", target = "licenses/LICENSE" }, { src = "./LICENSE-MIT", target = "licenses/LICENSE-MIT" }, { src = "./NOTICE", target = "licenses/NOTICE" }, - { src = "./dist/licenses/octos-LICENSE", target = "licenses/octos-LICENSE" }, + { src = "./dist/runtime/licenses/octos-LICENSE", target = "licenses/octos-LICENSE" }, { src = "./dist/resources/rinx", target = "rinx" }, ] diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index 3ef3133b..b85dd11d 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -535,3 +535,7 @@ Rechecked in isolated worktrees and disposable data directories: Signed-in native mini-app AI/approval flows still require a disposable Matrix login. The OnePlus 6 is absent from ADB; the four visible emulators are not substitutes for its device test and were left untouched. + +The merged ROM Home source also produced development-signed Home and Bridge +APKs using the ROM-patched cargo-makepad. The Home APK contains the exact pinned +kernel. Neither APK was installed; the installed Home remains untouched. diff --git a/packaging/README-octos.md b/packaging/README-octos.md index 28bba55f..7d97cbd3 100644 --- a/packaging/README-octos.md +++ b/packaging/README-octos.md @@ -19,8 +19,10 @@ python3 tools/package-octos.py desktop ``` This builds the pinned kernel with `--locked --no-default-features --features -api,git,ast`, stages `dist/octos-`, its SHA-256 receipt and license, -and cargo-packager installs it beside Rinx. macOS signs the nested kernel +api,git,ast`, stages `dist/runtime/octos-`, its SHA-256 receipt and license, +and cargo-packager installs it beside Rinx. Runtime staging stays below +`dist/runtime/` so the release uploader cannot mistake a Windows kernel `.exe` +for a Rinx installer. macOS signs the nested kernel before signing and notarizing the app. For cross-packaging, set `RINX_KERNEL_TARGET` to the same target passed to cargo-packager; the Rust target and platform linker must already be installed. diff --git a/tools/package-octos.py b/tools/package-octos.py index 70d5881b..437e01b4 100644 --- a/tools/package-octos.py +++ b/tools/package-octos.py @@ -127,7 +127,7 @@ def build_kernel(source, lock, work, target, sdk=None, offline=False): return work / "build" / target / "release" / ("octos.exe" if "windows" in target else "octos") -def stage(kernel, lock, target, source, out=ROOT / "dist", prebuilt=False): +def stage(kernel, lock, target, source, out=ROOT / "dist/runtime", prebuilt=False): # Android executes this PIE binary out of nativeLibraryDir. Packaging a # host binary under liboctos.so would install successfully but fail at run. if target == ANDROID: From dceaa7d80d100d37b57b1625c62dd3b344599e47 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:21:29 -0700 Subject: [PATCH 3/9] fix: configure Android auxiliary HTTPS clients and verify device runtime --- docs/adr/0007-host-owned-octos-app-peers.md | 34 ++++++++++++++++++--- src/agent_chat/ops/backend.rs | 2 +- src/article_app/remote_images.rs | 6 ++-- src/http.rs | 19 ++++++++++++ src/lib.rs | 1 + src/login/homeserver.rs | 5 ++- src/sliding_sync.rs | 6 +--- tools/package-octos.py | 9 +++++- tools/test_package_octos.py | 19 ++++++++++++ 9 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 src/http.rs diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index b85dd11d..980bfb3d 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -518,7 +518,8 @@ Rechecked in isolated worktrees and disposable data directories: - Standalone local and authenticated remote process tests pass with the aligned dependency. The local test also passes from a private bundle with `RINX_OCTOS_BIN` removed, finding the adjacent packaged executable. -- Four packaging checks pass. cargo-packager 0.10.1 produces a macOS bundle +- Six packaging checks pass, including seven-character Git revisions from + shallow source builds. cargo-packager 0.10.1 produces a macOS bundle with `Contents/MacOS/octos` and its license. This inclusion check reuses the existing fast-build Rinx binary; it is not a notarized distribution test. - Native Makepad instrumentation verifies the standalone development bundle's @@ -530,12 +531,37 @@ Rechecked in isolated worktrees and disposable data directories: - Standalone Android builds as the separate `dev.makepad.rinx.validation` package. APK v2/v3 signature verification passes and the bundled `lib/arm64-v8a/liboctos.so` matches the staged kernel's SHA-256 exactly. - This is a development APK; no device installation is claimed. + This development APK is installed under its separate package on OnePlus 6. Signed-in native mini-app AI/approval flows still require a disposable Matrix -login. The OnePlus 6 is absent from ADB; the four visible emulators are not -substitutes for its device test and were left untouched. +login. No normal Rinx account or AI credentials are copied into the test apps. The merged ROM Home source also produced development-signed Home and Bridge APKs using the ROM-patched cargo-makepad. The Home APK contains the exact pinned kernel. Neither APK was installed; the installed Home remains untouched. + +#### OnePlus 6 follow-up (2026-09-27) + +The phone runs Android 15. The packaged kernel starts under the separate test +app's UID and reports `552767d`, the abbreviated pinned revision. Sixteen +checks pass against that Android executable over stdio with a scripted model: +two app peers and three isolated request contexts, streamed/persisted replies, +history isolation, host-token rejection, repeated turns, the system agent's +input/question/answer exchange and second input, and context closure while +another context remains usable. This exercises the kernel protocol, not a +signed-in native mini-app flow. + +The native standalone UI exposed a reqwest 0.13 Android verifier panic during +the browser-registration metadata request. Auxiliary HTTP clients now use the +same bundled trust roots as the Matrix client, retaining certificate and +hostname verification. This also covers article image downloads and the +optional agent transport. Nine homeserver tests pass. The rebuilt test APK +successfully discovers matrix.org and renders its sign-in choices on the phone. + +A separate `dev.makepad.octosense.rinxvalidation` APK launches Rinx in-process +and displays its sign-in screen. Its validation manifest omits the Home intent; +it does not replace the installed Home or Bridge. The unrelated privileged +bridge correctly rejects its test identity. Native Back testing exposed the +shell's Activity fallback closing the host before its Rust navigation handles +Back; a ROM callback fix and device retest are in progress. Signed-in native +AI/approval flows and production ROM integration are not yet claimed complete. diff --git a/src/agent_chat/ops/backend.rs b/src/agent_chat/ops/backend.rs index b17b10f9..c7166daa 100644 --- a/src/agent_chat/ops/backend.rs +++ b/src/agent_chat/ops/backend.rs @@ -11,7 +11,7 @@ pub struct Transport(reqwest::Client); impl Transport { pub fn new() -> Result { Ok(Self( - reqwest::Client::builder() + crate::http::client_builder() .no_proxy() .redirect(reqwest::redirect::Policy::none()) .timeout(std::time::Duration::from_secs(15)) diff --git a/src/article_app/remote_images.rs b/src/article_app/remote_images.rs index 7cf494e4..5c8cd65f 100644 --- a/src/article_app/remote_images.rs +++ b/src/article_app/remote_images.rs @@ -1,7 +1,7 @@ //! Host-controlled HTTP(S) image loading. Renderers receive bounded PNG bytes only. use article_makepad::content::{Images, prepare_image}; use futures_util::{stream, StreamExt}; -use matrix_sdk::reqwest::{Client, redirect::Policy}; +use matrix_sdk::reqwest::redirect::Policy; use std::{collections::BTreeMap, sync::Arc, time::Duration}; fn allowed(url: &url::Url) -> bool { @@ -34,8 +34,8 @@ fn allowed(url: &url::Url) -> bool { } pub async fn load(urls: Vec) -> Images { - let Ok(client) = Client::builder() - .timeout(Duration::from_secs(20)) + let Ok(client) = crate::http::client_builder() + .timeout(Duration::from_secs(20)) .user_agent("Rinx article preview") .redirect(Policy::custom(|attempt| { if attempt.previous().len() >= 5 || !allowed(attempt.url()) { diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 00000000..018b0988 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,19 @@ +//! TLS configuration shared by Rinx's auxiliary HTTP clients. + +#[cfg(target_os = "android")] +pub(crate) fn android_root_certificates() -> Vec { + webpki_root_certs::TLS_SERVER_ROOT_CERTS + .iter() + .filter_map(|der| matrix_sdk::reqwest::Certificate::from_der(der.as_ref()).ok()) + .collect() +} + +pub(crate) fn client_builder() -> matrix_sdk::reqwest::ClientBuilder { + let builder = matrix_sdk::reqwest::Client::builder(); + // reqwest 0.13 defaults to a JVM-backed verifier on Android. Makepad + // does not initialize that verifier; use the same bundled trust roots + // as the Matrix SDK client, with certificate/hostname checks enabled. + #[cfg(target_os = "android")] + let builder = builder.tls_certs_only(android_root_certificates()); + builder +} diff --git a/src/lib.rs b/src/lib.rs index 1b18c7f7..4056655e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,6 +100,7 @@ pub mod media_cache; pub mod verification; pub mod utils; +mod http; pub mod temp_storage; pub mod location; pub mod image_utils; diff --git a/src/login/homeserver.rs b/src/login/homeserver.rs index c3f47526..747c8282 100644 --- a/src/login/homeserver.rs +++ b/src/login/homeserver.rs @@ -127,7 +127,10 @@ async fn browser_registration_supported(client: &Client) -> bool { let Ok(metadata_url) = client.homeserver().join("_matrix/client/v1/auth_metadata") else { return false; }; - let Ok(response) = matrix_sdk::reqwest::Client::new().get(metadata_url).timeout(std::time::Duration::from_secs(10)).send().await else { + let Ok(http) = crate::http::client_builder().build() else { + return false; + }; + let Ok(response) = http.get(metadata_url).timeout(std::time::Duration::from_secs(10)).send().await else { return false; }; if !response.status().is_success() { diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 5b8d34e4..856e8ac8 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -117,13 +117,9 @@ pub fn build_sqlite_store_config( pub(crate) fn use_android_tls_roots(builder: matrix_sdk::ClientBuilder) -> matrix_sdk::ClientBuilder { #[cfg(target_os = "android")] let builder = { - let roots = webpki_root_certs::TLS_SERVER_ROOT_CERTS - .iter() - .filter_map(|der| matrix_sdk::reqwest::Certificate::from_der(der.as_ref()).ok()) - .collect(); builder .disable_built_in_root_certificates() - .add_root_certificates(roots) + .add_root_certificates(crate::http::android_root_certificates()) }; builder } diff --git a/tools/package-octos.py b/tools/package-octos.py index 437e01b4..d273c408 100644 --- a/tools/package-octos.py +++ b/tools/package-octos.py @@ -49,6 +49,13 @@ def host_target(): return next(line.split(": ", 1)[1] for line in text.splitlines() if line.startswith("host: ")) +def matches_revision(version, revision): + # Git chooses the abbreviation length from the checkout's object set: + # a shallow build commonly reports seven characters, a full clone more. + match = re.fullmatch(r"octos \S+ \(([0-9a-f]{7,40}) \d{4}-\d{2}-\d{2}\)", version.strip()) + return bool(match and revision.startswith(match[1])) + + def prepare_source(lock, work, offline=False): source = work / "src" if not (source / ".git").exists(): @@ -182,7 +189,7 @@ def main(argv=None): parser.error("Prebuilt reuse is limited to the native target; cross builds must use the pinned source") kernel = args.kernel.resolve(strict=True) version = subprocess.check_output([str(kernel), "--version"], text=True) - if lock["revision"][:8] not in version: + if not matches_revision(version, lock["revision"]): parser.error("Prebuilt kernel does not report the pinned revision") else: kernel = build_kernel(source, lock, work, target, args.sdk, args.offline) diff --git a/tools/test_package_octos.py b/tools/test_package_octos.py index 74358784..5316838e 100644 --- a/tools/test_package_octos.py +++ b/tools/test_package_octos.py @@ -16,6 +16,25 @@ class PackagingTests(unittest.TestCase): def test_repository_runtime_and_ohos_contract_have_the_same_pin(self): package.read_lock() + def test_prebuilt_revision_accepts_git_abbreviations(self): + revision = package.read_lock()["revision"] + for length in (7, 8, 12, 40): + with self.subTest(length=length): + self.assertTrue(package.matches_revision( + f"octos 2.0.3-rc.13 ({revision[:length]} 2026-09-26)", revision)) + + def test_prebuilt_revision_rejects_mismatch_or_unverifiable_output(self): + revision = package.read_lock()["revision"] + for version in ( + "octos 2.0.3-rc.13 (abcdef0 2026-09-26)", + f"octos 2.0.3-rc.13 ({revision[:8]}0 2026-09-26)", + f"octos 2.0.3-rc.13 ({revision[:6]} 2026-09-26)", + f"unrelated output containing {revision}", + "octos 2.0.3-rc.13", + ): + with self.subTest(version=version): + self.assertFalse(package.matches_revision(version, revision)) + def test_mismatched_kernel_pin_is_rejected(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) From ebcdd2db03eb8c1ea06e152659a98c11c5e2e097 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:05:00 -0700 Subject: [PATCH 4/9] docs: record current Mac and OnePlus deployment validation --- docs/adr/0007-host-owned-octos-app-peers.md | 67 ++++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index e0d49dda..a814f03a 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -499,7 +499,7 @@ fixtures. No provider key or Matrix credential is used anywhere. | 5. Workspace, transcript, memory and tool isolation, incl. automatic injection and simultaneous mini-app requests | octos `should_isolate_app_peer_workspace_and_memory_from_the_system_and_each_other`, `should_open_isolated_request_contexts_and_refuse_them_after_close`, `should_refuse_a_binding_that_shares_state_with_another_app_peer`, `should_never_extract_an_app_bound_session_into_the_profile_memory` | Met (kernel tests) | | 6. Model selection affects only the intended peer, keeps credentials with the host, reports fallback, leaves the default | octos `should_select_a_configured_model_for_one_peer_without_touching_the_profile_default`; a request context runs on its peer's lane | Met (kernel tests) | | 7. Standalone local and remote work without OctoSense; hosted cannot spawn a kernel; closing leaves others usable | Rinx `tests/standalone_local.rs` and `tests/standalone_remote.rs`. The shell tests `rinx_is_hosted_with_the_shells_service_and_starts_no_kernel`. Shell CI keeps Rinx's `standalone` / `octos-local` / `octos-remote` out of every graph. Native Desktop run: no kernel process started by launching Rinx | Met (process level + native launch) | -| 8. macOS desktop and OnePlus 6 ROM Home native flows | macOS Desktop: hosted launch only (hidden window, remote bridge). OnePlus 6: nothing | **Partially verified** on macOS; **ROM Home on device is unverified**, because the phone is in use by someone else and was not touched | +| 8. macOS desktop and OnePlus 6 ROM Home native flows | macOS: native standalone discovery and earlier hosted launch/close/reopen. OnePlus 6: standalone and separate hosted validation APKs launch; matrix.org discovery, single-event Back/edge swipe, IME dismissal, background/resume and restart verified (details below) | **Partially verified**: signed-in mini-app/approval flows and privileged production ROM integration remain unverified | Deployment completion criteria: @@ -508,16 +508,17 @@ Deployment completion criteria: | Standalone local | `standalone_rinx_owns_one_local_runtime_and_stops_only_it`: no kernel before the first request, then the packaged kernel (explicit path, never PATH) under Rinx's data root. Two mini apps share it with separate contexts. Exit stops Rinx's kernel and leaves another owner's running. A restart resumes the same peer | Met at process level | | Standalone remote | `standalone_rinx_uses_an_explicit_authenticated_remote_server` against `octos serve --auth-token `: a wrong token is refused, a turn runs in a server-provisioned workspace, the server token is never written to disk, an account change drops the late reply, and exit leaves the server running | Met at process level | | Hosted desktop | Launch before AppCard: native (Desktop#50 build, hidden window). System peer, question/answer and second-app survival: real-kernel broker tests. A signed-in Rinx mini app exchanging with the system agent natively: not run (needs a Matrix account) | Partially verified | -| Hosted ROM Home | ROM#34 builds and its tests pass on macOS and in CI; no APK was installed | Unverified (no device access) | +| Hosted ROM Home | Consolidated OctoSense test APK installed separately on OnePlus 6: native launch, Back, edge swipe, background/resume and restart. No Rinx-owned kernel starts before authorization | Partially verified; signed-in mini-app flows and the privileged installed Home/Bridge are not covered | Known limits and follow-ups: -- The earlier repeated-input failure was traced to the scripted model reusing - the same tool-call ID (`call_1`). The kernel correctly deduplicated that - occurrence. System Apps #18 gives each call a fresh ID and adds a regression - that completes a question/answer exchange, then executes another input on - the same peer. Both exchanges pass against `552767dd`; no production kernel - scheduling change was needed. +- The initial repeated-input check passed against `552767dd` after the + scripted model used unique tool-call IDs (System Apps #18). That did not + cover providers that reuse an ID in a later turn. The consolidated + OctoSense fixture deliberately reuses `call_1`; kernel `a6ea8505` handles + that case. Both the question/answer exchange and the next system-to-peer + input now pass on the real Android kernel. The earlier unique-ID result + alone was insufficient to claim repeated-input compatibility. - Rinx's `ServiceExecutor` now serves the first action set: `status`, `list_rooms`, `open_room`, `draft_message`, `read_room` (grant-gated), `open_mini_app` (the reviewed app only; Run still grants it) and @@ -592,10 +593,46 @@ and displays its sign-in screen. Its validation manifest omits the Home intent; it does not replace the installed Home or Bridge. The unrelated privileged bridge correctly rejects its test identity. Native Back testing exposed the shell's Activity fallback closing the host before its Rust navigation handles -Back; a ROM callback fix and device retest are in progress. Signed-in native -AI/approval flows and production ROM integration are not yet claimed complete. - -After the repository consolidation, Rinx follows main’s app-peers pin -`OctoSense@f38aa250` and kernel `a6ea8505`. Packaging follows that same kernel -pin. Device results above identify the pre-consolidation build; validation of -the updated graph is recorded separately. +Back. Subsequent predictive-Back testing also caught duplicate compatibility +key events. The corrected callback and key routing are verified below. +Signed-in native AI/approval flows and production ROM integration are not yet +claimed complete. + +#### Consolidated runtime validation (2026-09-27) + +Rinx `e6443ae1` follows main’s app-peers pin `OctoSense@f38aa250`, Makepad +`6cf03859` and kernel `a6ea8505`. Runtime packaging follows that kernel pin. +The preceding results identify the earlier builds; these checks use the +updated graph: + +- Six packaging checks and nine homeserver tests pass. Both Rinx CI jobs pass. +- On macOS, Makepad instrumentation verifies native startup and public + matrix.org discovery. Real-kernel process tests pass for packaged local + discovery (without `RINX_OCTOS_BIN`), per-context isolation, restart and + owner-only shutdown, and for the authenticated remote deployment. +- OnePlus 6 standalone validation package version `2026092708` starts and + discovers matrix.org without the verifier panic. Its packaged kernel's + hash matches the source-build receipt and reports `a6ea850` on the phone. +- All 16 Android kernel checks pass using the consolidated scripted-model + fixture, including repeated tool-call IDs across turns. These are protocol + tests under the test app's UID, separate from native signed-in UI tests. +- The consolidated OctoSense dependency check finds one Rinx, one Makepad + runtime and one shared Octos service; the hosted graph contains no Rinx + standalone kernel features. +- The hosted validation APK (`2026092710`, OctoSense `33eaf68`, Rinx + `e6443ae1`) includes the same verified Android kernel. It fixes the renamed + phone crate's theme asset path and owns system Back through Android's + dispatcher. The Makepad overlay suppresses the compatibility Back key + while that callback is registered, preventing two navigations per press. + The clean pinned runtime plus both patches reproduces the locked tree. +- On OnePlus 6, the Back key and an edge swipe each produce exactly one + navigation event and return Rinx to the launcher without closing the host. + IME Back dismisses the keyboard before app navigation. Reopen and + background/resume keep the same host process. After process restart, + Rinx launches again and discovers matrix.org. No separate kernel process + exists under the host UID before an authorized request. These tests use + the separate validation identity, not the privileged Home/Bridge identity. + +Signed-in native mini-app requests, consent dialogs and cross-app exchanges +remain unverified until a Matrix login is provided in the validation app. +The validation packages do not replace normal Rinx, Home or Bridge. From 065de61a9badd04b351e9f270e7f9007be5a1c8d Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:13:54 -0700 Subject: [PATCH 5/9] fix: bind assistant account after Matrix login and restore --- Cargo.lock | 32 ++++---- Cargo.toml | 2 +- docs/adr/0007-host-owned-octos-app-peers.md | 17 +++- src/octos_service.rs | 86 +++++++++++++++++++++ src/octoscript_apps/mod.rs | 2 +- src/sliding_sync.rs | 26 ++++--- 6 files changed, 132 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 818e5ca0..6bdf7d5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7908,11 +7908,11 @@ dependencies = [ [[package]] name = "octosense-app-peers" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=f38aa250c0d37f44d3f0426242bec1606b61e616#f38aa250c0d37f44d3f0426242bec1606b61e616" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" dependencies = [ "futures-util", "log", - "octosense-octos-core", + "octosense-kernel", "serde_json", "tokio", "tokio-tungstenite 0.24.0", @@ -7931,10 +7931,23 @@ dependencies = [ "serde_json", ] +[[package]] +name = "octosense-kernel" +version = "0.1.0" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" +dependencies = [ + "log", + "octos-cli", + "octosense-llm-config", + "serde_json", + "tokio", + "tracing-subscriber", +] + [[package]] name = "octosense-llm-config" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=f38aa250c0d37f44d3f0426242bec1606b61e616#f38aa250c0d37f44d3f0426242bec1606b61e616" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" dependencies = [ "argon2", "chacha20poly1305 0.10.1", @@ -7947,19 +7960,6 @@ dependencies = [ "url", ] -[[package]] -name = "octosense-octos-core" -version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=f38aa250c0d37f44d3f0426242bec1606b61e616#f38aa250c0d37f44d3f0426242bec1606b61e616" -dependencies = [ - "log", - "octos-cli", - "octosense-llm-config", - "serde_json", - "tokio", - "tracing-subscriber", -] - [[package]] name = "ohos-sys" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 2aced479..cd8588f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ uuid = { version = "1", features = ["v4"] } ## The assistant (ADR 0007): the scoped service contract a host injects. The ## local-runtime and remote connectors are optional (`octos-local`, ## `octos-remote`), so a hosted build links no runtime of its own. -octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "f38aa250c0d37f44d3f0426242bec1606b61e616" } +octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "c8068891a3a454a3a9b5b02236971294973e5087" } octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "68f6a9df55692b5d8ef8873a12721e279a3f40d6" } octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } octoscript-widgets = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index a814f03a..50301436 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -633,6 +633,17 @@ updated graph: exists under the host UID before an authorized request. These tests use the separate validation identity, not the privileged Home/Bridge identity. -Signed-in native mini-app requests, consent dialogs and cross-app exchanges -remain unverified until a Matrix login is provided in the validation app. -The validation packages do not replace normal Rinx, Home or Bridge. +The subsequent signed-in OnePlus check successfully read the Matrix profile +through the native mini-app, but exposed an account-binding error: login +invalidated the previous session before storing the new Matrix client, so +the injected assistant retained a signed-out account. Client replacement +now revokes old contexts, stores the new client, then binds the assistant. +Invalidation after login failure explicitly revokes instead of reading the +stale client. An offline regression using real SDK sessions and the broker +passes login, restore, account switch, login failure and logout for all +three deployments. Rinx's service dependency follows OctoSense `c806889` +and its renamed `octosense-kernel` crate. + +Native assistant requests, consent dialogs and cross-app exchanges still +need verification with this fix. The validation packages do not replace +normal Rinx, Home or Bridge. diff --git a/src/octos_service.rs b/src/octos_service.rs index 606e56dc..840e349b 100644 --- a/src/octos_service.rs +++ b/src/octos_service.rs @@ -143,6 +143,14 @@ pub fn sync_account() { } } +/// Revoke requests immediately, including when login failed but the Matrix +/// client has not been cleared yet. Never rebind that stale client's account. +pub fn revoke_account() { + if let Some(service) = state().service.clone() { + service.set_account(None); + } +} + /// Hosted Rinx is closing: release its leases, leave the shared kernel. pub fn release() { let service = state().service.take(); @@ -416,6 +424,84 @@ mod local { mod tests { use super::*; + /// Exercise real SDK session restoration and broker account gates without + /// starting a kernel, contacting a homeserver or writing a user profile. + #[tokio::test] + async fn matrix_login_restore_switch_and_logout_bind_and_revoke_assistant_contexts() { + use octosense_app_peers::{broker::{BoxFuture, Broker, BrokerConfig, Connector, Link}, ContextSpec}; + use crate::sliding_sync::replace_client; + + struct Offline; + impl Connector for Offline { + fn available(&self) -> Result<(), String> { Err("offline test".into()) } + fn connect(&self) -> BoxFuture<'static, Result, String>> { + panic!("account binding must not need a kernel connection"); + } + fn owns_runtime(&self) -> bool { false } + fn shutdown(&self) {} + } + struct Reset; + impl Drop for Reset { + fn drop(&mut self) { + replace_client(None); + *state() = State::default(); + } + } + let _reset = Reset; + async fn client(user: &str) -> matrix_sdk::Client { + let client = matrix_sdk::Client::builder() + .homeserver_url("http://127.0.0.1:1") + .server_versions([matrix_sdk::ruma::api::MatrixVersion::V1_0]) + .build().await.unwrap(); + client.matrix_auth().restore_session( + serde_json::from_value(serde_json::json!({ + "user_id": user, "device_id": "ACCOUNT_BINDING_TEST", + "access_token": "offline-fixture-token" + })).unwrap(), + Default::default(), + ).await.unwrap(); + client + } + let alice = client("@alice:example.org").await; + let bob = client("@bob:example.org").await; + for deployment in [Deployment::Hosted, Deployment::StandaloneLocal, Deployment::StandaloneRemote] { + replace_client(None); + *state() = State::default(); + let service: Arc = Arc::new(Broker::new( + BrokerConfig::new(deployment, "_main", "_main:api:root", "rinx", "Rinx", rinx_services()), + Arc::new(Offline), + )); + match deployment { + Deployment::Hosted => install_hosted(Some(service.clone())), + Deployment::StandaloneLocal => install_standalone(service.clone(), StandaloneMode::Local), + Deployment::StandaloneRemote => install_standalone(service.clone(), StandaloneMode::Remote { + url: "ws://127.0.0.1:1".into(), profile: "_main".into(), + }), + } + let open = |account: &str| service.open_context(ContextSpec { + account: account.into(), instance: "test-mini-app".into(), services: rinx_services(), + }); + assert!(open("@alice:example.org").is_err()); + replace_client(Some(alice.clone())); + let first = open("@alice:example.org").expect("login must bind the new account"); + replace_client(Some(alice.clone())); + assert!(!first.is_open(), "even the same account's old login must be revoked"); + let restored = open("@alice:example.org").expect("restored login must be bound"); + replace_client(Some(bob.clone())); + assert!(!restored.is_open()); + assert!(open("@alice:example.org").is_err()); + let current = open("@bob:example.org").unwrap(); + crate::octoscript_apps::invalidate_sessions(); + assert!(!current.is_open(), "login failure must revoke before CLIENT is cleared"); + assert!(open("@bob:example.org").is_err()); + replace_client(Some(bob.clone())); + let relogged = open("@bob:example.org").unwrap(); + replace_client(None); + assert!(!relogged.is_open()); + assert!(open("@bob:example.org").is_err()); + } + } + #[test] fn standalone_mode_round_trips_without_secrets() { let mode = StandaloneMode::Remote { url: "https://octos.example".into(), profile: "me".into() }; diff --git a/src/octoscript_apps/mod.rs b/src/octoscript_apps/mod.rs index 8ebf5c12..87c21182 100644 --- a/src/octoscript_apps/mod.rs +++ b/src/octoscript_apps/mod.rs @@ -8,7 +8,7 @@ pub fn invalidate_sessions() { AUTHORITY.invalidate(); // Account change or logout: the assistant's contexts of the previous // account are revoked too (ADR 0007). - crate::octos_service::sync_account(); + crate::octos_service::revoke_account(); // And the assistant's waiting and running calls. crate::assistant::invalidate(); } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 856e8ac8..0496c63f 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -3085,6 +3085,16 @@ fn get_room_timeline(room_id: &RoomId) -> Option> { /// The logged-in Matrix client, which can be freely and cheaply cloned. static CLIENT: Mutex> = Mutex::new(None); +/// Revoke the old session before replacing it, then bind the assistant to +/// the newly installed account. Used for both fresh login and session restore. +pub(crate) fn replace_client(client: Option) -> Option { + crate::article_app::invalidate_sessions(); + crate::octoscript_apps::invalidate_sessions(); + let previous = std::mem::replace(&mut *CLIENT.lock().unwrap(), client); + crate::octos_service::sync_account(); + previous +} + pub fn get_client() -> Option { CLIENT.lock().unwrap().clone() } @@ -3503,9 +3513,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { enqueue_rooms_list_update(RoomsListUpdate::Status { status }); // Store this active client in our global Client state so that other tasks can access it. - crate::article_app::invalidate_sessions(); - crate::octoscript_apps::invalidate_sessions(); - if let Some(_existing) = CLIENT.lock().unwrap().replace(client.clone()) { + if let Some(_existing) = replace_client(Some(client.clone())) { error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); } @@ -3551,9 +3559,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg }); // Clear the stored client so the next login attempt doesn't trigger the // "unexpectedly replaced an existing client" warning. - crate::article_app::invalidate_sessions(); - crate::octoscript_apps::invalidate_sessions(); - let _ = CLIENT.lock().unwrap().take(); + let _ = replace_client(None); abort_and_await_handles(&mut subscriber_task_handles).await; continue 'login_loop; } @@ -3706,9 +3712,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } // No-ops if `clear_app_state` already cleared these. - crate::article_app::invalidate_sessions(); - crate::octoscript_apps::invalidate_sessions(); - let _ = CLIENT.lock().unwrap().take(); + let _ = replace_client(None); let _ = SYNC_SERVICE.lock().unwrap().take(); SYNC_SERVICE_ASSUMED_RUNNING.store(false, Ordering::Release); continue 'login_loop; @@ -5988,9 +5992,7 @@ impl UserPowerLevels { /// Keeps `REQUEST_SENDER` alive, and also the `matrix_worker_task /// which needs to keep running to receive the next login request. pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { - crate::article_app::invalidate_sessions(); - crate::octoscript_apps::invalidate_sessions(); - CLIENT.lock().unwrap().take(); + replace_client(None); SYNC_SERVICE.lock().unwrap().take(); SYNC_SERVICE_ASSUMED_RUNNING.store(false, Ordering::Release); set_blocked_users(HashSet::default()); From 11d794d6180a3a16728dc38cf80f8ff935dec887 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:21:08 -0700 Subject: [PATCH 6/9] fix: preserve Octos session results in mini-app callbacks --- src/octoscript_apps/octos.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/octoscript_apps/octos.rs b/src/octoscript_apps/octos.rs index e541e0fa..450b7cde 100644 --- a/src/octoscript_apps/octos.rs +++ b/src/octoscript_apps/octos.rs @@ -57,6 +57,12 @@ fn mini_app_value(data: &Value) -> Value { json!({"text": data.get("text").cloned().unwrap_or(Value::Null), "event": event}) } +fn completed_reply(result: Result) -> ServiceEvent { + // The broker already returns each service's public result. Wrapping all + // results in `text` hides session_id/messages from history callbacks. + ServiceEvent::Complete(result.and_then(rinx_miniapp_core::bounded_reply)) +} + fn forward(lease: Lease, reply: SyncSender) -> octosense_app_peers::EventSink { let reply = std::sync::Mutex::new(reply); Arc::new(move |event| { @@ -75,11 +81,7 @@ fn forward(lease: Lease, reply: SyncSender) -> octosense_app_peers Err(_) => return, } } - ContextEvent::Complete(result) => ServiceEvent::Complete( - result - .map(|v| json!({"text": v.get("text").cloned().unwrap_or(v.clone()), "turn_id": v.get("turn_id")})) - .and_then(rinx_miniapp_core::bounded_reply), - ), + ContextEvent::Complete(result) => completed_reply(result), }; if let Ok(reply) = reply.lock() { let _ = reply.try_send(event); @@ -146,6 +148,23 @@ impl OctosProvider for ContextProvider { mod tests { use super::*; + #[test] + fn service_callbacks_keep_session_history_and_turn_fields() { + for result in [ + json!({"open": true, "model": {"lane": "primary"}}), + json!({"session_id": "context-a", "messages": [{"role": "assistant", "content": "hello"}]}), + json!({"turn_id": "turn-a", "text": "hello"}), + ] { + let ServiceEvent::Complete(Ok(reply)) = completed_reply(Ok(result.clone())) else { + panic!("expected a successful service callback"); + }; + assert_eq!(reply, result); + } + assert!(matches!(completed_reply(Err("revoked".into())), ServiceEvent::Complete(Err(e)) if e == "revoked")); + let too_large = json!({"messages": ["x".repeat(rinx_miniapp_core::MAX_REPLY_BYTES)]}); + assert!(matches!(completed_reply(Ok(too_large)), ServiceEvent::Complete(Err(_)))); + } + #[test] fn approval_requests_keep_the_shape_the_native_prompt_reads() { let value = mini_app_value(&json!({ From 889452f5e928e095491a6dc0f08503a0bde2e140 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:30:39 -0700 Subject: [PATCH 7/9] docs: record signed-in OnePlus mini-app validation --- docs/adr/0007-host-owned-octos-app-peers.md | 26 ++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/adr/0007-host-owned-octos-app-peers.md b/docs/adr/0007-host-owned-octos-app-peers.md index 50301436..49f28b17 100644 --- a/docs/adr/0007-host-owned-octos-app-peers.md +++ b/docs/adr/0007-host-owned-octos-app-peers.md @@ -644,6 +644,26 @@ passes login, restore, account switch, login failure and logout for all three deployments. Rinx's service dependency follows OctoSense `c806889` and its renamed `octosense-kernel` crate. -Native assistant requests, consent dialogs and cross-app exchanges still -need verification with this fix. The validation packages do not replace -normal Rinx, Home or Bridge. +The signed-in test also found that the mini-app adapter wrapped every +completed result as `text`, hiding `session_id` and `messages` from history +callbacks. It now preserves the broker's result while retaining the reply +size limit; regression tests cover session, history, turn and error results. + +The follow-up OnePlus APK `2026092712` (OctoSense `0336018`, Rinx `11d794d6`) +includes both fixes and the merged shared shell from `c806889`. Native UI +checks pass for Matrix session restoration, profile access, Octos session +open/history, an AI reply, and a tool approval prompt whose Deny action +reaches the kernel. Closing during a delayed response returns to Discover +without a late UI update; reopening creates a new context and replies again. +System Back closes the mini-app; an edge swipe from Discover returns to the +launcher with the same host process. Process inspection finds exactly one +kernel child under that host. The standalone APK `2026092710` built from +the same Rinx source also starts and discovers matrix.org. + +AI replies use a temporary scripted provider with the real packaged Android +kernel. Its profile and its own ADB reverse tunnel are removed afterward. +No Matrix messages were sent and no room access was granted to the fixture. +These checks do not claim paid-provider inference, native cross-app agent +exchange, approval acceptance, or privileged production Home/Bridge +integration. The validation packages do not replace normal Rinx, Home or +Bridge. From e5a10dc57a88aa34979e7424c73a23018de7f847 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:56:17 -0700 Subject: [PATCH 8/9] build: follow rewritten OctoSense peer dependency history --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bdf7d5c..ffb11747 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7908,7 +7908,7 @@ dependencies = [ [[package]] name = "octosense-app-peers" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" dependencies = [ "futures-util", "log", @@ -7934,7 +7934,7 @@ dependencies = [ [[package]] name = "octosense-kernel" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" dependencies = [ "log", "octos-cli", @@ -7947,7 +7947,7 @@ dependencies = [ [[package]] name = "octosense-llm-config" version = "0.1.0" -source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=c8068891a3a454a3a9b5b02236971294973e5087#c8068891a3a454a3a9b5b02236971294973e5087" +source = "git+https://github.com/OctoSense-org/OctoSense.git?rev=35d9d1218e1b8b165977a4f019fdea9f2675dc90#35d9d1218e1b8b165977a4f019fdea9f2675dc90" dependencies = [ "argon2", "chacha20poly1305 0.10.1", diff --git a/Cargo.toml b/Cargo.toml index cd8588f3..9512508a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ uuid = { version = "1", features = ["v4"] } ## The assistant (ADR 0007): the scoped service contract a host injects. The ## local-runtime and remote connectors are optional (`octos-local`, ## `octos-remote`), so a hosted build links no runtime of its own. -octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "c8068891a3a454a3a9b5b02236971294973e5087" } +octosense-app-peers = { git = "https://github.com/OctoSense-org/OctoSense.git", rev = "35d9d1218e1b8b165977a4f019fdea9f2675dc90" } octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "68f6a9df55692b5d8ef8873a12721e279a3f40d6" } octoscript-makepad = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } octoscript-widgets = { git = "https://github.com/OctoSense-org/Octoscript-Makepad.git", rev = "6881fb6c3c3220e407633b0ba5211c3d42c7e625" } From 8a7b935bf3e636eaca2fa2b6f17865e36d1e6bbb Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:56:18 -0700 Subject: [PATCH 9/9] ui: integrate article editor SVG actions and compact confirmations --- resources/i18n/en.json | 18 +- resources/i18n/zh-CN.json | 18 +- resources/icons/article_bold.svg | 3 + resources/icons/article_bullet.svg | 3 + resources/icons/article_down.svg | 3 + resources/icons/article_heading.svg | 3 + resources/icons/article_italic.svg | 3 + resources/icons/article_numbered.svg | 3 + resources/icons/article_palette.svg | 3 + resources/icons/article_quote.svg | 3 + resources/icons/article_redo.svg | 3 + resources/icons/article_refresh.svg | 3 + resources/icons/article_rule.svg | 3 + resources/icons/article_save.svg | 3 + .../icons/article_spacing_comfortable.svg | 3 + resources/icons/article_spacing_compact.svg | 3 + resources/icons/article_split.svg | 3 + resources/icons/article_table.svg | 3 + resources/icons/article_type_large.svg | 3 + resources/icons/article_type_small.svg | 3 + resources/icons/article_undo.svg | 3 + resources/icons/article_unlink.svg | 3 + resources/icons/article_up.svg | 3 + resources/icons/article_width_full.svg | 3 + resources/icons/article_width_medium.svg | 3 + resources/icons/article_width_small.svg | 3 + src/article_app/ui.rs | 467 ++++++++++++------ 27 files changed, 433 insertions(+), 142 deletions(-) create mode 100644 resources/icons/article_bold.svg create mode 100644 resources/icons/article_bullet.svg create mode 100644 resources/icons/article_down.svg create mode 100644 resources/icons/article_heading.svg create mode 100644 resources/icons/article_italic.svg create mode 100644 resources/icons/article_numbered.svg create mode 100644 resources/icons/article_palette.svg create mode 100644 resources/icons/article_quote.svg create mode 100644 resources/icons/article_redo.svg create mode 100644 resources/icons/article_refresh.svg create mode 100644 resources/icons/article_rule.svg create mode 100644 resources/icons/article_save.svg create mode 100644 resources/icons/article_spacing_comfortable.svg create mode 100644 resources/icons/article_spacing_compact.svg create mode 100644 resources/icons/article_split.svg create mode 100644 resources/icons/article_table.svg create mode 100644 resources/icons/article_type_large.svg create mode 100644 resources/icons/article_type_small.svg create mode 100644 resources/icons/article_undo.svg create mode 100644 resources/icons/article_unlink.svg create mode 100644 resources/icons/article_up.svg create mode 100644 resources/icons/article_width_full.svg create mode 100644 resources/icons/article_width_medium.svg create mode 100644 resources/icons/article_width_small.svg diff --git a/resources/i18n/en.json b/resources/i18n/en.json index a4ec3e7d..ac3f2891 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -1086,5 +1086,21 @@ "Assistant access": "Assistant access", "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
": "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
", "The assistant may not read any chat.": "The assistant may not read any chat.", - "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "Chats this account lets the assistant read. Revoke one and the assistant asks again." + "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "Chats this account lets the assistant read. Revoke one and the assistant asks again.", + "Bold": "Bold", + "Bulleted list": "Bulleted list", + "Code": "Code", + "Heading": "Heading", + "Horizontal rule": "Horizontal rule", + "Italic": "Italic", + "Numbered list": "Numbered list", + "Quote": "Quote", + "Undo": "Undo", + "Redo": "Redo", + "Move up": "Move up", + "Move down": "Move down", + "Remove block": "Remove block", + "Full width": "Full width", + "Medium width": "Medium width", + "Small width": "Small width" } diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 65d8b324..001379df 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -1086,5 +1086,21 @@ "Assistant access": "助理访问权限", "
  • Chats this account lets the assistant read. You chose \"Always allow\" for each; revoke one and the assistant asks again.
": "
  • 此账号允许助理读取的聊天。每一个都是你选择了“始终允许”;撤销后,助理会再次询问。
", "The assistant may not read any chat.": "助理不能读取任何聊天。", - "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "此账号允许助理读取的聊天。撤销后,助理会再次询问。" + "Chats this account lets the assistant read. Revoke one and the assistant asks again.": "此账号允许助理读取的聊天。撤销后,助理会再次询问。", + "Bold": "粗体", + "Bulleted list": "无序列表", + "Code": "代码", + "Heading": "标题", + "Horizontal rule": "分隔线", + "Italic": "斜体", + "Numbered list": "有序列表", + "Quote": "引用", + "Undo": "撤销", + "Redo": "重做", + "Move up": "上移", + "Move down": "下移", + "Remove block": "删除内容块", + "Full width": "全宽", + "Medium width": "中等宽度", + "Small width": "小宽度" } diff --git a/resources/icons/article_bold.svg b/resources/icons/article_bold.svg new file mode 100644 index 00000000..4273df17 --- /dev/null +++ b/resources/icons/article_bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_bullet.svg b/resources/icons/article_bullet.svg new file mode 100644 index 00000000..1510c9cf --- /dev/null +++ b/resources/icons/article_bullet.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_down.svg b/resources/icons/article_down.svg new file mode 100644 index 00000000..b51ee7a5 --- /dev/null +++ b/resources/icons/article_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_heading.svg b/resources/icons/article_heading.svg new file mode 100644 index 00000000..9ca6f571 --- /dev/null +++ b/resources/icons/article_heading.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_italic.svg b/resources/icons/article_italic.svg new file mode 100644 index 00000000..c6357e8c --- /dev/null +++ b/resources/icons/article_italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_numbered.svg b/resources/icons/article_numbered.svg new file mode 100644 index 00000000..13499b3b --- /dev/null +++ b/resources/icons/article_numbered.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_palette.svg b/resources/icons/article_palette.svg new file mode 100644 index 00000000..182672bb --- /dev/null +++ b/resources/icons/article_palette.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_quote.svg b/resources/icons/article_quote.svg new file mode 100644 index 00000000..baee825a --- /dev/null +++ b/resources/icons/article_quote.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_redo.svg b/resources/icons/article_redo.svg new file mode 100644 index 00000000..16efa692 --- /dev/null +++ b/resources/icons/article_redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_refresh.svg b/resources/icons/article_refresh.svg new file mode 100644 index 00000000..b20296e0 --- /dev/null +++ b/resources/icons/article_refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_rule.svg b/resources/icons/article_rule.svg new file mode 100644 index 00000000..5c86ff23 --- /dev/null +++ b/resources/icons/article_rule.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_save.svg b/resources/icons/article_save.svg new file mode 100644 index 00000000..cae0ff27 --- /dev/null +++ b/resources/icons/article_save.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_spacing_comfortable.svg b/resources/icons/article_spacing_comfortable.svg new file mode 100644 index 00000000..70df7349 --- /dev/null +++ b/resources/icons/article_spacing_comfortable.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_spacing_compact.svg b/resources/icons/article_spacing_compact.svg new file mode 100644 index 00000000..1b4461bd --- /dev/null +++ b/resources/icons/article_spacing_compact.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_split.svg b/resources/icons/article_split.svg new file mode 100644 index 00000000..dc7c66d9 --- /dev/null +++ b/resources/icons/article_split.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_table.svg b/resources/icons/article_table.svg new file mode 100644 index 00000000..213e6867 --- /dev/null +++ b/resources/icons/article_table.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_type_large.svg b/resources/icons/article_type_large.svg new file mode 100644 index 00000000..fb09c7d7 --- /dev/null +++ b/resources/icons/article_type_large.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_type_small.svg b/resources/icons/article_type_small.svg new file mode 100644 index 00000000..29c2088c --- /dev/null +++ b/resources/icons/article_type_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_undo.svg b/resources/icons/article_undo.svg new file mode 100644 index 00000000..0fc4011f --- /dev/null +++ b/resources/icons/article_undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_unlink.svg b/resources/icons/article_unlink.svg new file mode 100644 index 00000000..d8c386ff --- /dev/null +++ b/resources/icons/article_unlink.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_up.svg b/resources/icons/article_up.svg new file mode 100644 index 00000000..8d0f5830 --- /dev/null +++ b/resources/icons/article_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_full.svg b/resources/icons/article_width_full.svg new file mode 100644 index 00000000..4678e17a --- /dev/null +++ b/resources/icons/article_width_full.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_medium.svg b/resources/icons/article_width_medium.svg new file mode 100644 index 00000000..d38bd3d7 --- /dev/null +++ b/resources/icons/article_width_medium.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/article_width_small.svg b/resources/icons/article_width_small.svg new file mode 100644 index 00000000..c9aaa1bd --- /dev/null +++ b/resources/icons/article_width_small.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/article_app/ui.rs b/src/article_app/ui.rs index 0ee18b46..db33b7d8 100644 --- a/src/article_app/ui.rs +++ b/src/article_app/ui.rs @@ -111,12 +111,27 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* mod.widgets.ArticleLabel = Label {width: Fill height: Fit flow: Flow.Right{wrap: true} draw_text +: {color: #x191919 text_style: theme.font_regular{font_size: 13}}} - mod.widgets.ArticleButton = RobrixNeutralIconButton {grab_key_focus: false height: 42 margin: 0 padding: Inset{left: 12 right: 12} draw_text +: {color: #x333333 text_style: theme.font_regular{font_size: 12}} draw_bg +: {color: #xffffff color_hover: #xf2f5f3 color_down: #xe7f2e9 border_radius: 6}} - mod.widgets.ArticlePrimary = mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_text +: {color: #xffffff color_hover: #xffffff color_down: #xffffff}} + mod.widgets.ArticleButton = RobrixNeutralIconButton { + grab_key_focus: false enable_long_press: true + width: 44 height: 44 margin: 0 padding: 0 spacing: 0 text: "" + align: Align{x: 0.5 y: 0.5} + icon_walk: Walk{width: 20 height: 20} + draw_icon +: {color: #x333333} + draw_bg +: {color: #xffffff color_hover: #xf2f5f3 color_down: #xe7f2e9 border_radius: 6} + } + mod.widgets.ArticlePrimary = mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_icon +: {color: #xffffff}} + mod.widgets.ArticleActions = View {width: Fill height: 44 flow: Right align: Align{x: 1.0 y: 0.5} spacing: 12} mod.widgets.ArticleInput = TextInput {width: Fill height: 44 draw_cursor +: {color: #x07c160} draw_bg +: {color: #xffffff color_hover: #xffffff color_focus: #xffffff color_empty: #xffffff border_color: #xe5e5e5 border_color_focus: #x07c160} draw_text +: {color: #x191919 color_focus: #x191919 color_hover: #x191919 text_style: theme.font_regular{font_size: 14}}} mod.widgets.ArticleHtml = Html {selectable: true width: Fill height: Fit padding: 0 font_size: 14 font_color: #x191919 draw_text.color: #x191919 text_style_normal: theme.font_regular{font_size: 14} text_style_bold: theme.font_bold{font_size: 14} text_style_italic: theme.font_italic{font_size: 14} text_style_bold_italic: theme.font_bold_italic{font_size: 14} rmath := ArticleMath {} rdiagram := ArticleDiagram {} rimage := ArticleImage {} remoji := ArticleEmoji {} rcode := ArticleCode {} rcell := ArticleCell {}} mod.widgets.ArticleRich = ArticleRichInput {width: Fill height: Fit draw_cursor +: {color: #x07c160} is_multiline: true flow: Flow.Right{wrap: true} padding: Inset{top: 4 bottom: 12 left: 0 right: 0} empty_text: #(crate::i18n::tr("Write your article…")) draw_bg +: {color: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000 color_empty: #x00000000 border_size: 0} draw_text +: {color: #x191919 color_hover: #x191919 color_focus: #x191919 text_style: theme.font_regular{font_size: 14}} draw_bold.text_style: theme.font_bold{font_size: 14} draw_italic.text_style: theme.font_italic{font_size: 14} draw_bold_italic.text_style: theme.font_bold_italic{font_size: 14}} - mod.widgets.ArticleTool = mod.widgets.ArticleButton {width: 34 height: 32 padding: 0 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #x00000000 color_hover: #xf0f0f0 color_down: #xe6e6e6 border_size: 0}} + mod.widgets.ArticleIconButton = mod.widgets.ArticleButton { + width: 44 height: 44 padding: 0 spacing: 0 text: "" + align: Align{x: 0.5 y: 0.5} + enable_long_press: true + icon_walk: Walk{width: 20 height: 20} + draw_icon +: {color: #x333333} + } + mod.widgets.ArticleTool = mod.widgets.ArticleIconButton {width: 36 height: 36 draw_bg +: {color: #x00000000 color_hover: #xf0f0f0 color_down: #xe6e6e6 border_size: 0}} // A compact theme swatch for the writing view's style panel: a tiny page with the // theme's paper, ink and accent, and the theme's name below. mod.widgets.ArticleThemeSwatch = NavigationBarButton {width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 0.0} padding: 0 spacing: 6 @@ -149,10 +164,11 @@ script_mod! { } mod.widgets.ArticlePanel = #(ArticlePanel::register_widget(vm)) { ..mod.widgets.SolidView + icon_tooltip: CalloutTooltip {} width: Fill height: Fill flow: Down draw_bg.color: #xffffff padding: Inset{top: mod.widgets.SAFE_INSET_PAD_TOP + #(TOP) bottom: mod.widgets.SAFE_INSET_PAD_BOTTOM} header := SolidView {width: Fill height: 52 flow: Right align: Align{y: 0.5} padding: Inset{left: 8 right: 12} spacing: 8 draw_bg.color: #xededed - article_back := RobrixNeutralIconButton {width: 36 height: 44 margin: 0 padding: 12 spacing: 0 draw_bg +: {color: #x00000000 color_hover: #x00000000 color_down: #x00000000 border_size: 0} draw_icon +: {svg: ICON_CHEVRON_LEFT color: #x191919} icon_walk: Walk{width: 8 height: 14}} + article_back := RobrixNeutralIconButton {enable_long_press: true width: 36 height: 44 margin: 0 padding: 12 spacing: 0 draw_bg +: {color: #x00000000 color_hover: #x00000000 color_down: #x00000000 border_size: 0} draw_icon +: {svg: ICON_CHEVRON_LEFT color: #x191919} icon_walk: Walk{width: 8 height: 14}} article_heading := mod.widgets.ArticleLabel {width: Fit max_lines: 1 draw_text.text_style: theme.font_bold{font_size: 16}} header_fill := View {width: Fill height: Fill} // The writing view's article title, centred in the header as in the atlas. @@ -161,20 +177,20 @@ script_mod! { } // Writing view controls, as in the redesigned editor atlas. write_controls := View {visible: false width: Fit height: Fill flow: Right align: Align{y: 0.5} spacing: 8 - write_style := mod.widgets.ArticleButton {height: 32 draw_text +: {color: #x07a858} draw_bg +: {border_size: 1.0 border_color: #x9fdcb8}} - RoundedView {width: Fit height: 32 flow: Right padding: 2 spacing: 0 draw_bg +: {color: #xffffff border_radius: 6.0} - write_mode_source := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Edit")) i18n_text: "Edit" draw_bg +: {border_radius: 5}} - write_mode_split := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Split")) i18n_text: "Split" draw_bg +: {border_radius: 5}} - write_mode_preview := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Preview")) i18n_text: "Preview" draw_bg +: {border_radius: 5}} + write_style := mod.widgets.ArticleIconButton {draw_icon.color: #x07a858 draw_bg +: {border_size: 1.0 border_color: #x9fdcb8} draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} + RoundedView {width: Fit height: 48 flow: Right padding: 2 spacing: 0 draw_bg +: {color: #xffffff border_radius: 6.0} + write_mode_source := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_EDIT}} + write_mode_split := mod.widgets.ArticleIconButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_split.svg")}} + write_mode_preview := mod.widgets.ArticleIconButton {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } write_saved := mod.widgets.ArticleLabel {width: Fit draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 11}}} - write_publish := mod.widgets.ArticlePrimary {width: Fit height: 32 padding: Inset{left: 16 right: 16} text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + write_publish := mod.widgets.ArticleIconButton {draw_icon.color: #xffffff draw_bg +: {color: #x07c160 color_hover: #x06ad56 color_down: #x05984b} draw_icon +: {svg: ICON_SEND}} } - article_save := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Save")) i18n_text: "Save" visible: false} - article_done := mod.widgets.ArticleButton {visible: false text: #(crate::i18n::tr("Done")) i18n_text: "Done" draw_text.color: #x07a858} - article_close := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Close")) i18n_text: "Close"} + article_save := mod.widgets.ArticleIconButton {visible: false draw_icon +: {svg: crate_resource("self://resources/icons/article_save.svg")}} + article_done := mod.widgets.ArticleIconButton {visible: false draw_icon.color: #x07a858 draw_icon +: {svg: ICON_CHECKMARK}} + article_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} // Library: a new article, as the atlas's + in the header. - article_new := mod.widgets.ArticleTool {visible: false width: 40 height: 40 text: "+" draw_text +: {color: #x07a858 text_style: theme.font_regular{font_size: 24}}} + article_new := mod.widgets.ArticleIconButton {visible: false draw_icon.color: #x07a858 draw_icon +: {svg: ICON_ADD}} } details := View {width: Fill height: Fill flow: Down padding: 28 spacing: 24 // The text scrolls on short windows; the actions stay in view. @@ -186,7 +202,7 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Write visually. Add images, choose a theme, review and publish.")) i18n_text: "Write visually. Add images, choose a theme, review and publish."} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Sharing this app shares neither your drafts nor your account permissions.")) i18n_text: "Sharing this app shares neither your drafts nor your account permissions." draw_text.color: #x777777} } - article_continue := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue with Rinx")) i18n_text: "Continue with Rinx"} + article_continue := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHEVRON_RIGHT}} } consent := View {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 24 // The text scrolls on short windows; the actions stay in view. @@ -196,15 +212,17 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("• Save your drafts and selected images on this device\n\n• Upload images and publish only after confirmation\n\n• Update or withdraw your own articles after confirmation")) i18n_text: "• Save your drafts and selected images on this device\n\n• Upload images and publish only after confirmation\n\n• Update or withdraw your own articles after confirmation"} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your password and session token stay with Rinx. Permission expires when you close this app or after one hour.")) i18n_text: "Your password and session token stay with Rinx. Permission expires when you close this app or after one hour." draw_text.color: #x777777} } - article_allow := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Allow and open")) i18n_text: "Allow and open"} - article_cancel := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel"} + mod.widgets.ArticleActions { + article_cancel := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_CLOSE}} + article_allow := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_icon +: {svg: ICON_CHECKMARK}} + } } library := SolidView {visible: false width: Fill height: Fill flow: Down padding: 16 spacing: 12 draw_bg.color: #xffffff article_search := mod.widgets.ArticleInput {draw_bg +: {color: #xf5f5f5 color_hover: #xf5f5f5 color_empty: #xf5f5f5 border_size: 0} empty_text: #(crate::i18n::tr("Search articles")) i18n_empty_text: "Search articles"} - View {width: Fill height: 42 flow: Right spacing: 6 - drafts_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Drafts")) i18n_text: "Drafts"} - published_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Published")) i18n_text: "Published"} - withdrawn_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Withdrawn")) i18n_text: "Withdrawn"} + View {width: Fill height: 44 flow: Right spacing: 6 + drafts_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_EDIT}} + published_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_SEND}} + withdrawn_tab := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_TRASH}} } library_empty := mod.widgets.ArticleLabel {text: #(crate::i18n::tr("No articles here yet. Create your first article.")) i18n_text: "No articles here yet. Create your first article." draw_text.color: #x777777} article_library := PortalList {width: Fill height: Fill @@ -222,39 +240,39 @@ script_mod! { Label {text: "›" draw_text +: {color: #xb0b0b0 text_style: theme.font_regular{font_size: 20}}} } } - article_import := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} text: #(crate::i18n::tr("Import Markdown or HTML file")) i18n_text: "Import Markdown or HTML file"} - article_share := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill text: #(crate::i18n::tr("Share app")) i18n_text: "Share app"} + article_import := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} draw_icon +: {svg: ICON_IMPORT}} + article_share := mod.widgets.ArticleButton {draw_bg +: {color: #xf5f5f5} width: Fill draw_icon +: {svg: ICON_SHARE}} } editor := View {visible: false width: Fill height: Fill flow: Right align: Align{x: 0.5} editor_sidebar := View {visible: false width: 230 height: Fill flow: Down padding: 18 spacing: 18 - sidebar_new := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("New article")) i18n_text: "New article"} - sidebar_library := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("My articles")) i18n_text: "My articles"} + sidebar_new := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_ADD}} + sidebar_library := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_FILE}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Article outline")) i18n_text: "Article outline" draw_text.text_style: theme.font_bold{font_size: 14}} outline := mod.widgets.ArticleLabel {draw_text.color: #x777777} View {width: Fill height: Fill} - sidebar_share := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Share app")) i18n_text: "Share app"} + sidebar_share := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_SHARE}} } editor_paper := SolidView {width: Fill height: Fill flow: Down padding: Inset{left: 20 right: 20 top: 16 bottom: 12} spacing: 8 draw_bg.color: #xffffff article_title := mod.widgets.ArticleInput {height: 52 empty_text: #(crate::i18n::tr("Article title")) i18n_empty_text: "Article title" draw_text.text_style: theme.font_bold{font_size: 18} draw_bg +: {border_size: 0 color: #x00000000 color_empty: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000}} article_author := mod.widgets.ArticleInput {height: 34 empty_text: #(crate::i18n::tr("Author")) i18n_empty_text: "Author" draw_bg +: {border_size: 0 color: #x00000000 color_empty: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_down: #x00000000} draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 12}}} - format_bar := ScrollXView {width: Fill height: 42 flow: Right spacing: 4 - article_bold := mod.widgets.ArticleButton {text: "B" width: 38 draw_text.text_style: theme.font_bold{font_size: 14}} - article_italic := mod.widgets.ArticleButton {text: "I" width: 38 draw_text.text_style: theme.font_italic{font_size: 14}} - article_link := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Link")) i18n_text: "Link"} - article_h2 := mod.widgets.ArticleButton {text: "H2" width: 42} - article_quote := mod.widgets.ArticleButton {text: "❝" width: 38} - article_list := mod.widgets.ArticleButton {text: "•" width: 38} - article_undo := mod.widgets.ArticleButton {text: "↶" width: 38} - article_redo := mod.widgets.ArticleButton {text: "↷" width: 38} - block_up := mod.widgets.ArticleButton {text: "↑" width: 38} - block_down := mod.widgets.ArticleButton {text: "↓" width: 38} - block_remove := mod.widgets.ArticleButton {text: "−" width: 38} + format_bar := ScrollXView {width: Fill height: 44 flow: Right spacing: 4 + article_bold := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + article_italic := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + article_link := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_LINK}} + article_h2 := mod.widgets.ArticleButton { width: 42 draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + article_quote := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + article_list := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + article_undo := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_undo.svg")}} + article_redo := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_redo.svg")}} + block_up := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_up.svg")}} + block_down := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: crate_resource("self://resources/icons/article_down.svg")}} + block_remove := mod.widgets.ArticleButton { width: 38 draw_icon +: {svg: ICON_TRASH}} } article_blocks := PortalList {width: Fill height: Fill SourceBlock := View {width: Fill height: Fit flow: Down spacing: 6 padding: Inset{top: 8 bottom: 12} rendered := View {width: Fill height: Fit body := mod.widgets.ArticleHtml {}} source_editor := View {visible: false width: Fill height: Fit rich := mod.widgets.ArticleRich {}} - source_toggle := mod.widgets.ArticleButton {height: 28 text: #(crate::i18n::tr("Edit source")) draw_text.text_style: theme.font_regular{font_size: 11}} + source_toggle := mod.widgets.ArticleButton {height: 28 draw_icon +: {svg: ICON_VIEW_SOURCE}} } TextBlock := View {width: Fill height: Fit flow: Right spacing: 8 prefix := Label {visible: false width: 16 height: Fit padding: Inset{top: 5} draw_text +: {color: #x07a858 text_style: theme.font_regular{font_size: 14}}} @@ -263,29 +281,29 @@ script_mod! { Picture := View {width: Fill height: Fit flow: Down spacing: 6 padding: Inset{top: 8 bottom: 12} picture := Image {width: Fill height: 210 fit: ImageFit.Smallest} caption := mod.widgets.ArticleLabel {draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 11}}} - image_settings := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Image settings")) i18n_text: "Image settings"} + image_settings := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_SETTINGS}} } Rule := View {width: Fill height: 30 align: Align{y: 0.5} SolidView {width: Fill height: 1 draw_bg.color: #xdddddd}} } article_stats := mod.widgets.ArticleLabel {draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 10}}} ScrollXView {width: Fill height: 44 flow: Right spacing: 6 - article_add_text := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Text")) i18n_text: "Text"} - article_images := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Images")) i18n_text: "Images"} - article_theme := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Style")) i18n_text: "Style"} - article_cover := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Cover")) i18n_text: "Cover"} - article_source := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Source")) i18n_text: "Source"} + article_add_text := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + article_images := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_ADD_PHOTO}} + article_theme := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} + article_cover := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_ADD_PHOTO}} + article_source := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_VIEW_SOURCE}} } - article_preview := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Full preview")) i18n_text: "Full preview"} + article_preview := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } editor_inspector := View {visible: false width: 260 height: Fill flow: Down padding: 18 spacing: 20 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 16}}} - inspector_theme := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Choose theme")) i18n_text: "Choose theme"} + inspector_theme := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} inspector_html := mod.widgets.ArticleHtml {} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover preview")) i18n_text: "Cover preview"} inspector_cover := Image {width: Fill height: 120 fit: ImageFit.Biggest} - inspector_cover_button := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cover and summary")) i18n_text: "Cover and summary"} + inspector_cover_button := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} View {width: Fill height: Fill} - inspector_preview := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Full preview")) i18n_text: "Full preview"} + inspector_preview := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } } writer := View {visible: false width: Fill height: Fill flow: Overlay @@ -298,18 +316,18 @@ script_mod! { write_title_small_box := View {visible: false width: Fill height: Fit padding: Inset{left: 12 right: 12 top: 12} write_title_small := mod.widgets.ArticleInput {height: 48 empty_text: #(crate::i18n::tr("Article title")) i18n_empty_text: "Article title" draw_text.text_style: theme.font_bold{font_size: 16}} } - write_toolbar := View {width: Fill height: 40 flow: Right align: Align{y: 0.5} padding: Inset{left: 10 right: 10} spacing: 0 - wt_bold := mod.widgets.ArticleTool {text: "B" draw_text.text_style: theme.font_bold{font_size: 13}} - wt_italic := mod.widgets.ArticleTool {text: "I" draw_text.text_style: theme.font_italic{font_size: 13}} - wt_heading := mod.widgets.ArticleTool {text: "H" draw_text.text_style: theme.font_bold{font_size: 13}} - wt_quote := mod.widgets.ArticleTool {text: "❝" } - wt_bullet := mod.widgets.ArticleTool {text: "•" } - wt_numbered := mod.widgets.ArticleTool {text: "1." } - wt_link := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Link")) i18n_text: "Link"} - wt_image := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Image")) i18n_text: "Image"} - wt_code := mod.widgets.ArticleTool {text: "" width: 40} - wt_table := mod.widgets.ArticleTool {text: "⊞" } - wt_rule := mod.widgets.ArticleTool {text: "—" } + write_toolbar := View {width: Fill height: 44 flow: Right align: Align{y: 0.5} padding: Inset{left: 10 right: 10} spacing: 0 + wt_bold := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + wt_italic := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + wt_heading := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + wt_quote := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + wt_bullet := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + wt_numbered := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_numbered.svg")}} + wt_link := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_LINK}} + wt_image := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_ADD_PHOTO}} + wt_code := mod.widgets.ArticleTool {draw_icon +: {svg: ICON_VIEW_SOURCE}} + wt_table := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_table.svg")}} + wt_rule := mod.widgets.ArticleTool {draw_icon +: {svg: crate_resource("self://resources/icons/article_rule.svg")}} } SolidView {width: Fill height: 1 draw_bg.color: #xe5e5e5} write_source := mod.widgets.ArticleInput {height: Fill is_multiline: true flow: Flow.Right{wrap: true} padding: Inset{left: 16 right: 16 top: 12 bottom: 12} empty_text: #(crate::i18n::tr("Write Markdown here…")) i18n_empty_text: "Write Markdown here…" draw_text +: {text_style: theme.font_regular{font_size: 13 line_spacing: 1.5}} draw_bg +: {border_size: 0 color_focus: #xffffff}} @@ -317,14 +335,14 @@ script_mod! { write_bottom := View {visible: false width: Fill height: Fit flow: Down padding: Inset{left: 12 right: 12 bottom: 8} spacing: 6 write_saved_small := mod.widgets.ArticleLabel {draw_text +: {color: #x999999 text_style: theme.font_regular{font_size: 11}}} RoundedView {width: Fill height: 44 flow: Right align: Align{x: 0.5 y: 0.5} padding: Inset{left: 6 right: 6} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 6.0} - wb_bold := mod.widgets.ArticleTool {width: Fill text: "B" draw_text.text_style: theme.font_bold{font_size: 14}} - wb_italic := mod.widgets.ArticleTool {width: Fill text: "I" draw_text.text_style: theme.font_italic{font_size: 14}} - wb_heading := mod.widgets.ArticleTool {width: Fill text: "H" draw_text.text_style: theme.font_bold{font_size: 14}} - wb_quote := mod.widgets.ArticleTool {width: Fill text: "❝"} - wb_bullet := mod.widgets.ArticleTool {width: Fill text: "•"} - wb_table := mod.widgets.ArticleTool {width: Fill text: "⊞"} - wb_image := mod.widgets.ArticleTool {width: Fill text: #(crate::i18n::tr("Image")) i18n_text: "Image"} - wb_link := mod.widgets.ArticleTool {width: Fill text: #(crate::i18n::tr("Link")) i18n_text: "Link"} + wb_bold := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_bold.svg")}} + wb_italic := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_italic.svg")}} + wb_heading := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_heading.svg")}} + wb_quote := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_quote.svg")}} + wb_bullet := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_bullet.svg")}} + wb_table := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_table.svg")}} + wb_image := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} + wb_link := mod.widgets.ArticleTool {width: Fill draw_icon +: {svg: ICON_LINK}} } } } @@ -353,9 +371,9 @@ script_mod! { } } write_themes_panel := SolidView {visible: false width: 320 height: Fill flow: Down padding: 16 spacing: 10 draw_bg.color: #xffffff - View {width: Fill height: 32 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text.text_style: theme.font_bold{font_size: 14}} - write_themes_close := mod.widgets.ArticleButton {text: "×" width: 32 height: 32} + write_themes_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } write_theme_list := PortalList {width: Fill height: Fill Swatches := View {width: Fill height: 108 flow: Right spacing: 10 padding: Inset{bottom: 12} @@ -378,15 +396,15 @@ script_mod! { } // Phone preview: opens the style sheet. write_style_fab_box := View {visible: false width: Fill height: Fill align: Align{x: 1.0 y: 1.0} padding: Inset{right: 16 bottom: 64} - write_style_fab := mod.widgets.ArticleButton {width: Fit height: 40 padding: Inset{left: 16 right: 16} draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 13}} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #x9fdcb8 border_radius: 20.0}} + write_style_fab := mod.widgets.ArticleIconButton {draw_icon.color: #x07a858 draw_bg +: {border_size: 1.0 border_color: #x9fdcb8 border_radius: 22.0} draw_icon +: {svg: crate_resource("self://resources/icons/article_palette.svg")}} } // Phone style sheet (atlas: theme-sheet). Tapping a style previews it; 应用 keeps it, × restores the previous one. write_theme_sheet := SolidView {visible: false width: Fill height: Fill flow: Down align: Align{y: 1.0} draw_bg.color: #x00000055 RoundedView {width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 8 bottom: 20} spacing: 12 draw_bg +: {color: #xffffff border_radius: 12.0} View {width: Fill height: Fit align: Align{x: 0.5} RoundedView {width: 36 height: 4 draw_bg +: {color: #xdddddd border_radius: 2.0}}} - View {width: Fill height: 30 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Article style")) i18n_text: "Article style" draw_text.text_style: theme.font_bold{font_size: 16}} - theme_sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + theme_sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } ScrollXView {width: Fill height: 108 flow: Right spacing: 10 s0 := mod.widgets.ArticleThemeSwatch {width: 68} @@ -402,22 +420,19 @@ script_mod! { s10 := mod.widgets.ArticleThemeSwatch {width: 68} s11 := mod.widgets.ArticleThemeSwatch {width: 68} } - theme_sheet_apply := mod.widgets.ArticlePrimary {height: 46 text: #(crate::i18n::tr("Apply")) i18n_text: "Apply"} + theme_sheet_apply := mod.widgets.ArticlePrimary {height: 46 draw_icon +: {svg: ICON_CHECKMARK}} } } // Phone insert-image sheet (atlas: insert-image). Camera and clipboard are not offered: Rinx cannot capture or paste images here yet. write_image_sheet := SolidView {visible: false width: Fill height: Fill flow: Down align: Align{y: 1.0} draw_bg.color: #x00000055 RoundedView {width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 8 bottom: 24} spacing: 12 draw_bg +: {color: #xffffff border_radius: 12.0} View {width: Fill height: Fit align: Align{x: 0.5} RoundedView {width: 36 height: 4 draw_bg +: {color: #xdddddd border_radius: 2.0}}} - View {width: Fill height: 30 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Insert image")) i18n_text: "Insert image" draw_text.text_style: theme.font_bold{font_size: 16}} - image_sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + image_sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } RoundedView {width: Fill height: Fit flow: Down draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 8.0} - image_sheet_pick := NavigationBarButton {width: Fill height: 56 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 16} spacing: 12 draw_bg +: {color_hover: #xf5f5f5 color_active: #xf5f5f5} - mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Choose from device")) i18n_text: "Choose from device" draw_text.text_style: theme.font_regular{font_size: 14}} - Label {text: "›" draw_text +: {color: #xb0b0b0 text_style: theme.font_regular{font_size: 20}}} - } + image_sheet_pick := mod.widgets.ArticleIconButton {width: Fill height: 56 draw_icon +: {svg: ICON_ADD_PHOTO}} } RoundedView {width: Fill height: 56 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 8} draw_bg +: {color: #xffffff border_size: 1.0 border_color: #xe5e5e5 border_radius: 8.0} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Lay out multiple images automatically")) i18n_text: "Lay out multiple images automatically" draw_text.text_style: theme.font_regular{font_size: 14}} @@ -435,13 +450,13 @@ script_mod! { // Desktop publish sheet (atlas: publish-sheet): cover, summary and chat, then 发布. write_sheet := SolidView {visible: false width: Fill height: Fill align: Align{x: 0.5 y: 0.5} draw_bg.color: #x00000055 RoundedView {width: 520 height: Fit flow: Down padding: Inset{left: 22 right: 22 top: 18 bottom: 18} spacing: 8 draw_bg +: {color: #xffffff border_radius: 8.0} - View {width: Fill height: 32 flow: Right align: Align{y: 0.5} + View {width: Fill height: 44 flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Publish article")) i18n_text: "Publish article" draw_text.text_style: theme.font_bold{font_size: 16}} - sheet_close := mod.widgets.ArticleTool {text: "×" draw_text.text_style: theme.font_regular{font_size: 18}} + sheet_close := mod.widgets.ArticleIconButton {draw_icon +: {svg: ICON_CLOSE}} } View {width: Fill height: Fit flow: Right align: Align{y: 0.5} mod.widgets.ArticleLabel {width: Fill text: #(crate::i18n::tr("Cover")) i18n_text: "Cover" draw_text.text_style: theme.font_bold{font_size: 12}} - sheet_cover_change := mod.widgets.ArticleTool {width: Fit padding: Inset{left: 8 right: 8} text: #(crate::i18n::tr("Change")) i18n_text: "Change" draw_text.color: #x07a858} + sheet_cover_change := mod.widgets.ArticleTool {width: 44 padding: Inset{left: 8 right: 8} draw_icon.color: #x07a858 draw_icon +: {svg: ICON_ADD_PHOTO}} } sheet_cover := Image {width: Fill height: 150 fit: ImageFit.Biggest} sheet_no_cover := mod.widgets.ArticleLabel {text: #(crate::i18n::tr("This article will be published without a cover.")) i18n_text: "This article will be published without a cover." draw_text.color: #x888888} @@ -454,8 +469,8 @@ script_mod! { } } View {width: Fill height: 44 flow: Right spacing: 12 margin: Inset{top: 6} - sheet_cancel := mod.widgets.ArticleButton {width: Fill height: 44 align: Align{x: 0.5 y: 0.5} text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel" draw_bg +: {color: #xf2f2f2}} - sheet_publish := mod.widgets.ArticlePrimary {width: Fill height: 44 text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + sheet_cancel := mod.widgets.ArticleButton {width: Fill height: 44 align: Align{x: 0.5 y: 0.5} draw_bg +: {color: #xf2f2f2} draw_icon +: {svg: ICON_CLOSE}} + sheet_publish := mod.widgets.ArticlePrimary {width: Fill height: 44 draw_icon +: {svg: ICON_SEND}} } } } @@ -466,10 +481,10 @@ script_mod! { source_report := ScrollYView {visible: false width: Fill height: 160 flow: Down source_issues := mod.widgets.ArticleLabel {draw_text +: {color: #x996020 text_style: theme.font_regular{font_size: 12}}} } - source_apply := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Apply and return to visual editing")) i18n_text: "Apply and return to visual editing"} + source_apply := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } images := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 14 - image_pick := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Choose from device")) i18n_text: "Choose from device"} + image_pick := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_ADD_PHOTO}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Selected images stay on this device until you confirm publication.")) i18n_text: "Selected images stay on this device until you confirm publication." draw_text.color: #x777777} image_library := PortalList {width: Fill height: Fill Asset := NavigationBarButton {width: Fill height: 130 flow: Right spacing: 14 padding: 10 @@ -480,22 +495,22 @@ script_mod! { } link_page := View {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 article_link_url := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("HTTPS link")) i18n_empty_text: "HTTPS link"} - link_apply := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Apply link")) i18n_text: "Apply link"} - link_remove := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Remove link")) i18n_text: "Remove link"} + link_apply := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_LINK}} + link_remove := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_unlink.svg")}} } image_settings := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 selected_image := Image {width: Fill height: 190 fit: ImageFit.Smallest} image_caption := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Caption")) i18n_empty_text: "Caption"} image_alt := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Alternative text")) i18n_empty_text: "Alternative text"} View {width: Fill height: 44 flow: Right spacing: 8 - image_full := mod.widgets.ArticleButton {width: Fill text: "100%"} - image_medium := mod.widgets.ArticleButton {width: Fill text: "75%"} - image_small := mod.widgets.ArticleButton {width: Fill text: "50%"} + image_full := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_full.svg")}} + image_medium := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_medium.svg")}} + image_small := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_width_small.svg")}} } - image_replace := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Replace image")) i18n_text: "Replace image"} - image_remove := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Remove image")) i18n_text: "Remove image" draw_text.color: #xe34d4d} + image_replace := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_ADD_PHOTO}} + image_remove := mod.widgets.ArticleButton {width: Fill draw_icon.color: #xe34d4d draw_icon +: {svg: ICON_TRASH}} View {width: Fill height: Fill} - image_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + image_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } themes := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 theme_list := PortalList {width: Fill height: Fill @@ -505,35 +520,35 @@ script_mod! { } } View {width: Fill height: 44 flow: Right spacing: 8 - size_normal := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Standard type")) i18n_text: "Standard type"} - size_large := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Large type")) i18n_text: "Large type"} + size_normal := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_type_small.svg")}} + size_large := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_type_large.svg")}} } View {width: Fill height: 44 flow: Right spacing: 8 - spacing_comfortable := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Comfortable")) i18n_text: "Comfortable"} - spacing_compact := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Compact")) i18n_text: "Compact"} + spacing_comfortable := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_spacing_comfortable.svg")}} + spacing_compact := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/article_spacing_compact.svg")}} } - theme_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + theme_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} } cover := SolidView {visible: false width: Fill height: Fill draw_bg.color: #xffffff ScrollYView {width: Fill height: Fill flow: Down padding: 16 spacing: 10 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover")) i18n_text: "Cover" draw_text.text_style: theme.font_bold{font_size: 13}} View {width: Fill height: Fit flow: Overlay align: Align{x: 1.0 y: 1.0} cover_wide := Image {width: Fill height: 190 fit: ImageFit.Biggest} - cover_pick := mod.widgets.ArticleButton {height: 32 margin: 8 text: #(crate::i18n::tr("Choose cover image")) i18n_text: "Choose cover image"} + cover_pick := mod.widgets.ArticleButton {height: 32 margin: 8 draw_icon +: {svg: ICON_ADD_PHOTO}} } mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Share card")) i18n_text: "Share card" draw_text.text_style: theme.font_bold{font_size: 13}} cover_square := Image {width: 100 height: 100 fit: ImageFit.Biggest} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Summary")) i18n_text: "Summary" draw_text.text_style: theme.font_bold{font_size: 13}} cover_summary := mod.widgets.ArticleInput {height: 85 is_multiline: true flow: Flow.Right{wrap: true} empty_text: #(crate::i18n::tr("Summary")) i18n_empty_text: "Summary"} View {width: Fill height: 12} - cover_done := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Done")) i18n_text: "Done"} + cover_done := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHECKMARK}} // Crop and placement adjustments, below the atlas's primary action. View {width: Fill height: 16} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Cover focal point · horizontal / vertical")) i18n_text: "Cover focal point · horizontal / vertical" draw_text.color: #x888888} cover_x := Slider {width: Fill height: 30 min: 0 max: 1000 step: 1} cover_y := Slider {width: Fill height: 30 min: 0 max: 1000 step: 1} cover_show := CheckBox {text: #(crate::i18n::tr("Show cover at the top of the article"))} - cover_remove := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Remove cover")) i18n_text: "Remove cover"} + cover_remove := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_TRASH}} } } preview := SolidView {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 12 draw_bg.color: #xffffff @@ -547,12 +562,12 @@ script_mod! { } } preview_stats := mod.widgets.ArticleLabel {draw_text +: {color: #x888888 text_style: theme.font_regular{font_size: 11}}} - css_preview_open := mod.widgets.ArticleButton {width: Fill visible: #(cfg!(feature = "html_preview")) text: #(crate::i18n::tr("HTML/CSS preview (experimental)")) i18n_text: "HTML/CSS preview (experimental)"} - preview_check := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Publication review")) i18n_text: "Publication review"} + css_preview_open := mod.widgets.ArticleButton {width: Fill visible: #(cfg!(feature = "html_preview")) draw_icon +: {svg: ICON_HTML_FILE}} + preview_check := mod.widgets.ArticlePrimary {draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} } css_preview := View {visible: false width: Fill height: Fill flow: Down padding: 20 spacing: 10 mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Tap links to open them. Return to the editor to edit or select text.")) i18n_text: "Tap links to open them. Return to the editor to edit or select text."} - css_preview_refresh := mod.widgets.ArticleButton {text: #(crate::i18n::tr("Refresh preview")) i18n_text: "Refresh preview"} + css_preview_refresh := mod.widgets.ArticleButton {draw_icon +: {svg: crate_resource("self://resources/icons/article_refresh.svg")}} css_preview_bitmap := mod.widgets.HtmlView {} } review := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 @@ -561,14 +576,14 @@ script_mod! { review_destination := mod.widgets.ArticleLabel {draw_text.color: #x576b95} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Images are uploaded only after your final confirmation.")) i18n_text: "Images are uploaded only after your final confirmation." draw_text.color: #x777777} View {width: Fill height: 12} - review_continue := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue")) i18n_text: "Continue"} + review_continue := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_CHEVRON_RIGHT}} } rooms := SolidView {visible: false width: Fill height: Fill flow: Down padding: 16 spacing: 12 draw_bg.color: #xffffff article_chat_search := mod.widgets.ArticleInput {empty_text: #(crate::i18n::tr("Search chats")) i18n_empty_text: "Search chats"} article_rooms := PortalList {width: Fill height: Fill Chat := mod.widgets.ArticleChatRow {} } - rooms_publish := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Publish")) i18n_text: "Publish"} + rooms_publish := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_SEND}} } confirm := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 20 confirm_cover := Image {width: Fill height: 170 fit: ImageFit.Biggest} @@ -578,20 +593,22 @@ script_mod! { publish_room := mod.widgets.ArticleLabel {draw_text +: {color: #x576b95 text_style: theme.font_bold{font_size: 15}}} confirm_details := mod.widgets.ArticleLabel {} View {width: Fill height: 12} - article_confirm := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Confirm publish")) i18n_text: "Confirm publish"} - article_change := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Back to editing")) i18n_text: "Back to editing"} + mod.widgets.ArticleActions { + article_change := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_EDIT}} + article_confirm := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_icon +: {svg: ICON_CHECKMARK}} + } } publication := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 18 publication_state := mod.widgets.ArticleLabel {draw_text +: {color: #x07a858 text_style: theme.font_bold{font_size: 24}}} publication_cover := Image {width: Fill height: 170 fit: ImageFit.Biggest} publication_title := mod.widgets.ArticleLabel {draw_text.text_style: theme.font_bold{font_size: 18}} publication_info := mod.widgets.ArticleLabel {} - publication_read := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Read full article")) i18n_text: "Read full article"} - publication_edit := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Edit article")) i18n_text: "Edit article"} - publication_withdraw := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Withdraw article")) i18n_text: "Withdraw article" draw_text.color: #xe34d4d} + publication_read := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: crate_resource("self://resources/icons/eye_open.svg")}} + publication_edit := mod.widgets.ArticleButton {width: Fill draw_icon +: {svg: ICON_EDIT}} + publication_withdraw := mod.widgets.ArticleButton {width: Fill draw_icon.color: #xe34d4d draw_icon +: {svg: ICON_TRASH}} View {width: Fill height: 12} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your local draft is preserved.")) i18n_text: "Your local draft is preserved." draw_text.color: #x777777} - publication_library := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("My articles")) i18n_text: "My articles"} + publication_library := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_FILE}} } published := View {visible: false width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: 32 spacing: 14 View {width: Fill height: 90} @@ -601,8 +618,8 @@ script_mod! { mod.widgets.ArticleLabel {width: Fit text: #(crate::i18n::tr("Published")) i18n_text: "Published" draw_text.text_style: theme.font_bold{font_size: 22}} published_info := mod.widgets.ArticleLabel {width: Fit draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 13}}} View {width: Fill height: Fill} - published_edit := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Continue editing")) i18n_text: "Continue editing"} - published_library := mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} text: #(crate::i18n::tr("Back to article list")) i18n_text: "Back to article list" draw_bg +: {border_size: 1.0 border_color: #xdddddd}} + published_edit := mod.widgets.ArticlePrimary {draw_icon +: {svg: ICON_EDIT}} + published_library := mod.widgets.ArticleButton {width: Fill height: 46 align: Align{x: 0.5 y: 0.5} draw_bg +: {border_size: 1.0 border_color: #xdddddd} draw_icon +: {svg: ICON_FILE}} } withdraw := ScrollYView {visible: false width: Fill height: Fill flow: Down padding: 24 spacing: 24 withdraw_title := mod.widgets.ArticleLabel {draw_text.text_style: theme.font_bold{font_size: 22}} @@ -611,8 +628,10 @@ script_mod! { mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Withdraw this article?")) i18n_text: "Withdraw this article?" draw_text.text_style: theme.font_bold{font_size: 20}} mod.widgets.ArticleLabel {text: #(crate::i18n::tr("Your local draft will remain. Copies already downloaded or forwarded cannot be recalled.")) i18n_text: "Your local draft will remain. Copies already downloaded or forwarded cannot be recalled."} View {width: Fill height: 12} - withdraw_confirm := mod.widgets.ArticlePrimary {text: #(crate::i18n::tr("Confirm withdrawal")) i18n_text: "Confirm withdrawal" draw_bg +: {color: #xe34d4d color_hover: #xd54444 color_down: #xc33b3b}} - withdraw_cancel := mod.widgets.ArticleButton {width: Fill text: #(crate::i18n::tr("Cancel")) i18n_text: "Cancel"} + mod.widgets.ArticleActions { + withdraw_cancel := mod.widgets.ArticleButton {draw_icon +: {svg: ICON_CLOSE}} + withdraw_confirm := mod.widgets.ArticlePrimary {width: 44 height: 44 draw_bg +: {color: #xe34d4d color_hover: #xd54444 color_down: #xc33b3b} draw_icon +: {svg: ICON_TRASH}} + } } article_status := mod.widgets.ArticleLabel {padding: Inset{left: 18 right: 18 top: 4 bottom: 8} draw_text +: {color: #x777777 text_style: theme.font_regular{font_size: 11}}} } @@ -627,6 +646,10 @@ pub struct ArticlePanel { source: ScriptObjectRef, #[deref] view: View, + #[live] + icon_tooltip: CalloutTooltip, + #[rust] + icon_hint: Option, #[rust] active: bool, #[rust] @@ -652,6 +675,8 @@ pub struct ArticlePanel { #[rust] viewport_width: f64, #[rust] + icon_labels: BTreeMap, + #[rust] css_request: String, #[cfg(feature = "html_preview")] #[rust] @@ -997,7 +1022,6 @@ impl ArticlePanel { self.view(cx, ids!(write_theme_sheet)).set_visible(cx, theme_sheet); self.view(cx, ids!(write_image_sheet)).set_visible(cx, self.write_image_sheet && !wide); self.view(cx, ids!(write_style_fab_box)).set_visible(cx, !wide && mode == WriteMode::Preview && !theme_sheet); - self.button(cx, ids!(write_style_fab)).set_text(cx, &format!("Aa {}", tr("Style"))); if theme_sheet { for (index, &theme) in Theme::ALL.iter().enumerate() { let swatch = self.widget(cx, &[LiveId::from_str(&format!("s{index}"))]); @@ -1017,15 +1041,15 @@ impl ArticlePanel { for (id, selected) in [(id!(write_mode_source), mode == WriteMode::Source), (id!(write_mode_split), mode == WriteMode::Split), (id!(write_mode_preview), mode == WriteMode::Preview)] { let mut button = self.button(cx, &[id]); let (bg, ink) = if selected { (color(0xe9f8ef), color(0x07a858)) } else { (color(0xffffff), color(0x555555)) }; - script_apply_eval!(cx, button, {draw_bg +: {color: #(bg)} draw_text +: {color: #(ink) color_hover: #(ink)}}); + script_apply_eval!(cx, button, {draw_bg +: {color: #(bg)}}); + if let Some(mut button) = button.borrow_mut() { button.draw_icon.color = ink; } } - let style = format!("{}:{}", tr("Style"), tr(self.doc.theme.name())); - self.button(cx, ids!(write_style)).set_text(cx, &style); let accent = color(accent); - let mut style_button = self.button(cx, ids!(write_style)); - script_apply_eval!(cx, style_button, {draw_text +: {color: #(accent) color_hover: #(accent)}}); - let mut fab = self.button(cx, ids!(write_style_fab)); - script_apply_eval!(cx, fab, {draw_text +: {color: #(accent) color_hover: #(accent) color_down: #(accent)}}); + for id in [id!(write_style), id!(write_style_fab)] { + if let Some(mut button) = self.button(cx, &[id]).borrow_mut() { + button.draw_icon.color = accent; + } + } let saved = tr(if self.dirty { "Editing…" } else if self.write_error.is_some() { "Saved as source only" } else { "Saved" }); let (stats_ink, stats) = match &self.write_error { Some(e) => (color(0xc05a00), Some(crate::i18n::format("Can't publish yet: {0}", &[("0", tr(e).to_owned())]))), @@ -1196,6 +1220,163 @@ impl ArticlePanel { self.view(cx, ids!(write_table_popup)).set_visible(cx, false); self.view.redraw(cx); } + fn set_icon_hint(&mut self, cx: &mut Cx, id: LiveId, label: &str) { + let uid = self.button(cx, &[id]).widget_uid(); + self.icon_labels.insert(uid, label.to_owned()); + } + fn hide_icon_hint(&mut self, cx: &mut Cx) { + if self.icon_hint.take().is_some() { + self.icon_tooltip.hide(cx); + } + } + fn icon_hints(&mut self, cx: &mut Cx, event: &Event) { + match event { + Event::MouseMove(_) | Event::LongPress(_) => {} + Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseLeave(_) + | Event::TouchUpdate(_) | Event::ClearHover | Event::Scroll(_) + | Event::KeyDown(_) | Event::BackPressed { .. } => { + self.hide_icon_hint(cx); + return; + } + _ => return, + } + let mut hint = None; + 'buttons: + for (id, label) in [ + (id!(article_back), "Back"), + (id!(article_continue), "Continue with Rinx"), + (id!(article_allow), "Allow and open"), + (id!(article_cancel), "Cancel"), + (id!(drafts_tab), "Drafts"), + (id!(published_tab), "Published"), + (id!(withdrawn_tab), "Withdrawn"), + (id!(article_import), "Import Markdown or HTML file"), + (id!(article_share), "Share app"), + (id!(sidebar_new), "New article"), + (id!(sidebar_library), "My articles"), + (id!(sidebar_share), "Share app"), + (id!(article_h2), "Heading"), + (id!(article_quote), "Quote"), + (id!(article_list), "Bulleted list"), + (id!(article_undo), "Undo"), + (id!(article_redo), "Redo"), + (id!(block_up), "Move up"), + (id!(block_down), "Move down"), + (id!(block_remove), "Remove block"), + (id!(source_toggle), "Edit source"), + (id!(image_settings), "Image settings"), + (id!(article_add_text), "Text"), + (id!(article_images), "Images"), + (id!(article_theme), "Style"), + (id!(article_cover), "Cover"), + (id!(article_source), "Source"), + (id!(article_preview), "Full preview"), + (id!(inspector_theme), "Choose theme"), + (id!(inspector_cover_button), "Cover and summary"), + (id!(inspector_preview), "Full preview"), + (id!(theme_sheet_apply), "Apply"), + (id!(sheet_cover_change), "Change"), + (id!(sheet_cancel), "Cancel"), + (id!(sheet_publish), "Publish"), + (id!(source_apply), "Apply and return to visual editing"), + (id!(image_pick), "Choose from device"), + (id!(link_apply), "Apply link"), + (id!(link_remove), "Remove link"), + (id!(image_full), "Full width"), + (id!(image_medium), "Medium width"), + (id!(image_small), "Small width"), + (id!(image_replace), "Replace image"), + (id!(image_remove), "Remove image"), + (id!(image_done), "Done"), + (id!(size_normal), "Standard type"), + (id!(size_large), "Large type"), + (id!(spacing_comfortable), "Comfortable"), + (id!(spacing_compact), "Compact"), + (id!(theme_done), "Done"), + (id!(cover_pick), "Choose cover image"), + (id!(cover_done), "Done"), + (id!(cover_remove), "Remove cover"), + (id!(css_preview_open), "HTML/CSS preview (experimental)"), + (id!(preview_check), "Publication review"), + (id!(css_preview_refresh), "Refresh preview"), + (id!(review_continue), "Continue"), + (id!(rooms_publish), "Publish"), + (id!(article_confirm), "Confirm publish"), + (id!(article_change), "Back to editing"), + (id!(publication_read), "Read full article"), + (id!(publication_edit), "Edit article"), + (id!(publication_withdraw), "Withdraw article"), + (id!(publication_library), "My articles"), + (id!(published_edit), "Continue editing"), + (id!(published_library), "Back to article list"), + (id!(withdraw_confirm), "Confirm withdrawal"), + (id!(withdraw_cancel), "Cancel"), + (id!(image_sheet_pick), "Choose from device"), + (id!(article_save), "Save"), + (id!(article_done), "Done"), + (id!(article_close), "Close"), + (id!(article_new), "New article"), + (id!(write_style), "Article style"), + (id!(write_mode_source), "Edit"), + (id!(write_mode_split), "Split"), + (id!(write_mode_preview), "Preview"), + (id!(write_publish), "Publish"), + (id!(write_style_fab), "Article style"), + (id!(write_themes_close), "Close"), + (id!(theme_sheet_close), "Close"), + (id!(image_sheet_close), "Close"), + (id!(sheet_close), "Close"), + (id!(article_bold), "Bold"), + (id!(article_italic), "Italic"), + (id!(article_link), "Link"), + (id!(wt_bold), "Bold"), + (id!(wt_italic), "Italic"), + (id!(wt_heading), "Heading"), + (id!(wt_quote), "Quote"), + (id!(wt_bullet), "Bulleted list"), + (id!(wt_numbered), "Numbered list"), + (id!(wt_link), "Link"), + (id!(wt_image), "Image"), + (id!(wt_code), "Code"), + (id!(wt_table), "Insert table"), + (id!(wt_rule), "Horizontal rule"), + (id!(wb_bold), "Bold"), + (id!(wb_italic), "Italic"), + (id!(wb_heading), "Heading"), + (id!(wb_quote), "Quote"), + (id!(wb_bullet), "Bulleted list"), + (id!(wb_link), "Link"), + (id!(wb_image), "Image"), + (id!(wb_table), "Insert table"), + ] { + for button in self.view.widgets(cx, &[&[id]]).iter() { + if button.borrow::