Use one shared Hide button for chat cards (#38999)

* feat(native-chat): one Hide button and toast for chat cards; PR card setting beside Changed files

The opened-PR card now uses the changed-files card's Hide control and Hidden
toast through a shared NativeChatCardHideButton, and its switch moves to
Settings > Chat UI > Appearance next to Changed files summary, where Reset
restores both. The saved key is unchanged.

* refactor(native-chat): read the opened-PR card switch through its predicate

* test(settings): Reset turns both hidden chat cards back on

* Preserve the existing opened pull request Hide aria label

* fix(native-chat): keep the gap before the PR card's Hide button
This commit is contained in:
Brennan Benson
2026-11-06 19:09:12 -08:00
committed by GitHub
parent 04bb0d0810
commit 299a47af4a
5 changed files with 115 additions and 52 deletions
@@ -0,0 +1,44 @@
import { EyeOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { hideNativeChatCardKind } from './native-chat-card-kind-hidden-toast'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
/** Put on a hideable chat card's outer element so its Hide button reveals on card hover. */
export const NATIVE_CHAT_HIDEABLE_CARD_CLASS = 'group/chat-card'
/** The Hide icon at the end of a chat card's header, shown on card hover or keyboard focus. */
export function NativeChatCardHideButton({
updates,
settingSectionId,
hiddenMessage,
ariaLabel
}: {
updates: Partial<GlobalSettings>
settingSectionId: string
hiddenMessage: string
ariaLabel?: string
}): React.JSX.Element {
const label = translate('components.native-chat.turnDiff.hide', 'Hide')
return (
// Keep the header one text line tall, as the transcript row-height estimate assumes.
<span className="-my-1 ml-auto flex shrink-0 transition-opacity can-hover:pointer-events-none can-hover:opacity-0 group-hover/chat-card:pointer-events-auto group-hover/chat-card:opacity-100 has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
aria-label={ariaLabel ?? label}
onClick={() => void hideNativeChatCardKind(updates, settingSectionId, hiddenMessage)}
>
<EyeOff aria-hidden className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{label}
</TooltipContent>
</Tooltip>
</span>
)
}
@@ -0,0 +1,26 @@
import { defineFixture } from '../../component-fixtures/component-fixture'
import { NativeChatOpenedPullRequestCard } from './NativeChatOpenedPullRequestCard'
import type { NativeChatOpenedPullRequest } from '../../../../shared/native-chat/tools/native-chat-opened-pull-requests'
const pullRequest: NativeChatOpenedPullRequest = {
provider: 'github',
url: 'https://github.com/stablyai/orca/pull/3',
slug: { owner: 'stablyai', repo: 'orca' },
number: 3
}
// Keyboard focus shows the Hide button, as card hover does, without a pointer in the headless shot.
function focusHide(frame: HTMLElement): void {
frame.ownerDocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
frame.querySelector<HTMLButtonElement>('button')?.focus()
}
export const card = defineFixture({
width: 560,
render: () => <NativeChatOpenedPullRequestCard pullRequests={[pullRequest]} />
})
export const hideShown = defineFixture({
width: 560,
render: () => <NativeChatOpenedPullRequestCard pullRequests={[pullRequest]} />,
interact: focusHide
})
@@ -10,7 +10,6 @@ import {
import { GitPullRequest } from 'lucide-react'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { EyeOffDismissButton } from '@/components/dismiss/EyeOffDismissButton'
import { getReviewStateIcon } from '@/components/github/review-state-presentation'
import { DiffLineCounts } from '@/components/right-sidebar/source-control/listing/diff-line-counts'
import type { GitHubIssueOrPRLink } from '../../../../shared/github/links'
@@ -20,7 +19,10 @@ import {
type NativeChatOpenedPullRequest
} from '../../../../shared/native-chat/tools/native-chat-opened-pull-requests'
import { NATIVE_CHAT_OPENED_PULL_REQUESTS_SETTING_SECTION_ID } from '../settings/chat/native-chat-opened-pull-requests-setting-target'
import { hideNativeChatCardKind } from './native-chat-card-kind-hidden-toast'
import {
NativeChatCardHideButton,
NATIVE_CHAT_HIDEABLE_CARD_CLASS
} from './NativeChatCardHideButton'
import {
githubLinkPreviewKey,
loadGitHubLinkPreview,
@@ -214,8 +216,10 @@ export function NativeChatOpenedPullRequestCard({
const [first] = pullRequests
const { githubRepo } = useContext(NativeChatLinkPeekContext)
return (
<div className="group relative rounded-lg border border-chat-code-border p-2 text-sm">
<div className="flex min-w-0 items-center px-1 pb-1 pr-6 text-xs text-chat-foreground-faint">
<div
className={`${NATIVE_CHAT_HIDEABLE_CARD_CLASS} rounded-lg border border-chat-code-border p-2 text-sm`}
>
<div className="flex min-w-0 items-center gap-1.5 px-1 pb-1 text-xs text-chat-foreground-faint">
<span className="min-w-0 truncate">
{pullRequests.length === 1 && first
? translate('components.native-chat.openedPullRequests.one', 'Opened {{name}}', {
@@ -227,24 +231,19 @@ export function NativeChatOpenedPullRequestCard({
{ count: pullRequests.length, defaultValue_one: 'Opened {{count}} pull request' }
)}
</span>
<NativeChatCardHideButton
ariaLabel={translate(
'components.native-chat.openedPullRequests.hide',
"Don't show opened pull requests"
)}
updates={{ nativeChatOpenedPullRequests: false }}
settingSectionId={NATIVE_CHAT_OPENED_PULL_REQUESTS_SETTING_SECTION_ID}
hiddenMessage={translate(
'components.native-chat.openedPullRequests.hidden',
"Orca won't show opened pull requests anymore"
)}
/>
</div>
<EyeOffDismissButton
label={translate(
'components.native-chat.openedPullRequests.hide',
"Don't show opened pull requests"
)}
onClick={() =>
void hideNativeChatCardKind(
{ nativeChatOpenedPullRequests: false },
NATIVE_CHAT_OPENED_PULL_REQUESTS_SETTING_SECTION_ID,
translate(
'components.native-chat.openedPullRequests.hidden',
"Orca won't show opened pull requests anymore"
)
)
}
className="absolute right-1.5 top-1.5 opacity-0 focus-visible:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100"
/>
<div className="space-y-0.5">
{pullRequests.map((pullRequest) => (
<OpenedPullRequestRow
@@ -69,6 +69,15 @@ export const opened = defineFixture({
frame.querySelector<HTMLButtonElement>('button')?.click()
}
})
export const hideShown = defineFixture({
width: 560,
render: () => <NativeChatTurnDiffRollup diff={diff} />,
// Keyboard focus shows the Hide button, as card hover does, without a pointer in the headless shot.
interact: (frame) => {
frame.ownerDocument.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
frame.querySelector<HTMLButtonElement>('button[aria-label="Hide"]')?.focus()
}
})
const session: NativeChatLiveSession = {
messages,
@@ -1,12 +1,14 @@
import { ChevronRight, EyeOff } from 'lucide-react'
import { ChevronRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { NATIVE_CHAT_TURN_DIFF_SETTING_SECTION_ID } from '../settings/chat/native-chat-turn-diff-setting-target'
import { DiffLineCounts } from '../right-sidebar/source-control/listing/diff-line-counts'
import { NativeChatDiffCard } from './NativeChatDiffCard'
import { NativeChatExpandable } from './NativeChatExpandable'
import { hideNativeChatCardKind } from './native-chat-card-kind-hidden-toast'
import {
NativeChatCardHideButton,
NATIVE_CHAT_HIDEABLE_CARD_CLASS
} from './NativeChatCardHideButton'
import { useNativeChatDisclosure } from './native-chat-disclosure-store'
import { nativeChatEditFileBaseName } from '../../../../shared/native-chat/tools/native-chat-edit-model'
import {
@@ -63,14 +65,6 @@ function TurnDiffFileRow({ file }: { file: NativeChatTurnDiffFile }): React.JSX.
)
}
function hideTurnDiffs(): Promise<void> {
return hideNativeChatCardKind(
{ nativeChatTurnDiffSummary: false },
NATIVE_CHAT_TURN_DIFF_SETTING_SECTION_ID,
translate('components.native-chat.turnDiff.hidden', "Orca won't show turn diffs anymore")
)
}
export function NativeChatTurnDiffRollup({
diff
}: {
@@ -83,7 +77,9 @@ export function NativeChatTurnDiffRollup({
const files = showAll ? diff.files : diff.files.slice(0, NATIVE_CHAT_TURN_DIFF_FILE_LIMIT)
const remaining = diff.files.length - files.length
return (
<div className="group/turn-diff rounded-lg border border-chat-code-border p-2 text-xs text-chat-foreground-faint">
<div
className={`${NATIVE_CHAT_HIDEABLE_CARD_CLASS} rounded-lg border border-chat-code-border p-2 text-xs text-chat-foreground-faint`}
>
<div className="flex min-w-0 items-center gap-1.5 px-1 pb-1">
<span className="min-w-0 truncate">
{translate('components.native-chat.turnDiff.changedFiles', '{{count}} changed files', {
@@ -95,25 +91,14 @@ export function NativeChatTurnDiffRollup({
{diff.truncated ? (
<span>{translate('components.native-chat.turnDiff.partial', 'Partial diff')}</span>
) : null}
{/* Shown on card hover or keyboard focus, like the transcript's other row actions. The
negative margin keeps the header one text line tall, as the row-height estimate assumes. */}
<span className="-my-1 ml-auto flex shrink-0 transition-opacity can-hover:pointer-events-none can-hover:opacity-0 group-hover/turn-diff:pointer-events-auto group-hover/turn-diff:opacity-100 has-[:focus-visible]:pointer-events-auto has-[:focus-visible]:opacity-100">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-xs"
aria-label={translate('components.native-chat.turnDiff.hide', 'Hide')}
onClick={() => void hideTurnDiffs()}
>
<EyeOff aria-hidden className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
{translate('components.native-chat.turnDiff.hide', 'Hide')}
</TooltipContent>
</Tooltip>
</span>
<NativeChatCardHideButton
updates={{ nativeChatTurnDiffSummary: false }}
settingSectionId={NATIVE_CHAT_TURN_DIFF_SETTING_SECTION_ID}
hiddenMessage={translate(
'components.native-chat.turnDiff.hidden',
"Orca won't show turn diffs anymore"
)}
/>
</div>
<div className="space-y-1">
{files.map((file) => (