Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3bafba6
[docs] reconcile merged recovery candidate and continuation plan
yyods Oct 31, 2026
b8376fe
[docs] record iPad acceptance access and preserved candidate evidence
yyods Oct 31, 2026
d1f8719
[red] reproduce landscape keyboard panel overflow
yyods Oct 31, 2026
e54a78a
[green] preserve landscape editing space above the keyboard
yyods Oct 31, 2026
88b7871
[docs] prepare recovery release artifacts and channel handoff
yyods Oct 31, 2026
b6b9998
[docs] bind release handoff to repaired production artifacts
yyods Oct 31, 2026
e219b74
[docs] distinguish Android WebView controls from candidate acceptance
yyods Oct 31, 2026
62b077c
[red] reproduce console expand with native keyboard ownership
yyods Oct 31, 2026
01a7ac8
[green] dismiss native keyboards before expanding the console
yyods Oct 31, 2026
758030a
[docs] bind final release package to native keyboard repair
yyods Oct 31, 2026
286d280
[docs] record Android recovery and app-only matrix acceptance
yyods Oct 31, 2026
221758c
[docs] record existing Xcode account upload route
yyods Oct 31, 2026
e6e6f94
[docs] correct Android acceptance inventory reference
yyods Oct 31, 2026
bd36731
[red] cover iPad keyboard palette geometry fallback
yyods Oct 31, 2026
bef67bd
[green] keep iPad keyboard palette clear of editing controls
yyods Oct 31, 2026
fe80d17
[docs] record coordinated Android fixture cleanup
yyods Oct 31, 2026
d9d058a
[docs] bind release artifacts to iPad keyboard geometry repair
yyods Oct 31, 2026
e297bb9
[docs] record completed iPad recovery and board acceptance
yyods Oct 31, 2026
ef19773
[docs] clarify Android Blockly asset loading semantics
yyods Oct 31, 2026
3b9afac
[docs] record iPad automation helper cleanup
yyods Oct 31, 2026
fc904d0
[docs] reconcile physical acceptance and remaining release gates
yyods Oct 31, 2026
3071560
[docs] admit app candidate with final physical qualification receipts
yyods Oct 31, 2026
6e444da
[docs] verify installed production identity and CI scope
yyods Oct 31, 2026
6124356
[docs] retain store-installed upgrade baselines for channel delivery
yyods Oct 31, 2026
7cdbb31
[docs] record complete qualification fixture cleanup
yyods Oct 31, 2026
124efb6
[docs] accept bounded recovery and declared app qualification
yyods Oct 31, 2026
af210b1
[docs] bind Android acceptance to final keyboard candidate
yyods Oct 31, 2026
8d21c5d
[docs] carry accepted Android setup into operator recipe
yyods Oct 31, 2026
36e7308
[docs] link final integration without predicting merge
yyods Oct 31, 2026
eccc639
[docs] date S1 inventory gaps and link accepted app results
yyods Oct 31, 2026
34dd89e
[docs] align S1 handoff with completed device acceptance
yyods Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@ or firmware release. Firmware v0.6.1 is published on the owner's confirmation;
incomplete automated evidence remains pending. The qualified v0.6.0 release
remains a separate historical baseline built from its annotated tag.

