Explorar o código

fix(app): accordion styles

Adam hai 1 mes
pai
achega
338393c016

+ 37 - 12
packages/ui/src/components/accordion.css

@@ -2,7 +2,7 @@
   display: flex;
   flex-direction: column;
   align-items: flex-start;
-  gap: 8px;
+  gap: 0px;
   align-self: stretch;
 
   [data-slot="accordion-item"] {
@@ -11,7 +11,11 @@
     flex-direction: column;
     align-items: flex-start;
     align-self: stretch;
-    overflow: clip;
+    overflow: visible;
+
+    & + [data-slot="accordion-item"] {
+      margin-top: -1px;
+    }
 
     [data-slot="accordion-header"] {
       width: 100%;
@@ -31,9 +35,10 @@
         cursor: default;
         user-select: none;
 
-        background-color: var(--surface-base);
+        background-color: var(--background-stronger);
         border: 1px solid var(--border-weak-base);
-        border-radius: var(--radius-md);
+        border-radius: 0;
+        box-shadow: none;
         overflow: clip;
         color: var(--text-strong);
         transition: background-color 0.15s ease;
@@ -47,7 +52,10 @@
         letter-spacing: var(--letter-spacing-normal);
 
         &:hover {
-          background-color: var(--surface-base);
+          background-color: var(--surface-base-hover);
+        }
+        &:active {
+          background-color: var(--surface-base-active);
         }
         &:focus-visible {
           outline: none;
@@ -58,23 +66,40 @@
       }
     }
 
-    &[data-expanded] {
-      [data-slot="accordion-trigger"] {
-        border-bottom-left-radius: 0;
-        border-bottom-right-radius: 0;
+    &:first-child {
+      [data-slot="accordion-header"] [data-slot="accordion-trigger"] {
+        border-top-left-radius: var(--radius-lg);
+        border-top-right-radius: var(--radius-lg);
+      }
+    }
+
+    &:last-child:not([data-expanded]) {
+      [data-slot="accordion-header"] [data-slot="accordion-trigger"] {
+        border-bottom-left-radius: var(--radius-lg);
+        border-bottom-right-radius: var(--radius-lg);
       }
+    }
 
+    &[data-expanded] {
       [data-slot="accordion-content"] {
         border: 1px solid var(--border-weak-base);
-        border-top: none;
-        border-bottom-left-radius: var(--radius-md);
-        border-bottom-right-radius: var(--radius-md);
+        border-top: 0;
+        background-color: var(--background-stronger);
+      }
+    }
+
+    &:last-child[data-expanded] {
+      [data-slot="accordion-content"] {
+        border-bottom-left-radius: var(--radius-lg);
+        border-bottom-right-radius: var(--radius-lg);
       }
     }
 
     [data-slot="accordion-content"] {
       overflow: hidden;
       width: 100%;
+      border: 0;
+      background-color: transparent;
     }
   }
 }

+ 0 - 1
packages/ui/src/components/message-part.css

@@ -1288,7 +1288,6 @@
 }
 
 [data-component="apply-patch-file-diff"] {
-  border-top: 1px solid var(--border-weaker-base);
   max-height: 420px;
   overflow-y: auto;
   scrollbar-width: none;

+ 8 - 64
packages/ui/src/components/session-review.css

@@ -1,7 +1,7 @@
 [data-component="session-review"] {
   display: flex;
   flex-direction: column;
-  gap: 8px;
+  gap: 0px;
   height: 100%;
   overflow-y: auto;
   scrollbar-width: none;
@@ -19,7 +19,8 @@
     top: 0;
     z-index: 20;
     background-color: var(--background-stronger);
-    height: 32px;
+    height: 40px;
+    padding-bottom: 8px;
     flex-shrink: 0;
     display: flex;
     justify-content: space-between;
@@ -57,70 +58,13 @@
   }
 
   [data-component="sticky-accordion-header"] {
-    top: 40px;
+    --sticky-accordion-top: 40px;
   }
 
-  [data-component="sticky-accordion-header"][data-expanded]::before,
-  [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
-    top: -40px;
-  }
-
-  [data-slot="session-review-diffs-group"] {
-    background-color: var(--background-stronger);
-    border-radius: var(--radius-lg);
-    border: 1px solid var(--border-weak-base);
-    overflow: clip;
-
-    [data-component="accordion"] {
-      gap: 0;
-    }
-
-    [data-component="accordion"] [data-slot="accordion-item"] {
-      overflow: visible;
-    }
-
-    [data-component="accordion"]
-      [data-slot="accordion-item"]
-      [data-slot="accordion-header"]
-      [data-slot="accordion-trigger"] {
-      border: 0;
-      border-radius: 0;
-      box-shadow: none;
-      background-color: transparent;
-
-      &:hover {
-        background-color: var(--surface-base-hover);
-      }
-
-      &:active {
-        background-color: var(--surface-base-active);
-      }
-    }
-
-    [data-component="accordion"]
-      [data-slot="accordion-item"]
-      + [data-slot="accordion-item"]
-      [data-slot="accordion-header"]
-      [data-slot="accordion-trigger"] {
-      border-top: 1px solid var(--border-weak-base);
-    }
-
-    [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] {
-      border: 0;
-      border-top: 1px solid var(--border-weak-base);
-      border-radius: 0;
-    }
-
-    [data-component="sticky-accordion-header"][data-expanded]::before,
-    [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
-      top: 0;
-    }
-
-    [data-slot="session-review-accordion-item"][data-selected]
-      [data-slot="accordion-header"]
-      [data-slot="accordion-trigger"] {
-      background-color: var(--surface-base-active);
-    }
+  [data-slot="session-review-accordion-item"][data-selected]
+    [data-slot="accordion-header"]
+    [data-slot="accordion-trigger"] {
+    background-color: var(--surface-base-active);
   }
 
   [data-slot="accordion-item"] {

+ 360 - 362
packages/ui/src/components/session-review.tsx

@@ -320,395 +320,393 @@ export const SessionReview = (props: SessionReviewProps) => {
       </div>
       <div data-slot="session-review-container" class={props.classes?.container}>
         <Show when={hasDiffs()} fallback={props.empty}>
-          <div data-slot="session-review-diffs-group">
-            <Accordion multiple value={open()} onChange={handleChange}>
-              <For each={props.diffs}>
-                {(diff) => {
-                  let wrapper: HTMLDivElement | undefined
-
-                  const expanded = createMemo(() => open().includes(diff.file))
-                  const [force, setForce] = createSignal(false)
-
-                  const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
-                  const commentedLines = createMemo(() => comments().map((c) => c.selection))
-
-                  const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
-                  const afterText = () => (typeof diff.after === "string" ? diff.after : "")
-                  const changedLines = () => diff.additions + diff.deletions
-
-                  const tooLarge = createMemo(() => {
-                    if (!expanded()) return false
-                    if (force()) return false
-                    if (isImageFile(diff.file)) return false
-                    return changedLines() > MAX_DIFF_CHANGED_LINES
-                  })
-
-                  const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
-                  const isDeleted = () =>
-                    diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
-                  const isImage = () => isImageFile(diff.file)
-                  const isAudio = () => isAudioFile(diff.file)
-
-                  const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
-                  const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
-                  const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
-
-                  const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
-                  const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
-                  const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
-                  const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
-
-                  const selectedLines = createMemo(() => {
-                    const current = selection()
-                    if (!current || current.file !== diff.file) return null
-                    return current.range
-                  })
-
-                  const draftRange = createMemo(() => {
-                    const current = commenting()
-                    if (!current || current.file !== diff.file) return null
-                    return current.range
-                  })
-
-                  const [draft, setDraft] = createSignal("")
-                  const [positions, setPositions] = createSignal<Record<string, number>>({})
-                  const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
-
-                  const getRoot = () => {
-                    const el = wrapper
-                    if (!el) return
-
-                    const host = el.querySelector("diffs-container")
-                    if (!(host instanceof HTMLElement)) return
-                    return host.shadowRoot ?? undefined
+          <Accordion multiple value={open()} onChange={handleChange}>
+            <For each={props.diffs}>
+              {(diff) => {
+                let wrapper: HTMLDivElement | undefined
+
+                const expanded = createMemo(() => open().includes(diff.file))
+                const [force, setForce] = createSignal(false)
+
+                const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === diff.file))
+                const commentedLines = createMemo(() => comments().map((c) => c.selection))
+
+                const beforeText = () => (typeof diff.before === "string" ? diff.before : "")
+                const afterText = () => (typeof diff.after === "string" ? diff.after : "")
+                const changedLines = () => diff.additions + diff.deletions
+
+                const tooLarge = createMemo(() => {
+                  if (!expanded()) return false
+                  if (force()) return false
+                  if (isImageFile(diff.file)) return false
+                  return changedLines() > MAX_DIFF_CHANGED_LINES
+                })
+
+                const isAdded = () => diff.status === "added" || (beforeText().length === 0 && afterText().length > 0)
+                const isDeleted = () =>
+                  diff.status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
+                const isImage = () => isImageFile(diff.file)
+                const isAudio = () => isAudioFile(diff.file)
+
+                const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
+                const [imageSrc, setImageSrc] = createSignal<string | undefined>(diffImageSrc)
+                const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle")
+
+                const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before)
+                const [audioSrc, setAudioSrc] = createSignal<string | undefined>(diffAudioSrc)
+                const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle")
+                const [audioMime, setAudioMime] = createSignal<string | undefined>(undefined)
+
+                const selectedLines = createMemo(() => {
+                  const current = selection()
+                  if (!current || current.file !== diff.file) return null
+                  return current.range
+                })
+
+                const draftRange = createMemo(() => {
+                  const current = commenting()
+                  if (!current || current.file !== diff.file) return null
+                  return current.range
+                })
+
+                const [draft, setDraft] = createSignal("")
+                const [positions, setPositions] = createSignal<Record<string, number>>({})
+                const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
+
+                const getRoot = () => {
+                  const el = wrapper
+                  if (!el) return
+
+                  const host = el.querySelector("diffs-container")
+                  if (!(host instanceof HTMLElement)) return
+                  return host.shadowRoot ?? undefined
+                }
+
+                const updateAnchors = () => {
+                  const el = wrapper
+                  if (!el) return
+
+                  const root = getRoot()
+                  if (!root) return
+
+                  const next: Record<string, number> = {}
+                  for (const item of comments()) {
+                    const marker = findMarker(root, item.selection)
+                    if (!marker) continue
+                    next[item.id] = markerTop(el, marker)
                   }
+                  setPositions(next)
 
-                  const updateAnchors = () => {
-                    const el = wrapper
-                    if (!el) return
-
-                    const root = getRoot()
-                    if (!root) return
-
-                    const next: Record<string, number> = {}
-                    for (const item of comments()) {
-                      const marker = findMarker(root, item.selection)
-                      if (!marker) continue
-                      next[item.id] = markerTop(el, marker)
-                    }
-                    setPositions(next)
-
-                    const range = draftRange()
-                    if (!range) {
-                      setDraftTop(undefined)
-                      return
-                    }
-
-                    const marker = findMarker(root, range)
-                    if (!marker) {
-                      setDraftTop(undefined)
-                      return
-                    }
-
-                    setDraftTop(markerTop(el, marker))
+                  const range = draftRange()
+                  if (!range) {
+                    setDraftTop(undefined)
+                    return
                   }
 
-                  const scheduleAnchors = () => {
-                    requestAnimationFrame(updateAnchors)
+                  const marker = findMarker(root, range)
+                  if (!marker) {
+                    setDraftTop(undefined)
+                    return
                   }
 
-                  createEffect(() => {
-                    comments()
-                    scheduleAnchors()
-                  })
-
-                  createEffect(() => {
-                    const range = draftRange()
-                    if (!range) return
-                    setDraft("")
-                    scheduleAnchors()
-                  })
-
-                  createEffect(() => {
-                    if (!open().includes(diff.file)) return
-                    if (!isImage()) return
-                    if (imageSrc()) return
-                    if (imageStatus() !== "idle") return
-                    if (isDeleted()) return
-
-                    const reader = props.readFile
-                    if (!reader) return
-
-                    setImageStatus("loading")
-                    reader(diff.file)
-                      .then((result) => {
-                        const src = dataUrl(result)
-                        if (!src) {
-                          setImageStatus("error")
-                          return
-                        }
-                        setImageSrc(src)
-                        setImageStatus("idle")
-                      })
-                      .catch(() => {
+                  setDraftTop(markerTop(el, marker))
+                }
+
+                const scheduleAnchors = () => {
+                  requestAnimationFrame(updateAnchors)
+                }
+
+                createEffect(() => {
+                  comments()
+                  scheduleAnchors()
+                })
+
+                createEffect(() => {
+                  const range = draftRange()
+                  if (!range) return
+                  setDraft("")
+                  scheduleAnchors()
+                })
+
+                createEffect(() => {
+                  if (!open().includes(diff.file)) return
+                  if (!isImage()) return
+                  if (imageSrc()) return
+                  if (imageStatus() !== "idle") return
+                  if (isDeleted()) return
+
+                  const reader = props.readFile
+                  if (!reader) return
+
+                  setImageStatus("loading")
+                  reader(diff.file)
+                    .then((result) => {
+                      const src = dataUrl(result)
+                      if (!src) {
                         setImageStatus("error")
-                      })
-                  })
-
-                  createEffect(() => {
-                    if (!open().includes(diff.file)) return
-                    if (!isAudio()) return
-                    if (audioSrc()) return
-                    if (audioStatus() !== "idle") return
-
-                    const reader = props.readFile
-                    if (!reader) return
-
-                    setAudioStatus("loading")
-                    reader(diff.file)
-                      .then((result) => {
-                        const src = dataUrl(result)
-                        if (!src) {
-                          setAudioStatus("error")
-                          return
-                        }
-                        setAudioMime(normalizeMimeType(result?.mimeType))
-                        setAudioSrc(src)
-                        setAudioStatus("idle")
-                      })
-                      .catch(() => {
+                        return
+                      }
+                      setImageSrc(src)
+                      setImageStatus("idle")
+                    })
+                    .catch(() => {
+                      setImageStatus("error")
+                    })
+                })
+
+                createEffect(() => {
+                  if (!open().includes(diff.file)) return
+                  if (!isAudio()) return
+                  if (audioSrc()) return
+                  if (audioStatus() !== "idle") return
+
+                  const reader = props.readFile
+                  if (!reader) return
+
+                  setAudioStatus("loading")
+                  reader(diff.file)
+                    .then((result) => {
+                      const src = dataUrl(result)
+                      if (!src) {
                         setAudioStatus("error")
-                      })
-                  })
-
-                  const handleLineSelected = (range: SelectedLineRange | null) => {
-                    if (!props.onLineComment) return
-
-                    if (!range) {
-                      setSelection(null)
-                      return
-                    }
-
-                    setSelection({ file: diff.file, range })
+                        return
+                      }
+                      setAudioMime(normalizeMimeType(result?.mimeType))
+                      setAudioSrc(src)
+                      setAudioStatus("idle")
+                    })
+                    .catch(() => {
+                      setAudioStatus("error")
+                    })
+                })
+
+                const handleLineSelected = (range: SelectedLineRange | null) => {
+                  if (!props.onLineComment) return
+
+                  if (!range) {
+                    setSelection(null)
+                    return
                   }
 
-                  const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
-                    if (!props.onLineComment) return
+                  setSelection({ file: diff.file, range })
+                }
 
-                    if (!range) {
-                      setCommenting(null)
-                      return
-                    }
-
-                    setSelection({ file: diff.file, range })
-                    setCommenting({ file: diff.file, range })
-                  }
+                const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
+                  if (!props.onLineComment) return
 
-                  const openComment = (comment: SessionReviewComment) => {
-                    setOpened({ file: comment.file, id: comment.id })
-                    setSelection({ file: comment.file, range: comment.selection })
+                  if (!range) {
+                    setCommenting(null)
+                    return
                   }
 
-                  const isCommentOpen = (comment: SessionReviewComment) => {
-                    const current = opened()
-                    if (!current) return false
-                    return current.file === comment.file && current.id === comment.id
-                  }
-
-                  return (
-                    <Accordion.Item
-                      value={diff.file}
-                      id={diffId(diff.file)}
-                      data-file={diff.file}
-                      data-slot="session-review-accordion-item"
-                      data-selected={props.focusedFile === diff.file ? "" : undefined}
-                    >
-                      <StickyAccordionHeader>
-                        <Accordion.Trigger>
-                          <div data-slot="session-review-trigger-content">
-                            <div data-slot="session-review-file-info">
-                              <FileIcon node={{ path: diff.file, type: "file" }} />
-                              <div data-slot="session-review-file-name-container">
-                                <Show when={diff.file.includes("/")}>
-                                  <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
-                                </Show>
-                                <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
-                                <Show when={props.onViewFile}>
-                                  <Tooltip value="Open file" placement="top" gutter={4}>
-                                    <button
-                                      data-slot="session-review-view-button"
-                                      type="button"
-                                      aria-label="Open file"
-                                      onClick={(e) => {
-                                        e.stopPropagation()
-                                        props.onViewFile?.(diff.file)
-                                      }}
-                                    >
-                                      <Icon name="open-file" size="small" />
-                                    </button>
-                                  </Tooltip>
-                                </Show>
-                              </div>
-                            </div>
-                            <div data-slot="session-review-trigger-actions">
-                              <Switch>
-                                <Match when={isAdded()}>
-                                  <div data-slot="session-review-change-group" data-type="added">
-                                    <span data-slot="session-review-change" data-type="added">
-                                      {i18n.t("ui.sessionReview.change.added")}
-                                    </span>
-                                    <DiffChanges changes={diff} />
-                                  </div>
-                                </Match>
-                                <Match when={isDeleted()}>
-                                  <span data-slot="session-review-change" data-type="removed">
-                                    {i18n.t("ui.sessionReview.change.removed")}
-                                  </span>
-                                </Match>
-                                <Match when={isImage()}>
-                                  <span data-slot="session-review-change" data-type="modified">
-                                    {i18n.t("ui.sessionReview.change.modified")}
-                                  </span>
-                                </Match>
-                                <Match when={true}>
-                                  <DiffChanges changes={diff} />
-                                </Match>
-                              </Switch>
-                              <span data-slot="session-review-diff-chevron">
-                                <Icon name="chevron-down" size="small" />
-                              </span>
+                  setSelection({ file: diff.file, range })
+                  setCommenting({ file: diff.file, range })
+                }
+
+                const openComment = (comment: SessionReviewComment) => {
+                  setOpened({ file: comment.file, id: comment.id })
+                  setSelection({ file: comment.file, range: comment.selection })
+                }
+
+                const isCommentOpen = (comment: SessionReviewComment) => {
+                  const current = opened()
+                  if (!current) return false
+                  return current.file === comment.file && current.id === comment.id
+                }
+
+                return (
+                  <Accordion.Item
+                    value={diff.file}
+                    id={diffId(diff.file)}
+                    data-file={diff.file}
+                    data-slot="session-review-accordion-item"
+                    data-selected={props.focusedFile === diff.file ? "" : undefined}
+                  >
+                    <StickyAccordionHeader>
+                      <Accordion.Trigger>
+                        <div data-slot="session-review-trigger-content">
+                          <div data-slot="session-review-file-info">
+                            <FileIcon node={{ path: diff.file, type: "file" }} />
+                            <div data-slot="session-review-file-name-container">
+                              <Show when={diff.file.includes("/")}>
+                                <span data-slot="session-review-directory">{`\u202A${getDirectory(diff.file)}\u202C`}</span>
+                              </Show>
+                              <span data-slot="session-review-filename">{getFilename(diff.file)}</span>
+                              <Show when={props.onViewFile}>
+                                <Tooltip value="Open file" placement="top" gutter={4}>
+                                  <button
+                                    data-slot="session-review-view-button"
+                                    type="button"
+                                    aria-label="Open file"
+                                    onClick={(e) => {
+                                      e.stopPropagation()
+                                      props.onViewFile?.(diff.file)
+                                    }}
+                                  >
+                                    <Icon name="open-file" size="small" />
+                                  </button>
+                                </Tooltip>
+                              </Show>
                             </div>
                           </div>
-                        </Accordion.Trigger>
-                      </StickyAccordionHeader>
-                      <Accordion.Content data-slot="session-review-accordion-content">
-                        <div
-                          data-slot="session-review-diff-wrapper"
-                          ref={(el) => {
-                            wrapper = el
-                            anchors.set(diff.file, el)
-                            scheduleAnchors()
-                          }}
-                        >
-                          <Show when={expanded()}>
+                          <div data-slot="session-review-trigger-actions">
                             <Switch>
-                              <Match when={isImage() && imageSrc()}>
-                                <div data-slot="session-review-image-container">
-                                  <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
-                                </div>
-                              </Match>
-                              <Match when={isImage() && isDeleted()}>
-                                <div data-slot="session-review-image-container" data-removed>
-                                  <span data-slot="session-review-image-placeholder">
-                                    {i18n.t("ui.sessionReview.change.removed")}
+                              <Match when={isAdded()}>
+                                <div data-slot="session-review-change-group" data-type="added">
+                                  <span data-slot="session-review-change" data-type="added">
+                                    {i18n.t("ui.sessionReview.change.added")}
                                   </span>
+                                  <DiffChanges changes={diff} />
                                 </div>
                               </Match>
-                              <Match when={isImage() && !imageSrc()}>
-                                <div data-slot="session-review-image-container">
-                                  <span data-slot="session-review-image-placeholder">
-                                    {imageStatus() === "loading"
-                                      ? i18n.t("ui.sessionReview.image.loading")
-                                      : i18n.t("ui.sessionReview.image.placeholder")}
-                                  </span>
-                                </div>
+                              <Match when={isDeleted()}>
+                                <span data-slot="session-review-change" data-type="removed">
+                                  {i18n.t("ui.sessionReview.change.removed")}
+                                </span>
                               </Match>
-                              <Match when={!isImage() && tooLarge()}>
-                                <div data-slot="session-review-large-diff">
-                                  <div data-slot="session-review-large-diff-title">
-                                    {i18n.t("ui.sessionReview.largeDiff.title")}
-                                  </div>
-                                  <div data-slot="session-review-large-diff-meta">
-                                    {i18n.t("ui.sessionReview.largeDiff.meta", {
-                                      limit: MAX_DIFF_CHANGED_LINES.toLocaleString(),
-                                      current: changedLines().toLocaleString(),
-                                    })}
-                                  </div>
-                                  <div data-slot="session-review-large-diff-actions">
-                                    <Button size="normal" variant="secondary" onClick={() => setForce(true)}>
-                                      {i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
-                                    </Button>
-                                  </div>
-                                </div>
+                              <Match when={isImage()}>
+                                <span data-slot="session-review-change" data-type="modified">
+                                  {i18n.t("ui.sessionReview.change.modified")}
+                                </span>
                               </Match>
-                              <Match when={!isImage()}>
-                                <Dynamic
-                                  component={diffComponent}
-                                  preloadedDiff={diff.preloaded}
-                                  diffStyle={diffStyle()}
-                                  onRendered={() => {
-                                    props.onDiffRendered?.()
-                                    scheduleAnchors()
-                                  }}
-                                  enableLineSelection={props.onLineComment != null}
-                                  onLineSelected={handleLineSelected}
-                                  onLineSelectionEnd={handleLineSelectionEnd}
-                                  selectedLines={selectedLines()}
-                                  commentedLines={commentedLines()}
-                                  before={{
-                                    name: diff.file!,
-                                    contents: typeof diff.before === "string" ? diff.before : "",
-                                  }}
-                                  after={{
-                                    name: diff.file!,
-                                    contents: typeof diff.after === "string" ? diff.after : "",
-                                  }}
-                                />
+                              <Match when={true}>
+                                <DiffChanges changes={diff} />
                               </Match>
                             </Switch>
-
-                            <For each={comments()}>
-                              {(comment) => (
-                                <LineComment
-                                  id={comment.id}
-                                  top={positions()[comment.id]}
-                                  onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
-                                  onClick={() => {
-                                    if (isCommentOpen(comment)) {
-                                      setOpened(null)
-                                      return
-                                    }
-
-                                    openComment(comment)
+                            <span data-slot="session-review-diff-chevron">
+                              <Icon name="chevron-down" size="small" />
+                            </span>
+                          </div>
+                        </div>
+                      </Accordion.Trigger>
+                    </StickyAccordionHeader>
+                    <Accordion.Content data-slot="session-review-accordion-content">
+                      <div
+                        data-slot="session-review-diff-wrapper"
+                        ref={(el) => {
+                          wrapper = el
+                          anchors.set(diff.file, el)
+                          scheduleAnchors()
+                        }}
+                      >
+                        <Show when={expanded()}>
+                          <Switch>
+                            <Match when={isImage() && imageSrc()}>
+                              <div data-slot="session-review-image-container">
+                                <img data-slot="session-review-image" src={imageSrc()} alt={diff.file} />
+                              </div>
+                            </Match>
+                            <Match when={isImage() && isDeleted()}>
+                              <div data-slot="session-review-image-container" data-removed>
+                                <span data-slot="session-review-image-placeholder">
+                                  {i18n.t("ui.sessionReview.change.removed")}
+                                </span>
+                              </div>
+                            </Match>
+                            <Match when={isImage() && !imageSrc()}>
+                              <div data-slot="session-review-image-container">
+                                <span data-slot="session-review-image-placeholder">
+                                  {imageStatus() === "loading"
+                                    ? i18n.t("ui.sessionReview.image.loading")
+                                    : i18n.t("ui.sessionReview.image.placeholder")}
+                                </span>
+                              </div>
+                            </Match>
+                            <Match when={!isImage() && tooLarge()}>
+                              <div data-slot="session-review-large-diff">
+                                <div data-slot="session-review-large-diff-title">
+                                  {i18n.t("ui.sessionReview.largeDiff.title")}
+                                </div>
+                                <div data-slot="session-review-large-diff-meta">
+                                  {i18n.t("ui.sessionReview.largeDiff.meta", {
+                                    limit: MAX_DIFF_CHANGED_LINES.toLocaleString(),
+                                    current: changedLines().toLocaleString(),
+                                  })}
+                                </div>
+                                <div data-slot="session-review-large-diff-actions">
+                                  <Button size="normal" variant="secondary" onClick={() => setForce(true)}>
+                                    {i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
+                                  </Button>
+                                </div>
+                              </div>
+                            </Match>
+                            <Match when={!isImage()}>
+                              <Dynamic
+                                component={diffComponent}
+                                preloadedDiff={diff.preloaded}
+                                diffStyle={diffStyle()}
+                                onRendered={() => {
+                                  props.onDiffRendered?.()
+                                  scheduleAnchors()
+                                }}
+                                enableLineSelection={props.onLineComment != null}
+                                onLineSelected={handleLineSelected}
+                                onLineSelectionEnd={handleLineSelectionEnd}
+                                selectedLines={selectedLines()}
+                                commentedLines={commentedLines()}
+                                before={{
+                                  name: diff.file!,
+                                  contents: typeof diff.before === "string" ? diff.before : "",
+                                }}
+                                after={{
+                                  name: diff.file!,
+                                  contents: typeof diff.after === "string" ? diff.after : "",
+                                }}
+                              />
+                            </Match>
+                          </Switch>
+
+                          <For each={comments()}>
+                            {(comment) => (
+                              <LineComment
+                                id={comment.id}
+                                top={positions()[comment.id]}
+                                onMouseEnter={() => setSelection({ file: comment.file, range: comment.selection })}
+                                onClick={() => {
+                                  if (isCommentOpen(comment)) {
+                                    setOpened(null)
+                                    return
+                                  }
+
+                                  openComment(comment)
+                                }}
+                                open={isCommentOpen(comment)}
+                                comment={comment.comment}
+                                selection={selectionLabel(comment.selection)}
+                              />
+                            )}
+                          </For>
+
+                          <Show when={draftRange()}>
+                            {(range) => (
+                              <Show when={draftTop() !== undefined}>
+                                <LineCommentEditor
+                                  top={draftTop()}
+                                  value={draft()}
+                                  selection={selectionLabel(range())}
+                                  onInput={setDraft}
+                                  onCancel={() => setCommenting(null)}
+                                  onSubmit={(comment) => {
+                                    props.onLineComment?.({
+                                      file: diff.file,
+                                      selection: range(),
+                                      comment,
+                                      preview: selectionPreview(diff, range()),
+                                    })
+                                    setCommenting(null)
                                   }}
-                                  open={isCommentOpen(comment)}
-                                  comment={comment.comment}
-                                  selection={selectionLabel(comment.selection)}
                                 />
-                              )}
-                            </For>
-
-                            <Show when={draftRange()}>
-                              {(range) => (
-                                <Show when={draftTop() !== undefined}>
-                                  <LineCommentEditor
-                                    top={draftTop()}
-                                    value={draft()}
-                                    selection={selectionLabel(range())}
-                                    onInput={setDraft}
-                                    onCancel={() => setCommenting(null)}
-                                    onSubmit={(comment) => {
-                                      props.onLineComment?.({
-                                        file: diff.file,
-                                        selection: range(),
-                                        comment,
-                                        preview: selectionPreview(diff, range()),
-                                      })
-                                      setCommenting(null)
-                                    }}
-                                  />
-                                </Show>
-                              )}
-                            </Show>
+                              </Show>
+                            )}
                           </Show>
-                        </div>
-                      </Accordion.Content>
-                    </Accordion.Item>
-                  )
-                }}
-              </For>
-            </Accordion>
-          </div>
+                        </Show>
+                      </div>
+                    </Accordion.Content>
+                  </Accordion.Item>
+                )
+              }}
+            </For>
+          </Accordion>
         </Show>
       </div>
     </div>

+ 0 - 43
packages/ui/src/components/session-turn.css

@@ -129,49 +129,6 @@
     flex-direction: column;
   }
 
-  [data-slot="session-turn-diffs-group"] {
-    background-color: var(--background-stronger);
-    border-radius: var(--radius-lg);
-    border: 1px solid var(--border-weak-base);
-    overflow: clip;
-
-    [data-component="accordion"] {
-      gap: 0;
-    }
-
-    [data-component="accordion"]
-      [data-slot="accordion-item"]
-      [data-slot="accordion-header"]
-      [data-slot="accordion-trigger"] {
-      border: 0;
-      border-radius: 0;
-      box-shadow: none;
-      background-color: transparent;
-
-      &:hover {
-        background-color: var(--surface-base-hover);
-      }
-
-      &:active {
-        background-color: var(--surface-base-active);
-      }
-    }
-
-    [data-component="accordion"]
-      [data-slot="accordion-item"]
-      + [data-slot="accordion-item"]
-      [data-slot="accordion-header"]
-      [data-slot="accordion-trigger"] {
-      border-top: 1px solid var(--border-weak-base);
-    }
-
-    [data-component="accordion"] [data-slot="accordion-item"][data-expanded] [data-slot="accordion-content"] {
-      border: 0;
-      border-top: 1px solid var(--border-weak-base);
-      border-radius: 0;
-    }
-  }
-
   [data-slot="session-turn-diff-trigger"] {
     display: flex;
     align-items: center;

+ 67 - 69
packages/ui/src/components/session-turn.tsx

@@ -315,78 +315,76 @@ export function SessionTurn(
                       <Collapsible.Content>
                         <Show when={open()}>
                           <div data-component="session-turn-diffs-content">
-                            <div data-slot="session-turn-diffs-group">
-                              <Accordion
-                                multiple
-                                value={expanded()}
-                                onChange={(value) => setExpanded(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}>
-                                        <Accordion.Header>
-                                          <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)}
+                            <Accordion
+                              multiple
+                              value={expanded()}
+                              onChange={(value) => setExpanded(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}>
+                                      <Accordion.Header>
+                                        <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 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>
-                                        </Accordion.Header>
-                                        <Accordion.Content>
-                                          <Show when={visible()}>
-                                            <div data-slot="session-turn-diff-view" data-scrollable>
-                                              <Dynamic
-                                                component={diffComponent}
-                                                before={{ name: diff.file, contents: diff.before }}
-                                                after={{ name: diff.file, contents: diff.after }}
-                                              />
                                             </div>
-                                          </Show>
-                                        </Accordion.Content>
-                                      </Accordion.Item>
-                                    )
-                                  }}
-                                </For>
-                              </Accordion>
-                            </div>
+                                          </div>
+                                        </Accordion.Trigger>
+                                      </Accordion.Header>
+                                      <Accordion.Content>
+                                        <Show when={visible()}>
+                                          <div data-slot="session-turn-diff-view" data-scrollable>
+                                            <Dynamic
+                                              component={diffComponent}
+                                              before={{ name: diff.file, contents: diff.before }}
+                                              after={{ name: diff.file, contents: diff.after }}
+                                            />
+                                          </div>
+                                        </Show>
+                                      </Accordion.Content>
+                                    </Accordion.Item>
+                                  )
+                                }}
+                              </For>
+                            </Accordion>
                           </div>
                         </Show>
                       </Collapsible.Content>

+ 6 - 10
packages/ui/src/components/sticky-accordion-header.css

@@ -1,18 +1,14 @@
 [data-component="sticky-accordion-header"] {
+  --sticky-accordion-top: 0px;
   position: sticky;
-  top: 0px;
+  top: var(--sticky-accordion-top);
+}
+
+[data-slot="accordion-item"]:first-child [data-component="sticky-accordion-header"] {
+  background-color: var(--background-base);
 }
 
 [data-component="sticky-accordion-header"][data-expanded],
 [data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"] {
   z-index: 10;
 }
-
-[data-component="sticky-accordion-header"][data-expanded]::before,
-[data-slot="accordion-item"][data-expanded] [data-component="sticky-accordion-header"]::before {
-  content: "";
-  z-index: -10;
-  position: absolute;
-  inset: 0;
-  background-color: var(--background-stronger);
-}