From 0d35c42966e4b65ce059d337982e11dffad40447 Mon Sep 17 00:00:00 2001 From: mira-2026 Date: Sun, 2 Aug 2026 20:17:34 +0200 Subject: [PATCH 1/2] feat: polish delivery and content workflows --- backend/src/development/developmentState.ts | 40 ++++++-- .../services/pullRequestPreviews/sandbox.ts | 4 +- backend/test/developmentStack.test.ts | 29 ++++++ backend/test/pullRequestPreview.test.ts | 3 + .../components/features/chat/ChatMarkdown.tsx | 23 ++++- .../features/database/TopQueriesTable.tsx | 37 ++----- .../features/delivery/PullRequestActions.tsx | 2 +- .../delivery/PullRequestDevelopmentCard.tsx | 6 +- .../delivery/useDeliveryController.ts | 23 +++++ .../features/files/viewers/CodePreview.tsx | 44 +++++---- .../features/files/viewers/JsonPreview.tsx | 40 +++++--- .../files/viewers/MarkdownPreview.tsx | 26 +++-- frontend/src/components/ui/CopyButton.tsx | 70 +++++++++++++ frontend/src/pages/Delivery.tsx | 23 ++++- frontend/src/pages/Moltbook.tsx | 20 ++-- frontend/src/test/componentBehavior.test.tsx | 50 ++++++++++ frontend/src/test/pageBehavior.test.tsx | 99 ++++++++++++++++++- .../test/pullRequestDevelopmentCard.test.tsx | 2 +- 18 files changed, 433 insertions(+), 108 deletions(-) create mode 100644 frontend/src/components/ui/CopyButton.tsx diff --git a/backend/src/development/developmentState.ts b/backend/src/development/developmentState.ts index 293491d76..d32df42b7 100644 --- a/backend/src/development/developmentState.ts +++ b/backend/src/development/developmentState.ts @@ -34,6 +34,10 @@ export interface DevelopmentStateResult { workspace: DevelopmentWorkspaceState; } +export interface DevelopmentStateOptions { + refreshDatabaseSnapshot?: boolean; +} + interface DevelopmentStateMarker { formatVersion: 1; owner: string; @@ -250,6 +254,17 @@ function createDevelopmentDatabaseSnapshot( } chmodSync(stagingPath, 0o600); scrubDevelopmentDatabase(stagingPath, shouldPreserveWebAuthnCredentials); + for (const suffix of ["-journal", "-shm", "-wal"]) { + const sidecarPath = `${targetPath}${suffix}`; + if (isPathPresentNoFollow(sidecarPath)) { + if (!isRealRegularFile(sidecarPath)) { + throw new Error( + `Development database sidecar must be a real regular file: ${sidecarPath}` + ); + } + rmSync(sidecarPath, { force: true }); + } + } renameSync(stagingPath, targetPath); } catch (error) { rmSync(stagingPath, { force: true }); @@ -433,7 +448,8 @@ export function developmentSecretEncryptionKey(config: DevelopmentStackConfig): * @returns Created or reuses isolated, ignored development state. */ export function prepareDevelopmentState( - config: DevelopmentStackConfig + config: DevelopmentStackConfig, + options: DevelopmentStateOptions = {} ): DevelopmentStateResult { assertOrCreateStateOwnership(config); ensurePrivateStateDirectory(config, config.openClawClientHome); @@ -448,21 +464,25 @@ export function prepareDevelopmentState( }); let database: DevelopmentStateResult["database"]; - if (isPathPresentNoFollow(config.databasePath)) { - if (!isRealRegularFile(config.databasePath)) { - throw new Error("Development database must be a real regular file"); - } - if (config.sourceWebAuthnRpId !== config.rpId) { - scrubDevelopmentDatabase(config.databasePath, false); - } - database = "reused"; - } else if (config.databaseSource) { + const hasDatabase = isPathPresentNoFollow(config.databasePath); + if (hasDatabase && !isRealRegularFile(config.databasePath)) { + throw new Error("Development database must be a real regular file"); + } + if ( + config.databaseSource && + (!hasDatabase || options.refreshDatabaseSnapshot === true) + ) { createDevelopmentDatabaseSnapshot( config.databaseSource, config.databasePath, config.sourceWebAuthnRpId === config.rpId ); database = "snapshot-created"; + } else if (hasDatabase) { + if (config.sourceWebAuthnRpId !== config.rpId) { + scrubDevelopmentDatabase(config.databasePath, false); + } + database = "reused"; } else { database = "created-empty"; } diff --git a/backend/src/services/pullRequestPreviews/sandbox.ts b/backend/src/services/pullRequestPreviews/sandbox.ts index 9048aff30..0220401b4 100644 --- a/backend/src/services/pullRequestPreviews/sandbox.ts +++ b/backend/src/services/pullRequestPreviews/sandbox.ts @@ -50,7 +50,9 @@ export function preparePreviewState( environment, config.dashboardRoot ); - prepareDevelopmentState(developmentConfig); + prepareDevelopmentState(developmentConfig, { + refreshDatabaseSnapshot: true, + }); return stateRoot; } diff --git a/backend/test/developmentStack.test.ts b/backend/test/developmentStack.test.ts index ee5d6c562..97e5306f4 100644 --- a/backend/test/developmentStack.test.ts +++ b/backend/test/developmentStack.test.ts @@ -539,6 +539,35 @@ describe("development stack", () => { .get() ).toEqual({ value: "preserved" }); reusedSnapshot.close(); + + const productionDatabase = new Database(sourceDatabase); + productionDatabase + .prepare("UPDATE app_config SET value = ? WHERE key = 'theme'") + .run("fresh"); + productionDatabase.close(); + expect( + prepareDevelopmentState(config, { refreshDatabaseSnapshot: true }) + ).toEqual({ + database: "snapshot-created", + releases: "reused", + workspace: "reused", + }); + const refreshedSnapshot = new Database(config.databasePath, { + readonly: true, + }); + expect( + refreshedSnapshot + .query( + "SELECT value FROM app_config WHERE key = 'development-marker'" + ) + .get() + ).toBeNull(); + expect( + refreshedSnapshot + .query("SELECT value FROM app_config WHERE key = 'theme'") + .get() + ).toEqual({ value: "fresh" }); + refreshedSnapshot.close(); rmSync(sourceDatabase); expect(prepareDevelopmentState(config)).toEqual({ database: "reused", diff --git a/backend/test/pullRequestPreview.test.ts b/backend/test/pullRequestPreview.test.ts index d0e43b3f3..db0e3e79b 100644 --- a/backend/test/pullRequestPreview.test.ts +++ b/backend/test/pullRequestPreview.test.ts @@ -757,6 +757,9 @@ describe("managed pull request preview", () => { ); expect(worktreeAddIndex).toBeGreaterThanOrEqual(0); expect(prepareStateSpy).toHaveBeenCalledTimes(1); + expect(prepareStateSpy).toHaveBeenCalledWith(expect.any(Object), { + refreshDatabaseSnapshot: true, + }); expect(protectFromCancellation).toHaveBeenCalledTimes(1); expect(fetchSpy).toHaveBeenCalledWith( "http://127.0.0.1:5173/api/health/ready", diff --git a/frontend/src/components/features/chat/ChatMarkdown.tsx b/frontend/src/components/features/chat/ChatMarkdown.tsx index 17718a811..1ce557fe6 100644 --- a/frontend/src/components/features/chat/ChatMarkdown.tsx +++ b/frontend/src/components/features/chat/ChatMarkdown.tsx @@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm"; import { CodeSyntaxHighlighter } from "../../../lib/syntaxHighlighter"; import { cn } from "../../../utils/cn"; +import { CopyButton } from "../../ui/CopyButton"; import { getPreCodeBlock, isJsonLike, @@ -24,8 +25,15 @@ function ChatCodeBlock({ code, language }: { code: string; language: string }) { if (parsedJson) { return (
-
- {JSON_LANGUAGES.has(language) ? language : "json"} +
+ + {JSON_LANGUAGES.has(language) ? language : "json"} + +
-
- {language} +
+ + {language} + +
(); - const [copied, setCopied] = useState(false); if (!enabled) { return ( @@ -67,21 +65,6 @@ export function TopQueriesTable({ ); } - /** - * Responds to copy events. - * @param query Query value. - */ - const handleCopy = async (query: string) => { - try { - await navigator.clipboard.writeText(query); - setCopied(true); - setTimeout(() => setCopied(false), 1500); - } catch (error_) { - setCopied(false); - console.error("Failed to copy query", error_); - } - }; - return ( <> { - setSelectedQuery(undefined); - setCopied(false); - }} + onClose={() => setSelectedQuery(undefined)} title="Query details" size="3xl" > @@ -138,15 +118,12 @@ export function TopQueriesTable({
- + content={selectedQuery.query} + label="Copy query" + variant="secondary" + />
diff --git a/frontend/src/components/features/delivery/PullRequestActions.tsx b/frontend/src/components/features/delivery/PullRequestActions.tsx
index a15cfd697..f63a44d62 100644
--- a/frontend/src/components/features/delivery/PullRequestActions.tsx
+++ b/frontend/src/components/features/delivery/PullRequestActions.tsx
@@ -308,6 +308,7 @@ export function PullRequestActions({ context, pr }: PullRequestActionsProperties
                     {previewActions.blockedMessage}
                 

) : undefined} + {previewActions.controls} {canConfiguredReviewerApproveReview(pr) ? ( + ); +} diff --git a/frontend/src/pages/Delivery.tsx b/frontend/src/pages/Delivery.tsx index db5500b99..3e329ed58 100644 --- a/frontend/src/pages/Delivery.tsx +++ b/frontend/src/pages/Delivery.tsx @@ -1,4 +1,5 @@ import { Rocket } from "lucide-react"; +import { useEffect, useRef } from "react"; import { actionLabel, @@ -32,6 +33,7 @@ import { messageFromError } from "../lib/errorMessage"; export function Delivery() { const { actionError, + actionProgress, confirmAction, deployments, deployBlockedReasonId, @@ -61,9 +63,18 @@ export function Delivery() { stackCandidates, stackGroups, } = useDeliveryController(); + const pageTopRef = useRef(null); + + useEffect(() => { + if (!actionProgress) return; + pageTopRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, [actionProgress]); return ( -
+

@@ -96,6 +107,16 @@ export function Delivery() {

+ {actionProgress ? ( +
+ + + {actionProgress} + + +
+ ) : undefined} + {lastResult ? ( setActiveTab(v)} - className="w-full [&>button]:flex-1 sm:[&>button]:flex-none" - /> +
+ setActiveTab(v)} + className="w-full [&>button]:flex-1 sm:[&>button]:flex-none" + /> +
{/* Feed Tab */} {activeTab === "feed" && ( - <> +
- +
)} {/* My Posts Tab */} diff --git a/frontend/src/test/componentBehavior.test.tsx b/frontend/src/test/componentBehavior.test.tsx index 7e5038aa4..018ec8f82 100644 --- a/frontend/src/test/componentBehavior.test.tsx +++ b/frontend/src/test/componentBehavior.test.tsx @@ -293,6 +293,56 @@ describe("shared component helpers", () => { expect(screen.getByText("sh")).toBeInTheDocument(); }); + it("copies chat code and structured file previews", async () => { + const originalClipboardDescriptor = Object.getOwnPropertyDescriptor( + navigator, + "clipboard" + ); + const writeText = jest.fn(async () => {}); + const user = userEvent.setup(); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + const view = render(); + + try { + await user.click(screen.getByRole("button", { name: "Copy code" })); + await waitFor(() => { + expect(writeText).toHaveBeenLastCalledWith('{"value":1}'); + }); + + view.rerender(); + await user.click(screen.getByRole("button", { name: "Copy JSON" })); + await waitFor(() => { + expect(writeText).toHaveBeenLastCalledWith('{"value": 1}'); + }); + + view.rerender(); + await user.click(screen.getByRole("button", { name: "Copy code" })); + await waitFor(() => { + expect(writeText).toHaveBeenLastCalledWith("const value = 1;"); + }); + + view.rerender(); + await user.click(screen.getByRole("button", { name: "Copy Markdown" })); + await waitFor(() => { + expect(writeText).toHaveBeenLastCalledWith("# Notes"); + }); + } finally { + view.unmount(); + if (originalClipboardDescriptor) { + Object.defineProperty( + navigator, + "clipboard", + originalClipboardDescriptor + ); + } else { + Reflect.deleteProperty(navigator, "clipboard"); + } + } + }); + it("renders alert variants, right-aligned dismissal, and clamped progress", async () => { const onDismiss = jest.fn(); expect(getProgressColor(10)).toBe("green"); diff --git a/frontend/src/test/pageBehavior.test.tsx b/frontend/src/test/pageBehavior.test.tsx index e13e1ae8c..07bab7499 100644 --- a/frontend/src/test/pageBehavior.test.tsx +++ b/frontend/src/test/pageBehavior.test.tsx @@ -82,6 +82,7 @@ const animationFrameState = { id: 0, frames: new Map(), }; +const scrollIntoViewMock = jest.fn(); const terminalApiState = { expectedExecCwd: "/tmp", wasJobStopped: false, @@ -2387,7 +2388,8 @@ describe("Mira Dashboard pages", () => { writable: true, }, }); - Element.prototype.scrollIntoView = jest.fn(); + scrollIntoViewMock.mockReset(); + Element.prototype.scrollIntoView = scrollIntoViewMock; }); afterEach(() => { @@ -2469,6 +2471,26 @@ describe("Mira Dashboard pages", () => { } }); + it("separates Moltbook feed sorting from the content tabs", async () => { + const view = renderPage(createElement(Moltbook)); + + const feedSort = await screen.findByRole("group", { + name: "Moltbook feed sort", + }); + const contentTabs = screen.getByRole("group", { + name: "Moltbook content", + }); + expect(contentTabs.parentElement?.tagName).toBe("DIV"); + expect(contentTabs.parentElement?.parentElement).toBe( + feedSort.parentElement?.parentElement + ); + expect(feedSort.parentElement).not.toBe(contentTabs.parentElement); + expect(feedSort.parentElement).toHaveClass("space-y-4", "lg:space-y-6"); + + view.unmount(); + view.queryClient.clear(); + }); + it("labels and dismisses file save errors", async () => { const user = userEvent.setup(); const originalFetch = fetch; @@ -3210,6 +3232,72 @@ describe("Mira Dashboard pages", () => { view.queryClient.clear(); }); + it("closes merge confirmations while the merge job continues", async () => { + const user = userEvent.setup(); + const defaultFetch = globalThis.fetch; + let finishMerge: ((response: Response) => void) | undefined; + const mergeResponse = new Promise((resolve) => { + finishMerge = resolve; + }); + Object.defineProperty(globalThis, "fetch", { + configurable: true, + value: jest.fn((input: RequestInfo | URL, init?: RequestInit) => { + const url = requestUrl(input); + const method = init?.method ?? "GET"; + if (method === "POST" && url === "/api/pull-requests/190/approve") { + expect(parseRequestBody(init)).toEqual({ + deploy: true, + expectedHeadSha: "a".repeat(40), + mergeStack: false, + }); + return mergeResponse; + } + return defaultFetch(input, init); + }), + writable: true, + }); + const view = renderPage(createElement(Delivery)); + + await screen.findByText("Expand backend coverage"); + await user.click(screen.getAllByRole("button", { name: "Merge + Deploy" })[0]!); + const dialog = screen.getByRole("dialog", { name: "Merge + Deploy" }); + await user.click(within(dialog).getByRole("button", { name: "Merge + Deploy" })); + + expect(screen.queryByRole("dialog", { name: "Merge + Deploy" })).toBeNull(); + expect( + screen.getByText("Merging PR #190 and preparing deploy...") + ).toBeInTheDocument(); + expect( + screen.getAllByRole("button", { name: "Merge + Deploy" })[0] + ).toBeDisabled(); + await waitFor(() => { + expect(scrollIntoViewMock).toHaveBeenCalledWith({ + behavior: "smooth", + block: "start", + }); + }); + + act(() => { + finishMerge?.( + Response.json({ + isOk: true, + message: "PR #190 merged. Deploy started", + }) + ); + }); + await waitFor(() => { + expect( + screen.getByText("PR #190 merged. Deploy started") + ).toBeInTheDocument(); + expect( + screen.queryByText("Merging PR #190 and preparing deploy...") + ).toBeNull(); + }); + + view.unmount(); + view.queryClient.clear(); + }); + it("shows dependent PR chains and creates GitHub stacks bottom-to-top", async () => { const user = userEvent.setup(); const defaultFetch = globalThis.fetch; @@ -3645,7 +3733,7 @@ describe("Mira Dashboard pages", () => { view.queryClient.clear(); }); - it("keeps PR dev status messages ahead of pull request action buttons", async () => { + it("keeps PR dev status and controls ahead of review actions", async () => { Object.defineProperty(globalThis, "fetch", { configurable: true, value: jest.fn((input: RequestInfo | URL, init?: RequestInit) => { @@ -3707,8 +3795,13 @@ describe("Mira Dashboard pages", () => { "PR dev status is unavailable: bun executable must resolve to an absolute path" ); const approveButton = screen.getByRole("button", { name: "Approve PR" }); + const runInDevButton = screen.getByRole("button", { name: "Run in dev" }); + expect( + previewStatus.compareDocumentPosition(runInDevButton) & + Node.DOCUMENT_POSITION_FOLLOWING + ).toBeTruthy(); expect( - previewStatus.compareDocumentPosition(approveButton) & + runInDevButton.compareDocumentPosition(approveButton) & Node.DOCUMENT_POSITION_FOLLOWING ).toBeTruthy(); expect(previewStatus).toHaveClass("col-span-full", "w-full"); diff --git a/frontend/src/test/pullRequestDevelopmentCard.test.tsx b/frontend/src/test/pullRequestDevelopmentCard.test.tsx index 0c0b6f028..a4863dcd5 100644 --- a/frontend/src/test/pullRequestDevelopmentCard.test.tsx +++ b/frontend/src/test/pullRequestDevelopmentCard.test.tsx @@ -23,7 +23,7 @@ describe("PullRequestDevelopmentCard", () => { it("renders the available and unavailable preview states", () => { const { rerender } = render(); - expect(screen.getByText("Available")).toBeInTheDocument(); + expect(screen.getByText("Available")).toHaveClass("text-green-400"); expect(screen.getByText(/fixed PR commit/u)).toBeInTheDocument(); expect(screen.queryByText(/hot reload/iu)).not.toBeInTheDocument(); expect( From 0d8669e1e0771e13e0ada45d284fbf5b6cd37f34 Mon Sep 17 00:00:00 2001 From: mira-2026 Date: Sun, 2 Aug 2026 20:28:20 +0200 Subject: [PATCH 2/2] test: cover preview refresh safety paths --- backend/test/developmentStack.test.ts | 26 ++++++++++++++++ frontend/src/test/componentBehavior.test.tsx | 32 ++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/backend/test/developmentStack.test.ts b/backend/test/developmentStack.test.ts index 97e5306f4..80e65a00f 100644 --- a/backend/test/developmentStack.test.ts +++ b/backend/test/developmentStack.test.ts @@ -568,6 +568,32 @@ describe("development stack", () => { .get() ).toEqual({ value: "fresh" }); refreshedSnapshot.close(); + + const staleSidecar = `${config.databasePath}-wal`; + writeFileSync(staleSidecar, "stale preview journal"); + expect( + prepareDevelopmentState(config, { refreshDatabaseSnapshot: true }) + .database + ).toBe("snapshot-created"); + expect(existsSync(staleSidecar)).toBe(false); + + symlinkSync(sourceDatabase, staleSidecar); + expect(() => + prepareDevelopmentState(config, { refreshDatabaseSnapshot: true }) + ).toThrow("Development database sidecar must be a real regular file"); + rmSync(staleSidecar); + + rmSync(config.databasePath); + symlinkSync(sourceDatabase, config.databasePath); + expect(() => + prepareDevelopmentState(config, { refreshDatabaseSnapshot: true }) + ).toThrow("Development database must be a real regular file"); + rmSync(config.databasePath); + expect( + prepareDevelopmentState(config, { refreshDatabaseSnapshot: true }) + .database + ).toBe("snapshot-created"); + rmSync(sourceDatabase); expect(prepareDevelopmentState(config)).toEqual({ database: "reused", diff --git a/frontend/src/test/componentBehavior.test.tsx b/frontend/src/test/componentBehavior.test.tsx index 018ec8f82..b1d0b75bc 100644 --- a/frontend/src/test/componentBehavior.test.tsx +++ b/frontend/src/test/componentBehavior.test.tsx @@ -103,6 +103,7 @@ import { AppErrorFallback } from "../components/ui/AppErrorFallback"; import { Badge } from "../components/ui/Badge"; import { Checkbox } from "../components/ui/Checkbox"; import { ConnectionStatus } from "../components/ui/ConnectionStatus"; +import { CopyButton } from "../components/ui/CopyButton"; import { ExpandableCard, ReadOnlyField } from "../components/ui/ExpandableCard"; import { FilterButtonGroup } from "../components/ui/FilterButtonGroup"; import { ProgressBar } from "../components/ui/ProgressBar"; @@ -343,6 +344,37 @@ describe("shared component helpers", () => { } }); + it("reports when clipboard copying is unavailable", async () => { + const originalClipboardDescriptor = Object.getOwnPropertyDescriptor( + navigator, + "clipboard" + ); + const user = userEvent.setup(); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: undefined, + }); + const view = render(); + + try { + await user.click(screen.getByRole("button", { name: "Copy notes" })); + expect( + await screen.findByRole("button", { name: "Copy failed" }) + ).toBeInTheDocument(); + } finally { + view.unmount(); + if (originalClipboardDescriptor) { + Object.defineProperty( + navigator, + "clipboard", + originalClipboardDescriptor + ); + } else { + Reflect.deleteProperty(navigator, "clipboard"); + } + } + }); + it("renders alert variants, right-aligned dismissal, and clamped progress", async () => { const onDismiss = jest.fn(); expect(getProgressColor(10)).toBe("green");