summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-04-03 20:24:57 +0530
committerGitHub <[email protected]>2026-04-03 09:54:57 -0500
commit35350b1d25a56665cf065eba68929fc00617fdd2 (patch)
tree91bf53b5d87ff9532ebf0a779c4dbe7bc99148f3 /packages/app/src
parent263dcf75b548810a149f08ea5e32e0f6754128d5 (diff)
downloadopencode-35350b1d25a56665cf065eba68929fc00617fdd2.tar.gz
opencode-35350b1d25a56665cf065eba68929fc00617fdd2.zip
feat: restore git-backed review modes (#20845)
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/context/global-sync/bootstrap.ts2
-rw-r--r--packages/app/src/context/global-sync/event-reducer.test.ts10
-rw-r--r--packages/app/src/context/global-sync/event-reducer.ts4
-rw-r--r--packages/app/src/i18n/en.ts2
-rw-r--r--packages/app/src/pages/session.tsx272
-rw-r--r--packages/app/src/pages/session/session-side-panel.tsx58
-rw-r--r--packages/app/src/pages/session/use-session-commands.tsx6
7 files changed, 266 insertions, 88 deletions
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index cf104ad97..7edd5a1ce 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -248,7 +248,7 @@ export async function bootstrapDirectory(input: {
input.sdk.vcs.get().then((x) => {
const next = x.data ?? input.store.vcs
input.setStore("vcs", next)
- if (next?.branch) input.vcsCache.setStore("value", next)
+ if (next) input.vcsCache.setStore("value", next)
}),
),
() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts
index cf2da135c..892129788 100644
--- a/packages/app/src/context/global-sync/event-reducer.test.ts
+++ b/packages/app/src/context/global-sync/event-reducer.test.ts
@@ -494,8 +494,10 @@ describe("applyDirectoryEvent", () => {
})
test("updates vcs branch in store and cache", () => {
- const [store, setStore] = createStore(baseState())
- const [cacheStore, setCacheStore] = createStore({ value: undefined as State["vcs"] })
+ const [store, setStore] = createStore(baseState({ vcs: { branch: "main", default_branch: "main" } }))
+ const [cacheStore, setCacheStore] = createStore({
+ value: { branch: "main", default_branch: "main" } as State["vcs"],
+ })
applyDirectoryEvent({
event: { type: "vcs.branch.updated", properties: { branch: "feature/test" } },
@@ -511,8 +513,8 @@ describe("applyDirectoryEvent", () => {
},
})
- expect(store.vcs).toEqual({ branch: "feature/test" })
- expect(cacheStore.value).toEqual({ branch: "feature/test" })
+ expect(store.vcs).toEqual({ branch: "feature/test", default_branch: "main" })
+ expect(cacheStore.value).toEqual({ branch: "feature/test", default_branch: "main" })
})
test("routes disposal and lsp events to side-effect handlers", () => {
diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts
index 5d8b7c4e3..4af636553 100644
--- a/packages/app/src/context/global-sync/event-reducer.ts
+++ b/packages/app/src/context/global-sync/event-reducer.ts
@@ -271,9 +271,9 @@ export function applyDirectoryEvent(input: {
break
}
case "vcs.branch.updated": {
- const props = event.properties as { branch: string }
+ const props = event.properties as { branch?: string }
if (input.store.vcs?.branch === props.branch) break
- const next = { branch: props.branch }
+ const next = { ...input.store.vcs, branch: props.branch }
input.setStore("vcs", next)
if (input.vcsCache) input.vcsCache.setStore("value", next)
break
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 39317b8d6..ace0efeb8 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -535,6 +535,8 @@ export const dict = {
"session.review.noVcs.createGit.action": "Create Git repository",
"session.review.noSnapshot": "Snapshot tracking is disabled in config, so session changes are unavailable",
"session.review.noChanges": "No changes",
+ "session.review.noUncommittedChanges": "No uncommitted changes yet",
+ "session.review.noBranchChanges": "No branch changes yet",
"session.files.selectToOpen": "Select a file to open",
"session.files.all": "All files",
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index ae895adbe..a81df9dd2 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1,4 +1,4 @@
-import type { Project, UserMessage } from "@opencode-ai/sdk/v2"
+import type { FileDiff, Project, UserMessage } from "@opencode-ai/sdk/v2"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useMutation } from "@tanstack/solid-query"
import {
@@ -68,6 +68,9 @@ type FollowupItem = FollowupDraft & { id: string }
type FollowupEdit = Pick<FollowupItem, "id" | "prompt" | "context">
const emptyFollowups: FollowupItem[] = []
+type ChangeMode = "git" | "branch" | "session" | "turn"
+type VcsMode = "git" | "branch"
+
type SessionHistoryWindowInput = {
sessionID: () => string | undefined
messagesReady: () => boolean
@@ -427,15 +430,16 @@ export default function Page() {
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
- const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
- const hasReview = createMemo(() => reviewCount() > 0)
+ const sessionCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
+ const hasSessionReview = createMemo(() => sessionCount() > 0)
+ const canReview = createMemo(() => !!sync.project)
const reviewTab = createMemo(() => isDesktop())
const tabState = createSessionTabs({
tabs,
pathFromTab: file.pathFromTab,
normalizeTab,
review: reviewTab,
- hasReview,
+ hasReview: canReview,
})
const contextOpen = tabState.contextOpen
const openedTabs = tabState.openedTabs
@@ -458,6 +462,12 @@ export default function Page() {
if (!id) return false
return sync.session.history.loading(id)
})
+ const diffsReady = createMemo(() => {
+ const id = params.id
+ if (!id) return true
+ if (!hasSessionReview()) return true
+ return sync.data.session_diff[id] !== undefined
+ })
const userMessages = createMemo(
() => messages().filter((m) => m.role === "user") as UserMessage[],
@@ -511,11 +521,22 @@ export default function Page() {
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
mobileTab: "session" as "session" | "changes",
- changes: "session" as "session" | "turn",
+ changes: "git" as ChangeMode,
newSessionWorktree: "main",
deferRender: false,
})
+ const [vcs, setVcs] = createStore({
+ diff: {
+ git: [] as FileDiff[],
+ branch: [] as FileDiff[],
+ },
+ ready: {
+ git: false,
+ branch: false,
+ },
+ })
+
const [followup, setFollowup] = persisted(
Persist.workspace(sdk.directory, "followup", ["followup.v1"]),
createStore<{
@@ -549,6 +570,68 @@ export default function Page() {
let todoTimer: number | undefined
let diffFrame: number | undefined
let diffTimer: number | undefined
+ const vcsTask = new Map<VcsMode, Promise<void>>()
+ const vcsRun = new Map<VcsMode, number>()
+
+ const bumpVcs = (mode: VcsMode) => {
+ const next = (vcsRun.get(mode) ?? 0) + 1
+ vcsRun.set(mode, next)
+ return next
+ }
+
+ const resetVcs = (mode?: VcsMode) => {
+ const list = mode ? [mode] : (["git", "branch"] as const)
+ list.forEach((item) => {
+ bumpVcs(item)
+ vcsTask.delete(item)
+ setVcs("diff", item, [])
+ setVcs("ready", item, false)
+ })
+ }
+
+ const loadVcs = (mode: VcsMode, force = false) => {
+ if (sync.project?.vcs !== "git") return Promise.resolve()
+ if (!force && vcs.ready[mode]) return Promise.resolve()
+
+ if (force) {
+ if (vcsTask.has(mode)) bumpVcs(mode)
+ vcsTask.delete(mode)
+ setVcs("ready", mode, false)
+ }
+
+ const current = vcsTask.get(mode)
+ if (current) return current
+
+ const run = bumpVcs(mode)
+
+ const task = sdk.client.vcs
+ .diff({ mode })
+ .then((result) => {
+ if (vcsRun.get(mode) !== run) return
+ setVcs("diff", mode, result.data ?? [])
+ setVcs("ready", mode, true)
+ })
+ .catch((error) => {
+ if (vcsRun.get(mode) !== run) return
+ console.debug("[session-review] failed to load vcs diff", { mode, error })
+ setVcs("diff", mode, [])
+ setVcs("ready", mode, true)
+ })
+ .finally(() => {
+ if (vcsTask.get(mode) === task) vcsTask.delete(mode)
+ })
+
+ vcsTask.set(mode, task)
+ return task
+ }
+
+ const refreshVcs = () => {
+ resetVcs()
+ const mode = untrack(vcsMode)
+ if (!mode) return
+ if (!untrack(wantsReview)) return
+ void loadVcs(mode, true)
+ }
createComputed((prev) => {
const open = desktopReviewOpen()
@@ -564,7 +647,42 @@ export default function Page() {
}, desktopReviewOpen())
const turnDiffs = createMemo(() => lastUserMessage()?.summary?.diffs ?? [])
- const reviewDiffs = createMemo(() => (store.changes === "session" ? diffs() : turnDiffs()))
+ const changesOptions = createMemo<ChangeMode[]>(() => {
+ const list: ChangeMode[] = []
+ if (sync.project?.vcs === "git") list.push("git")
+ if (
+ sync.project?.vcs === "git" &&
+ sync.data.vcs?.branch &&
+ sync.data.vcs?.default_branch &&
+ sync.data.vcs.branch !== sync.data.vcs.default_branch
+ ) {
+ list.push("branch")
+ }
+ list.push("session", "turn")
+ return list
+ })
+ const vcsMode = createMemo<VcsMode | undefined>(() => {
+ if (store.changes === "git" || store.changes === "branch") return store.changes
+ })
+ const reviewDiffs = createMemo(() => {
+ if (store.changes === "git") return vcs.diff.git
+ if (store.changes === "branch") return vcs.diff.branch
+ if (store.changes === "session") return diffs()
+ return turnDiffs()
+ })
+ const reviewCount = createMemo(() => {
+ if (store.changes === "git") return vcs.diff.git.length
+ if (store.changes === "branch") return vcs.diff.branch.length
+ if (store.changes === "session") return sessionCount()
+ return turnDiffs().length
+ })
+ const hasReview = createMemo(() => reviewCount() > 0)
+ const reviewReady = createMemo(() => {
+ if (store.changes === "git") return vcs.ready.git
+ if (store.changes === "branch") return vcs.ready.branch
+ if (store.changes === "session") return !hasSessionReview() || diffsReady()
+ return true
+ })
const newSessionWorktree = createMemo(() => {
if (store.newSessionWorktree === "create") return "create"
@@ -630,13 +748,7 @@ export default function Page() {
scrollToMessage(msgs[targetIndex], "auto")
}
- const diffsReady = createMemo(() => {
- const id = params.id
- if (!id) return true
- if (!hasReview()) return true
- return sync.data.session_diff[id] !== undefined
- })
- const reviewEmptyKey = createMemo(() => {
+ const sessionEmptyKey = createMemo(() => {
const project = sync.project
if (project && !project.vcs) return "session.review.noVcs"
if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
@@ -790,7 +902,7 @@ export default function Page() {
sessionKey,
() => {
setStore("messageId", undefined)
- setStore("changes", "session")
+ setStore("changes", "git")
setUi("pendingMessage", undefined)
},
{ defer: true },
@@ -799,6 +911,39 @@ export default function Page() {
createEffect(
on(
+ () => sdk.directory,
+ () => {
+ resetVcs()
+ },
+ { defer: true },
+ ),
+ )
+
+ createEffect(
+ on(
+ () => [sync.data.vcs?.branch, sync.data.vcs?.default_branch] as const,
+ (next, prev) => {
+ if (prev === undefined || same(next, prev)) return
+ refreshVcs()
+ },
+ { defer: true },
+ ),
+ )
+
+ const stopVcs = sdk.event.listen((evt) => {
+ if (evt.details.type !== "file.watcher.updated") return
+ const props =
+ typeof evt.details.properties === "object" && evt.details.properties
+ ? (evt.details.properties as Record<string, unknown>)
+ : undefined
+ const file = typeof props?.file === "string" ? props.file : undefined
+ if (!file || file.startsWith(".git/")) return
+ refreshVcs()
+ })
+ onCleanup(stopVcs)
+
+ createEffect(
+ on(
() => params.dir,
(dir) => {
if (!dir) return
@@ -919,6 +1064,40 @@ export default function Page() {
}
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
+ const wantsReview = createMemo(() =>
+ isDesktop()
+ ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
+ : store.mobileTab === "changes",
+ )
+
+ createEffect(() => {
+ const list = changesOptions()
+ if (list.includes(store.changes)) return
+ const next = list[0]
+ if (!next) return
+ setStore("changes", next)
+ })
+
+ createEffect(() => {
+ const mode = vcsMode()
+ if (!mode) return
+ if (!wantsReview()) return
+ void loadVcs(mode)
+ })
+
+ createEffect(
+ on(
+ () => sync.data.session_status[params.id ?? ""]?.type,
+ (next, prev) => {
+ const mode = vcsMode()
+ if (!mode) return
+ if (!wantsReview()) return
+ if (next !== "idle" || prev === undefined || prev === "idle") return
+ void loadVcs(mode, true)
+ },
+ { defer: true },
+ ),
+ )
const fileTreeTab = () => layout.fileTree.tab()
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
@@ -965,21 +1144,23 @@ export default function Page() {
loadFile: file.load,
})
- const changesOptions = ["session", "turn"] as const
- const changesOptionsList = [...changesOptions]
-
const changesTitle = () => {
- if (!hasReview()) {
+ if (!canReview()) {
return null
}
+ const label = (option: ChangeMode) => {
+ if (option === "git") return language.t("ui.sessionReview.title.git")
+ if (option === "branch") return language.t("ui.sessionReview.title.branch")
+ if (option === "session") return language.t("ui.sessionReview.title")
+ return language.t("ui.sessionReview.title.lastTurn")
+ }
+
return (
<Select
- options={changesOptionsList}
+ options={changesOptions()}
current={store.changes}
- label={(option) =>
- option === "session" ? language.t("ui.sessionReview.title") : language.t("ui.sessionReview.title.lastTurn")
- }
+ label={label}
onSelect={(option) => option && setStore("changes", option)}
variant="ghost"
size="small"
@@ -988,20 +1169,34 @@ export default function Page() {
)
}
- const emptyTurn = () => (
+ const empty = (text: string) => (
<div class="h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6">
- <div class="text-14-regular text-text-weak max-w-56">{language.t("session.review.noChanges")}</div>
+ <div class="text-14-regular text-text-weak max-w-56">{text}</div>
</div>
)
+ const reviewEmptyText = createMemo(() => {
+ if (store.changes === "git") return language.t("session.review.noUncommittedChanges")
+ if (store.changes === "branch") return language.t("session.review.noBranchChanges")
+ if (store.changes === "turn") return language.t("session.review.noChanges")
+ return language.t(sessionEmptyKey())
+ })
+
const reviewEmpty = (input: { loadingClass: string; emptyClass: string }) => {
- if (store.changes === "turn") return emptyTurn()
+ if (store.changes === "git" || store.changes === "branch") {
+ if (!reviewReady()) return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
+ return empty(reviewEmptyText())
+ }
- if (hasReview() && !diffsReady()) {
+ if (store.changes === "turn") {
+ return empty(reviewEmptyText())
+ }
+
+ if (hasSessionReview() && !diffsReady()) {
return <div class={input.loadingClass}>{language.t("session.review.loadingChanges")}</div>
}
- if (reviewEmptyKey() === "session.review.noVcs") {
+ if (sessionEmptyKey() === "session.review.noVcs") {
return (
<div class={input.emptyClass}>
<div class="flex flex-col gap-3">
@@ -1021,7 +1216,7 @@ export default function Page() {
return (
<div class={input.emptyClass}>
- <div class="text-14-regular text-text-weak max-w-56">{language.t(reviewEmptyKey())}</div>
+ <div class="text-14-regular text-text-weak max-w-56">{reviewEmptyText()}</div>
</div>
)
}
@@ -1128,7 +1323,7 @@ export default function Page() {
const pending = tree.pendingDiff
if (!pending) return
if (!tree.reviewScroll) return
- if (!diffsReady()) return
+ if (!reviewReady()) return
const attempt = (count: number) => {
if (tree.pendingDiff !== pending) return
@@ -1169,10 +1364,7 @@ export default function Page() {
const id = params.id
if (!id) return
- const wants = isDesktop()
- ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
- : store.mobileTab === "changes"
- if (!wants) return
+ if (!wantsReview()) return
if (sync.data.session_diff[id] !== undefined) return
if (sync.status === "loading") return
@@ -1181,13 +1373,7 @@ export default function Page() {
createEffect(
on(
- () =>
- [
- sessionKey(),
- isDesktop()
- ? desktopFileTreeOpen() || (desktopReviewOpen() && activeTab() === "review")
- : store.mobileTab === "changes",
- ] as const,
+ () => [sessionKey(), wantsReview()] as const,
([key, wants]) => {
if (diffFrame !== undefined) cancelAnimationFrame(diffFrame)
if (diffTimer !== undefined) window.clearTimeout(diffTimer)
@@ -1867,6 +2053,12 @@ export default function Page() {
</div>
<SessionSidePanel
+ canReview={canReview}
+ diffs={reviewDiffs}
+ diffsReady={reviewReady}
+ empty={reviewEmptyText}
+ hasReview={hasReview}
+ reviewCount={reviewCount}
reviewPanel={reviewPanel}
activeDiff={tree.activeDiff}
focusReviewDiff={focusReviewDiff}
diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx
index c07942627..86f932ea2 100644
--- a/packages/app/src/pages/session/session-side-panel.tsx
+++ b/packages/app/src/pages/session/session-side-panel.tsx
@@ -8,6 +8,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Mark } from "@opencode-ai/ui/logo"
import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
+import type { FileDiff } from "@opencode-ai/sdk/v2"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
import { useDialog } from "@opencode-ai/ui/context/dialog"
@@ -18,7 +19,6 @@ import { useCommand } from "@/context/command"
import { useFile, type SelectedLineRange } from "@/context/file"
import { useLanguage } from "@/context/language"
import { useLayout } from "@/context/layout"
-import { useSync } from "@/context/sync"
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
import { FileTabContent } from "@/pages/session/file-tabs"
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
@@ -26,6 +26,12 @@ import { setSessionHandoff } from "@/pages/session/handoff"
import { useSessionLayout } from "@/pages/session/session-layout"
export function SessionSidePanel(props: {
+ canReview: () => boolean
+ diffs: () => FileDiff[]
+ diffsReady: () => boolean
+ empty: () => string
+ hasReview: () => boolean
+ reviewCount: () => number
reviewPanel: () => JSX.Element
activeDiff?: string
focusReviewDiff: (path: string) => void
@@ -33,12 +39,11 @@ export function SessionSidePanel(props: {
size: Sizing
}) {
const layout = useLayout()
- const sync = useSync()
const file = useFile()
const language = useLanguage()
const command = useCommand()
const dialog = useDialog()
- const { params, sessionKey, tabs, view } = useSessionLayout()
+ const { sessionKey, tabs, view } = useSessionLayout()
const isDesktop = createMediaQuery("(min-width: 768px)")
@@ -53,24 +58,7 @@ export function SessionSidePanel(props: {
})
const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px"))
- const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
- const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
- const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
- const hasReview = createMemo(() => reviewCount() > 0)
- const diffsReady = createMemo(() => {
- const id = params.id
- if (!id) return true
- if (!hasReview()) return true
- return sync.data.session_diff[id] !== undefined
- })
-
- const reviewEmptyKey = createMemo(() => {
- if (sync.project && !sync.project.vcs) return "session.review.noVcs"
- if (sync.data.config.snapshot === false) return "session.review.noSnapshot"
- return "session.review.noChanges"
- })
-
- const diffFiles = createMemo(() => diffs().map((d) => d.file))
+ const diffFiles = createMemo(() => props.diffs().map((d) => d.file))
const kinds = createMemo(() => {
const merge = (a: "add" | "del" | "mix" | undefined, b: "add" | "del" | "mix") => {
if (!a) return b
@@ -81,7 +69,7 @@ export function SessionSidePanel(props: {
const normalize = (p: string) => p.replaceAll("\\\\", "/").replace(/\/+$/, "")
const out = new Map<string, "add" | "del" | "mix">()
- for (const diff of diffs()) {
+ for (const diff of props.diffs()) {
const file = normalize(diff.file)
const kind = diff.status === "added" ? "add" : diff.status === "deleted" ? "del" : "mix"
@@ -135,7 +123,7 @@ export function SessionSidePanel(props: {
pathFromTab: file.pathFromTab,
normalizeTab,
review: reviewTab,
- hasReview,
+ hasReview: props.canReview,
})
const contextOpen = tabState.contextOpen
const openedTabs = tabState.openedTabs
@@ -240,12 +228,12 @@ export function SessionSidePanel(props: {
onCleanup(stop)
}}
>
- <Show when={reviewTab()}>
+ <Show when={reviewTab() && props.canReview()}>
<Tabs.Trigger value="review">
<div class="flex items-center gap-1.5">
<div>{language.t("session.tab.review")}</div>
- <Show when={hasReview()}>
- <div>{reviewCount()}</div>
+ <Show when={props.hasReview()}>
+ <div>{props.reviewCount()}</div>
</Show>
</div>
</Tabs.Trigger>
@@ -304,7 +292,7 @@ export function SessionSidePanel(props: {
</Tabs.List>
</div>
- <Show when={reviewTab()}>
+ <Show when={reviewTab() && props.canReview()}>
<Tabs.Content value="review" class="flex flex-col h-full overflow-hidden contain-strict">
<Show when={activeTab() === "review"}>{props.reviewPanel()}</Show>
</Tabs.Content>
@@ -378,8 +366,10 @@ export function SessionSidePanel(props: {
>
<Tabs.List>
<Tabs.Trigger value="changes" class="flex-1" classes={{ button: "w-full" }}>
- {reviewCount()}{" "}
- {language.t(reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other")}
+ {props.reviewCount()}{" "}
+ {language.t(
+ props.reviewCount() === 1 ? "session.review.change.one" : "session.review.change.other",
+ )}
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
{language.t("session.files.all")}
@@ -387,9 +377,9 @@ export function SessionSidePanel(props: {
</Tabs.List>
<Tabs.Content value="changes" class="bg-background-stronger px-3 py-0">
<Switch>
- <Match when={hasReview()}>
+ <Match when={props.hasReview() || !props.diffsReady()}>
<Show
- when={diffsReady()}
+ when={props.diffsReady()}
fallback={
<div class="px-2 py-2 text-12-regular text-text-weak">
{language.t("common.loading")}
@@ -408,11 +398,7 @@ export function SessionSidePanel(props: {
/>
</Show>
</Match>
- <Match when={true}>
- {empty(
- language.t(sync.project && !sync.project.vcs ? "session.review.noChanges" : reviewEmptyKey()),
- )}
- </Match>
+ <Match when={true}>{empty(props.empty())}</Match>
</Switch>
</Tabs.Content>
<Tabs.Content value="all" class="bg-background-stronger px-3 py-0">
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index 430fd46f8..239795373 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -52,11 +52,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
if (!id) return
return sync.session.get(id)
}
- const hasReview = () => {
- const id = params.id
- if (!id) return false
- return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0
- }
+ const hasReview = () => !!params.id
const normalizeTab = (tab: string) => {
if (!tab.startsWith("file://")) return tab
return file.tab(tab)