Skip to content

EDGE: SwiftUI/Compose renderers, native testing suite, and the v4 element pipeline#173

Open
shanerbaner82 wants to merge 213 commits into
mainfrom
element
Open

EDGE: SwiftUI/Compose renderers, native testing suite, and the v4 element pipeline#173
shanerbaner82 wants to merge 213 commits into
mainfrom
element

Conversation

@shanerbaner82

Copy link
Copy Markdown
Contributor

Summary

This is the accumulated element branch — the EDGE element pipeline and its supporting work — merging into main. It is large (180 commits, 336 files, ~+42k/−4.5k) and represents a major release's worth of changes. Highlights by area:

Rendering

  • Yoga removal — replaced the Yoga C++ layout engine + UIKit view tree with a pure SwiftUI Layout implementation on iOS and a Compose Layout on Android.
  • Cross-platform Android↔iOS visual parity work (cross-axis stretch defaults, font-padding alignment).
  • Chrome seam decoupling core from the native-ui plugin (NativeRootHostRegistry + ChromeContributorRegistry + theme/transition).

Animation & interaction

  • Declarative animations, transforms, press feedback, SharedValue + gesture-area, with cross-platform parity.

Testing

  • Native::test() component testing suite (FakeBridge + TestableComponent), plus this session's coverage: assertTransition, wire-event interactions, FakeBridge scripting/recording, and Route::native HTTP registration.

Platform / native

  • Geolocation control plane (watchPosition()/clearWatch() + background watches).
  • Android runtime hardening: park-and-reuse persistent runtime, once-per-process guards, SHUTDOWN wire event, adb timeouts.
  • Pointer-backed native→PHP event channel.
  • Jump/dev-server lifecycle fixes (multiple dev servers, hot-reload nav-stack restore, session takeover).

Accessibility

  • HasA11y on the base Element, assertAccessible(), Image alt text, NavAction a11y labels + audit rules.

Reactivity

  • Livewire-style Computed/Lazy/Poll attributes on NativeComponent.

Test plan

  • Full local suite green: 446 passed (1463 assertions), 3 skipped, 4 risky.

Note: this branch bundles a very large surface area. Reviewers may prefer to review by theme using the commit grouping above.

🤖 Generated with Claude Code

shanerbaner82 and others added 30 commits February 18, 2026 11:04
Replace coroutine-based parallel unzip with direct-to-disk streaming to
reduce memory overhead. Update PluginMakeHookCommand to use new plugin
accessor methods instead of accessing manifest properties directly.
…d AGP bump

- Add compile_sdk, min_sdk, target_sdk config options with build-time replacement
- Block signals during php_embed_shutdown to prevent TSRM mutex crashes
- Reuse PHP runtime for artisan commands instead of reinitializing
- Replace codename-based binary URLs with versioned bin.nativephp.com URLs
- Bump AGP from 8.13.1 to 8.13.2
- Rewrite Android renderer to V2 wire format with string-typed nodes, generic props, and pluggable renderer registry
- Add Edge element system with 26 native elements, Blade component rendering, Tailwind class parser, and callback registry
- Add NativeComponent lifecycle, NativeRouter navigation, and artisan scaffolding commands (native:make, native:rm, native:validate)
- Extend plugin system with UI component plugin support, renderer registration codegen, and manifest validation
- Add Edge component and Blade rendering tests, update Android binary download URLs
…iveElementCollector calls

Bypasses Blade's ComponentTagCompiler and IoC resolution entirely. Native tags
now compile to NativeElementCollector::leaf()/open()/close() calls, eliminating
class instantiation and sub-view rendering overhead per element.
Arrow functions capture by value, causing duplicate node IDs when siblings
share the same starting counter. Switch to a regular closure with &$nextId.
Extracts text from slot content so <native:button>Click me</native:button>
works the same as <native:button label=\"Click me\" />.
… for native screens

Navigation transitions: Transition enum on PHP side, UI.SetTransition bridge
function, AnimatedContent with configurable enter/exit specs on Kotlin side.
Supports slide, fade, scale, and instant swap.

Tree patching: PHP diffs old vs new render tree and writes minimal patch ops
to shared memory via nativephp_ui_patch(). JNI exposes patch buffer read/ack.
Kotlin applies patches to the live tree without full re-parse.

