diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 7451052cd0..11118c4613 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -132,16 +132,19 @@ directories are not supported by this checkpoint format. ## API and UI -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. +Cached-file inspection is an opt-in development tool. Enable **Allow viewing +cached task files** in Experimental settings under Paperclip Developer Mode. +Task properties then show **Files → View cached files** beneath Execution in +the Workspace section. The dialog previews and downloads the task, project, +assigned agent, and responsible user's saved collections. Missing bindings are +empty; private user files are available only to that user. Existing server +ownership checks apply independently of the visibility setting. -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. +The inspector is read-only and clearly identifies saved copies that can lag +behind agent edits. It does not list repositories or the live sandbox disk. +Agent, project, and profile pages have no standalone stored-file entry points. +Live sandbox filesystem inspection remains a separate future feature. The +editable stored-file browser remains in Storybook for design reference. All routes start at `/api/companies/:companyId/work-folders/:scope/:ownerId`: diff --git a/packages/shared/src/feature-catalog.ts b/packages/shared/src/feature-catalog.ts index bd934c1703..b784749ef0 100644 --- a/packages/shared/src/feature-catalog.ts +++ b/packages/shared/src/feature-catalog.ts @@ -213,6 +213,13 @@ export const INSTANCE_FEATURE_CATALOG: Record { + it("defaults cached task file inspection off and accepts opt-in and opt-out patches", () => { + expect(instanceExperimentalSettingsSchema.parse({}).enableCachedTaskFiles).toBe(false); + for (const enableCachedTaskFiles of [true, false]) { + expect(patchInstanceExperimentalSettingsSchema.parse({ enableCachedTaskFiles })).toEqual({ enableCachedTaskFiles }); + } + }); + it("defaults the streamlined UI on and accepts an explicit patch", () => { expect(instanceExperimentalSettingsSchema.parse({}).enableStreamlinedUi).toBe(true); expect( diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index a23c361574..ba49bc7b67 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -64,6 +64,7 @@ export const instanceExperimentalSettingsSchema = z.object({ enableStatusCards: z.boolean().default(false), enableDecisions: z.boolean().default(false), enableGoalsSidebarLink: z.boolean().default(false), + enableCachedTaskFiles: z.boolean().default(false), enableServerInfoDebugView: z.boolean().default(false), enablePaperclipDeveloperMode: z.boolean().default(false), enableSimplifiedEnglishInteractions: z.boolean().default(false), diff --git a/server/src/__tests__/instance-settings-service.test.ts b/server/src/__tests__/instance-settings-service.test.ts index 475a2fdd5a..b3a3a9b7b6 100644 --- a/server/src/__tests__/instance-settings-service.test.ts +++ b/server/src/__tests__/instance-settings-service.test.ts @@ -7,6 +7,12 @@ import { } from "../services/instance-settings.js"; describe("instance settings service", () => { + it("defaults cached task file inspection off and retains explicit saved settings", () => { + expect(normalizeExperimentalSettings({}).enableCachedTaskFiles).toBe(false); + expect(normalizeExperimentalSettings({ enableCachedTaskFiles: true }).enableCachedTaskFiles).toBe(true); + expect(normalizeExperimentalSettings({ enableCachedTaskFiles: false }).enableCachedTaskFiles).toBe(false); + }); + it("ignores retired experimental flags without resetting current settings", () => { expect(normalizeExperimentalSettings({ enableEnvironments: true, @@ -17,6 +23,7 @@ describe("instance settings service", () => { enableExperimentalFileViewer: true, enableBuiltInAgents: true, enableGoalsSidebarLink: true, + enableCachedTaskFiles: false, enableServerInfoDebugView: true, enablePaperclipDeveloperMode: true, autoRestartDevServerWhenIdle: true, @@ -45,6 +52,7 @@ describe("instance settings service", () => { enableStatusCards: false, enableDecisions: false, enableGoalsSidebarLink: true, + enableCachedTaskFiles: false, enableServerInfoDebugView: true, enablePaperclipDeveloperMode: true, enableSimplifiedEnglishInteractions: false, diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index b695455612..2c3db0d6eb 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -242,6 +242,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta enableStatusCards: parsed.data.enableStatusCards ?? false, enableDecisions: parsed.data.enableDecisions ?? false, enableGoalsSidebarLink: parsed.data.enableGoalsSidebarLink ?? false, + enableCachedTaskFiles: parsed.data.enableCachedTaskFiles ?? false, enableServerInfoDebugView: parsed.data.enableServerInfoDebugView ?? false, enablePaperclipDeveloperMode: parsed.data.enablePaperclipDeveloperMode ?? false, enableSimplifiedEnglishInteractions: parsed.data.enableSimplifiedEnglishInteractions ?? false, @@ -279,6 +280,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta enableStatusCards: false, enableDecisions: false, enableGoalsSidebarLink: false, + enableCachedTaskFiles: false, enableServerInfoDebugView: false, enablePaperclipDeveloperMode: false, enableSimplifiedEnglishInteractions: false, diff --git a/ui/src/components/CachedTaskFilesButton.test.tsx b/ui/src/components/CachedTaskFilesButton.test.tsx new file mode 100644 index 0000000000..937e7190ec --- /dev/null +++ b/ui/src/components/CachedTaskFilesButton.test.tsx @@ -0,0 +1,54 @@ +// @vitest-environment jsdom +import type { ComponentProps } from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CachedTaskFilesButton } from "./CachedTaskFilesButton"; + +vi.mock("@/components/WorkFolderBrowser", () => ({ + WorkFolderBrowser: ({ owner, readOnly }: { owner: unknown; readOnly: boolean }) => +
{JSON.stringify({ owner, readOnly })}
, +})); + +const issue = { id: "task-1", companyId: "company-1", projectId: "project-1", assigneeAgentId: "agent-1", responsibleUserId: "user-1" }; +const roots: ReturnType[] = []; +afterEach(async () => { + await act(async () => roots.splice(0).forEach((root) => root.unmount())); + document.body.innerHTML = ""; +}); +async function open(context: ComponentProps["issue"] = issue, currentUserId = "user-1") { + const container = document.createElement("div"); + document.body.append(container); + const root = createRoot(container); + roots.push(root); + await act(async () => root.render()); + await act(async () => container.querySelector("button")!.click()); +} +async function select(label: string) { + const tab = [...document.querySelectorAll('[role="tab"]')].find((node) => node.textContent === label)!; + await act(async () => { tab.focus(); }); +} +describe("cached task context inspector", () => { + it("uses each task binding and exposes only inspection mode", async () => { + await open(); + for (const [label, scope, ownerId] of [["Task", "task", "task-1"], ["Project", "project", "project-1"], ["Agent", "agent", "agent-1"], ["Responsible user", "user", "user-1"]]) { + await select(label!); + expect(JSON.parse(document.querySelector('[data-testid="browser"]')!.textContent!)).toEqual({ owner: { companyId: "company-1", scope, ownerId }, readOnly: true }); + } + expect(document.body.textContent).toContain("not the live sandbox filesystem"); + }); + it("does not mount a private-user browser for a different viewer", async () => { + await open(issue, "another-user"); + await select("Responsible user"); + expect(document.body.textContent).toContain("private to the responsible user"); + expect(document.querySelector('[data-testid="browser"]')).toBeNull(); + }); + it("leaves missing bindings unbound without selecting a substitute owner", async () => { + await open({ ...issue, projectId: null, assigneeAgentId: null, responsibleUserId: null }); + for (const label of ["Project", "Agent", "Responsible user"]) { + await select(label); + expect(document.body.textContent).toContain("empty and unbound"); + expect(document.querySelector('[data-testid="browser"]')).toBeNull(); + } + }); +}); diff --git a/ui/src/components/CachedTaskFilesButton.tsx b/ui/src/components/CachedTaskFilesButton.tsx new file mode 100644 index 0000000000..e79db1e8ab --- /dev/null +++ b/ui/src/components/CachedTaskFilesButton.tsx @@ -0,0 +1,47 @@ +import type { Issue, WorkFolderScope } from "@paperclipai/shared"; +import { WorkFolderBrowser } from "@/components/WorkFolderBrowser"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +type TaskContext = Pick; + +export function CachedTaskFilesButton({ issue, currentUserId }: { issue: TaskContext; currentUserId?: string }) { + const folders: { scope: WorkFolderScope; label: string; ownerId: string | null }[] = [ + { scope: "task", label: "Task", ownerId: issue.id }, + { scope: "project", label: "Project", ownerId: issue.projectId }, + { scope: "agent", label: "Agent", ownerId: issue.assigneeAgentId }, + { scope: "user", label: "Responsible user", ownerId: issue.responsibleUserId }, + ]; + return ( + + + + + + + Cached task files + + Saved copies for this task’s current context, not the live sandbox filesystem. Changes are saved every three minutes and when a run ends, so these copies may be behind. Repository checkpoints are not browsable here. + + + + + {folders.map(({ scope, label }) => {label})} + + {folders.map(({ scope, label, ownerId }) => ( + +

Cached copy of $HOME/{scope}/ · Preview and download only

+ {!ownerId ? ( +

No {label.toLowerCase()} is bound to this task. This folder is empty and unbound.

+ ) : scope === "user" && ownerId !== currentUserId ? ( +

These cached files are private to the responsible user.

+ ) : ( + + )} +
+ ))} +
+
+
+ ); +} diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index c012f1d19b..6f2af9403e 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -513,6 +513,15 @@ describe("IssueProperties", () => { document.body.innerHTML = ""; }); + it.each([false, true])("gates cached files in properties with the opt-in flag (%s)", async (enabled) => { + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableCachedTaskFiles: enabled }); + const root = renderProperties(container, { issue: createIssue({ projectId: null }), childIssues: [], onUpdate: vi.fn(), inline: true }); + await waitForAssertion(() => expect(mockInstanceSettingsApi.getExperimental).toHaveBeenCalled()); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(container.textContent?.includes("View cached files")).toBe(enabled); + act(() => root.unmount()); + }); + it("marks the task-detail property typography and section rhythm", () => { const root = renderProperties(container, { issue: createIssue(), diff --git a/ui/src/components/WorkFolderBrowser.test.tsx b/ui/src/components/WorkFolderBrowser.test.tsx index 9919cf4ba1..f3fafa71cf 100644 --- a/ui/src/components/WorkFolderBrowser.test.tsx +++ b/ui/src/components/WorkFolderBrowser.test.tsx @@ -7,16 +7,22 @@ import { WorkFolderBrowser } from "./WorkFolderBrowser"; const owner = { companyId: "company", scope: "task" as const, ownerId: "task" }; const key = ["work-folders", owner.companyId, owner.scope, owner.ownerId]; -function render(statuses: WorkFolderSyncStatus[], lastOperationAt: string | null) { +function render(statuses: WorkFolderSyncStatus[], lastOperationAt: string | null, readOnly = false) { const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }); client.setQueryData([...key, "files", false], { files: [], lastOperationAt }); client.setQueryData([...key, "sync"], statuses); - return renderToStaticMarkup(); + return renderToStaticMarkup(); } const checkpoint: WorkFolderSyncStatus = { runId: "run", state: "saved", active: false, lastSavedAt: "2026-09-07T12:00:00.000Z", error: null, refreshRequested: false }; describe("work folder save feedback", () => { + it("keeps inspection free of controls that mutate the cache or refresh the sandbox", () => { + const html = render([checkpoint], null, true); + for (const action of ["Upload", "Create folder", "Refresh sandbox", "Delete", "Restore", "Purge"]) expect(html).not.toContain(action); + expect(html).toContain("Trash"); + expect(html).toContain("No cached files have been saved for this scope."); + }); it("labels direct file operations separately from agent checkpoints", () => { const html = render([checkpoint], "2026-09-07T12:01:00.000Z"); expect(html).toContain('role="status">Saved'); diff --git a/ui/src/components/WorkFolderBrowser.tsx b/ui/src/components/WorkFolderBrowser.tsx index 8db51ad482..29dc7e0d3d 100644 --- a/ui/src/components/WorkFolderBrowser.tsx +++ b/ui/src/components/WorkFolderBrowser.tsx @@ -29,7 +29,7 @@ function tree(files: WorkFile[]) { sort(root.children); return root.children; } -export function WorkFolderBrowser({ owner, exampleFiles }: { owner: WorkFolderOwner; exampleFiles?: WorkFile[] }) { +export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false }: { owner: WorkFolderOwner; exampleFiles?: WorkFile[]; readOnly?: boolean }) { const queryClient = useQueryClient(); const [trash, setTrash] = useState(false); const [selectedPath, setSelectedPath] = useState(null); @@ -48,6 +48,7 @@ export function WorkFolderBrowser({ owner, exampleFiles }: { owner: WorkFolderOw const preview = useQuery({ queryKey: [...key, "preview", selected?.path, selected?.sha256], queryFn: () => workFoldersApi.preview(owner, selected!), enabled: !exampleFiles && !trash && selected?.kind === "file", retry: false }); const mutation = useMutation({ mutationFn: async (action: { type: "upload"; files: File[] } | { type: "mkdir" } | { type: "delete"; path: string } | { type: "restore" | "purge"; fileId: string } | { type: "refresh" }) => { + if (readOnly) throw new Error("Cached file inspection is read-only."); if (action.type === "upload") for (const file of action.files) await workFoldersApi.upload(owner, file, directory ? `${directory}/${file.name}` : file.name, crypto.randomUUID()); else if (action.type === "mkdir") { await workFoldersApi.operation(owner, { action: "mkdir", path: directory }, crypto.randomUUID()); setExpanded((before) => new Set([...before, directory])); } else if (action.type === "delete") await workFoldersApi.operation(owner, { action: "delete", path: action.path }, crypto.randomUUID()); @@ -67,18 +68,18 @@ export function WorkFolderBrowser({ owner, exampleFiles }: { owner: WorkFolderOw const disabled = mutation.isPending || Boolean(exampleFiles); return
- + {!readOnly && <> { const chosen = Array.from(event.currentTarget.files ?? []); event.currentTarget.value = ""; if (chosen.length) mutation.mutate({ type: "upload", files: chosen }); - }} /> + }} />} - + {!readOnly && } {saving ? "Saving…" : saveFailed ? "Save failed" : "Saved"} {lastSaved && Last agent save {new Date(lastSaved).toLocaleTimeString()}} {lastOperation && (!lastSaved || lastOperation > lastSaved) && Files updated {new Date(lastOperation).toLocaleTimeString()}}
- {!trash &&
+ {!readOnly && !trash &&
setDirectory(event.target.value)} placeholder="Root folder" />
} @@ -86,20 +87,20 @@ export function WorkFolderBrowser({ owner, exampleFiles }: { owner: WorkFolderOw {failed &&

{failed.error}

}

{announcement}

{trash ?
{files.length === 0 ?

Trash is empty.

: files.map((file) =>
- {file.path} + {file.path}{!readOnly && <> Permanently delete {file.path}? This deleted copy and its deleted children will no longer be recoverable. Cancel mutation.mutate({ type: "purge", fileId: file.id })}>Permanently delete - + }
)}
:
{ setSelectedPath(filePath); setExpanded((before) => { const next = new Set(before); if (next.has(filePath)) next.delete(filePath); else next.add(filePath); return next; }); }} - onSelectFile={setSelectedPath} loading={!exampleFiles && filesQuery.isLoading} empty={{ title: "No files yet", description: "Upload files here, or create them during a sandbox run." }} ariaLabel={`${owner.scope} files`} />
+ onSelectFile={setSelectedPath} loading={!exampleFiles && filesQuery.isLoading} empty={{ title: "No files yet", description: readOnly ? "No cached files have been saved for this scope." : "Upload files here, or create them during a sandbox run." }} ariaLabel={`${owner.scope} files`} />
{selected &&
{selected.path} {selected.kind === "file" && !exampleFiles && } -
} + {!readOnly && }
} {preview.isLoading ?

Loading preview…

: preview.error ?

{preview.error.message}

: preview.data ?
:

Select a file to preview it.

}
diff --git a/ui/src/components/issue-properties/IssueProperties.tsx b/ui/src/components/issue-properties/IssueProperties.tsx index 6e43d07208..6517420c73 100644 --- a/ui/src/components/issue-properties/IssueProperties.tsx +++ b/ui/src/components/issue-properties/IssueProperties.tsx @@ -1,3 +1,4 @@ +import { CachedTaskFilesButton } from "@/components/CachedTaskFilesButton"; import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType } from "react"; import { createPortal } from "react-dom"; import { PROPERTIES_PANE_HEADER_SLOT_ID } from "../PropertiesPanel"; @@ -2697,7 +2698,7 @@ export function IssueProperties({ - {workspacePickerEligible || hasWorkspaceRuntimeControls || issue.currentExecutionWorkspace?.branchName || issue.currentExecutionWorkspace?.cwd || issue.executionWorkspaceId ? ( + {experimentalSettings?.enableCachedTaskFiles || workspacePickerEligible || hasWorkspaceRuntimeControls || issue.currentExecutionWorkspace?.branchName || issue.currentExecutionWorkspace?.cwd || issue.executionWorkspaceId ? ( {workspacePickerEligible ? ( ) : null} + {experimentalSettings?.enableCachedTaskFiles && ( + + + + )} {showWorkspaceDetailLink && issue.executionWorkspaceId && ( { + await renderPage(); + const toggle = container.querySelector('section[aria-labelledby="developer-mode-heading"] button[aria-label="Allow viewing cached task files"]'); + expect(toggle?.getAttribute("aria-checked")).toBe("false"); + await act(() => toggle!.click()); + await flushReact(); + expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({ enableCachedTaskFiles: true }); + expect(toggle?.getAttribute("aria-checked")).toBe("true"); + await act(() => toggle!.click()); + await flushReact(); + expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({ enableCachedTaskFiles: false }); + }); + it("renders a page-level warning about instability and lack of guarantees", async () => { await renderPage(); diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 9efacbd95e..28fffee821 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -463,6 +463,16 @@ export function InstanceExperimentalSettings() {

+ toggleMutation.mutate({ enableCachedTaskFiles: checked })} + disabled={toggleMutation.isPending} + settingKey="enableCachedTaskFiles" + managed={managedKeys.enableCachedTaskFiles} + ariaLabel="Allow viewing cached task files" + /> { @@ -152,7 +157,7 @@ export function WorkFolderStoryProvider({ mutations: { retry: false }, }, }); - seed(value); + seed(value, enableCachedTaskFiles); return value; }); const [ready, setReady] = useState(false); @@ -170,6 +175,12 @@ export function WorkFolderStoryProvider({ const response = await fixture.handle(request); if (response) return response; const url = new URL(request.url); + if (url.pathname === "/api/instance/settings/experimental" && ["GET", "PATCH"].includes(request.method)) { + const previous = client.getQueryData(queryKeys.instance.experimentalSettings) ?? {}; + const settings = request.method === "PATCH" ? { ...previous, ...await request.json() } : previous; + client.setQueryData(queryKeys.instance.experimentalSettings, settings); + return Response.json(settings); + } if (url.pathname.startsWith("/api/") && request.method !== "GET") { // Incidental page read markers are harmless. Other page mutations are outside this demo. if (url.pathname.endsWith("/read")) diff --git a/ui/storybook/stories/cached-task-files.stories.tsx b/ui/storybook/stories/cached-task-files.stories.tsx new file mode 100644 index 0000000000..182c3d00d4 --- /dev/null +++ b/ui/storybook/stories/cached-task-files.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { CachedTaskFilesButton } from "@/components/CachedTaskFilesButton"; +import { WorkFolderStoryProvider, workFolderTask } from "../fixtures/WorkFolderStoryProvider"; + +const meta = { + title: "Work folders/Cached task inspector", + component: CachedTaskFilesButton, + args: { issue: workFolderTask, currentUserId: "user-board" }, + decorators: [(Story) => ], +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const BrowseCachedContext: Story = { + play: async ({ canvasElement }) => { + const page = within(canvasElement.ownerDocument.body); + await userEvent.click(page.getByRole("button", { name: "View cached files" })); + await expect(page.getByRole("dialog", { name: "Cached task files" })).toBeVisible(); + for (const scope of ["Task", "Project", "Agent", "Responsible user"]) { + await userEvent.click(page.getByRole("tab", { name: scope })); + await userEvent.click(await page.findByRole("treeitem", { name: "README.md" })); + await expect(page.getByRole("link", { name: "Download" })).toBeVisible(); + await expect(page.queryByRole("button", { name: "Upload" })).not.toBeInTheDocument(); + } + await userEvent.click(page.getByRole("tab", { name: "Task" })); + }, +}; +export const PrivateResponsibleUser: Story = { + args: { currentUserId: "another-user" }, + play: async ({ canvasElement }) => { + const page = within(canvasElement.ownerDocument.body); + await userEvent.click(page.getByRole("button", { name: "View cached files" })); + await userEvent.click(page.getByRole("tab", { name: "Responsible user" })); + await expect(page.getByText("These cached files are private to the responsible user.")).toBeVisible(); + await expect(page.queryByRole("tree")).not.toBeInTheDocument(); + }, +}; +export const UnboundProject: Story = { + args: { issue: { ...workFolderTask, projectId: null } }, + play: async ({ canvasElement }) => { + const page = within(canvasElement.ownerDocument.body); + await userEvent.click(page.getByRole("button", { name: "View cached files" })); + await userEvent.click(page.getByRole("tab", { name: "Project" })); + await expect(page.getByText("No project is bound to this task. This folder is empty and unbound.")).toBeVisible(); + await expect(page.queryByRole("tree")).not.toBeInTheDocument(); + }, +}; diff --git a/ui/storybook/stories/work-folder-pages.stories.tsx b/ui/storybook/stories/work-folder-pages.stories.tsx index 497bbcf7ee..c91b7913f8 100644 --- a/ui/storybook/stories/work-folder-pages.stories.tsx +++ b/ui/storybook/stories/work-folder-pages.stories.tsx @@ -6,6 +6,7 @@ import { Layout } from "@/components/Layout"; import { IssueDetail } from "@/pages/IssueDetail"; import { AgentDetail } from "@/pages/AgentDetail"; import { ProjectDetail } from "@/pages/ProjectDetail"; +import { InstanceExperimentalSettings } from "@/pages/InstanceExperimentalSettings"; import { ProfileSettings } from "@/pages/ProfileSettings"; import { useCompany } from "@/context/CompanyContext"; import { @@ -17,13 +18,15 @@ import { import { WORK_FOLDER_COMPANY } from "../fixtures/workFolders"; import type { WorkFolderScope } from "@paperclipai/shared"; +type PageScope = WorkFolderScope | "settings"; const paths = { + settings: "/PAP/company/settings/instance/experimental", task: `/PAP/issues/${workFolderTask.identifier}`, agent: `/PAP/agents/${workFolderAgent.urlKey}`, project: `/PAP/projects/${workFolderProject.urlKey}`, user: "/PAP/company/settings/instance/profile", }; -function Page({ scope }: { scope: WorkFolderScope }) { +function Page({ scope }: { scope: PageScope }) { const { selectedCompanyId, setSelectedCompanyId } = useCompany(); const location = useLocation(); const navigate = useNavigate(); @@ -40,6 +43,7 @@ function Page({ scope }: { scope: WorkFolderScope }) { }> + } /> } /> } /> } /> @@ -59,18 +63,18 @@ const meta = { docs: { description: { component: - "Current production pages after removing the stored-file entry points. Persisted copies do not represent the live sandbox filesystem. Debug inspection of saved files and live sandbox browsing are deferred features. Page mutations are not simulated.", + "Task properties expose cached-file inspection only when enabled in development settings. The inspector previews and downloads saved copies, not the live sandbox filesystem. Other page mutations are not simulated.", }, }, }, - args: { scope: "task" }, + args: { scope: "task", enableCachedTaskFiles: false }, argTypes: { scope: { control: false } }, - render: ({ scope }: { scope: WorkFolderScope }) => ( - + render: ({ scope, enableCachedTaskFiles }: { scope: PageScope; enableCachedTaskFiles: boolean }) => ( + ), -} satisfies Meta<{ scope: WorkFolderScope }>; +} satisfies Meta<{ scope: PageScope; enableCachedTaskFiles: boolean }>; export default meta; type Story = StoryObj; export const TaskPage: Story = { args: { scope: "task" } }; @@ -81,3 +85,6 @@ export const MobileTaskPage: Story = { args: { scope: "task" }, globals: { viewport: { value: "mobile" } }, }; + +export const TaskPageCachedFiles: Story = { args: { scope: "task", enableCachedTaskFiles: true } }; +export const DevelopmentSettings: Story = { args: { scope: "settings" } }; diff --git a/ui/storybook/stories/work-folders.stories.tsx b/ui/storybook/stories/work-folders.stories.tsx index b7d6dc12d1..7183a88877 100644 --- a/ui/storybook/stories/work-folders.stories.tsx +++ b/ui/storybook/stories/work-folders.stories.tsx @@ -19,7 +19,7 @@ const meta = { docs: { description: { component: - "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.", + "Editable stored-file browser prototype using disposable in-memory data. The experimental task inspector reuses this browser in read-only mode. Saved copies are not the live sandbox filesystem.", }, }, }, @@ -43,7 +43,7 @@ const meta = {

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