فهرست منبع

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

Co-authored-by: David Hill <iamdavidhill@gmail.com>
Shoubhit Dash 4 ماه پیش
والد
کامیت
9d57f21f9f
3فایلهای تغییر یافته به همراه134 افزوده شده و 143 حذف شده
  1. 28 36
      packages/ui/src/components/session-turn.css
  2. 102 107
      packages/ui/src/components/session-turn.tsx
  3. 4 0
      packages/ui/src/i18n/en.ts

+ 28 - 36
packages/ui/src/components/session-turn.css

@@ -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-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;
 
-    [data-component="diff-changes"][data-variant="bars"] {
-      transform: translateY(1px);
+    &:hover {
+      color: var(--text-link-base);
     }
   }
 

+ 102 - 107
packages/ui/src/components/session-turn.tsx

@@ -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)
-
-                                createEffect(
-                                  on(
-                                    active,
-                                    (value) => {
-                                      if (!value) {
-                                        setVisible(false)
-                                        return
-                                      }
-
-                                      requestAnimationFrame(() => {
-                                        if (!active()) return
-                                        setVisible(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>
+                <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) {
+                                  setShown(false)
+                                  return
+                                }
+
+                                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>
-                                    </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()}>

+ 4 - 0
packages/ui/src/i18n/en.ts

@@ -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",