Skip to content

[chore] Split DriveExplorer into hooks and views - #5570

Open
ardaerzin wants to merge 3 commits into
release/v0.106.2from
fe-chore/drive-explorer-split
Open

[chore] Split DriveExplorer into hooks and views#5570
ardaerzin wants to merge 3 commits into
release/v0.106.2from
fe-chore/drive-explorer-split

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Context

DriveExplorer.tsx had grown to 2587 lines holding 14 inline components plus the whole browsing body: the lazy tree pipeline, selection, search, the per-parent horizontal scroll groups, pane drag, keyboard navigation, the row virtualizer, and the upload staging flow.

Changes

The presentational half moves to sibling modules, then the remaining body splits into hooks. The Drives/ folder is flat by convention, so everything joins it as siblings rather than introducing subdirectories.

views    DriveBreadcrumb, DriveTreeRow, DriveFileContentViewer,
         DriveFilePreview, FolderTile, FolderView, DriveHeader,
         DriveTreeList, DriveTreePane, DriveToolbar, DriveExplorerStates,
         DrivePendingTiles
hooks    useDriveFilters, useDriveSelection, useDriveTreeData,
         useDriveTreePane, useTreeGroupScroll, useDriveTreeViewport,
         useDriveTreeReveal, useDriveTreeKeyboard, useDriveDownloadAll,
         useDriveUploads
helpers  driveTypes, driveTreeView, useDelayedTrue; motion tokens append
         to the existing driveMotion.ts

DriveExplorer.tsx is 409 lines.

Shared refs stay inside the hook that owns them. The tree scroll element never leaves useDriveTreeViewport (group scroll attaches through attachTreeWheel(el)), and the group maps never leave useTreeGroupScroll (rows read scrollXFor(parent)).

Ordering matters for uploads and is preserved. useDriveUploads is called above the tree so its uploadFiles feed buildDriveTree. That is what makes an in-flight upload render as a real row under its destination folder instead of a separate pinned list. pendingUploadByPath then decorates the resulting rows.

currentFolder and commitStaged stay in the root on purpose. They read the selection, which derives from the tree, which consumes useDriveUploads. Moving them into the hook would be circular, and keeping them out preserved their dependency arrays exactly.

Tests

  • pnpm --filter @agenta/oss exec tsc --noEmit passes with zero errors, as do @agenta/entity-ui and @agenta/ee.
  • pnpm lint-fix across the repo makes no changes. Prettier clean.
  • Effect bodies, dependency arrays and lazy initializers moved verbatim, comments included.
  • Rebuilt against current release/v0.106.1 rather than rebased, because the upload and staging feature (512 added lines) landed inside the regions this refactor deletes. Verified line by line: every behavioural line survives, and NEXT_PUBLIC_AGENT_FILE_UPLOADS still reaches the drop targets, the header upload button and the staged inbox.

Not runtime verified. The gates above are static.

One follow-up left in place: the module doc still says "Phase 1 is read-only", which uploads made false. It was kept verbatim rather than silently edited in a structural refactor.

What to QA

  • Open the files drawer and expand a deep folder. Rows appear, and a slow level shows shimmer placeholders that swap in place with no height jump.
  • Scroll a nested folder's rows sideways. Siblings move together, other folders stay put.
  • Drag a file onto a folder row and onto the tree background. Both upload to the right destination, and the in-flight item shows as a row under that folder.
  • Fail an upload (drop something oversized). The tile offers Dismiss, and Retry works.
  • Drag the tree pane divider, then collapse and expand it. The content pane tracks the width in one pass with no second snap.
  • Arrow-key around the tree and the tile grid. Cmd/Ctrl+Down opens, Cmd/Ctrl+Up steps out.
  • Regression: Download all still produces a zip, and a partial mount failure still shows the inline warning with Try again.

