Skip to content

fix(android): bundle every ABI's _sysconfigdata into the ABI-common stdlib.zip#218

Merged
FeodorFitsner merged 2 commits into
mainfrom
fix/android-x86_64-sysconfigdata
Jun 29, 2026
Merged

fix(android): bundle every ABI's _sysconfigdata into the ABI-common stdlib.zip#218
FeodorFitsner merged 2 commits into
mainfrom
fix/android-x86_64-sysconfigdata

Conversation

@FeodorFitsner

Copy link
Copy Markdown
Contributor

Problem

The pure stdlib.zip is ABI-common and built once from the primary ABI (abis.first(), e.g. arm64-v8a). But _sysconfigdata__<arch> is arch-specific: each ABI ships its own (e.g. _sysconfigdata__android_x86_64-linux-android), and CPython imports the one matching the running device at startup (sysconfig._init_posix, pulled in by ctypes).

So on a non-primary ABI (e.g. an x86_64 emulator when arm64-v8a is primary) the embedded interpreter crashes on startup:

ModuleNotFoundError: No module named '_sysconfigdata__android_x86_64-linux-android'

This was caught by Flet's new on-device flet test CI running on an x86_64 Android emulator (passes on arm64 devices/emulators, where the primary ABI's sysconfigdata happens to match).

Fix

The primary splitStdlib task now also harvests every other ABI's stdlib/_sysconfigdata* from its libpythonbundle.so into stdlib.zip — depending on the other ABIs' untar so their bundles exist to read, and holding non-primary tasks until the primary has read them (each task deletes its own bundle at the end).

Validation

A patched APK build's stdlib.zip now contains both _sysconfigdata__android_aarch64-linux-android.pyc and _sysconfigdata__android_x86_64-linux-android.pyc (previously only the aarch64 one).

…tdlib.zip

The pure stdlib.zip is built once from the primary ABI (abis.first(), e.g.
arm64-v8a), but _sysconfigdata__<arch> is arch-specific: each ABI ships its own
(e.g. _sysconfigdata__android_x86_64-linux-android) and CPython imports the one
matching the running device at startup (sysconfig, pulled in by ctypes). So on
a non-primary ABI (e.g. an x86_64 emulator) the embedded interpreter crashed
with 'ModuleNotFoundError: No module named _sysconfigdata__android_x86_64-linux-android'.

The primary splitStdlib task now also harvests every other ABI's
stdlib/_sysconfigdata* from its libpythonbundle.so into stdlib.zip (depending on
the other ABIs' untar and holding non-primary tasks until the primary has read
their bundles).
FeodorFitsner added a commit to flet-dev/flet that referenced this pull request Jun 26, 2026
…anch

Temporarily override serious_python_android + serious_python_platform_interface
to flet-dev/serious-python#218 (fix/android-x86_64-sysconfigdata) so the android
x86_64 CI leg validates the fix end-to-end (embedded Python no longer crashes
with ModuleNotFoundError: _sysconfigdata__android_x86_64-linux-android).

Locally confirmed: pubspec.lock resolves to the branch and stdlib.zip now ships
both aarch64 and x86_64 _sysconfigdata. Revert to the pub.dev release once #218
ships.
…id dup)

The previous harvest matched stdlib/_sysconfigdata*, which also catches the
generic, ABI-identical _sysconfigdata__linux_ that some Python versions (e.g.
3.12) ship in every ABI. The primary ABI already adds that via the stdlib loop,
so re-adding it from a non-primary ABI threw 'java.util.zip.ZipException:
duplicate entry: _sysconfigdata__linux_.pyc'. Match only the per-ABI
_sysconfigdata__android_<arch> modules (unique per ABI), which is exactly what
CPython imports on-device. Add CHANGELOG entry (4.1.1).
@FeodorFitsner FeodorFitsner merged commit b8e7c87 into main Jun 29, 2026
25 of 79 checks passed
@FeodorFitsner FeodorFitsner deleted the fix/android-x86_64-sysconfigdata branch June 29, 2026 15:16
FeodorFitsner added a commit that referenced this pull request Jun 29, 2026
Lockstep version bump across all packages (pubspecs, darwin podspec,
android gradle) and CHANGELOG entries.

