[chore] Split DriveExplorer into hooks and views - #5570
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughDriveExplorer.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. ChangesDrive Explorer Refactor
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
Railway Preview Environment
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (7)
web/oss/src/components/Drives/useDriveSelection.ts (1)
51-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an explicit
eslint-disable-next-line react-hooks/exhaustive-deps.
selectis intentionally omitted so the effect only reacts to prop changes; without the disable commentpnpm lint-fixwill either flag it or "fix" it by addingselect, 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 winAvoid the
as unknown as MountFiledouble cast.This silently accepts any future required field on
MountFile. Build the object typed (or widen the tree-build input toPick<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 valuePrefer 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 winDeduplicate the staged/upload tile wrapper and stabilize
renderTile.The two
motion.divwrappers (Lines 260-274 and 281-299) are identical except for the child; extract one<PendingTileMotion>wrapper. Also,renderTileis a fresh closure on every render, soVirtualTileGridre-renders every cell on any parent state change (e.g.repoExpanded) — wrap it inuseCallbackor lift the tile into a memoized component.As per coding guidelines, "Minimize React re-renders with
useMemo,useCallback, andReact.memowhere 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 valueRaw 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, orvar(--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 valueHoist the toggle closure out of the row map.
A fresh
onToggle(and thesetExpandedupdater) is allocated per visible row on every render, defeating any futureReact.memoonTreeRow. A singleuseCallback((path: string) => setExpanded(...))above the return would be reused by all rows.As per coding guidelines: "Minimize React re-renders with
useMemo,useCallback, andReact.memowhere 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 valueSeparator is mouse-only.
role="separator"with pointer handlers but notabIndex={0}, arrow-key handling, oraria-valuenow/aria-valuemin/aria-valuemaxmakes 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
📒 Files selected for processing (27)
web/oss/src/components/Drives/DriveBreadcrumb.tsxweb/oss/src/components/Drives/DriveExplorer.tsxweb/oss/src/components/Drives/DriveExplorerStates.tsxweb/oss/src/components/Drives/DriveFileContentViewer.tsxweb/oss/src/components/Drives/DriveFilePreview.tsxweb/oss/src/components/Drives/DriveHeader.tsxweb/oss/src/components/Drives/DrivePendingTiles.tsxweb/oss/src/components/Drives/DriveToolbar.tsxweb/oss/src/components/Drives/DriveTreeList.tsxweb/oss/src/components/Drives/DriveTreePane.tsxweb/oss/src/components/Drives/DriveTreeRow.tsxweb/oss/src/components/Drives/FolderTile.tsxweb/oss/src/components/Drives/FolderView.tsxweb/oss/src/components/Drives/driveMotion.tsweb/oss/src/components/Drives/driveTreeView.tsweb/oss/src/components/Drives/driveTypes.tsweb/oss/src/components/Drives/useDelayedTrue.tsweb/oss/src/components/Drives/useDriveDownloadAll.tsweb/oss/src/components/Drives/useDriveFilters.tsweb/oss/src/components/Drives/useDriveSelection.tsweb/oss/src/components/Drives/useDriveTreeData.tsweb/oss/src/components/Drives/useDriveTreeKeyboard.tsweb/oss/src/components/Drives/useDriveTreePane.tsweb/oss/src/components/Drives/useDriveTreeReveal.tsweb/oss/src/components/Drives/useDriveTreeViewport.tsweb/oss/src/components/Drives/useDriveUploads.tsweb/oss/src/components/Drives/useTreeGroupScroll.ts
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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, | ||
| }, | ||
| ] |
There was a problem hiding this comment.
🎯 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.
| 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, | |
| }, | |
| ] |
| <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> |
There was a problem hiding this comment.
🎯 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.
| <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> |
| 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]) |
There was a problem hiding this comment.
🩺 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.
| 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]) |
| 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) : []) | ||
| }) |
There was a problem hiding this comment.
🎯 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 -SRepository: 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.
| 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]) |
There was a problem hiding this comment.
🎯 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 -B3Repository: 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 -200Repository: 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.tsxRepository: 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)}")
PYRepository: 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.
Context
DriveExplorer.tsxhad 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.DriveExplorer.tsxis 409 lines.Shared refs stay inside the hook that owns them. The tree scroll element never leaves
useDriveTreeViewport(group scroll attaches throughattachTreeWheel(el)), and the group maps never leaveuseTreeGroupScroll(rows readscrollXFor(parent)).Ordering matters for uploads and is preserved.
useDriveUploadsis called above the tree so itsuploadFilesfeedbuildDriveTree. That is what makes an in-flight upload render as a real row under its destination folder instead of a separate pinned list.pendingUploadByPaththen decorates the resulting rows.currentFolderandcommitStagedstay in the root on purpose. They read the selection, which derives from the tree, which consumesuseDriveUploads. Moving them into the hook would be circular, and keeping them out preserved their dependency arrays exactly.Tests
pnpm --filter @agenta/oss exec tsc --noEmitpasses with zero errors, as do@agenta/entity-uiand@agenta/ee.pnpm lint-fixacross the repo makes no changes. Prettier clean.release/v0.106.1rather 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, andNEXT_PUBLIC_AGENT_FILE_UPLOADSstill 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