The current compatible app feature set now targets `0.2.0`; build `5` was
handed off through Transporter for TestFlight. This candidate identity improves
test and evidence traceability but does not designate a qualified store release
or claim App Store Connect processing, beta-review approval, or availability.
The next prepared Android open-testing candidate is `0.2.0 (8)`; its bundle
and submission packet do not claim Play submission, approval, or rollout.
The merged app source now targets `0.3.0+9`. Its local Android and iPad Preview
installations support scoped engineering acceptance for bounded recovery and
the declared app-only matrix. Device closeout is complete. [PR #41](https://github.com/PyBLE-dev/PyBLE/pull/41) tracks final
integration; required CI and merge precede publishing. Actual channel delivery
remains pending. The recorded existing-channel baseline is Android
Play `0.2.0 (8)` and iPad TestFlight `0.2.0 (6)`; the earlier build-5 Transporter
and build-8 submission handoffs remain historical artifact records.

### App source

- Prepared app source candidate `0.3.0+9` with bounded on-device current-text
- Merged app source candidate `0.3.0+9` with bounded on-device current-text
recovery using Drift. Exact text and metadata commit atomically; startup
offers offline preview, Restore and confirmed Discard, with separate local
checkpoint status and an explicit Save recovery copy action. Storage errors
preserve the previous copy and live text, with explicit volatile continuation.
- Fixed landscape editing with the software keyboard open: the console output
strip temporarily collapses to its status/actions bar, and navigation
destinations remain reachable by scrolling. Expanding requests keyboard
dismissal from the current platform window, including native Blocks fields.
iPad layout also accounts for keyboards reopened through the keyboard palette
without applying the same overlap twice; document/console state is retained.
- Added document-epoch and connection-session guards for Editor Save/Run,
Files Open/Upload and Blocks-to-Editor replacement. Restored drafts require
current-destination confirmation before board writes; a board swap after text
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fetches and validates the complete batch, shows the exact destination paths,
and asks separately before overwriting existing files. Import never creates a
remote folder hierarchy, opens an editor document, or runs downloaded code.

The next App source candidate is `0.3.0+9`; App and firmware versions are
The merged App source candidate is `0.3.0+9`; App and firmware versions are
independent. The published Android version is `0.2.0`, available through
[Google Play open testing](https://play.google.com/store/apps/details?id=dev.pyble.pyble).
iPad testing continues through TestFlight. The
Expand All @@ -93,7 +93,7 @@ same Flutter source and app test gates. The iOS/iPadOS project minimum is 15.

### Next app candidate: local draft recovery

The `0.3.0+9` source candidate adds one on-device current-text recovery copy.
The merged `0.3.0+9` candidate adds one on-device current-text recovery copy.
After restart, preview and explicitly Restore or Discard the last committed
copy before connecting. “Saved locally” means the matching transaction finished;
pending edits may not survive immediate termination. The separate **Save
Expand All @@ -104,8 +104,12 @@ This bounded feature covers text up to 1 MiB, including Python explicitly copied
from Blocks. Blocks workspace graphs, projects/history and exported backups
remain outside this increment. Uninstalling or clearing app data removes local
copies. See the [candidate execution record](docs/developments/2026-q4-reliable-workflow/c1-s2/00_Execution_Record.md)
for automated and device evidence; source implementation does not establish
store availability or release qualification.
for accepted automated and candidate-specific device evidence, including the
declared five-profile × two-platform app matrix and test conditions. Local
Android and iPad Preview installations remain separate from store delivery.
The [release readiness record](docs/developments/2026-q4-reliable-workflow/c1-s2/06_Release_Readiness.md)
tracks the engineering-qualified artifacts, completed device closeout and channel handoff;
`0.3.0 (9)` is not yet established as a store-delivered update.

### Firmware

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
// SPDX-License-Identifier: MIT
// Part of PyBLE (https://pyble.dev) — see /LICENSE.

package dev.pyble.pyble

import android.view.inputmethod.InputMethodManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "dev.pyble/keyboard")
.setMethodCallHandler { call, result ->
if (call.method == "dismissKeyboard") {
val manager = getSystemService(InputMethodManager::class.java)
window.decorView.windowToken?.let { token ->
manager?.hideSoftInputFromWindow(token, 0)
}
// Acknowledge the request; Flutter observes actual view insets.
result.success(null)
} else {
result.notImplemented()
}
}
}
}
125 changes: 125 additions & 0 deletions app/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// SPDX-License-Identifier: MIT
// Part of PyBLE (https://pyble.dev) — see /LICENSE.

import Flutter
import UIKit

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
private var keyboardInsets: PybleKeyboardInsets?

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
Expand All @@ -12,5 +17,125 @@ import UIKit

func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
guard let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "PybleKeyboardDismissal")
else { return }
keyboardInsets = PybleKeyboardInsets(registrar: registrar)
let keyboard = FlutterMethodChannel(
name: "dev.pyble/keyboard", binaryMessenger: registrar.messenger())
keyboard.setMethodCallHandler { [weak registrar] call, result in
guard call.method == "dismissKeyboard" else {
result(FlutterMethodNotImplemented)
return
}
// End editing in this engine's window, including embedded WKWebView fields.
registrar?.viewController?.viewIfLoaded?.window?.endEditing(true)
// Acknowledge the request; Flutter observes actual view insets.
result(nil)
}
}
}

