Repository navigation
Releases: ddtcorex/dsh-maestro-mobile
Release list
v1.8.1
Fixed
- Session-header drawer toggle: 0.2.2's masked
titleClustermade the
absolutely positioned control untappable (present, sized, never hit). The
phone branch now turns that mask off and the toggle keeps its composer-gutter
place at x=16. - Session-delete route: it registered an exact
/apipath onwebServer, and
the launch-token fence is a/apiprefix route an exact route shadows, so the
handler ran unauthenticated. It now callsconnection.requestRejection()and
fails closed with 503 when the host publishes no such service. - Settings sheet: one content column instead of five left edges; the tab strip
fades only a cut edge; the panel is a column instead of the host's grid (the
drag handle had been auto-placed into the 36px header cell); a row's switcher
no longer overflows the column; the panel honours its own height cap. - Response compression now installs only when the host does not compress: 0.2.2
ships compression@1.8.2 and the deployed profile enables it, so the
process-wide patch was dead weight. - The contract scan hit-tests interactive controls and drives a real session
first (hit=15 skip=33->hit=26 skip=25on the same page).
Changed
- Peer ranges accept the running DSH prerelease (
>=0.2.2-0 <0.3.0-0,
cordis~4.0.6-alpha.1); devDependencies pin the same release sopnpm verify
type-checks against the host the plugin runs on. - Removed the inert
dsh.client.injectentry (dsh-client-ui-primitivesis a
platform seed, not a boot row).
What's Changed
- fix: make the plugin work on DSH 0.2.2-alpha.2 (toggle, delete-route auth, settings sheet) by @ddtcorex in #89
Full Changelog: v1.8.0...v1.8.1
v1.8.0
Added
- Composer clearance for the landscape keyboard band — while the frame
carriesdata-mobile-nav-composer-tight, the sticky seat outranks the
opaque session header (z-index: 31) and the stats row yields its height
to the editing row, so rotating to landscape with the keyboard up no
longer hides the input row. Pure geometry plus hysteresis, fail-open,
complements the iOS-only keyboard lift. Ported fromdsh-web-mobile
v3.2.2 (#246). - Manual ask-card fullscreen with answer scroll-back — an expand button
in the card header grows the card to the measured header-to-composer
band (only when that band beats the hostmin(60vh, 520px)cap), and the
focused answer field is scrolled back into the body intersect visual
viewport, so typing stays visible with the keyboard up. Manual toggle
only, fail-open to the host default. Ported fromdsh-web-mobile
v3.2.2 (#246).
Fixed
- Drawer menu taps stay inside the drawer — a
panelRowbutton that
opens a menu (aria-haspopup) no longer counts as navigation, so the
More menu no longer closes the drawer and floats over the session.
Ported fromdsh-web-mobilev3.2.2 (#246). - Ask-card overflow convergence and 44px touch targets — the card body
pinsoverflow-x: hidden, the question title caps at
min(23vh, 155px)with internal scroll, and the header actions widen to
a 20px gap with 44x44 hit boxes. Ported fromdsh-web-mobile
v3.2.2 (#246).
What's Changed
- feat(mobile): port upstream v3.2.2 phone batch by @ddtcorex in #87
- chore(release): 1.8.0 by @ddtcorex in #88
Full Changelog: v1.7.4...v1.8.0
v1.7.4
Fixed
- Large JSON responses no longer block the host event loop — response
compression runs off the event loop (brotliCompress/gzipvia the
libuv thread pool);end()returns before headers are written and the
reply completes from the zlib callback. Callers that flush headers during
the window win (body replays verbatim), compression errors degrade to
identity, and the callback never throws (process-wide patch). Fixes the
first-run notice stuck at saving behind a multi-megabyte response.
Ported fromdsh-web-mobilev3.2.1 (#233). - The image lightbox keeps a full-viewport hit area on tablet — the
tablet dialog cap (min(100vw - 32px, 720px)centered) matched the host
ImageLightboxthrough its:not(...)branch (stage last child is the
closebutton > svg), leaving a dead strip on the right side. A positive
div[role="dialog"][aria-modal="true"]:has(> :first-child > img)override
restores full-bleed after the cap. Ported fromdsh-web-mobilev3.2.1
(#234).
What's Changed
- fix(mobile): compress large JSON off the event loop by @ddtcorex in #84
- fix(mobile): keep the image lightbox full-viewport on tablet by @ddtcorex in #85
- chore(release): 1.7.4 by @ddtcorex in #86
Full Changelog: v1.7.3...v1.7.4
v1.7.3
Fixed
- Chrome for Android no longer pans the whole page when the keyboard opens
— the viewport meta gainsinteractive-widget=resizes-content, opting
into the WebView keyboard model (layout viewport shrinks instead of the
browser panning the document and scrolling the top bar off-screen).
Engines that do not recognise the field (iOS Safari, Firefox Android)
ignore it. Consequence folded in: a shrinking layout viewport collapses
the classicinnerHeight - visualViewport.heightkeyboard inset to ~0,
so both keyboard readers now compare against the shared resting baseline
(restingViewportHeight, one home incore/composer-dom.ts) — otherwise
the tap-time blur fires while the keyboard is up and the composer jumps.
Ported fromdsh-web-mobilev3.2.0 (#226). - The stats strip is marked again on hosts that split StatsPills — the
slow path hunts in two tiers (_rootfirst, then[data-slot]inside the
composer stack) sharing oneisStatsStripCandidatepredicate, so a future
host reshape fixes one place. Tier 2 is forward-looking for the 0.2.1
split shape (unverifiable live: the installed host is 0.2.0-rc.2, where
tier 1 short-circuits) — confirm on a real 0.2.1 host before relying on
it. Ported fromdsh-web-mobilev3.2.0 (#216). - The drawer keeps an opaque fill under wallpaper plugins — gated on
body[data-dsh-bg], light#ffffff(this file's own bg-base fallback)
and dark#151517(upstream's bg-base fallback for the same token); the
no-wallpaper path keeps resolving the theme token. Appearance under a real
wallpaper install still needs on-device re-check. Ported from
dsh-web-mobilev3.2.0 (#229). - Edge-zone touches no longer lose their native scroll on iOS — the
swipetouchmoveprevent is gated on the horizontal axis lock
(shouldPreventTouchMove); pre-lock touches scroll natively
(touch-action: pan-yalready covers the horizontal race). Ported from
dsh-web-mobilev3.2.0 (#223).
Audited, no change
- Header chips need no 0.2.1-split fix: the lineage trigger is already a
shape anchor (_trigger[aria-haspopup][aria-expanded], not_root) and
the hover scope already a marker + role (data-lineage-root,
role="tree"); the jobs chip is already role/slot-identified. The
_titleRow36px reserve sits on a non-scroll flex row for an out-of-flow
toggle, sopadding-rightis correct there (the #217 margin lesson applies
to scroll containers only).
What's Changed
- fix(mobile): reference dsh-web-mobile v3.2.0 batch by @ddtcorex in #82
- chore(release): 1.7.3 by @ddtcorex in #83
Full Changelog: v1.7.2...v1.7.3
v1.7.2
Fixed
- Settings sheet probe follows the Account menu — since DSH 0.2.1
(account login) Settings no longer lives in the drawer: the drawer holds
Plugins / Automation tasks / the Account menu, and Settings is a menuitem
inside it. The probe clicked a drawer Settings entry that no longer exists
and aborted atsheet.open; it now drives Account menu → Settings, with
all geometry assertions unchanged. Shipped code needed no change (sheet
bottom-anchoring, horizontal nav pills, close hit-test, drawer footer slot
and toolbar reparent all verified live on 0.2.1-rc.2).
Fixed
- In-place TPS text rewrites wake the stats task again — the shared
full-tree observer never openedcharacterData, so frames where React
rewrites the TPS text node without rebuilding it woke no task and the
readout sat stale. The observer now delivers those records through an
explicitcharacterDatadirty key (mutationDirtyKeyin the DOM-free
core, fail-open to the tree sentinel), and the stats task declares that
key next to'*'. Ported fromdsh-web-mobilev3.1.0 (C08). - A throwing host
selectPanelno longer wedges the panel exit — the
call runs after the exit state flips but before the fallback timer arms,
so a host error left the exit stuck with no safety net. The fallback now
installs in afinallywhile the error still propagates. Ported from
dsh-web-mobilev3.1.0.
Changed
- DSH peer floor raised to
0.2.0-rc.2(>=0.2.0-rc.2 <0.3.0-0on every
@deepseek-ai/dsh-*peer, owner decision 2026-10-07). Hosts older than
0.2.0-rc.2 are no longer supported. Measured: all 13 peers publish
0.2.0-rc.2, which pnpm resolves under default prerelease semantics and the
DSH gate accepts withincludePrerelease.
Removed
-
Pre-0.2.0-rc.2 host compat in the session-delete row resolution: the
displayTitlelegacy label match and the tolerance for a missing
session.untitleddictionary key.untitledLabelis now a required input. -
Pre-0.2.0-rc.2 host compat in session deletion: the refusal of a live
session on a host without an agent disposal face (every supported host's
AgenthascancelandwhenIdle) and the flatpersistence.list()entry
shape (supported hosts return snapshots carrying.header). A live session
that does not converge to idle is still refused with409 session-busy. -
Pre-0.2.0-rc.2 host compat in the client: the rc-era and Regular host
icon candidates (the host exports the Medium weight), the dropped
SessionListState.currentandsessions.clearmembers, and the manager
snapshotitemsread in session row resolution. -
Legacy
?mobile-nav-debug=1diagnostics alias, only
?dsh-maestro-mobile-debug=1shows the floating debug bar now.
What's Changed
- chore(mobile): raise the DSH peer floor and remove old host shims by @ddtcorex in #76
- fix(mobile): prefix locale namespace and pin streaming fast path by @ddtcorex in #77
- fix(mobile): wake stats-line on in-place TPS text rewrites by @ddtcorex in #78
- fix(mobile): arm panel-exit fallback even when selectPanel throws by @ddtcorex in #79
- fix(mobile): open Settings via the Account menu in the sheet probe by @ddtcorex in #80
- chore(release): 1.7.2 by @ddtcorex in #81
Full Changelog: v1.7.1...v1.7.2
v1.7.1
Fixed
- Deleting a session no longer reports failure when it succeeded — the
host moves the session to trash then aborts the reply, so the browser
fetch rejects withFailed to fetchfor a delete that landed. The
session list is now the judge: on a rejected request the list is
re-read bounded (4 attempts x 350ms) and only an id that survives every
attempt shows the error row. - Opening the model / reasoning-level menu no longer raises the soft
keyboard — the host drills into the model pane and focuses its search
field from a passive effect. The field's focus method is shadowed, armed
on capture-phasepointerdownstrictly before the effect can run; a real
tap on the field still focuses natively. No hashed-class fallback, per
the hash-prefix ban. - The iOS composer stays above the form-assistant bar while typing —
the host selection-scroll helper mixes layout coordinates with the visual
caret rect once the iOS keyboard forces a nonzero window scroll, dropping
the seat behind the assistant bar on every keystroke. The seat is lifted
from screen-space quantities only (seat bottom vsvisualViewport
height), fail-open on pinch or scroll-channel drift. The active path
needs a real iPhone; headless cannot verify it. - Dragging a selection handle across the header no longer jumps the
viewport to the top of the session — while a conversation selection
exists the header seat exits hit-testing (pointer-eventsonly, no
layout change), so the drag stays in message text. Complements the
data-mobile-nav-draggingdrawer-yield mark on a different axis.
What's Changed
- docs(mobile): correct row id, slots, paths and test commands by @ddtcorex in #70
- ci: call the reusable workflows at their new repository name by @ddtcorex in #71
- fix(mobile): prose floor at the host secondary tier, and opt code out of iOS text autosizing by @ddtcorex in #72
- fix(mobile): port upstream v3.0.5 phone fixes by @ddtcorex in #74
- chore: release 1.7.1 by @ddtcorex in #75
Full Changelog: v1.7.0...v1.7.1
v1.7.0
Added
- Opening or switching to a session no longer raises the soft keyboard:
the host'sInputBarfocuses the Lexical editor from a React effect keyed on
sessionId(InputBar.tsx:178-182), so every switch handed the WebView a DOM
focus and both iOS and Android put the IME over half the screen. A new
session-focus-guardeffect arms an 800 ms window on a real current-session
change and suppresses that focus in two layers, because one is not enough: an
own-property shadow over the editor'sfocus(), plus a synchronous
focusin-capture blur. The blur must be synchronous because a remount's host
focus runs in the commit's synchronous layout-effect phase, before any
MutationObservermicrotask, so the freshly mounted editor is focused while
it still carries no shadow; a deferred blur does not help either, since the
IME has already begun by the next macrotask. The window arms only on a real
switch (retainedBy.mainView > 0, the same rulesession-menu.ts:73uses,
because0.1.6-alpha.2droppedSessionListState.current), so list churn
cannot flash the guard, and apointerdownon the editor closes the window
early so a user's own tap always opens the keyboard. The guard owns its own
data-mobile-nav-session-guardmarker and deliberately does not touch
data-mobile-nav-focus-shadow, whichcomposer-plus-toggle.tsowns on
documentElementandcomposer-focus-release.ts:269reads. Added
pnpm probe:session-focus, an INFO probe that reports the DOM half of the
guard against a live host and exits 0 unconditionally. Gated on a device: the
soft-keyboard behaviour cannot be settled headless.
Fixed
createFocusShadowis now reference counted per element. The composer's
+menu shadow and the new session guard can hold the same editor at once, and
they were independent instances: whichever restored first ran
delete element.focusand unblocked nativefocus()for the other, so the
surviving guard still reportedarmedwhile the keyboard rose inside the
window it believed it owned. Holds now live in aWeakMap<FocusableLike, number>
and only the last holder deletes the override, which protects every existing
caller; a repeatedrestore()stays a no-op, so a stranded hold can never
block focus forever.
What's Changed
- fix(mobile): keep the phone font floor on prose only by @ddtcorex in #64
- fix(mobile): drive the panel probe with a real touch on a clear screen by @ddtcorex in #65
- fix(mobile): re-anchor dsh peer ceiling to 0.3.0-0 by @ddtcorex in #66
- fix(mobile): resolve a title-less session row after the 0.2.0-rc.2 label move by @ddtcorex in #67
- feat(mobile): stop a session switch from raising the phone keyboard by @ddtcorex in #68
- chore: release v1.7.0 by @ddtcorex in #69
Full Changelog: v1.6.2...v1.7.0
v1.6.2
Fixed
- The session-row Delete item is back on 0.1.7 hosts — the host composes
the row menu from a slot list and 0.1.7 prepended "Pin session", so the menu
holds four items and the oldlabels.length === 3signature stopped matching:
Delete disappeared with no error anywhere. The gate is now an inclusive label
signature (rename + fork + archive present, the unarchive twin rejected, extra
items allowed), read from the live host locale so wording changes stay
harmless. Verified live on 0.1.7-rc.2: five items with Delete injected;
pnpm probe:session-delete7/1/0 (the skip is the destructive step, which
needs an explicit session id). - The Settings sheet is a bottom sheet again — the overlay guard was written
as:has()nested inside:has(), which is an invalid selector: Chrome
throws and drops the whole rule from the CSSOM with no error, so the four
overlay rules never applied a single declaration and the sheet rendered as a
floating centred card (equal 56.5px gaps on an 844px viewport) since
2026-08-30, while every source-text assertion kept passing. Flattened to
overlay:has(navList); measured after: 4 overlay rules in the CSSOM,
align-items: flex-endlanded, panel bottom flush with the viewport. Guards:
tests/css-selector-validity.test.tsfails the build on the nested shape and
pnpm probe:settings-sheetasserts the CSSOM rule count plus the
bottom-anchored geometry (8/8) — a text regex cannot see a rule the browser
rejected. - An oversized delete body is answered, not cut off — the route destroyed
the socket at the overflow point, racing the error response so the client saw
an empty reply instead of the reason. The reader drains to the natural end
and the route answers 413payload-too-large; the cap stays at 64 KiB.
Verified live after the host restart: a 70 KB body returns the 413 JSON. end(callback)no longer writes the callback's source into the response
— the deferred-response replay forwarded theend()tail blindly: a function
argument was buffered as body text (and the callback never fired), an
encoding token was written as a second body chunk, and a bufferedwrite()'s
callback was dropped. Arguments are now classified (function = callback,
encoding = consumed by the buffering, buffered write callbacks replay once,
in order, right after the realend()).
tests/compress-response.test.tsdrives a real HTTP server through the patch
and fails all three behaviours against the pre-fix code.
Added
- Deleted session logs are recoverable for 24 hours — the directory moves
into<root>/.sessions-trash/with every canonical payload renamed to
<name>.trashplus a best-effortmanifest.json, instead ofrm -rf. The
rename precedes the move because the host scans every directory under the
storage root as a project directory, trash included, and a canonical payload
name there breaks the whole session list; entries older than 24h are purged
inline on a later deletion, never on a timer. A stash failure after a live
teardown answerscleanup-failedwithdeletedLiveSession: trueand still
settles the workspace accounting, so the caller is never told a session is
intact when it is already gone — and the client refreshes the list on that
code for the same reason. - A reparent-leak probe —
pnpm probe:reparent-leakdrives a Settings
open/close cycle and a session switch and asserts no moved host node is
orphaned and no page exception orconsole.erroris raised (5/5 on rc.2).
That measurement is why the three reparent sites stay: same hazard class as
upstream's #104/#105, no observable fault here.
What's Changed
- fix(mobile): rc.2 session-menu compat, sheet anchoring, and delete-route hardening by @ddtcorex in #62
- chore: release v1.6.2 by @ddtcorex in #63
Full Changelog: v1.6.1...v1.6.2
v1.6.1
Removed
- Dead modules, and the composer marker got one home — a reachability +
reference scan oversrc/(every module reachable from the two entries, every
export referenced somewhere,tsc --noUnusedLocals --noUnusedParametersclean
on both faces) left four things to delete:
components/BottomSheet.tsxand itsstyles/sheet.css.tssection — the
component had no importer and its[data-mobile-sheet]rules therefore styled
nothing; the walker that builds the client bundle never inlined it, so only the
stylesheet section was being shipped.effects/overlay-backdrop-fab.ts— the
legacy manualframe.appendChildbackdrop/FAB task: nothing installed it, and
because itsfadeHookwas the only thing that ever set it, the swipe-close
callfadeOverlayOut()had been a silent no-op.isGestureConsumed— a
gesture-guard.tsexport referenced nowhere, not even by its own tests. The
[data-composer-input]literal now lives once, incore/composer-dom.ts,
together with theeditorElement()copy both composer effects carried and the
visual-viewport reader; five modules used to spell the marker out.
probe:composer-plus17/17,probe:pointer-gating6/6 and
probe:multi-width33/33 after the deletions,pnpm test223/223.
Fixed
- The drawer's dimming layer fades with the slide-out again — the swipe
layer's close commit called a fade that had been a silent no-op since the
shell.overlayslot took over the backdrop (its hook's only setter lived in
the legacy overlay task nothing installed), so the dimming snapped away ~280ms
after the drawer had already left: the host flipsdata-sidebar-collapsedwhen
the animation lands, and React unmounts the layer at that moment. The new
effects/backdrop-fade.tsresolves the layer itself and drives the same two
inline properties the retired task did —opacityandpointer-events, both
!importantso React's style prop and the layer's entry animation cannot
outrank them — with the rect flushed between the transition and the target so
the fade provably starts at opacity 1. Resolving the element instead of taking
a registered hook is deliberate: that seam is what went stale, and a function
that finds its own element cannot. No restore is needed, because the layer is
conditionally rendered per open. Gate:pnpm probe:backdrop-fade(4 rows: the
layer opens opaque, goes mid-fade while the drawer slides — 12 partial frames
and a final opacity of 0 — lands gone with the drawer closed, and a
non-animated backdrop tap still closes cleanly). A/B: with the fade call
disabled the mid-fade row FAILs with the layer at opacity 1 for the whole
755ms window, which is the reported symptom. - The composer "+" stops raising the keyboard on the host's second focus —
reported after the release above landed: tapping+with the keyboard dismissed
still brought it up. The focus shadow was lifting too early — the host focuses
the editor again from the effect that runs when its menu opens, which is after
this plugin's bubble handler has read the menu as "not open" (mounted, not yet
laid out).FOCUS_SHADOW_MIN_MS(700ms, still bounded by the 1.5s cap) makes
the window a floor rather than a click-scoped one, and afocusincapture-phase
blur takes back any focus that reaches the editor during the window, because a
blur in a macrotask is too late (the IME has started). Both come from the
community pluginmexiaosqwq/dsh-web-mobile'scomposer-keyboard-guard.ts,
which measured the timing on a real iPhone; their v3.0.2 does not touch this
path.probe:composer-plusgates the window (shadow-window-outlives-the-click,
shadow-lifts-on-its-own), and the take-back rule is unit-tested
(shouldTakeBackArmedFocus) because this host leaves the editor unfocusable
while its menu is open, so a focus-driving probe row would pass either way.
Refs: mexiaosqwq/dsh-web-mobile v3.0.0/v3.0.1. - The composer no longer drops the keyboard while it is being typed in —
reported from the phone as "the keyboard hides by itself" after the fix above
landed. The release trusts the keyboard reading, and that reading was the
classicinnerHeight - visualViewport.heightinset: on a page that cannot
scroll (this shell is a full-height flex layout), iOS shrinks the LAYOUT
viewport with the keyboard too, so both heights move together and the inset
stays near zero while the keyboard is up. The reading now also compares against
the tallestvisualViewport.heightseen recently (resetting on rotation) and
treats either signal saying "up" as up, and a keystroke in the editor
(beforeinput/input/keydown/compositionupdate) keeps the release
away for 1.5s whatever the viewport says.probe:composer-plusgained
focus-release-holds-when-both-heights-shrink(emulating the iOS case: both
heights shrink by the same amount), A/B-validated against the inset-only
reading, where it FAILs with the editor losing focus and the release count
rising while the emulated keyboard is up. - The iOS keyboard stops coming back on the first tap of the composer "+" —
reported from the phone after the focus shadow landed. The shadow neutralises a
focus()the host calls, and the tap that raises the keyboard focuses nothing
at all: iOS keeps the composer's contenteditable as the active element once
the keyboard goes away, and WebKit shows the keyboard for that retained
editable on the next tap. The plugin now releases the focus while the keyboard
is hidden — at install, on a 500ms heartbeat, on an editorfocusin, on a tap,
onvisualViewportresize/scroll, and as soon as the "+" shadow lifts — and
never during the tap (the uncompensated blur that bounced the composer row),
never while the keyboard is up, never inside the 700ms grace after a finger
lands on the editor, and never while the visual viewport is unreadable (a
pinch-zoomed viewport reads as "no keyboard"). The blur's own viewport nudge is
undone by restoring the scroll on the next frame when it moved by
NUDGE_MAX_PXor less. The invariant is a state, not an event: measured, a
focus can land on the editor with nofocusindispatched at all, which is what
the heartbeat is for.probe:composer-plusruns the battery under an iPhone
user agent (the release arms ondetectIosWebKit) and gained three rows for
it — precondition, release armed, and the release resuming after an editor tap
— A/B-validated against the release inverted to never install for iOS, where
they report the retained focus,releases=null, and the retained focus
surviving a "+" tap. - The composer probe no longer inherits the typing scene's fake viewport —
the scene replaceswindow.visualViewportto emulate a keyboard and only put
back an own descriptor, which Chrome does not have (the property lives on the
prototype): the stub outlived the scene, every later row read "keyboard up",
and the focus rows were silently disarmed. It now deletes the stub when there
is nothing to restore, and acomposer.focus-release-preconditionrow reports
the keyboard inset it is asserting against. A probe throw also lands as a
probe.crashedFAIL row with aSUMMARYinstead of a stack, and the
touch taps use the camelCasetouchEndCDP event type (touchendis rejected
with-32602, which killed the run mid-battery).
What's Changed
- fix(mobile): use an installer-resolvable dsh peer floor by @ddtcorex in #56
- fix(mobile): stop the jobs chip from taking the lineage trigger's place by @ddtcorex in #57
- fix(mobile): scope the lineage chip's hover swallow to the chip, not to a stale hash by @ddtcorex in #58
- fix(mobile): hold the composer keyboard on iOS and drop dead modules by @ddtcorex in #59
- fix(mobile): fade the drawer backdrop with the slide-out again by @ddtcorex in #60
- chore: release v1.6.1 by @ddtcorex in #61
Full Changelog: v1.6.0...v1.6.1
v1.6.0
Added
- Sidebar panels have a way back to the conversation — a global panel
(Plugins, Skills, …) replaces the main column and the host ships no exit:
PanelRow's onClick is a bareselectPanel(id), so re-tapping the selected
row re-selects it, and a panel page renders no session header — which is
where the drawer toggle lives. One sharedexit()now serves three routes:
the system back key (one history entry armed while a panel owns the main
column, given back when the panel leaves by another route so it cannot
swallow the user's next back press), a re-tap of the already-selected panel
row ([class*="panelRow"][aria-current="page"]), and the shell FAB, which
reads as "back to conversation" there and moves to the top-left corner
(data-mobile-nav-fab-mode="exit-panel") instead of the hero seat that used
to float over the panel's own head. The host's panel-selection face is
capability-probed, so a host generation withoutselectPanelleaves every
route inert instead of throwing. Refs: mexiaosqwq/dsh-web-mobile v3.0.0/v3.0.1. pnpm probe:composer-plus,pnpm probe:panel-exitand
pnpm probe:multi-width— three live gates for the phone interactions
above: the composer menu across four taps (open/close/open/close), the panel
FAB back face plus back-key and re-tap exits and their history bookkeeping,
and the layout tiers (phone 320/360/390/430, tablet 768/1023, desktop 1280)
including the header icon actually painting ansvg path. All three were
A/B-validated against the pre-fix behaviour so they fail on a build that
lacks the fix.pnpm testgainstests/phone-tier-gating.test.ts, which
locks the tier structure (amin-width: 768pxblock must also cap 1023 and
require a coarse pointer; the tablet overrides must come after the mobile
gate).
Fixed
- A second tap on the composer "+" closes the command menu — the button's
onClickfocuses the editor before toggling the launcher, and that focus
re-enterscontroller.track(), whose first act isclearLauncher(); the
host's "already open ⇒ dismiss" branch then compares against a null launcher
and is unreachable, so every tap re-opened the menu (the host's own
aria-expandedstaysfalsewhile it is visibly open). The plugin now
remembers the menu's state in the click capture phase and, in the bubble
phase after React's handler, closes it through the host's own Escape path
when it is still open. The opening tap is never touched. - Tapping "+" releases the editor focus while the menu is open — the
command menu needs no soft keyboard, but that same focus call re-raises the
IME on Android and slides the composer row up under the user's second tap
(measured upstream: visual viewport 754 → 471 about 170ms after the tap, with
no DOM event reaching the page). A tap on "+" now drops the focus before the
click fires and repeats the release at 120/320/640ms while the menu is on
screen, cancelled the moment the user touches the editor. - The two probe reds are gone —
probe:panel-fontnow opens a session
through the drawer (the injecteddsh.sessions.currentis only a hint on this
host) and waits for the conversation history to render before measuring the
content font axis: it used to sample while the flow still showed
Loading history..., so the axis row was red against a placeholder. The axis is
live on 0.1.7-alpha.2 (15px -> 22px, floor held).smoke:cdpno longer seeds
localStorage['dsh.sessions.current']with the requested session: the restored
selection and the probe's own navigation opened the same session twice, the app
released the first reference while the right sidebar awaited it, and the host
loggedSidebar Session opening failed: ... is released— a page error from the
probe's boot, not the plugin (with an unrestorable id the same drive logs zero).
The scenario opens a session through the drawer instead, which also exercises
the real header toggle. Result:probe:panel-font12/12 andsmoke:cdp
17 pass / 1 state-skip / 0 fail withpage.errors count=0. - Tapping "+" no longer raises the keyboard on iOS — the Android half of
this (blur the editor after the tap) is not enough there: iOS follows DOM
focus, so once the host'sfocusDraftEditorinside the button'sonClickhas
run the keyboard is up and a laterblur()does not take it back (reported on
iOS after the first fix landed). The editor's programmaticfocusis now
neutralised for the length of the interaction (armed on pointerdown and again
in the click capture phase, which is what covers a click with no pointerdown
at all), and it is always given back — on the next tap anywhere, as soon as the
menu leaves the DOM, on a hard 1.5s cap, and on dispose — because an override
that outlives the interaction is worse than the bug it prevents: the editor
could never be focused again (the exact bug the community plugin fixed in
v3.0.1). The blur the Android fix introduced is now conditional — it runs only
while the keyboard is already hidden (shouldDropEditorFocus, read from the
visual viewport). Blurring an editor whose keyboard is UP starts the hide
animation, and the keyboard is the composer's floor, so the row slid down under
the finger on the FIRST tap only (reported from the iPhone after the first iOS
fix); blurring one whose keyboard is already down is the state the IME re-rises
from, and there the blur is what keeps the row still.probe:composer-plusnow
gates all of it, including a typing-state scene (emulated keyboard) where the
focus must survive the tap. A second phone report pinned the blur itself as the
remaining movement: the composer bounced up and back within 10-20ms, which no
keyboard can do (iOS takes ~250ms to show or hide one) but a programmatic blur
on iOS can, because it nudges the visual viewport. iOS therefore never blurs —
the focus shadow is the whole defence there — while Android keeps the blur,
which is what stops the IME re-rising into a hidden-keyboard editor. The shadow
is also armed ontouchstartnow (idempotent), because iOS can fire the touch
before the pointer event. - Host icons are resolved by name at runtime — the host's icon exports are
generation-specific (IconXxxOutline16on the 0.1.0-rc line versus
IconXxxOutlineRegular/…Mediumon 0.1.7) and the two generations share no
names, so a static import resolves toundefinedon the other one and React
reports "Element type is invalid" for the whole plugin tree. The first
candidate name the installed host actually exports now wins, and a name
nothing matches renders nothing rather than crashing. - The plugin stylesheet replaces its previous copy — a plugin re-applied in
the same JS environment (client hot reload, or a second apply whose dispose
never ran) stacked a second<style>tag with the same rules, and the older
tag could win on source order inside the cascade: the symptom was "I changed
the CSS and nothing moved" while the served bundle was correct. The mount now
removes any tag carrying the plugin'sdata-pluginmarker first, keeps the
fresh tag last in<head>for its!importantoverrides, and never
resurrects a disposed tag from the deferred re-append.
What's Changed
- chore: target DSH 0.1.7-alpha.2 by @ddtcorex in #51
- feat(mobile): panel exit, composer "+" fixes and icon/stylesheet hardening by @ddtcorex in #52
- fix(mobile): keep the iOS keyboard down when the composer "+" is tapped by @ddtcorex in #53
- fix(mobile): clear the two probe reds at their source by @ddtcorex in #54
- chore: release v1.6.0 by @ddtcorex in #55
Full Changelog: v1.5.0...v1.6.0