Contains the two Android fixes since v4.1.0:
* Guard getLongVersionCode() for API < 28 to prevent launch crash on
  Android 8.1 and below (#219).
* Bundle every ABI's _sysconfigdata into the ABI-common stdlib.zip,
  fixing a non-primary-ABI startup ModuleNotFoundError (#218).
FeodorFitsner added a commit to flet-dev/flet that referenced this pull request Jun 30, 2026
* fix(controls): preserve concrete value type when constructing ValueKey

`ValueKey(controlKey.value)` produced `ValueKey<Object>(value)` because
`controlKey.value` is statically typed `Object`. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
`ValueKey<String>('foo')` — making `find.byKey(Key('foo'))` /
`find.byKey(ValueKey('foo'))` in flutter_test fail to locate any
Flet-rendered control by user-assigned key.

Switch-dispatch on the runtime type so a String value yields
`ValueKey<String>`, int → `ValueKey<int>`, etc. This matches what
`Key('foo')` (factory for `ValueKey<String>('foo')`) and analogous
test-side constructions produce.

Repro: flet_example in flet-dev/serious-python on the dart-bridge
branch — its integration_test/app_test.dart with
`find.byKey(Key('increment'))` for an IconButton with
`key="increment"` was finding 0 widgets until this fix.

* feat(transport): add dart_bridge in-process transport (alongside socket)

Adds a third transport (`FletDartBridgeServer` + Dart-side channel-builder
injection) that exchanges Flet's MsgPack protocol over the in-process
`dart_bridge` byte channel — the prebuilt-binary FFI bridge consumed by
serious_python plugins via `package:serious_python/bridge.dart`.

Coexists with the existing UDS / TCP socket transport. Activation:
- Python: `FLET_DART_BRIDGE_PORT=<port>` env var + `is_embedded()` true.
- Dart: pass `FletApp(channelBuilder: ...)` — the embedder constructs a
  `FletBackendChannel` impl wrapping a `PythonBridge` and feeds it in.

`flet` package itself stays Python-independent: it does NOT depend on
`serious_python` or know about `PythonBridge`. The whole PythonBridge
wiring lives in the embedder's code (proven by a forthcoming
`flet_ffi_example` in serious-python). What lands here in `flet` is just
the seam.

Python side:
- New `flet/messaging/flet_dart_bridge_server.py` — `FletDartBridgeServer`
  with the same protocol dispatch as `FletSocketServer`, lazy-imported so
  non-embedded runs never load `dart_bridge`. Inbound: `__on_bytes`
  enqueues payloads from the C-callback thread onto an asyncio.Queue
  drained by `__inbound_loop`. Outbound: `send_message` calls
  `dart_bridge.send_bytes(port, packb(...))`.
- `flet/app.py`: `run_async` selection block grows a third arm:
    if is_embedded() and FLET_DART_BRIDGE_PORT: dart_bridge
    elif is_socket_server:                       socket (existing)
    else:                                        web (existing)
- New helper `__run_dart_bridge_server` modelled on `__run_socket_server`.

Dart side:
- New `FletBackendChannelBuilder` typedef in
  `transport/flet_backend_channel.dart`.
- `FletApp` accepts optional `channelBuilder`; `FletBackend` honours it in
  `connect()` and skips the URL-scheme factory when present. URL-based
  routing for socket / websocket / mock / Pyodide is unchanged.

Wire protocol — unchanged. Same `[ClientAction, body]` MsgPack frames,
same encoder/decoder, same Session dispatch. Only the byte transport
differs.

* feat(transport): export FletBackendChannel + msgpack helpers from flet.dart (lets embedders implement channelBuilder)

* fix(transport): park embedded dart_bridge run loop until host shutdown

The dart_bridge transport has no accept loop equivalent — start() registers a
byte handler with libdart_bridge and returns immediately. Without an explicit
wait, run_async() falls through to conn.close() as soon as main() returns,
killing the bridge before any Dart-side frame can arrive. The embedded
interpreter then exits even though the Flutter host is still running.

Mirror the existing url_prefix/socket-server arm: wait on the terminate event
when is_embedded() and FLET_DART_BRIDGE_PORT are both set.

* templates(build): migrate from sockets to PythonBridge FFI transport

Switches the production transport in `flet build`'s generated app from
TCP/UDS sockets to the in-process dart_bridge FFI channel that the
serious-python `dart-bridge` branch exposes. Web mode (websocket) and
developer mode (external Python process over TCP/UDS) stay unchanged —
PythonBridge only makes sense when the Python interpreter is embedded
in the same OS process as Flutter.

main.dart:
  * Two long-lived PythonBridge instances created in prepareApp():
    `_bridge` carries the MsgPack-framed Flet protocol; `_exitBridge`
    is a dedicated outbound channel for Python's exit code (replaces
    the legacy stdout-callback ServerSocket).
  * pageUrl = `dartbridge://<port>`; env exports FLET_DART_BRIDGE_PORT
    and FLET_DART_BRIDGE_EXIT_PORT. The Python flet package's app.py
    picks up FLET_DART_BRIDGE_PORT and starts FletDartBridgeServer
    instead of FletSocketServer.
  * `_DartBridgeBackendChannel` (lifted from flet_ffi_example): wraps
    PythonBridge as a FletBackendChannel — streaming msgpack decoder
    on inbound, encoder + 30s retry loop on outbound. Injected into
    FletApp via the `channelBuilder` parameter added in the flet PR.
  * runPythonApp drops the ServerSocket setup; subscribes to
    `_exitBridge.messages` and reuses the existing error-screen /
    `exit(code)` handling unchanged.
  * Dropped the now-unused `getUnusedPort` helper.

python.dart:
  * Drops the `socket` callback channel and FLET_PYTHON_CALLBACK_SOCKET_ADDR.
  * `flet_exit` posts the exit code as raw UTF-8 bytes via
    `dart_bridge.send_bytes(FLET_DART_BRIDGE_EXIT_PORT, ...)`.
  * stdout/stderr → FLET_APP_CONSOLE file redirection preserved (the
    Dart side reads it for the error screen on `flet_exit(100)`).

pubspec.yaml:
  * `serious_python` pinned to the dart-bridge branch via git ref —
    1.0.1 on pub.dev predates PythonBridge. Swap to a version pin
    once serious_python ships a release carrying the bridge API.
  * Added `msgpack_dart: ^1.0.1` for the channel's wire codec.

Dev mode (--debug + page URL in args) still creates no bridges and
FletApp resolves transport via its URL-scheme factory; web mode reads
Uri.base unchanged.

* Add path for serious-python git dependency

Add a `path: src/serious_python` entry to the serious-python git dependency in sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml. This directs the package resolver to the subdirectory within the referenced repo (ref: dart-bridge) so the Dart package is loaded from src/serious_python instead of the repository root.

* Bump 3.13.14 / 3.14.6 / Pyodide 314.0.0; thread date-based python-build vars

Mirror the serious-python registry bump:
  * 3.12 row: Astral PBS date 20260610 (CPython 3.12.13 unchanged).
  * 3.13 row: CPython 3.13.14, PBS date 20260610.
  * 3.14 row: CPython 3.14.6, PBS date 20260610, Pyodide 314.0.0 GA.
  * All three rows gain `python_build_date: "20260611"` for the new
    date-keyed flet-dev/python-build release scheme.

The 3.13 wheel platform tag was also wrong — `pyodide-2025.0-wasm32`
where it should have been `pyemscripten-2025.0-wasm32` (the prefix
transition happened at Pyodide 0.28/0.29, not at 314.0). `flet build web
--python-version 3.13` would have failed to match Pyodide-built native
wheels. Fixed in the registry and called out in the 0.86.0 changelog.

`build_base.py` now exports two new env vars alongside the existing
`SERIOUS_PYTHON_VERSION` so the serious-python platform plugin build
scripts can construct the new URL form (`…/<YYYYMMDD>/python-*-<full>-*`):
  * SERIOUS_PYTHON_FULL_VERSION  → python_release.standalone
  * SERIOUS_PYTHON_BUILD_DATE    → python_release.python_build_date

Both are set in `package_env` (for `serious_python:main package`) and
`build_env` (for the subsequent `flutter build`).

Breaking-changes docs for 0.86: new 0.86.0 section in the index plus two
new guide pages covering (a) the default-Python bump 3.12 → 3.14 with
three pin options, and (b) the removal of `flet.version.pyodide_version`
/ `PYODIDE_VERSION` with the registry-lookup replacement. The dart_bridge
default-transport migration guide is intentionally not in this commit;
it'll be authored separately.

