diff --git a/src/renderer/src/components/WorktreeJumpPalette.tsx b/src/renderer/src/components/WorktreeJumpPalette.tsx
index 906a345d5..9ea18fdbb 100644
--- a/src/renderer/src/components/WorktreeJumpPalette.tsx
+++ b/src/renderer/src/components/WorktreeJumpPalette.tsx
@@ -247,7 +247,9 @@ function getSettingsTargetFromSectionId(sectionId: string): {
}
export default function WorktreeJumpPalette(): React.JSX.Element | null {
- const { i18n } = useTranslation()
+ // Why: subscribe this palette to language changes; translated memo contents
+ // recompute on the rerender without using i18n.language as a fake dependency.
+ useTranslation()
const visible = useAppStore((s) => s.activeModal === 'worktree-palette')
const closeModal = useAppStore((s) => s.closeModal)
const openModal = useAppStore((s) => s.openModal)
@@ -581,10 +583,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
() => buildCmdJSettingsResults(settingsSections),
[settingsSections]
)
- const actionResults = useMemo(
- () => buildCmdJActionResults(getCmdJQuickActions()),
- [i18n.language]
- )
+ const actionResults = useMemo(() => buildCmdJActionResults(getCmdJQuickActions()), [])
const prefetchCreateWorkspaceBaseForComposer = useCallback((initialRepoId?: string): void => {
const state = useAppStore.getState()
@@ -782,7 +781,7 @@ export default function WorktreeJumpPalette(): React.JSX.Element | null {
appendPaletteListEntries(entries, visibleOpenTabItems)
}
return entries
- }, [hasQuery, paletteSections, showCreateAction, worktreeItems.length, i18n.language])
+ }, [hasQuery, paletteSections, showCreateAction, worktreeItems.length])
const selectionItemIds = useMemo(
() => getWorktreePaletteSelectionItemIds(listEntries),
diff --git a/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx b/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx
new file mode 100644
index 000000000..6cc3d36d8
--- /dev/null
+++ b/src/renderer/src/components/right-sidebar/PullRequestComposer.generate-tooltip.test.tsx
@@ -0,0 +1,116 @@
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it, vi } from 'vitest'
+import { TooltipProvider } from '@/components/ui/tooltip'
+import { PullRequestComposer } from './SourceControl'
+import { resolveDropdownItems } from './source-control-dropdown-items'
+import { resolvePrimaryAction } from './source-control-primary-action'
+
+type RenderPullRequestComposerOptions = {
+ generating?: boolean
+ generateDisabled?: boolean
+ generateDisabledReason?: string
+}
+
+function renderPullRequestComposer({
+ generating = false,
+ generateDisabled = false,
+ generateDisabledReason
+}: RenderPullRequestComposerOptions = {}): string {
+ const sourceControlInputs = {
+ stagedCount: 1,
+ hasUnstagedChanges: false,
+ hasStageableChanges: false,
+ hasPartiallyStagedChanges: false,
+ hasMessage: true,
+ hasUnresolvedConflicts: false,
+ isCommitting: false,
+ isRemoteOperationActive: false,
+ upstreamStatus: { hasUpstream: true, ahead: 1, behind: 0 }
+ }
+ const primaryAction = resolvePrimaryAction(sourceControlInputs)
+
+ return renderToStaticMarkup(
+
+
+
+ )
+}
+
+function elementByLabel(markup: string, tagName: string, label: string): string {
+ const element = [...markup.matchAll(new RegExp(`<${tagName}\\b[\\s\\S]*?${tagName}>`, 'g'))]
+ .map((match) => match[0])
+ .find((entry) => entry.includes(`aria-label="${label}"`))
+
+ if (!element) {
+ throw new Error(`${tagName} not found: ${label}`)
+ }
+
+ return element
+}
+
+describe('PullRequestComposer generate tooltip', () => {
+ it('renders hosted review labels without leaking interpolation placeholders', () => {
+ const markup = renderPullRequestComposer()
+
+ expect(markup).toContain('aria-label="Generate pull request details with AI"')
+ expect(markup).not.toContain('{{value0}}')
+ expect(markup).not.toContain('title="Generate {{value0}} details with AI"')
+ })
+
+ it('keeps enabled generation controls as direct tooltip triggers', () => {
+ const markup = renderPullRequestComposer()
+ const button = elementByLabel(markup, 'button', 'Generate pull request details with AI')
+
+ expect(button).toContain('data-slot="tooltip-trigger"')
+ })
+
+ it('wraps only disabled generation controls so the disabled reason can show on hover', () => {
+ const markup = renderPullRequestComposer({
+ generateDisabled: true,
+ generateDisabledReason: 'Stage changes before generating.'
+ })
+ const wrapper = elementByLabel(markup, 'span', 'Generate pull request details with AI')
+ const button = elementByLabel(markup, 'button', 'Generate pull request details with AI')
+
+ expect(wrapper).toContain('data-slot="tooltip-trigger"')
+ expect(button).toContain('disabled=""')
+ expect(button).toContain('data-slot="button"')
+ })
+
+ it('keeps the active stop control focusable as the tooltip trigger', () => {
+ const markup = renderPullRequestComposer({ generating: true, generateDisabled: true })
+ const button = elementByLabel(markup, 'button', 'Stop generating pull request details')
+
+ expect(button).toContain('data-slot="tooltip-trigger"')
+ expect(button).not.toContain('disabled=""')
+ })
+})
diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx
index 7fb84e20a..752aaeda1 100644
--- a/src/renderer/src/components/right-sidebar/SourceControl.tsx
+++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx
@@ -4718,7 +4718,7 @@ type PullRequestComposerProps = {
onDropdownAction: (kind: DropdownActionKind) => void
}
-function PullRequestComposer({
+export function PullRequestComposer({
provider,
branch,
base,
@@ -4780,6 +4780,48 @@ function PullRequestComposer({
// the user can't race the request — the hook otherwise rejects the result
// with "Fields changed while generating" and silently drops the draft.
const fieldsLocked = generating
+ const generateDetailsLabel = translate(
+ 'auto.components.right.sidebar.SourceControl.02d8c04339',
+ 'Generate {{value0}} details with AI',
+ { value0: copy.reviewLabel }
+ )
+ const stopGeneratingDetailsLabel = translate(
+ 'auto.components.right.sidebar.SourceControl.b355e740b2',
+ 'Stop generating {{value0}} details',
+ { value0: copy.reviewLabel }
+ )
+ const generateTooltipLabel = generating
+ ? stopGeneratingDetailsLabel
+ : (generateDisabledReason ?? generateDetailsLabel)
+ const generateButton = generating ? (
+
+ ) : (
+
+ )
return (
@@ -4796,54 +4838,18 @@ function PullRequestComposer({
{aiGenerationEnabled ? (
- generating ? (
-
- ) : (
-
- )
+
+ {!generating && generateDisabled ? (
+
+ {generateButton}
+
+ ) : (
+ {generateButton}
+ )}
+
+ {generateTooltipLabel}
+
+
) : null}
diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
index eed03a10b..33830ba84 100644
--- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
+++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
@@ -491,7 +491,9 @@ export function buildSettingsNavigationMetadata({
}
export function useSettingsNavigationMetadata(): SettingsNavSection[] {
- const { i18n } = useTranslation()
+ // Why: subscribe metadata consumers to language changes; translated memo
+ // contents refresh on rerender without depending on i18n.language directly.
+ useTranslation()
const repos = useAppStore((state) => state.repos)
const activeRuntimeEnvironmentId = useAppStore(
(state) => state.settings?.activeRuntimeEnvironmentId
@@ -518,6 +520,6 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] {
isWebClient,
repos
}),
- [i18n.language, isMac, isWindows, isWindowsTerminalHost, isWebClient, repos]
+ [isMac, isWindows, isWindowsTerminalHost, isWebClient, repos]
)
}