/// Measures the current Flutter view's public layout guide instead of keyboard
/// notification frames, which can be offscreen after an iPad palette transition.
private final class PybleKeyboardInsets {
private weak var registrar: FlutterPluginRegistrar?
private let channel: FlutterMethodChannel
private var observer: PybleKeyboardGuideView?
private var generation: Int?
private var lastBottom: CGFloat?

init(registrar: FlutterPluginRegistrar) {
self.registrar = registrar
channel = FlutterMethodChannel(
name: "dev.pyble/keyboard/insets", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler { [weak self] call, result in
guard let self = self, let epoch = call.arguments as? Int else {
result(FlutterMethodNotImplemented)
return
}
switch call.method {
case "observe":
self.observe(generation: epoch)
result(nil)
case "stop":
if self.generation == epoch { self.stop() }
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}

private func observe(generation: Int) {
if self.generation != generation {
stop()
self.generation = generation
}
guard let view = registrar?.viewController?.viewIfLoaded else {
observer?.removeFromSuperview()
observer = nil
publish(0)
return
}
if observer?.superview !== view {
observer?.removeFromSuperview()
let guideView = PybleKeyboardGuideView()
guideView.translatesAutoresizingMaskIntoConstraints = false
guideView.isUserInteractionEnabled = false
guideView.accessibilityElementsHidden = true
guideView.backgroundColor = .clear
guideView.changed = { [weak self] bottom in self?.publish(bottom) }
observer = guideView
view.addSubview(guideView)
NSLayoutConstraint.activate([
guideView.topAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
guideView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
guideView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
guideView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
view.layoutIfNeeded()
}
// An explicit refresh must return a snapshot even if UIKit has not moved:
// Dart may have rejected the previous value while its window size lagged.
lastBottom = nil
observer?.reportOverlap()
}

private func publish(_ bottom: CGFloat) {
guard let generation = generation, bottom != lastBottom else { return }
lastBottom = bottom
channel.invokeMethod("inset", arguments: ["generation": generation, "bottom": bottom])
}

private func stop() {
generation = nil
observer?.removeFromSuperview()
observer = nil
lastBottom = nil
}
}

private final class PybleKeyboardGuideView: UIView {
var changed: ((CGFloat) -> Void)?

override func layoutSubviews() {
super.layoutSubviews()
reportOverlap()
}

override func didMoveToWindow() {
super.didMoveToWindow()
reportOverlap()
}

func reportOverlap() {
guard let owner = superview, window != nil else {
changed?(0)
return
}
// On iOS 15/16 a hidden/undocked guide ends at the safe-area bottom.
// A real docked keyboard or input palette reports its full overlap.
let overlap = min(owner.bounds.height, max(0, bounds.height))
changed?(overlap <= owner.safeAreaInsets.bottom + 0.5 ? 0 : overlap)
}
}
3 changes: 3 additions & 0 deletions app/lib/app/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:pyble/editor/recovery_gate.dart';
import 'package:pyble/theme/theme.dart';

import 'shell.dart';
import 'keyboard_insets.dart';

/// Navigator used by explicit user confirmations, never by local recovery itself.
final GlobalKey<NavigatorState> appNavigatorKey = GlobalKey<NavigatorState>();
Expand All @@ -45,6 +46,8 @@ class PybleApp extends StatelessWidget {
highContrastDarkTheme: PybleTheme.highContrastDark,
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
builder: (BuildContext context, Widget? child) =>
KeyboardInsets(child: child ?? const SizedBox.shrink()),
home: const RecoveryGate(child: PybleShell()),
);
}
Expand Down
20 changes: 20 additions & 0 deletions app/lib/app/keyboard_dismissal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Part of PyBLE (https://pyble.dev) — see /LICENSE.

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

const MethodChannel _keyboardChannel = MethodChannel('dev.pyble/keyboard');

/// Requests dismissal of the current window's keyboard, including native
/// WebView fields. Completion does not assert that the keyboard is hidden;
/// layout continues to follow the actual view insets.
Future<void> dismissSoftwareKeyboard() async {
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS)) {
await _keyboardChannel.invokeMethod<void>('dismissKeyboard');
} else {
await SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
}
}
Loading
Loading