Publish docs tables (`publish/index.md`, `publish/web/static-website`)
updated to the new patch + Pyodide versions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(transport): DataChannel API for high-throughput widget byte streams

Adds dedicated byte channels (`ft.DataChannel`) that let widgets exchange
bulk binary data (image frames, audio buffers, ML tensors) with their
Python counterpart without going through the MsgPack control protocol.

Architecture:

* `package:flet` exposes abstract `DataChannel` + `DataChannelFactory`.
  Embedders inject a fast-path factory; absent that, a built-in
  `ProtocolMuxedDataChannelFactory` muxes channel bytes over the active
  Flet protocol transport.
* Python side: `ft.DataChannel` ABC with `_DartBridgeDataChannel`
  (embedded native, dedicated PythonBridge) and `_ProtocolMuxedDataChannel`
  (muxed fallback) impls. `Control.get_data_channel(id)` resolves a
  channel allocated on the Dart side.
* Handshake: control-level event `data_channel_open` carrying
  `{channel_name, channel_id}` — push-driven, no polling, no race.

Wire format change (breaking):

* All transports now prefix every packet with a 1-byte type
  discriminator: `0x00` = MsgPack-encoded Flet protocol frame,
  `0x01` = raw DataChannel frame (`[channel_id:u32 LE][bytes]`).
* Stream-oriented transports (UDS/TCP) gain a 4-byte little-endian
  length prefix; message-oriented transports (WebSocket, postMessage,
  dart_bridge) keep native message boundaries.
* `StreamingMsgpackDeserializer` removed — every inbound packet is one
  complete MsgPack value, decoded via one-shot `msgpack.deserialize`.
  Same simplification on the Python side: `Unpacker.feed` loops →
  `msgpack.unpackb(payload)`.

Updated all four Connection subclasses (`FletSocketServer`,
`FletDartBridgeServer`, `flet_web.fastapi.FletApp`, `PyodideConnection`)
and all five Dart transports (socket, WebSocket, JavaScript/postMessage,
mock, JS stub) to the new framing. Pyodide outbound uses Transferable
ArrayBuffer for zero-copy across the worker boundary.

Three smoke tests in `packages/flet/test/transport/data_channel_test.dart`
cover factory allocation, inbound routing by channel id, and the outbound
muxed packet shape.

* feat(flet-charts): migrate MatplotlibChartCanvas to DataChannel

Replaces the `_invoke_method`-based `apply_full` / `apply_diff` / `clear`
plumbing with a dedicated `DataChannel` carrying 1-byte-opcode frames
(0x01=full PNG, 0x02=diff PNG, 0x03=clear). PNG bytes no longer pay
MsgPack encode/decode — they flow at memory-bandwidth-class speed in
embedded native mode and at near-bandwidth speed in dev/web modes (raw-
byte frames muxed over the protocol transport).

Backpressure follows the WebAgg pattern: Dart sends a 1-byte `[0xFF]`
ack back over the same channel after each apply chain resolves; the
canvas exposes `set_on_frame_applied(callback)` so `MatplotlibChart`
clears `_waiting` only after Dart confirms the frame painted, mirroring
mpl.js's `img.onload → waiting=false` flow. Without this gate,
interactive drags pile up frames in the Dart-side queue and replay in a
burst.

The 3D example (`examples/.../matplotlib_chart/three_d/main.py`) adds a
status bar showing avg full/diff frame size, total bytes transferred,
sliding-window transfer speed, FPS, and per-stage latency (dart-side
paint vs mpl-side render+idle) so users can see where time is spent.

GPU / CPU strategy code in both State subclasses is unchanged — only
the source of frames switched from `_invokeMethod(...)` to the channel
listener.

* refactor(build): split native FFI runtime out of main.dart for web compat

`flet build web` was failing to compile with errors like "Type 'Pointer'
not found" because the build template's `main.dart` unconditionally
imported `package:serious_python/bridge.dart` and
`package:serious_python/serious_python.dart`, both of which transitively
pull in `dart:ffi` types via `package:serious_python_platform_interface`.
`dart:ffi` isn't available in the web compile target.

Extract everything that touches `serious_python` into a separate
`native_runtime.dart`:

* `initBridges(envVars) → pageUrl` — creates the protocol + exit
  PythonBridge instances and stamps env vars.
* `channelBuilder`, `dataChannelFactory` getters for the embedded
  PythonBridge-backed transports.
* `runPython(...)` — wraps `SeriousPython.runProgram` + the exit-bridge
  listener.
* `extractAppAssets(...)` — wraps `extractAssetZip`.
* The `_DartBridgeBackendChannel`, `_PythonBridgeDataChannel`, and
  `_PythonBridgeDataChannelFactory` impls.

`main.dart` now uses a conditional import:

    import 'native_runtime_stub.dart'
        if (dart.library.ffi) 'native_runtime.dart' as nrt;

On web, the stub (`native_runtime_stub.dart`) is selected — every
entry point either returns null or throws `UnsupportedError`, and is
guarded behind `kIsWeb` so the stub is never reached at runtime. The
result: `flet build web` compiles cleanly without `dart:ffi` ever
entering the compile graph.

No behavior change on native (mobile/desktop) builds — they pick up the
real `native_runtime.dart` via the conditional and execute the same code
that lived in `main.dart` before.

* fix(web): switch Pyodide worker to module type + pyodide.mjs

Pyodide >= 0.29 (and 314.0.0, the Python 3.14 line) throws "Classic web
workers are not supported" inside any worker where `importScripts` is
callable. python-worker.js was spawned as a classic worker, so booting
the Python 3.13 / 3.14 lines surfaced a hard error before any user code
ran.

Switch to module workers across both the flet web client and the
`flet build` template:

* `new Worker(url, { type: "module" })` — module workers don't expose
  `importScripts`, so Pyodide's check passes.
* `importScripts(pyodideUrl)` → `const { loadPyodide } = await
  import(pyodideUrl)` — the dynamic-import form module workers must
  use.
* All `pyodideUrl` defaults flip from `pyodide.js` to `pyodide.mjs` —
  the ES-module variant has the named export the dynamic import expects.

URL injection paths:

* `flet publish` / `flet run --web` go through `patch_index.py`, which
  now injects `pyodide.mjs` URLs (both CDN and `--no-cdn` branches).
