Skip to content
Open
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
24 changes: 15 additions & 9 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1854,20 +1854,26 @@ function Write(props: ToolProps<typeof WriteTool>) {
if (!props.input.content) return ""
return props.input.content
})
const truncated = createMemo(() => Boolean(props.metadata.truncated))

return (
<Switch>
<Match when={props.metadata.diagnostics !== undefined}>
<BlockTool title={"# Wrote " + normalizePath(props.input.filePath!)} part={props.part}>
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text}
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
/>
</line_number>
<Show
when={!truncated()}
fallback={<text fg={theme.textMuted}>(large file — content not shown)</text>}
>
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text}
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
/>
</line_number>
</Show>
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.filePath ?? ""} />
</BlockTool>
</Match>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ function EditBody(props: { request: PermissionRequest }) {

const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "")
const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "")
const truncated = createMemo(() => Boolean(props.request.metadata?.truncated))
const additions = createMemo(() => (props.request.metadata?.additions as number) ?? 0)
const deletions = createMemo(() => (props.request.metadata?.deletions as number) ?? 0)

const view = createMemo(() => {
const diffStyle = config.diff_style
Expand Down Expand Up @@ -101,7 +104,11 @@ function EditBody(props: { request: PermissionRequest }) {
</Show>
<Show when={!diff()}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>No diff provided</text>
<text fg={theme.textMuted}>
{truncated()
? `Large file — diff omitted (+${additions()} / -${deletions()} lines)`
: "No diff provided"}
</text>
</box>
</Show>
</box>
Expand Down
76 changes: 37 additions & 39 deletions packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher"
import { Instance } from "../project/instance"
import { Patch } from "../patch"
import { createTwoFilesPatch, diffLines } from "diff"
import { assertExternalDirectoryEffect } from "./external-directory"
import { trimDiff } from "./edit"
import { buildDisplayDiff, LARGE_FILE_FORMAT_BYTES } from "./edit"
import { LSP } from "../lsp"
import { AppFileSystem } from "../filesystem"
import DESCRIPTION from "./apply_patch.txt"
Expand Down Expand Up @@ -59,6 +58,7 @@ export const ApplyPatchTool = Tool.define(
diff: string
additions: number
deletions: number
truncated: boolean
}> = []

let totalDiff = ""
Expand All @@ -72,26 +72,20 @@ export const ApplyPatchTool = Tool.define(
const oldContent = ""
const newContent =
hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))

let additions = 0
let deletions = 0
for (const change of diffLines(oldContent, newContent)) {
if (change.added) additions += change.count || 0
if (change.removed) deletions += change.count || 0
}
const display = buildDisplayDiff(filePath, oldContent, newContent)

fileChanges.push({
filePath,
oldContent,
newContent,
type: "add",
diff,
additions,
deletions,
diff: display.diff,
additions: display.additions,
deletions: display.deletions,
truncated: display.truncated,
})

totalDiff += diff + "\n"
totalDiff += display.diff + "\n"
break
}

Expand All @@ -115,14 +109,7 @@ export const ApplyPatchTool = Tool.define(
return yield* Effect.fail(new Error(`apply_patch verification failed: ${error}`))
}

const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))

let additions = 0
let deletions = 0
for (const change of diffLines(oldContent, newContent)) {
if (change.added) additions += change.count || 0
if (change.removed) deletions += change.count || 0
}
const display = buildDisplayDiff(filePath, oldContent, newContent)

const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined
yield* assertExternalDirectoryEffect(ctx, movePath)
Expand All @@ -133,34 +120,34 @@ export const ApplyPatchTool = Tool.define(
newContent,
type: hunk.move_path ? "move" : "update",
movePath,
diff,
additions,
deletions,
diff: display.diff,
additions: display.additions,
deletions: display.deletions,
truncated: display.truncated,
})

totalDiff += diff + "\n"
totalDiff += display.diff + "\n"
break
}

case "delete": {
const contentToDelete = yield* afs
.readFileString(filePath)
.pipe(Effect.catch((error) => Effect.fail(new Error(`apply_patch verification failed: ${error}`))))
const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, ""))

const deletions = contentToDelete.split("\n").length
const display = buildDisplayDiff(filePath, contentToDelete, "")

fileChanges.push({
filePath,
oldContent: contentToDelete,
newContent: "",
type: "delete",
diff: deleteDiff,
diff: display.diff,
additions: 0,
deletions,
deletions: display.truncated ? display.deletions : contentToDelete.split("\n").length,
truncated: display.truncated,
})

totalDiff += deleteDiff + "\n"
totalDiff += display.diff + "\n"
break
}
}
Expand All @@ -175,7 +162,9 @@ export const ApplyPatchTool = Tool.define(
additions: change.additions,
deletions: change.deletions,
movePath: change.movePath,
truncated: change.truncated,
}))
const anyTruncated = fileChanges.some((c) => c.truncated)

// Check permissions if needed
const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/"))
Expand All @@ -187,6 +176,7 @@ export const ApplyPatchTool = Tool.define(
filepath: relativePaths.join(", "),
diff: totalDiff,
files,
truncated: anyTruncated,
},
})

Expand Down Expand Up @@ -226,7 +216,10 @@ export const ApplyPatchTool = Tool.define(
}

if (edited) {
yield* format.file(edited)
const editedSize = Buffer.byteLength(change.newContent, "utf8")
if (editedSize <= LARGE_FILE_FORMAT_BYTES) {
yield* format.file(edited)
}
yield* bus.publish(File.Event.Edited, { file: edited })
}
}
Expand All @@ -236,13 +229,17 @@ export const ApplyPatchTool = Tool.define(
yield* bus.publish(FileWatcher.Event.Updated, update)
}

