diff --git a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx b/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx
index 94c136415dc2..0ab62d52b995 100644
--- a/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx
+++ b/packages/opencode/src/cli/cmd/run/scrollback.writer.tsx
@@ -7,6 +7,9 @@ import { toolFiletype, toolStructuredFinal } from "./tool"
import { RUN_THEME_FALLBACK, transparent, type RunTheme } from "./theme"
import type { EntryLayout, RunEntryBody, ScrollbackOptions, StreamCommit } from "./types"
+const MAX_DIFF_RENDER_CHARS = 512 * 1024
+const MAX_DIFF_RENDER_LINES = 5_000
+
function todoText(item: { status: string; content: string }): string {
if (item.status === "completed") {
return `[✓] ${item.content}`
@@ -27,6 +30,17 @@ function todoColor(theme: RunTheme, status: string) {
return status === "in_progress" ? theme.footer.warning : theme.block.muted
}
+function tooLargeDiff(diff: string) {
+ if (diff.length > MAX_DIFF_RENDER_CHARS) return true
+ let lines = 1
+ for (let i = 0; i < diff.length; i++) {
+ if (diff.charCodeAt(i) !== 10) continue
+ lines++
+ if (lines > MAX_DIFF_RENDER_LINES) return true
+ }
+ return false
+}
+
export function entryGroupKey(commit: StreamCommit): string | undefined {
if (!commit.partID) {
return undefined
@@ -193,27 +207,33 @@ export function RunEntryContent(props: {
{item.title}
{item.diff.trim() ? (
-
-
-
+ tooLargeDiff(item.diff) ? (
+
+ Diff too large to render safely; open the file or inspect the patch from disk.
+
+ ) : (
+
+
+
+ )
) : (
-{item.deletions ?? 0} line{item.deletions === 1 ? "" : "s"}
diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts
index f974a457ad7b..2064782cdd2c 100644
--- a/packages/opencode/src/snapshot/index.ts
+++ b/packages/opencode/src/snapshot/index.ts
@@ -1,6 +1,5 @@
import { Cause, Duration, Effect, Layer, Schedule, Schema, Semaphore, Context } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
-import { formatPatch, structuredPatch } from "diff"
import path from "path"
import { AppProcess } from "@opencode-ai/core/process"
import { InstanceState } from "@/effect/instance-state"
@@ -31,6 +30,9 @@ export type FileDiff = typeof FileDiff.Type
const log = Log.create({ service: "snapshot" })
const prune = "7.days"
const limit = 2 * 1024 * 1024
+const diffPatchLineLimit = 1_000
+const diffPatchByteLimit = 256 * 1024
+const diffPatchTotalByteLimit = 2 * 1024 * 1024
const core = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]
const cfg = ["-c", "core.autocrlf=false", ...core]
const quote = [...cfg, "-c", "core.quotepath=false"]
@@ -506,133 +508,6 @@ export const layer: Layer.Layer item.text),
- ),
- ]
- }
- if (row.status === "deleted") {
- return [
- yield* git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(
- Effect.map((item) => item.text),
- ),
- "",
- ]
- }
- return yield* Effect.all(
- [
- git([...cfg, ...args(["show", `${from}:${row.file}`])]).pipe(Effect.map((item) => item.text)),
- git([...cfg, ...args(["show", `${to}:${row.file}`])]).pipe(Effect.map((item) => item.text)),
- ],
- { concurrency: 2 },
- )
- })
-
- const load = Effect.fnUntraced(
- function* (rows: Row[]) {
- const refs = rows.flatMap((row) => {
- if (row.binary) return []
- if (row.status === "added")
- return [{ file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref]
- if (row.status === "deleted") {
- return [{ file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref]
- }
- return [
- { file: row.file, side: "before", ref: `${from}:${row.file}` } satisfies Ref,
- { file: row.file, side: "after", ref: `${to}:${row.file}` } satisfies Ref,
- ]
- })
- if (!refs.length) return new Map()
-
- const batch = yield* appProcess.run(
- ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
- cwd: state.directory,
- extendEnv: true,
- }),
- { stdin: refs.map((item) => item.ref).join("\n") + "\n" },
- )
- if (batch.exitCode !== 0) {
- log.info("git cat-file --batch failed during snapshot diff, falling back to per-file git show", {
- stderr: batch.stderr.toString("utf8"),
- refs: refs.length,
- })
- return
- }
- const out = batch.stdout
-
- const fail = (msg: string, extra?: Record) => {
- log.info(msg, { ...extra, refs: refs.length })
- return undefined
- }
-
- const map = new Map()
- const dec = new TextDecoder()
- let i = 0
- for (const ref of refs) {
- let end = i
- while (end < out.length && out[end] !== 10) end += 1
- if (end >= out.length) {
- return fail(
- "git cat-file --batch returned a truncated header during snapshot diff, falling back to per-file git show",
- )
- }
-
- const head = dec.decode(out.slice(i, end))
- i = end + 1
- const hit = map.get(ref.file) ?? { before: "", after: "" }
- if (head.endsWith(" missing")) {
- map.set(ref.file, hit)
- continue
- }
-
- const match = head.match(/^[0-9a-f]+ blob (\d+)$/)
- if (!match) {
- return fail(
- "git cat-file --batch returned an unexpected header during snapshot diff, falling back to per-file git show",
- { head },
- )
- }
-
- const size = Number(match[1])
- if (!Number.isInteger(size) || size < 0 || i + size >= out.length || out[i + size] !== 10) {
- return fail(
- "git cat-file --batch returned truncated content during snapshot diff, falling back to per-file git show",
- { head },
- )
- }
-
- const text = dec.decode(out.slice(i, i + size))
- if (ref.side === "before") hit.before = text
- if (ref.side === "after") hit.after = text
- map.set(ref.file, hit)
- i += size + 1
- }
-
- if (i !== out.length) {
- return fail(
- "git cat-file --batch returned trailing data during snapshot diff, falling back to per-file git show",
- )
- }
-
- return map
- },
- Effect.scoped,
- Effect.catch(() =>
- Effect.succeed