* `flet build web` uses the cookiecutter template's index.html, which
  was hardcoded at `/pyodide/pyodide.js` regardless of `--no-cdn`.
  Replace with a Jinja conditional that honors `cookiecutter.no_cdn`
  and uses the new `cookiecutter.pyodide_version` variable for the
  jsdelivr CDN URL. `build_base.py` populates `pyodide_version` from
  the resolved `python_release.pyodide`.

Forward-compatible across all three supported Pyodide lines:
0.27.7 (Python 3.12), 0.29.4 (Python 3.13), 314.0.0 (Python 3.14).
Older lines accept module workers too; 0.29+ require them.

* docs(0.86.0): DataChannel + protocol framing breaking-change guide

* CHANGELOG: new features (DataChannel API), improvements (length-prefix
  framing + type-byte discriminator, StreamingMsgpackDeserializer
  removed), breaking changes (wire format on stream transports, mixed
  flet versions across `flet run` CLI and runtime no longer supported).
* New breaking-changes guide
  `data-channel-protocol-upgrade.md` — migration notes for users with
  custom backends speaking the Flet protocol, plus a heads-up for
  anyone subclassing `MatplotlibChartCanvas` (the Dart-side
  `_invokeMethod` handler no longer fires).
* Add the new guide to the 0.86.0 entry in the breaking-changes index.

* perf(web): transfer Pyodide-worker bytes to main via Transferable ArrayBuffer

The worker→main `postMessage` path was structured-cloning every bulk
payload (matplotlib PNG frames, etc.) — measurable cost at ~300 KB
per frame. Switch to Transferable: extract the Uint8Array's
underlying ArrayBuffer and pass it in the second argument to
postMessage. Main thread receives the buffer with ownership transferred,
no copy.

The matching main→worker (Dart→Python) direction already used
Transferable since the DataChannel landing. Both directions are now
zero-copy across the worker boundary on Pyodide.

This does not move the matplotlib bottleneck — that's WASM-compute-bound
on mplot3d — but trims a few ms of structured-clone cost per frame and
makes the perf budget closer to what the dart_bridge embedded path
delivers natively.

* fix(flet-charts): restore await-based backpressure for matplotlib frames

The sync `apply_full` + side-channel `_on_frame_applied` callback was
losing matplotlib "draw" events in pyodide mode. Sequence:

1. `_receive_loop` reads frame bytes, calls `apply_full(bytes)` — sync,
   returns immediately.
2. Loop iterates, reads next event from `_receive_queue`.
3. Next event is a `"draw"` notification matplotlib emitted just after
   the previous frame (figure dirty again from mouse drag).