DriveExplorer.tsx: 2587 -> 409 lines, across 26 flat siblings (the Drives
folder has no subdirectories; new modules join it as siblings).

  views    DriveBreadcrumb, DriveTreeRow, DriveFileContentViewer,
           DriveFilePreview, FolderTile, FolderView, DriveHeader,
           DriveTreeList, DriveTreePane, DriveToolbar, DriveExplorerStates,
           DrivePendingTiles
  hooks    useDriveFilters, useDriveSelection, useDriveTreeData,
           useDriveTreePane, useTreeGroupScroll, useDriveTreeViewport,
           useDriveTreeReveal, useDriveTreeKeyboard, useDriveDownloadAll,
           useDriveUploads
  helpers  driveTypes, driveTreeView, useDelayedTrue; motion tokens append
           to the existing driveMotion.ts

Refs stay inside their owning hook: the scroll element never leaves
useDriveTreeViewport (group scroll attaches via attachTreeWheel) and the
group maps never leave useTreeGroupScroll (rows read scrollXFor(parent)).

Split against current content so the upload/staging feature survives.
useDriveUploads is called ABOVE the tree so its uploadFiles feed
buildDriveTree — that is what makes an in-flight upload render as a real
row under its destination folder rather than a pinned list — and
pendingUploadByPath then decorates the resulting rows. retry/dismiss are
exposed as named accessors rather than leaking the upload object.

currentFolder and commitStaged stay in the root: they read the selection,
which derives from the tree, which consumes useDriveUploads — moving them
into the hook would be circular. Verified line-by-line against the upstream
diff.
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 30, 2026
@vercel

vercel Bot commented Jul 30, 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 Jul 30, 2026 10:53pm

Request Review

@dosubot dosubot Bot added Frontend refactoring A code change that neither fixes a bug nor adds a feature labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ed43d86d-a3cb-42bc-9332-a12fa1157fec

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
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Redesigned drive explorer with a resizable, searchable file tree and keyboard navigation.
    • Added breadcrumb navigation, folder tiles, file previews, metadata, copy-path, and download actions.
    • Added upload staging with progress, retry, dismiss, drag-and-drop, and folder-specific upload options.
    • Added “Download all” for drive contents.
    • Added filters for file origins, hidden files, and Git-ignored files.
  • Bug Fixes
    • Improved loading, empty, error, and upload-status states with clearer feedback and retry options.

Walkthrough

DriveExplorer.tsx is refactored from a monolithic ~2500-line component into a composition root wiring new hooks (filters, selection, tree pane/data/viewport/keyboard/reveal, uploads, download-all) and new components (breadcrumb, toolbar, tree pane/list/row, folder/file views, header, pending tiles, empty/error states). No public API of DriveExplorer changed.

Changes

Drive Explorer Refactor

Layer / File(s) Summary
Shared types, motion, and tree utilities
driveTypes.ts, driveMotion.ts, driveTreeView.ts, useDelayedTrue.ts, useTreeGroupScroll.ts
Adds DriveScope/DriveId types, motion presets (PANE_FADE, revealFade, META_REVEAL), tree-flattening utilities (flattenTree, parentOf, collectFolderPaths), a delayed-boolean hook, and per-group horizontal scroll tracking.
Breadcrumb
DriveBreadcrumb.tsx
Adds driveRootLabel and DriveBreadcrumb rendering home button and clickable path segments.
Toolbar
DriveToolbar.tsx
Adds toolbar with tree toggle, search, origin filter, hidden/gitignored toggles.
Selection & filter hooks
useDriveFilters.ts, useDriveSelection.ts
Centralizes search/origin/hidden state and per-mount persisted selection/expanded-folder state.
Tree data/viewport/keyboard/reveal/pane hooks
useDriveTreeData.ts, useDriveTreeViewport.ts, useDriveTreeKeyboard.ts, useDriveTreeReveal.ts, useDriveTreePane.ts
Derives filtered/flattened tree rows, virtualization/focus, keyboard navigation, scroll-into-view reveal, and animated pane width/drag behavior.
Tree rendering components
DriveTreeRow.tsx, DriveTreeList.tsx, DriveTreePane.tsx
Renders virtualized rows with drop targets and context menus, and the two-pane resizable layout.
Upload staging
useDriveUploads.ts, DrivePendingTiles.tsx
Manages mount-backed uploads, drag/drop, staged inbox reconciliation, and upload/staged tile UI.
Download all
useDriveDownloadAll.ts
Computes archive mounts and drives toast-based archive download.
Folder/file views and header
FolderTile.tsx, FolderView.tsx, DriveFileContentViewer.tsx, DriveFilePreview.tsx, DriveHeader.tsx
Renders folder tile grids, file content/preview crossfades, and the drawer header with navigation, upload/download, and details toggling.
Empty/error states
DriveExplorerStates.tsx
Adds DriveErrorState and DriveEmptyState for terminal body states.
DriveExplorer composition
DriveExplorer.tsx
Replaces inline logic with hook/component wiring across search, selection, tree pane, uploads, download-all, and rendering.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DriveExplorer
  participant useDriveTreeData
  participant useDriveUploads
  participant DriveTreePane
  participant DriveTreeList
  participant FolderView

  DriveExplorer->>useDriveTreeData: compute flatRows, indexByPath
  DriveExplorer->>useDriveUploads: uploadIntoFolder, pendingUploadByPath
  DriveExplorer->>DriveTreePane: render tree pane with rows
  DriveTreePane->>DriveTreeList: pass flatRows, virtualizer, drop props
  DriveTreeList->>DriveExplorer: onSelect / onToggle callbacks
  DriveExplorer->>FolderView: render selected folder with entries
