Skip to content

feat(frontend): @agenta/entities/drive — the drive's headless layer leaves the app - #5876

Draft
ardaerzin wants to merge 1 commit into
pkg/observabilityfrom
pkg/entities-drive
Draft

feat(frontend): @agenta/entities/drive — the drive's headless layer leaves the app#5876
ardaerzin wants to merge 1 commit into
pkg/observabilityfrom
pkg/entities-drive

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

The drive's queries, mutations, selectors and types move to @agenta/entities/drive. The UI
follows in the next lane; splitting them keeps each diff readable.
Not run in a browser — static gates only (pnpm lint-fix 24/24, tsc --noEmit clean
for @agenta/shared, ui, entities, entity-ui, settings-ui, oss, ee, mobile).

Stacked on pkg/observability; review only this lane's diff.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Error Error Aug 11, 2026 5:10am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

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: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b45a3867-d72c-46f0-8960-f62ba6c65538

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
    • Added Drive support for browsing files and folders with searchable, filterable, expandable trees.
    • Added drag-and-drop uploads, staged file handling, image previews, and PDF thumbnails.
    • Added file-type labels, sizes, timestamps, recent-change indicators, and hidden/internal file handling.
    • Added persistent selections, resizable tree panes, smooth transitions, and loading states.
  • Accessibility
    • Added keyboard navigation for file lists and folder trees, including Home/End and arrow-key controls.
  • Configuration
    • Added a feature flag for enabling or disabling agent file uploads.

Walkthrough

The PR centralizes Drive utilities in @agenta/entities. It adds tree management, selection, navigation, drag-and-drop uploads, file previews, PDF thumbnails, motion utilities, feature flags, and a public ./drive package export.

Changes

Drive package centralization

