diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 3fd28305368..3f46add2962 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -2,6 +2,7 @@ import path from "path" import { Global } from "../global" import fs from "fs/promises" import z from "zod" +import { Lock } from "../util/lock" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" @@ -43,31 +44,75 @@ export namespace Auth { } export async function all(): Promise> { - const file = Bun.file(filepath) - const data = await file.json().catch(() => ({}) as Record) - return Object.entries(data).reduce( - (acc, [key, value]) => { - const parsed = Info.safeParse(value) - if (!parsed.success) return acc - acc[key] = parsed.data - return acc - }, - {} as Record, - ) + const release = await Lock.read("auth") + try { + const file = Bun.file(filepath) + + if (!(await file.exists())) return {} + + const data = await file.json() + + if (typeof data !== "object" || data === null) { + throw new Error("auth.json contains invalid data") + } + + return Object.entries(data).reduce( + (acc, [key, value]) => { + const parsed = Info.safeParse(value) + if (!parsed.success) return acc + acc[key] = parsed.data + return acc + }, + {} as Record, + ) + } finally { + release[Symbol.dispose]() + } } export async function set(key: string, info: Info) { - const file = Bun.file(filepath) - const data = await all() - await Bun.write(file, JSON.stringify({ ...data, [key]: info }, null, 2)) - await fs.chmod(file.name!, 0o600) + const release = await Lock.write("auth") + try { + const file = Bun.file(filepath) + const exists = await file.exists() + const rawData = exists ? await file.json() : null + const data: Record = {} + + if (typeof rawData === "object" && rawData !== null) { + Object.entries(rawData).forEach(([k, v]) => { + const parsed = Info.safeParse(v) + if (parsed.success) data[k] = parsed.data + }) + } + + data[key] = info + await Bun.write(file, JSON.stringify(data, null, 2)) + await fs.chmod(filepath, 0o600) + } finally { + release[Symbol.dispose]() + } } export async function remove(key: string) { - const file = Bun.file(filepath) - const data = await all() - delete data[key] - await Bun.write(file, JSON.stringify(data, null, 2)) - await fs.chmod(file.name!, 0o600) + const release = await Lock.write("auth") + try { + const file = Bun.file(filepath) + const exists = await file.exists() + const rawData = exists ? await file.json() : null + const data: Record = {} + + if (typeof rawData === "object" && rawData !== null) { + Object.entries(rawData).forEach(([k, v]) => { + const parsed = Info.safeParse(v) + if (parsed.success) data[k] = parsed.data + }) + } + + delete data[key] + await Bun.write(file, JSON.stringify(data, null, 2)) + await fs.chmod(filepath, 0o600) + } finally { + release[Symbol.dispose]() + } } } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 8523f61e548..2dab4a42ad0 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1794,6 +1794,20 @@ function Task(props: ToolProps) { const local = useLocal() const sync = useSync() + // Ensure child session data is loaded before accessing it + createEffect(async () => { + const childSessionId = props.metadata.sessionId + if (!childSessionId) return + const hasMessages = !!sync.data.message[childSessionId]?.length + if (!hasMessages) { + try { + await sync.session.sync(childSessionId) + } catch { + // Silently ignore sync errors - activity will just not show + } + } + }) + const current = createMemo(() => props.metadata.summary?.findLast( (x: { id: string; tool: string; state: { status: string; title?: string } }) => x.state.status !== "pending",