mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Use scrollable hover cards for source control variable previews (#5029)
Replace the variable chip Tooltips with HoverCards to allow scrolling through long prompt previews (such as the base prompt template). * Pass action-specific base prompt previews (commit, PR, branch name) into the text generation dialogs to populate the variable chips. * Style the hover cards to support vertical scrolling with a sleek scrollbar and update theme colors to match design guidelines. * Add unit tests for the variable chips and dialog form behavior.
This commit is contained in:
+57
-2
@@ -1,11 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import React, { type ReactNode } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildCommitMessageGenerationParams } from './SourceControlTextGenerationDialog'
|
||||
import { getDefaultSourceControlTextGenerationSaveTargetKey } from './SourceControlTextGenerationDialogForm'
|
||||
import {
|
||||
getDefaultSourceControlTextGenerationSaveTargetKey,
|
||||
SourceControlTextGenerationDialogForm
|
||||
} from './SourceControlTextGenerationDialogForm'
|
||||
import {
|
||||
applyCommitMessageGenerationDefaults,
|
||||
applySourceControlTextGenerationDefaults
|
||||
} from './SourceControlTextGenerationDefaults'
|
||||
|
||||
vi.mock('../source-control/SourceControlActionVariableChips', () => ({
|
||||
SourceControlActionVariableChips: ({
|
||||
variablePreviews
|
||||
}: {
|
||||
variablePreviews?: Partial<Record<string, string>>
|
||||
}) =>
|
||||
React.createElement('div', {
|
||||
'data-variable-previews': JSON.stringify(variablePreviews ?? {})
|
||||
})
|
||||
}))
|
||||
|
||||
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 }: { children?: ReactNode }) =>
|
||||
React.createElement('div', null, children),
|
||||
SelectTrigger: ({ children }: { children?: ReactNode }) =>
|
||||
React.createElement('button', null, children),
|
||||
SelectValue: () => React.createElement('span')
|
||||
}))
|
||||
|
||||
describe('buildCommitMessageGenerationParams', () => {
|
||||
it('defaults saved text-generation recipes to the global target when repo and global are available', () => {
|
||||
expect(
|
||||
@@ -24,6 +56,29 @@ describe('buildCommitMessageGenerationParams', () => {
|
||||
).toBe('global')
|
||||
})
|
||||
|
||||
it('passes the base prompt preview to variable chips in text generation dialogs', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
React.createElement(SourceControlTextGenerationDialogForm, {
|
||||
actionId: 'commitMessage',
|
||||
generateLabel: 'Generate',
|
||||
settings: null,
|
||||
repo: null,
|
||||
baseParams: {
|
||||
agentId: 'codex',
|
||||
model: 'gpt-5.4-mini',
|
||||
commandInputTemplate: '{basePrompt}'
|
||||
},
|
||||
basePromptPreview: 'You are generating a single git commit message.',
|
||||
saveTargets: [],
|
||||
onGenerate: () => {},
|
||||
onOpenChange: () => {},
|
||||
onSaveDefaults: () => {}
|
||||
})
|
||||
)
|
||||
|
||||
expect(markup).toContain('You are generating a single git commit message.')
|
||||
})
|
||||
|
||||
it('preserves the resolved model and thinking level for the selected agent', () => {
|
||||
expect(
|
||||
buildCommitMessageGenerationParams({
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import type { SourceControlTextActionId } from '../../../../shared/source-control-ai-actions'
|
||||
import type { GlobalSettings, Repo } from '../../../../shared/types'
|
||||
import type { SourceControlAiWriteTarget } from '../../../../shared/source-control-ai-recipe-save'
|
||||
import { buildBranchNamePrompt } from '../../../../shared/branch-name-from-work'
|
||||
import { buildCommitMessagePrompt } from '../../../../shared/commit-message-generation'
|
||||
import { buildPullRequestFieldsPrompt } from '../../../../shared/pull-request-generation'
|
||||
import {
|
||||
SourceControlTextGenerationDialogForm,
|
||||
type SourceControlTextGenerationSaveTarget
|
||||
@@ -42,6 +45,40 @@ type SourceControlTextGenerationDialogProps = SourceControlTextGenerationBaseDia
|
||||
generateLabel: string
|
||||
}
|
||||
|
||||
function buildBasePromptPreview(actionId: SourceControlTextActionId): string {
|
||||
switch (actionId) {
|
||||
case 'commitMessage':
|
||||
return buildCommitMessagePrompt(
|
||||
{
|
||||
branch: 'feature/example',
|
||||
stagedSummary: 'M src/example.ts',
|
||||
stagedPatch: 'diff --git a/src/example.ts b/src/example.ts\n+addSourceControlAiPreview()'
|
||||
},
|
||||
''
|
||||
)
|
||||
case 'pullRequest':
|
||||
return buildPullRequestFieldsPrompt(
|
||||
{
|
||||
branch: 'feature/example',
|
||||
base: 'main',
|
||||
branchChangedByPreparation: false,
|
||||
currentTitle: 'Draft title',
|
||||
currentBody: 'Draft description',
|
||||
currentDraft: false,
|
||||
commitSummary: 'a1b2c3d Add Source Control AI prompt previews',
|
||||
changeSummary: 'src/example.ts | 12 ++++++++++--',
|
||||
patch: 'diff --git a/src/example.ts b/src/example.ts\n+addSourceControlAiPreview()'
|
||||
},
|
||||
''
|
||||
)
|
||||
case 'branchName':
|
||||
return buildBranchNamePrompt({
|
||||
firstPrompt: 'Add source-control AI prompt previews',
|
||||
assistantMessage: 'I will update the generation dialog variable chip preview.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function SourceControlTextGenerationDialog({
|
||||
actionId,
|
||||
title,
|
||||
@@ -126,6 +163,7 @@ export function SourceControlTextGenerationDialog({
|
||||
settings={settings}
|
||||
repo={repo ?? null}
|
||||
baseParams={baseParams}
|
||||
basePromptPreview={buildBasePromptPreview(actionId)}
|
||||
saveTargets={saveTargets}
|
||||
onGenerate={onGenerate}
|
||||
onOpenChange={onOpenChange}
|
||||
|
||||
@@ -45,6 +45,7 @@ type SourceControlTextGenerationDialogFormProps = {
|
||||
settings: GlobalSettings | null
|
||||
repo: Pick<Repo, 'id' | 'sourceControlAi'> | null
|
||||
baseParams: ResolvedSourceControlAiGenerationParams | null
|
||||
basePromptPreview?: string
|
||||
saveTargets: SourceControlTextGenerationSaveTarget[]
|
||||
onGenerate: (params: ResolvedSourceControlAiGenerationParams) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -78,6 +79,7 @@ export function SourceControlTextGenerationDialogForm({
|
||||
settings,
|
||||
repo,
|
||||
baseParams,
|
||||
basePromptPreview,
|
||||
saveTargets,
|
||||
onGenerate,
|
||||
onOpenChange,
|
||||
@@ -285,6 +287,7 @@ export function SourceControlTextGenerationDialogForm({
|
||||
/>
|
||||
<SourceControlActionVariableChips
|
||||
actionId={actionId}
|
||||
variablePreviews={basePromptPreview ? { basePrompt: basePromptPreview } : undefined}
|
||||
onInsert={(variable) => {
|
||||
const separator =
|
||||
commandTemplate.endsWith('\n') || commandTemplate.length === 0 ? '' : ' '
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { SourceControlActionVariableChips } from './SourceControlActionVariableChips'
|
||||
|
||||
vi.mock('../ui/hover-card', () => ({
|
||||
HoverCard: ({ children }: { children: ReactNode }) => (
|
||||
<div data-slot="hover-card">{children}</div>
|
||||
),
|
||||
HoverCardContent: ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<div data-slot="hover-card-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
HoverCardTrigger: ({ children }: { children: ReactNode }) => (
|
||||
<div data-slot="hover-card-trigger">{children}</div>
|
||||
)
|
||||
}))
|
||||
|
||||
describe('SourceControlActionVariableChips', () => {
|
||||
it('renders variable details in a scrollable hover card', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<SourceControlActionVariableChips
|
||||
actionId="commitMessage"
|
||||
variablePreviews={{ basePrompt: 'Generate a commit message.\n\nInclude staged changes.' }}
|
||||
onInsert={() => {}}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('data-slot="hover-card-content"')
|
||||
expect(markup).toContain('scrollbar-sleek')
|
||||
expect(markup).toContain('overflow-y-auto')
|
||||
expect(markup).toContain('Generate a commit message.')
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type SourceControlActionId
|
||||
} from '../../../../shared/source-control-ai-actions'
|
||||
import { Button } from '../ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '../ui/hover-card'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type SourceControlActionVariableChipsProps = {
|
||||
@@ -28,7 +28,7 @@ function hasVariablePreview(
|
||||
)
|
||||
}
|
||||
|
||||
function SourceControlVariableTooltip({
|
||||
function SourceControlVariableDetails({
|
||||
variable,
|
||||
preview
|
||||
}: {
|
||||
@@ -38,17 +38,25 @@ function SourceControlVariableTooltip({
|
||||
if (preview !== undefined) {
|
||||
if (variable === 'basePrompt') {
|
||||
return (
|
||||
<pre className="scrollbar-sleek max-h-72 max-w-[min(32rem,calc(100vw-2rem))] overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{preview || translate("auto.components.source.control.SourceControlActionVariableChips.4bf6d88039", "(empty)")}
|
||||
<pre className="whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{preview ||
|
||||
translate(
|
||||
'auto.components.source.control.SourceControlActionVariableChips.4bf6d88039',
|
||||
'(empty)'
|
||||
)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="font-mono text-[11px] text-background/70">{`{${variable}}`}</div>
|
||||
<pre className="scrollbar-sleek max-h-72 max-w-[min(32rem,calc(100vw-2rem))] overflow-auto rounded-sm bg-background/10 p-2 whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{preview || translate("auto.components.source.control.SourceControlActionVariableChips.4bf6d88039", "(empty)")}
|
||||
<div className="font-mono text-[11px] text-muted-foreground">{`{${variable}}`}</div>
|
||||
<pre className="rounded-sm bg-background/60 p-2 whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{preview ||
|
||||
translate(
|
||||
'auto.components.source.control.SourceControlActionVariableChips.4bf6d88039',
|
||||
'(empty)'
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
@@ -59,12 +67,16 @@ function SourceControlVariableTooltip({
|
||||
<div className="max-w-80 space-y-2 text-left leading-relaxed">
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-mono text-[11px]">{`{${variable}}`}</div>
|
||||
<div className="text-background/80">{info.description}</div>
|
||||
<div className="text-muted-foreground">{info.description}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-wide text-background/60">
|
||||
{translate("auto.components.source.control.SourceControlActionVariableChips.6b921a0ac2", "Example")}</div>
|
||||
<pre className="scrollbar-sleek max-h-40 overflow-auto rounded-sm bg-background/10 p-2 whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.source.control.SourceControlActionVariableChips.6b921a0ac2',
|
||||
'Example'
|
||||
)}
|
||||
</div>
|
||||
<pre className="rounded-sm bg-background/60 p-2 whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
|
||||
{info.example}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -82,14 +94,18 @@ export function SourceControlActionVariableChips({
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
||||
<Braces className="size-3" />
|
||||
{translate("auto.components.source.control.SourceControlActionVariableChips.1b77798d5f", "Variables")}</span>
|
||||
{translate(
|
||||
'auto.components.source.control.SourceControlActionVariableChips.1b77798d5f',
|
||||
'Variables'
|
||||
)}
|
||||
</span>
|
||||
{SOURCE_CONTROL_ACTION_VARIABLES[actionId].map((variable) => {
|
||||
const preview = hasVariablePreview(variablePreviews, variable)
|
||||
? variablePreviews?.[variable]
|
||||
: undefined
|
||||
return (
|
||||
<Tooltip key={variable}>
|
||||
<TooltipTrigger asChild>
|
||||
<HoverCard key={variable} openDelay={150} closeDelay={120}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="inline-flex">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -102,11 +118,16 @@ export function SourceControlActionVariableChips({
|
||||
{`{${variable}}`}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6} className="px-2 py-2 text-left">
|
||||
<SourceControlVariableTooltip variable={variable} preview={preview} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
side="top"
|
||||
sideOffset={6}
|
||||
collisionPadding={12}
|
||||
className="scrollbar-sleek max-h-[min(18rem,calc(100vh-2rem))] w-[min(32rem,calc(100vw-2rem))] overflow-y-auto p-2 text-left text-xs"
|
||||
>
|
||||
<SourceControlVariableDetails variable={variable} preview={preview} />
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user