Layer / File(s) Summary
Package contracts and shared integrations
web/packages/agenta-entities/package.json, web/packages/agenta-entities/src/drive/index.ts, web/packages/agenta-entities/src/drive/configDrive.ts, web/packages/agenta-entities/src/drive/driveMedia.ts, web/packages/agenta-entities/src/drive/*.ts, web/packages/agenta-entities/tests/unit/dropEntries.test.ts
The package exposes the Drive barrel and adds required dependencies. Shared state, API, and authentication imports replace local utilities. useConfigDrive accepts sessionId directly, and downloadMountFile becomes public.
Drive tree data and selection
web/packages/agenta-entities/src/drive/driveTree.ts, web/packages/agenta-entities/src/drive/driveTreeView.ts, web/packages/agenta-entities/src/drive/useDriveSelection.ts, web/packages/agenta-entities/src/drive/useDriveFilters.ts
The PR adds tree construction, filtering, statistics, path helpers, loading rows, persisted per-mount selection, and ancestor expansion.
Tree navigation and viewport behavior
web/packages/agenta-entities/src/drive/driveKeyboard.ts, web/packages/agenta-entities/src/drive/useDriveTreeKeyboard.ts, web/packages/agenta-entities/src/drive/useDriveTreeViewport.ts, web/packages/agenta-entities/src/drive/useDriveTreeReveal.ts, web/packages/agenta-entities/src/drive/useDriveTreePane.ts, web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts
The PR adds keyboard navigation, virtualized row focusing, selected-path reveal, pane resizing and animation, and horizontal group scrolling.
File staging and drag-and-drop
web/packages/agenta-entities/src/drive/dropEntries.ts, web/packages/agenta-entities/src/drive/useDriveDrop.ts, web/packages/agenta-entities/src/drive/useImagePreviews.ts, web/packages/agenta-entities/src/drive/pdfThumb.ts, web/packages/agenta-entities/src/drive/driveFlags.ts, web/packages/agenta-entities/src/drive/driveTypes.ts
The PR adds recursive directory-drop handling, upload and staging hooks, image object URL management, PDF first-page rendering, staged file types, and an environment-backed upload flag.
Drive labels, kinds, motion, and recency
web/packages/agenta-entities/src/drive/driveKinds.ts, web/packages/agenta-entities/src/drive/driveLabels.ts, web/packages/agenta-entities/src/drive/driveMotion.ts, web/packages/agenta-entities/src/drive/recentChange.ts, web/packages/agenta-entities/src/drive/useDelayedTrue.ts
The PR adds file-kind and mount-label helpers, shared animation variants, recent-change timing, and delayed activation behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant useDriveDrop
  participant dropEntries
  participant DriveUpload
  Browser->>useDriveDrop: drop files or directories
  useDriveDrop->>dropEntries: readDroppedFiles(dataTransfer)
  dropEntries-->>useDriveDrop: return relative-path files
  useDriveDrop->>DriveUpload: upload non-empty file list
Loading

Possibly related PRs

  • Agenta-AI/agenta#5306: Introduces overlapping Drive utilities that this PR centralizes in @agenta/entities.
  • Agenta-AI/agenta#5459: Relates to the Drive upload, drag-and-drop, preview, and staging utilities.
  • Agenta-AI/agenta#5628: Relates to shared drag-and-drop handling and recursive dropped-directory extraction.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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 clearly identifies the drive headless layer moving to @agenta/entities/drive.
Description check ✅ Passed The description accurately explains the drive logic migration, planned UI follow-up, review scope, and validation status.
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 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pkg/entities-drive

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

❤️ Share

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

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

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.

@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 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: 11

🧹 Nitpick comments (12)
web/packages/agenta-entities/src/drive/useDelayedTrue.ts (1)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce this to one short comment line.

This comment documents normal hook behavior. Keep a longer comment only for a surprising constraint.

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/packages/agenta-entities/src/drive/dropEntries.ts (1)

74-76: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider traversing children concurrently.

The loop awaits each child before it starts the next one. For a deep folder drop, each file also costs one entry.file callback round-trip, so total latency grows with file count. Promise.all keeps the same output order and removes the serialization.

♻️ Optional refactor
-    const collected: DroppedFile[] = []
-    for (const child of children) collected.push(...(await collectDropEntry(child, dirPrefix)))
-    return collected
+    const lists = await Promise.all(children.map((child) => collectDropEntry(child, dirPrefix)))
+    return lists.flat()
web/packages/agenta-entities/src/drive/useDriveDrop.ts (1)

183-204: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stabilize the returned dropProps object and handlers.

useStageDrop builds a new dropProps object with new inline handlers on every render. Hosts that spread these props onto a memoized child re-render on every parent render. The coding guidelines require memoized handlers and objects for props.

Wrap the handlers in useCallback and the returned object in useMemo. Keep the hook call order stable by computing the disabled result after the hooks run.

♻️ Proposed refactor sketch
 export function useStageDrop(
     onFiles: ((files: DroppedFile[]) => void) | false | null | undefined,
 ): {
     dropActive: boolean
     dropProps: FileDropProps
 } {
     const [dropActive, setDropActive] = useState(false)
-    if (!onFiles) return {dropActive: false, dropProps: {}}
-    return {
-        dropActive,
-        dropProps: {
-            onDragOver: (e) => {
-                if (!isFileDrag(e)) return
-                e.preventDefault()
-                setDropActive(true)
-            },
-            onDragLeave: () => setDropActive(false),
-            onDrop: (e) => {
-                if (!isFileDrag(e)) return
-                e.preventDefault()
-                setDropActive(false)
-                void readDroppedFiles(e.dataTransfer).then((files) => {
-                    if (files.length) onFiles(files)
-                })
-            },
-        },
-    }
+    const onDragOver = useCallback((e: React.DragEvent<HTMLElement>) => {
+        if (!isFileDrag(e)) return
+        e.preventDefault()
+        setDropActive(true)
+    }, [])
+    const onDragLeave = useCallback(() => setDropActive(false), [])
+    const onDrop = useCallback(
+        (e: React.DragEvent<HTMLElement>) => {
+            if (!isFileDrag(e)) return
+            e.preventDefault()
+            setDropActive(false)
+            if (!onFiles) return
+            void readDroppedFiles(e.dataTransfer).then((files) => {
+                if (files.length) onFiles(files)
+            })
+        },
+        [onFiles],
+    )
+    return useMemo(
+        () =>
+            onFiles
+                ? {dropActive, dropProps: {onDragOver, onDragLeave, onDrop}}
+                : {dropActive: false, dropProps: {}},
+        [onFiles, dropActive, onDragOver, onDragLeave, onDrop],
+    )
 }

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

Source: Coding guidelines

web/packages/agenta-entities/src/drive/useImagePreviews.ts (1)

27-46: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a [files] dependency for the reconcile effect.

Both call sites pass a memoized array (useDriveUploads.ts line 101 and useMountUpload.ts line 217). With [files] the reconcile runs only when the input array identity changes, and the behavior stays identical for callers that pass a fresh array each render. Today the filter, Set build, and Map scan run after every commit of the host.

♻️ Optional refactor
         if (changed) setSnapshot(new Map(map))
-    })
+    }, [files])
web/packages/agenta-entities/src/drive/pdfThumb.ts (1)

14-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reset the cached promise when the import fails.

pdfjsPromise keeps a rejected promise. If the dynamic chunk load fails once, for example on a transient network error, every later call to renderPdfFirstPage rejects and returns null for the rest of the session. Clear the cache on rejection so a later thumbnail request can retry.

♻️ Proposed resiliency fix
 async function loadPdfjs(): Promise<PdfjsModule> {
     if (!pdfjsPromise) {
-        pdfjsPromise = import("pdfjs-dist").then((pdfjs) => {
-            pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"
-            return pdfjs
-        })
+        pdfjsPromise = import("pdfjs-dist")
+            .then((pdfjs) => {
+                pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"
+                return pdfjs
+            })
+            .catch((error) => {
+                pdfjsPromise = null // a failed chunk load must not poison every later render
+                throw error
+            })
     }
     return pdfjsPromise
 }
web/packages/agenta-entities/src/drive/driveTree.ts (1)

190-198: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

applyLoadedCounts can understate a lazily loaded folder's item count.

applyLoadedCounts replaces the backend item_count with children.length for every folder that has at least one loaded child. If a level is only partially loaded, or if a filter pruned children, the row shows fewer items than the folder holds. The comment covers only the agent-files fold point.

Consider restricting the override to folders that are fully loaded, or to the fold-point mount only.

web/packages/agenta-entities/src/drive/useDriveSelection.ts (3)

88-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare the effect dependencies or silence the rule explicitly.

The effect calls select but lists only initialPath. react-hooks/exhaustive-deps reports this. The current omission is intentional, so record it in code instead of leaving a lint error. Add select to the deps with a ref guard on the previous initialPath, or add a targeted eslint-disable-next-line with the reason.

♻️ Proposed fix
     useEffect(() => {
         if (initialPath != null) select(initialPath)
+        // eslint-disable-next-line react-hooks/exhaustive-deps -- fire on initialPath changes only
     }, [initialPath])

As per coding guidelines: "Before committing frontend changes, run pnpm lint-fix from the web directory."

Source: Coding guidelines


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

Consider atomWithStorage for the per-drive last selection.

The last-viewed path per drive is a persisted UI preference. The current module-level atom family loses it on reload, and every visited mountId keeps an entry for the page lifetime. If cross-reload restore is wanted, use atomWithStorage with an agenta:-prefixed key and stringStorage for the nullable string. If it must stay session-scoped, state that in the comment.

As per coding guidelines: "Use atomWithStorage for persisted preferences, UI state, recently used items, and form drafts; prefix storage keys with agenta:. Use stringStorage for nullable strings."

Source: Coding guidelines


30-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the narrative block comments. These modules carry multi-line comments that explain design intent and history rather than a surprising constraint. The repository rule allows longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements. Keep the ones that record a real hazard, for example the ReDoS note in driveTree.ts at L23-L25, and move the rest into the module doc block or a design note.

  • web/packages/agenta-entities/src/drive/useDriveSelection.ts#L30-L33: reduce the empty-mountId rationale to one line.
  • web/packages/agenta-entities/src/drive/driveTree.ts#L75-L83: reduce the isFolderEntry heuristic comment to one line and keep the inference rule only.
  • web/packages/agenta-entities/src/drive/driveTreeView.ts#L16-L19: reduce the SKELETON_ROW_CAP explanation to one line.
  • web/packages/agenta-entities/src/drive/useDriveTreePane.ts#L63-L69: move the anticipated-shift explanation into the module doc block and keep one line here.
  • web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts#L17-L20: reduce the group-scroll model 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/packages/agenta-entities/src/drive/useDriveTreePane.ts (1)

29-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider keyboard support for the resize handle.

The pane width changes through pointer events only. Keyboard users cannot resize the pane. Add a role="separator" handle with aria-valuenow and arrow-key stepping between TREE_MIN and TREE_MAX when the UI lane lands.

web/packages/agenta-entities/src/drive/useDriveTreeReveal.ts (1)

28-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing setExpanded dependency.

The expansion effect uses setExpanded but lists only selectedPath and selectedIsFolder. setExpanded arrives as a prop, so react-hooks/exhaustive-deps reports it. Add it to the deps; the setter identity is stable, so behavior does not change.

♻️ Proposed fix
-    }, [selectedPath, selectedIsFolder])
+    }, [selectedPath, selectedIsFolder, setExpanded])

As per coding guidelines: "Before committing frontend changes, run pnpm lint-fix from the web directory."

Source: Coding guidelines

web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts (1)

66-69: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Throttle the re-render bump to one per frame.

bumpGroupScroll() runs on every wheel event that changes an offset. A trackpad emits many events per frame, so the tree re-renders more often than it paints. Coalesce the bump with requestAnimationFrame, and cancel the pending frame in the teardown.

As per coding guidelines: "debounce or throttle search, filter, scroll, and resize handlers."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 10f53913-a885-4522-996b-8b030cad3afd

📥 Commits

Reviewing files that changed from the base of the PR and between 26c16d8 and e2d62a8.

📒 Files selected for processing (31)
  • web/packages/agenta-entities/package.json
  • web/packages/agenta-entities/src/drive/agentDrive.ts
  • web/packages/agenta-entities/src/drive/configDrive.ts
  • web/packages/agenta-entities/src/drive/driveFlags.ts
  • web/packages/agenta-entities/src/drive/driveKeyboard.ts
  • web/packages/agenta-entities/src/drive/driveKinds.ts
  • web/packages/agenta-entities/src/drive/driveLabels.ts
  • web/packages/agenta-entities/src/drive/driveMedia.ts
  • web/packages/agenta-entities/src/drive/driveMotion.ts
  • web/packages/agenta-entities/src/drive/driveRepo.ts
  • web/packages/agenta-entities/src/drive/driveTree.ts
  • web/packages/agenta-entities/src/drive/driveTreeView.ts
  • web/packages/agenta-entities/src/drive/driveTypes.ts
  • web/packages/agenta-entities/src/drive/dropEntries.ts
  • web/packages/agenta-entities/src/drive/index.ts
  • web/packages/agenta-entities/src/drive/pdfThumb.ts
  • web/packages/agenta-entities/src/drive/recentChange.ts
  • web/packages/agenta-entities/src/drive/useDelayedTrue.ts
  • web/packages/agenta-entities/src/drive/useDriveDrop.ts
  • web/packages/agenta-entities/src/drive/useDriveFilters.ts
  • web/packages/agenta-entities/src/drive/useDriveSelection.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeKeyboard.ts
  • web/packages/agenta-entities/src/drive/useDriveTreePane.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeReveal.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeViewport.ts
  • web/packages/agenta-entities/src/drive/useDriveUploads.ts
  • web/packages/agenta-entities/src/drive/useImagePreviews.ts
  • web/packages/agenta-entities/src/drive/useMountUpload.ts
  • web/packages/agenta-entities/src/drive/useSessionDrive.ts
  • web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts
  • web/packages/agenta-entities/tests/unit/dropEntries.test.ts

Comment on lines +80 to +82
"motion": "^12.0.0",
"openapi-json-schema": "^2.0.0",
"pdfjs-dist": "^4.10.38",

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 | 🔴 Critical | ⚡ Quick win

Commit the lockfile for the new runtime dependencies.

package.json now declares motion and pdfjs-dist, but the pipeline reports that pnpm install --frozen-lockfile fails because pnpm-lock.yaml lacks both entries. Regenerate and commit the lockfile before merging this change.

Source: Pipeline failures


import {type Mount} from "@agenta/entities/session"
import {App} from "antd"
import {axios, getAgentaApiUrl, getAuthToken} from "@agenta/shared/api"

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'mounts/.*/files/download|mounts/files/export|axios\.(get|post)|fetch\(' \
  web/packages/agenta-entities/src/drive web/packages/agenta-sdk

Repository: Agenta-AI/agenta

Length of output: 7798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Resources file =="
wc -l web/packages/agenta-sdk/src/resources.ts
sed -n '1,220p' web/packages/agenta-sdk/src/resources.ts

echo "== Drive media relevant sections =="
sed -n '1,80p' web/packages/agenta-entities/src/drive/driveMedia.ts
sed -n '230,345p' web/packages/agenta-entities/src/drive/driveMedia.ts

echo "== API package exports/axios config snippets =="
sed -n '1,140p' web/packages/agenta-shared/src/api.ts
sed -n '1,100p' web/packages/agenta-sdk/src/config.ts

echo "== SDK config package file list =="
fd -a . web/packages/agenta-sdk/src -t f | sed -n '1,120p'

echo "== Search for raw axios/fetch and Fern resource accessor patterns in web packages =="
rg -n "(from \"`@agenta/sdk`\"|from '`@agenta/sdk`'|axios\.get|axios\.post|axios\.blob|fetch\(|queryParams:|responseType: \"blob\"|responseType: \"stream\")" web/packages -g '*.ts' -g '*.tsx' | sed -n '1,220p'

Repository: Agenta-AI/agenta

Length of output: 14665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate api/config files =="
fd -a '(^|/)api\.ts$|(^|/)config\.ts$|resources\.ts$' web/packages | sed -n '1,200p'

echo "== Shared api file =="
api_file="$(fd '^api\.ts$' web/packages -d 4 | grep shared | head -n 1)"
echo "${api_file:-NOT_FOUND}"
if [ -n "${api_file:-}" ]; then
  wc -l "$api_file"
  sed -n '1,160p' "$api_file"
fi

echo "== SDK config =="
config_file="$(fd 'config\.ts$' web/packages/agenta-sdk/src | head -n 1 || true)"
if [ -n "${config_file:-}" ]; then
  wc -l "$config_file"
  sed -n '1,140p' "$config_file"
fi

echo "== Mounts resource client definition/imports =="
rg -n "file.*download|files.*download|files/download|export|download|FetchBlobs|Blobs|Blob" \
  -S web/packages -g '*resource*' -g '*.ts' | sed -n '1,240p'

echo "== SDK package package.json refs =="
for f in web/packages/agenta-entities/package.json web/packages/agenta-sdk/package.json; do
  echo "--- $f"
  cat "$f"
done

echo "== MountsClient method names if declaration exists =="
tmp="$(mktemp -d)"
cd "$tmp"
set +e
npm view `@agentaai/api-client` 2>/dev/null | sed -n '1,160p'
set -e

Repository: Agenta-AI/agenta

Length of output: 1408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All repository files named api.ts =="
git ls-files '*api.ts' | sed -n '1,200p'

echo "== File under review imports =="
sed -n '1,20p' web/packages/agenta-entities/src/drive/driveMedia.ts

echo "== Repository-wide imports of `@agenta/shared/api` =="
rg -n '`@agenta/shared/api`|shared/api' -S . | sed -n '1,200p'

echo "== SDK build config package names =="
sed -n '1,220p' web/packages/agenta-sdk/src/config.ts
sed -n '1,120p' web/packages/agenta-sdk/package.json

echo "== Mount route and mount client route references =="
rg -n 'files/download|files/export|downloadMountArchive|fetchMountFileBlob|MountsClient' web -S | sed -n '1,260p'

Repository: Agenta-AI/agenta

Length of output: 33288


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generated MountsClient get methods around files/download and export =="
sed -n '520,1010p' web/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.ts

echo "== MountsClient declarations for raw response methods =="
sed -n '1,70p' web/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.ts

echo "== check source package.json dependency version/workspace =="
sed -n '1,220p' web/packages/agenta-api-client/package.json

Repository: Agenta-AI/agenta

Length of output: 24664


Route mount binary downloads through @agenta/sdk/resources.

fetchMountFileBlob and downloadMountArchive still call raw axios/fetch paths at web/packages/agenta-entities/src/drive/driveMedia.ts:55, :277, and :304. The Fern MountsClient already exposes binary responses for downloadMountFile and exportMountFiles; add resource accessors in web/packages/agenta-sdk/src/routes.ts/resources.ts as needed and use them instead of bypassing the SDK.

Source: Coding guidelines

Comment on lines +19 to +20
/** Object URL for an image preview, else null (icon fallback). Owned by DriveExplorer. */
previewUrl: string | 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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the ownership note for previewUrl.

The object URL lifecycle now belongs to useImagePreviews, which mints and revokes the URL (web/packages/agenta-entities/src/drive/useImagePreviews.ts). useDriveUploads fills previewUrl from that hook. The reference to DriveExplorer points at the UI layer and is no longer accurate for this headless package.

📝 Proposed doc fix
-    /** Object URL for an image preview, else null (icon fallback). Owned by DriveExplorer. */
+    /** Object URL for an image preview, else null (icon fallback). Owned by `useImagePreviews`. */
📝 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
/** Object URL for an image preview, else null (icon fallback). Owned by DriveExplorer. */
previewUrl: string | null
/** Object URL for an image preview, else null (icon fallback). Owned by `useImagePreviews`. */
previewUrl: string | null

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 11

🧹 Nitpick comments (12)
web/packages/agenta-entities/src/drive/useDelayedTrue.ts (1)

3-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce this to one short comment line.

This comment documents normal hook behavior. Keep a longer comment only for a surprising constraint.

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/packages/agenta-entities/src/drive/dropEntries.ts (1)

74-76: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider traversing children concurrently.

The loop awaits each child before it starts the next one. For a deep folder drop, each file also costs one entry.file callback round-trip, so total latency grows with file count. Promise.all keeps the same output order and removes the serialization.

♻️ Optional refactor
-    const collected: DroppedFile[] = []
-    for (const child of children) collected.push(...(await collectDropEntry(child, dirPrefix)))
-    return collected
+    const lists = await Promise.all(children.map((child) => collectDropEntry(child, dirPrefix)))
+    return lists.flat()
web/packages/agenta-entities/src/drive/useDriveDrop.ts (1)

183-204: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stabilize the returned dropProps object and handlers.

useStageDrop builds a new dropProps object with new inline handlers on every render. Hosts that spread these props onto a memoized child re-render on every parent render. The coding guidelines require memoized handlers and objects for props.

Wrap the handlers in useCallback and the returned object in useMemo. Keep the hook call order stable by computing the disabled result after the hooks run.

♻️ Proposed refactor sketch
 export function useStageDrop(
     onFiles: ((files: DroppedFile[]) => void) | false | null | undefined,
 ): {
     dropActive: boolean
     dropProps: FileDropProps
 } {
     const [dropActive, setDropActive] = useState(false)
-    if (!onFiles) return {dropActive: false, dropProps: {}}
-    return {
-        dropActive,
-        dropProps: {
-            onDragOver: (e) => {
-                if (!isFileDrag(e)) return
-                e.preventDefault()
-                setDropActive(true)
-            },
-            onDragLeave: () => setDropActive(false),
-            onDrop: (e) => {
-                if (!isFileDrag(e)) return
-                e.preventDefault()
-                setDropActive(false)
-                void readDroppedFiles(e.dataTransfer).then((files) => {
-                    if (files.length) onFiles(files)
-                })
-            },
-        },
-    }
+    const onDragOver = useCallback((e: React.DragEvent<HTMLElement>) => {
+        if (!isFileDrag(e)) return
+        e.preventDefault()
+        setDropActive(true)
+    }, [])
+    const onDragLeave = useCallback(() => setDropActive(false), [])
+    const onDrop = useCallback(
+        (e: React.DragEvent<HTMLElement>) => {
+            if (!isFileDrag(e)) return
+            e.preventDefault()
+            setDropActive(false)
+            if (!onFiles) return
+            void readDroppedFiles(e.dataTransfer).then((files) => {
+                if (files.length) onFiles(files)
+            })
+        },
+        [onFiles],
+    )
+    return useMemo(
+        () =>
+            onFiles
+                ? {dropActive, dropProps: {onDragOver, onDragLeave, onDrop}}
+                : {dropActive: false, dropProps: {}},
+        [onFiles, dropActive, onDragOver, onDragLeave, onDrop],
+    )
 }

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

Source: Coding guidelines

web/packages/agenta-entities/src/drive/useImagePreviews.ts (1)

27-46: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a [files] dependency for the reconcile effect.

Both call sites pass a memoized array (useDriveUploads.ts line 101 and useMountUpload.ts line 217). With [files] the reconcile runs only when the input array identity changes, and the behavior stays identical for callers that pass a fresh array each render. Today the filter, Set build, and Map scan run after every commit of the host.

♻️ Optional refactor
         if (changed) setSnapshot(new Map(map))
-    })
+    }, [files])
web/packages/agenta-entities/src/drive/pdfThumb.ts (1)

