Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/console/app/src/routes/zen/v1/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ZenData } from "@opencode-ai/console-core/model.js"

export async function OPTIONS(input: APIEvent) {
return new Response(null, {
status: 200,
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ async function getAllSessions(): Promise<Session.Info[]> {
const sessions: Session.Info[] = []

const projectKeys = await Storage.list(["project"])
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key).catch(() => undefined)))

for (const project of projects) {
if (!project) continue

const sessionKeys = await Storage.list(["session", project.id])
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key)))
const projectSessions = await Promise.all(sessionKeys.map((key) => Storage.read<Session.Info>(key).catch(() => undefined)))

for (const session of projectSessions) {
if (session) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function DialogAgent() {
return (
<DialogSelect
title="Select agent"
current={local.agent.current().name}
current={local.agent.current()?.name ?? ""}
options={options()}
onSelect={(option) => {
local.agent.set(option.value)
Expand Down
18 changes: 12 additions & 6 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,10 @@ export function Prompt(props: PromptProps) {
promptModelWarning()
return
}
const currentAgent = local.agent.current()
if (!currentAgent) {
return
}
const sessionID = props.sessionID
? props.sessionID
: await (async () => {
Expand Down Expand Up @@ -531,7 +535,7 @@ export function Prompt(props: PromptProps) {
if (store.mode === "shell") {
sdk.client.session.shell({
sessionID,
agent: local.agent.current().name,
agent: currentAgent.name,
model: {
providerID: selectedModel.providerID,
modelID: selectedModel.modelID,
Expand All @@ -552,7 +556,7 @@ export function Prompt(props: PromptProps) {
sessionID,
command: command.slice(1),
arguments: args.join(" "),
agent: local.agent.current().name,
agent: currentAgent.name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID,
variant,
Expand All @@ -569,7 +573,7 @@ export function Prompt(props: PromptProps) {
sessionID,
...selectedModel,
messageID,
agent: local.agent.current().name,
agent: currentAgent.name,
model: selectedModel,
variant,
parts: [
Expand Down Expand Up @@ -690,7 +694,8 @@ export function Prompt(props: PromptProps) {
const highlight = createMemo(() => {
if (keybind.leader) return theme.border
if (store.mode === "shell") return theme.primary
return local.agent.color(local.agent.current().name)
const agent = local.agent.current()
return agent ? local.agent.color(agent.name) : theme.primary
})

const showVariant = createMemo(() => {
Expand All @@ -701,7 +706,8 @@ export function Prompt(props: PromptProps) {
})

const spinnerDef = createMemo(() => {
const color = local.agent.color(local.agent.current().name)
const agent = local.agent.current()
const color = agent ? local.agent.color(agent.name) : theme.primary
return {
frames: createFrames({
color,
Expand Down Expand Up @@ -935,7 +941,7 @@ export function Prompt(props: PromptProps) {
/>
<box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
<text fg={highlight()}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
{store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current()?.name ?? "Agent")}{" "}
</text>
<Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}>
Expand Down
21 changes: 15 additions & 6 deletions packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const agent = iife(() => {
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [agentStore, setAgentStore] = createStore<{
current: string
current: string | undefined
}>({
current: agents()[0].name,
current: agents()[0]?.name,
})
const { theme } = useTheme()
const colors = createMemo(() => [
Expand All @@ -54,7 +54,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return agents()
},
current() {
return agents().find((x) => x.name === agentStore.current)!
if (!agentStore.current) return agents()[0]
return agents().find((x) => x.name === agentStore.current) ?? agents()[0]
},
set(name: string) {
if (!agents().some((x) => x.name === name))
Expand Down Expand Up @@ -179,6 +180,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({

const currentModel = createMemo(() => {
const a = agent.current()
if (!a) return fallbackModel()
return (
getFirstValidModel(
() => modelStore.model[a.name],
Expand Down Expand Up @@ -219,6 +221,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
cycle(direction: 1 | -1) {
const current = currentModel()
if (!current) return
const currentAgent = agent.current()
if (!currentAgent) return
const recent = modelStore.recent
const index = recent.findIndex((x) => x.providerID === current.providerID && x.modelID === current.modelID)
if (index === -1) return
Expand All @@ -227,7 +231,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (next >= recent.length) next = 0
const val = recent[next]
if (!val) return
setModelStore("model", agent.current().name, { ...val })
setModelStore("model", currentAgent.name, { ...val })
},
cycleFavorite(direction: 1 | -1) {
const favorites = modelStore.favorite.filter((item) => isModelValid(item))
Expand All @@ -239,6 +243,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
return
}
const currentAgent = agent.current()
if (!currentAgent) return
const current = currentModel()
let index = -1
if (current) {
Expand All @@ -253,7 +259,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
const next = favorites[index]
if (!next) return
setModelStore("model", agent.current().name, { ...next })
setModelStore("model", currentAgent.name, { ...next })
const uniq = uniqueBy([next, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
if (uniq.length > 10) uniq.pop()
setModelStore(
Expand All @@ -264,6 +270,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
},
set(model: { providerID: string; modelID: string }, options?: { recent?: boolean }) {
batch(() => {
const currentAgent = agent.current()
if (!currentAgent) return
if (!isModelValid(model)) {
toast.show({
message: `Model ${model.providerID}/${model.modelID} is not valid`,
Expand All @@ -272,7 +280,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})
return
}
setModelStore("model", agent.current().name, model)
setModelStore("model", currentAgent.name, model)
if (options?.recent) {
const uniq = uniqueBy([model, ...modelStore.recent], (x) => `${x.providerID}/${x.modelID}`)
if (uniq.length > 10) uniq.pop()
Expand Down Expand Up @@ -368,6 +376,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
// Automatically update model when agent changes
createEffect(() => {
const value = agent.current()
if (!value) return
if (value.model) {
if (isModelValid(value.model))
model.set({
Expand Down
38 changes: 32 additions & 6 deletions packages/opencode/src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,21 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.read(target)
const result = await Bun.file(target).json()
return result as T
const content = await Bun.file(target).text()
if (!content.trim()) {
throw new NotFoundError({ message: `Empty file: ${target}` })
}
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
if (hasControlCharacters) {
throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
}
try {
const result = JSON.parse(content)
return result as T
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
throw new NotFoundError({ message: `Failed to parse JSON from ${target}: ${message}` })
}
})
}

Expand All @@ -181,10 +194,23 @@ export namespace Storage {
const target = path.join(dir, ...key) + ".json"
return withErrorHandling(async () => {
using _ = await Lock.write(target)
const content = await Bun.file(target).json()
fn(content)
await Bun.write(target, JSON.stringify(content, null, 2))
return content as T
const content = await Bun.file(target).text()
if (!content.trim()) {
throw new NotFoundError({ message: `Empty file: ${target}` })
}
const hasControlCharacters = /[\x00-\x08\x0B\x0C\x0E-\x1F]/.test(content)
if (hasControlCharacters) {
throw new NotFoundError({ message: `Corrupted file detected: ${target}` })
}
try {
const parsed = JSON.parse(content)
fn(parsed)
await Bun.write(target, JSON.stringify(parsed, null, 2))
return parsed as T
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
throw new NotFoundError({ message: `Failed to parse JSON from ${target}: ${message}` })
}
})
}

Expand Down
21 changes: 19 additions & 2 deletions packages/sdk/js/src/gen/client/client.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,27 @@ export const createClient = (config: Config = {}): Client => {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]()
break
case "json": {
const text = await response.text()
if (!text) {
data = {}
} else {
try {
data = JSON.parse(text)
} catch (parseError) {
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError)
throw new Error(
`Failed to parse JSON response: ${errorMessage}. Status: ${response.status}, URL: ${response.url}`
)
}
}
break
}
case "text":
data = await response.text()
break
case "stream":
return opts.responseStyle === "data"
? response.body
Expand Down
21 changes: 19 additions & 2 deletions packages/sdk/js/src/v2/gen/client/client.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,27 @@ export const createClient = (config: Config = {}): Client => {
case "arrayBuffer":
case "blob":
case "formData":
case "json":
case "text":
data = await response[parseAs]()
break
case "json": {
const text = await response.text()
if (!text) {
data = {}
} else {
try {
data = JSON.parse(text)
} catch (parseError) {
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError)
throw new Error(
`Failed to parse JSON response: ${errorMessage}. Status: ${response.status}, URL: ${response.url}`
)
}
}
break
}
case "text":
data = await response.text()
break
case "stream":
return opts.responseStyle === "data"
? response.body
Expand Down