// Notify LSP of file changes and collect diagnostics
for (const change of fileChanges) {
if (change.type === "delete") continue
const target = change.movePath ?? change.filePath
yield* lsp.touchFile(target, true)
// Notify LSP of file changes and collect diagnostics (skip for very large files)
const skipLsp = fileChanges.some((c) => Buffer.byteLength(c.newContent, "utf8") > LARGE_FILE_FORMAT_BYTES)
let diagnostics: Record<string, import("../lsp/client").LSPClient.Diagnostic[]> = {}
if (!skipLsp) {
for (const change of fileChanges) {
if (change.type === "delete") continue
const target = change.movePath ?? change.filePath
yield* lsp.touchFile(target, true)
}
diagnostics = yield* lsp.diagnostics()
}
const diagnostics = yield* lsp.diagnostics()

// Generate output summary
const summaryLines = fileChanges.map((change) => {
Expand Down Expand Up @@ -272,6 +269,7 @@ export const ApplyPatchTool = Tool.define(
diff: totalDiff,
files,
diagnostics,
truncated: anyTruncated,
},
output,
}
Expand Down
33 changes: 33 additions & 0 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,39 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
}
}

export const LARGE_FILE_DIFF_BYTES = 256 * 1024
export const LARGE_FILE_FORMAT_BYTES = 2 * 1024 * 1024

export interface DisplayDiff {
diff: string
truncated: boolean
additions: number
deletions: number
}

export function buildDisplayDiff(filepath: string, oldContent: string, newContent: string): DisplayDiff {
const oldSize = Buffer.byteLength(oldContent, "utf8")
const newSize = Buffer.byteLength(newContent, "utf8")
if (oldSize > LARGE_FILE_DIFF_BYTES || newSize > LARGE_FILE_DIFF_BYTES) {
const oldLines = oldContent ? oldContent.split("\n").length : 0
const newLines = newContent ? newContent.split("\n").length : 0
return {
diff: "",
truncated: true,
additions: Math.max(0, newLines - oldLines),
deletions: Math.max(0, oldLines - newLines),
}
}
let additions = 0
let deletions = 0
for (const change of diffLines(oldContent, newContent)) {
if (change.added) additions += change.count || 0
if (change.removed) deletions += change.count || 0
}
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, oldContent, newContent))
return { diff, truncated: false, additions, deletions }
}

export function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
Expand Down
47 changes: 28 additions & 19 deletions packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as path from "path"
import { Effect } from "effect"
import { Tool } from "./tool"
import { LSP } from "../lsp"
import { createTwoFilesPatch } from "diff"
import { LSPClient } from "../lsp/client"
import DESCRIPTION from "./write.txt"
import { Bus } from "../bus"
import { File } from "../file"
Expand All @@ -12,7 +12,7 @@ import { Format } from "../format"
import { FileTime } from "../file/time"
import { AppFileSystem } from "../filesystem"
import { Instance } from "../project/instance"
import { trimDiff } from "./edit"
import { buildDisplayDiff, LARGE_FILE_FORMAT_BYTES } from "./edit"
import { assertExternalDirectoryEffect } from "./external-directory"

const MAX_PROJECT_DIAGNOSTICS_FILES = 5
Expand Down Expand Up @@ -43,19 +43,24 @@ export const WriteTool = Tool.define(
const contentOld = exists ? yield* fs.readFileString(filepath) : ""
if (exists) yield* filetime.assert(ctx.sessionID, filepath)

const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
const display = buildDisplayDiff(filepath, contentOld, params.content)
yield* ctx.ask({
permission: "edit",
patterns: [path.relative(Instance.worktree, filepath)],
always: ["*"],
metadata: {
filepath,
diff,
diff: display.diff,
truncated: display.truncated,
additions: display.additions,
deletions: display.deletions,
},
})

yield* fs.writeWithDirs(filepath, params.content)
yield* format.file(filepath)
const newSize = Buffer.byteLength(params.content, "utf8")
const skipHeavy = newSize > LARGE_FILE_FORMAT_BYTES
if (!skipHeavy) yield* format.file(filepath)
yield* bus.publish(File.Event.Edited, { file: filepath })
yield* bus.publish(FileWatcher.Event.Updated, {
file: filepath,
Expand All @@ -64,21 +69,24 @@ export const WriteTool = Tool.define(
yield* filetime.read(ctx.sessionID, filepath)

let output = "Wrote file successfully."
yield* lsp.touchFile(filepath, true)
const diagnostics = yield* lsp.diagnostics()
const normalizedFilepath = AppFileSystem.normalizePath(filepath)
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) {
const current = file === normalizedFilepath
if (!current && projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
const block = LSP.Diagnostic.report(current ? filepath : file, issues)
if (!block) continue
if (current) {
output += `\n\nLSP errors detected in this file, please fix:\n${block}`
continue
let diagnostics: Record<string, LSPClient.Diagnostic[]> = {}
if (!skipHeavy) {
yield* lsp.touchFile(filepath, true)
diagnostics = yield* lsp.diagnostics()
const normalizedFilepath = AppFileSystem.normalizePath(filepath)
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) {
const current = file === normalizedFilepath
if (!current && projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
const block = LSP.Diagnostic.report(current ? filepath : file, issues)
if (!block) continue
if (current) {
output += `\n\nLSP errors detected in this file, please fix:\n${block}`
continue
}
projectDiagnosticsCount++
output += `\n\nLSP errors detected in other files:\n${block}`
}
projectDiagnosticsCount++
output += `\n\nLSP errors detected in other files:\n${block}`
}

return {
Expand All @@ -87,6 +95,7 @@ export const WriteTool = Tool.define(
diagnostics,
filepath,
exists: exists,
truncated: display.truncated,
},
output,
}
Expand Down
Loading
Loading