diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx index a40786a2581..1eb484b2b39 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx @@ -10,6 +10,7 @@ import { AGENT_CATALOG, getAgentLabel } from '@/lib/agent-catalog' import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface' import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab' import { useAppStore } from '@/store' +import { useRepoById } from '@/store/selectors' import { renderSourceControlActionCommandTemplate, type SourceControlActionRecipe, @@ -26,6 +27,7 @@ import { SourceControlAgentActionDialogForm, type SourceControlAgentActionDeliveryPlanState } from './SourceControlAgentActionDialogForm' +import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' export type SourceControlAgentActionDialogProps = { open: boolean @@ -93,6 +95,7 @@ export function SourceControlAgentActionDialog({ onStart }: SourceControlAgentActionDialogProps): React.JSX.Element { const settings = useAppStore((state) => state.settings) + const repo = useRepoById(repoId ?? null) const ensureDetectedAgents = useAppStore((state) => state.ensureDetectedAgents) const ensureRemoteDetectedAgents = useAppStore((state) => state.ensureRemoteDetectedAgents) const [commandTemplate, setCommandTemplate] = useState( @@ -311,12 +314,23 @@ export function SourceControlAgentActionDialog({ : saveTargetValue === 'global' ? ({ type: 'global' } as const) : null - if (saveTarget && onSaveAgentDefault) { - await onSaveAgentDefault(saveTarget, actionId, { - agentId: selectedAgent, - commandInputTemplate: commandTemplate, - agentArgs + const launchRecipe = { + agentId: selectedAgent, + commandInputTemplate: commandTemplate, + agentArgs + } + const launchRecipeAlreadySaved = Boolean( + saveTarget && + sourceControlActionRecipeMatchesTarget({ + actionId, + target: saveTarget, + recipe: launchRecipe, + settings, + repo }) + ) + if (saveTarget && onSaveAgentDefault && !launchRecipeAlreadySaved) { + await onSaveAgentDefault(saveTarget, actionId, launchRecipe) } onLaunched?.() handleOpenChange(false) @@ -339,8 +353,10 @@ export function SourceControlAgentActionDialog({ onStart, promptDelivery, refreshDetectedAgents, + repo, repoId, saveTargetValue, + settings, selectedAgent, trimmedCommandInput, worktreeId @@ -356,7 +372,7 @@ export function SourceControlAgentActionDialog({ return ( - + {title} {description} @@ -374,6 +390,8 @@ export function SourceControlAgentActionDialog({ baseCommandInput={baseCommandInput} saveTargetValue={saveTargetValue} saveTargets={saveTargets} + settings={settings} + repo={repo} canSaveAgentDefault={Boolean(onSaveAgentDefault)} deliveryPlan={deliveryPlan} canStart={canStart} diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index 7d0a367c72e..dcafa4e6d09 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -15,8 +15,10 @@ import { import type { AgentCatalogEntry } from '@/lib/agent-catalog' import { cn } from '@/lib/utils' import type { SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' -import type { TuiAgent } from '../../../../shared/types' +import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save' +import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' import { SourceControlActionVariableChips } from '../source-control/SourceControlActionVariableChips' +import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' export type SourceControlAgentActionDeliveryPlanState = | { status: 'idle' } @@ -36,6 +38,8 @@ type SourceControlAgentActionDialogFormProps = { baseCommandInput: string saveTargetValue: string saveTargets: { value: string; label: string }[] + settings: GlobalSettings | null + repo: Pick | null canSaveAgentDefault: boolean deliveryPlan: SourceControlAgentActionDeliveryPlanState canStart: boolean @@ -49,6 +53,19 @@ type SourceControlAgentActionDialogFormProps = { onStart: () => void } +function sourceControlLaunchSaveTargetFromValue( + value: string, + repo: Pick | null +): SourceControlAiWriteTarget | null { + if (value === 'repo' && repo?.id) { + return { type: 'repo', repoId: repo.id } + } + if (value === 'global') { + return { type: 'global' } + } + return null +} + export function SourceControlAgentActionDialogForm({ actionId, agentOptions, @@ -62,6 +79,8 @@ export function SourceControlAgentActionDialogForm({ baseCommandInput, saveTargetValue, saveTargets, + settings, + repo, canSaveAgentDefault, deliveryPlan, canStart, @@ -74,9 +93,34 @@ export function SourceControlAgentActionDialogForm({ onOpenSettings, onStart }: SourceControlAgentActionDialogFormProps): React.JSX.Element { + const selectedRecipe = selectedAgent + ? { + agentId: selectedAgent, + commandInputTemplate: commandTemplate, + agentArgs + } + : null + const savableTargets = saveTargets + .map((target) => sourceControlLaunchSaveTargetFromValue(target.value, repo)) + .filter((target): target is SourceControlAiWriteTarget => target !== null) + const allLaunchRecipesAlreadySaved = Boolean( + selectedRecipe && + savableTargets.length > 0 && + savableTargets.every((target) => + sourceControlActionRecipeMatchesTarget({ + actionId, + target, + recipe: selectedRecipe, + settings, + repo + }) + ) + ) + const showSaveLaunchRecipe = canSaveAgentDefault && selectedAgent && !allLaunchRecipesAlreadySaved + return ( <> -
+
{hasEnabledAgents || selectedAgent ? ( @@ -140,7 +184,7 @@ export function SourceControlAgentActionDialogForm({ rows={12} value={commandTemplate} onChange={(event) => onCommandTemplateChange(event.target.value)} - className="min-h-[14rem] w-full resize-y rounded-md border border-border bg-background px-2.5 py-2 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring" + className="box-border min-h-[14rem] min-w-0 w-full max-w-full resize-y rounded-md border border-border bg-background px-2.5 py-2 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring" />
- {canSaveAgentDefault && selectedAgent ? ( + {showSaveLaunchRecipe ? (
@@ -208,7 +231,7 @@ export function SourceControlTextGenerationDialogForm({ placeholder="--model sonnet" onChange={(event) => { setAgentArgs(event.target.value) - setPlan({ status: 'idle' }) + setGenerationError(null) }} className="h-8 font-mono text-xs" /> @@ -225,9 +248,9 @@ export function SourceControlTextGenerationDialogForm({ spellCheck={false} onChange={(event) => { setCommandTemplate(event.target.value) - setPlan({ status: 'idle' }) + setGenerationError(null) }} - className="w-full resize-y rounded-md border border-border bg-background px-2.5 py-2 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring" + className="box-border min-w-0 w-full max-w-full resize-y rounded-md border border-border bg-background px-2.5 py-2 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-ring" />
- {plan.status !== 'idle' ? ( -
- {plan.status === 'error' ? ( - - - {plan.error} - - ) : ( - <> -
- - {plan.delivery} -
-
Launch: {plan.commandLabel}
-
{plan.caveat}
- - )} + {showSaveRecipeControl ? ( +
+ +
) : null} + + {generationError ? ( +

+ + {generationError} +

+ ) : null}
- - - {saveTargets.map((saveTarget) => { - const targetKey = - saveTarget.target.type === 'repo' ? `repo:${saveTarget.target.repoId}` : 'global' - return ( - - ) - })} + + {selectedSaveTarget && !defaultsAlreadySaved ? ( + + ) : null}