Compare commits

...
Author SHA1 Message Date
Shoubhit Dash 5c1e7ca00f feat(app): show last turn changes in review panel 2026-09-27 15:06:22 +05:30
3 changed files with 68 additions and 30 deletions
@@ -39,6 +39,30 @@ test("restores review mode and selected file per session", async ({ page }) => {
await expectSelectedFile(page, "gamma.ts")
})
test("shows and restores last turn changes from the session diff", async ({ page }) => {
await setup(page)
await page.route(`**/api/session/${sessionA}/diff**`, (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: [diff("src/delta.ts")] }),
}),
)
await page.goto(sessionHref(sessionA))
await expectSessionTitle(page, titleA)
await page.getByRole("button", { name: "Toggle review" }).click()
await page.getByRole("button", { name: "Git changes" }).click()
await page.getByRole("option", { name: "Last turn changes" }).click()
await expect(page.getByRole("button", { name: "Last turn changes" })).toBeVisible()
await expectSelectedFile(page, "delta.ts")
await page.reload()
await expectSessionTitle(page, titleA)
await expect(page.getByRole("button", { name: "Last turn changes" })).toBeVisible()
await expectSelectedFile(page, "delta.ts")
})
for (const tab of ["Context", "Open file", "README.md"]) {
test(`restores the selected ${tab} pane tab after switching sessions and reloading`, async ({ page }) => {
await setup(page)
+42 -26
View File
@@ -21,7 +21,6 @@ import { reviewDiffDirectory, reviewDiffNeedsLoad, reviewRootDirectory } from ".
import type { DiffStyle } from "./review-tab"
export type ChangeMode = "git" | "branch" | "turn"
type VcsMode = "git" | "branch"
export function createSessionReview(input: {
session: SessionModel
@@ -64,15 +63,13 @@ export function createSessionReview(input: {
) {
list.push("branch")
}
// Turn diffs compare session snapshots, which the server only captures in Git projects.
if (project?.vcs === "git" && input.session.identity.sessionID()) list.push("turn")
return list
})
const mobileChanges = createMemo(
() => !input.session.isDesktop() && !input.screen.terminal.open() && state.mobileTab === "changes",
)
const vcsMode = createMemo<VcsMode | undefined>(() => {
const value = mode()
return value === "git" || value === "branch" ? value : undefined
})
const vcsKey = createMemo(
() =>
[
@@ -90,22 +87,28 @@ export function createSessionReview(input: {
(input.session.tabs.activeTab() === "review" || !!input.session.tabs.activeFileTab()))
: mobileChanges(),
)
const vcsQuery = createQuery(() => {
const value = vcsMode()
const turnKey = createMemo(() => [server.scope, "session-turn", input.session.identity.sessionID()] as const)
const diffQuery = createQuery(() => {
const value = mode()
const sessionID = input.session.identity.sessionID()
const turn = value === "turn"
return {
queryKey: [...vcsKey(), value] as const,
queryKey: turn ? turnKey() : ([...vcsKey(), value] as const),
enabled: server.connection.status() === "connected" && wantsReview() && !!input.session.project()?.vcs,
refetchOnMount: "always" as const,
refetchOnWindowFocus: true,
queryFn: value
? () =>
// A finished turn's diff is immutable and expensive, so only the idle transition refreshes it.
refetchOnWindowFocus: !turn,
queryFn: turn
? sessionID
? () => server.api.session.diff({ sessionID })
: skipToken
: () =>
server.api.vcs
.diff({
location: { directory: location().directory },
mode: value === "git" ? "working" : value,
})
.then((result) => result.data)
: skipToken,
.then((result) => result.data),
}
})
const detailsQuery = createQuery(() => ({
@@ -136,16 +139,13 @@ export function createSessionReview(input: {
on(
() => input.screen.review.open() || mobileChanges(),
(open, previous) => {
if (!open || previous || !input.screen.files.open() || vcsQuery.isFetching) return
if (!open || previous || !input.screen.files.open() || diffQuery.isFetching) return
refresh()
},
{ defer: true },
),
)
const diffs = () => {
if (mode() === "git" || mode() === "branch") return vcsQuery.isFetched ? (vcsQuery.data ?? []) : []
return []
}
const diffs = () => (diffQuery.isFetched ? (diffQuery.data ?? []) : [])
const activeFile = () => {
const list = diffs()
const selected = selectedFile()
@@ -155,17 +155,12 @@ export function createSessionReview(input: {
const count = () => diffs().length
const hasChanges = () => count() > 0
const ready = () => {
// A project without VCS never enables vcsQuery, so its status stays "pending" forever.
// A project without VCS never enables diffQuery, so its status stays "pending" forever.
const project = input.session.project()
if (project && !project.vcs) return true
if (mode() === "git" || mode() === "branch") return !vcsQuery.isPending
return true
return !diffQuery.isPending
}
const loadDiff = async (path: string, version?: number): Promise<FileDiffInfo | undefined> => {
const value = vcsMode()
if (!value) return undefined
const root = reviewRootDirectory(input.session.project()?.worktree ?? location().directory)
const directory = reviewDiffDirectory(root, path)
const source = diffs().find((diff) => diff.file === path)
const valid = (diff: FileDiffInfo | undefined): FileDiffInfo | undefined => {
if (!diff || !source) return undefined
@@ -173,6 +168,26 @@ export function createSessionReview(input: {
if (reviewDiffNeedsLoad(diff)) return undefined
return diff
}
const value = mode()
// Full-file patches past the server's output budget come back empty; bounded context usually fits.
if (value === "turn") {
const sessionID = input.session.identity.sessionID()
if (!sessionID) return undefined
return queryClient
.fetchQuery({
queryKey: [...turnKey(), "bounded", version] as const,
staleTime: Number.POSITIVE_INFINITY,
retry: 2,
queryFn: () => server.api.session.diff({ sessionID, context: 3 }),
})
.then((result) => valid(result.find((diff) => diff.file === path)))
.catch((error) => {
console.debug("[session-review] failed to load bounded turn diff", { path, error })
return undefined
})
}
const root = reviewRootDirectory(input.session.project()?.worktree ?? location().directory)
const directory = reviewDiffDirectory(root, path)
const request = (scope: string, context?: number) =>
queryClient
.fetchQuery({
@@ -357,6 +372,7 @@ export function createSessionReview(input: {
(next, previous) => {
if (next !== "idle" || previous === undefined || previous === "idle") return
refresh()
void queryClient.invalidateQueries({ queryKey: turnKey() })
},
{ defer: true },
),
@@ -402,7 +418,7 @@ export function createSessionReview(input: {
open: () => state.detailsOpen,
setOpen: (open: boolean) => setState("detailsOpen", open),
},
diffVersion: () => vcsQuery.dataUpdatedAt,
diffVersion: () => diffQuery.dataUpdatedAt,
diffStyle: {
current: layout.review.diffStyle,
set: (style: DiffStyle) => layout.review.setDiffStyle(style),
+2 -4
View File
@@ -252,7 +252,6 @@ function ReviewTitle(props: { review: SessionReviewModel }) {
function ReviewEmpty(props: { review: SessionReviewModel; loadingClass: string }) {
const language = useLanguage()
const loading = () => (props.review.mode() === "git" || props.review.mode() === "branch") && !props.review.ready()
const noGit = () => props.review.noGit()
const text = () => {
if (props.review.mode() === "git") return language.t("session.review.noUncommittedChanges")
@@ -261,7 +260,7 @@ function ReviewEmpty(props: { review: SessionReviewModel; loadingClass: string }
}
return (
<Switch>
<Match when={loading()}>
<Match when={!props.review.ready()}>
<div class={props.loadingClass}>{language.t("session.review.loadingChanges")}</div>
</Match>
<Match when={noGit()}>
@@ -285,11 +284,10 @@ function ReviewEmpty(props: { review: SessionReviewModel; loadingClass: string }
function ReviewPanelEmpty(props: { review: SessionReviewModel }) {
const language = useLanguage()
const loading = () => (props.review.mode() === "git" || props.review.mode() === "branch") && !props.review.ready()
const noGit = () => props.review.noGit()
return (
<Switch>
<Match when={loading()}>
<Match when={!props.review.ready()}>
<div class="px-6 py-4 text-text-weak">{language.t("session.review.loadingChanges")}</div>
</Match>
<Match when={noGit()}>