14-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reset the cached promise when the import fails.

pdfjsPromise keeps a rejected promise. If the dynamic chunk load fails once, for example on a transient network error, every later call to renderPdfFirstPage rejects and returns null for the rest of the session. Clear the cache on rejection so a later thumbnail request can retry.

♻️ Proposed resiliency fix
 async function loadPdfjs(): Promise<PdfjsModule> {
     if (!pdfjsPromise) {
-        pdfjsPromise = import("pdfjs-dist").then((pdfjs) => {
-            pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"
-            return pdfjs
-        })
+        pdfjsPromise = import("pdfjs-dist")
+            .then((pdfjs) => {
+                pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs"
+                return pdfjs
+            })
+            .catch((error) => {
+                pdfjsPromise = null // a failed chunk load must not poison every later render
+                throw error
+            })
     }
     return pdfjsPromise
 }
web/packages/agenta-entities/src/drive/driveTree.ts (1)

190-198: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

applyLoadedCounts can understate a lazily loaded folder's item count.

applyLoadedCounts replaces the backend item_count with children.length for every folder that has at least one loaded child. If a level is only partially loaded, or if a filter pruned children, the row shows fewer items than the folder holds. The comment covers only the agent-files fold point.

Consider restricting the override to folders that are fully loaded, or to the fold-point mount only.

