Skip to content

Merge main into Android support - #33

Merged
heoblitz merged 45 commits into
feature/android-supportfrom
feature/android-support-main-backmerge
Nov 3, 2026
Merged

heoblitz merged 45 commits into
feature/android-supportfrom
feature/android-support-main-backmerge

Conversation

@heoblitz

@heoblitz heoblitz commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Merge main at 9c028e4 into feature/android-support, preserving both the Android branch's SDK/iOS layout and main's independently selectable iOS plugin products.

  • Reconcile Swift source and test moves, package targets, panel build destinations, theme tests, and Android panel packaging.
  • Preserve literal Android StringSet rows alongside main's type-preserving Array editor and read-only handling of invalid containers. Add a regression test for JSON-looking strings.
  • Rebuild committed panels and retain the Android branch's connection and origin-validation changes.

Review note: main removes the app-state and counter controls from Plugin Sample. The Android sample carries that same updated panel.

Merge this PR with a merge commit, not squash or rebase, so main's ancestry remains recorded for the later Android-to-main merge.

Test Plan

Passed locally:

  • node --test script/tests/package.test.mjs: 3 package-boundary tests.
  • yarn build and script/test web: type checking, 35 tests, and the panel asset harness.
  • script/test swift: SDK, Mac/CLI, and external SDK consumer tests.
  • script/test native: 29 native integration tests.
  • xcodebuild -project Necto.xcodeproj -scheme Necto -configuration Debug -destination 'platform=macOS' -derivedDataPath Build/Tests CODE_SIGNING_ALLOWED=NO build.
  • xcodebuild -project Necto.xcodeproj -scheme ExampleApp -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath Build/Backmerge CODE_SIGNING_ALLOWED=NO build.
  • SDK/Android/gradlew -p SDK/Android --offline :necto-core:test :necto-okhttp:test :necto-android:testDebugUnitTest :sample:assembleDebug, with the local JDK and Android SDK configured: 67 tests and the debug sample build.
  • Compared all 24 packaged panel files with the merged iOS sources and Android manifest overrides: identical.
  • git diff --check: clean.

CI verification:

All six CI jobs passed, including the full Android ./gradlew build, script/test-android-artifacts, and simulator connection/CLI E2E. This covers the full Android build that was blocked locally by Maven certificate validation.

Remaining verification limits:

  • Physical-device USB connections and manual UI flows were not rerun.

heoblitz and others added 30 commits October 12, 2026 10:52
Replace Objective-C touch injection with a Swift runtime implementation
NSNumber casts to Bool whenever it holds 0 or 1, so a number read from
UserDefaults or a property list became a Boolean. Preferences showed a
nested 1 as true and saved it back as a Bool on the next edit.
preferences.set converted an Int with Int(_:), which traps outside its
range, and passed null to UserDefaults, which raises an exception for a
value that is not a property list. Either one terminated the app being
debugged. Both are now refused as invalid input.
Saving an Array or Dictionary rewrote every row from its text, so a
stored string such as "1" or "true" came back as a number or a Bool
after any edit. A container holding a Date or Data is described rather
than encoded as JSON, and the panel opened it as an empty editor whose
Save replaced the stored value. An emptied number field saved 0.

Rows now round-trip to the value they showed, a container that cannot
be read is shown read-only, and an empty or unsafe number is refused.
The detail of a key was written to the pane whenever it came back.
Requests overlap, so the answer for a key already left could arrive
last and show under the key selected since, where the next save would
write that value to it. An answer is now applied only while its key is
still selected in the same suite.
JSON values keep numbers as Double, which holds every integer only
below 2^53. A larger one inside a stored container was rounded on the
way to the panel, and saving any other row wrote the rounded number
back. Such a container is now described rather than encoded, which the
panel shows read-only.
Files previews the first 32 kB of a file and Network keeps the first
512 kB of a body. Both cuts count bytes, so one can land inside a
character of two to four bytes, and the bytes left over made the whole
head fail to decode: a long Korean log showed as binary, and a large
JSON response lost its text.

The incomplete character at the cut is now dropped. Anything else that
is not UTF-8 still reads as binary, and a file read whole is decoded
strictly as before.
Files and Network load a row's detail when it is selected, and wrote
the answer to the pane whenever it came back. Requests overlap, so the
answer for a row already left could arrive last and replace what the
selected row shows. In Files that put one file's text under another
file's name, where Edit and Save would write it over that file.

An answer is now applied only while its row is still selected, as the
Events panel already does, and a save or delete that finishes after the
selection moved no longer changes the new selection's pane.
script/build and the manual ExampleApp command left the product
location to Xcode, which by default is a directory under
~/Library/Developer/Xcode/DerivedData, or Build/Build/Products with
-derivedDataPath Build. The paths printed by the script and given in
the README, the install guide and the tutorials then did not exist:
open Build/Products/Debug/Necto.app and simctl install
Build/Products/Debug-iphonesimulator/ExampleApp.app both failed.

Both builds now set SYMROOT to Build/Products, as script/test-e2e
already does for its own products.
A body kept whole that still ends inside a character is not text, as
Files already treats a file read whole.
task_threads returns a port for every thread along with the list, and
only the list was released. Each port kept outlived its thread as a
dead name, so an observed Performance panel, sampling once a second,
left one behind in the app for every thread it ever ran.
Files keeps listings by path alone. One still on its way when the root
was switched landed under the new root, which then listed the old
root's files as its own.
The URL, the method and header names went into the command as they
were. A URL with an apostrophe in it produced a command the shell could
not read, and one written for the purpose ran whatever followed the
quote when the command was pasted. Each part is now a single quoted
argument.
fix: keep Preferences from crashing the app or changing the types of stored values
Fix socket connection cancellation deadlock
fix: build the apps where the documentation says they are
The capture re-issued every request through a new ephemeral session and
reported to the app only once the response was complete. With it
installed:

- cookies were neither sent nor stored, so cookie sessions stopped
  working;
- data reached the app before the response did, which made
  URLSession.bytes(for:) fail and told delegates things out of order;
- a streamed response arrived all at once when it ended;
- every request opened a connection of its own.

Requests now run in one delegate-driven session built from the default
configuration, and the response and its data are handed over as they
arrive. Only the head of a large response is kept for the record.

Redirects are still followed by the capture. Handing them to the app's
session was tried: told the load was cancelled for the redirect, the
session sometimes reported that cancellation instead of following.

install() covers URLSession.shared only, so the comment and the setup
guide now say how to capture a session built from a configuration, and
what capturing one changes.
The shared re-issuing session handled cookies through the shared store
for every captured request, so a session whose configuration turned
cookies off, or kept a store of its own, sent cookies it never agreed
to. What a configuration says about cookies cannot be seen from a
URLProtocol, so the protocol an app puts into its own configuration now
re-issues through a session that handles none, as before, and only the
protocol registered for URLSession.shared — whose policy is the shared
store's — keeps cookies working.

Tasks are now keyed by identity in the loader: a task identifier is
only unique within one session, and the loader serves two.
fix: read text whose preview is cut inside a character
fix: keep URLSession capture from changing how requests behave
@heoblitz
heoblitz merged commit 3ff6872 into feature/android-support Nov 3, 2026
6 checks passed
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