feat(mobile): synchronized pill composer transitions and draft screen fixes

Drop the animated pill/composer morph in favor of instant swaps that are
synchronized with the keyboard choreography: a new oc:keyboard-intent
event collapses the composer (flushSync) before the hide compensation is
measured, so keyboard travel and composer height change land as a single
chat motion on both iOS and Android (Android also gains keyboard signals
and deterministic re-pins around its native resize). The WKWebView caret
is hidden during the transition so it no longer flies to its new position.

Draft screen: starter chips hide instantly while the keyboard is up and
the centered title rides the keyboard shift compensation instead of
double-jumping; the composer drag handle also works in dictation mode;
the highlight mirror is disabled on mobile so the caret matches the text.

Fixes: worktree discovery and the GitHub auth probe now wait for the
runtime connection (no more empty branch pickers / stale auth on cold
start), worktree discovery merges per project instead of clobbering the
persisted map, the cross-project session list resets on instance switch
(with an in-flight load guard) so no stale sessions linger, and mobile
overlay content contains its overscroll instead of bouncing the page.
This commit is contained in:
Bohdan Triapitsyn
2026-07-05 00:05:38 +03:00
parent fb839b66a9
commit dbcb655e43
8 changed files with 239 additions and 27 deletions
+34 -1
View File
@@ -67,12 +67,13 @@ Options:
--web-mode <hmr|hmr-lan|full>
--mobile-mode <ios-sim-local|ios-sim-lan|android-local|android-lan>
--mobile-task <task>
--adb-address <host:port> Wireless ADB address for android-connect
--vsix-cleanup <delete|keep>
--version <semver>
-h, --help
Mobile tasks:
build, sync, android-devices, android-deploy-usb, android-run, android-logcat,
build, sync, android-devices, android-connect, android-deploy-usb, android-run, android-logcat,
ios-sim-build, ios-sim-run, ios-sim-serve, ios-sim-kill, ios-device-sync-debug
`);
}
@@ -115,6 +116,9 @@ function parseArgs(argv) {
case '--mobile-task':
options.mobileTask = readValue();
break;
case '--adb-address':
options.adbAddress = readValue();
break;
case '--vsix-cleanup':
options.vsixCleanup = readValue();
break;
@@ -206,6 +210,29 @@ async function chooseValue(current, choices, message) {
return value;
}
async function chooseText(current, message, placeholder) {
if (current) return current;
ensurePromptable();
const value = await text({ message, placeholder });
if (isCancel(value)) {
cancel('Operation cancelled.');
process.exit(130);
}
return value;
}
function validateAdbAddress(address) {
const normalized = String(address || '').trim();
if (!/^[^\s:]+:\d{1,5}$/.test(normalized)) {
throw new Error('Invalid wireless ADB address. Use host:port, e.g. 192.168.1.139:38181');
}
const port = Number(normalized.split(':').at(-1));
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error('Invalid wireless ADB port. Use a port between 1 and 65535.');
}
return normalized;
}
function detectLanIp() {
for (const addresses of Object.values(os.networkInterfaces())) {
for (const address of addresses || []) {
@@ -432,6 +459,7 @@ async function mobileTools(options, config) {
{ value: 'build', label: 'Build mobile web assets' },
{ value: 'sync', label: 'Sync native projects' },
{ value: 'android-devices', label: 'Android: list USB devices' },
{ value: 'android-connect', label: 'Android: connect wireless ADB device' },
{ value: 'android-deploy-usb', label: 'Android: rebuild + deploy to USB device' },
{ value: 'android-run', label: 'Android: install + launch existing APK' },
{ value: 'android-logcat', label: 'Android: logcat' },
@@ -453,6 +481,11 @@ async function mobileTools(options, config) {
case 'build': return mobileRun('Building mobile web assets', 'build');
case 'sync': return mobileRun('Syncing native projects', 'sync');
case 'android-devices': return mobileRun('Listing Android USB devices', 'android:devices');
case 'android-connect': {
const address = validateAdbAddress(await chooseText(options.adbAddress, 'Enter wireless ADB address', '192.168.1.139:38181'));
step(`Connecting wireless ADB device at ${address}`, () => run('node', ['scripts/with-mobile-env.mjs', `adb connect ${quote(address)}`], { cwd: mobileCwd }));
return mobileRun('Listing Android devices', 'android:devices');
}
case 'android-deploy-usb':
mobileRun('Building Android debug APK', 'build:android:debug');
return mobileRun('Installing and launching Android app on USB device', 'android:run');