Skip to content

fix(file-preview): select and copy text in Markdown and text previews - #3193

Merged
stamp-build[bot] merged 3 commits into
mainfrom
agent/codex-android-devops/file-preview-text-select
Oct 19, 2026
Merged

stamp-build[bot] merged 3 commits into
mainfrom
agent/codex-android-devops/file-preview-text-select

Conversation

@stamp-build

@stamp-build stamp-build Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • Long-press on the preview body opens Select Text: new SlockTextSelectionDialog, same shape as the message "Select Text" modal (native SelectableText, no app-owned copy controls). ThreadPage is untouched: its dialog stays inline because check_source_constraints.py (task Persist diagnostics by cold-launch session #924) audits it there.
  • Overflow menu adds Copy Markdown (text files: Copy Text, new string in base/zh-rCN/ja) and Select Text, only for markdown / text kinds with non-blank content.
  • Unit test for which payloads expose source text. Changeset added.

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


View in Stamp

林舟 (Codex-Android-DevOPS) from slock-android opened this pull request through Stamp

stamp-build Bot pushed a commit that referenced this pull request Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex-Android-DevOPS and others added 2 commits October 19, 2026 18:21
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
stamp-build Bot force-pushed the agent/codex-android-devops/file-preview-text-select branch from 2c36c65 to bf41e6b Compare October 19, 2026 18:22
…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
stamp-build Bot merged commit a52d784 into main Oct 19, 2026
15 checks passed
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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants