Compare commits

...

9 Commits

Author SHA1 Message Date
opencode 833706cda4 release: v0.15.13 2025-10-21 20:24:20 +00:00
Aiden Cline 2a951cea38 ignore: reword 2025-10-21 14:43:34 -05:00
Dax Raad d9a8d2032a fix sourcemapping so errors show proper stack trace 2025-10-21 15:32:24 -04:00
Dax Raad d7cdabe8b7 refactor acp args 2025-10-21 15:24:09 -04:00
Aiden Cline e7c74d13cc ignore: reword contributing.md 2025-10-21 14:12:33 -05:00
opencode 6ac5a447c2 release: v0.15.12 2025-10-21 17:31:56 +00:00
Aiden Cline cb4670e6de ignore: add label 2025-10-21 11:12:52 -05:00
Aiden Cline ca0f3902b7 fix: provider option transforms (#3331) 2025-10-21 11:08:21 -05:00
Dax Raad e9996342a7 core: provide line-level statistics in file diffs to help users understand the scale of changes 2025-10-21 11:54:41 -04:00
24 changed files with 303 additions and 165 deletions
+2 -6
View File
@@ -12,14 +12,10 @@ We want to make it easy for you to contribute to OpenCode. Here are the most com
However, any UI or core product feature must go through a design review with the core team before implementation.
> [!IMPORTANT]
> We do not accept PRs for core features.
Take a look at recent git history to understand what usually lands.
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with either of the following labels:
If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
> [!NOTE]
+11 -11
View File
@@ -37,7 +37,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -109,7 +109,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -147,7 +147,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -163,7 +163,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.15.11",
"version": "0.15.13",
"bin": {
"opencode": "./bin/opencode",
},
@@ -226,7 +226,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -246,7 +246,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.15.11",
"version": "0.15.13",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -257,7 +257,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -270,7 +270,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@kobalte/core": "catalog:",
"@solidjs/meta": "catalog:",
@@ -291,7 +291,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.15.11"
"version": "0.15.13"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.15.11",
"version": "0.15.13",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "0.15.11",
"version": "0.15.13",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.15.11",
"version": "0.15.13",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "0.15.11",
"version": "0.15.13",
"description": "",
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "0.15.11",
"version": "0.15.13",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.15.11",
"version": "0.15.13",
"name": "opencode",
"type": "module",
"private": true,
+1 -1
View File
@@ -40,7 +40,7 @@ for (const [os, arch] of targets) {
await $`tar -xf ../../node_modules/${watcher.replace("@parcel/", "parcel-")}-*.tgz -C ../../node_modules/${watcher} --strip-components=1`
await Bun.build({
sourcemap: true,
sourcemap: "external",
compile: {
target: `bun-${os}-${arch}` as any,
outfile: `dist/${name}/bin/opencode`,
+4 -7
View File
@@ -1,7 +1,7 @@
import type { CommandModule } from "yargs"
import { ACPServer } from "../../acp/server"
import { cmd } from "./cmd"
export const AcpCommand: CommandModule = {
export const AcpCommand = cmd({
command: "acp",
describe: "Start ACP (Agent Client Protocol) server",
builder: (yargs) => {
@@ -12,10 +12,7 @@ export const AcpCommand: CommandModule = {
})
},
handler: async (opts) => {
if (opts["cwd"] && typeof opts["cwd"] === "string") {
process.chdir(opts["cwd"])
}
if (opts.cwd) process.chdir(opts["cwd"])
await ACPServer.start()
},
}
})
@@ -105,6 +105,24 @@ export namespace ProviderTransform {
return result
}
export function providerOptions(npm: string | undefined, providerID: string, options: { [x: string]: any }) {
switch (npm) {
case "@ai-sdk/openai":
case "@ai-sdk/azure":
return {
["openai" as string]: options,
}
case "@ai-sdk/anthropic":
return {
["anthropic" as string]: options,
}
default:
return {
[providerID]: options,
}
}
}
export function maxOutputTokens(
providerID: string,
options: Record<string, any>,
+2 -3
View File
@@ -15,6 +15,7 @@ import { Token } from "../util/token"
import { Log } from "../util/log"
import { SessionLock } from "./lock"
import { NamedError } from "../util/error"
import { ProviderTransform } from "@/provider/transform"
export namespace SessionCompaction {
const log = Log.create({ service: "session.compaction" })
@@ -143,9 +144,7 @@ export namespace SessionCompaction {
const stream = streamText({
maxRetries: 10,
model: model.language,
providerOptions: {
[model.npm === "@ai-sdk/openai" ? "openai" : model.providerID]: model.info.options,
},
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, model.info.options),
abortSignal: signal,
onError(error) {
log.error("stream error", {
+2 -9
View File
@@ -147,12 +147,7 @@ export namespace Session {
})
})
export async function createNext(input: {
id?: string
title?: string
parentID?: string
directory: string
}) {
export async function createNext(input: { id?: string; title?: string; parentID?: string; directory: string }) {
const result: Info = {
id: Identifier.descending("session", input.id),
version: Installation.VERSION,
@@ -372,9 +367,7 @@ export namespace Session {
.add(new Decimal(tokens.input).mul(input.model.cost?.input ?? 0).div(1_000_000))
.add(new Decimal(tokens.output).mul(input.model.cost?.output ?? 0).div(1_000_000))
.add(new Decimal(tokens.cache.read).mul(input.model.cost?.cache_read ?? 0).div(1_000_000))
.add(
new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000),
)
.add(new Decimal(tokens.cache.write).mul(input.model.cost?.cache_write ?? 0).div(1_000_000))
.toNumber(),
tokens,
}
+2 -7
View File
@@ -282,10 +282,7 @@ export namespace SessionPrompt {
OUTPUT_TOKEN_MAX,
),
abortSignal: abort.signal,
providerOptions: {
[model.npm === "@ai-sdk/openai" || model.npm === "@ai-sdk/azure" ? "openai" : model.providerID]:
params.options,
},
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
@@ -1822,9 +1819,7 @@ export namespace SessionPrompt {
}
generateText({
maxOutputTokens: small.info.reasoning ? 1500 : 20,
providerOptions: {
[small.providerID]: options,
},
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
messages: [
...SystemPrompt.title(small.providerID).map(
(x): ModelMessage => ({
+31 -57
View File
@@ -26,15 +26,8 @@ export namespace Snapshot {
.nothrow()
log.info("initialized")
}
await $`git --git-dir ${git} add .`
.quiet()
.cwd(Instance.directory)
.nothrow()
const hash = await $`git --git-dir ${git} write-tree`
.quiet()
.cwd(Instance.directory)
.nothrow()
.text()
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
const hash = await $`git --git-dir ${git} write-tree`.quiet().cwd(Instance.directory).nothrow().text()
log.info("tracking", { hash, cwd: Instance.directory, git })
return hash.trim()
}
@@ -47,14 +40,8 @@ export namespace Snapshot {
export async function patch(hash: string): Promise<Patch> {
const git = gitdir()
await $`git --git-dir ${git} add .`
.quiet()
.cwd(Instance.directory)
.nothrow()
const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
const result = await $`git --git-dir ${git} diff --name-only ${hash} -- .`.quiet().cwd(Instance.directory).nothrow()
// If git diff fails, return empty patch
if (result.exitCode !== 0) {
@@ -77,11 +64,10 @@ export namespace Snapshot {
export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot })
const git = gitdir()
const result =
await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
.quiet()
.cwd(Instance.worktree)
.nothrow()
const result = await $`git --git-dir=${git} read-tree ${snapshot} && git --git-dir=${git} checkout-index -a -f`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
log.error("failed to restore snapshot", {
@@ -100,18 +86,16 @@ export namespace Snapshot {
for (const file of item.files) {
if (files.has(file)) continue
log.info("reverting", { file, hash: item.hash })
const result =
await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
const result = await $`git --git-dir=${git} checkout ${item.hash} -- ${file}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
const relativePath = path.relative(Instance.worktree, file)
const checkTree = await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (result.exitCode !== 0) {
const relativePath = path.relative(Instance.worktree, file)
const checkTree =
await $`git --git-dir=${git} ls-tree ${item.hash} -- ${relativePath}`
.quiet()
.cwd(Instance.worktree)
.nothrow()
if (checkTree.exitCode === 0 && checkTree.text().trim()) {
log.info("file existed in snapshot but checkout failed, keeping", {
file,
@@ -128,14 +112,8 @@ export namespace Snapshot {
export async function diff(hash: string) {
const git = gitdir()
await $`git --git-dir ${git} add .`
.quiet()
.cwd(Instance.directory)
.nothrow()
const result = await $`git --git-dir=${git} diff ${hash} -- .`
.quiet()
.cwd(Instance.worktree)
.nothrow()
await $`git --git-dir ${git} add .`.quiet().cwd(Instance.directory).nothrow()
const result = await $`git --git-dir=${git} diff ${hash} -- .`.quiet().cwd(Instance.worktree).nothrow()
if (result.exitCode !== 0) {
log.warn("failed to get diff", {
@@ -153,37 +131,33 @@ export namespace Snapshot {
export const FileDiff = z
.object({
file: z.string(),
left: z.string(),
right: z.string(),
before: z.string(),
after: z.string(),
additions: z.number(),
deletions: z.number(),
})
.meta({
ref: "FileDiff",
})
export type FileDiff = z.infer<typeof FileDiff>
export async function diffFull(
from: string,
to: string,
): Promise<FileDiff[]> {
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
const git = gitdir()
const result: FileDiff[] = []
for await (const line of $`git --git-dir=${git} diff --name-only ${from} ${to} -- .`
for await (const line of $`git --git-dir=${git} diff --numstat ${from} ${to} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
.lines()) {
if (!line) continue
const left = await $`git --git-dir=${git} show ${from}:${line}`
.quiet()
.nothrow()
.text()
const right = await $`git --git-dir=${git} show ${to}:${line}`
.quiet()
.nothrow()
.text()
const [additions, deletions, file] = line.split("\t")
const before = await $`git --git-dir=${git} show ${from}:${file}`.quiet().nothrow().text()
const after = await $`git --git-dir=${git} show ${to}:${file}`.quiet().nothrow().text()
result.push({
file: line,
left,
right,
file,
before,
after,
additions: parseInt(additions),
deletions: parseInt(deletions),
})
}
return result
+213 -49
View File
@@ -33,9 +33,7 @@ test("tracks deleted files correctly", async () => {
await $`rm ${tmp.path}/a.txt`.quiet()
expect((await Snapshot.patch(before!)).files).toContain(
`${tmp.path}/a.txt`,
)
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/a.txt`)
},
})
})
@@ -93,15 +91,11 @@ test("multiple file operations", async () => {
await Snapshot.revert([await Snapshot.patch(before!)])
expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(
tmp.extra.aContent,
)
expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent)
expect(await Bun.file(`${tmp.path}/c.txt`).exists()).toBe(false)
// Note: revert currently only removes files, not directories
// The empty directory will remain
expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(
tmp.extra.bContent,
)
expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent)
},
})
})
@@ -129,10 +123,7 @@ test("binary file handling", async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(
`${tmp.path}/image.png`,
new Uint8Array([0x89, 0x50, 0x4e, 0x47]),
)
await Bun.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
const patch = await Snapshot.patch(before!)
expect(patch.files).toContain(`${tmp.path}/image.png`)
@@ -153,9 +144,7 @@ test("symlink handling", async () => {
await $`ln -s ${tmp.path}/a.txt ${tmp.path}/link.txt`.quiet()
expect((await Snapshot.patch(before!)).files).toContain(
`${tmp.path}/link.txt`,
)
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/link.txt`)
},
})
})
@@ -170,9 +159,7 @@ test("large file handling", async () => {
await Bun.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
expect((await Snapshot.patch(before!)).files).toContain(
`${tmp.path}/large.txt`,
)
expect((await Snapshot.patch(before!)).files).toContain(`${tmp.path}/large.txt`)
},
})
})
@@ -190,9 +177,7 @@ test("nested directory revert", async () => {
await Snapshot.revert([await Snapshot.patch(before!)])
expect(
await Bun.file(`${tmp.path}/level1/level2/level3/deep.txt`).exists(),
).toBe(false)
expect(await Bun.file(`${tmp.path}/level1/level2/level3/deep.txt`).exists()).toBe(false)
},
})
})
@@ -226,9 +211,7 @@ test("revert with empty patches", async () => {
expect(Snapshot.revert([])).resolves.toBeUndefined()
// Should not crash with patches that have empty file lists
expect(
Snapshot.revert([{ hash: "dummy", files: [] }]),
).resolves.toBeUndefined()
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
},
})
})
@@ -543,13 +526,9 @@ test("restore function", async () => {
await Snapshot.restore(before!)
expect(await Bun.file(`${tmp.path}/a.txt`).exists()).toBe(true)
expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(
tmp.extra.aContent,
)
expect(await Bun.file(`${tmp.path}/a.txt`).text()).toBe(tmp.extra.aContent)
expect(await Bun.file(`${tmp.path}/new.txt`).exists()).toBe(true) // New files should remain
expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(
tmp.extra.bContent,
)
expect(await Bun.file(`${tmp.path}/b.txt`).text()).toBe(tmp.extra.bContent)
},
})
})
@@ -601,14 +580,12 @@ test("revert preserves file that existed in snapshot when deleted then recreated
expect(await Bun.file(`${tmp.path}/newfile.txt`).exists()).toBe(false)
expect(await Bun.file(`${tmp.path}/existing.txt`).exists()).toBe(true)
expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe(
"original content",
)
expect(await Bun.file(`${tmp.path}/existing.txt`).text()).toBe("original content")
},
})
})
test("diffFull function", async () => {
test("diffFull with new file additions", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
@@ -617,26 +594,103 @@ test("diffFull function", async () => {
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/new.txt`, "new content")
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(1)
const newFileDiff = diffs[0]
expect(newFileDiff.file).toBe("new.txt")
expect(newFileDiff.before).toBe("")
expect(newFileDiff.after).toBe("new content")
expect(newFileDiff.additions).toBe(1)
expect(newFileDiff.deletions).toBe(0)
},
})
})
test("diffFull with file modifications", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/b.txt`, "modified content")
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(2)
expect(diffs.length).toBe(1)
const newFileDiff = diffs.find((d) => d.file === "new.txt")
expect(newFileDiff).toBeDefined()
expect(newFileDiff!.left).toBe("")
expect(newFileDiff!.right).toBe("new content")
const modifiedFileDiff = diffs.find((d) => d.file === "b.txt")
expect(modifiedFileDiff).toBeDefined()
expect(modifiedFileDiff!.left).toBe(tmp.extra.bContent)
expect(modifiedFileDiff!.right).toBe("modified content")
const modifiedFileDiff = diffs[0]
expect(modifiedFileDiff.file).toBe("b.txt")
expect(modifiedFileDiff.before).toBe(tmp.extra.bContent)
expect(modifiedFileDiff.after).toBe("modified content")
expect(modifiedFileDiff.additions).toBeGreaterThan(0)
expect(modifiedFileDiff.deletions).toBeGreaterThan(0)
},
})
})
test("diffFull with file deletions", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await $`rm ${tmp.path}/a.txt`.quiet()
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(1)
const removedFileDiff = diffs[0]
expect(removedFileDiff.file).toBe("a.txt")
expect(removedFileDiff.before).toBe(tmp.extra.aContent)
expect(removedFileDiff.after).toBe("")
expect(removedFileDiff.additions).toBe(0)
expect(removedFileDiff.deletions).toBe(1)
},
})
})
test("diffFull with multiple line additions", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3")
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(1)
const multiDiff = diffs[0]
expect(multiDiff.file).toBe("multi.txt")
expect(multiDiff.before).toBe("")
expect(multiDiff.after).toBe("line1\nline2\nline3")
expect(multiDiff.additions).toBe(3)
expect(multiDiff.deletions).toBe(0)
},
})
})
test("diffFull with addition and deletion", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
@@ -654,13 +708,123 @@ test("diffFull function", async () => {
const addedFileDiff = diffs.find((d) => d.file === "added.txt")
expect(addedFileDiff).toBeDefined()
expect(addedFileDiff!.left).toBe("")
expect(addedFileDiff!.right).toBe("added content")
expect(addedFileDiff!.before).toBe("")
expect(addedFileDiff!.after).toBe("added content")
expect(addedFileDiff!.additions).toBe(1)
expect(addedFileDiff!.deletions).toBe(0)
const removedFileDiff = diffs.find((d) => d.file === "a.txt")
expect(removedFileDiff).toBeDefined()
expect(removedFileDiff!.left).toBe(tmp.extra.aContent)
expect(removedFileDiff!.right).toBe("")
expect(removedFileDiff!.before).toBe(tmp.extra.aContent)
expect(removedFileDiff!.after).toBe("")
expect(removedFileDiff!.additions).toBe(0)
expect(removedFileDiff!.deletions).toBe(1)
},
})
})
test("diffFull with multiple additions and deletions", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3")
await Bun.write(`${tmp.path}/multi2.txt`, "single line")
await $`rm ${tmp.path}/a.txt`.quiet()
await $`rm ${tmp.path}/b.txt`.quiet()
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(4)
const multi1Diff = diffs.find((d) => d.file === "multi1.txt")
expect(multi1Diff).toBeDefined()
expect(multi1Diff!.additions).toBe(3)
expect(multi1Diff!.deletions).toBe(0)
const multi2Diff = diffs.find((d) => d.file === "multi2.txt")
expect(multi2Diff).toBeDefined()
expect(multi2Diff!.additions).toBe(1)
expect(multi2Diff!.deletions).toBe(0)
const removedADiff = diffs.find((d) => d.file === "a.txt")
expect(removedADiff).toBeDefined()
expect(removedADiff!.additions).toBe(0)
expect(removedADiff!.deletions).toBe(1)
const removedBDiff = diffs.find((d) => d.file === "b.txt")
expect(removedBDiff).toBeDefined()
expect(removedBDiff!.additions).toBe(0)
expect(removedBDiff!.deletions).toBe(1)
},
})
})
test("diffFull with no changes", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(0)
},
})
})
test("diffFull with binary file changes", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03]))
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(1)
const binaryDiff = diffs[0]
expect(binaryDiff.file).toBe("binary.bin")
expect(binaryDiff.before).toBe("")
},
})
})
test("diffFull with whitespace changes", async () => {
await using tmp = await bootstrap()
await Instance.provide({
directory: tmp.path,
fn: async () => {
await Bun.write(`${tmp.path}/whitespace.txt`, "line1\nline2")
const before = await Snapshot.track()
expect(before).toBeTruthy()
await Bun.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n")
const after = await Snapshot.track()
expect(after).toBeTruthy()
const diffs = await Snapshot.diffFull(before!, after!)
expect(diffs.length).toBe(1)
const whitespaceDiff = diffs[0]
expect(whitespaceDiff.file).toBe("whitespace.txt")
expect(whitespaceDiff.additions).toBeGreaterThan(0)
},
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.15.11",
"version": "0.15.13",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.15.11",
"version": "0.15.13",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+4 -2
View File
@@ -585,8 +585,10 @@ export type Todo = {
export type FileDiff = {
file: string
left: string
right: string
before: string
after: string
additions: number
deletions: number
}
export type UserMessage = {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "0.15.11",
"version": "0.15.13",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.15.11",
"version": "0.15.13",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "0.15.11",
"version": "0.15.13",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.15.11",
"version": "0.15.13",
"publisher": "sst-dev",
"repository": {
"type": "git",