فهرست منبع

Merge dev into sqlite2

Dax Raad 2 ماه پیش
والد
کامیت
4d50a32979
35فایلهای تغییر یافته به همراه518 افزوده شده و 165 حذف شده
  1. 3 3
      AGENTS.md
  2. 1 0
      STATS.md
  3. 29 59
      packages/app/src/components/dialog-settings.tsx
  4. 145 5
      packages/app/src/components/settings-providers.tsx
  5. 40 13
      packages/app/src/context/command.tsx
  6. 1 1
      packages/app/src/context/layout.tsx
  7. 65 10
      packages/app/src/context/notification.tsx
  8. 1 1
      packages/app/src/i18n/ar.ts
  9. 1 1
      packages/app/src/i18n/br.ts
  10. 1 1
      packages/app/src/i18n/da.ts
  11. 1 1
      packages/app/src/i18n/de.ts
  12. 14 1
      packages/app/src/i18n/en.ts
  13. 1 1
      packages/app/src/i18n/es.ts
  14. 1 1
      packages/app/src/i18n/fr.ts
  15. 1 1
      packages/app/src/i18n/ja.ts
  16. 1 1
      packages/app/src/i18n/ko.ts
  17. 1 1
      packages/app/src/i18n/no.ts
  18. 1 1
      packages/app/src/i18n/pl.ts
  19. 1 1
      packages/app/src/i18n/ru.ts
  20. 1 1
      packages/app/src/i18n/zh.ts
  21. 1 1
      packages/app/src/i18n/zht.ts
  22. 6 5
      packages/app/src/pages/home.tsx
  23. 32 27
      packages/app/src/pages/layout.tsx
  24. 3 3
      packages/console/app/src/routes/changelog/index.css
  25. 1 1
      packages/desktop/index.html
  26. 2 2
      packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
  27. 11 12
      packages/opencode/src/provider/provider.ts
  28. 30 0
      packages/opencode/src/server/server.ts
  29. 1 1
      packages/opencode/src/session/prompt.ts
  30. 4 3
      packages/opencode/test/provider/provider.test.ts
  31. 32 0
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  32. 29 0
      packages/sdk/js/src/v2/gen/types.gen.ts
  33. 50 0
      packages/sdk/openapi.json
  34. 2 0
      packages/ui/src/components/tabs.css
  35. 4 6
      packages/web/src/content/docs/providers.mdx

+ 3 - 3
AGENTS.md

@@ -13,9 +13,9 @@
 - Keep things in one function unless composable or reusable
 - Avoid `try`/`catch` where possible
 - Avoid using the `any` type
-- Use Bun APIs when possible (e.g., `Bun.file()` instead of `fs.existsSync()`)
-- For sync file reads, use `readFileSync` from `fs` (Bun.file is async-only)
-- Avoid generated file artifacts - prefer build-time `define` globals for bundled data
+- Prefer single word variable names where possible
+- Use Bun APIs when possible, like `Bun.file()`
+- Rely on type inference when possible; avoid explicit type annotations or interfaces unless necessary for exports or clarity
 
 ### Naming
 

+ 1 - 0
STATS.md

@@ -211,3 +211,4 @@
 | 2026-01-23 | 6,096,236 (+329,896) | 2,096,235 (+66,748)  | 8,192,471 (+396,644) |
 | 2026-01-24 | 6,371,019 (+274,783) | 2,156,870 (+60,635)  | 8,527,889 (+335,418) |
 | 2026-01-25 | 6,639,082 (+268,063) | 2,187,853 (+30,983)  | 8,826,935 (+299,046) |
+| 2026-01-26 | 6,941,620 (+302,538) | 2,232,115 (+44,262)  | 9,173,735 (+346,800) |

+ 29 - 59
packages/app/src/components/dialog-settings.tsx

