From 36e251c52b146e34a6bfd2f50cec0e41a4fae78c Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 8 Nov 2026 01:36:07 +0000 Subject: [PATCH 1/3] fix(web): pull request Viewed checkboxes are disabled on connections that cannot mark files On a connection without source-control:write, the Code tab offered an enabled Viewed checkbox whose every tick the server rejected, so it reverted with an error toast. The checkbox now reads the setFilesViewed command's permissionAtom and renders disabled when the command is not permitted, while viewed marks and the counter stay readable. Co-Authored-By: Claude Opus 5.5 --- .../PullRequestCodeTab.browser.test.tsx | 36 ++++++++++++++++--- .../pullRequest/PullRequestCodeTab.tsx | 10 ++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.browser.test.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.browser.test.tsx index cea33e74ffc99..9ff4cd1c4311e 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.browser.test.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.browser.test.tsx @@ -9,7 +9,7 @@ import { expect, it, vi } from "vite-plus/test"; import "~/index.css"; -const { diffAtom, pagedDiffAtom } = await vi.hoisted(async () => { +const { diffAtom, pagedDiffAtom, filesViewedAtom, allowed, denied } = await vi.hoisted(async () => { const { AsyncResult, Atom } = await import("effect/reactivity"); const filePatch = (path: string) => [ @@ -47,10 +47,17 @@ const { diffAtom, pagedDiffAtom } = await vi.hoisted(async () => { pagedDiffAtom: Atom.family((cursor: string) => Atom.make(() => AsyncResult.success(pages[cursor]!)), ), + filesViewedAtom: Atom.make(() => AsyncResult.success({ files: [], truncated: false })), + allowed: Atom.make(true), + denied: Atom.make(false), }; }); -const mocks = vi.hoisted(() => ({ paged: false, diffCommits: [] as Array })); +const mocks = vi.hoisted(() => ({ + paged: false, + diffCommits: [] as Array, + canMarkViewed: true, +})); vi.mock(import("~/state/use-atom-command"), async (original) => ({ ...(await original()), @@ -66,6 +73,11 @@ vi.mock(import("~/state/pullRequests"), async (original) => { mocks.diffCommits.push(input.commit); return mocks.paged ? pagedDiffAtom(input.cursor ?? "first") : diffAtom; }) as never, + filesViewed: (() => filesViewedAtom) as never, + setFilesViewed: { + ...actual.pullRequestEnvironment.setFilesViewed, + permissionAtom: () => (mocks.canMarkViewed ? allowed : denied), + }, }, }; }); @@ -125,20 +137,22 @@ const detail: PullRequestDetailView = { }, }; -function setup({ paged }: { paged: boolean }) { +function setup({ paged, canMarkViewed = true }: { paged: boolean; canMarkViewed?: boolean }) { mocks.paged = paged; mocks.diffCommits = []; + mocks.canMarkViewed = canMarkViewed; } const tab = ( refreshToken: number, revealRequest: { readonly path: string; readonly nonce: number } | null = null, + pullRequest: PullRequestDetailView = detail, ) => ( {}} environmentId={EnvironmentId.make("environment")} - reference={detail} - detail={detail} + reference={pullRequest} + detail={pullRequest} selectedCommitOid={null} onSelectedCommitChange={() => {}} onRefresh={() => {}} @@ -235,3 +249,15 @@ it("never requests the diff of a selected commit the pull request no longer has" .toBeVisible(); expect(mocks.diffCommits).not.toContain("gone"); }); + +it("disables the Viewed checkbox on a connection that cannot mark files viewed", async () => { + setup({ paged: false, canMarkViewed: false }); + const container = document.createElement("div"); + container.className = "flex h-[700px] w-[900px]"; + await using screen = await render( + tab(0, null, { ...detail, capabilities: { ...detail.capabilities, viewedFiles: "host" } }), + { container }, + ); + + await expect.element(screen.getByRole("checkbox", { name: "Viewed" })).toBeDisabled(); +}); diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index a456c84bf5320..4d40e167bfe02 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -30,7 +30,7 @@ import { TriangleAlertIcon, } from "lucide-react"; import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown } from "lucide"; -import { useAtomRefresh } from "@effect/atom-react"; +import { useAtomRefresh, useAtomValue } from "@effect/atom-react"; import * as Schema from "effect/Schema"; import { createContext, @@ -253,6 +253,7 @@ function reviewThreadsByPath(threads: ReadonlyArray) { interface FileViewedToggleState { readonly enabled: boolean; + readonly canMarkViewed: boolean; readonly isViewed: (path: string) => boolean; readonly isStale: (path: string) => boolean; readonly setFileViewed: (fileKey: string, viewed: boolean, extendRange: boolean) => void; @@ -279,12 +280,13 @@ function FileViewedToggle({ {stat}