feat(app): v2 review panel overhaul (#31882)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
Aarav Sareen 2026-07-02 13:11:58 +05:30 committed by GitHub
commit 7d2618637f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3438 additions and 214 deletions

View file

@ -127,6 +127,12 @@
height: 24px;
}
}
&[data-variant="ghost"][data-scope="file-tree-v2"] {
> [data-slot="collapsible-trigger"] {
height: 28px;
}
}
}
@keyframes slideDown {

View file

@ -1,11 +1,14 @@
import { onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
import { onCleanup, onMount, splitProps, type ComponentProps, Show, mergeProps } from "solid-js"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createStore } from "solid-js/store"
import { useI18n } from "../context/i18n"
export type ScrollViewThumbVisibility = "hover" | "scroll"
export interface ScrollViewProps extends ComponentProps<"div"> {
viewportRef?: (el: HTMLDivElement) => void
orientation?: "vertical" | "horizontal" // currently only vertical is fully implemented for thumb
thumbVisibility?: ScrollViewThumbVisibility
}
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
@ -72,10 +75,10 @@ export function scrollTopFromThumbPointer(input: {
export function ScrollView(props: ScrollViewProps) {
const i18n = useI18n()
const merged = mergeProps({ orientation: "vertical" }, props)
const merged = mergeProps({ orientation: "vertical", thumbVisibility: "hover" }, props)
const [local, events, rest] = splitProps(
merged,
["class", "children", "viewportRef", "orientation", "style"],
["class", "children", "viewportRef", "orientation", "thumbVisibility", "style"],
[
"onScroll",
"onWheel",
@ -96,16 +99,37 @@ export function ScrollView(props: ScrollViewProps) {
const [state, setState] = createStore({
isHovered: false,
isDragging: false,
isScrolling: false,
thumbHeight: 0,
thumbTop: 0,
showThumb: false,
})
const isHovered = () => state.isHovered
const isDragging = () => state.isDragging
const isScrolling = () => state.isScrolling
const thumbHeight = () => state.thumbHeight
const thumbTop = () => state.thumbTop
const showThumb = () => state.showThumb
let scrollIdleTimer: ReturnType<typeof setTimeout> | undefined
const markScrolling = () => {
if (local.thumbVisibility !== "scroll") return
setState("isScrolling", true)
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
scrollIdleTimer = setTimeout(() => setState("isScrolling", false), 800)
}
const thumbVisible = () => {
if (isDragging()) return true
if (local.thumbVisibility === "scroll") return isScrolling()
return isHovered()
}
onCleanup(() => {
if (scrollIdleTimer !== undefined) clearTimeout(scrollIdleTimer)
})
const updateThumb = () => {
if (!viewportRef) return
const { scrollTop, scrollHeight, clientHeight } = viewportRef
@ -240,9 +264,15 @@ export function ScrollView(props: ScrollViewProps) {
data-scrollable
onScroll={(e) => {
updateThumb()
markScrolling()
if (typeof events.onScroll === "function") events.onScroll(e as any)
}}
onWheel={events.onWheel as any}
onWheel={(e) => {
markScrolling()
const handler = events.onWheel
if (typeof handler === "function") handler(e as any)
if (Array.isArray(handler)) handler[0](handler[1], e as any)
}}
onTouchStart={events.onTouchStart as any}
onTouchMove={events.onTouchMove as any}
onTouchEnd={events.onTouchEnd as any}
@ -266,7 +296,7 @@ export function ScrollView(props: ScrollViewProps) {
ref={thumbRef}
onPointerDown={onThumbPointerDown}
class="scroll-view__thumb"
data-visible={isHovered() || isDragging()}
data-visible={thumbVisible()}
data-dragging={isDragging()}
style={{
height: `${thumbHeight()}px`,

View file

@ -15,6 +15,23 @@ export const dict: Record<string, string> = {
"ui.sessionReview.largeDiff.title": "Diff too large to render",
"ui.sessionReview.largeDiff.meta": "Limit: {{limit}} changed lines. Current: {{current}} changed lines.",
"ui.sessionReview.largeDiff.renderAnyway": "Render anyway",
"ui.sessionReviewV2.expandMode": "Expand or collapse diff",
"ui.sessionReviewV2.filterFiles": "Filter files",
"ui.sessionReviewV2.toggleSidebar": "Toggle file tree",
"ui.sessionReviewV2.showAllLines": "Show all lines",
"ui.sessionReviewV2.hideNonDiffLines": "Hide non-diff lines",
"ui.sessionReviewV2.unifiedDiff": "Unified diff",
"ui.sessionReviewV2.splitDiff": "Split diff",
"ui.sessionReviewV2.previousFile": "Previous file",
"ui.sessionReviewV2.nextFile": "Next file",
"ui.sessionReviewV2.diffView": "Diff view",
"ui.sessionReviewV2.empty.noGit.title": "No tracked changes",
"ui.sessionReviewV2.empty.noGit.description": "Track, review, and undo changes in this project",
"ui.sessionReviewV2.empty.noGit.action": "Create Git repository",
"ui.sessionReviewV2.empty.noGit.actionLoading": "Creating Git repository...",
"ui.sessionReviewV2.empty.changes.title": "No file changes yet",
"ui.sessionReviewV2.empty.changes.description": "Project changes will appear here",
"ui.sessionReview.openFile": "Open file",
"ui.sessionReview.selection.line": "line {{line}}",
"ui.sessionReview.selection.lines": "lines {{start}}-{{end}}",
@ -35,6 +52,7 @@ export const dict: Record<string, string> = {
"ui.lineComment.editorLabel.suffix": "",
"ui.lineComment.placeholder": "Add comment",
"ui.lineComment.submit": "Comment",
"ui.lineComment.cancel": "Cancel",
"ui.sessionTurn.steps.show": "Show steps",
"ui.sessionTurn.steps.hide": "Hide steps",

View file

@ -0,0 +1,120 @@
[data-component="file-tree-v2"] {
display: flex;
flex-direction: column;
gap: 2px;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"] {
box-sizing: border-box;
width: 100%;
min-width: 0;
height: 28px;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
padding-right: 8px;
border: none;
border-radius: 6px;
background-color: transparent;
color: var(--v2-text-text-muted);
text-align: left;
cursor: pointer;
transition:
background-color 120ms ease,
color 120ms ease;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-ignored] {
color: var(--v2-text-text-faint);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"]:hover {
background-color: var(--v2-overlay-simple-overlay-hover);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] {
color: var(--v2-text-text-base);
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected]:hover {
background-color: var(--v2-overlay-simple-overlay-pressed);
}
[data-component="file-tree-v2"] .filetree-icon--mono {
color: var(--v2-icon-icon-muted);
}
[data-component="file-tree-v2"] .filetree-iconpair .filetree-icon--color {
opacity: 0;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] .filetree-iconpair .filetree-icon--color {
opacity: 1;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] .filetree-iconpair .filetree-icon--mono {
opacity: 0;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-chevron"] {
color: var(--v2-text-text-muted);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-chevron"] svg {
transition: transform 120ms ease;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-chevron"]:not([data-expanded]) svg {
transform: rotate(-90deg);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-row"][data-selected] [data-slot="file-tree-v2-chevron"] {
color: var(--v2-text-text-base);
}
[data-component="file-tree-v2"] .filetree-iconpair {
position: relative;
display: inline-flex;
width: 16px;
height: 16px;
}
[data-component="file-tree-v2"] .filetree-iconpair [data-component="file-icon"] {
position: absolute;
inset: 0;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"] {
box-sizing: border-box;
flex: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
font-size: 11px;
font-weight: 530;
line-height: 1;
letter-spacing: -0.04px;
text-align: center;
text-transform: uppercase;
font-variant-numeric: tabular-nums lining-nums;
font-feature-settings:
"tnum" on,
"lnum" on;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="modified"] {
opacity: 0;
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="added"] {
color: var(--v2-state-fg-success);
}
[data-component="file-tree-v2"] [data-slot="file-tree-v2-change"][data-change="deleted"] {
color: var(--v2-state-fg-danger);
}

View file

@ -57,6 +57,10 @@ const icons = {
viewBox: "0 0 16 16",
body: `<path d="M5 6.5L8 9.5L11 6.5" stroke="currentColor"/>`,
},
collapse: {
viewBox: "0 0 16 16",
body: `<path d="M8 1V6M11 3L8 6L5 3" stroke="currentColor"/><path d="M8 15V10M11 13L8 10L5 13" stroke="currentColor"/><path d="M4 8H6" stroke="currentColor"/><path d="M7 8H9" stroke="currentColor"/><path d="M10 8H12" stroke="currentColor"/>`,
},
check: {
viewBox: "0 0 16 16",
body: `<path d="M3.53613 8.17857L6.39328 11.75L12.4647 4.25" stroke="currentColor"/>`,
@ -93,6 +97,26 @@ const icons = {
viewBox: "0 0 16 16",
body: `<path d="M2.5 7.5H3.5V8.5H2.5V7.5Z" stroke="currentColor"/><path d="M7.5 7.5H8.5V8.5H7.5V7.5Z" stroke="currentColor"/><path d="M12.5 7.5H13.5V8.5H12.5V7.5Z" stroke="currentColor"/>`,
},
expand: {
viewBox: "0 0 16 16",
body: `<path d="M8.25 6.17773V1.17773M11.25 4.17773L8.25 1.17773L5.25 4.17773" stroke="currentColor"/><path d="M8.25 9.17773V14.1777M11.25 11.1777L8.25 14.1777L5.25 11.1777" stroke="currentColor"/><path d="M4.25 7.67773H12.25" stroke="currentColor"/>`,
},
filetree: {
viewBox: "0 0 16 16",
body: `<path d="M2.5 1.5V12.2484H6.75M2.5 4.74838H6.75" stroke="currentColor"/><rect x="8.5" y="3.2168" width="6" height="3" fill="none" stroke="currentColor"/><rect x="8.5" y="10.75" width="6" height="3" fill="none" stroke="currentColor"/>`,
},
split: {
viewBox: "0 0 16 16",
body: `<path d="M1 14H15L15 2H1V14Z" stroke="currentColor"/><rect x="3" y="4" width="4" height="8" fill="currentColor" fill-opacity="0.5"/><rect x="9" y="4" width="4" height="8" fill="currentColor" fill-opacity="0.5"/>`,
},
unified: {
viewBox: "0 0 16 16",
body: `<path d="M3.00001 4.00045L12.9998 4L13 6.99955L3 7L3.00001 4.00045Z" fill="currentColor" fill-opacity="0.5"/><path d="M3.0001 9H13L12.9999 12H3L3.0001 9Z" fill="currentColor" fill-opacity="0.5"/><path d="M1 14H15L15 2H1V14Z" stroke="currentColor"/>`,
},
review: {
viewBox: "0 0 20 20",
body: `<path d="M7 14.5H13M7 7.99512H10.0049M10.0049 7.99512H13M10.0049 7.99512V5M10.0049 7.99512V11M18 18V2L2 2L2 18H18Z" stroke="currentColor"/>`,
},
"outline-sliders": {
viewBox: "0 0 16 16",
body: `<path d="M11.7779 4.66675H14.4446M11.7779 4.66675C11.7779 5.77132 10.8825 6.66675 9.77789 6.66675C8.67332 6.66675 7.77789 5.77132 7.77789 4.66675M11.7779 4.66675C11.7779 3.56218 10.8825 2.66675 9.77789 2.66675C8.67332 2.66675 7.77789 3.56218 7.77789 4.66675M1.55566 4.66675H7.77789M4.22233 11.3334H1.55566M4.22233 11.3334C4.22233 12.438 5.11776 13.3334 6.22233 13.3334C7.3269 13.3334 8.22233 12.438 8.22233 11.3334M4.22233 11.3334C4.22233 10.2288 5.11776 9.33341 6.22233 9.33341C7.3269 9.33341 8.22233 10.2288 8.22233 11.3334M14.4446 11.3334H8.22233" stroke="currentColor"/>`,

View file

@ -53,11 +53,30 @@
align-items: center;
align-self: stretch;
padding: 0;
gap: 6px;
min-width: 0;
flex: 1 1 auto;
min-height: 0;
}
[data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] {
display: flex;
flex: none;
align-items: center;
justify-content: center;
padding-left: 8px;
color: var(--v2-icon-icon-muted);
}
[data-component="text-input-v2"] [data-slot="text-input-v2-leading-icon"] :is(svg, [data-slot="icon-svg"]) {
display: block;
flex: none;
}
[data-component="text-input-v2"][data-leading-icon] [data-slot="text-input-v2-input"] {
padding-left: 0;
}
[data-component="text-input-v2"] [data-slot="text-input-v2-input"] {
display: block;
width: 100%;
@ -81,6 +100,12 @@
color: var(--v2-text-text-faint);
}
[data-component="text-input-v2"] [data-slot="text-input-v2-input"][type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
appearance: none;
display: none;
}
[data-component="text-input-v2"][data-numeric] [data-slot="text-input-v2-input"] {
font-variant-numeric: tabular-nums;
}
@ -134,6 +159,14 @@
color: currentColor;
}
[data-component="text-input-v2"] [data-slot="text-input-v2-icon-button"][data-variant="clear"] {
width: 28px;
height: 28px;
padding: 0;
border-radius: 6px;
margin-right: -8px;
}
[data-component="text-input-v2"][data-invalid]:not([data-disabled]) [data-slot="text-input-v2-input"] {
color: var(--v2-state-fg-danger);
caret-color: var(--v2-state-fg-danger);

View file

@ -1,13 +1,20 @@
import { type ComponentProps, Show, splitProps } from "solid-js"
import { type ComponentProps, type JSX, Show, splitProps } from "solid-js"
import { Icon } from "./icon"
import "./text-input-v2.css"
export interface TextInputV2Props extends Omit<ComponentProps<"input">, "type"> {
/** Icon or adornment shown before the field value. */
leadingIcon?: JSX.Element
/** Show the trailing copy action. */
showCopyButton?: boolean
/** Show the trailing clear action. */
showClearButton?: boolean
/** Accessible label for the copy button. */
copyLabel?: string
/** Accessible label for the clear button. */
clearLabel?: string
onCopyClick?: (event: MouseEvent) => void
onClearClick?: (event: MouseEvent) => void
/** Apply tabular numerals to the field value. */
numeric?: boolean
/** Error styling for the field and value text. */
@ -21,9 +28,13 @@ export function TextInputV2(props: TextInputV2Props) {
const [local, inputProps] = splitProps(props, [
"class",
"classList",
"leadingIcon",
"showCopyButton",
"showClearButton",
"copyLabel",
"clearLabel",
"onCopyClick",
"onClearClick",
"numeric",
"invalid",
"appearance",
@ -37,12 +48,16 @@ export function TextInputV2(props: TextInputV2Props) {
data-invalid={local.invalid ? "" : undefined}
data-numeric={local.numeric ? "" : undefined}
data-appearance={local.appearance ?? "base"}
data-leading-icon={local.leadingIcon ? "" : undefined}
classList={{
...local.classList,
[local.class ?? ""]: !!local.class,
}}
>
<div data-slot="text-input-v2-value">
<Show when={local.leadingIcon}>
<span data-slot="text-input-v2-leading-icon">{local.leadingIcon}</span>
</Show>
<input
{...inputProps}
type={inputProps.type ?? "text"}
@ -51,15 +66,26 @@ export function TextInputV2(props: TextInputV2Props) {
data-slot="text-input-v2-input"
/>
</div>
<Show when={local.showCopyButton}>
<Show when={local.showClearButton || local.showCopyButton}>
<button
type="button"
data-slot="text-input-v2-icon-button"
aria-label={local.copyLabel ?? "Copy"}
data-variant={local.showClearButton ? "clear" : "copy"}
aria-label={local.showClearButton ? (local.clearLabel ?? "Clear") : (local.copyLabel ?? "Copy")}
disabled={local.disabled}
onClick={local.onCopyClick}
onMouseDown={(event) => {
if (!local.showClearButton) return
event.preventDefault()
}}
onClick={(event) => {
if (local.showClearButton) {
local.onClearClick?.(event)
return
}
local.onCopyClick?.(event)
}}
>
<Icon name="copy" />
<Icon name={local.showClearButton ? "xmark-small" : "copy"} />
</button>
</Show>
</div>