From f097fe4937bcf2f6ab426fe336cd58d7140e2c73 Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 15 May 2026 11:04:21 -0500 Subject: [PATCH] Add upstream diff link for project workspaces --- .../plugin-workspace-diff/src/ui/index.tsx | 14 +++++++++-- ui/src/pages/ProjectWorkspaceDetail.tsx | 25 ++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/plugins/plugin-workspace-diff/src/ui/index.tsx b/packages/plugins/plugin-workspace-diff/src/ui/index.tsx index 5b4c916e5d7..6241bdb4c1e 100644 --- a/packages/plugins/plugin-workspace-diff/src/ui/index.tsx +++ b/packages/plugins/plugin-workspace-diff/src/ui/index.tsx @@ -21,6 +21,16 @@ type WorkspaceDiffData = WorkspaceDiffResponse; type WorkspacePatchDiffOptions = PatchDiffProps["options"]; type DiffViewMode = "working-tree" | "head"; +function readInitialView(): DiffViewMode { + if (typeof window === "undefined") return "working-tree"; + return new URLSearchParams(window.location.search).get("diffView") === "head" ? "head" : "working-tree"; +} + +function readInitialBaseRef() { + if (typeof window === "undefined") return ""; + return new URLSearchParams(window.location.search).get("baseRef") ?? ""; +} + function buttonClass(active = false) { return [ "inline-flex h-8 items-center justify-center rounded-md border px-2.5 text-xs font-medium transition-colors", @@ -273,8 +283,8 @@ function CollapsedFilePanel({ export function ChangesTab({ context }: PluginDetailTabProps) { const toast = usePluginToast(); const [mode, setMode] = useState("split"); - const [view, setView] = useState("working-tree"); - const [baseRef, setBaseRef] = useState(""); + const [view, setView] = useState(() => readInitialView()); + const [baseRef, setBaseRef] = useState(() => readInitialBaseRef()); const [includeUntracked, setIncludeUntracked] = useState(false); const [expandedFiles, setExpandedFiles] = useState>(() => new Set()); const [selectedPath, setSelectedPath] = useState(null); diff --git a/ui/src/pages/ProjectWorkspaceDetail.tsx b/ui/src/pages/ProjectWorkspaceDetail.tsx index b5297d41040..fb90e69d7d1 100644 --- a/ui/src/pages/ProjectWorkspaceDetail.tsx +++ b/ui/src/pages/ProjectWorkspaceDetail.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link, useLocation, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { isUuidLike, type ProjectWorkspace } from "@paperclipai/shared"; -import { ArrowLeft, Check, ExternalLink, Loader2, Sparkles } from "lucide-react"; +import { ArrowLeft, Check, ExternalLink, GitCompare, Loader2, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tabs } from "@/components/ui/tabs"; @@ -41,11 +41,20 @@ type ProjectWorkspaceVisibility = ProjectWorkspace["visibility"]; type ProjectWorkspaceBaseTab = "configuration"; type ProjectWorkspacePluginTab = `plugin:${string}`; type ProjectWorkspaceTab = ProjectWorkspaceBaseTab | ProjectWorkspacePluginTab; +const WORKSPACE_CHANGES_TAB = "plugin:paperclip.workspace-diff:workspace-changes-tab" as ProjectWorkspacePluginTab; function isProjectWorkspacePluginTab(value: string | null): value is ProjectWorkspacePluginTab { return typeof value === "string" && value.startsWith("plugin:"); } +function upstreamBaseRef(workspace: ProjectWorkspace) { + if (workspace.defaultRef) return workspace.defaultRef; + if (workspace.repoRef) { + return workspace.repoRef.startsWith("origin/") ? workspace.repoRef : `origin/${workspace.repoRef.replace(/^refs\/heads\//, "")}`; + } + return "origin/master"; +} + const SOURCE_TYPE_OPTIONS: Array<{ value: ProjectWorkspaceSourceType; label: string; description: string }> = [ { value: "local_path", label: "Local git checkout", description: "A local path Paperclip can use directly." }, { value: "non_git_path", label: "Local non-git path", description: "A local folder without git semantics." }, @@ -394,6 +403,14 @@ export function ProjectWorkspaceDetail() { }; const sourceTypeDescription = SOURCE_TYPE_OPTIONS.find((option) => option.value === form.sourceType)?.description ?? null; + const changesVsUpstreamUrl = (() => { + const params = new URLSearchParams({ + tab: WORKSPACE_CHANGES_TAB, + diffView: "head", + baseRef: upstreamBaseRef(workspace), + }); + return `${projectWorkspaceUrl(project, routeWorkspaceId)}?${params.toString()}`; + })(); const handleTabChange = (tab: ProjectWorkspaceTab) => { const workspacePath = projectWorkspaceUrl(project, routeWorkspaceId); if (isProjectWorkspacePluginTab(tab)) { @@ -415,6 +432,12 @@ export function ProjectWorkspaceDetail() {
{workspace.isPrimary ? "Primary workspace" : "Secondary workspace"}
+ handleTabChange(value as ProjectWorkspaceTab)}>