web/packages/agenta-entities/src/drive/useDriveSelection.ts (3)

88-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare the effect dependencies or silence the rule explicitly.

The effect calls select but lists only initialPath. react-hooks/exhaustive-deps reports this. The current omission is intentional, so record it in code instead of leaving a lint error. Add select to the deps with a ref guard on the previous initialPath, or add a targeted eslint-disable-next-line with the reason.

♻️ Proposed fix
     useEffect(() => {
         if (initialPath != null) select(initialPath)
+        // eslint-disable-next-line react-hooks/exhaustive-deps -- fire on initialPath changes only
     }, [initialPath])

As per coding guidelines: "Before committing frontend changes, run pnpm lint-fix from the web directory."

Source: Coding guidelines


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

Consider atomWithStorage for the per-drive last selection.

The last-viewed path per drive is a persisted UI preference. The current module-level atom family loses it on reload, and every visited mountId keeps an entry for the page lifetime. If cross-reload restore is wanted, use atomWithStorage with an agenta:-prefixed key and stringStorage for the nullable string. If it must stay session-scoped, state that in the comment.

As per coding guidelines: "Use atomWithStorage for persisted preferences, UI state, recently used items, and form drafts; prefix storage keys with agenta:. Use stringStorage for nullable strings."

Source: Coding guidelines


