Skip to content

fix(mobile): recover terminal input lease on attach and preserve picked file names - #32987

Merged
AmoabaKelvin merged 12 commits into
stablyai:mainfrom
zhangxaochen:zhangxaochen/fix-mobile-terminal-file-attach-lease-and-name
Oct 26, 2026
Merged

AmoabaKelvin merged 12 commits into
stablyai:mainfrom
zhangxaochen:zhangxaochen/fix-mobile-terminal-file-attach-lease-and-name

Conversation

@zhangxaochen

@zhangxaochen zhangxaochen commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​193 $\color{#cf222e}{\Huge{\mathbf{−}}}$​77 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​116
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​126 $\color{#cf222e}{\Huge{\mathbf{−}}}$​59 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​67

ELI5

Long-pressing the mobile terminal attach button to upload any file no longer fails with Attach failed (reconnecting) after the app returns from the system file picker or a brief background freeze, and uploaded files now keep their original filename and extension (e.g. .txt, .pdf) instead of always being saved as .png.

What Changed

  • Before:
    • When the mobile app briefly disconnected or was frozen while the Android file picker was open, nativeChatInputLeaseReadyRef was cleared on disconnect (false), and when the previous terminal.subscribe stream ended (unsubscribeTerminal), useMobileSessionTerminalSubscriptionFoundation only bumped coveredStreamRevision when showNativeChatRef.current was true. In Terminal view (showNativeChat === false), neither coveredStreamRevision nor leaseReady changed, and ensureSessionTabs() skipped unchanged tab snapshots on reconnect—leaving the terminal permanently unsubscribed and causing useMobileAttachmentInputLeaseGate to time out with Attach failed (reconnecting).
    • saveClipboardImageBufferAsTempFile hardcoded the host temp filename to orca-paste-${Date.now()}-${randomUUID()}.png, renaming any non-image file picked via long-press (pickImage('files')) to .png.
  • After:
    • useMobileSessionTerminalSubscriptionFoundation bumps coveredStreamRevision whenever the active handle's stream is torn down without a rendered lease drop, regardless of showNativeChatRef.current, and useMobileNativeChatTerminalStream applies the same bounded 3-attempt + 5s healthy-stream proof to uncovered terminal resubscribes.
    • useMobileSessionStartup resubscribes an unsubscribed active terminal on reconnect after ensureSessionTabs(), useMobileAttachmentInputLeaseGate actively triggers lease recovery when waiting on a connected terminal tab, and useMobileNativeChatInputLease updates readyRef.current synchronously inside replace(next).
    • media-picker.web.ts, mobile-image-source-picker.ts (widened to type: '*/*'), mobile-image-attachment.ts, mobile-clipboard-image.ts, clipboard-params.ts, clipboard.ts, and clipboard-image-temp-file.ts thread and sanitize fileName so host temp files preserve their original extension.

Why

Users attach non-image files (logs, text files, docs) to terminal TUI agents on mobile via long-press on the attach button, and opening the OS file picker routinely backgrounds the app long enough to cycle the WebSocket or stream subscription. Recovering the terminal subscription/lease automatically and preserving the file's original extension makes mobile terminal file attachment reliable.

Linked Issue

Fixes #32985

Visual Proof

N/A - Verified on Android hardware (com.stably.orca.mobile) and covered by unit tests across use-mobile-attachment-input-lease-gate.test.ts, use-mobile-native-chat-terminal-stream.test.ts, mobile-image-attachment.test.ts, mobile-image-source-picker.test.ts, and clipboard-image-temp-file.test.ts.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below
  1. mobile/src/session/use-mobile-attachment-input-lease-gate.test.ts: Verifies immediate and 500ms forced terminal input lease recovery.
  2. mobile/src/session/use-mobile-native-chat-terminal-stream.test.ts: Verifies bounded resubscribe for uncovered terminal view on streamRevision bump.
  3. mobile/src/session/mobile-image-attachment.test.ts, mobile-image-source-picker.test.ts, & src/main/window/clipboard-image-temp-file.test.ts: Verifies type: '*/*', fileName forwarding, and sanitized filename/extension preservation on host temp files.

AI Disclosure

Assisted by Google DeepMind Antigravity agentic coding assistant for root-cause investigation, implementation, and testing.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Maintainer changes

Thanks for the careful report and fix. I merged current main into the branch and kept your commits; my changes are in the commits on top.

Reconnect while the picker is open (Terminal view)

  • Kept your root-cause fix: unsubscribeTerminal now signals a stream teardown in Terminal view too, not only while native chat covers the terminal.
  • Dropped the extra recovery paths (the attach gate's resubscribe callback and forced resubscribe after 500 ms, the startup resubscribe, the synchronous lease ref write). With the teardown signal in place nothing needs them, and they were timing-based.
  • Instead of a second retry counter for Terminal view, a rendered terminal with no stream now goes through the existing bounded rearm path (3 attempts, refilled after 5 s of a healthy stream or when terminal.list drops and reports the handle again), the same one covered chat uses.
  • subscribeToTerminal now returns whether it actually sent a subscribe, so a subscribe the bridge refuses immediately (for example at its stream cap) spends an attempt instead of retrying on every render.
  • The list callbacks read the handle from a ref, so switching terminal tabs doesn't rebuild the tabs controller or rerun session startup.

File names on the host

  • Only clipboard.startImageUpload carries the optional fileName. Older hosts ignore the unknown key and keep the .png name; the single-frame fallback only runs against hosts too old to know the field, so it doesn't send it.
  • The host reuses the chat attachments' sanitizeAgentSessionAttachmentName (leaf only, Windows reserved names, UTF-8 byte bound), keeps only letters, digits, space, ., _ and - so the typed path can't expand in a shell ($(id).txt becomes __id_.txt), and saves the file as <temp>/orca-paste-<uuid>/<name>. A flat orca-paste-…-notes.txt would read as an Orca long-text paste in native chat, and could exceed 255 bytes.
  • Picked images keep the host's orca-paste-*.png name (the shared agentSessionPastedImageName), since Grok history and the "Pasted image" label recognize pasted images by that name. Only non-image picks carry their own name. The web shell reuses the chat file picker's naming (nameMedia, or a name from the MIME type on older shells).

Visual proof (iOS simulator, Terminal view, long-press Attach → Files → notes.txt, plain shell terminal, dev desktop on a scratch profile)

Before (main 5a4dd30) After (this branch)
before after

Before, the text file lands as orca-paste-<ms>-<uuid>.png, with the text inside it. After, it lands as orca-paste-<uuid>/notes.txt. The after capture is from f74ec01, one commit before the shell-safe name rule, which leaves notes.txt unchanged. The reconnect fix isn't practical to stage on a device; use-mobile-session-terminal-resubscribe-snapshot.test.tsx drives the real foundation, subscription and rearm hooks through a stream that ends after the lease was already dropped. It fails on main (no second subscribe) and passes here.

Testing: focused mobile suites (stream/rearm, resubscribe snapshot, subscription, web and native pickers, attach, clipboard upload, lease gate) and host suites (clipboard-image-temp-file, clipboard RPC, agent-session-attachments), plus check:code-quality:changed and check:react-doctor:changed.

Left out on purpose: Windows paths are still wrapped in double quotes by the shared shellEscapePath, which desktop drag-and-drop also uses. That's tracked separately in #28783; picked names here no longer contain $ or % either way.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2e9b9546-28ed-4c16-9690-4456924470e1
Commits

Reviewing files that changed from the base of the PR and between 1553d40 and 46907df.

Files selected for processing: 2
  • src/main/window/clipboard-image-temp-file.test.ts
  • src/main/window/clipboard-image-temp-file.ts
Files skipped from review as they are similar to previous changes: 1
  • src/main/window/clipboard-image-temp-file.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Walkthrough

Mobile file picking now accepts any file type and carries available file names through upload requests. The host sanitizes supplied names when creating temporary files. Terminal attachment handling requests lease recovery when readiness is missing. Terminal subscription hooks update stream recovery behavior and subscribe to an active terminal after session tab setup when its subscription is absent.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Minimal · up to 46907

The change preserves picked file names and recovers terminal subscriptions after reconnect. No concrete merge-blocking risk was established in the reviewed code.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 16 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes both primary changes: terminal input lease recovery and preservation of picked file names.
Linked Issues check Passed Issue #32985 requires terminal subscription and input-lease recovery after Terminal-view stream teardown, reconnect, and an unready lease on a connected terminal tab. The changes add stream-revision r…
Out of Scope Changes check Passed The changes stay within issue #32985. Subscription recovery, lease-gate changes, filename propagation and sanitization, RPC contract updates, and related tests directly support terminal attachment rec…
Description check Passed The pull request description is complete and aligned with the template. It includes the ELI5 summary, detailed changes, rationale, linked issue, visual proof, testing details, AI disclosure, upstream-…
Full details: Linked Issues check

Explanation

Issue #32985 requires terminal subscription and input-lease recovery after Terminal-view stream teardown, reconnect, and an unready lease on a connected terminal tab. The changes add stream-revision recovery, bounded resubscription, reconnect subscription, and forced lease recovery. Issue #32985 also requires any-file selection and sanitized filename preservation. The changes use */*, forward fileName, and preserve extensions in host temporary files. The listed tests cover these recovery and filename paths.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #32985. Subscription recovery, lease-gate changes, filename propagation and sanitization, RPC contract updates, and related tests directly support terminal attachment recovery or filename preservation. No unrelated product change is demonstrated.

Full details: Description check

Explanation

The pull request description is complete and aligned with the template. It includes the ELI5 summary, detailed changes, rationale, linked issue, visual proof, testing details, AI disclosure, upstream-boundary checkbox, and checklist. The optional Review and Notes headings are not included, but the relevant considerations are covered elsewhere and this does not prevent review.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a32681c5-2b33-48bc-ac34-b73576310955
📥 Commits

Reviewing files that changed from the base of the PR and between 41db63f and 1553d40.

📒 Files selected for processing: 19
  • mobile/src/platform/media-picker-contract.ts
  • mobile/src/platform/media-picker.web.ts
  • mobile/src/session/mobile-clipboard-image.ts
  • mobile/src/session/mobile-image-attachment.test.ts
  • mobile/src/session/mobile-image-attachment.ts
  • mobile/src/session/mobile-image-source-picker.test.ts
  • mobile/src/session/mobile-image-source-picker.ts
  • mobile/src/session/use-mobile-attachment-input-lease-gate.test.ts
  • mobile/src/session/use-mobile-attachment-input-lease-gate.ts
  • mobile/src/session/use-mobile-native-chat-input-lease.ts
  • mobile/src/session/use-mobile-native-chat-terminal-stream.test.ts
  • mobile/src/session/use-mobile-native-chat-terminal-stream.ts
  • mobile/src/session/use-mobile-session-attachments.ts
  • mobile/src/session/use-mobile-session-startup.ts
  • mobile/src/session/use-mobile-session-terminal-subscription-foundation.ts
  • src/main/runtime/rpc/methods/clipboard.ts
  • src/main/window/clipboard-image-temp-file.test.ts
  • src/main/window/clipboard-image-temp-file.ts
  • src/shared/rpc-contract/clipboard-params.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

zhangxaochen and others added 11 commits October 24, 2026 23:01
…le-terminal-file-attach-lease-and-name

# Conflicts:
#	mobile/src/platform/media-picker.web.ts
#	mobile/src/session/native-chat/use-mobile-native-chat-terminal-stream.ts
…m path

The teardown signal alone restores Terminal view after a reconnect, so drop
the attach-gate recovery callback, the startup resubscribe and the synchronous
lease ref write. Uncovered missing streams now go through the same rearm
budget as covered chat instead of a second copy of it.
Reuse the chat attachment name sanitizer and give the paste its own temp
folder, so the name fits the filesystem limit without new byte math. The name
rides only on clipboard.startImageUpload; hosts without it never get here,
and older hosts drop the unknown key and keep the .png name.
…d-terminal rearms

Transcript readers (Grok history, the Pasted image label) recognize an
uploaded image by its orca-paste-*.png name, so only non-image picks carry
their own name. Rendered terminals now spend the rearm budget too, so they
also get its refill when terminal.list drops and reports the handle again.
…ed at once

A synchronous refusal ends the stream before subscribe returns, so the ref
check read it as gated and the rearm retried on every render. subscribeToTerminal
now reports whether it sent, and the rearm charges on that.
Read the reconciled handle from a ref so list callbacks keep their identity
across tab switches, name pasted images with the shared helper, and drop
conditional spreads and a dead resume guard.
…cters

Agents that take raw image paths get the saved path unescaped, so a picked
$(id).txt would expand if the paste reached a shell. Only letters, marks,
digits, space, dot, underscore and hyphen survive; also closes PowerShell
$() expansion inside the Windows double-quote form.
…e stubs

Image uploads send no fileName key at all, so recorded and older-host
startImageUpload requests stay byte-identical; test stubs of subscribeToTerminal
now return the sent flag.
@AmoabaKelvin
AmoabaKelvin merged commit 8291297 into stablyai:main Oct 26, 2026
41 of 42 checks passed
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.

fix(mobile): recover terminal input lease on attach and preserve picked file names

2 participants