From 5d4065645f24a9a4b41a2e44d05f231928c96323 Mon Sep 17 00:00:00 2001 From: Enis Date: Sun, 2 Aug 2026 12:37:38 +0200 Subject: [PATCH] feat(web): add branch changes and review actions - Add header actions to open branch diffs and insert review prompts - Make the review prompt configurable in source control settings --- .../web/src/components/ChatView.logic.test.ts | 15 ++++++ apps/web/src/components/ChatView.logic.ts | 6 +++ apps/web/src/components/ChatView.tsx | 23 ++++++++ apps/web/src/components/chat/ChatHeader.tsx | 53 +++++++++++++++++++ .../settings/SourceControlWritingSettings.tsx | 34 ++++++++++++ docs/integrations/source-control-providers.md | 5 ++ packages/contracts/src/settings.test.ts | 14 +++++ packages/contracts/src/settings.ts | 7 +++ 8 files changed, 157 insertions(+) diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 39285438d1af..3a6dbdf06b16 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -21,6 +21,7 @@ import { dismissBranchMismatchForSession, getStartedThreadModelChangeBlockReason, hasServerAcknowledgedLocalDispatch, + insertReviewPromptIntoDraft, isBranchMismatchDismissedForSession, reconcileMountedTerminalThreadIds, reconcileRetainedMountedThreadIds, @@ -36,6 +37,20 @@ const projectId = ProjectId.make("project-1"); const threadId = ThreadId.make("thread-1"); const now = "2026-03-29T00:00:00.000Z"; +describe("insertReviewPromptIntoDraft", () => { + it("inserts into an empty composer", () => { + expect(insertReviewPromptIntoDraft("", " Review these changes. ")).toBe( + "Review these changes.", + ); + }); + + it("appends without overwriting an existing draft", () => { + expect(insertReviewPromptIntoDraft("Keep this context. \n", "Review these changes.")).toBe( + "Keep this context.\n\nReview these changes.", + ); + }); +}); + function makeThread(overrides: Partial = {}): Thread { return { id: threadId, diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 04b35fd45516..e62cc06c99ea 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -564,3 +564,9 @@ export function hasServerAcknowledgedLocalDispatch(input: { input.localDispatch.sessionUpdatedAt !== (session?.updatedAt ?? null) ); } + +export function insertReviewPromptIntoDraft(currentPrompt: string, reviewPrompt: string): string { + const current = currentPrompt.trimEnd(); + const review = reviewPrompt.trim(); + return current.length > 0 ? `${current}\n\n${review}` : review; +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 26950866bb17..5af782b1cc44 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -260,6 +260,7 @@ import { deriveComposerSendState, dismissBranchMismatchForSession, hasServerAcknowledgedLocalDispatch, + insertReviewPromptIntoDraft, isBranchMismatchDismissedForSession, shouldShowBranchMismatchBanner, getStartedThreadModelChangeBlockReason, @@ -3068,6 +3069,25 @@ function ChatViewContent(props: ChatViewProps) { onDiffPanelOpen, planSidebarOpen, ]); + const openBranchChanges = useCallback(() => { + if (!activeThreadRef || !isServerThread || !isGitRepo) return; + useDiffPanelStore.getState().selectGitScope(activeThreadRef, "branch"); + addDiffSurface(); + }, [activeThreadRef, addDiffSurface, isGitRepo, isServerThread]); + const insertReviewPrompt = useCallback(() => { + const currentPrompt = + useComposerDraftStore.getState().getComposerDraft(composerDraftTarget)?.prompt ?? ""; + setComposerDraftPrompt( + composerDraftTarget, + insertReviewPromptIntoDraft(currentPrompt, primaryServerSettings.reviewPrompt), + ); + scheduleComposerFocus(); + }, [ + composerDraftTarget, + primaryServerSettings.reviewPrompt, + scheduleComposerFocus, + setComposerDraftPrompt, + ]); const addFilesSurface = useCallback(() => { if (!activeThreadRef || !activeProject) return; useRightPanelStore.getState().open(activeThreadRef, "files"); @@ -5716,6 +5736,9 @@ function ChatViewContent(props: ChatViewProps) { availableEditors={availableEditors} rightPanelOpen={rightPanelOpen} gitCwd={gitCwd} + canOpenChanges={isServerThread && isGitRepo} + onOpenChanges={openBranchChanges} + onReview={insertReviewPrompt} onNewThreadInProject={handleNewThreadInActiveProject} onRunProjectScript={runProjectScript} onAddProjectScript={saveProjectScript} diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index c0a5cada5e1e..dfa7dd4715e2 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -6,6 +6,7 @@ import { type ThreadId, } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { FileDiffIcon, ScanSearchIcon } from "lucide-react"; import { memo } from "react"; import GitActionsControl from "../GitActionsControl"; import { type DraftId } from "~/composerDraftStore"; @@ -19,6 +20,7 @@ import { usePrimaryEnvironmentId } from "../../state/environments"; import { useT3ProjectFileScripts } from "~/hooks/useT3ProjectFileScripts"; import { ProjectFavicon } from "../ProjectFavicon"; import { cn } from "~/lib/utils"; +import { Button } from "../ui/button"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -34,6 +36,9 @@ interface ChatHeaderProps { availableEditors: ReadonlyArray; rightPanelOpen: boolean; gitCwd: string | null; + canOpenChanges: boolean; + onOpenChanges: () => void; + onReview: () => void; onNewThreadInProject: () => void; onRunProjectScript: (script: ProjectScript) => void; onAddProjectScript: (input: NewProjectScriptInput) => Promise; @@ -70,6 +75,9 @@ export const ChatHeader = memo(function ChatHeader({ availableEditors, rightPanelOpen, gitCwd, + canOpenChanges, + onOpenChanges, + onReview, onNewThreadInProject, onRunProjectScript, onAddProjectScript, @@ -163,6 +171,51 @@ export const ChatHeader = memo(function ChatHeader({ : null } /> + {activeProjectName ? ( + <> + + + } + > + + + Changes + + + + {canOpenChanges + ? "View files changed on this branch" + : "Changes are available after this thread starts"} + + + + + } + > + + Review + + Insert the configured review prompt + + + ) : null} {activeProjectName && ( } /> + + + updateSettings({ reviewPrompt: DEFAULT_UNIFIED_SETTINGS.reviewPrompt }) + } + /> + ) : null + } + > +
+