Hot reload: file changes trigger HOT_RELOAD event through shared memory,
PHP flushes compiled views and writes .hot_restart signal, Kotlin detects
it and re-executes the native route. Includes error screen rendering for
recovery from render failures.

Also: CallbackRegistry now parses method arguments and maintains stable IDs
across re-renders, LazyColumn virtualization for scroll views, container
click handling, and improved adb push reliability in WatchesAndroid.
…stem

Replaces the mmap-based shared memory architecture (NpuiSharedRegion)
with a direct flat buffer system (NphpElementRegion) that eliminates the
watcher thread polling model. PHP now builds flat nodes in malloc'd
buffers and pushes tree updates directly to Kotlin via JNI
(NativeElement_PostTreeUpdate → NativeElementBridge.postTreeUpdate).

Key changes:
- native_functions.c: Host-registered PHP functions compiled into
  libphp_wrapper.so via additional_functions, replacing the separate
  .so extension. Implements nativephp_element_init/publish/wait_event/
  reset/shutdown with flat node serialization and interned type table.
- bridge_jni.cpp: Replaces old UI bridge JNI with Element bridge
  methods. Returns DirectByteBuffer for zero-copy flat buffer access.
  Caches NativeElementBridge class/method refs for direct C→Kotlin push.
- php_bridge.c: Per-request php_embed_init/shutdown with mutex
  serialization. Registers host functions via setup_embed_module().
- NativeElementBridge.kt: Reads flat buffer on PHP thread via JNI,
  parses tree, and posts to main thread for Compose recomposition.
- NativeUIBridge.kt: Stripped to observable state holder, delegates
  all event sending to NativeElementBridge.
- PHPBridge.kt: Persistent runtime with ensureRuntimeInitialized(),
  uses nativeHandleRequest instead of nativeHandleRequestOnce.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Updates NativeComponent and NativeRouter to use the new element runtime
functions (nativephp_element_init/publish/wait_event/reset/shutdown)
instead of nativephp_ui_*. Removes the PHP-side tree diffing/patching
logic from NativeComponent since the flat buffer system handles this
at the C layer.

- NativeComponent: Remove previousTree, diffTree, diffNode. Add
  __syncProperty for two-way model binding with updatedFoo hooks.
- NativeRouter: Use nativephp_element_* for init, reset, publish,
  and shutdown.
- NativeServiceProvider: Return 204 when .hot_restart file exists
  to prevent WebView from loading stale content during hot reload.
- CallbackRegistry: Add lookup() method for reverse expression→ID
  resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NativeTagPrecompiler: Expand @model="propName" into :value and
  _change bindings that call __syncProperty for two-way data binding.
- Element: Support 2-value and 3-value padding/margin shorthand
  (CSS-style: vertical/horizontal, top/horizontal/bottom).
- UIFunctions.SetBackground: New bridge function to set the Activity
  window background color, controlling what shows behind transparent
  system bars and safe area insets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces end-to-end interaction latency tracking across the full
pipeline (Compose click → PHP processing → tree update → frame drawn)
and per-frame Android rendering pipeline breakdown via FrameMetrics.

- PerformanceTracker: Tracks T0-T3 round-trip for press/text/toggle
  events with pipeline breakdown. Captures FrameMetrics (input,
  animation, layout, draw, sync). Exports structured JSON for
  charting with percentile stats and jank detection.
- PerfFunctions: Bridge functions (Perf.Enable/Disable/Reset/Export/
  Summary/StartCaptureWindow/StopCaptureWindow/SimulatePress/
  SimulateTextChange/SimulateToggle) controllable from PHP.
- BenchmarkComponent: PHP-side benchmark suite with render throughput,
  interactive latency, and deep tree stress tests.
- NativeUIRenderer: LaunchedEffect + withFrameNanos to detect when
  frames with new trees actually draw.
- BridgeFunctionRegistration: Register Perf.* and UI.SetBackground
  bridge functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Track nativeUIThread to properly join old PHP threads before
  restarting, avoiding races between overlapping hot reload cycles.
- Fix hot restart signal path to match PHP's storage_path() which
  resolves to persisted_data/storage/ (set by LARAVEL_STORAGE_PATH).
- Start NativeElementBridge.startWatching() alongside NativeUIBridge
  for the new element runtime.
- Attach PerformanceTracker FrameMetrics listener on create, detach
  on destroy.