30-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shorten the narrative block comments. These modules carry multi-line comments that explain design intent and history rather than a surprising constraint. The repository rule allows longer comments only for genuinely surprising constraints such as bugs, races, or ordering requirements. Keep the ones that record a real hazard, for example the ReDoS note in driveTree.ts at L23-L25, and move the rest into the module doc block or a design note.

  • web/packages/agenta-entities/src/drive/useDriveSelection.ts#L30-L33: reduce the empty-mountId rationale to one line.
  • web/packages/agenta-entities/src/drive/driveTree.ts#L75-L83: reduce the isFolderEntry heuristic comment to one line and keep the inference rule only.
  • web/packages/agenta-entities/src/drive/driveTreeView.ts#L16-L19: reduce the SKELETON_ROW_CAP explanation to one line.
  • web/packages/agenta-entities/src/drive/useDriveTreePane.ts#L63-L69: move the anticipated-shift explanation into the module doc block and keep one line here.
  • web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts#L17-L20: reduce the group-scroll model 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/packages/agenta-entities/src/drive/useDriveTreePane.ts (1)

29-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider keyboard support for the resize handle.

The pane width changes through pointer events only. Keyboard users cannot resize the pane. Add a role="separator" handle with aria-valuenow and arrow-key stepping between TREE_MIN and TREE_MAX when the UI lane lands.

