diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index f0d0092bc..a9f63daab 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -591,6 +591,11 @@ pub(crate) struct AppSettings { rename = "gitDiffIgnoreWhitespaceChanges" )] pub(crate) git_diff_ignore_whitespace_changes: bool, + #[serde( + default = "default_git_pull_request_prompt", + rename = "gitPullRequestPrompt" + )] + pub(crate) git_pull_request_prompt: String, #[serde( default = "default_system_notifications_enabled", rename = "systemNotificationsEnabled" @@ -924,6 +929,20 @@ fn default_git_diff_ignore_whitespace_changes() -> bool { false } +fn default_git_pull_request_prompt() -> String { + [ + "You are reviewing a GitHub pull request.", + "PR: #{{number}} {{title}}", + "URL: {{url}}", + "Author: @{{author}}", + "Branches: {{baseRef}} <- {{headRef}}", + "Updated: {{updatedAt}}{{draftState}}{{descriptionSection}}", + "", + "Diff: {{diffSummary}}{{questionSection}}", + ] + .join("\n") +} + fn default_experimental_collab_enabled() -> bool { false } @@ -1169,6 +1188,7 @@ impl Default for AppSettings { system_notifications_enabled: true, preload_git_diffs: default_preload_git_diffs(), git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(), + git_pull_request_prompt: default_git_pull_request_prompt(), experimental_collab_enabled: false, collaboration_modes_enabled: true, steer_enabled: true, @@ -1329,6 +1349,10 @@ mod tests { assert!(settings.system_notifications_enabled); assert!(settings.preload_git_diffs); assert!(!settings.git_diff_ignore_whitespace_changes); + assert_eq!( + settings.git_pull_request_prompt, + default_git_pull_request_prompt() + ); assert!(settings.collaboration_modes_enabled); assert!(settings.steer_enabled); assert!(settings.unified_exec_enabled); diff --git a/src/App.tsx b/src/App.tsx index 98eaf16d5..5156d6b93 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1543,6 +1543,7 @@ function MainApp() { activeWorkspace, selectedPullRequest, gitPullRequestDiffs, + gitPullRequestPromptTemplate: appSettings.gitPullRequestPrompt, filePanelMode, gitPanelMode, centerMode, diff --git a/src/features/git/hooks/usePullRequestComposer.test.tsx b/src/features/git/hooks/usePullRequestComposer.test.tsx index 210c07189..33ea9b724 100644 --- a/src/features/git/hooks/usePullRequestComposer.test.tsx +++ b/src/features/git/hooks/usePullRequestComposer.test.tsx @@ -54,6 +54,7 @@ const makeOptions = (overrides: Partial { pullRequest, diffs, "Question?", + "Default template", ); expect(options.startThreadForWorkspace).toHaveBeenCalledWith( disconnectedWorkspace.id, @@ -193,6 +195,7 @@ describe("usePullRequestComposer", () => { pullRequest, diffs, "/src-tauri/something", + "Default template", ); expect(options.startThreadForWorkspace).toHaveBeenCalledWith( connectedWorkspace.id, diff --git a/src/features/git/hooks/usePullRequestComposer.ts b/src/features/git/hooks/usePullRequestComposer.ts index 08a050b86..342754495 100644 --- a/src/features/git/hooks/usePullRequestComposer.ts +++ b/src/features/git/hooks/usePullRequestComposer.ts @@ -11,6 +11,7 @@ type UsePullRequestComposerOptions = { activeWorkspace: WorkspaceInfo | null; selectedPullRequest: GitHubPullRequest | null; gitPullRequestDiffs: GitHubPullRequestDiff[]; + gitPullRequestPromptTemplate: string; filePanelMode: "git" | "files" | "prompts"; gitPanelMode: "diff" | "log" | "issues" | "prs"; centerMode: "chat" | "diff"; @@ -40,6 +41,7 @@ export function usePullRequestComposer({ activeWorkspace, selectedPullRequest, gitPullRequestDiffs, + gitPullRequestPromptTemplate, filePanelMode, gitPanelMode, centerMode, @@ -120,6 +122,7 @@ export function usePullRequestComposer({ selectedPullRequest, gitPullRequestDiffs, trimmed, + gitPullRequestPromptTemplate, ); const threadId = await startThreadForWorkspace(activeWorkspace.id, { activate: false, @@ -135,6 +138,7 @@ export function usePullRequestComposer({ clearActiveImages, connectWorkspace, gitPullRequestDiffs, + gitPullRequestPromptTemplate, handleSend, selectedPullRequest, sendUserMessageToThread, diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index e8ee54ac6..09862590e 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -11,6 +11,7 @@ import { import type { ComponentProps } from "react"; import { describe, expect, it, vi } from "vitest"; import type { AppSettings, WorkspaceInfo } from "../../../types"; +import { DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE } from "../../../utils/pullRequestPrompt"; import { SettingsView } from "./SettingsView"; vi.mock("@tauri-apps/plugin-dialog", () => ({ @@ -68,6 +69,7 @@ const baseSettings: AppSettings = { systemNotificationsEnabled: true, preloadGitDiffs: true, gitDiffIgnoreWhitespaceChanges: false, + gitPullRequestPrompt: DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE, experimentalCollabEnabled: false, collaborationModesEnabled: true, steerEnabled: true, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index c808e62e5..c0777d8ec 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -36,6 +36,7 @@ import { clampCodeFontSize, normalizeFontFamily, } from "../../../utils/fonts"; +import { DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE } from "../../../utils/pullRequestPrompt"; import { useGlobalAgentsMd } from "../hooks/useGlobalAgentsMd"; import { useGlobalCodexConfigToml } from "../hooks/useGlobalCodexConfigToml"; import { useSettingsOpenAppDrafts } from "../hooks/useSettingsOpenAppDrafts"; @@ -208,6 +209,10 @@ export function SettingsView({ ); const [orbitAccessClientSecretRefDraft, setOrbitAccessClientSecretRefDraft] = useState(appSettings.orbitAccessClientSecretRef ?? ""); + const [gitPullRequestPromptDraft, setGitPullRequestPromptDraft] = useState( + appSettings.gitPullRequestPrompt, + ); + const [gitPullRequestPromptSaving, setGitPullRequestPromptSaving] = useState(false); const [orbitStatusText, setOrbitStatusText] = useState(null); const [orbitAuthCode, setOrbitAuthCode] = useState(null); const [orbitVerificationUrl, setOrbitVerificationUrl] = useState( @@ -419,6 +424,10 @@ export function SettingsView({ setOrbitAccessClientSecretRefDraft(appSettings.orbitAccessClientSecretRef ?? ""); }, [appSettings.orbitAccessClientSecretRef]); + useEffect(() => { + setGitPullRequestPromptDraft(appSettings.gitPullRequestPrompt); + }, [appSettings.gitPullRequestPrompt]); + useEffect(() => { setScaleDraft(`${Math.round(clampUiScale(appSettings.uiScale) * 100)}%`); }, [appSettings.uiScale]); @@ -528,6 +537,8 @@ export function SettingsView({ const codexDirty = nextCodexBin !== (appSettings.codexBin ?? null) || nextCodexArgs !== (appSettings.codexArgs ?? null); + const gitPullRequestPromptDirty = + gitPullRequestPromptDraft !== appSettings.gitPullRequestPrompt; const trimmedScale = scaleDraft.trim(); const parsedPercent = trimmedScale @@ -548,6 +559,43 @@ export function SettingsView({ } }; + const handleSaveGitPullRequestPrompt = useCallback(async () => { + if (!gitPullRequestPromptDirty) { + return; + } + const nextValue = gitPullRequestPromptDraft.trim() + ? gitPullRequestPromptDraft + : DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE; + setGitPullRequestPromptSaving(true); + setGitPullRequestPromptDraft(nextValue); + try { + await onUpdateAppSettings({ + ...appSettings, + gitPullRequestPrompt: nextValue, + }); + } finally { + setGitPullRequestPromptSaving(false); + } + }, [ + appSettings, + gitPullRequestPromptDirty, + gitPullRequestPromptDraft, + onUpdateAppSettings, + ]); + + const handleResetGitPullRequestPrompt = useCallback(async () => { + setGitPullRequestPromptSaving(true); + setGitPullRequestPromptDraft(DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE); + try { + await onUpdateAppSettings({ + ...appSettings, + gitPullRequestPrompt: DEFAULT_PULL_REQUEST_PROMPT_TEMPLATE, + }); + } finally { + setGitPullRequestPromptSaving(false); + } + }, [appSettings, onUpdateAppSettings]); + const updateRemoteBackendSettings = useCallback( async ({ host, @@ -1431,6 +1479,12 @@ export function SettingsView({ )} {activeSection === "server" && ( diff --git a/src/features/settings/components/sections/SettingsGitSection.tsx b/src/features/settings/components/sections/SettingsGitSection.tsx index 4f054bcb0..da9b4dfde 100644 --- a/src/features/settings/components/sections/SettingsGitSection.tsx +++ b/src/features/settings/components/sections/SettingsGitSection.tsx @@ -3,17 +3,29 @@ import type { AppSettings } from "../../../../types"; type SettingsGitSectionProps = { appSettings: AppSettings; onUpdateAppSettings: (next: AppSettings) => Promise; + gitPullRequestPromptDraft: string; + gitPullRequestPromptDirty: boolean; + gitPullRequestPromptSaving: boolean; + onSetGitPullRequestPromptDraft: (next: string) => void; + onSaveGitPullRequestPrompt: () => Promise; + onResetGitPullRequestPrompt: () => Promise; }; export function SettingsGitSection({ appSettings, onUpdateAppSettings, + gitPullRequestPromptDraft, + gitPullRequestPromptDirty, + gitPullRequestPromptSaving, + onSetGitPullRequestPromptDraft, + onSaveGitPullRequestPrompt, + onResetGitPullRequestPrompt, }: SettingsGitSectionProps) { return (
Git
- Manage how diffs are loaded in the Git sidebar. + Manage how diffs are loaded and pull request prompts are composed.
@@ -55,6 +67,47 @@ export function SettingsGitSection({
+
+ +
+ Template used when asking questions about GitHub pull requests. Available tokens:{" "} + {"{{number}}"}, {"{{title}}"},{" "} + {"{{url}}"}, {"{{author}}"},{" "} + {"{{baseRef}}"}, {"{{headRef}}"},{" "} + {"{{updatedAt}}"}, {"{{draftState}}"},{" "} + {"{{descriptionSection}}"}, {"{{diffSummary}}"},{" "} + {"{{questionSection}}"}. +
+