chore: cleanup

This commit is contained in:
adamelmore
2026-01-26 09:55:37 -06:00
parent 801eb5d2cb
commit b8e8d82323
3 changed files with 622 additions and 674 deletions

View File

@@ -22,6 +22,7 @@ export default function FileTree(props: {
nodeClass?: string nodeClass?: string
level?: number level?: number
allowed?: readonly string[] allowed?: readonly string[]
modified?: readonly string[]
draggable?: boolean draggable?: boolean
tooltip?: boolean tooltip?: boolean
onFileClick?: (file: FileNode) => void onFileClick?: (file: FileNode) => void
@@ -50,6 +51,12 @@ export default function FileTree(props: {
return { files, dirs } return { files, dirs }
}) })
const marks = createMemo(() => {
const modified = props.modified
if (!modified || modified.length === 0) return
return new Set(modified)
})
createEffect(() => { createEffect(() => {
const current = filter() const current = filter()
if (!current) return if (!current) return
@@ -89,7 +96,7 @@ export default function FileTree(props: {
<Dynamic <Dynamic
component={local.as ?? "div"} component={local.as ?? "div"}
classList={{ classList={{
"w-full flex items-center gap-x-2 rounded-md px-2 py-1 hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true, "w-full min-w-0 flex items-center gap-x-2 rounded-md px-2 py-1 hover:bg-surface-raised-base-hover active:bg-surface-base-active transition-colors cursor-pointer": true,
...(local.classList ?? {}), ...(local.classList ?? {}),
[local.class ?? ""]: !!local.class, [local.class ?? ""]: !!local.class,
[props.nodeClass ?? ""]: !!props.nodeClass, [props.nodeClass ?? ""]: !!props.nodeClass,
@@ -125,13 +132,16 @@ export default function FileTree(props: {
{local.children} {local.children}
<span <span
classList={{ classList={{
"text-12-regular whitespace-nowrap truncate": true, "flex-1 min-w-0 text-12-regular whitespace-nowrap truncate": true,
"text-text-weaker": local.node.ignored, "text-text-weaker": local.node.ignored,
"text-text-weak": !local.node.ignored, "text-text-weak": !local.node.ignored,
}} }}
> >
{local.node.name} {local.node.name}
</span> </span>
{local.node.type === "file" && marks()?.has(local.node.path) ? (
<div class="shrink-0 size-1.5 rounded-full bg-surface-warning-strong" />
) : null}
</Dynamic> </Dynamic>
) )
} }
@@ -173,6 +183,7 @@ export default function FileTree(props: {
path={node.path} path={node.path}
level={level + 1} level={level + 1}
allowed={props.allowed} allowed={props.allowed}
modified={props.modified}
draggable={props.draggable} draggable={props.draggable}
tooltip={props.tooltip} tooltip={props.tooltip}
onFileClick={props.onFileClick} onFileClick={props.onFileClick}

View File

@@ -425,6 +425,8 @@ export default function Page() {
} }
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : [])) const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
const emptyDiffFiles: string[] = []
const diffFiles = createMemo(() => diffs().map((d) => d.file), emptyDiffFiles, { equals: same })
const diffsReady = createMemo(() => { const diffsReady = createMemo(() => {
const id = params.id const id = params.id
if (!id) return true if (!id) return true
@@ -1934,47 +1936,9 @@ export default function Page() {
class="relative flex-1 min-w-0 h-full border-l border-border-weak-base flex" class="relative flex-1 min-w-0 h-full border-l border-border-weak-base flex"
> >
<div class="flex-1 min-w-0 h-full"> <div class="flex-1 min-w-0 h-full">
<Show when={layout.fileTree.opened() && fileTreeTab() === "changes"}>
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<Switch>
<Match when={hasReview()}>
<Show <Show
when={diffsReady()} when={layout.fileTree.opened() && fileTreeTab() === "changes"}
fallback={ fallback={
<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
}
>
<SessionReviewTab
diffs={diffs}
view={view}
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
onScrollRef={setReviewScroll}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={(path) => {
const value = file.tab(path)
tabs().open(value)
file.load(path)
}}
/>
</Show>
</Match>
<Match when={true}>
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-13-regular text-text-weak max-w-56">No changes in this session yet</div>
</div>
</Match>
</Switch>
</div>
</div>
</Show>
<Show when={!layout.fileTree.opened() || fileTreeTab() === "all"}>
<DragDropProvider <DragDropProvider
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
@@ -2026,9 +1990,11 @@ export default function Page() {
</Tabs.Trigger> </Tabs.Trigger>
</Show> </Show>
<SortableProvider ids={openedTabs()}> <SortableProvider ids={openedTabs()}>
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For> <For each={openedTabs()}>
{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}
</For>
</SortableProvider> </SortableProvider>
<div class="bg-background-base h-full flex items-center justify-center border-b border-border-weak-base px-3"> <div class="bg-background-base h-full shrink-0 sticky right-0 z-10 flex items-center justify-center border-b border-l border-border-weak-base px-3">
<TooltipKeybind <TooltipKeybind
title={language.t("command.file.open")} title={language.t("command.file.open")}
keybind={command.keybind("file.open")} keybind={command.keybind("file.open")}
@@ -2114,13 +2080,13 @@ export default function Page() {
</Show> </Show>
</Tabs.Content> </Tabs.Content>
</Show> </Show>
<For each={openedTabs()}> <For each={openedTabs()}>
{(tab) => { {(tab) => {
let scroll: HTMLDivElement | undefined let scroll: HTMLDivElement | undefined
let scrollFrame: number | undefined let scrollFrame: number | undefined
let pending: { x: number; y: number } | undefined let pending: { x: number; y: number } | undefined
let codeScroll: HTMLElement[] = [] let codeScroll: HTMLElement[] = []
let focusToken = 0
const path = createMemo(() => file.pathFromTab(tab)) const path = createMemo(() => file.pathFromTab(tab))
const state = createMemo(() => { const state = createMemo(() => {
@@ -2184,8 +2150,6 @@ export default function Page() {
const [positions, setPositions] = createSignal<Record<string, number>>({}) const [positions, setPositions] = createSignal<Record<string, number>>({})
const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined) const [draftTop, setDraftTop] = createSignal<number | undefined>(undefined)
const empty = {} as Record<string, number>
const commentLabel = (range: SelectedLineRange) => { const commentLabel = (range: SelectedLineRange) => {
const start = Math.min(range.start, range.end) const start = Math.min(range.start, range.end)
const end = Math.max(range.start, range.end) const end = Math.max(range.start, range.end)
@@ -2219,22 +2183,12 @@ export default function Page() {
return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2) return rect.top - wrapperRect.top + Math.max(0, (rect.height - 20) / 2)
} }
const equal = (a: Record<string, number>, b: Record<string, number>) => {
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length) return false
for (const key of aKeys) {
if (a[key] !== b[key]) return false
}
return true
}
const updateComments = () => { const updateComments = () => {
const el = wrap const el = wrap
const root = getRoot() const root = getRoot()
if (!el || !root) { if (!el || !root) {
setPositions((prev) => (Object.keys(prev).length === 0 ? prev : empty)) setPositions({})
setDraftTop((prev) => (prev === undefined ? prev : undefined)) setDraftTop(undefined)
return return
} }
@@ -2245,7 +2199,7 @@ export default function Page() {
next[comment.id] = markerTop(el, marker) next[comment.id] = markerTop(el, marker)
} }
setPositions((prev) => (equal(prev, next) ? prev : next)) setPositions(next)
const range = commenting() const range = commenting()
if (!range) { if (!range) {
@@ -2259,18 +2213,11 @@ export default function Page() {
return return
} }
const nextTop = markerTop(el, marker) setDraftTop(markerTop(el, marker))
setDraftTop((prev) => (prev === nextTop ? prev : nextTop))
} }
let commentFrame: number | undefined
const scheduleComments = () => { const scheduleComments = () => {
if (commentFrame !== undefined) return requestAnimationFrame(updateComments)
commentFrame = requestAnimationFrame(() => {
commentFrame = undefined
updateComments()
})
} }
createEffect(() => { createEffect(() => {
@@ -2278,13 +2225,9 @@ export default function Page() {
scheduleComments() scheduleComments()
}) })
createEffect(() => {
commenting()
scheduleComments()
})
createEffect(() => { createEffect(() => {
const range = commenting() const range = commenting()
scheduleComments()
if (!range) return if (!range) return
setDraft("") setDraft("")
}) })
@@ -2299,50 +2242,9 @@ export default function Page() {
const target = fileComments().find((comment) => comment.id === focus.id) const target = fileComments().find((comment) => comment.id === focus.id)
if (!target) return if (!target) return
focusToken++
const token = focusToken
setOpenedComment(target.id) setOpenedComment(target.id)
setCommenting(null) setCommenting(null)
file.setSelectedLines(p, target.selection) file.setSelectedLines(p, target.selection)
const scrollTo = (attempt: number) => {
if (token !== focusToken) return
const root = scroll
if (!root) {
if (attempt >= 120) return
requestAnimationFrame(() => scrollTo(attempt + 1))
return
}
const anchor = root.querySelector(`[data-comment-id="${target.id}"]`)
const ready =
anchor instanceof HTMLElement &&
anchor.style.pointerEvents !== "none" &&
anchor.style.opacity !== "0"
const shadow = getRoot()
const marker = shadow ? findMarker(shadow, target.selection) : undefined
const node = (ready ? anchor : (marker ?? wrap)) as HTMLElement | undefined
if (!node) {
if (attempt >= 120) return
requestAnimationFrame(() => scrollTo(attempt + 1))
return
}
const rootRect = root.getBoundingClientRect()
const targetRect = node.getBoundingClientRect()
const offset = targetRect.top - rootRect.top
const next = root.scrollTop + offset - rootRect.height / 2 + targetRect.height / 2
root.scrollTop = Math.max(0, next)
if (ready || marker) return
if (attempt >= 120) return
requestAnimationFrame(() => scrollTo(attempt + 1))
}
requestAnimationFrame(() => scrollTo(0))
requestAnimationFrame(() => comments.clearFocus()) requestAnimationFrame(() => comments.clearFocus())
}) })
@@ -2392,6 +2294,8 @@ export default function Page() {
id={comment.id} id={comment.id}
top={positions()[comment.id]} top={positions()[comment.id]}
open={openedComment() === comment.id} open={openedComment() === comment.id}
comment={comment.comment}
selection={commentLabel(comment.selection)}
onMouseEnter={() => { onMouseEnter={() => {
const p = path() const p = path()
if (!p) return if (!p) return
@@ -2404,8 +2308,6 @@ export default function Page() {
setOpenedComment((current) => (current === comment.id ? null : comment.id)) setOpenedComment((current) => (current === comment.id ? null : comment.id))
file.setSelectedLines(p, comment.selection) file.setSelectedLines(p, comment.selection)
}} }}
comment={comment.comment}
selection={commentLabel(comment.selection)}
/> />
)} )}
</For> </For>
@@ -2416,28 +2318,26 @@ export default function Page() {
top={draftTop()} top={draftTop()}
value={draft()} value={draft()}
selection={commentLabel(range())} selection={commentLabel(range())}
onInput={setDraft} onInput={(value) => setDraft(value)}
onCancel={() => setCommenting(null)} onCancel={() => setCommenting(null)}
onSubmit={(comment) => { onSubmit={(value) => {
const p = path() const p = path()
if (!p) return if (!p) return
addCommentToContext({ addCommentToContext({
file: p, file: p,
selection: range(), selection: range(),
comment, comment: value,
origin: "file", origin: "file",
}) })
setCommenting(null) setCommenting(null)
}} }}
onPopoverFocusOut={(e) => { onPopoverFocusOut={(e: FocusEvent) => {
const target = e.relatedTarget as Node | null const current = e.currentTarget as HTMLDivElement
if (target && e.currentTarget.contains(target)) return const target = e.relatedTarget
// Delay to allow click handlers to fire first if (target instanceof Node && current.contains(target)) return
setTimeout(() => { setTimeout(() => {
if ( if (!document.activeElement || !current.contains(document.activeElement)) {
!document.activeElement ||
!e.currentTarget.contains(document.activeElement)
) {
setCommenting(null) setCommenting(null)
} }
}, 0) }, 0)
@@ -2572,7 +2472,6 @@ export default function Page() {
) )
onCleanup(() => { onCleanup(() => {
if (commentFrame !== undefined) cancelAnimationFrame(commentFrame)
for (const item of codeScroll) { for (const item of codeScroll) {
item.removeEventListener("scroll", handleCodeScroll) item.removeEventListener("scroll", handleCodeScroll)
} }
@@ -2638,6 +2537,45 @@ export default function Page() {
</Show> </Show>
</DragOverlay> </DragOverlay>
</DragDropProvider> </DragDropProvider>
}
>
<div class="flex flex-col h-full overflow-hidden bg-background-stronger contain-strict">
<div class="relative pt-2 flex-1 min-h-0 overflow-hidden">
<Switch>
<Match when={hasReview()}>
<Show
when={diffsReady()}
fallback={
<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
}
>
<SessionReviewTab
diffs={diffs}
view={view}
diffStyle={layout.review.diffStyle()}
onDiffStyleChange={layout.review.setDiffStyle}
onScrollRef={setReviewScroll}
onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })}
comments={comments.all()}
focusedComment={comments.focus()}
onFocusedCommentChange={comments.setFocus}
onViewFile={(path) => {
const value = file.tab(path)
tabs().open(value)
file.load(path)
}}
/>
</Show>
</Match>
<Match when={true}>
<div class="h-full px-6 pb-30 flex flex-col items-center justify-center text-center gap-6">
<Mark class="w-14 opacity-10" />
<div class="text-13-regular text-text-weak max-w-56">No changes in this session yet</div>
</div>
</Match>
</Switch>
</div>
</div>
</Show> </Show>
</div> </div>
@@ -2647,7 +2585,7 @@ export default function Page() {
<Tabs variant="pill" value={fileTreeTab()} onChange={setFileTreeTabValue} class="h-full"> <Tabs variant="pill" value={fileTreeTab()} onChange={setFileTreeTabValue} class="h-full">
<Tabs.List> <Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}> <Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
Changes {reviewCount()} {reviewCount() === 1 ? "Change" : "Changes"}
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}> <Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
All files All files
@@ -2662,7 +2600,7 @@ export default function Page() {
> >
<FileTree <FileTree
path="" path=""
allowed={diffs().map((d) => d.file)} allowed={diffFiles()}
draggable={false} draggable={false}
tooltip={false} tooltip={false}
onFileClick={(node) => focusReviewDiff(node.path)} onFileClick={(node) => focusReviewDiff(node.path)}
@@ -2675,7 +2613,7 @@ export default function Page() {
</Switch> </Switch>
</Tabs.Content> </Tabs.Content>
<Tabs.Content value="all" class="bg-background-base p-2"> <Tabs.Content value="all" class="bg-background-base p-2">
<FileTree path="" onFileClick={(node) => openTab(file.tab(node.path))} /> <FileTree path="" modified={diffFiles()} onFileClick={(node) => openTab(file.tab(node.path))} />
</Tabs.Content> </Tabs.Content>
</Tabs> </Tabs>
</div> </div>

View File

@@ -219,7 +219,6 @@
height: auto; height: auto;
padding: 6px; padding: 6px;
gap: 4px; gap: 4px;
border-bottom: 1px solid var(--border-weak-base);
background-color: var(--background-base); background-color: var(--background-base);
&::after { &::after {
@@ -230,7 +229,7 @@
[data-slot="tabs-trigger-wrapper"] { [data-slot="tabs-trigger-wrapper"] {
height: 32px; height: 32px;
border: none; border: none;
border-radius: 999px; border-radius: var(--radius-sm);
background-color: transparent; background-color: transparent;
gap: 0; gap: 0;