diff --git a/apps/code/src/renderer/features/git-interaction/components/CreatePrDialog.tsx b/apps/code/src/renderer/features/git-interaction/components/CreatePrDialog.tsx index ffc50c117..bf269dd61 100644 --- a/apps/code/src/renderer/features/git-interaction/components/CreatePrDialog.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/CreatePrDialog.tsx @@ -2,9 +2,11 @@ import { ErrorContainer, GenerateButton, } from "@features/git-interaction/components/GitInteractionDialogs"; +import { useFixWithAgent } from "@features/git-interaction/hooks/useFixWithAgent"; import { useGitInteractionStore } from "@features/git-interaction/state/gitInteractionStore"; import type { CreatePrStep } from "@features/git-interaction/types"; import type { DiffStats } from "@features/git-interaction/utils/diffStats"; +import { buildCreatePrFlowErrorPrompt } from "@features/git-interaction/utils/errorPrompts"; import { CheckCircle, Circle, @@ -133,6 +135,9 @@ export function CreatePrDialog({ }: CreatePrDialogProps) { const store = useGitInteractionStore(); const { actions } = store; + const { canFixWithAgent, fixWithAgent } = useFixWithAgent(() => + buildCreatePrFlowErrorPrompt(store.createPrFailedStep), + ); const { createPrStep: step } = store; const isExecuting = step !== "idle" && step !== "complete"; @@ -299,7 +304,17 @@ export function CreatePrDialog({ /> {step === "error" && store.createPrError && ( - + { + fixWithAgent(store.createPrError ?? ""); + actions.closeCreatePr(); + } + : undefined + } + /> )} diff --git a/apps/code/src/renderer/features/git-interaction/components/GitInteractionDialogs.tsx b/apps/code/src/renderer/features/git-interaction/components/GitInteractionDialogs.tsx index 4dfe7610a..6f9f964d5 100644 --- a/apps/code/src/renderer/features/git-interaction/components/GitInteractionDialogs.tsx +++ b/apps/code/src/renderer/features/git-interaction/components/GitInteractionDialogs.tsx @@ -26,7 +26,13 @@ import { useState } from "react"; const ICON_SIZE = 14; -export function ErrorContainer({ error }: { error: string }) { +export function ErrorContainer({ + error, + onFixWithAgent, +}: { + error: string; + onFixWithAgent?: () => void; +}) { const [copied, setCopied] = useState(false); const handleCopy = async () => { @@ -59,16 +65,30 @@ export function ErrorContainer({ error }: { error: string }) { > {error} - - - - - + + {onFixWithAgent && ( + + + + + + )} + + + + + + diff --git a/apps/code/src/renderer/features/git-interaction/hooks/useFixWithAgent.ts b/apps/code/src/renderer/features/git-interaction/hooks/useFixWithAgent.ts new file mode 100644 index 000000000..ae767ddf1 --- /dev/null +++ b/apps/code/src/renderer/features/git-interaction/hooks/useFixWithAgent.ts @@ -0,0 +1,52 @@ +import { DEFAULT_TAB_IDS } from "@features/panels/constants/panelConstants"; +import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; +import { findTabInTree } from "@features/panels/store/panelTree"; +import { getSessionService } from "@features/sessions/service/service"; +import { useSessionForTask } from "@features/sessions/stores/sessionStore"; +import { useNavigationStore } from "@stores/navigationStore"; +import { useCallback } from "react"; +import type { FixWithAgentPrompt } from "../utils/errorPrompts"; + +/** + * Hook that sends a structured error prompt to the active agent session. + * Derives taskId and session readiness from stores. + * + * `canFixWithAgent` is true when there's an active, connected session. + */ +export function useFixWithAgent( + buildPrompt: (error: string) => FixWithAgentPrompt, +): { + canFixWithAgent: boolean; + fixWithAgent: (error: string) => Promise; +} { + const taskId = useNavigationStore((s) => + s.view.type === "task-detail" ? s.view.data?.id : undefined, + ); + const session = useSessionForTask(taskId); + const isSessionReady = session?.status === "connected"; + + const canFixWithAgent = !!(taskId && isSessionReady); + + const fixWithAgent = useCallback( + async (error: string) => { + if (!taskId || !isSessionReady) return; + + const { label, context } = buildPrompt(error); + + const prompt = `${context}\n\n\`\`\`\n${error}\n\`\`\``; + getSessionService().sendPrompt(taskId, prompt); + + const { taskLayouts, setActiveTab } = usePanelLayoutStore.getState(); + const layout = taskLayouts[taskId]; + if (layout) { + const result = findTabInTree(layout.panelTree, DEFAULT_TAB_IDS.LOGS); + if (result) { + setActiveTab(taskId, result.panelId, DEFAULT_TAB_IDS.LOGS); + } + } + }, + [buildPrompt, taskId, isSessionReady], + ); + + return { canFixWithAgent, fixWithAgent }; +} diff --git a/apps/code/src/renderer/features/git-interaction/utils/errorPrompts.ts b/apps/code/src/renderer/features/git-interaction/utils/errorPrompts.ts new file mode 100644 index 000000000..607102a2e --- /dev/null +++ b/apps/code/src/renderer/features/git-interaction/utils/errorPrompts.ts @@ -0,0 +1,39 @@ +import type { CreatePrStep } from "../types"; + +export interface FixWithAgentPrompt { + label: string; + context: string; +} + +export function buildCreatePrFlowErrorPrompt( + failedStep: CreatePrStep | null, +): FixWithAgentPrompt { + return { + label: `Fix PR creation error`, + context: `The user tried to create a pull request using the Create PR button in the UI, but it failed at the ${failedStep} step. + +This flow is supposed to follow these steps: +1. [creating-branch] Create a new feature branch, if needed (required if on default branch, optional otherwise) +2. [committing] Commit changes +3. [pushing] Push to remote +4. [creating-pr] Create PR + +When an error occurs, the app automatically performs a rollback. This means you are likely in the pre-error state, e.g. back on the user's original branch without any new commits. + +Rollback scenarios: +1. Branch creation fails -> check out user's original branch +2. Commit fails -> use git reset to get back to the user's original state + +Common errors and resolutions: +- **Duplicate branch names** - guide the user towards using a different branch name, or sorting out any git issues that have led them to this state +- **Commit hook failures** - this may be the result of missing dependencies, check failure (e.g. lints), or something else. Ensure you fully understand the issue before proceeding. + +Your task is to help the user diagnose and fix the underlying issue (e.g. resolve merge conflicts, fix authentication, clean up git state). + +IMPORTANT: +- Do NOT attempt to complete the PR flow yourself (no commit, push, or gh pr create). The user will retry via the "Create PR" button once the issue is resolved. +- You may fix the underlying issue, but always confirm destructive actions with the user first. +- Start by diagnosing: run read-only commands to understand the current state before taking action. +- When the issue is resolved, remind the user to retry by clicking the "Create PR" button in the UI.`, + }; +} diff --git a/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx b/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx index 4b5e75d2e..6079b3ec8 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/parseFileMentions.tsx @@ -2,7 +2,7 @@ import { baseComponents, defaultRemarkPlugins, } from "@features/editor/components/MarkdownRenderer"; -import { File, GithubLogo } from "@phosphor-icons/react"; +import { File, GithubLogo, Warning } from "@phosphor-icons/react"; import { Code, Text } from "@radix-ui/themes"; import type { ReactNode } from "react"; import { memo } from "react"; @@ -10,8 +10,9 @@ import type { Components } from "react-markdown"; import ReactMarkdown from "react-markdown"; const MENTION_TAG_REGEX = - /|/g; -const MENTION_TAG_TEST = /<(?:file\s+path|github_issue\s+number)="[^"]+"/; + /||[\s\S]*?<\/error_context>/g; +const MENTION_TAG_TEST = + /<(?:file\s+path|github_issue\s+number|error_context\s+label)="[^"]+"/; const inlineComponents: Components = { ...baseComponents, @@ -113,6 +114,14 @@ export function parseMentionTags(content: string): ReactNode[] { onClick={issueUrl ? () => window.open(issueUrl, "_blank") : undefined} />, ); + } else if (match[5]) { + parts.push( + } + label={match[5]} + />, + ); } lastIndex = matchIndex + match[0].length;