feat(ui): redesign modified files section in session turn (#20348)

Co-authored-by: David Hill <iamdavidhill@gmail.com>
pull/20297/merge
Shoubhit Dash 2026-04-03 19:02:53 +05:30 committed by GitHub
parent 3deee3a02b
commit 9d57f21f9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 131 additions and 140 deletions

View File

@ -92,33 +92,15 @@
min-width: 0; min-width: 0;
} }
[data-slot="session-turn-diffs"] [data-slot="session-turn-diffs-header"] {
> [data-component="collapsible"]
> [data-slot="collapsible-trigger"][aria-expanded="true"] {
position: sticky;
top: var(--sticky-accordion-top, 0px);
z-index: 20;
height: 40px;
padding-bottom: 8px;
background-color: var(--background-stronger);
}
[data-component="session-turn-diffs-trigger"] {
width: 100%;
display: flex; display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 0;
}
[data-slot="session-turn-diffs-title"] {
display: inline-flex;
align-items: baseline; align-items: baseline;
gap: 8px; gap: 8px;
padding-bottom: 12px;
} }
[data-slot="session-turn-diffs-label"] { [data-slot="session-turn-diffs-label"] {
font-variant-numeric: tabular-nums;
color: var(--text-strong); color: var(--text-strong);
font-family: var(--font-family-sans); font-family: var(--font-family-sans);
font-size: var(--font-size-base); font-size: var(--font-size-base);
@ -126,28 +108,38 @@
line-height: var(--line-height-large); line-height: var(--line-height-large);
} }
[data-slot="session-turn-diffs-count"] { [data-slot="session-turn-diffs-toggle"] {
color: var(--text-base); color: var(--text-interactive-base);
font-family: var(--font-family-sans); font-family: var(--font-family-sans);
font-variant-numeric: tabular-nums;
font-size: var(--font-size-base); font-size: var(--font-size-base);
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
line-height: var(--line-height-x-large); line-height: var(--line-height-large);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease;
margin-left: 4px;
} }
[data-slot="session-turn-diffs-meta"] { [data-component="session-turn-diffs-group"]:hover [data-slot="session-turn-diffs-toggle"] {
display: inline-flex; opacity: 1;
align-items: center; }
gap: 8px;
flex-shrink: 0;
[data-slot="collapsible-arrow"] { [data-component="session-turn-diffs-group"][data-show-all] [data-slot="session-turn-diffs-toggle"] {
margin-left: -6px; opacity: 1;
transform: translateY(2px); }
}
[data-component="diff-changes"][data-variant="bars"] { [data-slot="session-turn-diffs-more"] {
transform: translateY(1px); color: var(--text-weak);
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
line-height: var(--line-height-large);
margin-top: 12px;
padding: 0 0 6px;
cursor: pointer;
transition: color 0.15s ease;
&:hover {
color: var(--text-link-base);
} }
} }

View File

