diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 6b568e9160..b897e394aa 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1220,7 +1220,10 @@ export const PromptInput: Component = (props) => { }) return undefined }) - if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) + if (session) { + layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) + navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) + } } if (!session) return diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 28fe628a80..e2fd0a7f45 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -33,6 +33,14 @@ type SessionTabs = { type SessionView = { scroll: Record reviewOpen?: string[] + pendingMessage?: string + pendingMessageAt?: number +} + +type TabHandoff = { + dir: string + id: string + at: number } export type LocalProject = Partial & { worktree: string; expanded: boolean } @@ -115,10 +123,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( }, sessionTabs: {} as Record, sessionView: {} as Record, + handoff: { + tabs: undefined as TabHandoff | undefined, + }, }), ) const MAX_SESSION_KEYS = 50 + const PENDING_MESSAGE_TTL_MS = 2 * 60 * 1000 const meta = { active: undefined as string | undefined, pruned: false } const used = new Map() @@ -411,6 +423,16 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( return { ready, + handoff: { + tabs: createMemo(() => store.handoff?.tabs), + setTabs(dir: string, id: string) { + setStore("handoff", "tabs", { dir, id, at: Date.now() }) + }, + clearTabs() { + if (!store.handoff?.tabs) return + setStore("handoff", "tabs", undefined) + }, + }, projects: { list, open(directory: string) { @@ -536,6 +558,49 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setStore("mobileSidebar", "opened", (x) => !x) }, }, + pendingMessage: { + set(sessionKey: string, messageID: string) { + const at = Date.now() + touch(sessionKey) + const current = store.sessionView[sessionKey] + if (!current) { + setStore("sessionView", sessionKey, { + scroll: {}, + pendingMessage: messageID, + pendingMessageAt: at, + }) + prune(meta.active ?? sessionKey) + return + } + + setStore( + "sessionView", + sessionKey, + produce((draft) => { + draft.pendingMessage = messageID + draft.pendingMessageAt = at + }), + ) + }, + consume(sessionKey: string) { + const current = store.sessionView[sessionKey] + const message = current?.pendingMessage + const at = current?.pendingMessageAt + if (!message || !at) return + + setStore( + "sessionView", + sessionKey, + produce((draft) => { + delete draft.pendingMessage + delete draft.pendingMessageAt + }), + ) + + if (Date.now() - at > PENDING_MESSAGE_TTL_MS) return + return message + }, + }, view(sessionKey: string | Accessor) { const key = typeof sessionKey === "function" ? sessionKey : () => sessionKey diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 46c9c9154f..1c5edbf2b4 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1000,69 +1000,6 @@ export default function Layout(props: ParentProps) { } } - async function deleteSession(session: Session) { - const [store, setStore] = globalSync.child(session.directory) - const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived) - const index = sessions.findIndex((s) => s.id === session.id) - const nextSession = sessions[index + 1] ?? sessions[index - 1] - - const result = await globalSDK.client.session - .delete({ directory: session.directory, sessionID: session.id }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("session.delete.failed.title"), - description: errorMessage(err), - }) - return false - }) - - if (!result) return - - setStore( - produce((draft) => { - const removed = new Set([session.id]) - - const byParent = new Map() - for (const item of draft.session) { - const parentID = item.parentID - if (!parentID) continue - const existing = byParent.get(parentID) - if (existing) { - existing.push(item.id) - continue - } - byParent.set(parentID, [item.id]) - } - - const stack = [session.id] - while (stack.length) { - const parentID = stack.pop() - if (!parentID) continue - - const children = byParent.get(parentID) - if (!children) continue - - for (const child of children) { - if (removed.has(child)) continue - removed.add(child) - stack.push(child) - } - } - - draft.session = draft.session.filter((s) => !removed.has(s.id)) - }), - ) - - if (session.id === params.id) { - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - } else { - navigate(`/${params.dir}/session`) - } - } - } - command.register(() => { const commands: CommandOption[] = [ { @@ -1316,15 +1253,6 @@ export default function Layout(props: ParentProps) { globalSync.project.meta(project.worktree, { name }) } - async function renameSession(session: Session, next: string) { - if (next === session.title) return - await globalSDK.client.session.update({ - directory: session.directory, - sessionID: session.id, - title: next, - }) - } - const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) if (current === next) return @@ -1475,33 +1403,6 @@ export default function Layout(props: ParentProps) { }) } - function DialogDeleteSession(props: { session: Session }) { - const handleDelete = async () => { - await deleteSession(props.session) - dialog.close() - } - - return ( - -
-
- - {language.t("session.delete.confirm", { name: props.session.title })} - -
-
- - -
-
-
- ) - } - function DialogDeleteWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ @@ -1855,10 +1756,6 @@ export default function Layout(props: ParentProps) { const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) - const [menu, setMenu] = createStore({ - open: false, - pendingRename: false, - }) const hoverPrefetch = { current: undefined as ReturnType | undefined } const cancelHoverPrefetch = () => { @@ -1885,7 +1782,7 @@ export default function Layout(props: ParentProps) { const item = ( - props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation - /> + + {props.session.title} + {(summary) => (
@@ -1972,7 +1864,10 @@ export default function Layout(props: ParentProps) { getLabel={messageLabel} onMessageSelect={(message) => { if (!isActive()) { - sessionStorage.setItem("opencode.pendingMessage", `${props.session.id}|${message.id}`) + layout.pendingMessage.set( + `${base64Encode(props.session.directory)}/${props.session.id}`, + message.id, + ) navigate(`${props.slug}/session/${props.session.id}`) return } @@ -1989,49 +1884,25 @@ export default function Layout(props: ParentProps) {
- setMenu("open", open)}> - - - - - { - if (!menu.pendingRename) return - event.preventDefault() - setMenu("pendingRename", false) - openEditor(`session:${props.session.id}`, props.session.title) - }} - > - { - setMenu("pendingRename", true) - setMenu("open", false) - }} - > - {language.t("common.rename")} - - archiveSession(props.session)}> - {language.t("common.archive")} - - - dialog.show(() => )}> - {language.t("common.delete")} - - - - + + { + event.preventDefault() + event.stopPropagation() + void archiveSession(props.session) + }} + /> +
) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index e8c61ee989..7ff4bebb4d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -16,13 +16,16 @@ import { createResizeObserver } from "@solid-primitives/resize-observer" import { Dynamic } from "solid-js/web" import { useLocal } from "@/context/local" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" -import { createStore } from "solid-js/store" +import { createStore, produce } from "solid-js/store" import { PromptInput } from "@/components/prompt-input" import { SessionContextUsage } from "@/components/session-context-usage" import { IconButton } from "@opencode-ai/ui/icon-button" import { Button } from "@opencode-ai/ui/button" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" +import { Dialog } from "@opencode-ai/ui/dialog" +import { InlineInput } from "@opencode-ai/ui/inline-input" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" @@ -73,10 +76,31 @@ import { same } from "@/utils/same" type DiffStyle = "unified" | "split" +type HandoffSession = { + prompt: string + files: Record +} + +const HANDOFF_MAX = 40 + const handoff = { - prompt: "", - terminals: [] as string[], - files: {} as Record, + session: new Map(), + terminal: new Map(), +} + +const touch = (map: Map, key: K, value: V) => { + map.delete(key) + map.set(key, value) + while (map.size > HANDOFF_MAX) { + const first = map.keys().next().value + if (first === undefined) return + map.delete(first) + } +} + +const setSessionHandoff = (key: string, patch: Partial) => { + const prev = handoff.session.get(key) ?? { prompt: "", files: {} } + touch(handoff.session, key, { ...prev, ...patch }) } interface SessionReviewTabProps { @@ -280,9 +304,47 @@ export default function Page() { .finally(() => setUi("responding", false)) } const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`) + const workspaceKey = createMemo(() => params.dir ?? "") + const workspaceTabs = createMemo(() => layout.tabs(workspaceKey)) const tabs = createMemo(() => layout.tabs(sessionKey)) const view = createMemo(() => layout.view(sessionKey)) + createEffect( + on( + () => params.id, + (id, prev) => { + if (!id) return + if (prev) return + + const pending = layout.handoff.tabs() + if (!pending) return + if (Date.now() - pending.at > 60_000) { + layout.handoff.clearTabs() + return + } + + if (pending.id !== id) return + layout.handoff.clearTabs() + if (pending.dir !== (params.dir ?? "")) return + + const from = workspaceTabs().tabs() + if (from.all.length === 0 && !from.active) return + + const current = tabs().tabs() + if (current.all.length > 0 || current.active) return + + const all = normalizeTabs(from.all) + const active = from.active ? normalizeTab(from.active) : undefined + tabs().setAll(all) + tabs().setActive(active && all.includes(active) ? active : all[0]) + + workspaceTabs().setAll([]) + workspaceTabs().setActive(undefined) + }, + { defer: true }, + ), + ) + if (import.meta.env.DEV) { createEffect( on( @@ -398,6 +460,213 @@ export default function Page() { if (!id) return false return sync.session.history.loading(id) }) + + const [title, setTitle] = createStore({ + draft: "", + editing: false, + saving: false, + menuOpen: false, + pendingRename: false, + }) + let titleRef: HTMLInputElement | undefined + + const errorMessage = (err: unknown) => { + if (err && typeof err === "object" && "data" in err) { + const data = (err as { data?: { message?: string } }).data + if (data?.message) return data.message + } + if (err instanceof Error) return err.message + return language.t("common.requestFailed") + } + + createEffect( + on( + () => params.id, + () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }), + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!params.id) return + setTitle({ editing: true, draft: info()?.title ?? "" }) + requestAnimationFrame(() => { + titleRef?.focus() + titleRef?.select() + }) + } + + const closeTitleEditor = () => { + if (title.saving) return + setTitle({ editing: false, saving: false }) + } + + const saveTitleEditor = async () => { + const sessionID = params.id + if (!sessionID) return + if (title.saving) return + + const next = title.draft.trim() + if (!next || next === (info()?.title ?? "")) { + setTitle({ editing: false, saving: false }) + return + } + + setTitle("saving", true) + await sdk.client.session + .update({ sessionID, title: next }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session[index].title = next + }), + ) + setTitle({ editing: false, saving: false }) + }) + .catch((err) => { + setTitle("saving", false) + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + async function archiveSession(sessionID: string) { + const session = sync.session.get(sessionID) + if (!session) return + + const sessions = sync.data.session ?? [] + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + await sdk.client.session + .update({ sessionID, time: { archived: Date.now() } }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session.splice(index, 1) + }), + ) + + if (params.id !== sessionID) return + if (session.parentID) { + navigate(`/${params.dir}/session/${session.parentID}`) + return + } + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + return + } + navigate(`/${params.dir}/session`) + }) + .catch((err) => { + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + + async function deleteSession(sessionID: string) { + const session = sync.session.get(sessionID) + if (!session) return false + + const sessions = (sync.data.session ?? []).filter((s) => !s.parentID && !s.time?.archived) + const index = sessions.findIndex((s) => s.id === sessionID) + const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) + + const result = await sdk.client.session + .delete({ sessionID }) + .then((x) => x.data) + .catch((err) => { + showToast({ + title: language.t("session.delete.failed.title"), + description: errorMessage(err), + }) + return false + }) + + if (!result) return false + + sync.set( + produce((draft) => { + const removed = new Set([sessionID]) + + const byParent = new Map() + for (const item of draft.session) { + const parentID = item.parentID + if (!parentID) continue + const existing = byParent.get(parentID) + if (existing) { + existing.push(item.id) + continue + } + byParent.set(parentID, [item.id]) + } + + const stack = [sessionID] + while (stack.length) { + const parentID = stack.pop() + if (!parentID) continue + + const children = byParent.get(parentID) + if (!children) continue + + for (const child of children) { + if (removed.has(child)) continue + removed.add(child) + stack.push(child) + } + } + + draft.session = draft.session.filter((s) => !removed.has(s.id)) + }), + ) + + if (params.id !== sessionID) return true + if (session.parentID) { + navigate(`/${params.dir}/session/${session.parentID}`) + return true + } + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + return true + } + navigate(`/${params.dir}/session`) + return true + } + + function DialogDeleteSession(props: { sessionID: string }) { + const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new")) + const handleDelete = async () => { + await deleteSession(props.sessionID) + dialog.close() + } + + return ( + +
+
+ + {language.t("session.delete.confirm", { name: title() })} + +
+
+ + +
+
+
+ ) + } + const emptyUserMessages: UserMessage[] = [] const userMessages = createMemo( () => messages().filter((m) => m.role === "user") as UserMessage[], @@ -545,8 +814,10 @@ export default function Page() { const hasScrollGesture = () => Date.now() - ui.scrollGesture < scrollGestureWindowMs createEffect(() => { - if (!params.id) return - sync.session.sync(params.id) + sdk.directory + const id = params.id + if (!id) return + sync.session.sync(id) }) createEffect(() => { @@ -614,10 +885,22 @@ export default function Page() { createEffect( on( - () => params.id, + sessionKey, () => { setStore("messageId", undefined) setStore("expanded", {}) + setUi("autoCreated", false) + }, + { defer: true }, + ), + ) + + createEffect( + on( + () => params.dir, + (dir) => { + if (!dir) return + setStore("newSessionWorktree", "main") }, { defer: true }, ), @@ -1125,12 +1408,15 @@ export default function Page() { activeDiff: undefined as string | undefined, }) - const reviewScroll = () => tree.reviewScroll - const setReviewScroll = (value: HTMLDivElement | undefined) => setTree("reviewScroll", value) - const pendingDiff = () => tree.pendingDiff - const setPendingDiff = (value: string | undefined) => setTree("pendingDiff", value) - const activeDiff = () => tree.activeDiff - const setActiveDiff = (value: string | undefined) => setTree("activeDiff", value) + createEffect( + on( + sessionKey, + () => { + setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined }) + }, + { defer: true }, + ), + ) const showAllFiles = () => { if (fileTreeTab() !== "changes") return @@ -1151,8 +1437,8 @@ export default function Page() { view={view} diffStyle={layout.review.diffStyle()} onDiffStyleChange={layout.review.setDiffStyle} - onScrollRef={setReviewScroll} - focusedFile={activeDiff()} + onScrollRef={(el) => setTree("reviewScroll", el)} + focusedFile={tree.activeDiff} onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })} comments={comments.all()} focusedComment={comments.focus()} @@ -1202,7 +1488,7 @@ export default function Page() { } const reviewDiffTop = (path: string) => { - const root = reviewScroll() + const root = tree.reviewScroll if (!root) return const id = reviewDiffId(path) @@ -1218,7 +1504,7 @@ export default function Page() { } const scrollToReviewDiff = (path: string) => { - const root = reviewScroll() + const root = tree.reviewScroll if (!root) return false const top = reviewDiffTop(path) @@ -1232,24 +1518,23 @@ export default function Page() { const focusReviewDiff = (path: string) => { const current = view().review.open() ?? [] if (!current.includes(path)) view().review.setOpen([...current, path]) - setActiveDiff(path) - setPendingDiff(path) + setTree({ activeDiff: path, pendingDiff: path }) } createEffect(() => { - const pending = pendingDiff() + const pending = tree.pendingDiff if (!pending) return - if (!reviewScroll()) return + if (!tree.reviewScroll) return if (!diffsReady()) return const attempt = (count: number) => { - if (pendingDiff() !== pending) return + if (tree.pendingDiff !== pending) return if (count > 60) { - setPendingDiff(undefined) + setTree("pendingDiff", undefined) return } - const root = reviewScroll() + const root = tree.reviewScroll if (!root) { requestAnimationFrame(() => attempt(count + 1)) return @@ -1267,7 +1552,7 @@ export default function Page() { } if (Math.abs(root.scrollTop - top) <= 1) { - setPendingDiff(undefined) + setTree("pendingDiff", undefined) return } @@ -1310,13 +1595,17 @@ export default function Page() { void sync.session.diff(id) }) + let treeDir: string | undefined createEffect(() => { + const dir = sdk.directory if (!isDesktop()) return if (!layout.fileTree.opened()) return if (sync.status === "loading") return fileTreeTab() - void file.tree.list("") + const refresh = treeDir !== dir + treeDir = dir + void (refresh ? file.tree.refresh("") : file.tree.list("")) }) const autoScroll = createAutoScroll({ @@ -1351,6 +1640,18 @@ export default function Page() { let scrollSpyFrame: number | undefined let scrollSpyTarget: HTMLDivElement | undefined + createEffect( + on( + sessionKey, + () => { + if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame) + scrollSpyFrame = undefined + scrollSpyTarget = undefined + }, + { defer: true }, + ), + ) + const anchor = (id: string) => `message-${id}` const setScrollRef = (el: HTMLDivElement | undefined) => { @@ -1465,20 +1766,14 @@ export default function Page() { window.history.replaceState(null, "", `#${anchor(id)}`) } - createEffect(() => { - const sessionID = params.id - if (!sessionID) return - const raw = sessionStorage.getItem("opencode.pendingMessage") - if (!raw) return - const parts = raw.split("|") - const pendingSessionID = parts[0] - const messageID = parts[1] - if (!pendingSessionID || !messageID) return - if (pendingSessionID !== sessionID) return - - sessionStorage.removeItem("opencode.pendingMessage") - setUi("pendingMessage", messageID) - }) + createEffect( + on(sessionKey, (key) => { + if (!params.id) return + const messageID = layout.pendingMessage.consume(key) + if (!messageID) return + setUi("pendingMessage", messageID) + }), + ) const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = scroller @@ -1692,7 +1987,7 @@ export default function Page() { createEffect(() => { if (!prompt.ready()) return - handoff.prompt = previewPrompt() + setSessionHandoff(sessionKey(), { prompt: previewPrompt() }) }) createEffect(() => { @@ -1712,20 +2007,22 @@ export default function Page() { return language.t("terminal.title") } - handoff.terminals = terminal.all().map(label) + touch(handoff.terminal, params.dir!, terminal.all().map(label)) }) createEffect(() => { if (!file.ready()) return - handoff.files = Object.fromEntries( - tabs() - .all() - .flatMap((tab) => { - const path = file.pathFromTab(tab) - if (!path) return [] - return [[path, file.selectedLines(path) ?? null] as const] - }), - ) + setSessionHandoff(sessionKey(), { + files: Object.fromEntries( + tabs() + .all() + .flatMap((tab) => { + const path = file.pathFromTab(tab) + if (!path) return [] + return [[path, file.selectedLines(path) ?? null] as const] + }), + ), + }) }) onCleanup(() => { @@ -1801,7 +2098,7 @@ export default function Page() { diffs={diffs} view={view} diffStyle="unified" - focusedFile={activeDiff()} + focusedFile={tree.activeDiff} onLineComment={(comment) => addCommentToContext({ ...comment, origin: "review" })} comments={comments.all()} focusedComment={comments.focus()} @@ -1954,20 +2251,108 @@ export default function Page() { centered(), }} > -
- - { - navigate(`/${params.dir}/session/${info()?.parentID}`) - }} - aria-label={language.t("common.goBack")} - /> - - -

{info()?.title}

+
+
+ + { + navigate(`/${params.dir}/session/${info()?.parentID}`) + }} + aria-label={language.t("common.goBack")} + /> + + + + {info()?.title} + + } + > + { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-16-medium text-text-strong grow-1 min-w-0" + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={() => closeTitleEditor()} + /> + + +
+ + {(id) => ( +
+ setTitle("menuOpen", open)} + > + + + + + { + if (!title.pendingRename) return + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + }} + > + { + setTitle({ pendingRename: true, menuOpen: false }) + }} + > + + {language.t("common.rename")} + + + void archiveSession(id())}> + + {language.t("common.archive")} + + + + dialog.show(() => )} + > + + {language.t("common.delete")} + + + + + +
+ )}
@@ -2147,7 +2532,7 @@ export default function Page() { when={prompt.ready()} fallback={
- {handoff.prompt || language.t("prompt.loading")} + {handoff.session.get(sessionKey())?.prompt || language.t("prompt.loading")}
} > @@ -2398,7 +2783,7 @@ export default function Page() { const p = path() if (!p) return null if (file.ready()) return file.selectedLines(p) ?? null - return handoff.files[p] ?? null + return handoff.session.get(sessionKey())?.files[p] ?? null }) let wrap: HTMLDivElement | undefined @@ -2892,7 +3277,7 @@ export default function Page() { allowed={diffFiles()} kinds={kinds()} draggable={false} - active={activeDiff()} + active={tree.activeDiff} onFileClick={(node) => focusReviewDiff(node.path)} />
@@ -2952,7 +3337,7 @@ export default function Page() { fallback={
- + {(title) => (
{title}