web/packages/agenta-entities/src/drive/useDriveTreeReveal.ts (1)

28-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing setExpanded dependency.

The expansion effect uses setExpanded but lists only selectedPath and selectedIsFolder. setExpanded arrives as a prop, so react-hooks/exhaustive-deps reports it. Add it to the deps; the setter identity is stable, so behavior does not change.

♻️ Proposed fix
-    }, [selectedPath, selectedIsFolder])
+    }, [selectedPath, selectedIsFolder, setExpanded])

As per coding guidelines: "Before committing frontend changes, run pnpm lint-fix from the web directory."

Source: Coding guidelines

web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts (1)

66-69: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Throttle the re-render bump to one per frame.

bumpGroupScroll() runs on every wheel event that changes an offset. A trackpad emits many events per frame, so the tree re-renders more often than it paints. Coalesce the bump with requestAnimationFrame, and cancel the pending frame in the teardown.

As per coding guidelines: "debounce or throttle search, filter, scroll, and resize handlers."

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 10f53913-a885-4522-996b-8b030cad3afd

📥 Commits

Reviewing files that changed from the base of the PR and between 26c16d8 and e2d62a8.

📒 Files selected for processing (31)
  • web/packages/agenta-entities/package.json
  • web/packages/agenta-entities/src/drive/agentDrive.ts
  • web/packages/agenta-entities/src/drive/configDrive.ts
  • web/packages/agenta-entities/src/drive/driveFlags.ts
  • web/packages/agenta-entities/src/drive/driveKeyboard.ts
  • web/packages/agenta-entities/src/drive/driveKinds.ts
  • web/packages/agenta-entities/src/drive/driveLabels.ts
  • web/packages/agenta-entities/src/drive/driveMedia.ts
  • web/packages/agenta-entities/src/drive/driveMotion.ts
  • web/packages/agenta-entities/src/drive/driveRepo.ts
  • web/packages/agenta-entities/src/drive/driveTree.ts
  • web/packages/agenta-entities/src/drive/driveTreeView.ts
  • web/packages/agenta-entities/src/drive/driveTypes.ts
  • web/packages/agenta-entities/src/drive/dropEntries.ts
  • web/packages/agenta-entities/src/drive/index.ts
  • web/packages/agenta-entities/src/drive/pdfThumb.ts
  • web/packages/agenta-entities/src/drive/recentChange.ts
  • web/packages/agenta-entities/src/drive/useDelayedTrue.ts
  • web/packages/agenta-entities/src/drive/useDriveDrop.ts
  • web/packages/agenta-entities/src/drive/useDriveFilters.ts
  • web/packages/agenta-entities/src/drive/useDriveSelection.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeKeyboard.ts
  • web/packages/agenta-entities/src/drive/useDriveTreePane.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeReveal.ts
  • web/packages/agenta-entities/src/drive/useDriveTreeViewport.ts
  • web/packages/agenta-entities/src/drive/useDriveUploads.ts
  • web/packages/agenta-entities/src/drive/useImagePreviews.ts
  • web/packages/agenta-entities/src/drive/useMountUpload.ts
  • web/packages/agenta-entities/src/drive/useSessionDrive.ts
  • web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts
  • web/packages/agenta-entities/tests/unit/dropEntries.test.ts
🛑 Comments failed to post (8)
web/packages/agenta-entities/src/drive/driveKeyboard.ts (1)

9-26: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find bindings of listArrowKeyDown and the row buttons in those surfaces.
rg -n -C5 'listArrowKeyDown' web
rg -n 'data-tree-main|data-row-main' web

Repository: Agenta-AI/agenta

Length of output: 6256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== driveKeyboard =="
cat -n web/packages/agenta-entities/src/drive/driveKeyboard.ts

echo "== DriveTreeRow relevant section =="
cat -n web/oss/src/components/Drives/DriveTreeRow.tsx | sed -n '80,140p'

