Compare commits

...

10 Commits

Author SHA1 Message Date
opencode
84f0c63fa1 release: v0.7.2
Some checks failed
Format / format (push) Has been cancelled
stats / stats (push) Has been cancelled
2025-09-11 17:02:59 +00:00
Dax Raad
3e9b451fb4 reduce LSP verbosity 2025-09-11 12:54:12 -04:00
Dax Raad
4ccf683527 remove block anchor edit 2025-09-11 12:53:10 -04:00
GitHub Action
b236ca9047 ignore: update download stats 2025-09-11 2025-09-11 12:04:26 +00:00
Dax Raad
aa9ebe5d7c ignore: compacting 2025-09-11 02:31:28 -04:00
Dax Raad
4c94753eda compaction improvements 2025-09-11 02:22:51 -04:00
GitHub Action
c3a55c35bb chore: format code 2025-09-11 05:33:59 +00:00
Frank
d5275010d5 wip: zen 2025-09-11 01:33:23 -04:00
Frank
dedfa563c2 wip: zen 2025-09-11 01:32:06 -04:00
GitHub Action
7aa57accf5 chore: format code 2025-09-11 03:59:39 +00:00
38 changed files with 597 additions and 200 deletions

View File

@@ -75,3 +75,4 @@
| 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) | | 2025-09-08 | 293,341 (+4,718) | 227,073 (+1,207) | 520,414 (+5,925) |
| 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) | | 2025-09-09 | 300,036 (+6,695) | 229,788 (+2,715) | 529,824 (+9,410) |
| 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) | | 2025-09-10 | 307,287 (+7,251) | 233,435 (+3,647) | 540,722 (+10,898) |
| 2025-09-11 | 314,083 (+6,796) | 237,356 (+3,921) | 551,439 (+10,717) |

View File

@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev", "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../packages/opencode/script/schema.ts ./.output/public/config.json", "build": "vinxi build && ../../packages/opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start", "start": "vinxi start",
"version": "0.7.1" "version": "0.7.2"
}, },
"dependencies": { "dependencies": {
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",

View File

@@ -551,7 +551,7 @@ function NewUserSection() {
) )
} }
export default function() { export default function () {
const params = useParams() const params = useParams()
const keys = createAsync(() => listKeys(params.id)) const keys = createAsync(() => listKeys(params.id))
const usage = createAsync(() => getUsageInfo(params.id)) const usage = createAsync(() => getUsageInfo(params.id))

View File

@@ -6,9 +6,7 @@ export function POST(input: APIEvent) {
return handler(input, { return handler(input, {
modifyBody: (body: any) => ({ modifyBody: (body: any) => ({
...body, ...body,
stream_options: { ...(body.stream ? { stream_options: { include_usage: true } } : {}),
include_usage: true,
},
}), }),
setAuthHeader: (headers: Headers, apiKey: string) => { setAuthHeader: (headers: Headers, apiKey: string) => {
headers.set("authorization", `Bearer ${apiKey}`) headers.set("authorization", `Bearer ${apiKey}`)

View File

@@ -1,4 +1,5 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import path from "node:path"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js" import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js" import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js" import { BillingTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
@@ -6,6 +7,29 @@ import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
import { Identifier } from "@opencode/cloud-core/identifier.js" import { Identifier } from "@opencode/cloud-core/identifier.js"
import { Resource } from "@opencode/cloud-resource" import { Resource } from "@opencode/cloud-resource"
type ModelCost = {
input: number
output: number
cacheRead: number
cacheWrite5m: number
cacheWrite1h: number
}
type Model = {
id: string
auth: boolean
cost: ModelCost | ((usage: any) => ModelCost)
headerMappings: Record<string, string>
providers: Record<
string,
{
api: string
apiKey: string
model: string
weight?: number
}
>
}
export async function handler( export async function handler(
input: APIEvent, input: APIEvent,
opts: { opts: {
@@ -28,13 +52,10 @@ export async function handler(
class CreditsError extends Error {} class CreditsError extends Error {}
class ModelError extends Error {} class ModelError extends Error {}
const MODELS = { const MODELS: Record<string, Model> = {
"claude-opus-4-1": { "claude-opus-4-1": {
id: "claude-opus-4-1" as const, id: "claude-opus-4-1" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
cost: { cost: {
input: 0.000015, input: 0.000015,
output: 0.000075, output: 0.000075,
@@ -43,13 +64,17 @@ export async function handler(
cacheWrite1h: 0.00003, cacheWrite1h: 0.00003,
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
},
},
}, },
"claude-sonnet-4": { "claude-sonnet-4": {
id: "claude-sonnet-4" as const, id: "claude-sonnet-4" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
cost: (usage: any) => { cost: (usage: any) => {
const totalInputTokens = const totalInputTokens =
usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
@@ -70,13 +95,17 @@ export async function handler(
} }
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
},
},
}, },
"claude-3-5-haiku": { "claude-3-5-haiku": {
id: "claude-3-5-haiku" as const, id: "claude-3-5-haiku" as const,
auth: true, auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
cost: { cost: {
input: 0.0000008, input: 0.0000008,
output: 0.000004, output: 0.000004,
@@ -85,13 +114,17 @@ export async function handler(
cacheWrite1h: 0.0000016, cacheWrite1h: 0.0000016,
}, },
headerMappings: {}, headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
},
},
}, },
"gpt-5": { "gpt-5": {
id: "gpt-5" as const, id: "gpt-5" as const,
auth: true, auth: true,
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
cost: { cost: {
input: 0.00000125, input: 0.00000125,
output: 0.00001, output: 0.00001,
@@ -100,28 +133,43 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
openai: {
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
},
},
}, },
"qwen3-coder": { "qwen3-coder": {
id: "qwen3-coder" as const, id: "qwen3-coder" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: { cost: {
input: 0.00000038, input: 0.00000045,
output: 0.00000153, output: 0.0000018,
cacheRead: 0, cacheRead: 0,
cacheWrite5m: 0, cacheWrite5m: 0,
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
weight: 4,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
weight: 1,
},
},
}, },
"kimi-k2": { "kimi-k2": {
id: "kimi-k2" as const, id: "kimi-k2" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "moonshotai/Kimi-K2-Instruct-0905",
cost: { cost: {
input: 0.0000006, input: 0.0000006,
output: 0.0000025, output: 0.0000025,
@@ -130,13 +178,24 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "moonshotai/Kimi-K2-Instruct-0905",
weight: 4,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/kimi-k2-instruct-0905",
weight: 1,
},
},
}, },
"grok-code": { "grok-code": {
id: "grok-code" as const, id: "grok-code" as const,
auth: false, auth: false,
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
cost: { cost: {
input: 0, input: 0,
output: 0, output: 0,
@@ -148,14 +207,18 @@ export async function handler(
"x-grok-conv-id": "x-opencode-session", "x-grok-conv-id": "x-opencode-session",
"x-grok-req-id": "x-opencode-request", "x-grok-req-id": "x-opencode-request",
}, },
providers: {
xai: {
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
},
},
}, },
// deprecated // deprecated
"qwen/qwen3-coder": { "qwen/qwen3-coder": {
id: "qwen/qwen3-coder" as const, id: "qwen/qwen3-coder" as const,
auth: true, auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: { cost: {
input: 0.00000038, input: 0.00000038,
output: 0.00000153, output: 0.00000153,
@@ -164,6 +227,20 @@ export async function handler(
cacheWrite1h: 0, cacheWrite1h: 0,
}, },
headerMappings: {}, headerMappings: {},
providers: {
baseten: {
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
weight: 5,
},
fireworks: {
api: "https://api.fireworks.ai/inference",
apiKey: Resource.FIREWORKS_API_KEY.value,
model: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct",
weight: 1,
},
},
}, },
} }
@@ -195,15 +272,19 @@ export async function handler(
const apiKey = await authenticate() const apiKey = await authenticate()
const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "") const isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
await checkCredits() await checkCredits()
const providerName = selectProvider()
const providerData = MODEL.providers[providerName]
logger.metric({ provider: providerName })
// Request to model provider // Request to model provider
const res = await fetch(new URL(url.pathname.replace(/^\/zen/, "") + url.search, MODEL.api), { const startTimestamp = Date.now()
const res = await fetch(path.posix.join(providerData.api, url.pathname.replace(/^\/zen/, "") + url.search), {
method: "POST", method: "POST",
headers: (() => { headers: (() => {
const headers = input.request.headers const headers = input.request.headers
headers.delete("host") headers.delete("host")
headers.delete("content-length") headers.delete("content-length")
opts.setAuthHeader(headers, MODEL.apiKey) opts.setAuthHeader(headers, providerData.apiKey)
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => { Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!) headers.set(k, headers.get(v)!)
}) })
@@ -211,7 +292,7 @@ export async function handler(
})(), })(),
body: JSON.stringify({ body: JSON.stringify({
...(opts.modifyBody?.(body) ?? body), ...(opts.modifyBody?.(body) ?? body),
model: MODEL.model, model: providerData.model,
}), }),
}) })
@@ -245,7 +326,6 @@ export async function handler(
const decoder = new TextDecoder() const decoder = new TextDecoder()
let buffer = "" let buffer = ""
let responseLength = 0 let responseLength = 0
let startTimestamp = Date.now()
function pump(): Promise<void> { function pump(): Promise<void> {
return ( return (
@@ -262,7 +342,6 @@ export async function handler(
logger.metric({ time_to_first_byte: Date.now() - startTimestamp }) logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
} }
responseLength += value.length responseLength += value.length
buffer += decoder.decode(value, { stream: true }) buffer += decoder.decode(value, { stream: true })
const parts = buffer.split("\n\n") const parts = buffer.split("\n\n")
@@ -344,6 +423,13 @@ export async function handler(
if (billing.balance <= 0) throw new CreditsError("Insufficient balance") if (billing.balance <= 0) throw new CreditsError("Insufficient balance")
} }
function selectProvider() {
const picks = Object.entries(MODEL.providers).flatMap(([name, provider]) =>
Array<string>(provider.weight ?? 1).fill(name),
)
return picks[Math.floor(Math.random() * picks.length)]
}
async function trackUsage(usage: any) { async function trackUsage(usage: any) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
opts.normalizeUsage(usage) opts.normalizeUsage(usage)
@@ -416,9 +502,25 @@ export async function handler(
"error.message": error.message, "error.message": error.message,
}) })
// Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message.
if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError) if (error instanceof AuthError || error instanceof CreditsError || error instanceof ModelError)
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 401 }) return new Response(
JSON.stringify({
type: "error",
error: { type: error.constructor.name, message: error.message },
}),
{ status: 401 },
)
return new Response(JSON.stringify({ error: { message: error.message } }), { status: 500 }) return new Response(
JSON.stringify({
type: "error",
error: {
type: "error",
message: error.message,
},
}),
{ status: 500 },
)
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core", "name": "@opencode/cloud-core",
"version": "0.7.1", "version": "0.7.2",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode/cloud-function", "name": "@opencode/cloud-function",
"version": "0.7.1", "version": "0.7.2",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -30,6 +30,10 @@ declare module "sst" {
type: "sst.sst.Linkable" type: "sst.sst.Linkable"
username: string username: string
} }
FIREWORKS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
GITHUB_APP_ID: { GITHUB_APP_ID: {
type: "sst.sst.Secret" type: "sst.sst.Secret"
value: string value: string

View File

@@ -30,6 +30,10 @@ declare module "sst" {
type: "sst.sst.Linkable" type: "sst.sst.Linkable"
username: string username: string
} }
FIREWORKS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
GITHUB_APP_ID: { GITHUB_APP_ID: {
type: "sst.sst.Secret" type: "sst.sst.Secret"
value: string value: string

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode/cloud-scripts", "name": "@opencode/cloud-scripts",
"version": "0.7.1", "version": "0.7.2",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -103,6 +103,7 @@ const ANTHROPIC_API_KEY = new sst.Secret("ANTHROPIC_API_KEY")
const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY") const OPENAI_API_KEY = new sst.Secret("OPENAI_API_KEY")
const XAI_API_KEY = new sst.Secret("XAI_API_KEY") const XAI_API_KEY = new sst.Secret("XAI_API_KEY")
const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY") const BASETEN_API_KEY = new sst.Secret("BASETEN_API_KEY")
const FIREWORKS_API_KEY = new sst.Secret("FIREWORKS_API_KEY")
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY") const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", { const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
properties: { value: auth.url.apply((url) => url!) }, properties: { value: auth.url.apply((url) => url!) },
@@ -136,6 +137,7 @@ new sst.cloudflare.x.SolidStart("Console", {
OPENAI_API_KEY, OPENAI_API_KEY,
XAI_API_KEY, XAI_API_KEY,
BASETEN_API_KEY, BASETEN_API_KEY,
FIREWORKS_API_KEY,
], ],
environment: { environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!), //VITE_DOCS_URL: web.url.apply((url) => url!),

View File

@@ -1,6 +1,6 @@
{ {
"name": "@opencode/function", "name": "@opencode/function",
"version": "0.7.1", "version": "0.7.2",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -30,6 +30,10 @@ declare module "sst" {
type: "sst.sst.Linkable" type: "sst.sst.Linkable"
username: string username: string
} }
FIREWORKS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
GITHUB_APP_ID: { GITHUB_APP_ID: {
type: "sst.sst.Secret" type: "sst.sst.Secret"
value: string value: string

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "0.7.1", "version": "0.7.2",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,

View File

@@ -16,6 +16,7 @@ import { Ide } from "../../ide"
import { Flag } from "../../flag/flag" import { Flag } from "../../flag/flag"
import { Session } from "../../session" import { Session } from "../../session"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { $ } from "bun"
declare global { declare global {
const OPENCODE_TUI_PATH: string const OPENCODE_TUI_PATH: string
@@ -111,8 +112,7 @@ export const TuiCommand = cmd({
hostname: args.hostname, hostname: args.hostname,
}) })
let cmd = ["go", "run", "./main.go"] let cmd = [] as string[]
let cwd = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File const tui = Bun.embeddedFiles.find((item) => (item as File).name.includes("tui")) as File
if (tui) { if (tui) {
let binaryName = tui.name let binaryName = tui.name
@@ -125,9 +125,13 @@ export const TuiCommand = cmd({
await Bun.write(file, tui, { mode: 0o755 }) await Bun.write(file, tui, { mode: 0o755 })
await fs.chmod(binary, 0o755) await fs.chmod(binary, 0o755)
} }
cwd = process.cwd()
cmd = [binary] cmd = [binary]
} }
if (!tui) {
const dir = Bun.fileURLToPath(new URL("../../../../tui/cmd/opencode", import.meta.url))
await $`go build -o ./dist/tui ./main.go`.cwd(dir)
cmd = [path.join(dir, "dist/tui")]
}
Log.Default.info("tui", { Log.Default.info("tui", {
cmd, cmd,
}) })

View File

@@ -30,7 +30,7 @@ export namespace Identifier {
function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string { function generateID(prefix: keyof typeof prefixes, descending: boolean, given?: string): string {
if (!given) { if (!given) {
return generateNewID(prefix, descending) return create(prefix, descending)
} }
if (!given.startsWith(prefixes[prefix])) { if (!given.startsWith(prefixes[prefix])) {
@@ -49,8 +49,8 @@ export namespace Identifier {
return result return result
} }
function generateNewID(prefix: keyof typeof prefixes, descending: boolean): string { export function create(prefix: keyof typeof prefixes, descending: boolean, timestamp?: number): string {
const currentTimestamp = Date.now() const currentTimestamp = timestamp ?? Date.now()
if (currentTimestamp !== lastTimestamp) { if (currentTimestamp !== lastTimestamp) {
lastTimestamp = currentTimestamp lastTimestamp = currentTimestamp

View File

@@ -86,6 +86,7 @@ export namespace Session {
time: z.object({ time: z.object({
created: z.number(), created: z.number(),
updated: z.number(), updated: z.number(),
compacting: z.number().optional(),
}), }),
revert: z revert: z
.object({ .object({
@@ -137,12 +138,17 @@ export namespace Session {
error: MessageV2.Assistant.shape.error, error: MessageV2.Assistant.shape.error,
}), }),
), ),
Compacted: Bus.event(
"session.compacted",
z.object({
sessionID: z.string(),
}),
),
} }
const state = Instance.state( const state = Instance.state(
() => { () => {
const pending = new Map<string, AbortController>() const pending = new Map<string, AbortController>()
const autoCompacting = new Map<string, boolean>()
const queued = new Map< const queued = new Map<
string, string,
{ {
@@ -156,7 +162,6 @@ export namespace Session {
return { return {
pending, pending,
autoCompacting,
queued, queued,
} }
}, },
@@ -714,24 +719,8 @@ export namespace Session {
})().then((x) => Provider.getModel(x.providerID, x.modelID)) })().then((x) => Provider.getModel(x.providerID, x.modelID))
let msgs = await messages(input.sessionID) let msgs = await messages(input.sessionID)
const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
// auto summarize if too long
if (previous && previous.tokens) {
const tokens =
previous.tokens.input + previous.tokens.cache.read + previous.tokens.cache.write + previous.tokens.output
if (model.info.limit.context && tokens > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
state().autoCompacting.set(input.sessionID, true)
await summarize({
sessionID: input.sessionID,
providerID: model.providerID,
modelID: model.info.id,
})
return prompt(input)
}
}
using abort = lock(input.sessionID) using abort = lock(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
@@ -999,7 +988,38 @@ export namespace Session {
error: e, error: e,
}) })
}, },
async prepareStep({ messages }) { async prepareStep({ messages, steps }) {
// Auto compact if too long
const tokens = (() => {
if (steps.length) {
const previous = steps.at(-1)
if (previous) return getUsage(model.info, previous.usage, previous.providerMetadata).tokens
}
const msg = msgs.findLast((x) => x.info.role === "assistant")?.info as MessageV2.Assistant
if (msg && msg.tokens) {
return msg.tokens
}
})()
if (tokens) {
log.info("compact check", tokens)
const count = tokens.input + tokens.cache.read + tokens.cache.write + tokens.output
if (model.info.limit.context && count > Math.max((model.info.limit.context - outputLimit) * 0.9, 0)) {
log.info("compacting in prepareStep")
const summarized = await summarize({
sessionID: input.sessionID,
providerID: model.providerID,
modelID: model.info.id,
})
const msgs = await Session.messages(input.sessionID).then((x) =>
x.filter((x) => x.info.id >= summarized.id),
)
return {
messages: MessageV2.toModelMessage(msgs),
}
}
}
// Add queued messages to the stream
const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed) const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
if (queue.length) { if (queue.length) {
for (const item of queue) { for (const item of queue) {
@@ -1756,10 +1776,22 @@ export namespace Session {
} }
export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) { export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
using abort = lock(input.sessionID) await update(input.sessionID, (draft) => {
draft.time.compacting = Date.now()
})
await using _ = defer(async () => {
await update(input.sessionID, (draft) => {
draft.time.compacting = undefined
})
})
const msgs = await messages(input.sessionID) const msgs = await messages(input.sessionID)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true) const start = Math.max(
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id) 0,
msgs.findLastIndex((msg) => msg.info.role === "assistant" && msg.info.summary === true),
)
const split = start + Math.floor((msgs.length - start) / 2)
log.info("summarizing", { start, split })
const toSummarize = msgs.slice(start, split)
const model = await Provider.getModel(input.providerID, input.modelID) const model = await Provider.getModel(input.providerID, input.modelID)
const system = [ const system = [
...SystemPrompt.summarize(model.providerID), ...SystemPrompt.summarize(model.providerID),
@@ -1767,36 +1799,8 @@ export namespace Session {
...(await SystemPrompt.custom()), ...(await SystemPrompt.custom()),
] ]
const next: MessageV2.Info = { const generated = await generateText({
id: Identifier.ascending("message"),
role: "assistant",
sessionID: input.sessionID,
system,
mode: "build",
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
summary: true,
cost: 0,
modelID: input.modelID,
providerID: model.providerID,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: { read: 0, write: 0 },
},
time: {
created: Date.now(),
},
}
await updateMessage(next)
const processor = createProcessor(next, model.info)
const stream = streamText({
maxRetries: 10, maxRetries: 10,
abortSignal: abort.signal,
model: model.language, model: model.language,
messages: [ messages: [
...system.map( ...system.map(
@@ -1805,7 +1809,7 @@ export namespace Session {
content: x, content: x,
}), }),
), ),
...MessageV2.toModelMessage(filtered), ...MessageV2.toModelMessage(toSummarize),
{ {
role: "user", role: "user",
content: [ content: [
@@ -1817,9 +1821,45 @@ export namespace Session {
}, },
], ],
}) })
const usage = getUsage(model.info, generated.usage, generated.providerMetadata)
const msg: MessageV2.Info = {
id: Identifier.create("message", false, toSummarize.at(-1)!.info.time.created + 1),
role: "assistant",
sessionID: input.sessionID,
system,
mode: "build",
path: {
cwd: Instance.directory,
root: Instance.worktree,
},
summary: true,
cost: usage.cost,
tokens: usage.tokens,
modelID: input.modelID,
providerID: model.providerID,
time: {
created: Date.now(),
completed: Date.now(),
},
}
await updateMessage(msg)
await updatePart({
type: "text",
sessionID: input.sessionID,
messageID: msg.id,
id: Identifier.ascending("part"),
text: generated.text,
time: {
start: Date.now(),
end: Date.now(),
},
})
const result = await processor.process(stream) Bus.publish(Event.Compacted, {
return result sessionID: input.sessionID,
})
return msg
} }
function isLocked(sessionID: string) { function isLocked(sessionID: string) {
@@ -1837,12 +1877,6 @@ export namespace Session {
log.info("unlocking", { sessionID }) log.info("unlocking", { sessionID })
state().pending.delete(sessionID) state().pending.delete(sessionID)
const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
if (isAutoCompacting) {
state().autoCompacting.delete(sessionID)
return
}
const session = await get(sessionID) const session = await get(sessionID)
if (session.parentID) return if (session.parentID) return

View File

@@ -107,14 +107,12 @@ export const EditTool = Tool.define("edit", {
for (const [file, issues] of Object.entries(diagnostics)) { for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue if (issues.length === 0) continue
if (file === filePath) { if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n` output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</file_diagnostics>\n`
continue continue
} }
output += `\n<project_diagnostics>\n${file}\n${issues
// TODO: may want to make more leniant for eslint
.filter((item) => item.severity === 1)
.map(LSP.Diagnostic.pretty)
.join("\n")}\n</project_diagnostics>\n`
} }
return { return {
@@ -599,7 +597,7 @@ export function replace(content: string, oldString: string, newString: string, r
for (const replacer of [ for (const replacer of [
SimpleReplacer, SimpleReplacer,
LineTrimmedReplacer, LineTrimmedReplacer,
BlockAnchorReplacer, // BlockAnchorReplacer,
WhitespaceNormalizedReplacer, WhitespaceNormalizedReplacer,
IndentationFlexibleReplacer, IndentationFlexibleReplacer,
EscapeNormalizedReplacer, EscapeNormalizedReplacer,

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "0.7.1", "version": "0.7.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@@ -1,3 +1,3 @@
{ {
".": "0.8.0" ".": "0.9.0"
} }

View File

@@ -1,4 +1,4 @@
configured_endpoints: 43 configured_endpoints: 43
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-97b61518d8666ea7cb310af04248e00bcf8dc9753ba3c7e84471df72b3232004.yml openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-46826ba8640557721614b0c9a3f1860681d825ca8d8b12869652fa25aacb0b4c.yml
openapi_spec_hash: a3500531973ad999c350b87c21aa3ab8 openapi_spec_hash: 33b8db6fde3021579b21325ce910197d
config_hash: 026ef000d34bf2f930e7b41e77d2d3ff config_hash: 026ef000d34bf2f930e7b41e77d2d3ff

View File

@@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.9.0 (2025-09-10)
Full Changelog: [v0.8.0...v0.9.0](https://github.com/sst/opencode-sdk-go/compare/v0.8.0...v0.9.0)
### Features
- **api:** api update ([2d3a28d](https://github.com/sst/opencode-sdk-go/commit/2d3a28df5657845aa4d73087e1737d1fc8c3ce1c))
## 0.8.0 (2025-09-01) ## 0.8.0 (2025-09-01)
Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0) Full Changelog: [v0.7.0...v0.8.0](https://github.com/sst/opencode-sdk-go/compare/v0.7.0...v0.8.0)

View File

@@ -24,7 +24,7 @@ Or to pin the version:
<!-- x-release-please-start-version --> <!-- x-release-please-start-version -->
```sh ```sh
go get -u 'github.com/sst/opencode-sdk-go@v0.8.0' go get -u 'github.com/sst/opencode-sdk-go@v0.9.0'
``` ```
<!-- x-release-please-end --> <!-- x-release-please-end -->

View File

@@ -50,33 +50,37 @@ func (r *AppService) Providers(ctx context.Context, query AppProvidersParams, op
} }
type Model struct { type Model struct {
ID string `json:"id,required"` ID string `json:"id,required"`
Attachment bool `json:"attachment,required"` Attachment bool `json:"attachment,required"`
Cost ModelCost `json:"cost,required"` Cost ModelCost `json:"cost,required"`
Limit ModelLimit `json:"limit,required"` Limit ModelLimit `json:"limit,required"`
Name string `json:"name,required"` Name string `json:"name,required"`
Options map[string]interface{} `json:"options,required"` Options map[string]interface{} `json:"options,required"`
Reasoning bool `json:"reasoning,required"` Reasoning bool `json:"reasoning,required"`
ReleaseDate string `json:"release_date,required"` ReleaseDate string `json:"release_date,required"`
Temperature bool `json:"temperature,required"` Temperature bool `json:"temperature,required"`
ToolCall bool `json:"tool_call,required"` ToolCall bool `json:"tool_call,required"`
JSON modelJSON `json:"-"` Experimental bool `json:"experimental"`
Provider ModelProvider `json:"provider"`
JSON modelJSON `json:"-"`
} }
// modelJSON contains the JSON metadata for the struct [Model] // modelJSON contains the JSON metadata for the struct [Model]
type modelJSON struct { type modelJSON struct {
ID apijson.Field ID apijson.Field
Attachment apijson.Field Attachment apijson.Field
Cost apijson.Field Cost apijson.Field
Limit apijson.Field Limit apijson.Field
Name apijson.Field Name apijson.Field
Options apijson.Field Options apijson.Field
Reasoning apijson.Field Reasoning apijson.Field
ReleaseDate apijson.Field ReleaseDate apijson.Field
Temperature apijson.Field Temperature apijson.Field
ToolCall apijson.Field ToolCall apijson.Field
raw string Experimental apijson.Field
ExtraFields map[string]apijson.Field Provider apijson.Field
raw string
ExtraFields map[string]apijson.Field
} }
func (r *Model) UnmarshalJSON(data []byte) (err error) { func (r *Model) UnmarshalJSON(data []byte) (err error) {
@@ -135,6 +139,26 @@ func (r modelLimitJSON) RawJSON() string {
return r.raw return r.raw
} }
type ModelProvider struct {
Npm string `json:"npm,required"`
JSON modelProviderJSON `json:"-"`
}
// modelProviderJSON contains the JSON metadata for the struct [ModelProvider]
type modelProviderJSON struct {
Npm apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ModelProvider) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r modelProviderJSON) RawJSON() string {
return r.raw
}
type Provider struct { type Provider struct {
ID string `json:"id,required"` ID string `json:"id,required"`
Env []string `json:"env,required"` Env []string `json:"env,required"`

View File

@@ -1562,34 +1562,38 @@ func (r configProviderJSON) RawJSON() string {
} }
type ConfigProviderModel struct { type ConfigProviderModel struct {
ID string `json:"id"` ID string `json:"id"`
Attachment bool `json:"attachment"` Attachment bool `json:"attachment"`
Cost ConfigProviderModelsCost `json:"cost"` Cost ConfigProviderModelsCost `json:"cost"`
Limit ConfigProviderModelsLimit `json:"limit"` Experimental bool `json:"experimental"`
Name string `json:"name"` Limit ConfigProviderModelsLimit `json:"limit"`
Options map[string]interface{} `json:"options"` Name string `json:"name"`
Reasoning bool `json:"reasoning"` Options map[string]interface{} `json:"options"`
ReleaseDate string `json:"release_date"` Provider ConfigProviderModelsProvider `json:"provider"`
Temperature bool `json:"temperature"` Reasoning bool `json:"reasoning"`
ToolCall bool `json:"tool_call"` ReleaseDate string `json:"release_date"`
JSON configProviderModelJSON `json:"-"` Temperature bool `json:"temperature"`
ToolCall bool `json:"tool_call"`
JSON configProviderModelJSON `json:"-"`
} }
// configProviderModelJSON contains the JSON metadata for the struct // configProviderModelJSON contains the JSON metadata for the struct
// [ConfigProviderModel] // [ConfigProviderModel]
type configProviderModelJSON struct { type configProviderModelJSON struct {
ID apijson.Field ID apijson.Field
Attachment apijson.Field Attachment apijson.Field
Cost apijson.Field Cost apijson.Field
Limit apijson.Field Experimental apijson.Field
Name apijson.Field Limit apijson.Field
Options apijson.Field Name apijson.Field
Reasoning apijson.Field Options apijson.Field
ReleaseDate apijson.Field Provider apijson.Field
Temperature apijson.Field Reasoning apijson.Field
ToolCall apijson.Field ReleaseDate apijson.Field
raw string Temperature apijson.Field
ExtraFields map[string]apijson.Field ToolCall apijson.Field
raw string
ExtraFields map[string]apijson.Field
} }
func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) { func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) {
@@ -1650,6 +1654,27 @@ func (r configProviderModelsLimitJSON) RawJSON() string {
return r.raw return r.raw
} }
type ConfigProviderModelsProvider struct {
Npm string `json:"npm,required"`
JSON configProviderModelsProviderJSON `json:"-"`
}
// configProviderModelsProviderJSON contains the JSON metadata for the struct
// [ConfigProviderModelsProvider]
type configProviderModelsProviderJSON struct {
Npm apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *ConfigProviderModelsProvider) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r configProviderModelsProviderJSON) RawJSON() string {
return r.raw
}
type ConfigProviderOptions struct { type ConfigProviderOptions struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`

View File

@@ -63,7 +63,8 @@ type EventListResponse struct {
// [EventListResponseEventSessionUpdatedProperties], // [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties], // [EventListResponseEventSessionDeletedProperties],
// [EventListResponseEventSessionIdleProperties], // [EventListResponseEventSessionIdleProperties],
// [EventListResponseEventSessionErrorProperties], [interface{}]. // [EventListResponseEventSessionErrorProperties],
// [EventListResponseEventSessionCompactedProperties], [interface{}].
Properties interface{} `json:"properties,required"` Properties interface{} `json:"properties,required"`
Type EventListResponseType `json:"type,required"` Type EventListResponseType `json:"type,required"`
JSON eventListResponseJSON `json:"-"` JSON eventListResponseJSON `json:"-"`
@@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError], // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted],
// [EventListResponseEventServerConnected]. // [EventListResponseEventServerConnected].
func (r EventListResponse) AsUnion() EventListResponseUnion { func (r EventListResponse) AsUnion() EventListResponseUnion {
return r.union return r.union
@@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// [EventListResponseEventPermissionUpdated], // [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited], // [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted], // [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or // [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted] or
// [EventListResponseEventServerConnected]. // [EventListResponseEventServerConnected].
type EventListResponseUnion interface { type EventListResponseUnion interface {
implementsEventListResponse() implementsEventListResponse()
@@ -193,6 +196,11 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventSessionError{}), Type: reflect.TypeOf(EventListResponseEventSessionError{}),
DiscriminatorValue: "session.error", DiscriminatorValue: "session.error",
}, },
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}),
DiscriminatorValue: "session.compacted",
},
apijson.UnionVariant{ apijson.UnionVariant{
TypeFilter: gjson.JSON, TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}), Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
@@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
return false return false
} }
type EventListResponseEventSessionCompacted struct {
Properties EventListResponseEventSessionCompactedProperties `json:"properties,required"`
Type EventListResponseEventSessionCompactedType `json:"type,required"`
JSON eventListResponseEventSessionCompactedJSON `json:"-"`
}
// eventListResponseEventSessionCompactedJSON contains the JSON metadata for the
// struct [EventListResponseEventSessionCompacted]
type eventListResponseEventSessionCompactedJSON struct {
Properties apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventSessionCompacted) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventSessionCompactedJSON) RawJSON() string {
return r.raw
}
func (r EventListResponseEventSessionCompacted) implementsEventListResponse() {}
type EventListResponseEventSessionCompactedProperties struct {
SessionID string `json:"sessionID,required"`
JSON eventListResponseEventSessionCompactedPropertiesJSON `json:"-"`
}
// eventListResponseEventSessionCompactedPropertiesJSON contains the JSON metadata
// for the struct [EventListResponseEventSessionCompactedProperties]
type eventListResponseEventSessionCompactedPropertiesJSON struct {
SessionID apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *EventListResponseEventSessionCompactedProperties) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r eventListResponseEventSessionCompactedPropertiesJSON) RawJSON() string {
return r.raw
}
type EventListResponseEventSessionCompactedType string
const (
EventListResponseEventSessionCompactedTypeSessionCompacted EventListResponseEventSessionCompactedType = "session.compacted"
)
func (r EventListResponseEventSessionCompactedType) IsKnown() bool {
switch r {
case EventListResponseEventSessionCompactedTypeSessionCompacted:
return true
}
return false
}
type EventListResponseEventServerConnected struct { type EventListResponseEventServerConnected struct {
Properties interface{} `json:"properties,required"` Properties interface{} `json:"properties,required"`
Type EventListResponseEventServerConnectedType `json:"type,required"` Type EventListResponseEventServerConnectedType `json:"type,required"`
@@ -1163,12 +1231,13 @@ const (
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted" EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
EventListResponseTypeSessionIdle EventListResponseType = "session.idle" EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
EventListResponseTypeSessionError EventListResponseType = "session.error" EventListResponseTypeSessionError EventListResponseType = "session.error"
EventListResponseTypeSessionCompacted EventListResponseType = "session.compacted"
EventListResponseTypeServerConnected EventListResponseType = "server.connected" EventListResponseTypeServerConnected EventListResponseType = "server.connected"
) )
func (r EventListResponseType) IsKnown() bool { func (r EventListResponseType) IsKnown() bool {
switch r { switch r {
case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected: case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeSessionCompacted, EventListResponseTypeServerConnected:
return true return true
} }
return false return false

View File

@@ -100,15 +100,17 @@ func (r FileStatus) IsKnown() bool {
} }
type FileNode struct { type FileNode struct {
Ignored bool `json:"ignored,required"` Absolute string `json:"absolute,required"`
Name string `json:"name,required"` Ignored bool `json:"ignored,required"`
Path string `json:"path,required"` Name string `json:"name,required"`
Type FileNodeType `json:"type,required"` Path string `json:"path,required"`
JSON fileNodeJSON `json:"-"` Type FileNodeType `json:"type,required"`
JSON fileNodeJSON `json:"-"`
} }
// fileNodeJSON contains the JSON metadata for the struct [FileNode] // fileNodeJSON contains the JSON metadata for the struct [FileNode]
type fileNodeJSON struct { type fileNodeJSON struct {
Absolute apijson.Field
Ignored apijson.Field Ignored apijson.Field
Name apijson.Field Name apijson.Field
Path apijson.Field Path apijson.Field
@@ -141,16 +143,18 @@ func (r FileNodeType) IsKnown() bool {
} }
type FileReadResponse struct { type FileReadResponse struct {
Content string `json:"content,required"` Content string `json:"content,required"`
Type FileReadResponseType `json:"type,required"` Diff string `json:"diff"`
JSON fileReadResponseJSON `json:"-"` Patch FileReadResponsePatch `json:"patch"`
JSON fileReadResponseJSON `json:"-"`
} }
// fileReadResponseJSON contains the JSON metadata for the struct // fileReadResponseJSON contains the JSON metadata for the struct
// [FileReadResponse] // [FileReadResponse]
type fileReadResponseJSON struct { type fileReadResponseJSON struct {
Content apijson.Field Content apijson.Field
Type apijson.Field Diff apijson.Field
Patch apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }
@@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string {
return r.raw return r.raw
} }
type FileReadResponseType string type FileReadResponsePatch struct {
Hunks []FileReadResponsePatchHunk `json:"hunks,required"`
NewFileName string `json:"newFileName,required"`
OldFileName string `json:"oldFileName,required"`
Index string `json:"index"`
NewHeader string `json:"newHeader"`
OldHeader string `json:"oldHeader"`
JSON fileReadResponsePatchJSON `json:"-"`
}
const ( // fileReadResponsePatchJSON contains the JSON metadata for the struct
FileReadResponseTypeRaw FileReadResponseType = "raw" // [FileReadResponsePatch]
FileReadResponseTypePatch FileReadResponseType = "patch" type fileReadResponsePatchJSON struct {
) Hunks apijson.Field
NewFileName apijson.Field
OldFileName apijson.Field
Index apijson.Field
NewHeader apijson.Field
OldHeader apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r FileReadResponseType) IsKnown() bool { func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) {
switch r { return apijson.UnmarshalRoot(data, r)
case FileReadResponseTypeRaw, FileReadResponseTypePatch: }
return true
} func (r fileReadResponsePatchJSON) RawJSON() string {
return false return r.raw
}
type FileReadResponsePatchHunk struct {
Lines []string `json:"lines,required"`
NewLines float64 `json:"newLines,required"`
NewStart float64 `json:"newStart,required"`
OldLines float64 `json:"oldLines,required"`
OldStart float64 `json:"oldStart,required"`
JSON fileReadResponsePatchHunkJSON `json:"-"`
}
// fileReadResponsePatchHunkJSON contains the JSON metadata for the struct
// [FileReadResponsePatchHunk]
type fileReadResponsePatchHunkJSON struct {
Lines apijson.Field
NewLines apijson.Field
NewStart apijson.Field
OldLines apijson.Field
OldStart apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
func (r *FileReadResponsePatchHunk) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileReadResponsePatchHunkJSON) RawJSON() string {
return r.raw
} }
type FileListParams struct { type FileListParams struct {

View File

@@ -2,4 +2,4 @@
package internal package internal
const PackageVersion = "0.8.0" // x-release-please-version const PackageVersion = "0.9.0" // x-release-please-version

View File

@@ -1332,15 +1332,17 @@ func (r sessionJSON) RawJSON() string {
} }
type SessionTime struct { type SessionTime struct {
Created float64 `json:"created,required"` Created float64 `json:"created,required"`
Updated float64 `json:"updated,required"` Updated float64 `json:"updated,required"`
JSON sessionTimeJSON `json:"-"` Compacting float64 `json:"compacting"`
JSON sessionTimeJSON `json:"-"`
} }
// sessionTimeJSON contains the JSON metadata for the struct [SessionTime] // sessionTimeJSON contains the JSON metadata for the struct [SessionTime]
type sessionTimeJSON struct { type sessionTimeJSON struct {
Created apijson.Field Created apijson.Field
Updated apijson.Field Updated apijson.Field
Compacting apijson.Field
raw string raw string
ExtraFields map[string]apijson.Field ExtraFields map[string]apijson.Field
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "0.7.1", "version": "0.7.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"

View File

@@ -50,6 +50,9 @@ export type Event =
| ({ | ({
type: "session.error" type: "session.error"
} & EventSessionError) } & EventSessionError)
| ({
type: "session.compacted"
} & EventSessionCompacted)
| ({ | ({
type: "server.connected" type: "server.connected"
} & EventServerConnected) } & EventServerConnected)
@@ -478,6 +481,7 @@ export type Session = {
time: { time: {
created: number created: number
updated: number updated: number
compacting?: number
} }
revert?: { revert?: {
messageID: string messageID: string
@@ -521,6 +525,13 @@ export type EventSessionError = {
} }
} }
export type EventSessionCompacted = {
type: "session.compacted"
properties: {
sessionID: string
}
}
export type EventServerConnected = { export type EventServerConnected = {
type: "server.connected" type: "server.connected"
properties: { properties: {

View File

@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"time"
"log/slog" "log/slog"
@@ -656,6 +657,9 @@ func (a *App) IsBusy() bool {
if len(a.Messages) == 0 { if len(a.Messages) == 0 {
return false return false
} }
if a.IsCompacting() {
return true
}
lastMessage := a.Messages[len(a.Messages)-1] lastMessage := a.Messages[len(a.Messages)-1]
if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok { if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
return casted.Time.Completed == 0 return casted.Time.Completed == 0
@@ -663,6 +667,13 @@ func (a *App) IsBusy() bool {
return false return false
} }
func (a *App) IsCompacting() bool {
if time.Since(time.UnixMilli(int64(a.Session.Time.Compacting))) < time.Second*30 {
return true
}
return false
}
func (a *App) HasAnimatingWork() bool { func (a *App) HasAnimatingWork() bool {
for _, msg := range a.Messages { for _, msg := range a.Messages {
switch casted := msg.Info.(type) { switch casted := msg.Info.(type) {

View File

@@ -385,6 +385,9 @@ func (m *editorComponent) Content() string {
} else if m.app.IsBusy() { } else if m.app.IsBusy() {
keyText := m.getInterruptKeyText() keyText := m.getInterruptKeyText()
status := "working" status := "working"
if m.app.IsCompacting() {
status = "compacting"
}
if m.app.CurrentPermission.ID != "" { if m.app.CurrentPermission.ID != "" {
status = "waiting for permission" status = "waiting for permission"
} }

View File

@@ -365,6 +365,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" lastAssistantMessage := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
for _, msg := range slices.Backward(m.app.Messages) { for _, msg := range slices.Backward(m.app.Messages) {
if assistant, ok := msg.Info.(opencode.AssistantMessage); ok { if assistant, ok := msg.Info.(opencode.AssistantMessage); ok {
if assistant.Time.Completed > 0 {
break
}
lastAssistantMessage = assistant.ID lastAssistantMessage = assistant.ID
break break
} }
@@ -475,6 +478,9 @@ func (m *messagesComponent) renderView() tea.Cmd {
} }
case opencode.AssistantMessage: case opencode.AssistantMessage:
if casted.Summary {
continue
}
if casted.ID == m.app.Session.Revert.MessageID { if casted.ID == m.app.Session.Revert.MessageID {
reverted = true reverted = true
revertedMessageCount = 1 revertedMessageCount = 1

View File

@@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
if matchIndex == -1 { if matchIndex == -1 {
a.app.Messages = append(a.app.Messages, app.Message{ // Extract the new message ID
var newMessageID string
switch casted := msg.Properties.Info.AsUnion().(type) {
case opencode.UserMessage:
newMessageID = casted.ID
case opencode.AssistantMessage:
newMessageID = casted.ID
}
// Find the correct insertion index by scanning backwards
// Most messages are added to the end, so start from the end
insertIndex := len(a.app.Messages)
for i := len(a.app.Messages) - 1; i >= 0; i-- {
var existingID string
switch casted := a.app.Messages[i].Info.(type) {
case opencode.UserMessage:
existingID = casted.ID
case opencode.AssistantMessage:
existingID = casted.ID
}
if existingID < newMessageID {
insertIndex = i + 1
break
}
}
// Create the new message
newMessage := app.Message{
Info: msg.Properties.Info.AsUnion(), Info: msg.Properties.Info.AsUnion(),
Parts: []opencode.PartUnion{}, Parts: []opencode.PartUnion{},
}) }
// Insert at the correct position
a.app.Messages = append(a.app.Messages[:insertIndex], append([]app.Message{newMessage}, a.app.Messages[insertIndex:]...)...)
} }
} }
case opencode.EventListResponseEventPermissionUpdated: case opencode.EventListResponseEventPermissionUpdated:
@@ -627,6 +657,10 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
slog.Error("Server error", "name", err.Name, "message", err.Data.Message) slog.Error("Server error", "name", err.Name, "message", err.Data.Message)
return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name))) return a, toast.NewErrorToast(err.Data.Message, toast.WithTitle(string(err.Name)))
} }
case opencode.EventListResponseEventSessionCompacted:
if msg.Properties.SessionID == a.app.Session.ID {
return a, toast.NewSuccessToast("Session compacted successfully")
}
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height a.width, a.height = msg.Width, msg.Height

View File

@@ -1,7 +1,7 @@
{ {
"name": "@opencode/web", "name": "@opencode/web",
"type": "module", "type": "module",
"version": "0.7.1", "version": "0.7.2",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev", "dev:remote": "sst shell --stage=dev --target=Web astro dev",

View File

@@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "0.7.1", "version": "0.7.2",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",

4
sst-env.d.ts vendored
View File

@@ -44,6 +44,10 @@ declare module "sst" {
type: "sst.sst.Linkable" type: "sst.sst.Linkable"
username: string username: string
} }
FIREWORKS_API_KEY: {
type: "sst.sst.Secret"
value: string
}
GITHUB_APP_ID: { GITHUB_APP_ID: {
type: "sst.sst.Secret" type: "sst.sst.Secret"
value: string value: string