summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2026-03-02 16:19:35 +0000
committerDavid Hill <[email protected]>2026-03-02 16:19:35 +0000
commit0a3a3216db5974efd3edc9a213054fd97d8dbd34 (patch)
tree8bc0554797fae57c0ffeaff6d27d34ffdb501433 /packages
parent633a3ba03adf9983e361efe994f34d405b573cbc (diff)
downloadopencode-0a3a3216db5974efd3edc9a213054fd97d8dbd34.tar.gz
opencode-0a3a3216db5974efd3edc9a213054fd97d8dbd34.zip
ui: move session review bottom padding
Remove bottom padding from the scroll wrapper and apply it to the accordion content instead.
Diffstat (limited to 'packages')
-rw-r--r--packages/app/src/pages/session/review-tab.tsx2
-rw-r--r--packages/ui/src/components/session-review.tsx528
2 files changed, 266 insertions, 264 deletions
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx
index 1b285407b..2b7eba324 100644
--- a/packages/app/src/pages/session/review-tab.tsx
+++ b/packages/app/src/pages/session/review-tab.tsx
@@ -176,7 +176,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) {
open={props.view().review.open()}
onOpenChange={props.view().review.setOpen}
classes={{
- root: props.classes?.root ?? "pb-6 pr-3",
+ root: props.classes?.root ?? "pr-3",
header: props.classes?.header ?? "px-3",
container: props.classes?.container ?? "pl-3",
}}
diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx
index 2f74207d5..c75baf921 100644
--- a/packages/ui/src/components/session-review.tsx
+++ b/packages/ui/src/components/session-review.tsx
@@ -621,279 +621,281 @@ export const SessionReview = (props: SessionReviewProps) => {
<div data-slot="session-review-container" class={props.classes?.container}>
<Show when={hasDiffs()} fallback={props.empty}>
- <Accordion multiple value={open()} onChange={handleChange}>
- <For each={files()}>
- {(file) => {
- let wrapper: HTMLDivElement | undefined
-
- const diff = createMemo(() => diffs().get(file))
- const item = () => diff()!
-
- const expanded = createMemo(() => open().includes(file))
- const force = () => !!store.force[file]
-
- const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file))
- const commentedLines = createMemo(() => comments().map((c) => c.selection))
-
- const beforeText = () => (typeof item().before === "string" ? item().before : "")
- const afterText = () => (typeof item().after === "string" ? item().after : "")
- const changedLines = () => item().additions + item().deletions
- const mediaKind = createMemo(() => mediaKindFromPath(file))
-
- const tooLarge = createMemo(() => {
- if (!expanded()) return false
- if (force()) return false
- if (mediaKind()) return false
- return changedLines() > MAX_DIFF_CHANGED_LINES
- })
-
- const isAdded = () =>
- item().status === "added" || (beforeText().length === 0 && afterText().length > 0)
- const isDeleted = () =>
- item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
-
- const selectedLines = createMemo(() => {
- const current = selection()
- if (!current || current.file !== file) return null
- return current.range
- })
-
- const draftRange = createMemo(() => {
- const current = commenting()
- if (!current || current.file !== file) return null
- return current.range
- })
-
- const commentsUi = createLineCommentController<SessionReviewComment>({
- comments,
- label: i18n.t("ui.lineComment.submit"),
- draftKey: () => file,
- state: {
- opened: () => {
- const current = opened()
- if (!current || current.file !== file) return null
- return current.id
+ <div class="pb-6">
+ <Accordion multiple value={open()} onChange={handleChange}>
+ <For each={files()}>
+ {(file) => {
+ let wrapper: HTMLDivElement | undefined
+
+ const diff = createMemo(() => diffs().get(file))
+ const item = () => diff()!
+
+ const expanded = createMemo(() => open().includes(file))
+ const force = () => !!store.force[file]
+
+ const comments = createMemo(() => (props.comments ?? []).filter((c) => c.file === file))
+ const commentedLines = createMemo(() => comments().map((c) => c.selection))
+
+ const beforeText = () => (typeof item().before === "string" ? item().before : "")
+ const afterText = () => (typeof item().after === "string" ? item().after : "")
+ const changedLines = () => item().additions + item().deletions
+ const mediaKind = createMemo(() => mediaKindFromPath(file))
+
+ const tooLarge = createMemo(() => {
+ if (!expanded()) return false
+ if (force()) return false
+ if (mediaKind()) return false
+ return changedLines() > MAX_DIFF_CHANGED_LINES
+ })
+
+ const isAdded = () =>
+ item().status === "added" || (beforeText().length === 0 && afterText().length > 0)
+ const isDeleted = () =>
+ item().status === "deleted" || (afterText().length === 0 && beforeText().length > 0)
+
+ const selectedLines = createMemo(() => {
+ const current = selection()
+ if (!current || current.file !== file) return null
+ return current.range
+ })
+
+ const draftRange = createMemo(() => {
+ const current = commenting()
+ if (!current || current.file !== file) return null
+ return current.range
+ })
+
+ const commentsUi = createLineCommentController<SessionReviewComment>({
+ comments,
+ label: i18n.t("ui.lineComment.submit"),
+ draftKey: () => file,
+ state: {
+ opened: () => {
+ const current = opened()
+ if (!current || current.file !== file) return null
+ return current.id
+ },
+ setOpened: (id) => setOpened(id ? { file, id } : null),
+ selected: selectedLines,
+ setSelected: (range) => setSelection(range ? { file, range } : null),
+ commenting: draftRange,
+ setCommenting: (range) => setCommenting(range ? { file, range } : null),
},
- setOpened: (id) => setOpened(id ? { file, id } : null),
- selected: selectedLines,
- setSelected: (range) => setSelection(range ? { file, range } : null),
- commenting: draftRange,
- setCommenting: (range) => setCommenting(range ? { file, range } : null),
- },
- getSide: selectionSide,
- clearSelectionOnSelectionEndNull: false,
- onSubmit: ({ comment, selection }) => {
- props.onLineComment?.({
- file,
- selection,
- comment,
- preview: selectionPreview(item(), selection),
- })
- },
- onUpdate: ({ id, comment, selection }) => {
- props.onLineCommentUpdate?.({
- id,
- file,
- selection,
- comment,
- preview: selectionPreview(item(), selection),
- })
- },
- onDelete: (comment) => {
- props.onLineCommentDelete?.({
- id: comment.id,
- file,
- })
- },
- editSubmitLabel: props.lineCommentActions?.saveLabel,
- renderCommentActions: props.lineCommentActions
- ? (comment, controls) => (
- <ReviewCommentMenu
- labels={props.lineCommentActions!}
- onEdit={controls.edit}
- onDelete={controls.remove}
- />
- )
- : undefined,
- })
-
- onCleanup(() => {
- anchors.delete(file)
- readyFiles.delete(file)
- searchHandles.delete(file)
- if (highlightedFile === file) highlightedFile = undefined
- })
-
- const handleLineSelected = (range: SelectedLineRange | null) => {
- if (!props.onLineComment) return
- commentsUi.onLineSelected(range)
- }
-
- const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
- if (!props.onLineComment) return
- commentsUi.onLineSelectionEnd(range)
- }
-
- return (
- <Accordion.Item
- value={file}
- id={diffId(file)}
- data-file={file}
- data-slot="session-review-accordion-item"
- data-selected={props.focusedFile === file ? "" : undefined}
- >
- <StickyAccordionHeader>
- <Accordion.Trigger>
- <div data-slot="session-review-trigger-content">
- <div data-slot="session-review-file-info">
- <FileIcon node={{ path: file, type: "file" }} />
- <div data-slot="session-review-file-name-container">
- <Show when={file.includes("/")}>
- <span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span>
- </Show>
- <span data-slot="session-review-filename">{getFilename(file)}</span>
- <Show when={props.onViewFile}>
- <Tooltip value={openFileLabel()} placement="top" gutter={4}>
- <button
- data-slot="session-review-view-button"
- type="button"
- aria-label={openFileLabel()}
- onClick={(e) => {
- e.stopPropagation()
- props.onViewFile?.(file)
- }}
- >
- <Icon name="open-file" size="small" />
- </button>
- </Tooltip>
- </Show>
+ getSide: selectionSide,
+ clearSelectionOnSelectionEndNull: false,
+ onSubmit: ({ comment, selection }) => {
+ props.onLineComment?.({
+ file,
+ selection,
+ comment,
+ preview: selectionPreview(item(), selection),
+ })
+ },
+ onUpdate: ({ id, comment, selection }) => {
+ props.onLineCommentUpdate?.({
+ id,
+ file,
+ selection,
+ comment,
+ preview: selectionPreview(item(), selection),
+ })
+ },
+ onDelete: (comment) => {
+ props.onLineCommentDelete?.({
+ id: comment.id,
+ file,
+ })
+ },
+ editSubmitLabel: props.lineCommentActions?.saveLabel,
+ renderCommentActions: props.lineCommentActions
+ ? (comment, controls) => (
+ <ReviewCommentMenu
+ labels={props.lineCommentActions!}
+ onEdit={controls.edit}
+ onDelete={controls.remove}
+ />
+ )
+ : undefined,
+ })
+
+ onCleanup(() => {
+ anchors.delete(file)
+ readyFiles.delete(file)
+ searchHandles.delete(file)
+ if (highlightedFile === file) highlightedFile = undefined
+ })
+
+ const handleLineSelected = (range: SelectedLineRange | null) => {
+ if (!props.onLineComment) return
+ commentsUi.onLineSelected(range)
+ }
+
+ const handleLineSelectionEnd = (range: SelectedLineRange | null) => {
+ if (!props.onLineComment) return
+ commentsUi.onLineSelectionEnd(range)
+ }
+
+ return (
+ <Accordion.Item
+ value={file}
+ id={diffId(file)}
+ data-file={file}
+ data-slot="session-review-accordion-item"
+ data-selected={props.focusedFile === file ? "" : undefined}
+ >
+ <StickyAccordionHeader>
+ <Accordion.Trigger>
+ <div data-slot="session-review-trigger-content">
+ <div data-slot="session-review-file-info">
+ <FileIcon node={{ path: file, type: "file" }} />
+ <div data-slot="session-review-file-name-container">
+ <Show when={file.includes("/")}>
+ <span data-slot="session-review-directory">{`\u202A${getDirectory(file)}\u202C`}</span>
+ </Show>
+ <span data-slot="session-review-filename">{getFilename(file)}</span>
+ <Show when={props.onViewFile}>
+ <Tooltip value={openFileLabel()} placement="top" gutter={4}>
+ <button
+ data-slot="session-review-view-button"
+ type="button"
+ aria-label={openFileLabel()}
+ onClick={(e) => {
+ e.stopPropagation()
+ props.onViewFile?.(file)
+ }}
+ >
+ <Icon name="open-file" size="small" />
+ </button>
+ </Tooltip>
+ </Show>
+ </div>
</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")}
+ <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={item()} />
+ </div>
+ </Match>
+ <Match when={isDeleted()}>
+ <span data-slot="session-review-change" data-type="removed">
+ {i18n.t("ui.sessionReview.change.removed")}
+ </span>
+ </Match>
+ <Match when={!!mediaKind()}>
+ <span data-slot="session-review-change" data-type="modified">
+ {i18n.t("ui.sessionReview.change.modified")}
</span>
+ </Match>
+ <Match when={true}>
<DiffChanges changes={item()} />
+ </Match>
+ </Switch>
+ <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(file, el)
+ }}
+ >
+ <Show when={expanded()}>
+ <Switch>
+ <Match when={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={() => setStore("force", file, true)}
+ >
+ {i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
+ </Button>
+ </div>
</div>
</Match>
- <Match when={isDeleted()}>
- <span data-slot="session-review-change" data-type="removed">
- {i18n.t("ui.sessionReview.change.removed")}
- </span>
- </Match>
- <Match when={!!mediaKind()}>
- <span data-slot="session-review-change" data-type="modified">
- {i18n.t("ui.sessionReview.change.modified")}
- </span>
- </Match>
<Match when={true}>
- <DiffChanges changes={item()} />
+ <Dynamic
+ component={fileComponent}
+ mode="diff"
+ preloadedDiff={item().preloaded}
+ diffStyle={diffStyle()}
+ expansionLineCount={searchExpanded() ? Number.MAX_SAFE_INTEGER : 20}
+ onRendered={() => {
+ readyFiles.add(file)
+ props.onDiffRendered?.()
+ }}
+ enableLineSelection={props.onLineComment != null}
+ enableHoverUtility={props.onLineComment != null}
+ onLineSelected={handleLineSelected}
+ onLineSelectionEnd={handleLineSelectionEnd}
+ onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
+ annotations={commentsUi.annotations()}
+ renderAnnotation={commentsUi.renderAnnotation}
+ renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined}
+ selectedLines={selectedLines()}
+ commentedLines={commentedLines()}
+ search={{
+ shortcuts: "disabled",
+ showBar: false,
+ disableVirtualization: searchExpanded(),
+ register: (handle: FileSearchHandle | null) => {
+ if (!handle) {
+ searchHandles.delete(file)
+ readyFiles.delete(file)
+ if (highlightedFile === file) highlightedFile = undefined
+ return
+ }
+
+ searchHandles.set(file, handle)
+ },
+ }}
+ before={{
+ name: file,
+ contents: typeof item().before === "string" ? item().before : "",
+ }}
+ after={{
+ name: file,
+ contents: typeof item().after === "string" ? item().after : "",
+ }}
+ media={{
+ mode: "auto",
+ path: file,
+ before: item().before,
+ after: item().after,
+ readFile: props.readFile,
+ }}
+ />
</Match>
</Switch>
- <span data-slot="session-review-diff-chevron">
- <Icon name="chevron-down" size="small" />
- </span>
- </div>
+ </Show>
</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(file, el)
- }}
- >
- <Show when={expanded()}>
- <Switch>
- <Match when={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={() => setStore("force", file, true)}
- >
- {i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
- </Button>
- </div>
- </div>
- </Match>
- <Match when={true}>
- <Dynamic
- component={fileComponent}
- mode="diff"
- preloadedDiff={item().preloaded}
- diffStyle={diffStyle()}
- expansionLineCount={searchExpanded() ? Number.MAX_SAFE_INTEGER : 20}
- onRendered={() => {
- readyFiles.add(file)
- props.onDiffRendered?.()
- }}
- enableLineSelection={props.onLineComment != null}
- enableHoverUtility={props.onLineComment != null}
- onLineSelected={handleLineSelected}
- onLineSelectionEnd={handleLineSelectionEnd}
- onLineNumberSelectionEnd={commentsUi.onLineNumberSelectionEnd}
- annotations={commentsUi.annotations()}
- renderAnnotation={commentsUi.renderAnnotation}
- renderHoverUtility={props.onLineComment ? commentsUi.renderHoverUtility : undefined}
- selectedLines={selectedLines()}
- commentedLines={commentedLines()}
- search={{
- shortcuts: "disabled",
- showBar: false,
- disableVirtualization: searchExpanded(),
- register: (handle: FileSearchHandle | null) => {
- if (!handle) {
- searchHandles.delete(file)
- readyFiles.delete(file)
- if (highlightedFile === file) highlightedFile = undefined
- return
- }
-
- searchHandles.set(file, handle)
- },
- }}
- before={{
- name: file,
- contents: typeof item().before === "string" ? item().before : "",
- }}
- after={{
- name: file,
- contents: typeof item().after === "string" ? item().after : "",
- }}
- media={{
- mode: "auto",
- path: file,
- before: item().before,
- after: item().after,
- readFile: props.readFile,
- }}
- />
- </Match>
- </Switch>
- </Show>
- </div>
- </Accordion.Content>
- </Accordion.Item>
- )
- }}
- </For>
- </Accordion>
+ </Accordion.Content>
+ </Accordion.Item>
+ )
+ }}
+ </For>
+ </Accordion>
+ </div>
</Show>
</div>
</ScrollView>