Skip to content
Closed
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
31 changes: 27 additions & 4 deletions packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function filetype(input?: string) {
return language
}

function EditBody(props: { request: PermissionRequest }) {
function EditBody(props: { request: PermissionRequest; full: boolean }) {
const themeState = useTheme()
const theme = themeState.theme
const syntax = themeState.syntax
Expand All @@ -52,6 +52,8 @@ function EditBody(props: { request: PermissionRequest }) {

const filepath = createMemo(() => (props.request.metadata?.filepath as string) ?? "")
const diff = createMemo(() => (props.request.metadata?.diff as string) ?? "")
const diffFull = createMemo(() => (props.request.metadata?.diffFull as string) ?? "")
const activeDiff = createMemo(() => (props.full && diffFull() ? diffFull() : diff()))

const view = createMemo(() => {
const diffStyle = sync.data.config.tui?.diff_style
Expand All @@ -67,10 +69,10 @@ function EditBody(props: { request: PermissionRequest }) {
<text fg={theme.textMuted}>{"→"}</text>
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
</box>
<Show when={diff()}>
<Show when={activeDiff()}>
<scrollbox height="100%">
<diff
diff={diff()}
diff={activeDiff()}
view={view()}
filetype={ft()}
syntaxStyle={syntax()}
Expand Down Expand Up @@ -120,6 +122,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
const sync = useSync()
const [store, setStore] = createStore({
stage: "permission" as PermissionStage,
full: false,
})

const session = createMemo(() => sync.data.session.find((s) => s.id === props.request.sessionID))
Expand Down Expand Up @@ -199,7 +202,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
body={
<Switch>
<Match when={props.request.permission === "edit"}>
<EditBody request={props.request} />
<EditBody request={props.request} full={store.full} />
</Match>
<Match when={props.request.permission === "read"}>
<TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
Expand Down Expand Up @@ -266,6 +269,9 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
escapeKey="reject"
fullscreen
full={Boolean(props.request.metadata?.diffFull)}
fullActive={store.full}
onFull={() => setStore("full", (current) => !current)}
onSelect={(option) => {
if (option === "always") {
setStore("stage", "always")
Expand Down Expand Up @@ -368,6 +374,9 @@ function Prompt<const T extends Record<string, string>>(props: {
options: T
escapeKey?: keyof T
fullscreen?: boolean
full?: boolean
fullActive?: boolean
onFull?: () => void
onSelect: (option: keyof T) => void
}) {
const { theme } = useTheme()
Expand All @@ -379,6 +388,7 @@ function Prompt<const T extends Record<string, string>>(props: {
expanded: false,
})
const diffKey = Keybind.parse("ctrl+f")[0]
const fullKey = Keybind.parse("ctrl+o")[0]

useKeyboard((evt) => {
if (evt.name === "left" || evt.name == "h") {
Expand Down Expand Up @@ -409,10 +419,18 @@ function Prompt<const T extends Record<string, string>>(props: {
evt.preventDefault()
evt.stopPropagation()
setStore("expanded", (v) => !v)
return
}

if (props.fullscreen && store.expanded && fullKey && props.full && Keybind.match(fullKey, keybind.parse(evt))) {
evt.preventDefault()
evt.stopPropagation()
props.onFull?.()
}
})

const hint = createMemo(() => (store.expanded ? "minimize" : "fullscreen"))
const fullHint = createMemo(() => (props.fullActive ? "collapse" : "full file"))
const renderer = useRenderer()

const content = () => (
Expand Down Expand Up @@ -476,6 +494,11 @@ function Prompt<const T extends Record<string, string>>(props: {
{"ctrl+f"} <span style={{ fg: theme.textMuted }}>{hint()}</span>
</text>
</Show>
<Show when={store.expanded && props.full}>
<text fg={theme.text}>
{"ctrl+o"} <span style={{ fg: theme.textMuted }}>{fullHint()}</span>
</text>
</Show>
<text fg={theme.text}>
{"⇆"} <span style={{ fg: theme.textMuted }}>select</span>
</text>
Expand Down
12 changes: 11 additions & 1 deletion packages/opencode/src/tool/apply_patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Instance } from "../project/instance"
import { Patch } from "../patch"
import { createTwoFilesPatch, diffLines } from "diff"
import { assertExternalDirectory } from "./external-directory"
import { trimDiff } from "./edit"
import { fullDiff, trimDiff } from "./edit"
import { LSP } from "../lsp"
import { Filesystem } from "../util/filesystem"
import DESCRIPTION from "./apply_patch.txt"
Expand Down Expand Up @@ -56,6 +56,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
}> = []

let totalDiff = ""
const totalDiffFull: string[] = []

for (const hunk of hunks) {
const filePath = path.resolve(Instance.directory, hunk.path)
Expand All @@ -67,6 +68,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
const newContent =
hunk.contents.length === 0 || hunk.contents.endsWith("\n") ? hunk.contents : `${hunk.contents}\n`
const diff = trimDiff(createTwoFilesPatch(filePath, filePath, oldContent, newContent))
const diffFull = fullDiff(filePath, oldContent, newContent)

let additions = 0
let deletions = 0
Expand All @@ -86,6 +88,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
})

totalDiff += diff + "\n"
totalDiffFull.push(diffFull)
break
}

Expand All @@ -110,6 +113,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
}

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

let additions = 0
let deletions = 0
Expand All @@ -133,6 +137,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
})

totalDiff += diff + "\n"
totalDiffFull.push(diffFull)
break
}

Expand All @@ -141,6 +146,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
throw new Error(`apply_patch verification failed: ${error}`)
})
const deleteDiff = trimDiff(createTwoFilesPatch(filePath, filePath, contentToDelete, ""))
const deleteDiffFull = fullDiff(filePath, contentToDelete, "")

const deletions = contentToDelete.split("\n").length

Expand All @@ -155,18 +161,21 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
})

totalDiff += deleteDiff + "\n"
totalDiffFull.push(deleteDiffFull)
break
}
}
}

// Check permissions if needed
const diffFull = totalDiffFull.join("\n")
await ctx.ask({
permission: "edit",
patterns: fileChanges.map((c) => path.relative(Instance.worktree, c.filePath)),
always: ["*"],
metadata: {
diff: totalDiff,
diffFull,
},
})

Expand Down Expand Up @@ -269,6 +278,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
title: output,
metadata: {
diff: totalDiff,
diffFull,
files,
diagnostics,
},
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/src/tool/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ function normalizeLineEndings(text: string): string {
return text.replaceAll("\r\n", "\n")
}

export function fullDiff(filePath: string, before: string, after: string, trim = true) {
const left = before.split("\n").length
const right = after.split("\n").length
const count = Math.max(left, right)
const diff = createTwoFilesPatch(filePath, filePath, before, after, undefined, undefined, {
context: count,
})
if (!trim) return diff
return trimDiff(diff)
}

export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
parameters: z.object({
Expand All @@ -49,6 +60,7 @@ export const EditTool = Tool.define("edit", {
await FileTime.withLock(filePath, async () => {
if (params.oldString === "") {
contentNew = params.newString
const diffFull = fullDiff(filePath, contentOld, contentNew)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
await ctx.ask({
permission: "edit",
Expand All @@ -57,6 +69,7 @@ export const EditTool = Tool.define("edit", {
metadata: {
filepath: filePath,
diff,
diffFull,
},
})
await Bun.write(filePath, params.newString)
Expand All @@ -75,6 +88,7 @@ export const EditTool = Tool.define("edit", {
contentOld = await file.text()
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)

const diffFull = fullDiff(filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew))
diff = trimDiff(
createTwoFilesPatch(filePath, filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew)),
)
Expand All @@ -85,6 +99,7 @@ export const EditTool = Tool.define("edit", {
metadata: {
filepath: filePath,
diff,
diffFull,
},
})

Expand All @@ -99,6 +114,7 @@ export const EditTool = Tool.define("edit", {
FileTime.read(ctx.sessionID, filePath)
})

const diffFull = fullDiff(filePath, normalizeLineEndings(contentOld), normalizeLineEndings(contentNew))
const filediff: Snapshot.FileDiff = {
file: filePath,
before: contentOld,
Expand All @@ -114,6 +130,7 @@ export const EditTool = Tool.define("edit", {
ctx.metadata({
metadata: {
diff,
diffFull,
filediff,
diagnostics: {},
},
Expand All @@ -136,6 +153,7 @@ export const EditTool = Tool.define("edit", {
metadata: {
diagnostics,
diff,
diffFull,
filediff,
},
title: `${path.relative(Instance.worktree, filePath)}`,
Expand Down
4 changes: 3 additions & 1 deletion packages/opencode/src/tool/write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { File } from "../file"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { trimDiff } from "./edit"
import { fullDiff, trimDiff } from "./edit"
import { assertExternalDirectory } from "./external-directory"

const MAX_DIAGNOSTICS_PER_FILE = 20
Expand All @@ -30,6 +30,7 @@ export const WriteTool = Tool.define("write", {
const contentOld = exists ? await file.text() : ""
if (exists) await FileTime.assert(ctx.sessionID, filepath)

const diffFull = fullDiff(filepath, contentOld, params.content)
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, params.content))
await ctx.ask({
permission: "edit",
Expand All @@ -38,6 +39,7 @@ export const WriteTool = Tool.define("write", {
metadata: {
filepath,
diff,
diffFull,
},
})

Expand Down