Loading

Possibly related PRs

  • Agenta-AI/agenta#5400: Prior work virtualizing the Drive Explorer tree and unifying the Files drawer, directly overlapping with the new tree/download-all wiring in this refactor.
  • Agenta-AI/agenta#5430: Both PRs wire partial-failure retry/alert behavior through DriveExplorer/DriveHeader, including partialErrored and retry controls.
  • Agenta-AI/agenta#5459: Overlaps with staged/mount-backed upload implementation and its wiring into DriveExplorer, DriveTreeList, and pending tile components.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main refactor: splitting DriveExplorer into hooks and view modules.
Description check ✅ Passed The description accurately summarizes the refactor and the extracted hooks, views, and helpers.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe-chore/drive-explorer-split

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

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-90a0.up.railway.app/w
Project agenta-oss-pr-5570
Image tag pr-5570-7304e25
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-07-30T23:04:20.059Z

@ardaerzin
ardaerzin changed the base branch from release/v0.106.1 to release/v0.106.2 July 30, 2026 18:08
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 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: 6

🧹 Nitpick comments (7)
web/oss/src/components/Drives/useDriveSelection.ts (1)

51-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an explicit eslint-disable-next-line react-hooks/exhaustive-deps.

select is intentionally omitted so the effect only reacts to prop changes; without the disable comment pnpm lint-fix will either flag it or "fix" it by adding select, silently changing behavior.

♻️ Suggested change
     useEffect(() => {
         if (initialPath != null) select(initialPath)
+        // eslint-disable-next-line react-hooks/exhaustive-deps -- prop-change only, must not refire on select identity
     }, [initialPath])
web/oss/src/components/Drives/useDriveUploads.ts (1)

50-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the as unknown as MountFile double cast.

This silently accepts any future required field on MountFile. Build the object typed (or widen the tree-build input to Pick<MountFile, "path" | "size">) so the compiler keeps verifying the synthetic nodes.

web/oss/src/components/Drives/DrivePendingTiles.tsx (1)

56-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer theme tokens over hard-coded rgba(0,0,0,…) overlays.

The retry pill, progress track, and remove button use raw color literals. Swapping to antd semantic token variables (e.g. var(--ant-color-bg-mask)) keeps light/dark consistent with the rest of the drive UI.

As per coding guidelines, "Consume theme colors through Ant Design semantic tokens, Tailwind color utilities, or supported var(--ag-color*) variables; do not use raw hex colors or --ag-c-* literals."

Source: Coding guidelines

web/oss/src/components/Drives/FolderView.tsx (1)

254-300: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Deduplicate the staged/upload tile wrapper and stabilize renderTile.

