Skip to content

feat(mobile): attachments go where the workspace lives; render cloud parts and path tags - #8852

Closed
superset-app[bot] wants to merge 10 commits into
mainfrom
harshith/attachments-mobile
Closed

superset-app[bot] wants to merge 10 commits into
mainfrom
harshith/attachments-mobile

Conversation

@superset-app

@superset-app superset-app Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Stacked. Draft until its dependencies are merged and released.

It does not depend on the desktop PR (#8851). The base is main, so until those merge the diff includes them. This PR's own change is commit bbfa063. The API (#8704) must be live before this ships, because rendering a cloud part calls attachment.resolveEach.

Summary

The mobile part of the attachments plan (v6):

  • cloud parts carry fileId and skip the import call;
  • the host branch;
  • seam 5's cloud branch for rendering;
  • path tags shown as badges.

Why

  • Mobile uploaded every attachment to our storage when it was attached, then had the host import it, even for a workspace on a machine someone owns. That breaks the plan's goal 2: local images stay off our servers.
  • A cloud chat imported every file into the box and sent its host id, so nothing without the box could render it.
  • A desktop message's <attachment path> tags showed as raw text on mobile.

How it works

  • Draft home: WorkspaceScreen tells lib/attachments/upload.ts the home of its workspace draft once the host resolves (setDraftAttachmentHome). A host draft does not upload at attach time. The home-screen draft (NewChatWidget) is unchanged: its host is not chosen yet when files are attached.
  • Chat send (lib/attachments/chatParts.ts, chatAttachmentParts):
    • host: each file is read as base64 and sent with attachments.upload through the relay; the part names attachmentId. A file over MAX_INLINE_ATTACHMENT_BYTES is refused before anything is sent.
    • cloud, and the box reads parts (hostReadsAttachmentParts(hostVersion)): wait for the uploads, attachFiles, then send parts with fileId only. No importFromCloud.
    • cloud on an older box: the old flow (attachFiles + importFromCloud, so the parts have attachmentId).
    • It sends attachments_sent / attachments_send_failed to PostHog with surface and home. The failure event uses rawErrorMessage.
  • Terminal (PTY) attachments: a host workspace now gets base64 filesystem.writeFile into .superset/attachments/ (create-only .gitignore, collision-probed names; lib/attachments/worktreeWrite.ts). A box keeps storage + materializeIntoWorkspace.
  • Rendering: useAttachmentFile takes a source (fileId or attachmentId). A cloud part resolves through attachment.resolveEach and File.downloadFileAsync into the cache, with no box needed. A host part reads through attachments.read as before. Path tags in a message's text show as file cards (name only, nothing fetched).
  • parseAttachmentTags / formatAttachmentTag moved from desktop's ChatSession/utils/attachmentTags into @superset/chat/core, with their test. Desktop's util index re-exports them, so desktop imports are unchanged.
  • If a send finds an attachment that never started uploading (attached while the draft's home was unknown), awaitAttachmentUploads now starts it. Before, it failed with "no longer available".

Not in this PR / differs from the plan

  • The new-workspace flow (useCreateTerminalWorkspace, home-screen new chat) still uploads at attach time and imports into the chosen host. The host is not known when files are attached, so a host-workspace launch from the home screen still goes through storage. The plan keeps the modal's cloud branch as it is and does not cover this case for mobile. It is a remaining goal-2 gap.
  • No Sentry event on a send failure, only PostHog (as on desktop).
  • No upload-at-attach for host workspaces, so a large file to a host is read and sent at send time with no progress ring.

tRPC compatibility

Testing

  • chatParts.test.ts: a cloud part carries fileId and skips the import; an older box gets the imported copy; a host part goes to the host and never to storage; a host refuses a file over the inline cap before sending.
  • worktreeWrite.test.ts: create-only writes into the worktree, with a suffix around an existing name.
  • apps/mobile bun test: 237 pass, 0 fail. packages/chat tests pass (97), including the moved attachmentTags test.
  • apps/mobile typecheck: no new errors. The tree has the same baseline errors as main, minus one this PR removes. Many of those baseline errors are "implicitly any" from client types that do not resolve in this sandbox, so mobile typecheck is weaker evidence here than usual.
  • apps/desktop typecheck passes, and the v2-workspace tests (896) pass after the parser move. bun run check:i18n passes, with translations written for all 16 locales. Biome is clean on the changed files.
  • Not verified: none of this ran on a simulator or device; this sandbox has no iOS tooling. The plan's manual checks are still open: a photo into a local, a relay-reached and a cloud workspace; desktop and mobile showing each other's images; a cloud image still rendering after its box is recreated.

Known limitations

saddlepaddle and others added 10 commits October 20, 2026 23:44
…-service bodies (SUPER-2658)

Pasting or dropping an image into a cloud workspace failed with "Failed to
send the file to the remote workspace" (terminal) or "Couldn't attach files /
value$1 is not iterable" (chat). The desktop was shipping the bytes as a
base64 `filesystem.writeFile` body over the streaming tRPC link. tRPC answers a
streamed batch before it has read the request body, and a Vercel sandbox's
ingress treats that early response as the end of the exchange and drops the
upload still in flight — a race, not a size limit: a 500 KB body on a home
uplink loses it about half the time.

Desktop: the chat composer, the terminal rich input, and the terminal
paste/drop override now take the route every other composer already takes
(mobile, the new-workspace modal): `attachment.createUpload`, a presigned PUT
to storage, `cloudWorkspace.attachFiles` for a box, then
`attachments.materializeIntoWorkspace` on the host. The host-service request
carries ids, never bytes. `uploadPastedFiles` is replaced by
`sendAttachmentsToWorkspace` + `useSendAttachments`.

host-service: `/trpc/*` buffers the request body before handing it to tRPC so
no response can start before the request is complete; the integration test
fails without it. `materializeIntoWorkspace` and the chat image writer share
one helper that creates `.superset/attachments` with its `.gitignore`, which
the former never wrote.
…ap files with the box

First API step of the attachment plan (SUPER-2874, seam 6).

- attachment.resolve no longer signs a URL for any composer upload in the
  org. A kept (ready) file resolves only through a cloud workspace the
  caller can see. An upload still in flight (pending) stays org-wide: a
  host resolves with its owner's credential, and the sender can be a
  teammate on that host. A box still sees only its own workspace's files.
- New attachment.resolveEach returns one entry per id, with
  { fileId, missing: true } for a file that is gone or not visible, so a
  transcript can render the rest. resolve keeps its output and errors for
  released hosts.
- The archive's final reap stage (and the fallback when it cannot be
  queued) detaches and deletes the files anchored to the workspace.
…fileId on cloud copies

Part of the attachments plan (PR 3: seams 8, 2, and the fileId carry-through
of seam 3).

The host's attachment store had no retention. Nothing deletes chat.db rows
and a terminal launch has no session, so there is no session delete to hook.
Instead, metadata.json records lastUsedAt. Upload and import set it, and every
path that hands an attachment out sets it again: read, agents.run's attached
files block, and the ACP resolver. A daily sweep (not in sandboxes) removes
entries unused for ATTACHMENT_RETENTION_DAYS. An entry written before this
change is stamped, not removed. The check and the rename that takes the
entry out of the store run in the same tick, so a hand-out on this event loop
cannot land between them. The slow delete runs async, off the loop.

A box's cached copy now keeps the fileId it was imported from, in its
metadata and in a small .by-file-id index, so the adapters (next PR) can find
a cloud part's cached copy without a scan. The first prompt of a cloud launch
spreads that metadata into its parts. Without fileId, the first attachments
of a cloud session would be the one kind a reader without the box cannot
render.

MAX_INLINE_ATTACHMENT_BYTES moves to @superset/shared/attachment-limits with
MAX_THUMBNAIL_BYTES and ATTACHMENT_RETENTION_DAYS. The host and the shared
launch schema use MAX_ATTACHMENTS for their max(10).
…tachments

Part of the attachments plan (PR 4, seam 4).

The Claude SDK adapter rendered an attachment part as `[name]`, and the Codex
adapter dropped it with a notice. Only ACP resolved it. This is why desktop
chat still flattens attachments into path tags in the text.

Now all three take one resolver from the host (chat-v3/resolveAttachment.ts).
It branches on the identity the part carries. `fileId` is a cloud workspace's
storage object: served from the box's cached copy, or imported now when the
store is empty, as on a recreated box. `attachmentId` is the host store entry
itself. Each hand-out stamps lastUsedAt for the sweep. Bytes are read only for
images, since every adapter gives other files by path.

- Claude: an image goes as a base64 image block in the SDK's content-array
  form. Any other file goes as a "# Attached files" line with its name and
  path, because store files are named by id.
- Codex: an image goes as app-server's `localImage` by path. Any other file
  goes as the same text block.
- An unreadable file is named to the agent, not dropped silently.
- Resolution is chained per adapter, so prompt order holds. A text-only prompt
  with nothing queued is still pushed synchronously.

The protocol's attachment part now needs `attachmentId` or `fileId` (it
needed `attachmentId` before), so a client can send a cloud part by fileId
alone. Clients send parts only to hosts at or above
ATTACHMENT_PARTS_MIN_HOST_VERSION (1.41.0, the first release with this
change). agents.run's attached files block uses the same attachedFilesBlock,
so terminal launches also list files by name.

