Merge branch 'dev' of https://github.com/sst/opencode into dev

This commit is contained in:
David Hill
2025-10-29 16:16:56 +00:00
22 changed files with 180 additions and 58 deletions

View File

@@ -17,6 +17,7 @@ If you are unsure if a PR would be accepted, feel free to ask a maintainer or lo
- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
> [!NOTE]
> PRs that ignore these guardrails will likely be closed.

View File

@@ -37,7 +37,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -64,7 +64,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -88,7 +88,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -109,7 +109,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -150,7 +150,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -166,7 +166,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.15.23",
"version": "0.15.25",
"bin": {
"opencode": "./bin/opencode",
},
@@ -230,7 +230,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -250,7 +250,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.15.23",
"version": "0.15.25",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -261,7 +261,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -274,7 +274,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/precision-diffs": "catalog:",
@@ -297,7 +297,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

View File

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

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "0.15.23",
"version": "0.15.25",
"private": true,
"type": "module",
"dependencies": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "0.15.23",
"version": "0.15.25",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "0.15.23",
"version": "0.15.25",
"description": "",
"type": "module",
"scripts": {

View File

@@ -393,9 +393,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " />
<div class="flex gap-x-3 items-baseline flex-[1_0_0]">
<span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
<Show when={i.release_date}>
<span class="text-12-medium text-text-weak overflow-hidden text-ellipsis truncate min-w-0">
{DateTime.fromFormat(i.release_date, "yyyy-MM-dd").toFormat("LLL yyyy")}
</span>
</Show>
</div>
</div>
<Show when={!i.cost || i.cost?.input === 0}>

View File

@@ -481,7 +481,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (!message) return ""
if (Array.isArray(message)) return message.map((m) => getMessageText(m)).join(" ")
const fileParts = sync.data.part[message.id]?.filter((p) => p.type === "file")
console.log(fileParts)
return sync.data.part[message.id]
?.filter((p) => p.type === "text")

View File

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

View File

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

View File

@@ -142,7 +142,9 @@ export namespace Installation {
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}`
export async function latest() {
return fetch(`https://registry.npmjs.org/opencode-ai/${CHANNEL}`)
const [major] = VERSION.split(".").map((x) => Number(x))
const channel = CHANNEL === "latest" ? `latest-${major}` : CHANNEL
return fetch(`https://registry.npmjs.org/opencode-ai/${channel}`)
.then((res) => {
if (!res.ok) throw new Error(res.statusText)
return res.json()

View File

@@ -286,7 +286,11 @@ export namespace SessionPrompt {
OUTPUT_TOKEN_MAX,
),
abortSignal: abort.signal,
providerOptions: ProviderTransform.providerOptions(model.npm, model.providerID, params.options),
providerOptions: ProviderTransform.providerOptions(
model.npm,
model.providerID,
params.options,
),
stopWhen: stepCountIs(1),
temperature: params.temperature,
topP: params.topP,
@@ -321,7 +325,11 @@ export namespace SessionPrompt {
async transformParams(args) {
if (args.type === "stream") {
// @ts-expect-error
args.params.prompt = ProviderTransform.message(args.params.prompt, model.providerID, model.modelID)
args.params.prompt = ProviderTransform.message(
args.params.prompt,
model.providerID,
model.modelID,
)
}
return args.params
},
@@ -504,7 +512,11 @@ export namespace SessionPrompt {
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (Wildcard.all(item.id, enabledTools) === false) continue
const schema = ProviderTransform.schema(input.providerID, input.modelID, z.toJSONSchema(item.parameters))
const schema = ProviderTransform.schema(
input.providerID,
input.modelID,
z.toJSONSchema(item.parameters),
)
tools[item.id] = tool({
id: item.id as any,
description: item.description,
@@ -521,6 +533,7 @@ export namespace SessionPrompt {
args,
},
)
item.parameters.parse(args)
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: options.abortSignal!,
@@ -585,17 +598,7 @@ export namespace SessionPrompt {
args,
},
)
const result = await execute(args, opts).catch((err: unknown) => {
log.error("Error executing tool", { error: err, tool: key })
return {
content: [
{
type: "text",
text: `Failed to execute tool: ${err instanceof Error ? err.message : String(err)}`,
},
],
}
})
const result = await execute(args, opts)
await Plugin.trigger(
"tool.execute.after",
@@ -809,7 +812,9 @@ export namespace SessionPrompt {
messageID: info.id,
sessionID: input.sessionID,
type: "file",
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
url:
`data:${part.mime};base64,` +
Buffer.from(await file.bytes()).toString("base64"),
mime: part.mime,
filename: part.filename!,
source: part.source,
@@ -883,7 +888,9 @@ export namespace SessionPrompt {
synthetic: true,
})
}
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.mode === "plan")
const wasPlan = input.messages.some(
(msg) => msg.info.role === "assistant" && msg.info.mode === "plan",
)
if (wasPlan && input.agent.name === "build") {
userMessage.parts.push({
id: Identifier.ascending("part"),
@@ -963,7 +970,10 @@ export namespace SessionPrompt {
partFromToolCall(toolCallID: string) {
return toolcalls[toolCallID]
},
async process(stream: StreamTextResult<Record<string, AITool>, never>, retries: { count: number; max: number }) {
async process(
stream: StreamTextResult<Record<string, AITool>, never>,
retries: { count: number; max: number },
) {
log.info("process")
if (!assistantMsg) throw new Error("call next() first before processing")
let shouldRetry = false
@@ -1094,7 +1104,10 @@ export namespace SessionPrompt {
status: "error",
input: value.input,
error: (value.error as any).toString(),
metadata: value.error instanceof Permission.RejectedError ? value.error.metadata : undefined,
metadata:
value.error instanceof Permission.RejectedError
? value.error.metadata
: undefined,
time: {
start: match.state.time.start,
end: Date.now(),
@@ -1218,7 +1231,11 @@ export namespace SessionPrompt {
error: e,
})
const error = MessageV2.fromError(e, { providerID: input.providerID })
if (retries.count < retries.max && MessageV2.APIError.isInstance(error) && error.data.isRetryable) {
if (
retries.count < retries.max &&
MessageV2.APIError.isInstance(error) &&
error.data.isRetryable
) {
shouldRetry = true
await Session.updatePart({
id: Identifier.ascending("part"),
@@ -1241,7 +1258,11 @@ export namespace SessionPrompt {
}
const p = await Session.getParts(assistantMsg.id)
for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") {
if (
part.type === "tool" &&
part.state.status !== "completed" &&
part.state.status !== "error"
) {
Session.updatePart({
...part,
state: {
@@ -1705,11 +1726,13 @@ export namespace SessionPrompt {
if (input.session.parentID) return
if (!Session.isDefaultTitle(input.session.title)) return
const isFirst =
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
.length === 1
input.history.filter(
(m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
).length === 1
if (!isFirst) return
const small =
(await Provider.getSmallModel(input.providerID)) ?? (await Provider.getModel(input.providerID, input.modelID))
(await Provider.getSmallModel(input.providerID)) ??
(await Provider.getModel(input.providerID, input.modelID))
const options = {
...ProviderTransform.options(small.providerID, small.modelID, input.session.id),
...small.info.options,

View File

@@ -29,7 +29,15 @@ export namespace SessionSummary {
)
async function summarizeSession(input: { sessionID: string; messages: MessageV2.WithParts[] }) {
const diffs = await computeDiff({ messages: input.messages })
const files = new Set(
input.messages
.flatMap((x) => x.parts)
.filter((x) => x.type === "patch")
.flatMap((x) => x.files),
)
const diffs = await computeDiff({ messages: input.messages }).then((x) =>
x.filter((x) => files.has(x.file)),
)
await Session.update(input.sessionID, (draft) => {
draft.summary = {
diffs,
@@ -39,7 +47,9 @@ export namespace SessionSummary {
async function summarizeMessage(input: { messageID: string; messages: MessageV2.WithParts[] }) {
const messages = input.messages.filter(
(m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
(m) =>
m.info.id === input.messageID ||
(m.info.role === "assistant" && m.info.parentID === input.messageID),
)
const msgWithParts = messages.find((m) => m.info.id === input.messageID)!
const userMsg = msgWithParts.info as MessageV2.User
@@ -50,11 +60,14 @@ export namespace SessionSummary {
}
await Session.updateMessage(userMsg)
const assistantMsg = messages.find((m) => m.info.role === "assistant")!.info as MessageV2.Assistant
const assistantMsg = messages.find((m) => m.info.role === "assistant")!
.info as MessageV2.Assistant
const small = await Provider.getSmallModel(assistantMsg.providerID)
if (!small) return
const textPart = msgWithParts.parts.find((p) => p.type === "text" && !p.synthetic) as MessageV2.TextPart
const textPart = msgWithParts.parts.find(
(p) => p.type === "text" && !p.synthetic,
) as MessageV2.TextPart
if (textPart && !userMsg.summary?.title) {
const result = await generateText({
maxOutputTokens: small.info.reasoning ? 1500 : 20,
@@ -81,7 +94,8 @@ export namespace SessionSummary {
if (
messages.some(
(m) =>
m.info.role === "assistant" && m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
m.info.role === "assistant" &&
m.parts.some((p) => p.type === "step-finish" && p.reason !== "tool-calls"),
)
) {
const result = await generateText({
@@ -114,7 +128,9 @@ export namespace SessionSummary {
let all = await Session.messages(input.sessionID)
if (input.messageID)
all = all.filter(
(x) => x.info.id === input.messageID || (x.info.role === "assistant" && x.info.parentID === input.messageID),
(x) =>
x.info.id === input.messageID ||
(x.info.role === "assistant" && x.info.parentID === input.messageID),
)
return computeDiff({

View File

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

View File

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

View File

@@ -125,6 +125,10 @@ import type {
TuiExecuteCommandErrors,
TuiShowToastData,
TuiShowToastResponses,
TuiControlNextData,
TuiControlNextResponses,
TuiControlResponseData,
TuiControlResponseResponses,
AuthSetData,
AuthSetResponses,
AuthSetErrors,
@@ -750,6 +754,40 @@ class Mcp extends _HeyApiClient {
}
}
class Control extends _HeyApiClient {
/**
* Get the next TUI request from the queue
*/
public next<ThrowOnError extends boolean = false>(
options?: Options<TuiControlNextData, ThrowOnError>,
) {
return (options?.client ?? this._client).get<TuiControlNextResponses, unknown, ThrowOnError>({
url: "/tui/control/next",
...options,
})
}
/**
* Submit a response to the TUI request queue
*/
public response<ThrowOnError extends boolean = false>(
options?: Options<TuiControlResponseData, ThrowOnError>,
) {
return (options?.client ?? this._client).post<
TuiControlResponseResponses,
unknown,
ThrowOnError
>({
url: "/tui/control/response",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
}
class Tui extends _HeyApiClient {
/**
* Append prompt to the TUI
@@ -878,6 +916,7 @@ class Tui extends _HeyApiClient {
},
})
}
control = new Control({ client: this._client })
}
class Auth extends _HeyApiClient {

View File

@@ -2632,6 +2632,46 @@ export type TuiShowToastResponses = {
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
export type TuiControlNextData = {
body?: never
path?: never
query?: {
directory?: string
}
url: "/tui/control/next"
}
export type TuiControlNextResponses = {
/**
* Next TUI request
*/
200: {
path: string
body: unknown
}
}
export type TuiControlNextResponse = TuiControlNextResponses[keyof TuiControlNextResponses]
export type TuiControlResponseData = {
body?: unknown
path?: never
query?: {
directory?: string
}
url: "/tui/control/response"
}
export type TuiControlResponseResponses = {
/**
* Response submitted successfully
*/
200: boolean
}
export type TuiControlResponseResponse =
TuiControlResponseResponses[keyof TuiControlResponseResponses]
export type AuthSetData = {
body?: Auth
path: {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "0.15.23",
"version": "0.15.25",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "0.15.23",
"version": "0.15.25",
"type": "module",
"exports": {
".": "./src/components/index.ts",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "0.15.23",
"version": "0.15.25",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

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