Skip to content

feat(frontend): agent chat attachments + drive uploads#5459

Draft
ardaerzin wants to merge 32 commits into
fe-feat/agent-voice-inputfrom
fe-feat/agent-attachments
Draft

feat(frontend): agent chat attachments + drive uploads#5459
ardaerzin wants to merge 32 commits into
fe-feat/agent-voice-inputfrom
fe-feat/agent-attachments

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

🚧 Parked — do not merge. The composer → model attachment delivery is held on the backend reference contract (design PR #5439: D10 opaque attachment_id, D11 still open), and the design approach is under active review (session-mount vs dedicated attachments-mount). The drive-upload half uses the existing mount upload endpoint and is functionally complete on the FE. Kept in draft until BE + design settle.

Stacked on fe-feat/agent-voice-input — review that PR first; this one shows only the attachments delta.

What

Frontend for agent multi-modality attachments, plus uploading files into the session drive from the Files drawer.

Composer attachments

  • Enable the attach button; per-kind size limits; drag-drop behaves at the edges.
  • Model audio-capability steers voice without ever refusing (courtesy gate on the FE, authority stays on the runner).
  • Preview composer attachments in the same Files drawer/viewer (reuse, not a replica).

Drive uploads (Files drawer)

  • Drop to upload in both the tree and the grid — folder highlight, spring-load into folders, drop onto a folder or the pane.
  • Drop to stage on a recents peek (chat rail / config Files) → opens the drawer with files staged → pick a destination → one context-aware Upload here button commits.
  • Optimistic items render as real tree rows / file tiles under their destination folder (not a separate list), with progress, retry, dismiss, a subtle pending tint, and list-motion; staged/in-flight state is deduped so a file never shows twice.
  • Upload transport streams real progress via the mount upload endpoint.

Shared plumbing / refactors

  • useDriveDrop + useStageDrop (drag/drop + drop-to-stage), PendingTile (one prop-driven tile for staged/uploading/error/done), useImagePreviews (shared object-URL lifecycle), a byte-source seam + explicit-files mode on the drive viewer, and mount download routes typed as binary for Fern.

Held / follow-ups

  • Delivery ref format (opaque attachment_id vs mount path) and the session-mount-vs-dedicated-mount question are the open design calls (thread on [docs] Plan agent multi-modality #5439).
  • Voice-as-audio also needs a non-Claude audio-capable model wired in.

Notes

  • Base: fe-feat/agent-voice-input. No backend changes in this PR.

@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 23, 2026 1:43pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added drag-and-drop file staging and uploads across chat and file browsing.
    • Added upload progress, error states, retry, dismiss, and cancellation controls.
    • Added attachment previews in a dedicated viewer, including local files.
    • Added support for audio capability indicators and improved voice-mode behavior.
    • Added per-file-type attachment limits and clearer acceptance messages.
  • Bug Fixes
    • Prevented sending messages while attachments are still uploading.
    • Improved handling of folders, rejected files, failed previews, and unsupported audio.
    • Improved file-grid animations when items are added or removed.

Walkthrough

The PR adds kind-based attachment limits, model modality detection, upload-state tracking, attachment previews, local drive rendering, staged file handling, drag-and-drop uploads, retry controls, and drive explorer integration.

Changes

Attachments and drive files

Layer / File(s) Summary
Attachment contracts and model capabilities
web/oss/src/components/AgentChatSlice/assets/attachments.ts, web/packages/agenta-entities/src/workflow/..., web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx
Attachment validation now uses enabled kinds and per-kind size limits. Model modalities determine audio capability and default voice mode.
Composer uploads and attachment viewer
web/oss/src/components/AgentChatSlice/...
The composer tracks upload progress and errors, blocks unsettled submissions, supports retry and previews, and stages dropped files in the attachment drawer.
Local drive file rendering
web/oss/src/components/Drives/driveFileSource.tsx, web/oss/src/components/Drives/renderers.tsx, web/oss/src/components/Drives/FilesDrawer.tsx
Drive renderers now read local object URLs and text through shared hooks while retaining mount-backed fallbacks.
Drive upload and drop engine
web/oss/src/components/Drives/driveMedia.ts, web/oss/src/components/Drives/useMountUpload.ts, web/oss/src/components/Drives/useDriveDrop.ts, web/oss/src/components/Drives/useImagePreviews.ts
Drive uploads support progress, cancellation, retry, deduplication, previews, folder drops, and staging drops.
Drive explorer staging integration
web/oss/src/components/Drives/DriveExplorer.tsx, web/oss/src/components/Drives/ContextRail.tsx, web/oss/src/components/Drives/SessionFilesDrawer.tsx, web/oss/src/components/Drives/StorageSection.tsx, web/oss/src/components/Drives/configDrive.ts, web/oss/src/components/Drives/VirtualTileGrid.tsx
The explorer renders staged and pending uploads, exposes picker and drop controls, propagates staged state through drawers, and animates item-count changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AgentConversation
  participant ComposerAttachments
  participant DriveExplorer
  participant uploadMountFile
  User->>AgentConversation: Add or drop attachment
  AgentConversation->>ComposerAttachments: Render queued file
  ComposerAttachments->>DriveExplorer: Open attachment viewer
  DriveExplorer->>uploadMountFile: Upload staged file
  uploadMountFile-->>DriveExplorer: Report progress or completion
  DriveExplorer-->>ComposerAttachments: Render preview or upload state
Loading

Possibly related PRs

  • Agenta-AI/agenta#5400: Extends the same session-scoped FilesDrawer surfaces with staged uploads and attachment viewing.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely matches the main frontend work: agent chat attachments and drive uploads.
Description check ✅ Passed The description is directly related and accurately summarizes the attachment and drive-upload changes.
Docstring Coverage ✅ Passed Docstring coverage is 61.11% which is sufficient. The required threshold is 60.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-feat/agent-attachments

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.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsx (1)

124-140: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clickable tiles are keyboard-inaccessible.

Chip (L124-140) and the image-preview tile (L339-346) both set role="button"/onClick for the new attachment-preview affordance, but neither adds tabIndex or an onKeyDown handler — keyboard users can't reach or activate them, despite being announced as buttons to assistive tech.

♻️ Proposed fix
 const Chip = ({
     children,
     className,
     onClick,
 }: {
     children: ReactNode
     className?: string
     onClick?: () => void
 }) => (
     <div
         role={onClick ? "button" : undefined}
+        tabIndex={onClick ? 0 : undefined}
         onClick={onClick}
+        onKeyDown={
+            onClick
+                ? (e) => {
+                      if (e.key === "Enter" || e.key === " ") {
+                          e.preventDefault()
+                          onClick()
+                      }
+                  }
+                : undefined
+        }
         className={...}
     >

Apply the same tabIndex/onKeyDown pair to the image tile's <div> at L339-346.

Also applies to: 339-346

🧹 Nitpick comments (2)
web/oss/src/components/Drives/driveFileSource.tsx (1)

10-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Keep new comments to one short line.

  • web/oss/src/components/Drives/driveFileSource.tsx#L10-L18: reduce the file-level explanation to a concise summary.
  • web/oss/src/components/Drives/StorageSection.tsx#L84-L85: shorten this behavioral description to one line.

As per coding guidelines, “Keep in-code comments to at most one short line; use longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements.”

Source: Coding guidelines

web/oss/src/components/Drives/driveMedia.ts (1)

185-191: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Raw axios here diverges from the Fern-client guideline — confirm no accessor path exists.

The comment justifies the escape hatch (fetch-based Fern can't stream upload progress), which is a legitimate constraint. Flagging only so it's a conscious deviation: the guideline requires frontend API calls to go through per-resource Fern accessors rather than raw axios. If multipart upload with progress genuinely can't be expressed through a Fern accessor, this is acceptable as-is; otherwise route it through the accessor.

As per coding guidelines: "Frontend API code must use per-resource Fern client accessors from @agenta/sdk/resources, never raw axios or the @agenta/sdk root barrel."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e85bcfbb-39f5-4d03-8ab6-e8cbcb0d2277

📥 Commits

Reviewing files that changed from the base of the PR and between 01067ee and 086b03f.

📒 Files selected for processing (24)
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx
  • web/oss/src/components/AgentChatSlice/assets/attachments.ts
  • web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx
  • web/oss/src/components/AgentChatSlice/components/AudioPlayer.tsx
  • web/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsx
  • web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.ts
  • web/oss/src/components/Drives/ContextRail.tsx
  • web/oss/src/components/Drives/DriveExplorer.tsx
  • web/oss/src/components/Drives/FilesDrawer.tsx
  • web/oss/src/components/Drives/SessionFilesDrawer.tsx
  • web/oss/src/components/Drives/StorageFilesHeader.tsx
  • web/oss/src/components/Drives/StorageSection.tsx
  • web/oss/src/components/Drives/VirtualTileGrid.tsx
  • web/oss/src/components/Drives/configDrive.ts
  • web/oss/src/components/Drives/driveFileSource.tsx
  • web/oss/src/components/Drives/driveMedia.ts
  • web/oss/src/components/Drives/renderers.tsx
  • web/oss/src/components/Drives/useDriveDrop.ts
  • web/oss/src/components/Drives/useImagePreviews.ts
  • web/oss/src/components/Drives/useMountUpload.ts
  • web/packages/agenta-entities/src/workflow/index.ts
  • web/packages/agenta-entities/src/workflow/state/index.ts
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts

Comment thread web/oss/src/components/AgentChatSlice/AgentConversation.tsx
Comment thread web/oss/src/components/Drives/renderers.tsx
Comment thread web/oss/src/components/Drives/useImagePreviews.ts Outdated
ardaerzin added 24 commits July 23, 2026 15:37
Whole-panel drag-and-drop already worked; its edges did not.

- Rejections could land in a closed panel. Pasting something unsupported set a rejection message nobody could see, so the attempt read as nothing having happened. Opening the panel is now part of addFiles and triggers on rejections as well as accepts, so every entry path reports back.
- Files were accepted while the composer was disabled — no provider key, or the onboarding IDE hand-off — which stages attachments into an input that cannot be sent. Drops are now refused in those states, and dragover no longer claims the drop, so the cursor stays honest.
- A dropped folder arrives in dataTransfer.files as a typeless zero-byte entry, so it was rejected as "isn't a supported file type". Folders are now detected during the drop (webkitGetAsEntry is only valid synchronously there) and named as folders.
- The drag overlay advertised the limit while already at it, inviting a drop it would reject wholesale. At the limit it says so instead.
… size limit

Attaching was reachable by paste, drop and voice message, while the one control actually labelled "attach files" stayed disabled and said "coming soon". It is now live, gated with the drop paths on the composer being usable — accepting files into an input that cannot be sent is the case worth blocking, not the button itself.

Limits become per-kind rather than one number for everything: a phone photo clears 5 MB routinely, an uploaded audio clip is larger still, and a text file is never either. Images and documents get 10 MB, audio 15 MB, with the count shared across kinds.

The shape also becomes the capability seam it was always meant to be — the accepted set is now a list of kinds, the picker's accept attribute and the "what can I attach" copy are derived from it, and narrowing that list is all capability gating will need to do. Size copy moves out of the panel and into the rejection, which can name the specific ceiling that was crossed instead of a single number that no longer exists.
…r refusing it

Voice-to-text produces text and works against any model; a voice message needs one that can actually take audio in. Nothing reflected that, so the mic pushed "voice message" everywhere, including configs that cannot use it.

Capability now steers, per design decision D6 — it drives defaults and messaging, never a refusal:

- modalitiesForModel reads the selected model's declared input modalities from the harness catalog already on the client (sibling to contextWindowForModel). null means the catalog is silent, treated as unknown, never as "no" — so a missing field can't demote voice.
- With no explicit user choice, the mic leads with voice-to-text only when the model is known not to hear audio; otherwise voice message stays the lead. An explicit pick always wins and is remembered (new atom key, so the old always-audio default doesn't stick).
- Choosing voice message on an audio-blind model still records and attaches — the agent can use the clip with its tools — it just carries a note in the mode menu and an ear-slash + tooltip on the tray tile saying the model won't hear it.

No model in the catalog declares audio today, so in practice this leads with dictation now and self-corrects the moment an audio-capable model is wired — no code change needed.
The name sat on the top row and the time was vertically centred against the two-row (name + progress) column, so it floated between them and read as misaligned. Name and time now share the top row with a common baseline, and the progress bar spans the full width beneath both — the standard voice-message layout.
The viewer's file bodies fetched bytes straight from the mount (content query, media/object-url hooks, download), which is the only thing tying the drawer to a mount. Route each through a DriveFileSourceContext that a host can provide to preview local, non-mount files (composer attachments) in the same drawer instead of a parallel viewer.

Pure refactor here: with no provider — every host today — the merge hooks return the mount result unchanged, so chat browse and the config panel behave exactly as before. Consumed by nothing yet; the attachments host and the explicit-file tree path follow.
DriveExplorer sourced its tree only from the mount's lazy per-directory loader. An optional explicitFiles prop now lets it render a flat list directly instead, for previewing local files (composer attachments) in the same drawer.

Three surgical changes: the tree sources from explicitFiles when present, and per-directory loading is short-circuited (a synthetic null-mount drive would otherwise report every folder as forever-loading). The rest is inert for free — repo probing, root-loading, and download-all are all already guarded on a real mount id, so a null-mount synthetic drive disables them without extra gating. FilesDrawer forwards the prop; when unset, everything is exactly as before.
Clicking an image thumbnail or a document chip in the composer now opens it in the same Files drawer used for agent files — the shared tree + viewer — rather than a separate previewer or nothing at all. Multiple attachments show as the tree; the clicked one is selected. Audio keeps its inline tray player, so it is not made clickable.

AttachmentViewerDrawer is the host: it turns the composer's in-memory uploads into an explicit file list plus a DriveFileSourceContext that resolves bytes from object URLs, and hands both to FilesDrawer with a synthetic mount-less drive. So it reuses DriveExplorer, the two-pane layout, tree virtualization, selection, breadcrumb, and every per-kind viewer body — no viewer is duplicated. Object URLs are built only while the drawer is open and revoked on close.

Kind resolution keys on the filename extension, so the host derives one from the media type when a name lacks it (a recorded clip, a pasted blob) — which is why resolveDriveFileKind itself needed no change.
… ready for wiring

Completes the attachment lifecycle so the upload transport is the only thing left to add.

Upload state is carried on antd's UploadFile fields (status / percent / error) and rendered by the tray, so the flow is one seam away from live:

- useAttachmentUploads owns the machine: enqueue on add, per-file progress, success, failure, retry, and abort-on-unmount. Its uploader is an injected function; until the transport exists it is left undefined, so enqueue is a no-op and files stay "done" (send behaviour unchanged). Provide the uploader and the whole flow runs — nothing else changes.
- Each tile shows its state: a progress scrim with percent while uploading, and a retry-able error overlay (message on hover) on failure.
- Aggregate: while any attachment is uploading or failed the message isn't ready, so send is gated and handleSubmit guards on it. All-"done" today, so inert until wired.

Preview errors, which were the missing half of the add-time guardrails:
- a tray thumbnail whose blob won't decode falls back to a broken-image tile instead of an empty box;
- the drawer viewer treats a local blob that fails to decode like a mount error, showing its "couldn't load" card.
Adds the write side of the drawer: uploadMountFile posts a file into a mount folder with real progress via axios onUploadProgress (Fern uses fetch, which can't stream upload progress), and useMountUpload manages the in-flight items — per-file progress, retry, dismiss, abort-on-unmount — refreshing the mount's file queries on success.

Distinct from delivering a composer attachment to the model (held on the reference contract): this writes into the agent's working files via an endpoint that already exists end to end. The DriveExplorer affordance that drives it follows.
Adds an Upload affordance to the drawer header: it writes the picked files into the currently-viewed folder (resolved to its mount — cwd or the folded agent-files mount), with per-item progress, retry, and dismiss from useMountUpload, and refreshes the listing on success so the new files appear.

Shown only for a writable mount-backed drive — hidden in the local-file attachment preview (explicitFiles mode), which has no mount to write to. Both real hosts (chat and config) point at the agent's drive, so both get it.

This is the unblocked upload path: it writes into the agent's working files via an endpoint that already exists, and does not depend on the held composer-attachment-to-model reference contract.
… drag hook

Two reusable pieces the tree and grid will share.

useMountUpload now tags each in-flight item with its destination folder (plus media type and, for images, an object-URL preview) and exposes itemsFor(folder), so a view can render the pending uploads inline in the folder they're landing in rather than only in a header strip.

useDriveDrop encapsulates the drag behaviour: window-level drag tracking, per-folder hover highlight, spring-load (hover ~0.7s to drill into a folder), and drop handlers that upload into the hovered folder or — for a container/background drop — the current folder. The views just spread its handler props onto folder targets and their container.
… tiles

Wires useDriveDrop into the folder grid: dragging files highlights the folder tile under the cursor, holding ~0.7s springs the view into it, and dropping uploads — onto a folder tile, or into the current folder for a background drop.

Uploads appear immediately as optimistic tiles in the open folder (synthetic nodes merged into the grid, drawn by UploadTile) with a live progress bar and, for images, a preview; on failure the tile offers retry/dismiss. On success the tile is replaced by the real file via a background refetch that keeps the grid on screen — the folder's loading gate already keys on first-load only, so there is no skeleton flash. Also drops the fetching term from the tree's per-dir loading for the same reason, and generalises the header Upload button to target any folder.

Gated to writable mounts (canUpload) — inert in the local-file attachment preview. The tree gets the same treatment next.
…p targets

Mirror the grid's drop behaviour on the tree pane: folder rows are drop
targets that spring-load into the folder on hover and upload on drop, and a
background drop over the pane uploads into the current folder. Folder rows tint
while hovered. The uploaded file surfaces via the tree's silent revalidate
(no skeleton flash), so no synthetic rows are injected into the virtualized list.
… tree

Make upload items enter/leave through the existing motion instead of popping:

- Grid: VirtualTileGrid now opens its reflow spring on an item-COUNT change
  (not only a column flip), so inserting or removing an optimistic upload tile
  glides the neighbouring tiles into their new slots instead of snapping. Tiles
  whose slot is unchanged (an end-append) spring to the same target, so they
  stay put. The upload tile fades+scales itself in, independent of the folder's
  one-shot reveal.
- Tree: a file appearing in an already-loaded folder (an upload landing via the
  silent revalidate) is recognised as first-ever-seen and fades in through the
  same revealFade used for first loads; a cached folder's re-expand does not
  re-fade, since those paths were seen before.
Dragging files onto a Files summary (the chat ContextRail or the config
StorageSection) captures them and opens the Files drawer with the files STAGED —
nothing is written yet. A recents peek is a cross-folder, derived list with no
destination of its own, so instead of guessing a folder the drawer lets the user
choose one:

- Staged files render as ghost tiles in the grid (dashed, no progress), pinned
  across folder navigation and removable individually.
- The user browses the tree to a destination; a single "Upload N here" button in
  the drawer header commits the staged files into the folder currently in view,
  at which point they flow through the existing upload path (progress tiles + the
  list motion).
- Removing every ghost tile or closing the drawer discards staging.

Threaded generically through FilesDrawer -> DriveExplorer, so both hosts share
one implementation: the chat host stages via a per-session atom, the config host
via the existing per-revision drawer-request atom.
…he tree

Three fixes from review of the optimistic upload/staged tiles:

- Sizing/overflow: the tiles previewed the full-size dropped image as a non-absolute
  flex child, whose intrinsic min-content height overrode the thumb's 4:3
  aspect-ratio and blew the tile past its grid slot (the real FileThumb dodges this
  only because its thumbnails are tiny). Take the preview image out of flow
  (absolute inset-0) so the box stays 4:3, like FileThumb's text-preview note.
- Style: lighter, image-preserving states — a scrim + inline percent for progress,
  a scrim + Retry/Dismiss for errors, and a clearer "pending" look for staged tiles
  (primary dashed frame, upload glyph, hover-reveal remove).
- Visible in both surfaces: pending items (staged + in-flight) are a drawer-global
  set now — pinned as a compact group at the top of the TREE and shown in the grid
  regardless of which folder is open, so a dropped file shows in both views no
  matter where it was dropped.
…ile tiles

- Layout shift: remove the full-width header upload list. It mounted/unmounted on
  every upload state change, shoving the toolbar and panes. Pending items already
  render as grid tiles + a pinned tree group, so the banner was also redundant.
- Tile styling: the staged/upload tiles now use the exact same frame as a real
  file tile (neutral border + fill, 4:3 thumb, name, one caption line); status is
  a small ABSOLUTE overlay only, so it never changes tile height or shifts the row.
  Staged = a "Staged" badge + hover remove; uploading = a thin bottom progress bar
  + "Uploading… N%"; failed = a corner warning badge + inline Failed/Retry/Dismiss.
  Drops the heavy primary tint, centered glyph, and dark scrim.
…ing at once

Keep the two flows (recents-drop = stage, in-drawer folder drop = upload now) but
make the state robust so they can't duplicate an item:

- fileKey(name+size+lastModified) gives a picked file a stable identity.
- upload() skips any file already in flight, so a re-drop can't spawn a second
  upload item for the same file.
- uploadIntoFolder() drops any STAGED copy of a file the moment it starts
  uploading, so the same file never renders as both a staged tile and an uploading
  tile. Committing staged (Upload here) already clears the inbox; a direct folder
  drop now prunes it too.
… once

The one-shot prune only fired at upload-start, so the real repro slipped through:
a file that failed (still in-flight) and was then dropped/staged again rendered
as BOTH a Staged tile and a Failed tile.

Make the invariant continuous: every in-flight item now carries its fileKey, and
DriveExplorer reconciles the staged inbox against the in-flight key set on every
change — any staged file that is already uploading or failed is dropped from the
inbox immediately. A file is therefore staged XOR in-flight, never both, whatever
the order of drops. Retry/Dismiss on the failed tile remain the way to act on it.
Strip the heavy decoration so a staged/upload tile is a normal file tile with a
quiet status line:

- Drop the black "Staged" pill and the error corner badge.
- One hover-reveal corner action on both (remove for staged, dismiss for failed),
  matching the file-tile idiom instead of always-on chrome.
- Progress is a hair-thin bar at the very bottom edge of the thumb; caption reads
  "Uploading N%".
- Failed: dimmed thumb + a compact "Failed · Retry" caption (dismiss moved to the
  corner action).
- Staged: clean thumb + a subtle "Ready to upload" caption with a small glyph.

Frame, thumb (4:3), name, and single caption line are byte-identical to
DriveFileRow's tile, so heights stay uniform with the folder/file tiles.
…grid snap

Root cause of the oversized tiles: the failed tile's caption contained a raw
<button> (Retry). Preflight is off in this app, so a button does NOT inherit
font-size and rendered at the UA default (~13px) — taller than the 11px caption
of every other tile — which pushed the tile past VirtualTileGrid's snapped row
height and made it overflow.

Fix: every dropped-tile caption is now a PLAIN text span, byte-identical to the
folder/file caption (name text-xs + caption text-[11px]). All actions moved to
absolute overlays on the thumb (Retry = centered pill on the dimmed thumb,
Dismiss/remove = hover corner X), which can't affect tile height. Heights are now
uniform with the surrounding tiles.
The image tiles were still taller than the folder/file tiles: a full-size
object-URL image defeats the `aspect-[4/3]` box even with the img positioned
absolutely, so the thumb rendered taller than 3/4 of its width and the tile
overflowed VirtualTileGrid's snapped row height.

Replace the aspect-ratio box with a padding-bottom aspect box: the thumb's
height is padding-bottom:75% of its width — a value the image cannot influence
because all content (image, progress, retry/dismiss overlays) lives in an
absolute inner layer. Thumb is now exactly 4:3, so the tiles measure the same as
the surrounding folder/file tiles.
…heir folder

Uploads no longer render as a pinned block outside the hierarchy. Each in-flight
upload now carries its drive-root-relative destination folder (+ size), and a
synthetic file entry is fed to buildDriveTree so the upload nests UNDER its folder
like any file. Both surfaces read that one tree:

- Tree: the real TreeRow renders it (correct icon/name/indent), with a trailing
  status only — spinner+% while uploading, Failed·Retry·Dismiss on error, plain
  file row once done.
- Grid: FolderView already reads the same nodes; the matching node is drawn as the
  progress/error tile (decorated by path via pendingUploadByPath).

Removes the bespoke PendingTreeRow + the grid's separate __upload__ synthetic
injection. Staged files (no destination yet) remain grid-only ghost tiles.
Drop the large "Upload N here" primary button — it duplicated the icon upload
button beside it. The single icon button now switches behavior by context:

- Files staged: primary-tinted (colorPrimaryBg fill + colorPrimary icon), tooltip
  "Upload N files here", commits the staged files into the current folder.
- Otherwise: neutral, tooltip "Upload to this folder", opens the file picker.

The colour change signals the difference; the tooltip carries the count.
Upload/staged items shared the neutral file-tile fill, so they blended in. Give
them a subtle primary tint (colorPrimaryBg fill + colorPrimaryBorder) in both
surfaces — the grid tile frame and the tree row — so a pending item is
distinguishable from a committed file without reading the caption.
Both surfaces already offer Retry + Dismiss on failure, but the grid tile's
Dismiss was hover-gated (opacity-0 until group-hover), easy to miss on a failed
item. Make it always visible in the error state so removing a failed upload is
obvious; the tree row already shows Retry/Dismiss inline.
Two near-identical blocks the new upload work had grown collapsed into shared,
prop-controlled units:

- useStageDrop(onFiles): the recents-peek drop handler (highlight + isFileDrag +
  onDragOver/Leave/Drop) was copy-pasted in ContextRail and StorageSection with
  only the drop action differing. It now lives beside useDriveDrop and takes the
  action as a callback (falsy = disabled). Exports FileDropProps for the shared
  handler-props shape.
- PendingTile: UploadTile and StagedTile were the same tile frame/thumb/name/
  caption with different status bits. They're now thin adapters over one
  PendingTile driven by a `state` prop (staged | uploading | error | done),
  caption, and onRetry/onRemove — the frame, aspect box, and corner action live
  in one place.

No behaviour change; tsc + eslint clean.
…eanup

The staged tiles (DriveExplorer) and in-flight uploads (useMountUpload) hand-rolled
the same "createObjectURL for images, revoke on settle" dance. Extract it:

- useImagePreviews(files): lazily mints an object URL per image file, revokes when
  a file leaves the list or on unmount, and returns a Map whose reference stays
  stable unless the previewed set changes (safe in memo deps). Also fixes an
  unmount leak the upload path had (previews weren't revoked on drawer close).
- useMountUpload: derives previewUrl from the hook (items now carry `file`); drops
  the manual createObjectURL + the two revoke sites.
- DriveExplorer: stagedPreviews is now one useImagePreviews(staged) call instead of
  a useMemo + cleanup effect.

Cleanup: removed the dead `itemsFor` and `mediaType` members of MountUploadItem/
MountUpload (nothing read them after uploads went global then tree-injected).
Restore the real upload transport: drop the simulated-progress branch in
uploadMountFile and the SIMULATE_UPLOAD success-persistence in useMountUpload, so
a completed upload drops the optimistic tile and the real file arrives via the
listing refetch (the file store endpoint is the source of truth again).
onDrop returned on attachmentsBlocked() before preventDefault(), so a file dropped
while blocked (recording active, or composer disabled) triggered the browser's
default action — navigating to the file and discarding the whole conversation.
And onDragOver only preventDefault'd when not blocked, so per the DnD spec the
panel wasn't even a drop target then. Make the panel ALWAYS intercept file drags
(preventDefault on enter/over/drop) and signal refusal via dropEffect="none" +
the overlay, instead of by declining to be a drop target.
…g render

useImagePreviews minted and revoked object URLs in the render body. Under React 19
concurrent rendering a discarded render would leak created URLs or invalidate
committed references. Move all createObjectURL/revokeObjectURL into a commit
effect over a ref-held live map (revoked on unmount), publishing a state snapshot
so consumers still re-render when a preview appears or disappears.
…ents

HtmlBody's assembly bailed at !mount?.id, so a local composer HTML attachment left
the Preview tab loading forever. Make inlineHtmlAssets skip mount-asset inlining
when there's no mount while still running the sanitize + nav-interceptor pipeline,
and let the effect proceed for local content.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx (1)

132-151: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the menu item array.

menuItems recreates JSX-bearing objects on every render before passing them to Dropdown. Wrap it in useMemo keyed by the relevant capability/support booleans.

As per coding guidelines: “Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b08179d6-b6b6-48f1-8303-e7fb05be1248

📥 Commits

Reviewing files that changed from the base of the PR and between 086b03f and 0b5dd83.

📒 Files selected for processing (24)
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx
  • web/oss/src/components/AgentChatSlice/assets/attachments.ts
  • web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx
  • web/oss/src/components/AgentChatSlice/components/AudioPlayer.tsx
  • web/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsx
  • web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.ts
  • web/oss/src/components/Drives/ContextRail.tsx
  • web/oss/src/components/Drives/DriveExplorer.tsx
  • web/oss/src/components/Drives/FilesDrawer.tsx
  • web/oss/src/components/Drives/SessionFilesDrawer.tsx
  • web/oss/src/components/Drives/StorageFilesHeader.tsx
  • web/oss/src/components/Drives/StorageSection.tsx
  • web/oss/src/components/Drives/VirtualTileGrid.tsx
  • web/oss/src/components/Drives/configDrive.ts
  • web/oss/src/components/Drives/driveFileSource.tsx
  • web/oss/src/components/Drives/driveMedia.ts
  • web/oss/src/components/Drives/renderers.tsx
  • web/oss/src/components/Drives/useDriveDrop.ts
  • web/oss/src/components/Drives/useImagePreviews.ts
  • web/oss/src/components/Drives/useMountUpload.ts
  • web/packages/agenta-entities/src/workflow/index.ts
  • web/packages/agenta-entities/src/workflow/state/index.ts
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
🚧 Files skipped from review as they are similar to previous changes (21)
  • web/packages/agenta-entities/src/workflow/index.ts
  • web/packages/agenta-entities/src/workflow/state/index.ts
  • web/oss/src/components/Drives/SessionFilesDrawer.tsx
  • web/packages/agenta-entities/src/workflow/state/inspectMeta.ts
  • web/oss/src/components/Drives/StorageSection.tsx
  • web/oss/src/components/Drives/StorageFilesHeader.tsx
  • web/oss/src/components/Drives/driveMedia.ts
  • web/oss/src/components/Drives/useImagePreviews.ts
  • web/oss/src/components/AgentChatSlice/components/AudioPlayer.tsx
  • web/oss/src/components/Drives/useMountUpload.ts
  • web/oss/src/components/Drives/VirtualTileGrid.tsx
  • web/oss/src/components/Drives/FilesDrawer.tsx
  • web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx
  • web/oss/src/components/AgentChatSlice/hooks/useAttachmentUploads.ts
  • web/oss/src/components/Drives/driveFileSource.tsx
  • web/oss/src/components/AgentChatSlice/assets/attachments.ts
  • web/oss/src/components/AgentChatSlice/components/ComposerAttachments.tsx
  • web/oss/src/components/Drives/ContextRail.tsx
  • web/oss/src/components/Drives/useDriveDrop.ts
  • web/oss/src/components/Drives/DriveExplorer.tsx
  • web/oss/src/components/AgentChatSlice/AgentConversation.tsx

Comment on lines +21 to +23
/** `null` = the person has not picked a mode, so capability decides which one leads. An explicit
* choice always wins and is never overridden. */
const voiceModeAtom = atomWithStorage<VoiceMode | null>("agenta:agent-chat:voice-mode.v2", null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate relevant files =="
fd -a 'VoiceInputButton\.tsx|AgentChatSlice|VoiceMode' . | sed 's#^\./##' | head -200

echo
echo "== VoiceInputButton relevant contents =="
if [ -f web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx ]; then
  wc -l web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx
  sed -n '1,180p' web/oss/src/components/AgentChatSlice/components/VoiceInputButton.tsx
fi

echo
echo "== Search for voice-mode storage keys and VoiceMode definitions =="
rg -n 'voice-mode|AudioInputButton|VoiceMode|audioPerceivable|mode\.v2|agent-chat:voice-mode' web || true

Repository: Agenta-AI/agenta

Length of output: 10938


Migrate the pre-v2 voice-mode preference.

The new .v2 storage key drops any existing "audio"/"transcribe" preference and falls back to capability-based selection. Either keep the previous storage key or read it once, preserve its value to .v2, then clear it.

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.

1 participant