Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1969,8 +1969,8 @@ export default function Page() {
classes={{ button: compact ? "w-full !py-2" : "w-full" }}
onClick={() => setStore("mobileTab", "changes")}
>
{"Modified Files"}
</Tabs.Trigger>
{"Files Changed"}
</Tabs.Trigger>
</Tabs.List>
</Tabs>
)
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/pages/session/session-side-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ export function SessionSidePanel(props: {
aria-controls={activeTab() === "review" ? reviewTabPanelID : undefined}
>
{props.hasReview()
? "Modified Files"
? "Files Changed"
: language.t("session.tab.review")}
</Tabs.Trigger>
</Show>
Expand Down Expand Up @@ -818,7 +818,7 @@ export function SessionSidePanel(props: {
</>
}
>
{"Modified Files"}
{"Files Changed"}
</Show>
</Tabs.Trigger>
<Tabs.Trigger value="all" class="flex-1" classes={{ button: "w-full" }}>
Expand Down
28 changes: 26 additions & 2 deletions packages/opencode/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ const layer: Layer.Layer<
// and the last step-finish snapshot hash (session-end ref)
let from: string | undefined
let lastStepFinish: string | undefined
// Collect all agent-touched files from PatchParts (absolute paths)
// Collect all agent-touched files from PatchParts and completed tool parts (absolute paths)
const agentFilesAbsolute = new Set<string>()

for (const msg of all) {
Expand All @@ -857,6 +857,14 @@ const layer: Layer.Layer<
if (part.type === "patch" && part.files) {
for (const file of part.files) agentFilesAbsolute.add(file)
}
// In-flight file tracking: also collect files from completed tool parts with filediff metadata
if (part.type === "tool") {
const toolPart = part as { tool?: string; state?: { status?: string; metadata?: Record<string, unknown> } }
if (toolPart.state?.status === "completed") {
const filediff = toolPart.state?.metadata?.filediff as { file?: string } | undefined
if (filediff?.file) agentFilesAbsolute.add(filediff.file)
}
}
}
}

Expand All @@ -878,12 +886,28 @@ const layer: Layer.Layer<
}

const allDiffs = yield* snapshot.diffFull(from, to).pipe(
Effect.catchCause(() => Effect.succeed([] as Snapshot.FileDiff[])),
Effect.catchCause((cause) =>
Effect.gen(function* () {
yield* Effect.logWarning("diffFull failed, falling through to per-file fallback", { cause })
return [] as Snapshot.FileDiff[]
}),
),
)
const filtered = allDiffs.filter(
(d: Snapshot.FileDiff) => d.file && agentFiles.has(d.file) && (d.additions ?? 0) + (d.deletions ?? 0) > 0,
)
if (filtered.length > 0) return filtered

// Fallback A: primary returned empty but from hash exists — per-file git show vs current disk
const perFileDiffs = yield* snapshot.diffFromDisk(from, [...agentFiles]).pipe(
Effect.catchCause((cause) =>
Effect.gen(function* () {
yield* Effect.logWarning("diffFromDisk failed, falling through to summary fallback", { cause })
return [] as Snapshot.FileDiff[]
}),
),
)
if (perFileDiffs.length > 0) return perFileDiffs
}
}

Expand Down
48 changes: 47 additions & 1 deletion packages/opencode/src/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface Interface {
readonly revert: (patches: Patch[]) => Effect.Effect<void>
readonly diff: (hash: string) => Effect.Effect<string>
readonly diffFull: (from: string, to: string) => Effect.Effect<FileDiff[]>
readonly diffFromDisk: (ref: string, files: string[]) => Effect.Effect<FileDiff[]>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/Snapshot") {}
Expand Down Expand Up @@ -758,14 +759,56 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
)
})

const diffFromDisk = Effect.fnUntraced(function* (ref: string, files: string[]) {
return yield* locked(
Effect.gen(function* () {
const result: FileDiff[] = []
const patchFn = (file: string, before: string, after: string) =>
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))

for (const file of files) {
const beforeResult = yield* git([...cfg, ...args(["show", `${ref}:${file}`])])
const before = beforeResult.code === 0 ? beforeResult.text : ""

const diskPath = path.join(state.worktree, file)
const after = yield* read(diskPath)

if (before === after) continue

// Compute actual line-level additions/deletions from the structured patch
const sp = structuredPatch(file, file, before, after, "", "")
let adds = 0
let dels = 0
for (const hunk of sp.hunks) {
for (const line of hunk.lines) {
if (line.startsWith("+")) adds++
else if (line.startsWith("-")) dels++
}
}

const status: "added" | "deleted" | "modified" = before === "" ? "added" : after === "" ? "deleted" : "modified"
result.push({
file,
patch: patchFn(file, before, after),
additions: adds,
deletions: dels,
status,
})
}

return result
}),
)
})

yield* cleanup().pipe(
Effect.catchCause((cause) => Effect.logError("cleanup loop failed", { cause: Cause.pretty(cause) })),
Effect.repeat(Schedule.spaced(Duration.hours(1))),
Effect.delay(Duration.minutes(1)),
Effect.forkScoped,
)

return { cleanup, track, patch, restore, revert, diff, diffFull }
return { cleanup, track, patch, restore, revert, diff, diffFull, diffFromDisk }
}),
)

Expand Down Expand Up @@ -794,6 +837,9 @@ const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Service | C
diffFull: Effect.fn("Snapshot.diffFull")(function* (from: string, to: string) {
return yield* InstanceState.useEffect(state, (s) => s.diffFull(from, to))
}),
diffFromDisk: Effect.fn("Snapshot.diffFromDisk")(function* (ref: string, files: string[]) {
return yield* InstanceState.useEffect(state, (s) => s.diffFromDisk(ref, files))
}),
})
}),
)
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/src/feature-plugins/sidebar/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
<text fg={theme().text}>{open() ? "▼" : "▶"}</text>
</Show>
<text fg={theme().text}>
<b>Modified Files</b>
<b>Files Changed</b>
</text>
</box>
<Show when={list().length <= 2 || open()}>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const dict: Record<string, string> = {
"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.sessionReviewV2.empty.changes.description": "File changes from current session will appear here",

"ui.sessionReview.openFile": "Open file",
"ui.sessionReview.selection.line": "line {{line}}",
Expand Down
Loading