From 4cabdb2351b3c63bff15a0d56469b5735a9b0028 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Wed, 10 Jun 2026 15:13:12 -0700 Subject: [PATCH] feat: add pierre file tree to full-screen git panel (#1487) * feat: add pierre file tree to full-screen git panel Render the dashboard git panel with real changed-file data using pierre diffs, add a full-screen toggle, and show a pierre FileTree explorer on the right when expanded. Co-authored-by: open-swe[bot] * feat: always show git panel as collapsible resizable card - git panel renders on every thread with collapse to a floating expand button and drag-resize, persisted to localStorage (matches left sidebar) - panel content wrapped in a rounded card with Git/Desktop/Terminal tabs above it; Desktop/Terminal and Review/Commits show Coming Soon - send button morphs into a stop button during an active run * fix: follow app theme for pierre diff rendering diffOptions used themeType "system" so @pierre/diffs followed the OS color scheme instead of the app's .dark class, producing unreadable dark-on-light diffs when the two disagreed. Add useDiffOptions() which resolves themeType from the app theme, and use it at all diff render sites. * fix: align git panel card bottom gutter with prompt bar --------- Co-authored-by: open-swe[bot] --- ui/bun.lock | 17 + ui/package.json | 1 + ui/src/components/agents/AgentGitPanel.tsx | 465 +++++++++++++++--- ui/src/components/agents/AgentThreadView.tsx | 6 +- .../agents/ported/CloudPromptBar.tsx | 32 +- .../components/agents/ported/MessageView.tsx | 3 +- .../agents/ported/SourceControlTile.tsx | 3 +- .../agents/ported/ToolExecution.tsx | 14 +- ui/src/components/agents/ported/index.ts | 7 +- ui/src/components/agents/utils/diffUtils.ts | 11 + ui/src/routeTree.gen.ts | 16 +- ui/yarn.lock | 18 + 12 files changed, 492 insertions(+), 101 deletions(-) diff --git a/ui/bun.lock b/ui/bun.lock index 39a02214..859964d8 100644 --- a/ui/bun.lock +++ b/ui/bun.lock @@ -7,8 +7,10 @@ "dependencies": { "@base-ui/react": "^1.4.1", "@fontsource-variable/inter": "^5.2.8", + "@monaco-editor/react": "^4.7.0", "@phosphor-icons/react": "^2.1.10", "@pierre/diffs": "^1.2.1", + "@pierre/trees": "1.0.0-beta.4", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query": "^5.100.10", @@ -22,6 +24,7 @@ "clsx": "^2.1.1", "diff": "^9.0.0", "lucide-react": "^1.16.0", + "monaco-editor": "^0.52.2", "nitro": "latest", "react": "^19.2.4", "react-dom": "^19.2.4", @@ -407,6 +410,10 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], @@ -447,6 +454,8 @@ "@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="], + "@pierre/trees": ["@pierre/trees@1.0.0-beta.4", "", { "dependencies": { "preact": "11.0.0-beta.0", "preact-render-to-string": "6.6.5" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="], @@ -1607,6 +1616,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "monaco-editor": ["monaco-editor@0.52.2", "", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "msw": ["msw@2.14.6", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg=="], @@ -1725,6 +1736,10 @@ "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "preact": ["preact@11.0.0-beta.0", "", {}, "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg=="], + + "preact-render-to-string": ["preact-render-to-string@6.6.5", "", { "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } }, "sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], @@ -1905,6 +1920,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], diff --git a/ui/package.json b/ui/package.json index 89a85374..94e68739 100644 --- a/ui/package.json +++ b/ui/package.json @@ -17,6 +17,7 @@ "@monaco-editor/react": "^4.7.0", "@phosphor-icons/react": "^2.1.10", "@pierre/diffs": "^1.2.1", + "@pierre/trees": "1.0.0-beta.4", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-devtools": "^0.10.0", "@tanstack/react-query": "^5.100.10", diff --git a/ui/src/components/agents/AgentGitPanel.tsx b/ui/src/components/agents/AgentGitPanel.tsx index 2b097479..1b264be1 100644 --- a/ui/src/components/agents/AgentGitPanel.tsx +++ b/ui/src/components/agents/AgentGitPanel.tsx @@ -1,62 +1,306 @@ -import { useMemo, useState } from "react"; -import { MultiFileDiff } from "@pierre/diffs/react"; -import { CaretDownIcon, GitPullRequestIcon } from "@phosphor-icons/react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { MultiFileDiff } from "@pierre/diffs/react" +import { + FileTree, + useFileTree, + useFileTreeSelection, +} from "@pierre/trees/react" +import { + ArrowsInIcon, + ArrowsOutIcon, + CaretDownIcon, + GitPullRequestIcon, + SidebarSimpleIcon, +} from "@phosphor-icons/react" +import type { GitStatus, GitStatusEntry } from "@pierre/trees" -import { diffOptions } from "@/components/agents/utils/diffUtils"; -import type { AgentThread } from "@/lib/agents/types"; -import { cn } from "@/lib/utils"; +import type { AgentThread } from "@/lib/agents/types" +import type { ChangedFileSummaryItem } from "@/components/agents/ported" +import { useDiffOptions } from "@/components/agents/utils/diffUtils" +import { summarizeChangedFiles } from "@/components/agents/ported" +import { Z } from "@/components/agents/z-index" +import { cn } from "@/lib/utils" interface AgentGitPanelProps { - thread: AgentThread; + thread: AgentThread +} + +interface PanelFile { + filePath: string + treePath: string + additions: number + deletions: number + originalContent: string + modifiedContent: string + status: GitStatus +} + +function deriveStatus(file: ChangedFileSummaryItem): GitStatus { + if (file.originalContent.length === 0 && file.modifiedContent.length > 0) { + return "added" + } + if (file.modifiedContent.length === 0 && file.originalContent.length > 0) { + return "deleted" + } + return "modified" +} + +function commonDirPrefix(paths: Array): string { + const first = paths[0] + if (paths.length === 0 || first === undefined) return "" + const base = first.split("/").slice(0, -1) + let depth = base.length + for (const path of paths) { + const segments = path.split("/").slice(0, -1) + let i = 0 + while (i < depth && i < segments.length && segments[i] === base[i]) i++ + depth = i + } + return depth === 0 ? "" : `${base.slice(0, depth).join("/")}/` +} + +const PANEL_STORAGE_WIDTH = "open-swe.gitpanel.width" +const PANEL_STORAGE_COLLAPSED = "open-swe.gitpanel.collapsed" +const PANEL_DEFAULT_WIDTH = 420 +const PANEL_MIN_WIDTH = 320 +const PANEL_MAX_WIDTH = 720 + +function readStoredPanelWidth(): number { + if (typeof window === "undefined") return PANEL_DEFAULT_WIDTH + const raw = window.localStorage.getItem(PANEL_STORAGE_WIDTH) + const parsed = raw ? Number(raw) : NaN + if (!Number.isFinite(parsed)) return PANEL_DEFAULT_WIDTH + return Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, parsed)) +} + +function readStoredPanelCollapsed(): boolean { + if (typeof window === "undefined") return false + return window.localStorage.getItem(PANEL_STORAGE_COLLAPSED) === "1" +} + +function PanelResizeHandle({ + width, + onResize, +}: { + width: number + onResize: (next: number) => void +}) { + const startRef = useRef<{ x: number; width: number } | null>(null) + const [dragging, setDragging] = useState(false) + + const onPointerDown = (e: React.PointerEvent) => { + e.preventDefault() + startRef.current = { x: e.clientX, width } + setDragging(true) + e.currentTarget.setPointerCapture(e.pointerId) + } + + const onPointerMove = (e: React.PointerEvent) => { + if (!startRef.current) return + onResize(startRef.current.width - (e.clientX - startRef.current.x)) + } + + const onPointerUp = (e: React.PointerEvent) => { + startRef.current = null + setDragging(false) + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId) + } + } + + useEffect(() => { + if (!dragging) return + const prev = document.body.style.cursor + document.body.style.cursor = "col-resize" + return () => { + document.body.style.cursor = prev + } + }, [dragging]) + + return ( +
+ ) +} + +function treeThemeStyle(): React.CSSProperties { + return { + "--trees-theme-sidebar-bg": "var(--ui-surface)", + "--trees-theme-sidebar-fg": "var(--ui-text)", + "--trees-theme-sidebar-border": "var(--ui-border)", + "--trees-theme-sidebar-header-fg": "var(--ui-text-dim)", + "--trees-theme-list-hover-bg": "var(--ui-panel-2)", + "--trees-theme-list-active-selection-bg": "var(--ui-accent-bubble)", + "--trees-theme-list-active-selection-fg": "var(--ui-text)", + "--trees-theme-input-bg": "var(--ui-panel)", + "--trees-theme-input-border": "var(--ui-border)", + "--trees-theme-focus-ring": "var(--ui-accent)", + "--trees-theme-scrollbar-thumb": "var(--ui-border)", + "--trees-theme-git-added-fg": "var(--ui-success)", + "--trees-theme-git-deleted-fg": "var(--ui-danger)", + "--trees-theme-git-modified-fg": "var(--ui-accent)", + } as React.CSSProperties } export function AgentGitPanel({ thread }: AgentGitPanelProps) { - const [tab, setTab] = useState<"diff" | "review" | "commits">("diff"); - const pr = thread.pr; + const [topTab, setTopTab] = useState<"git" | "desktop" | "terminal">("git") + const [tab, setTab] = useState<"diff" | "review" | "commits">("diff") + const [collapsed, setCollapsedState] = useState(() => readStoredPanelCollapsed()) + const [width, setWidthState] = useState(() => readStoredPanelWidth()) + const [fullScreen, setFullScreen] = useState(false) - const fileContents = useMemo(() => { - const defaults: Record = { - "app.js": { - original: "export function init() {\n mount();\n}\n", - modified: "export function init() {\n // test comment\n mount();\n}\n", - }, - "contextPanel.js": { - original: "export function renderPanel() {\n return panel;\n}\n", - modified: "export function renderPanel() {\n // test comment\n return panel;\n}\n", - }, - "settings.js": { - original: "export const defaults = {};\n", - modified: "export const defaults = {};\n// test comment\n", - }, - }; + const setCollapsed = useCallback((next: boolean) => { + setCollapsedState(next) + window.localStorage.setItem(PANEL_STORAGE_COLLAPSED, next ? "1" : "0") + }, []) - return (thread.changedFiles ?? []).map((file) => ({ - path: file.path, + const setWidth = useCallback((next: number) => { + const clamped = Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, next)) + setWidthState(clamped) + window.localStorage.setItem(PANEL_STORAGE_WIDTH, String(clamped)) + }, []) + const [selectedTreePath, setSelectedTreePath] = useState(null) + const sectionRefs = useRef>({}) + const pr = thread.pr + + const chunks = useMemo( + () => thread.messages.flatMap((message) => message.chunks), + [thread.messages] + ) + + const files = useMemo>(() => { + const summary = summarizeChangedFiles(chunks) + const prefix = commonDirPrefix(summary.map((file) => file.filePath)) + return summary.map((file) => ({ + filePath: file.filePath, + treePath: + prefix && file.filePath.startsWith(prefix) + ? file.filePath.slice(prefix.length) + : file.filePath, additions: file.additions, - original: defaults[file.path]?.original ?? "", - modified: defaults[file.path]?.modified ?? file.patch ?? "", - })); - }, [thread.changedFiles]); + deletions: file.deletions, + originalContent: file.originalContent, + modifiedContent: file.modifiedContent, + status: deriveStatus(file), + })) + }, [chunks]) + + const totals = useMemo( + () => + files.reduce( + (acc, file) => ({ + additions: acc.additions + file.additions, + deletions: acc.deletions + file.deletions, + }), + { additions: 0, deletions: 0 } + ), + [files] + ) + + useEffect(() => { + if (!selectedTreePath) return + const target = files.find((file) => file.treePath === selectedTreePath) + if (!target) return + sectionRefs.current[target.filePath]?.scrollIntoView({ + block: "start", + behavior: "smooth", + }) + }, [selectedTreePath, files]) + + if (collapsed) { + return ( + + ) + } return ( -
-
- {tab === "diff" && fileContents.length > 0 ? ( -
- {fileContents.map((file) => ( - - ))} -
- ) : ( -
- {tab === "commits" ? "Commit history will appear here." : "No diff available."} +
+
+ {tab === "diff" && files.length > 0 ? ( +
+ {files.map((file) => ( + { + sectionRefs.current[file.filePath] = node + }} + /> + ))} +
+ ) : ( +
+ {tab === "diff" ? "No diff available." : "Coming Soon"} +
+ )} +
+ + {fullScreen && files.length > 0 && ( +
+
)}
+ + )} +
+ {!fullScreen && } - ); + ) } function FileDiffSection({ file, + sectionRef, }: { - file: { path: string; additions: number; original: string; modified: string }; + file: PanelFile + sectionRef: (node: HTMLDivElement | null) => void }) { - const [open, setOpen] = useState(true); + const [open, setOpen] = useState(true) + const diffOptions = useDiffOptions() return ( -
+
{open && ( -
+
)}
- ); + ) +} + +function FileTreeExplorer({ + files, + selectedTreePath, + onSelect, +}: { + files: Array + selectedTreePath: string | null + onSelect: (path: string) => void +}) { + const paths = useMemo(() => files.map((file) => file.treePath), [files]) + const gitStatus = useMemo>( + () => files.map((file) => ({ path: file.treePath, status: file.status })), + [files] + ) + + const { model } = useFileTree({ + paths, + gitStatus, + initialExpansion: "open", + flattenEmptyDirectories: true, + search: true, + icons: "standard", + }) + + useEffect(() => { + model.resetPaths(paths) + }, [model, paths]) + + useEffect(() => { + model.setGitStatus(gitStatus) + }, [model, gitStatus]) + + const selection = useFileTreeSelection(model) + useEffect(() => { + const path = selection[0] + if (path) onSelect(path) + }, [selection, onSelect]) + + useEffect(() => { + if (selectedTreePath) { + model.scrollToPath(selectedTreePath, { focus: false }) + } + }, [model, selectedTreePath]) + + return ( +
+ +
+ ) } diff --git a/ui/src/components/agents/AgentThreadView.tsx b/ui/src/components/agents/AgentThreadView.tsx index f867d137..0032e6fc 100644 --- a/ui/src/components/agents/AgentThreadView.tsx +++ b/ui/src/components/agents/AgentThreadView.tsx @@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query" import type { PendingPrompt } from "@/lib/agents/pendingPrompts" import type { AgentThread, ImageChunk, Message } from "@/lib/agents/types" import type { ModelSelection } from "@/lib/agents/useModelOptions" +import { AgentGitPanel } from "@/components/agents/AgentGitPanel" import { AgentPromptBar } from "@/components/agents/AgentPromptBar" import { MessageView } from "@/components/agents/ported" import { @@ -169,8 +170,8 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) { hasActiveRun && thread.messages.length === 0 && pendingPrompts.length > 0 return ( -
-
+
+
{hasMessages ? (
)}
+
) } diff --git a/ui/src/components/agents/ported/CloudPromptBar.tsx b/ui/src/components/agents/ported/CloudPromptBar.tsx index f60e2a40..dc38b1cf 100644 --- a/ui/src/components/agents/ported/CloudPromptBar.tsx +++ b/ui/src/components/agents/ported/CloudPromptBar.tsx @@ -360,14 +360,14 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({ - {canCancel && ( + {canCancel && !canSubmit ? ( + ) : ( + )} - -
diff --git a/ui/src/components/agents/ported/MessageView.tsx b/ui/src/components/agents/ported/MessageView.tsx index ca9e211e..9bc63bdf 100644 --- a/ui/src/components/agents/ported/MessageView.tsx +++ b/ui/src/components/agents/ported/MessageView.tsx @@ -3,7 +3,7 @@ import { useRef, useEffect, useLayoutEffect, useCallback, memo, useMemo, useStat import { ChevronDown, ChevronUp } from "lucide-react"; import { diffLines } from "diff"; import { MultiFileDiff } from "@pierre/diffs/react"; -import { diffOptions } from "@/components/agents/utils/diffUtils"; +import { useDiffOptions } from "@/components/agents/utils/diffUtils"; import { CodeBlock } from "./CodeBlock"; import { Markdown } from "./Markdown"; import { ToolExecution } from "./ToolExecution"; @@ -180,6 +180,7 @@ const TurnChangedFilesCard = memo(function TurnChangedFilesCard({ projectPath?: string; }) { const [expandedByPath, setExpandedByPath] = useState>({}); + const diffOptions = useDiffOptions(); const toggleFile = useCallback((filePath: string) => { setExpandedByPath((prev) => ({ ...prev, [filePath]: !prev[filePath] })); diff --git a/ui/src/components/agents/ported/SourceControlTile.tsx b/ui/src/components/agents/ported/SourceControlTile.tsx index 0bff6d86..0b433fc1 100644 --- a/ui/src/components/agents/ported/SourceControlTile.tsx +++ b/ui/src/components/agents/ported/SourceControlTile.tsx @@ -21,7 +21,7 @@ import { X, Undo2, } from "lucide-react"; -import { diffOptions } from "@/components/agents/utils/diffUtils"; +import { useDiffOptions } from "@/components/agents/utils/diffUtils"; const STATUS_LABELS: Partial> = { "index-modified": "M", @@ -961,6 +961,7 @@ const DiffCard = memo(function DiffCard({ defaultExpanded: boolean; }) { const [expanded, setExpanded] = useState(defaultExpanded); + const diffOptions = useDiffOptions(); return (
setExpanded((prev) => !prev), []); + const diffOptions = useDiffOptions(); + const inlineDiffOptions = useMemo( + () => ({ ...diffOptions, disableFileHeader: true }), + [diffOptions] + ); const scrollRef = useRef(null); const [scrolledFromTop, setScrolledFromTop] = useState(false); const [scrolledFromBottom, setScrolledFromBottom] = useState(false); diff --git a/ui/src/components/agents/ported/index.ts b/ui/src/components/agents/ported/index.ts index 480cf790..fd1e312f 100644 --- a/ui/src/components/agents/ported/index.ts +++ b/ui/src/components/agents/ported/index.ts @@ -7,7 +7,12 @@ export { HeaderBar } from "./HeaderBar"; export { Logo } from "./Logo"; export { Markdown } from "./Markdown"; export { MarkdownTable } from "./MarkdownTable"; -export { MessageView, summarizeChangedFiles, type MessageViewScrollControl } from "./MessageView"; +export { + MessageView, + summarizeChangedFiles, + type ChangedFileSummaryItem, + type MessageViewScrollControl, +} from "./MessageView"; export { PanelResizeHandle } from "./PanelResizeHandle"; export { ShellCommand } from "./ShellCommand"; export { TodoList } from "./TodoList"; diff --git a/ui/src/components/agents/utils/diffUtils.ts b/ui/src/components/agents/utils/diffUtils.ts index 4880c561..6db34b0a 100644 --- a/ui/src/components/agents/utils/diffUtils.ts +++ b/ui/src/components/agents/utils/diffUtils.ts @@ -1,3 +1,6 @@ +import { useMemo } from "react"; +import { useResolvedTheme } from "@/lib/theme"; + export const DIFF_UNSAFE_CSS = ` [data-diffs-header], [data-diff], @@ -62,3 +65,11 @@ export const diffOptions = { unsafeCSS: DIFF_UNSAFE_CSS, collapsedContextThreshold: 4, }; + +export function useDiffOptions() { + const resolvedTheme = useResolvedTheme(); + return useMemo( + () => ({ ...diffOptions, themeType: resolvedTheme }), + [resolvedTheme] + ); +} diff --git a/ui/src/routeTree.gen.ts b/ui/src/routeTree.gen.ts index 4a9003cc..e9ff5def 100644 --- a/ui/src/routeTree.gen.ts +++ b/ui/src/routeTree.gen.ts @@ -125,8 +125,8 @@ export interface FileRoutesByFullPath { '/review': typeof ReviewRoute '/usage': typeof UsageRoute '/agents/$threadId': typeof AgentsThreadIdRoute - '/review/styles': typeof ReviewStylesRoute '/agents/instructions': typeof AgentsInstructionsRoute + '/review/styles': typeof ReviewStylesRoute '/agents/': typeof AgentsIndexRoute '/agents/automations/$scheduleId': typeof AgentsAutomationsScheduleIdRoute '/agents/automations/new': typeof AgentsAutomationsNewRoute @@ -143,8 +143,8 @@ export interface FileRoutesByTo { '/review': typeof ReviewRoute '/usage': typeof UsageRoute '/agents/$threadId': typeof AgentsThreadIdRoute - '/review/styles': typeof ReviewStylesRoute '/agents/instructions': typeof AgentsInstructionsRoute + '/review/styles': typeof ReviewStylesRoute '/agents': typeof AgentsIndexRoute '/agents/automations/$scheduleId': typeof AgentsAutomationsScheduleIdRoute '/agents/automations/new': typeof AgentsAutomationsNewRoute @@ -163,8 +163,8 @@ export interface FileRoutesById { '/review': typeof ReviewRoute '/usage': typeof UsageRoute '/agents/$threadId': typeof AgentsThreadIdRoute - '/review_/styles': typeof ReviewStylesRoute '/agents_/instructions': typeof AgentsInstructionsRoute + '/review_/styles': typeof ReviewStylesRoute '/agents/': typeof AgentsIndexRoute '/agents/automations/$scheduleId': typeof AgentsAutomationsScheduleIdRoute '/agents/automations/new': typeof AgentsAutomationsNewRoute @@ -184,8 +184,8 @@ export interface FileRouteTypes { | '/review' | '/usage' | '/agents/$threadId' - | '/review/styles' | '/agents/instructions' + | '/review/styles' | '/agents/' | '/agents/automations/$scheduleId' | '/agents/automations/new' @@ -202,8 +202,8 @@ export interface FileRouteTypes { | '/review' | '/usage' | '/agents/$threadId' - | '/review/styles' | '/agents/instructions' + | '/review/styles' | '/agents' | '/agents/automations/$scheduleId' | '/agents/automations/new' @@ -221,8 +221,8 @@ export interface FileRouteTypes { | '/review' | '/usage' | '/agents/$threadId' - | '/review_/styles' | '/agents_/instructions' + | '/review_/styles' | '/agents/' | '/agents/automations/$scheduleId' | '/agents/automations/new' @@ -240,8 +240,8 @@ export interface RootRouteChildren { MySettingsRoute: typeof MySettingsRoute ReviewRoute: typeof ReviewRoute UsageRoute: typeof UsageRoute - ReviewStylesRoute: typeof ReviewStylesRoute AgentsInstructionsRoute: typeof AgentsInstructionsRoute + ReviewStylesRoute: typeof ReviewStylesRoute ReviewRepositoriesOwnerRoute: typeof ReviewRepositoriesOwnerRoute } @@ -398,8 +398,8 @@ const rootRouteChildren: RootRouteChildren = { MySettingsRoute: MySettingsRoute, ReviewRoute: ReviewRoute, UsageRoute: UsageRoute, - ReviewStylesRoute: ReviewStylesRoute, AgentsInstructionsRoute: AgentsInstructionsRoute, + ReviewStylesRoute: ReviewStylesRoute, ReviewRepositoriesOwnerRoute: ReviewRepositoriesOwnerRoute, } export const routeTree = rootRouteImport diff --git a/ui/yarn.lock b/ui/yarn.lock index 62b2cfcc..db60be9f 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1658,6 +1658,14 @@ resolved "https://registry.yarnpkg.com/@pierre/theme/-/theme-1.0.3.tgz#eec66e55576f981000f4816263632d3ba1479229" integrity sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA== +"@pierre/trees@1.0.0-beta.4": + version "1.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@pierre/trees/-/trees-1.0.0-beta.4.tgz#d19881431e2396c19641b6bd36d1d4e5a267cac8" + integrity sha512-OfT1yk9ne8Te5+GB5zUY8yqE6B8BqjBHQJleH4lu8ltwNpoocZl4vXt1AzlEExpxI/pp+AFX5QG+lR3JjtTEag== + dependencies: + preact "11.0.0-beta.0" + preact-render-to-string "6.6.5" + "@rolldown/binding-android-arm64@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz#ebe1264e43ba5bb224c58c85e0ac238f87e5ad14" @@ -6710,6 +6718,16 @@ powershell-utils@^0.1.0: resolved "https://registry.yarnpkg.com/powershell-utils/-/powershell-utils-0.1.0.tgz#5a42c9a824fb4f2f251ccb41aaae73314f5d6ac2" integrity sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A== +preact-render-to-string@6.6.5: + version "6.6.5" + resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-6.6.5.tgz#1be84cc8565e3bcfcf2a33cb9c61185e6498ab31" + integrity sha512-O6MHzYNIKYaiSX3bOw0gGZfEbOmlIDtDfWwN1JJdc/T3ihzRT6tGGSEWE088dWrEDGa1u7101q+6fzQnO9XCPA== + +preact@11.0.0-beta.0: + version "11.0.0-beta.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-11.0.0-beta.0.tgz#c6e5c6e8657abf18b4a637eae88b5b377d36973f" + integrity sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg== + prettier-plugin-tailwindcss@^0.7.2: version "0.7.4" resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.4.tgz#eadd3f9d2e4bfba2994b22f80dda25804a608a9a"