echo "== DriveFileRow buttons and exported JSX =="
rg -n -C5 'DriveFileRow|data-row-main|data-tree-main|button\|button' web/oss/src/components/Drives/DriveFileRow.tsx

echo "== list handler usages near render =="
cat -n web/oss/src/components/Drives/ContextRail.tsx | sed -n '230,270p'
cat -n web/oss/src/components/Drives/StorageSection.tsx | sed -n '136,165p'

echo "== JS DOM probe: roving behavior with descendant/readonly/disabled buttons =="
node - <<'JS'
const rows = [
  '<button id="main-0" data-row-main>Main0</button>',
  '<button id="main-1" data-row-main>Main1<menu><button id="menu-a">Menu</button></menu></button>',
  '<button id="main-2" data-row-main disabled>Main2</button>',
].join("");
const temp = document.createElement("tbody");
temp.innerHTML = rows;
const all = Array.from(temp.querySelectorAll("button"));
const main = Array.from(temp.querySelectorAll("button[data-row-main]")).filter(r => !r.disabled);
let current = main[0];
const keys = ["ArrowDown", "ArrowDown", "End", "ArrowUp", "ArrowUp"];
const events = [];
for (const key of keys) {
  const e = new KeyboardEvent("keydown", {key, bubbles: true});
  const target = current;
  target.dispatchEvent(e);
  if (key.startsWith("Arrow")) e.preventDefault();
  const rowsList = e.currentTarget.querySelectorAll("button");
  const idx = Array.from(rowsList).findIndex(r => r === document.activeElement);
  const next = idx < 0 ? 0 : Math.min(Math.max(idx + (key === "ArrowDown" ? 1 : -1), 0), rowsList.length - 1);
  rowsList[next]?.focus();
  events.push({key, from: target.id, allRowIndex: idx, allNext: rowsList[next]?.id, activeElement: document.activeElement?.id});
  current = rowsList[next];
}
console.log(JSON.stringify({
  allRowCount: all.length,
  allIndices: all.map(b => b.id),
  mainRowCount: main.length,
  main: main.map(b => ({id: b.id, text: b.textContent?.trim()})),
  events,
  nextButtonFromMainIndexViaAll: main.map(b => {
    const idx = all.indexOf(b);
    return {id: b.id, index: idx, allNext: all[idx + 1]?.id};
  })
}, null, 2));
JS

Repository: Agenta-AI/agenta

Length of output: 12735


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DriveFileRow implementation =="
wc -l web/oss/src/components/Drives/DriveFileRow.tsx
cat -n web/oss/src/components/Drives/DriveFileRow.tsx | sed -n '120,260p'

echo "== DriveRetryButton implementation =="
cat -n web/oss/src/components/Drives/DriveFileRow.tsx | sed -n '1,80p'

echo "== KeyboardEvent imports/usages in web =="
rg -n 'KeyboardEvent<|KeyboardEvent\(' web/oss web/packages | head -80

echo "== DriveFileRow button search =="
rg -n -C4 '<button|data-row|data-tree|disabled' web/oss/src/components/Drives/DriveFileRow.tsx

Repository: Agenta-AI/agenta

Length of output: 19322


Restrict the row query to row controls and skip disabled buttons.

listArrowKeyDown is bound by the drive context rail and config Files list, and both render DriveFileRow. Inside the row, DriveRetryButton renders a disabled button when busy, so the current querySelectorAll("button") query includes it as a row stop while focus() does nothing. Use a row selector and !r.disabled so arrow keys skip retry controls and secondary row actions if they are added.

web/packages/agenta-entities/src/drive/driveTree.ts (2)

114-119: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a GB tier to humanSize.

Files of 1 GB or more render as large MB values, for example "2048.0 MB". Agent-written drives can contain such files.

♻️ Proposed fix
 export const humanSize = (bytes?: number | null): string => {
     if (bytes == null) return ""
     if (bytes < 1024) return `${bytes} B`
     if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
-    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+    return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
 }
📝 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.

export const humanSize = (bytes?: number | null): string => {
    if (bytes == null) return ""
    if (bytes < 1024) return `${bytes} B`
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
}

202-219: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

A folder that matches the query loses its contents.

In filterDriveTree, a folder survives when its own path matches, but its children are replaced by the filtered list. A folder whose name matches then renders as empty, because no descendant path contains the query string. Users normally expect the matched folder's contents.

♻️ Proposed fix
                 const children = walk(node.children)
-                return children.length || node.path.toLowerCase().includes(q)
-                    ? {...node, children}
-                    : null
+                if (node.path.toLowerCase().includes(q)) return node
+                return children.length ? {...node, children} : null
📝 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.

/** Case-insensitive path-substring filter; folders survive when any descendant matches. */
export function filterDriveTree(nodes: DriveTreeNode[], query: string): DriveTreeNode[] {
    const q = query.trim().toLowerCase()
    if (!q) return nodes
    const walk = (list: DriveTreeNode[]): DriveTreeNode[] =>
        list
            .map((node) => {
                if (!node.isFolder) {
                    return node.path.toLowerCase().includes(q) ? node : null
                }
                const children = walk(node.children)
                if (node.path.toLowerCase().includes(q)) return node
                return children.length ? {...node, children} : null
            })
            .filter((n): n is DriveTreeNode => Boolean(n))
    return walk(nodes)
}
web/packages/agenta-entities/src/drive/recentChange.ts (1)

30-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Refresh now before the early return.

If lastTouchedAt changes to an expired timestamp while a prior timer is active, cleanup stops the timer and this branch does not update now. The stale value can keep isRecentlyChanged true until another prop change.

Proposed fix
     useEffect(() => {
-        if (lastTouchedAt == null || Date.now() - lastTouchedAt >= windowMs) return
-        setNow(Date.now())
+        const currentNow = Date.now()
+        setNow(currentNow)
+        if (lastTouchedAt == null || currentNow - lastTouchedAt >= windowMs) return
         const id = setInterval(() => {
📝 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 currentNow = Date.now()
        setNow(currentNow)
        if (lastTouchedAt == null || currentNow - lastTouchedAt >= windowMs) return
web/packages/agenta-entities/src/drive/useDriveDrop.ts (1)

52-92: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear the spring timer on unmount.

springTimer holds a pending window.setTimeout. No cleanup path cancels it when the component unmounts. If the host unmounts during the 700 ms hover countdown, the callback still runs and calls onNavigate(path) plus setHoverPath(null) after unmount. That produces an unexpected navigation and keeps the closure alive.

Add an unmount effect that calls clearSpring.

🛡️ Proposed fix
     const clearSpring = useCallback(() => {
         window.clearTimeout(springTimer.current)
         springTimer.current = undefined
         springPath.current = null
     }, [])
+
+    // The spring timeout must not outlive the host — it would navigate after unmount.
+    useEffect(() => clearSpring, [clearSpring])
📝 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 springTimer = useRef<number | undefined>(undefined)
    const springPath = useRef<string | null>(null)
    const clearSpring = useCallback(() => {
        window.clearTimeout(springTimer.current)
        springTimer.current = undefined
        springPath.current = null
    }, [])

    // The spring timeout must not outlive the host — it would navigate after unmount.
    useEffect(() => clearSpring, [clearSpring])

    // Window-level drag tracking for the overall `dragging` flag (depth counter absorbs the
    // dragenter/leave flicker from moving across child elements).
    const depth = useRef(0)
    useEffect(() => {
        if (!enabled) return
        const has = (e: DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes("Files")
        const onEnter = (e: DragEvent) => {
            if (has(e)) {
                depth.current += 1
                setDragging(true)
            }
        }
        const onLeave = () => {
            depth.current = Math.max(0, depth.current - 1)
            if (depth.current === 0) setDragging(false)
        }
        const onEnd = () => {
            depth.current = 0
            setDragging(false)
            setHoverPath(null)
            clearSpring()
        }
        window.addEventListener("dragenter", onEnter)
        window.addEventListener("dragleave", onLeave)
        window.addEventListener("drop", onEnd)
        window.addEventListener("dragend", onEnd)
        return () => {
            window.removeEventListener("dragenter", onEnter)
            window.removeEventListener("dragleave", onLeave)
            window.removeEventListener("drop", onEnd)
            window.removeEventListener("dragend", onEnd)
        }
    }, [enabled, clearSpring])
web/packages/agenta-entities/src/drive/useDriveTreeKeyboard.ts (1)

36-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Resolve activePath from the closest row element.

The handler reads data-path from document.activeElement only. If focus sits on a control inside a row, for example a row action button, activePath is null and idx is -1. ArrowDown and ArrowUp then jump to row 0 instead of the neighbour row, and ArrowLeft and ArrowRight do nothing useful.

♻️ Proposed fix
-            const activePath = (document.activeElement as HTMLElement | null)?.getAttribute(
-                "data-path",
-            )
+            const activeEl = document.activeElement as HTMLElement | null
+            const activePath =
+                activeEl?.closest<HTMLElement>("[data-path]")?.getAttribute("data-path") ?? null
📝 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 activeEl = document.activeElement as HTMLElement | null
            const activePath =
                activeEl?.closest<HTMLElement>("[data-path]")?.getAttribute("data-path") ?? null
            const idx = activePath != null ? (indexByPath.get(activePath) ?? -1) : -1
web/packages/agenta-entities/src/drive/useDriveTreePane.ts (1)

49-58: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a cancelled or lost pointer during the resize drag.

onTreeHandleUp runs on pointer up only. If the browser fires pointercancel, or if pointer capture is lost, treeDrag.current stays set and treeDragging stays true. The pane then keeps its dragging state and continues to follow later pointer moves over the handle.

Export one end handler and bind it to onPointerUp, onPointerCancel, and onLostPointerCapture.

♻️ Proposed fix
     const onTreeHandleUp = useCallback(
-        (e: React.PointerEvent<HTMLDivElement>) => {
+        (e: React.PointerEvent<HTMLDivElement>) => {
             if (!treeDrag.current) return
             treeDrag.current = null
             setTreeDragging(false)
             setTreeWidth(Math.round(paneW.get()))
-            e.currentTarget.releasePointerCapture?.(e.pointerId)
+            if (e.currentTarget.hasPointerCapture?.(e.pointerId))
+                e.currentTarget.releasePointerCapture?.(e.pointerId)
         },
         [paneW],
     )

Bind it as onPointerUp={onTreeHandleUp} and onPointerCancel={onTreeHandleUp} in the consuming component.

web/packages/agenta-entities/src/drive/useTreeGroupScroll.ts (1)

59-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle deltaMode page units and clamp per group width.

Two edge cases in the offset math:

  1. e.deltaMode === 2 (DOM_DELTA_PAGE) falls into the 1 branch, so a page-unit wheel event scrolls a few pixels.
  2. maxScroll uses el.clientWidth of the whole scroll container. Each group's rows are indented by depth, so the usable width per group is smaller. The clamp then permits over-scroll for deep groups.
♻️ Proposed fix for the delta unit
-            const unit = e.deltaMode === 1 ? 16 : 1
+            const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? el.clientWidth : 1

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