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;
}
[data-slot="session-turn-diffs"]
> [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%;
[data-slot="session-turn-diffs-header"] {
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;
gap: 8px;
padding-bottom: 12px;
}
[data-slot="session-turn-diffs-label"] {
font-variant-numeric: tabular-nums;
color: var(--text-strong);
font-family: var(--font-family-sans);
font-size: var(--font-size-base);
@ -126,28 +108,38 @@
line-height: var(--line-height-large);
}
[data-slot="session-turn-diffs-count"] {
color: var(--text-base);
[data-slot="session-turn-diffs-toggle"] {
color: var(--text-interactive-base);
font-family: var(--font-family-sans);
font-variant-numeric: tabular-nums;
font-size: var(--font-size-base);
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"] {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
[data-component="session-turn-diffs-group"]:hover [data-slot="session-turn-diffs-toggle"] {
opacity: 1;
}
[data-slot="collapsible-arrow"] {
margin-left: -6px;
transform: translateY(2px);
}
[data-component="session-turn-diffs-group"][data-show-all] [data-slot="session-turn-diffs-toggle"] {
opacity: 1;
}
[data-component="diff-changes"][data-variant="bars"] {
transform: translateY(1px);
[data-slot="session-turn-diffs-more"] {
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 { Accordion } from "./accordion"
import { StickyAccordionHeader } from "./sticky-accordion-header"
import { Collapsible } from "./collapsible"
import { DiffChanges } from "./diff-changes"
import { Icon } from "./icon"
import { TextShimmer } from "./text-shimmer"
@ -241,23 +240,20 @@ export function SessionTurn(
}, [])
.reverse()
})
const MAX_FILES = 10
const edited = createMemo(() => diffs().length)
const [state, setState] = createStore({
open: false,
showAll: false,
expanded: [] as string[],
})
const open = () => state.open
const showAll = () => state.showAll
const expanded = () => state.expanded
createEffect(
on(
open,
(value, prev) => {
if (!value && prev) setState("expanded", [])
},
{ defer: true },
),
)
const overflow = createMemo(() => Math.max(0, edited() - MAX_FILES))
const visible = createMemo(() => (showAll() ? diffs() : diffs().slice(0, MAX_FILES)))
const toggleAll = () => {
autoScroll.pause()
setState("showAll", !showAll())
}
const assistantMessages = createMemo(
() => {
@ -425,101 +421,100 @@ export function SessionTurn(
</Show>
<SessionRetry status={status()} show={active()} />
<Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={(value) => setState("open", value)} variant="ghost">
<Collapsible.Trigger>
<div data-component="session-turn-diffs-trigger">
<div data-slot="session-turn-diffs-title">
<span data-slot="session-turn-diffs-label">{i18n.t("ui.sessionReview.change.modified")}</span>
<span data-slot="session-turn-diffs-count">
{edited()} {i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")}
</span>
<div data-slot="session-turn-diffs-meta">
<DiffChanges changes={diffs()} variant="bars" />
<Collapsible.Arrow />
</div>
</div>
</div>
</Collapsible.Trigger>
<Collapsible.Content>
<Show when={open()}>
<div data-component="session-turn-diffs-content">
<Accordion
multiple
style={{ "--sticky-accordion-offset": "40px" }}
value={expanded()}
onChange={(value) =>
setState("expanded", Array.isArray(value) ? value : value ? [value] : [])
}
>
<For each={diffs()}>
{(diff) => {
const active = createMemo(() => expanded().includes(diff.file))
const [visible, setVisible] = createSignal(false)
<div
data-slot="session-turn-diffs"
data-component="session-turn-diffs-group"
data-show-all={showAll() || undefined}
>
<div data-slot="session-turn-diffs-header">
<span data-slot="session-turn-diffs-label">
{edited()} {i18n.t("ui.sessionTurn.diffs.changed")}{" "}
{i18n.t(edited() === 1 ? "ui.common.file.one" : "ui.common.file.other")}
</span>
<DiffChanges changes={diffs()} />
<Show when={overflow() > 0}>
<span data-slot="session-turn-diffs-toggle" onClick={toggleAll}>
{showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")}
</span>
</Show>
</div>
<div data-component="session-turn-diffs-content">
<Accordion
multiple
style={{ "--sticky-accordion-offset": "40px" }}
value={expanded()}
onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
>
<For each={visible()}>
{(diff) => {
const active = createMemo(() => expanded().includes(diff.file))
const [shown, setShown] = createSignal(false)
createEffect(
on(
active,
(value) => {
if (!value) {
setVisible(false)
return
}
createEffect(
on(
active,
(value) => {
if (!value) {
setShown(false)
return
}
requestAnimationFrame(() => {
if (!active()) return
setVisible(true)
})
},
{ defer: true },
),
)
requestAnimationFrame(() => {
if (!active()) return
setShown(true)
})
},
{ defer: true },
),
)
return (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader>
<Accordion.Trigger>
<div data-slot="session-turn-diff-trigger">
<span data-slot="session-turn-diff-path">
<Show when={diff.file.includes("/")}>
<span data-slot="session-turn-diff-directory">
{`\u202A${getDirectory(diff.file)}\u202C`}
</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>
return (
<Accordion.Item value={diff.file}>
<StickyAccordionHeader>
<Accordion.Trigger>
<div data-slot="session-turn-diff-trigger">
<span data-slot="session-turn-diff-path">
<Show when={diff.file.includes("/")}>
<span data-slot="session-turn-diff-directory">
{`\u202A${getDirectory(diff.file)}\u202C`}
</span>
</Show>
</Accordion.Content>
</Accordion.Item>
)
}}
</For>
</Accordion>
</div>
</Show>
</Collapsible.Content>
</Collapsible>
<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={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>
</Show>
<Show when={error()}>

View File

@ -38,6 +38,10 @@ export const dict: Record<string, string> = {
"ui.sessionTurn.steps.hide": "Hide steps",
"ui.sessionTurn.summary.response": "Response",
"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.inSeconds": "in {{seconds}}s",