Merge branch 'dev' into effect-sync-event

This commit is contained in:
Kit Langton
2026-04-02 11:48:33 -04:00
committed by GitHub
73 changed files with 407 additions and 331 deletions
+6 -2
View File
@@ -120,6 +120,10 @@ class TokenRefreshRequest extends Schema.Class<TokenRefreshRequest>("TokenRefres
const clientId = "opencode-cli"
const eagerRefreshThreshold = Duration.minutes(5)
const eagerRefreshThresholdMs = Duration.toMillis(eagerRefreshThreshold)
const isTokenFresh = (tokenExpiry: number | null, now: number) =>
tokenExpiry != null && tokenExpiry > now + eagerRefreshThresholdMs
const mapAccountServiceError =
(message = "Account service operation failed") =>
@@ -219,7 +223,7 @@ export namespace Account {
const account = maybeAccount.value
const now = yield* Clock.currentTimeMillis
if (account.token_expiry && account.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
if (isTokenFresh(account.token_expiry, now)) {
return account.access_token
}
@@ -229,7 +233,7 @@ export namespace Account {
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
const now = yield* Clock.currentTimeMillis
if (row.token_expiry && row.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
if (isTokenFresh(row.token_expiry, now)) {
return row.access_token
}
@@ -12,3 +12,4 @@ Focus on information that would be helpful for continuing the conversation, incl
Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.
Do not respond to any questions in the conversation, only output the summary.
Respond in the same language the user used in the conversation.
+54 -5
View File
@@ -1,6 +1,7 @@
import z from "zod"
import { EOL } from "os"
import { NamedError } from "@opencode-ai/util/error"
import { logo as glyphs } from "./logo"
export namespace UI {
const wordmark = [
@@ -47,12 +48,60 @@ export namespace UI {
}
export function logo(pad?: string) {
const result = []
for (const row of wordmark) {
if (pad) result.push(pad)
result.push(row)
result.push(EOL)
if (!process.stdout.isTTY && !process.stderr.isTTY) {
const result = []
for (const row of wordmark) {
if (pad) result.push(pad)
result.push(row)
result.push(EOL)
}
return result.join("").trimEnd()
}
const result: string[] = []
const reset = "\x1b[0m"
const left = {
fg: "\x1b[90m",
shadow: "\x1b[38;5;235m",
bg: "\x1b[48;5;235m",
}
const right = {
fg: reset,
shadow: "\x1b[38;5;238m",
bg: "\x1b[48;5;238m",
}
const gap = " "
const draw = (line: string, fg: string, shadow: string, bg: string) => {
const parts: string[] = []
for (const char of line) {
if (char === "_") {
parts.push(bg, " ", reset)
continue
}
if (char === "^") {
parts.push(fg, bg, "▀", reset)
continue
}
if (char === "~") {
parts.push(shadow, "▀", reset)
continue
}
if (char === " ") {
parts.push(" ")
continue
}
parts.push(fg, char, reset)
}
return parts.join("")
}
glyphs.left.forEach((row, index) => {
if (pad) result.push(pad)
result.push(draw(row, left.fg, left.shadow, left.bg))
result.push(gap)
const other = glyphs.right[index] ?? ""
result.push(draw(other, right.fg, right.shadow, right.bg))
result.push(EOL)
})
return result.join("").trimEnd()
}
+24 -4
View File
@@ -48,7 +48,19 @@ process.on("uncaughtException", (e) => {
})
})
const cli = yargs(hideBin(process.argv))
const args = hideBin(process.argv)
function show(out: string) {
const text = out.trimStart()
if (!text.startsWith("opencode ")) {
process.stderr.write(UI.logo() + EOL + EOL)
process.stderr.write(text)
return
}
process.stderr.write(out)
}
const cli = yargs(args)
.parserConfiguration({ "populate--": true })
.scriptName("opencode")
.wrap(100)
@@ -130,7 +142,7 @@ const cli = yargs(hideBin(process.argv))
process.stderr.write("Database migration complete." + EOL)
}
})
.usage("\n" + UI.logo())
.usage("")
.completion("completion", "generate shell completion script")
.command(AcpCommand)
.command(McpCommand)
@@ -162,7 +174,7 @@ const cli = yargs(hideBin(process.argv))
msg?.startsWith("Invalid values:")
) {
if (err) throw err
cli.showHelp("log")
cli.showHelp(show)
}
if (err) throw err
process.exit(1)
@@ -170,7 +182,15 @@ const cli = yargs(hideBin(process.argv))
.strict()
try {
await cli.parse()
if (args.includes("-h") || args.includes("--help")) {
await cli.parse(args, (err: Error | undefined, _argv: unknown, out: string) => {
if (err) throw err
if (!out) return
show(out)
})
} else {
await cli.parse()
}
} catch (e) {
let data: Record<string, any> = {}
if (e instanceof NamedError) {
@@ -44,6 +44,7 @@ import { createGateway } from "@ai-sdk/gateway"
import { createTogetherAI } from "@ai-sdk/togetherai"
import { createPerplexity } from "@ai-sdk/perplexity"
import { createVercel } from "@ai-sdk/vercel"
import { createVenice } from "venice-ai-sdk-provider"
import {
createGitLab,
VERSION as GITLAB_PROVIDER_VERSION,
@@ -139,6 +140,7 @@ export namespace Provider {
"@ai-sdk/vercel": createVercel,
"gitlab-ai-provider": createGitLab,
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
"venice-ai-sdk-provider": createVenice,
}
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
@@ -190,6 +190,7 @@ export namespace SessionCompaction {
Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.
The summary that you construct will be used so that another agent can read it and continue the work.
Do not call any tools. Respond only with the summary text.
Respond in the same language as the user's messages in the conversation.
When constructing the summary, try to stick to this template:
---
+11 -1
View File
@@ -756,7 +756,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
const model = input.model ?? agent.model ?? (yield* lastModel(input.sessionID))
const userMsg: MessageV2.User = {
id: MessageID.ascending(),
id: input.messageID ?? MessageID.ascending(),
sessionID: input.sessionID,
time: { created: Date.now() },
role: "user",
@@ -1362,9 +1362,18 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}
if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
const lastAssistantMsg = msgs.findLast(
(msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
)
// Some providers return "stop" even when the assistant message contains tool calls.
// Keep the loop running so tool results can be sent back to the model.
const hasToolCalls = lastAssistantMsg?.parts.some((part) => part.type === "tool") ?? false
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastUser.id < lastAssistant.id
) {
log.info("exiting loop", { sessionID })
@@ -1818,6 +1827,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
export const ShellInput = z.object({
sessionID: SessionID.zod,
messageID: MessageID.zod.optional(),
agent: z.string(),
model: z
.object({
+1 -1
View File
@@ -54,7 +54,7 @@ export namespace SessionRetry {
if (MessageV2.APIError.isInstance(error)) {
if (!error.data.isRetryable) return undefined
if (error.data.responseBody?.includes("FreeUsageLimitError"))
return `Free usage exceeded, add credits https://opencode.ai/zen`
return `Free usage exceeded, subscribe to Go https://opencode.ai/go`
return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
}