@@ -21,34 +21,32 @@ export const DialogSettings: Component = () => {
     <Dialog size="x-large">
       <Tabs orientation="vertical" variant="settings" defaultValue="general" class="h-full settings-dialog">
         <Tabs.List>
-          <div
-            style={{
-              display: "flex",
-              "flex-direction": "column",
-              "justify-content": "space-between",
-              height: "100%",
-              width: "100%",
-            }}
-          >
-            <div
-              style={{
-                display: "flex",
-                "flex-direction": "column",
-                gap: "12px",
-                width: "100%",
-                "padding-top": "12px",
-              }}
-            >
-              <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
-              <div style={{ display: "flex", "flex-direction": "column", gap: "6px", width: "100%" }}>
-                <Tabs.Trigger value="general">
-                  <Icon name="sliders" />
-                  {language.t("settings.tab.general")}
-                </Tabs.Trigger>
-                <Tabs.Trigger value="shortcuts">
-                  <Icon name="keyboard" />
-                  {language.t("settings.tab.shortcuts")}
-                </Tabs.Trigger>
+          <div class="flex flex-col justify-between h-full w-full">
+            <div class="flex flex-col gap-3 w-full pt-3">
+              <div class="flex flex-col gap-3">
+                <div class="flex flex-col gap-1.5">
+                  <Tabs.SectionTitle>{language.t("settings.section.desktop")}</Tabs.SectionTitle>
+                  <div class="flex flex-col gap-1.5 w-full">
+                    <Tabs.Trigger value="general">
+                      <Icon name="sliders" />
+                      {language.t("settings.tab.general")}
+                    </Tabs.Trigger>
+                    <Tabs.Trigger value="shortcuts">
+                      <Icon name="keyboard" />
+                      {language.t("settings.tab.shortcuts")}
+                    </Tabs.Trigger>
+                  </div>
+                </div>
+
+                <div class="flex flex-col gap-1.5">
+                  <Tabs.SectionTitle>{language.t("settings.section.server")}</Tabs.SectionTitle>
+                  <div class="flex flex-col gap-1.5 w-full">
+                    <Tabs.Trigger value="providers">
+                      <Icon name="server" />
+                      {language.t("settings.providers.title")}
+                    </Tabs.Trigger>
+                  </div>
+                </div>
               </div>
             </div>
             <div class="flex flex-col gap-1 pl-1 py-1 text-12-medium text-text-weak">
@@ -56,31 +54,6 @@ export const DialogSettings: Component = () => {
               <span class="text-11-regular">v{platform.version}</span>
             </div>
           </div>
-          {/* <Tabs.SectionTitle>Server</Tabs.SectionTitle> */}
-          {/* <Tabs.Trigger value="permissions"> */}
-          {/*   <Icon name="checklist" /> */}
-          {/*   Permissions */}
-          {/* </Tabs.Trigger> */}
-          {/* <Tabs.Trigger value="providers"> */}
-          {/*   <Icon name="server" /> */}
-          {/*   Providers */}
-          {/* </Tabs.Trigger> */}
-          {/* <Tabs.Trigger value="models"> */}
-          {/*   <Icon name="brain" /> */}
-          {/*   Models */}
-          {/* </Tabs.Trigger> */}
-          {/* <Tabs.Trigger value="agents"> */}
-          {/*   <Icon name="task" /> */}
-          {/*   Agents */}
-          {/* </Tabs.Trigger> */}
-          {/* <Tabs.Trigger value="commands"> */}
-          {/*   <Icon name="console" /> */}
-          {/*   Commands */}
-          {/* </Tabs.Trigger> */}
-          {/* <Tabs.Trigger value="mcp"> */}
-          {/*   <Icon name="mcp" /> */}
-          {/*   MCP */}
-          {/* </Tabs.Trigger> */}
         </Tabs.List>
         <Tabs.Content value="general" class="no-scrollbar">
           <SettingsGeneral />
@@ -88,12 +61,9 @@ export const DialogSettings: Component = () => {
         <Tabs.Content value="shortcuts" class="no-scrollbar">
           <SettingsKeybinds />
         </Tabs.Content>
-        {/* <Tabs.Content value="permissions" class="no-scrollbar"> */}
-        {/*   <SettingsPermissions /> */}
-        {/* </Tabs.Content> */}
-        {/* <Tabs.Content value="providers" class="no-scrollbar"> */}
-        {/*   <SettingsProviders /> */}
-        {/* </Tabs.Content> */}
+        <Tabs.Content value="providers" class="no-scrollbar">
+          <SettingsProviders />
+        </Tabs.Content>
         {/* <Tabs.Content value="models" class="no-scrollbar"> */}
         {/*   <SettingsModels /> */}
         {/* </Tabs.Content> */}

+ 145 - 5
packages/app/src/components/settings-providers.tsx

@@ -1,14 +1,154 @@
-import { Component } from "solid-js"
+import { Button } from "@opencode-ai/ui/button"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
+import { Tag } from "@opencode-ai/ui/tag"
+import { showToast } from "@opencode-ai/ui/toast"
+import type { IconName } from "@opencode-ai/ui/icons/provider"
+import { popularProviders, useProviders } from "@/hooks/use-providers"
+import { createMemo, type Component, For, Show } from "solid-js"
 import { useLanguage } from "@/context/language"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { DialogConnectProvider } from "./dialog-connect-provider"
+import { DialogSelectProvider } from "./dialog-select-provider"
+
+type ProviderSource = "env" | "api" | "config" | "custom"
+type ProviderMeta = { source?: ProviderSource }
 
 export const SettingsProviders: Component = () => {
+  const dialog = useDialog()
   const language = useLanguage()
+  const globalSDK = useGlobalSDK()
+  const providers = useProviders()
+
+  const connected = createMemo(() => providers.connected())
+  const popular = createMemo(() => {
+    const connectedIDs = new Set(connected().map((p) => p.id))
+    const items = providers
+      .popular()
+      .filter((p) => !connectedIDs.has(p.id))
+      .slice()
+    items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id))
+    return items
+  })
+
+  const source = (item: unknown) => (item as ProviderMeta).source
+
+  const type = (item: unknown) => {
+    const current = source(item)
+    if (current === "env") return language.t("settings.providers.tag.environment")
+    if (current === "api") return language.t("provider.connect.method.apiKey")
+    if (current === "config") return language.t("settings.providers.tag.config")
+    if (current === "custom") return language.t("settings.providers.tag.custom")
+    return language.t("settings.providers.tag.other")
+  }
+
+  const canDisconnect = (item: unknown) => source(item) !== "env"
+
+  const disconnect = async (providerID: string, name: string) => {
+    await globalSDK.client.auth
+      .remove({ providerID })
+      .then(async () => {
+        await globalSDK.client.global.dispose()
+        showToast({
+          variant: "success",
+          icon: "circle-check",
+          title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }),
+          description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }),
+        })
+      })
+      .catch((err: unknown) => {
+        const message = err instanceof Error ? err.message : String(err)
+        showToast({ title: language.t("common.requestFailed"), description: message })
+      })
+  }
 
   return (
-    <div class="flex flex-col h-full overflow-y-auto">
-      <div class="flex flex-col gap-6 p-6 max-w-[600px]">
-        <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
-        <p class="text-14-regular text-text-weak">{language.t("settings.providers.description")}</p>
+    <div class="flex flex-col h-full overflow-y-auto no-scrollbar" style={{ padding: "0 40px 40px 40px" }}>
+      <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-raised-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
+        <div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
+          <h2 class="text-16-medium text-text-strong">{language.t("settings.providers.title")}</h2>
+        </div>
+      </div>
+
+      <div class="flex flex-col gap-8 max-w-[720px]">
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.connected")}</h3>
+          <div class="bg-surface-raised-base px-4 rounded-lg">
+            <Show
+              when={connected().length > 0}
+              fallback={
+                <div class="py-4 text-14-regular text-text-weak">
+                  {language.t("settings.providers.connected.empty")}
+                </div>
+              }
+            >
+              <For each={connected()}>
+                {(item) => (
+                  <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+                    <div class="flex items-center gap-3 min-w-0">
+                      <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
+                      <span class="text-14-regular text-text-strong truncate">{item.name}</span>
+                      <Tag>{type(item)}</Tag>
+                    </div>
+                    <Show when={canDisconnect(item)}>
+                      <Button size="small" variant="ghost" onClick={() => void disconnect(item.id, item.name)}>
+                        {language.t("common.disconnect")}
+                      </Button>
+                    </Show>
+                  </div>
+                )}
+              </For>
+            </Show>
+          </div>
+        </div>
+
+        <div class="flex flex-col gap-1">
+          <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.providers.section.popular")}</h3>
+          <div class="bg-surface-raised-base px-4 rounded-lg">
+            <For each={popular()}>
+              {(item) => (
+                <div class="flex items-center justify-between gap-4 py-3 border-b border-border-weak-base last:border-none">
+                  <div class="flex items-center gap-x-3 min-w-0">
+                    <ProviderIcon id={item.id as IconName} class="size-5 shrink-0 icon-strong-base" />
+                    <span class="text-14-regular text-text-strong">{item.name}</span>
+                    <Show when={item.id === "opencode"}>
+                      <Tag>{language.t("dialog.provider.tag.recommended")}</Tag>
+                    </Show>
+                    <Show when={item.id === "anthropic"}>
+                      <div class="text-14-regular text-text-weak">{language.t("dialog.provider.anthropic.note")}</div>
+                    </Show>
+                    <Show when={item.id === "openai"}>
+                      <div class="text-14-regular text-text-weak">{language.t("dialog.provider.openai.note")}</div>
+                    </Show>
+                    <Show when={item.id.startsWith("github-copilot")}>
+                      <div class="text-14-regular text-text-weak">{language.t("dialog.provider.copilot.note")}</div>
+                    </Show>
+                  </div>
+                  <Button
+                    size="large"
+                    variant="secondary"
+                    icon="plus-small"
+                    onClick={() => {
+                      dialog.show(() => <DialogConnectProvider provider={item.id} />)
+                    }}
+                  >
+                    {language.t("common.connect")}
+                  </Button>
+                </div>
+              )}
+            </For>
+          </div>
+
+          <Button
+            variant="ghost"
+            class="px-0 py-0 text-14-medium text-text-strong text-left justify-start hover:bg-transparent active:bg-transparent"
+            onClick={() => {
+              dialog.show(() => <DialogSelectProvider />)
+            }}
+          >
+            {language.t("dialog.provider.viewAll")}
+          </Button>
+        </div>
       </div>
     </div>
   )

+ 40 - 13
packages/app/src/context/command.tsx

@@ -24,6 +24,15 @@ function normalizeKey(key: string) {
   return key.toLowerCase()
 }
 
+function signature(key: string, ctrl: boolean, meta: boolean, shift: boolean, alt: boolean) {
+  const mask = (ctrl ? 1 : 0) | (meta ? 2 : 0) | (shift ? 4 : 0) | (alt ? 8 : 0)
+  return `${key}:${mask}`
+}
+
+function signatureFromEvent(event: KeyboardEvent) {
+  return signature(normalizeKey(event.key), event.ctrlKey, event.metaKey, event.shiftKey, event.altKey)
+}
+
 export type KeybindConfig = string
 
 export interface Keybind {
@@ -223,6 +232,30 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
 
     const suspended = () => suspendCount() > 0
 
+    const palette = createMemo(() => {
+      const config = settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND
+      const keybinds = parseKeybind(config)
+      return new Set(keybinds.map((kb) => signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)))
+    })
+
+    const keymap = createMemo(() => {
+      const map = new Map<string, CommandOption>()
+      for (const option of options()) {
+        if (option.id.startsWith(SUGGESTED_PREFIX)) continue
+        if (option.disabled) continue
+        if (!option.keybind) continue
+
+        const keybinds = parseKeybind(option.keybind)
+        for (const kb of keybinds) {
+          if (!kb.key) continue
+          const sig = signature(kb.key, kb.ctrl, kb.meta, kb.shift, kb.alt)
+          if (map.has(sig)) continue
+          map.set(sig, option)
+        }
+      }
+      return map
+    })
+
     const run = (id: string, source?: "palette" | "keybind" | "slash") => {
       for (const option of options()) {
         if (option.id === id || option.id === "suggested." + id) {
@@ -239,24 +272,18 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
     const handleKeyDown = (event: KeyboardEvent) => {
       if (suspended() || dialog.active) return
 
-      const paletteKeybinds = parseKeybind(settings.keybinds.get(PALETTE_ID) ?? DEFAULT_PALETTE_KEYBIND)
-      if (matchKeybind(paletteKeybinds, event)) {
+      const sig = signatureFromEvent(event)
+
+      if (palette().has(sig)) {
         event.preventDefault()
         showPalette()
         return
       }
 
-      for (const option of options()) {
-        if (option.disabled) continue
-        if (!option.keybind) continue
-
-        const keybinds = parseKeybind(option.keybind)
-        if (matchKeybind(keybinds, event)) {
-          event.preventDefault()
-          option.onSelect?.("keybind")
-          return
-        }
-      }
+      const option = keymap().get(sig)
+      if (!option) return
+      event.preventDefault()
+      option.onSelect?.("keybind")
     }
 
     onMount(() => {

+ 1 - 1
packages/app/src/context/layout.tsx

@@ -218,7 +218,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
     }
 
     function enrich(project: { worktree: string; expanded: boolean }) {
-      const [childStore] = globalSync.child(project.worktree)
+      const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
       const projectID = childStore.project
       const metadata = projectID
         ? globalSync.data.project.find((x) => x.id === projectID)

+ 65 - 10
packages/app/src/context/notification.tsx

@@ -1,5 +1,5 @@
 import { createStore } from "solid-js/store"
-import { createEffect, onCleanup } from "solid-js"
+import { createEffect, createMemo, onCleanup } from "solid-js"
 import { useParams } from "@solidjs/router"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { useGlobalSDK } from "./global-sdk"
@@ -52,6 +52,15 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
     const settings = useSettings()
     const language = useLanguage()
 
+    const empty: Notification[] = []
+
+    const currentDirectory = createMemo(() => {
+      if (!params.dir) return
+      return base64Decode(params.dir)
+    })
+
+    const currentSession = createMemo(() => params.id)
+
     const [store, setStore, _, ready] = persisted(
       Persist.global("notification", ["notification.v1"]),
       createStore({
@@ -72,13 +81,59 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
       setStore("list", (list) => pruneNotifications([...list, notification]))
     }
 
+    const index = createMemo(() => {
+      const sessionAll = new Map<string, Notification[]>()
+      const sessionUnseen = new Map<string, Notification[]>()
+      const projectAll = new Map<string, Notification[]>()
+      const projectUnseen = new Map<string, Notification[]>()
+
+      for (const notification of store.list) {
+        const session = notification.session
+        if (session) {
+          const list = sessionAll.get(session)
+          if (list) list.push(notification)
+          else sessionAll.set(session, [notification])
+          if (!notification.viewed) {
+            const unseen = sessionUnseen.get(session)
+            if (unseen) unseen.push(notification)
+            else sessionUnseen.set(session, [notification])
+          }
+        }
+
+        const directory = notification.directory
+        if (directory) {
+          const list = projectAll.get(directory)
+          if (list) list.push(notification)
+          else projectAll.set(directory, [notification])
+          if (!notification.viewed) {
+            const unseen = projectUnseen.get(directory)
+            if (unseen) unseen.push(notification)
+            else projectUnseen.set(directory, [notification])
+          }
+        }
+      }
+
+      return {
+        session: {
+          all: sessionAll,
+          unseen: sessionUnseen,
+        },
+        project: {
+          all: projectAll,
+          unseen: projectUnseen,
+        },
+      }
+    })
+
     const unsub = globalSDK.event.listen((e) => {
-      const directory = e.name
       const event = e.details
+      if (event.type !== "session.idle" && event.type !== "session.error") return
+
+      const directory = e.name
       const time = Date.now()
-      const activeDirectory = params.dir ? base64Decode(params.dir) : undefined
-      const activeSession = params.id
       const viewed = (sessionID?: string) => {
+        const activeDirectory = currentDirectory()
+        const activeSession = currentSession()
         if (!activeDirectory) return false
         if (!activeSession) return false
         if (!sessionID) return false
@@ -88,7 +143,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
       switch (event.type) {
         case "session.idle": {
           const sessionID = event.properties.sessionID
-          const [syncStore] = globalSync.child(directory)
+          const [syncStore] = globalSync.child(directory, { bootstrap: false })
           const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
           const session = match.found ? syncStore.session[match.index] : undefined
           if (session?.parentID) break
@@ -115,7 +170,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
         }
         case "session.error": {
           const sessionID = event.properties.sessionID
-          const [syncStore] = globalSync.child(directory)
+          const [syncStore] = globalSync.child(directory, { bootstrap: false })
           const match = sessionID ? Binary.search(syncStore.session, sessionID, (s) => s.id) : undefined
           const session = sessionID && match?.found ? syncStore.session[match.index] : undefined
           if (session?.parentID) break
@@ -148,10 +203,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
       ready,
       session: {
         all(session: string) {
-          return store.list.filter((n) => n.session === session)
+          return index().session.all.get(session) ?? empty
         },
         unseen(session: string) {
-          return store.list.filter((n) => n.session === session && !n.viewed)
+          return index().session.unseen.get(session) ?? empty
         },
         markViewed(session: string) {
           setStore("list", (n) => n.session === session, "viewed", true)
@@ -159,10 +214,10 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
       },
       project: {
         all(directory: string) {
-          return store.list.filter((n) => n.directory === directory)
+          return index().project.all.get(directory) ?? empty
         },
         unseen(directory: string) {
-          return store.list.filter((n) => n.directory === directory && !n.viewed)
+          return index().project.unseen.get(directory) ?? empty
         },
         markViewed(directory: string) {
           setStore("list", (n) => n.directory === directory, "viewed", true)

+ 1 - 1
packages/app/src/i18n/ar.ts

@@ -100,7 +100,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "نماذج مجانية مقدمة من OpenCode",
   "dialog.model.unpaid.addMore.title": "إضافة المزيد من النماذج من موفرين مشهورين",
 
-  "dialog.provider.viewAll": "عرض جميع الموفرين",
+  "dialog.provider.viewAll": "عرض المزيد من الموفرين",
 
   "provider.connect.title": "اتصال {{provider}}",
   "provider.connect.title.anthropicProMax": "تسجيل الدخول باستخدام Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/br.ts

@@ -100,7 +100,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Modelos gratuitos fornecidos pelo OpenCode",
   "dialog.model.unpaid.addMore.title": "Adicionar mais modelos de provedores populares",
 
-  "dialog.provider.viewAll": "Ver todos os provedores",
+  "dialog.provider.viewAll": "Ver mais provedores",
 
   "provider.connect.title": "Conectar {{provider}}",
   "provider.connect.title.anthropicProMax": "Entrar com Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/da.ts

@@ -98,7 +98,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Gratis modeller leveret af OpenCode",
   "dialog.model.unpaid.addMore.title": "Tilføj flere modeller fra populære udbydere",
 
-  "dialog.provider.viewAll": "Vis alle udbydere",
+  "dialog.provider.viewAll": "Vis flere udbydere",
 
   "provider.connect.title": "Forbind {{provider}}",
   "provider.connect.title.anthropicProMax": "Log ind med Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/de.ts

@@ -102,7 +102,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Kostenlose Modelle von OpenCode",
   "dialog.model.unpaid.addMore.title": "Weitere Modelle von beliebten Anbietern hinzufügen",
 
-  "dialog.provider.viewAll": "Alle Anbieter anzeigen",
+  "dialog.provider.viewAll": "Mehr Anbieter anzeigen",
 
   "provider.connect.title": "{{provider}} verbinden",
   "provider.connect.title.anthropicProMax": "Mit Claude Pro/Max anmelden",

+ 14 - 1
packages/app/src/i18n/en.ts

@@ -100,7 +100,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Free models provided by OpenCode",
   "dialog.model.unpaid.addMore.title": "Add more models from popular providers",
 
-  "dialog.provider.viewAll": "View all providers",
+  "dialog.provider.viewAll": "Show more providers",
 
   "provider.connect.title": "Connect {{provider}}",
   "provider.connect.title.anthropicProMax": "Login with Claude Pro/Max",
@@ -137,6 +137,9 @@ export const dict = {
   "provider.connect.toast.connected.title": "{{provider}} connected",
   "provider.connect.toast.connected.description": "{{provider}} models are now available to use.",
 
+  "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected",
+  "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.",
+
   "model.tag.free": "Free",
   "model.tag.latest": "Latest",
   "model.provider.anthropic": "Anthropic",
@@ -159,6 +162,8 @@ export const dict = {
   "common.loading": "Loading",
   "common.loading.ellipsis": "...",
   "common.cancel": "Cancel",
+  "common.connect": "Connect",
+  "common.disconnect": "Disconnect",
   "common.submit": "Submit",
   "common.save": "Save",
   "common.saving": "Saving...",
@@ -491,6 +496,7 @@ export const dict = {
   "sidebar.project.viewAllSessions": "View all sessions",
 
   "settings.section.desktop": "Desktop",
+  "settings.section.server": "Server",
   "settings.tab.general": "General",
   "settings.tab.shortcuts": "Shortcuts",
 
@@ -599,6 +605,13 @@ export const dict = {
 
   "settings.providers.title": "Providers",
   "settings.providers.description": "Provider settings will be configurable here.",
+  "settings.providers.section.connected": "Connected providers",
+  "settings.providers.connected.empty": "No connected providers",
+  "settings.providers.section.popular": "Popular providers",
+  "settings.providers.tag.environment": "Environment",
+  "settings.providers.tag.config": "Config",
+  "settings.providers.tag.custom": "Custom",
+  "settings.providers.tag.other": "Other",
   "settings.models.title": "Models",
   "settings.models.description": "Model settings will be configurable here.",
   "settings.agents.title": "Agents",

+ 1 - 1
packages/app/src/i18n/es.ts

@@ -98,7 +98,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Modelos gratuitos proporcionados por OpenCode",
   "dialog.model.unpaid.addMore.title": "Añadir más modelos de proveedores populares",
 
-  "dialog.provider.viewAll": "Ver todos los proveedores",
+  "dialog.provider.viewAll": "Ver s proveedores",
 
   "provider.connect.title": "Conectar {{provider}}",
   "provider.connect.title.anthropicProMax": "Iniciar sesión con Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/fr.ts

@@ -98,7 +98,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Modèles gratuits fournis par OpenCode",
   "dialog.model.unpaid.addMore.title": "Ajouter plus de modèles de fournisseurs populaires",
 
-  "dialog.provider.viewAll": "Voir tous les fournisseurs",
+  "dialog.provider.viewAll": "Voir plus de fournisseurs",
 
   "provider.connect.title": "Connecter {{provider}}",
   "provider.connect.title.anthropicProMax": "Connexion avec Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/ja.ts

@@ -98,7 +98,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "OpenCodeが提供する無料モデル",
   "dialog.model.unpaid.addMore.title": "人気のプロバイダーからモデルを追加",
 
-  "dialog.provider.viewAll": "すべてのプロバイダーを表示",
+  "dialog.provider.viewAll": "さらにプロバイダーを表示",
 
   "provider.connect.title": "{{provider}}を接続",
   "provider.connect.title.anthropicProMax": "Claude Pro/Maxでログイン",

+ 1 - 1
packages/app/src/i18n/ko.ts

@@ -102,7 +102,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "OpenCode에서 제공하는 무료 모델",
   "dialog.model.unpaid.addMore.title": "인기 공급자의 모델 추가",
 
-  "dialog.provider.viewAll": "모든 공급자 보기",
+  "dialog.provider.viewAll": "더 많은 공급자 보기",
 
   "provider.connect.title": "{{provider}} 연결",
   "provider.connect.title.anthropicProMax": "Claude Pro/Max로 로그인",

+ 1 - 1
packages/app/src/i18n/no.ts

@@ -103,7 +103,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Gratis modeller levert av OpenCode",
   "dialog.model.unpaid.addMore.title": "Legg til flere modeller fra populære leverandører",
 
-  "dialog.provider.viewAll": "Vis alle leverandører",
+  "dialog.provider.viewAll": "Vis flere leverandører",
 
   "provider.connect.title": "Koble til {{provider}}",
   "provider.connect.title.anthropicProMax": "Logg inn med Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/pl.ts

@@ -100,7 +100,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Darmowe modele dostarczane przez OpenCode",
   "dialog.model.unpaid.addMore.title": "Dodaj więcej modeli od popularnych dostawców",
 
-  "dialog.provider.viewAll": "Zobacz wszystkich dostawców",
+  "dialog.provider.viewAll": "Zobacz więcej dostawców",
 
   "provider.connect.title": "Połącz {{provider}}",
   "provider.connect.title.anthropicProMax": "Zaloguj się z Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/ru.ts

@@ -100,7 +100,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "Бесплатные модели от OpenCode",
   "dialog.model.unpaid.addMore.title": "Добавьте больше моделей от популярных провайдеров",
 
-  "dialog.provider.viewAll": "Посмотреть всех провайдеров",
+  "dialog.provider.viewAll": "Показать больше провайдеров",
 
   "provider.connect.title": "Подключить {{provider}}",
   "provider.connect.title.anthropicProMax": "Войти с помощью Claude Pro/Max",

+ 1 - 1
packages/app/src/i18n/zh.ts

@@ -102,7 +102,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免费模型",
   "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型",
 
-  "dialog.provider.viewAll": "查看全部提供商",
+  "dialog.provider.viewAll": "查看更多提供商",
 
   "provider.connect.title": "连接 {{provider}}",
   "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录",

+ 1 - 1
packages/app/src/i18n/zht.ts

@@ -102,7 +102,7 @@ export const dict = {
   "dialog.model.unpaid.freeModels.title": "OpenCode 提供的免費模型",
   "dialog.model.unpaid.addMore.title": "從熱門提供者新增更多模型",
 
-  "dialog.provider.viewAll": "查看全部提供者",
+  "dialog.provider.viewAll": "查看更多提供者",
 
   "provider.connect.title": "連線 {{provider}}",
   "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登入",

+ 6 - 5
packages/app/src/pages/home.tsx

@@ -23,6 +23,11 @@ export default function Home() {
   const server = useServer()
   const language = useLanguage()
   const homedir = createMemo(() => sync.data.path.home)
+  const recent = createMemo(() => {
+    return sync.data.project
+      .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
+      .slice(0, 5)
+  })
 
   function openProject(directory: string) {
     layout.projects.open(directory)
@@ -84,11 +89,7 @@ export default function Home() {
               </Button>
             </div>
             <ul class="flex flex-col gap-2">
-              <For
-                each={sync.data.project
-                  .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
-                  .slice(0, 5)}
-              >
+              <For each={recent()}>
                 {(project) => (
                   <Button
                     size="large"

+ 32 - 27
packages/app/src/pages/layout.tsx

@@ -350,7 +350,7 @@ export default function Layout(props: ParentProps) {
       const props = e.details.properties
       if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
 
-      const [store] = globalSync.child(directory)
+      const [store] = globalSync.child(directory, { bootstrap: false })
       const session = store.session.find((s) => s.id === props.sessionID)
       const sessionKey = `${directory}:${props.sessionID}`
 
@@ -419,7 +419,7 @@ export default function Layout(props: ParentProps) {
         toastBySession.delete(sessionKey)
         alertedAtBySession.delete(sessionKey)
       }
-      const [store] = globalSync.child(currentDir)
+      const [store] = globalSync.child(currentDir, { bootstrap: false })
       const childSessions = store.session.filter((s) => s.parentID === currentSession)
       for (const child of childSessions) {
         const childKey = `${currentDir}:${child.id}`
@@ -433,17 +433,18 @@ export default function Layout(props: ParentProps) {
     })
   })
 
-  function sortSessions(a: Session, b: Session) {
-    const now = Date.now()
+  function sortSessions(now: number) {
     const oneMinuteAgo = now - 60 * 1000
-    const aUpdated = a.time.updated ?? a.time.created
-    const bUpdated = b.time.updated ?? b.time.created
-    const aRecent = aUpdated > oneMinuteAgo
-    const bRecent = bUpdated > oneMinuteAgo
-    if (aRecent && bRecent) return a.id.localeCompare(b.id)
-    if (aRecent && !bRecent) return -1
-    if (!aRecent && bRecent) return 1
-    return bUpdated - aUpdated
+    return (a: Session, b: Session) => {
+      const aUpdated = a.time.updated ?? a.time.created
+      const bUpdated = b.time.updated ?? b.time.created
+      const aRecent = aUpdated > oneMinuteAgo
+      const bRecent = bUpdated > oneMinuteAgo
+      if (aRecent && bRecent) return a.id.localeCompare(b.id)
+      if (aRecent && !bRecent) return -1
+      if (!aRecent && bRecent) return 1
+      return bUpdated - aUpdated
+    }
   }
 
   const [scrollSessionKey, setScrollSessionKey] = createSignal<string | undefined>(undefined)
@@ -475,7 +476,7 @@ export default function Layout(props: ParentProps) {
     const direct = projects.find((p) => p.worktree === directory)
     if (direct) return direct
 
-    const [child] = globalSync.child(directory)
+    const [child] = globalSync.child(directory, { bootstrap: false })
     const id = child.project
     if (!id) return
 
@@ -596,6 +597,7 @@ export default function Layout(props: ParentProps) {
   const currentSessions = createMemo(() => {
     const project = currentProject()
     if (!project) return [] as Session[]
+    const compare = sortSessions(Date.now())
     if (workspaceSetting()) {
       const dirs = workspaceIds(project)
       const activeDir = params.dir ? base64Decode(params.dir) : ""
@@ -608,7 +610,7 @@ export default function Layout(props: ParentProps) {
         const dirSessions = dirStore.session
           .filter((session) => session.directory === dirStore.path.directory)
           .filter((session) => !session.parentID && !session.time?.archived)
-          .toSorted(sortSessions)
+          .toSorted(compare)
         result.push(...dirSessions)
       }
       return result
@@ -617,7 +619,7 @@ export default function Layout(props: ParentProps) {
     return projectStore.session
       .filter((session) => session.directory === projectStore.path.directory)
       .filter((session) => !session.parentID && !session.time?.archived)
-      .toSorted(sortSessions)
+      .toSorted(compare)
   })
 
   type PrefetchQueue = {
@@ -659,7 +661,7 @@ export default function Layout(props: ParentProps) {
   }
 
   async function prefetchMessages(directory: string, sessionID: string, token: number) {
-    const [, setStore] = globalSync.child(directory)
+    const [, setStore] = globalSync.child(directory, { bootstrap: false })
 
     return retry(() => globalSDK.client.session.messages({ directory, sessionID, limit: prefetchChunk }))
       .then((messages) => {
@@ -717,7 +719,7 @@ export default function Layout(props: ParentProps) {
     const directory = session.directory
     if (!directory) return
 
-    const [store] = globalSync.child(directory)
+    const [store] = globalSync.child(directory, { bootstrap: false })
     const cached = untrack(() => store.message[session.id] !== undefined)
     if (cached) return
 
@@ -1817,7 +1819,7 @@ export default function Layout(props: ParentProps) {
       const directory = store.activeWorkspace
       if (!directory) return
 
-      const [workspaceStore] = globalSync.child(directory)
+      const [workspaceStore] = globalSync.child(directory, { bootstrap: false })
       const kind =
         directory === project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
       const name = workspaceLabel(directory, workspaceStore.vcs?.branch, project.id)
@@ -1843,7 +1845,7 @@ export default function Layout(props: ParentProps) {
       workspaceStore.session
         .filter((session) => session.directory === workspaceStore.path.directory)
         .filter((session) => !session.parentID && !session.time?.archived)
-        .toSorted(sortSessions),
+        .toSorted(sortSessions(Date.now())),
     )
     const local = createMemo(() => props.directory === props.project.worktree)
     const active = createMemo(() => {
@@ -2048,7 +2050,7 @@ export default function Layout(props: ParentProps) {
     const [open, setOpen] = createSignal(false)
 
     const label = (directory: string) => {
-      const [data] = globalSync.child(directory)
+      const [data] = globalSync.child(directory, { bootstrap: false })
       const kind =
         directory === props.project.worktree ? language.t("workspace.type.local") : language.t("workspace.type.sandbox")
       const name = workspaceLabel(directory, data.vcs?.branch, props.project.id)
@@ -2056,20 +2058,23 @@ export default function Layout(props: ParentProps) {
     }
 
     const sessions = (directory: string) => {
-      const [data] = globalSync.child(directory)
+      const [data] = globalSync.child(directory, { bootstrap: false })
+      const root = workspaceKey(directory)
       return data.session
-        .filter((session) => session.directory === data.path.directory)
+        .filter((session) => workspaceKey(session.directory) === root)
         .filter((session) => !session.parentID && !session.time?.archived)
-        .toSorted(sortSessions)
+        .toSorted(sortSessions(Date.now()))
         .slice(0, 2)
     }
 
     const projectSessions = () => {
-      const [data] = globalSync.child(props.project.worktree)
+      const directory = props.project.worktree
+      const [data] = globalSync.child(directory, { bootstrap: false })
+      const root = workspaceKey(directory)
       return data.session
-        .filter((session) => session.directory === data.path.directory)
+        .filter((session) => workspaceKey(session.directory) === root)
         .filter((session) => !session.parentID && !session.time?.archived)
-        .toSorted(sortSessions)
+        .toSorted(sortSessions(Date.now()))
         .slice(0, 2)
     }
 
@@ -2196,7 +2201,7 @@ export default function Layout(props: ParentProps) {
       workspaceStore.session
         .filter((session) => session.directory === workspaceStore.path.directory)
         .filter((session) => !session.parentID && !session.time?.archived)
-        .toSorted(sortSessions),
+        .toSorted(sortSessions(Date.now())),
     )
     const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
     const hasMore = createMemo(() => workspaceStore.sessionTotal > workspaceStore.session.length)

+ 3 - 3
packages/console/app/src/routes/changelog/index.css

@@ -478,8 +478,8 @@
     [data-component="highlights"] {
       display: flex;
       flex-direction: column;
-      gap: 2rem;
-      margin-bottom: 1.5rem;
+      gap: 3rem;
+      margin-bottom: 0.75rem;
     }
 
     [data-component="collapsible-sections"] {
@@ -563,7 +563,7 @@
       }
 
       [data-slot="highlight-item"] {
-        margin-bottom: 24px;
+        margin-bottom: 48px;
 
         &:last-child {
           margin-bottom: 0;

+ 1 - 1
packages/desktop/index.html

@@ -17,7 +17,7 @@
   </head>
   <body class="antialiased overscroll-none text-12-regular overflow-hidden">
     <noscript>You need to enable JavaScript to run this app.</noscript>
-    <div id="root" class="flex flex-col h-dvh"></div>
+    <div id="root" class="flex flex-col h-dvh p-px"></div>
     <div data-tauri-decorum-tb class="w-0 h-0 hidden" />
     <script src="/src/index.tsx" type="module"></script>
   </body>

+ 2 - 2
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -503,7 +503,7 @@ export function Session() {
       },
     },
     {
-      title: "Toggle code concealment",
+      title: conceal() ? "Disable code concealment" : "Enable code concealment",
       value: "session.toggle.conceal",
       keybind: "messages_toggle_conceal" as any,
       category: "Session",
@@ -539,7 +539,7 @@ export function Session() {
       },
     },
     {
-      title: "Toggle diff wrapping",
+      title: diffWrapMode() === "word" ? "Disable diff wrapping" : "Enable diff wrapping",
       value: "session.toggle.diffwrap",
       category: "Session",
       slash: {

+ 11 - 12
packages/opencode/src/provider/provider.ts

@@ -854,10 +854,9 @@ export namespace Provider {
       // Load for the main provider if auth exists
       if (auth) {
         const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider])
-        mergeProvider(plugin.auth.provider, {
-          source: "custom",
-          options: options,
-        })
+        const opts = options ?? {}
+        const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
+        mergeProvider(providerID, patch)
       }
 
       // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists
@@ -870,10 +869,11 @@ export namespace Provider {
               () => Auth.get(enterpriseProviderID) as any,
               database[enterpriseProviderID],
             )
-            mergeProvider(enterpriseProviderID, {
-              source: "custom",
-              options: enterpriseOptions,
-            })
+            const opts = enterpriseOptions ?? {}
+            const patch: Partial<Info> = providers[enterpriseProviderID]
+              ? { options: opts }
+              : { source: "custom", options: opts }
+            mergeProvider(enterpriseProviderID, patch)
           }
         }
       }
@@ -889,10 +889,9 @@ export namespace Provider {
       const result = await fn(data)
       if (result && (result.autoload || providers[providerID])) {
         if (result.getModel) modelLoaders[providerID] = result.getModel
-        mergeProvider(providerID, {
-          source: "custom",
-          options: result.options,
-        })
+        const opts = result.options ?? {}
+        const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
+        mergeProvider(providerID, patch)
       }
     }
 

+ 30 - 0
packages/opencode/src/server/server.ts

@@ -441,6 +441,36 @@ export namespace Server {
             return c.json(true)
           },
         )
+        .delete(
+          "/auth/:providerID",
+          describeRoute({
+            summary: "Remove auth credentials",
+            description: "Remove authentication credentials",
+            operationId: "auth.remove",
+            responses: {
+              200: {
+                description: "Successfully removed authentication credentials",
+                content: {
+                  "application/json": {
+                    schema: resolver(z.boolean()),
+                  },
+                },
+              },
+              ...errors(400),
+            },
+          }),
+          validator(
+            "param",
+            z.object({
+              providerID: z.string(),
+            }),
+          ),
+          async (c) => {
+            const providerID = c.req.valid("param").providerID
+            await Auth.remove(providerID)
+            return c.json(true)
+          },
+        )
         .get(
           "/event",
           describeRoute({

+ 1 - 1
packages/opencode/src/session/prompt.ts

@@ -1347,7 +1347,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
 
     const session = await Session.get(input.sessionID)
     if (session.revert) {
-      SessionRevert.cleanup(session)
+      await SessionRevert.cleanup(session)
     }
     const agent = await Agent.get(input.agent)
     const model = input.model ?? agent.model ?? (await lastModel(input.sessionID))

+ 4 - 3
packages/opencode/test/provider/provider.test.ts

@@ -46,9 +46,10 @@ test("provider loaded from env variable", async () => {
     fn: async () => {
       const providers = await Provider.list()
       expect(providers["anthropic"]).toBeDefined()
-      // Note: source becomes "custom" because CUSTOM_LOADERS run after env loading
-      // and anthropic has a custom loader that merges additional options
-      expect(providers["anthropic"].source).toBe("custom")
+      // Provider should retain its connection source even if custom loaders
+      // merge additional options.
+      expect(providers["anthropic"].source).toBe("env")
+      expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined()
     },
   })
 })

+ 32 - 0
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -9,6 +9,8 @@ import type {
   AppLogResponses,
   AppSkillsResponses,
   Auth as Auth3,
+  AuthRemoveErrors,
+  AuthRemoveResponses,
   AuthSetErrors,
   AuthSetResponses,
   CommandListResponses,
@@ -3054,6 +3056,36 @@ export class Formatter extends HeyApiClient {
 }
 
 export class Auth2 extends HeyApiClient {
+  /**
+   * Remove auth credentials
+   *
+   * Remove authentication credentials
+   */
+  public remove<ThrowOnError extends boolean = false>(
+    parameters: {
+      providerID: string
+      directory?: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "providerID" },
+            { in: "query", key: "directory" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).delete<AuthRemoveResponses, AuthRemoveErrors, ThrowOnError>({
+      url: "/auth/{providerID}",
+      ...options,
+      ...params,
+    })
+  }
+
   /**
    * Set auth credentials
    *

+ 29 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -4867,6 +4867,35 @@ export type FormatterStatusResponses = {
 
 export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses]
 
+export type AuthRemoveData = {
+  body?: never
+  path: {
+    providerID: string
+  }
+  query?: {
+    directory?: string
+  }
+  url: "/auth/{providerID}"
+}
+
+export type AuthRemoveErrors = {
+  /**
+   * Bad request
+   */
+  400: BadRequestError
+}
+
+export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors]
+
+export type AuthRemoveResponses = {
+  /**
+   * Successfully removed authentication credentials
+   */
+  200: boolean
+}
+
+export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses]
+
 export type AuthSetData = {
   body?: Auth
   path: {

+ 50 - 0
packages/sdk/openapi.json

@@ -5709,6 +5709,56 @@
             "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n  ...\n})"
           }
         ]
+      },
+      "delete": {
+        "operationId": "auth.remove",
+        "parameters": [
+          {
+            "in": "query",
+            "name": "directory",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "in": "path",
+            "name": "providerID",
+            "schema": {
+              "type": "string"
+            },
+            "required": true
+          }
+        ],
+        "summary": "Remove auth credentials",
+        "description": "Remove authentication credentials",
+        "responses": {
+          "200": {
+            "description": "Successfully removed authentication credentials",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "boolean"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Bad request",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/BadRequestError"
+                }
+              }
+            }
+          }
+        },
+        "x-codeSamples": [
+          {
+            "lang": "js",
+            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n  ...\n})"
+          }
+        ]
       }
     },
     "/event": {

+ 2 - 0
packages/ui/src/components/tabs.css

@@ -239,6 +239,7 @@
       background-color: transparent;
 
       [data-slot="tabs-trigger"] {
+        height: 100%;
         padding: 0 8px;
         gap: 8px;
         justify-content: flex-start;
@@ -333,6 +334,7 @@
           gap: 12px;
           justify-content: flex-start;
           width: 100%;
+          height: 100%;
         }
 
         [data-component="icon"] {

+ 4 - 6
packages/web/src/content/docs/providers.mdx

@@ -254,12 +254,6 @@ For custom inference profiles, use the model and provider name in the key and se
 
 ### Anthropic
 
-We recommend signing up for [Claude Pro](https://www.anthropic.com/news/claude-pro) or [Max](https://www.anthropic.com/max).
-
-:::info
-We've received reports of some users having their subscriptions blocked while using it with OpenCode.
-:::
-
 1. Once you've signed up, run the `/connect` command and select Anthropic.
 
    ```txt
@@ -284,6 +278,10 @@ We've received reports of some users having their subscriptions blocked while us
    /models
    ```
 
+:::info
+Using your Claude Pro/Max subscription in OpenCode is not officially supported by [Anthropic](https://anthropic.com).
+:::
+
 ##### Using API keys
 
 You can also select **Create an API Key** if you don't have a Pro/Max subscription. It'll also open your browser and ask you to login to Anthropic and give you a code you can paste in your terminal.