Repository navigation
fix(file-preview): select and copy text in Markdown and text previews - #3193
Merged
stamp-build[bot] merged 3 commits intoOct 19, 2026
Merged
Conversation
stamp-build Bot
pushed a commit
that referenced
this pull request
Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rendered Markdown in the file preview is not selectable, so long-press did nothing. Long-pressing the body now opens Select Text (the same native selectable dialog a message uses, moved to SlockTextSelectionDialog), and the overflow menu adds Copy Markdown / Copy Text and Select Text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stamp-build
Bot
force-pushed
the
agent/codex-android-devops/file-preview-text-select
branch
from
October 19, 2026 18:22
2c36c65 to
bf41e6b
Compare
…924 audit check_source_constraints.py audits ThreadMessageTextSelectionDialog in ThreadPage, so the message dialog stays as it was; the file preview uses its own SlockTextSelectionDialog with the same native SelectableText shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stamp-build Bot
added a commit
that referenced
this pull request
Oct 19, 2026
…#3193) (#3194) Cherry-pick of a52d784 for 1.14.2. Release branch adaptation: the dialog card keeps the 1.14 cream background + border (slockRaftCardSurface is main-only), changeset filed under 1.14.2. Co-authored-by: stamp-build[bot] <328845999+stamp-build[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
stamp-build Bot
pushed a commit
that referenced
this pull request
Oct 19, 2026
…aftNativeSelectableText Merged main brought SlockTextSelectionDialog, written against Kuikly's SelectableText; render it through the CMP platform text view instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build>
stamp-build Bot
added a commit
that referenced
this pull request
Oct 20, 2026
…form (#3192) * build(buildSrc): extract the no-Kuikly dependency guard for reuse Move the verifyCoreHasNoKuikly task body into buildSrc as registerNoKuiklyGuard() so the upcoming Compose Multiplatform UI module can register the same source + resolved-graph check. :core keeps its task name, configurations and wiring (check, preBuild, compileKotlin*). Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(ui-cmp): add the Compose Multiplatform page module and router split New :ui-cmp module (Android + iOS, CMP 1.8.2) on top of :core and :designsystem, guarded by verifyUiCmpHasNoKuikly: - SlockCmpPages/SlockCmpRouter: the single migrated-pages allowlist that decides Kuikly vs CMP per page name. Product list is empty; debug builds admit compose_cmp_hello. Each page carries a rollback flag key and a SlockCmpRouteOverride seam (none installed: code default decides). - SlockCmpRoot: RaftTheme, LocalSlockPageArgs (page name + unchanged pageData JSON) and LocalSlockHost (HostCapabilities + host navigator). - BridgeWireHostCapabilities: HostCapabilities over the HRBridgeModule wire format (same method names and Kuikly-compatible params JSON as BridgeModule), so both native bridges serve CMP pages unchanged. - SlockCmpIos: ComposeUIViewController factory and the Swift/ObjC-facing bridge and navigator protocols. - compose_cmp_hello: debug demo page (theme, args, toast, close). Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(android): host Compose Multiplatform pages in SlockCmpActivity - SlockCmpActivity (ComponentActivity): renders SlockCmpRoot for the pageName/pageData extras SlockKuiklyActivity uses; edge to edge with adjustResize, system-bar icons follow the resolved Raft theme, system back and closePage finish the Activity, openPage goes back through SlockKuiklyActivity.start. - Router split: slockKuiklyHostActivityClass picks SlockCmpActivity for pages SlockCmpRouter routes to CMP, so SlockKuiklyActivity.start (the Kuikly router adapter, MainActivity, notifications) needs no change. Intents naming SlockKuiklyActivity directly (diagnostics deep-link alias, tests) are forwarded once routed. SlockApplication configures the router (debug builds admit the debug-only demo page). - HostCapabilities: BridgeWireHostCapabilities over a SlockKuiklyBridgeModule instance, called on the main thread like the Kuikly renderer does. The module takes an optional host Activity; Kuikly pages pass none, so their module still derives it from the render context. Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(ios): push Compose Multiplatform pages from the existing navigation - :shared links :ui-cmp into the iOS framework and exports it (the app keeps a single Kotlin framework); :core stays unexported. - SlockPageControllerFactory (Swift): the iOS router split. KRRouterHandler, HRBridgeModule openPage and the SwiftUI root ask it for a page; routed pages get SlockCmpIos.makeViewController (ComposeUIViewController) with the unchanged pageName/pageData, everything else the same KuiklyRenderViewController as before. Configured at launch (DEBUG admits the debug-only demo page). - SlockCmpPageNavigator: push/present and pop/dismiss like KRRouterHandler. - SlockCmpBridgeHost (ObjC): HostCapabilities transport that forwards each call to a private HRBridgeModule with the Kuikly module argument shape. - Compose resources: compose.ios.resources.sync stays false; the Podfile script phase now also runs :ui-cmp:syncComposeResourcesForXcodeApp, which copies the prepared resources of :designsystem and :ui-cmp (Raft fonts) to <App>.app/compose-resources/composeResources/<package>/, the path the CMP runtime reads. The committed CocoaPods phase in project.pbxproj matches. Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci: cover :ui-cmp in tests, build cache key, iOS compile and source scans - test.yml: run :ui-cmp:testDebugUnitTest, compile it for iosSimulatorArm64, upload its reports, and add :designsystem/:ui-cmp sources and build scripts to the build-output cache key and paths (androidApp now depends on both, so a change there must miss the cache). - plan_test_tier.sh: ui-cmp iosMain changes keep the iOS compile on main. - check_auth_session_writes.sh and i18n_code_literal_scan.py include the ui-cmp source roots. Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * docs(designsystem): describe how the iOS app now receives the fonts Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(cmp-shell): review fixes — iOS nav bar/full-screen pop, Android flag forwarding, map-compat scan, host gap doc - iOS: CMP controllers hide the system nav bar and allow full-screen swipe-back, matching KuiklyRenderViewController under the FD swizzle. - Android: forward NEW_TASK/NEW_DOCUMENT/MULTIPLE_TASK when redirecting a routed page from SlockKuiklyActivity to SlockCmpActivity. - check_commonmain_map_compat.sh also scans ui-cmp commonMain. - Document the Android capability gap for SlockCmpActivity hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ui-cmp): use the raft-design type scale names in the demo page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * build(shared): compile :shared against upstream Compose Multiplatform instead of Kuikly Phase A of the CMP big switch. :shared drops the Kuikly compose fork, Kuikly core/core-annotations, core-render-android, the Kuikly KSP page processor and mermaid-kuikly, and depends on CMP 1.8.2 (ui, foundation, material3, components-resources, ui-backhandler), JetBrains lifecycle 2.8.4, Coil 3.2.0, :designsystem and :ui-cmp (now api on every target). compileSdk moves to 35 as upstream Compose 1.8 requires. NetworkKMM, resource-core and the KuiklyBase datetime stay. The branch is expected not to compile until the per-file fixes land. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * chore(scripts): add the Kuikly-to-CMP reference rewrite script scripts/cmp_migrate_imports.py rewrites every com.tencent.kuikly.* name in :shared, :androidApp and the vendored markdown sources: the forked compose packages to androidx.compose.*, Kuikly-only symbols and Kuikly core (Pager, Module, nvi JSON, @Page) to the build.raft.app.compat layer under their existing simple names, Kuikly lifecycle to JetBrains androidx.lifecycle. It is idempotent so it can be re-run after rebases; --check reports pending changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * refactor(shared): run cmp_migrate_imports.py over :shared, :androidApp and markdown Output of scripts/cmp_migrate_imports.py; no hand edits. Remaining com.tencent.kuikly.core.base / core.views references are the native-view DSL and are migrated by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * chore(scripts): map Kuikly PlayState, VideoPlayControl, BorderStyle and Size core.views.PlayState / VideoPlayControl and core.base.BorderStyle go to the compat layer; core.base.Size (markdown image sizes) maps to androidx.compose.ui.geometry.Size, which has the same width/height shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): add the build.raft.app.compat layer for Kuikly-only APIs Keeps the Kuikly API surface :shared pages use, under the same simple names, on top of upstream Compose and the CMP hosts, so the per-file fixes stay small: - compat.json: JSONObject / JSONArray / JSONException with Kuikly's coercions and byte-identical toString (via toSlockJsonString). - compat.pager: Pager / ComposeContainer (setContent member), PageData (params + insets filled from WindowInsets), @Page, IPagerEventObserver, IModuleCreator, PagerManager, BridgeManager, IPagerId/pagerId, LocalActivity (getPager(), pageData), urlParams. - compat.module: Module (toNative over SlockCmpHost.bridgeTransport, main-thread callbacks), RouterModule (host navigator), CodecModule (sha256 in Kotlin), PerformanceModule (null data), PerformanceData. - compat.ui: KuiklyFont (Android res/font, iOS SystemFont), BackHandler (CMP ui-backhandler), keyboardHeightChange (WindowInsets.ime), nestedScroll(NestedScrollMode), rememberAsyncImageBitmap (Coil), DrawableResource/imageResource, TextInputState, BorderStyle, PlayState, VideoPlayControl; setProp/bouncesEnable/scrollToTop/ keyboardDismissModeInteractiveIOS are documented no-ops. Supporting changes: SlockCmpHost gains an optional bridgeTransport (both hosts pass theirs); BasePager drops Kuikly snapshot/thread verification; the bundled TTFs move from androidApp to :shared res/font so KuiklyFont can resolve them; :shared compile tasks depend on the SQLDelight generator directly (the Kuikly KSP tasks used to carry that edge) and compose resources get a package. Also includes the reference-rewrite output for the PlayState/VideoPlayControl/BorderStyle/Size rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): register every page for the CMP hosts (RaftPageRegistry) scripts/cmp_generate_page_registry.py replaces the Kuikly KSP page processor: it reads each @Page("name") class (resolving const page-name constants) and writes pageName -> factory maps, one for commonMain and one for the gated visual fixtures (src/visualFixturesOff holds the empty stand-in for builds without -PslockVisualFixtures). RaftPageRegistry exposes pageName -> @Composable for those pages, and RaftPagerHost runs a page inside SlockCmpRoot: creates the Pager with the unchanged pageData, calls willInit/created, fills PageData insets, maps lifecycle resume/pause (and an `active` flag for child pages) to viewDidAppear/viewDidDisappear, and provides LocalActivity and LocalSlockPageArgs. :ui-cmp gets a RaftPageProvider seam (SlockCmpRouter.installPageProvider) that SlockCmpRoot falls back to; hosts do not install it yet and routing (rendersOnCmp) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): compat follow-ups from the first CMP compile - compat.ui: pull-to-refresh stand-in (PullToRefreshState, rememberPullToRefreshState, pullToRefreshItem; indicator only while refreshing, no gesture), PointerEvent.nativeEvent (always null), withKuiklyIdleContext(pagerId, block) with Kuikly's signature. - Android jvmTarget 11 for :shared (Coil 3 inline functions are JVM 11). - Drop compose components-resources from :shared: it has no compose resources and the generator's source wiring breaks under the afterEvaluate setSrcDirs. - Page registry generator skips the compat package and comments (a KDoc example was registered as a page). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): move shared foundations off Kuikly native views Foundational files every page group depends on: - SlockLucideIcon: TEMPORARY stub with the same API (rounded outline in the icon colour, rotation/spin kept); the ImageVector port lands separately. - SlockNativePixelAvatar / SlockNativeTiltedBadgeChrome: drawn with Canvas instead of native Kuikly views. - SlockSandboxWebView: placeholder box (TODO(cmp-switch) AndroidView / UIKitView); controller, navigation state and API unchanged. - SlockKuiklyChildPage: composes the child page inline through RaftPagerHost (active -> appear/disappear), same pageData contract. - SlockTabBar: native floating tab bars fall back to the Compose dock. - BasicWidget: square/themed corners without setProp, Modal on upstream Dialog (no ref/scrimColor), text fields without native props, the native editing-state reader reads the Compose TextFieldValue, Hover uses stickyHeader; TabPager keepItemAlive -> beyondViewportPageCount. - Drop Kuikly's beyondBoundsItemCount list argument (12 call sites), stale base.bridgeModule imports and the Android native dispatch capture; key-event extension imports in AuthPages. - SlockInProcessNotifyModule (a Kuikly render module) moves to the androidApp Kuikly host it belongs to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): settings drawer, thread selection text and mention sheet on CMP - SettingsHomePage: replace Kuikly MoveableDrawer with a new Compose RaftMoveableDrawer (push-style drawer, same open/close/progress/isOpen API, horizontal drag gated by userScrollEnabled, drawer content kept composed like the Kuikly pager page); scrim DivView node becomes a plain Box with the existing clickable; SelectionContainer without Kuikly state; drop com.tencent.kuikly.core imports. - ThreadPage: SelectableText -> SelectionContainer { Text }, import the Modifier testTag (the semantics property was imported instead). - ThreadComposerPage: Kuikly ModalBottomSheet(visible, dismissOnDrag) -> M3 ModalBottomSheet with sheet state (no drag handle, no insets, transparent rectangle container) shown while detailsVisible. - Visual fixtures: drop the SelectableText import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): composer Enter/Send routing on the CMP text field BasicWidget TextField (shared file, additive change): the previously ignored imeActionRequested / Enter routing props are expressed in Compose. - New raftResolvedKeyboardOptions: a multi-line field that did not request an IME action falls back to ImeAction.Default (newline key), matching the Kuikly text area line-mode contract. - New Modifier.raftImeEnterAction: when imeAction is Send, hardware Enter sends on key-up and Shift+Enter inserts a newline (multi-line), porting SlockKuiklyActivity.handleSlockTextFieldEnterKey. Soft-keyboard Send keeps going through KeyboardActions.onSend. TODO(cmp-switch): systemNewlineAction ("Insert newline" selection-menu item) still ignored; iOS soft Send on multi-line fields unverified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): real pull-to-refresh gesture for list-header refresh items Rewrite compat RaftPullToRefreshCompat.kt (owned by this group): the Kuikly-named PullToRefreshState now wraps a material3 pull-to-refresh state; new Modifier.raftPullToRefresh(state, enabled) applies material3 Modifier.pullToRefresh (the PullToRefreshBox engine) to the list, and pullToRefreshItem registers onRefresh/threshold and grows with the pull distance so the existing Slock indicator renders from pullProgress. Wire the modifier at every call site: activity inbox (+ state body), home list, saved panel (SettingsHomePage), members list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): add RaftVideoPlayer and RaftViewCapture for CMP RaftVideoPlayer (expect/actual) replaces Kuikly's native VideoView: - Android: Media3 ExoPlayer + PlayerView in AndroidView, ported from androidApp SlockKRVideoViewAdapter (progress contract/attempt gate, native controller, fullscreen + back handling). - iOS: AVPlayer in an embedded AVPlayerViewController via UIKitViewController, ported from SlockKRVideoViewHandler.m (playback audio session, end/failed/stalled notifications; status and time are polled every 500ms instead of KVO). Keeps the VideoPlayControl/PlayState control surface; seeks are issued once per request id. RaftViewCapture replaces nativeRef + toImage(FILE): Modifier.raftCaptureRef records the subtree into a GraphicsLayer; captureToPngFile() encodes the toImageBitmap() result as PNG into an app-cache/tmp file and returns a file:// ref (admitted by the existing temporary-file retirement). Outside group ownership: shared/build.gradle.kts androidMain adds media3-exoplayer, media3-ui, core-ktx and activity-compose (additive). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): move file preview video and editor capture off Kuikly views SlockFilePreviewKuiklyVideo now renders RaftVideoPlayer (also used by the inline thread video and hidden inline audio players), replacing MakeKuiklyComposeNode<VideoView> and the seekToTimeMs setProp. The image editor captures through Modifier.raftCaptureRef/captureToPngFile instead of nativeRef/ViewRef/toImage; the capture runs NonCancellable so a late file still reaches the operation-gate cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(markdown): paint inline chip boxes and reference underlines on upstream Compose Kuikly's SpanStyle carried inlineBoxStyle and textDecoration color/thickness/offset; upstream SpanStyle has none of them. - RaftInlineBoxStyle / RaftInlineUnderlineStyle travel as string annotations over the boxed range; Modifier.raftInlineDecorations paints them behind the text from the TextLayoutResult (per line segment, rounded fill + border, coloured underline). - SlockMarkdownTextProvider.RichText applies the painter; inline code (vendored annotator), rich-text tag chips / ordinary-mention underline and message-permalink chips emit the annotations. - Pressed tag/permalink states use a span background instead of the Kuikly box swap (TODO(cmp-switch)). - Placeholder(...) now passes PlaceholderVerticalAlign (TextCenter for icons/spacers, AboveBaseline for markdown images). TODO(cmp-switch): chip padding/margins no longer reserve advance width, vertical box extent is estimated from the span font size, and markdown link underline thickness/offset is dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(markdown): draw Mermaid diagrams with a commonMain Compose Canvas mermaid-kuikly (MermaidKuiklyRenderer on a Kuikly CanvasView) is gone. RaftMermaidCanvas ports it to a Compose Canvas over the same mermaid-layout LayoutScene: rects (rounded), ellipses (fill opacity), lines/polylines (dashed 6/4), polygons (linear gradient), text anchored at the alphabetic baseline, #rgb/#rgba/#rrggbb/#rrggbbaa colours. SlockMermaidDiagramBlock is now a plain commonMain function; the identical android/ios actuals are deleted. Build: the mermaid-core / layout-api / layout-simple deps move from androidMain + iosMain to commonMain (KMP artifacts publish common metadata) — shared/build.gradle.kts is touched for this, outside the markdown ownership. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(ui-cmp,shared): route every registered page to CMP; host events and sync bridge calls - SlockCmpRouter: the allowlist becomes "every registered page" (the :ui-cmp debug pages admitted in this build plus everything the installed RaftPageProvider renders). The per-page rollback seam (SlockCmpRouteOverride, rollbackFlagKey, enabledByDefault) is removed: there is no Kuikly host left to roll back to. configure(debugBuild) / rendersOnCmp / page keep their signatures (iOS SlockCmpIos unchanged). - SlockCmpHost gains RaftHostEvents (native host -> page events, the Kuikly sendEvent channel: window insets, adaptive panes); HostBridgeTransport gains callSync (default null) for Kuikly syncCall bridge methods. - :shared compat (additive, shared files): RaftPagerHost forwards host events to its page as pager events; Module.toNative(syncCall = true) answers from HostBridgeTransport.callSync when the host serves the method (falls back to the previous async + empty value otherwise). RaftPageRegistry.install doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): Raft Coil image loader on NetworkKMM (CMP switch, group 7) Coil 3.2 had no network fetcher, so every remote image (avatars, attachment thumbnails, rememberAsyncImageBitmap / imageResource) stayed blank after the Kuikly removal. Add the Raft singleton ImageLoader: - RaftImageSource mapper/keyer/fetchers: http(s) over SharedBinaryDownloader (single flight per content key, Coil disk cache 128MB + memory cache), assets:// bundled assets (Android APK assets; iOS main bundle, MR bundle, asset catalog, ported from KuiklyRenderComponentExpandHandler), inline <svg>, and slock-avatar-render-v1 managed avatar models read through a host hook (RaftImageHost.managedImageReader, registerDecoded + lease close via RaftManagedImageCommitInterceptor). - task #403 keys: url = slockAvatarContentIdentity fingerprint, managed = nativeBytesKey; commit generation validated (newer commit evicts the decoded entry) instead of being part of the key. - coil-svg 3.2.0 for SVG decoding; androidx.webkit added to androidMain for the upcoming sandbox WebView (build.gradle.kts). Shared compat (additive): rememberAsyncImageBitmap loads via the Raft loader at original size; RaftPagerHost / RaftPageRegistry.install() call raftInstallImageLoader(). TODO(cmp-switch): hosts must install RaftImageHost.managedImageReader; the #403 contentKey wire field (url<->managed sharing) is not emitted yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): real sandbox WebView via AndroidView / UIKitView (CMP switch, group 7) SlockSandboxWebView was a placeholder box. It now renders through the new expect RaftSandboxWebViewHost with the same parameters, controller commands and events, so SettingsStandaloneWebViewHost, InAppBrowserPage, release notes, server setup and the HTML file preview work unchanged. - Android: RaftAndroidSandboxWebView ports androidApp SlockSandboxWebViewRenderView (settings, navigation fence, document-start auth script + bootstrap fallback, raftHostPort WebMessageListener bridge, runtime JS gating, desktop initial scale, renderer crash recovery, file chooser via RaftWebViewFileChooserHost, host main-document hook). - iOS: RaftIosSandboxWebView ports iosApp SlockSandboxWebView.m (WKWebView rebuild on configuration attrs, ephemeral data store, user scripts, host bridge with origin/document checks, KVO navigation state, pageZoom desktop fit, content-process recovery, loadCommit). - Host message kind mapping moved to raftDecodeWebViewHostMessage (common). TODO(cmp-switch): Android host must implement RaftWebViewFileChooserHost and set RaftAndroidWebViewHooks.mainDocumentProvider (release notes cache). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): iOS sandbox WebView polls navigation state instead of KVO Kotlin/Native 2.1 exposes observeValueForKeyPath only as an NSObject category extension, so it cannot be overridden (cross-compiled check: "Unresolved reference NSKeyValueObservingProtocol"). Replace the KVO of the .m port with a 250ms NSTimer poll while a WebView is attached; navigation state callbacks stay de-duplicated. TODO(cmp-switch). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): avoid ComposeContainer setContent JVM signature clash Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): iOS compile fixes for AVAudioSession import and SystemFont opt-in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci: point Android source-contract checks at SlockCmpActivity.kt The Android page host source moved from SlockKuiklyActivity.kt to SlockCmpActivity.kt (the bridge module moved with it). Path-only update in scripts/ci and the workflow path filters; the checked contracts are unchanged (thread image compression and attachment save mapping contracts pass). Outside group 6 ownership; mechanical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(android): install a test Main dispatcher for AndroidAppScopeRealtimeOwnerTest The CMP switch brings kotlinx-coroutines 1.10.2; AndroidAppScopeRealtimeOwner's scope uses Dispatchers.Main.immediate, which has no platform dispatcher in a JVM unit test, so its initializer failed (8 tests). Set/reset an UnconfinedTestDispatcher around each test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(android): host every page on CMP; delete the Kuikly host Every page (RaftProductPages.g.kt and, in fixture builds, RaftVisualFixturePages.g.kt) now renders through SlockCmpRoot on Android. Host - SlockCmpActivity is the page host. It is the former SlockKuiklyActivity with the Kuikly render view delegator replaced by a ComposeView running SlockCmpRoot inside the existing container layout. Everything else is kept: edge-to-edge and the pageData window-insets contract, the slockWindowInsetsChanged / slockAdaptivePaneChanged events (now sent as RaftHostEvents pager events), login transition routing, shell window registration, realtime visibility, attachment pickers / camera / save-as launchers and their saved state, screenshot feedback, the host error surface (now also for unknown pages), the display mode request and the diagnostics deep link. - Back: the host fallback callback is registered before the page composes, so Compose BackHandlers run first. The Kuikly onBackPressed and KEYCODE_BACK interception is removed. - Navigation: SlockCmpNavigator.openPage/closePage take the old Kuikly router adapter's logic (login replacement, post-auth hand-off, app shell launch, slockReplaceCurrentPage). - Activity classes: SlockAppShellActivity / SlockDocumentShellActivity extend SlockCmpActivity (keeping their task and document launch modes). SlockKuiklyActivity is now a thin SlockCmpActivity subclass, kept in the manifest for the diagnostics alias and for Intents from older versions. slockKuiklyHostActivityClass routes every non-shell page to SlockCmpActivity. The rollback redirect is gone. The page hosts use adjustResize so Compose gets IME insets on every API level. - Startup: SlockApplication installs RaftPageRegistry (all registered pages) and RaftAndroidHostHooks: managed avatar reads for RaftImageHost (beginRender / registerDecoded / lease close) and release-notes main documents for RaftAndroidWebViewHooks. SlockCmpActivity implements RaftWebViewFileChooserHost. - Cold-start splash: released on the page's first frame. TODO(cmp-switch): this used to wait for the home child page load. Native capabilities - RaftHostActivity: the interface the bridge needs from its host (lifecycle owner, content container, close, attachment upload / save-as pickers, native Google auth, performance scene, realtime connect / reconcile / disconnect / cursor). SlockKuiklyBridgeModule casts to it instead of SlockKuiklyActivity, so every host-bound method works from CMP pages. - SlockKuiklyBridgeModule no longer extends KuiklyRenderBaseModule; callbacks are RaftBridgeCallback. KuiklyBridgeModuleTransport serves HostBridgeTransport.callSync for urlEncode / urlDecode / currentTimestamp on the calling thread. - Share image chrome fonts now come from :shared res/font through ResourcesCompat (the TTFs moved there in Phase A). Deleted (Kuikly-only) - Render views and adapters: SlockKuiklyPageRenderView, SlockKuiklyTextFieldViews, SlockLucideIconRenderView, SlockPixelAvatarRenderView, SlockNativeTiltedBadgeChromeRenderView, SlockSandboxWebViewRenderView, SlockKRVideoViewAdapter, SlockInProcessNotifyModule, SlockKuiklyPerformanceDiagnostics, and the Kuikly image / font / log / exception / router adapters. SlockKuiklyThreadAdapter remains as a plain executor. - Their unit tests: SlockKuiklyFontAdapterTest, SlockKuiklyImageAdapterAvatarCacheTest, SlockKuiklyPageRenderViewTest, SlockKuiklyTextFieldViewsTest, SlockSandboxWebViewRenderViewTest. SlockCmpHostRoutingTest and HomeColdStartSplashGateTest are updated. - Gradle: the kuikly-open compose modules are dropped from the OSS license requirement list, and the Kuikly runtime pin comments are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(shared): drop SlockMermaidNativeScrollOwnershipTest It only exercised Kuikly core-render-android scroll arbitration (KRRecyclerView / NestedHorizontalChildInterceptor), which no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(markdown): port inline chip/underline assertions to RaftInlineBoxStyle annotations SpanStyle.inlineBoxStyle and textDecorationColor/Thickness/Offset are gone on upstream Compose; chips and reference underlines now travel as RaftInlineBoxStyle / RaftInlineUnderlineStyle annotations. Assertions read raftInlineBoxRanges() and the slockMarkdownRichTextTagInline*Style lowering instead. The image-state no-op test counts snapshot writes (Size is a value class, assertSame is meaningless). markdownLinksUseTwoSpUnderlineThicknessAndOffset is @Ignore'd with TODO(cmp-switch): markdown links lost the 2sp underline thickness/offset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(type): read bundled font names through a per-platform test hook KuiklyFont is now a compat function returning ResourceFont (Android) / SystemFont (iOS), so the face name is recovered via raftTestBundledFontKey (R.font field name / SystemFont identity) and compared as the normalized resource key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(file-preview): port capture-slot test to RaftViewCaptureRef Moved to androidUnitTest: RaftViewCaptureRef wraps a GraphicsLayer and has an internal constructor, so the slot's identity contract uses opaque instances. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(thread): assert upstream pointer-input reset condition across recomposition Upstream SuspendPointerInputElement.equals compares the handler instance; the node only restarts the gesture when a key or the handler class changes. Assert that condition instead of Kuikly's element equality. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(thread): wait for the applied secondary refresh result The immediate secondary refresh can log result=pending_first_content before FirstContentReady; the wait loop matched it and asserted before the stashed apply ran. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(shared): render Lucide icons with Compose ImageVectors on CMP Replace the temporary SlockLucideIcon stub with real Lucide rendering: - scripts/generate_lucide_icons.py: new `compose` target emits a compact commonMain path table (RaftLucideIconPaths.kt, generated, ~100KB vs 430KB before) with only non-default per-path attributes (fills, stroke widths, opacities, caps/joins, fill rules) plus the filled-bookmark variant. Removed the dead `android` (s2c) and `android-path` targets; added `--check` for the compose/shared Kotlin outputs. - RaftLucideImageVectors: lazily builds and caches ImageVectors (24x24 viewBox, stroke width 2 in viewBox units, round caps/joins, no fill), tinted via ColorFilter.tint; direct-colour paints are baked in. - SlockLucideIcon: public API unchanged. strokeWidth overrides every stroked path, non-blank fill selects the filled variant and tints with the fill colour, unknown names keep the layout slot, spin uses linear easing like the old native renderer. - Remove the dead native renderers: Android SlockLucidePathIcons.kt and its connected test, iOS SlockLucideIconView.{h,m} (+ pbxproj entries), stale baseline-profile lines, and scripts/test_android_lucide_native_renderer.py (its checks were folded into test_generate_lucide_icons.py). - Add RaftLucideImageVectorsTest (every SlockLucideIconName resolves to a non-empty vector, stroke model, overrides, fills, caching). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(tokens): generate compose/shared tokens against upstream Compose Color Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(ui-cmp,shared): iOS page factory with host events, sync bridge calls and host hooks - SlockCmpIos.makePage (replaces makeViewController) returns a RaftIosPage: the Compose view controller plus sendEvent(event, dataJson), which delivers host -> page events through the page's RaftHostEvents on the main thread (the Kuikly sendWithEvent counterpart). An optional RaftIosPageListener gets the resolved dark theme (SlockCmpRoot onDarkThemeResolved) for the status bar. - SlockCmpIosBridge gains callSync(method, paramsJson); the iOS transport serves HostBridgeTransport.callSync from it (Kuikly syncCall methods). - :shared iosMain RaftIosHostHooks.install(managedImages): installs RaftPageRegistry (every page on CMP, Raft Coil loader) and RaftImageHost.managedImageReader over the app's durable avatar store (RaftIosManagedImageStore, implemented in Swift). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * feat(ios): host every page on CMP; delete the Kuikly host Every page now renders through Compose Multiplatform on iOS. Host - RaftPageViewController (the former KuiklyRenderViewController) is the page host: SlockPageControllerFactory creates it for every page name and embeds the CMP page (SlockCmpIos.makePage) as a child view controller. It keeps the host duties: pageData host context (time, pixel safe areas, now also a per-scene slockWindowId), adaptive pane facts (slockAdaptivePaneChanged), live window insets (slockWindowInsetsChanged, new on iOS, the Android contract), themeDidChanged on userInterfaceStyle changes, the status bar style from the page's resolved theme, the dormant adaptive navigation transaction and the conversation forward gesture bit. Page visibility comes from the embedded Compose controller's lifecycle (RaftPagerHost sends viewDidAppear / viewDidDisappear). Full-screen swipe back and hidden nav bar are set on the container (fd_* flags). - RaftPageRouter (the former KRRouterHandler: identity-only pageData, slockReplaceCurrentPage, login replacement dedupe) serves the CMP pages' RouterModule navigation through SlockCmpPageNavigator. - Startup (iOSApp.init): RaftIosHostHooks.install(managedImages:) installs RaftPageRegistry and the managed avatar reader; SlockIosAvatarCacheHost serves exact committed avatar bytes (RaftIosManagedImageStore). - Root SwiftUI page: KuiklyRenderViewPage -> RaftPageView. Bridge - HRBridgeModule no longer extends KRBaseModule: it is one NSObject per page with a weak slockHostController (the RaftPageViewController) replacing hr_rootView / kr_viewController for presentation, navigation, share-image capture, adaptive navigation and the conversation forward gesture. KR_PARAM_KEY / KR_CALLBACK_KEY / KuiklyRenderCallback become RaftBridgeParamKey / RaftBridgeCallbackKey / RaftBridgeCallback. - SlockCmpBridgeHost owns the page's module and implements callSync for the synchronous methods (urlEncode, urlDecode - new, form encoding like the Android host -, currentTimestamp, dateFormatter) on the calling thread. - Adaptive navigation: TODO(cmp-switch) the draft editor snapshot was the Kuikly KRTextAreaView; CMP text fields are not UITextViews, so prepare rejects with editor_drift (shared keeps the compact route). Deleted (Kuikly-only) - KuiklyExpand/: KuiklyRenderViewController, KRRouterHandler, KuiklyRenderComponentExpandHandler (image loading is the Kotlin Raft Coil loader), SlockKRVideoViewHandler (RaftVideoPlayer), SlockSandboxWebView + NavigationCommit (RaftIosSandboxWebView), SlockKuiklyPageView, SlockPixelAvatarView, SlockNativeFloatingTabBarView + SlockStackedItemTabBar (the shared Compose dock renders the iOS 26 tab bar, TODO in :shared). Surviving files moved to Host/ (HRBridgeModule, adaptive navigation session, editor snapshot, diagnostics headers) and Cmp/. - Unit tests of deleted code: SlockImageLoaderTests, SlockMediaAudioSessionTests, SlockSandboxWebViewTests, SlockStackedItemTabBarTests. SlockRouterLoginTransitionTests now drive RaftPageRouter / RaftPageViewController. - Podfile / Podfile.lock: the OpenKuiklyIOSRender pod and its release artifact fetch. pbxproj: every reference of the deleted files. - Fonts: unchanged, the TTFs stay registered through UIAppFonts (KuiklyFont compat SystemFont(name)); designsystem fonts ship as compose-resources. Scripts outside compose/iosApp (path updates only): scripts/ci check_thread_image_compression_contract.py, test_attachment_save_mapping_contract.py (HRBridgeModule.m path), test_kuikly_raft_consumer_contract.py (iOS must not consume a Kuikly pod any more). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ui-cmp): replay sticky iOS host events after the page's first composition The iOS page container publishes adaptive pane and window inset snapshots from layout / viewDidAppear, which can run before RaftPagerHost subscribed to the page's RaftHostEvents. RaftIosPage keeps the latest slockAdaptivePaneChanged and slockWindowInsetsChanged payloads and re-sends them once the first composition is applied (replace-semantics snapshots, duplicates are harmless). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci(mermaid): retarget plane confinement to the single commonMain adapter The CMP switch replaced the per-platform SlockMermaidDiagramBlock actuals with one commonMain implementation (SlockMermaidDiagramBlock + RaftMermaidCanvas). The gate now requires those adapter files, forbids per-platform actuals from returning, and keeps build.raft.mermaid imports out of every other production source set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci(visual-pages): also guard the generated CMP page registries The Kuikly KSP page registry is gone; pages now resolve through the generated RaftProductPages.g.kt / RaftVisualFixturePages.g.kt maps. The no-production-visual-pages gate now also fails when the product registry registers a *_visual page, when the visualFixturesOff stand-in is not an emptyMap(), or when either generated registry is stale. iosMain joins the scanned production source sets, and the lint workflow triggers on the newly scanned paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci(kuikly): retire the Android Kuikly release admission The Android host and :shared no longer consume Kuikly UI, so: - test.yml drops the kuikly-artifacts job; android/ios need only plan. The iOS job admits the immutable release itself while the iOS Podfile still pods OpenKuiklyIOSRender (TODO(cmp-switch)). - Android-only workflows and the Android release preflight stop verifying the Kuikly manifest. - The tiering contract, aggregate verifier and git-diff path-state contract follow the new job graph. - Version catalog loses the kuikly-open entries, buildSrc loses the unused KotlinBuildVar Kuikly pin, and settings stops admitting kuikly-open groups (the repo block, renamed raftArtifactsKbaMetadata, still serves the KBA stdlib/atomicfu metadata resource-core needs). - The consumer contract now asserts Kuikly stays out of Gradle and the Android lanes and stays immutable for the remaining iOS consumer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci(kuikly): delete the Kuikly release manifest machinery With the iOS host on CMP (merged c91c25746) nothing consumes Kuikly UI any more, so the immutable-release admission has no subject: - delete kuikly_release_manifest_gate.py, verify_kuikly_release_manifest.sh, fetch_kuikly_release_artifact.py and compose/gradle/kuikly-release-closure.json; - drop the manifest-gate self-test from the Test plan job and the admission step from every iOS lane (test.yml ios, ios-release, ios-simulator-app, qa-acceptance-ios, visual-testing); - test_kuikly_raft_consumer_contract.py becomes the no-Kuikly-UI contract: it fails if a kuikly-open coordinate/plugin, the Kuikly pod, or the retired release plumbing returns (KuiklyBase network/resource stay); - AGENTS.md release section, the KB recipe (now stale) and the agent landing table follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(thread): restore system text selection menu in Select Text dialog on CMP (task #924) The CMP switch rendered ThreadMessageTextSelectionDialog with Compose SelectionContainer { Text }, which drops platform text actions (Android PROCESS_TEXT such as Translate, iOS Look Up/Translate/Share). Add commonMain `expect RaftNativeSelectableText(text, style, modifier)`: - Android: AndroidView(TextView) with setTextIsSelectable(true), TextStyle applied (size, line height, color, typeface resolved through LocalFontFamilyResolver from the :shared res/font faces), wrap-content height. Native id R.id.thread_message_selection_text (new ids.xml) plus testTagsAsResourceId so the testTag is exposed as resource-id. - iOS: UIKitView(UITextView) non-editable, selectable, non-scrolling, zero insets, font from the registered app font name (SystemFont identity), height from sizeThatFits for the offered width. check_source_constraints.py: task #924 rule now requires RaftNativeSelectableText( (docstring Kuikly bridge -> CMP platform text view); SelectionContainer stays forbidden; self-test fixtures updated (also rejects bare SelectableText( and an extra SelectionContainer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shell): hide inactive keep-alive child pages again Kuikly hid an inactive SlockKuiklyChildPage natively (visibility=INVISIBLE). The CMP port composes the child inline and only forwarded `active` to the pager appear/disappear events, so every retained app-shell tab was drawn and hit-tested on top of the active one. On device: after visiting Settings the Home tab kept showing Settings and swallowed its touches (chat list unreachable), and Tasks' empty state bled through the Members detail pane. Measure the child but skip placement while inactive: unplaced content is not drawn and gets no pointer input, while its composition/state stays alive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci: retarget feedback host script test to the CMP WebView hosts; key build cache on vendored markdown - scripts/tests/feedback-host-scripts.test.mjs read the deleted Kuikly SlockSandboxWebViewRenderView.kt / KuiklyExpand SlockSandboxWebView.m; it now extracts the bridge scripts and fence assertions from RaftSandboxWebView.{android,ios}.kt (all 6 tests pass). - test.yml build-output cache key now includes third_party/kuikly-compose- markdown and third_party/markdown-parser, which are :shared srcDirs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * ci(source-constraints): retarget task #127 Send snapshot rule to the CMP TextField Kuikly's CoreTextField/AutoHeightTextAreaView traversal is gone; on CMP the Send snapshot reader is bound to the Compose TextFieldValue. The rule now pins that the reader reads the live value through rememberUpdatedState (never the composition-time capture) and is unbound on dispose; the composer-side bounded timeout/opaque-token fail-safe stays pinned as before. Task #991 keeps applying on CMP (docstring notes why). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(androidTest): port connected tests off the deleted Kuikly host - Delete SlockKuiklyInactiveHostRenderingTest (covered only the Kuikly SlockKuiklyPageRenderView active prop) and drop it from the first-character emulator suite, its self-test and the test.yml artifacts. - SlockMultilineTextFieldValueConnectedTest: remove the cases that drove Kuikly native editor views (SlockKuikly{TextField,TextArea}View, KRTextProps ordering, getTextInputState renderer method, Android 14 ArrowKeyMovementMethod anchor), incl. the nested SlockComposerFinishComposingOnStateReadConnectedTest; port the attachment-comment CJK composition / mid-text edit case to the Compose editor via semantics + the focused view's InputConnection. - SlockComposerFirstCharacterBaselineTest: assert first-character line height/baseline stability on the Compose TextLayoutResult instead of Kuikly's HRLineHeightSpan. - AndroidLoginTransitionRoutingConnectedTest: drive the production SlockCmpActivity.openPage router instead of the Kuikly router adapter. - Launch SlockCmpActivity (not the legacy SlockKuiklyActivity alias) and fix the notification page-data companion call. :androidApp:assembleConnectedTestAndroidTest -PslockVisualFixtures=true compiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * style: drop trailing blank lines flagged by git diff --check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(android): stop the default focus highlight tinting the whole window After a Dialog closed (or a page was popped) following key input, the whole window - status bar area included - stayed covered by a light gray layer until the activity was recreated; taps did not clear it. Root cause: the activity root FrameLayout (slock_kuikly_root) is focusable/focusableInTouchMode, kept from fcba62879 as a focus sink so clearFocus() does not bounce focus into the first text field. When a Dialog window closes, focus returns to that root. If the window is out of touch mode at that moment (hardware/IME key events, `adb input text`), Android draws its default focus highlight - a translucent gray drawable over the full bounds of a focused view without a focus-state background. Re-entering touch mode does not refresh the drawable state of a view that stays focused, so the highlight stuck. Under Kuikly the native render views kept focus; with CMP the single ComposeView hands it back to the root. Keep the root as a focus sink but disable its default focus highlight (android:defaultFocusHighlightEnabled, minSdk 29). Verified on the emulator: dialog close and non-touch-mode with the root focused no longer tint the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(data): hop UI-originated store writes onto the store dispatcher Debug logcat repeatedly reported `store_write_off_dispatcher store=SharedHomeStore thread=main` (and the same for SharedMessageBodyFontSizeStore). Stack traces showed three entry points writing store state on the caller's thread: - SharedHomeStore.hydrateCachedHome, from the app shell LaunchedEffect; - SharedMessageBodyFontSizeStore.ensureLoaded/acceptAuthoritative, from the conversation page LaunchedEffect / settings; - the process ReadReceiptCoordinator's onPrediction/onFailure fan-out, which calls SharedHomeStore/SharedActivityStore.dispatch(ApplyRead- Receipt{Prediction,Failure}) synchronously from the thread store's page scope. Decision: the confinement design (SlockStoreThreading, 507bd903e / c97807e70) makes the provider-owned stores' state single-writer on the serial store dispatcher and has dispatch() hop intents onto it; the UI thread is just another caller. These paths were already off-dispatcher under Kuikly (the writes ran on Kuikly's context thread, the "known gaps" the handler logs instead of throwing); CMP only made them show up as thread=main. Moving the confinement to Main would re-serialize store work (network results, realtime patches, SQLite-adjacent reductions) behind composition and undo the dispatcher design, so the fix is to make the remaining entry points hop like dispatch() does, not to silence or re-home the check: - raftWithStoreScope(): suspend entry points run their body on the store scope's own dispatcher (the store dispatcher in production, the test dispatcher in unit tests) and still suspend the caller until it finishes, so callers keep their semantics. Used by hydrateCachedHome (whose diagnostics context vars were also unsynchronized) and the font-size store's ensureLoaded/acceptAuthoritative. - The provider launches the read-receipt fan-out on each store's scope. The serial dispatcher runs tasks FIFO, so a prediction still lands before its failure; the stores' own lock-based ordering is unchanged. - SharedMessageTranslationStore's public mutators (request*/retry/ translateNow/setShowOriginal/reset) were called from the UI the same way and also mutated the unsynchronized in-flight batch set; they now launch on the store scope. Not changed (not observed, noted for follow-up): the feature-flagged ReadStateV1 canary projection (applyReadStateV1Projection) and the home/activity reset() paths still apply synchronously on their caller. Verified on the emulator with the debug handler on: cold start, opening a channel (font size load + read prediction) and navigating log no violations any more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(compat): report Android keyboard height without the navigation bar The composer sat ~44px (navigation bar + 12dp base) above the keyboard. Root cause: the compat keyboardHeightChange reported the raw WindowInsets.ime bottom, which on Android already includes the navigation bar. Kuikly's Android11PlusKeyboardWatcher reported `ime - navigationBars` (verified in the core-render-android classes), and the Android host advertises that contract with slockKeyboardInsetExcludesBottomSafeArea=true, so pages add their bottom safe area back while the keyboard is open - the navigation bar was counted twice. The window itself does not resize (edge-to-edge, setDecorFitsSystemWindows(false)), so keyboardHeightChange is the only IME source; it now keeps Kuikly's per-platform meaning again: Android subtracts WindowInsets.navigationBars, iOS keeps the full keyboard frame (Kuikly iOS semantics, host does not set the flag). Shared compat layer: keyboardHeightChange body changed (bug fix to match the Kuikly contract it emulates) plus a new internal expect/actual raftKeyboardHeightExcludesNavigationBars. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(modal): render Modal dialogs edge to edge without a window dim Every Modal (CMP Dialog) was dimmed twice and bottom sheets such as the self-hosted server sheet left a gray strip over the navigation bar. Root cause: Kuikly's Modal was a full-screen in-window overlay with a transparent scrim; Slock draws its own SlockModalDismissBarrier and pads for safe areas/keyboard itself. The compat Modal now opens a platform Dialog window whose defaults differ: Android dims the window behind (theme backgroundDimEnabled), fits it between the system bars and paints the dialog window's own navigation-bar background; iOS applies the default scrim and platform/keyboard insets. This was the open TODO(cmp-switch) in slockModalDialogProperties. - raftModalDialogProperties (expect/actual): Android decorFitsSystemWindows=false; iOS usePlatformInsets=false, useSoftwareKeyboardInset=false, scrimColor=Transparent (named args checked against the skiko DialogProperties of CMP 1.8.2; iOS not compiled on this box). - RaftModalDialogWindowEffect (Android): clears FLAG_DIM_BEHIND, makes the dialog's status/navigation bar backgrounds transparent and copies the host's light bar appearance. BasicWidget (shared): Modal calls the new effect and slockModalDialogProperties delegates to the new function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(composer): finish the IME composition at the Send snapshot Send could carry an open IME composing region (mid-dictation, mid-CJK conversion) into the sent value and leave the IME composing into the field the message was sent from. Under Kuikly the Android editor's getTextInputState ran finishComposingText() (fallback: removeComposingSpans) before answering the Send read (7ae5b181a, connected test SlockComposerFinishComposingOnStateReadConnectedTest, removed with the Kuikly host): the composing characters were committed as-is and the IME was told the composition was over. On CMP the shared TextField's reader returns the live Compose TextFieldValue (task #127 wiring kept), whose text includes the composing characters but whose composition range was still open. The reader now applies raftWithFinishedComposition() to that state: text and selection unchanged (no characters dropped, so the sent text is exactly what is on screen), composition cleared. The composer writes the snapshot back via acceptEditorValue before sending; for Compose that is a composition-only change, which commits the buffer's composition and restarts input - the CMP equivalent of finishComposingText - so the IME cannot keep composing into the sent text. Shared BasicWidget: additive helper + the reader's bind lambda; check_source_constraints.py rule #127 still holds. Unit tests cover the snapshot (text/selection kept, composition cleared) and the committed no-op case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): opt in to SystemFont in the iOS font-name test hook Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(thread): newer-pagination test hosts SlockCmpActivity after merging main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): opt in to experimental iOS DialogProperties for the modal window Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ios): match the Kotlin-exported bridge selector callMethod:paramsJson:callback_: The iOS app crashed at launch with an unrecognized selector because Kotlin/Native exports SlockCmpIosBridge.call with a callback_ label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(android): report CMP child page loads to the host render readiness SlockCmpActivity.renderReadinessSnapshot() lost its child page signal when the Kuikly host was deleted: onSlockKuiklyChildPageLoadComplete came from the native child render view, and CMP composes child pages inline, so childPageLoad stayed null and the child-page visibility probe never became ready. SlockKuiklyChildPage now reports its load result (the same one it hands to onLoadSuccess/onLoadFailure) through a new optional SlockCmpHost.childPageLoads listener (RaftChildPageLoadListener, additive with a null default in :ui-cmp), and SlockCmpActivity records it in renderReadinessChildLoads again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(android): keep the real IME out of the CJK composition test The CMP editor requests the soft keyboard on focus, so Gboard started an input view on the attachment-comment editor while the test drove the same editor through its own InputConnection. The visible IME reacts to selection/composition updates it did not make (finishing the "n"/"ni" composition locally, re-composing "review" at the cursor on CI), which produced "nni", "ni你好\nreview" or "你好\nX" instead of the expected values. Add hideSoftKeyboardAndAwaitHidden() to the visual input host support: it hides the IME through the same WindowInsetsController (cancelling a pending show) and waits until the IME reports hidden, leaving focus and the input session intact. The exact-value assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(shared): port the file-preview text selection dialog (#3193) to RaftNativeSelectableText Merged main brought SlockTextSelectionDialog, written against Kuikly's SelectableText; render it through the CMP platform text view instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(thread): find Thread rows in the Compose accessibility tree AccessibilityNodeInfo.findAccessibilityNodeInfosByText() delegates virtual-node lookups to the host view's AccessibilityNodeProvider, and Compose's provider does not implement that search, so inside the CMP ComposeView it returned nothing although the rows were in the tree. The newer-pagination gate therefore never saw AUTO-MSG-0008 and timed out on "initial 16-row focused Thread history". Add raftFindNodesByText(), a tree walk with the framework's matching rule (case-insensitive contains on text or content description), and use it for the message markers, the Retry row and the Back-to-bottom pill. The compatibility-dialog lookups keep the framework search: those are View-based system windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(android): drop the startup provider from the connected-test APK androidx.tracing.perfetto merges androidx.startup's InitializationProvider into the androidTest manifest, but the class ships only in the tested app APK. ActivityScenario.close() starts InstrumentationActivityInvoker's EmptyActivity in the test package's own process, which then crashed with ClassNotFoundException; after a few runs the "keeps stopping" dialog took window focus from the next connected test. Remove the provider from the test APK manifest only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * test(android): drive the CMP composer editors through Compose semantics composer-session-retirement and thread-composer-suggestion-interaction still searched the window for an Android EditText, which the CMP host no longer creates, and timed out before doing anything. RaftComposeTextFieldTestSupport reads text fields (SetText semantics nodes) from the activity's own SemanticsOwner on the main thread: text, selection, focus, window bounds, and the semantics node id as the editor identity (it changes only when the field is re-created, like the old EditText instance). It deliberately avoids a Compose test rule, whose test-driven recomposer only advances when the test synchronizes with it and froze the suggestion test's pixel oracle (the image-action marker never recomposed). raftAwaitTextInputSession() waits for the focused view to report a text editor before the test opens an InputConnection, since Compose starts the input session asynchronously after focus. SlockThreadImePaintConnectedTest (session retirement): same product contract - same editor kept, cleared after Send, focused with the IME up, exactly one request, first composition accepted at [4, 4). Compose has no composing-region semantics, so the probe updates the open composition ("sent" -> "sends") and requires the replacement, which only happens if the region survived the composer's controlled-value round trip. A visible Gboard finishes composing regions it did not create, so the keyboard is parked for that probe and shown again afterwards, and the retained editor must take it back. Message markers use the Compose-aware accessibility search. ThreadComposerSuggestionPopoverConnectedTest: focus and caret through semantics, keep the suppressed-keyboard host contract by hiding the IME that the focused/tapped Compose field requests, and type through the focused view's InputConnection. Assertions are unchanged. SlockMultilineTextFieldValueConnectedTest also waits for the input session before typing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * chore(agents): remove the vendored Kuikly skills, rules and KuiklyUI reference Mobile is moving to Compose Multiplatform for 1.15 (artin, 2026-10-19). The Kuikly AI skills, DSL rules, sync script and the KuiklyUI submodule steer agents toward Kuikly; drop them and point AGENTS.md at the migration instead. Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> (cherry picked from commit 9c002131d661aa3316a02cb5eecbdcd585de7b80) * ci: no-Kuikly-UI contract tolerates a repo with no .gitmodules Removing the KuiklyUI reference submodule deleted the last .gitmodules entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ios): pin the CMP bridge call selector and test the host conforms Kotlin/Native exported SlockCmpIosBridge.call as callMethod:paramsJson:callback_: (mangled to avoid HostBridgeTransport.call's selector), and the ObjC host had to match the mangled name. Rename the parameter to resultSink and pin it with @ObjCName so the selector is callMethod:paramsJson:resultSink:, update SlockCmpBridgeHost, and make a missing protocol method a compile error there. Add RaftCmpBridgeSelectorTests (iosAppTests): SlockCmpBridgeHost must answer every required selector of SharedSlockCmpIosBridge, and the call selectors are pinned. Registered in project.pbxproj and run by the CI login-router xcodebuild step (-only-testing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ios): CMP pages pop from the leading edge only RaftPageViewController (and the FD push swizzle for it) enabled full-screen interactive pop. FDFullscreenPopGesture only yields to UIScrollView, which a CMP page does not contain, so a right drag on a Compose horizontal scroller in a pushed page popped the page. Use edge-strip admission (40pt default) instead; the right-edge conversation forward gesture and app-shell handling are independent of this flag. TODO(cmp-switch): restore full-screen pop once shared code reports horizontal-scroll consumption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(android): never leak the managed-avatar render lease on cancellation withContext resumes its caller with a prompt cancellation guarantee: when an image request was cancelled while beginRender ran on IO, the returned read was discarded and its close() never ran, so the avatar cache's retire/clearAll waited forever on renderDrained at logout/server switch. raftOpenRenderLease runs the open NonCancellable, hands the lease out via a side slot and closes it when the caller is cancelled before hand-over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(composer): Send reads the live field value, not the last composition The Send snapshot reader returned rememberUpdatedState(value), i.e. the value of the last recomposition, so an IME commit and Send within one frame lost the last edit. TextField now records the most recent value it has seen (RaftLiveTextFieldValue): the onValueChange wrapper records the edit before calling the caller, every committed composition records the caller's value. The reader still finishes the composition. Shared BasicWidget change (TextField internals + new internal class). check_source_constraints rule #127 anchors moved to the live-value wiring; intent unchanged (Send reads the live value, reader unbinds with field). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Lagoon <raft+lagoon@agents.stamp.build> * fix(ios): record Kotlin unhandled exceptions in diagnostics again SlockIosDiagnosticsRecordUnhandledException lost its only caller with the Kuikly render controller's onUnhandledException. RaftIosHostHooks.install now takes a RaftIosUnhandledExceptionSink and installs a Kotlin/Native unhandled exception hook that forwards reason/stack t…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Markdown / plain-text file previews could not be long-pressed to copy (artin report, wanted in 1.14.2). Rendered Markdown is not a selectable text view, so long-press did nothing.
SlockTextSelectionDialog, same shape as the message "Select Text" modal (nativeSelectableText, no app-owned copy controls). ThreadPage is untouched: its dialog stays inline becausecheck_source_constraints.py(task Persist diagnostics by cold-launch session #924) audits it there.markdown/textkinds with non-blank content.Not covered
Selection happens on the source text, not on the rendered Markdown in place. Device check needed on Android / iOS / OHOS: long-press body → dialog opens, drag handles + copy; menu copy → toast + clipboard; long-press does not break scrolling.
Will be cherry-picked to release/1.14.2 (SlockFilePreview.kt is identical on both branches).
🤖 Generated with Claude Code