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
+36 -5
View File
@@ -616,6 +616,15 @@
transition-duration: 0.2s;
}
/* WKWebView draws the text caret as a native layer that ignores CSS transforms:
after the keyboard rise it visibly flies from the pre-keyboard position to
the final one. Hide it during the transition (+ UIKit's reposition lag,
see oc-kb-caret-hold timing in useNativeMobileChrome) and pop it back in. */
:root.oc-capacitor-app.oc-kb-caret-hold textarea,
:root.oc-capacitor-app.oc-kb-caret-hold input {
caret-color: transparent;
}
/* The composer keeps its 1rem bottom padding while the keyboard is down (breathing
room above the home indicator), but that gap looks artificial sitting right above
the keyboard — so tighten it while the keyboard is open. Snaps at the start of the
@@ -625,15 +634,37 @@
padding-bottom: 12px;
}
/* Content cross-fade for the pill ↔ full composer morph. The wrapper animates
its height (FLIP in ChatInput); the freshly mounted state fades in on top. */
/* Draft starter chips leave the moment the keyboard starts rising
(oc-keyboard-open lands at keyboardWillShow) and return when it's gone —
with the keyboard up there is only room for the draft title. Instant
show/hide (no squish animation); the title's own keyboard compensation
(.oc-draft-center below) carries the smooth motion. */
:root.oc-capacitor-app.oc-keyboard-open .oc-draft-starters {
display: none;
}
/* The draft title is vertically centered, so it has no scroll-pinning to
compensate the keyboard like the chat does: during the slide the shell keeps
its full height and only snaps shorter at settle, which made the centered
title glide down (starters collapsing) and then JUMP up (shell snap). Ride
the same choreography as the composer: translate the centered block up by
half the keyboard shift (exactly how far the center moves after the snap),
with the transition gated on .oc-kb-animating so the settle swap — shift
back to 0 in the same frame the layout shrinks — is invisible. */
:root.oc-capacitor-app .oc-draft-center {
transform: translateY(calc(-0.5 * var(--oc-kb-shift, 0px)));
}
:root.oc-capacitor-app.oc-kb-animating .oc-draft-center {
transition: transform 0.25s cubic-bezier(0.38, 0.7, 0.125, 1);
}
:root.oc-capacitor-app.oc-kb-animating.oc-kb-hide .oc-draft-center {
transition-duration: 0.2s;
}
@keyframes oc-composer-morph-fade {
from { opacity: 0; }
to { opacity: 1; }
}
.oc-composer-morph-fade {
animation: oc-composer-morph-fade 0.24s ease-out both;
}
/* Voice overlay variant: the overlay BACKGROUND appears immediately (it rides
the morphing shape), but its content stays hidden until the shape has mostly