- Reduce hot reload poll delay from 500ms sleep to 100ms + continue
  for faster restart detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- RunCommand: Add --no-vite flag to skip Vite dev server for
  native-only development workflows.
- RunsAndroid, RunsIos: Skip Vite startup when --no-vite is set.
- WatchesAndroid, WatchesIos: Skip Vite server, port detection,
  forwarding, and file filtering when --no-vite is set.
- InstallsAndroid: Update PHP binary download URLs to element
  runtime builds (android-element-8.5.3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the full pipeline from PHP Element runtime through JNI
flat buffer to Kotlin Compose rendering, covering the flat node
binary format, type interning, event system, and direct push
architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip re-downloading PHP binaries on subsequent installs when the same
version zip already exists locally. Applies to both Android and iOS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Throw NativeDumpException from VarDumper handler to prevent exit(1)
- Render styled dump screen (dark theme) with variable output
- Catch exceptions from both render() and dispatch() event handlers
- Show error screen with exception details and stack trace
- Font size controls ([ – ] / [ + ]) on both dump and error screens
- Back button dismisses overlay and re-renders the component
- Plain dump() calls log to storage/logs/edge-nav.log without throwing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shanerbaner82 and others added 30 commits July 5, 2026 18:12
The AAB build needs compileSdk 36 (+ NDK 27 / cmake), but the CI image
mobiledevops/android-sdk-image:34.0.1 ships SDK 34, so Gradle can't
resolve android-36. It builds cleanly locally with SDK 36 installed —
a CI-environment gap, not a code regression. Switch the trigger to
workflow_dispatch until the runner provides SDK 36, so it stops gating PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The QR Builder must use endroid/qr-code 6.x's named-constructor API
(new Builder(data: ...)), not the 5.x fluent Builder::create() — a stale
composer.lock (endroid 5.1.0) had masked which API is real. endroid ^6.x
requires PHP ^8.4, so bump the php constraint to ^8.4 and refresh the lock
(endroid now resolves to 6.1.3). PHPStan + suite green against 6.1.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mobile-air now requires PHP ^8.4 (endroid 6.x), so composer install fails
on 8.3. Drop the 8.3 matrix leg and move static-analysis + code-style jobs
to 8.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ComposeFlexLayout (Android): a flex container with an absolute-positioned
child rendered its flow content via matchParentSize(), so the content was
sized to match the Box — whose size was driven only by the (typically tiny)
absolute child. Any real content (e.g. a code block with an absolute copy
button) got clipped to the badge/button's bounds and rendered blank. Flow
content now sizes normally and determines the Box; absolute children overlay
on top via .align.

NativeRootTabsRenderer (iOS): fix Tab(role: .search) so results actually show.
- Attach `.searchable` to the NavigationStack (Apple's documented iOS 26
  structure) via a new SearchTabContainer; previously it was on the inner List,
  which made the field float over the previously-selected tab without ever
  showing the results view.
- Move selection off the search tab when a result navigates (onChange of
  current_uri), so tapping a result reveals the destination instead of
  stranding the user on an empty search screen.
- Suppress the per-screen nav-bar `.searchable` on regular tabs when a
  dedicated search-role tab exists (the tabs-root nav_search_* props belong to
  that tab, not to every screen) — otherwise every tab sprouted a second,
  results-less search bar.

NativeRootTabsRenderer (Android): mirror the search-tab un-stranding — snap
selection back to the active tab when a navigation changes the current URI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rance API

Fold the device/dialog/file/system plugins into core: register their native
bridge functions in core's BridgeFunctionRegistration (iOS + Android), bind the
facades as core singletons, and declare a Composer conflict so the standalone
plugins can't be installed alongside v4. Add the VIBRATE/FLASHLIGHT Android
permissions the Device functions need.

Also add the System appearance/dark-mode API: System::appearance()/isDarkMode()/
isLightMode(), the isDark()/isLight()/theme() global helpers, and an
AppearanceChanged native event (pushed from iOS colorScheme / Android
onConfigurationChanged) that dispatches globally via BroadcastsGlobally and
keeps System's cached appearance fresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nested <native:text> now composes ordered inline runs into one attributed/
annotated string per platform (the precompiler emits textOpen/textClose; the
collector buffers runs in document order). Add select-text/select-none classes
(and Element::selectable()) that scope native text selection to a subtree —
SelectionContainer on Android, .textSelection on iOS. Clamp Text::fontWeight()
to the supported 1-7 range, and apply element-specific class attributes in
Element::class() so programmatic ->class() matches the blade collector.

Also plumbs a scroll-anchor prop on scroll containers; renderer support is
still pending, so it is currently a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add NavBar::titleView()/logo() to render a custom element or Blade view in the
bar's centered principal slot (wrapped in a TopBarTitle marker; iOS .principal
toolbar item / Android TopAppBar title slot, forced inline). Support an inline
<native:bottom-bar> in a screen's blade: it's hoisted out of the content tree
to the native-chrome root and pinned via .safeAreaInset(.bottom) (iOS) /
Scaffold(bottomBar) + imePadding() (Android), keeping it above the keyboard;
falls back to the layout's bottomBar(). Switch the SwiftUI roots to
.ignoresSafeArea(.container) so keyboard avoidance shifts content correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Vite dev server no longer starts automatically during native:run /
native:watch. Add a --vite flag (backed by a shouldRunVite() helper) so it
starts only when explicitly requested. --no-vite is kept but is now redundant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the 🐛 debug-log launcher FAB (and its bottom-sheet log/scheduler
viewer) that showed in the bottom-right corner on DEBUG builds. Deletes
the self-contained DebugLogViewer.kt and the now-unused isDebugVersion()
helper and render block in MainActivity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a deep or universal link opened an already-running native-ui app,
the link was silently dropped — it foregrounded the current screen
without navigating. Cold-start links worked; warm ones didn't.

