mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Improve source control action dialog layout and recipe saving UX (#5153)
* Improve source control agent action dialog layout and recipe UX - Constrain dialog and scroll area heights to prevent viewport overflow. - Add variable chips to easily insert the base prompt with tooltip previews. - Keep the recipe save controls visible when a recipe is already saved, showing informational status text instead of hiding them. - Update localized copy across multiple languages and reduce textarea rows. - Add unit tests for the variable chip preview and save target visibility. * Fix recipe-saved check in source control action dialog * Evaluate only the selected save target instead of checking all available targets, as the action only writes to the selected target. * Update daemon PTY adapter test fake PID to prevent collision with real host OS processes during runtime directory lookups.
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -56,6 +56,7 @@ export function SourceControlAgentActionDialog(
|
||||
actionId,
|
||||
title,
|
||||
description,
|
||||
baseCommandInput,
|
||||
savedCommandInputTemplate,
|
||||
onOpenSettings,
|
||||
startLabel = 'Start agent',
|
||||
@@ -88,13 +89,14 @@ export function SourceControlAgentActionDialog(
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="flex h-[70vh] min-w-0 flex-col overflow-hidden sm:max-w-2xl">
|
||||
<DialogContent className="flex max-h-[min(82vh,42rem)] min-w-0 flex-col overflow-hidden sm:max-w-2xl">
|
||||
<DialogHeader className="shrink-0">
|
||||
<DialogTitle className="text-sm">{title}</DialogTitle>
|
||||
<DialogDescription className="text-xs">{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<SourceControlAgentActionDialogForm
|
||||
actionId={actionId}
|
||||
baseCommandInput={baseCommandInput}
|
||||
agentOptions={agentOptions}
|
||||
selectedAgent={selectedAgent}
|
||||
hasEnabledAgents={hasEnabledAgents}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SourceControlAgentActionDialogForm } from './SourceControlAgentActionDialogForm'
|
||||
import type { GlobalSettings, Repo } from '../../../../shared/types'
|
||||
|
||||
vi.mock('@/components/agent/AgentCombobox', () => ({
|
||||
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<Record<string, string>>
|
||||
}) =>
|
||||
React.createElement('div', {
|
||||
'data-variable-previews': JSON.stringify(variablePreviews ?? {})
|
||||
})
|
||||
}))
|
||||
|
||||
function renderForm(
|
||||
overrides: Partial<React.ComponentProps<typeof SourceControlAgentActionDialogForm>> = {}
|
||||
): 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<Repo, 'id' | 'sourceControlAi'>
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -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 (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
||||
<div className="min-h-0 min-w-0 flex-1 space-y-4 overflow-y-auto pr-1 scrollbar-sleek">
|
||||
<div className="flex min-h-0 flex-col gap-4">
|
||||
<div className="min-h-0 min-w-0 max-h-[min(60vh,31rem)] space-y-4 overflow-y-auto pr-1 scrollbar-sleek">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">
|
||||
{translate(
|
||||
@@ -213,8 +214,8 @@ export function SourceControlAgentActionDialogForm({
|
||||
</Label>
|
||||
<p className="mt-1 text-[11px] leading-4 text-muted-foreground">
|
||||
{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.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -234,17 +235,21 @@ export function SourceControlAgentActionDialogForm({
|
||||
</div>
|
||||
<textarea
|
||||
id="source-control-agent-command-input"
|
||||
rows={10}
|
||||
rows={7}
|
||||
value={commandTemplate}
|
||||
onChange={(event) => onCommandTemplateChange(event.target.value)}
|
||||
className="box-border min-h-[7.75rem] 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"
|
||||
className="box-border min-h-[6.5rem] 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"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<SourceControlActionVariableChips
|
||||
actionId={actionId}
|
||||
variablePreviews={{ basePrompt: baseCommandInput }}
|
||||
onInsert={(variable) => {
|
||||
const separator =
|
||||
commandTemplate.endsWith('\n') || commandTemplate.length === 0 ? '' : ' '
|
||||
onCommandTemplateChange(`${commandTemplate}${separator}{${variable}}`)
|
||||
}}
|
||||
/>
|
||||
<p className="text-[11px] leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.d8f40128ee',
|
||||
"{basePrompt} is Orca's default prompt."
|
||||
)}
|
||||
</p>
|
||||
{!commandTemplateIncludesBasePrompt ? (
|
||||
<p className="flex items-start gap-1.5 rounded-md border border-destructive/30 bg-destructive/5 px-2.5 py-2 text-[11px] leading-4 text-destructive">
|
||||
<TriangleAlert className="mt-px size-3 shrink-0" />
|
||||
@@ -274,16 +279,26 @@ export function SourceControlAgentActionDialogForm({
|
||||
/>
|
||||
<span>
|
||||
<span className="block text-xs font-semibold">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.c29f9cf266',
|
||||
"Save this prompt and don't show this review next time"
|
||||
)}
|
||||
{selectedLaunchRecipeAlreadySaved
|
||||
? translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.b0da3a4d3e',
|
||||
'Launch recipe already saved'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.c29f9cf266',
|
||||
"Save this prompt and don't show this review next time"
|
||||
)}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-[11px] leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.6cefcdfba1',
|
||||
'You can change it later in Source Control AI settings.'
|
||||
)}
|
||||
{selectedLaunchRecipeAlreadySaved
|
||||
? translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.bff4795a6d',
|
||||
'Change the agent, arguments, or prompt template to update the saved recipe.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.SourceControlAgentActionDialogForm.6cefcdfba1',
|
||||
'You can change it later in Source Control AI settings.'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -7980,7 +7980,10 @@
|
||||
"3e8f21954f": "error",
|
||||
"74168d7ada": "idle",
|
||||
"1d47db9bf0": "No enabled agents",
|
||||
"c7ff8cef11": "Detecting agents..."
|
||||
"c7ff8cef11": "Detecting agents...",
|
||||
"b0da3a4d3e": "Launch recipe already saved",
|
||||
"bff4795a6d": "Change the agent, arguments, or prompt template to update the saved recipe.",
|
||||
"5c75b24735": "Customize what the agent receives before Orca starts it."
|
||||
},
|
||||
"SourceControlTextGenerationDialog": {
|
||||
"c5b7fa7cb6": "Save as global default",
|
||||
|
||||
@@ -7853,7 +7853,10 @@
|
||||
"3e8f21954f": "error",
|
||||
"74168d7ada": "idle",
|
||||
"1d47db9bf0": "No hay agents habilitados",
|
||||
"c7ff8cef11": "Detectando agents..."
|
||||
"c7ff8cef11": "Detectando agents...",
|
||||
"b0da3a4d3e": "La receta de lanzamiento ya está guardada",
|
||||
"bff4795a6d": "Cambia el agente, los argumentos o la plantilla del prompt para actualizar la receta guardada.",
|
||||
"5c75b24735": "Personaliza lo que recibe el agente antes de que Orca lo inicie."
|
||||
},
|
||||
"SourceControlTextGenerationDialog": {
|
||||
"c5b7fa7cb6": "Guardar como valor predeterminado global",
|
||||
|
||||
@@ -7961,7 +7961,10 @@
|
||||
"6cefcdfba1": "You can change it later in Source Control AI settings.",
|
||||
"c29f9cf266": "Save this prompt and don't show this review next time",
|
||||
"d8f40128ee": "{basePrompt} is Orca's default prompt.",
|
||||
"ea4788705e": "Cancel"
|
||||
"ea4788705e": "Cancel",
|
||||
"b0da3a4d3e": "起動レシピはすでに保存されています",
|
||||
"bff4795a6d": "保存済みのレシピを更新するには、agent、引数、またはプロンプトテンプレートを変更してください。",
|
||||
"5c75b24735": "Orca が起動する前に agent が受け取る内容をカスタマイズします。"
|
||||
},
|
||||
"SourceControlTextGenerationDialog": {
|
||||
"c5b7fa7cb6": "グローバルデフォルトとして保存",
|
||||
|
||||
@@ -7961,7 +7961,10 @@
|
||||
"6cefcdfba1": "You can change it later in Source Control AI settings.",
|
||||
"c29f9cf266": "Save this prompt and don't show this review next time",
|
||||
"d8f40128ee": "{basePrompt} is Orca's default prompt.",
|
||||
"ea4788705e": "Cancel"
|
||||
"ea4788705e": "Cancel",
|
||||
"b0da3a4d3e": "실행 레시피가 이미 저장되었습니다",
|
||||
"bff4795a6d": "저장된 레시피를 업데이트하려면 agent, 인수 또는 프롬프트 템플릿을 변경하세요.",
|
||||
"5c75b24735": "Orca가 시작하기 전에 agent가 받을 내용을 사용자 지정합니다."
|
||||
},
|
||||
"SourceControlTextGenerationDialog": {
|
||||
"c5b7fa7cb6": "전역 기본값으로 저장",
|
||||
|
||||
@@ -7961,7 +7961,10 @@
|
||||
"6cefcdfba1": "You can change it later in Source Control AI settings.",
|
||||
"c29f9cf266": "Save this prompt and don't show this review next time",
|
||||
"d8f40128ee": "{basePrompt} is Orca's default prompt.",
|
||||
"ea4788705e": "Cancel"
|
||||
"ea4788705e": "Cancel",
|
||||
"b0da3a4d3e": "启动配方已保存",
|
||||
"bff4795a6d": "更改 Agent、参数或提示词模板以更新已保存的配方。",
|
||||
"5c75b24735": "自定义 Orca 启动 Agent 前发送给它的内容。"
|
||||
},
|
||||
"SourceControlTextGenerationDialog": {
|
||||
"c5b7fa7cb6": "保存为全局默认值",
|
||||
|
||||
Reference in New Issue
Block a user