Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions web/packages/agenta-entities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@
"./shared/openapi": "./src/shared/openapi/index.ts",
"./shared/execution": "./src/shared/execution/index.ts",
"./shared/execution/invocationErrors": "./src/shared/execution/invocationErrors.ts",
"./shared/invalidation": "./src/shared/invalidation/index.ts"
"./shared/invalidation": "./src/shared/invalidation/index.ts",
"./drive": "./src/drive/index.ts"
},
"dependencies": {
"@agentaai/api-client": "workspace:../agenta-api-client",
"@agenta/sdk": "workspace:../agenta-sdk",
"@agenta/shared": "workspace:../agenta-shared",
"@agenta/ui": "workspace:../agenta-ui",
"@agentaai/api-client": "workspace:../agenta-api-client",
"@tanstack/query-persist-client-core": "5.100.9",
"fast-deep-equal": "^3.1.3",
"jotai-scheduler": "^0.0.5",
"lodash": "^4.17.23",
"motion": "^12.0.0",
"openapi-json-schema": "^2.0.0",
"pdfjs-dist": "^4.10.38",
Comment on lines +80 to +82

Copy link
Copy Markdown
Contributor

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

"stable-hash": "^0.0.6",
"uuid": "^11.1.1",
"zod": "^4.3.6"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
* runner, so `null` covers both "backend not deployed yet" and "agent never ran": the App drive
* row simply stays gated until a mount appears, and lights up by itself once one does.
*/
import {queryAgentMounts, type Mount} from "@agenta/entities/session"
import {projectIdAtom} from "@agenta/shared/state"
import {atomFamily} from "jotai/utils"
import {atomWithQuery} from "jotai-tanstack-query"

import {projectIdAtom} from "@/oss/state/project"
import {queryAgentMounts, type Mount} from "@agenta/entities/session"

export const agentMountQueryFamily = atomFamily((artifactId: string) =>
atomWithQuery<Mount | null>((get) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@
* resolve the same session/artifact drive via {@link useConfigDrive} and share one drawer request
* via {@link configFilesDrawerAtomFamily}, keyed by the edited revision.
*/
import {isSessionFresh} from "@agenta/chat/state"
import {workflowMolecule} from "@agenta/entities/workflow"
import {atom, useAtomValue} from "jotai"
import {atomFamily} from "jotai/utils"

import {useChatScopeKey} from "@/oss/components/AgentChatSlice/state/scope"
import {
activeSessionIdAtomFamily,
sessionsListAtomFamily,
} from "@/oss/components/AgentChatSlice/state/sessions"
import {workflowMolecule} from "@agenta/entities/workflow"

import {type DroppedFile} from "./dropEntries"
import {useSessionDriveSummary, type SessionDriveData} from "./useSessionDrive"
Expand All @@ -33,28 +27,28 @@ export const configFilesDrawerAtomFamily = atomFamily((_revisionId: string) =>
)

/**
* The drive backing the config panel's Files region: the active conversation's cwd mount plus the
* agent's durable folder (resolved from the edited revision's artifact). Resolves the session id
* the same way the chat does — a stale active id (closed tab) falls back to the first open tab,
* and a brand-new never-run tab holds the queries off until its first run.
* The drive backing the config panel's Files region: the conversation's cwd mount plus the
* agent's durable folder (resolved from the edited revision's artifact).
*
* The SESSION is the host's to resolve and is passed in — the desktop derives it from its open
* chat tabs (a stale active id falls back to the first open tab, a never-run tab resolves to
* none), a session-scoped surface already knows it. An empty id holds the queries off, which is
* the "open a conversation to browse them here" state.
*/
export function useConfigDrive(revisionId?: string | null): {
export function useConfigDrive(
revisionId?: string | null,
sessionId?: string | null,
): {
drive: SessionDriveData
sessionId: string
artifactId?: string
} {
const scope = useChatScopeKey()
const artifactId = useAtomValue(workflowMolecule.selectors.workflowId(revisionId ?? ""))
const sessions = useAtomValue(sessionsListAtomFamily(scope))
const rawActiveId = useAtomValue(activeSessionIdAtomFamily(scope))
const resolvedId = sessions.some((s) => s.id === rawActiveId)
? rawActiveId
: (sessions[0]?.id ?? "")
const sessionId = resolvedId && !isSessionFresh(resolvedId) ? resolvedId : ""
const resolvedSessionId = sessionId ?? ""

// Summary only: the config header/body show a count + the latest handful. The browse drawer
// gets its own full drive, gated on open (see StorageSection), so the whole tree is never
// fetched just to render this always-mounted section.
const drive = useSessionDriveSummary(sessionId, artifactId ?? undefined)
return {drive, sessionId, artifactId: artifactId ?? undefined}
const drive = useSessionDriveSummary(resolvedSessionId, artifactId ?? undefined)
return {drive, sessionId: resolvedSessionId, artifactId: artifactId ?? undefined}
}
8 changes: 8 additions & 0 deletions web/packages/agenta-entities/src/drive/driveFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {getEnv} from "@agenta/shared"

/**
* Agent file uploads. Default ON; only an explicit "false" disables it.
* Lives here rather than in the chat slice so the drive code carries no app-layer import.
*/
export const isAgentFileUploadsEnabled = (): boolean =>
(getEnv("NEXT_PUBLIC_AGENT_FILE_UPLOADS") || "").toLowerCase() !== "false"
6 changes: 6 additions & 0 deletions web/packages/agenta-entities/src/drive/driveLabels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {type Mount} from "@agenta/entities/session"

/** Breadcrumb root label. Mount slugs are the RESERVED form (`__ag__<uuid5>__cwd`) — surface
* only the human tail ("cwd"), never the uuid (spec: raw ids stay out of labels). */
export const driveRootLabel = (mount: Mount | null): string =>
mount?.slug?.split("__").filter(Boolean).pop() ?? "cwd"
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@
* through the authenticated client once (cached blob → object URL). BACKEND ASK: an inline
* disposition (or real signed URLs) to let media stream natively.
*/
import {useCallback, useEffect, useMemo, useState} from "react"
import {useEffect, useMemo, useState} from "react"

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

Copy link
Copy Markdown
Contributor

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

import {projectIdAtom} from "@agenta/shared/state"
import {useAtomValue} from "jotai"
import {atomFamily} from "jotai/utils"
import {atomWithQuery} from "jotai-tanstack-query"

import axios from "@/oss/lib/api/assets/axiosConfig"
import {getAgentaApiUrl} from "@/oss/lib/helpers/api"
import {getJWT} from "@/oss/services/api"
import {projectIdAtom} from "@/oss/state/project"
import {type Mount} from "@agenta/entities/session"

import {renderPdfFirstPage} from "./pdfThumb"

Expand Down Expand Up @@ -202,7 +199,7 @@ export async function uploadMountFile({

/** Raw-bytes save of one file. Module-private on purpose: it reports failure as `false`, which every
* call site used to drop on the floor — go through {@link useDriveFileDownload}, which reports it. */
async function downloadMountFile({
export async function downloadMountFile({
mount,
path,
projectId,
Expand All @@ -225,31 +222,6 @@ async function downloadMountFile({
return true
}

/** `download(mount, path)` for ONE drive file, reporting the outcome through the themed toast
* (App.useApp, so it renders correctly in dark mode). 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<boolean> {
const {message} = App.useApp()
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
},
[message, projectId],
)
}

/** Download the WHOLE drive as ONE zip ("download all") — spanning every mount the drive folds in
* (cwd at the root, the agent's durable folder under `agent-files/`). The backend STREAMS the zip
* (never buffered whole) and reads file bodies with bounded concurrency.
Expand Down Expand Up @@ -300,7 +272,7 @@ export async function downloadMountArchive({
let writable: WritableFileStreamLike | null = null
try {
writable = await handle.createWritable()
const jwt = await getJWT()
const jwt = await getAuthToken()
const url = `${getAgentaApiUrl()}/mounts/files/export?project_id=${encodeURIComponent(projectId)}`
const response = await fetch(url, {
method: "POST",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* clone keeps objects in a packfile (zlib + deltas), impractical to unpack in the browser — so we
* surface branch, remote and the HEAD short SHA, which are all plain text.
*/
import {mountFileContentQueryFamily, type Mount} from "@agenta/entities/session"
import {useAtomValue} from "jotai"

import {mountFileContentQueryFamily, type Mount} from "@agenta/entities/session"

const SHA_RE = /^[0-9a-f]{40}$/i

export interface RepoHead {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ export interface DriveId {
label: string
value: string
}

/** A file staged for upload but not yet committed to the drive. */
export interface StagedTileItem {
id: string
name: string
file: File
/** Object URL for an image preview, else null (icon fallback). Owned by DriveExplorer. */
previewUrl: string | null
Comment on lines +19 to +20

Copy link
Copy Markdown
Contributor

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

}
34 changes: 34 additions & 0 deletions web/packages/agenta-entities/src/drive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Drive (agent files) — HEADLESS. Queries, tree building, uploads/downloads, drop parsing and the
* session/artifact resolution behind the config panel's Files region. Zero JSX and zero UI imports:
* rendering lives in `@agenta/entity-ui/drive`, and anything app-specific (which conversation,
* which markdown renderer, the bearer token) arrives as a parameter or a registration.
*/
export * from "./agentDrive"
export * from "./configDrive"
export * from "./driveFlags"
export * from "./driveKeyboard"
export * from "./driveKinds"
export * from "./driveLabels"
export * from "./driveMedia"
export * from "./driveMotion"
export * from "./driveRepo"
export * from "./driveTree"
export * from "./driveTreeView"
export * from "./driveTypes"
export * from "./dropEntries"
export * from "./pdfThumb"
export * from "./recentChange"
export * from "./useDelayedTrue"
export * from "./useDriveDrop"
export * from "./useDriveFilters"
export * from "./useDriveSelection"
export * from "./useDriveTreeKeyboard"
export * from "./useDriveTreePane"
export * from "./useDriveTreeReveal"
export * from "./useDriveTreeViewport"
export * from "./useDriveUploads"
export * from "./useImagePreviews"
export * from "./useMountUpload"
export * from "./useSessionDrive"
export * from "./useTreeGroupScroll"
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import {useCallback, useEffect, useMemo, useRef} from "react"

import {type MountFile} from "@agenta/entities/session"

import {isAgentFileUploadsEnabled} from "@/oss/components/AgentChatSlice/assets/constants"

import {type StagedTileItem} from "./DrivePendingTiles"
import {isAgentFileUploadsEnabled} from "./driveFlags"
import {type StagedTileItem} from "./driveTypes"
import {type DroppedFile} from "./dropEntries"
import {useDriveDrop} from "./useDriveDrop"
import {useImagePreviews} from "./useImagePreviews"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {useCallback, useEffect, useMemo, useRef, useState} from "react"

import {type Mount} from "@agenta/entities/session"
import {projectIdAtom} from "@agenta/shared/state"
import {useAtomValue} from "jotai"
import {queryClientAtom} from "jotai-tanstack-query"

import {projectIdAtom} from "@/oss/state/project"
import {type Mount} from "@agenta/entities/session"

import {uploadMountFile} from "./driveMedia"
import {type DroppedFile} from "./dropEntries"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {useCallback, useMemo} from "react"

import {useAtomValue} from "jotai"

import {
latestMountFilesQueryFamily,
mountFilesQueryFamily,
Expand All @@ -11,7 +13,6 @@ import {
type Mount,
type MountFile,
} from "@agenta/entities/session"
import {useAtomValue} from "jotai"

import {agentMountQueryFamily} from "./agentDrive"
import {cleanPath, driveFileStats, isInternalDrivePath, relativeTime} from "./driveTree"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import {beforeEach, describe, expect, it, vi} from "vitest"

import axios from "@/oss/lib/api/assets/axiosConfig"
import {axios} from "@agenta/shared/api"

import {uploadMountFile} from "./driveMedia"
import {uploadMountFile} from "../../src/drive/driveMedia"
import {
collectDropEntry,
type DropEntry,
type DropEntryReader,
readDroppedFiles,
} from "./dropEntries"
} from "../../src/drive/dropEntries"

vi.mock("@/oss/lib/api/assets/axiosConfig", () => ({
default: {post: vi.fn(), get: vi.fn()},
}))

vi.mock("@/oss/lib/helpers/api", () => ({
vi.mock("@agenta/shared/api", () => ({
axios: {post: vi.fn(), get: vi.fn()},
getAgentaApiUrl: vi.fn(() => "https://api.example.test"),
getAuthToken: vi.fn(async () => undefined),
}))

const file = (name: string) => new File([name], name, {type: "text/plain"})
Expand Down
Loading