@ -12,7 +12,6 @@ import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions
import { Card } from "./card" import { Card } from "./card"
import { Accordion } from "./accordion" import { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header" import { StickyAccordionHeader } from "./sticky-accordion-header"
import { Collapsible } from "./collapsible"
import { DiffChanges } from "./diff-changes" import { DiffChanges } from "./diff-changes"
import { Icon } from "./icon" import { Icon } from "./icon"
import { TextShimmer } from "./text-shimmer" import { TextShimmer } from "./text-shimmer"
@ -241,23 +240,20 @@ export function SessionTurn(
}, []) }, [])
.reverse() .reverse()
}) })
const MAX_FILES = 10
const edited = createMemo(() => diffs().length) const edited = createMemo(() => diffs().length)
const [state, setState] = createStore({ const [state, setState] = createStore({
open: false, showAll: false,
expanded: [] as string[], expanded: [] as string[],
}) })
const open = () => state.open const showAll = () => state.showAll
const expanded = () => state.expanded const expanded = () => state.expanded
const overflow = createMemo(() => Math.max(0, edited() - MAX_FILES))
createEffect( const visible = createMemo(() => (showAll() ? diffs() : diffs().slice(0, MAX_FILES)))
on( const toggleAll = () => {
open, autoScroll.pause()
(value, prev) => { setState("showAll", !showAll())
if (!value && prev) setState("expanded", []) }
},
{ defer: true },
),
)
const assistantMessages = createMemo( const assistantMessages = createMemo(
() => { () => {
@ -425,101 +421,100 @@ export function SessionTurn(
</Show> </Show>
<SessionRetry status={status()} show={active()} /> <SessionRetry status={status()} show={active()} />
<Show when={edited() > 0 && !working()}> <Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs"> <div
<Collapsible open={open()} onOpenChange={(value) => setState("open", value)} variant="ghost"> data-slot="session-turn-diffs"
<Collapsible.Trigger> data-component="session-turn-diffs-group"
<div data-component="session-turn-diffs-trigger"> data-show-all={showAll() || undefined}
<div data-slot="session-turn-diffs-title"> >
<span data-slot="session-turn-diffs-label">{i18n.t("ui.sessionReview.change.modified")}</span> <div data-slot="session-turn-diffs-header">
<span data-slot="session-turn-diffs-count"> <span data-slot="session-turn-diffs-label">
{edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")} {edited()} {i18n.t("ui.sessionTurn.diffs.changed")}{" "}
</span> {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")}
<div data-slot="session-turn-diffs-meta"> </span>
<DiffChanges changes={diffs()} variant="bars" /> <DiffChanges changes={diffs()} />
<Collapsible.Arrow /> <Show when={overflow() > 0}>
</div> <span data-slot="session-turn-diffs-toggle" onClick={toggleAll}>
</div> {showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")}
</div> </span>
</Collapsible.Trigger> </Show>
<Collapsible.Content> </div>
<Show when={open()}> <div data-component="session-turn-diffs-content">
<div data-component="session-turn-diffs-content"> <Accordion
<Accordion multiple
multiple style={{ "--sticky-accordion-offset": "40px" }}
style={{ "--sticky-accordion-offset": "40px" }} value={expanded()}
value={expanded()} onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
onChange={(value) => >
setState("expanded", Array.isArray(value) ? value : value ? [value] : []) <For each={visible()}>
} {(diff) => {
> const active = createMemo(() => expanded().includes(diff.file))
<For each={diffs()}> const [shown, setShown] = createSignal(false)
{(diff) => {
const active = createMemo(() => expanded().includes(diff.file))
const [visible, setVisible] = createSignal(false)
createEffect( createEffect(
on( on(
active, active,
(value) => { (value) => {
if (!value) { if (!value) {
setVisible(false) setShown(false)
return return
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!active()) return if (!active()) return
setVisible(true) setShown(true)
}) })
}, },
{ defer: true }, { defer: true },
), ),
) )
return ( return (
<Accordion.Item value={diff.file}> <Accordion.Item value={diff.file}>
<StickyAccordionHeader> <StickyAccordionHeader>
<Accordion.Trigger> <Accordion.Trigger>
<div data-slot="session-turn-diff-trigger"> <div data-slot="session-turn-diff-trigger">
<span data-slot="session-turn-diff-path"> <span data-slot="session-turn-diff-path">
<Show when={diff.file.includes("/")}> <Show when={diff.file.includes("/")}>
<span data-slot="session-turn-diff-directory"> <span data-slot="session-turn-diff-directory">
{`\u202A${getDirectory(diff.file)}\u202C`} {`\u202A${getDirectory(diff.file)}\u202C`}
</span> </span>
</Show>
<span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
</span>
<div data-slot="session-turn-diff-meta">
<span data-slot="session-turn-diff-changes">
<DiffChanges changes={diff} />
</span>
<span data-slot="session-turn-diff-chevron">
<Icon name="chevron-down" size="small" />
</span>
</div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content>
<Show when={visible()}>
<div data-slot="session-turn-diff-view" data-scrollable>
<Dynamic
component={fileComponent}
mode="diff"
before={{ name: diff.file, contents: diff.before }}
after={{ name: diff.file, contents: diff.after }}
/>
</div>
</Show> </Show>
</Accordion.Content> <span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
</Accordion.Item> </span>
) <div data-slot="session-turn-diff-meta">
}} <span data-slot="session-turn-diff-changes">
</For> <DiffChanges changes={diff} />
</Accordion> </span>
</div> <span data-slot="session-turn-diff-chevron">
</Show> <Icon name="chevron-down" size="small" />
</Collapsible.Content> </span>
</Collapsible> </div>
</div>
</Accordion.Trigger>
</StickyAccordionHeader>
<Accordion.Content>
<Show when={shown()}>
<div data-slot="session-turn-diff-view" data-scrollable>
<Dynamic
component={fileComponent}
mode="diff"
before={{ name: diff.file, contents: diff.before }}
after={{ name: diff.file, contents: diff.after }}
/>
</div>
</Show>
</Accordion.Content>
</Accordion.Item>
)
}}
</For>
</Accordion>
<Show when={!showAll() && overflow() > 0}>
<div data-slot="session-turn-diffs-more" onClick={toggleAll}>
{i18n.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })}
</div>
</Show>
</div>
</div> </div>
</Show> </Show>
<Show when={error()}> <Show when={error()}>

View File

@ -38,6 +38,10 @@ export const dict: Record<string, string> = {
"ui.sessionTurn.steps.hide": "Hide steps", "ui.sessionTurn.steps.hide": "Hide steps",
"ui.sessionTurn.summary.response": "Response", "ui.sessionTurn.summary.response": "Response",
"ui.sessionTurn.diff.showMore": "Show more changes ({{count}})", "ui.sessionTurn.diff.showMore": "Show more changes ({{count}})",
"ui.sessionTurn.diffs.changed": "Changed",
"ui.sessionTurn.diffs.showAll": "Show all",
"ui.sessionTurn.diffs.showLess": "Show less",
"ui.sessionTurn.diffs.more": "+{{count}} more files",
"ui.sessionTurn.retry.retrying": "retrying", "ui.sessionTurn.retry.retrying": "retrying",
"ui.sessionTurn.retry.inSeconds": "in {{seconds}}s", "ui.sessionTurn.retry.inSeconds": "in {{seconds}}s",