From 0fc33e2a069d0fc9b77efe8e71ef6ee629278537 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:35:50 +0800 Subject: [PATCH 001/157] feat(app): /new-session route for new design (#31457) --- packages/app/src/app.tsx | 92 +++++++++++++++++-- packages/app/src/components/file-tree.test.ts | 2 + packages/app/src/components/prompt-input.tsx | 15 ++- .../components/prompt-input/submit.test.ts | 12 +++ .../app/src/components/prompt-input/submit.ts | 16 +++- packages/app/src/components/titlebar.tsx | 85 +++++++++++++++-- packages/app/src/context/comments.test.ts | 2 + packages/app/src/context/layout.tsx | 11 ++- packages/app/src/context/tabs.tsx | 25 +++-- packages/app/src/context/terminal.test.ts | 2 + packages/app/src/pages/directory-layout.tsx | 4 +- packages/app/src/pages/new-session.tsx | 78 ++++++++++++++++ packages/app/src/pages/session.tsx | 19 +--- .../pages/session/new-session-layout.test.ts | 14 --- .../src/pages/session/new-session-layout.ts | 4 - 15 files changed, 320 insertions(+), 61 deletions(-) create mode 100644 packages/app/src/pages/new-session.tsx delete mode 100644 packages/app/src/pages/session/new-session-layout.test.ts diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index ad9aa3543f..5bf18b657a 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -9,7 +9,7 @@ import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" import { ThemeProvider } from "@opencode-ai/ui/theme/context" import { MetaProvider } from "@solidjs/meta" -import { type BaseRouterProps, Navigate, Route, Router } from "@solidjs/router" +import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router" import { QueryClient, QueryClientProvider } from "@tanstack/solid-query" import { Effect } from "effect" import { @@ -43,25 +43,88 @@ import { PromptProvider } from "@/context/prompt" import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server" import { SettingsProvider, useSettings } from "@/context/settings" import { TerminalProvider } from "@/context/terminal" -import { TabsProvider } from "@/context/tabs" +import { TabsProvider, useTabs, type DraftTab } from "@/context/tabs" +import { SDKProvider, useSDK } from "@/context/sdk" import { WslServersProvider } from "@/wsl/context" -import DirectoryLayout from "@/pages/directory-layout" +import DirectoryLayout, { DirectoryDataProvider } from "@/pages/directory-layout" import Layout from "@/pages/layout" import { ErrorPage } from "./pages/error" import { useCheckServerHealth } from "./utils/server-health" const HomeRoute = lazy(() => import("@/pages/home")) const Session = lazy(() => import("@/pages/session")) +const NewSession = lazy(() => import("@/pages/new-session")) const SessionRoute = Object.assign( - () => ( - - - - ), + () => { + const settings = useSettings() + const params = useParams() + const [search] = useSearchParams<{ draftId?: string; prompt?: string }>() + const sdk = useSDK() + const server = useServer() + const tabs = useTabs() + + // When the new layout is enabled, the legacy new-session route (/:dir/session with no id) + // is replaced by a draft at /new-session?draftId=… + createEffect(() => { + if (!settings.general.newLayoutDesigns()) return + if (params.id || search.draftId) return + if (!tabs.ready() || !sdk.directory) return + tabs.newDraft({ server: server.key, directory: sdk.directory }, search.prompt) + }) + + return ( + + + + ) + }, { preload: Session.preload }, ) +function DraftRoute() { + const [search] = useSearchParams<{ draftId?: string }>() + const tabs = useTabs() + return ( + + }> + {(draftID) => } + + + ) +} + +function ResolvedDraftRoute(props: { draftID: string }) { + const server = useServer() + const tabs = useTabs() + const draft = createMemo(() => + tabs.store.find((tab): tab is DraftTab => tab.type === "draft" && tab.draftID === props.draftID), + ) + + createEffect(() => { + const current = draft() + if (current && current.server !== server.key) server.setActive(current.server) + }) + + // Key on the directory so retargeting the draft's project re-instantiates the + // SDK/data providers for the new directory while keeping the same draft id. + const directory = () => draft()?.directory + + return ( + + {(dir) => ( + + + + + + + + )} + + ) +} + function UiI18nBridge(props: ParentProps) { const language = useLanguage() return {props.children} @@ -141,6 +204,18 @@ function SessionProviders(props: ParentProps) { ) } +// The draft page only renders the prompt composer, so it drops TerminalProvider. +// FileProvider and CommentsProvider stay because PromptInput uses file search and comment context. +function DraftProviders(props: ParentProps) { + return ( + + + {props.children} + + + ) +} + function RouterRoot(props: ParentProps<{ appChildren?: JSX.Element }>) { return ( @@ -335,6 +410,7 @@ export function AppInterface(props: { )} > + } /> diff --git a/packages/app/src/components/file-tree.test.ts b/packages/app/src/components/file-tree.test.ts index 29e20b4807..20bffc41a3 100644 --- a/packages/app/src/components/file-tree.test.ts +++ b/packages/app/src/components/file-tree.test.ts @@ -8,6 +8,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@/context/file", () => ({ useFile: () => ({ diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index bdf55fee05..835bc23819 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -31,9 +31,10 @@ import { FileAttachmentPart, } from "@/context/prompt" import { useLayout } from "@/context/layout" -import { useNavigate } from "@solidjs/router" +import { useNavigate, useSearchParams } from "@solidjs/router" import { useSDK } from "@/context/sdk" import { useServer } from "@/context/server" +import { useTabs } from "@/context/tabs" import { useSync } from "@/context/sync" import { useComments } from "@/context/comments" import { Button } from "@opencode-ai/ui/button" @@ -144,6 +145,8 @@ export const PromptInput: Component = (props) => { const platform = usePlatform() const pickDirectory = useDirectoryPicker() const settings = useSettings() + const tabsStore = useTabs() + const [search] = useSearchParams<{ draftId?: string }>() const { params, tabs, view } = useSessionLayout() let editorRef!: HTMLDivElement let fileInputRef: HTMLInputElement | undefined @@ -1398,6 +1401,16 @@ export const PromptInput: Component = (props) => { } layout.projects.open(worktree) server.projects.touch(worktree) + + // On the draft route, retarget the existing draft in place so we keep the same + // draft id (and its tab/prompt) instead of spawning a new draft for the new directory. + const draftID = search.draftId + if (draftID) { + tabsStore.updateDraft(draftID, { server: server.key, directory: worktree }) + restoreFocus() + return + } + navigate(`/${base64Encode(worktree)}/session`) } const addProject = () => { diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 092731a9ea..3e5f0ff1a4 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -61,6 +61,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => params, + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/sdk/v2/client", () => ({ @@ -103,6 +105,16 @@ beforeAll(async () => { }), })) + mock.module("@/context/server", () => ({ + useServer: () => ({ key: "server-key" }), + })) + + mock.module("@/context/tabs", () => ({ + useTabs: () => ({ + promoteDraft: () => undefined, + }), + })) + mock.module("@/context/prompt", () => ({ usePrompt: () => ({ current: () => promptValue, diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 024bdd7ae2..bd4d985088 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -2,9 +2,11 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client" import { showToast } from "@/utils/toast" import { base64Encode } from "@opencode-ai/core/util/encode" import { Binary } from "@opencode-ai/core/util/binary" -import { useNavigate, useParams } from "@solidjs/router" +import { useNavigate, useParams, useSearchParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" +import { useServer } from "@/context/server" +import { useTabs } from "@/context/tabs" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" @@ -213,6 +215,9 @@ export function createPromptSubmit(input: PromptSubmitInput) { const layout = useLayout() const language = useLanguage() const params = useParams() + const [search] = useSearchParams<{ draftId?: string }>() + const server = useServer() + const tabs = useTabs() const pendingKey = (sessionID: string) => ScopedKey.from(sdk.scope, sessionID) const errorMessage = (err: unknown) => { @@ -381,7 +386,14 @@ export function createPromptSubmit(input: PromptSubmitInput) { if (shouldAutoAccept) permission.enableAutoAccept(session.id, sessionDirectory) local.session.promote(sessionDirectory, session.id) layout.handoff.setTabs(base64Encode(sessionDirectory), session.id) - navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) + const draftID = search.draftId + if (draftID) + tabs.promoteDraft(draftID, { + server: server.key, + dirBase64: base64Encode(sessionDirectory), + sessionId: session.id, + }) + else navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`) } } if (!session) { diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 019f4f4708..a4fcde5aac 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -280,7 +280,8 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const matchRoute = (route: LayoutRoute) => { if (route.type === "home") return - if (route.type === "dir-new-sesssion") { + if (route.type === "draft") { + return tabsStore.find((item) => item.type === "draft" && item.draftID === route.draftID) } if (route.type === "session") { const main = tabsStore.find( @@ -447,13 +448,33 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { refreshTabsAreOverflowing() }) - if (tab.type !== "session") return null + const divider = () => + i() !== 0 && ( + + ) + + if (tab.type === "draft") { + return ( + <> + {divider()} + { + navigateTab(tab) + ref.scrollIntoView({ behavior: "instant" }) + }} + onClose={() => tabsStoreActions.removeTab(i())} + /> + > + ) + } return ( <> - {i() !== 0 && ( - - )} + {divider()} {(session) => { - console.log({ session: session() }) const project = createMemo(() => projectForSession(session(), serverCtx()?.projects.list() ?? [])) return ( @@ -853,6 +873,59 @@ function ProjectTabAvatar(props: { ) } +function DraftTabItem(props: { + ref?: HTMLDivElement + href: string + title: string + active?: boolean + onNavigate: () => void + onClose: () => void +}) { + const closeTab = (event: MouseEvent) => { + event.preventDefault() + event.stopPropagation() + props.onClose() + } + return ( + { + if (event.button !== 1) return + closeTab(event) + }} + > + { + event.preventDefault() + props.onNavigate() + }} + class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 overflow-hidden text-[13px] font-medium leading-5 text-v2-text-text-faint group-data-[active='true']:text-[var(--v2-text-text-base)]" + > + + + + {props.title} + + + { + event.preventDefault() + event.stopPropagation() + }} + onClick={closeTab} + icon={} + aria-label="Close tab" + /> + + + ) +} + function NewSessionTabItem(props: { ref?: HTMLDivElement; href: string; title: string; onClose: () => void }) { const closeTab = (event: MouseEvent) => { event.preventDefault() diff --git a/packages/app/src/context/comments.test.ts b/packages/app/src/context/comments.test.ts index 82fa170f2f..5050409a81 100644 --- a/packages/app/src/context/comments.test.ts +++ b/packages/app/src/context/comments.test.ts @@ -8,6 +8,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/ui/context", () => ({ createSimpleContext: () => ({ diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 49d04df546..f9e954b98b 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -77,6 +77,7 @@ export type ReviewDiffStyle = "unified" | "split" export type LayoutRoute = | { type: "home" } + | { type: "draft"; draftID: string; server?: ServerConnection.Key } | { type: "dir-new-sesssion"; dir: string; dirBase64: string; server?: ServerConnection.Key } | { type: "session"; dir: string; dirBase64: string; sessionId: string; server?: ServerConnection.Key } @@ -120,10 +121,16 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { } } -const currentRoute = (pathname: string): LayoutRoute => { +const currentRoute = (pathname: string, search: string): LayoutRoute => { const parts = pathname.split("/").filter(Boolean) if (parts.length === 0) return { type: "home" } + if (parts[0] === "new-session") { + const draftID = new URLSearchParams(search).get("draftId") + if (!draftID) return { type: "home" } + return { type: "draft", draftID } + } + const dirBase64 = parts[0] const dir = decode64(dirBase64) if (!dir) return { type: "home" } @@ -145,7 +152,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( const platform = usePlatform() const location = useLocation() const route = createMemo(() => { - const value = currentRoute(location.pathname) + const value = currentRoute(location.pathname, location.search) if (value.type === "home") return value return { ...value, server: server.key } }) diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index cc43bac03b..7f6274cf99 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -5,7 +5,7 @@ import { createStore, produce } from "solid-js/store" import { Persist, persisted, removePersisted, draftPersistedKeys } from "@/utils/persist" import { ServerConnection, useServer } from "./server" import { createEffect, startTransition } from "solid-js" -import { useNavigate, useParams } from "@solidjs/router" +import { useLocation, useNavigate, useParams } from "@solidjs/router" import { usePlatform } from "./platform" import { uuid } from "@/utils/uuid" import { SessionTabsRemovedDetail } from "@/components/titlebar-session-events" @@ -65,6 +65,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const params = useParams() const navigate = useNavigate() + const location = useLocation() const closing = new Set() @@ -123,14 +124,20 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ ) }, promoteDraft(draftID: string, session: Omit) { - const active = `${location.pathname}${location.search}` === draftHref(draftID) - setStore( - produce((tabs) => { - const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) - if (index !== -1) tabs[index] = { type: "session", ...session } - }), - ) - if (active) navigateTab({ type: "session", ...session }) + // We're viewing this draft when /new-session?draftId=… points at it. Promoting + // replaces the draft tab with a session tab, so the draft route would stop resolving + // and fall back home. Navigate to the new session first so we leave /new-session + // before the draft is removed from the store. + const active = location.pathname === "/new-session" && location.query.draftId === draftID + startTransition(() => { + setStore( + produce((tabs) => { + const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID) + if (index !== -1) tabs[index] = { type: "session", ...session } + }), + ) + if (active) navigateTab({ type: "session", ...session }) + }) removeDraftPersisted(draftID) }, removeTab: (index: number) => { diff --git a/packages/app/src/context/terminal.test.ts b/packages/app/src/context/terminal.test.ts index 4f16953c79..25ef35dd41 100644 --- a/packages/app/src/context/terminal.test.ts +++ b/packages/app/src/context/terminal.test.ts @@ -9,6 +9,8 @@ beforeAll(async () => { mock.module("@solidjs/router", () => ({ useNavigate: () => () => undefined, useParams: () => ({}), + useLocation: () => ({}), + useSearchParams: () => [{}, () => undefined], })) mock.module("@opencode-ai/ui/context", () => ({ createSimpleContext: () => ({ diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 2d0dfd81dc..e03d5c206f 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -10,7 +10,7 @@ import { useSync } from "@/context/sync" import { decode64 } from "@/utils/base64" import { Schema } from "effect" -function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { +export function DirectoryDataProvider(props: ParentProps<{ directory: string; draftID?: string }>) { const location = useLocation() const navigate = useNavigate() const params = useParams() @@ -18,6 +18,8 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) { const slug = createMemo(() => base64Encode(props.directory)) createEffect(() => { + // A draft lives at /new-session?draftId=… and has no directory segment to normalize. + if (props.draftID) return const next = sync.data.path.directory if (!next || next === props.directory) return const path = location.pathname.slice(slug().length + 1) diff --git a/packages/app/src/pages/new-session.tsx b/packages/app/src/pages/new-session.tsx new file mode 100644 index 0000000000..16f30af378 --- /dev/null +++ b/packages/app/src/pages/new-session.tsx @@ -0,0 +1,78 @@ +import { createEffect, createMemo, onMount, untrack } from "solid-js" +import { createStore } from "solid-js/store" +import { useSearchParams } from "@solidjs/router" +import { NewSessionDesignView } from "@/components/session" +import { useComments } from "@/context/comments" +import { usePrompt } from "@/context/prompt" +import { useSDK } from "@/context/sdk" +import { useSync } from "@/context/sync" +import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer" + +/** + * The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt + * composer for a brand-new session — no terminal, review pane, file tree, or message + * timeline. Submitting promotes the draft into a real session (see prompt-input/submit). + */ +export default function NewSessionPage() { + const prompt = usePrompt() + const sdk = useSDK() + const sync = useSync() + const comments = useComments() + const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>() + + let inputRef: HTMLDivElement | undefined + + const composer = createSessionComposerState() + + const [store, setStore] = createStore({ + worktree: "main", + }) + + const newSessionWorktree = createMemo(() => { + if (store.worktree === "create") return "create" + const project = sync.project + if (project && sdk.directory !== project.worktree) return sdk.directory + return "main" + }) + + createEffect(() => { + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) + }) + + onMount(() => { + requestAnimationFrame(() => inputRef?.focus()) + }) + + return ( + + + + + + { + inputRef = el + }} + newSessionWorktree={newSessionWorktree()} + onNewSessionWorktreeReset={() => setStore("worktree", "main")} + onSubmit={() => comments.clear()} + onResponseSubmit={() => {}} + setPromptDockRef={() => {}} + /> + + + + + + ) +} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index cc80a27031..f7bcc93bdc 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -31,7 +31,7 @@ import { Button } from "@opencode-ai/ui/button" import { showToast } from "@/utils/toast" import { checksum } from "@opencode-ai/core/util/encode" import { useLocation, useSearchParams } from "@solidjs/router" -import { NewSessionDesignView, NewSessionView, SessionHeader } from "@/components/session" +import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch" import { useServerSync } from "@/context/server-sync" @@ -63,7 +63,6 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel" import { TerminalPanel } from "@/pages/session/terminal-panel" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" -import { shouldUseV2NewSessionPage } from "@/pages/session/new-session-layout" import { Identifier } from "@/utils/id" import { diffs as list } from "@/utils/diffs" import { Persist, persisted } from "@/utils/persist" @@ -271,13 +270,10 @@ export default function Page() { const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() - const isV2NewSessionPage = () => - shouldUseV2NewSessionPage({ newLayoutDesigns: newSessionDesign(), sessionID: params.id }) - const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage()) + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) const desktopFileTreeOpen = createMemo( () => isDesktop() && - !isV2NewSessionPage() && shouldShowFileTree({ desktopV2: platform.platform === "desktop" && settings.general.newLayoutDesigns(), showFileTree: settings.general.showFileTree(), @@ -1757,10 +1753,9 @@ export default function Page() { - }> - {composerRegion("inline")} - + diff --git a/packages/app/src/pages/session/new-session-layout.test.ts b/packages/app/src/pages/session/new-session-layout.test.ts deleted file mode 100644 index 436e0a59c7..0000000000 --- a/packages/app/src/pages/session/new-session-layout.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { shouldUseV2NewSessionPage } from "./new-session-layout" - -describe("shouldUseV2NewSessionPage", () => { - test("keeps disabled pages on the legacy layout", () => { - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false, sessionID: "ses_123" })).toBe(false) - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: false })).toBe(false) - }) - - test("uses the v2 layout only for enabled new-session pages", () => { - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true })).toBe(true) - expect(shouldUseV2NewSessionPage({ newLayoutDesigns: true, sessionID: "ses_123" })).toBe(false) - }) -}) diff --git a/packages/app/src/pages/session/new-session-layout.ts b/packages/app/src/pages/session/new-session-layout.ts index 7429c7c7e8..edd953ed2d 100644 --- a/packages/app/src/pages/session/new-session-layout.ts +++ b/packages/app/src/pages/session/new-session-layout.ts @@ -1,6 +1,2 @@ /** Inline new-session content width — keep in sync with session composer `placement === "inline"`. */ export const NEW_SESSION_CONTENT_WIDTH = "w-full max-w-[720px] px-0" - -export function shouldUseV2NewSessionPage(input: { newLayoutDesigns: boolean; sessionID?: string }) { - return input.newLayoutDesigns && !input.sessionID -} From 8a2cfc00c93afc32a79979b7a928bc55d6483934 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 10 Jun 2026 00:08:26 -0400 Subject: [PATCH 002/157] feat(core): add project reference guidance (#31601) --- .opencode/opencode.jsonc | 11 +- packages/core/src/config/plugin/reference.ts | 4 + packages/core/src/config/reference.ts | 4 + packages/core/src/location-layer.ts | 24 ++- packages/core/src/location.ts | 7 +- .../src/plugin/skill/customize-opencode.md | 46 +++++ packages/core/src/reference.ts | 40 ++++- packages/core/src/reference/guidance.ts | 69 ++++++++ packages/core/src/ripgrep.ts | 8 +- packages/core/src/session/runner/llm.ts | 8 +- packages/core/src/v1/config/config.ts | 4 +- packages/core/src/v1/config/migrate.ts | 3 +- packages/core/src/v1/config/reference.ts | 24 --- packages/core/test/config/config.test.ts | 8 +- packages/core/test/location-layer.test.ts | 18 +- packages/core/test/reference-guidance.test.ts | 77 +++++++++ packages/core/test/reference.test.ts | 39 ++++- packages/core/test/ripgrep.test.ts | 26 ++- .../core/test/session-runner-recorded.test.ts | 3 + packages/core/test/session-runner.test.ts | 3 + packages/opencode/src/agent/agent.ts | 24 ++- packages/opencode/src/cli/cmd/debug/file.ts | 3 +- packages/opencode/src/cli/cmd/debug/v2.ts | 9 +- .../routes/instance/httpapi/handlers/file.ts | 4 +- .../routes/instance/httpapi/handlers/pty.ts | 13 +- packages/opencode/src/session/system.ts | 36 +++- packages/opencode/test/agent/agent.test.ts | 23 ++- .../agent/plugin-agent-regression.test.ts | 2 + .../test/server/httpapi-reference.test.ts | 10 +- packages/opencode/test/session/system.test.ts | 2 + packages/sdk/js/src/v2/gen/types.gen.ts | 43 ++--- packages/sdk/openapi.json | 108 +++++++----- packages/server/src/groups/location.ts | 4 +- .../server/src/middleware/session-location.ts | 11 +- .../tui/src/component/prompt/autocomplete.tsx | 40 ++--- packages/web/astro.config.mjs | 1 + packages/web/src/content/docs/references.mdx | 157 ++++++++++++++++++ packages/web/src/content/docs/tui.mdx | 2 +- 38 files changed, 753 insertions(+), 165 deletions(-) create mode 100644 packages/core/src/reference/guidance.ts delete mode 100644 packages/core/src/v1/config/reference.ts create mode 100644 packages/core/test/reference-guidance.test.ts create mode 100644 packages/web/src/content/docs/references.mdx diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 7f07577f8c..b0f7d59447 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -2,8 +2,15 @@ "$schema": "https://opencode.ai/config.json", "provider": {}, "permission": {}, - "reference": { - "effect": "github.com/Effect-TS/effect-smol", + "references": { + "effect": { + "repository": "github.com/Effect-TS/effect-smol", + "description": "Use for Effect v4 and effect-smol implementation details", + }, + "opencode-local": { + "path": "~/.local/share/opencode", + "description": "Contains opencode logs and data", + }, }, "mcp": {}, "tools": { diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index 81e0804b4e..22c7664996 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -33,11 +33,15 @@ export const Plugin = { path: AbsolutePath.make( localPath(directory, global.home, typeof entry === "string" ? entry : entry.path), ), + description: typeof entry === "string" ? undefined : entry.description, + hidden: typeof entry === "string" ? undefined : entry.hidden, }) : new Reference.GitSource({ type: "git", repository: typeof entry === "string" ? entry : entry.repository, branch: typeof entry === "string" ? undefined : entry.branch, + description: typeof entry === "string" ? undefined : entry.description, + hidden: typeof entry === "string" ? undefined : entry.hidden, }), ) } diff --git a/packages/core/src/config/reference.ts b/packages/core/src/config/reference.ts index 040169855f..4518eb4f87 100644 --- a/packages/core/src/config/reference.ts +++ b/packages/core/src/config/reference.ts @@ -5,10 +5,14 @@ import { Schema } from "effect" export class Git extends Schema.Class("ConfigV2.Reference.Git")({ repository: Schema.String, branch: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export class Local extends Schema.Class("ConfigV2.Reference.Local")({ path: Schema.String, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export const Entry = Schema.Union([Schema.String, Git, Local]) diff --git a/packages/core/src/location-layer.ts b/packages/core/src/location-layer.ts index a0578066a0..2bf32fd755 100644 --- a/packages/core/src/location-layer.ts +++ b/packages/core/src/location-layer.ts @@ -1,4 +1,4 @@ -import { Layer, LayerMap } from "effect" +import { Effect, Layer, LayerMap } from "effect" import { Location } from "./location" import { Policy } from "./policy" import { Config } from "./config" @@ -23,6 +23,7 @@ import { Watcher } from "./filesystem/watcher" import { LocationMutation } from "./location-mutation" import { FileMutation } from "./file-mutation" import { Reference } from "./reference" +import { ReferenceGuidance } from "./reference/guidance" import { RepositoryCache } from "./repository-cache" import { Pty } from "./pty" import { SkillV2 } from "./skill" @@ -45,6 +46,9 @@ import { FetchHttpClient } from "effect/unstable/http" export class LocationServiceMap extends LayerMap.Service()("@opencode/example/LocationServiceMap", { lookup: (ref: Location.Ref) => { + const boot = Layer.effectDiscard( + Effect.logInfo("booting location services", { directory: ref.directory, workspaceID: ref.workspaceID }), + ) const location = Location.layer(ref) const systemContext = SystemContextBuiltIns.locationLayer const base = Layer.mergeAll( @@ -74,6 +78,7 @@ export class LocationServiceMap extends LayerMap.Service()(" const image = Image.layer.pipe(Layer.provide(services)) const mutation = FileMutation.locationLayer.pipe(Layer.provide(services)) const skillGuidance = SkillGuidance.locationLayer.pipe(Layer.provide(services)) + const referenceGuidance = ReferenceGuidance.locationLayer.pipe(Layer.provide(services)) const todos = SessionTodo.layer.pipe(Layer.provide(services)) const questions = QuestionV2.locationLayer.pipe(Layer.provide(services)) const builtInTools = BuiltInTools.locationLayer.pipe( @@ -89,10 +94,21 @@ export class LocationServiceMap extends LayerMap.Service()(" Layer.provide(services), Layer.provide(model), Layer.provide(skillGuidance), + Layer.provide(referenceGuidance), ) - return Layer.mergeAll(services, image, mutation, resources, todos, questions, model, runner, builtInTools).pipe( - Layer.fresh, - ) + return Layer.mergeAll( + boot, + services, + image, + mutation, + resources, + todos, + questions, + model, + runner, + builtInTools, + referenceGuidance, + ).pipe(Layer.fresh) }, idleTimeToLive: "60 minutes", dependencies: [ diff --git a/packages/core/src/location.ts b/packages/core/src/location.ts index b8020b3c78..ebfb096f13 100644 --- a/packages/core/src/location.ts +++ b/packages/core/src/location.ts @@ -5,11 +5,10 @@ import { WorkspaceV2 } from "./workspace" export * as Location from "./location" -export const Ref = Schema.Struct({ +export class Ref extends Schema.Class("Location.Ref")({ directory: AbsolutePath, - workspaceID: Schema.optional(WorkspaceV2.ID), -}).annotate({ identifier: "Location.Ref" }) -export type Ref = typeof Ref.Type + workspaceID: Schema.optional(WorkspaceV2.ID).pipe(Schema.withConstructorDefault(Effect.succeed(undefined))), +}) {} export class Info extends Schema.Class("Location.Info")({ directory: AbsolutePath, diff --git a/packages/core/src/plugin/skill/customize-opencode.md b/packages/core/src/plugin/skill/customize-opencode.md index 99b112d9a5..1c1cbdf3c2 100644 --- a/packages/core/src/plugin/skill/customize-opencode.md +++ b/packages/core/src/plugin/skill/customize-opencode.md @@ -73,6 +73,19 @@ Every field is optional. "urls": ["https://example.com/.well-known/skills/"] }, + "references": { + "docs": { + "path": "../docs", + "description": "Use for product behavior and documentation conventions" + }, + "sdk": { + "repository": "owner/sdk", + "branch": "main", + "description": "Use for SDK implementation details", + "hidden": true + } + }, + "agent": { "my-agent": { "model": "anthropic/claude-sonnet-4-6", @@ -136,6 +149,7 @@ Shape notes worth being explicit about: - `model` always carries a provider prefix: `"anthropic/claude-sonnet-4-6"`. - `skills` is an object with `paths` and/or `urls`, not an array. +- `references` is an object keyed by alias. Each value is a local path, Git repository, or string shorthand. - `agent` is an object keyed by agent name, not an array. - `plugin` is an array of strings or `[name, options]` tuples, not an object. - `mcp[name].command` is an array of strings, never a single string. `type` is required. @@ -172,6 +186,38 @@ Register skills from non-default locations via `skills.paths` (scanned recursively for `**/SKILL.md`) and `skills.urls` (each URL serves a list of skills). +## References + +References make local directories and Git repositories outside the active +project available as supporting context. Configure them under `references`, +keyed by the alias used in `@` autocomplete: + +```json +{ + "references": { + "docs": { + "path": "../product-docs", + "description": "Use for product behavior and terminology" + }, + "effect": { + "repository": "Effect-TS/effect", + "branch": "main", + "description": "Use for Effect implementation details" + } + } +} +``` + +Local `path` values may be relative to the declaring config, absolute, or use +`~/`. Git `repository` values accept Git URLs, host/path references, and GitHub +`owner/repo` shorthand; `branch` is optional. Both forms support optional +`description` and `hidden` fields. + +- Only references with a `description` are advertised to agents in system context. +- `hidden: true` removes a reference from TUI `@` autocomplete only. It remains available to agents and by direct path. +- Reference directories are automatically allowed through the external-directory boundary; normal read/edit/tool permissions still apply. +- String shorthand is supported: use `"docs": "../docs"` for local paths or `"effect": "Effect-TS/effect"` for Git repositories. + ## Agents Two ways to define an agent. Use the file form for anything non-trivial. diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 9c354f55c3..66eb160eb4 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -9,21 +9,19 @@ import { RepositoryCache } from "./repository-cache" import { AbsolutePath } from "./schema" import { State } from "./state" -export class Info extends Schema.Class("Reference.Info")({ - name: Schema.String, - path: AbsolutePath, - source: Schema.suspend(() => Source), -}) {} - export class LocalSource extends Schema.Class("Reference.LocalSource")({ type: Schema.Literal("local"), path: AbsolutePath, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export class GitSource extends Schema.Class("Reference.GitSource")({ type: Schema.Literal("git"), repository: Schema.String, branch: Schema.String.pipe(Schema.optional), + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), }) {} export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type")) @@ -33,6 +31,14 @@ export const Event = { Updated: EventV2.define({ type: "reference.updated", schema: {} }), } +export class Info extends Schema.Class("Reference.Info")({ + name: Schema.String, + path: AbsolutePath, + description: Schema.String.pipe(Schema.optional), + hidden: Schema.Boolean.pipe(Schema.optional), + source: Source, +}) {} + type Data = { sources: Map } @@ -71,7 +77,16 @@ export const layer = Layer.effect( const seen = new Map() for (const [name, source] of editor.list()) { if (source.type === "local") { - materialized.set(name, new Info({ name, path: source.path, source })) + materialized.set( + name, + new Info({ + name, + path: source.path, + description: source.description, + hidden: source.hidden, + source, + }), + ) continue } const repository = Repository.parse(source.repository) @@ -86,7 +101,16 @@ export const layer = Layer.effect( const target = Repository.cachePath(global.repos, repository) if (seen.has(target) && seen.get(target) !== source.branch) continue seen.set(target, source.branch) - materialized.set(name, new Info({ name, path: AbsolutePath.make(target), source })) + materialized.set( + name, + new Info({ + name, + path: AbsolutePath.make(target), + description: source.description, + hidden: source.hidden, + source, + }), + ) yield* cache.ensure({ reference: repository, branch: source.branch, refresh: true }).pipe( Effect.catchCause((cause) => Effect.logWarning("failed to materialize reference", { diff --git a/packages/core/src/reference/guidance.ts b/packages/core/src/reference/guidance.ts new file mode 100644 index 0000000000..f567264768 --- /dev/null +++ b/packages/core/src/reference/guidance.ts @@ -0,0 +1,69 @@ +export * as ReferenceGuidance from "./guidance" + +import { Context, Effect, Layer, Schema } from "effect" +import { PluginBoot } from "../plugin/boot" +import { Reference } from "../reference" +import { SystemContext } from "../system-context/index" + +const Summary = Schema.Struct({ + name: Schema.String, + path: Schema.String, + description: Schema.String.pipe(Schema.optional), +}) + +const render = (references: ReadonlyArray) => + [ + "Project references provide additional directories that can be accessed when relevant.", + "", + ...references.flatMap((reference) => [ + " ", + ` ${reference.name}`, + ` ${reference.path}`, + ...(reference.description === undefined ? [] : [` ${reference.description}`]), + " ", + ]), + "", + ].join("\n") + +export interface Interface { + readonly load: () => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/v2/ReferenceGuidance") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const boot = yield* PluginBoot.Service + const references = yield* Reference.Service + + return Service.of({ + load: Effect.fn("ReferenceGuidance.load")(function* () { + yield* boot.wait() + const available = (yield* references.list()) + .filter((reference) => reference.description !== undefined) + .map((reference) => ({ + name: reference.name, + path: reference.path, + description: reference.description, + })) + .toSorted((a, b) => a.name.localeCompare(b.name)) + if (available.length === 0) return SystemContext.empty + return SystemContext.make({ + key: SystemContext.Key.make("core/reference-guidance"), + codec: Schema.toCodecJson(Schema.Array(Summary)), + load: Effect.succeed(available), + baseline: render, + update: (_previous, current) => + [ + "The available project references have changed. This list supersedes the previous reference list.", + render(current), + ].join("\n"), + removed: () => "Project reference guidance is no longer available. Do not use previously listed references.", + }) + }), + }) + }), +) + +export const locationLayer = layer diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index 18b7b29870..822c80458c 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -161,10 +161,10 @@ export const layer = Layer.effect( args: [ "--no-config", "--files", - "--glob=!**/.git/**", ...(input.hidden ? ["--hidden"] : []), ...(input.follow ? ["--follow"] : []), `--glob=${input.pattern}`, + "--glob=!**/.git/**", ".", ], parse: (line) => @@ -195,10 +195,10 @@ export const layer = Layer.effect( args: [ "--no-config", "--files", - "--glob=!**/.git/**", ...(input.hidden ? ["--hidden"] : []), ...(input.follow ? ["--follow"] : []), - `--glob=${input.pattern}`, + ...(input.pattern === "*" ? [] : [`--glob=${input.pattern}`]), + "--glob=!**/.git/**", ".", ], parse: (line) => { @@ -226,9 +226,9 @@ export const layer = Layer.effect( "--no-config", "--json", "--hidden", - "--glob=!**/.git/**", "--no-messages", ...(input.include ? [`--glob=${input.include}`] : []), + "--glob=!**/.git/**", "--", input.pattern, input.file ?? ".", diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index 88ba79098a..02a1eb3fed 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -19,6 +19,7 @@ import { QuestionV2 } from "../../question" import { SystemContext } from "../../system-context/index" import { SystemContextRegistry } from "../../system-context/registry" import { SkillGuidance } from "../../skill/guidance" +import { ReferenceGuidance } from "../../reference/guidance" import { ToolRegistry } from "../../tool/registry" import { ToolOutputStore } from "../../tool-output-store" import { SessionContextEpoch } from "../context-epoch" @@ -98,6 +99,7 @@ export const layer = Layer.effect( const location = yield* Location.Service const systemContext = yield* SystemContextRegistry.Service const skillGuidance = yield* SkillGuidance.Service + const referenceGuidance = yield* ReferenceGuidance.Service const config = yield* Config.Service const db = (yield* Database.Service).db const compaction = SessionCompaction.make({ events, llm, config: yield* config.entries() }) @@ -166,9 +168,9 @@ export const layer = Layer.effect( const sameModel = Schema.toEquivalence(Schema.UndefinedOr(ModelV2.Ref)) const loadSystemContext = (agent: AgentV2.Selection) => - Effect.all([systemContext.load(), skillGuidance.load(agent)], { concurrency: "unbounded" }).pipe( - Effect.map(SystemContext.combine), - ) + Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], { + concurrency: "unbounded", + }).pipe(Effect.map(SystemContext.combine)) const runTurnAttempt = Effect.fn("SessionRunner.runTurn")(function* ( sessionID: SessionSchema.ID, diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index cea9d3454b..5c520846da 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -3,6 +3,7 @@ export * as ConfigV1 from "./config" import { Schema } from "effect" import { NonNegativeInt, PositiveInt, type DeepMutable } from "../../schema" import { ConfigExperimental } from "../../config/experimental" +import { ConfigReference } from "../../config/reference" import { ConfigAgentV1 } from "./agent" import { ConfigAttachmentV1 } from "./attachment" import { ConfigCommandV1 } from "./command" @@ -13,7 +14,6 @@ import { ConfigMCPV1 } from "./mcp" import { ConfigPermissionV1 } from "./permission" import { ConfigPluginV1 } from "./plugin" import { ConfigProviderV1 } from "./provider" -import { ConfigReferenceV1 } from "./reference" import { ConfigServerV1 } from "./server" import { ConfigSkillsV1 } from "./skills" @@ -42,7 +42,7 @@ export const Info = Schema.Struct({ description: "Command configuration, see https://opencode.ai/docs/commands", }), skills: Schema.optional(ConfigSkillsV1.Info).annotate({ description: "Additional skill folder paths" }), - reference: Schema.optional(ConfigReferenceV1.Info).annotate({ + references: Schema.optional(ConfigReference.Info).annotate({ description: "Named git or local directory references", }), watcher: Schema.optional(Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))) })), diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 417e0217e1..19bf48aa74 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -12,7 +12,6 @@ const keys = new Set([ "logLevel", "server", "command", - "reference", "snapshot", "plugin", "autoshare", @@ -63,7 +62,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) { skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])], commands: info.command, instructions: info.instructions, - references: info.reference, + references: info.references, plugins: info.plugin?.map((plugin) => typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] }, ), diff --git a/packages/core/src/v1/config/reference.ts b/packages/core/src/v1/config/reference.ts deleted file mode 100644 index 2e562b9f35..0000000000 --- a/packages/core/src/v1/config/reference.ts +++ /dev/null @@ -1,24 +0,0 @@ -export * as ConfigReferenceV1 from "./reference" - -import { Schema } from "effect" - -const Git = Schema.Struct({ - repository: Schema.String.annotate({ - description: "Git repository URL, host/path reference, or GitHub owner/repo shorthand", - }), - branch: Schema.optional(Schema.String).annotate({ - description: "Branch or ref to clone and inspect", - }), -}) - -const Local = Schema.Struct({ - path: Schema.String.annotate({ - description: "Absolute path, ~/ path, or workspace-relative path to a local reference directory", - }), -}) - -export const Entry = Schema.Union([Schema.String, Git, Local]).annotate({ identifier: "ReferenceConfigEntry" }) -export type Entry = Schema.Schema.Type - -export const Info = Schema.Record(Schema.String, Entry).annotate({ identifier: "ReferenceConfig" }) -export type Info = Schema.Schema.Type diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 04e1c062f6..5f62cbce61 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -464,7 +464,9 @@ describe("Config", () => { ["@my-org/audit-plugin", { endpoint: "https://audit.example.com" }], ], skills: { paths: ["./skills"], urls: ["https://example.com/.well-known/skills/"] }, - reference: { docs: { path: "../docs" } }, + references: { + docs: { path: "../docs", description: "Use for product documentation", hidden: true }, + }, attachment: { image: { auto_resize: false, max_width: 1200 } }, provider: { custom: { @@ -540,7 +542,9 @@ describe("Config", () => { { package: "@my-org/audit-plugin", options: { endpoint: "https://audit.example.com" } }, ]) expect(documents[0]?.info.skills).toEqual(["./skills", "https://example.com/.well-known/skills/"]) - expect(documents[0]?.info.references).toEqual({ docs: { path: "../docs" } }) + expect(documents[0]?.info.references).toEqual({ + docs: { path: "../docs", description: "Use for product documentation", hidden: true }, + }) expect(documents[0]?.info.attachments).toEqual({ image: { auto_resize: false, max_width: 1200 } }) expect(documents[0]?.info.providers?.custom).toMatchObject({ request: { body: { apiKey: "secret" } }, diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 1a348891d7..ecffef0992 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -1,10 +1,11 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" -import { Effect, Layer, Schema } from "effect" +import { Effect, Equal, Hash, Layer, Schema } from "effect" import { Tool } from "@opencode-ai/core/public" import { Catalog } from "@opencode-ai/core/catalog" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { Location } from "@opencode-ai/core/location" import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -44,6 +45,16 @@ const it = testEffect( ) describe("LocationServiceMap", () => { + it.effect("compares equivalent location refs by value", () => + Effect.sync(() => { + const directory = AbsolutePath.make("/project") + expect(Equal.equals(Location.Ref.make({ directory }), Location.Ref.make({ directory }))).toBe(true) + expect(Hash.hash(Location.Ref.make({ directory }))).toBe( + Hash.hash(Location.Ref.make({ directory, workspaceID: undefined })), + ) + }), + ) + it.live("isolates location state while sharing location policy with catalog", () => Effect.acquireRelease( Effect.promise(() => Promise.all([tmpdir(), tmpdir()])), @@ -79,7 +90,10 @@ describe("LocationServiceMap", () => { providers: yield* catalog.provider.all(), tools: yield* toolDefinitions(yield* ToolRegistry.Service), } - }).pipe(Effect.scoped, Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(directory) }))) + }).pipe( + Effect.scoped, + Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(directory) }))), + ) const blockedState = yield* update(blocked.path) expect(blockedState.providers.some((provider) => provider.id === ProviderV2.ID.make("test"))).toBe(false) diff --git a/packages/core/test/reference-guidance.test.ts b/packages/core/test/reference-guidance.test.ts new file mode 100644 index 0000000000..5e1aba1923 --- /dev/null +++ b/packages/core/test/reference-guidance.test.ts @@ -0,0 +1,77 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { Reference } from "@opencode-ai/core/reference" +import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" +import { SystemContext } from "@opencode-ai/core/system-context/index" +import { it } from "./lib/effect" + +describe("ReferenceGuidance", () => { + it.effect("lists available references in the system context", () => + Effect.gen(function* () { + const guidance = yield* ReferenceGuidance.Service + const generation = yield* SystemContext.initialize(yield* guidance.load()) + + expect(generation.baseline).toContain("") + expect(generation.baseline).toContain("docs") + expect(generation.baseline).toContain("/docs") + expect(generation.baseline).toContain("Use for product documentation") + }).pipe( + Effect.provide(ReferenceGuidance.layer), + Effect.provide( + Layer.mock(Reference.Service, { + list: () => + Effect.succeed([ + new Reference.Info({ + name: "docs", + path: AbsolutePath.make("/docs"), + description: "Use for product documentation", + source: new Reference.LocalSource({ + type: "local", + path: AbsolutePath.make("/docs"), + description: "Use for product documentation", + }), + }), + ]), + }), + ), + Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), + ), + ) + + it.effect("omits guidance when no references are available", () => + Effect.gen(function* () { + const guidance = yield* ReferenceGuidance.Service + const generation = yield* SystemContext.initialize(yield* guidance.load()) + expect(generation.baseline).toBe("") + }).pipe( + Effect.provide(ReferenceGuidance.layer), + Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })), + Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), + ), + ) + + it.effect("omits references without descriptions", () => + Effect.gen(function* () { + const guidance = yield* ReferenceGuidance.Service + const generation = yield* SystemContext.initialize(yield* guidance.load()) + expect(generation.baseline).toBe("") + }).pipe( + Effect.provide(ReferenceGuidance.layer), + Effect.provide( + Layer.mock(Reference.Service, { + list: () => + Effect.succeed([ + new Reference.Info({ + name: "docs", + path: AbsolutePath.make("/docs"), + source: new Reference.LocalSource({ type: "local", path: AbsolutePath.make("/docs") }), + }), + ]), + }), + ), + Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), + ), + ) +}) diff --git a/packages/core/test/reference.test.ts b/packages/core/test/reference.test.ts index 58b28bf06b..dfa8a202a6 100644 --- a/packages/core/test/reference.test.ts +++ b/packages/core/test/reference.test.ts @@ -19,10 +19,16 @@ describe("Reference", () => { const scope = yield* Scope.make() const update = yield* references.transform().pipe(Effect.provideService(Scope.Scope, scope)) const path = AbsolutePath.make("/docs") - yield* update((editor) => editor.add("docs", new Reference.LocalSource({ type: "local", path }))) + const source = new Reference.LocalSource({ + type: "local", + path, + description: "Use for API documentation", + hidden: true, + }) + yield* update((editor) => editor.add("docs", source)) expect(yield* references.list()).toEqual([ - new Reference.Info({ name: "docs", path, source: new Reference.LocalSource({ type: "local", path }) }), + new Reference.Info({ name: "docs", path, description: "Use for API documentation", hidden: true, source }), ]) yield* Scope.close(scope, Exit.void) @@ -58,4 +64,33 @@ describe("Reference", () => { Effect.provide(Global.defaultLayer), ), ) + + it.effect("preserves configured Git descriptions", () => + Effect.gen(function* () { + const references = yield* Reference.Service + const update = yield* references.transform() + const repository = Repository.parseRemote("owner/repo") + const source = new Reference.GitSource({ + type: "git", + repository: "owner/repo", + description: "Use for SDK implementation details", + }) + yield* update((editor) => editor.add("sdk", source)) + + expect(yield* references.list()).toEqual([ + new Reference.Info({ + name: "sdk", + path: AbsolutePath.make(Repository.cachePath(Global.Path.repos, repository)), + description: "Use for SDK implementation details", + source, + }), + ]) + }).pipe( + Effect.scoped, + Effect.provide(Reference.layer), + Effect.provide(cache), + Effect.provide(EventV2.defaultLayer), + Effect.provide(Global.defaultLayer), + ), + ) }) diff --git a/packages/core/test/ripgrep.test.ts b/packages/core/test/ripgrep.test.ts index c91d1dd7c9..d7efe2a2e3 100644 --- a/packages/core/test/ripgrep.test.ts +++ b/packages/core/test/ripgrep.test.ts @@ -10,7 +10,27 @@ import { testEffect } from "./lib/effect" const it = testEffect(Ripgrep.defaultLayer) describe("Ripgrep", () => { - it.live("allows caller globs to re-include git metadata", () => + it.live("keeps ignored files out of catch-all find results", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "node_modules", "pkg"), { recursive: true })) + yield* Effect.promise(() => fs.mkdir(path.join(tmp.path, "src"))) + yield* Effect.promise(() => Bun.$`git init -q ${tmp.path}`) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, ".gitignore"), "node_modules/\n")) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "node_modules", "pkg", "index.js"), "ignored\n")) + yield* Effect.promise(() => fs.writeFile(path.join(tmp.path, "src", "index.js"), "included\n")) + + const files = yield* (yield* Ripgrep.Service).find({ cwd: tmp.path, pattern: "*", limit: 10 }) + expect(files.map((item) => item.path)).toContain(RelativePath.make("src/index.js")) + expect(files.map((item) => item.path)).not.toContain(RelativePath.make("node_modules/pkg/index.js")) + }), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ), + ) + + it.live("never includes git metadata", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), (tmp) => @@ -23,7 +43,7 @@ describe("Ripgrep", () => { const files = yield* ripgrep.find({ cwd: tmp.path, pattern: "**/*", limit: 10 }) expect(files.map((item) => item.path)).toContain(RelativePath.make(".opencode/config")) - expect(files.map((item) => item.path)).toContain(RelativePath.make(".git/config")) + expect(files.map((item) => item.path)).not.toContain(RelativePath.make(".git/config")) const observed: string[] = [] const limited = yield* ripgrep.find({ @@ -36,7 +56,7 @@ describe("Ripgrep", () => { const matches = yield* ripgrep.grep({ cwd: tmp.path, pattern: "needle", include: "config", limit: 10 }) expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".opencode/config")) - expect(matches.map((item) => item.entry.path)).toContain(RelativePath.make(".git/config")) + expect(matches.map((item) => item.entry.path)).not.toContain(RelativePath.make(".git/config")) }), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ), diff --git a/packages/core/test/session-runner-recorded.test.ts b/packages/core/test/session-runner-recorded.test.ts index fc136d9607..e8da56a3a5 100644 --- a/packages/core/test/session-runner-recorded.test.ts +++ b/packages/core/test/session-runner-recorded.test.ts @@ -25,6 +25,7 @@ import { Location } from "@opencode-ai/core/location" import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" import { SystemContext } from "@opencode-ai/core/system-context" import { SkillGuidance } from "@opencode-ai/core/skill/guidance" +import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { describe, expect } from "bun:test" import { eq } from "drizzle-orm" import { Effect, Layer } from "effect" @@ -70,6 +71,7 @@ const models = SessionRunnerModel.layerWith(() => Effect.succeed(model)) const systemContext = SystemContextRegistry.layer const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer)) const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) +const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) })) const runner = SessionRunnerLLM.defaultLayer.pipe( Layer.provide(database), @@ -82,6 +84,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe( Layer.provide(location), Layer.provide(agents), Layer.provide(skillGuidance), + Layer.provide(referenceGuidance), Layer.provide(config), ) const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner)) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 73ff5f47ff..af17de1759 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -48,6 +48,7 @@ import { SessionStore } from "@opencode-ai/core/session/store" import { SystemContext } from "@opencode-ai/core/system-context" import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry" import { SkillGuidance } from "@opencode-ai/core/skill/guidance" +import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { ModelV2 } from "@opencode-ai/core/model" import { Location } from "@opencode-ai/core/location" import { ProviderV2 } from "@opencode-ai/core/provider" @@ -215,6 +216,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, { : SystemContext.empty, ), }) +const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) }) const config = Layer.succeed( Config.Service, Config.Service.of({ @@ -243,6 +245,7 @@ const runner = SessionRunnerLLM.layer.pipe( Layer.provide(location), Layer.provide(agents), Layer.provide(skillGuidance), + Layer.provide(referenceGuidance), Layer.provide(config), ) const coordinator = SessionRunCoordinator.layer.pipe(Layer.provide(runner)) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 86dc417ca5..1424400871 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -24,9 +24,13 @@ import { Effect, Context, Layer, Schema } from "effect" import { InstanceState } from "@/effect/instance-state" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" -import { type DeepMutable } from "@opencode-ai/core/schema" +import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { Reference } from "@opencode-ai/core/reference" +import { Location } from "@opencode-ai/core/location" export const Info = Schema.Struct({ name: Schema.String, @@ -89,15 +93,21 @@ export const layer = Layer.effect( const plugin = yield* Plugin.Service const skill = yield* Skill.Service const provider = yield* Provider.Service + const locations = yield* LocationServiceMap const state = yield* InstanceState.make( Effect.fn("Agent.state")(function* (ctx) { const cfg = yield* config.get() const skillDirs = yield* skill.dirs() + const referenceDirs = yield* Effect.gen(function* () { + yield* (yield* PluginBoot.Service).wait() + return (yield* (yield* Reference.Service).list()).map((reference) => reference.path) + }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })))) const whitelistedDirs = [ Truncate.GLOB, path.join(Global.Path.tmp, "*"), ...skillDirs.map((dir) => path.join(dir, "*")), + ...referenceDirs.map((dir) => path.join(dir, "*")), ] const readonlyExternalDirectory = { "*": "ask", @@ -429,8 +439,18 @@ export const defaultLayer = layer.pipe( Layer.provide(Auth.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(Skill.defaultLayer), + Layer.provide(LocationServiceMap.layer), ) -export const node = LayerNode.make(layer, [Config.node, Auth.node, Plugin.node, Skill.node, Provider.node]) +const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, []) + +export const node = LayerNode.make(layer, [ + Config.node, + Auth.node, + Plugin.node, + Skill.node, + Provider.node, + locationServiceMapNode, +]) export * as Agent from "./agent" diff --git a/packages/opencode/src/cli/cmd/debug/file.ts b/packages/opencode/src/cli/cmd/debug/file.ts index 5ad31985df..21447ba468 100644 --- a/packages/opencode/src/cli/cmd/debug/file.ts +++ b/packages/opencode/src/cli/cmd/debug/file.ts @@ -2,13 +2,14 @@ import { EOL } from "os" import { Effect } from "effect" import { FileSystem } from "@opencode-ai/core/filesystem" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { Location } from "@opencode-ai/core/location" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { effectCmd } from "../../effect-cmd" import { cmd } from "../cmd" const filesystem = (effect: Effect.Effect) => effect.pipe( - Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })), + Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(process.cwd()) }))), Effect.provide(LocationServiceMap.layer), ) diff --git a/packages/opencode/src/cli/cmd/debug/v2.ts b/packages/opencode/src/cli/cmd/debug/v2.ts index aab7018982..74288529d4 100644 --- a/packages/opencode/src/cli/cmd/debug/v2.ts +++ b/packages/opencode/src/cli/cmd/debug/v2.ts @@ -2,6 +2,7 @@ import { EOL } from "os" import { Effect, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { Location } from "@opencode-ai/core/location" import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { AbsolutePath } from "@opencode-ai/core/schema" import { effectCmd } from "../../effect-cmd" @@ -37,9 +38,11 @@ export const V2Command = effectCmd({ }).pipe( Effect.withSpan("Cli.debug.v2"), Effect.provide( - LocationServiceMap.get({ - directory: AbsolutePath.make(process.cwd()), - }), + LocationServiceMap.get( + Location.Ref.make({ + directory: AbsolutePath.make(process.cwd()), + }), + ), ), Effect.provide(LocationServiceMap.layer), ), diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts index fae43d10c5..556edcd5e3 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts @@ -18,7 +18,9 @@ export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handl const filesystem = Effect.fnUntraced(function* (effect: Effect.Effect) { return yield* effect.pipe( - Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + Effect.provide( + locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + ), ) }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts index d8f9e55a06..0058c66b52 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/pty.ts @@ -7,6 +7,7 @@ import { handlePtyInput } from "@opencode-ai/core/pty/input" import { PtyID } from "@opencode-ai/core/pty/schema" import { PtyTicket } from "@opencode-ai/core/pty/ticket" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" import { Shell } from "@/shell/shell" import { EffectBridge } from "@/effect/bridge" @@ -41,13 +42,15 @@ export const ptyHandlers = HttpApiBuilder.group(InstanceHttpApi, "pty", (handler const cors = yield* CorsConfig const locations = yield* LocationServiceMap const unregister = registerDisposer((directory) => - Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })), + Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))), ) yield* Effect.addFinalizer(() => Effect.sync(unregister)) const pty = Effect.fnUntraced(function* (effect: Effect.Effect) { return yield* effect.pipe( - Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + Effect.provide( + locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + ), ) }) @@ -158,13 +161,15 @@ export const ptyConnectHandlers = HttpApiBuilder.group(PtyConnectApi, "pty-conne const cors = yield* CorsConfig const locations = yield* LocationServiceMap const unregister = registerDisposer((directory) => - Effect.runPromise(locations.invalidate({ directory: AbsolutePath.make(directory) })), + Effect.runPromise(locations.invalidate(Location.Ref.make({ directory: AbsolutePath.make(directory) }))), ) yield* Effect.addFinalizer(() => Effect.sync(unregister)) const pty = Effect.fnUntraced(function* (effect: Effect.Effect) { return yield* effect.pipe( - Effect.provide(locations.get({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + Effect.provide( + locations.get(Location.Ref.make({ directory: AbsolutePath.make((yield* InstanceState.context).directory) })), + ), ) }) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index cee669d145..74401779d3 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -16,6 +16,11 @@ import type { Provider } from "@/provider/provider" import type { Agent } from "@/agent/agent" import { Permission } from "@/permission" import { Skill } from "@/skill" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { Location } from "@opencode-ai/core/location" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { Reference } from "@opencode-ai/core/reference" export function provider(model: Provider.Model) { if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) @@ -44,10 +49,15 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const skill = yield* Skill.Service + const locations = yield* LocationServiceMap return Service.of({ environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) { const ctx = yield* InstanceState.context + const references = yield* Effect.gen(function* () { + yield* (yield* PluginBoot.Service).wait() + return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined) + }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })))) return [ [ `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`, @@ -60,7 +70,25 @@ export const layer = Layer.effect( ` Today's date: ${new Date().toDateString()}`, ``, ].join("\n"), - ] + references.length === 0 + ? undefined + : [ + "Project references provide additional directories that can be accessed when relevant.", + "", + ...references + .toSorted((a, b) => a.name.localeCompare(b.name)) + .flatMap((reference) => [ + " ", + ` ${reference.name}`, + ` ${reference.path}`, + ...(reference.description === undefined + ? [] + : [` ${reference.description}`]), + " ", + ]), + "", + ].join("\n"), + ].filter((part): part is string => part !== undefined) }), skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) { @@ -80,8 +108,10 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer)) +export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer), Layer.provide(LocationServiceMap.layer)) -export const node = LayerNode.make(layer, [Skill.node]) +const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, []) + +export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode]) export * as SystemPrompt from "./system" diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 3683229fc5..67071bce73 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -14,6 +14,7 @@ import { Plugin } from "../../src/plugin" import { Provider } from "../../src/provider/provider" import { Skill } from "../../src/skill" import { Truncate } from "../../src/tool/truncate" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" const agentLayer = (flags: Partial = {}) => Agent.layer.pipe( @@ -22,6 +23,7 @@ const agentLayer = (flags: Partial = {}) => Layer.provide(Auth.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(Skill.defaultLayer), + Layer.provide(LocationServiceMap.layer), Layer.provide(RuntimeFlags.layer(flags)), ) @@ -119,7 +121,7 @@ it.instance( }), { config: { - reference: { + references: { effect: "github.com/effect/effect-smol", effectFull: { repository: "Effect-TS/effect", @@ -601,6 +603,25 @@ description: Permission skill. { git: true }, ) +it.instance( + "project reference directories are allowed for external_directory", + () => + Effect.gen(function* () { + const test = yield* TestInstance + const build = yield* load((svc) => svc.get("build")) + const target = path.resolve(test.directory, "../docs/reference/notes.md") + expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow") + }), + { + git: true, + config: { + references: { + docs: "../docs", + }, + }, + }, +) + it.instance("defaultAgent returns build when no default_agent config", () => Effect.gen(function* () { const agent = yield* load((svc) => svc.defaultAgent()) diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index 2421275716..4c894b0a01 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -1,5 +1,6 @@ import { expect } from "bun:test" import { FSUtil } from "@opencode-ai/core/fs-util" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import path from "path" @@ -43,6 +44,7 @@ const agentLayer = Agent.layer.pipe( Layer.provide(SkillTest.empty), Layer.provide(provider.layer), Layer.provide(pluginLayer), + Layer.provide(LocationServiceMap.layer), Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), ) diff --git a/packages/opencode/test/server/httpapi-reference.test.ts b/packages/opencode/test/server/httpapi-reference.test.ts index 0c939bb70c..dcb260588b 100644 --- a/packages/opencode/test/server/httpapi-reference.test.ts +++ b/packages/opencode/test/server/httpapi-reference.test.ts @@ -16,7 +16,7 @@ describe("reference HttpApi", () => { config: { formatter: false, lsp: false, - reference: { + references: { docs: "./docs", effect: { repository: "Effect-TS/effect", branch: "main" }, bad: "not-a-repo", @@ -35,18 +35,26 @@ describe("reference HttpApi", () => { { name: "docs", path: path.join(tmp.path, "docs"), + description: null, + hidden: null, source: { type: "local", path: path.join(tmp.path, "docs"), + description: null, + hidden: null, }, }, { name: "effect", path: path.join(Global.Path.repos, "github.com", "Effect-TS", "effect"), + description: null, + hidden: null, source: { type: "git", repository: "Effect-TS/effect", branch: "main", + description: null, + hidden: null, }, }, ]) diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 28b1bcac82..69cec7bdcc 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -5,6 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error" import { Skill } from "../../src/skill" import { Permission } from "../../src/permission" import { SystemPrompt } from "../../src/session/system" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { testEffect } from "../lib/effect" const skills: Skill.Info[] = [ @@ -42,6 +43,7 @@ const build: Agent.Info = { const it = testEffect( SystemPrompt.layer.pipe( + Layer.provide(LocationServiceMap.layer), Layer.provide( Layer.succeed( Skill.Service, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 2b4677e40a..9e9c74c6ec 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1685,26 +1685,6 @@ export type ServerConfig = { cors?: Array } -export type ReferenceConfigEntry = - | string - | { - /** - * Git repository URL, host/path reference, or GitHub owner/repo shorthand - */ - repository: string - branch?: string - } - | { - /** - * Absolute path, ~/ path, or workspace-relative path to a local reference directory - */ - path: string - } - -export type ReferenceConfig = { - [key: string]: ReferenceConfigEntry -} - export type PermissionActionConfig = "ask" | "allow" | "deny" export type PermissionObjectConfig = { @@ -1948,7 +1928,9 @@ export type Config = { paths?: Array urls?: Array } - reference?: ReferenceConfig + references?: { + [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal + } watcher?: { ignore?: Array } @@ -3723,6 +3705,19 @@ export type SyncEventSessionNextCompactionEnded = { } } +export type ConfigV2ReferenceGit = { + repository: string + branch?: string + description?: string + hidden?: boolean +} + +export type ConfigV2ReferenceLocal = { + path: string + description?: string + hidden?: boolean +} + export type PolicyEffect = "allow" | "deny" export type ConfigV2ExperimentalPolicy = { @@ -4174,17 +4169,23 @@ export type QuestionV2Reply = { export type ReferenceLocalSource = { type: "local" path: string + description?: string + hidden?: boolean } export type ReferenceGitSource = { type: "git" repository: string branch?: string + description?: string + hidden?: boolean } export type ReferenceInfo = { name: string path: string + description?: string + hidden?: boolean source: ReferenceLocalSource | ReferenceGitSource } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 162e7d495f..3bb92d700f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -17628,44 +17628,6 @@ "additionalProperties": false, "description": "Server configuration for opencode serve and web commands" }, - "ReferenceConfigEntry": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "object", - "properties": { - "repository": { - "type": "string", - "description": "Git repository URL, host/path reference, or GitHub owner/repo shorthand" - }, - "branch": { - "type": "string" - } - }, - "required": ["repository"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Absolute path, ~/ path, or workspace-relative path to a local reference directory" - } - }, - "required": ["path"], - "additionalProperties": false - } - ] - }, - "ReferenceConfig": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/ReferenceConfigEntry" - } - }, "PermissionActionConfig": { "type": "string", "enum": ["ask", "allow", "deny"] @@ -18256,8 +18218,21 @@ }, "additionalProperties": false }, - "reference": { - "$ref": "#/components/schemas/ReferenceConfig" + "references": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceGit" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceLocal" + } + ] + } }, "watcher": { "type": "object", @@ -23918,6 +23893,41 @@ "required": ["type", "id", "syncEvent"], "additionalProperties": false }, + "ConfigV2ReferenceGit": { + "type": "object", + "properties": { + "repository": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["repository"], + "additionalProperties": false + }, + "ConfigV2ReferenceLocal": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + } + }, + "required": ["path"], + "additionalProperties": false + }, "PolicyEffect": { "type": "string", "enum": ["allow", "deny"] @@ -25190,6 +25200,12 @@ }, "path": { "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" } }, "required": ["type", "path"], @@ -25207,6 +25223,12 @@ }, "branch": { "type": "string" + }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" } }, "required": ["type", "repository"], @@ -25221,6 +25243,12 @@ "path": { "type": "string" }, + "description": { + "type": "string" + }, + "hidden": { + "type": "boolean" + }, "source": { "anyOf": [ { diff --git a/packages/server/src/groups/location.ts b/packages/server/src/groups/location.ts index 6a5815ca25..4ee082c717 100644 --- a/packages/server/src/groups/location.ts +++ b/packages/server/src/groups/location.ts @@ -57,12 +57,12 @@ export class LocationMiddleware extends HttpApiMiddleware.Service< function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref { const query = new URL(request.url, "http://localhost").searchParams const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"] - return { + return Location.Ref.make({ directory: AbsolutePath.make( query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(), ), workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined, - } + }) } export const layer = Layer.effect( diff --git a/packages/server/src/middleware/session-location.ts b/packages/server/src/middleware/session-location.ts index e98c0db9da..7306cf76b8 100644 --- a/packages/server/src/middleware/session-location.ts +++ b/packages/server/src/middleware/session-location.ts @@ -1,5 +1,6 @@ import { Database } from "@opencode-ai/core/database/database" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { Location } from "@opencode-ai/core/location" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" import { SessionTable } from "@opencode-ai/core/session/sql" @@ -54,10 +55,12 @@ export const sessionLocationLayer = Layer.effect( return yield* effect.pipe( Effect.provide( - locations.get({ - directory: AbsolutePath.make(row.directory), - workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, - }), + locations.get( + Location.Ref.make({ + directory: AbsolutePath.make(row.directory), + workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined, + }), + ), ), ) }), diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index 1c177f8866..b543d7c38e 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -278,7 +278,7 @@ export function Autocomplete(props: { const { baseQuery } = extractLineRange(search()) const slash = baseQuery.indexOf("/") const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash) - return syncV2.data.reference.find((item) => item.name === alias) + return syncV2.data.reference.find((item) => !item.hidden && item.name === alias) }) function normalizeMentionPath(filePath: string) { @@ -411,25 +411,27 @@ export function Autocomplete(props: { }) const referenceAliases = createMemo(() => - syncV2.data.reference.map( - (reference): AutocompleteOption => ({ - display: "@" + reference.name, - description: " dir", - onSelect: () => { - insertPart(reference.name, { - type: "file", - mime: "application/x-directory", - filename: reference.name, - url: pathToFileURL(reference.path).href, - source: { + syncV2.data.reference + .filter((reference) => !reference.hidden) + .map( + (reference): AutocompleteOption => ({ + display: "@" + reference.name, + description: ` ${reference.source.type === "git" ? reference.source.repository : reference.source.path}`, + onSelect: () => { + insertPart(reference.name, { type: "file", - text: { start: 0, end: 0, value: "" }, - path: reference.name, - }, - }) - }, - }), - ), + mime: "application/x-directory", + filename: reference.name, + url: pathToFileURL(reference.path).href, + source: { + type: "file", + text: { start: 0, end: 0, value: "" }, + path: reference.name, + }, + }) + }, + }), + ), ) const commands = createMemo((): AutocompleteOption[] => { diff --git a/packages/web/astro.config.mjs b/packages/web/astro.config.mjs index 78b3587817..48d6f3c557 100644 --- a/packages/web/astro.config.mjs +++ b/packages/web/astro.config.mjs @@ -264,6 +264,7 @@ export default defineConfig({ "mcp-servers", "acp", "skills", + "references", "custom-tools", ], }, diff --git a/packages/web/src/content/docs/references.mdx b/packages/web/src/content/docs/references.mdx new file mode 100644 index 0000000000..2df002c331 --- /dev/null +++ b/packages/web/src/content/docs/references.mdx @@ -0,0 +1,157 @@ +--- +title: References +description: Add local directories and Git repositories as project references. +--- + +References give OpenCode access to directories outside the current project. Use them to make documentation, shared libraries, examples, or another repository available while you work. + +References are configured by alias in `opencode.json` or `opencode.jsonc`. + +```jsonc title="opencode.jsonc" +{ + "$schema": "https://opencode.ai/config.json", + "references": { + "docs": { + "path": "../product-docs", + "description": "Use for product behavior and documentation conventions", + }, + "sdk": { + "repository": "anomalyco/opencode-sdk-js", + "branch": "main", + "description": "Use for JavaScript SDK implementation details", + }, + }, +} +``` + +--- + +## Local directories + +Use `path` to reference a local directory. + +```jsonc title="opencode.jsonc" +{ + "references": { + "docs": { + "path": "../docs", + }, + }, +} +``` + +Paths can be: + +- Relative to the config file that defines the reference +- Absolute, such as `/home/user/docs` +- Relative to your home directory, such as `~/docs` + +You can also use a string shorthand: + +```jsonc title="opencode.jsonc" +{ + "references": { + "docs": "../docs", + }, +} +``` + +--- + +## Git repositories + +Use `repository` to reference a Git repository. OpenCode materializes the repository in its local repository cache and makes the checked-out source available as a reference directory. + +```jsonc title="opencode.jsonc" +{ + "references": { + "effect": { + "repository": "Effect-TS/effect", + "branch": "main", + }, + }, +} +``` + +`repository` accepts Git URLs, host/path references, and GitHub `owner/repo` shorthand. The optional `branch` field selects a branch or ref. Without `branch`, OpenCode uses the repository's default branch. + +You can use string shorthand when you do not need a branch, description, or other options: + +```jsonc title="opencode.jsonc" +{ + "references": { + "effect": "Effect-TS/effect", + }, +} +``` + +:::note +Git references are refreshed asynchronously. A newly configured repository may take a moment to finish cloning or updating. +::: + +--- + +## Describe usage + +Add `description` to explain when an agent should use a reference. + +```jsonc title="opencode.jsonc" +{ + "references": { + "design-system": { + "path": "../design-system", + "description": "Use when implementing UI components or design tokens", + }, + }, +} +``` + +OpenCode includes references with descriptions in agent context. Descriptions should be short and specific enough to distinguish references with similar content. References without descriptions remain available through autocomplete and direct use, but are not advertised to agents. + +--- + +## Hide autocomplete entries + +Set `hidden` to `true` to omit a reference from `@` autocomplete in the TUI. + +```jsonc title="opencode.jsonc" +{ + "references": { + "internal": { + "path": "../internal", + "description": "Use for internal implementation details", + "hidden": true, + }, + }, +} +``` + +`hidden` only affects autocomplete. A hidden reference with a description remains included in agent context. + +--- + +## Use references + +Configured references appear in TUI `@` autocomplete. Type `@alias` to attach the reference root, or `@alias/` to search for files inside it. + +```text +Compare this implementation with @sdk/src/client.ts +``` + +Agents also receive the resolved paths and descriptions of configured references that have descriptions in their system context, so they can inspect a reference when it is relevant without you attaching it manually. + +OpenCode automatically allows reference directories through its external-directory permission boundary. Normal tool permissions still apply; for example, an agent that cannot edit files does not gain edit access because a directory is configured as a reference. + +--- + +## Configure fields + +| Field | Local | Git | Description | +| ------------- | ----- | --- | ------------------------------------------------ | +| `path` | Yes | No | Local reference directory | +| `repository` | No | Yes | Git URL, host/path, or GitHub `owner/repo` value | +| `branch` | No | Yes | Optional Git branch or ref | +| `description` | Yes | Yes | Guidance describing when to use the reference | +| `hidden` | Yes | Yes | Hide the reference from TUI `@` autocomplete | + +Reference aliases cannot be empty or contain `/`, whitespace, backticks, or commas. diff --git a/packages/web/src/content/docs/tui.mdx b/packages/web/src/content/docs/tui.mdx index cd668a3bef..a03797b302 100644 --- a/packages/web/src/content/docs/tui.mdx +++ b/packages/web/src/content/docs/tui.mdx @@ -41,7 +41,7 @@ How is auth handled in @packages/functions/src/api/index.ts? The content of the file is added to the conversation automatically. -Configured references also appear in `@` autocomplete. Type `@alias` to add the reference root as context, or type `@alias/` to autocomplete files inside that reference. +Configured [references](/docs/references) also appear in `@` autocomplete. Type `@alias` to add the reference root as context, or type `@alias/` to autocomplete files inside that reference. ```text "@docs/README.md" Compare our setup with @docs/README.md From 91073360c6904448bce5b6da9e933604944ed015 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:24:41 -0500 Subject: [PATCH 003/157] fix(mcp): make client creation failure-safe (#31595) --- packages/opencode/src/mcp/index.ts | 62 ++++++++++++-------- packages/opencode/test/mcp/lifecycle.test.ts | 28 +++++++++ 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 63a91ffe4d..5ce4719ee5 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -27,7 +27,7 @@ import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2 } from "@opencode-ai/core/event" import { TuiEvent } from "@/server/tui-event" import open from "open" -import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" +import { Cause, Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" @@ -446,31 +446,45 @@ export const layer = Layer.effect( ) }) - const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCPV1.Info) { - if (mcp.enabled === false) { - return DISABLED_RESULT - } + const create = Effect.fn("MCP.create")( + function* (key: string, mcp: ConfigMCPV1.Info) { + if (mcp.enabled === false) { + return DISABLED_RESULT + } - const { client: mcpClient, status } = - mcp.type === "remote" - ? yield* connectRemote(key, mcp as ConfigMCPV1.Info & { type: "remote" }) - : yield* connectLocal(key, mcp as ConfigMCPV1.Info & { type: "local" }) + const { client: mcpClient, status } = + mcp.type === "remote" + ? yield* connectRemote(key, mcp as ConfigMCPV1.Info & { type: "remote" }) + : yield* connectLocal(key, mcp as ConfigMCPV1.Info & { type: "local" }) - if (!mcpClient) { - if (status.status !== "connected" && status.status !== "disabled") { - yield* Effect.logWarning("server unavailable", { key, type: mcp.type, status: status.status }) + if (!mcpClient) { + if (status.status !== "connected" && status.status !== "disabled") { + yield* Effect.logWarning("server unavailable", { key, type: mcp.type, status: status.status }) + } + return { status } satisfies CreateResult } - return { status } satisfies CreateResult - } - const listed = mcpClient.getServerCapabilities()?.tools ? yield* defs(mcpClient, mcp.timeout) : [] - if (!listed) { - yield* Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore) - return { status: { status: "failed", error: "Failed to get tools" } } satisfies CreateResult - } - - return { mcpClient, status, defs: listed } satisfies CreateResult - }) + return yield* Effect.gen(function* () { + const listed = mcpClient.getServerCapabilities()?.tools ? yield* defs(mcpClient, mcp.timeout) : [] + if (!listed) { + return yield* Effect.fail(new Error("Failed to get tools")) + } + return { mcpClient, status, defs: listed } satisfies CreateResult + }).pipe( + Effect.catchCause((cause) => + Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore, Effect.andThen(Effect.failCause(cause))), + ), + ) + }, + Effect.map((result): CreateResult => result), + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt + const error = Cause.squash(cause) + return Effect.succeed({ + status: { status: "failed", error: error instanceof Error ? error.message : String(error) }, + }) + }), + ) const cfgSvc = yield* Config.Service const descendants = Effect.fnUntraced( @@ -537,9 +551,7 @@ export const layer = Layer.effect( return } - const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void)) - if (!result) return - + const result = yield* create(key, mcp) s.status[key] = result.status if (result.mcpClient) { s.clients[key] = result.mcpClient diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index d4ae15c26b..da525d166e 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -8,6 +8,7 @@ import { testEffect } from "../lib/effect" // Per-client state for controlling mock behavior interface MockClientState { capabilities: { tools?: object; prompts?: object; resources?: object } + capabilitiesShouldThrow: boolean tools: Array<{ name: string; description?: string; inputSchema: object; outputSchema?: object }> listToolsCalls: number listPromptsCalls: number @@ -51,6 +52,7 @@ function getOrCreateClientState(name?: string): MockClientState { if (!state) { state = { capabilities: { tools: {}, prompts: {}, resources: {} }, + capabilitiesShouldThrow: false, tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }], listToolsCalls: 0, listPromptsCalls: 0, @@ -155,6 +157,7 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ } getServerCapabilities() { + if (this._state?.capabilitiesShouldThrow) throw new Error("capability discovery failed") return this._state?.capabilities } @@ -566,6 +569,31 @@ it.instance( }, ) +it.instance( + "returns failed and closes the client when SDK initialization throws", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "defective-server" + const serverState = getOrCreateClientState("defective-server") + serverState.capabilitiesShouldThrow = true + + const result = yield* mcp.add("defective-server", { + type: "local", + command: ["echo", "test"], + }) + + expect(statusName(result.status, "defective-server")).toBe("failed") + expect((yield* mcp.status())["defective-server"]).toEqual({ + status: "failed", + error: "capability discovery failed", + }) + expect(serverState.closed).toBe(true) + }), + ), + { config: { mcp: {} } }, +) + it.instance( "falls back when MCP output schema refs fail SDK tool discovery", () => From 954d618790816232505fca58848398dac969b484 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:46:01 -0500 Subject: [PATCH 004/157] fix(opencode): support Anthropic fallback responses (#31611) --- bun.lock | 16 +++++++--------- bunfig.toml | 2 +- packages/console/function/package.json | 2 +- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index 0aa3c67193..c3f1aed44e 100644 --- a/bun.lock +++ b/bun.lock @@ -175,7 +175,7 @@ "name": "@opencode-ai/console-function", "version": "1.17.0", "dependencies": { - "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", "@ai-sdk/openai-compatible": "2.0.37", "@openauthjs/openauth": "0.0.0-20250322224806", @@ -246,7 +246,7 @@ "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.71", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", @@ -504,7 +504,7 @@ "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.71", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", @@ -1014,7 +1014,7 @@ "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.81", "@ai-sdk/openai": "3.0.67", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-PsSh7a6qW+3kQXPs1kD4wDwuZby0t1PIaB6j/1aMKmPFJ5LxcIcULLMF/bjITLt5o/8lc0t6TXIwG0zlhH7uZw=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.82", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-WKKou2wbhGGYV8PSALAPyV2YY4nfCqCPkyBzYtJtDA9yCcIFwsbtkTNgg7bqtLCVzeEsY7wwxRoCWy+EMfrw/A=="], "@ai-sdk/azure": ["@ai-sdk/azure@3.0.49", "", { "dependencies": { "@ai-sdk/openai": "3.0.48", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wskgAL+OmrHG7by/iWIxEBQCEdc1mDudha/UZav46i0auzdFfsDB/k2rXZaC4/3nWSgMZkxr0W3ncyouEGX/eg=="], @@ -5446,7 +5446,9 @@ "@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], + "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -5808,8 +5810,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/core/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], - "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -6154,8 +6154,6 @@ "nypm/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], - "opencode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], - "opencode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], diff --git a/bunfig.toml b/bunfig.toml index 283e9fbfe7..23cfb62bbe 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64"] [test] root = "./do-not-run-tests-from-root" diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 960759e864..fd5d768907 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -17,7 +17,7 @@ "@typescript/native-preview": "catalog:" }, "dependencies": { - "@ai-sdk/anthropic": "3.0.64", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", "@ai-sdk/openai-compatible": "2.0.37", "@opencode-ai/console-core": "workspace:*", diff --git a/packages/core/package.json b/packages/core/package.json index ef527f634f..3ca96188c6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -62,7 +62,7 @@ "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.71", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d9652649bf..78240c6861 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -57,7 +57,7 @@ "@agentclientprotocol/sdk": "0.21.0", "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/amazon-bedrock": "4.0.112", - "@ai-sdk/anthropic": "3.0.71", + "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.49", "@ai-sdk/cerebras": "2.0.41", "@ai-sdk/cohere": "3.0.27", From 174ab583434032688758d5d96035d18d46fe9f55 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:58:40 -0500 Subject: [PATCH 005/157] fix(mcp): apply timeouts to prompts and resources (#31612) --- packages/opencode/src/mcp/index.ts | 25 +++++++++----- packages/opencode/test/mcp/lifecycle.test.ts | 35 ++++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 5ce4719ee5..d40594bace 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -740,7 +740,7 @@ export const layer = Layer.effect( const withClient = Effect.fnUntraced(function* ( clientName: string, - fn: (client: MCPClient) => Promise, + fn: (client: MCPClient, timeout?: number) => Promise, label: string, meta?: Record, ) { @@ -750,8 +750,11 @@ export const layer = Layer.effect( yield* Effect.logWarning(`client not found for ${label}`, { clientName }) return undefined } + const cfg = yield* cfgSvc.get() + const configured = cfg.mcp?.[clientName] + const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined return yield* Effect.tryPromise({ - try: () => fn(client), + try: () => fn(client, s.config[clientName]?.timeout ?? staticTimeout ?? cfg.experimental?.mcp_timeout), catch: (error) => error, }).pipe( Effect.tapError((error) => @@ -770,15 +773,21 @@ export const layer = Layer.effect( name: string, args?: Record, ) { - return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", { - promptName: name, - }) + return yield* withClient( + clientName, + (client, timeout) => client.getPrompt({ name, arguments: args }, { timeout }), + "getPrompt", + { promptName: name }, + ) }) const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) { - return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", { - resourceUri, - }) + return yield* withClient( + clientName, + (client, timeout) => client.readResource({ uri: resourceUri }, { timeout }), + "readResource", + { resourceUri }, + ) }) const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) { diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index da525d166e..004909fb6e 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -13,6 +13,8 @@ interface MockClientState { listToolsCalls: number listPromptsCalls: number listResourcesCalls: number + getPromptTimeout?: number + readResourceTimeout?: number requestCalls: number listToolsShouldFail: boolean listToolsError: string @@ -206,6 +208,16 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ return { resources: this._state?.resources ?? [] } } + async getPrompt(_params: unknown, options?: { timeout?: number }) { + if (this._state) this._state.getPromptTimeout = options?.timeout + return { messages: [] } + } + + async readResource(params: { uri: string }, options?: { timeout?: number }) { + if (this._state) this._state.readResourceTimeout = options?.timeout + return { contents: [{ uri: params.uri, text: "test" }] } + } + async close() { if (this._state) this._state.closed = true } @@ -758,6 +770,29 @@ it.instance( }, ) +it.instance( + "uses per-server timeouts for prompt and resource requests", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "timeout-server" + const serverState = getOrCreateClientState("timeout-server") + + yield* mcp.add("timeout-server", { + type: "local", + command: ["echo", "test"], + timeout: 2500, + }) + yield* mcp.getPrompt("timeout-server", "test") + yield* mcp.readResource("timeout-server", "test://resource") + + expect(serverState.getPromptTimeout).toBe(2500) + expect(serverState.readResourceTimeout).toBe(2500) + }), + ), + { config: { mcp: {}, experimental: { mcp_timeout: 5000 } } }, +) + it.instance( "resource-only servers connect without listing tools", () => From 5e342f711467068548d408be13a0e134182689a3 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 05:00:09 +0000 Subject: [PATCH 006/157] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index eada8b6a90..f8b1c26627 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-jm6Xfv4q9qD+00QQYBlgKBJhn4G2mlJdCkbKXxYdfOk=", - "aarch64-linux": "sha256-V0+Rsj4XBqZkwj5Pioqt330t6MXDhB30NFcgxnNKuvU=", - "aarch64-darwin": "sha256-1RGsodkMU7jAqUWoUZIxGIYr5Q1qvvru/F51Ay00ypw=", - "x86_64-darwin": "sha256-HwhgDwb6P2C7WvekGyyejSbxT2P8C7v7eMvcxF+Q6Ao=" + "x86_64-linux": "sha256-q/4M9kQ0VlNepcHuLc9zxNPWkxFfUlw/7YlqEFRpx3g=", + "aarch64-linux": "sha256-HGqFQpr81+Bh8KHkDcDSphsn+NzQ7sl2OQbyYjtzaQM=", + "aarch64-darwin": "sha256-dqHXeuav5D0x7KuDVfuCMOlriSqPL19aZeScA7gWZRc=", + "x86_64-darwin": "sha256-yK3hzJF1FuC3KfmMYgG2zgyn2T2pRAlfG6M0P2ghPsA=" } } From e0449c0b9647c60810ccfa72a6367051a37ac359 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:49:11 +1000 Subject: [PATCH 007/157] fix(desktop): restore macOS auto-updates (#31621) --- bun.lock | 136 ++-------------------------------- bunfig.toml | 2 +- packages/desktop/package.json | 2 +- 3 files changed, 8 insertions(+), 132 deletions(-) diff --git a/bun.lock b/bun.lock index c3f1aed44e..b2a1b15ff4 100644 --- a/bun.lock +++ b/bun.lock @@ -357,7 +357,7 @@ "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", "electron": "42.3.3", - "electron-builder": "26.15.0", + "electron-builder": "26.15.2", "electron-vite": "^5", "solid-js": "catalog:", "sury": "11.0.0-alpha.4", @@ -1340,7 +1340,7 @@ "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], - "@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + "@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="], "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], @@ -2864,7 +2864,7 @@ "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], - "app-builder-lib": ["app-builder-lib@26.15.0", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^2.2.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.15.0", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.15.0", "electron-builder-squirrel-windows": "26.15.0" } }, "sha512-j2+P6Lh+l/VuWfXZWSs7u+OAPqYJQGnZZO30M833XQQaRuyohm4RZk7Gw4nQXfeyQH9GqXaTwR16Y0LaVTlS+g=="], + "app-builder-lib": ["app-builder-lib@26.15.2", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^2.2.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.15.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.15.2", "electron-builder-squirrel-windows": "26.15.2" } }, "sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ=="], "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], @@ -2986,8 +2986,6 @@ "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="], @@ -3106,8 +3104,6 @@ "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="], "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], @@ -3252,8 +3248,6 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -3306,7 +3300,7 @@ "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], - "dmg-builder": ["dmg-builder@26.15.0", "", { "dependencies": { "app-builder-lib": "26.15.0", "builder-util": "26.15.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-oS8MWttbpIUF/2v8LOEY+f4ayL84ipMOarZvdRMl/pxlhLxAYjYMklTXHEXIl37Ig+qJv/bVF7HgyIoOoZyMWA=="], + "dmg-builder": ["dmg-builder@26.15.2", "", { "dependencies": { "app-builder-lib": "26.15.2", "builder-util": "26.15.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA=="], "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], @@ -3356,7 +3350,7 @@ "electron": ["electron@42.3.3", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-0MwYp9wTb7TrtTalOYqeW+suqd9T/Znstr/nDLKqFGIjHdBZX339guo3mQqTPURRZ/UQmYM4uMpzKpI5wLptfQ=="], - "electron-builder": ["electron-builder@26.15.0", "", { "dependencies": { "app-builder-lib": "26.15.0", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.0", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-zd4cfvjHmtyGqMaDudg5rAjNUkwIJDz8ICaCsz77hFKcjMQHcZNNNCs/C4phwN9+gEVwmhvpKMzNFum6fs/n6A=="], + "electron-builder": ["electron-builder@26.15.2", "", { "dependencies": { "app-builder-lib": "26.15.2", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.2", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A=="], "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], @@ -3368,7 +3362,7 @@ "electron-log": ["electron-log@5.4.4", "", {}, "sha512-istWgaXjBfURBSS8LWVW9C3jsc6+ac+tY1lXrQEOTp0lVj+a4OlO1Tmqb36GgnEUDv92DGC9VI1HNXwJinWpgA=="], - "electron-publish": ["electron-publish@26.15.0", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-pt6K3ol/a+o3HbqmYkL2NYlVH5pd34tL4FPRcgX8E88xQAqQyIsseXe4vWy7Pq2BaYy+iFGJrtInZe11FFAQwQ=="], + "electron-publish": ["electron-publish@26.15.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.0", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg=="], "electron-store": ["electron-store@11.0.2", "", { "dependencies": { "conf": "^15.0.2", "type-fest": "^5.0.1" } }, "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ=="], @@ -3788,8 +3782,6 @@ "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], @@ -3864,8 +3856,6 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], @@ -3894,8 +3884,6 @@ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -4058,8 +4046,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - "loglevelnext": ["loglevelnext@6.0.0", "", {}, "sha512-FDl1AI2sJGjHHG3XKJd6sG3/6ncgiGCQ0YkW46nxe7SfqQq6hujd9CvFXIXtkGBUN83KPZ2KSOJK8q5P0bSSRQ=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -4410,8 +4396,6 @@ "opentui-spinner": ["opentui-spinner@0.0.6", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="], - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "oxc-minify": ["oxc-minify@0.96.0", "", { "optionalDependencies": { "@oxc-minify/binding-android-arm64": "0.96.0", "@oxc-minify/binding-darwin-arm64": "0.96.0", "@oxc-minify/binding-darwin-x64": "0.96.0", "@oxc-minify/binding-freebsd-x64": "0.96.0", "@oxc-minify/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-minify/binding-linux-arm-musleabihf": "0.96.0", "@oxc-minify/binding-linux-arm64-gnu": "0.96.0", "@oxc-minify/binding-linux-arm64-musl": "0.96.0", "@oxc-minify/binding-linux-riscv64-gnu": "0.96.0", "@oxc-minify/binding-linux-s390x-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-gnu": "0.96.0", "@oxc-minify/binding-linux-x64-musl": "0.96.0", "@oxc-minify/binding-wasm32-wasi": "0.96.0", "@oxc-minify/binding-win32-arm64-msvc": "0.96.0", "@oxc-minify/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA=="], @@ -4754,8 +4738,6 @@ "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], @@ -5186,10 +5168,6 @@ "unifont": ["unifont@0.5.2", "", { "dependencies": { "css-tree": "^3.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.0" } }, "sha512-LzR4WUqzH9ILFvjLAUU7dK3Lnou/qd5kD+IakBtBK4S15/+x2y9VX+DcWQv6s551R6W+vzwgVS6tFg3XggGBgg=="], - "unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], - - "unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -5312,8 +5290,6 @@ "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], @@ -5696,12 +5672,6 @@ "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - "@electron/rebuild/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "@electron/rebuild/node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], - - "@electron/rebuild/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "@electron/universal/fs-extra": ["fs-extra@11.3.5", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg=="], "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -5982,10 +5952,6 @@ "babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -6012,8 +5978,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "dir-compare/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -6116,8 +6080,6 @@ "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "md-to-react-email/marked": ["marked@7.0.4", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ=="], @@ -6166,12 +6128,6 @@ "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "ora/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-retry/retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], @@ -6210,10 +6166,6 @@ "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - "restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "roarr/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], @@ -6444,20 +6396,6 @@ "@electron/notarize/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - "@electron/rebuild/node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "@electron/rebuild/node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], - - "@electron/rebuild/node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], - - "@electron/rebuild/node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], - - "@electron/rebuild/node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - - "@electron/rebuild/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@electron/rebuild/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@electron/universal/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], @@ -6788,8 +6726,6 @@ "electron-builder-squirrel-windows/app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], - "electron-builder-squirrel-windows/app-builder-lib/@electron/rebuild": ["@electron/rebuild@4.0.4", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg=="], - "electron-builder-squirrel-windows/app-builder-lib/builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], "electron-builder-squirrel-windows/app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], @@ -6852,8 +6788,6 @@ "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], @@ -6862,8 +6796,6 @@ "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -7022,26 +6954,6 @@ "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], - - "@electron/rebuild/node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], - - "@electron/rebuild/node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "@electron/rebuild/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@electron/rebuild/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@electron/rebuild/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@electron/rebuild/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -7252,20 +7164,6 @@ "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "@electron/rebuild/node-gyp/make-fetch-happen/@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], - - "@electron/rebuild/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "@sentry/bundler-plugin-core/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -7304,14 +7202,6 @@ "tw-to-css/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "archiver-utils/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -7319,19 +7209,5 @@ "js-beautify/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "js-beautify/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/bunfig.toml b/bunfig.toml index 23cfb62bbe..b914f4b67b 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,7 +2,7 @@ exact = true # Only install newly resolved package versions published at least 3 days ago. minimumReleaseAge = 259200 -minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64"] +minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@ai-sdk/anthropic", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-arm64-musl", "@opentui/core-linux-x64", "@opentui/core-linux-x64-musl", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider", "@ff-labs/fff-node", "@ff-labs/fff-bun", "@ff-labs/fff-bin-darwin-arm64", "@ff-labs/fff-bin-darwin-x64", "@ff-labs/fff-bin-linux-arm64-gnu", "@ff-labs/fff-bin-linux-arm64-musl", "@ff-labs/fff-bin-linux-x64-gnu", "@ff-labs/fff-bin-linux-x64-musl", "@ff-labs/fff-bin-win32-arm64", "@ff-labs/fff-bin-win32-x64", "app-builder-lib", "dmg-builder", "electron-builder", "electron-publish"] [test] root = "./do-not-run-tests-from-root" diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f462070cdb..38977ce918 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -49,7 +49,7 @@ "@typescript/native-preview": "catalog:", "@valibot/to-json-schema": "1.6.0", "electron": "42.3.3", - "electron-builder": "26.15.0", + "electron-builder": "26.15.2", "electron-vite": "^5", "solid-js": "catalog:", "sury": "11.0.0-alpha.4", From 826419127ae0c2b742b9db866c4a9afb27a5ae2c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 06:07:15 +0000 Subject: [PATCH 008/157] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index f8b1c26627..1bbea78c1b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-q/4M9kQ0VlNepcHuLc9zxNPWkxFfUlw/7YlqEFRpx3g=", - "aarch64-linux": "sha256-HGqFQpr81+Bh8KHkDcDSphsn+NzQ7sl2OQbyYjtzaQM=", - "aarch64-darwin": "sha256-dqHXeuav5D0x7KuDVfuCMOlriSqPL19aZeScA7gWZRc=", - "x86_64-darwin": "sha256-yK3hzJF1FuC3KfmMYgG2zgyn2T2pRAlfG6M0P2ghPsA=" + "x86_64-linux": "sha256-nix3Ogrt4nFv+HSZpYZ3VqIQc+g1SdP+DSgu72Yjoqw=", + "aarch64-linux": "sha256-L634j49jyNoKhC1MDyF+Grxy++yhWgcbXsxKDgYhKAk=", + "aarch64-darwin": "sha256-ha692TeekwiV0irhIxdwE8/1x1bLKtzSwcjDvetiPqM=", + "x86_64-darwin": "sha256-JRwKof0cTFwOcOyLb6l+kF/HlCiFmMWzG1y3U1esyyM=" } } From 97e713e8aac75a0254c34d134f0608af5cb4935c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 10 Jun 2026 04:05:03 -0400 Subject: [PATCH 009/157] zen: deepseek v4 pro --- packages/web/src/content/docs/ar/zen.mdx | 4 +++- packages/web/src/content/docs/bs/zen.mdx | 4 +++- packages/web/src/content/docs/da/zen.mdx | 4 +++- packages/web/src/content/docs/de/zen.mdx | 4 +++- packages/web/src/content/docs/es/zen.mdx | 4 +++- packages/web/src/content/docs/fr/zen.mdx | 4 +++- packages/web/src/content/docs/it/zen.mdx | 4 +++- packages/web/src/content/docs/ja/zen.mdx | 4 +++- packages/web/src/content/docs/ko/zen.mdx | 4 +++- packages/web/src/content/docs/nb/zen.mdx | 4 +++- packages/web/src/content/docs/pl/zen.mdx | 4 +++- packages/web/src/content/docs/pt-br/zen.mdx | 4 +++- packages/web/src/content/docs/ru/zen.mdx | 4 +++- packages/web/src/content/docs/th/zen.mdx | 4 +++- packages/web/src/content/docs/tr/zen.mdx | 4 +++- packages/web/src/content/docs/zen.mdx | 4 +++- packages/web/src/content/docs/zh-cn/zen.mdx | 4 +++- packages/web/src/content/docs/zh-tw/zen.mdx | 4 +++- 18 files changed, 54 insertions(+), 18 deletions(-) diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index f8dd274d24..9ab89b935b 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -90,6 +90,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -139,7 +140,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index 706c5993ef..51191e4099 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -95,6 +95,7 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index 6d0bbbbad6..b306deea65 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -95,6 +95,7 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 434ba94ac5..c2ea51e555 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -86,6 +86,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index 5eec9d2d53..bdec02a2d2 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -95,6 +95,7 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index 8901f8d906..50ce5fa312 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -86,6 +86,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index 46624c618a..fb99529e60 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -95,6 +95,7 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index 37114b5c67..8b6958bf2b 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -86,6 +86,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index fa50766495..16b915f4ec 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -86,6 +86,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 12410210c7..11c7400e99 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -95,6 +95,7 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index 698c2f7133..4516594cdc 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -95,6 +95,7 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index c33b7d83dc..c0b6faba09 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -86,6 +86,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index dad83c2dd0..a47b579a43 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -95,6 +95,7 @@ OpenCode Zen работает как любой другой провайдер | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index 12c2b1f6dc..79cd87bc48 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -88,6 +88,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -137,7 +138,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index 33b9f6d34f..84ba1c89da 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -86,6 +86,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index cff6ab545e..caa609912b 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -95,6 +95,7 @@ You can also access our models through the following API endpoints. | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -146,7 +147,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 14765d7d6a..489ee60a67 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -86,6 +86,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -135,7 +136,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index 93c4465f4f..6a271da570 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -90,6 +90,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -140,7 +141,8 @@ https://opencode.ai/zen/v1/models | Qwen3.7 Plus | $0.40 | $1.60 | $0.04 | $0.50 | | Qwen3.6 Plus | $0.50 | $3.00 | $0.05 | $0.625 | | Qwen3.5 Plus | $0.20 | $1.20 | $0.02 | $0.25 | -| DeepSeek V4 Flash | $0.14 | $0.28 | $0.03 | - | +| DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | +| DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | From 90fb32be30f12ea8c552980ddba8269ae9e45cf9 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:32:35 +1000 Subject: [PATCH 010/157] fix(core): accept deprecated reference config key (#31659) --- .opencode/opencode.jsonc | 4 ++- packages/core/src/v1/config/config.ts | 3 ++ packages/core/src/v1/config/migrate.ts | 3 +- packages/core/test/config/config.test.ts | 38 ++++++++++++++++++++ packages/opencode/test/config/config.test.ts | 20 +++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 3 ++ 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index b0f7d59447..ae68a477dc 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -2,7 +2,9 @@ "$schema": "https://opencode.ai/config.json", "provider": {}, "permission": {}, - "references": { + // TODO: flip back to `references` once a release containing the v1 `reference` migration ships. + // The release pipeline runs the latest published opencode against this file, which only knows `reference`. + "reference": { "effect": { "repository": "github.com/Effect-TS/effect-smol", "description": "Use for Effect v4 and effect-smol implementation details", diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 5c520846da..2e773f71e2 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -45,6 +45,9 @@ export const Info = Schema.Struct({ references: Schema.optional(ConfigReference.Info).annotate({ description: "Named git or local directory references", }), + reference: Schema.optional(ConfigReference.Info).annotate({ + description: "@deprecated Use 'references' field instead. Named git or local directory references", + }), watcher: Schema.optional(Schema.Struct({ ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))) })), snapshot: Schema.optional(Schema.Boolean).annotate({ description: diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index 19bf48aa74..3b4f13868e 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -12,6 +12,7 @@ const keys = new Set([ "logLevel", "server", "command", + "reference", "snapshot", "plugin", "autoshare", @@ -62,7 +63,7 @@ export function migrate(info: typeof ConfigV1.Info.Type) { skills: info.skills && [...(info.skills.paths ?? []), ...(info.skills.urls ?? [])], commands: info.command, instructions: info.instructions, - references: info.references, + references: info.references ?? info.reference, plugins: info.plugin?.map((plugin) => typeof plugin === "string" ? plugin : { package: plugin[0], options: plugin[1] }, ), diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 5f62cbce61..6275d8fed3 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -70,7 +70,9 @@ describe("Config", () => { Effect.sync(() => { expect(ConfigMigrateV1.isV1({ snapshot: false })).toBe(true) expect(ConfigMigrateV1.isV1({ snapshot: false, agents: {} })).toBe(true) + expect(ConfigMigrateV1.isV1({ reference: {} })).toBe(true) expect(ConfigMigrateV1.isV1({ shell: "/bin/zsh", model: "anthropic/claude" })).toBe(false) + expect(ConfigMigrateV1.isV1({ references: {} })).toBe(false) }), ) @@ -431,6 +433,42 @@ describe("Config", () => { ), ) + it.live("migrates the deprecated reference key into references", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + yield* Effect.promise(() => + fs.writeFile( + path.join(tmp.path, "opencode.json"), + JSON.stringify({ + reference: { + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }, + }), + ), + ) + + return yield* Effect.gen(function* () { + const config = yield* Config.Service + const documents = (yield* config.entries()).filter((entry) => entry.type === "document") + + expect(documents).toHaveLength(1) + expect(documents[0]?.info.references).toEqual({ + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }) + }).pipe(Effect.provide(testLayer(tmp.path))) + }), + ), + ), + ) + it.live("migrates v1 configuration when a v1-only key is present", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index c016431bc7..f4a0cd4a28 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -722,6 +722,26 @@ it.instance("migrates mode field to agent field", () => }), ) +it.instance("accepts the deprecated reference field", () => + Effect.gen(function* () { + const test = yield* TestInstance + yield* writeConfigEffect(test.directory, { + $schema: "https://opencode.ai/config.json", + reference: { + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }, + }) + const config = yield* Config.use.get() + expect(config.reference).toEqual({ + local: { path: "../library" }, + sdk: { repository: "github.com/example/sdk", branch: "main" }, + shorthand: "github.com/example/docs", + }) + }), +) + it.instance("loads config from .opencode directory", () => Effect.gen(function* () { const test = yield* TestInstance diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9e9c74c6ec..7f5e765a41 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1931,6 +1931,9 @@ export type Config = { references?: { [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal } + reference?: { + [key: string]: string | ConfigV2ReferenceGit | ConfigV2ReferenceLocal + } watcher?: { ignore?: Array } From 2cf68f32b53ba48f9d56d739e839065c2729f4ed Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 10:34:23 +0000 Subject: [PATCH 011/157] chore: generate --- packages/sdk/openapi.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 3bb92d700f..1dffe1ef5d 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -18234,6 +18234,22 @@ ] } }, + "reference": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceGit" + }, + { + "$ref": "#/components/schemas/ConfigV2ReferenceLocal" + } + ] + } + }, "watcher": { "type": "object", "properties": { From 4c9abff445e827b1fb350429d7e14dafa0cb1996 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 10 Jun 2026 11:37:58 +0000 Subject: [PATCH 012/157] sync release versions for v1.17.1 --- bun.lock | 52 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 28 files changed, 53 insertions(+), 53 deletions(-) diff --git a/bun.lock b/bun.lock index b2a1b15ff4..cbc4e7f7e7 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.0", + "version": "1.17.1", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -110,7 +110,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -146,7 +146,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -173,7 +173,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -195,7 +195,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -219,7 +219,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -239,7 +239,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.0", + "version": "1.17.1", "bin": { "opencode": "./bin/opencode", }, @@ -330,7 +330,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -384,7 +384,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -398,7 +398,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "effect": "catalog:", }, @@ -410,7 +410,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -441,7 +441,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -457,7 +457,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74", @@ -476,7 +476,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -494,7 +494,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.0", + "version": "1.17.1", "bin": { "opencode": "./bin/opencode", }, @@ -622,7 +622,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -660,7 +660,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "cross-spawn": "catalog:", }, @@ -675,7 +675,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -689,7 +689,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -702,7 +702,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -735,7 +735,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -754,7 +754,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -794,7 +794,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -822,7 +822,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -871,7 +871,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index b2e729baec..46ef54c8fa 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.0", + "version": "1.17.1", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 5ca011a88d..db71b46ea5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "bin": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index fb6516e23c..90a33f3739 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 8de0eb9ea0..8f2af5f79b 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index fd5d768907..daaf063d56 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.0", + "version": "1.17.1", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 9f5ca70bed..fe7991eec1 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.0", + "version": "1.17.1", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 00ad843980..e2d6296407 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 3ca96188c6..723e7ee8fe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 38977ce918..ef1ba07738 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index c54bc51d3b..8ae0b1037e 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 1eceee8d7b..c1c49df152 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 3d5f62cb09..9c85f5aea0 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 8ef0a883b4..30ce5920e6 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.0", + "version": "1.17.1", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index b1843b9929..db40e0a229 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 6f5a2427f0..8118c79d8a 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 78240c6861..229739e312 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.0", + "version": "1.17.1", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index cf3707526c..ae6ce12cf6 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 88ac1d7ccb..5970822f84 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 88db3dd742..639c853c8d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 4ac2380428..621680eac3 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 3e184fb618..c2f25df0e3 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 2a9899dfc1..d26d6bbb81 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 3cdd064df8..33438363c7 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 7b4729c5d3..266aa4a6c0 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.17.0", + "version": "1.17.1", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 68826465b3..06609b9f6d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.17.0", + "version": "1.17.1", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 739c786993..0b5a2355d4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.17.0", + "version": "1.17.1", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index a54c9b6e5d..172b4cfd9a 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.17.0", + "version": "1.17.1", "publisher": "sst-dev", "repository": { "type": "git", From 5863e1254c13ef42d43a378f19a2adabc511788f Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 10:13:54 -0400 Subject: [PATCH 013/157] put fff behind flag --- packages/core/src/filesystem/search.ts | 3 ++- packages/core/src/flag/flag.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 850e0c1d5d..4280631908 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -9,6 +9,7 @@ import { FSUtil } from "../fs-util" import { Location } from "../location" import { Ripgrep } from "../ripgrep" import { RelativePath } from "../schema" +import { Flag } from "../flag/flag" export interface Interface { readonly find: (input: FileSystem.FindInput) => Effect.Effect @@ -236,4 +237,4 @@ export const fffLayer = Layer.effect( }), ) -export const defaultLayer = Layer.unwrap(Effect.sync(() => (Fff.available() ? fffLayer : ripgrepLayer))) +export const defaultLayer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_ENABLE_FFF ? fffLayer : ripgrepLayer))) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index b8b655c883..da0da4f212 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -30,6 +30,7 @@ export const Flag = { OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], + OPENCODE_ENABLE_FFF: process.env["OPENCODE_ENABLE_FFF"], // Experimental OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( From c9e2a38bf42fda4813762b9ca2deb1edc5d62f89 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:14:27 -0500 Subject: [PATCH 014/157] ci: change model from gpt-5.4-nano to gpt-5.4-mini (#31695) --- .opencode/agent/triage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 03df339cb8..11c4c816cf 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode/gpt-5.4-nano +model: opencode/gpt-5.4-mini color: "#44BA81" tools: "*": false From 1dad38d1b58ad88b239328f5897b60a4703ca9cc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 10:19:40 -0400 Subject: [PATCH 015/157] fix(core): do not gate fff on initial scan --- packages/core/src/filesystem/search.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index 4280631908..de13c37eb8 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -139,11 +139,6 @@ export const fffLayer = Layer.effect( }).pipe(Effect.orDie) if (!result.ok) return yield* Effect.die(result.error) yield* Effect.addFinalizer(() => Effect.sync(() => result.value.destroy()).pipe(Effect.ignore)) - const scanned = yield* Effect.tryPromise({ - try: () => result.value.waitForScan(5_000), - catch: (cause) => cause, - }).pipe(Effect.orDie) - if (!scanned.ok || !scanned.value) return yield* Effect.die(scanned.ok ? "fff scan timed out" : scanned.error) return Service.of({ glob: (input) => Effect.sync(() => { From 538cfaff0d4c43365417fd41cf4f387689dc58e8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 10:21:25 -0400 Subject: [PATCH 016/157] feat(core): enable fff by default --- packages/core/src/filesystem/search.ts | 2 +- packages/core/src/flag/flag.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index de13c37eb8..ce5f50cc6e 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -232,4 +232,4 @@ export const fffLayer = Layer.effect( }), ) -export const defaultLayer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_ENABLE_FFF ? fffLayer : ripgrepLayer))) +export const defaultLayer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF ? ripgrepLayer : fffLayer))) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index da0da4f212..d27486c518 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -30,7 +30,7 @@ export const Flag = { OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], - OPENCODE_ENABLE_FFF: process.env["OPENCODE_ENABLE_FFF"], + OPENCODE_DISABLE_FFF: truthy("OPENCODE_DISABLE_FFF"), // Experimental OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( From e4300e9b7433e068c3d57ac41fcb39bc5de3d32e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 10:22:00 -0400 Subject: [PATCH 017/157] fix(core): disable fff by default on windows --- packages/core/src/flag/flag.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index d27486c518..a0eb78a13e 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -6,6 +6,7 @@ export function truthy(key: string) { } const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"] +const fff = process.env["OPENCODE_DISABLE_FFF"] function enabledByExperimental(key: string) { return process.env[key] === undefined ? truthy("OPENCODE_EXPERIMENTAL") : truthy(key) @@ -30,7 +31,7 @@ export const Flag = { OPENCODE_FAKE_VCS: process.env["OPENCODE_FAKE_VCS"], OPENCODE_SERVER_PASSWORD: process.env["OPENCODE_SERVER_PASSWORD"], OPENCODE_SERVER_USERNAME: process.env["OPENCODE_SERVER_USERNAME"], - OPENCODE_DISABLE_FFF: truthy("OPENCODE_DISABLE_FFF"), + OPENCODE_DISABLE_FFF: fff === undefined ? process.platform === "win32" : truthy("OPENCODE_DISABLE_FFF"), // Experimental OPENCODE_EXPERIMENTAL_FILEWATCHER: Config.boolean("OPENCODE_EXPERIMENTAL_FILEWATCHER").pipe( From 3ad6923c61438faeb0c4677ee55f0301ddbe43ba Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:26:08 -0500 Subject: [PATCH 018/157] fix(opencode): let subagents use their own permissions (#31696) --- packages/opencode/src/agent/agent.ts | 3 + .../src/agent/subagent-permissions.ts | 16 +--- packages/opencode/src/tool/task.ts | 4 - packages/opencode/test/agent/agent.test.ts | 29 +++++++ .../agent/plan-mode-subagent-bypass.test.ts | 82 ++++--------------- 5 files changed, 50 insertions(+), 84 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 1424400871..b1430314ff 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -160,6 +160,9 @@ export const layer = Layer.effect( Permission.fromConfig({ question: "allow", plan_exit: "allow", + task: { + general: "deny", + }, external_directory: { [path.join(Global.Path.data, "plans", "*")]: "allow", }, diff --git a/packages/opencode/src/agent/subagent-permissions.ts b/packages/opencode/src/agent/subagent-permissions.ts index 56da42626c..b1b99b484e 100644 --- a/packages/opencode/src/agent/subagent-permissions.ts +++ b/packages/opencode/src/agent/subagent-permissions.ts @@ -1,31 +1,23 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" -import type { Permission } from "../permission" import type { Agent } from "./agent" /** * Build the `permission` ruleset for a subagent's session when it's spawned * via the task tool. Combines: * - * 1. The parent **agent's** edit-class deny rules — Plan Mode's file-edit - * restriction lives on the agent ruleset, not on the session, so a - * subagent that only inherited the parent SESSION's permission would - * silently bypass it. (#26514) - * 2. The parent **session's** deny rules and external_directory rules — - * same forwarding the original code already did. - * 3. Default `todowrite` and `task` denies if the subagent's own ruleset + * 1. The parent session's deny rules and external_directory rules. + * Parent agent restrictions only govern that agent; the subagent's own + * permissions determine its capabilities. + * 2. Default `todowrite` and `task` denies if the subagent's own ruleset * doesn't already permit them. */ export function deriveSubagentSessionPermission(input: { parentSessionPermission: PermissionV1.Ruleset - parentAgent: Agent.Info | undefined subagent: Agent.Info }): PermissionV1.Ruleset { const canTask = input.subagent.permission.some((rule) => rule.permission === "task") const canTodo = input.subagent.permission.some((rule) => rule.permission === "todowrite") - const parentAgentDenies = - input.parentAgent?.permission.filter((rule) => rule.action === "deny" && rule.permission === "edit") ?? [] return [ - ...parentAgentDenies, ...input.parentSessionPermission.filter( (rule) => rule.permission === "external_directory" || rule.action === "deny", ), diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index dac340184c..b0a866c90e 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -122,12 +122,8 @@ export const TaskTool = Tool.define( ? yield* sessions.get(SessionID.make(params.task_id)).pipe(Effect.catchCause(() => Effect.succeed(undefined))) : undefined const parent = yield* sessions.get(ctx.sessionID) - const parentAgent = parent.agent - ? yield* agent.get(parent.agent).pipe(Effect.catchCause(() => Effect.succeed(undefined))) - : undefined const childPermission = deriveSubagentSessionPermission({ parentSessionPermission: parent.permission ?? [], - parentAgent, subagent: next, }) const childToolDenies = [ diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 67071bce73..1df95b5c0f 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -85,6 +85,35 @@ it.instance("plan agent denies edits except .opencode/plans/*", () => }), ) +it.instance("plan agent denies the general subagent by default", () => + Effect.gen(function* () { + const plan = yield* load((svc) => svc.get("plan")) + expect(plan).toBeDefined() + expect(Permission.evaluate("task", "general", plan!.permission).action).toBe("deny") + expect(Permission.evaluate("task", "explore", plan!.permission).action).toBe("allow") + expect(Permission.evaluate("task", "custom", plan!.permission).action).toBe("allow") + }), +) + +it.instance( + "user permission can allow the general subagent from plan mode", + () => + Effect.gen(function* () { + const plan = yield* load((svc) => svc.get("plan")) + expect(plan).toBeDefined() + expect(Permission.evaluate("task", "general", plan!.permission).action).toBe("allow") + }), + { + config: { + permission: { + task: { + general: "allow", + }, + }, + }, + }, +) + it.instance("explore agent denies edit and write", () => Effect.gen(function* () { const explore = yield* load((svc) => svc.get("explore")) diff --git a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts index de0e2cd46a..a58a5ddf2f 100644 --- a/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts +++ b/packages/opencode/test/agent/plan-mode-subagent-bypass.test.ts @@ -1,24 +1,4 @@ import { PermissionV1 } from "@opencode-ai/core/v1/permission" -/** - * Reproducer for opencode issue #26514: - * - * In Plan Mode (the `plan` agent), the main agent's edit/write tools are - * blocked by the plan agent's permission ruleset (`edit: { "*": "deny" }`). - * However, when the plan agent spawns a subagent via the `task` tool, the - * subagent retains full file modification capabilities — a security bypass. - * - * This test replicates the permission ruleset that would govern a - * `general` subagent when launched from a `plan` parent session, mirroring - * the logic in `src/tool/task.ts` (filtered parent permissions ++ runtime - * subagent agent permissions, evaluated as in `session/prompt.ts`). - * - * The expected (secure) behavior is that the subagent inherits the plan - * mode read-only restriction and `edit`/`write` resolve to `deny`. On - * origin/dev this assertion fails because the parent **agent** permissions - * are not propagated to the subagent — only the parent **session** - * permissions are passed through, and Plan Mode's restrictions live on the - * agent, not the session. - */ import { expect } from "bun:test" import { Effect } from "effect" import { Agent } from "../../src/agent/agent" @@ -45,7 +25,7 @@ function testAgent(input: { // exercises the actual helper that task.ts uses to build the subagent's // session permission, so any regression in that helper trips this test. -it.instance("[#26514] subagent spawned from plan mode inherits read-only restriction (edit denied)", () => +it.instance("subagent permissions take precedence over parent agent restrictions", () => Effect.gen(function* () { const planAgent = yield* Agent.use.get("plan") const generalAgent = yield* Agent.use.get("general") @@ -57,15 +37,10 @@ it.instance("[#26514] subagent spawned from plan mode inherits read-only restric // tool layer — see Permission.disabled / EDIT_TOOLS.) expect(Permission.evaluate("edit", "/some/file.ts", planAgent!.permission).action).toBe("deny") - // Simulate the plan-mode parent session: in real flow the plan - // session's `permission` field is empty (Plan Mode lives on the agent - // ruleset, not the session). So we pass [] through as the parent - // session permission, exactly like the actual code path. const parentSessionPermission: PermissionV1.Ruleset = [] const subagentSessionPermission = deriveSubagentSessionPermission({ parentSessionPermission, - parentAgent: planAgent, subagent: generalAgent!, }) @@ -73,40 +48,29 @@ it.instance("[#26514] subagent spawned from plan mode inherits read-only restric // ruleset: Permission.merge(agent.permission, session.permission ?? []) const effective = Permission.merge(generalAgent!.permission, subagentSessionPermission) - expect(Permission.evaluate("edit", "/some/file.ts", effective).action).toBe("deny") - expect(Permission.evaluate("edit", "/another/path/index.tsx", effective).action).toBe("deny") + expect(Permission.evaluate("edit", "/some/file.ts", effective).action).not.toBe("deny") + expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual(new Set()) }), ) -it.instance("[#26514] explore subagent launched from plan mode also stays read-only", () => - // Sibling check: even though `explore` is intrinsically read-only, the - // bug surface is the same. Including this case to document that the fix - // should propagate the parent **agent** permissions, not just deny edit - // when the subagent happens to already deny it. +it.instance("subagent's own read-only restriction remains effective", () => Effect.gen(function* () { - const planAgent = yield* Agent.use.get("plan") const explore = yield* Agent.use.get("explore") - expect(planAgent).toBeDefined() expect(explore).toBeDefined() const parentSessionPermission: PermissionV1.Ruleset = [] const subagentSessionPermission = deriveSubagentSessionPermission({ parentSessionPermission, - parentAgent: planAgent, subagent: explore!, }) const effective = Permission.merge(explore!.permission, subagentSessionPermission) - // Already deny — sanity check. expect(Permission.evaluate("edit", "/x.ts", effective).action).toBe("deny") }), ) it.instance( - "[#26514] custom user subagent launched from plan mode bypasses Plan Mode read-only", - // The most damaging case: a user-defined subagent with default - // permissions (allow-by-default, like `general`). The subagent must NOT - // be able to edit when the parent agent is `plan`. + "custom subagent can explicitly enable edits denied to its parent agent", () => Effect.gen(function* () { const planAgent = yield* Agent.use.get("plan") @@ -117,14 +81,13 @@ it.instance( const parentSessionPermission: PermissionV1.Ruleset = [] const subagentSessionPermission = deriveSubagentSessionPermission({ parentSessionPermission, - parentAgent: planAgent, subagent: my!, }) const effective = Permission.merge(my!.permission, subagentSessionPermission) - // BUG: on origin/dev edit resolves to "allow" because the plan - // agent's `edit: deny *` rule never reaches the subagent. - expect(Permission.evaluate("edit", "/some/file.ts", effective).action).toBe("deny") + expect(Permission.evaluate("edit", "/some/file.ts", planAgent!.permission).action).toBe("deny") + expect(Permission.evaluate("edit", "/some/file.ts", effective).action).toBe("allow") + expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual(new Set()) }), { config: { @@ -132,29 +95,17 @@ it.instance( my_subagent: { description: "A user-defined subagent", mode: "subagent", + permission: { + edit: "allow", + }, }, }, }, }, ) -it.effect("[#26700] controller self-restrictions do not erase executor permissions", () => +it.effect("subagent self permissions are preserved", () => Effect.sync(() => { - const controller = testAgent({ - name: "controller", - mode: "primary", - permission: { - "*": "deny", - read: "deny", - bash: "deny", - task: { - "*": "deny", - executor: "allow", - }, - edit: "deny", - write: "deny", - }, - }) const executor = testAgent({ name: "executor", mode: "subagent", @@ -166,8 +117,7 @@ it.effect("[#26700] controller self-restrictions do not erase executor permissio "*": "deny", worker: "allow", }, - edit: "deny", - write: "deny", + edit: "allow", }, }) @@ -175,7 +125,6 @@ it.effect("[#26700] controller self-restrictions do not erase executor permissio executor.permission, deriveSubagentSessionPermission({ parentSessionPermission: [], - parentAgent: controller, subagent: executor, }), ) @@ -184,9 +133,7 @@ it.effect("[#26700] controller self-restrictions do not erase executor permissio expect(Permission.evaluate("bash", "git status", effective).action).toBe("allow") expect(Permission.evaluate("task", "worker", effective).action).toBe("allow") expect(Permission.evaluate("task", "other", effective).action).toBe("deny") - expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual( - new Set(["edit", "write", "apply_patch"]), - ) + expect(Permission.disabled(["edit", "write", "apply_patch"], effective)).toEqual(new Set()) }), ) @@ -203,7 +150,6 @@ it.effect("subagent inherits parent session deny rules as hard runtime ceilings" executor.permission, deriveSubagentSessionPermission({ parentSessionPermission: Permission.fromConfig({ bash: "deny" }), - parentAgent: undefined, subagent: executor, }), ) From 02608a4e9791c256df7e32ae88ec72a7df16623e Mon Sep 17 00:00:00 2001 From: Ayush Thakur <51413362+Ayushlm10@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:32:09 +0530 Subject: [PATCH 019/157] fix: recover from expired enterprise auth on remote config load (#31661) --- packages/core/src/v1/config/error.ts | 5 +++ packages/opencode/src/cli/cmd/providers.ts | 5 ++- packages/opencode/src/cli/error.ts | 12 +++++++ packages/opencode/src/config/config.ts | 18 ++++++++--- packages/opencode/test/config/config.test.ts | 34 ++++++++++++++++++-- 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/core/src/v1/config/error.ts b/packages/core/src/v1/config/error.ts index 268a6eb202..abf3a09276 100644 --- a/packages/core/src/v1/config/error.ts +++ b/packages/core/src/v1/config/error.ts @@ -32,3 +32,8 @@ export const DirectoryTypoError = NamedError.create("ConfigDirectoryTypoError", dir: Schema.String, suggestion: Schema.String, }) + +export const RemoteAuthError = NamedError.create("ConfigRemoteAuthError", { + url: Schema.String, + remote: Schema.String, +}) diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 620940b383..cec20a0230 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -1,3 +1,4 @@ +import type { Argv } from "yargs" import { Auth } from "../../auth" import { cmd } from "./cmd" import { CliError, effectCmd, fail } from "../effect-cmd" @@ -298,7 +299,9 @@ export const ProvidersListCommand = effectCmd({ export const ProvidersLoginCommand = effectCmd({ command: "login [url]", describe: "log in to a provider", - builder: (yargs) => + // URL login skips instance bootstrap, which would load remote config with the stale token and crash before re-auth. + instance: (args) => !args.url, + builder: (yargs: Argv) => yargs .positional("url", { describe: "opencode auth provider", diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index ef724bbbef..407547e4e5 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -94,6 +94,18 @@ export function FormatError(input: unknown): string | undefined { return stringField(configFrontmatter, "message") ?? "" } + // ConfigRemoteAuthError: { url: string, remote: string } + const remoteAuth = configData(input, "ConfigRemoteAuthError") + if (remoteAuth) { + const url = stringField(remoteAuth, "url") + const remote = stringField(remoteAuth, "remote") + return [ + `Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`, + `Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).`, + ...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []), + ].join("\n") + } + // ConfigInvalidError: { path?: string, message?: string, issues?: Array<{ message: string, path: string[] }> } const configInvalid = configData(input, "ConfigInvalidError") if (configInvalid) { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 480f08f81d..7f568f4920 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,10 +19,11 @@ import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state" import { FSUtil } from "@opencode-ai/core/fs-util" import { InstanceState } from "@/effect/instance-state" import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { containsPath, type InstanceContext } from "../project/instance-context" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" +import { RemoteAuthError } from "@opencode-ai/core/v1/config/error" import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission" import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin" import { ConfigAgent } from "./agent" @@ -187,6 +188,7 @@ export const layer = Layer.effect( url: string, headers: Record | undefined, schema: S, + loginOrigin: string, ) { const response = yield* HttpClient.filterStatusOk(withTransientReadRetry(http)) .execute( @@ -195,7 +197,15 @@ export const layer = Layer.effect( .pipe( Effect.catch((error) => Effect.die(new Error(`failed to fetch remote config from ${url}: ${String(error)}`))), ) - return yield* HttpClientResponse.schemaBodyJson(schema)(response).pipe( + const body = yield* response.text.pipe( + Effect.catch((error) => Effect.die(new Error(`failed to read remote config from ${url}: ${String(error)}`))), + ) + // An auth proxy can answer with an HTML login page at HTTP 200 (passes filterStatusOk); treat it as a re-auth error, not a decode failure. + const contentType = (response.headers["content-type"] ?? "").toLowerCase() + if (contentType.includes("html") || /^\s* Effect.die(new Error(`failed to decode remote config from ${url}: ${String(error)}`))), ) }) @@ -348,7 +358,7 @@ export const layer = Layer.effect( authEnv[value.key] = value.token const wellknownURL = `${url}/.well-known/opencode` yield* Effect.logDebug("fetching remote config", { url: wellknownURL }) - const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown) + const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown, url) const remote = yield* Effect.promise(() => substituteWellKnownRemoteConfig({ value: wellknown.remote_config, @@ -360,7 +370,7 @@ export const layer = Layer.effect( const fetchedConfig = remote ? yield* Effect.gen(function* () { yield* Effect.logDebug("fetching remote config", { url: remote.url }) - const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json) + const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json, url) if (isRecord(data) && isRecord(data.config)) return data.config if (isRecord(data)) return data return yield* Effect.die( diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index f4a0cd4a28..02ace53668 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1,6 +1,7 @@ import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" -import { Effect, Exit, Layer, Option } from "effect" +import { Cause, Effect, Exit, Layer, Option } from "effect" +import { NamedError } from "@opencode-ai/core/util/error" import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config } from "@/config/config" @@ -71,6 +72,7 @@ const wellKnownAuth = (url: string) => function remoteConfigClient(input: { wellKnown: unknown remote?: unknown + remoteHtml?: string seen: { wellKnown?: string; remote?: string; authorization?: string } }) { return HttpClient.make((request) => { @@ -78,9 +80,17 @@ function remoteConfigClient(input: { input.seen.wellKnown = request.url return Effect.succeed(json(request, input.wellKnown)) } - if (input.remote !== undefined && request.url.includes("config.example.com")) { + if (request.url.includes("config.example.com") && (input.remote !== undefined || input.remoteHtml !== undefined)) { input.seen.remote = request.url input.seen.authorization = request.headers.authorization + if (input.remoteHtml !== undefined) { + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(input.remoteHtml, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } }), + ), + ) + } return Effect.succeed(json(request, input.remote)) } return Effect.succeed(json(request, {}, 404)) @@ -214,6 +224,7 @@ const wellKnown = (input: { config?: unknown remoteConfig?: { url: string; headers?: Record } remote?: unknown + remoteHtml?: string wellKnown?: unknown }) => { const seen: { wellKnown?: string; remote?: string; authorization?: string } = {} @@ -224,6 +235,7 @@ const wellKnown = (input: { ...(input.remoteConfig !== undefined ? { remote_config: input.remoteConfig } : {}), }, remote: input.remote, + remoteHtml: input.remoteHtml, }) return { seen, @@ -1635,6 +1647,24 @@ invalidRemoteWellKnown.it.instance("wellknown remote_config rejects non-object c }), ) +const loginPageWellKnown = wellKnown({ + remoteConfig: { url: "https://config.example.com/opencode.json" }, + remoteHtml: "Sign inLogin required", +}) + +loginPageWellKnown.it.instance( + "wellknown remote_config surfaces an actionable auth error when the gateway returns an HTML login page", + () => + Effect.gen(function* () { + const exit = yield* Config.use.get().pipe(Effect.exit) + expect(loginPageWellKnown.seen.remote).toBe("https://config.example.com/opencode.json") + expect(Exit.isFailure(exit)).toBe(true) + const error = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined + expect(NamedError.hasName(error, "ConfigRemoteAuthError")).toBe(true) + expect((error as { data?: { url?: string } }).data?.url).toBe("https://example.com") + }), +) + describe("resolvePluginSpec", () => { test("keeps package specs unchanged", async () => { await using tmp = await tmpdir() From 649618c50ac382a763921201cb2b53e0dff88e0c Mon Sep 17 00:00:00 2001 From: mridul <65942753+rexdotsh@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:16:09 +0530 Subject: [PATCH 020/157] fix(app): restore device attachment picker (#31707) --- .../app/src/components/dialog-select-file.tsx | 5 --- packages/app/src/components/prompt-input.tsx | 41 ++++++------------- .../prompt-input/server-attachment.test.ts | 32 --------------- .../prompt-input/server-attachment.ts | 8 ---- 4 files changed, 12 insertions(+), 74 deletions(-) delete mode 100644 packages/app/src/components/prompt-input/server-attachment.test.ts delete mode 100644 packages/app/src/components/prompt-input/server-attachment.ts diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index b231d3dc86..dcc69435a8 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -264,7 +264,6 @@ function createSessionEntries(props: { export function DialogSelectFile(props: { mode?: DialogSelectFileMode onOpenFile?: (path: string) => void - onSelectFile?: (path: string) => void }) { const command = useCommand() const language = useLanguage() @@ -379,10 +378,6 @@ export function DialogSelectFile(props: { } if (!item.path) return - if (props.onSelectFile) { - props.onSelectFile(item.path) - return - } open(item.path) } diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 835bc23819..007f6bf5b0 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -53,7 +53,6 @@ import { usePermission } from "@/context/permission" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" -import { serverAttachmentFile } from "./prompt-input/server-attachment" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom" @@ -473,34 +472,18 @@ export const PromptInput: Component = (props) => { const escBlur = () => platform.platform === "desktop" && platform.os === "macos" const pick = () => { - if (server.isLocal()) { - pickAttachmentFiles({ - picker: platform.openAttachmentPickerDialog, - directory: () => sdk.directory, - fallback: () => fileInputRef?.click(), - onFile: addAttachment, - onError: (error) => - showToast({ - variant: "error", - title: language.t("common.requestFailed"), - description: error instanceof Error ? error.message : String(error), - }), - }) - return - } - void import("@/components/dialog-select-file").then((module) => - dialog.show(() => ( - { - void sdk.client.v2.fs - .read({ path }) - .then((response) => response.data?.data) - .then((data) => data && addAttachments([serverAttachmentFile(path, data)])) - }} - /> - )), - ) + pickAttachmentFiles({ + picker: platform.openAttachmentPickerDialog, + directory: () => sdk.directory, + fallback: () => fileInputRef?.click(), + onFile: addAttachment, + onError: (error) => + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: error instanceof Error ? error.message : String(error), + }), + }) } const setMode = (mode: "normal" | "shell") => { diff --git a/packages/app/src/components/prompt-input/server-attachment.test.ts b/packages/app/src/components/prompt-input/server-attachment.test.ts deleted file mode 100644 index d4de9af0b8..0000000000 --- a/packages/app/src/components/prompt-input/server-attachment.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { serverAttachmentFile } from "./server-attachment" - -describe("serverAttachmentFile", () => { - test("creates a file from server text content", async () => { - const file = serverAttachmentFile("docs/readme.txt", { - uri: "file:///docs/readme.txt", - name: "readme.txt", - content: "hello", - encoding: "utf8", - mime: "text/plain", - }) - - expect(file.name).toBe("readme.txt") - expect(file.type).toBe("text/plain") - expect(await file.text()).toBe("hello") - }) - - test("creates a file from server base64 content", async () => { - const file = serverAttachmentFile("images/pixel.png", { - uri: "file:///images/pixel.png", - name: "pixel.png", - content: "aGVsbG8=", - encoding: "base64", - mime: "image/png", - }) - - expect(file.name).toBe("pixel.png") - expect(file.type).toBe("image/png") - expect(await file.text()).toBe("hello") - }) -}) diff --git a/packages/app/src/components/prompt-input/server-attachment.ts b/packages/app/src/components/prompt-input/server-attachment.ts deleted file mode 100644 index 1bd7c14824..0000000000 --- a/packages/app/src/components/prompt-input/server-attachment.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getFilename } from "@opencode-ai/core/util/path" -import type { FileSystemContent } from "@opencode-ai/sdk/v2" - -export function serverAttachmentFile(path: string, data: FileSystemContent) { - const content = - data.encoding === "utf8" ? data.content : Uint8Array.from(atob(data.content), (char) => char.charCodeAt(0)) - return new File([content], getFilename(path), { type: data.mime }) -} From e1073e5d1899a858f61b47c2a80eb2d077ec9d26 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 15:48:14 +0000 Subject: [PATCH 021/157] chore: generate --- packages/app/src/components/dialog-select-file.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index dcc69435a8..4080c8ce85 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -261,10 +261,7 @@ function createSessionEntries(props: { return { sessions } } -export function DialogSelectFile(props: { - mode?: DialogSelectFileMode - onOpenFile?: (path: string) => void -}) { +export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFile?: (path: string) => void }) { const command = useCommand() const language = useLanguage() const layout = useLayout() From 2e0f88d0e373f1841cc672196934a4eb7d6f8b2c Mon Sep 17 00:00:00 2001 From: Filip <34747899+neriousy@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:54:49 +0200 Subject: [PATCH 022/157] fix(desktop): restore linux launcher identity (#31709) --- .../desktop/electron-builder.config.test.ts | 48 +++++++++++++++++++ packages/desktop/electron-builder.config.ts | 45 +++++++++++++---- .../resources/linux/opencode-desktop.desktop | 10 ++++ 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 packages/desktop/electron-builder.config.test.ts create mode 100644 packages/desktop/resources/linux/opencode-desktop.desktop diff --git a/packages/desktop/electron-builder.config.test.ts b/packages/desktop/electron-builder.config.test.ts new file mode 100644 index 0000000000..eee2b825cb --- /dev/null +++ b/packages/desktop/electron-builder.config.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from "bun:test" +import type { Configuration } from "electron-builder" + +const legacyDesktopEntry = "resources/linux/opencode-desktop.desktop" + +const channels = [ + { channel: "dev", appId: "ai.opencode.desktop.dev" }, + { channel: "beta", appId: "ai.opencode.desktop.beta" }, + { channel: "prod", appId: "ai.opencode.desktop" }, +] as const + +for (const channel of channels) { + test(`uses one Linux desktop identity for ${channel.channel}`, async () => { + const previous = process.env.OPENCODE_CHANNEL + process.env.OPENCODE_CHANNEL = channel.channel + + const module = await import(`./electron-builder.config.ts?channel=${channel.channel}`) + const config = module.default as Configuration + + if (previous === undefined) delete process.env.OPENCODE_CHANNEL + else process.env.OPENCODE_CHANNEL = previous + + expect(config.appId).toBe(channel.appId) + expect(config.extraMetadata?.desktopName).toBe(`${channel.appId}.desktop`) + expect(config.linux?.executableName).toBe(channel.appId) + expect(config.linux?.desktop?.entry?.StartupWMClass).toBe(channel.appId) + }) +} + +test("keeps a hidden prod launcher for old Linux pins", async () => { + const previous = process.env.OPENCODE_CHANNEL + process.env.OPENCODE_CHANNEL = "prod" + + const module = await import("./electron-builder.config.ts?compat=prod") + const config = module.default as Configuration + + if (previous === undefined) delete process.env.OPENCODE_CHANNEL + else process.env.OPENCODE_CHANNEL = previous + + expect(config.deb?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`) + expect(config.rpm?.fpm?.[0]).toEndWith(`${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop`) + + const desktop = await Bun.file(legacyDesktopEntry).text() + expect(desktop).toContain("Exec=/opt/OpenCode/ai.opencode.desktop %U") + expect(desktop).toContain("Icon=ai.opencode.desktop") + expect(desktop).toContain("StartupWMClass=ai.opencode.desktop") + expect(desktop).toContain("NoDisplay=true") +}) diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index 7fd03ae621..23afe811c2 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -6,8 +6,14 @@ import { promisify } from "node:util" import type { Configuration } from "electron-builder" const execFileAsync = promisify(execFile) -const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..") +const packageDir = path.dirname(fileURLToPath(import.meta.url)) +const rootDir = path.resolve(packageDir, "../..") const signScript = path.join(rootDir, "script", "sign-windows.ps1") +// The Electron 42 packaging update briefly installed Linux launchers/icons under +// "opencode-desktop". Keep that hidden desktop entry around so existing GNOME/KDE +// pins still resolve after the canonical app id changes back to ai.opencode.desktop. +const legacyDesktopEntry = path.join(packageDir, "resources", "linux", "opencode-desktop.desktop") +const legacyDesktopEntryFpm = `${legacyDesktopEntry}=/usr/share/applications/opencode-desktop.desktop` async function signWindows(configuration: { path: string }) { if (process.platform !== "win32") return @@ -26,12 +32,26 @@ const channel = (() => { return "dev" })() -const getBase = (): Configuration => ({ +const APP_IDS = { + dev: "ai.opencode.desktop.dev", + beta: "ai.opencode.desktop.beta", + prod: "ai.opencode.desktop", +} as const + +const getBase = (appId: string): Configuration => ({ artifactName: "opencode-desktop-${os}-${arch}.${ext}", directories: { output: "dist", buildResources: "resources", }, + // Linux launchers are .desktop files, so this is the desktop file name, + // not just the app id. For prod, app id "ai.opencode.desktop" becomes + // "ai.opencode.desktop.desktop". + // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html + // https://www.electron.build/docs/linux/ + extraMetadata: { + desktopName: `${appId}.desktop`, + }, files: ["out/**/*", "resources/**/*"], extraResources: [ { @@ -74,19 +94,27 @@ const getBase = (): Configuration => ({ linux: { icon: `resources/icons`, category: "Development", - executableName: "opencode-desktop", + executableName: appId, + desktop: { + entry: { + // Match the installed .desktop file and hicolor icon basename so + // Linux shells can associate the running Electron window with its launcher. + StartupWMClass: appId, + }, + }, target: ["AppImage", "deb", "rpm"], }, }) function getConfig() { - const base = getBase() + const appId = APP_IDS[channel] + const base = getBase(appId) switch (channel) { case "dev": { return { ...base, - appId: "ai.opencode.desktop.dev", + appId, productName: "OpenCode Dev", rpm: { packageName: "opencode-dev" }, } @@ -94,7 +122,7 @@ function getConfig() { case "beta": { return { ...base, - appId: "ai.opencode.desktop.beta", + appId, productName: "OpenCode Beta", protocols: { name: "OpenCode Beta", schemes: ["opencode"] }, publish: { provider: "github", owner: "anomalyco", repo: "opencode-beta", channel: "latest" }, @@ -104,11 +132,12 @@ function getConfig() { case "prod": { return { ...base, - appId: "ai.opencode.desktop", + appId, productName: "OpenCode", protocols: { name: "OpenCode", schemes: ["opencode"] }, publish: { provider: "github", owner: "anomalyco", repo: "opencode", channel: "latest" }, - rpm: { packageName: "opencode" }, + deb: { fpm: [legacyDesktopEntryFpm] }, + rpm: { packageName: "opencode", fpm: [legacyDesktopEntryFpm] }, } } } diff --git a/packages/desktop/resources/linux/opencode-desktop.desktop b/packages/desktop/resources/linux/opencode-desktop.desktop new file mode 100644 index 0000000000..a5f677412c --- /dev/null +++ b/packages/desktop/resources/linux/opencode-desktop.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=OpenCode +Exec=/opt/OpenCode/ai.opencode.desktop %U +Terminal=false +Type=Application +Icon=ai.opencode.desktop +StartupWMClass=ai.opencode.desktop +NoDisplay=true +Comment=Open source AI coding agent +Categories=Development; From 2c652732048abdaddee5f5f8e75fbb1aff1c7824 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 10 Jun 2026 16:42:54 +0000 Subject: [PATCH 023/157] sync release versions for v1.17.2 --- bun.lock | 52 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 28 files changed, 53 insertions(+), 53 deletions(-) diff --git a/bun.lock b/bun.lock index cbc4e7f7e7..0ebb18a515 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.1", + "version": "1.17.2", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -110,7 +110,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -146,7 +146,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -173,7 +173,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -195,7 +195,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -219,7 +219,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -239,7 +239,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.1", + "version": "1.17.2", "bin": { "opencode": "./bin/opencode", }, @@ -330,7 +330,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -384,7 +384,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -398,7 +398,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "effect": "catalog:", }, @@ -410,7 +410,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -441,7 +441,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -457,7 +457,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74", @@ -476,7 +476,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -494,7 +494,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.1", + "version": "1.17.2", "bin": { "opencode": "./bin/opencode", }, @@ -622,7 +622,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -660,7 +660,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "cross-spawn": "catalog:", }, @@ -675,7 +675,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -689,7 +689,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -702,7 +702,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -735,7 +735,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -754,7 +754,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -794,7 +794,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -822,7 +822,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -871,7 +871,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index 46ef54c8fa..d70d021b29 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.1", + "version": "1.17.2", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index db71b46ea5..e6532b880f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "bin": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 90a33f3739..96f80cdb70 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 8f2af5f79b..f5d20ac10d 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index daaf063d56..d863daf251 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.1", + "version": "1.17.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index fe7991eec1..2bfa1a60dc 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.1", + "version": "1.17.2", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index e2d6296407..436ef4e32b 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 723e7ee8fe..0de9fbdb3c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ef1ba07738..97593d8848 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 8ae0b1037e..cd8ad23a9a 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index c1c49df152..5ffe2a0430 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 9c85f5aea0..9d792e16cc 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 30ce5920e6..7c5d66c3be 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.1", + "version": "1.17.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index db40e0a229..00d549396c 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 8118c79d8a..afa820eb3b 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 229739e312..d0a3d283ab 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.1", + "version": "1.17.2", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ae6ce12cf6..c62b30c24f 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5970822f84..c902d8d346 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 639c853c8d..5acd2d1875 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 621680eac3..6313f5e55c 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index c2f25df0e3..3faa1f42c2 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index d26d6bbb81..42bc47fbb5 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 33438363c7..60dc92effe 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 266aa4a6c0..868474e627 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.17.1", + "version": "1.17.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 06609b9f6d..1ebd8b68cd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.17.1", + "version": "1.17.2", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index 0b5a2355d4..b3d3277cc4 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.17.1", + "version": "1.17.2", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 172b4cfd9a..7c129c4011 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.17.1", + "version": "1.17.2", "publisher": "sst-dev", "repository": { "type": "git", From 14ec7ed5b311413f3a93c081e45da9d8336f5d93 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 13:28:48 -0400 Subject: [PATCH 024/157] fix fff disabling logic --- packages/core/src/filesystem/search.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/filesystem/search.ts b/packages/core/src/filesystem/search.ts index ce5f50cc6e..0f123f5b9c 100644 --- a/packages/core/src/filesystem/search.ts +++ b/packages/core/src/filesystem/search.ts @@ -232,4 +232,6 @@ export const fffLayer = Layer.effect( }), ) -export const defaultLayer = Layer.unwrap(Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF ? ripgrepLayer : fffLayer))) +export const defaultLayer = Layer.unwrap( + Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)), +) From bed780fac90cdc6a626dd1f723f0ef3c919cf089 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Wed, 10 Jun 2026 19:30:43 +0200 Subject: [PATCH 025/157] chore: bump gitlab-ai-provider to 6.9.1 (#31728) --- bun.lock | 6 +++--- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index 0ebb18a515..b85e481f99 100644 --- a/bun.lock +++ b/bun.lock @@ -288,7 +288,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.9.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -564,7 +564,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.9.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -3626,7 +3626,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.8.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-KwHASXkHtDcgrzTXZVp9Dyx6t8m9nK0R2fCm47MWcxxQ1kOBt3f2LZugtu1kOby8i4Sbd+kvBSYM66PGkDclng=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.9.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-2qd8UCwS3qTGk0uVq+tMT+06cpMjTw8ShECCAnI3+pXynnxG3rEo4tRojl9ES281pA+WwijsO6gCHuiuleYBjg=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], diff --git a/packages/core/package.json b/packages/core/package.json index 0de9fbdb3c..b06dd53a82 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -104,7 +104,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.9.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index d0a3d283ab..9cb6fd9fef 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -117,7 +117,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.8.0", + "gitlab-ai-provider": "6.9.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", From 5b54203168658adf4f50adb6d81cb22528b6e43b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 17:44:15 +0000 Subject: [PATCH 026/157] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 1bbea78c1b..3c65d0dc04 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-nix3Ogrt4nFv+HSZpYZ3VqIQc+g1SdP+DSgu72Yjoqw=", - "aarch64-linux": "sha256-L634j49jyNoKhC1MDyF+Grxy++yhWgcbXsxKDgYhKAk=", - "aarch64-darwin": "sha256-ha692TeekwiV0irhIxdwE8/1x1bLKtzSwcjDvetiPqM=", - "x86_64-darwin": "sha256-JRwKof0cTFwOcOyLb6l+kF/HlCiFmMWzG1y3U1esyyM=" + "x86_64-linux": "sha256-bQFK3yO9Y5m2zIlZ16wmJ05WOLNuvYvHMDbV+lEHzkg=", + "aarch64-linux": "sha256-sP/DzyfDnyeGOAo0cx50Jas3X4RGuLxfAdoTrv355c0=", + "aarch64-darwin": "sha256-88zVvcQyKok6qoi70FtLdYkUhyNyD1hrcDTUDEuOtiM=", + "x86_64-darwin": "sha256-CUXFpNm274+ekBTZzD/hkocNFAexOBTMzIZFyGBh8MA=" } } From 8688ed7e3ea40d5a4e9c863f7041fae48a3becdc Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:55:37 -0500 Subject: [PATCH 027/157] feat(web): data link --- packages/console/app/src/component/header.tsx | 23 ++++++++----------- packages/console/app/src/i18n/en.ts | 1 + 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx index cc45ed534f..a0e5ffc0ca 100644 --- a/packages/console/app/src/component/header.tsx +++ b/packages/console/app/src/component/header.tsx @@ -13,10 +13,9 @@ import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg" import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg" import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg" import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg" -import { A, createAsync, useNavigate } from "@solidjs/router" +import { A, useNavigate } from "@solidjs/router" import { createMemo, Match, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" -import { github } from "~/lib/github" import { createEffect, onCleanup } from "solid-js" import { config } from "~/config" import { useI18n } from "~/context/i18n" @@ -40,16 +39,6 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo const navigate = useNavigate() const i18n = useI18n() const language = useLanguage() - const githubData = createAsync(() => github()) - const starCount = createMemo(() => - githubData()?.stars - ? new Intl.NumberFormat("en-US", { - notation: "compact", - compactDisplay: "short", - maximumFractionDigits: 0, - }).format(githubData()?.stars) - : config.github.starsFormatted.compact, - ) const [store, setStore] = createStore({ mobileMenuOpen: false, @@ -155,12 +144,15 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo - {i18n.t("nav.github")} [{starCount()}] + {i18n.t("nav.github")} {i18n.t("nav.docs")} + + {i18n.t("nav.data")} + {i18n.t("nav.zen")} @@ -252,12 +244,15 @@ export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: bo - {i18n.t("nav.github")} [{starCount()}] + {i18n.t("nav.github")} {i18n.t("nav.docs")} + + {i18n.t("nav.data")} + {i18n.t("nav.zen")} diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index de5f53941b..d4655550d6 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -1,6 +1,7 @@ export const dict = { "nav.github": "GitHub", "nav.docs": "Docs", + "nav.data": "Data", "nav.changelog": "Changelog", "nav.discord": "Discord", "nav.x": "X", From 936363e7a8e91c1e7c17f54dcbd53c3c92883dbf Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 10 Jun 2026 18:00:25 +0000 Subject: [PATCH 028/157] sync release versions for v1.17.3 --- bun.lock | 52 ++++++++++----------- packages/app/package.json | 2 +- packages/cli/package.json | 2 +- packages/console/app/package.json | 2 +- packages/console/core/package.json | 2 +- packages/console/function/package.json | 2 +- packages/console/mail/package.json | 2 +- packages/console/support/package.json | 2 +- packages/core/package.json | 2 +- packages/desktop/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/enterprise/package.json | 2 +- packages/function/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 2 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/slack/package.json | 2 +- packages/stats/app/package.json | 2 +- packages/stats/core/package.json | 2 +- packages/stats/server/package.json | 2 +- packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/web/package.json | 2 +- sdks/vscode/package.json | 2 +- 28 files changed, 53 insertions(+), 53 deletions(-) diff --git a/bun.lock b/bun.lock index b85e481f99..fe3e8751a9 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -85,7 +85,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.17.2", + "version": "1.17.3", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -110,7 +110,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -146,7 +146,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -173,7 +173,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -195,7 +195,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -219,7 +219,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -239,7 +239,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.17.2", + "version": "1.17.3", "bin": { "opencode": "./bin/opencode", }, @@ -330,7 +330,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@zip.js/zip.js": "2.7.62", "effect": "catalog:", @@ -384,7 +384,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -398,7 +398,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "effect": "catalog:", }, @@ -410,7 +410,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -441,7 +441,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -457,7 +457,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74", @@ -476,7 +476,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -494,7 +494,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.17.2", + "version": "1.17.3", "bin": { "opencode": "./bin/opencode", }, @@ -622,7 +622,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", @@ -660,7 +660,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "cross-spawn": "catalog:", }, @@ -675,7 +675,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -689,7 +689,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -702,7 +702,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@ibm/plex": "6.4.1", "@opencode-ai/stats-core": "workspace:*", @@ -735,7 +735,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -754,7 +754,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -794,7 +794,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -822,7 +822,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/core": "workspace:*", @@ -871,7 +871,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", diff --git a/packages/app/package.json b/packages/app/package.json index d70d021b29..74327923d7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.17.2", + "version": "1.17.3", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index e6532b880f..3ca6eceb0e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "bin": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 96f80cdb70..81bfd0b249 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index f5d20ac10d..447d372575 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index d863daf251..effd8a57d5 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.17.2", + "version": "1.17.3", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 2bfa1a60dc..982be9dd93 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.17.2", + "version": "1.17.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 436ef4e32b..a6f3503a36 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index b06dd53a82..4d1885b3f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 97593d8848..f8b2f202b8 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index cd8ad23a9a..6b153d2969 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 5ffe2a0430..f35b35e8d3 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 9d792e16cc..cf6e29d4fe 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 7c5d66c3be..fd2c3d5460 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.17.2", + "version": "1.17.3", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 00d549396c..2b7397f4ad 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index afa820eb3b..1ec2286ae6 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 9cb6fd9fef..9c1468b167 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.17.2", + "version": "1.17.3", "name": "opencode", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index c62b30c24f..acd0c4fab9 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index c902d8d346..60a1ed8c42 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 5acd2d1875..1ef95c188d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index 6313f5e55c..c75492b433 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 3faa1f42c2..87ca5b0c0c 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index 42bc47fbb5..351d5d7f5b 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 60dc92effe..390862ff00 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/tui/package.json b/packages/tui/package.json index 868474e627..b28df1bafd 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.17.2", + "version": "1.17.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 1ebd8b68cd..06776cfa12 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.17.2", + "version": "1.17.3", "type": "module", "license": "MIT", "exports": { diff --git a/packages/web/package.json b/packages/web/package.json index b3d3277cc4..f2b6d4ba5c 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.17.2", + "version": "1.17.3", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 7c129c4011..f3fc130833 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.17.2", + "version": "1.17.3", "publisher": "sst-dev", "repository": { "type": "git", From bb82aab5c87a6722b080f21c98763488b717b5fc Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 15:30:56 -0400 Subject: [PATCH 029/157] update opencode.jsonc --- .opencode/opencode.jsonc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index ae68a477dc..b0f7d59447 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -2,9 +2,7 @@ "$schema": "https://opencode.ai/config.json", "provider": {}, "permission": {}, - // TODO: flip back to `references` once a release containing the v1 `reference` migration ships. - // The release pipeline runs the latest published opencode against this file, which only knows `reference`. - "reference": { + "references": { "effect": { "repository": "github.com/Effect-TS/effect-smol", "description": "Use for Effect v4 and effect-smol implementation details", From 722f4dd4164f7d0cd9f29047a44cf1077fdd42dd Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Wed, 10 Jun 2026 21:33:31 +0200 Subject: [PATCH 030/157] chore: pin gitlab-ai-provider to 6.9.0 (#31741) --- bun.lock | 6 +++--- packages/core/package.json | 2 +- packages/opencode/package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bun.lock b/bun.lock index fe3e8751a9..9c83598cec 100644 --- a/bun.lock +++ b/bun.lock @@ -288,7 +288,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.1", + "gitlab-ai-provider": "6.9.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -564,7 +564,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.1", + "gitlab-ai-provider": "6.9.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -3626,7 +3626,7 @@ "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.9.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-2qd8UCwS3qTGk0uVq+tMT+06cpMjTw8ShECCAnI3+pXynnxG3rEo4tRojl9ES281pA+WwijsO6gCHuiuleYBjg=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.9.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-CEdKuUfbKwDfYXsY+LwvxXFLdu6hl7cwTA1lMM8Hzvfq6rxUwzVS2YXCS9hs79H2fkZnu/vMZ3aFO/8Ll80GyQ=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], diff --git a/packages/core/package.json b/packages/core/package.json index 4d1885b3f4..cd608639d9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -104,7 +104,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.1", + "gitlab-ai-provider": "6.9.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 9c1468b167..c1c65dc92b 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -117,7 +117,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.9.1", + "gitlab-ai-provider": "6.9.0", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", From f43b0d3afd80f4bb3b13982a25e350525d9ab189 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:38:08 -0500 Subject: [PATCH 031/157] fix(mcp): apply timeouts to catalog requests (#31618) --- packages/opencode/src/mcp/catalog.ts | 138 +++++++++++++++++++ packages/opencode/src/mcp/index.ts | 196 +++++---------------------- 2 files changed, 170 insertions(+), 164 deletions(-) create mode 100644 packages/opencode/src/mcp/catalog.ts diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts new file mode 100644 index 0000000000..71979dfd6c --- /dev/null +++ b/packages/opencode/src/mcp/catalog.ts @@ -0,0 +1,138 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { + CallToolResultSchema, + ListToolsResultSchema, + ToolSchema, + type Tool as MCPToolDef, +} from "@modelcontextprotocol/sdk/types.js" +import { dynamicTool, jsonSchema, type JSONSchema7, type Tool } from "ai" +import { Effect } from "effect" + +const DEFAULT_TIMEOUT = 30_000 +const MAX_LIST_PAGES = 1_000 + +const TolerantListToolsResultSchema = ListToolsResultSchema.extend({ + tools: ToolSchema.omit({ outputSchema: true }).array(), +}) + +export async function paginate( + list: (cursor?: string) => Promise, + items: (result: R) => T[], +) { + const result: T[] = [] + const cursors = new Set() + let cursor: string | undefined + + for (let page = 0; page < MAX_LIST_PAGES; page++) { + const page = await list(cursor) + result.push(...items(page)) + if (page.nextCursor === undefined) return result + if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`) + cursors.add(page.nextCursor) + cursor = page.nextCursor + } + + throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`) +} + +export function defs(client: Client, timeout?: number) { + return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void)) +} + +export function convertTool(mcpTool: MCPToolDef, client: Client, timeout?: number): Tool { + const inputSchema: JSONSchema7 = { + ...(mcpTool.inputSchema as JSONSchema7), + type: "object", + properties: (mcpTool.inputSchema.properties ?? {}) as JSONSchema7["properties"], + additionalProperties: false, + } + + return dynamicTool({ + description: mcpTool.description ?? "", + inputSchema: jsonSchema(inputSchema), + execute: (args: unknown, options) => + client.callTool( + { + name: mcpTool.name, + arguments: (args || {}) as Record, + }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + signal: options.abortSignal, + timeout, + }, + ), + }) +} + +export function fetch( + clientName: string, + client: Client, + list: (client: Client) => Promise, + label: string, +) { + return Effect.tryPromise({ + try: () => list(client), + catch: (error) => error, + }).pipe( + Effect.tapError((error) => + Effect.logWarning(`failed to get ${label}`, { + clientName, + error: error instanceof Error ? error.message : String(error), + }), + ), + Effect.map((items) => { + const sanitizedClient = sanitize(clientName) + return Object.fromEntries( + items.map((item) => [sanitizedClient + ":" + sanitize(item.name), { ...item, client: clientName }]), + ) + }), + Effect.orElseSucceed(() => undefined), + ) +} + +export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") + +export function prompts(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) + return paginate( + (cursor) => client.listPrompts(cursor === undefined ? undefined : { cursor }, { timeout }), + (result) => result.prompts, + ) +} + +export function resources(client: Client, timeout?: number) { + if (!client.getServerCapabilities()?.resources) return Promise.resolve([]) + return paginate( + (cursor) => client.listResources(cursor === undefined ? undefined : { cursor }, { timeout }), + (result) => result.resources, + ) +} + +function listTools(client: Client, timeout: number) { + return Effect.tryPromise({ + try: () => + paginate( + async (cursor) => { + const params = cursor === undefined ? undefined : { cursor } + try { + return await client.listTools(params, { timeout }) + } catch (error) { + if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error + return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout }) + } + }, + (result) => result.tools, + ), + catch: (error) => (error instanceof Error ? error : new Error(String(error))), + }) +} + +function isOutputSchemaValidationError(error: Error) { + return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test( + error.message, + ) +} + +export * as McpCatalog from "./catalog" diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index d40594bace..0ff39589a2 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -1,5 +1,5 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" -import { dynamicTool, type Tool, jsonSchema, type JSONSchema7 } from "ai" +import { type Tool } from "ai" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { Client } from "@modelcontextprotocol/sdk/client/index.js" @@ -7,13 +7,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { - CallToolResultSchema, - ListToolsResultSchema, - ToolSchema, - type Tool as MCPToolDef, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js" +import { type Tool as MCPToolDef, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js" import { Config } from "@/config/config" import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" import { NamedError } from "@opencode-ai/core/util/error" @@ -32,13 +26,10 @@ import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { McpCatalog } from "./catalog" const DEFAULT_TIMEOUT = 30_000 -const TolerantListToolsResultSchema = ListToolsResultSchema.extend({ - tools: ToolSchema.omit({ outputSchema: true }).array(), -}) - export const Resource = Schema.Struct({ name: Schema.String, uri: Schema.String, @@ -112,122 +103,10 @@ function isMcpConfigured(entry: McpEntry): entry is ConfigMCPV1.Info { return typeof entry === "object" && entry !== null && "type" in entry } -const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_") -const MAX_LIST_PAGES = 1_000 - function remoteURL(value: string) { if (URL.canParse(value)) return new URL(value) } -function isOutputSchemaValidationError(error: Error) { - return /can't resolve reference|resolves to more than one schema|outputSchema|schema.*reference|reference.*schema/i.test( - error.message, - ) -} - -async function paginate( - list: (cursor?: string) => Promise, - items: (result: R) => T[], -) { - const result: T[] = [] - const cursors = new Set() - let cursor: string | undefined - - for (let page = 0; page < MAX_LIST_PAGES; page++) { - const page = await list(cursor) - result.push(...items(page)) - if (page.nextCursor === undefined) return result - if (cursors.has(page.nextCursor)) throw new Error(`MCP list returned duplicate cursor: ${page.nextCursor}`) - cursors.add(page.nextCursor) - cursor = page.nextCursor - } - - throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`) -} - -function listTools(client: MCPClient, timeout: number) { - return Effect.tryPromise({ - try: () => - paginate( - async (cursor) => { - const params = cursor === undefined ? undefined : { cursor } - try { - return await client.listTools(params, { timeout }) - } catch (error) { - if (!(error instanceof Error) || !isOutputSchemaValidationError(error)) throw error - return client.request({ method: "tools/list", params }, TolerantListToolsResultSchema, { timeout }) - } - }, - (result) => result.tools, - ), - catch: (err) => (err instanceof Error ? err : new Error(String(err))), - }) -} - -// Convert MCP tool definition to AI SDK Tool type -function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool { - const inputSchema = mcpTool.inputSchema - - // Spread first, then override type to ensure it's always "object" - const schema: JSONSchema7 = { - ...(inputSchema as JSONSchema7), - type: "object", - properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"], - additionalProperties: false, - } - - return dynamicTool({ - description: mcpTool.description ?? "", - inputSchema: jsonSchema(schema), - execute: async (args: unknown, options) => { - return client.callTool( - { - name: mcpTool.name, - arguments: (args || {}) as Record, - }, - CallToolResultSchema, - { - resetTimeoutOnProgress: true, - signal: options.abortSignal, - timeout, - }, - ) - }, - }) -} - -function defs(client: MCPClient, timeout?: number) { - return listTools(client, timeout ?? DEFAULT_TIMEOUT).pipe(Effect.catch(() => Effect.void)) -} - -function fetchFromClient( - clientName: string, - client: Client, - listFn: (c: Client) => Promise, - label: string, -) { - return Effect.tryPromise({ - try: () => listFn(client), - catch: (error) => error, - }).pipe( - Effect.tapError((error) => - Effect.logWarning(`failed to get ${label}`, { - clientName, - error: error instanceof Error ? error.message : String(error), - }), - ), - Effect.map((items) => { - const out: Record = {} - const sanitizedClient = sanitize(clientName) - for (const item of items) { - out[sanitizedClient + ":" + sanitize(item.name)] = { ...item, client: clientName } - } - return out - }), - Effect.orElseSucceed(() => undefined), - ) -} - interface CreateResult { mcpClient?: MCPClient status: Status @@ -465,7 +344,7 @@ export const layer = Layer.effect( } return yield* Effect.gen(function* () { - const listed = mcpClient.getServerCapabilities()?.tools ? yield* defs(mcpClient, mcp.timeout) : [] + const listed = mcpClient.getServerCapabilities()?.tools ? yield* McpCatalog.defs(mcpClient, mcp.timeout) : [] if (!listed) { return yield* Effect.fail(new Error("Failed to get tools")) } @@ -516,7 +395,7 @@ export const layer = Layer.effect( client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - const listed = await bridge.promise(defs(client, timeout)) + const listed = await bridge.promise(McpCatalog.defs(client, timeout)) if (!listed) return if (s.clients[name] !== client || s.status[name]?.status !== "connected") return @@ -670,6 +549,11 @@ export const layer = Layer.effect( s.status[name] = { status: "disabled" } }) + function requestTimeout(s: State, name: string, configured: McpEntry | undefined, fallback?: number) { + const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined + return s.config[name]?.timeout ?? staticTimeout ?? fallback + } + const tools = Effect.fn("MCP.tools")(function* () { const result: Record = {} const s = yield* InstanceState.get(state) @@ -681,15 +565,15 @@ export const layer = Layer.effect( for (const [clientName, client] of Object.entries(s.clients)) { if (s.status[clientName]?.status !== "connected") continue const mcpConfig = config[clientName] - const entry = mcpConfig && isMcpConfigured(mcpConfig) ? mcpConfig : s.config[clientName] const listed = s.defs[clientName] if (!listed) { yield* Effect.logWarning("missing cached tools for connected server", { clientName }) continue } - const timeout = entry?.timeout ?? defaultTimeout + const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout) for (const mcpTool of listed) { - result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(mcpTool, client, timeout) + const key = McpCatalog.sanitize(clientName) + "_" + McpCatalog.sanitize(mcpTool.name) + result[key] = McpCatalog.convertTool(mcpTool, client, timeout) } } return result @@ -697,45 +581,31 @@ export const layer = Layer.effect( function collectFromConnected( s: State, - listFn: (c: Client) => Promise, + listFn: (c: Client, timeout?: number) => Promise, label: string, ) { - return Effect.forEach( - Object.entries(s.clients).filter(([name]) => s.status[name]?.status === "connected"), - ([clientName, client]) => - fetchFromClient(clientName, client, listFn, label).pipe(Effect.map((items) => Object.entries(items ?? {}))), - { concurrency: "unbounded" }, - ).pipe(Effect.map((results) => Object.fromEntries(results.flat()))) + return Effect.gen(function* () { + const cfg = yield* cfgSvc.get() + return yield* Effect.forEach( + Object.entries(s.clients).filter(([name]) => s.status[name]?.status === "connected"), + ([clientName, client]) => + McpCatalog.fetch( + clientName, + client, + (c) => listFn(c, requestTimeout(s, clientName, cfg.mcp?.[clientName], cfg.experimental?.mcp_timeout)), + label, + ).pipe(Effect.map((items) => Object.entries(items ?? {}))), + { concurrency: "unbounded" }, + ).pipe(Effect.map((results) => Object.fromEntries(results.flat()))) + }) } const prompts = Effect.fn("MCP.prompts")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected( - s, - (c) => - c.getServerCapabilities()?.prompts - ? paginate( - (cursor) => c.listPrompts(cursor === undefined ? undefined : { cursor }), - (result) => result.prompts, - ) - : Promise.resolve([]), - "prompts", - ) + return yield* collectFromConnected(yield* InstanceState.get(state), McpCatalog.prompts, "prompts") }) const resources = Effect.fn("MCP.resources")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected( - s, - (c) => - c.getServerCapabilities()?.resources - ? paginate( - (cursor) => c.listResources(cursor === undefined ? undefined : { cursor }), - (result) => result.resources, - ) - : Promise.resolve([]), - "resources", - ) + return yield* collectFromConnected(yield* InstanceState.get(state), McpCatalog.resources, "resources") }) const withClient = Effect.fnUntraced(function* ( @@ -751,10 +621,8 @@ export const layer = Layer.effect( return undefined } const cfg = yield* cfgSvc.get() - const configured = cfg.mcp?.[clientName] - const staticTimeout = configured && isMcpConfigured(configured) ? configured.timeout : undefined return yield* Effect.tryPromise({ - try: () => fn(client, s.config[clientName]?.timeout ?? staticTimeout ?? cfg.experimental?.mcp_timeout), + try: () => fn(client, requestTimeout(s, clientName, cfg.mcp?.[clientName], cfg.experimental?.mcp_timeout)), catch: (error) => error, }).pipe( Effect.tapError((error) => @@ -877,7 +745,7 @@ export const layer = Layer.effect( const listed = client ? client.getServerCapabilities()?.tools - ? yield* defs(client, mcpConfig.timeout) + ? yield* McpCatalog.defs(client, mcpConfig.timeout) : [] : undefined if (!client || !listed) { From c51a1588c7c5829323f7158f8005881328d8c610 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Wed, 10 Jun 2026 21:47:05 +0200 Subject: [PATCH 032/157] tui: fix session list search filtering (#31748) Fix #31182 Close #31211 --- bun.lock | 3 --- packages/tui/package.json | 1 - packages/tui/src/util/signal.ts | 16 +++++++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 9c83598cec..8e80a71807 100644 --- a/bun.lock +++ b/bun.lock @@ -803,7 +803,6 @@ "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", - "@solid-primitives/scheduled": "1.5.2", "clipboardy": "4.0.0", "diff": "catalog:", "effect": "catalog:", @@ -5794,8 +5793,6 @@ "@opencode-ai/llm/@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@opencode-ai/tui/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], - "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="], diff --git a/packages/tui/package.json b/packages/tui/package.json index b28df1bafd..03b0fb5886 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -54,7 +54,6 @@ "@opentui/core": "catalog:", "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", - "@solid-primitives/scheduled": "1.5.2", "clipboardy": "4.0.0", "diff": "catalog:", "effect": "catalog:", diff --git a/packages/tui/src/util/signal.ts b/packages/tui/src/util/signal.ts index 7d20ae04ba..e28c680cd4 100644 --- a/packages/tui/src/util/signal.ts +++ b/packages/tui/src/util/signal.ts @@ -1,9 +1,19 @@ import { createEffect, createSignal, on, onCleanup, type Accessor } from "solid-js" -import { debounce, type Scheduled } from "@solid-primitives/scheduled" -export function createDebouncedSignal(value: T, ms: number): [Accessor, Scheduled<[value: T]>] { +export function createDebouncedSignal(value: T, ms: number): [Accessor, (value: T) => void] { const [get, set] = createSignal(value) - return [get, debounce((v: T) => set(() => v), ms)] + let timer: ReturnType | undefined + const debounced = (next: T) => { + if (timer) clearTimeout(timer) + timer = setTimeout(() => { + timer = undefined + set(() => next) + }, ms) + } + onCleanup(() => { + if (timer) clearTimeout(timer) + }) + return [get, debounced] } export function createFadeIn(show: Accessor, enabled: Accessor) { From 07b983e82fcb808101593a5268c10e6e38ed9976 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:59:36 -0500 Subject: [PATCH 033/157] feat(mcp): support server log notifications (#31752) --- packages/opencode/src/mcp/index.ts | 29 +++++++++++++++++++- packages/opencode/test/mcp/lifecycle.test.ts | 3 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 0ff39589a2..5a73322510 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -7,7 +7,12 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" -import { type Tool as MCPToolDef, ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js" +import { + type LoggingMessageNotification, + LoggingMessageNotificationSchema, + type Tool as MCPToolDef, + ToolListChangedNotificationSchema, +} from "@modelcontextprotocol/sdk/types.js" import { Config } from "@/config/config" import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp" import { NamedError } from "@opencode-ai/core/util/error" @@ -391,6 +396,10 @@ export const layer = Layer.effect( ) function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) { + client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => + bridge.promise(serverLog(name, notification.params)), + ) + if (!client.getServerCapabilities()?.tools) return client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { if (s.clients[name] !== client || s.status[name]?.status !== "connected") return @@ -404,6 +413,24 @@ export const layer = Layer.effect( }) } + function serverLog(name: string, params: LoggingMessageNotification["params"]) { + const fields = { server: name, logger: params.logger, level: params.level, data: params.data } + switch (params.level) { + case "debug": + return Effect.logDebug("MCP server log", fields) + case "info": + case "notice": + return Effect.logInfo("MCP server log", fields) + case "warning": + return Effect.logWarning("MCP server log", fields) + case "error": + case "critical": + case "alert": + case "emergency": + return Effect.logError("MCP server log", fields) + } + } + const state = yield* InstanceState.make( Effect.fn("MCP.state")(function* () { const cfg = yield* cfgSvc.get() diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 004909fb6e..0aef477d14 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -1,4 +1,5 @@ import { expect, mock, beforeEach } from "bun:test" +import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js" import { Cause, Effect, Exit } from "effect" import type { MCP as MCPNS } from "../../src/mcp/index" import { testEffect } from "../lib/effect" @@ -394,7 +395,7 @@ it.instance( { name: "next_tool", description: "next", inputSchema: { type: "object", properties: {} } }, ] - const handler = Array.from(serverState.notificationHandlers.values())[0] + const handler = serverState.notificationHandlers.get(ToolListChangedNotificationSchema) expect(handler).toBeDefined() yield* Effect.promise(() => handler?.()) From 6e2bcafd34174297ffdfaf0450861a3f536cf62c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 10 Jun 2026 20:09:04 +0000 Subject: [PATCH 034/157] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 3c65d0dc04..2993af93e8 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-bQFK3yO9Y5m2zIlZ16wmJ05WOLNuvYvHMDbV+lEHzkg=", - "aarch64-linux": "sha256-sP/DzyfDnyeGOAo0cx50Jas3X4RGuLxfAdoTrv355c0=", - "aarch64-darwin": "sha256-88zVvcQyKok6qoi70FtLdYkUhyNyD1hrcDTUDEuOtiM=", - "x86_64-darwin": "sha256-CUXFpNm274+ekBTZzD/hkocNFAexOBTMzIZFyGBh8MA=" + "x86_64-linux": "sha256-hDa35N5BYvO+x1S02AvYGXwDv8LM4qlt+7A8tZ/3hug=", + "aarch64-linux": "sha256-/99PwCH2ofV8plr5kbND+r7R5jJnRJ1O4YvRAlM8HNM=", + "aarch64-darwin": "sha256-jeQATO8cGjUpLivgnz95kKHdRQM8pfmD+VgMoW8IDM0=", + "x86_64-darwin": "sha256-S+SvtiXIEn39oEoegQLFohtYutp5qtengBVAHmcVmSI=" } } From eb70b6137b1a9a02ccf9e53c7e20c7a7e714f478 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 10 Jun 2026 17:21:40 -0400 Subject: [PATCH 035/157] test(opencode): simplify test registry layer wiring (#31761) --- packages/core/src/effect/layer-node.ts | 9 ++- .../test/effect/app-graph-types.test.ts | 14 +++-- .../opencode/test/effect/app-graph.test.ts | 25 ++++---- packages/opencode/test/tool/registry.test.ts | 63 ++++--------------- 4 files changed, 40 insertions(+), 71 deletions(-) diff --git a/packages/core/src/effect/layer-node.ts b/packages/core/src/effect/layer-node.ts index e287333607..c6ee6b2369 100644 --- a/packages/core/src/effect/layer-node.ts +++ b/packages/core/src/effect/layer-node.ts @@ -44,13 +44,20 @@ type CheckReplacementErrors = [Exclude } -export function replace( +export function replaceWithNode( source: Node, replacement: Node, E2> & CheckReplacementErrors>, ): Replacement { return { source, replacement } } +export function replace( + source: Node, + replacement: Layer.Layer, E2, never> & CheckReplacementErrors>, +): Replacement { + return { source, replacement: make(replacement as Layer.Layer, []) } +} + export function buildLayer(node: Node, options?: { readonly replacements?: readonly Replacement[] }) { const replacements = new Map(options?.replacements?.map((item) => [item.source, item.replacement])) const cache = new Map() diff --git a/packages/opencode/test/effect/app-graph-types.test.ts b/packages/opencode/test/effect/app-graph-types.test.ts index ecfe6af333..527c4daf54 100644 --- a/packages/opencode/test/effect/app-graph-types.test.ts +++ b/packages/opencode/test/effect/app-graph-types.test.ts @@ -91,14 +91,18 @@ void (0 as unknown as ClosedRequires) void (0 as unknown as ClosedError) const replacement = LayerNode.make(Layer.succeed(A, A.of({ value: "a" })), []) -LayerNode.replace(a, replacement) -LayerNode.replace(notFoundOrDiskA, notFoundA) -LayerNode.replace(notFoundOrDiskA, diskA) +LayerNode.replace(a, Layer.succeed(A, A.of({ value: "a" }))) +LayerNode.replace(notFoundOrDiskA, notFoundAImplementation) +LayerNode.replace(notFoundOrDiskA, diskAImplementation) +LayerNode.replaceWithNode(a, replacement) // @ts-expect-error An override for A must still provide A -LayerNode.replace(a, b) +LayerNode.replaceWithNode(a, b) // @ts-expect-error A replacement cannot introduce NetworkError -LayerNode.replace(notFoundOrDiskA, networkA) +LayerNode.replace(notFoundOrDiskA, networkAImplementation) + +// @ts-expect-error A replacement layer must not have unresolved dependencies +LayerNode.replace(b, bImplementation) test("type exploration compiles", () => {}) diff --git a/packages/opencode/test/effect/app-graph.test.ts b/packages/opencode/test/effect/app-graph.test.ts index d89d2680b5..7ae7a982ba 100644 --- a/packages/opencode/test/effect/app-graph.test.ts +++ b/packages/opencode/test/effect/app-graph.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { Cause, Context, Effect, Exit, Layer } from "effect" import { LayerNode } from "@opencode-ai/core/effect/layer-node" -const { buildLayer: build, group, replace } = LayerNode +const { buildLayer: build, group, replace, replaceWithNode } = LayerNode const node = LayerNode.make class Value extends Context.Service()("test/Value") {} @@ -30,7 +30,7 @@ describe("app graph", () => { }) test("applies overrides before dependency materialization", async () => { - const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), []) + const replacement = Layer.succeed(Value, Value.of({ value: "simulation" })) const graph = build(greeting, { replacements: [replace(value, replacement)] }) const result = Effect.gen(function* () { return (yield* Greeting).text @@ -102,7 +102,7 @@ describe("app graph", () => { ), [value], ) - const replacement = node(Layer.succeed(Value, Value.of({ value: "simulation" })), []) + const replacement = Layer.succeed(Value, Value.of({ value: "simulation" })) const graph = build(group([left, right]), { replacements: [replace(value, replacement)] }) const result = Effect.gen(function* () { @@ -153,7 +153,7 @@ describe("app graph", () => { ) const result = Effect.gen(function* () { return (yield* Greeting).text - }).pipe(Effect.provide(build(greeting, { replacements: [replace(value, replacement)] }))) + }).pipe(Effect.provide(build(greeting, { replacements: [replaceWithNode(value, replacement)] }))) expect(await Effect.runPromise(result)).toBe("hello replacement") }) @@ -161,15 +161,12 @@ describe("app graph", () => { test("does not acquire unreachable replacements", async () => { let acquisitions = 0 const unreachable = node(Layer.succeed(Value, Value.of({ value: "unreachable" })), []) - const replacement = node( - Layer.effect( - Value, - Effect.sync(() => { - acquisitions++ - return Value.of({ value: "replacement" }) - }), - ), - [], + const replacement = Layer.effect( + Value, + Effect.sync(() => { + acquisitions++ + return Value.of({ value: "replacement" }) + }), ) await Effect.runPromise( @@ -200,7 +197,7 @@ describe("app graph", () => { const consumer = node(greetingImplementation, [value]) ;(replacement.dependencies as LayerNode.Node[]).push(consumer) - expect(() => build(consumer, { replacements: [replace(value, replacement)] })).toThrow( + expect(() => build(consumer, { replacements: [replaceWithNode(value, replacement)] })).toThrow( "Cycle detected in app graph: layer#1 -> layer#2 -> layer#1", ) }) diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index 34f11c5a70..27447616fb 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -3,31 +3,15 @@ import path from "path" import fs from "fs/promises" import { fileURLToPath, pathToFileURL } from "url" import { Effect, Layer, Result, Schema } from "effect" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" -import { Database } from "@opencode-ai/core/database/database" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { ToolRegistry } from "@/tool/registry" import { Tool } from "@/tool/tool" import { disposeAllInstances, TestInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { TestConfig } from "../fixture/config" -import { FSUtil } from "@opencode-ai/core/fs-util" +import { Config } from "@/config/config" import { Plugin } from "@/plugin" -import { Question } from "@/question" -import { Todo } from "@/session/todo" -import { Skill } from "@/skill" import { Agent } from "@/agent/agent" -import { BackgroundJob } from "@/background/job" -import { Session } from "@/session/session" -import { SessionStatus } from "@/session/status" -import { Provider } from "@/provider/provider" -import { Git } from "@/git" -import { LSP } from "@/lsp/lsp" -import { Instruction } from "@/session/instruction" -import { EventV2Bridge } from "@/event-v2-bridge" -import { FetchHttpClient } from "effect/unstable/http" -import { Format } from "@/format" -import { Ripgrep } from "@opencode-ai/core/ripgrep" -import * as Truncate from "@/tool/truncate" import { InstanceState } from "@/effect/instance-state" import { ToolJsonSchema } from "@/tool/json-schema" @@ -36,41 +20,10 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" -const node = CrossSpawnSpawner.defaultLayer const configLayer = TestConfig.layer({ directories: () => InstanceState.directory.pipe(Effect.map((dir) => [path.join(dir, ".opencode")])), }) -type RegistryLayerOptions = { - flags?: Partial - plugin?: Layer.Layer -} - -const registryLayer = (opts: RegistryLayerOptions = {}) => - ToolRegistry.layer - .pipe( - Layer.provide(configLayer), - Layer.provide(opts.plugin ?? Plugin.defaultLayer), - Layer.provide(Question.defaultLayer), - Layer.provide(Todo.defaultLayer), - Layer.provide(Skill.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(Session.defaultLayer), - Layer.provide(Layer.mergeAll(SessionStatus.defaultLayer, BackgroundJob.defaultLayer)), - Layer.provide(Provider.defaultLayer), - Layer.provide(Git.defaultLayer), - Layer.provide(LSP.defaultLayer), - Layer.provide(Instruction.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(FetchHttpClient.layer), - Layer.provide(Format.defaultLayer), - Layer.provide(Layer.mergeAll(node, Database.defaultLayer)), - Layer.provide(Ripgrep.defaultLayer), - Layer.provide(Truncate.defaultLayer), - ) - .pipe(Layer.provide(RuntimeFlags.layer(opts.flags ?? {}))) - // Fake Plugin.Service that returns a single plugin whose `tool` map contains // one definition with `args: undefined`. Used to exercise the plugin entry // point of `fromPlugin` for the #27451 / #27630 regression. @@ -95,9 +48,17 @@ const brokenPluginLayer = Layer.succeed( }), ) -const it = testEffect(Layer.mergeAll(registryLayer(), node, Agent.defaultLayer)) +const root = LayerNode.group([ToolRegistry.node, Agent.node]) +const replacements = [ + LayerNode.replace(Config.node, configLayer), + LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer()), +] + +const it = testEffect(LayerNode.buildLayer(root, { replacements })) const withBrokenPlugin = testEffect( - Layer.mergeAll(registryLayer({ plugin: brokenPluginLayer }), node, Agent.defaultLayer), + LayerNode.buildLayer(root, { + replacements: [...replacements, LayerNode.replace(Plugin.node, brokenPluginLayer)], + }), ) afterEach(async () => { From cc226464676843a60b0d418b20e5519ecb36e165 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Wed, 10 Jun 2026 19:09:56 -0400 Subject: [PATCH 036/157] docs: add branch naming guidance --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c839668586..02b1c4cb77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,12 @@ - The default branch in this repo is `dev`. - Local `main` ref may not exist; use `dev` or `origin/dev` for diffs. +## Branch Names + +Use a short branch name of at most three words, separated by hyphens. Do not use slashes or type prefixes such as `feat/` or `fix/`. + +Examples: `session-recovery`, `fix-scroll-state`, `regenerate-sdk`. + ## Commits and PR Titles Use conventional commit-style messages and PR titles: `type(scope): summary`. From 51891d56e748779a95b19d056328fecdd1e1397e Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Wed, 10 Jun 2026 17:25:55 -0700 Subject: [PATCH 037/157] fix(snapshot): reuse source git objects to avoid re-hashing huge repos (#31798) --- packages/opencode/src/snapshot/index.ts | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index a9b3411cc5..fd25437bb0 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -191,6 +191,46 @@ export const layer: Layer.Layer line.trim()) + .filter(Boolean) + const alternates: string[] = [] + for (const candidate of [sourceObjects, ...chained]) { + if (yield* exists(candidate)) alternates.push(candidate) + } + if (!alternates.length) return + + yield* fs.ensureDir(path.join(state.gitdir, "objects", "info")).pipe(Effect.orDie) + yield* fs + .writeFileString(path.join(state.gitdir, "objects", "info", "alternates"), alternates.join("\n") + "\n") + .pipe(Effect.orDie) + + // Seed the index from the source repo so already-hashed entries are reused. + // Best-effort: a missing/incompatible index just falls back to a full add. + const sourceIndex = path.join(source, "index") + if (yield* exists(sourceIndex)) { + yield* fs.copyFile(sourceIndex, path.join(state.gitdir, "index")).pipe(Effect.catch(() => Effect.void)) + } + }) + const add = Effect.fnUntraced(function* () { yield* sync() const [diff, other] = yield* Effect.all( @@ -288,6 +328,12 @@ export const layer: Layer.Layer Date: Wed, 10 Jun 2026 22:19:06 -0400 Subject: [PATCH 038/157] test(opencode): simplify share layer wiring (#31811) --- .../opencode/test/share/share-next.test.ts | 171 ++++++++---------- 1 file changed, 78 insertions(+), 93 deletions(-) diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 168243abb5..454e152505 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -1,15 +1,14 @@ -import { NodeFileSystem } from "@effect/platform-node" import { beforeEach, describe, expect } from "bun:test" import { Effect, Exit, Layer, Option } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { httpClient } from "@opencode-ai/core/effect/layer-node-platform" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" -import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" -import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { EventV2Bridge } from "../../src/event-v2-bridge" -import { Config } from "@/config/config" -import { Provider } from "@/provider/provider" import { Session } from "@/session/session" import type { SessionID } from "../../src/session/schema" import { ShareNext } from "@/share/share-next" @@ -20,13 +19,7 @@ import { provideTmpdirInstance } from "../fixture/fixture" import { resetDatabase } from "../fixture/db" import { pollWithTimeout, testEffect } from "../lib/effect" -const env = Layer.mergeAll( - Session.defaultLayer, - AccountRepo.defaultLayer, - Database.defaultLayer, - NodeFileSystem.layer, - CrossSpawnSpawner.defaultLayer, -) +const env = LayerNode.buildLayer(CrossSpawnSpawner.node) const it = testEffect(env) const json = (req: Parameters[0], body: unknown, status = 200) => @@ -40,35 +33,28 @@ const json = (req: Parameters[0], body: unkno const none = HttpClient.make(() => Effect.die("unexpected http call")) -function live(client: HttpClient.HttpClient) { - const http = Layer.succeed(HttpClient.HttpClient, client) - return ShareNext.layer.pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(Account.layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(http))), - Layer.provide(Config.defaultLayer), - Layer.provide(Database.defaultLayer), - Layer.provide(http), - Layer.provide(Provider.defaultLayer), - Layer.provide(Session.defaultLayer), +function requestLayer(client: HttpClient.HttpClient) { + return LayerNode.buildLayer( + LayerNode.group([ShareNext.node, AccountRepo.node]), + { + replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], + }, ) } -function wired(client: HttpClient.HttpClient) { - const http = Layer.succeed(HttpClient.HttpClient, client) - return Layer.mergeAll( - EventV2Bridge.defaultLayer, - ShareNext.layer, - Session.defaultLayer, - AccountRepo.defaultLayer, - Database.defaultLayer, - NodeFileSystem.layer, - CrossSpawnSpawner.defaultLayer, - ).pipe( - Layer.provide(EventV2Bridge.defaultLayer), - Layer.provide(Account.layer.pipe(Layer.provide(AccountRepo.defaultLayer), Layer.provide(http))), - Layer.provide(Config.defaultLayer), - Layer.provide(http), - Layer.provide(Provider.defaultLayer), +function integrationLayer(client: HttpClient.HttpClient) { + return LayerNode.buildLayer( + LayerNode.group([ + ShareNext.node, + EventV2Bridge.node, + Session.node, + SessionProjector.node, + AccountRepo.node, + Database.node, + ]), + { + replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], + }, ) } @@ -115,7 +101,7 @@ describe("ShareNext", () => { expect(req.baseUrl).toBe("https://legacy-share.example.com") expect(req.headers).toEqual({}) }), - ).pipe(Effect.provide(live(none))), + ).pipe(Effect.provide(requestLayer(none))), { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) @@ -130,7 +116,7 @@ describe("ShareNext", () => { expect(req.api.create).toBe("/api/share") expect(req.headers).toEqual({}) }), - ).pipe(Effect.provide(live(none))), + ).pipe(Effect.provide(requestLayer(none))), ), ) @@ -139,7 +125,7 @@ describe("ShareNext", () => { Effect.gen(function* () { yield* seed("https://control.example.com", "org-1") - const req = yield* ShareNext.use.request().pipe(Effect.provide(live(none))) + const req = yield* ShareNext.use.request() expect(req.api.create).toBe("/api/shares") expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync") @@ -150,31 +136,31 @@ describe("ShareNext", () => { authorization: "Bearer st_test_token", "x-org-id": "org-1", }) - }), + }).pipe(Effect.provide(requestLayer(none))), ), ) it.live("create posts share, persists it, and returns the result", () => provideTmpdirInstance( - () => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const seen: HttpClientRequest.HttpClientRequest[] = [] - const client = HttpClient.make((req) => { - seen.push(req) - if (req.url.endsWith("/api/share")) { - return Effect.succeed( - json(req, { - id: "shr_abc", - url: "https://legacy-share.example.com/share/abc", - secret: "sec_123", - }), - ) - } - return Effect.succeed(json(req, { ok: true })) - }) + () => { + const seen: HttpClientRequest.HttpClientRequest[] = [] + const client = HttpClient.make((req) => { + seen.push(req) + if (req.url.endsWith("/api/share")) { + return Effect.succeed( + json(req, { + id: "shr_abc", + url: "https://legacy-share.example.com/share/abc", + secret: "sec_123", + }), + ) + } + return Effect.succeed(json(req, { ok: true })) + }) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) - const result = yield* ShareNext.use.create(session.id).pipe(Effect.provide(live(client))) + const result = yield* (yield* ShareNext.Service).create(session.id) expect(result.id).toBe("shr_abc") expect(result.url).toBe("https://legacy-share.example.com/share/abc") @@ -188,60 +174,59 @@ describe("ShareNext", () => { expect(seen).toHaveLength(1) expect(seen[0].method).toBe("POST") expect(seen[0].url).toBe("https://legacy-share.example.com/api/share") - }), + }).pipe(Effect.provide(integrationLayer(client))) + }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) it.live("remove deletes the persisted share and calls the delete endpoint", () => provideTmpdirInstance( - () => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const seen: HttpClientRequest.HttpClientRequest[] = [] - const client = HttpClient.make((req) => { - seen.push(req) - if (req.method === "POST") { - return Effect.succeed( - json(req, { - id: "shr_abc", - url: "https://legacy-share.example.com/share/abc", - secret: "sec_123", - }), - ) - } - return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 }))) - }) + () => { + const seen: HttpClientRequest.HttpClientRequest[] = [] + const client = HttpClient.make((req) => { + seen.push(req) + if (req.method === "POST") { + return Effect.succeed( + json(req, { + id: "shr_abc", + url: "https://legacy-share.example.com/share/abc", + secret: "sec_123", + }), + ) + } + return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 }))) + }) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) + const service = yield* ShareNext.Service - yield* Effect.gen(function* () { - yield* ShareNext.use.create(session.id) - yield* ShareNext.use.remove(session.id) - }).pipe(Effect.provide(live(client))) + yield* service.create(session.id) + yield* service.remove(session.id) expect(yield* share(session.id)).toBeUndefined() expect(seen.map((req) => [req.method, req.url])).toEqual([ ["POST", "https://legacy-share.example.com/api/share"], ["DELETE", "https://legacy-share.example.com/api/share/shr_abc"], ]) - }), + }).pipe(Effect.provide(integrationLayer(client))) + }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), ) it.live("create fails on a non-ok response and does not persist a share", () => - provideTmpdirInstance(() => - Effect.gen(function* () { - const session = yield* Session.use.create({ title: "test" }) - const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) + provideTmpdirInstance(() => { + const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500))) + return Effect.gen(function* () { + const session = yield* (yield* Session.Service).create({ title: "test" }) - const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))).pipe( - Effect.provide(live(client)), - ) + const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id))) expect(Exit.isFailure(exit)).toBe(true) expect(yield* share(session.id)).toBeUndefined() - }), - ), + }).pipe(Effect.provide(integrationLayer(client))) + }), ) it.live("ShareNext coalesces rapid diff events into one delayed sync with latest data", () => @@ -336,7 +321,7 @@ describe("ShareNext", () => { status: "modified", }, ]) - }).pipe(Effect.provide(wired(client))) + }).pipe(Effect.provide(integrationLayer(client))) }, { config: { enterprise: { url: "https://legacy-share.example.com" } } }, ), From ff967e582ca471dd1a8a3d71378207c47c6923e0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 11 Jun 2026 02:20:38 +0000 Subject: [PATCH 039/157] chore: generate --- packages/opencode/test/share/share-next.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 454e152505..dc0742cf60 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -34,12 +34,9 @@ const json = (req: Parameters[0], body: unkno const none = HttpClient.make(() => Effect.die("unexpected http call")) function requestLayer(client: HttpClient.HttpClient) { - return LayerNode.buildLayer( - LayerNode.group([ShareNext.node, AccountRepo.node]), - { - replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], - }, - ) + return LayerNode.buildLayer(LayerNode.group([ShareNext.node, AccountRepo.node]), { + replacements: [LayerNode.replace(httpClient, Layer.succeed(HttpClient.HttpClient, client))], + }) } function integrationLayer(client: HttpClient.HttpClient) { From 8bf0675997675b1d2af05b07614fc33e47517dd7 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 10 Jun 2026 22:55:01 -0400 Subject: [PATCH 040/157] feat(server): add v2 session API endpoints (#31822) --- .../test/server/httpapi-exercise/index.ts | 25 +++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 123 ++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 160 +++++++++++++++++- packages/server/src/api.ts | 2 + packages/server/src/groups/location.ts | 19 ++- packages/server/src/groups/question.ts | 15 ++ packages/server/src/groups/session.ts | 36 ++++ packages/server/src/handlers.ts | 2 + packages/server/src/handlers/location.ts | 18 ++ packages/server/src/handlers/question.ts | 7 + packages/server/src/handlers/session.ts | 31 ++++ 11 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 packages/server/src/handlers/location.ts diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index d4a8b5f4e7..3b97c50834 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -646,6 +646,7 @@ const scenarios: Scenario[] = [ object(body) check(body.healthy === true, "v2 server should report healthy") }), + http.protected.get("/api/location", "v2.location.get").json(200, object), http.protected.get("/api/agent", "v2.agent.list").json(200, locationData(array)), http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)), http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)), @@ -698,6 +699,14 @@ const scenarios: Scenario[] = [ headers: ctx.headers(), })) .json(200, data(array)), + http.protected + .get("/api/session/{sessionID}/question", "v2.session.question.list") + .seeded((ctx) => ctx.session({ title: "Question list owner" })) + .at((ctx) => ({ + path: route("/api/session/{sessionID}/question", { sessionID: ctx.state.id }), + headers: ctx.headers(), + })) + .json(200, data(array)), http.protected .post("/api/session/{sessionID}/permission/{requestID}/reply", "v2.session.permission.reply") .seeded((ctx) => ctx.session({ title: "Permission owner" })) @@ -807,6 +816,22 @@ const scenarios: Scenario[] = [ headers: ctx.headers(), })) .status(400, undefined, "none"), + http.protected + .post("/api/session", "v2.session.create") + .at((ctx) => ({ + path: "/api/session", + headers: { ...ctx.headers(), "content-type": "application/json" }, + body: {}, + })) + .json(200, data(object)), + http.protected + .get("/api/session/{sessionID}", "v2.session.get") + .seeded((ctx) => ctx.session({ title: "Session get" })) + .at((ctx) => ({ + path: route("/api/session/{sessionID}", { sessionID: ctx.state.id }), + headers: ctx.headers(), + })) + .json(200, data(object)), http.protected .get("/api/session/{sessionID}/context", "v2.session.context") .at((ctx) => ({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index c16ab6dd1a..9e70c4b44a 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -92,6 +92,7 @@ import type { GlobalUpgradeResponses, InstanceDisposeErrors, InstanceDisposeResponses, + LocationRef, LspStatusErrors, LspStatusResponses, McpAddErrors, @@ -274,6 +275,8 @@ import type { V2FsReadResponses, V2HealthGetErrors, V2HealthGetResponses, + V2LocationGetErrors, + V2LocationGetResponses, V2ModelListErrors, V2ModelListResponses, V2PermissionRequestListErrors, @@ -294,6 +297,10 @@ import type { V2SessionCompactResponses, V2SessionContextErrors, V2SessionContextResponses, + V2SessionCreateErrors, + V2SessionCreateResponses, + V2SessionGetErrors, + V2SessionGetResponses, V2SessionListErrors, V2SessionListResponses, V2SessionMessagesErrors, @@ -304,6 +311,8 @@ import type { V2SessionPermissionReplyResponses, V2SessionPromptErrors, V2SessionPromptResponses, + V2SessionQuestionListErrors, + V2SessionQuestionListResponses, V2SessionQuestionRejectErrors, V2SessionQuestionRejectResponses, V2SessionQuestionReplyErrors, @@ -5013,6 +5022,30 @@ export class Health extends HeyApiClient { } } +export class Location extends HeyApiClient { + /** + * Get location + * + * Resolve the requested location or the server default location. + */ + public get( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/location", + ...options, + ...params, + }) + } +} + export class Agent extends HeyApiClient { /** * List agents @@ -5106,6 +5139,29 @@ export class Permission2 extends HeyApiClient { } export class Question2 extends HeyApiClient { + /** + * List session question requests + * + * Retrieve pending question requests owned by a session. + */ + public list( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get< + V2SessionQuestionListResponses, + V2SessionQuestionListErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/question", + ...options, + ...params, + }) + } + /** * Reply to pending question request * @@ -5225,6 +5281,68 @@ export class Session3 extends HeyApiClient { }) } + /** + * Create session + * + * Create a session at the requested location. + */ + public create( + parameters?: { + id?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + location?: LocationRef + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "body", key: "id" }, + { in: "body", key: "agent" }, + { in: "body", key: "model" }, + { in: "body", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/session", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Get session + * + * Retrieve a session by ID. + */ + public get( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}", + ...options, + ...params, + }) + } + /** * Send message * @@ -5779,6 +5897,11 @@ export class V2 extends HeyApiClient { return (this._health ??= new Health({ client: this.client })) } + private _location?: Location + get location(): Location { + return (this._location ??= new Location({ client: this.client })) + } + private _agent?: Agent get agent(): Agent { return (this._agent ??= new Agent({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 7f5e765a41..101a837c61 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2737,18 +2737,18 @@ export type InvalidCursorError = { message: string } -export type ConflictError = { - _tag: "ConflictError" - message: string - resource?: string -} - export type SessionNotFoundError = { _tag: "SessionNotFoundError" sessionID: string message: string } +export type ConflictError = { + _tag: "ConflictError" + message: string + resource?: string +} + export type ServiceUnavailableError = { _tag: "ServiceUnavailableError" message: string @@ -9453,6 +9453,40 @@ export type V2HealthGetResponses = { export type V2HealthGetResponse = V2HealthGetResponses[keyof V2HealthGetResponses] +export type V2LocationGetData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/location" +} + +export type V2LocationGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2LocationGetError = V2LocationGetErrors[keyof V2LocationGetErrors] + +export type V2LocationGetResponses = { + /** + * Location.Info + */ + 200: LocationInfo +} + +export type V2LocationGetResponse = V2LocationGetResponses[keyof V2LocationGetResponses] + export type V2AgentListData = { body?: never path?: never @@ -9531,6 +9565,83 @@ export type V2SessionListResponses = { export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] +export type V2SessionCreateData = { + body: { + id?: string + agent?: string + model?: { + id: string + providerID: string + variant?: string + } + location?: LocationRef + } + path?: never + query?: never + url: "/api/session" +} + +export type V2SessionCreateErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SessionCreateError = V2SessionCreateErrors[keyof V2SessionCreateErrors] + +export type V2SessionCreateResponses = { + /** + * Success + */ + 200: { + data: SessionV2Info + } +} + +export type V2SessionCreateResponse = V2SessionCreateResponses[keyof V2SessionCreateResponses] + +export type V2SessionGetData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}" +} + +export type V2SessionGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionGetError = V2SessionGetErrors[keyof V2SessionGetErrors] + +export type V2SessionGetResponses = { + /** + * Success + */ + 200: { + data: SessionV2Info + } +} + +export type V2SessionGetResponse = V2SessionGetResponses[keyof V2SessionGetResponses] + export type V2SessionPromptData = { body: { id?: string @@ -10310,6 +10421,43 @@ export type V2QuestionRequestListResponses = { export type V2QuestionRequestListResponse = V2QuestionRequestListResponses[keyof V2QuestionRequestListResponses] +export type V2SessionQuestionListData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/question" +} + +export type V2SessionQuestionListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionQuestionListError = V2SessionQuestionListErrors[keyof V2SessionQuestionListErrors] + +export type V2SessionQuestionListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionQuestionListResponse = V2SessionQuestionListResponses[keyof V2SessionQuestionListResponses] + export type V2SessionQuestionReplyData = { body: QuestionV2Reply path: { diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts index 52b9daf7bf..eab0dbfc44 100644 --- a/packages/server/src/api.ts +++ b/packages/server/src/api.ts @@ -14,9 +14,11 @@ import { HealthGroup } from "./groups/health" import { QuestionGroup } from "./groups/question" import { ReferenceGroup } from "./groups/reference" import { Authorization } from "./middleware/authorization" +import { LocationGroup } from "./groups/location" export const Api = HttpApi.make("server") .add(HealthGroup) + .add(LocationGroup) .add(AgentGroup) .add(SessionGroup) .add(MessageGroup) diff --git a/packages/server/src/groups/location.ts b/packages/server/src/groups/location.ts index 4ee082c717..e1249bb959 100644 --- a/packages/server/src/groups/location.ts +++ b/packages/server/src/groups/location.ts @@ -5,7 +5,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema" import { WorkspaceV2 } from "@opencode-ai/core/workspace" import { Effect, Layer, Schema } from "effect" import { HttpServerRequest } from "effect/unstable/http" -import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" +import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi" export const LocationQuery = Schema.Struct({ location: Schema.optional( @@ -54,6 +54,23 @@ export class LocationMiddleware extends HttpApiMiddleware.Service< } >()("@opencode/HttpApiLocation") {} +export const LocationGroup = HttpApiGroup.make("server.location") + .add( + HttpApiEndpoint.get("location.get", "/api/location", { + query: LocationQuery, + success: Location.Info, + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.location.get", + summary: "Get location", + description: "Resolve the requested location or the server default location.", + }), + ), + ) + .middleware(LocationMiddleware) + function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref { const query = new URL(request.url, "http://localhost").searchParams const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"] diff --git a/packages/server/src/groups/question.ts b/packages/server/src/groups/question.ts index 94993884bf..cb8932129e 100644 --- a/packages/server/src/groups/question.ts +++ b/packages/server/src/groups/question.ts @@ -24,6 +24,21 @@ export const QuestionGroup = HttpApiGroup.make("server.question") ) .annotateMerge(OpenApi.annotations({ title: "questions", description: "Experimental question routes." })) .middleware(LocationMiddleware) + .add( + HttpApiEndpoint.get("session.question.list", "/api/session/:sessionID/question", { + params: { sessionID: SessionV2.ID }, + success: Schema.Struct({ data: Schema.Array(QuestionV2.Request) }), + error: SessionNotFoundError, + }) + .middleware(SessionLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.question.list", + summary: "List session question requests", + description: "Retrieve pending question requests owned by a session.", + }), + ), + ) .add( HttpApiEndpoint.post("session.question.reply", "/api/session/:sessionID/question/:requestID/reply", { params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID }, diff --git a/packages/server/src/groups/session.ts b/packages/server/src/groups/session.ts index 467e466e12..604fcf9073 100644 --- a/packages/server/src/groups/session.ts +++ b/packages/server/src/groups/session.ts @@ -16,6 +16,9 @@ import { UnknownError, } from "../errors" import { SessionLocationMiddleware } from "../middleware/session-location" +import { AgentV2 } from "@opencode-ai/core/agent" +import { ModelV2 } from "@opencode-ai/core/model" +import { Location } from "@opencode-ai/core/location" const SessionsQueryFields = { workspace: WorkspaceV2.ID.pipe(Schema.optional), @@ -105,6 +108,39 @@ export const SessionGroup = HttpApiGroup.make("server.session") }), ), ) + .add( + HttpApiEndpoint.post("session.create", "/api/session", { + payload: Schema.Struct({ + id: SessionV2.ID.pipe(Schema.optional), + agent: AgentV2.ID.pipe(Schema.optional), + model: ModelV2.Ref.pipe(Schema.optional), + location: Location.Ref.pipe(Schema.optional), + }), + success: Schema.Struct({ data: SessionV2.Info }), + }) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.create", + summary: "Create session", + description: "Create a session at the requested location.", + }), + ), + ) + .add( + HttpApiEndpoint.get("session.get", "/api/session/:sessionID", { + params: { sessionID: SessionV2.ID }, + success: Schema.Struct({ data: SessionV2.Info }), + error: SessionNotFoundError, + }) + .middleware(SessionLocationMiddleware) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.get", + summary: "Get session", + description: "Retrieve a session by ID.", + }), + ), + ) .add( HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", { params: { sessionID: SessionV2.ID }, diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index c8ad548a0a..6a29bed861 100644 --- a/packages/server/src/handlers.ts +++ b/packages/server/src/handlers.ts @@ -18,9 +18,11 @@ import { HealthHandler } from "./handlers/health" import { QuestionHandler } from "./handlers/question" import { ReferenceHandler } from "./handlers/reference" import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local" +import { LocationHandler } from "./handlers/location" export const handlers = Layer.mergeAll( HealthHandler, + LocationHandler, AgentHandler, SessionHandler, MessageHandler, diff --git a/packages/server/src/handlers/location.ts b/packages/server/src/handlers/location.ts new file mode 100644 index 0000000000..ded8c8c2e0 --- /dev/null +++ b/packages/server/src/handlers/location.ts @@ -0,0 +1,18 @@ +import { Location } from "@opencode-ai/core/location" +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Api } from "../api" + +export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (handlers) => + handlers.handle( + "location.get", + Effect.fn(function* () { + const location = yield* Location.Service + return new Location.Info({ + directory: location.directory, + workspaceID: location.workspaceID, + project: location.project, + }) + }), + ), +) diff --git a/packages/server/src/handlers/question.ts b/packages/server/src/handlers/question.ts index f404166090..151557c508 100644 --- a/packages/server/src/handlers/question.ts +++ b/packages/server/src/handlers/question.ts @@ -29,6 +29,13 @@ export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (han return yield* response((yield* QuestionV2.Service).list()) }), ) + .handle( + "session.question.list", + Effect.fn(function* (ctx) { + const requests = yield* (yield* QuestionV2.Service).list() + return { data: requests.filter((request) => request.sessionID === ctx.params.sessionID) } + }), + ) .handle( "session.question.reply", Effect.fn(function* (ctx) { diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 8726f92f8b..66383cfbff 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -10,6 +10,7 @@ import { SessionNotFoundError, UnknownError, } from "../errors" +import { AbsolutePath } from "@opencode-ai/core/schema" const DefaultSessionsLimit = 50 @@ -61,6 +62,36 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl } }), ) + .handle( + "session.create", + Effect.fn(function* (ctx) { + return { + data: yield* session.create({ + id: ctx.payload.id, + agent: ctx.payload.agent, + model: ctx.payload.model, + location: ctx.payload.location ?? { directory: AbsolutePath.make(process.cwd()) }, + }), + } + }), + ) + .handle( + "session.get", + Effect.fn(function* (ctx) { + return { + data: yield* session.get(ctx.params.sessionID).pipe( + Effect.catchTag( + "Session.NotFoundError", + (error) => + new SessionNotFoundError({ + sessionID: error.sessionID, + message: `Session not found: ${error.sessionID}`, + }), + ), + ), + } + }), + ) .handle( "session.prompt", Effect.fn(function* (ctx) { From 38536cf733f5052d53bd6d9f22e896e5c75481c9 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 10 Jun 2026 22:55:21 -0400 Subject: [PATCH 041/157] test(opencode): simplify processor layer wiring (#31823) --- .../test/session/processor-effect.test.ts | 69 +++++++------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index d68873ec7e..d85d41bfba 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -1,6 +1,6 @@ -import { NodeFileSystem } from "@effect/platform-node" import { SessionV1 } from "@opencode-ai/core/v1/session" import { Database } from "@opencode-ai/core/database/database" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2Bridge } from "@/event-v2-bridge" import { expect } from "bun:test" import { tool } from "ai" @@ -8,11 +8,6 @@ import { Cause, Effect, Exit, Fiber, Layer, Stream } from "effect" import path from "path" import z from "zod" import type { Agent } from "../../src/agent/agent" -import { Agent as AgentSvc } from "../../src/agent/agent" -import { Config } from "@/config/config" -import { Image } from "@/image/image" -import { Permission } from "../../src/permission" -import { Plugin } from "../../src/plugin" import { Provider } from "@/provider/provider" import { Session } from "@/session/session" @@ -22,7 +17,6 @@ import { SessionProcessor } from "../../src/session/processor" import { MessageID, PartID, SessionID } from "../../src/session/schema" import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" -import { Snapshot } from "../../src/snapshot" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -31,6 +25,7 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { SessionEvent } from "@opencode-ai/core/session/event" +import { SessionProjector } from "@opencode-ai/core/session/projector" import { LLMEvent } from "@opencode-ai/llm" const summary = Layer.succeed( @@ -171,29 +166,23 @@ const assistant = Effect.fn("TestSession.assistant")(function* ( return msg }) -const status = SessionStatus.layer.pipe(Layer.provideMerge(EventV2Bridge.defaultLayer)) -const infra = Layer.mergeAll(NodeFileSystem.layer, CrossSpawnSpawner.defaultLayer) -const deps = Layer.mergeAll( - Session.defaultLayer, - Snapshot.defaultLayer, - AgentSvc.defaultLayer, - Permission.defaultLayer, - Plugin.defaultLayer, - Config.defaultLayer, - LLM.defaultLayer, - Provider.defaultLayer, - status, - Database.defaultLayer, - EventV2Bridge.defaultLayer, -).pipe(Layer.provideMerge(infra)) -const env = Layer.mergeAll( - TestLLMServer.layer, - SessionProcessor.layer.pipe( - Layer.provide(summary), - Layer.provide(Image.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provideMerge(deps), - ), +const root = LayerNode.group([ + SessionProcessor.node, + Session.node, + SessionProjector.node, + Provider.node, + Database.node, + EventV2Bridge.node, + SessionStatus.node, + CrossSpawnSpawner.node, +]) +const replacements = [ + LayerNode.replace(SessionSummary.node, summary), + LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })), +] +const env = LayerNode.buildLayer( + LayerNode.group([root, LayerNode.make(TestLLMServer.layer, [])]), + { replacements }, ) const it = testEffect(env) @@ -218,13 +207,9 @@ const providerErrorLLM = Layer.succeed( ), }), ) -const providerErrorEnv = SessionProcessor.layer.pipe( - Layer.provide(summary), - Layer.provide(Image.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provide(providerErrorLLM), - Layer.provideMerge(deps), -) +const providerErrorEnv = LayerNode.buildLayer(root, { + replacements: [...replacements, LayerNode.replace(LLM.node, providerErrorLLM)], +}) const itProviderError = testEffect(providerErrorEnv) const fragmentFailureLLM = Layer.succeed( @@ -241,13 +226,9 @@ const fragmentFailureLLM = Layer.succeed( ), }), ) -const fragmentFailureEnv = SessionProcessor.layer.pipe( - Layer.provide(summary), - Layer.provide(Image.defaultLayer), - Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), - Layer.provide(fragmentFailureLLM), - Layer.provideMerge(deps), -) +const fragmentFailureEnv = LayerNode.buildLayer(root, { + replacements: [...replacements, LayerNode.replace(LLM.node, fragmentFailureLLM)], +}) const itFragmentFailure = testEffect(fragmentFailureEnv) const boot = Effect.fn("test.boot")(function* () { From 69623c2b794b2a8e4ba2d751f96feae213885c96 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 11 Jun 2026 02:56:49 +0000 Subject: [PATCH 042/157] chore: generate --- .../test/session/processor-effect.test.ts | 5 +- packages/sdk/openapi.json | 351 +++++++++++++++++- packages/server/src/groups/session.ts | 15 +- 3 files changed, 349 insertions(+), 22 deletions(-) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index d85d41bfba..c8f40d0de1 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -180,10 +180,7 @@ const replacements = [ LayerNode.replace(SessionSummary.node, summary), LayerNode.replace(RuntimeFlags.node, RuntimeFlags.layer({ experimentalEventSystem: true })), ] -const env = LayerNode.buildLayer( - LayerNode.group([root, LayerNode.make(TestLLMServer.layer, [])]), - { replacements }, -) +const env = LayerNode.buildLayer(LayerNode.group([root, LayerNode.make(TestLLMServer.layer, [])]), { replacements }) const it = testEffect(env) diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 1dffe1ef5d..b4d425e24e 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -9958,6 +9958,74 @@ ] } }, + "/api/location": { + "get": { + "tags": ["opencode HttpApi"], + "operationId": "v2.location.get", + "parameters": [ + { + "name": "location", + "in": "query", + "schema": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "workspace": { + "type": "string" + } + }, + "additionalProperties": false + }, + "required": false, + "style": "deepObject", + "explode": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Location.Info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationInfo" + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Resolve the requested location or the server default location.", + "summary": "Get location", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.location.get({\n ...\n})" + } + ] + } + }, "/api/agent": { "get": { "tags": ["opencode HttpApi"], @@ -10163,6 +10231,180 @@ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.list({\n ...\n})" } ] + }, + "post": { + "tags": ["sessions"], + "operationId": "v2.session.create", + "parameters": [], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionV2Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + } + }, + "description": "Create a session at the requested location.", + "summary": "Create session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^ses" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "providerID"], + "additionalProperties": false + }, + "location": { + "$ref": "#/components/schemas/LocationRef" + } + }, + "additionalProperties": false + } + } + }, + "required": true + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.create({\n ...\n})" + } + ] + } + }, + "/api/session/{sessionID}": { + "get": { + "tags": ["sessions"], + "operationId": "v2.session.get", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SessionV2Info" + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve a session by ID.", + "summary": "Get session", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.get({\n ...\n})" + } + ] } }, "/api/session/{sessionID}/prompt": { @@ -11917,6 +12159,91 @@ ] } }, + "/api/session/{sessionID}/question": { + "get": { + "tags": ["session questions"], + "operationId": "v2.session.question.list", + "parameters": [ + { + "name": "sessionID", + "in": "path", + "schema": { + "type": "string", + "pattern": "^ses" + }, + "required": true + } + ], + "security": [], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionV2Request" + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + }, + "400": { + "description": "InvalidRequestError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequestError" + } + } + } + }, + "401": { + "description": "UnauthorizedError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthorizedError" + } + } + } + }, + "404": { + "description": "SessionNotFoundError", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/SessionNotFoundError" + }, + { + "$ref": "#/components/schemas/SessionNotFoundError" + } + ] + } + } + } + } + }, + "description": "Retrieve pending question requests owned by a session.", + "summary": "List session question requests", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.session.question.list({\n ...\n})" + } + ] + } + }, "/api/session/{sessionID}/question/{requestID}/reply": { "post": { "tags": ["session questions"], @@ -20623,38 +20950,38 @@ "required": ["_tag", "message"], "additionalProperties": false }, - "ConflictError": { + "SessionNotFoundError": { "type": "object", "properties": { "_tag": { "type": "string", - "enum": ["ConflictError"] + "enum": ["SessionNotFoundError"] }, - "message": { + "sessionID": { "type": "string" }, - "resource": { + "message": { "type": "string" } }, - "required": ["_tag", "message"], + "required": ["_tag", "sessionID", "message"], "additionalProperties": false }, - "SessionNotFoundError": { + "ConflictError": { "type": "object", "properties": { "_tag": { "type": "string", - "enum": ["SessionNotFoundError"] + "enum": ["ConflictError"] }, - "sessionID": { + "message": { "type": "string" }, - "message": { + "resource": { "type": "string" } }, - "required": ["_tag", "sessionID", "message"], + "required": ["_tag", "message"], "additionalProperties": false }, "ServiceUnavailableError": { @@ -28875,6 +29202,10 @@ "name": "opencode HttpApi", "description": "Experimental HttpApi surface for selected instance routes." }, + { + "name": "opencode HttpApi", + "description": "Experimental HttpApi surface for selected instance routes." + }, { "name": "sessions", "description": "Experimental session routes." diff --git a/packages/server/src/groups/session.ts b/packages/server/src/groups/session.ts index 604fcf9073..a208de82f0 100644 --- a/packages/server/src/groups/session.ts +++ b/packages/server/src/groups/session.ts @@ -117,14 +117,13 @@ export const SessionGroup = HttpApiGroup.make("server.session") location: Location.Ref.pipe(Schema.optional), }), success: Schema.Struct({ data: SessionV2.Info }), - }) - .annotateMerge( - OpenApi.annotations({ - identifier: "v2.session.create", - summary: "Create session", - description: "Create a session at the requested location.", - }), - ), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.session.create", + summary: "Create session", + description: "Create a session at the requested location.", + }), + ), ) .add( HttpApiEndpoint.get("session.get", "/api/session/:sessionID", { From 47a45601fd3d801d61524dd8cf7733feb1e17dc8 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 10 Jun 2026 23:34:35 -0400 Subject: [PATCH 043/157] refactor(tui): replace v2 sync with data context (#31826) --- packages/tui/src/app.tsx | 6 +- packages/tui/src/component/dialog-model.tsx | 91 +- .../src/component/dialog-workspace-create.tsx | 2 +- .../src/component/dialog-workspace-list.tsx | 2 +- .../tui/src/component/prompt/autocomplete.tsx | 36 +- packages/tui/src/component/use-connected.tsx | 8 +- packages/tui/src/context/data.tsx | 567 ++++++++ packages/tui/src/context/local.tsx | 43 +- packages/tui/src/context/sync-v2.tsx | 465 ------- packages/tui/src/feature-plugins/builtins.ts | 2 - .../tui/src/feature-plugins/home/footer.tsx | 8 +- .../src/feature-plugins/system/session-v2.tsx | 1196 ----------------- .../src/routes/home/session-destination.tsx | 8 +- packages/tui/test/cli/tui/data.test.tsx | 433 ++++++ packages/tui/test/cli/tui/sync-v2.test.tsx | 619 --------- packages/tui/test/fixture/tui-sdk.ts | 7 + 16 files changed, 1113 insertions(+), 2380 deletions(-) create mode 100644 packages/tui/src/context/data.tsx delete mode 100644 packages/tui/src/context/sync-v2.tsx delete mode 100644 packages/tui/src/feature-plugins/system/session-v2.tsx create mode 100644 packages/tui/test/cli/tui/data.test.tsx delete mode 100644 packages/tui/test/cli/tui/sync-v2.test.tsx diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index edf9085ae8..a05dda1b4b 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -34,7 +34,7 @@ import { useEvent } from "./context/event" import { SDKProvider, useSDK } from "./context/sdk" import { StartupLoading } from "./component/startup-loading" import { SyncProvider, useSync } from "./context/sync" -import { SyncProviderV2 } from "./context/sync-v2" +import { DataProvider } from "./context/data" import { LocalProvider, useLocal } from "./context/local" import { DialogModel } from "./component/dialog-model" import { useConnected } from "./component/use-connected" @@ -294,7 +294,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { > - + @@ -315,7 +315,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { - + diff --git a/packages/tui/src/component/dialog-model.tsx b/packages/tui/src/component/dialog-model.tsx index 15da1a65dc..b540e86b27 100644 --- a/packages/tui/src/component/dialog-model.tsx +++ b/packages/tui/src/component/dialog-model.tsx @@ -1,17 +1,17 @@ import { createMemo, createSignal } from "solid-js" import { useLocal } from "../context/local" -import { useSync } from "../context/sync" -import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda" +import { map, pipe, filter, sortBy, take } from "remeda" import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { DialogVariant } from "./dialog-variant" import * as fuzzysort from "fuzzysort" import { useConnected } from "./use-connected" +import { useData } from "../context/data" export function DialogModel(props: { providerID?: string }) { const local = useLocal() - const sync = useSync() + const data = useData() const dialog = useDialog() const [query, setQuery] = createSignal("") @@ -29,19 +29,21 @@ export function DialogModel(props: { providerID?: string }) { function toOptions(items: typeof favorites, category: string) { if (!showSections) return [] return items.flatMap((item) => { - const provider = sync.data.provider.find((x) => x.id === item.providerID) + const provider = data.location.provider.list()?.find((provider) => provider.id === item.providerID) if (!provider) return [] - const model = provider.models[item.modelID] + const model = data.location.model + .list() + ?.find((model) => model.providerID === item.providerID && model.id === item.modelID) if (!model) return [] return [ { key: item, value: { providerID: provider.id, modelID: model.id }, - title: model.name ?? item.modelID, + title: model.name, description: provider.name, category, disabled: provider.id === "opencode" && model.id.includes("-nano"), - footer: model.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, + footer: model.cost[0]?.input === 0 && provider.id === "opencode" ? "Free" : undefined, onSelect: () => { onSelect(provider.id, model.id) }, @@ -59,42 +61,45 @@ export function DialogModel(props: { providerID?: string }) { ) const providerOptions = pipe( - sync.data.provider, + data.location.model.list() ?? [], + filter((model) => model.status !== "deprecated"), + filter((model) => (props.providerID ? model.providerID === props.providerID : true)), sortBy( - (provider) => provider.id !== "opencode", - (provider) => provider.name, - ), - flatMap((provider) => - pipe( - provider.models, - entries(), - filter(([_, info]) => info.status !== "deprecated"), - filter(([_, info]) => (props.providerID ? info.providerID === props.providerID : true)), - map(([model, info]) => ({ - value: { providerID: provider.id, modelID: model }, - title: info.name ?? model, - releaseDate: info.release_date, - description: favorites.some((item) => item.providerID === provider.id && item.modelID === model) - ? "(Favorite)" - : undefined, - category: connected() ? provider.name : undefined, - disabled: provider.id === "opencode" && model.includes("-nano"), - footer: info.cost?.input === 0 && provider.id === "opencode" ? "Free" : undefined, - onSelect() { - onSelect(provider.id, model) - }, - })), - filter((x) => { - if (!showSections) return true - if (favorites.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID)) - return false - if (recents.some((item) => item.providerID === x.value.providerID && item.modelID === x.value.modelID)) - return false - return true - }), - (options) => sortModelOptions(options, props.providerID !== undefined), - ), + (model) => model.providerID !== "opencode", + (model) => data.location.provider.list()?.find((provider) => provider.id === model.providerID)?.name ?? "", + [(model) => model.time.released, "desc"], ), + map((model) => ({ + value: { providerID: model.providerID, modelID: model.id }, + title: model.name, + releaseDate: model.time.released, + description: favorites.some((item) => item.providerID === model.providerID && item.modelID === model.id) + ? "(Favorite)" + : undefined, + category: connected() + ? data.location.provider.list()?.find((provider) => provider.id === model.providerID)?.name + : undefined, + disabled: !model.enabled || (model.providerID === "opencode" && model.id.includes("-nano")), + footer: model.cost[0]?.input === 0 && model.providerID === "opencode" ? "Free" : undefined, + onSelect() { + onSelect(model.providerID, model.id) + }, + })), + filter((option) => { + if (!showSections) return true + if ( + favorites.some( + (item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID, + ) + ) + return false + if ( + recents.some((item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID) + ) + return false + return true + }), + (options) => sortModelOptions(options, props.providerID !== undefined), ) const popularProviders = !connected() @@ -119,7 +124,7 @@ export function DialogModel(props: { providerID?: string }) { }) const provider = createMemo(() => - props.providerID ? sync.data.provider.find((x) => x.id === props.providerID) : null, + props.providerID ? data.location.provider.list()?.find((item) => item.id === props.providerID) : null, ) const title = createMemo(() => { @@ -172,7 +177,7 @@ export function DialogModel(props: { providerID?: string }) { ) } -export function sortModelOptions( +export function sortModelOptions( options: T[], newestFirst: boolean, ) { diff --git a/packages/tui/src/component/dialog-workspace-create.tsx b/packages/tui/src/component/dialog-workspace-create.tsx index 98c71bb000..b3e807b2fd 100644 --- a/packages/tui/src/component/dialog-workspace-create.tsx +++ b/packages/tui/src/component/dialog-workspace-create.tsx @@ -132,7 +132,7 @@ export async function warpWorkspaceSession(input: { input.project.workspace.set(input.workspaceID) - await input.sync.bootstrap({ fatal: false }).catch(() => undefined) + await input.sync.bootstrap() const dir = input.project.instance.directory() || input.sync.path.directory if (dir) { diff --git a/packages/tui/src/component/dialog-workspace-list.tsx b/packages/tui/src/component/dialog-workspace-list.tsx index eab2acf7c8..b37efc6225 100644 --- a/packages/tui/src/component/dialog-workspace-list.tsx +++ b/packages/tui/src/component/dialog-workspace-list.tsx @@ -82,7 +82,7 @@ export function DialogWorkspaceList() { route.navigate({ type: "home" }) } await project.workspace.sync() - await sync.bootstrap({ fatal: false }).catch(() => undefined) + await sync.bootstrap() setRemoving(undefined) } diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index b543d7c38e..a8a6b12883 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -9,7 +9,7 @@ import { useEditorContext } from "../../context/editor" import { useProject } from "../../context/project" import { useSDK } from "../../context/sdk" import { useSync } from "../../context/sync" -import { useSyncV2 } from "../../context/sync-v2" +import { useData } from "../../context/data" import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useTuiConfig } from "../../config" @@ -85,7 +85,7 @@ export function Autocomplete(props: { const editor = useEditorContext() const sdk = useSDK() const sync = useSync() - const syncV2 = useSyncV2() + const data = useData() const project = useProject() const slashes = useCommandSlashes() const modeStack = useOpencodeModeStack() @@ -273,12 +273,14 @@ export function Autocomplete(props: { } } + const references = createMemo(() => data.location.reference.list() ?? []) + const referenceMatch = createMemo(() => { if (!store.visible || store.visible === "/") return const { baseQuery } = extractLineRange(search()) const slash = baseQuery.indexOf("/") const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash) - return syncV2.data.reference.find((item) => !item.hidden && item.name === alias) + return references().find((item) => !item.hidden && item.name === alias) }) function normalizeMentionPath(filePath: string) { @@ -312,14 +314,12 @@ export function Autocomplete(props: { async (query) => { if (!store.visible || store.visible === "/") return [] if (referenceMatch()) return [] - const { lineRange, baseQuery } = extractLineRange(query ?? "") // Get files from SDK - const result = await sdk.client.v2.fs.find({ + const result = await sdk.client.find.files({ query: baseQuery, - limit: "20", - location: { workspace: project.workspace.current() }, + workspace: project.workspace.current(), }) const options: AutocompleteOption[] = [] @@ -329,14 +329,15 @@ export function Autocomplete(props: { if (!result.error && result.data) { const width = props.anchor().width - 4 options.push( - ...result.data.data.map((item): AutocompleteOption => { - const { filename, url, part } = createFilePart(item.path, lineRange) + ...result.data.map((item): AutocompleteOption => { + const { filename, url, part } = createFilePart(item, lineRange) + const isDir = item.endsWith("/") return { display: Locale.truncateMiddle(filename, width), value: filename, - isDirectory: item.type === "directory", - path: item.path, + isDirectory: isDir, + path: item, onSelect: () => { insertPart(filename, part) }, @@ -389,16 +390,15 @@ export function Autocomplete(props: { }) const agents = createMemo(() => { - const agents = sync.data.agent - return agents + return (data.location.agent.list() ?? []) .filter((agent) => !agent.hidden && agent.mode !== "primary") .map( (agent): AutocompleteOption => ({ - display: "@" + agent.name, + display: "@" + agent.id, onSelect: () => { - insertPart(agent.name, { + insertPart(agent.id, { type: "agent", - name: agent.name, + name: agent.id, source: { start: 0, end: 0, @@ -411,7 +411,7 @@ export function Autocomplete(props: { }) const referenceAliases = createMemo(() => - syncV2.data.reference + references() .filter((reference) => !reference.hidden) .map( (reference): AutocompleteOption => ({ @@ -471,8 +471,6 @@ export function Autocomplete(props: { const commandsValue = commands() const searchValue = search() - // @/... — narrow to the matched reference, files come from fff - // already ranked so there is no re-ranking here. if (store.visible === "@" && referenceMatchValue) { return referenceAliasesValue.filter((item) => item.display === `@${referenceMatchValue.name}`) } diff --git a/packages/tui/src/component/use-connected.tsx b/packages/tui/src/component/use-connected.tsx index 6a9e5f82a9..47886d67ad 100644 --- a/packages/tui/src/component/use-connected.tsx +++ b/packages/tui/src/component/use-connected.tsx @@ -1,9 +1,7 @@ import { createMemo } from "solid-js" -import { useSync } from "../context/sync" +import { useData } from "../context/data" export function useConnected() { - const sync = useSync() - return createMemo(() => - sync.data.provider.some((x) => x.id !== "opencode" || Object.values(x.models).some((y) => y.cost?.input !== 0)), - ) + const data = useData() + return createMemo(() => (data.location.provider.list() ?? []).some((provider) => provider.enabled !== false)) } diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx new file mode 100644 index 0000000000..23df05c5f7 --- /dev/null +++ b/packages/tui/src/context/data.tsx @@ -0,0 +1,567 @@ +import { useEvent } from "./event" +import type { + AgentV2Info, + CommandV2Info, + Event, + LocationRef, + ModelV2Info, + PermissionSavedInfo, + PermissionV2Request, + ProviderV2Info, + QuestionV2Request, + ReferenceInfo, + SessionMessage, + SessionMessageAssistant, + SessionMessageAssistantReasoning, + SessionMessageAssistantText, + SessionMessageAssistantTool, + SessionV2Info, + SkillV2Info, +} from "@opencode-ai/sdk/v2" +import { createStore, produce } from "solid-js/store" +import { createSimpleContext } from "./helper" +import { useSDK } from "./sdk" +import { createSignal, onMount } from "solid-js" + +type LocationData = { + agent?: AgentV2Info[] + command?: CommandV2Info[] + model?: ModelV2Info[] + provider?: ProviderV2Info[] + reference?: ReferenceInfo[] + skill?: SkillV2Info[] +} + +type Data = { + session: { + info: Record + message: Record + permission: Record + question: Record + } + project: { + permission: Record + } + location: Record +} + +function locationKey(location: LocationRef) { + return JSON.stringify([location.directory, location.workspaceID]) +} + +function locationQuery(ref?: LocationRef) { + return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined +} + +export const { use: useData, provider: DataProvider } = createSimpleContext({ + name: "Data", + init: () => { + const [store, setStore] = createStore({ + session: { + info: {}, + message: {}, + permission: {}, + question: {}, + }, + project: { + permission: {}, + }, + location: {}, + }) + + const event = useEvent() + const sdk = useSDK() + const [defaultLocation, setDefaultLocation] = createSignal({ + directory: sdk.directory ?? process.cwd(), + }) + + const message = { + update(sessionID: string, fn: (messages: SessionMessage[]) => void) { + setStore( + "session", + "message", + produce((draft) => { + fn((draft[sessionID] ??= [])) + }), + ) + }, + prepend(messages: SessionMessage[], item: SessionMessage) { + if (messages.some((existing) => existing.id === item.id)) return + messages.unshift(item) + }, + activeAssistant(messages: SessionMessage[]) { + const item = messages.find((item) => item.type === "assistant" && !item.time.completed) + return item?.type === "assistant" ? item : undefined + }, + assistant(messages: SessionMessage[], messageID: string) { + const item = messages.find((item) => item.type === "assistant" && item.id === messageID) + return item?.type === "assistant" ? item : undefined + }, + activeShell(messages: SessionMessage[], callID: string) { + const item = messages.find((item) => item.type === "shell" && item.callID === callID) + return item?.type === "shell" ? item : undefined + }, + latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { + return assistant?.content.findLast( + (item): item is SessionMessageAssistantTool => + item.type === "tool" && (callID === undefined || item.id === callID), + ) + }, + latestText(assistant: SessionMessageAssistant | undefined, textID: string) { + return assistant?.content.findLast( + (item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID, + ) + }, + latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) { + return assistant?.content.findLast( + (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID, + ) + }, + } + + event.subscribe((event) => { + switch (event.type) { + case "session.next.agent.switched": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "agent-switched", + agent: event.properties.agent, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.model.switched": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "model-switched", + model: event.properties.model, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.prompted": { + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "user", + text: event.properties.prompt.text, + files: event.properties.prompt.files, + agents: event.properties.prompt.agents, + time: { created: event.properties.timestamp }, + }) + }) + break + } + case "session.next.prompt.admitted": + break + case "session.next.prompt.promoted": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "user", + text: event.properties.prompt.text, + files: event.properties.prompt.files, + agents: event.properties.prompt.agents, + time: { created: event.properties.timeCreated }, + }) + }) + break + case "session.next.context.updated": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "system", + text: event.properties.text, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.synthetic": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "synthetic", + sessionID: event.properties.sessionID, + text: event.properties.text, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.shell.started": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "shell", + callID: event.properties.callID, + command: event.properties.command, + output: "", + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.shell.ended": + message.update(event.properties.sessionID, (draft) => { + const match = message.activeShell(draft, event.properties.callID) + if (!match) return + match.output = event.properties.output + match.time.completed = event.properties.timestamp + }) + break + case "session.next.step.started": + message.update(event.properties.sessionID, (draft) => { + if (draft.some((message) => message.id === event.properties.assistantMessageID)) return + const currentAssistant = message.activeAssistant(draft) + if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp + message.prepend(draft, { + id: event.properties.assistantMessageID, + type: "assistant", + agent: event.properties.agent, + model: event.properties.model, + content: [], + snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined, + time: { created: event.properties.timestamp }, + }) + }) + break + case "session.next.step.ended": + message.update(event.properties.sessionID, (draft) => { + const currentAssistant = message.assistant(draft, event.properties.assistantMessageID) + if (!currentAssistant) return + currentAssistant.time.completed = event.properties.timestamp + currentAssistant.finish = event.properties.finish + currentAssistant.cost = event.properties.cost + currentAssistant.tokens = event.properties.tokens + if (event.properties.snapshot) + currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot } + }) + break + case "session.next.step.failed": + message.update(event.properties.sessionID, (draft) => { + const currentAssistant = message.assistant(draft, event.properties.assistantMessageID) + if (!currentAssistant) return + currentAssistant.time.completed = event.properties.timestamp + currentAssistant.finish = "error" + currentAssistant.error = event.properties.error + }) + break + case "session.next.text.started": + message.update(event.properties.sessionID, (draft) => { + message.assistant(draft, event.properties.assistantMessageID)?.content.push({ + type: "text", + id: event.properties.textID, + text: "", + }) + }) + break + case "session.next.text.delta": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestText( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.textID, + ) + if (match) match.text += event.properties.delta + }) + break + case "session.next.text.ended": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestText( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.textID, + ) + if (match) match.text = event.properties.text + }) + break + case "session.next.tool.input.started": + message.update(event.properties.sessionID, (draft) => { + message.assistant(draft, event.properties.assistantMessageID)?.content.push({ + type: "tool", + id: event.properties.callID, + name: event.properties.name, + time: { created: event.properties.timestamp }, + state: { status: "pending", input: "" }, + }) + }) + break + case "session.next.tool.input.delta": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (match?.state.status === "pending") match.state.input += event.properties.delta + }) + break + case "session.next.tool.input.ended": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (match?.state.status === "pending") match.state.input = event.properties.text + }) + break + case "session.next.tool.called": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (!match) return + match.time.ran = event.properties.timestamp + match.provider = event.properties.provider + match.state = { status: "running", input: event.properties.input, structured: {}, content: [] } + }) + break + case "session.next.tool.progress": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (match?.state.status !== "running") return + match.state.structured = event.properties.structured + match.state.content = [...event.properties.content] + }) + break + case "session.next.tool.success": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (match?.state.status !== "running") return + match.state = { + status: "completed", + input: match.state.input, + structured: event.properties.structured, + content: [...event.properties.content], + result: event.properties.result, + } + match.provider = { + executed: event.properties.provider.executed || match.provider?.executed === true, + metadata: match.provider?.metadata, + resultMetadata: event.properties.provider.metadata, + } + match.time.completed = event.properties.timestamp + }) + break + case "session.next.tool.failed": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestTool( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.callID, + ) + if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return + match.state = { + status: "error", + error: event.properties.error, + input: typeof match.state.input === "string" ? {} : match.state.input, + structured: match.state.status === "running" ? match.state.structured : {}, + content: match.state.status === "running" ? match.state.content : [], + result: event.properties.result, + } + match.provider = { + executed: event.properties.provider.executed || match.provider?.executed === true, + metadata: match.provider?.metadata, + resultMetadata: event.properties.provider.metadata, + } + match.time.completed = event.properties.timestamp + }) + break + case "session.next.reasoning.started": + message.update(event.properties.sessionID, (draft) => { + message.assistant(draft, event.properties.assistantMessageID)?.content.push({ + type: "reasoning", + id: event.properties.reasoningID, + text: "", + providerMetadata: event.properties.providerMetadata, + }) + }) + break + case "session.next.reasoning.delta": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestReasoning( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.reasoningID, + ) + if (match) match.text += event.properties.delta + }) + break + case "session.next.reasoning.ended": + message.update(event.properties.sessionID, (draft) => { + const match = message.latestReasoning( + message.assistant(draft, event.properties.assistantMessageID), + event.properties.reasoningID, + ) + if (match) { + match.text = event.properties.text + if (event.properties.providerMetadata !== undefined) + match.providerMetadata = event.properties.providerMetadata + } + }) + break + case "session.next.retried": + case "session.next.compaction.started": + case "session.next.compaction.delta": + break + case "session.next.compaction.ended": + message.update(event.properties.sessionID, (draft) => { + message.prepend(draft, { + id: event.properties.messageID, + type: "compaction", + reason: event.properties.reason, + summary: event.properties.text, + recent: event.properties.recent, + time: { created: event.properties.timestamp }, + }) + }) + break + case "reference.updated": + void result.location.reference.refresh() + break + } + }) + + const result = { + session: { + get(sessionID: string) { + return store.session.info[sessionID] + }, + async refresh(sessionID: string) { + const result = await sdk.client.v2.session.get({ sessionID }, { throwOnError: true }) + setStore("session", "info", sessionID, result.data.data) + }, + message: { + list(sessionID: string) { + return store.session.message[sessionID] + }, + async refresh(sessionID: string) { + const result = await sdk.client.v2.session.messages({ sessionID }, { throwOnError: true }) + setStore("session", "message", sessionID, result.data.data) + }, + }, + permission: { + list(sessionID: string) { + return store.session.permission[sessionID] + }, + async refresh(sessionID: string) { + const result = await sdk.client.v2.session.permission.list({ sessionID }, { throwOnError: true }) + setStore("session", "permission", sessionID, result.data.data) + }, + }, + question: { + list(sessionID: string) { + return store.session.question[sessionID] + }, + async refresh(sessionID: string) { + const result = await sdk.client.v2.session.question.list({ sessionID }, { throwOnError: true }) + setStore("session", "question", sessionID, result.data.data) + }, + }, + }, + project: { + permission: { + list(projectID: string) { + return store.project.permission[projectID] + }, + async refresh(projectID: string) { + const result = await sdk.client.v2.permission.saved.list({ projectID }, { throwOnError: true }) + setStore("project", "permission", projectID, result.data.data) + }, + }, + }, + location: { + default() { + return defaultLocation() + }, + async refresh(ref?: LocationRef) { + const response = await sdk.client.v2.location.get({ location: locationQuery(ref) }, { throwOnError: true }) + const location = response.data + const key = locationKey(location) + if (!store.location[key]) setStore("location", key, {}) + if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID }) + }, + agent: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.agent + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.agent.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "agent", result.data.data) + }, + }, + command: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.command + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.command.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "command", result.data.data) + }, + }, + model: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.model + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.model.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "model", result.data.data) + }, + }, + provider: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.provider + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.provider.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "provider", result.data.data) + }, + }, + reference: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.reference + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.reference.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "reference", result.data.data) + }, + }, + skill: { + list(location?: LocationRef) { + return store.location[locationKey(location ?? defaultLocation())]?.skill + }, + async refresh(ref?: LocationRef) { + const result = await sdk.client.v2.skill.list({ location: locationQuery(ref) }, { throwOnError: true }) + const key = locationKey(result.data.location) + setStore("location", key, "skill", result.data.data) + }, + }, + }, + } + + onMount(() => { + void Promise.allSettled([ + result.location.refresh(), + result.location.agent.refresh(), + result.location.model.refresh(), + result.location.provider.refresh(), + result.location.reference.refresh(), + result.location.command.refresh(), + result.location.skill.refresh(), + ]) + .then((settled) => { + for (const failure of settled.filter((item) => item.status === "rejected")) + console.error("Failed to refresh default location data", failure.reason) + }) + }) + + return result + }, +}) diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index d9d0c492f3..f82ea7d1bd 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -12,6 +12,7 @@ import { readJson, writeJsonAtomic } from "../util/persistence" import { useTheme } from "./theme" import { useToast } from "../ui/toast" import { useRoute } from "./route" +import { useData } from "./data" export type LocalTheme = { secondary: RGBA @@ -51,15 +52,17 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ name: "Local", init: () => { const sync = useSync() + const data = useData() const sdk = useSDK() const toast = useToast() const theme = useTheme().theme const route = useRoute() const paths = useTuiPaths() + const providers = createMemo(() => data.location.provider.list() ?? []) + const models = createMemo(() => data.location.model.list() ?? []) function isModelValid(model: { providerID: string; modelID: string }) { - const provider = sync.data.provider.find((x) => x.id === model.providerID) - return !!provider?.models[model.modelID] + return models().some((item) => item.providerID === model.providerID && item.id === model.modelID && item.enabled) } function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) { @@ -71,8 +74,18 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } function createAgent() { - const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden)) - const visibleAgents = createMemo(() => sync.data.agent.filter((x) => !x.hidden)) + const all = createMemo(() => + (data.location.agent.list() ?? []).map((agent) => ({ + ...agent, + name: agent.id, + native: false, + model: agent.model + ? { providerID: agent.model.providerID, modelID: agent.model.id, variant: agent.model.variant } + : undefined, + })), + ) + const agents = createMemo(() => all().filter((agent) => agent.mode !== "subagent" && !agent.hidden)) + const visibleAgents = createMemo(() => all().filter((agent) => !agent.hidden)) const [agentStore, setAgentStore] = createStore({ current: undefined as string | undefined, }) @@ -218,15 +231,11 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - const provider = sync.data.provider[0] - if (!provider) return undefined - const defaultModel = sync.data.provider_default[provider.id] - const firstModel = Object.values(provider.models)[0] - const model = defaultModel ?? firstModel?.id + const model = models().find((item) => item.enabled && providers().some((provider) => provider.id === item.providerID)) if (!model) return undefined return { - providerID: provider.id, - modelID: model, + providerID: model.providerID, + modelID: model.id, } }) @@ -261,12 +270,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ reasoning: false, } } - const provider = sync.data.provider.find((x) => x.id === value.providerID) - const info = provider?.models[value.modelID] + const provider = providers().find((item) => item.id === value.providerID) + const info = models().find((item) => item.providerID === value.providerID && item.id === value.modelID) return { provider: provider?.name ?? value.providerID, model: info?.name ?? value.modelID, - reasoning: info?.capabilities?.reasoning ?? false, + reasoning: false, } }), cycle(direction: 1 | -1) { @@ -372,10 +381,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ list() { const m = currentModel() if (!m) return [] - const provider = sync.data.provider.find((x) => x.id === m.providerID) - const info = provider?.models[m.modelID] - if (!info?.variants) return [] - return Object.keys(info.variants) + const info = models().find((item) => item.providerID === m.providerID && item.id === m.modelID) + return info?.variants.map((variant) => variant.id) ?? [] }, set(value: string | undefined) { const m = currentModel() diff --git a/packages/tui/src/context/sync-v2.tsx b/packages/tui/src/context/sync-v2.tsx deleted file mode 100644 index c3102d2517..0000000000 --- a/packages/tui/src/context/sync-v2.tsx +++ /dev/null @@ -1,465 +0,0 @@ -import { useEvent } from "./event" -import type { - Event, - ReferenceInfo, - SessionMessage, - SessionMessageAssistant, - SessionMessageAssistantReasoning, - SessionMessageAssistantText, - SessionMessageAssistantTool, -} from "@opencode-ai/sdk/v2" -import { createStore, produce, reconcile } from "solid-js/store" -import { createSimpleContext } from "./helper" -import { useSDK } from "./sdk" -import { useProject } from "./project" -import { createEffect } from "solid-js" - -function activeAssistant(messages: SessionMessage[]) { - const index = messages.findIndex((message) => message.type === "assistant" && !message.time.completed) - if (index < 0) return - const assistant = messages[index] - return assistant?.type === "assistant" ? assistant : undefined -} - -function ownedAssistant(messages: SessionMessage[], messageID: string) { - const message = messages.find((message) => message.type === "assistant" && message.id === messageID) - return message?.type === "assistant" ? message : undefined -} - -function activeShell(messages: SessionMessage[], callID: string) { - const index = messages.findIndex((message) => message.type === "shell" && message.callID === callID) - if (index < 0) return - const shell = messages[index] - return shell?.type === "shell" ? shell : undefined -} - -function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) { - return assistant?.content.findLast( - (item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID), - ) -} - -function latestText(assistant: SessionMessageAssistant | undefined, textID: string) { - return assistant?.content.findLast( - (item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID, - ) -} - -function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) { - return assistant?.content.findLast( - (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID, - ) -} - -function prepend(messages: SessionMessage[], message: SessionMessage) { - if (messages.some((item) => item.id === message.id)) return - messages.unshift(message) -} - -export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({ - name: "SyncV2", - init: () => { - const [store, setStore] = createStore<{ - messages: { - [sessionID: string]: SessionMessage[] - } - reference: ReferenceInfo[] - }>({ - messages: {}, - reference: [], - }) - - const event = useEvent() - const sdk = useSDK() - const project = useProject() - const applied = new Set() - const buffering = new Map() - const syncing = new Map>() - - function duplicate(id: string) { - if (applied.has(id)) return true - applied.add(id) - if (applied.size <= 1000) return false - const oldest = applied.values().next() - if (!oldest.done) applied.delete(oldest.value) - return false - } - - function update(sessionID: string, fn: (messages: SessionMessage[]) => void) { - setStore( - "messages", - produce((draft) => { - fn((draft[sessionID] ??= [])) - }), - ) - } - - async function hydrate(sessionID: string) { - const pending: Event[] = [] - const before = JSON.parse(JSON.stringify(store.messages[sessionID] ?? [])) as SessionMessage[] - buffering.set(sessionID, pending) - try { - const response = await sdk.client.v2.session.messages({ sessionID }) - const messages = response.data?.data ?? [] - const snapshotIDs = new Set(messages.map((message) => message.id)) - setStore( - "messages", - sessionID, - reconcile([...messages, ...before.filter((message) => !snapshotIDs.has(message.id))]), - ) - buffering.delete(sessionID) - for (const event of pending) apply(event) - } catch (error) { - buffering.delete(sessionID) - throw error - } - } - - function sync(sessionID: string) { - const existing = syncing.get(sessionID) - if (existing) return existing - const result = hydrate(sessionID).finally(() => syncing.delete(sessionID)) - syncing.set(sessionID, result) - return result - } - - async function syncReferences(workspace = project.workspace.current()) { - const result = await sdk.client.v2.reference.list({ location: { workspace } }) - if (workspace !== project.workspace.current()) return - setStore("reference", reconcile(result.data?.data ?? [])) - } - - createEffect(() => { - project.workspace.current() - void syncReferences() - }) - - function apply(event: Event) { - switch (event.type) { - case "session.next.agent.switched": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "agent-switched", - agent: event.properties.agent, - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.model.switched": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "model-switched", - model: event.properties.model, - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.prompted": { - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "user", - text: event.properties.prompt.text, - files: event.properties.prompt.files, - agents: event.properties.prompt.agents, - time: { created: event.properties.timestamp }, - }) - }) - break - } - case "session.next.prompt.admitted": - break - case "session.next.prompt.promoted": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "user", - text: event.properties.prompt.text, - files: event.properties.prompt.files, - agents: event.properties.prompt.agents, - time: { created: event.properties.timeCreated }, - }) - }) - break - case "session.next.context.updated": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "system", - text: event.properties.text, - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.synthetic": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "synthetic", - sessionID: event.properties.sessionID, - text: event.properties.text, - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.shell.started": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "shell", - callID: event.properties.callID, - command: event.properties.command, - output: "", - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.shell.ended": - update(event.properties.sessionID, (draft) => { - const match = activeShell(draft, event.properties.callID) - if (!match) return - match.output = event.properties.output - match.time.completed = event.properties.timestamp - }) - break - case "session.next.step.started": - update(event.properties.sessionID, (draft) => { - if (draft.some((message) => message.id === event.properties.assistantMessageID)) return - const currentAssistant = activeAssistant(draft) - if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp - prepend(draft, { - id: event.properties.assistantMessageID, - type: "assistant", - agent: event.properties.agent, - model: event.properties.model, - content: [], - snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined, - time: { created: event.properties.timestamp }, - }) - }) - break - case "session.next.step.ended": - update(event.properties.sessionID, (draft) => { - const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID) - if (!currentAssistant) return - currentAssistant.time.completed = event.properties.timestamp - currentAssistant.finish = event.properties.finish - currentAssistant.cost = event.properties.cost - currentAssistant.tokens = event.properties.tokens - if (event.properties.snapshot) - currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot } - }) - break - case "session.next.step.failed": - update(event.properties.sessionID, (draft) => { - const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID) - if (!currentAssistant) return - currentAssistant.time.completed = event.properties.timestamp - currentAssistant.finish = "error" - currentAssistant.error = event.properties.error - }) - break - case "session.next.text.started": - update(event.properties.sessionID, (draft) => { - ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({ - type: "text", - id: event.properties.textID, - text: "", - }) - }) - break - case "session.next.text.delta": - update(event.properties.sessionID, (draft) => { - const match = latestText( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.textID, - ) - if (match) match.text += event.properties.delta - }) - break - case "session.next.text.ended": - update(event.properties.sessionID, (draft) => { - const match = latestText( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.textID, - ) - if (match) match.text = event.properties.text - }) - break - case "session.next.tool.input.started": - update(event.properties.sessionID, (draft) => { - ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({ - type: "tool", - id: event.properties.callID, - name: event.properties.name, - time: { created: event.properties.timestamp }, - state: { status: "pending", input: "" }, - }) - }) - break - case "session.next.tool.input.delta": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (match?.state.status === "pending") match.state.input += event.properties.delta - }) - break - case "session.next.tool.input.ended": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (match?.state.status === "pending") match.state.input = event.properties.text - }) - break - case "session.next.tool.called": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (!match) return - match.time.ran = event.properties.timestamp - match.provider = event.properties.provider - match.state = { status: "running", input: event.properties.input, structured: {}, content: [] } - }) - break - case "session.next.tool.progress": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (match?.state.status !== "running") return - match.state.structured = event.properties.structured - match.state.content = [...event.properties.content] - }) - break - case "session.next.tool.success": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (match?.state.status !== "running") return - match.state = { - status: "completed", - input: match.state.input, - structured: event.properties.structured, - content: [...event.properties.content], - result: event.properties.result, - } - match.provider = { - executed: event.properties.provider.executed || match.provider?.executed === true, - metadata: match.provider?.metadata, - resultMetadata: event.properties.provider.metadata, - } - match.time.completed = event.properties.timestamp - }) - break - case "session.next.tool.failed": - update(event.properties.sessionID, (draft) => { - const match = latestTool( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.callID, - ) - if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return - match.state = { - status: "error", - error: event.properties.error, - input: typeof match.state.input === "string" ? {} : match.state.input, - structured: match.state.status === "running" ? match.state.structured : {}, - content: match.state.status === "running" ? match.state.content : [], - result: event.properties.result, - } - match.provider = { - executed: event.properties.provider.executed || match.provider?.executed === true, - metadata: match.provider?.metadata, - resultMetadata: event.properties.provider.metadata, - } - match.time.completed = event.properties.timestamp - }) - break - case "session.next.reasoning.started": - update(event.properties.sessionID, (draft) => { - ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({ - type: "reasoning", - id: event.properties.reasoningID, - text: "", - providerMetadata: event.properties.providerMetadata, - }) - }) - break - case "session.next.reasoning.delta": - update(event.properties.sessionID, (draft) => { - const match = latestReasoning( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.reasoningID, - ) - if (match) match.text += event.properties.delta - }) - break - case "session.next.reasoning.ended": - update(event.properties.sessionID, (draft) => { - const match = latestReasoning( - ownedAssistant(draft, event.properties.assistantMessageID), - event.properties.reasoningID, - ) - if (match) { - match.text = event.properties.text - if (event.properties.providerMetadata !== undefined) - match.providerMetadata = event.properties.providerMetadata - } - }) - break - case "session.next.retried": - case "session.next.compaction.started": - case "session.next.compaction.delta": - break - case "session.next.compaction.ended": - update(event.properties.sessionID, (draft) => { - prepend(draft, { - id: event.properties.messageID, - type: "compaction", - reason: event.properties.reason, - summary: event.properties.text, - recent: event.properties.recent, - time: { created: event.properties.timestamp }, - }) - }) - break - case "reference.updated": - void syncReferences() - break - } - } - - event.subscribe((event) => { - if (duplicate(event.id)) return - if ("sessionID" in event.properties && typeof event.properties.sessionID === "string") - buffering.get(event.properties.sessionID)?.push(event) - apply(event) - }) - - const result = { - data: store, - session: { - message: { - sync, - fromSession(sessionID: string) { - const messages = store.messages[sessionID] - if (!messages) return [] - return messages - }, - }, - }, - } - - return result - }, -}) diff --git a/packages/tui/src/feature-plugins/builtins.ts b/packages/tui/src/feature-plugins/builtins.ts index f0ed7ab899..b67923f3c5 100644 --- a/packages/tui/src/feature-plugins/builtins.ts +++ b/packages/tui/src/feature-plugins/builtins.ts @@ -10,7 +10,6 @@ import SidebarTodo from "./sidebar/todo" import DiffViewer from "./system/diff-viewer" import Notifications from "./system/notifications" import PluginManager from "./system/plugins" -import SessionV2Debug from "./system/session-v2" import WhichKey from "./system/which-key" export type BuiltinTuiPlugin = Omit & { @@ -33,6 +32,5 @@ export function createBuiltinPlugins(options: { experimentalEventSystem: boolean PluginManager, WhichKey, DiffViewer, - ...(options.experimentalEventSystem ? [SessionV2Debug] : []), ] } diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index 41bee5da5a..ae6a24658e 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -4,19 +4,21 @@ import { createMemo, Match, Show, Switch } from "solid-js" import { abbreviateHome } from "../../runtime" import { useTuiPaths } from "../../context/runtime" import { useHomeSessionDestination } from "../../routes/home/session-destination" +import { useData } from "../../context/data" const id = "internal:home-footer" function Directory(props: { api: TuiPluginApi }) { const theme = () => props.api.theme.current const destination = useHomeSessionDestination() + const data = useData() const paths = useTuiPaths() const dir = createMemo(() => { const selected = destination?.destination() - if (!selected || selected.type === "new") return - const out = abbreviateHome(selected.directory, paths.home) + const directory = !selected || selected.type === "new" ? data.location.default().directory : selected.directory + const out = abbreviateHome(directory, paths.home) const branch = - selected.directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined + directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined if (branch) return out + ":" + branch return out }) diff --git a/packages/tui/src/feature-plugins/system/session-v2.tsx b/packages/tui/src/feature-plugins/system/session-v2.tsx deleted file mode 100644 index 8a03ec5dd0..0000000000 --- a/packages/tui/src/feature-plugins/system/session-v2.tsx +++ /dev/null @@ -1,1196 +0,0 @@ -import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui" -import type { BuiltinTuiPlugin } from "../builtins" -import { useSyncV2 } from "../../context/sync-v2" -import { SplitBorder } from "../../ui/border" -import { Spinner } from "../../component/spinner" -import { useTheme } from "../../context/theme" -import { useLocal } from "../../context/local" -import { reasoningSummary, useThinkingMode } from "../../context/thinking" -import { useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid" -import { RGBA, TextAttributes, type BoxRenderable, type SyntaxStyle } from "@opentui/core" -import { useBindings } from "../../keymap" -import { Locale } from "../../util/locale" -import { useTuiPaths } from "../../context/runtime" -import { LANGUAGE_EXTENSIONS } from "../../util/filetype" -import { toolDisplayMetadata, webSearchProviderLabel } from "../../util/tool-display" -import path from "path" -import stripAnsi from "strip-ansi" -import type { - SessionMessage, - SessionMessageAgentSwitched, - SessionMessageAssistant, - SessionMessageAssistantReasoning, - SessionMessageAssistantText, - SessionMessageAssistantTool, - SessionMessageCompaction, - SessionMessageModelSwitched, - SessionMessageShell, - SessionMessageUser, - ToolFileContent, - ToolTextContent, -} from "@opencode-ai/sdk/v2" -import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from "solid-js" -import { collapseToolOutput } from "../../util/collapse-tool-output" -import { setPreLayoutSiblingMargin } from "../../util/layout" - -const id = "internal:session-v2-debug" -const route = "session.v2.messages" - -function currentSessionID(api: TuiPluginApi) { - const current = api.route.current - if (current.name !== "session") return - const sessionID = current.params?.sessionID - return typeof sessionID === "string" ? sessionID : undefined -} - -function View(props: { api: TuiPluginApi; sessionID: string }) { - const sync = useSyncV2() - const dimensions = useTerminalDimensions() - const { theme, syntax, subtleSyntax } = useTheme() - const messages = createMemo(() => sync.data.messages[props.sessionID] ?? []) - const renderedMessages = createMemo(() => messages().toReversed()) - const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant")) - const lastUserCreated = (index: number) => - renderedMessages() - .slice(0, index) - .findLast((message) => message.type === "user")?.time.created - - createEffect(() => { - void sync.session.message.sync(props.sessionID) - }) - - useBindings(() => ({ - bindings: [ - { - key: "escape", - desc: "Back to session", - group: "Session", - cmd() { - props.api.route.navigate("session", { sessionID: props.sessionID }) - }, - }, - ], - })) - - return ( - - - - - - - - - - {(message, index) => ( - - - - - - - - - <>> - - - <>> - - - - - - - - - - - - - - - - - - )} - - - - - - - ) -} - -function MissingData(props: { label: string; detail: string }) { - const { theme } = useTheme() - return ( - - - MISSING DATA {props.label} - - {props.detail} - - ) -} - -function UserMessage(props: { message: SessionMessageUser; index: number }) { - const { theme } = useTheme() - const attachments = createMemo(() => [...(props.message.files ?? []), ...(props.message.agents ?? [])]) - return ( - - {props.message.text} - - - - {(file) => ( - - {file.mime} - {file.name ?? file.uri} - - )} - - - {(agent) => ( - - agent - {agent.name} - - )} - - - - - ) -} - -function ShellMessage(props: { message: SessionMessageShell }) { - const { theme } = useTheme() - const dimensions = useTerminalDimensions() - const output = createMemo(() => stripAnsi(props.message.output.trim())) - const [expanded, setExpanded] = createSignal(false) - const maxLines = 10 - const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6)) - const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars())) - const limited = createMemo(() => { - if (expanded() || !collapsed().overflow) return output() - return collapsed().output - }) - return ( - setExpanded((prev) => !prev) : undefined} - > - - $ {props.message.command} - - {limited()} - - - {expanded() ? "Click to collapse" : "Click to expand"} - - - - ) -} - -function CompactionMessage(props: { message: SessionMessageCompaction }) { - const { theme } = useTheme() - return ( - - ) -} - -function AgentSwitchedMessage(props: { message: SessionMessageAgentSwitched }) { - const { theme } = useTheme() - const local = useLocal() - return ( - - - ▣ - Switched agent to - {Locale.titlecase(props.message.agent)} - - - ) -} - -function ModelSwitchedMessage(props: { message: SessionMessageModelSwitched }) { - const { theme } = useTheme() - const model = createMemo(() => { - const variant = props.message.model.variant ? `/${props.message.model.variant}` : "" - return `${props.message.model.providerID}/${props.message.model.id}${variant}` - }) - return ( - - - ◇ - Switched model to - {model()} - - - ) -} - -function UnknownMessage(props: { message: SessionMessage }) { - return -} - -function AssistantMessage(props: { - message: SessionMessageAssistant - sessionID: string - last: boolean - syntax: SyntaxStyle - subtleSyntax: SyntaxStyle - start?: number -}) { - const { theme } = useTheme() - const local = useLocal() - const duration = createMemo(() => { - if (!props.message.time.completed) return 0 - return props.message.time.completed - (props.start ?? props.message.time.created) - }) - const model = createMemo(() => { - const variant = props.message.model.variant ? `/${props.message.model.variant}` : "" - return `${props.message.model.providerID}/${props.message.model.id}${variant}` - }) - const final = createMemo(() => props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish)) - return ( - <> - - {(part) => ( - - - - - - props.message.time.completed} - /> - - - - - - )} - - - - - - - {props.message.error} - - - - - - ▣ - {Locale.titlecase(props.message.agent)} - · {model()} - - · {Locale.duration(duration())} - - - - - > - ) -} - -function AssistantText(props: { part: SessionMessageAssistantText; syntax: SyntaxStyle }) { - const { theme } = useTheme() - return ( - - - - - - ) -} - -function AssistantReasoning(props: { - part: SessionMessageAssistantReasoning - subtleSyntax: SyntaxStyle - completedAt: () => number | undefined -}) { - const { theme } = useTheme() - const thinking = useThinkingMode() - const [expanded, setExpanded] = createSignal(false) - const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim()) - const inMinimal = createMemo(() => thinking.mode() === "hide") - // v2 reasoning parts have no per-part `time.end` (see SessionMessageAssistantReasoning - // in the v2 SDK); we settle on parent-message completion instead. - const isDone = createMemo(() => props.completedAt() !== undefined) - const summary = createMemo(() => reasoningSummary(content())) - - const toggle = () => { - if (!inMinimal()) return - setExpanded((prev) => !prev) - } - - return ( - - - - - - - - - - - - - ) -} - -function ReasoningHeader(props: { toggleable: boolean; open: boolean; done: boolean; title: string | null }) { - const { theme } = useTheme() - const fg = () => - props.open - ? RGBA.fromValues(theme.warning.r, theme.warning.g, theme.warning.b, theme.thinkingOpacity) - : theme.warning - - return ( - - - - {props.title ? "Thinking: " + props.title : "Thinking"} - - - - - - {props.open ? "- " : "+ "} - - Thought - - : - {props.title} - - - - - ) -} - -function AssistantTool(props: { part: SessionMessageAssistantTool; sessionID: string }) { - const input = createMemo(() => toolInputRecord(props.part.state.input)) - const toolprops = { - get input() { - return input() - }, - get metadata() { - return toolDisplayMetadata(props.part.state) - }, - get output() { - return props.part.state.status === "pending" ? undefined : toolOutput(props.part.state.content) - }, - sessionID: props.sessionID, - part: props.part, - } - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -} - -type ToolProps = { - input: Record - metadata: Record - output?: string - sessionID: string - part: SessionMessageAssistantTool -} - -function GenericTool(props: ToolProps) { - const { theme } = useTheme() - const dimensions = useTerminalDimensions() - const output = createMemo(() => props.output?.trim() ?? "") - const [expanded, setExpanded] = createSignal(false) - const maxLines = 3 - const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6)) - const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars())) - const limited = createMemo(() => { - if (expanded() || !collapsed().overflow) return output() - return collapsed().output - }) - return ( - - {props.part.name} {input(props.input)} - - } - > - setExpanded((prev) => !prev) : undefined} - > - - {limited()} - - {expanded() ? "Click to collapse" : "Click to expand"} - - - - - ) -} - -function InlineTool(props: { - icon: string - complete: unknown - pending: string - spinner?: boolean - children: JSX.Element - part: SessionMessageAssistantTool -}) { - const { theme } = useTheme() - const renderer = useRenderer() - const [hover, setHover] = createSignal(false) - const [showError, setShowError] = createSignal(false) - const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined)) - const complete = createMemo(() => !!props.complete) - const denied = createMemo(() => { - const message = error() - if (!message) return false - return ( - message.includes("QuestionRejectedError") || - message.includes("rejected permission") || - message.includes("specified a rule") || - message.includes("user dismissed") - ) - }) - const fg = createMemo(() => { - if (error()) return theme.error - if (complete()) return theme.textMuted - return theme.text - }) - const attributes = createMemo(() => (denied() ? TextAttributes.STRIKETHROUGH : undefined)) - return ( - error() && setHover(true)} - onMouseOut={() => setHover(false)} - onMouseUp={() => { - if (!error()) return - if (renderer.getSelection()?.getSelectedText()) return - setShowError((prev) => !prev) - }} - ref={(el: BoxRenderable) => { - setPreLayoutSiblingMargin(el, (previous) => (previous?.id.startsWith("text-") ? 1 : 0)) - }} - > - - - - - - - - {props.icon} - - - - - ~ - - - - - - - - - - {props.children} - - - - - {props.pending} - - - - - - - {error()} - - - - - ) -} - -function BlockTool(props: { - title: string - children: JSX.Element - part?: SessionMessageAssistantTool - onClick?: () => void - spinner?: boolean -}) { - const { theme } = useTheme() - const renderer = useRenderer() - const [hover, setHover] = createSignal(false) - const error = createMemo(() => (props.part?.state.status === "error" ? props.part.state.error.message : undefined)) - return ( - props.onClick && setHover(true)} - onMouseOut={() => setHover(false)} - onMouseUp={() => { - if (renderer.getSelection()?.getSelectedText()) return - props.onClick?.() - }} - flexShrink={0} - > - - {props.title} - - } - > - {props.title.replace(/^# /, "")} - - {props.children} - - {error()} - - - ) -} - -function Bash(props: ToolProps) { - const { theme } = useTheme() - const dimensions = useTerminalDimensions() - const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim())) - const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part)) - const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`) - const [expanded, setExpanded] = createSignal(false) - const maxLines = 10 - const maxChars = createMemo(() => maxLines * Math.max(20, dimensions().width - 6)) - const collapsed = createMemo(() => collapseToolOutput(output(), maxLines, maxChars())) - const limited = createMemo(() => { - if (expanded() || !collapsed().overflow) return output() - return collapsed().output - }) - return ( - - - setExpanded((prev) => !prev) : undefined} - > - - $ {command()} - {limited()} - - {expanded() ? "Click to collapse" : "Click to expand"} - - - - - - - {command()} - - - - ) -} - -function Glob(props: ToolProps) { - const normalizePath = usePathNormalizer() - return ( - - Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} - in {normalizePath(stringValue(props.input.path))} - - {(count) => ( - <> - ({count()} {count() === 1 ? "match" : "matches"}) - > - )} - - - ) -} - -function Read(props: ToolProps) { - const normalizePath = usePathNormalizer() - const { theme } = useTheme() - const loaded = createMemo(() => - arrayValue(props.metadata.loaded).filter((item): item is string => typeof item === "string"), - ) - return ( - <> - - Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "} - {input(props.input, ["filePath"])} - - - {(filepath) => ( - - - ↳ Loaded {normalizePath(filepath)} - - - )} - - > - ) -} - -function Grep(props: ToolProps) { - const normalizePath = usePathNormalizer() - return ( - - Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "} - in {normalizePath(stringValue(props.input.path))} - - {(matches) => ( - <> - ({matches()} {matches() === 1 ? "match" : "matches"}) - > - )} - - - ) -} - -function WebFetch(props: ToolProps) { - return ( - - WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)} - - ) -} - -function WebSearch(props: ToolProps) { - const label = createMemo(() => webSearchProviderLabel(props.metadata.provider)) - return ( - - {label()} "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "} - {(results) => <>({results()} results)>} - - ) -} - -function Write(props: ToolProps) { - const normalizePath = usePathNormalizer() - const { theme, syntax } = useTheme() - const filePath = createMemo(() => stringValue(props.input.filePath) ?? "") - const content = createMemo(() => stringValue(props.input.content) ?? "") - return ( - - - - - - - - - - - - Write {normalizePath(filePath())} - - - - ) -} - -function Edit(props: ToolProps) { - const normalizePath = usePathNormalizer() - const { theme, syntax } = useTheme() - const dimensions = useTerminalDimensions() - const filePath = createMemo(() => stringValue(props.input.filePath) ?? "") - const diff = createMemo(() => stringValue(props.metadata.diff)) - return ( - - - {(diff) => ( - - - 120 ? "split" : "unified"} - filetype={filetype(filePath())} - syntaxStyle={syntax()} - showLineNumbers={true} - width="100%" - wrapMode="word" - fg={theme.text} - addedBg={theme.diffAddedBg} - removedBg={theme.diffRemovedBg} - contextBg={theme.diffContextBg} - addedSignColor={theme.diffHighlightAdded} - removedSignColor={theme.diffHighlightRemoved} - lineNumberFg={theme.diffLineNumber} - lineNumberBg={theme.diffContextBg} - addedLineNumberBg={theme.diffAddedLineNumberBg} - removedLineNumberBg={theme.diffRemovedLineNumberBg} - /> - - - - )} - - - - Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })} - - - - ) -} - -function ApplyPatch(props: ToolProps) { - const normalizePath = usePathNormalizer() - const { theme, syntax } = useTheme() - const dimensions = useTerminalDimensions() - const files = createMemo(() => arrayValue(props.metadata.files).flatMap((item) => (isRecord(item) ? [item] : []))) - const fileTitle = (file: Record) => { - const type = stringValue(file.type) - const relativePath = stringValue(file.relativePath) ?? stringValue(file.filePath) ?? "patch" - if (type === "delete") return "# Deleted " + relativePath - if (type === "add") return "# Created " + relativePath - if (type === "move") return "# Moved " + normalizePath(stringValue(file.filePath)) + " → " + relativePath - return "← Patched " + relativePath - } - return ( - - 0}> - - {(file) => ( - - - -{numberValue(file.deletions) ?? 0} line{numberValue(file.deletions) === 1 ? "" : "s"} - - } - > - {(patch) => ( - - 120 ? "split" : "unified"} - filetype={filetype(stringValue(file.filePath) ?? stringValue(file.relativePath))} - syntaxStyle={syntax()} - showLineNumbers={true} - width="100%" - wrapMode="word" - fg={theme.text} - addedBg={theme.diffAddedBg} - removedBg={theme.diffRemovedBg} - contextBg={theme.diffContextBg} - addedSignColor={theme.diffHighlightAdded} - removedSignColor={theme.diffHighlightRemoved} - lineNumberFg={theme.diffLineNumber} - lineNumberBg={theme.diffContextBg} - addedLineNumberBg={theme.diffAddedLineNumberBg} - removedLineNumberBg={theme.diffRemovedLineNumberBg} - /> - - )} - - - )} - - - - - Patch - - - - ) -} - -function TodoWrite(props: ToolProps) { - const { theme } = useTheme() - const todos = createMemo(() => arrayValue(props.input.todos).flatMap((item) => (isRecord(item) ? [item] : []))) - return ( - - 0 && props.part.state.status === "completed"}> - - - - {(todo) => ( - - {todoIcon(stringValue(todo.status))} {stringValue(todo.content)} - - )} - - - - - - - Updating todos... - - - - ) -} - -function Question(props: ToolProps) { - const { theme } = useTheme() - const questions = createMemo(() => - arrayValue(props.input.questions).flatMap((item) => (isRecord(item) ? [item] : [])), - ) - const answers = createMemo(() => arrayValue(props.metadata.answers)) - return ( - - 0}> - - - - {(question, index) => ( - - {stringValue(question.question)} - {formatAnswer(answers()[index()])} - - )} - - - - - - - Asked {questions().length} question{questions().length === 1 ? "" : "s"} - - - - ) -} - -function Skill(props: ToolProps) { - return ( - - Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}" - - ) -} - -function Task(props: ToolProps) { - const content = createMemo(() => { - const description = stringValue(props.input.description) - if (!description) return pendingInput(props.part) - return `${Locale.titlecase(stringValue(props.input.subagent_type) ?? "General")} Task — ${description}` - }) - return ( - - {content()} - - ) -} - -function Diagnostics(props: { diagnostics: unknown; filePath: string }) { - const normalizePath = usePathNormalizer() - const { theme } = useTheme() - const errors = createMemo(() => { - if (!isRecord(props.diagnostics)) return [] - const value = props.diagnostics[normalizePath(props.filePath)] ?? props.diagnostics[props.filePath] - return arrayValue(value) - .flatMap((item) => (isRecord(item) ? [item] : [])) - .filter((diagnostic) => diagnostic.severity === 1) - .slice(0, 3) - }) - return ( - - - - {(diagnostic) => Error {stringValue(diagnostic.message)}} - - - - ) -} - -function toolOutput(content?: Array) { - return (content ?? []) - .map((item) => { - if (item.type === "text") return item.text.trim() - const source = item.uri - return `[file ${item.name ?? source}]` - }) - .filter(Boolean) - .join("\n") -} - -function toolInputRecord(input: string | Record) { - if (typeof input === "string") return {} - return input -} - -function pendingInput(part: SessionMessageAssistantTool) { - if (part.state.status !== "pending") return "" - return part.state.input.trim() -} - -function toolComplete(part: SessionMessageAssistantTool) { - if (part.state.status === "pending") return pendingInput(part) - return part.state.status === "completed" || part.state.status === "error" || part.state.status === "running" -} - -function stringValue(value: unknown) { - return typeof value === "string" ? value : undefined -} - -function numberValue(value: unknown) { - return typeof value === "number" ? value : undefined -} - -function arrayValue(value: unknown): unknown[] { - return Array.isArray(value) ? value : [] -} - -function isRecord(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value) -} - -function input(input: Record, omit?: string[]) { - const primitives = Object.entries(input).filter(([key, value]) => { - if (omit?.includes(key)) return false - return typeof value === "string" || typeof value === "number" || typeof value === "boolean" - }) - if (primitives.length === 0) return "" - return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]` -} - -function usePathNormalizer() { - const cwd = useTuiPaths().cwd - return (input?: string) => normalizePath(input, cwd) -} - -function normalizePath(input: string | undefined, cwd: string) { - if (!input) return "" - const absolute = path.isAbsolute(input) ? input : path.resolve(cwd, input) - const relative = path.relative(cwd, absolute) - if (!relative) return "." - if (!relative.startsWith("..")) return relative - return absolute -} - -function filetype(input?: string) { - if (!input) return "none" - const language = LANGUAGE_EXTENSIONS[path.extname(input)] - if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript" - return language -} - -function todoIcon(status?: string) { - if (status === "completed") return "✓" - if (status === "in_progress") return "~" - if (status === "cancelled") return "✕" - return "☐" -} - -function formatAnswer(answer: unknown) { - if (!Array.isArray(answer)) return "(no answer)" - if (answer.length === 0) return "(no answer)" - return answer.filter((item): item is string => typeof item === "string").join(", ") -} - -const tui: TuiPlugin = async (api) => { - api.route.register([ - { - name: route, - render(input) { - const sessionID = input.params?.sessionID - if (typeof sessionID !== "string") { - return Missing sessionID - } - return - }, - }, - ]) - - api.keymap.registerLayer({ - commands: [ - { - name: route, - title: "View v2 session messages", - category: "Debug", - namespace: "palette", - suggested: () => api.route.current.name === "session", - enabled: () => api.route.current.name === "session", - run() { - const sessionID = currentSessionID(api) - if (!sessionID) return - api.route.navigate(route, { sessionID }) - api.ui.dialog.clear() - }, - }, - ], - }) -} - -const plugin: BuiltinTuiPlugin = { - id, - tui, -} - -export default plugin diff --git a/packages/tui/src/routes/home/session-destination.tsx b/packages/tui/src/routes/home/session-destination.tsx index 352010840b..1e4c650327 100644 --- a/packages/tui/src/routes/home/session-destination.tsx +++ b/packages/tui/src/routes/home/session-destination.tsx @@ -7,8 +7,7 @@ import { type ParentProps, type Setter, } from "solid-js" -import { useSync } from "../../context/sync" -import { useTuiPaths } from "../../context/runtime" +import { useData } from "../../context/data" export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new" } @@ -21,11 +20,10 @@ type Context = { const HomeSessionDestinationContext = createContext() export function HomeSessionDestinationProvider(props: ParentProps) { - const sync = useSync() - const paths = useTuiPaths() + const data = useData() const [selected, setDestination] = createSignal() const destination = createMemo( - () => selected() ?? { type: "directory", directory: sync.path.directory || paths.cwd, subdirectory: false }, + () => selected() ?? { type: "directory", directory: data.location.default().directory, subdirectory: false }, ) return ( boolean, timeout = 2000) { + const start = Date.now() + while (!fn()) { + if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") + await Bun.sleep(10) + } +} + +function global(payload: Event): GlobalEvent { + return { directory, project: "proj_test", payload } +} + +function emitEvent(events: ReturnType, payload: Event) { + events.emit(global(payload)) +} + +test("refreshes resources into reactive getters", async () => { + const location = { + directory, + project: { id: "proj_test", directory }, + } + const calls = createFetch((url) => { + if (url.pathname === "/api/session/ses_test") + return json({ + data: { + id: "ses_test", + projectID: "proj_test", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 0, updated: 0 }, + title: "Test session", + location: { directory }, + }, + }) + if (url.pathname === "/api/agent") + return json({ + location, + data: [ + { id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }, + ], + }) + return undefined + }) + const events = createEventSource() + let data!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + data = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + expect(data.location.default()).toEqual({ directory }) + expect(data.session.get("ses_test")).toBeUndefined() + expect(data.location.agent.list(location)).toBeUndefined() + + await data.session.refresh("ses_test") + await data.location.agent.refresh() + + expect(data.session.get("ses_test")?.title).toBe("Test session") + expect(data.location.default()).toEqual({ directory, workspaceID: undefined }) + expect(data.location.agent.list(location)?.map((agent) => agent.id)).toEqual(["build"]) + } finally { + app.renderer.destroy() + } +}) + +test("refreshes references after updates", async () => { + const events = createEventSource() + let requests = 0 + const calls = createFetch((url) => { + if (url.pathname !== "/api/reference") return + requests++ + return json({ + location: { directory, project: { id: "proj_test", directory } }, + data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }], + }) + }) + let data!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + data = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + await wait(() => requests === 1) + emitEvent(events, { id: "evt_reference_1", type: "reference.updated", properties: {} }) + await wait(() => data.location.reference.list()?.length === 1) + expect(data.location.reference.list()?.[0]?.name).toBe("docs") + } finally { + app.renderer.destroy() + } +}) + +test("settles pending tools when a live failure arrives", async () => { + const events = createEventSource() + const calls = createFetch() + let sync!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + sync = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + emitEvent(events, { + id: "evt_agent_1", + type: "session.next.agent.switched", + properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" }, + }) + emitEvent(events, { + id: "evt_model_1", + type: "session.next.model.switched", + properties: { + sessionID: "session-1", + messageID: "msg_model_1", + timestamp: 0, + model: { id: "model-1", providerID: "provider-1" }, + }, + }) + emitEvent(events, { + id: "evt_step_started_1", + type: "session.next.step.started", + properties: { + sessionID: "session-1", + assistantMessageID: "msg_explicit_assistant_9", + timestamp: 1, + agent: "build", + model: { id: "model-1", providerID: "provider-1" }, + }, + }) + emitEvent(events, { + id: "evt_input_1", + type: "session.next.tool.input.started", + properties: { + sessionID: "session-1", + assistantMessageID: "msg_explicit_assistant_9", + timestamp: 2, + callID: "call-1", + name: "bash", + }, + }) + emitEvent(events, { + id: "evt_called_1", + type: "session.next.tool.called", + properties: { + sessionID: "session-1", + timestamp: 2, + assistantMessageID: "msg_explicit_assistant_9", + callID: "call-1", + tool: "bash", + input: {}, + provider: { executed: false, metadata: { fake: { call: true } } }, + }, + }) + emitEvent(events, { + id: "evt_failed_1", + type: "session.next.tool.failed", + properties: { + sessionID: "session-1", + timestamp: 3, + assistantMessageID: "msg_explicit_assistant_9", + callID: "call-1", + error: { type: "unknown", message: "aborted" }, + provider: { executed: false, metadata: { fake: { result: true } } }, + }, + }) + + await wait(() => { + const assistant = sync.session.message.list("session-1")?.[0] + return ( + assistant?.type === "assistant" && + assistant.content[0]?.type === "tool" && + assistant.content[0].state.status === "error" + ) + }) + + const assistant = sync.session.message.list("session-1")?.[0] + expect(assistant?.type).toBe("assistant") + if (assistant?.type !== "assistant") return + expect(assistant.id).toBe("msg_explicit_assistant_9") + const tool = assistant.content[0] + expect(tool?.type).toBe("tool") + if (tool?.type !== "tool") return + expect(tool.state.status).toBe("error") + if (tool.state.status !== "error") return + expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" }) + expect(tool.state.input).toEqual({}) + expect(tool.state.structured).toEqual({}) + expect(tool.state.content).toEqual([]) + expect(tool.provider).toEqual({ + executed: false, + metadata: { fake: { call: true } }, + resultMetadata: { fake: { result: true } }, + }) + expect((sync.session.message.list("session-1") ?? []).map((message) => message.type)).toEqual([ + "assistant", + "model-switched", + "agent-switched", + ]) + } finally { + app.renderer.destroy() + } +}) + +test("renders admitted prompts only after promotion", async () => { + const events = createEventSource() + const calls = createFetch() + let sync!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + sync = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + emitEvent(events, { + id: "evt_admitted_1", + type: "session.next.prompt.admitted", + properties: { + sessionID: "session-1", + messageID: "msg_user_1", + timestamp: 0, + prompt: { text: "hello" }, + delivery: "steer", + }, + }) + expect(sync.session.message.list("session-1") ?? []).toEqual([]) + + emitEvent(events, { + id: "evt_promoted_1", + type: "session.next.prompt.promoted", + properties: { + sessionID: "session-1", + messageID: "msg_user_1", + timestamp: 1, + prompt: { text: "hello" }, + timeCreated: 0, + }, + }) + + await wait(() => sync.session.message.list("session-1")?.length === 1) + const message = sync.session.message.list("session-1")?.[0] + expect(message?.type).toBe("user") + if (message?.type !== "user") return + expect(message).toMatchObject({ id: "msg_user_1", text: "hello" }) + } finally { + app.renderer.destroy() + } +}) + +test("renders a promoted prompt when admission was missed", async () => { + const events = createEventSource() + const calls = createFetch() + let sync!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + sync = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + emitEvent(events, { + id: "evt_promoted_1", + type: "session.next.prompt.promoted", + properties: { + sessionID: "session-1", + messageID: "msg_user_1", + timestamp: 1, + prompt: { text: "hello" }, + timeCreated: 0, + }, + }) + + await wait(() => sync.session.message.list("session-1")?.length === 1) + expect(sync.session.message.list("session-1")?.[0]?.id).toBe("msg_user_1") + } finally { + app.renderer.destroy() + } +}) + +test("projects live context updates with their message ID", async () => { + const events = createEventSource() + const calls = createFetch() + let sync!: ReturnType + let ready!: () => void + const mounted = new Promise((resolve) => { + ready = resolve + }) + + function Probe() { + sync = useData() + onMount(ready) + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await mounted + emitEvent(events, { + id: "evt_context_1", + type: "session.next.context.updated", + properties: { + sessionID: "session-1", + messageID: "msg_context_1", + timestamp: 1, + text: "Updated context", + }, + }) + + await wait(() => sync.session.message.list("session-1")?.length === 1) + expect(sync.session.message.list("session-1")?.[0]).toMatchObject({ + id: "msg_context_1", + type: "system", + text: "Updated context", + }) + } finally { + app.renderer.destroy() + } +}) diff --git a/packages/tui/test/cli/tui/sync-v2.test.tsx b/packages/tui/test/cli/tui/sync-v2.test.tsx deleted file mode 100644 index 7d6ba5f835..0000000000 --- a/packages/tui/test/cli/tui/sync-v2.test.tsx +++ /dev/null @@ -1,619 +0,0 @@ -/** @jsxImportSource @opentui/solid */ -import { expect, test } from "bun:test" -import { testRender } from "@opentui/solid" -import type { Event, GlobalEvent } from "@opencode-ai/sdk/v2" -import { onMount } from "solid-js" -import { ProjectProvider } from "../../../src/context/project" -import { SDKProvider } from "../../../src/context/sdk" -import { SyncProviderV2, useSyncV2 } from "../../../src/context/sync-v2" -import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk" -import { TestTuiContexts } from "../../fixture/tui-environment" - -async function wait(fn: () => boolean, timeout = 2000) { - const start = Date.now() - while (!fn()) { - if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") - await Bun.sleep(10) - } -} - -function global(payload: Event): GlobalEvent { - return { directory, project: "proj_test", payload } -} - -function emitTwice(events: ReturnType, payload: Event) { - const event = global(payload) - events.emit(event) - events.emit(event) -} - -test("sync v2 refreshes references after updates", async () => { - const events = createEventSource() - let requests = 0 - const calls = createFetch((url) => { - if (url.pathname !== "/api/reference") return - requests++ - return json({ - location: { directory, project: { id: "proj_test", directory } }, - data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }], - }) - }) - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - await wait(() => requests === 1) - events.emit(global({ id: "evt_reference_1", type: "reference.updated", properties: {} })) - await wait(() => sync.data.reference.length === 1) - expect(sync.data.reference[0]?.name).toBe("docs") - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 settles pending tools when a live failure arrives", async () => { - const events = createEventSource() - const calls = createFetch() - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_agent_1", - type: "session.next.agent.switched", - properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" }, - }) - emitTwice(events, { - id: "evt_model_1", - type: "session.next.model.switched", - properties: { - sessionID: "session-1", - messageID: "msg_model_1", - timestamp: 0, - model: { id: "model-1", providerID: "provider-1" }, - }, - }) - emitTwice(events, { - id: "evt_step_started_1", - type: "session.next.step.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_explicit_assistant_9", - timestamp: 1, - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - }, - }) - emitTwice(events, { - id: "evt_input_1", - type: "session.next.tool.input.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_explicit_assistant_9", - timestamp: 2, - callID: "call-1", - name: "bash", - }, - }) - emitTwice(events, { - id: "evt_called_1", - type: "session.next.tool.called", - properties: { - sessionID: "session-1", - timestamp: 2, - assistantMessageID: "msg_explicit_assistant_9", - callID: "call-1", - tool: "bash", - input: {}, - provider: { executed: false, metadata: { fake: { call: true } } }, - }, - }) - emitTwice(events, { - id: "evt_failed_1", - type: "session.next.tool.failed", - properties: { - sessionID: "session-1", - timestamp: 3, - assistantMessageID: "msg_explicit_assistant_9", - callID: "call-1", - error: { type: "unknown", message: "aborted" }, - provider: { executed: false, metadata: { fake: { result: true } } }, - }, - }) - - await wait(() => { - const assistant = sync.session.message.fromSession("session-1")[0] - return ( - assistant?.type === "assistant" && - assistant.content[0]?.type === "tool" && - assistant.content[0].state.status === "error" - ) - }) - - const assistant = sync.session.message.fromSession("session-1")[0] - expect(assistant?.type).toBe("assistant") - if (assistant?.type !== "assistant") return - expect(assistant.id).toBe("msg_explicit_assistant_9") - const tool = assistant.content[0] - expect(tool?.type).toBe("tool") - if (tool?.type !== "tool") return - expect(tool.state.status).toBe("error") - if (tool.state.status !== "error") return - expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" }) - expect(tool.state.input).toEqual({}) - expect(tool.state.structured).toEqual({}) - expect(tool.state.content).toEqual([]) - expect(tool.provider).toEqual({ - executed: false, - metadata: { fake: { call: true } }, - resultMetadata: { fake: { result: true } }, - }) - expect(sync.session.message.fromSession("session-1").map((message) => message.type)).toEqual([ - "assistant", - "model-switched", - "agent-switched", - ]) - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 renders admitted prompts only after promotion", async () => { - const events = createEventSource() - const calls = createFetch() - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_admitted_1", - type: "session.next.prompt.admitted", - properties: { - sessionID: "session-1", - messageID: "msg_user_1", - timestamp: 0, - prompt: { text: "hello" }, - delivery: "steer", - }, - }) - expect(sync.session.message.fromSession("session-1")).toEqual([]) - - emitTwice(events, { - id: "evt_promoted_1", - type: "session.next.prompt.promoted", - properties: { - sessionID: "session-1", - messageID: "msg_user_1", - timestamp: 1, - prompt: { text: "hello" }, - timeCreated: 0, - }, - }) - - await wait(() => sync.session.message.fromSession("session-1").length === 1) - const message = sync.session.message.fromSession("session-1")[0] - expect(message?.type).toBe("user") - if (message?.type !== "user") return - expect(message).toMatchObject({ id: "msg_user_1", text: "hello" }) - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 renders a promoted prompt when admission was missed", async () => { - const events = createEventSource() - const calls = createFetch() - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_promoted_1", - type: "session.next.prompt.promoted", - properties: { - sessionID: "session-1", - messageID: "msg_user_1", - timestamp: 1, - prompt: { text: "hello" }, - timeCreated: 0, - }, - }) - - await wait(() => sync.session.message.fromSession("session-1").length === 1) - expect(sync.session.message.fromSession("session-1")[0]?.id).toBe("msg_user_1") - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 projects live context updates with their message ID", async () => { - const events = createEventSource() - const calls = createFetch() - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_context_1", - type: "session.next.context.updated", - properties: { - sessionID: "session-1", - messageID: "msg_context_1", - timestamp: 1, - text: "Updated context", - }, - }) - - await wait(() => sync.session.message.fromSession("session-1").length === 1) - expect(sync.session.message.fromSession("session-1")[0]).toMatchObject({ - id: "msg_context_1", - type: "system", - text: "Updated context", - }) - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 preserves live events while snapshot hydration is in flight", async () => { - const events = createEventSource() - const response = Promise.withResolvers() - const calls = createFetch((url) => { - if (url.pathname === "/api/session/session-1/message") return response.promise - return undefined - }) - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - const hydration = sync.session.message.sync("session-1") - emitTwice(events, { - id: "evt_agent_1", - type: "session.next.agent.switched", - properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" }, - }) - response.resolve(json({ data: [] })) - await hydration - - expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([ - ["msg_agent_1", "agent-switched"], - ]) - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 replaces stale cached rows while preserving in-flight live rows", async () => { - const events = createEventSource() - const response = Promise.withResolvers() - const calls = createFetch((url) => { - if (url.pathname === "/api/session/session-1/message") return response.promise - return undefined - }) - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_promoted_1", - type: "session.next.prompt.promoted", - properties: { - sessionID: "session-1", - messageID: "msg_user_1", - timestamp: 1, - prompt: { text: "stale" }, - timeCreated: 0, - }, - }) - await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_user_1") - const hydration = sync.session.message.sync("session-1") - emitTwice(events, { - id: "evt_agent_1", - type: "session.next.agent.switched", - properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 2, agent: "build" }, - }) - await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_agent_1") - response.resolve( - json({ - data: [{ id: "msg_user_1", type: "user", text: "fresh", time: { created: 0 } }], - }), - ) - await hydration - - expect(sync.session.message.fromSession("session-1").map((message) => [message.id, message.type])).toEqual([ - ["msg_agent_1", "agent-switched"], - ["msg_user_1", "user"], - ]) - expect(sync.session.message.fromSession("session-1")[1]).toMatchObject({ text: "fresh" }) - } finally { - app.renderer.destroy() - } -}) - -test("sync v2 preserves snapshot order and metadata for in-flight updates", async () => { - const events = createEventSource() - const response = Promise.withResolvers() - const calls = createFetch((url) => { - if (url.pathname === "/api/session/session-1/message") return response.promise - return undefined - }) - let sync!: ReturnType - let ready!: () => void - const mounted = new Promise((resolve) => { - ready = resolve - }) - - function Probe() { - sync = useSyncV2() - onMount(ready) - return - } - - const app = await testRender(() => ( - - - - - - - - - - )) - - try { - await mounted - emitTwice(events, { - id: "evt_step_older", - type: "session.next.step.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_assistant_older", - timestamp: 0, - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - emitTwice(events, { - id: "evt_step_1", - type: "session.next.step.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_assistant_old", - timestamp: 1, - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - await wait(() => sync.session.message.fromSession("session-1")[0]?.id === "msg_assistant_old") - const hydration = sync.session.message.sync("session-1") - emitTwice(events, { - id: "evt_text_1", - type: "session.next.text.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_assistant_old", - timestamp: 2, - textID: "text-1", - }, - }) - emitTwice(events, { - id: "evt_text_older", - type: "session.next.text.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_assistant_older", - timestamp: 2, - textID: "text-older", - }, - }) - await wait(() => { - const messages = sync.session.message.fromSession("session-1") - return messages.every((message) => message.type !== "assistant" || message.content[0]?.type === "text") - }) - response.resolve( - json({ - data: [ - { - id: "msg_assistant_new", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [], - time: { created: 3 }, - }, - { - id: "msg_assistant_old", - type: "assistant", - metadata: { source: "snapshot" }, - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [], - time: { created: 1 }, - }, - ], - }), - ) - await hydration - emitTwice(events, { - id: "evt_step_late_duplicate", - type: "session.next.step.started", - properties: { - sessionID: "session-1", - assistantMessageID: "msg_assistant_old", - timestamp: 1, - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - - expect(sync.session.message.fromSession("session-1").map((message) => message.id)).toEqual([ - "msg_assistant_new", - "msg_assistant_old", - "msg_assistant_older", - ]) - expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[1]))).toMatchObject({ - metadata: { source: "snapshot" }, - content: [{ type: "text", id: "text-1", text: "" }], - }) - expect(JSON.parse(JSON.stringify(sync.session.message.fromSession("session-1")[2]))).toMatchObject({ - content: [{ type: "text", id: "text-older", text: "" }], - }) - } finally { - app.renderer.destroy() - } -}) diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index d18228fcaf..9866b91322 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -59,6 +59,13 @@ export function createFetch(override?: FetchHandler) { if (url.pathname === "/config/providers") return json({ providers: {}, default: {} }) if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory }) + if (url.pathname === "/api/location") + return json({ directory, project: { id: "proj_test", directory: worktree } }) + if (["/api/agent", "/api/model", "/api/provider", "/api/command", "/api/skill"].includes(url.pathname)) + return json({ + location: { directory, project: { id: "proj_test", directory: worktree } }, + data: [], + }) if (url.pathname === "/project/current") return json({ id: "proj_test" }) if (url.pathname === "/api/reference") return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] }) From 51b10b128e1a8a9cff19718bef0c40b12b389e5a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 11 Jun 2026 03:36:12 +0000 Subject: [PATCH 044/157] chore: generate --- packages/tui/src/component/dialog-model.tsx | 4 +--- packages/tui/src/context/data.tsx | 9 ++++----- packages/tui/src/context/local.tsx | 4 +++- packages/tui/src/feature-plugins/home/footer.tsx | 3 +-- packages/tui/test/cli/tui/data.test.tsx | 4 +--- packages/tui/test/fixture/tui-sdk.ts | 3 +-- 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/tui/src/component/dialog-model.tsx b/packages/tui/src/component/dialog-model.tsx index b540e86b27..1cfd8586c6 100644 --- a/packages/tui/src/component/dialog-model.tsx +++ b/packages/tui/src/component/dialog-model.tsx @@ -88,9 +88,7 @@ export function DialogModel(props: { providerID?: string }) { filter((option) => { if (!showSections) return true if ( - favorites.some( - (item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID, - ) + favorites.some((item) => item.providerID === option.value.providerID && item.modelID === option.value.modelID) ) return false if ( diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 23df05c5f7..bc398181f9 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -555,11 +555,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ result.location.reference.refresh(), result.location.command.refresh(), result.location.skill.refresh(), - ]) - .then((settled) => { - for (const failure of settled.filter((item) => item.status === "rejected")) - console.error("Failed to refresh default location data", failure.reason) - }) + ]).then((settled) => { + for (const failure of settled.filter((item) => item.status === "rejected")) + console.error("Failed to refresh default location data", failure.reason) + }) }) return result diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index f82ea7d1bd..cfeb6b9fd4 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -231,7 +231,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ } } - const model = models().find((item) => item.enabled && providers().some((provider) => provider.id === item.providerID)) + const model = models().find( + (item) => item.enabled && providers().some((provider) => provider.id === item.providerID), + ) if (!model) return undefined return { providerID: model.providerID, diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx index ae6a24658e..88b49b003e 100644 --- a/packages/tui/src/feature-plugins/home/footer.tsx +++ b/packages/tui/src/feature-plugins/home/footer.tsx @@ -17,8 +17,7 @@ function Directory(props: { api: TuiPluginApi }) { const selected = destination?.destination() const directory = !selected || selected.type === "new" ? data.location.default().directory : selected.directory const out = abbreviateHome(directory, paths.home) - const branch = - directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined + const branch = directory === (props.api.state.path.directory || paths.cwd) ? props.api.state.vcs?.branch : undefined if (branch) return out + ":" + branch return out }) diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index dce3131576..af9f65cbf1 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -46,9 +46,7 @@ test("refreshes resources into reactive getters", async () => { if (url.pathname === "/api/agent") return json({ location, - data: [ - { id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }, - ], + data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }], }) return undefined }) diff --git a/packages/tui/test/fixture/tui-sdk.ts b/packages/tui/test/fixture/tui-sdk.ts index 9866b91322..e0bd361636 100644 --- a/packages/tui/test/fixture/tui-sdk.ts +++ b/packages/tui/test/fixture/tui-sdk.ts @@ -59,8 +59,7 @@ export function createFetch(override?: FetchHandler) { if (url.pathname === "/config/providers") return json({ providers: {}, default: {} }) if (url.pathname === "/experimental/console") return json({ consoleManagedProviders: [], switchableOrgCount: 0 }) if (url.pathname === "/path") return json({ home: "", state: "", config: "", worktree, directory }) - if (url.pathname === "/api/location") - return json({ directory, project: { id: "proj_test", directory: worktree } }) + if (url.pathname === "/api/location") return json({ directory, project: { id: "proj_test", directory: worktree } }) if (["/api/agent", "/api/model", "/api/provider", "/api/command", "/api/skill"].includes(url.pathname)) return json({ location: { directory, project: { id: "proj_test", directory: worktree } }, From bf05e8a1224d6560f7a441f70d09e0c77e50e931 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:12:02 -0500 Subject: [PATCH 045/157] fix(mcp): preserve headers during auth and debug (#31802) --- packages/opencode/src/cli/cmd/mcp.ts | 2 ++ packages/opencode/src/mcp/index.ts | 5 ++++- packages/opencode/test/mcp/oauth-browser.test.ts | 13 +++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index 3a49337044..8fa6459ce2 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -744,6 +744,7 @@ export const McpDebugCommand = effectCmd({ const response = await fetch(serverConfig.url, { method: "POST", headers: { + ...serverConfig.headers, "Content-Type": "application/json", Accept: "application/json, text/event-stream", }, @@ -792,6 +793,7 @@ export const McpDebugCommand = effectCmd({ // Try creating transport with auth provider to trigger discovery const transport = new StreamableHTTPClientTransport(new URL(serverConfig.url), { authProvider, + requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined, }) try { diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 5a73322510..75da812348 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -741,7 +741,10 @@ export const layer = Layer.effect( auth, ) - const transport = new StreamableHTTPClientTransport(url, { authProvider }) + const transport = new StreamableHTTPClientTransport(url, { + authProvider, + requestInit: mcpConfig.headers ? { headers: mcpConfig.headers } : undefined, + }) return yield* Effect.tryPromise({ try: () => { diff --git a/packages/opencode/test/mcp/oauth-browser.test.ts b/packages/opencode/test/mcp/oauth-browser.test.ts index 03660af2bf..ab807e4391 100644 --- a/packages/opencode/test/mcp/oauth-browser.test.ts +++ b/packages/opencode/test/mcp/oauth-browser.test.ts @@ -38,7 +38,7 @@ class MockUnauthorizedError extends Error { const transportCalls: Array<{ type: "streamable" | "sse" url: string - options: { authProvider?: unknown } + options: { authProvider?: unknown; requestInit?: RequestInit } }> = [] // Mock the transport constructors @@ -46,7 +46,10 @@ void mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ StreamableHTTPClientTransport: class MockStreamableHTTP { url: string authProvider: { redirectToAuthorization?: (url: URL) => Promise } | undefined - constructor(url: URL, options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise } }) { + constructor( + url: URL, + options?: { authProvider?: { redirectToAuthorization?: (url: URL) => Promise }; requestInit?: RequestInit }, + ) { this.url = url.toString() this.authProvider = options?.authProvider transportCalls.push({ @@ -127,11 +130,12 @@ const mcpTest = testEffect( ) const service = MCP.Service as unknown as Effect.Effect -const config = (name: string) => ({ +const config = (name: string, headers?: Record) => ({ mcp: { [name]: { type: "remote" as const, url: "https://example.com/mcp", + headers, }, }, }) @@ -227,6 +231,7 @@ mcpTest.instance( expect(failure).toEqual(Option.none()) expect(typeof url).toBe("string") expect(url).toContain("https://") + expect(transportCalls.at(-1)?.options.requestInit?.headers).toEqual({ "X-Custom-Header": "custom-value" }) }), - { config: config("test-oauth-server-3") }, + { config: config("test-oauth-server-3", { "X-Custom-Header": "custom-value" }) }, ) From dac0dd5309d0a960e76b2783074c965380c69fb6 Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 11 Jun 2026 02:31:17 -0400 Subject: [PATCH 046/157] feat(core): add connector authentication (#31837) --- .../20260611035744_credential/migration.sql | 12 + .../20260611035744_credential/snapshot.json | 2190 +++++++++++++++++ packages/core/src/auth.ts | 340 --- packages/core/src/catalog.ts | 59 +- packages/core/src/connector.ts | 496 ++++ packages/core/src/connector/schema.ts | 9 + packages/core/src/credential.ts | 329 +++ packages/core/src/credential/sql.ts | 21 + packages/core/src/database/migration.gen.ts | 1 + .../migration/20260611035744_credential.ts | 23 + packages/core/src/location-layer.ts | 6 +- packages/core/src/plugin.ts | 8 - packages/core/src/plugin/account.ts | 45 - packages/core/src/plugin/agent.ts | 2 +- packages/core/src/plugin/boot.ts | 15 +- packages/core/src/plugin/models-dev.ts | 20 + .../plugin/provider/cloudflare-ai-gateway.ts | 2 +- .../plugin/provider/cloudflare-workers-ai.ts | 7 +- .../core/src/plugin/provider/openai-auth.ts | 251 ++ packages/core/src/plugin/provider/openai.ts | 7 + packages/core/src/plugin/provider/opencode.ts | 2 +- packages/core/src/provider.ts | 5 +- packages/core/test/account.test.ts | 284 --- packages/core/test/catalog.test.ts | 52 +- packages/core/test/connector.test.ts | 403 +++ packages/core/test/credential.test.ts | 207 ++ packages/core/test/location-layer.test.ts | 8 +- .../core/test/plugin/fixtures/models-dev.json | 14 + packages/core/test/plugin/models-dev.test.ts | 70 + .../core/test/plugin/provider-azure.test.ts | 33 +- .../provider-cloudflare-workers-ai.test.ts | 40 +- .../core/test/plugin/provider-gitlab.test.ts | 50 +- packages/core/test/plugin/provider-helper.ts | 9 + .../core/test/plugin/provider-openai.test.ts | 39 +- .../test/plugin/provider-opencode.test.ts | 5 +- .../test/server/httpapi-exercise/index.ts | 43 + .../server/httpapi-public-openapi.test.ts | 24 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 310 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 527 +++- packages/server/src/api.ts | 2 + packages/server/src/groups/connector.ts | 133 + packages/server/src/handlers.ts | 2 + packages/server/src/handlers/connector.ts | 102 + packages/tui/src/context/data.tsx | 26 +- packages/tui/src/context/event.ts | 3 +- packages/tui/test/cli/tui/data.test.tsx | 57 + packages/tui/test/fixture/tui-sdk.ts | 2 +- 47 files changed, 5433 insertions(+), 862 deletions(-) create mode 100644 packages/core/migration/20260611035744_credential/migration.sql create mode 100644 packages/core/migration/20260611035744_credential/snapshot.json delete mode 100644 packages/core/src/auth.ts create mode 100644 packages/core/src/connector.ts create mode 100644 packages/core/src/connector/schema.ts create mode 100644 packages/core/src/credential.ts create mode 100644 packages/core/src/credential/sql.ts create mode 100644 packages/core/src/database/migration/20260611035744_credential.ts delete mode 100644 packages/core/src/plugin/account.ts create mode 100644 packages/core/src/plugin/provider/openai-auth.ts delete mode 100644 packages/core/test/account.test.ts create mode 100644 packages/core/test/connector.test.ts create mode 100644 packages/core/test/credential.test.ts create mode 100644 packages/core/test/plugin/fixtures/models-dev.json create mode 100644 packages/core/test/plugin/models-dev.test.ts create mode 100644 packages/server/src/groups/connector.ts create mode 100644 packages/server/src/handlers/connector.ts diff --git a/packages/core/migration/20260611035744_credential/migration.sql b/packages/core/migration/20260611035744_credential/migration.sql new file mode 100644 index 0000000000..c950306e46 --- /dev/null +++ b/packages/core/migration/20260611035744_credential/migration.sql @@ -0,0 +1,12 @@ +CREATE TABLE `credential` ( + `id` text PRIMARY KEY, + `connector_id` text NOT NULL, + `method_id` text NOT NULL, + `label` text NOT NULL, + `value` text NOT NULL, + `active` integer DEFAULT false NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `credential_connector_active_idx` ON `credential` (`connector_id`) WHERE "credential"."active" = 1; diff --git a/packages/core/migration/20260611035744_credential/snapshot.json b/packages/core/migration/20260611035744_credential/snapshot.json new file mode 100644 index 0000000000..13cc247abf --- /dev/null +++ b/packages/core/migration/20260611035744_credential/snapshot.json @@ -0,0 +1,2190 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "f25f9126-c7dc-4882-9ff4-af27e11d2da1", + "prevIds": [ + "d1bfa125-b81e-4c61-9b6e-e74abf6e488f" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "credential", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "connector_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "method_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "label", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "active", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'build'", + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "replacement_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "revision", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "credential_pk", + "table": "credential", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "connector_id", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"credential\".\"active\" = 1", + "origin": "manual", + "name": "credential_connector_active_idx", + "entityType": "indexes", + "table": "credential" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + } + ], + "renames": [] +} diff --git a/packages/core/src/auth.ts b/packages/core/src/auth.ts deleted file mode 100644 index f5f23c6e54..0000000000 --- a/packages/core/src/auth.ts +++ /dev/null @@ -1,340 +0,0 @@ -export * as Auth from "./auth" - -import path from "path" -import { Effect, Layer, Option, Schema, Context, SynchronizedRef } from "effect" -import { Identifier } from "./util/identifier" -import { NonNegativeInt, withStatics } from "./schema" -import { Global } from "./global" -import { FSUtil } from "./fs-util" -import { EventV2 } from "./event" - -export const ID = Schema.String.pipe( - Schema.brand("Auth.ID"), - withStatics((schema) => ({ create: () => schema.make("acc_" + Identifier.ascending()) })), -) -export type ID = typeof ID.Type - -export const ServiceID = Schema.String.pipe(Schema.brand("ServiceID")) -export type ServiceID = typeof ServiceID.Type - -export const OrgID = Schema.String.pipe(Schema.brand("OrgID")) -export type OrgID = typeof OrgID.Type -export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken")) -export type AccessToken = typeof AccessToken.Type -export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken")) -export type RefreshToken = typeof RefreshToken.Type - -export class OAuthCredential extends Schema.Class("Auth.OAuthCredential")({ - type: Schema.Literal("oauth"), - refresh: Schema.String, - access: Schema.String, - expires: NonNegativeInt, -}) {} - -export class ApiKeyCredential extends Schema.Class("Auth.ApiKeyCredential")({ - type: Schema.Literal("api"), - key: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), -}) {} - -export const Credential = Schema.Union([OAuthCredential, ApiKeyCredential]) - .pipe(Schema.toTaggedUnion("type")) - .annotate({ - identifier: "Auth.Credential", - }) -export type Credential = Schema.Schema.Type - -export class Info extends Schema.Class("Auth.Info")({ - id: ID, - serviceID: ServiceID, - description: Schema.String, - credential: Credential, -}) {} - -export class FileWriteError extends Schema.TaggedErrorClass()("Auth.FileWriteError", { - operation: Schema.Union([Schema.Literal("migrate"), Schema.Literal("write")]), - cause: Schema.Defect, -}) {} - -export type Error = FileWriteError - -export const Event = { - Added: EventV2.define({ - type: "account.added", - schema: { - account: Info, - }, - }), - Removed: EventV2.define({ - type: "account.removed", - schema: { - account: Info, - }, - }), - Switched: EventV2.define({ - type: "account.switched", - schema: { - serviceID: ServiceID, - from: Schema.optional(ID), - to: Schema.optional(ID), - }, - }), -} - -interface Writable { - version: 2 - accounts: Record - active: Record -} - -const decodeV1 = Schema.decodeUnknownOption(Schema.Record(Schema.String, Credential)) - -function migrate(old: Record): Writable { - const accounts: Record = {} - const active: Record = {} - for (const [serviceID, value] of Object.entries(old)) { - const decoded = Option.getOrElse(decodeV1({ [serviceID]: value }), () => ({})) - const parsed = (decoded as Record)[serviceID] - if (!parsed) continue - const id = Identifier.ascending() - const account = ID.make(id) - const brandedServiceID = ServiceID.make(serviceID) - accounts[id] = new Info({ - id: account, - serviceID: brandedServiceID, - description: "default", - credential: parsed, - }) - active[brandedServiceID] = account - } - return { version: 2, accounts, active } -} - -export interface Interface { - readonly get: (id: ID) => Effect.Effect - readonly all: () => Effect.Effect - readonly create: (input: { - serviceID: ServiceID - credential: Credential - description?: string - }) => Effect.Effect - readonly update: (id: ID, updates: Partial>) => Effect.Effect - readonly remove: (id: ID) => Effect.Effect - readonly activate: (id: ID) => Effect.Effect - readonly active: (serviceID: ServiceID) => Effect.Effect - readonly activeAll: () => Effect.Effect, Error> - readonly forService: (serviceID: ServiceID) => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/Account") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const fsys = yield* FSUtil.Service - const global = yield* Global.Service - const events = yield* EventV2.Service - const file = path.join(global.data, "account.json") - const legacyFile = path.join(global.data, "auth.json") - - const writeMigrated = Effect.fnUntraced(function* (raw: Record) { - const migrated = migrate(raw) - yield* fsys - .writeJson(file, migrated, 0o600) - .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "migrate", cause }))) - return migrated - }) - - const parseAuthContent = () => { - try { - return JSON.parse(process.env.OPENCODE_AUTH_CONTENT ?? "") - } catch {} - } - - const load: () => Effect.Effect = Effect.fnUntraced(function* () { - if (process.env.OPENCODE_AUTH_CONTENT) { - const raw = parseAuthContent() - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - return { version: 2, accounts: {}, active: {} } - } - - const legacy = yield* fsys.readJson(legacyFile).pipe(Effect.orElseSucceed(() => null)) - if (legacy && typeof legacy === "object") return yield* writeMigrated(legacy as Record) - - const raw = yield* fsys.readJson(file).pipe(Effect.orElseSucceed(() => null)) - - if (raw && typeof raw === "object") { - if ("version" in raw && raw.version === 2) return raw as Writable - return yield* writeMigrated(raw as Record) - } - - return { version: 2, accounts: {}, active: {} } - }) - - const write = (data: Writable) => - fsys - .writeJson(file, data, 0o600) - .pipe(Effect.mapError((cause) => new FileWriteError({ operation: "write", cause }))) - - const state = SynchronizedRef.makeUnsafe( - yield* load().pipe(Effect.orElseSucceed((): Writable => ({ version: 2, accounts: {}, active: {} }))), - ) - - const activate = Effect.fn("Auth.activate")(function* (id: ID) { - const data = yield* SynchronizedRef.get(state) - const account = data.accounts[id] - if (!account) return - const activated = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const nextAccount = data.accounts[id] - if (!nextAccount) return [undefined, data] as const - - const next = { ...data, active: { ...data.active, [nextAccount.serviceID]: id } } - yield* write(next) - return [{ serviceID: nextAccount.serviceID, from: data.active[nextAccount.serviceID], to: id }, next] as const - }), - ) - if (activated) yield* events.publish(Event.Switched, activated) - }) - - const result: Interface = { - get: Effect.fn("Auth.get")(function* (id) { - return (yield* SynchronizedRef.get(state)).accounts[id] - }), - - all: Effect.fn("Auth.all")(function* () { - return Object.values((yield* SynchronizedRef.get(state)).accounts) - }), - - active: Effect.fn("Auth.active")(function* (serviceID) { - const data = yield* SynchronizedRef.get(state) - return ( - data.accounts[data.active[serviceID]] ?? Object.values(data.accounts).find((a) => a.serviceID === serviceID) - ) - }), - - activeAll: Effect.fn("Auth.activeAll")(function* () { - const data = yield* SynchronizedRef.get(state) - const result = new Map() - for (const account of Object.values(data.accounts)) { - if (!result.has(account.serviceID)) result.set(account.serviceID, account) - } - for (const [serviceID, id] of Object.entries(data.active)) { - const account = data.accounts[id] - if (account) result.set(ServiceID.make(serviceID), account) - } - return result - }), - - forService: Effect.fn("Auth.list")(function* (serviceID) { - return Object.values((yield* SynchronizedRef.get(state)).accounts).filter((a) => a.serviceID === serviceID) - }), - - create: Effect.fn("Auth.add")(function* (input) { - const id = ID.make(Identifier.ascending()) - const account = new Info({ - id, - serviceID: input.serviceID, - description: input.description ?? "default", - credential: input.credential, - }) - const added = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const next = { - ...data, - accounts: { ...data.accounts, [account.id]: account }, - active: { ...data.active, [account.serviceID]: account.id }, - } - - yield* write(next) - return [ - { - account, - switched: { serviceID: account.serviceID, from: data.active[account.serviceID], to: account.id }, - }, - next, - ] as const - }), - ) - yield* events.publish(Event.Added, { account: added.account }) - yield* events.publish(Event.Switched, added.switched) - return added.account - }), - - update: Effect.fn("Auth.update")(function* (id, updates) { - const existing = (yield* SynchronizedRef.get(state)).accounts[id] - if (!existing) return - yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - if (!data.accounts[id]) return [undefined, data] as const - - const next = { - ...data, - accounts: { - ...data.accounts, - [id]: new Info({ - id, - serviceID: existing.serviceID, - description: updates.description ?? existing.description, - credential: updates.credential ?? existing.credential, - }), - }, - } - - yield* write(next) - return [undefined, next] as const - }), - ) - }), - - remove: Effect.fn("Auth.remove")(function* (id) { - const removed = yield* SynchronizedRef.modifyEffect( - state, - Effect.fnUntraced(function* (data) { - const accounts = { ...data.accounts } - const active = { ...data.active } - const removed = accounts[id] - if (!removed) return [undefined, data] as const - const wasActive = active[removed.serviceID] === id - delete accounts[id] - const replacement = Object.values(accounts).find((account) => account.serviceID === removed.serviceID) - if (wasActive) { - if (replacement) active[removed.serviceID] = replacement.id - else delete active[removed.serviceID] - } - - const next = { ...data, accounts, active } - yield* write(next) - return [ - { - account: removed, - switched: wasActive ? { serviceID: removed.serviceID, from: id, to: replacement?.id } : undefined, - }, - next, - ] as const - }), - ) - if (removed) { - yield* events.publish(Event.Removed, { account: removed.account }) - if (removed.switched) yield* events.publish(Event.Switched, removed.switched) - } - }), - - activate, - } - - return Service.of(result) - }), -) - -export const defaultLayer = layer.pipe( - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Global.defaultLayer), - Layer.provide(EventV2.defaultLayer), -) diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index c44eec5d83..6d41dd2a65 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -10,6 +10,8 @@ import { Location } from "./location" import { EventV2 } from "./event" import { Policy } from "./policy" import { State } from "./state" +import { Credential } from "./credential" +import { ConnectorSchema } from "./connector/schema" export type ProviderRecord = { provider: ProviderV2.Info @@ -94,10 +96,26 @@ export const layer = Layer.effect( const plugin = yield* PluginV2.Service const events = yield* EventV2.Service const policy = yield* Policy.Service + const credentials = yield* Credential.Service const scope = yield* Scope.Scope - const resolve = (model: ModelV2.Info) => { - const provider = state.get().providers.get(model.providerID)!.provider + const project = (provider: ProviderV2.Info, active: Map) => { + const credential = active.get(ConnectorSchema.ID.make(provider.id)) + if (!credential) return provider + const body = { ...provider.request.body } + if (credential.value.type === "key") { + body.apiKey = credential.value.key + Object.assign(body, credential.value.metadata ?? {}) + } + if (credential.value.type === "oauth") body.apiKey = credential.value.access + return new ProviderV2.Info({ + ...provider, + enabled: { via: "credential", credentialID: credential.id }, + request: { ...provider.request, body }, + }) + } + + const resolve = (model: ModelV2.Info, provider: ProviderV2.Info) => { const api = model.api.type === "native" && !model.api.url && Object.keys(model.api.settings).length === 0 ? { ...provider.api, id: model.api.id } @@ -193,8 +211,7 @@ export const layer = Layer.effect( } }), }) - const available = (model: ModelV2.Info) => - state.get().providers.get(model.providerID)?.provider.enabled !== false && model.enabled + const active = () => credentials.activeAll().pipe(Effect.orDie) yield* events.subscribe(PluginV2.Event.Added).pipe( // Plugin registries are location scoped even though the event bus is process scoped. @@ -214,17 +231,16 @@ export const layer = Layer.effect( provider: { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { const record = yield* getRecord(providerID) - return record.provider + return project(record.provider, yield* active()) }), all: Effect.fn("CatalogV2.provider.all")(function* () { - return Array.fromIterable(state.get().providers.values()).map((record) => record.provider) + const credentials = yield* active() + return Array.fromIterable(state.get().providers.values()).map((record) => project(record.provider, credentials)) }), available: Effect.fn("CatalogV2.provider.available")(function* () { - return Array.fromIterable(state.get().providers.values()) - .map((record) => record.provider) - .filter((provider) => provider.enabled) + return (yield* result.provider.all()).filter((provider) => provider.enabled) }), }, @@ -233,29 +249,35 @@ export const layer = Layer.effect( const record = yield* getRecord(providerID) const model = record.models.get(modelID) if (!model) return yield* new ModelNotFoundError({ providerID, modelID }) - return resolve(model) + return resolve(model, project(record.provider, yield* active())) }), all: Effect.fn("CatalogV2.model.all")(function* () { + const credentials = yield* active() return pipe( Array.fromIterable(state.get().providers.values()), - Array.flatMap((record) => Array.fromIterable(record.models.values())), - Array.map(resolve), + Array.flatMap((record) => { + const provider = project(record.provider, credentials) + return Array.fromIterable(record.models.values()).map((model) => resolve(model, provider)) + }), Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)), ) }), available: Effect.fn("CatalogV2.model.available")(function* () { - return (yield* result.model.all()).filter(available) + const providers = new Map((yield* result.provider.all()).map((provider) => [provider.id, provider])) + return (yield* result.model.all()).filter( + (model) => providers.get(model.providerID)?.enabled !== false && model.enabled, + ) }), default: Effect.fn("CatalogV2.model.default")(function* () { const defaultModel = state.get().defaultModel if (defaultModel) { - const provider = state.get().providers.get(defaultModel.providerID)?.provider - if (provider?.enabled !== false) { + const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option) + if (Option.isSome(provider) && provider.value.enabled !== false) { const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option) - if (Option.isSome(model) && available(model.value)) return model + if (Option.isSome(model) && model.value.enabled) return model } } @@ -269,10 +291,11 @@ export const layer = Layer.effect( small: Effect.fn("CatalogV2.model.small")(function* (providerID) { const record = state.get().providers.get(providerID) if (!record) return Option.none() + const provider = project(record.provider, yield* active()) if (providerID === ProviderV2.ID.opencode) { const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano")) - if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano)) + if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(resolve(gpt5Nano, provider)) } const candidates = pipe( @@ -300,7 +323,7 @@ export const layer = Layer.effect( return pipe( items, Array.sortWith((item) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2, Order.Number), - Array.map((item) => resolve(item.model)), + Array.map((item) => resolve(item.model, provider)), Array.head, ) } diff --git a/packages/core/src/connector.ts b/packages/core/src/connector.ts new file mode 100644 index 0000000000..f17dbbd339 --- /dev/null +++ b/packages/core/src/connector.ts @@ -0,0 +1,496 @@ +export * as Connector from "./connector" + +import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect" +import { castDraft, enableMapSet, type Draft } from "immer" +import { Credential } from "./credential" +import { ConnectorSchema } from "./connector/schema" +import { withStatics } from "./schema" +import { State } from "./state" +import { Identifier } from "./util/identifier" +import { KeyedMutex } from "./effect/keyed-mutex" +import { EventV2 } from "./event" + +export const ID = ConnectorSchema.ID +export type ID = ConnectorSchema.ID + +export const MethodID = ConnectorSchema.MethodID +export type MethodID = ConnectorSchema.MethodID + +export const AttemptID = Schema.String.pipe( + Schema.brand("Connector.AttemptID"), + withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })), +) +export type AttemptID = typeof AttemptID.Type + +export const When = Schema.Struct({ + key: Schema.String, + op: Schema.Literals(["eq", "neq"]), + value: Schema.String, +}).annotate({ identifier: "Connector.When" }) +export type When = typeof When.Type + +export class TextPrompt extends Schema.Class("Connector.TextPrompt")({ + type: Schema.Literal("text"), + key: Schema.String, + message: Schema.String, + placeholder: Schema.optional(Schema.String), + when: Schema.optional(When), +}) {} + +export class SelectPrompt extends Schema.Class("Connector.SelectPrompt")({ + type: Schema.Literal("select"), + key: Schema.String, + message: Schema.String, + options: Schema.Array( + Schema.Struct({ + label: Schema.String, + value: Schema.String, + hint: Schema.optional(Schema.String), + }), + ), + when: Schema.optional(When), +}) {} + +export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) +export type Prompt = typeof Prompt.Type + +export class OAuthMethod extends Schema.Class("Connector.OAuthMethod")({ + id: MethodID, + type: Schema.Literal("oauth"), + label: Schema.String, + prompts: Schema.optional(Schema.Array(Prompt)), +}) {} + +export class KeyMethod extends Schema.Class("Connector.KeyMethod")({ + id: MethodID, + type: Schema.Literal("key"), + label: Schema.String, + prompts: Schema.optional(Schema.Array(Prompt)), +}) {} + +export const Method = Schema.Union([OAuthMethod, KeyMethod]).pipe(Schema.toTaggedUnion("type")) +export type Method = typeof Method.Type + +export class Info extends Schema.Class("Connector.Info")({ + id: ID, + name: Schema.String, + methods: Schema.Array(Method), +}) {} + +export type Inputs = Readonly<{ [key: string]: string }> + +export type OAuthAuthorization = { + readonly url: string + readonly instructions: string +} & ( + | { + readonly mode: "auto" + readonly callback: Effect.Effect + } + | { + readonly mode: "code" + readonly callback: (code: string) => Effect.Effect + } +) + +export interface OAuthImplementation { + readonly connectorID: ID + readonly method: OAuthMethod + readonly authorize: (inputs: Inputs) => Effect.Effect + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect +} + +export interface KeyImplementation { + readonly connectorID: ID + readonly method: KeyMethod + readonly authorize: (key: string, inputs: Inputs) => Effect.Effect +} + +export type Implementation = OAuthImplementation | KeyImplementation + +function isKeyImplementation(implementation: Implementation): implementation is KeyImplementation { + return implementation.method.type === "key" +} + +function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation { + return implementation.method.type === "oauth" +} + +export class Attempt extends Schema.Class("Connector.Attempt")({ + attemptID: AttemptID, + url: Schema.String, + instructions: Schema.String, + mode: Schema.Literals(["auto", "code"]), + time: Schema.Struct({ + created: Schema.Number, + expires: Schema.Number, + }), +}) {} + +const Time = Schema.Struct({ + created: Schema.Number, + expires: Schema.Number, +}) + +export const AttemptStatus = Schema.Union([ + Schema.Struct({ status: Schema.Literal("pending"), time: Time }), + Schema.Struct({ status: Schema.Literal("complete"), time: Time }), + Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }), + Schema.Struct({ status: Schema.Literal("expired"), time: Time }), +]).pipe(Schema.toTaggedUnion("status")) +export type AttemptStatus = typeof AttemptStatus.Type + +export class CodeRequiredError extends Schema.TaggedErrorClass()("Connector.CodeRequired", { + attemptID: AttemptID, +}) {} + +export class AuthorizationError extends Schema.TaggedErrorClass()("Connector.Authorization", { + cause: Schema.Defect, +}) {} + +export type Error = CodeRequiredError | AuthorizationError + +export const Event = { + Updated: EventV2.define({ + type: "connector.updated", + schema: {}, + }), +} + +type Entry = { + connector: Info + implementations: Map +} + +type Data = { + connectors: Map +} + +export type Editor = { + list: () => readonly Info[] + get: (id: ID) => Info | undefined + update: (id: ID, update: (connector: Draft>) => void) => void + remove: (id: ID) => void + method: { + update: (implementation: Implementation) => void + remove: (connectorID: ID, methodID: MethodID) => void + } +} + +export interface Interface { + /** Registers a scoped transform over the connector registry. */ + readonly transform: State.Interface["transform"] + /** Registers and immediately applies a scoped connector registry update. */ + readonly update: State.Interface["update"] + /** Returns one connector with its serializable login methods. */ + readonly get: (id: ID) => Effect.Effect + /** Returns all connectors with their serializable login methods. */ + readonly list: () => Effect.Effect + /** Refreshes an OAuth credential with its originating method. */ + readonly refresh: (credentialID: Credential.ID) => Effect.Effect