A failed resolve reports to Sentry. The host has no PostHog client, so the
plan's PostHog event is not sent from here.
The #8712 commit folded into this branch was based on a newer main that had
changed cloud-workspace.ts's imports. Keep both: listPullRequests from main,
and reapWorkspaceFiles from #8712.
… attachments

An attachment-only message can carry an empty text part, and the Anthropic
API rejects an empty text content block.
…floor

Desktop and mobile both decide between attachment parts and path tags from
a host's version. One function next to ATTACHMENT_PARTS_MIN_HOST_VERSION keeps
the two clients on the same rule. It coerces the version, so a canary or a
dev build of a release at or above the floor counts.
…parts and path tags

Part of the attachments plan (mobile PR).

Mobile uploaded every attachment to our storage when it was attached, and
then had the host import it. That also happened for a workspace on a machine
someone owns, whose bytes should never reach our servers. Mobile also showed a
desktop path tag as raw text.

- A host workspace's draft no longer uploads at attach time. At send, chat
  attachments go to the host as base64 attachments.upload (the part names
  attachmentId). Terminal attachments are written into the worktree with
  create-only base64 filesystem.writeFile. Both refuse a file over
  MAX_INLINE_ATTACHMENT_BYTES.
- A cloud chat part carries its fileId and skips importFromCloud when the box
  reads parts (hostReadsAttachmentParts). An older box still gets the copy it
  imports.
- useAttachmentFile reads a fileId part from storage (resolveEach, then a
  download into the cache), so it shows without the box. A host part still
  reads from the host.
- Path tags in a message show as file cards. parseAttachmentTags moved from
  desktop into @superset/chat/core so both clients parse them the same way.
- A send that finds an attachment that never started uploading (its draft's
  home was unknown when it was attached) starts it.
@coderabbitai

coderabbitai Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 23aaceca-12d8-47a6-a791-a6e4dd0c328b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger 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.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@expo

expo Bot commented Oct 24, 2026

Copy link
Copy Markdown

Preview update published for development build db42788c (fingerprint e28b4b490ccc). Open the build on your phone and scan:

Update group 50c68a7d

Copy link
Copy Markdown
Collaborator

Folded into #9064, which now carries the whole attachments plan (commit list in its description). Closing in favor of that PR.

This branch was previously deployed

1 inactive deployment
preview — bbfa063f Deployed Oct 24, 2026 by superset-app[bot] via Deploy Marketing #12699
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.

3 participants