diff --git a/src/main/daemon/daemon-pty-adapter.test.ts b/src/main/daemon/daemon-pty-adapter.test.ts index f0a099c23b7..e6f0f6e617c 100644 --- a/src/main/daemon/daemon-pty-adapter.test.ts +++ b/src/main/daemon/daemon-pty-adapter.test.ts @@ -33,7 +33,9 @@ function createMockSubprocess(): SubprocessHandle & { let onDataCb: ((data: string) => void) | null = null let onExitCb: ((code: number) => void) | null = null return { - pid: 66666, + // Why: getCwd falls back to OS pid lookup; a plausible fake pid can + // collide with an unrelated local process and leak its cwd into tests. + pid: 999_999_999, getForegroundProcess: vi.fn(() => null), write: vi.fn(), resize: vi.fn(), diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx index dd5db5f5753..eb3429bb187 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx @@ -56,6 +56,7 @@ export function SourceControlAgentActionDialog( actionId, title, description, + baseCommandInput, savedCommandInputTemplate, onOpenSettings, startLabel = 'Start agent', @@ -88,13 +89,14 @@ export function SourceControlAgentActionDialog( return ( - + {title} {description} ({ + default: ({ value }: { value: string | null }) => + React.createElement('div', { 'data-agent-value': value ?? '' }) +})) + +vi.mock('@/components/ui/dialog', () => ({ + DialogFooter: ({ children }: { children?: ReactNode }) => + React.createElement('div', null, children) +})) + +vi.mock('@/components/ui/select', () => ({ + Select: ({ children }: { children?: ReactNode }) => React.createElement('div', null, children), + SelectContent: ({ children }: { children?: ReactNode }) => + React.createElement('div', null, children), + SelectItem: ({ children, value }: { children?: ReactNode; value: string }) => + React.createElement('div', { 'data-select-item': value }, children), + SelectTrigger: ({ children }: { children?: ReactNode }) => + React.createElement('button', null, children), + SelectValue: () => React.createElement('span') +})) + +vi.mock('../source-control/SourceControlActionVariableChips', () => ({ + SourceControlActionVariableChips: ({ + variablePreviews + }: { + variablePreviews?: Partial> + }) => + React.createElement('div', { + 'data-variable-previews': JSON.stringify(variablePreviews ?? {}) + }) +})) + +function renderForm( + overrides: Partial> = {} +): string { + return renderToStaticMarkup( + React.createElement(SourceControlAgentActionDialogForm, { + actionId: 'resolveConflicts', + baseCommandInput: 'Resolve the merge conflicts reported for this pull request.', + agentOptions: [], + selectedAgent: 'codex', + hasEnabledAgents: true, + detecting: false, + statusCopy: null, + agentArgs: '', + commandTemplate: '{basePrompt}', + savedCommandInputTemplate: '{basePrompt}', + saveLaunchRecipe: true, + saveTargetValue: 'global', + saveTargets: [ + { value: 'none', label: "Don't save" }, + { value: 'global', label: 'All repositories' } + ], + settings: null, + repo: null, + canSaveAgentDefault: true, + deliveryPlan: { status: 'idle' }, + canStart: true, + isStarting: false, + startLabel: 'Start agent', + onSelectedAgentChange: () => {}, + onAgentArgsChange: () => {}, + onCommandTemplateChange: () => {}, + onSaveLaunchRecipeChange: () => {}, + onSaveAgentDefaultChange: () => {}, + onCancel: () => {}, + onStart: () => {}, + ...overrides + }) + ) +} + +function settingsWithSavedGlobalRecipe(): GlobalSettings { + return { + sourceControlAi: { + enabled: true, + agentId: 'codex', + selectedModelByAgent: {}, + selectedThinkingByModel: {}, + customAgentCommand: '', + instructionsByOperation: {}, + actions: { + resolveConflicts: { + agentId: 'codex', + commandInputTemplate: '{basePrompt}' + } + } + } + } as unknown as GlobalSettings +} + +const repoWithoutSavedRecipe = { + id: 'repo-1', + sourceControlAi: { enabled: true } +} satisfies Pick + +describe('SourceControlAgentActionDialogForm', () => { + it('passes the base prompt preview to the variable chip hover content', () => { + const markup = renderForm() + + expect(markup).toContain('Resolve the merge conflicts reported for this pull request.') + }) + + it('keeps save target controls visible when the current launch recipe is already saved', () => { + const settings = settingsWithSavedGlobalRecipe() + + const markup = renderForm({ settings }) + + expect(markup).toContain('Launch recipe already saved') + expect(markup).toContain('Save for') + expect(markup).toContain('All repositories') + }) + + it('checks already-saved copy against the selected save target', () => { + const settings = settingsWithSavedGlobalRecipe() + const saveTargets = [ + { value: 'none', label: "Don't save" }, + { value: 'repo', label: 'This repository' }, + { value: 'global', label: 'All repositories' } + ] + + const globalMarkup = renderForm({ + settings, + repo: repoWithoutSavedRecipe, + saveTargets, + saveTargetValue: 'global' + }) + const repoMarkup = renderForm({ + settings, + repo: repoWithoutSavedRecipe, + saveTargets, + saveTargetValue: 'repo' + }) + + expect(globalMarkup).toContain('Launch recipe already saved') + expect(globalMarkup).not.toContain('Save & start agent') + expect(repoMarkup).not.toContain('Launch recipe already saved') + expect(repoMarkup).toContain('Save & start agent') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index 44276430846..779a200087c 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -17,6 +17,7 @@ import { cn } from '@/lib/utils' import type { SourceControlLaunchActionId } from '../../../../shared/source-control-ai-actions' 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' import { translate } from '@/i18n/i18n' @@ -27,6 +28,7 @@ export type SourceControlAgentActionDeliveryPlanState = type SourceControlAgentActionDialogFormProps = { actionId: SourceControlLaunchActionId + baseCommandInput: string agentOptions: AgentCatalogEntry[] selectedAgent: TuiAgent | null hasEnabledAgents: boolean @@ -70,6 +72,7 @@ function sourceControlLaunchSaveTargetFromValue( export function SourceControlAgentActionDialogForm({ actionId, + baseCommandInput, agentOptions, selectedAgent, hasEnabledAgents, @@ -106,26 +109,24 @@ export function SourceControlAgentActionDialogForm({ agentArgs } : null - const savableTargets = saveTargets - .map((target) => sourceControlLaunchSaveTargetFromValue(target.value, repo)) - .filter((target): target is SourceControlAiWriteTarget => target !== null) - const allLaunchRecipesAlreadySaved = Boolean( + const selectedSaveTarget = sourceControlLaunchSaveTargetFromValue(saveTargetValue, repo) + // Why: start/save only writes the selected target, so the dialog copy must not + // depend on whether other available targets also match. + const selectedLaunchRecipeAlreadySaved = Boolean( selectedRecipe && - savableTargets.length > 0 && - savableTargets.every((target) => - sourceControlActionRecipeMatchesTarget({ - actionId, - target, - recipe: selectedRecipe, - settings, - repo - }) - ) + selectedSaveTarget && + sourceControlActionRecipeMatchesTarget({ + actionId, + target: selectedSaveTarget, + recipe: selectedRecipe, + settings, + repo + }) ) - const showSaveLaunchRecipe = canSaveAgentDefault && selectedAgent && !allLaunchRecipesAlreadySaved + const showSaveLaunchRecipe = Boolean(canSaveAgentDefault && selectedAgent) const saveScopeTargets = saveTargets.filter((target) => target.value !== 'none') const effectiveStartLabel = - showSaveLaunchRecipe && saveLaunchRecipe + showSaveLaunchRecipe && saveLaunchRecipe && !selectedLaunchRecipeAlreadySaved ? translate( 'auto.components.right.sidebar.SourceControlAgentActionDialogForm.5421a96acb', 'Save & start agent' @@ -133,8 +134,8 @@ export function SourceControlAgentActionDialogForm({ : startLabel return ( -
-
+
+

{translate( - 'auto.components.right.sidebar.SourceControlAgentActionDialogForm.1bb611240f', - "Use {basePrompt} for Orca's default prompt." + 'auto.components.right.sidebar.SourceControlAgentActionDialogForm.5c75b24735', + 'Customize what the agent receives before Orca starts it.' )}

@@ -234,17 +235,21 @@ export function SourceControlAgentActionDialogForm({