Root cause: native-ui runs a single PHP event loop that blocks inside
the current screen. DeepLinkRouter's warm branch used navigateWithInertia
(window.router.visit), which only exists in the Inertia/WebView runtime;
and a plain webView.load(php://…) for a Route::native screen never commits
because it queues behind the running loop.

Fix: when NativeUIBridge.isActive, wake the running loop with a
__deeplink native event carrying the target route. NativeComponent's
dispatchNativeEvent turns it into a NavigationIntent::NAVIGATE and stops
the loop, so NativeRouter resolves the URI (with route params) and pushes
the screen — the same path an in-app @press navigate uses. Inertia apps
keep the existing navigateWithInertia behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Android mirror of the iOS fix. When a deep or app link opened an
already-running native-ui app, MainActivity called webView.loadUrl(),
which never routes a Route::native screen because the single PHP event
loop is blocked on the current screen — the link was silently dropped.

Add navigateWarm(): when NativeUIBridge.isActive, dispatch a __deeplink
native event carrying the route (NativeElementBridge.sendNativeEvent) to
wake the loop; NativeComponent::dispatchNativeEvent turns it into a
NavigationIntent::NAVIGATE and NativeRouter pushes the screen. WebView/
Inertia apps keep the direct loadUrl(). Applied to all three warm-entry
sites (app/custom-scheme link, notification URL, FCM URL).

Verified on emulator: warm jump:// and https app links both route
host-stripped to Docs. Pairs with the shared PHP handler in 209498e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Release-optimized, non-debuggable build that shell profilers (Macrobenchmark,
Perfetto, simpleperf) can attach to. Debug-signed so it installs with plain
adb install — no keystore, no manual zipalign/apksigner — and native:run
installs + launches it like a debug build. isProfileable injects
<profileable shell="true"> for this variant only, so production release and
bundle stay clean.

Also adds the androidx.profileinstaller dependency so the APK-embedded
baseline profile is installed and AOT-compiled on sideloaded builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libphp_wrapper.so registers its JNI methods against NativeElementBridge at
System.loadLibrary time. R8 can't see JNI references, and it stripped the
"unused" external funs (NativeElementBridge lives under ui.nativerender, so
the existing com.**.bridge.** keep rule missed it) — every minified build
crashed on boot with NoSuchMethodError: nativeElementWaitUpdate. Keep the
bridge class and any class declaring native methods.

Unblocks NATIVEPHP_ANDROID_MINIFY_ENABLED=true (74 MB -> 27 MB APK).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured on Pixel 9 (Macrobenchmark, profileable build, 10 iters). Beats
React Native (341ms) and Flutter (248ms); also drops idle memory 149 -> 119MB
and eliminates the P95/P99 animation jank tail. The changes:

- Paint the splash first: WebView (Chromium) creation and the PHP boot kickoff
  move to window.decorView.post — a Perfetto android_startup breakdown showed
  241ms of activityStart plus 161ms of main-thread uninterruptible I/O sleep
  when boot preceded first paint. Bridge/renderer registration moves onto the
  boot thread ahead of the PHP boot.
- Lazy PHPBridge so System.loadLibrary("php_wrapper") leaves the TTID path.
- Build Laravel bootstrap caches once at bundle extraction (config:cache,
  event:cache, view:cache — not route:cache; closure routes). Skip the
  per-boot cacert MD5 verification; refresh only on extraction or DEBUG.
- Defer the background queue worker (a second full Laravel runtime) 2.5s past
  first paint; reportFullyDrawn once the start URL loads.
- Ship a 12k-rule Baseline Profile covering Compose + the runtime boot path;
  regenerate with native-benchmark/android/generate-baseline-profile.sh.
- Black windowBackground in both themes (kills the light-mode white flash);
  splash bitmap decodes off the first composition on Dispatchers.IO.
- FrameTracker no longer self-starts its Choreographer callback + 4Hz publish
  loop when the overlay is disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Named scale (leading-none/tight/snug/normal/relaxed/loose) as unitless
multipliers of font size, plus arbitrary leading-[1.4] (multiplier) and
leading-[24px] (absolute) forms, wired through TailwindParser into the
Text element's lineHeight prop. Covered in the parser and text-run tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uninstalls the plugins whose functionality moved into core in v4 (device,
dialog, file, system) in one pass: deregisters their service providers,
removes the composer packages, and cleans up source dirs. The plugin
argument is now optional to support the batch path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render a remote WebView app over Jump: when a jump://webview?host=&port=
deep link activates a session, PHPSchemeHandler forwards php://127.0.0.1
requests to the remote dev server instead of the local embedded PHP. The
WebView keeps loading php://127.0.0.1 — same rendering surface, origin, and
JS bridge, just a different response source — and dev-server-host links the
remote app emits are rewritten back to php://127.0.0.1 so navigation stays
in the WebView.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same guard, lighter mechanism: during a native render, any view whose cached
compiled file lacks the native-precompiler marker (e.g. compiled by a web
render or `view:cache`) is force-recompiled — now via a view creator in
NativeServiceProvider instead of swapping the stock Blade engine for a
subclass. Covers nested @includes; the root view keeps the same check in
renderBladeBoundToSelf(). Also what keeps the new on-extraction `view:cache`
(cold-start caching) safe for native-ui screens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docs show gestures-enabled, label-visibility, background-color,
image-url, show-close-button, badge-color and open-in-browser, but the
elements only matched the camelCase spellings. Attributes reach elements
verbatim from the precompiler, so the documented forms were silently
dropped. Accept both, camelCase winning when both are present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same class of bug as the side-nav elements: attributes arrive verbatim
and only the camelCase spelling was matched, so an inline badge-color
attribute silently no-oped. camelCase wins when both are present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Text::applyAttributes only matched the camelCase keys the Tailwind
class parser produces, so every hand-written kebab attribute the docs
show (font-size, font-weight, text-align, max-lines, and the rest)
was silently dropped. Accept both spellings, camelCase winning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TailwindParser::resolveColorValue() resolves standalone color values —
Tailwind palette names (red-300), white/black/transparent, CSS hex
(#RGB/#RGBA/#RRGGBB/#RRGGBBAA), and /N opacity modifiers on any of them
— to wire-format hex (#RRGGBB / #AARRGGBB). Theme config tokens and
element color props resolve through it (native-ui side).

normalizeHex() now converts authored CSS-order alpha hex (#RRGGBBAA) to
the Android-style #AARRGGBB both native ColorParsers read, and rejects
invalid hex instead of shipping it raw — fixes bg-[#8B5CF680] rendering
as green (alpha byte read as red).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UIColor(hex:) read 8-digit hex as CSS-order #RRGGBBAA while both
ColorParsers (Swift NativeUINode + Kotlin) read #AARRGGBB — the same
payload rendered different colors depending on which chrome consumed
it. PHP now converts authored CSS hex to #AARRGGBB before sending, so
every native parser reads one convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The collector splits the parser's `dark` sub-array into dark_bg_color /
dark_color / dark_border_color props for blade elements, but the
programmatic Element::class() path never did — Element-returning
render() trees silently dropped ALL dark-mode styling and rendered
light hexes in both modes. Route the same buildDarkProps() output into
mergeDarkProps() so both construction paths emit identical wire props.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chrome text can now render in a bundled custom font, three tiers with
most-specific-wins precedence: per-screen NavBarOptions/TabBarOptions
->font(), per-bar NavBar/TabBar ->font(), and a layout-wide
NativeLayout::$font (applied via defaultFont(), set-if-unset). Unset
tiers fall through to the theme's app-wide font-family default. The
token flows as fontName through toRootProps()/toElement() into a
font_name prop on the chrome sentinels (folded nav_font_name on tabs).

Core renderers resolve the token through plugin-populated seams so core
still compiles standalone: Android extends NativeUIThemeProvider with a
fontFamilyResolver (titles, tab labels, badges pass it as fontFamily);
iOS adds NativeChromeFontResolver (token → PostScript name), used by
the stack renderer's principal-slot title path — inline-mode bars with
a font switch to that path since system titles expose no font hook —
and injected into the tabs renderer's UITabBarAppearance (iOS ≤ 25;
Liquid Glass bars reject appearance overrides, so labels keep the
system font there). System-drawn large titles keep the app default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…End, test harness)

GestureArea gains continuous pinch (SharedValue scale with source-side
min/max clamping, seed/valueOf store additions) and discrete swipe /
pinch-end callbacks; the precompiler + collector wire the new
@swipe/@pinchEnd Blade events, SV-bound opacity pre-seeds from the
style literal, and TestableComponent gets swipe()/pinch() drivers.
Pairs with the gesture renderers in nativephp/native-ui (pushed to its
main). 16 gesture tests; full suite green (559 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While a forwarded remote app owns the WebView: drop local native-ui
runloop publishes so they can't yank the session off screen
(NativeElementBridge guard), route side-nav/chrome links that target
the dev-server host back through the php:// forward instead of the
system browser, add a global 3-finger swipe-right escape hatch back to
the Jump home, plus router.php / JumpCommand forwarding fixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t run

native:jump left orphaned servers holding their ports on Cmd+C, so each
run escalated (3000->3008, 8000->8002, 3001->3009, ...). Three primary
causes: (1) killExistingServers() was dead code — never called — so a
prior run's leaked ports were never reclaimed; (2) proc_terminate() only
signals direct children, missing the php -S / artisan serve worker
grandchildren; (3) the Unix Workerman bridge was launched detached
(exec "&") and untracked, so nothing ever signalled it. Signal handlers
were also installed only after up to ~120s of blocking startup.

Fix:
- Spawn each server as its own process-group leader (posix_setsid shim)
  and tear down with killGroup() = SIGTERM->SIGKILL on the negative PGID,
  reaping the entire subtree incl. grandchild workers. ps-tree fallback
  when pcntl is unavailable; taskkill /F /T on Windows.
- Arm SIGINT/SIGTERM/SIGHUP handlers + register_shutdown_function BEFORE
  any spawn, with pcntl_async_signals(true), so Cmd+C during startup is
  honoured. Teardown ignores further terminating signals so a second
  Ctrl+C can't abort the SIGTERM->SIGKILL escalation mid-flight.
- Call killExistingServers() at startup to reclaim a prior run's ports
  (registry-driven), plus identity-checked backstops: a canonical-port
  sweep and a PPID-1 orphan sweep that reaps registry-less leftovers and
  the Workerman masters (which hold no listening port). All gated by
  isJumpOwnedProcess() so unrelated services (e.g. boost:mcp on 3002) and
  live sibling projects are never touched.
- killGroupIfOwned() identity-checks prior-run registry leader PIDs before
  a group kill, so a recycled PID can't nuke an innocent process group.
- Track Windows bridge/vite PIDs; broaden the Windows reaper to match all
  jump scripts (router + websocket-server + vite-hmr) and tree-kill.

Cross-platform: macOS/Linux process groups; Windows taskkill /F /T;
no-pcntl Unix ps-tree fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the 🧭 nav-decide / handleNavigation traces and the external-nav
diagnostic added while debugging why WebView-session links escaped to the
system browser. Kept the low-frequency WS lifecycle logs, which stay
useful for diagnosing the device bridge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants