diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 88a9eec63a..7451052cd0 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -132,11 +132,16 @@ directories are not supported by this checkpoint format. ## API and UI -The task, agent, project, and current-user pages expose a Files dialog using the -shared file tree and viewer. It supports uploads, folder creation, previews, -downloads, deletion, trash restore/purge, and sync state with the last agent save -time. Direct file-operation timestamps are labeled separately as “Files updated”; -a delete, restore, or idempotent receipt is not presented as an agent checkpoint. +The task, agent, project, and current-user pages do not expose stored-file +browsing. The 2026-09-08 design review removed these entry points: durable copies +can lag behind a running sandbox and do not represent its complete filesystem. +The persistence and synchronization APIs remain available to the runtime. + +Two separate features are deferred: debug inspection of persisted collections, +and authorized inspection of the live sandbox filesystem across its task, +agent, user, project, and repository context. The stored-file browser remains +only as an unshipped Storybook prototype. It must not be presented as a live +sandbox view. Page stories show the current pages without Files buttons. All routes start at `/api/companies/:companyId/work-folders/:scope/:ownerId`: @@ -166,7 +171,9 @@ new pinned staging stack with the branch's Cloud image and matching migrator. The deployed harness must target that tenant URL without launching a local server. Enumerate every sandbox-capable adapter/engine and native profile exposed by the stack; missing credentials or skipped required profiles block -acceptance. Record real browser operations, two actual 180-second intervals, +acceptance. Verify that the removed stored-file entry points are absent from +the UI; browser-based file-management acceptance is deferred with the separate +inspection features. Record API/runner operations, two actual 180-second intervals, short-run flushes, independent task checkouts, identity/privacy boundaries, interrupted saves, and recovery without the original sandbox or app volume. diff --git a/ui/src/components/WorkFolderBrowser.tsx b/ui/src/components/WorkFolderBrowser.tsx index 28761bac8e..8db51ad482 100644 --- a/ui/src/components/WorkFolderBrowser.tsx +++ b/ui/src/components/WorkFolderBrowser.tsx @@ -1,6 +1,6 @@ import { useId, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Download, FolderOpen, FolderPlus, RefreshCw, RotateCcw, Trash2, Upload } from "lucide-react"; +import { Download, FolderPlus, RefreshCw, RotateCcw, Trash2, Upload } from "lucide-react"; import type { WorkFile, WorkFolderOwner } from "@paperclipai/shared"; import { workFoldersApi } from "@/api/work-folders"; import { FileTree, type FileTreeNode } from "@/components/FileTree"; @@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; function tree(files: WorkFile[]) { const root: FileTreeNode = { name: "", path: "", kind: "dir", children: [] }; @@ -30,16 +29,6 @@ function tree(files: WorkFile[]) { sort(root.children); return root.children; } -export function WorkFolderButton({ owner, label = "Files" }: { owner: WorkFolderOwner; label?: string }) { - return - - {label} - {owner.scope === "user" ? "Your private Paperclip files in this company." : `Files shared with this ${owner.scope}'s sandbox runs.`} Changes from running agents are saved every three minutes and when a run ends. - - - ; -} - export function WorkFolderBrowser({ owner, exampleFiles }: { owner: WorkFolderOwner; exampleFiles?: WorkFile[] }) { const queryClient = useQueryClient(); const [trash, setTrash] = useState(false); diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 7f90fcc257..626e6cd02a 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -59,7 +59,6 @@ import { cn } from "../lib/utils"; import { describeRunRetryState } from "../lib/runRetryState"; import { Button } from "@/components/ui/button"; import { Tabs } from "@/components/ui/tabs"; -import { WorkFolderButton } from "@/components/WorkFolderBrowser"; import { PageTabBar } from "../components/PageTabBar"; import { AuditFeed } from "./audit/AuditFeed"; import { Skeleton } from "@/components/ui/skeleton"; @@ -1374,8 +1373,6 @@ export function AgentDetail() { ) : null} - - {actionError &&

{actionError}

} {isPendingApproval && (
diff --git a/ui/src/pages/DesignGuide.tsx b/ui/src/pages/DesignGuide.tsx index 4b89e233f5..5bdd23e832 100644 --- a/ui/src/pages/DesignGuide.tsx +++ b/ui/src/pages/DesignGuide.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { WorkFolderBrowser } from "@/components/WorkFolderBrowser"; import { ServicesList } from "./apps/app-detail/ServicesPanel"; import { ComposioProvenanceChip } from "./apps/ComposioProvenanceChip"; import type { ComposioServiceRow } from "./apps/composio-services"; @@ -483,14 +482,6 @@ export function DesignGuide() { {/* ============================================================ */} {/* COVERAGE */} {/* ============================================================ */} -
- - -
-

This page should be updated when new UI primitives or app-level patterns ship. diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 5d31247f23..00dde7513c 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -171,7 +171,6 @@ import { useStreamlinedUiEnabled } from "../hooks/useStreamlinedUiEnabled"; import { workModeMetaFor } from "../lib/work-mode-meta"; import { IssueContinuationHandoff } from "../components/IssueContinuationHandoff"; import { IssueAttachmentsSection } from "../components/IssueAttachmentsSection"; -import { WorkFolderButton } from "@/components/WorkFolderBrowser"; import { IssueDocumentsSection } from "../components/IssueDocumentsSection"; import { IssuePlanDecompositionsSection } from "../components/IssuePlanDecompositionsSection"; import { IssueOutputSection } from "../components/issue-output/IssueOutputSection"; @@ -7470,8 +7469,6 @@ export function IssueDetail() { /> )} - - {taskChatShellEnabled ? null : ( - {selectedCompanyId && }

); diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index 68405992a6..8d9a64df7e 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -37,7 +37,6 @@ import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { Tabs } from "@/components/ui/tabs"; -import { WorkFolderButton } from "@/components/WorkFolderBrowser"; import { PluginLauncherOutlet } from "@/plugins/launchers"; import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots"; import { @@ -822,7 +821,6 @@ export function ProjectDetail() { ) : null}
- ( - + args: { scope: "task" }, + argTypes: { scope: { control: false } }, + render: ({ scope }: { scope: WorkFolderScope }) => ( + ), -} satisfies Meta<{ scope: WorkFolderScope; scenario: WorkFolderScenario }>; +} satisfies Meta<{ scope: WorkFolderScope }>; export default meta; type Story = StoryObj; -const openFiles: NonNullable = async ({ - canvasElement, - args, -}) => { - const label = workFolderLabels[args.scope]; - await userEvent.click( - await within(canvasElement).findByRole( - "button", - { name: label }, - { timeout: 15000 }, - ), - ); - await expect( - await within(canvasElement.ownerDocument.body).findByRole("dialog", { - name: label, - }), - ).toBeVisible(); -}; export const TaskPage: Story = { args: { scope: "task" } }; -export const TaskFilesOpen: Story = { - args: { scope: "task" }, - play: openFiles, -}; export const AgentPage: Story = { args: { scope: "agent" } }; -export const AgentFilesOpen: Story = { - args: { scope: "agent" }, - play: openFiles, -}; export const ProjectPage: Story = { args: { scope: "project" } }; -export const ProjectFilesOpen: Story = { - args: { scope: "project" }, - play: openFiles, -}; export const ProfileSettingsPage: Story = { args: { scope: "user" } }; -export const MyFilesOpen: Story = { args: { scope: "user" }, play: openFiles }; -export const TaskSaveFailed: Story = { - args: { scope: "task", scenario: "failed" }, - play: openFiles, -}; -export const ProjectEmptyFolder: Story = { - args: { scope: "project", scenario: "empty" }, - play: openFiles, -}; -export const MobileTaskFilesOpen: Story = { +export const MobileTaskPage: Story = { args: { scope: "task" }, globals: { viewport: { value: "mobile" } }, - play: openFiles, }; diff --git a/ui/storybook/stories/work-folders.stories.tsx b/ui/storybook/stories/work-folders.stories.tsx index bf6597445b..b7d6dc12d1 100644 --- a/ui/storybook/stories/work-folders.stories.tsx +++ b/ui/storybook/stories/work-folders.stories.tsx @@ -1,12 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import { expect, userEvent, within } from "storybook/test"; -import { - WorkFolderBrowser, - WorkFolderButton, -} from "@/components/WorkFolderBrowser"; +import { WorkFolderBrowser } from "@/components/WorkFolderBrowser"; import { WorkFolderStoryProvider } from "../fixtures/WorkFolderStoryProvider"; import { - workFolderLabels, workFolderOwners, type WorkFolderScenario, } from "../fixtures/workFolders"; @@ -15,20 +11,19 @@ import type { WorkFolderScope } from "@paperclipai/shared"; type Args = { scope: WorkFolderScope; scenario: WorkFolderScenario; - presentation: "browser" | "button"; }; const meta = { - title: "Work folders/Components", + title: "Work folders/Stored-file prototype", parameters: { layout: "padded", docs: { description: { component: - "The actual reusable WorkFolderBrowser and WorkFolderButton. All file operations are in-memory fixtures: upload, create folder, delete, restore, purge, refresh, and download are safe to try. Change scope in Controls; use the toolbar for dark/light and mobile review. Reload a story to reset its files.", + "Unshipped stored-file browser prototype. Application entry points were removed because saved copies are not the live sandbox filesystem. This Storybook-only review surface uses disposable in-memory data; debug inspection and live sandbox browsing need separate designs.", }, }, }, - args: { scope: "task", scenario: "saved", presentation: "browser" }, + args: { scope: "task", scenario: "saved" }, argTypes: { scope: { control: "select", options: ["task", "agent", "project", "user"] }, scenario: { @@ -43,22 +38,15 @@ const meta = { "uploadFailed", ], }, - presentation: { control: "radio", options: ["browser", "button"] }, }, - render: ({ scope, scenario, presentation }) => ( - + render: ({ scope, scenario }) => ( +
- {presentation === "button" ? ( - - ) : ( - - )} +

+ Stored-file prototype — not exposed in the app. These are saved + copies, not the live sandbox filesystem. +

+
), @@ -67,15 +55,6 @@ export default meta; type Story = StoryObj; export const BrowseAndManage: Story = {}; -export const FileButton: Story = { args: { presentation: "button" } }; -export const OpenDialog: Story = { - args: { presentation: "button" }, - play: async ({ canvasElement }) => { - await userEvent.click( - await within(canvasElement).findByRole("button", { name: "Task files" }), - ); - }, -}; export const MarkdownPreview: Story = { play: async ({ canvasElement }) => { await userEvent.click(await within(canvasElement).findByText("README.md"));