277 Commits

  • Fix Linux Cloud Agent setup for Android and desktop (#51)
    * Fix Cloud Agent setup so Linux builds can sign and launch.
    
    The debug keystore must use alias key0, matching the Android signing config. The install script also provisions JDK 21, Android SDK 37.2, and the libraries Compose Desktop needs to open a window.
    
    Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
    
    * Run Cloud Agent install from the app repo in a multi-repo workspace.
    
    The setup script lives under the app checkout, and the debug keystore alias on main still has to be rewritten to key0 before Gradle signs the APK.
    
    Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
    
    * Call the Cloud Agent setup script directly from environment.json.
    
    Install stays a single command. Keystore alias, JDK, Android SDK, and desktop libraries stay in cloud-setup.sh.
    
    Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
    
    ---------
    
    Co-authored-by: Cursor Agent <cursoragent@cursor.com>
  • Update IDEA settings
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • v1.2.1, bugfixes
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Delete .xml
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • ci(release): drop iOS IPA job and release asset
    Skip Kotlin Native link and unsigned IPA packaging in release builds; publish
    Android, desktop, and Linux artifacts only.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Delete stale workflows
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Merge branch 'feature/desktop'
    Desktop client, release CI, About/legal build metadata, and 1.2.0 version bump.
  • Release 1.2.0: bump version and stable versionCode for CI
    Set default versionName to 1.2.0 with versionCode 120999 so Android stays
    above 1.2.0-beta builds. Teach set-version to derive stable semver codes
    as digits*1000+999 while keeping prerelease strip behavior.
  • Inject git metadata into AppBuildInfo for About and legal links
    Pass gitCommitSha and gitHubRepository from CI Gradle properties and local
    git remote, generate them in AppBuildInfo, and resolve repo-relative legal
    markdown links to GitHub blob URLs. Extend About with commit and license
    links; keep EN/RU strings in sync.
  • Fix iOS IPA zip path when packaging from Gradle working directory.
    Resolve release-staging to an absolute path before cd into the Payload stage.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Give iOS release job enough Gradle heap for KMP compile.
    Avoid Java heap space failures during linkReleaseFrameworkIosArm64 on macOS runners.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Build iOS release framework via linkReleaseFrameworkIosArm64 in CI.
    Avoid embedAndSignAppleFrameworkForXcode when Xcode env detection fails on GitHub macOS runners; stage ComposeApp.framework under xcode-frameworks manually.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Fix release CI JVM artifact transfer and iOS SDK naming.
    Tar JVM build outputs for artifact upload, extract before packaging verify, derive iphoneos SDK_NAME from xcrun, and skip appimagetool permission checks on CI.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Fix release iOS CI hang and JVM artifact uploads.
    Build the KMP framework with plain Gradle logs before xcodebuild, skip the Xcode Gradle script phase, and include gitignored build outputs in jvm-compile-outputs artifacts.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Fix Windows taskbar icon and improve Parallels beta builds.
    Stamp AppUserModel IDs on installer shortcuts to match the desktop app, embed proper ICOs in launchers, derive beta setup names from versionName, and sync/copy helpers for the Windows VM workflow.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Fix macOS run skill
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Restore release files
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Massive refactor and cleanup
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Massive refactor and cleanup #2
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Fix iOS build
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Final refactoring
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Fix desktop message context menu placement and input handling.
    Use in-window popups with window-space anchors clamped to the chat pane, measure before reveal, and limit pointer blocking to the active message.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Update IDEA settings
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Clean up
  • ci(release): workflow_dispatch only so caches survive across releases
    - drop the tag-push trigger: caches saved by tag-scoped runs are visible
      to that tag only (GitHub docs, 'Restrictions for accessing a cache'),
      so every tag build started cold and warmed nobody
    - dispatch instead, preferably from the default branch; the tag does not
      need to exist, publish creates it at the built commit; warn when
      dispatching from a non-default branch
    - merge check-build into resolve-version (one runner + one checkout
      instead of two)
    - merge record-build into publish: save the build-success marker at the
      start of publish
    - fix softprops input: overwrite_existing does not exist upstream
      (silently ignored); the valid input is overwrite_files
    
    ci(release): dump jpackage out/err logs when macOS packaging fails
    
    ci(windows): align rust cache save key with restore key (arch + v5)
    
    The save action wrote windows-rust-Windows-v3-<lock> with no arch while
    restore fanned out on windows-rust-Windows-<arch>-v4-<lock> — so every
    saved entry was unreachable by design. It also made the two matrix legs
    race to reserve the same key: ARM won, x64 got 'another job may be
    creating this cache'. Save now uses the exact restore key (arch + v5);
    restore falls back only to same-arch v5 prefixes to avoid pulling the
    stale archless v3 entries into the wrong build.
    
    ci: add temporary cache-inspect workflow
    
    ci(cache): one-shot push trigger so the temp inspector can run from a branch
    
    ci(cache): fix field name in inspector (api uses key)
    
    ci(cache): inspect via exact original path lists, per-OS matrix
    
    ci(cache): save only tools/android/build-jar gradle scopes
    
    Packaging scopes' build trees (1.3-2 GB each, 6 scopes) were ~90% installer
    outputs and jlink runtime duplicates that actions/cache negation patterns
    are unable to exclude (actions/toolkit#713). Saving ~7 GB per run kept the
    repo at the 10 GB cache cap and LRU-evicted build-jar/android/windows-rust
    entries within minutes of being saved. Packaging scopes are now
    restore-only: they fall back to the build-jar scope via restore-keys, and
    their outputs regenerate from the JAR artifact in a couple of minutes.
    
    Also remove the temporary cache-inspect workflow (purpose served).
    
    ci(cache): unify gradle-build path lists so restore-keys cross-hit
    
    Restores differ per path list: actions/cache computes a version stamp from
    the path list and only matches restore-keys via a version stamp (hash of
    the path list): entries with different path lists are invisible to each
    other's restore-keys, even with a shared key prefix. The build-jar scope
    kept compose/tmp while the generic list negated it, so the
    '-v2-build-jar-' fallback never matched for packaging scopes (measured in
    run 39349881796: 'Cache not found' listing the fallback prefix). Drop the
    ineffective '!*/compose/tmp' negation (no-op at save time anyway per
    actions/toolkit#713) so all non-android scopes share one version stamp;
    packaging restores now warm from the build-jar entry.
    
    ci: enable Gradle configuration cache in CI; temporary --profile on Linux packaging
    
    ci: revert configuration cache (script-closure tasks not serializable); keep --profile diagnostic
    
    ci: drop temporary --profile diagnostic (profile captured; deb/rpm identified as the quiet ~45s/~23s hogs)
    
    ci: build the macOS x64 DMG on an Intel runner (macos-26-intel)
    
    macos-latest resolves to an arm64 image, so the 'Intel' matrix job ran on
    Apple Silicon and jpackage shipped arm64 binaries under the -intel.dmg
    name (v1.2.0-beta18): Intel Macs refuse it with 'this program isn't
    supported on this mac'. jpackage cannot cross-build, and the Gradle
    daemon JVM criteria (gradle-daemon-jvm.properties) provision the daemon
    for the host arch, so every arch decision followed the runner. Pin each
    matrix entry to its native runner.
    
    proguard: keep JNA entire API from stripping (native GetStaticMethodID lookups)
    
    jnidispatch resolves com.sun.jna.Native#dispose by name from native code
    during initIDs; ProGuard stripped it (not a Java-native method, no Java
    references), so every Windows package died at launch with
    'Failed to launch JVM':
      UnsatisfiedLinkError: Can't obtain static method dispose
          from class com.sun.jna.Native
          at com.sun.jna.platform.win32.Shell32.<clinit>
    Linux/macOS never load Shell32, so only Windows builds were affected
    (both x64 and arm64). Also keep classes extending/implementing JNA
    (Libraries, Callbacks, Structures are loaded reflectively at runtime).
    
    ci: speed up release builds; desktop/chat and theme updates
    
    Share JVM compile outputs from Build JAR, fix macOS Playwright caching,
    cache DMG export, and enable configuration cache on CI. Includes desktop
    chrome, attachment preview, system theme, and related shared fixes.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    ci: drop configuration cache on release (Compose ProGuard incompatible)
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
    
    ci: skip JVM recompile on macOS/Windows when Build JAR artifact is restored
    
    Gradle has no cross-OS execution history for restored classes; gate
    compileKotlinJvm, desktop compileKotlin, and generateAppBuildInfo when
    FROMCHAT_PREBUILT_JVM is set and outputs are present.
    
    Co-authored-by: Cursor <cursoragent@cursor.com>
  • Desktop overhaul: title bar polish, tray, drag-drop validation and rejection UI
    - Title bar: hover animation on window buttons; restore icon flipped so the
      lower tile sits left of the upper one; 2-pane search morph, mode-switch
      continuity and NavTransitionMuting to stop one-frame garbled chat switches
    - Linux: WM_CLASS "FromChat" (main class FromChat.kt), skiko/virgl workaround
      shipped at build time via compileSkikoVirglPatch (GraphicsApi_jvmKt shadow in
      desktop.jar), MESA_GL_VERSION_OVERRIDE=3.0 (+vblank_mode=3) baked into the
      .desktop Exec on ARM64, Mac tray icon fix
    - Attachment drag-drop: reject non-file payloads (desktop shortcuts) with a
      bridge-scoped AttachmentDropRejection — only the hovered element tints with
      "This isn't a file", the element rattles once, stays scaled back ~1s and then
      blurs release, tint and scale fade together; strict isFile validation so
      nothing is ever sent under name "file"
    - Attachments: rounded tile icon, failed-image tile UI, cancel-X progress state
      fixes, DecryptedFile/ImageCache hardening (no more cache_write_failed)
    - Mac: hide image capture button on desktop
    - Skill: build-linux-vm fast iteration loop learned from the VM session
  • Replace AWT XEmbed tray with native SNI tray on Linux
    On any DE hosting StatusNotifierItem (GNOME 46+/55 with the AppIndicator
    extension, KDE, etc.) the shell now hosts both the icon and the menu:
    we export org.kde.StatusNotifierItem + com.canonical.dbusmenu over
    dbus-java (pure Kotlin) and register with StatusNotifierItemWatcher.
    GNOME renders the popup itself from our DbusMenu tree
    (Connected | sep | Show | About FromChat | sep | Quit); label changes
    go out as ItemsPropertiesUpdated. AWT/XEmbed stays as fallback for
    X11-only shells; Windows/macOS untouched.
    
    dbus-java quirks resolved in pure Kotlin (details in tray-sni/DESIGN.md):
    properties are served by a hand-rolled org.freedesktop.DBus.Properties
    impl with explicit-signature Variants (DBusBoundProperty cannot express
    a(iiay) from Kotlin), and outer return structs (GetLayout/AboutToShowGroup)
    implement DBusSerializable to dodge the erased-field export XML bug.
    
    Verified on Ubuntu ARM64 VM: registers on the real session bus; icon in
    the GNOME panel; native menu opens; Show/About/Quit callbacks reach the
    app on the EDT; hardware-accelerated rendering unchanged (0 fallbacks,
    0 shader errors).
  • Fix tray menu scaling/anchor on HiDPI; persist window geometry safely
    - DesktopTrayMenu: apply the screen density to the tray menu's own Window,
      anchor from the TrayIcon event instead of stale MouseInfo, dp-based gap
    - DesktopScale: usable accessors before composition (awtScale/contentDensity/sizeRatio)
    - Main: color tray icon pre-scaled to 48x48 with isImageAutoSize, skip
      persisting the first post-restore geometry pass and maximized windows
  • Stop the tray setup from throwing when no tray host is reachable
    Diagnosed from inside the Ubuntu 26.04 VM via prlctl: the app's own log at
    ~/.local/share/FromChat/fromchat/logs/current.log contains
    
        at java.desktop/java.awt.SystemTray.getSystemTray(Unknown Source)
    
    so getSystemTray() throws where isSupported() had returned true. GNOME has no
    StatusNotifierItem host for AWT's libappindicator bridge to register with, and
    the deb's libayatana-appindicator3-1 dependency is present — so the dependency
    was the wrong lever.
    
    DesktopTrayHost called getSystemTray() unguarded while the two lines around it
    were already runCatching, so the throw escaped the DisposableEffect as an
    uncaught exception: the tray icon never appeared and nothing was clickable. It
    is now guarded and logged (TrayMenu "System tray unavailable: <class>: <msg>"),
    so the cause lands in current.log instead of as a stack trace with no context.
    
    This makes the failure graceful; it does not make AWT trays work on GNOME.
    Doing that needs a real StatusNotifierItem over D-Bus rather than AWT.
    
    Flip the tray context menu below the cursor when there is no room above
    
    On GNOME the tray icon sits in the top bar, so the menu could only ever be
    placed by clamping: the old code always asked for anchor.y - height, and when
    that fell outside the working area coerceIn pinned it to usable.y regardless
    of where the click was. That is what made the menu look offset from the click.
    
    Placement now tries up-and-left first and flips to below (and to the right when
    there is no room on the left) instead of clamping, with a small 4px gap from the
    cursor.
  • Honour HiDPI/desktop scaling on Linux
    Reported on Ubuntu 26.04 (GNOME, Parallels) and Linux x64: the app renders
    at 1:1 and looks tiny on a 2K screen — it "completely ignores scaling".
    
    Compose derives LocalDensity from the AWT device configuration, and on a HiDPI
    or fractional-scaled desktop Java frequently still reports 96 DPI. GDK exports
    the real scale as GDK_SCALE (integer) times GDK_DPI_SCALE (fractional), and
    sun.java2d.uiScale is the AWT property that picks it up, so it is now passed
    through to the packaged launcher (and to `run`) on Linux.
    
    Only applied when the computed scale is above 1:1, so unscaled desktops keep
    Java's own detection and nothing changes for them.
  • Build native ARM64 Windows installer tooling instead of reusing the x64
    The ARM64 installer still died with "installer has no payload for this CPU
    (x86_64)". Two installers, two architectures, and each needs tooling compiled
    for its own target.
    
    fromchat-pack embeds the app-image payload, and payload.rs selects it with
    std::env::consts::ARCH — the stub's own compile-time architecture, not the
    machine's. The stub is a Windows x64 binary, so on ARM64 it runs emulated,
    reports x86_64, and looks for an x86_64 payload that the ARM-only bundle never
    embeds. The ARM asset was therefore structurally incapable of installing.
    
    So the ARM64 leg now builds its own aarch64-pc-windows-msvc tooling on the ARM
    runner (cargo build --release --workspace with no --target builds for the
    host) instead of downloading the x64 artifact, which is now x64-only.
    ensureWindowsRustBinaries is existence-based and rebuilds when binaries are
    absent, so this leg is self-healing if the explicit build step ever misfires.
    
    The Rust cache key was also arch-blind — windows-rust-${{ runner.os }}-v3-... —
    so an x64 target/ directory could be restored onto the ARM runner and make the
    "binaries ready" check pass with the wrong architecture. It now includes
    ${{ runner.arch }} and is bumped to v4, so the two legs cannot share a cache.
    
    This also reverts the PROCESSOR_ARCHITEW6432 sniffing added in 35492fc: with
    natively compiled stubs it is unnecessary, and it would have been actively
    wrong — an x64 installer running emulated on ARM64 should install the x64
    payload, not be redirected to the arm64 one.
    
    Turn the Windows tooling job into a 2-entry matrix
    
    The ARM64 fix had grown into duplicated, arch-conditional steps inside
    windows-package: rust-toolchain, rust cache, a binary verification loop and a
    build step, all gated on matrix.arch. That is per-architecture data, not control
    flow, so it belongs in a matrix.
    
    windows-common (x64-only) becomes windows-tools, a 2-entry matrix over
    {x64 -> windows-latest -> x86_64-pc-windows-msvc} and
    {arm64 -> windows-11-arm -> aarch64-pc-windows-msvc}. Each leg builds the
    installer tooling natively — cargo has no --target, so it builds for the host —
    and uploads it as windows-rust-tools-<arch>. windows-package then downloads
    that artifact unconditionally again, and its two setup-java-gradle calls
    collapse into one driven by a matrix key.
    
    The ARM leg still needs its own step for the packaging JDK
    (ensure-windows-arm64-jdk.cmd); that one is genuinely arch-specific rather than
    matrix data.
    
    This keeps the invariant that matters: fromchat-pack selects the embedded
    payload with std::env::consts::ARCH, so each architecture's installer must be
    compiled for its own target or it reports the wrong CPU and finds no payload.
  • Fix the mangled control.tar.zst and duplicate Depends in the .deb
    beta11 shipped a subtly broken package. jpackage on a modern dpkg emits
    **zstd** control tars (control.tar.zst), which the injection did not handle:
    its `when` fell through to plain `tar cf`, so the rebuilt member was an
    uncompressed tar still named control.tar.zst. dpkg tolerated it, but it is a
    malformed package.
    
    Verified against the actual beta11 artifact: the control member started with
    `2e 2f 00 00` ("./", i.e. an uncompressed tar) and after this fix starts with
    `28 b5 2f fd`, the zstd magic. data.tar.zst is byte-identical and the member
    set is unchanged, because only the control member is ever rewritten.
    
    Also fixed a second bug visible in the shipped control file: jpackage emitted
    no Depends line, so replacing one *and* prepending one produced two. Every
    Depends line is now stripped before a single one is prepended.
    
    The injection self-checks the rebuilt member's framing and the control tar's
    non-emptiness, and a failure still only warns and leaves the deb untouched.
  • Give the .deb the tray dependency, and log whether the tray exists
    AWT's Linux tray is not built into the JDK: it dlopens libappindicator, and
    without it SystemTray.isSupported() is false. The app gates the whole tray on
    that call (Main.kt:409) and silently renders nothing when it is false — which
    is why the tray on KDE Plasma 6 had no working clicks and no explanation in the
    log.
    
    jpackage cannot be told about dependencies through Compose's `linux { }` DSL,
    so the generated .deb shipped a control file with no Depends at all and pulled
    nothing in. packageReleaseLinux now injects
    `libayatana-appindicator3-1 | libappindicator3-1, libdbus-1-3` into that
    control file (Ayatana is the maintained package; the legacy name stays as an
    alternative for older distros).
    
    The `ar` container is rewritten in Kotlin rather than shelled out to, because
    BSD ar (macOS) injects a `__.SYMDEF SORTED` member while GNU ar (Linux, where
    this runs) does not, and rebuilding a package with the wrong dialect corrupts
    it. Member order is preserved and headers are reused verbatim, with only the
    size field refreshed for the replaced control member. Everything is wrapped so
    a failure logs a warning and leaves the deb exactly as jpackage built it,
    rather than failing the release.
    
    Verified against a synthetic GNU-style .deb: members read back in order
    (debian-binary, control.tar.gz, data.tar.gz), the control file gains the
    Depends line and keeps Package/Architecture, and the data payload round-trips
    intact. BSD `ar t` also reads the result.
    
    Main.kt now logs SystemTray.isSupported() once per run, so the next report
    says outright whether the tray was available instead of us inferring it.
    
    Still not covered: the .rpm gets no dependency injection (no jpackage option
    and no easy rpm rewrite), and the AppImage still relies on the host having
    libappindicator.
  • Fix the white-square tray icon on KDE Plasma
    On KDE Plasma 6 (StatusNotifierItem host) the tray icon rendered as a solid
    white square and clicks did nothing.
    
    The square is explained by the asset: app_icon.png is a white glyph on
    transparency, and all 3277 of its fully transparent pixels carry white
    (255,255,255) RGB. Plasma hands the icon AWT supplies to the StatusNotifier
    host, which does not honour the alpha channel — so every transparent pixel
    painted white and the whole 22px tile became a white square.
    
    The Linux tray now uses the coloured app icon instead (saturated
    purple/pink/blue gradient), which survives a lost alpha channel, and skips
    stripTrayMatteBackground for it — that helper exists to erase a white matte
    background behind the macOS template glyph, and applied to the coloured icon
    it would punch holes in its white glyph. macOS and Windows keep the template
    glyph and the matte stripping exactly as before.
  • Fix macOS launch, Windows ARM64 install, and notification translation
    macOS: the app died before the first frame with "MacOS does not support
    OPENGL rendering API" from SkiaLayer's constructor. Compose's platform DSL
    (windows { } / macOS { }) has no jvmArgs of its own, so the unqualified
    jvmArgs(...) in `windows { }` resolved to JvmApplication.jvmArgs — the one
    application-level list the plugin writes into the launcher config of *every*
    platform it packages. The published macOS bundle therefore shipped
    -Dskiko.renderApi=OPENGL and -Dsun.java2d.d3d=true. GPU args now come from
    hostGpuJvmArgs(), added per host: OPENGL on Linux and Windows x64, ANGLE on
    Windows ARM64, nothing on macOS (Skiko defaults to Metal there). Verified by
    repackaging locally: the macOS FromChat.cfg no longer carries those flags and
    the packaged app starts, connects and renders.
    
    Windows ARM64: the installer aborted with "installer has no payload for this
    CPU (x86_64)". payload.rs picked the payload with std::env::consts::ARCH,
    which is the *installer's* compile-time arch — and the stub is built once on
    windows-latest (x64). On Windows ARM64 it runs emulated, reports x86_64 and
    looks for an x64 payload the ARM-only bundle never embeds. host_arch() now
    prefers PROCESSOR_ARCHITEW6432, which carries the machine's real architecture
    for WOW64/emulated processes. payload_zstd() is only called by the runtime
    binaries, never by the packer, so this cannot affect what gets packed.
    
    Notifications: a Russian UI produced notifications titled "Main chat" (the
    English default) because getString() outside a composition resolves against
    the default resource. The chats screen already resolves the title the way the
    UI renders it, so it now publishes that through LocalizedChatTitles and the
    sink reuses it — same string the user sees, no second resolution path.
  • Fix release ProGuard: Adoptium JDK 25 ships no jmods
    Compose Desktop's AbstractProguardTask builds ProGuard's -libraryjars by
    walking <javaHome>/jmods and nothing else. Adoptium stopped shipping the
    jmods/ directory in JDK 25 archives (verified: 0 jmods entries in both the
    linux/x64 and macOS/aarch64 Temurin 25.0.4.1 tarballs, vs 71 for Temurin
    17), so on the runners the desktop packaging JDK resolves to
    /usr/lib/jvm/temurin-25-jdk-amd64 and ProGuard gets no JDK whatsoever.
    Every class then fails to resolve java.lang.Object — 1.3M
    "can't find superclass or interface java.lang.Object" warnings, followed by
    "Please correct the above warnings first".
    
    This is why the Java 25 toolchain bump broke every release while the
    v1.2.0-beta release on the toolchain 17 build kept working, and why a local
    build with the JetBrains Runtime (which does ship jmods) succeeded while CI
    did not. ProGuard's version was never the cause; 7.10.0 was still kept
    because it is what understands this project's Kotlin 2.4 metadata.
    
    Install Zulu 25 (still ships jmods) in the desktop-proguard job and export
    it as FROMCHAT_PACKAGING_JDK; setup-java runs before Temurin 17 so JAVA_HOME
    stays on 17 for Gradle and the Android build. resolvePackagingJdkHome() now
    warns loudly when the JDK it picks has no jmods/, instead of leaving it to
    ProGuard's wall of warnings.
    
    Use setup-java's `path` output for FROMCHAT_PACKAGING_JDK
    
    setup-java@v5 names the installed JDK's location `path`, not `java-path`
    (the v4 name), so the beta4 run exported an empty value, fell back to the
    jmods-less /usr/lib/jvm/temurin-25-jdk-amd64 and failed with the same
    "can't find superclass or interface java.lang.Object" wall of warnings.
    
    The Gradle-side guard added in b0159eb correctly named the offending JDK
    in the beta4 log, which is what pointed at the empty export.
    
    Give the Linux packaging jobs a jmods-capable JDK too
    
    beta5 proved the ProGuard fix: Build JAR passed in 9m35s, and Windows
    (x64 + ARM) and macOS (arm64 + Intel) all packaged successfully. Only the
    two Linux jobs failed, at :app:desktop:createRuntimeImage — jlink exits 1
    because jlink defaults its --module-path to <javaHome>/jmods, the same
    directory ProGuard needs. So the jmods gap bites twice: ProGuard degrades
    into a wall of warnings, jlink just dies.
    
    macOS/Windows runners happen to resolve a JDK 25 that still ships jmods;
    ubuntu-latest's /usr/lib/jvm only has the jmods-less Temurin 25. Point the
    Linux jobs at Zulu 25 via the setup action's desktop-jdk input, and stop
    the packaging step's `export FROMCHAT_PACKAGING_JDK="$JAVA_HOME"` from
    clobbering it with Temurin 17 (hence ${VAR:-$JAVA_HOME}).
    
    Also widen the Gradle guard to name jlink, since that is what actually
    fails on a jmods-less JDK during packaging.
    
    Fix two Windows ARM64 failures: JDK-8371740 and a needless JDK download
    
    Neither is related to the jmods work; both were visible only on the
    windows-11-arm runner.
    
    1. Gradle hung at build shutdown. The runner's ambient JAVA_HOME is Temurin
       21.0.12 aarch64, and JDK-8371740 — a LinkedTransferQueue race fixed only
       in JDK 26 — makes Gradle on Windows ARM64 hang whenever it runs on JDK
       21+, failing the build after the installer was already written with
       "Timeout waiting for concurrent jobs to complete". Gradle's own repo and
       linroid/Ketch both carry workarounds for this. Pin the Gradle JVM to 17,
       which is what the x64 job already does; Adoptium ships no Windows aarch64
       17, hence the new `java-distribution` input and zulu there. Packaging is
       unaffected: resolvePackagingJdkHome() rejects 17 as too old and falls back
       to the Java 25 toolchain JDK for jpackage.
    
    2. "Ensure ARM64 packaging JDK" stalled for minutes and looked hung. The
       script only scanned C:\Program Files\Eclipse Adoptium, but hosted ARM64
       runners ship their ARM64 Temurin 21 under the hostedtoolcache, so the scan
       missed it and fell through to downloading a ~180 MB JDK via
       Invoke-WebRequest + Expand-Archive. Re-running the job showed it still
       installing 3m24s later. Accept JAVA_HOME when it reports os.arch=aarch64 —
       the script's own comments already call Java 21 good enough as the Gradle
       daemon JVM.
    
    Stop the Gradle build cache from being invalidated on every release
    
    Two independent defects, both found while a release sat at 8.83 GB of the
    10 GB per-repo cache limit with entries being evicted.
    
    1. set-version rewrote build.gradle.kts with sed on every run, and that file
       is hashed into the Gradle build-cache key — so each release produced a
       brand-new key and orphaned the previous one. The repo inventory showed the
       damage: windows-package-x64, macos-x64 and windows-package-arm64 each had
       two ~1.1 GB entries differing only in that hash. build.gradle.kts now reads
       -PversionName/-PversionCode (injected as ORG_GRADLE_PROJECT_* by
       set-version) and keeps its previous values as defaults, so local builds are
       unchanged. Types stay String/Integer for the `as Int`/`as String` consumers
       in app/android and app/shared. Side benefit: hash-release-build-inputs also
       hashes build.gradle.kts, so the "inputs unchanged" marker no longer churns
       per version either.
    
    2. Every desktop scope archived the Android build dirs (never built there)
       plus the final installers, the exported ProGuard jars, the jpackage runtime
       image and the ProGuard temp output — all regenerated for each version, and
       together the bulk of a ~1.7 GB entry. Those paths are now excluded, which
       is what kept the repo over the limit and into eviction (GitHub evicts by
       oldest last-accessed once the limit is hit).
    
    Restore and save path lists stay in sync: actions/cache silently skips the save
    when they differ.
    
    Cross-tag reuse is NOT addressed here and cannot be: per the docs, a run for one
    tag cannot read another tag's cache, and the automatic retry against the default
    branch is a no-op for us because main has no app/desktop module to warm it.
    
    Stage each packaging host's own Skiko/JavaFX natives
    
    The beta7 desktop builds cannot start. Both failures the user reported
    (macOS arm64 and Linux arm64) are the same bug:
    
      ERROR: [Desktop] uncaught on main Failed to loade library
      .../libskiko-macos-arm64.dylib
      Caused by: java.lang.UnsatisfiedLinkError: Can't load library
    
    ProGuard runs once, on ubuntu-latest, and its output is shipped to every
    packaging job. But the Compose desktop runtime classpath is host-specific:
    that single ProGuard run froze in skiko-awt-runtime-linux-x64 and
    javafx-*-27-linux, and nothing for any other target. Inspecting the published
    macOS arm64 DMG confirmed it — 140 jars, the entire ProGuard set, with
    skiko-awt-runtime-linux-x64 as the only Skiko runtime and no
    libskiko-*-arm64 anywhere in the bundle, plus no JavaFX natives either. The
    macOS x64 build is broken the same way; Linux x64 is the only target that
    happened to match, and the Windows installers shipped Linux natives too.
    
    stageCiPrebuiltProguard now also stages the packaging host's own natives
    (skiko-awt-runtime-<os>-<arch>, javafx-*-<os>-<arch>) from its resolved
    runtimeClasspath. Skiko and JavaFX each pick their library by os/arch, so the
    foreign Linux copies stay inert and cost only some bundle size. The task
    fails loudly if a host resolves no natives at all.
    
    Verified locally end-to-end with a prebuilt-ProGuard packaging run:
    Contents/app/libskiko-macos-arm64.dylib (21.3 MB) is now present, which is
    the exact path Skiko failed to load before.
    
    Keep ProGuard's output cached in the one scope that runs it
    
    Follow-up to 0e7b181, which excluded compose/tmp from every desktop scope
    to shrink the cache entries. That was too blunt for the build-jar scope:
    compose/tmp/main-release/proguard holds proguardReleaseJars' *output*, so
    without those jars Gradle can never consider the task up-to-date and
    re-runs its ~8 minutes on every re-run of a tag.
    
    Split the non-android path list in two. build-jar keeps compose/tmp; the
    packaging scopes drop it, because they never run ProGuard — they stage the
    Build JAR artifact's jars instead, so their copy is a redundant 122 MB.
  • Fix ProGuard release build on the Java 25 toolchain
    The Compose plugin defaults to ProGuard 7.8.0, which cannot process the Java 25
    class files produced by jvmToolchain(25): it fails to load the JDK itself and
    aborts with thousands of "can't find referenced class java.lang.Class" /
    "the configuration refers to the unknown class 'java.lang.String'" warnings,
    ending in "Please correct the above warnings first". Every release build has
    been failing there since the toolchain bump.
    
    Pin ProGuard 7.10.0 (7.9.0 added Java 25 support; 7.10.0 is the first release
    that also understands the Kotlin 2.4 metadata this project compiles against).
    
    Also ignore JOGL's JavaFX bridge. JetBrains Runtime ships jogl.all.jmod in its
    jmods directory, which ProGuard loads as a library jar; its classes extend
    program-side JavaFX classes, so JBR-based (macOS) builds fail with
    "7 instances of library classes depending on program classes". Temurin JDKs
    ship no such jmod, so that part is macOS/JBR-only.
    
    Verified locally: :app:desktop:exportReleaseProguardForCi succeeds and exports
    app/desktop/build/ci/proguard/.
  • Fix silent launch failure: require Java 25 packaging JDK
    jvmToolchain(25) (f89619a) compiles classes to class file 69, but
    resolvePackagingJdkHome() accepted any JDK that shipped jpackage. On Windows
    ARM64 the candidate list hardcoded jdk-21-arm64 first and
    ensure-windows-arm64-jdk.cmd provisioned Temurin 21; on macOS the first
    candidate was Adoptium 17. jpackage then bundled a Java 17/21 runtime and the
    installed app died at launch with UnsupportedClassVersionError on the hidden
    launcher's stderr (invisible: GUI subsystem, no error dialogs).
    
    CI hit this too: setup-java-gradle installs Temurin 17 and the workflows
    export FROMCHAT_PACKAGING_JDK="$JAVA_HOME", which the resolver returned
    verbatim — so the x64 installer built by desktop-windows-x64.yml was broken
    on launch.
    
    - build.gradle.kts: version-gate FROMCHAT_PACKAGING_JDK / JAVA_HOME /
      candidate JDKs to >= desktopJvmTarget (25) with a warning, then fall back to
      the Gradle-provisioned toolchain JDK (recursive .gradle/jdks scan).
    - ensure-windows-arm64-jdk.cmd: prefer an installed jdk-25* or the
      Gradle-provisioned Java 25 toolchain JDK; Temurin 21 remains only the
      last-resort daemon JVM.
    
    Ported from c53b54b, minus ensure-windows-x64-jdk.cmd and diag-launch.ps1:
    this branch has no packageWindowsX64 path to feed, and diag-launch.ps1 is
    machine-specific and unreferenced.
    
    Verified on macOS: createDistributable now bundles JAVA_VERSION 25.0.4.1,
    matching class file 69, where a previous build had bundled 17.0.19.
  • Fix Windows tray menu: open instantly and at the click point
    The tray right-click popup took roughly 500ms to appear, then flashed at the
    top-left of the display at the wrong size before settling; on HiDPI Windows it
    was also pinned to the bottom-right corner of the screen rather than the click.
    
    - Show the popup transiently (Window.opacity = 0) while Compose measures it,
      then size, position and reveal it. This removes the open delay without the
      off-screen parking trick, which Windows clamps back to (0,0) and paints,
      causing the flash at the wrong size.
    - Derive the anchor from MouseInfo rather than the TrayIcon event and normalise
      it into the graphics configuration's coordinate space. On HiDPI Windows the
      event carries raw physical pixels, which landed outside every screen and
      pinned the menu to the bottom-right corner.
    - Clamp to the monitor's working area (bounds minus taskbar insets) so the menu
      is never hidden behind the taskbar.
    - Pick the monitor that actually contains the click instead of the one the
      unpositioned window happens to sit on.
  • Fix desktop
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Fix poor desktop performance
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • Chat list selection mode fix, part 1
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Chat list selection mode fix, part 2
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Chat list selection mode fix, part 3
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
    
    Chat list selection mode fix, part 4
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • More colors
    Update dependencies
    
    Fix sync on Arch Linux
    
    Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
  • feat: Google Sans Flex variable font, MotionTokens, and expressive shape scale
    feat: tintedHaze modifier for colorful (primary-tinted) blur
    
    feat: ExpressiveListItem and ExpressiveTints
    
    feat: PixelTopAppBar with word-by-word staggered title reveal
    
    feat: FloatingNavBar pill (colorful haze, drag-to-swap, animated labels)
    
    feat: shared-axis + emphasized spring nav transitions, premium switch haptics
    
    - App.kt: rework root NavHost transitions onto EmphasizedSpring; add sharedAxisDetail* helpers (IntOffset spring) and apply them to all DM/profile/settings-detail destinations so list→detail slides with a subtle scale+fade.
    - MotionTokens: add SharedAxisSpring (IntOffset sprite).
    - HapticFeedbackEvent: add SwitchPress / SwitchToggle.
    - SwitchHaptics (utils:shared): LocalSwitchHaptic + switchHapticCallbacks(); (app:shared) provideSwitchHaptics() wraps rememberHapticFeedback.
    - BackHandler android/ios: two-stage premium switch feel (EFFECT_TICK on press, EFFECT_CLICK / UIImpactFeedbackStyleHeavy on commit).
    - Components.kt SwitchListItem: new hapticFeedback param (default true), press tick via interactionSource.interactions, commit click on state flip.
    - App.kt: provideSwitchHaptics() at the root.
    - Also lands leftover Batch 6: Settings navBarStyle/navBarShowTitle keys, MainScreen floating-pill toggle, SettingsTab per-row tints.
    
    Change font, implement new bottom bar
  • Gitignore device hardware input settings
    Delete deviceHardwareInput.xml