The two motion.div wrappers (Lines 260-274 and 281-299) are identical except for the child; extract one <PendingTileMotion> wrapper. Also, renderTile is a fresh closure on every render, so VirtualTileGrid re-renders every cell on any parent state change (e.g. repoExpanded) — wrap it in useCallback or lift the tile into a memoized component.

As per coding guidelines, "Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects, especially in lists."

Source: Coding guidelines

web/oss/src/components/Drives/DriveTreeRow.tsx (1)

73-81: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Raw Ant Design CSS variable used for the primary-bg tint in two places. Both sites hardcode bg-[var(--ant-color-primary-bg)] instead of a sanctioned theme channel (Ant Design semantic token, Tailwind color utility, or var(--ag-color*)), so the tint bypasses the project's token pipeline and its light/dark verification.

  • web/oss/src/components/Drives/DriveTreeRow.tsx#L73-L81: replace the pending-upload tint with the semantic Tailwind utility (e.g. bg-colorPrimaryBg).
  • web/oss/src/components/Drives/DriveTreeList.tsx#L100-L108: replace the drop-hover tint with the same utility so both row states stay in sync.

As per coding guidelines: "Consume theme colors through Ant Design semantic tokens, Tailwind color utilities, or supported var(--ag-color*) variables; do not use raw hex colors or --ag-c-* literals."

Source: Coding guidelines

web/oss/src/components/Drives/DriveTreeList.tsx (1)

148-155: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Hoist the toggle closure out of the row map.

A fresh onToggle (and the setExpanded updater) is allocated per visible row on every render, defeating any future React.memo on TreeRow. A single useCallback((path: string) => setExpanded(...)) above the return would be reused by all rows.

As per coding guidelines: "Minimize React re-renders with useMemo, useCallback, and React.memo where appropriate; avoid unstable inline functions and objects, especially in lists."

Source: Coding guidelines

web/oss/src/components/Drives/DriveTreePane.tsx (1)

77-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Separator is mouse-only.

role="separator" with pointer handlers but no tabIndex={0}, arrow-key handling, or aria-valuenow/aria-valuemin/aria-valuemax makes the resize unreachable by keyboard and unannounced. The toolbar toggle keeps the pane usable, so this is an enhancement rather than a blocker.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e680dee-ce45-4f37-ac4b-a92f69f7a51e

📥 Commits

Reviewing files that changed from the base of the PR and between a0ef2cc and bbb64a8.

📒 Files selected for processing (27)
  • web/oss/src/components/Drives/DriveBreadcrumb.tsx
  • web/oss/src/components/Drives/DriveExplorer.tsx
  • web/oss/src/components/Drives/DriveExplorerStates.tsx
  • web/oss/src/components/Drives/DriveFileContentViewer.tsx
  • web/oss/src/components/Drives/DriveFilePreview.tsx
  • web/oss/src/components/Drives/DriveHeader.tsx
  • web/oss/src/components/Drives/DrivePendingTiles.tsx
  • web/oss/src/components/Drives/DriveToolbar.tsx
  • web/oss/src/components/Drives/DriveTreeList.tsx
  • web/oss/src/components/Drives/DriveTreePane.tsx
  • web/oss/src/components/Drives/DriveTreeRow.tsx
  • web/oss/src/components/Drives/FolderTile.tsx
  • web/oss/src/components/Drives/FolderView.tsx
  • web/oss/src/components/Drives/driveMotion.ts
  • web/oss/src/components/Drives/driveTreeView.ts
  • web/oss/src/components/Drives/driveTypes.ts
  • web/oss/src/components/Drives/useDelayedTrue.ts
  • web/oss/src/components/Drives/useDriveDownloadAll.ts
  • web/oss/src/components/Drives/useDriveFilters.ts
  • web/oss/src/components/Drives/useDriveSelection.ts
  • web/oss/src/components/Drives/useDriveTreeData.ts
  • web/oss/src/components/Drives/useDriveTreeKeyboard.ts
  • web/oss/src/components/Drives/useDriveTreePane.ts
  • web/oss/src/components/Drives/useDriveTreeReveal.ts
  • web/oss/src/components/Drives/useDriveTreeViewport.ts
  • web/oss/src/components/Drives/useDriveUploads.ts
  • web/oss/src/components/Drives/useTreeGroupScroll.ts