4. Gate check: `_waiting=True` (ack hasn't arrived from Dart yet) →
   **drop the event**.
5. Ack arrives 200+ ms later, `_waiting=False`, but the queue is empty
   and matplotlib doesn't re-emit "draw" until next mouse event.

Result in pyodide: ~1.5 fps observed, vs the 0.85 `_invoke_method`
implementation's much higher rate. The 0.85 pattern wasn't faster
because it lacked an ack — it had one (the INVOKE_METHOD reply). It
was faster because `await self._invoke_method(...)` **blocked the
`_receive_loop`** during the round-trip, so matplotlib events queued
naturally in `_receive_queue` and were processed in order after the
await returned, rather than being eagerly drained against a stale gate.

Fix: re-introduce the await pattern at the canvas level.

* `MatplotlibChartCanvas.apply_full / apply_diff / clear` are now
  async. Each enqueues a per-frame `asyncio.Future`, sends the channel
  packet, and awaits the future.
* `_on_dart_message` resolves the head future when `[0xFF]` arrives.
* `MatplotlibChart._receive_loop` awaits each `apply_*` call —
  matplotlib events that arrive during the wait stay queued and are
  processed after the ack returns. Same behaviour shape as 0.85's
  `_invoke_method` round-trip, but over the DataChannel transport
  (no msgpack on the bulk payload).
* `set_on_frame_applied(cb)` is preserved as a pure observer callback
  for instrumentation (e.g. the 3D example's stats panel) — no longer
  load-bearing for backpressure.

The 3D example's `apply_full` / `apply_diff` wrappers updated to
`async def` + `await` accordingly.

* ci: fix web client build after flet.version.pyodide_version removal

The multi-version Python PR (#6577) removed flet.version.pyodide_version
but the 'Get Pyodide version' step still read it, failing every
'Build Flet Client for Web' run. Resolve the version from the
flet_cli.utils.python_versions registry instead (default release's
Pyodide), and replace the hand-rolled tarball + wheel downloads with
flet_cli.utils.pyodide.ensure_pyodide — the hardcoded
micropip-0.8.0/packaging-24.2 filenames would have silently broken on
the new Pyodide line (3.14's lock resolves micropip 0.11.1), since
curl without -f writes 404 pages into the .whl files.

Cherry-picked from 2d8f4a1 on fix-android-arch-filtering.

Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>

* docs(breaking-changes): drop dead /docs/extending-flet/data-channels link

The 0.86 protocol-framing breaking-change guide linked to a DataChannel
API reference page that doesn't exist yet — there's no extending-flet/
folder, and no DataChannel doc has been authored. Docusaurus' broken-link
scan failed the docs build on every push. Replace the link with prose
pointing at the data_channel.py module docstring; dedicated reference
pages can land in a follow-up once the API doc generator covers it.

* ci: bump Node 20 actions to Node 24 versions

GitHub Actions emitted Node.js 20 deprecation warnings on every job in
run 27457389406. Node 20 will be removed from runners 2026-09-16. Bump
the affected actions to their latest Node 24 majors across all workflows:

- actions/checkout@v4 → v6
- actions/setup-node@v4 → v6 (v6 limited auto-cache to npm, the website
  uses yarn via corepack — no caching behavior change)
- actions/upload-artifact@v4 / v5.0.0 → v7
- actions/download-artifact@v4 → v8
- astral-sh/setup-uv@v6 → v8.2.0 (v8 dropped the major @v8 tag for
  supply-chain reasons, full tag required)
- dart-lang/setup-dart@<old SHA> → v1.7.2

All six actions' action.yml now declare `runs.using: node24`.

* fix(tester): preserve ValueKey value type in find_by_key

`ValueKey(controlKey.value)` produces `ValueKey<Object>` because
`controlKey.value` is statically typed Object. Flutter's `ValueKey.==`
is runtimeType-strict, so `ValueKey<Object>('foo')` never equals
the `ValueKey<String>('foo')` that ControlWidget assigns to the
rendered widget — making `find_by_key("foo")` from Python tests
find 0 widgets.

Mirrors the fix already applied in control_widget.dart (7367050).
Switch-dispatch on the runtime type so String → ValueKey<String>,
int → ValueKey<int>, etc.

Resolves the cascade of "RangeError: no indices are valid: 0" and
"assert 0 == 1" failures across apps, controls/core, controls/material,
controls/cupertino, and controls/theme integration suites.

* fix(tests): update example imports after folder rename in #6545

#6545 renamed 131 example folders (mostly basic/ → descriptive
control name, plus example_1/2/3, nested_themes_1/2 collapsing, and
removing the basic/ wrapper where there was only one example) but the
matching imports in packages/flet/integration_tests/examples/ were
never updated. Test collection failed with ModuleNotFoundError on
every affected suite (examples/apps, examples/extensions, and
examples/controls/{core,cupertino,material}).

Rewrites the 45 test files referencing those modules to the new paths
derived from the rename history of commit 1b2e914.

* Docusaurus 3.10.1 and Node.js 24

* feat(cli): add 'flet --version --json' and source CI version/dep reads from it

Add a --json flag to 'flet --version' that emits a machine-readable
document (Flet/Flutter versions, supported Python/Pyodide table, Linux
build deps). CI workflows and the publish docs now read it via jq instead
of importing Flet internals with 'python -c'.

Move the canonical Linux apt dependency list from flet.utils.linux_deps
(runtime package) to flet_cli.utils.linux_deps (build tooling), where it
sits next to python_versions.py and is a same-package import for the CLI.

* ci: pin Windows runners to windows-2025-vs2026

GitHub is redirecting windows-latest to windows-2025-vs2026 by June 15,
2026. Pin the label explicitly in the Flet Build Test and Build & Publish
workflows to silence the redirect notice and make the image deterministic.

* Allow flutter_secure_storage updates (^10.0.0)

Update pubspec.yaml dependency for flutter_secure_storage from fixed 10.0.0 to caret ^10.0.0, allowing compatible minor/patch updates instead of pinning to a single patch version. This lets the package accept backwards-compatible releases without manual changes. Fix #6586

* Resolve Python/Pyodide versions from python-build's manifest

Drop flet's hand-mirrored SUPPORTED_PYTHON_VERSIONS table and load the
supported Python/Pyodide/dart_bridge set from python-build's date-keyed
manifest.json — the single source of truth shared with serious_python.

- python_versions.py: pin one PYTHON_BUILD_RELEASE_DATE; fetch that release's
  manifest.json (cached immutably under ~/.flet/cache/python-build, offline
  fallback to cache) and parse it lazily. Module constants become
  get_supported_python_versions()/get_default_python_version(); resolution
  logic unchanged. Dev/CI overrides: FLET_PYTHON_BUILD_RELEASE_DATE,
  FLET_PYTHON_BUILD_MANIFEST.
- flet build: pass only SERIOUS_PYTHON_VERSION; serious_python derives the full
  version, build date, and dart_bridge version from its committed snapshot.
  Drops the SERIOUS_PYTHON_FULL_VERSION/SERIOUS_PYTHON_BUILD_DATE exports.
- flet --version: drop the Python/Pyodide matrix (stays offline); --json keeps
  flet/flutter/linux_dependencies.
- ci.yml: read the default Pyodide version via the manifest-backed resolver
  instead of jq over `flet --version --json`.
- Docs: update the removed-pyodide-version-export guide + CHANGELOG to the new
  accessors; document the pin in CONTRIBUTING.
- Add offline tests driven by FLET_PYTHON_BUILD_MANIFEST.

* Pin screen_brightness_macos to 2.1.2 (SPM macOS deployment-target regression)

screen_brightness_macos 2.1.3 ("Fix: swift package manager warning") ships a
Package.swift declaring macOS 10.11, below FlutterFramework's 10.15 SPM floor,
so `flutter build macos` fails to resolve with Swift Package Manager enabled.
Pinning the app-facing screen_brightness alone doesn't help — the federated
macOS implementation is separately versioned. Override the impl to the last
good 2.1.2 in both the build template and the client app.

Upstream: aaassseee/screen_brightness#99

* flet build: clean build dir when the bundled Python version changes

Switching --python-version (or requires-python) between builds left the previous
version's compiled bytecode in the reused build directory's native bundles
(stdlib/site-packages .pyc), crashing the app at runtime with
`ImportError: bad magic number`. Record the resolved Python short version in the
build dir and, when it changes, wipe the build dir so the native bundles are
regenerated for the new interpreter.

* feat(testing): add `flet test` for on-device integration testing

Let Flet users write and run integration tests for their apps. Tests live in
`tests/` and drive the app running on-device (built monolithic app with embedded
Python over dart_bridge): find controls by key/text, tap, enter text, assert
state and screenshots.

- New `flet test` CLI command (mirrors `flet debug`): provisions a Flutter test
  host via the build pipeline in test mode, packages the app's Python, then runs
  pytest. Supports platform positional + `--device-id`, `-k`, `-u` (goldens), `-v`.
- pytest plugin shipped with `flet` (zero boilerplate): function-scoped
  `flet_app` fixture (fresh app per test); also runs via plain `uv run pytest`,
  with `FLET_TEST_PLATFORM`/`FLET_TEST_DEVICE`/`FLET_TEST_GOLDEN` env overrides.
- Independent tester channel: Dart `RemoteWidgetTester` <-> Python `RemoteTester`
  over a raw socket with length-prefixed JSON frames, separate from Flet's
  transport. The Flutter WidgetTester driver moved into `packages/flet` behind
  `package:flet/testing.dart`, shared by host (`runFletHostTest`) and device
  (`runFletDeviceTest`) modes.
- `flet create` scaffolds `tests/test_main.py` + pytest config; build template
  gains a test_mode-gated integration_test entry point.
- Docs: getting-started/integration-testing guide + cli/flet-test reference.

* fix(testing): extract integration-test driver into flet_integration_test package

`dart pub publish --dry-run` for `packages/flet` failed: its lib/ imported the
dev-only `flutter_test`/`integration_test` packages, which pub forbids (packages
used in lib/ must be in `dependencies`). Putting the driver inside `flet` was the
wrong call — it can't ship to pub.dev that way.

Move the concrete Flutter driver (flutter_tester, flutter_test_finder,
device_test, host_test, remote_widget_tester, frame_decoder) into a new
`packages/flet_integration_test` package (publish_to: none) that depends on flet
+ integration_test. flet's published lib/ no longer references any test-only
package; the abstract Tester/TestFinder interfaces stay in flet as before.

- packages/flet: drop integration_test dev-dep, remove lib/testing.dart entry.
- packages/flet_integration_test: new package; cross-package imports of
  Tester/TestFinder/Lock collapse to package:flet/flet.dart; redundant dart:io
  imports dropped (flet re-exports it). Standard Flutter .gitignore.
- client + build template: import package:flet_integration_test instead of
  package:flet/testing.dart; add it as a path dev-dependency (test_mode-gated in
  the template).
- build_base: for local dev, rewrite the flet_integration_test path the same way
  it already rewrites flet (it's publish_to:none, only resolvable from the repo).

Verified: flet `pub publish --dry-run` passes; flet_integration_test and client
integration_test analyze clean.

* fix(testing): inject test driver at build time instead of templating it

The build template referenced flet_integration_test by a repo-relative path
gated with a Jinja `{% if %}` block. That broke two things for the released
(zipped) template:

1. The release pipeline patches the template pubspec with patch_pubspec_version.py,
   which does a yaml.safe_load round-trip. The uncommented `{% if %}` block made
   the pubspec invalid YAML, so the patch/zip step would fail on tag.
2. A repo-relative path can't resolve once the template is zipped and shipped.

Stop putting the test dependencies in the template pubspec. flet-cli now injects
them after rendering (build_base.create_flutter_project), gated on test_mode:
- local dev: flet_integration_test by path (+ dependency_override), like flet.
- end user: flet_integration_test as a git dependency pinned to this flet
  version's tag (the package is publish_to:none, never on pub.dev) — consistent
  with how the template already pulls serious_python from git.

The template pubspec is now plain valid YAML again (the patch tooling round-trips
it cleanly) and a normal `flet build` never pulls the test driver.

flet_integration_test depends on flet by version (not path) with a local
dependency_override, so flet unifies to a single source across the graph whether
it's consumed by path (repo) or git (user); flutter_test becomes a regular dep so
test hosts get it transitively.

Verified: template pubspec parses; patch_pubspec_version.py round-trips it in both
release and dev modes; `flet test` provisioning injects the deps and
`flutter pub get` resolves; flet_integration_test analyzes clean.

* docs(testing): add Testing types reference + link API symbols in guide

Add a "Testing" section under Reference > Types with stubs for FletTestApp,
Tester, Finder and DisposalMode (website/docs/types/testing/), wired into the
sidebar. Replace the stale top-level mkdocs-style stubs (types/finder.md,
flettestapp.md, tester.md) that used the old `:::` syntax.

Link every API class/method/property mentioned in the integration-testing guide
to its reference page using the `[label][flet.testing.Symbol]` xref format, like
other docs.

* docs(testing): fix unresolved reST xrefs in FletTestApp docstrings

The new FletTestApp reference page surfaced two reST cross-references that didn't
resolve (caught by the docs build's reST xref check):

- `FletTestApp.tester` referenced the internal, undocumented
  `flet.testing.remote_tester.RemoteTester` via :class: — changed to plain code.
- `create_gif` referenced `:meth:`Page.take_animation``; the documented symbol is
  `flet.BasePage.take_animation` — corrected the target.

Verified: full docusaurus build + check_docs.sh pass (reST xrefs OK).

* ci(testing): add flet-test workflow + Counter test app

New `.github/workflows/flet-test.yml` runs `flet test` (on-device) across macOS,
iOS, Windows, Linux and Android in a single matrix, against a new
`sdk/python/examples/apps/flet_test_counter` app (keyed +/- buttons,
interaction-only test, no goldens). The embedded app is built with Python 3.14;
the host venv stays on 3.13. Per-platform handling: xvfb for Linux, a booted
simulator (UDID) for iOS, reactivecircus/android-emulator-runner (KVM) for
Android. Pins the in-repo flet packages like flet_build_test.

Allowlist UDID/udid in typos (legitimate iOS term + simctl JSON field).

Verified locally: `flet test macos --python-version 3.14` -> 1 passed.

* fix(testing): drive device-mode integration tests with benchmarkLive

After merging flet-0.86, `flet test` (device mode) hung: the very first
`WidgetTester.pump()` never returned, so the tester never connected and the run
timed out.

Root cause (isolated by single-variable bisection): the build template's #6616
`BootHost` boot structure (`runApp(StatefulWidget)` -> `initState` ->
`await prepareApp()` -> `setState`) deadlocks the default `fadePointers` frame
policy under `flutter test` — `pump()` schedules a frame and blocks on
`_pendingFrame` until it's drawn, but that frame never arrives during this boot.
Swapping only `BootHost` back to the old `runApp(FutureBuilder(...))` shape made
it pass reliably on the same Flutter 3.44.3, and the old structure on 3.44 was
fine — so it's the boot structure, not the Flutter bump or the boot screen
animation.

Fix lives entirely in the test driver (no shipping-app/template change):
- `runFletDeviceTest` sets `framePolicy = benchmarkLive` — Flutter's documented
  policy "for running the test on a device". Its `pump()` doesn't wait on an
  engine-drawn frame (just delays), while framework-requested frames (the app's
  setState/animations, incl. Python's dart_bridge updates) still render.
- Because benchmarkLive pumps don't advance wall-clock or force frames, the
  driver waits for *sustained* tree idle (the boot screen's CircularProgress
  Indicator keeps it busy until Python renders the page) before connecting, and
  `FlutterWidgetTester.pumpAndSettle` (gated on benchmarkLive only, so host mode
  is untouched) pumps with real delays until the tree stays idle — so async
  tap -> on_click -> control-update round-trips land before asserting.

Verified: counter app (+/- buttons, 0 -> 1 -> -1) passes 3/3 on macOS / Flutter
3.44.3; host-mode driver unchanged.

* fix(testing): use FutureBuilder boot path under test + propagate flutter test failures

Replaces the benchmarkLive approach (previous commit), which was wrong: it
un-blocked WidgetTester.pump() but its continuous redraw triggered a
rebuild-during-build (`!_dirty`) that *failed* the on-device flutter test — and
that failure was hidden by a false-green bug (below). Verified by comparison on
Flutter 3.44.3: BootHost+benchmarkLive => `!_dirty`, 0 passed/1 failed; the
FutureBuilder boot path => `All tests passed!`, clean, 3/3.

Two fixes:
- Template main.dart: under `FLET_TEST`, boot via the old
  `runApp(FutureBuilder(future: prepareApp(), ...))` shape instead of #6616's
  `BootHost`. BootHost's StatefulWidget/initState-async/setState boot deadlocks
  `tester.pump()` (it blocks on a frame that never arrives during boot);
  FutureBuilder is driven cleanly. Production builds are unchanged (still
  BootHost). The app — embedded Python over dart_bridge, FletApp — is identical.
- FletTestApp.teardown: check the `flutter test` process exit code and raise if
  non-zero. The host-side find/tap assertions can all pass while the on-device
  `testWidgets` body fails (e.g. a widget exception), so pytest was reporting a
  pass over a failed flutter run — a false green. Now surfaced.

Revert the benchmarkLive changes to the device driver (device_test.dart,
flutter_tester.dart) — default frame policy works with the FutureBuilder path.

Verified: counter (+/-, 0 -> 1 -> -1) genuinely passes 3/3 on macOS / 3.44.3
(`All tests passed!`, no `!_dirty`, pytest + flutter agree).

* fix(testing): use resolved Flutter exe path when spawning flutter test (Windows)

On Windows the device-mode run failed at fixture setup with
`FileNotFoundError [WinError 2]`: FletTestApp spawned `flutter test` via
`create_subprocess_exec("flutter", ...)`, but Windows' CreateProcess does no
PATHEXT lookup, so a bare "flutter" (really `flutter.bat`) isn't found.

`flet test` already resolves the real Flutter executable (full path, `.bat` on
Windows) for provisioning — pass it to the pytest subprocess as
`FLET_TEST_FLUTTER_EXE` (and propagate it via `_TEST_ENV_KEYS`), and have
FletTestApp use it as argv[0], falling back to a bare "flutter" on PATH.

* fix(testing): name the test binary after project_name (Windows/Linux desktop)

Windows and Linux `flet test` failed after a successful build with
`Unable to start executable ... Failed to find "<project_name>.exe/binary"`.
Root cause (the "doesn't build on desktop" hypothesis was wrong — Flutter does
build): the Windows/Linux runner sets the executable OUTPUT_NAME to
`artifact_name`, but `flutter test -d <desktop>` launches the binary by the
Flutter pubspec `name` (== project_name). When `[tool.flet] artifact` differs
from the project name (e.g. `flet-test-counter` vs `flet_test_counter`), the
built binary and the launched path don't match. macOS is unaffected (its `.app`
is located by the product/artifact name).

In test mode, pin `artifact_name = project_name` so the desktop binary's name
matches what the integration-test host launches. Verified: macOS still passes
(now builds `flet_test_counter.app`); fixes the Windows/Linux launch path.

* fix(test): pass serious_python native-build env to flet test

flet test spawns its own 'flutter test integration_test' (via FletTestApp)
instead of going through _run_flutter_command, so it never received the
serious_python build-time env that flet build sets. Most critically
SERIOUS_PYTHON_APP was unset, which makes the Android packageApp Gradle task
early-return and leave a stale app.zip (old-Python main.pyc) in the APK,
crashing the embedded runtime with 'ImportError: bad magic number'.

Extract the serious_python native-build env into a shared
_serious_python_build_env() and use it from both _run_flutter_command and
flet test's _flutter_path_env, so the two paths bundle an identical app and
can't drift. Adds SERIOUS_PYTHON_APP, SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES
and SP_NATIVE_SET to the test env (and _TEST_ENV_KEYS).

* ci(flet-test): capture android logcat; force software GL on linux

Android: stream device-side logs (embedded Python stdout/stderr, Flet,
Flutter, native crashes) to a file during the run and dump them in a
collapsible group afterwards, so on-device failures are diagnosable from CI.

Linux: xvfb has no GPU, so the Flutter GTK app crashes on GL context
creation (exit 79); install Mesa's software GL (llvmpipe) and force it via
LIBGL_ALWAYS_SOFTWARE/GALLIUM_DRIVER.

* ci(flet-test): non-blocking android logcat dump; linux GL diagnostics

Android: stop streaming logcat live (verbose device logs bog down the
software emulator and stall the job); instead dump the relevant slice of the
ring buffer after the run with non-blocking 'adb logcat -d'.

Linux: add a failure-diagnostic step that reports the active GL renderer
(glxinfo) and runs the built bundle directly to surface its exit-79 crash
output, which the test harness otherwise swallows.

* fix(flet-test): wait for first render in counter test; robust CI diagnostics

The counter test asserted on the first frame, but on a device the embedded
Python cold start (interpreter init + import flet + main()) can take several
seconds — longer than the device driver's fixed warmup — so find_by_text('0')
ran before the app rendered and returned 0 on the slow CI emulator (passed
locally on a faster one). pump_and_settle only settles Flutter frames, not a
pending python->dart round-trip, so poll for the first render instead.

CI: make the android logcat dump run even on failure (|| CODE=$?) with a
tight python+crash filter that won't bog the emulator; cap the linux
bundle-diagnostic with timeout so it can't hang the job.

* ci(flet-test): non-blocking android logcat; disable AT-SPI a11y on linux

Android: drop the background 'adb logcat &' (a streaming child can keep the
emulator-runner script from finishing); dump the ring buffer after the run
with non-blocking 'adb logcat -d' instead.

Linux: the app and software GL are fine (glxinfo shows llvmpipe; the bundle
runs directly without crashing) — exit 79 is specific to the integration_test
path, which enables the semantics tree and makes GTK embed an ATK a11y socket
that doesn't exist under xvfb. Disable the AT-SPI bridge (NO_AT_BRIDGE/GTK_A11Y).

* fix(flet-test): kill android false-green; poll 60s for render; logcat to artifact

The android job reported success while pytest actually failed: the
emulator-runner ran the multi-line script such that 'exit $CODE' saw an empty
CODE (and a '\'-continuation in the logcat line broke, dumping the entire
unfiltered logcat = ~58k console lines). Run the script as a single folded
line so the test command is last and its exit code is the job's, and write a
filtered device log (embedded Python + crashes only) to a file via an EXIT
trap, uploaded as an artifact instead of streaming to the console.

Also: the counter never rendered within the 10s poll window on the slow CI
emulator (cold-start embedded Python is much slower there), so poll on a 60s
deadline instead of a fixed 40 attempts.

* test(flet-test): pull serious_python from x86_64 sysconfigdata fix branch

Temporarily override serious_python_android + serious_python_platform_interface
to flet-dev/serious-python#218 (fix/android-x86_64-sysconfigdata) so the android
x86_64 CI leg validates the fix end-to-end (embedded Python no longer crashes
with ModuleNotFoundError: _sysconfigdata__android_x86_64-linux-android).

Locally confirmed: pubspec.lock resolves to the branch and stdlib.zip now ships
both aarch64 and x86_64 _sysconfigdata. Revert to the pub.dev release once #218
ships.

* ci(flet-test): add 40-min job timeout so a hung emulator auto-cancels

The android on-device run can wedge (emulator goes offline) and run until the
default 6h limit; cap the job at 40 minutes.

* fix(testing): don't hang in RemoteTester.stop() waiting on a dead client

After an on-device test passes, teardown calls RemoteTester.stop(), which did
'await self._server.wait_closed()' with no timeout. wait_closed() blocks until
the active _handle_client finishes, but _read_loop blocks on readexactly() until
EOF — and the on-device app's socket close doesn't always deliver EOF to us
(seen on Linux), so the asyncio loop hung forever after 'All tests passed!' (the
flet test process never exits). Cancel the read task so _handle_client completes,
close the writer, and bound wait_closed() with a timeout.

* fix(testing): target generated Flutter device test driver

* ci(flet-test): capture x86_64 linux integration-test verbose diagnostic

The linux job fails with 'No tests were found' + exit 79 on the x86_64 official
flutter (passes on arm64). flet_test_app already uses the file-form target and
verifies app_test.dart is non-empty, so it's neither. Re-run the integration
test directly with --verbose (unreachable dummy server) to capture which build
target/entrypoint flutter uses, whether the testWidgets body runs, and the exit
reason; upload the full verbose log as an artifact.

* fix(testing): skip linux ready-to-show wait under flet test

* fix(testing): show linux test window without ready wait

* fix(testing): size hidden linux integration test surface

* ci(flet-test): remove linux diagnostic artifact

* refactor(testing): use directory target, keep generated-driver guard

The dir->file change in 17d368b was not what fixed Linux (the window-realize
/ ready-to-show fixes were; both the dir and file forms reported 'No tests were
found' until then). Revert the flutter test target to the directory form
('integration_test') and keep only the useful part: in device mode, validate
the generated integration_test/app_test.dart exists and is non-empty so a
missing/empty driver surfaces as a clear error instead of a confusing
'No tests were found'.

* test(flet-test): simplify counter test to plain pump_and_settle + assert

Drop the _find_text_when_ready polling helper. It was a band-aid for the
android render race, but the real cause was the serious_python x86_64 crash
(PR #218) — now fixed. Try the plain template-style test and let CI confirm the
counter renders in time on the slow emulator.

* Bump Flutter SDK to 3.44.4

Update .fvmrc to pin Flutter version 3.44.4 (patch bump from 3.44.3) to ensure a consistent SDK across development and CI environments.

* ci(flet-test): drop dead mesa-utils + obsolete a11y env

- mesa-utils was only used by glxinfo in the (removed) Diagnose Linux step.
- NO_AT_BRIDGE/GTK_A11Y were added mid-debugging but didn't fix Linux (the
  window realize / ready-to-show change did); the Atk-CRITICAL warnings were
  non-fatal. Remove them and the now-inaccurate comment. Keep the software-GL
  env (xvfb has no GPU) and the android logcat artifact (only window into
  on-device failures).

* feat(cli): install Flutter on arm64 Linux via git clone; add CI leg

Flutter publishes no prebuilt arm64 Linux SDK (releases are x64-only), so
flet-cli's install_flutter downloaded a broken x64 tarball on arm64 Linux. For
arm64 Linux, clone the SDK at the version tag instead; the first 'flutter' run
then fetches the arch-appropriate engine/Dart artifacts (how fvm/git installs
work).

Add a 'linux-arm64' CI leg (ubuntu-24.04-arm) that skips the Flutter setup
action so 'flet test' installs Flutter via this path, exercising it end-to-end.

* fix(cli): precache engine after arm64 Linux Flutter clone

A bare git clone has no bin/cache, so 'dart run serious_python:main' failed
with 'could not find package sky_engine ... solving failed'. Run
'flutter precache --linux' right after the clone to populate the engine
artifacts (sky_engine + the Linux desktop engine) the prebuilt archives ship.

* test(flet-test): matrix Python 3.12/3.13/3.14; app shows + test asserts version

- CI matrix now crosses each platform with python 3.12/3.13/3.14 (job env
  PYTHON_VERSION/EXPECTED_PYTHON_VERSION from matrix; dropped the workflow_dispatch
  python_version input the matrix supersedes).
- Counter app displays 'Python <platform.python_version()>'.
- test_counter asserts the app reports the expected major.minor
  (EXPECTED_PYTHON_VERSION), falling back to 'any version shown' locally.
  Validated on macOS (renders Python 3.14.6; 1 passed).

* chore: use released serious_python 4.1.1; drop temp git override

serious_python 4.1.1 (with the x86_64 _sysconfigdata fix, PR #218) is on
pub.dev. Bump the build template serious_python 4.1.0 -> 4.1.1 and remove the
temporary git override from flet_test_counter (the fix branch was deleted after
release, which broke 'flutter pub get' on fresh runners — the linux-arm64 legs
failed with 'could not find git ref fix/android-x86_64-sysconfigdata').

* test: remove test_flet_test_app.py unit test

It imported FletTestApp, which pulls in the screenshot-comparison deps
(numpy/Pillow/scikit-image) from the optional 'test' extra at module load.
The base unit-test suite installs flet without that extra, so collection
failed with ModuleNotFoundError: No module named 'numpy'. The
__flutter_test_target device-mode guard is exercised end-to-end by the
flet-test on-device workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: ndonkoHenri <robotcoder4@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant