feat(mobile): complete source-session clone and Android sheet fixes - #5440
Conversation
…agent create Add an optional cloneFromKiloSessionId to the create schema and change the RPC method to return a discriminated ready/in_progress/rejected result. Old callers can ignore the returned value.
Add cloneFromKiloSessionId to PrepareSessionInput, map it into the grouped SessionCreateRequest clone object, and persist it in grouped session metadata via registerSession. Old inputs without the field keep the empty-session bootstrap.
Add a bounded source export method and destination stage/inspect/finalize/reset methods to SessionIngestDO. Export reads only session, message, part, and session_diff rows in (ingested_at, id) order with a stable digest; reset wipes SQLite before the R2 delete await.
Add cloneFromKiloSessionId to basePrepareSessionNextSchema; both personal and organization prepare forward it unchanged to the worker.
Add a bounded resumable clone helper that copies source ingest rows to the destination DO with identity rewriting and destination-owned R2 bodies. Wire it into createSessionForCloudAgent with the typed result, detect destination claims before any write, and add matching-source delete for clone rollback.
…ership Forward cloneFromKiloSessionId into the session-ingest create call and map its typed result: rejected to BAD_REQUEST session_clone_failed, a missing acknowledgement to SERVICE_UNAVAILABLE session_clone_unavailable, and in_progress to CONFLICT creation_in_progress. Route retryable and unknown clone outcomes through the reconcile-pending ledger hook, persist sandbox allocation progress before the ingest call, include the clone source in the create-intent fingerprint and registration command, and make rollback allocation-aware with a matching-source delete.
Extract the post-allocation registration and admission effect into a helper, rebuild a clone allocation from ledger progress with the stored initial message id, resume a clone with its stored ids in reconcile step (b) when the ownership row is absent, and tombstone destination ids after an explicit rejection so the next intent never resumes them.
Prefer snapshot restore when session metadata carries a clone or a preparedAt, set requireSnapshot for clones, and reject a missing clone snapshot with a typed error instead of importing an empty session on both the worker and wrapper sides. Non-clone sessions keep the 404 fallback.
Replace the bounded text-seed continuation with a full Cloud Agent clone. The continue action sends cloneFromKiloSessionId with a fixed continuation prompt and a fresh initial message id, never drains history, never queries connected instances, and never writes a share payload. A missing Cloud Agent destination shows a terminal message. Delete the continuation seed and the remote continuation builders.
The back Pressable used h-13 w-13 (52 points). The shipped icon-button geometry is h-11 w-11 (44 points). The mounted test now derives the title font size from the rendered class and fails when title size plus top and bottom slop drops below 44 points.
Overview uses DetailScreenScrollView. Discussion and Files state chrome use useDetailScreenBottomPadding. Files floating actions and form-sheet footers add the system inset directly. The file navigator list carries the Android inset in its bottom content padding. Mounted tests cover zero and nonzero insets across the named states.
iOS keeps the native pageSheet Modal. Android gets a transparent Modal with a dimmed blocking scrim and a bottom-aligned half-height surface. Part detail, session context, and message details migrate to the wrapper.
File part preview, attachment text preview, and the child session sheet render through SessionPageSheet, keeping their loading, retry, terminal, and empty states.
Add a subagent-only bg-background layer that stays mounted while the sheet identity is held and visible is false, so the Android slide-out dismissal never reveals session content as a white frame. iOS keeps its native pageSheet dismissal. Extract the layer into a focused component and cover its lifetime with a mounted test.
Make PrepareSessionInput a discriminated union: the clone-only variant
requires the source session, autoInitiate true, and an operationKey, and
forbids prompt, initialPayload, and initialMessageId. prepareInputTo
SessionCreateRequest omits initialTurn for a clone, the fingerprint
hashes the omitted turn as { type: 'none' }, and clone-only creation
registers the session without admitting an initial message.
Make basePrepareSessionNextSchema and the web PrepareSessionInput type two-variant unions. The clone-only variant requires cloneFromKiloSessionId, autoInitiate true, and operationKey, and forbids prompt, initialPayload, and initialMessageId.
Map only an unmatched gitUrl to Connect repository and a matched repo with an unresolved model to Back to sessions. Set persistent retry guidance on repository fetch failure. Open GitHub integration setup instead of the expired repo-picker and clear terminal guidance after the Connect action runs. Cover the Connect action with a unit test.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (6 files)
Previous Review Summaries (5 snapshots, latest commit 3b3d771)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 3b3d771)Status: No Issues Found | Recommendation: Merge Files Reviewed (5 files)
Previous review (commit 62c6b6d)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 861925f)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (11 files)
Fix these issues in Kilo Cloud Previous review (commit 8bb7ab6)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (94 files)
Fix these issues in Kilo Cloud Previous review (commit e6c4e63)Status: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (94 files)
Reviewed by grok-4.6 · Input: 46.6K · Output: 4.4K · Cached: 271.5K Review guidance: REVIEW.md from base branch |
…tus bar Replace the half-height scrim sheet with an opaque full-window Modal. The surface pads the top safe-area inset, so the content clears the system status bar. iOS keeps the native pageSheet.
The next line already discards the Android detent on iOS, so the guard changed nothing.
The layer covered the session while the transparent Android Modal slid out. The Modal is now opaque and full height, so the cover is dead weight. Three recorded Back dismissals show no white frame without it.
|
|
||
| // Delete the captured R2 objects last, after the SQLite wipe is complete. | ||
| if (r2Keys.length > 0) { | ||
| await this.env.SESSION_INGEST_R2.delete(r2Keys); |
There was a problem hiding this comment.
bot: wipeStorage() (shared by clear() and resetCloneStage(), used to roll back a failed clone) calls this.env.SESSION_INGEST_R2.delete(r2Keys) with no try/catch, unlike the sibling cleanup path deleteDestinationR2Bodies in session-clone.ts which deliberately swallows R2 delete errors. If this R2 delete throws (transient R2 error), it propagates out of resetCloneStage()/clear() and turns what should be a clean typed rejected clone result (or a normal destination reset) into an unhandled exception at the RPC boundary, even though the SQLite wipe has already committed by that point.
Suggested fix: Wrap the R2 delete in wipeStorage() in a try/catch that logs and swallows the error (mirroring deleteDestinationR2Bodies), so a transient R2 failure during rollback/reset never surfaces as an uncaught exception; the SQLite state is already consistent at that point regardless.
| }); | ||
| } | ||
|
|
||
| // `Session already registered` means the registration already committed on a |
There was a problem hiding this comment.
bot: A 'Session already registered' rejection from the per-session DO is treated as idempotent success for both clone and non-clone create paths, based solely on the error string with no request-identity correlation (e.g. matching operationKey) confirming the already-registered state belongs to this caller's own retried attempt. This is scoped fairly tightly because the DO name is derived from allocation.cloudAgentSessionId, which is unique per create attempt, but the comment's justification ("the DO method is not idempotent, so the retry returns this rejection") is asserted rather than verified against an explicit request-identity check.
Suggested fix: Consider confirming (or adding a code comment cross-referencing) that cloudAgentSessionId allocation guarantees this DO name can only ever be hit by a retry of the same logical create request, so a future refactor doesn't accidentally let an unrelated conflict be swallowed as success.
Summary
The prepareSession contract gains a clone-only variant keyed on cloneFromKiloSessionId. A clone create requires autoInitiate true and a stable operationKey, forbids prompt, initialMessageId, and initialPayload, and registers no synthetic initial turn, so the copied transcript is the session state and the destination waits for the user's next prompt. Old callers that omit the field keep the empty-session bootstrap unchanged.
Files
apps/web/src/lib/cloud-agent-next/cloud-agent-client.ts— splits the prepare input type into non-clone and clone-only variants.apps/web/src/routers/cloud-agent-next-schemas.ts— adds the discriminated clone variant to the shared prepare schema.services/cloud-agent-next/src/router/schemas.ts— adds the clone-only prepare variant and includes it in the prepare input union.services/cloud-agent-next/src/session/session-requests.ts— makes the initial turn optional and adds the clone source field.services/cloud-agent-next/src/persistence/session-metadata.ts— adds the clone metadata schema holding the source session ID.services/cloud-agent-next/src/persistence/CloudAgentSession.ts— registers metadata with an optional message and a clone field, so a clone records no synthetic initial turn.services/cloud-agent-next/src/router/handlers/session-prepare.ts— maps a clone create to an undefined initial turn and threads the clone source into the session create request.services/cloud-agent-next/src/session-service.ts— treats a clone as requiring snapshot restore and forwards the git URL and clone source to session-ingest.services/cloud-agent-next/src/shared/wrapper-bootstrap.ts— adds the requireSnapshot workspace flag to the wrapper bootstrap contract.services/cloud-agent-next/wrapper/src/session-bootstrap.ts— fails a clone bootstrap when the required snapshot is missing instead of falling back to an empty import.The session-ingest service gains a bounded, resumable clone that copies a source session's ingest rows into a destination Durable Object before the PostgreSQL row is written. The copy rewrites session, message, and part identities, verifies a SHA-256 digest of the source against the staged rows, and returns typed rejection codes (source_access_denied, organization_mismatch, malformed_source_data, missing_source_body, source_digest_changed, destination_conflict, clone_setup_failed) that are a stable contract. A failed clone resets the destination stage and deletes its R2 bodies, leaving no PostgreSQL row behind.
Files
packages/session-ingest-contracts/src/rpc-contract.ts— adds cloneFromKiloSessionId to the create schema, the rejection-code union, and the ready/in_progress/rejected result shape.services/session-ingest/src/clone/session-clone.ts— new bounded, resumable clone orchestrator with digest verification and typed rejection.services/session-ingest/src/dos/SessionIngestDO.ts— adds clone stage meta plus exportCloneBatch, stageCloneBatch, inspectCloneStage, finalizeCloneStage, and resetCloneStage.services/session-ingest/src/session-ingest-rpc.ts— runs the clone before the row insert, idempotently claims existing destinations, and rolls back a clone only when it matches its source.services/session-ingest/src/ingest/metadata.ts— tracks whether a git_url write was actually applied so a refused Cloud Agent write no longer emits a phantom session.updated event.The clone-only create path now settles correctly after a lost response. A Session already registered rejection is treated as idempotent success instead of rolling back a committed clone, and a fully-succeeded clone-only reconcile settles completed instead of returning creation_in_progress forever.
Files
services/cloud-agent-next/src/session/session-registration.ts— treats the lost-response registration rejection as idempotent success and settles a fully-succeeded clone-only reconcile as completed.Continue in a new session now clones the full source session through the cloud prepare path instead of seeding a synthetic prompt or spawning a remote session. The hook resolves one destination, retries retryable clone failures with a fixed backoff, and surfaces terminal guidance or a retry message, so the read-only session footer always shows the right next step. The multi-destination continue picker and its bridge are deleted.
Files
apps/mobile/src/components/agents/use-continue-session.ts— rewrites the hook to resolve a single cloud destination, retry with backoff, and emit terminal or retry guidance.apps/mobile/src/components/agents/continuation-seed.ts— replaces destination enumeration with a single-outcome resolver that keeps the failure reason.apps/mobile/src/components/agents/use-continue-cloud-create.ts— sends the clone-only prepare input with a hoisted operation key and a persisted safe-retry row.apps/mobile/src/components/agents/session-detail-content.tsx— renders the guidance states and the Connect repository action.apps/mobile/src/components/agents/session-detail-content-helpers.ts— adds the Connect repository action that opens GitHub setup and clears terminal guidance.apps/mobile/src/app/(app)/_layout.tsx— removes the continue-picker route registration.apps/mobile/src/app/(app)/agent-chat/continue-picker.tsx— deleted; the multi-destination picker screen is gone.apps/mobile/src/components/agents/continue-picker-bridge.ts— deleted; the picker bridge module is gone.apps/mobile/src/components/agents/continue-picker-rows.ts— deleted; the picker row mapper is gone.Repository lists from the cloud-agent routers are now ordered by the caller's Cloud Agent usage history. Used repositories sort by session count descending, latest use descending, then normalized key; unused repositories keep provider order, and a rank-read failure returns the original order unchanged. The ordering is a backend contract consumed by web and mobile.
Files
apps/web/src/lib/cloud-agent/order-repositories.ts— new usage-ranking helper reading cli_sessions_v2 usage grouped by normalized git URL.apps/web/src/routers/cloud-agent-next-router.ts— applies the ranking to personal GitHub and GitLab repository lists.apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts— applies the ranking to organization GitHub, GitLab, and Bitbucket repository lists.apps/mobile/src/lib/use-new-session-repos.ts— drops a type assertion now that the tRPC result is typed.One shared sheet surface now hosts all six session-page sheets. On Android it renders an opaque full-window modal whose surface pads the top safe-area inset, so the content clears the system status bar; on iOS it keeps the native pageSheet modal, so each sheet keeps its existing content and safe-area footer.
Files
apps/mobile/src/components/agents/session-page-sheet.tsx— new shared surface with the iOS pageSheet and the full-height Android variant padded by the top inset.apps/mobile/src/components/agents/session-context-sheet.tsx— swaps its own modal for the shared surface.apps/mobile/src/components/agents/part-detail-sheet.tsx— swaps its own modal for the shared surface.apps/mobile/src/components/agents/message-details-sheet.tsx— swaps its own modal for the shared surface and routes the select-text view through it.apps/mobile/src/components/agents/child-session-sheet.tsx— swaps its own modal for the shared surface.apps/mobile/src/components/agents/file-part-renderer.tsx— hosts the file preview modal on the shared surface.apps/mobile/src/components/agents/attachment-preview-strip.tsx— hosts the attachment text preview on the shared surface.The subagent sheet keeps its Modal mounted after close, so the dismissal animation runs before the sheet identity is released. Android has no
Modal.onDismiss, so it releases on a 350-millisecond timer; iOS releases from the native callback.Files
apps/mobile/src/components/agents/child-session-sheet-state.ts— adds the sheet mount lifecycle helpers.The session screen header now gives the back control and the rename title separate 44-point tap targets. The back button is a fixed 44-point square and the title pressable carries its own hit slop, so the two controls no longer overlap and neither is clipped.
Files
apps/mobile/src/components/screen-header.tsx— sizes the back control to 44 points and gives the title pressable an asymmetric hit slop.The PR review screen now clears the Android bottom system navigation or taskbar inset across its Overview, Discussion, and Files tabs and their form footers. List footers, floating actions, empty and error states, and the form-sheet footer add the system bottom inset, so the last control never sits under the system bar.
Files
apps/mobile/src/lib/form-sheet.ts— computes the Android top inset only on Android for the full-sheet detent.apps/mobile/src/components/pr-review/diff/pr-diff-file-list-loading.tsx— adds bottom clearance to the first-page skeleton.apps/mobile/src/components/pr-review/diff/pr-diff-file-list.tsx— adds bottom clearance to the reconnect and retryable first-page states.apps/mobile/src/components/pr-review/diff/pr-diff-file-navigator.tsx— applies the inset-aware list content style.apps/mobile/src/components/pr-review/diff/pr-diff-floating-actions.tsx— includes the Android bottom inset in the bar's padding.apps/mobile/src/components/pr-review/diff/pr-diff-hunk-rows.tsx— adds bottom clearance to the tab-state and empty-files views.apps/mobile/src/components/pr-review/discussion/pr-review-discussion-list.tsx— adds bottom clearance to the list footer.apps/mobile/src/components/pr-review/pr-form-sheet-chrome.tsx— adds the Android bottom inset to the form-sheet footer.apps/mobile/src/components/pr-review/pr-review-discussion-tab.tsx— adds bottom clearance to the loading, empty, and error states.apps/mobile/src/components/pr-review/pr-review-screen.tsx— switches the Overview tab to the detail-screen scroll view that owns the bottom inset.Two new seed fixtures support the E2E flows. The frequent-repository-order fixture records one deterministic Cloud Agent repository use so the ordering E2E can assert the used repository moves ahead, and the mobile-sheet fixture seeds four read-only transcripts through the local session-ingest worker for the sheet hit-area E2E.
Files
dev/seed/app/frequent-repository-order.ts— new seed topic with before, used, and cleanup states.dev/seed/lib/frequent-repository-order-fixture.ts— fixture helpers that read the user's GitHub integration and insert or delete the fixture session row.dev/seed/app/mobile-sheet-fixtures.ts— new seed topic that resets and ingests four deterministic transcripts.dev/seed/lib/mobile-sheet-fixtures.ts— pure fixture builders and status parsing with no side effects.Tests: 44 test files changed (3 deleted, 13 added, 28 modified).
Generated: none.
Verification
The Android and iOS rounds ran 10 cases across four verification rounds; all 10 passed.
The rounds reproduced no defect on the unfixed build, so none remains.
Cases b1 and b2 were rerun on the full-height Android sheet; both passed. The b1 rerun drove the same five sheets (part detail, file preview, context usage, message details, child session) and asserted for each that the surface spans the whole window, that Done sits below the 136-pixel status bar, and that the removed scrim is absent. The context sheet scrolled its long content with the header fixed. The b2 rerun confirmed Back dismisses the child sheet and returns the session; no frame of the recording shows a white flash.
The b2 rerun then removed the Android dismiss layer and ran three more recorded dismissals. Every frame of all four recordings was measured: the blank frames stay at the cream background (mean blue 242), and no frame reaches white (mean blue 252 or more). The layer was dead weight on the opaque sheet, so it is deleted.
Recording: /Users/igor/Projects/.scratch/kilo-workflow-consolidated-kilo-workflow-2ade.sOVgsu/e2e-android-b-KBS5Nv/b2-dismiss.mp4 shows the Android Back dismissal of the subagent sheet.
Visual Changes
Android session screen sheets (part detail, file preview, context, message details, child session). Each sheet opens full height and starts below the system status bar. In the picture, the Context usage sheet fills the window, the clock and the status icons stay visible above it, and Done sits at its top right.
Android subagent sheet dismiss. Back dismisses the child sheet without a white frame and returns the session screen. The picture is the last recorded frame after Back: the transcript and Continue are on screen, with no white area.
Android PR review form sheet on a content-rich PR. The Submit review form sheet now keeps its footer controls above the bottom system navigation bar. In the picture, the Submit review and Cancel controls sit mid-screen, far above the bottom gesture bar. The handoff names these candidates for the tab insets, but both candidates show the form sheet; no picture shows the Overview, Files, or Discussion tab lists.
Android PR empty state. The empty PR Overview content now ends above the bottom system navigation bar. In the picture, the +10 / -2 head footer row sits above the bottom bar; a dev-only toast at the bottom does not cover it.
iOS session screen sheets. iOS sheets keep the native pageSheet geometry: a rounded-top sheet below the status bar. In the picture, the Context usage sheet starts with rounded corners below the status bar, and Done sits at its top right.
Reviewer Notes
No human steps required.
Notes: none.