Comment on lines +58 to +69
export const DriveFileDownloadButton = ({mount, path}: {mount: Mount | null; path: string}) => {
const projectId = useAtomValue(projectIdAtom)
return (
<Button
icon={<DownloadSimple size={13} />}
disabled={!mount}
onClick={() => void downloadMountFile({mount, path, projectId})}
>
Download
</Button>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Download failures are silent.

downloadMountFile resolves false when the blob fetch fails (web/oss/src/components/Drives/driveMedia.ts lines 194-215), but the result is discarded — the user gets no feedback and no loading state, so a failed click looks identical to a successful one. Consider a loading flag plus an error toast, matching useDriveDownloadAll's toast reporting.

Comment on lines +103 to +120
const overflow: MenuProps["items"] = [
...ids.map((id) => ({
key: id.key,
label: (
<div className="flex flex-col gap-0.5 py-0.5">
<span className="text-xs font-medium">Copy {id.label}</span>
<span className="font-mono text-[10px] text-colorTextTertiary">{id.value}</span>
</div>
),
})),
{type: "divider" as const},
{
key: "download-all",
label: downloadingAll ? "Preparing download…" : "Download all",
icon: <DownloadSimple size={14} />,
disabled: !onDownloadAll || downloadingAll,
},
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Stray leading divider when ids is empty.

DriveExplorer passes driveIds ?? [], so for hosts without drive ids the overflow menu starts with a divider above "Download all".

🎨 Proposed fix
     const overflow: MenuProps["items"] = [
         ...ids.map((id) => ({
             key: id.key,
             label: (
                 <div className="flex flex-col gap-0.5 py-0.5">
                     <span className="text-xs font-medium">Copy {id.label}</span>
                     <span className="font-mono text-[10px] text-colorTextTertiary">{id.value}</span>
                 </div>
             ),
         })),
-        {type: "divider" as const},
+        ...(ids.length ? [{type: "divider" as const}] : []),
         {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const overflow: MenuProps["items"] = [
...ids.map((id) => ({
key: id.key,
label: (
<div className="flex flex-col gap-0.5 py-0.5">
<span className="text-xs font-medium">Copy {id.label}</span>
<span className="font-mono text-[10px] text-colorTextTertiary">{id.value}</span>
</div>
),
})),
{type: "divider" as const},
{
key: "download-all",
label: downloadingAll ? "Preparing download…" : "Download all",
icon: <DownloadSimple size={14} />,
disabled: !onDownloadAll || downloadingAll,
},
]
const overflow: MenuProps["items"] = [
...ids.map((id) => ({
key: id.key,
label: (
<div className="flex flex-col gap-0.5 py-0.5">
<span className="text-xs font-medium">Copy {id.label}</span>
<span className="font-mono text-[10px] text-colorTextTertiary">{id.value}</span>
</div>
),
})),
...(ids.length ? [{type: "divider" as const}] : []),
{
key: "download-all",
label: downloadingAll ? "Preparing download…" : "Download all",
icon: <DownloadSimple size={14} />,
disabled: !onDownloadAll || downloadingAll,
},
]

Comment on lines +107 to +122
<Tooltip title={showHidden ? "Hide hidden files" : "Show hidden files"}>
<Button
type="text"
aria-label={showHidden ? "Hide hidden files" : "Show hidden files"}
aria-pressed={!showHidden}
icon={
showHidden ? (
<Eye size={16} className="block" />
) : (
<EyeClosed size={16} className="block" />
)
}
onClick={() => setShowHidden((v) => !v)}
className={showHidden ? "!text-colorTextTertiary" : "!text-colorPrimary"}
/>
</Tooltip>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

aria-pressed is inverted on the hidden-files toggle.

The button toggles showHidden, so pressed state should be showHidden — not !showHidden. As written, screen readers announce the toggle as off exactly when hidden files are shown. Note the git-ignored toggle right below uses the non-inverted form, so the two controls report opposite conventions. Pairing an action-verb aria-label with aria-pressed also compounds the ambiguity; a static label reads better.

♿ Proposed fix
             <Button
                 type="text"
-                aria-label={showHidden ? "Hide hidden files" : "Show hidden files"}
-                aria-pressed={!showHidden}
+                aria-label="Show hidden files"
+                aria-pressed={showHidden}
                 icon={
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Tooltip title={showHidden ? "Hide hidden files" : "Show hidden files"}>
<Button
type="text"
aria-label={showHidden ? "Hide hidden files" : "Show hidden files"}
aria-pressed={!showHidden}
icon={
showHidden ? (
<Eye size={16} className="block" />
) : (
<EyeClosed size={16} className="block" />
)
}
onClick={() => setShowHidden((v) => !v)}
className={showHidden ? "!text-colorTextTertiary" : "!text-colorPrimary"}
/>
</Tooltip>
<Tooltip title={showHidden ? "Hide hidden files" : "Show hidden files"}>
<Button
type="text"
aria-label="Show hidden files"
aria-pressed={showHidden}
icon={
showHidden ? (
<Eye size={16} className="block" />
) : (
<EyeClosed size={16} className="block" />
)
}
onClick={() => setShowHidden((v) => !v)}
className={showHidden ? "!text-colorTextTertiary" : "!text-colorPrimary"}
/>
</Tooltip>

Comment on lines +34 to +48
const handleDownloadAll = useCallback(async () => {
if (!archiveMounts.length || downloadingAll) return
setDownloadingAll(true)
const key = "drive-download-all"
message.open({type: "loading", key, content: "Preparing download…", duration: 0})
const result = await downloadMountArchive({
mounts: archiveMounts,
projectId,
filename: `${driveRootLabel(drive.mount)}-files.zip`,
})
if (result.cancelled) message.destroy(key)
else if (result.ok) message.open({type: "success", key, content: "Download ready"})
else message.open({type: "error", key, content: result.error ?? "Download failed"})
setDownloadingAll(false)
}, [archiveMounts, drive.mount, projectId, downloadingAll, message])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap the await in try/finally — a throw permanently wedges the button and leaves the toast up.

downloadMountArchive is not exception-free: in the streaming path await handle.createWritable() sits outside its inner try (web/oss/src/components/Drives/driveMedia.ts lines 225-308), so a rejection propagates here. downloadingAll then stays true forever (guard at Line 35 blocks all retries) and the duration: 0 loading toast never closes.

🛡️ Proposed fix
-        const result = await downloadMountArchive({
-            mounts: archiveMounts,
-            projectId,
-            filename: `${driveRootLabel(drive.mount)}-files.zip`,
-        })
-        if (result.cancelled) message.destroy(key)
-        else if (result.ok) message.open({type: "success", key, content: "Download ready"})
-        else message.open({type: "error", key, content: result.error ?? "Download failed"})
-        setDownloadingAll(false)
+        try {
+            const result = await downloadMountArchive({
+                mounts: archiveMounts,
+                projectId,
+                filename: `${driveRootLabel(drive.mount)}-files.zip`,
+            })
+            if (result.cancelled) message.destroy(key)
+            else if (result.ok) message.open({type: "success", key, content: "Download ready"})
+            else message.open({type: "error", key, content: result.error ?? "Download failed"})
+        } catch {
+            message.open({type: "error", key, content: "Download failed"})
+        } finally {
+            setDownloadingAll(false)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDownloadAll = useCallback(async () => {
if (!archiveMounts.length || downloadingAll) return
setDownloadingAll(true)
const key = "drive-download-all"
message.open({type: "loading", key, content: "Preparing download…", duration: 0})
const result = await downloadMountArchive({
mounts: archiveMounts,
projectId,
filename: `${driveRootLabel(drive.mount)}-files.zip`,
})
if (result.cancelled) message.destroy(key)
else if (result.ok) message.open({type: "success", key, content: "Download ready"})
else message.open({type: "error", key, content: result.error ?? "Download failed"})
setDownloadingAll(false)
}, [archiveMounts, drive.mount, projectId, downloadingAll, message])
const handleDownloadAll = useCallback(async () => {
if (!archiveMounts.length || downloadingAll) return
setDownloadingAll(true)
const key = "drive-download-all"
message.open({type: "loading", key, content: "Preparing download…", duration: 0})
try {
const result = await downloadMountArchive({
mounts: archiveMounts,
projectId,
filename: `${driveRootLabel(drive.mount)}-files.zip`,
})
if (result.cancelled) message.destroy(key)
else if (result.ok) message.open({type: "success", key, content: "Download ready"})
else message.open({type: "error", key, content: result.error ?? "Download failed"})
} catch {
message.open({type: "error", key, content: "Download failed"})
} finally {
setDownloadingAll(false)
}
}, [archiveMounts, drive.mount, projectId, downloadingAll, message])

Comment on lines +29 to +36
const [persistedSelection, setPersistedSelection] = useAtom(driveSelectionAtomFamily(mountId))
const [selectedPath, setSelectedPath] = useState<string | null>(
() => initialPath ?? persistedSelection ?? null,
)
const [expanded, setExpanded] = useState<Set<string>>(() => {
const init = initialPath ?? persistedSelection ?? null
return new Set(init ? ancestorPaths(init) : [])
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Is drive.mount available synchronously on first render, or async?
fd useSessionDrive --exec cat -n {}

Repository: Agenta-AI/agenta

Length of output: 32406


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="web/oss/src/components/Drives/useDriveSelection.ts"
if [ -f "$file" ]; then
  echo "== useDriveSelection.ts outline =="
  ast-grep outline "$file" || true
  echo "== useDriveSelection.ts lines =="
  cat -n "$file"
fi

echo "== DriveExplorer mountId usage =="
fd DriveExplorer.tsx --exec sh -c 'echo "--- $1"; cat -n "$1" | sed -n "70,120p"' sh {}

echo "== driveSelectionAtomFamily usages =="
rg -n "driveSelectionAtomFamily|driveSelection|mountId" web/oss/src/components/Drives web/oss/src -S

Repository: Agenta-AI/agenta

Length of output: 21460


Gate drive selection persistence on a real mountId.

DriveExplorer passes mountId: drive.mount?.id ?? "", while useSessionDrive can render with drive.mount === null until mount discovery completes. On async mount loading, useDriveSelection seeds selectedPath/expanded from the empty mount's atom, then the root-landing effect persists "", so the drawer re-opens at root instead of using the last resolved mount’s selection. Avoid seeding or writing "" persistence until a non-empty mountId is available, or gate the hook until the mount resolves.

Comment on lines +44 to +51
const activePaths = useMemo(() => {
const set = new Set<string>([""])
for (const p of expanded) set.add(p)
if (selectedPath && !/\.[a-z0-9]{1,8}$/i.test(selectedPath.split("/").pop() ?? "")) {
set.add(selectedPath)
}
return [...set]
}, [expanded, selectedPath])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how selectedPath/isFolder is determined upstream, and how useLazyDriveTree
# handles a non-directory path in activePaths.
rg -n "isFolder" web/oss/src/components/Drives/useDriveSelection.ts web/oss/src/components/Drives/useLazyDriveTree.ts -A3 -B3

Repository: Agenta-AI/agenta

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching Drive tree components:\n'
fd -i 'useDriveTree|DriveTree|Drives' . || true

printf '\nRepository files under web/oss/src/components (top-level):\n'
git ls-files 'web/oss/src/components/' | sed 's#^`#/`#' | sort | sed 's#[^/]*##g' | uniq -c | sort -n | tail -50

printf '\nSearch relevant symbols:\n'
rg -n "useDriveTreeData|activePaths|useLazyDriveTree|isFolder|selectedPath" web/oss/src/components web/packages web/src 2>/dev/null | head -200

Repository: Agenta-AI/agenta

Length of output: 22167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'useDriveTreeData lines 1-120:\n'
sed -n '1,120p' web/oss/src/components/Drives/useDriveTreeData.ts

printf '\nuseLazyDriveTree outline and relevant lines 79-180:\n'
ast-grep outline web/oss/src/components/Drives/useLazyDriveTree.tsx --view expanded || true
sed -n '70,180p' web/oss/src/components/Drives/useLazyDriveTree.tsx

printf '\nDriveExplorer selected handling around render/loading:\n'
sed -n '238,262p'(web/oss/src/components/Drives/DriveExplorer.tsx) 2>/dev/null || sed -n '238,262p' web/oss/src/components/Drives/DriveExplorer.tsx

Repository: Agenta-AI/agenta

Length of output: 11772


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'DriveExplorer selected rendering/loading:\n'
sed -n '238,262p' web/oss/src/components/Drives/DriveExplorer.tsx

printf '\nDriveExplorer selected data props:\n'
sed -n '150,220p' web/oss/src/components/Drives/DriveExplorer.tsx

printf '\nDirSubscriber implementation:\n'
sed -n '24,62p' web/oss/src/components/Drives/useLazyDriveTree.tsx

printf '\nActive behavior probe for extension heuristic:\n'
python3 - <<'PY'
import re
def active(selected):
    set_ = {""}
    if selected and not re.search(r"\.[a-z0-9]{1,8}$", (selected.split("/")[-1] or ""), re.I):
        set_.add(selected)
    return selected in set_
for name in ["README", "Dockerfile", ".gitignore", ".env", "LICENSE", "App.app", "Project.xcodeproj", "Framework.framework", "v1.2"]:
    print(f"{name}: active={active(name)}")
PY

Repository: Agenta-AI/agenta

Length of output: 6528


Use a reliable folder signal for selected paths.

activePaths currently skips folders whose names end with .app/.xcodeproj/.framework and keeps extensionless files, and DriveExplorer only shows children when selectedNode?.isFolder === true. Since the lazy subscriber is keyed only by active paths, select-extension files issue a directory query unnecessarily, while dot-suffixed folders fail to load their directory at all and render an empty file preview with selectedPath !== "" && !lazyTree.loadedDirs.has(selectedPath). Prefer passing isFolder upstream or building activePaths from selectedNode?.isFolder.

Report single-file download failures. Four call sites dropped
downloadMountFile's `false`, so a failed click looked identical to a
successful one. Lift the one correct implementation (the context menu's)
into useDriveFileDownload, route every site through it, give the shared
DriveFileDownloadButton a loading state, drop DriveHeader's duplicate
button, and un-export downloadMountFile so the pattern can't return.

Stop downloadMountArchive from throwing past its result contract:
handle.createWritable() sat outside the try, so a refused write
permission escaped every caller's result handling — stranding the
download-all button (its in-flight flag gates retries) and the folder-zip
toast. Also reset the flag in `finally`.

Key drive selection persistence on a real mount id. drive.mount resolves
async, so early renders keyed the selection atom on "" — a slot shared by
every mountless drive — and the root-landing effect wrote to it. Read ""
as empty, never write it, adopt the drive's persisted selection (or flush
a pre-mount pick) once the id lands, and stop auto-landing on root from
overwriting persistence. The local-file drive has no mount at all, so the
gate is on persistence, not on selection.

Derive the selection's folder-ness from the backend's is_folder instead
of its name. The name guess made extensionless files (LICENSE,
Dockerfile) issue a needless directory query; it now covers only the cold
window before any level lands, corrected during render so a wrong guess
never mounts a subscriber. The heuristic itself is one shared helper —
DriveExplorer's skeleton held a second, divergent copy.

Fix the inverted aria-pressed on the hidden-files toggle and align all
three toolbar toggles on one convention: a static label naming the
control, aria-pressed carrying the state, dynamic wording in the tooltip.
Drop the overflow menu's leading divider when there are no drive ids.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend refactoring A code change that neither fixes a bug nor adds a feature size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant