From 6f215846bb2d1cf4d09743cbf41ef77cca9ebc34 Mon Sep 17 00:00:00 2001 From: Arda Erzin Date: Sat, 8 Aug 2026 22:20:49 +0300 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20@agenta/entity-ui/drive=20?= =?UTF-8?q?=E2=80=94=20the=20drive=20explorer,=20tree=20and=20file=20surfa?= =?UTF-8?q?ces=20leave=20the=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/AttachmentViewerDrawer.tsx | 6 +- .../src/components/Drives/chatFileRefs.tsx | 10 +- .../Drives/useChatScopeSessionId.ts | 26 +++++ .../pages/overview/agent/AgentFilesCard.tsx | 5 +- .../src/drive}/ContextRail.tsx | 63 ++++++++----- .../src/drive}/DriveBreadcrumb.tsx | 9 +- .../src/drive}/DriveExplorer.tsx | 31 +++--- .../src/drive}/DriveExplorerSkeleton.tsx | 37 +++++++- .../src/drive}/DriveExplorerStates.tsx | 6 +- .../src/drive}/DriveFileCard.tsx | 24 +++-- .../src/drive}/DriveFileContentViewer.tsx | 6 +- .../src/drive}/DriveFilePreview.tsx | 7 +- .../src/drive}/DriveFileRow.tsx | 6 +- .../src/drive}/DriveHeader.tsx | 94 ++++++++++--------- .../src/drive}/DriveItemContextMenu.tsx | 73 +++++++------- .../src/drive}/DrivePendingTiles.tsx | 15 +-- .../src/drive}/DriveToolbar.tsx | 5 +- .../src/drive}/DriveTreeList.tsx | 23 +++-- .../src/drive}/DriveTreePane.tsx | 5 +- .../src/drive}/DriveTreeRow.tsx | 6 +- .../agenta-entity-ui/src/drive}/FileThumb.tsx | 13 ++- .../src/drive}/FilesDrawer.tsx | 6 +- .../src/drive}/FolderTile.tsx | 2 +- .../src/drive}/FolderView.tsx | 21 +++-- .../agenta-entity-ui/src/drive}/OriginTag.tsx | 5 +- .../src/drive}/SessionFilesDrawer.tsx | 4 +- .../src/drive}/StorageFilesHeader.tsx | 17 +++- .../src/drive}/StorageSection.tsx | 50 +++++++--- .../src/drive}/VirtualTileGrid.tsx | 0 .../src/drive}/driveFileSource.tsx | 3 +- .../src/drive}/driveIcons.tsx | 3 +- .../src/drive/driveMarkdown.tsx | 53 +++++++++++ .../src/drive}/driveSessionContext.tsx | 0 .../agenta-entity-ui/src/drive}/fileMeta.tsx | 7 +- .../agenta-entity-ui/src/drive/index.ts | 47 ++++++++++ .../agenta-entity-ui/src/drive}/quickLook.tsx | 0 .../agenta-entity-ui/src/drive}/renderers.tsx | 71 +++++++++----- .../agenta-entity-ui/src/drive}/repoMeta.tsx | 2 +- .../src/drive}/useDriveDownloadAll.ts | 12 +-- .../src/drive/useDriveFileDownload.ts | 31 ++++++ .../src/drive}/useDriveTreeData.ts | 16 +++- .../src/drive}/useLazyDriveTree.tsx | 5 +- .../src/drive}/useUploadReveal.ts | 6 +- .../tests/unit}/useUploadReveal.test.ts | 2 +- 44 files changed, 548 insertions(+), 285 deletions(-) create mode 100644 web/oss/src/components/Drives/useChatScopeSessionId.ts rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/ContextRail.tsx (93%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveBreadcrumb.tsx (89%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveExplorer.tsx (95%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveExplorerSkeleton.tsx (74%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveExplorerStates.tsx (91%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveFileCard.tsx (88%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveFileContentViewer.tsx (93%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveFilePreview.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveFileRow.tsx (99%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveHeader.tsx (78%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveItemContextMenu.tsx (70%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DrivePendingTiles.tsx (94%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveToolbar.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveTreeList.tsx (91%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveTreePane.tsx (97%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/DriveTreeRow.tsx (98%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/FileThumb.tsx (94%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/FilesDrawer.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/FolderTile.tsx (95%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/FolderView.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/OriginTag.tsx (93%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/SessionFilesDrawer.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/StorageFilesHeader.tsx (88%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/StorageSection.tsx (87%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/VirtualTileGrid.tsx (100%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/driveFileSource.tsx (96%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/driveIcons.tsx (96%) create mode 100644 web/packages/agenta-entity-ui/src/drive/driveMarkdown.tsx rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/driveSessionContext.tsx (100%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/fileMeta.tsx (98%) create mode 100644 web/packages/agenta-entity-ui/src/drive/index.ts rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/quickLook.tsx (100%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/renderers.tsx (91%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/repoMeta.tsx (98%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/useDriveDownloadAll.ts (88%) create mode 100644 web/packages/agenta-entity-ui/src/drive/useDriveFileDownload.ts rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/useDriveTreeData.ts (97%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/useLazyDriveTree.tsx (98%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/src/drive}/useUploadReveal.ts (98%) rename web/{oss/src/components/Drives => packages/agenta-entity-ui/tests/unit}/useUploadReveal.test.ts (97%) diff --git a/web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx b/web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx index 59ba8dcc5e..5840549eca 100644 --- a/web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx +++ b/web/oss/src/components/AgentChatSlice/components/AttachmentViewerDrawer.tsx @@ -5,9 +5,9 @@ import {isViewable} from "@agenta/chat/assets" import type {StagedUpload as UploadFile} from "@agenta/chat/model" import {type MountFile} from "@agenta/entities/session" -import {type DriveFileSource, DriveFileSourceContext} from "@/oss/components/Drives/driveFileSource" -import {FilesDrawer} from "@/oss/components/Drives/FilesDrawer" -import {type SessionDriveData} from "@/oss/components/Drives/useSessionDrive" +import {type DriveFileSource, DriveFileSourceContext} from "@agenta/entity-ui/drive" +import {FilesDrawer} from "@agenta/entity-ui/drive" +import {type SessionDriveData} from "@agenta/entities/drive" /** * Previews the composer's attachments in the shared Files drawer — the same tree + viewer used for diff --git a/web/oss/src/components/Drives/chatFileRefs.tsx b/web/oss/src/components/Drives/chatFileRefs.tsx index aaa844a097..874e57bf1c 100644 --- a/web/oss/src/components/Drives/chatFileRefs.tsx +++ b/web/oss/src/components/Drives/chatFileRefs.tsx @@ -25,11 +25,11 @@ import { import {atom, useAtomValue} from "jotai" import {atomFamily} from "jotai/utils" -import {agentMountQueryFamily} from "./agentDrive" -import {DriveFileInlineRef} from "./DriveFileCard" -import {useDriveArtifactId, useDriveSessionId} from "./driveSessionContext" -import {cleanPath} from "./driveTree" -import {AGENT_FILES_DIR} from "./useSessionDrive" +import {agentMountQueryFamily} from "@agenta/entities/drive" +import {DriveFileInlineRef} from "@agenta/entity-ui/drive" +import {useDriveArtifactId, useDriveSessionId} from "@agenta/entity-ui/drive" +import {cleanPath} from "@agenta/entities/drive" +import {AGENT_FILES_DIR} from "@agenta/entities/drive" /** A span that could NAME a file; strip a leading `./` and require a path-ish shape: a slash, or a * letter-led trailing extension (`.ts`, `.tar.gz`). A bare `/[./]/` matched any dotted token — diff --git a/web/oss/src/components/Drives/useChatScopeSessionId.ts b/web/oss/src/components/Drives/useChatScopeSessionId.ts new file mode 100644 index 0000000000..2fcf44f6bc --- /dev/null +++ b/web/oss/src/components/Drives/useChatScopeSessionId.ts @@ -0,0 +1,26 @@ +import {isSessionFresh} from "@agenta/chat/state" +import {useAtomValue} from "jotai" + +import {useChatScopeKey} from "@/oss/components/AgentChatSlice/state/scope" +import { + activeSessionIdAtomFamily, + sessionsListAtomFamily, +} from "@/oss/components/AgentChatSlice/state/sessions" + +/** + * This app's answer to "which conversation's drive is the config panel showing?" — resolved the + * same way the chat resolves it: a stale active id (closed tab) falls back to the first open tab, + * and a brand-new never-run tab resolves to none so the queries stay held off. + * + * Lives here, not in the shared drive code: open chat TABS are a desktop concept. A + * session-scoped surface passes its own id straight through instead. + */ +export const useChatScopeSessionId = (): string => { + const scope = useChatScopeKey() + const sessions = useAtomValue(sessionsListAtomFamily(scope)) + const rawActiveId = useAtomValue(activeSessionIdAtomFamily(scope)) + const resolved = sessions.some((s) => s.id === rawActiveId) + ? rawActiveId + : (sessions[0]?.id ?? "") + return resolved && !isSessionFresh(resolved) ? resolved : "" +} diff --git a/web/oss/src/components/pages/overview/agent/AgentFilesCard.tsx b/web/oss/src/components/pages/overview/agent/AgentFilesCard.tsx index 3e5fedbcbd..276326f487 100644 --- a/web/oss/src/components/pages/overview/agent/AgentFilesCard.tsx +++ b/web/oss/src/components/pages/overview/agent/AgentFilesCard.tsx @@ -8,12 +8,11 @@ import {useAtomValue} from "jotai" import dynamic from "next/dynamic" import {timeAgo} from "@/oss/components/AgentChatSlice/state/sessions" -import {agentMountQueryFamily} from "@/oss/components/Drives/agentDrive" -import {AGENT_FILES_DIR, useSessionDrive} from "@/oss/components/Drives/useSessionDrive" +import {AGENT_FILES_DIR, agentMountQueryFamily, useSessionDrive} from "@agenta/entities/drive" // The whole drive explorer, pulled in only once the drawer is actually opened. const FilesDrawer = dynamic( - () => import("@/oss/components/Drives/FilesDrawer").then((mod) => mod.FilesDrawer), + () => import("@agenta/entity-ui/drive").then((mod) => mod.FilesDrawer), {ssr: false}, ) diff --git a/web/oss/src/components/Drives/ContextRail.tsx b/web/packages/agenta-entity-ui/src/drive/ContextRail.tsx similarity index 93% rename from web/oss/src/components/Drives/ContextRail.tsx rename to web/packages/agenta-entity-ui/src/drive/ContextRail.tsx index d1875d8fd1..066d08259a 100644 --- a/web/oss/src/components/Drives/ContextRail.tsx +++ b/web/packages/agenta-entity-ui/src/drive/ContextRail.tsx @@ -5,9 +5,18 @@ * fetch). Uses the same elevated dark surface as the build-mode Inspector so the right dock reads * as one panel across modes. A file row opens Quick Look; "View all files" opens the Files drawer. */ -import {isSessionFresh} from "@agenta/chat/state" +import {listArrowKeyDown} from "@agenta/entities/drive" +import {FILE_ITEM_VARIANTS, FILE_SPRING} from "@agenta/entities/drive" +import {relativeTime} from "@agenta/entities/drive" +import {type DroppedFile} from "@agenta/entities/drive" +import {isRecentlyChanged, useRecentChangeClock} from "@agenta/entities/drive" +import {type FileDropProps, useStageDrop} from "@agenta/entities/drive" +import {driveHasMixedOrigins, useSessionDriveSummary} from "@agenta/entities/drive" +import {isSessionFresh} from "@agenta/entities/session" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {Tag} from "@agenta/ui/components/presentational" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {CircleNotch, FolderOpen, FolderSimple, Sidebar} from "@phosphor-icons/react" -import {Button, Tag, Tooltip, Typography} from "antd" import {useAtom, useSetAtom} from "jotai" import {atomWithStorage} from "jotai/utils" import {AnimatePresence, MotionConfig, motion} from "motion/react" @@ -20,17 +29,20 @@ import { SKELETON_ROW_COUNT, } from "./DriveFileRow" import {DriveItemContextMenu, useCopyDrivePath, useDriveItemDownload} from "./DriveItemContextMenu" -import {listArrowKeyDown} from "./driveKeyboard" -import {FILE_ITEM_VARIANTS, FILE_SPRING} from "./driveMotion" import {useDriveArtifactId} from "./driveSessionContext" -import {relativeTime} from "./driveTree" -import {type DroppedFile} from "./dropEntries" import {driveQuickLookAtomFamily} from "./quickLook" -import {isRecentlyChanged, useRecentChangeClock} from "./recentChange" -import {type FileDropProps, useStageDrop} from "./useDriveDrop" -import {driveHasMixedOrigins, useSessionDriveSummary} from "./useSessionDrive" -const {Text} = Typography +/** antd `Typography.Text` stand-in: the only prop this module used is `type="secondary"`. */ +const Text = ({ + type: _type, + className, + children, + ...rest +}: React.HTMLAttributes & {type?: "secondary"}) => ( + + {children} + +) // Shared elevated surface with the Inspector (build-spec §elevation) — the right dock reads the // same in chat (this rail) and build (Inspector). Theme-aware tokens, same as the Inspector panel. @@ -112,7 +124,7 @@ export function ContextRail({ style={{width: STRIP_WIDTH, borderColor: BORDER}} {...stageDropProps} > - + @@ -126,7 +138,7 @@ export function ContextRail({ ? "Some files couldn’t be loaded — open to retry" : `${drive.fileCount}${drive.fileCountCapped ? "+" : ""} file${drive.fileCount === 1 && !drive.fileCountCapped ? "" : "s"} in this conversation` } - placement="left" + side="left" > ) : null} {busy ? ( - + @@ -197,10 +209,11 @@ const ExpandedRail = ({
Files {drive.fileCount > 0 ? ( - - {drive.fileCount} - {drive.fileCountCapped ? "+" : ""} - + ) : null}
{/* A mount failure badges the open-drawer folder and swaps its tooltip to the retry @@ -211,7 +224,7 @@ const ExpandedRail = ({ ? "Some files couldn’t be loaded — open to retry" : "Open the Files drawer" } - placement="bottom" + side="bottom" >
) +/** The header + toolbar bands, matching DriveHeader/DriveToolbar's own geometry (both are + * `h-auto shrink-0 … border-b … px-3 py-2`), so the chrome does not appear out of nowhere when + * the real explorer mounts. */ +const ChromeSkeleton = () => ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+ +) + /** * Drawer-body placeholder: the tree pane (unless hidden) + the content pane — mirroring the real body * so the skeleton→content swap never shifts the layout. + * + * `withChrome` adds the header + toolbar bands. The explorer's OWN loading state leaves it off (its + * real header already renders above this), but the drawer's `next/dynamic` fallback needs it: at + * that point nothing of the explorer exists yet, so without it the drawer opens on a bare slab of + * content skeleton with no header or toolbar — which is not the shape it settles into. */ export const DriveExplorerSkeleton = ({ mode = "grid", showTree = true, + withChrome = false, }: { mode?: "grid" | "preview" showTree?: boolean + withChrome?: boolean }) => ( -
- {showTree ? : null} - +
+ {withChrome ? : null} +
+ {showTree ? : null} + +
) diff --git a/web/oss/src/components/Drives/DriveExplorerStates.tsx b/web/packages/agenta-entity-ui/src/drive/DriveExplorerStates.tsx similarity index 91% rename from web/oss/src/components/Drives/DriveExplorerStates.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveExplorerStates.tsx index 84efda013e..93f3dab825 100644 --- a/web/oss/src/components/Drives/DriveExplorerStates.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveExplorerStates.tsx @@ -3,12 +3,12 @@ * retry) and the empty drive. Split out so DriveExplorer's body branch reads as a three-line switch; * the loading state is {@link DriveExplorerSkeleton}. */ +import {type DriveScope} from "@agenta/entities/drive" +import {type SessionDriveData} from "@agenta/entities/drive" +import {Alert} from "@agenta/ui/ui" import {Tray} from "@phosphor-icons/react" -import {Alert} from "antd" import {DriveRetryButton} from "./DriveFileRow" -import {type DriveScope} from "./driveTypes" -import {type SessionDriveData} from "./useSessionDrive" export function DriveErrorState({drive}: {drive: SessionDriveData}) { return ( diff --git a/web/oss/src/components/Drives/DriveFileCard.tsx b/web/packages/agenta-entity-ui/src/drive/DriveFileCard.tsx similarity index 88% rename from web/oss/src/components/Drives/DriveFileCard.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveFileCard.tsx index 18db163ad3..2c15c91a6c 100644 --- a/web/oss/src/components/Drives/DriveFileCard.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveFileCard.tsx @@ -5,20 +5,32 @@ * download enrich from the ambient conversation drive when the file resolves against the * listing; deletes render struck-through with no actions. */ +import {fileTypeLabel} from "@agenta/entities/drive" +import {humanSize} from "@agenta/entities/drive" +import {useSessionDrive} from "@agenta/entities/drive" import {mountPathMatchesToolPath, type FileActivityOp} from "@agenta/entities/session" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {Tag} from "@agenta/ui/components/presentational" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {DownloadSimple} from "@phosphor-icons/react" -import {Button, Tag, Tooltip, Typography} from "antd" import {useSetAtom} from "jotai" import {driveFileIcon} from "./driveIcons" -import {fileTypeLabel} from "./driveKinds" -import {useDriveFileDownload} from "./driveMedia" import {useDriveArtifactId, useDriveSessionId} from "./driveSessionContext" -import {humanSize} from "./driveTree" import {driveQuickLookAtomFamily} from "./quickLook" -import {useSessionDrive} from "./useSessionDrive" +import {useDriveFileDownload} from "./useDriveFileDownload" -const {Text} = Typography +/** antd `Typography.Text` stand-in: the only prop this module used is `type="secondary"`. */ +const Text = ({ + type: _type, + className, + children, + ...rest +}: React.HTMLAttributes & {type?: "secondary"}) => ( + + {children} + +) const OP_META: Record = { write: {label: "Created", color: "green"}, diff --git a/web/oss/src/components/Drives/DriveFileContentViewer.tsx b/web/packages/agenta-entity-ui/src/drive/DriveFileContentViewer.tsx similarity index 93% rename from web/oss/src/components/Drives/DriveFileContentViewer.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveFileContentViewer.tsx index 0edb4857e7..c20d1ab491 100644 --- a/web/oss/src/components/Drives/DriveFileContentViewer.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveFileContentViewer.tsx @@ -1,13 +1,13 @@ import {useState} from "react" +import {resolveDriveFileKind} from "@agenta/entities/drive" import {type Mount} from "@agenta/entities/session" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" import {DownloadSimple} from "@phosphor-icons/react" -import {Button} from "antd" import {AnimatePresence, motion} from "motion/react" -import {resolveDriveFileKind} from "./driveKinds" -import {useDriveFileDownload} from "./driveMedia" import {DriveFileBody} from "./renderers" +import {useDriveFileDownload} from "./useDriveFileDownload" /** The content viewer — the renderer registry (build-spec 3): kind-matched body, size caps, * Download fallback. Shared by the drawer preview and the chat Quick Look. diff --git a/web/oss/src/components/Drives/DriveFilePreview.tsx b/web/packages/agenta-entity-ui/src/drive/DriveFilePreview.tsx similarity index 96% rename from web/oss/src/components/Drives/DriveFilePreview.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveFilePreview.tsx index 7110a36874..2c0d15b908 100644 --- a/web/oss/src/components/Drives/DriveFilePreview.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveFilePreview.tsx @@ -1,17 +1,18 @@ import {useState} from "react" +import {META_REVEAL} from "@agenta/entities/drive" +import {fileOrigin} from "@agenta/entities/drive" import {type Mount} from "@agenta/entities/session" import {CopyButton} from "@agenta/ui/components/presentational" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {Info} from "@phosphor-icons/react" -import {Button, Tooltip} from "antd" import {AnimatePresence, motion} from "motion/react" import {DriveBreadcrumb} from "./DriveBreadcrumb" import {DriveFileContentViewer, DriveFileDownloadButton} from "./DriveFileContentViewer" -import {META_REVEAL} from "./driveMotion" import {DriveFileMetaList} from "./fileMeta" import {OriginTag} from "./OriginTag" -import {fileOrigin} from "./useSessionDrive" /** Right pane: a FIXED header (breadcrumb + name + copy/details/download actions + expandable * metadata) over a scrollable content viewer — same shape and interactions as the chat single-file diff --git a/web/oss/src/components/Drives/DriveFileRow.tsx b/web/packages/agenta-entity-ui/src/drive/DriveFileRow.tsx similarity index 99% rename from web/oss/src/components/Drives/DriveFileRow.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveFileRow.tsx index dcf03bc229..93ca774d60 100644 --- a/web/oss/src/components/Drives/DriveFileRow.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveFileRow.tsx @@ -14,15 +14,15 @@ */ import {type CSSProperties, type ReactNode} from "react" +import {isHiddenPath} from "@agenta/entities/drive" +import {AGENT_FILES_DIR, fileOrigin, type DriveRecentFile} from "@agenta/entities/drive" import {type Mount} from "@agenta/entities/session" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {ArrowClockwise, CircleNotch, FolderSimple} from "@phosphor-icons/react" -import {Tooltip} from "antd" import {driveFileIcon} from "./driveIcons" -import {isHiddenPath} from "./driveTree" import {FileThumb} from "./FileThumb" import {AGENT_ACCENT, OriginTag} from "./OriginTag" -import {AGENT_FILES_DIR, fileOrigin, type DriveRecentFile} from "./useSessionDrive" export type DriveFileVariant = "row" | "card" | "tile" diff --git a/web/oss/src/components/Drives/DriveHeader.tsx b/web/packages/agenta-entity-ui/src/drive/DriveHeader.tsx similarity index 78% rename from web/oss/src/components/Drives/DriveHeader.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveHeader.tsx index ff79387526..fc6e5e466d 100644 --- a/web/oss/src/components/Drives/DriveHeader.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveHeader.tsx @@ -1,5 +1,17 @@ +import {humanSize} from "@agenta/entities/drive" +import {type DriveId} from "@agenta/entities/drive" +import {fileOrigin} from "@agenta/entities/drive" import {type Mount} from "@agenta/entities/session" import {CopyButton} from "@agenta/ui/components/presentational" +import {Tag, EnhancedButton as Button} from "@agenta/ui/components/presentational" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + SimpleTooltip as Tooltip, +} from "@agenta/ui/ui" import { ArrowsIn, ArrowsOut, @@ -11,14 +23,10 @@ import { WarningCircle, X, } from "@phosphor-icons/react" -import {Button, Dropdown, type MenuProps, Tag, Tooltip} from "antd" import {DriveBreadcrumb} from "./DriveBreadcrumb" import {DriveFileDownloadButton} from "./DriveFileContentViewer" import {DriveRetryButton} from "./DriveFileRow" -import {humanSize} from "./driveTree" -import {type DriveId} from "./driveTypes" -import {fileOrigin} from "./useSessionDrive" /** * DriveHeader — the drawer's ONE header. The breadcrumb IS the header (its last crumb the current @@ -98,26 +106,6 @@ export const DriveHeader = ({ // A file always has details (size/modified); a folder only when it's a repo. Nothing selected // (transient null before the root auto-selects) → no toggle. const hasDetails = isFolder ? isRepo : selectedPath != null - const overflow: MenuProps["items"] = [ - ...ids.map((id) => ({ - key: id.key, - label: ( -
- Copy {id.label} - {id.value} -
- ), - })), - // Only a separator when there IS something above it — a host without drive ids (the ids - // resolve async, and the local-file drive never has any) otherwise opens on a stray rule. - ...(ids.length ? [{type: "divider" as const}] : []), - { - key: "download-all", - label: downloadingAll ? "Preparing download…" : "Download all", - icon: , - disabled: !onDownloadAll || downloadingAll, - }, - ] return (
@@ -148,7 +136,7 @@ export const DriveHeader = ({ rootLabel={rootLabel} onNavigate={onNavigate} /> - + {atRoot ? `${totalCount}${totalCapped ? "+" : ""} file${totalCount === 1 ? "" : "s"}` : isFolder @@ -160,7 +148,7 @@ export const DriveHeader = ({ : null} {!isFolder && showOrigin && selectedPath ? ( - + {fileOrigin(selectedPath) === "agent" ? "Agent" : "Session"} ) : null} @@ -243,24 +231,42 @@ export const DriveHeader = ({ {!isFolder && selectedPath ? ( ) : null} - { - if (key === "download-all") return onDownloadAll?.() - const hit = ids.find((id) => id.key === key) - if (hit) copyText(hit.value, `${hit.label} copied`) - }, - }} - > -
) diff --git a/web/oss/src/components/Drives/DriveItemContextMenu.tsx b/web/packages/agenta-entity-ui/src/drive/DriveItemContextMenu.tsx similarity index 70% rename from web/oss/src/components/Drives/DriveItemContextMenu.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveItemContextMenu.tsx index 12ea83ba2a..9229413a75 100644 --- a/web/oss/src/components/Drives/DriveItemContextMenu.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveItemContextMenu.tsx @@ -6,25 +6,25 @@ * * "Copy path" copies the PRESENTED path (the `agent-files/`-folded path the breadcrumb shows), the * same value the file-preview "Copy path" affordance copies — so folders finally get the copy - * affordance files already had. The copy feedback rides `App.useApp()`'s message so it themes - * correctly in dark mode (a static `message.*` would render light — see the dark-mode notes). + * affordance files already had. The copy feedback rides the kit's imperative message service, + * which renders through the app's own outlet and themes with it. */ -import {type ReactElement, useCallback, useMemo} from "react" +import {type ReactElement, useCallback} from "react" +import {downloadMountArchive} from "@agenta/entities/drive" +import {type SessionDriveData} from "@agenta/entities/drive" +import {projectIdAtom} from "@agenta/shared/state" +import {message} from "@agenta/ui/app-message" +import {ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger} from "@agenta/ui/ui" import {ArrowSquareOut, Copy, DownloadSimple, FolderOpen} from "@phosphor-icons/react" -import {App, Dropdown, type MenuProps} from "antd" import {useAtomValue} from "jotai" -import {projectIdAtom} from "@/oss/state/project" +import {useDriveFileDownload} from "./useDriveFileDownload" -import {downloadMountArchive, useDriveFileDownload} from "./driveMedia" -import {type SessionDriveData} from "./useSessionDrive" - -/** A `copy(text, successMessage?)` bound to the themed message toast (App.useApp so it renders +/** A `copy(text, successMessage?)` bound to the themed message toast (the kit service so it renders * correctly in dark mode). The generic primitive behind {@link useCopyDrivePath}; the drawer header * reuses it for the drive/owner ids. */ export function useCopyText(): (text: string, successMessage?: string) => void { - const {message} = App.useApp() return useCallback( (text: string, successMessage = "Copied") => { if (!text || !navigator.clipboard) return @@ -50,7 +50,6 @@ export function useCopyDrivePath(): (path: string) => void { export function useDriveItemDownload( drive: SessionDriveData, ): (path: string, isFolder: boolean) => void { - const {message} = App.useApp() const projectId = useAtomValue(projectIdAtom) const downloadFile = useDriveFileDownload() return useCallback( @@ -105,34 +104,30 @@ export const DriveItemContextMenu = ({ className?: string children: ReactElement | ReactElement[] }) => { - const items = useMemo(() => { - const list: NonNullable = [ - { - key: "open", - label: isFolder ? "Open folder" : "Open file", - icon: isFolder ? : , - }, - {key: "copy", label: "Copy path", icon: }, - ] - if (onDownload) { - list.push({ - key: "download", - label: isFolder ? "Download as zip" : "Download", - icon: , - }) - } - return list - }, [isFolder, onDownload]) - const onClick: NonNullable = ({key, domEvent}) => { - // The tiles/rows are themselves click targets — keep a menu click from also selecting them. - domEvent.stopPropagation() - if (key === "open") onOpen() - else if (key === "copy") onCopyPath(path) - else if (key === "download") onDownload?.(path, isFolder) - } + // The tiles/rows are themselves click targets — keep a menu click from also selecting them. + const stop = (event: {stopPropagation: () => void}) => event.stopPropagation() + return ( - -
{children}
-
+ + +
{children}
+
+ + + {isFolder ? : } + {isFolder ? "Open folder" : "Open file"} + + onCopyPath(path)}> + + Copy path + + {onDownload ? ( + onDownload(path, isFolder)}> + + {isFolder ? "Download as zip" : "Download"} + + ) : null} + +
) } diff --git a/web/oss/src/components/Drives/DrivePendingTiles.tsx b/web/packages/agenta-entity-ui/src/drive/DrivePendingTiles.tsx similarity index 94% rename from web/oss/src/components/Drives/DrivePendingTiles.tsx rename to web/packages/agenta-entity-ui/src/drive/DrivePendingTiles.tsx index 0cea60c63a..aef00b5c06 100644 --- a/web/oss/src/components/Drives/DrivePendingTiles.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DrivePendingTiles.tsx @@ -4,10 +4,10 @@ * both thin adapters over the ONE {@link PendingTile} look. Rendered by {@link FolderView}; the state * behind them lives in {@link useDriveUploads}. */ +import {type StagedTileItem} from "@agenta/entities/drive" +import {type MountUploadItem} from "@agenta/entities/drive" import {File as FileIcon, X} from "@phosphor-icons/react" -import {type MountUploadItem} from "./useMountUpload" - /** The ONE grid tile for a pending item (staged or in-flight upload) — a real-file-tile look with a * subtle primary tint. `UploadTile`/`StagedTile` are thin adapters that map their item to this. */ type PendingTileState = "staged" | "uploading" | "error" | "done" @@ -44,7 +44,6 @@ const PendingTile = ({
{previewUrl ? ( - // eslint-disable-next-line @next/next/no-img-element {name} onRemove(item.id) : undefined} /> ) + +export type {StagedTileItem} diff --git a/web/oss/src/components/Drives/DriveToolbar.tsx b/web/packages/agenta-entity-ui/src/drive/DriveToolbar.tsx similarity index 96% rename from web/oss/src/components/Drives/DriveToolbar.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveToolbar.tsx index 7acc45b506..c000e0a45a 100644 --- a/web/oss/src/components/Drives/DriveToolbar.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveToolbar.tsx @@ -3,11 +3,12 @@ * search box, the origin segmented control, and the hidden / git-ignored visibility toggles. Pure * presentation over {@link useDriveFilters} + {@link useDriveTreePane} state. */ +import {type FileOrigin} from "@agenta/entities/drive" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {InputAffix as Input, Segmented, SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {Eye, EyeClosed, FileDashed, MagnifyingGlass, SidebarSimple} from "@phosphor-icons/react" -import {Button, Input, Segmented, Tooltip} from "antd" import {ORIGIN_TIP} from "./OriginTag" -import {type FileOrigin} from "./useSessionDrive" export function DriveToolbar({ search, diff --git a/web/oss/src/components/Drives/DriveTreeList.tsx b/web/packages/agenta-entity-ui/src/drive/DriveTreeList.tsx similarity index 91% rename from web/oss/src/components/Drives/DriveTreeList.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveTreeList.tsx index 5f3a38b051..c648850249 100644 --- a/web/oss/src/components/Drives/DriveTreeList.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveTreeList.tsx @@ -6,18 +6,27 @@ */ import {type Dispatch, type SetStateAction} from "react" -import {Typography} from "antd" +import {revealFade} from "@agenta/entities/drive" +import {parentOf, type FlatTreeRow} from "@agenta/entities/drive" +import {type DriveDrop} from "@agenta/entities/drive" +import {type DriveTreeViewport} from "@agenta/entities/drive" +import {type MountUploadItem} from "@agenta/entities/drive" import {motion} from "motion/react" import {DriveItemContextMenu} from "./DriveItemContextMenu" -import {revealFade} from "./driveMotion" import {TreeLoadingRow, TreeRow} from "./DriveTreeRow" -import {parentOf, type FlatTreeRow} from "./driveTreeView" -import {type DriveDrop} from "./useDriveDrop" -import {type DriveTreeViewport} from "./useDriveTreeViewport" -import {type MountUploadItem} from "./useMountUpload" -const {Text} = Typography +/** antd `Typography.Text` stand-in: the only prop this module used is `type="secondary"`. */ +const Text = ({ + type: _type, + className, + children, + ...rest +}: React.HTMLAttributes & {type?: "secondary"}) => ( + + {children} + +) export function DriveTreeList({ flatRows, diff --git a/web/oss/src/components/Drives/DriveTreePane.tsx b/web/packages/agenta-entity-ui/src/drive/DriveTreePane.tsx similarity index 97% rename from web/oss/src/components/Drives/DriveTreePane.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveTreePane.tsx index 508767a03f..bda0ec1f41 100644 --- a/web/oss/src/components/Drives/DriveTreePane.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveTreePane.tsx @@ -5,11 +5,10 @@ */ import {type KeyboardEvent, type ReactNode} from "react" +import {type DriveDrop} from "@agenta/entities/drive" +import {type DriveTreePaneState} from "@agenta/entities/drive" import {motion} from "motion/react" -import {type DriveDrop} from "./useDriveDrop" -import {type DriveTreePaneState} from "./useDriveTreePane" - export function DriveTreePane({ pane, treeScrollRef, diff --git a/web/oss/src/components/Drives/DriveTreeRow.tsx b/web/packages/agenta-entity-ui/src/drive/DriveTreeRow.tsx similarity index 98% rename from web/oss/src/components/Drives/DriveTreeRow.tsx rename to web/packages/agenta-entity-ui/src/drive/DriveTreeRow.tsx index d6f15ad358..70bbc1d304 100644 --- a/web/oss/src/components/Drives/DriveTreeRow.tsx +++ b/web/packages/agenta-entity-ui/src/drive/DriveTreeRow.tsx @@ -1,13 +1,13 @@ import {useEffect, useRef} from "react" +import {humanSize, isHiddenPath, type DriveTreeNode} from "@agenta/entities/drive" +import {type MountUploadItem} from "@agenta/entities/drive" +import {fileOrigin} from "@agenta/entities/drive" import {CaretDown, CaretRight, CircleNotch, FolderSimple} from "@phosphor-icons/react" import {FOCUS_RING} from "./DriveFileRow" import {driveFileIcon} from "./driveIcons" -import {humanSize, isHiddenPath, type DriveTreeNode} from "./driveTree" import {OriginTag} from "./OriginTag" -import {type MountUploadItem} from "./useMountUpload" -import {fileOrigin} from "./useSessionDrive" /** One tree row (folder or file), indented by depth; selection = fill + primary ring. Renders a * SINGLE row — the hierarchy is materialized by {@link flattenTree}, not recursion, so each row is diff --git a/web/oss/src/components/Drives/FileThumb.tsx b/web/packages/agenta-entity-ui/src/drive/FileThumb.tsx similarity index 94% rename from web/oss/src/components/Drives/FileThumb.tsx rename to web/packages/agenta-entity-ui/src/drive/FileThumb.tsx index 03a2a02be2..585d598816 100644 --- a/web/oss/src/components/Drives/FileThumb.tsx +++ b/web/packages/agenta-entity-ui/src/drive/FileThumb.tsx @@ -18,15 +18,14 @@ */ import {memo, useState} from "react" +import {resolveDriveFileKind, type DriveFileKind} from "@agenta/entities/drive" +import {mountFileDownloadUrl, mountFileThumbnailQueryFamily} from "@agenta/entities/drive" +import {type DriveRecentFile} from "@agenta/entities/drive" import {mountFileContentQueryFamily, type Mount} from "@agenta/entities/session" +import {projectIdAtom} from "@agenta/shared/state" import {useAtomValue} from "jotai" -import {projectIdAtom} from "@/oss/state/project" - import {driveFileIcon} from "./driveIcons" -import {resolveDriveFileKind, type DriveFileKind} from "./driveKinds" -import {mountFileDownloadUrl, mountFileThumbnailQueryFamily} from "./driveMedia" -import {type DriveRecentFile} from "./useSessionDrive" const IMG_CAP = 8 * 1024 * 1024 const PDF_CAP = 4 * 1024 * 1024 @@ -91,7 +90,7 @@ function FileThumbImpl({ if (isImage && imgUrl && !failed) { return (
- {/* eslint-disable-next-line @next/next/no-img-element -- generated data URL; next/image can't optimize it */} + {/* generated data URL; next/image can't optimize it */} {/* object-top so the tile shows the page's title area, not its middle. */} - {/* eslint-disable-next-line @next/next/no-img-element -- generated data URL */} + {/* generated data URL */} import("./DriveExplorer").then((m) => m.DriveExplorer), { ssr: false, - loading: () => , + loading: () => , }) /** diff --git a/web/oss/src/components/Drives/FolderTile.tsx b/web/packages/agenta-entity-ui/src/drive/FolderTile.tsx similarity index 95% rename from web/oss/src/components/Drives/FolderTile.tsx rename to web/packages/agenta-entity-ui/src/drive/FolderTile.tsx index 88ceed0f31..7945c2454b 100644 --- a/web/oss/src/components/Drives/FolderTile.tsx +++ b/web/packages/agenta-entity-ui/src/drive/FolderTile.tsx @@ -1,7 +1,7 @@ +import {isHiddenPath, type DriveTreeNode} from "@agenta/entities/drive" import {FolderSimple} from "@phosphor-icons/react" import {FOCUS_RING} from "./DriveFileRow" -import {isHiddenPath, type DriveTreeNode} from "./driveTree" /** A subfolder tile — same shape as the file tile (4:3 icon "thumbnail" + name + meta) so folders * and files form ONE uniform grid instead of short folder cards stretching to the file-tile height. */ diff --git a/web/oss/src/components/Drives/FolderView.tsx b/web/packages/agenta-entity-ui/src/drive/FolderView.tsx similarity index 96% rename from web/oss/src/components/Drives/FolderView.tsx rename to web/packages/agenta-entity-ui/src/drive/FolderView.tsx index 2f0fc59c03..53731624ec 100644 --- a/web/oss/src/components/Drives/FolderView.tsx +++ b/web/packages/agenta-entity-ui/src/drive/FolderView.tsx @@ -1,26 +1,27 @@ import {useEffect, useMemo, useRef, useState} from "react" +import {META_REVEAL, PANE_FADE, revealFade} from "@agenta/entities/drive" +import {useRepoInfo} from "@agenta/entities/drive" +import {humanSize, type DriveTreeNode} from "@agenta/entities/drive" +import {parentOf} from "@agenta/entities/drive" +import {isRecentlyChanged, useRecentChangeClock} from "@agenta/entities/drive" +import {useDelayedTrue} from "@agenta/entities/drive" +import {type DriveDrop} from "@agenta/entities/drive" +import {type MountUploadItem} from "@agenta/entities/drive" +import {type SessionDriveData} from "@agenta/entities/drive" import {CopyButton} from "@agenta/ui/components/presentational" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" import {FolderSimple, GitBranch, Tray} from "@phosphor-icons/react" -import {Button, Tooltip} from "antd" import {AnimatePresence, motion} from "motion/react" import {DriveBreadcrumb} from "./DriveBreadcrumb" import {TileGridSkeleton} from "./DriveExplorerSkeleton" import {DriveFileRow} from "./DriveFileRow" import {DriveItemContextMenu, useCopyDrivePath, useDriveItemDownload} from "./DriveItemContextMenu" -import {META_REVEAL, PANE_FADE, revealFade} from "./driveMotion" import {StagedTile, UploadTile, type StagedTileItem} from "./DrivePendingTiles" -import {useRepoInfo} from "./driveRepo" -import {humanSize, type DriveTreeNode} from "./driveTree" -import {parentOf} from "./driveTreeView" import {FolderTile} from "./FolderTile" -import {isRecentlyChanged, useRecentChangeClock} from "./recentChange" import {DriveRepoMetaList} from "./repoMeta" -import {useDelayedTrue} from "./useDelayedTrue" -import {type DriveDrop} from "./useDriveDrop" -import {type MountUploadItem} from "./useMountUpload" -import {type SessionDriveData} from "./useSessionDrive" import {VirtualTileGrid} from "./VirtualTileGrid" /** Right pane when a FOLDER is selected: fixed header (clickable breadcrumb + folder name) over a diff --git a/web/oss/src/components/Drives/OriginTag.tsx b/web/packages/agenta-entity-ui/src/drive/OriginTag.tsx similarity index 93% rename from web/oss/src/components/Drives/OriginTag.tsx rename to web/packages/agenta-entity-ui/src/drive/OriginTag.tsx index ea02ba7eed..29e7b8b055 100644 --- a/web/oss/src/components/Drives/OriginTag.tsx +++ b/web/packages/agenta-entity-ui/src/drive/OriginTag.tsx @@ -4,9 +4,8 @@ * surface (rows/cards/tiles and the drawer tree) — only shown when a drive holds both kinds. The * tooltip spells out what each scope means (the tags alone don't). */ -import {Tooltip} from "antd" - -import {type FileOrigin} from "./useSessionDrive" +import {type FileOrigin} from "@agenta/entities/drive" +import {SimpleTooltip as Tooltip} from "@agenta/ui/ui" // Agent-teal, matching the config self-commit indicator. export const AGENT_ACCENT = "var(--ag-c-13C2C2, #13c2c2)" diff --git a/web/oss/src/components/Drives/SessionFilesDrawer.tsx b/web/packages/agenta-entity-ui/src/drive/SessionFilesDrawer.tsx similarity index 96% rename from web/oss/src/components/Drives/SessionFilesDrawer.tsx rename to web/packages/agenta-entity-ui/src/drive/SessionFilesDrawer.tsx index 3eea2469eb..8f84bd9814 100644 --- a/web/oss/src/components/Drives/SessionFilesDrawer.tsx +++ b/web/packages/agenta-entity-ui/src/drive/SessionFilesDrawer.tsx @@ -9,15 +9,15 @@ */ import {useMemo} from "react" +import {type DroppedFile} from "@agenta/entities/drive" +import {useSessionDriveSummary} from "@agenta/entities/drive" import {atom, useAtom} from "jotai" import {atomFamily} from "jotai/utils" import {type DriveId} from "./DriveExplorer" import {useDriveArtifactId} from "./driveSessionContext" -import {type DroppedFile} from "./dropEntries" import {FilesDrawer} from "./FilesDrawer" import {driveQuickLookAtomFamily} from "./quickLook" -import {useSessionDriveSummary} from "./useSessionDrive" // Keyed by session id — every mounted pane has its own host, so a shared open flag would leak the // drawer's open state across sessions on a tab switch. diff --git a/web/oss/src/components/Drives/StorageFilesHeader.tsx b/web/packages/agenta-entity-ui/src/drive/StorageFilesHeader.tsx similarity index 88% rename from web/oss/src/components/Drives/StorageFilesHeader.tsx rename to web/packages/agenta-entity-ui/src/drive/StorageFilesHeader.tsx index 25bfb77158..4b1a13f1f8 100644 --- a/web/oss/src/components/Drives/StorageFilesHeader.tsx +++ b/web/packages/agenta-entity-ui/src/drive/StorageFilesHeader.tsx @@ -6,19 +6,26 @@ * file). Slotted into the entity-ui `AgentOperationsSections` header by the app layer, which owns * the chat session state that package can't reach. */ +import {configFilesDrawerAtomFamily, useConfigDrive} from "@agenta/entities/drive" +import {Skeleton} from "@agenta/ui/ui" import {CircleNotch, FolderOpen} from "@phosphor-icons/react" -import {Skeleton} from "antd" import {useSetAtom} from "jotai" -import {configFilesDrawerAtomFamily, useConfigDrive} from "./configDrive" import {DriveWarningBadge, FOCUS_RING} from "./DriveFileRow" -export default function StorageFilesHeader({revisionId}: {revisionId?: string | null}) { - const {drive} = useConfigDrive(revisionId) +export default function StorageFilesHeader({ + revisionId, + sessionId, +}: { + revisionId?: string | null + /** The conversation whose drive this is. The host resolves it; empty = no conversation. */ + sessionId?: string | null +}) { + const {drive} = useConfigDrive(revisionId, sessionId) const setDrawer = useSetAtom(configFilesDrawerAtomFamily(revisionId ?? "")) if (drive.isLoading) { - return + return } if (drive.errored) { diff --git a/web/oss/src/components/Drives/StorageSection.tsx b/web/packages/agenta-entity-ui/src/drive/StorageSection.tsx similarity index 87% rename from web/oss/src/components/Drives/StorageSection.tsx rename to web/packages/agenta-entity-ui/src/drive/StorageSection.tsx index 9cebeb5752..84496bb533 100644 --- a/web/oss/src/components/Drives/StorageSection.tsx +++ b/web/packages/agenta-entity-ui/src/drive/StorageSection.tsx @@ -10,26 +10,34 @@ */ import {useMemo} from "react" +import {isAgentFileUploadsEnabled} from "@agenta/entities/drive" +import {configFilesDrawerAtomFamily, useConfigDrive} from "@agenta/entities/drive" +import {listArrowKeyDown} from "@agenta/entities/drive" +import {FILE_ITEM_VARIANTS, FILE_SPRING} from "@agenta/entities/drive" +import {humanSize, relativeTime} from "@agenta/entities/drive" +import {isRecentlyChanged, useRecentChangeClock} from "@agenta/entities/drive" +import {useStageDrop} from "@agenta/entities/drive" +import {driveHasMixedOrigins, type DriveRecentFile} from "@agenta/entities/drive" import {CircleNotch} from "@phosphor-icons/react" -import {Typography} from "antd" import {useAtom} from "jotai" import {AnimatePresence, MotionConfig, motion} from "motion/react" -import {isAgentFileUploadsEnabled} from "@/oss/components/AgentChatSlice/assets/constants" - -import {configFilesDrawerAtomFamily, useConfigDrive} from "./configDrive" import {type DriveId} from "./DriveExplorer" import {DriveFileRow, DriveRetryButton, SKELETON_ROW_COUNT} from "./DriveFileRow" import {DriveItemContextMenu, useCopyDrivePath, useDriveItemDownload} from "./DriveItemContextMenu" -import {listArrowKeyDown} from "./driveKeyboard" -import {FILE_ITEM_VARIANTS, FILE_SPRING} from "./driveMotion" -import {humanSize, relativeTime} from "./driveTree" import {FilesDrawer} from "./FilesDrawer" -import {isRecentlyChanged, useRecentChangeClock} from "./recentChange" -import {useStageDrop} from "./useDriveDrop" -import {driveHasMixedOrigins, type DriveRecentFile} from "./useSessionDrive" -const {Text} = Typography +/** antd `Typography.Text` stand-in: the only prop this module used is `type="secondary"`. */ +const Text = ({ + type: _type, + className, + children, + ...rest +}: React.HTMLAttributes & {type?: "secondary"}) => ( + + {children} + +) const RecentFileRow = ({ file, @@ -76,8 +84,15 @@ const RecentFileRow = ({ ) -export default function StorageSection({revisionId}: {revisionId?: string | null}) { - const {drive, sessionId} = useConfigDrive(revisionId) +export default function StorageSection({ + revisionId, + sessionId, +}: { + revisionId?: string | null + /** The conversation whose drive this is. The host resolves it; empty = no conversation. */ + sessionId?: string | null +}) { + const {drive} = useConfigDrive(revisionId, sessionId) // Drawer request is shared with the Files header (which opens it at the root); rows open it // preselected on the clicked file. const [drawer, setDrawer] = useAtom(configFilesDrawerAtomFamily(revisionId ?? "")) @@ -154,7 +169,12 @@ export default function StorageSection({revisionId}: {revisionId?: string | null ? Array.from({length: SKELETON_ROW_COUNT}, (_, i) => ( ( | null = null + +export const registerDriveCodeBlock = (component: ComponentType): void => { + codeBlockRenderer = component +} + +export const DriveCodeBlock = ({language, value}: DriveCodeBlockProps) => { + const Renderer = codeBlockRenderer + if (Renderer) return + return
{value}
+} + +export interface DriveMarkdownProps { + content: string + className?: string +} + +/** + * The markdown renderer the file preview uses. + * + * Injected rather than imported: the desktop renders its chat Markdown component (Lexical/Prism + * chrome and all), and another host may render its own. Registered once at startup; until then a + * preview falls back to plain preformatted text rather than failing to render. + */ +let markdownRenderer: ComponentType | null = null + +export const registerDriveMarkdown = (component: ComponentType): void => { + markdownRenderer = component +} + +export const DriveMarkdown = ({content, className}: DriveMarkdownProps) => { + const Renderer = markdownRenderer + if (Renderer) return + return ( +
+            {content}
+        
+ ) +} diff --git a/web/oss/src/components/Drives/driveSessionContext.tsx b/web/packages/agenta-entity-ui/src/drive/driveSessionContext.tsx similarity index 100% rename from web/oss/src/components/Drives/driveSessionContext.tsx rename to web/packages/agenta-entity-ui/src/drive/driveSessionContext.tsx diff --git a/web/oss/src/components/Drives/fileMeta.tsx b/web/packages/agenta-entity-ui/src/drive/fileMeta.tsx similarity index 98% rename from web/oss/src/components/Drives/fileMeta.tsx rename to web/packages/agenta-entity-ui/src/drive/fileMeta.tsx index f804312afb..71473ba0e7 100644 --- a/web/oss/src/components/Drives/fileMeta.tsx +++ b/web/packages/agenta-entity-ui/src/drive/fileMeta.tsx @@ -9,15 +9,14 @@ */ import {useEffect, useState} from "react" +import {fileTypeLabel, resolveDriveFileKind, type DriveFileKind} from "@agenta/entities/drive" +import {useMountFileMediaSrc} from "@agenta/entities/drive" +import {humanSize, relativeTime} from "@agenta/entities/drive" import {mountFileContentQueryFamily, type Mount} from "@agenta/entities/session" import {CaretRight} from "@phosphor-icons/react" import {useAtomValue} from "jotai" import {AnimatePresence, motion} from "motion/react" -import {fileTypeLabel, resolveDriveFileKind, type DriveFileKind} from "./driveKinds" -import {useMountFileMediaSrc} from "./driveMedia" -import {humanSize, relativeTime} from "./driveTree" - // Extension → MIME (best-effort; the listing carries no content-type). Mirrors the renderer's // kind table so previews and this block never disagree about what a file is. const MIME: Record = { diff --git a/web/packages/agenta-entity-ui/src/drive/index.ts b/web/packages/agenta-entity-ui/src/drive/index.ts new file mode 100644 index 0000000000..d8aa643622 --- /dev/null +++ b/web/packages/agenta-entity-ui/src/drive/index.ts @@ -0,0 +1,47 @@ +/** + * Drive (agent files) — UI. The Files region, the browse drawer, the explorer/tree/preview and + * their rows, plus the toast-reporting download hooks, over the headless layer in + * `@agenta/entities/drive`. antd-free so the mobile app renders it. + */ +export * from "./ContextRail" +export * from "./DriveBreadcrumb" +export * from "./DriveExplorer" +export * from "./DriveExplorerSkeleton" +export * from "./DriveExplorerStates" +export * from "./DriveFileCard" +export * from "./DriveFileContentViewer" +export * from "./DriveFilePreview" +export * from "./DriveFileRow" +export * from "./DriveHeader" +export * from "./DriveItemContextMenu" +export * from "./DrivePendingTiles" +export * from "./DriveToolbar" +export * from "./DriveTreeList" +export * from "./DriveTreePane" +export * from "./DriveTreeRow" +export * from "./FileThumb" +export * from "./FilesDrawer" +export * from "./FolderTile" +export * from "./FolderView" +export * from "./OriginTag" +export * from "./SessionFilesDrawer" +export * from "./StorageFilesHeader" +export * from "./StorageSection" +export * from "./VirtualTileGrid" +export * from "./driveFileSource" +export * from "./driveIcons" +export * from "./driveMarkdown" +export * from "./driveSessionContext" +export * from "./fileMeta" +export * from "./quickLook" +export * from "./renderers" +export * from "./repoMeta" +export * from "./useDriveDownloadAll" +export * from "./useDriveFileDownload" +export * from "./useDriveTreeData" +export * from "./useLazyDriveTree" +export * from "./useUploadReveal" + +// Default-exported components (`export *` does not carry a default). +export {default as StorageFilesHeader} from "./StorageFilesHeader" +export {default as StorageSection} from "./StorageSection" diff --git a/web/oss/src/components/Drives/quickLook.tsx b/web/packages/agenta-entity-ui/src/drive/quickLook.tsx similarity index 100% rename from web/oss/src/components/Drives/quickLook.tsx rename to web/packages/agenta-entity-ui/src/drive/quickLook.tsx diff --git a/web/oss/src/components/Drives/renderers.tsx b/web/packages/agenta-entity-ui/src/drive/renderers.tsx similarity index 91% rename from web/oss/src/components/Drives/renderers.tsx rename to web/packages/agenta-entity-ui/src/drive/renderers.tsx index b7b79e04ec..e779675e1c 100644 --- a/web/oss/src/components/Drives/renderers.tsx +++ b/web/packages/agenta-entity-ui/src/drive/renderers.tsx @@ -11,14 +11,15 @@ */ import {useEffect, useMemo, useRef, useState} from "react" +import {driveCodeLanguage, resolveDriveFileKind, type DriveFileKind} from "@agenta/entities/drive" +import {fetchMountFileBlob} from "@agenta/entities/drive" +import {humanSize} from "@agenta/entities/drive" import {type Mount} from "@agenta/entities/session" +import {projectIdAtom} from "@agenta/shared/state" +import {EnhancedButton as Button} from "@agenta/ui/components/presentational" +import {Segmented, Skeleton} from "@agenta/ui/ui" import {DownloadSimple, FileDashed} from "@phosphor-icons/react" -import {Button, Segmented, Skeleton} from "antd" import {useAtomValue} from "jotai" -import dynamic from "next/dynamic" - -import Markdown from "@/oss/components/AgentChatSlice/assets/markdown" -import {projectIdAtom} from "@/oss/state/project" import { useDriveDownload, @@ -26,16 +27,12 @@ import { useDriveMediaSrc, useDriveObjectUrl, } from "./driveFileSource" -import {driveCodeLanguage, resolveDriveFileKind, type DriveFileKind} from "./driveKinds" -import {fetchMountFileBlob} from "./driveMedia" -import {humanSize} from "./driveTree" +import {DriveCodeBlock, DriveMarkdown} from "./driveMarkdown" -// Lexical + lazy-Shiki code block (theme-paired highlighting). Loaded only when a code body -// actually opens — @lexical/code-shiki is an ~8.7 MB chunk that must stay out of first load. -const LazyCodeBlock = dynamic(() => import("@/oss/components/DynamicCodeBlock/CodeBlock"), { - ssr: false, - loading: () => , -}) +// The host's code viewer (see `registerDriveCodeBlock`). The desktop registers a Lexical + +// lazy-Shiki block — an ~8.7 MB chunk it keeps out of first load — so the indirection is also +// what stops that chunk reaching a host that never opens code files. +const LazyCodeBlock = DriveCodeBlock // Inline-render caps (bytes). Over-cap is a graceful card, never a frozen tab. const TEXT_CAP = 1.5 * 1024 * 1024 @@ -141,7 +138,11 @@ const TextBody = ({ if (contentQuery.isPending) return ( - +
+ {Array.from({length: 6}).map((_, i) => ( + + ))} +
) if (typeof content !== "string") @@ -154,7 +155,7 @@ const TextBody = ({ return (
- +
) @@ -188,7 +189,11 @@ const CodeBody = ({mount, path}: {mount: Mount | null; path: string}) => { if (contentQuery.isPending) return ( - +
+ {Array.from({length: 6}).map((_, i) => ( + + ))} +
) if (value == null) @@ -217,7 +222,11 @@ const CsvBody = ({mount, path}: {mount: Mount | null; path: string}) => { if (contentQuery.isPending) return ( - +
+ {Array.from({length: 6}).map((_, i) => ( + + ))} +
) if (typeof content !== "string" || rows.length === 0) @@ -443,7 +452,11 @@ const HtmlBody = ({ if (contentQuery.isPending) return ( - +
+ {Array.from({length: 6}).map((_, i) => ( + + ))} +
) if (typeof content !== "string") @@ -452,10 +465,10 @@ const HtmlBody = ({ return (
- - size="small" + setView(next as "preview" | "source")} options={[ {value: "preview", label: "Preview"}, {value: "source", label: "Source"}, @@ -465,7 +478,11 @@ const HtmlBody = ({ {view === "preview" ? ( assembled == null ? (
- +
+ {Array.from({length: 6}).map((_, i) => ( + + ))} +
) : ( // allow-scripts runs ONLY our interceptor (agent scripts were stripped); still no @@ -492,7 +509,11 @@ const HtmlBody = ({ const MediaLoading = () => ( - +
+ {Array.from({length: 5}).map((_, i) => ( + + ))} +
) @@ -508,7 +529,7 @@ const ImageBody = ({mount, path}: {mount: Mount | null; path: string}) => {
- {/* eslint-disable-next-line @next/next/no-img-element -- object URL, next/image can't optimize */} + {/* object URL, next/image can't optimize */} {path.split("/").pop() { const cwd = drive.mount diff --git a/web/packages/agenta-entity-ui/src/drive/useDriveFileDownload.ts b/web/packages/agenta-entity-ui/src/drive/useDriveFileDownload.ts new file mode 100644 index 0000000000..301226c1be --- /dev/null +++ b/web/packages/agenta-entity-ui/src/drive/useDriveFileDownload.ts @@ -0,0 +1,31 @@ +import {useCallback} from "react" + +import {downloadMountFile} from "@agenta/entities/drive" +import {type Mount} from "@agenta/entities/session" +import {projectIdAtom} from "@agenta/shared/state" +import {message} from "@agenta/ui/app-message" +import {useAtomValue} from "jotai" + +/** `download(mount, path)` for ONE drive file, reporting the outcome through the themed toast + * (the kit message service, which renders through the app's own outlet). THE way to trigger a single-file download: + * {@link downloadMountFile} resolves `false` on failure, so a bare fire-and-forget call left a failed + * click looking exactly like a successful one. Stable — safe to pass down to list items. */ +export function useDriveFileDownload(): (mount: Mount | null, path: string) => Promise { + const projectId = useAtomValue(projectIdAtom) + return useCallback( + async (mount: Mount | null, path: string) => { + // `path` is mount-RELATIVE, so it alone doesn't identify a file: `agent-files/notes.md` + // and a cwd `notes.md` both arrive here as "notes.md" and would share a toast. + const key = `drive-download:${mount?.id ?? "none"}:${path}` + message.open({type: "loading", key, content: "Downloading…", duration: 0}) + const ok = await downloadMountFile({mount, path, projectId}) + message.open( + ok + ? {type: "success", key, content: "Downloaded"} + : {type: "error", key, content: "Download failed"}, + ) + return ok + }, + [projectId], + ) +} diff --git a/web/oss/src/components/Drives/useDriveTreeData.ts b/web/packages/agenta-entity-ui/src/drive/useDriveTreeData.ts similarity index 97% rename from web/oss/src/components/Drives/useDriveTreeData.ts rename to web/packages/agenta-entity-ui/src/drive/useDriveTreeData.ts index 531e183258..d412aee10a 100644 --- a/web/oss/src/components/Drives/useDriveTreeData.ts +++ b/web/packages/agenta-entity-ui/src/drive/useDriveTreeData.ts @@ -7,12 +7,22 @@ */ import {useCallback, useEffect, useMemo, useRef, useState} from "react" +import { + buildDriveTree, + filterDriveTree, + isHiddenPath, + type DriveTreeNode, +} from "@agenta/entities/drive" +import { + EMPTY_STR_SET, + collectFolderPaths, + flattenTree, + looksLikeFilePath, +} from "@agenta/entities/drive" +import {fileOrigin, type FileOrigin, type SessionDriveData} from "@agenta/entities/drive" import {type MountFile} from "@agenta/entities/session" -import {buildDriveTree, filterDriveTree, isHiddenPath, type DriveTreeNode} from "./driveTree" -import {EMPTY_STR_SET, collectFolderPaths, flattenTree, looksLikeFilePath} from "./driveTreeView" import {useLazyDriveTree} from "./useLazyDriveTree" -import {fileOrigin, type FileOrigin, type SessionDriveData} from "./useSessionDrive" export function useDriveTreeData({ drive, diff --git a/web/oss/src/components/Drives/useLazyDriveTree.tsx b/web/packages/agenta-entity-ui/src/drive/useLazyDriveTree.tsx similarity index 98% rename from web/oss/src/components/Drives/useLazyDriveTree.tsx rename to web/packages/agenta-entity-ui/src/drive/useLazyDriveTree.tsx index 0de4f2c203..d6d028a26d 100644 --- a/web/oss/src/components/Drives/useLazyDriveTree.tsx +++ b/web/packages/agenta-entity-ui/src/drive/useLazyDriveTree.tsx @@ -12,12 +12,11 @@ */ import {useCallback, useEffect, useMemo, useReducer, useState, type ReactNode} from "react" +import {cleanPath} from "@agenta/entities/drive" +import {AGENT_FILES_DIR, type SessionDriveData} from "@agenta/entities/drive" import {mountDirQueryFamily, mountFilesQueryFamily, type MountFile} from "@agenta/entities/session" import {useAtomValue} from "jotai" -import {cleanPath} from "./driveTree" -import {AGENT_FILES_DIR, type SessionDriveData} from "./useSessionDrive" - /** The fold prefix that maps a mount-RELATIVE path back to its PRESENTED path — "" for the cwd mount, * "agent-files/" for the folded agent mount. Derived from (presentedPath, mountRelPath) so it needs no * reference to which mount it is: the presented path is the mount-relative path with the fold prepended. */ diff --git a/web/oss/src/components/Drives/useUploadReveal.ts b/web/packages/agenta-entity-ui/src/drive/useUploadReveal.ts similarity index 98% rename from web/oss/src/components/Drives/useUploadReveal.ts rename to web/packages/agenta-entity-ui/src/drive/useUploadReveal.ts index ed65b4d9da..af552e8ee5 100644 --- a/web/oss/src/components/Drives/useUploadReveal.ts +++ b/web/packages/agenta-entity-ui/src/drive/useUploadReveal.ts @@ -12,10 +12,9 @@ */ import {useCallback, useEffect, useRef, useState} from "react" +import {isHiddenPath} from "@agenta/entities/drive" import {type MountFile} from "@agenta/entities/session" -import {App} from "antd" - -import {isHiddenPath} from "./driveTree" +import {message} from "@agenta/ui/app-message" /** How long a batch waits for its destination listing to settle before we rule on it, and the hard * stop after which an unconfirmed batch is dropped without a verdict (never flip a filter on a @@ -102,7 +101,6 @@ export function useUploadReveal({ showGitignored: boolean setShowGitignored: (next: boolean) => void }) { - const {message} = App.useApp() const [batches, setBatches] = useState([]) // Completed paths held for BATCH_MS so a folder drop toasts once, not once per file. const buffer = useRef([]) diff --git a/web/oss/src/components/Drives/useUploadReveal.test.ts b/web/packages/agenta-entity-ui/tests/unit/useUploadReveal.test.ts similarity index 97% rename from web/oss/src/components/Drives/useUploadReveal.test.ts rename to web/packages/agenta-entity-ui/tests/unit/useUploadReveal.test.ts index a8aeb6f8fb..85d084fed7 100644 --- a/web/oss/src/components/Drives/useUploadReveal.test.ts +++ b/web/packages/agenta-entity-ui/tests/unit/useUploadReveal.test.ts @@ -1,6 +1,6 @@ import {describe, expect, it} from "vitest" -import {ruleOnBatch, type UploadBatch} from "./useUploadReveal" +import {ruleOnBatch, type UploadBatch} from "../../src/drive/useUploadReveal" const batch = (over: Partial = {}): UploadBatch => ({ key: "k",