mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into opentui
This commit is contained in:
commit
5925693f31
25 changed files with 293 additions and 100 deletions
1
STATS.md
1
STATS.md
|
|
@ -117,3 +117,4 @@
|
|||
| 2025-10-21 | 548,721 (+7,457) | 479,703 (+6,751) | 1,028,424 (+14,208) |
|
||||
| 2025-10-22 | 557,949 (+9,228) | 491,395 (+11,692) | 1,049,344 (+20,920) |
|
||||
| 2025-10-23 | 564,716 (+6,767) | 498,736 (+7,341) | 1,063,452 (+14,108) |
|
||||
| 2025-10-24 | 572,692 (+7,976) | 506,905 (+8,169) | 1,079,597 (+16,145) |
|
||||
|
|
|
|||
44
bun.lock
44
bun.lock
|
|
@ -39,7 +39,11 @@
|
|||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
|
|
@ -66,7 +70,11 @@
|
|||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
|
|
@ -90,7 +98,11 @@
|
|||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
@ -111,7 +123,11 @@
|
|||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
|
|
@ -150,7 +166,11 @@
|
|||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
|
|
@ -166,7 +186,11 @@
|
|||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
|
|
@ -241,7 +265,11 @@
|
|||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
|
|
@ -261,7 +289,11 @@
|
|||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.81.0",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
|
|
@ -272,7 +304,11 @@
|
|||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
|
|
@ -285,7 +321,11 @@
|
|||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@pierre/precision-diffs": "0.0.2-alpha.1-1",
|
||||
|
|
@ -308,7 +348,11 @@
|
|||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
<<<<<<< HEAD
|
||||
"version": "0.15.15",
|
||||
=======
|
||||
"version": "0.15.16",
|
||||
>>>>>>> dev
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
|
|
|||
|
|
@ -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.15"
|
||||
"version": "0.15.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ibm/plex": "6.4.1",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const TuiCommand = cmd({
|
|||
|
||||
;(async () => {
|
||||
// if (Installation.isLocal()) return
|
||||
const config = await Config.global()
|
||||
const config = await Config.get()
|
||||
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
|
||||
const latest = await Installation.latest().catch(() => {})
|
||||
if (!latest) return
|
||||
|
|
|
|||
|
|
@ -838,4 +838,126 @@ export namespace LSPServer {
|
|||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const LuaLS: Info = {
|
||||
id: "lua-ls",
|
||||
root: NearestRoot([
|
||||
".luarc.json",
|
||||
".luarc.jsonc",
|
||||
".luacheckrc",
|
||||
".stylua.toml",
|
||||
"stylua.toml",
|
||||
"selene.toml",
|
||||
"selene.yml",
|
||||
]),
|
||||
extensions: [".lua"],
|
||||
async spawn(root) {
|
||||
let bin = Bun.which("lua-language-server", {
|
||||
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
||||
})
|
||||
|
||||
if (!bin) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("downloading lua-language-server from GitHub releases")
|
||||
|
||||
const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
|
||||
if (!releaseResponse.ok) {
|
||||
log.error("Failed to fetch lua-language-server release info")
|
||||
return
|
||||
}
|
||||
|
||||
const release = await releaseResponse.json()
|
||||
|
||||
const platform = process.platform
|
||||
const arch = process.arch
|
||||
let assetName = ""
|
||||
|
||||
let lualsArch: string = arch
|
||||
if (arch === "arm64") lualsArch = "arm64"
|
||||
else if (arch === "x64") lualsArch = "x64"
|
||||
else if (arch === "ia32") lualsArch = "ia32"
|
||||
|
||||
let lualsPlatform: string = platform
|
||||
if (platform === "darwin") lualsPlatform = "darwin"
|
||||
else if (platform === "linux") lualsPlatform = "linux"
|
||||
else if (platform === "win32") lualsPlatform = "win32"
|
||||
|
||||
const ext = platform === "win32" ? "zip" : "tar.gz"
|
||||
|
||||
assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
|
||||
|
||||
const supportedCombos = [
|
||||
"darwin-arm64.tar.gz",
|
||||
"darwin-x64.tar.gz",
|
||||
"linux-x64.tar.gz",
|
||||
"linux-arm64.tar.gz",
|
||||
"win32-x64.zip",
|
||||
"win32-ia32.zip",
|
||||
]
|
||||
|
||||
const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
|
||||
if (!supportedCombos.includes(assetSuffix)) {
|
||||
log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
|
||||
return
|
||||
}
|
||||
|
||||
const asset = release.assets.find((a: any) => a.name === assetName)
|
||||
if (!asset) {
|
||||
log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
|
||||
return
|
||||
}
|
||||
|
||||
const downloadUrl = asset.browser_download_url
|
||||
const downloadResponse = await fetch(downloadUrl)
|
||||
if (!downloadResponse.ok) {
|
||||
log.error("Failed to download lua-language-server")
|
||||
return
|
||||
}
|
||||
|
||||
const tempPath = path.join(Global.Path.bin, assetName)
|
||||
await Bun.file(tempPath).write(downloadResponse)
|
||||
|
||||
// Unlike zls which is a single self-contained binary,
|
||||
// lua-language-server needs supporting files (meta/, locale/, etc.)
|
||||
// Extract entire archive to dedicated directory to preserve all files
|
||||
const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
|
||||
|
||||
// Remove old installation if exists
|
||||
const stats = await fs.stat(installDir).catch(() => undefined)
|
||||
if (stats) {
|
||||
await fs.rm(installDir, { force: true, recursive: true })
|
||||
}
|
||||
|
||||
await fs.mkdir(installDir, { recursive: true })
|
||||
|
||||
if (ext === "zip") {
|
||||
await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().nothrow()
|
||||
} else {
|
||||
await $`tar -xzf ${tempPath} -C ${installDir}`.nothrow()
|
||||
}
|
||||
|
||||
await fs.rm(tempPath, { force: true })
|
||||
|
||||
// Binary is located in bin/ subdirectory within the extracted archive
|
||||
bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
|
||||
|
||||
if (!(await Bun.file(bin).exists())) {
|
||||
log.error("Failed to extract lua-language-server binary")
|
||||
return
|
||||
}
|
||||
|
||||
if (platform !== "win32") {
|
||||
await $`chmod +x ${bin}`.nothrow()
|
||||
}
|
||||
|
||||
log.info(`installed lua-language-server`, { bin })
|
||||
}
|
||||
|
||||
return {
|
||||
process: spawn(bin, {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Bus } from "../bus"
|
|||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||
import { Hono } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { streamSSE } from "hono/streaming"
|
||||
import { stream, streamSSE } from "hono/streaming"
|
||||
import { Session } from "../session"
|
||||
import z from "zod/v4"
|
||||
import { Provider } from "../provider/provider"
|
||||
|
|
@ -814,10 +814,14 @@ export namespace Server {
|
|||
),
|
||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
const msg = await SessionPrompt.prompt({ ...body, sessionID })
|
||||
return c.json(msg)
|
||||
c.status(200)
|
||||
c.header("Content-Type", "application/json")
|
||||
return stream(c, async (stream) => {
|
||||
const sessionID = c.req.valid("param").id
|
||||
const body = c.req.valid("json")
|
||||
const msg = await SessionPrompt.prompt({ ...body, sessionID })
|
||||
stream.write(JSON.stringify(msg))
|
||||
})
|
||||
},
|
||||
)
|
||||
.post(
|
||||
|
|
|
|||
|
|
@ -269,8 +269,9 @@ export namespace MessageV2 {
|
|||
}),
|
||||
summary: z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
diffs: Snapshot.FileDiff.array(),
|
||||
text: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
}).meta({
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ import { Provider } from "@/provider/provider"
|
|||
import { fn } from "@/util/fn"
|
||||
import z from "zod"
|
||||
import { Session } from "."
|
||||
import { generateText } from "ai"
|
||||
import { generateText, type ModelMessage } from "ai"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
|
||||
import { ProviderTransform } from "@/provider/transform"
|
||||
import { SystemPrompt } from "./system"
|
||||
|
||||
export namespace SessionSummary {
|
||||
export const summarize = fn(
|
||||
z.object({
|
||||
|
|
@ -37,19 +40,43 @@ export namespace SessionSummary {
|
|||
const messages = input.messages.filter(
|
||||
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
|
||||
)
|
||||
const userMsg = messages.find((m) => m.info.id === input.messageID)!
|
||||
const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
|
||||
const userMsg = msgWithParts.info as MessageV2.User
|
||||
const diffs = await computeDiff({ messages })
|
||||
userMsg.info.summary = {
|
||||
userMsg.summary = {
|
||||
...userMsg.summary,
|
||||
diffs,
|
||||
text: "",
|
||||
}
|
||||
if (
|
||||
Flag.OPENCODE_EXPERIMENTAL_TURN_SUMMARY &&
|
||||
messages.every((m) => m.info.role !== "assistant" || m.info.time.completed)
|
||||
) {
|
||||
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
|
||||
const small = await Provider.getSmallModel(assistantMsg.providerID)
|
||||
if (!small) return
|
||||
await Session.updateMessage(userMsg)
|
||||
|
||||
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
|
||||
const small = await Provider.getSmallModel(assistantMsg.providerID)
|
||||
if (!small) return
|
||||
|
||||
const textPart = msgWithParts.parts.find((p) => p.type === "text" && p.synthetic === false) as MessageV2.TextPart
|
||||
if (textPart && !userMsg.summary?.title) {
|
||||
const result = await generateText({
|
||||
maxOutputTokens: small.info.reasoning ? 1500 : 20,
|
||||
providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, {}),
|
||||
messages: [
|
||||
...SystemPrompt.title(small.providerID).map(
|
||||
(x): ModelMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user" as const,
|
||||
content: textPart?.text ?? "",
|
||||
},
|
||||
],
|
||||
model: small.language,
|
||||
})
|
||||
userMsg.summary.title = result.text
|
||||
await Session.updateMessage(userMsg)
|
||||
}
|
||||
|
||||
if (messages.every((m) => m.info.role !== "assistant" || m.info.time.completed)) {
|
||||
const result = await generateText({
|
||||
model: small.language,
|
||||
maxOutputTokens: 100,
|
||||
|
|
@ -65,12 +92,9 @@ export namespace SessionSummary {
|
|||
},
|
||||
],
|
||||
})
|
||||
userMsg.info.summary = {
|
||||
text: result.text,
|
||||
diffs: [],
|
||||
}
|
||||
userMsg.summary.body = result.text
|
||||
await Session.updateMessage(userMsg)
|
||||
}
|
||||
await Session.updateMessage(userMsg.info)
|
||||
}
|
||||
|
||||
export const diff = fn(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export namespace Storage {
|
|||
const MIGRATIONS: Migration[] = [
|
||||
async (dir) => {
|
||||
const project = path.resolve(dir, "../project")
|
||||
if (!fs.exists(project)) return
|
||||
for await (const projectDir of new Bun.Glob("*").scan({
|
||||
cwd: project,
|
||||
onlyFiles: false,
|
||||
|
|
@ -177,8 +178,7 @@ export namespace Storage {
|
|||
|
||||
async function withErrorHandling<T>(body: () => Promise<T>) {
|
||||
return body().catch((e) => {
|
||||
if (!(e instanceof Error))
|
||||
throw e
|
||||
if (!(e instanceof Error)) throw e
|
||||
const errnoException = e as NodeJS.ErrnoException
|
||||
if (errnoException.code === "ENOENT") {
|
||||
throw new NotFoundError({ message: `Resource not found: ${errnoException.path}` })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { BashTool } from "../../src/tool/bash"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
const ctx = {
|
||||
|
|
@ -15,7 +14,6 @@ const ctx = {
|
|||
|
||||
const bash = await BashTool.init()
|
||||
const projectRoot = path.join(__dirname, "../..")
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("tool.bash", () => {
|
||||
test("basic", async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { PatchTool } from "../../src/tool/patch"
|
||||
import { Log } from "../../src/util/log"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import * as fs from "fs/promises"
|
||||
|
|
@ -10,51 +9,46 @@ const ctx = {
|
|||
sessionID: "test",
|
||||
messageID: "",
|
||||
toolCallID: "",
|
||||
agent: "build",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
metadata: () => {},
|
||||
}
|
||||
|
||||
const patchTool = await PatchTool.init()
|
||||
Log.init({ print: false })
|
||||
|
||||
describe("tool.patch", () => {
|
||||
test("should validate required parameters", async () => {
|
||||
await Instance.provide({
|
||||
directory: "/tmp",
|
||||
fn: async () => {
|
||||
await expect(
|
||||
patchTool.execute({ patchText: "" }, ctx)
|
||||
).rejects.toThrow("patchText is required")
|
||||
await expect(patchTool.execute({ patchText: "" }, ctx)).rejects.toThrow("patchText is required")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should validate patch format", async () => {
|
||||
await Instance.provide({
|
||||
directory: "/tmp",
|
||||
fn: async () => {
|
||||
await expect(
|
||||
patchTool.execute({ patchText: "invalid patch" }, ctx)
|
||||
).rejects.toThrow("Failed to parse patch")
|
||||
await expect(patchTool.execute({ patchText: "invalid patch" }, ctx)).rejects.toThrow("Failed to parse patch")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should handle empty patch", async () => {
|
||||
await Instance.provide({
|
||||
directory: "/tmp",
|
||||
fn: async () => {
|
||||
const emptyPatch = `*** Begin Patch
|
||||
*** End Patch`
|
||||
|
||||
await expect(
|
||||
patchTool.execute({ patchText: emptyPatch }, ctx)
|
||||
).rejects.toThrow("No file changes found in patch")
|
||||
|
||||
await expect(patchTool.execute({ patchText: emptyPatch }, ctx)).rejects.toThrow(
|
||||
"No file changes found in patch",
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should reject files outside working directory", async () => {
|
||||
await Instance.provide({
|
||||
directory: "/tmp",
|
||||
|
|
@ -63,17 +57,17 @@ describe("tool.patch", () => {
|
|||
*** Add File: /etc/passwd
|
||||
+malicious content
|
||||
*** End Patch`
|
||||
|
||||
await expect(
|
||||
patchTool.execute({ patchText: maliciousPatch }, ctx)
|
||||
).rejects.toThrow("is not in the current working directory")
|
||||
|
||||
await expect(patchTool.execute({ patchText: maliciousPatch }, ctx)).rejects.toThrow(
|
||||
"is not in the current working directory",
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should handle simple add file operation", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -82,13 +76,13 @@ describe("tool.patch", () => {
|
|||
+Hello World
|
||||
+This is a test file
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText }, ctx)
|
||||
|
||||
|
||||
expect(result.title).toContain("files changed")
|
||||
expect(result.metadata.diff).toBeDefined()
|
||||
expect(result.output).toContain("Patch applied successfully")
|
||||
|
||||
|
||||
// Verify file was created
|
||||
const filePath = path.join(fixture.path, "test-file.txt")
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
|
|
@ -96,10 +90,10 @@ describe("tool.patch", () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should handle file with context update", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -109,24 +103,24 @@ describe("tool.patch", () => {
|
|||
+const DEBUG = false
|
||||
+const VERSION = "1.0"
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText }, ctx)
|
||||
|
||||
|
||||
expect(result.title).toContain("files changed")
|
||||
expect(result.metadata.diff).toBeDefined()
|
||||
expect(result.output).toContain("Patch applied successfully")
|
||||
|
||||
|
||||
// Verify file was created with correct content
|
||||
const filePath = path.join(fixture.path, "config.js")
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
expect(content).toBe("const API_KEY = \"test-key\"\nconst DEBUG = false\nconst VERSION = \"1.0\"")
|
||||
expect(content).toBe('const API_KEY = "test-key"\nconst DEBUG = false\nconst VERSION = "1.0"')
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should handle multiple file operations", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -138,13 +132,13 @@ describe("tool.patch", () => {
|
|||
*** Add File: file3.txt
|
||||
+Content of file 3
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText }, ctx)
|
||||
|
||||
|
||||
expect(result.title).toContain("3 files changed")
|
||||
expect(result.metadata.diff).toBeDefined()
|
||||
expect(result.output).toContain("Patch applied successfully")
|
||||
|
||||
|
||||
// Verify all files were created
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const filePath = path.join(fixture.path, `file${i}.txt`)
|
||||
|
|
@ -154,10 +148,10 @@ describe("tool.patch", () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should create parent directories when adding nested files", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -165,26 +159,29 @@ describe("tool.patch", () => {
|
|||
*** Add File: deep/nested/file.txt
|
||||
+Deep nested content
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText }, ctx)
|
||||
|
||||
|
||||
expect(result.title).toContain("files changed")
|
||||
expect(result.output).toContain("Patch applied successfully")
|
||||
|
||||
|
||||
// Verify nested file was created
|
||||
const nestedPath = path.join(fixture.path, "deep", "nested", "file.txt")
|
||||
const exists = await fs.access(nestedPath).then(() => true).catch(() => false)
|
||||
const exists = await fs
|
||||
.access(nestedPath)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
expect(exists).toBe(true)
|
||||
|
||||
|
||||
const content = await fs.readFile(nestedPath, "utf-8")
|
||||
expect(content).toBe("Deep nested content")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should generate proper unified diff in metadata", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -195,9 +192,9 @@ describe("tool.patch", () => {
|
|||
+line 2
|
||||
+line 3
|
||||
*** End Patch`
|
||||
|
||||
|
||||
await patchTool.execute({ patchText: patchText1 }, ctx)
|
||||
|
||||
|
||||
// Now create an update patch
|
||||
const patchText2 = `*** Begin Patch
|
||||
*** Update File: test.txt
|
||||
|
|
@ -207,9 +204,9 @@ describe("tool.patch", () => {
|
|||
+line 2 updated
|
||||
line 3
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText: patchText2 }, ctx)
|
||||
|
||||
|
||||
expect(result.metadata.diff).toBeDefined()
|
||||
expect(result.metadata.diff).toContain("@@")
|
||||
expect(result.metadata.diff).toContain("-line 2")
|
||||
|
|
@ -217,10 +214,10 @@ describe("tool.patch", () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
test("should handle complex patch with multiple operations", async () => {
|
||||
await using fixture = await tmpdir()
|
||||
|
||||
|
||||
await Instance.provide({
|
||||
directory: fixture.path,
|
||||
fn: async () => {
|
||||
|
|
@ -238,26 +235,26 @@ describe("tool.patch", () => {
|
|||
+ "debug": true
|
||||
+}
|
||||
*** End Patch`
|
||||
|
||||
|
||||
const result = await patchTool.execute({ patchText }, ctx)
|
||||
|
||||
|
||||
expect(result.title).toContain("3 files changed")
|
||||
expect(result.metadata.diff).toBeDefined()
|
||||
expect(result.output).toContain("Patch applied successfully")
|
||||
|
||||
|
||||
// Verify all files were created
|
||||
const newPath = path.join(fixture.path, "new.txt")
|
||||
const newContent = await fs.readFile(newPath, "utf-8")
|
||||
expect(newContent).toBe("This is a new file\nwith multiple lines")
|
||||
|
||||
|
||||
const existingPath = path.join(fixture.path, "existing.txt")
|
||||
const existingContent = await fs.readFile(existingPath, "utf-8")
|
||||
expect(existingContent).toBe("old content\nnew line\nmore content")
|
||||
|
||||
|
||||
const configPath = path.join(fixture.path, "config.json")
|
||||
const configContent = await fs.readFile(configPath, "utf-8")
|
||||
expect(configContent).toBe('{\n "version": "1.0",\n "debug": true\n}')
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
@ -24,4 +24,4 @@
|
|||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"typecheck": "tsgo --noEmit",
|
||||
|
|
@ -26,4 +26,4 @@
|
|||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -614,8 +614,9 @@ export type UserMessage = {
|
|||
created: number
|
||||
}
|
||||
summary?: {
|
||||
title?: string
|
||||
body?: string
|
||||
diffs: Array<FileDiff>
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bun run src/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/components/index.ts",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
|
|||
| svelte | .svelte | Auto-installs for Svelte projects |
|
||||
| astro | .astro | Auto-installs for Astro projects |
|
||||
| jdtls | .java | `Java SDK (version 21+)` installed |
|
||||
| lua-ls | .lua | Auto-installs for Lua projects |
|
||||
|
||||
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "0.15.15",
|
||||
"version": "0.15.16",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue