Compare commits

...

13 Commits

Author SHA1 Message Date
c72effda28 Added opencode.json template with current working config
Some checks failed
Format / format (push) Has been cancelled
2025-09-11 21:57:17 +02:00
d78bbff439 Remove path-based restrictions 2025-09-11 21:01:09 +02:00
6835f9084c Add script for simplified local build 2025-09-11 20:49:03 +02:00
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
43 changed files with 715 additions and 218 deletions

View File

@@ -75,3 +75,4 @@
| 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-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) |

43
build_local.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/bash
set -e
echo "Building opencode locally..."
# Get the absolute paths
REPO_ROOT="$(cd "$(dirname "$0")" && pwd)"
TUI_DIR="$REPO_ROOT/packages/tui"
SERVER_DIR="$REPO_ROOT/packages/opencode"
TUI_BINARY_PATH="$TUI_DIR/cmd/opencode/dist/tui"
echo "Repository root: $REPO_ROOT"
# Build the TUI binary
echo "Building TUI binary..."
cd "$TUI_DIR"
go build -o cmd/opencode/dist/tui cmd/opencode/main.go
echo "✓ TUI built: $TUI_BINARY_PATH"
# Build the server CLI with embedded TUI path
echo "Building server CLI..."
cd "$SERVER_DIR"
bun build \
--define OPENCODE_TUI_PATH="'$TUI_BINARY_PATH'" \
--define OPENCODE_VERSION="'0.7.2'" \
--compile \
--target=bun-linux-x64 \
--outfile=opencode-cli \
./src/index.ts
echo "✓ Server CLI built: $SERVER_DIR/opencode-cli"
echo ""
echo "Build complete!"
echo ""
echo "To run opencode from anywhere:"
echo " $SERVER_DIR/opencode-cli"
echo ""
echo "To run from a specific directory:"
echo " $SERVER_DIR/opencode-cli /path/to/your/project"
echo ""
echo "To run headless server only:"
echo " $SERVER_DIR/opencode-cli serve"

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 && ../../packages/opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.7.1"
"version": "0.7.2"
},
"dependencies": {
"@ibm/plex": "6.4.1",

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import type { APIEvent } from "@solidjs/start/server"
import path from "node:path"
import { Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
import { KeyTable } from "@opencode/cloud-core/schema/key.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 { 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(
input: APIEvent,
opts: {
@@ -28,13 +52,10 @@ export async function handler(
class CreditsError extends Error {}
class ModelError extends Error {}
const MODELS = {
const MODELS: Record<string, Model> = {
"claude-opus-4-1": {
id: "claude-opus-4-1" as const,
auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
cost: {
input: 0.000015,
output: 0.000075,
@@ -43,13 +64,17 @@ export async function handler(
cacheWrite1h: 0.00003,
},
headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-opus-4-1-20250805",
},
},
},
"claude-sonnet-4": {
id: "claude-sonnet-4" as const,
auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
cost: (usage: any) => {
const totalInputTokens =
usage.inputTokens + usage.cacheReadTokens + usage.cacheWrite5mTokens + usage.cacheWrite1hTokens
@@ -70,13 +95,17 @@ export async function handler(
}
},
headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-sonnet-4-20250514",
},
},
},
"claude-3-5-haiku": {
id: "claude-3-5-haiku" as const,
auth: true,
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
cost: {
input: 0.0000008,
output: 0.000004,
@@ -85,13 +114,17 @@ export async function handler(
cacheWrite1h: 0.0000016,
},
headerMappings: {},
providers: {
anthropic: {
api: "https://api.anthropic.com",
apiKey: Resource.ANTHROPIC_API_KEY.value,
model: "claude-3-5-haiku-20241022",
},
},
},
"gpt-5": {
id: "gpt-5" as const,
auth: true,
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
cost: {
input: 0.00000125,
output: 0.00001,
@@ -100,28 +133,43 @@ export async function handler(
cacheWrite1h: 0,
},
headerMappings: {},
providers: {
openai: {
api: "https://api.openai.com",
apiKey: Resource.OPENAI_API_KEY.value,
model: "gpt-5",
},
},
},
"qwen3-coder": {
id: "qwen3-coder" as const,
auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: {
input: 0.00000038,
output: 0.00000153,
input: 0.00000045,
output: 0.0000018,
cacheRead: 0,
cacheWrite5m: 0,
cacheWrite1h: 0,
},
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": {
id: "kimi-k2" as const,
auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "moonshotai/Kimi-K2-Instruct-0905",
cost: {
input: 0.0000006,
output: 0.0000025,
@@ -130,13 +178,24 @@ export async function handler(
cacheWrite1h: 0,
},
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": {
id: "grok-code" as const,
auth: false,
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
cost: {
input: 0,
output: 0,
@@ -148,14 +207,18 @@ export async function handler(
"x-grok-conv-id": "x-opencode-session",
"x-grok-req-id": "x-opencode-request",
},
providers: {
xai: {
api: "https://api.x.ai",
apiKey: Resource.XAI_API_KEY.value,
model: "grok-code",
},
},
},
// deprecated
"qwen/qwen3-coder": {
id: "qwen/qwen3-coder" as const,
auth: true,
api: "https://inference.baseten.co",
apiKey: Resource.BASETEN_API_KEY.value,
model: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
cost: {
input: 0.00000038,
output: 0.00000153,
@@ -164,6 +227,20 @@ export async function handler(
cacheWrite1h: 0,
},
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 isFree = FREE_WORKSPACES.includes(apiKey?.workspaceID ?? "")
await checkCredits()
const providerName = selectProvider()
const providerData = MODEL.providers[providerName]
logger.metric({ provider: providerName })
// 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",
headers: (() => {
const headers = input.request.headers
headers.delete("host")
headers.delete("content-length")
opts.setAuthHeader(headers, MODEL.apiKey)
opts.setAuthHeader(headers, providerData.apiKey)
Object.entries(MODEL.headerMappings ?? {}).forEach(([k, v]) => {
headers.set(k, headers.get(v)!)
})
@@ -211,7 +292,7 @@ export async function handler(
})(),
body: JSON.stringify({
...(opts.modifyBody?.(body) ?? body),
model: MODEL.model,
model: providerData.model,
}),
})
@@ -245,7 +326,6 @@ export async function handler(
const decoder = new TextDecoder()
let buffer = ""
let responseLength = 0
let startTimestamp = Date.now()
function pump(): Promise<void> {
return (
@@ -262,7 +342,6 @@ export async function handler(
logger.metric({ time_to_first_byte: Date.now() - startTimestamp })
}
responseLength += value.length
buffer += decoder.decode(value, { stream: true })
const parts = buffer.split("\n\n")
@@ -344,6 +423,13 @@ export async function handler(
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) {
const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } =
opts.normalizeUsage(usage)
@@ -416,9 +502,25 @@ export async function handler(
"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)
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",
"name": "@opencode/cloud-core",
"version": "0.7.1",
"version": "0.7.2",
"private": true,
"type": "module",
"dependencies": {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-scripts",
"version": "0.7.1",
"version": "0.7.2",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"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 XAI_API_KEY = new sst.Secret("XAI_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 AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
properties: { value: auth.url.apply((url) => url!) },
@@ -136,6 +137,7 @@ new sst.cloudflare.x.SolidStart("Console", {
OPENAI_API_KEY,
XAI_API_KEY,
BASETEN_API_KEY,
FIREWORKS_API_KEY,
],
environment: {
//VITE_DOCS_URL: web.url.apply((url) => url!),

57
opencode.template.jsonc Normal file
View File

@@ -0,0 +1,57 @@
{
// Template opencode configuration with restrictive permissions
// Copy this file to ~/.config/opencode.json (or opencode.json inside a repo)
// and modify as needed
"$schema": "https://opencode.ai/config.json",
"mcp": {
"weather": {
"type": "local",
"command": ["opencode", "x", "@h1deya/mcp-server-weather"]
}
},
"permission": {
"bash": {
"git branch -d": "ask",
"git branch -D": "ask",
"git status": "allow",
"git diff": "allow",
"git log": "allow",
"git show": "allow",
"git branch": "allow",
"git remote": "allow",
"git fetch": "allow",
"git pull": "allow",
"git merge": "ask",
"git rebase": "ask",
"git commit": "ask",
"git push": "ask",
"git checkout": "ask",
"git switch": "ask",
"git reset": "ask",
"git revert": "ask",
"git rm": "ask",
"git mv": "ask",
"git worktree": "ask",
"cat": "allow",
"tail": "allow",
"head": "allow",
"less": "allow",
"more": "allow",
"grep": "allow",
"find": "allow",
"ls": "allow",
"pwd": "allow",
"cd": "allow",
"mkdir": "ask",
"rm": "ask",
"rmdir": "ask",
"mv": "ask",
"cp": "ask",
"touch": "ask",
"chmod": "ask",
"chown": "ask",
"su": "deny",
"sudo": "deny"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,6 +86,7 @@ export namespace Session {
time: z.object({
created: z.number(),
updated: z.number(),
compacting: z.number().optional(),
}),
revert: z
.object({
@@ -137,12 +138,17 @@ export namespace Session {
error: MessageV2.Assistant.shape.error,
}),
),
Compacted: Bus.event(
"session.compacted",
z.object({
sessionID: z.string(),
}),
),
}
const state = Instance.state(
() => {
const pending = new Map<string, AbortController>()
const autoCompacting = new Map<string, boolean>()
const queued = new Map<
string,
{
@@ -156,7 +162,6 @@ export namespace Session {
return {
pending,
autoCompacting,
queued,
}
},
@@ -714,24 +719,8 @@ export namespace Session {
})().then((x) => Provider.getModel(x.providerID, x.modelID))
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
// 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)
const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
@@ -999,7 +988,38 @@ export namespace Session {
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)
if (queue.length) {
for (const item of queue) {
@@ -1756,10 +1776,22 @@ export namespace Session {
}
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 lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const start = Math.max(
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 system = [
...SystemPrompt.summarize(model.providerID),
@@ -1767,36 +1799,8 @@ export namespace Session {
...(await SystemPrompt.custom()),
]
const next: MessageV2.Info = {
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({
const generated = await generateText({
maxRetries: 10,
abortSignal: abort.signal,
model: model.language,
messages: [
...system.map(
@@ -1805,7 +1809,7 @@ export namespace Session {
content: x,
}),
),
...MessageV2.toModelMessage(filtered),
...MessageV2.toModelMessage(toSummarize),
{
role: "user",
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)
return result
Bus.publish(Event.Compacted, {
sessionID: input.sessionID,
})
return msg
}
function isLocked(sessionID: string) {
@@ -1837,12 +1877,6 @@ export namespace Session {
log.info("unlocking", { sessionID })
state().pending.delete(sessionID)
const isAutoCompacting = state().autoCompacting.get(sessionID) ?? false
if (isAutoCompacting) {
state().autoCompacting.delete(sessionID)
return
}
const session = await get(sessionID)
if (session.parentID) return

View File

@@ -4,7 +4,7 @@ import { exec } from "child_process"
import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { Permission } from "../permission"
import { Filesystem } from "../util/filesystem"
// import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
@@ -87,11 +87,11 @@ export const BashTool = Tool.define("bash", {
.text()
.then((x) => x.trim())
log.info("resolved path", { arg, resolved })
if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
throw new Error(
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
)
}
// if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
// throw new Error(
// `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
// )
// }
}
}

View File

@@ -13,7 +13,7 @@ import DESCRIPTION from "./edit.txt"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
// import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Agent } from "../agent/agent"
@@ -35,9 +35,9 @@ export const EditTool = Tool.define("edit", {
}
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filePath)) {
throw new Error(`File ${filePath} is not in the current working directory`)
}
// if (!Filesystem.contains(Instance.directory, filePath)) {
// throw new Error(`File ${filePath} is not in the current working directory`)
// }
const agent = await Agent.get(ctx.agent)
let diff = ""
@@ -107,14 +107,12 @@ export const EditTool = Tool.define("edit", {
for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue
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
}
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 {
@@ -599,7 +597,7 @@ export function replace(content: string, oldString: string, newString: string, r
for (const replacer of [
SimpleReplacer,
LineTrimmedReplacer,
BlockAnchorReplacer,
// BlockAnchorReplacer,
WhitespaceNormalizedReplacer,
IndentationFlexibleReplacer,
EscapeNormalizedReplacer,

View File

@@ -5,7 +5,7 @@ import { Tool } from "./tool"
import { LSP } from "../lsp"
import { FileTime } from "../file/time"
import DESCRIPTION from "./read.txt"
import { Filesystem } from "../util/filesystem"
// import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
const DEFAULT_READ_LIMIT = 2000
@@ -23,9 +23,9 @@ export const ReadTool = Tool.define("read", {
if (!path.isAbsolute(filepath)) {
filepath = path.join(process.cwd(), filepath)
}
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`)
}
// if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
// throw new Error(`File ${filepath} is not in the current working directory`)
// }
const file = Bun.file(filepath)
if (!(await file.exists())) {

View File

@@ -7,7 +7,7 @@ import DESCRIPTION from "./write.txt"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
import { Filesystem } from "../util/filesystem"
// import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Agent } from "../agent/agent"
@@ -19,9 +19,9 @@ export const WriteTool = Tool.define("write", {
}),
async execute(params, ctx) {
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filepath)) {
throw new Error(`File ${filepath} is not in the current working directory`)
}
// if (!Filesystem.contains(Instance.directory, filepath)) {
// throw new Error(`File ${filepath} is not in the current working directory`)
// }
const file = Bun.file(filepath)
const exists = await file.exists()

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,13 @@
# 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)
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 -->
```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 -->

View File

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

View File

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

View File

@@ -63,7 +63,8 @@ type EventListResponse struct {
// [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties],
// [EventListResponseEventSessionIdleProperties],
// [EventListResponseEventSessionErrorProperties], [interface{}].
// [EventListResponseEventSessionErrorProperties],
// [EventListResponseEventSessionCompactedProperties], [interface{}].
Properties interface{} `json:"properties,required"`
Type EventListResponseType `json:"type,required"`
JSON eventListResponseJSON `json:"-"`
@@ -105,6 +106,7 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted],
// [EventListResponseEventServerConnected].
func (r EventListResponse) AsUnion() EventListResponseUnion {
return r.union
@@ -118,7 +120,8 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// [EventListResponseEventPermissionUpdated],
// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
// [EventListResponseEventSessionCompacted] or
// [EventListResponseEventServerConnected].
type EventListResponseUnion interface {
implementsEventListResponse()
@@ -193,6 +196,11 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventSessionError{}),
DiscriminatorValue: "session.error",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventSessionCompacted{}),
DiscriminatorValue: "session.compacted",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventServerConnected{}),
@@ -1108,6 +1116,66 @@ func (r EventListResponseEventSessionErrorType) IsKnown() bool {
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 {
Properties interface{} `json:"properties,required"`
Type EventListResponseEventServerConnectedType `json:"type,required"`
@@ -1163,12 +1231,13 @@ const (
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
EventListResponseTypeSessionError EventListResponseType = "session.error"
EventListResponseTypeSessionCompacted EventListResponseType = "session.compacted"
EventListResponseTypeServerConnected EventListResponseType = "server.connected"
)
func (r EventListResponseType) IsKnown() bool {
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 false

View File

@@ -100,15 +100,17 @@ func (r FileStatus) IsKnown() bool {
}
type FileNode struct {
Ignored bool `json:"ignored,required"`
Name string `json:"name,required"`
Path string `json:"path,required"`
Type FileNodeType `json:"type,required"`
JSON fileNodeJSON `json:"-"`
Absolute string `json:"absolute,required"`
Ignored bool `json:"ignored,required"`
Name string `json:"name,required"`
Path string `json:"path,required"`
Type FileNodeType `json:"type,required"`
JSON fileNodeJSON `json:"-"`
}
// fileNodeJSON contains the JSON metadata for the struct [FileNode]
type fileNodeJSON struct {
Absolute apijson.Field
Ignored apijson.Field
Name apijson.Field
Path apijson.Field
@@ -141,16 +143,18 @@ func (r FileNodeType) IsKnown() bool {
}
type FileReadResponse struct {
Content string `json:"content,required"`
Type FileReadResponseType `json:"type,required"`
JSON fileReadResponseJSON `json:"-"`
Content string `json:"content,required"`
Diff string `json:"diff"`
Patch FileReadResponsePatch `json:"patch"`
JSON fileReadResponseJSON `json:"-"`
}
// fileReadResponseJSON contains the JSON metadata for the struct
// [FileReadResponse]
type fileReadResponseJSON struct {
Content apijson.Field
Type apijson.Field
Diff apijson.Field
Patch apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
@@ -163,19 +167,64 @@ func (r fileReadResponseJSON) RawJSON() string {
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 (
FileReadResponseTypeRaw FileReadResponseType = "raw"
FileReadResponseTypePatch FileReadResponseType = "patch"
)
// fileReadResponsePatchJSON contains the JSON metadata for the struct
// [FileReadResponsePatch]
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 {
switch r {
case FileReadResponseTypeRaw, FileReadResponseTypePatch:
return true
}
return false
func (r *FileReadResponsePatch) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
func (r fileReadResponsePatchJSON) RawJSON() string {
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 {

View File

@@ -2,4 +2,4 @@
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 {
Created float64 `json:"created,required"`
Updated float64 `json:"updated,required"`
JSON sessionTimeJSON `json:"-"`
Created float64 `json:"created,required"`
Updated float64 `json:"updated,required"`
Compacting float64 `json:"compacting"`
JSON sessionTimeJSON `json:"-"`
}
// sessionTimeJSON contains the JSON metadata for the struct [SessionTime]
type sessionTimeJSON struct {
Created apijson.Field
Updated apijson.Field
Compacting apijson.Field
raw string
ExtraFields map[string]apijson.Field
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -592,10 +592,40 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
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(),
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:
@@ -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)
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:
msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height

View File

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

View File

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

4
sst-env.d.ts vendored
View File

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