ソースを参照

Merge branch 'dev' into llm-centralization

Dax Raad 4 ヶ月 前
コミット
a5914f4d7c
56 ファイル変更772 行追加279 行削除
  1. 6 0
      .github/workflows/publish.yml
  2. 2 0
      STATS.md
  3. 19 18
      bun.lock
  4. 6 0
      github/action.yml
  5. 1 1
      nix/hashes.json
  6. 2 2
      package.json
  7. 1 1
      packages/console/app/package.json
  8. 1 1
      packages/console/core/package.json
  9. 1 1
      packages/console/function/package.json
  10. 1 1
      packages/console/mail/package.json
  11. 2 1
      packages/desktop/package.json
  12. 172 27
      packages/desktop/src/components/prompt-input.tsx
  13. 14 5
      packages/desktop/src/components/terminal.tsx
  14. 40 35
      packages/desktop/src/pages/layout.tsx
  15. 1 1
      packages/enterprise/package.json
  16. 6 6
      packages/extensions/zed/extension.toml
  17. 1 1
      packages/function/package.json
  18. 10 2
      packages/opencode/Dockerfile
  19. 1 1
      packages/opencode/package.json
  20. 3 0
      packages/opencode/script/build.ts
  21. 4 4
      packages/opencode/script/publish.ts
  22. 10 41
      packages/opencode/src/bun/index.ts
  23. 28 5
      packages/opencode/src/cli/cmd/github.ts
  24. 1 1
      packages/opencode/src/cli/cmd/tui/app.tsx
  25. 6 1
      packages/opencode/src/cli/cmd/tui/attach.ts
  26. 8 16
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  27. 4 1
      packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
  28. 18 0
      packages/opencode/src/format/formatter.ts
  29. 1 0
      packages/opencode/src/lsp/language.ts
  30. 141 0
      packages/opencode/src/lsp/server.ts
  31. 20 5
      packages/opencode/src/provider/transform.ts
  32. 2 0
      packages/opencode/src/session/llm.ts
  33. 9 0
      packages/opencode/src/session/retry.ts
  34. 7 4
      packages/opencode/src/tool/edit.ts
  35. 12 2
      packages/opencode/src/tool/write.ts
  36. 2 2
      packages/plugin/package.json
  37. 2 2
      packages/sdk/js/package.json
  38. 1 1
      packages/slack/package.json
  39. 1 1
      packages/tauri/package.json
  40. 2 2
      packages/tauri/src-tauri/tauri.conf.json
  41. 1 1
      packages/ui/package.json
  42. 3 1
      packages/ui/src/components/basic-tool.tsx
  43. 1 1
      packages/ui/src/components/button.css
  44. 0 1
      packages/ui/src/components/message-part.tsx
  45. 18 12
      packages/ui/src/components/session-turn.css
  46. 81 55
      packages/ui/src/components/session-turn.tsx
  47. 2 2
      packages/ui/src/styles/animations.css
  48. 1 1
      packages/util/package.json
  49. 1 0
      packages/web/astro.config.mjs
  50. 1 1
      packages/web/package.json
  51. 19 0
      packages/web/src/content/docs/acp.mdx
  52. 15 11
      packages/web/src/content/docs/ecosystem.mdx
  53. 1 0
      packages/web/src/content/docs/formatters.mdx
  54. 1 0
      packages/web/src/content/docs/lsp.mdx
  55. 57 0
      packages/web/src/content/docs/network.mdx
  56. 1 1
      sdks/vscode/package.json

+ 6 - 0
.github/workflows/publish.yml

@@ -64,6 +64,12 @@ jobs:
           username: ${{ github.repository_owner }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
       - uses: actions/setup-node@v4
         with:
           node-version: "24"

+ 2 - 0
STATS.md

@@ -168,3 +168,5 @@
 | 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786)   | 2,017,599 (+32,189) |
 | 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
 | 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
+| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
+| 2025-12-14 | 1,082,042 (+8,481)  | 1,052,425 (+7,817)  | 2,134,467 (+16,298) |

+ 19 - 18
bun.lock

@@ -20,7 +20,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -133,6 +133,7 @@
         "@solid-primitives/active-element": "2.1.3",
         "@solid-primitives/audio": "1.4.2",
         "@solid-primitives/event-bus": "1.1.2",
+        "@solid-primitives/media": "2.3.3",
         "@solid-primitives/resize-observer": "2.1.3",
         "@solid-primitives/scroll": "2.1.3",
         "@solid-primitives/storage": "4.3.3",
@@ -169,7 +170,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -198,7 +199,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
@@ -214,7 +215,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -306,7 +307,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -326,7 +327,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.88.1",
         "@tsconfig/node22": "catalog:",
@@ -337,7 +338,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -350,7 +351,7 @@
     },
     "packages/tauri": {
       "name": "@opencode-ai/tauri",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@opencode-ai/desktop": "workspace:*",
         "@tauri-apps/api": "^2",
@@ -375,7 +376,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -410,7 +411,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -421,7 +422,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.0.152",
+      "version": "1.0.153",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -477,7 +478,7 @@
     "@tailwindcss/vite": "4.1.11",
     "@tsconfig/bun": "1.0.9",
     "@tsconfig/node22": "22.0.2",
-    "@types/bun": "1.3.3",
+    "@types/bun": "1.3.4",
     "@types/luxon": "3.7.1",
     "@types/node": "22.13.9",
     "@typescript/native-preview": "7.0.0-dev.20251207.1",
@@ -1703,7 +1704,7 @@
 
     "@types/braces": ["@types/[email protected]", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
 
-    "@types/bun": ["@types/[email protected].3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
+    "@types/bun": ["@types/[email protected].4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
 
     "@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
 
@@ -2009,7 +2010,7 @@
 
     "bun-pty": ["[email protected]", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
 
-    "bun-types": ["[email protected].3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
+    "bun-types": ["[email protected].4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
 
     "bun-webgpu": ["[email protected]", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],
 

+ 6 - 0
github/action.yml

@@ -17,6 +17,11 @@ inputs:
     description: "Custom prompt to override the default prompt"
     required: false
 
+  use_github_token:
+    description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
+    required: false
+    default: "false"
+
 runs:
   using: "composite"
   steps:
@@ -51,3 +56,4 @@ runs:
         MODEL: ${{ inputs.model }}
         SHARE: ${{ inputs.share }}
         PROMPT: ${{ inputs.prompt }}
+        USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
-  "nodeModules": "sha256-nWSAnQEm/t1ESZe23dr4JnIOJQ0JLN0w4NVoMJajbVQ="
+  "nodeModules": "sha256-lgPsYtNJT7a+mDk5cTiEJLlBnTMTjxZCl8bw5WxcuaM="
 }

+ 2 - 2
package.json

@@ -4,7 +4,7 @@
   "description": "AI-powered development tool",
   "private": true,
   "type": "module",
-  "packageManager": "[email protected].3",
+  "packageManager": "[email protected].4",
   "scripts": {
     "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
     "typecheck": "bun turbo typecheck",
@@ -20,7 +20,7 @@
       "packages/slack"
     ],
     "catalog": {
-      "@types/bun": "1.3.3",
+      "@types/bun": "1.3.4",
       "@hono/zod-validator": "0.4.2",
       "ulid": "3.0.1",
       "@kobalte/core": "0.13.11",

+ 1 - 1
packages/console/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",

+ 1 - 1
packages/console/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-core",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "private": true,
   "type": "module",
   "dependencies": {

+ 1 - 1
packages/console/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-function",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-mail",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",

+ 2 - 1
packages/desktop/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/desktop",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "description": "",
   "type": "module",
   "exports": {
@@ -37,6 +37,7 @@
     "@solid-primitives/active-element": "2.1.3",
     "@solid-primitives/audio": "1.4.2",
     "@solid-primitives/event-bus": "1.1.2",
+    "@solid-primitives/media": "2.3.3",
     "@solid-primitives/resize-observer": "2.1.3",
     "@solid-primitives/scroll": "2.1.3",
     "@solid-primitives/storage": "4.3.3",

+ 172 - 27
packages/desktop/src/components/prompt-input.tsx

@@ -1,18 +1,7 @@
 import { useFilteredList } from "@opencode-ai/ui/hooks"
-import {
-  createEffect,
-  on,
-  Component,
-  Show,
-  For,
-  onMount,
-  onCleanup,
-  Switch,
-  Match,
-  createSignal,
-  createMemo,
-} from "solid-js"
+import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
 import { createStore } from "solid-js/store"
+import { makePersisted } from "@solid-primitives/storage"
 import { createFocusSignal } from "@solid-primitives/active-element"
 import { useLocal } from "@/context/local"
 import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
@@ -81,22 +70,85 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
   const [store, setStore] = createStore<{
     popoverIsOpen: boolean
+    historyIndex: number
+    savedPrompt: Prompt | null
+    placeholder: number
   }>({
     popoverIsOpen: false,
+    historyIndex: -1,
+    savedPrompt: null,
+    placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
   })
 
-  const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length))
+  const MAX_HISTORY = 100
+  const [history, setHistory] = makePersisted(
+    createStore<{
+      entries: Prompt[]
+    }>({
+      entries: [],
+    }),
+    {
+      name: "prompt-history.v1",
+    },
+  )
 
-  onMount(() => {
-    const interval = setInterval(() => {
-      setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length)
-    }, 6500)
-    onCleanup(() => clearInterval(interval))
-  })
+  const clonePromptParts = (prompt: Prompt): Prompt =>
+    prompt.map((part) =>
+      part.type === "text"
+        ? { ...part }
+        : {
+            ...part,
+            selection: part.selection ? { ...part.selection } : undefined,
+          },
+    )
+
+  const promptLength = (prompt: Prompt) => prompt.reduce((len, part) => len + part.content.length, 0)
+
+  const applyHistoryPrompt = (prompt: Prompt, position: "start" | "end") => {
+    const length = position === "start" ? 0 : promptLength(prompt)
+    session.prompt.set(prompt, length)
+    requestAnimationFrame(() => {
+      editorRef.focus()
+      setCursorPosition(editorRef, length)
+    })
+  }
+
+  const getCaretLineState = () => {
+    const selection = window.getSelection()
+    if (!selection || selection.rangeCount === 0) return { collapsed: false, onFirstLine: false, onLastLine: false }
+    const range = selection.getRangeAt(0)
+    const rect = range.getBoundingClientRect()
+    const editorRect = editorRef.getBoundingClientRect()
+    const style = window.getComputedStyle(editorRef)
+    const paddingTop = parseFloat(style.paddingTop) || 0
+    const paddingBottom = parseFloat(style.paddingBottom) || 0
+    let lineHeight = parseFloat(style.lineHeight)
+    if (!Number.isFinite(lineHeight)) lineHeight = parseFloat(style.fontSize) || 16
+    const scrollTop = editorRef.scrollTop
+    let relativeTop = rect.top - editorRect.top - paddingTop + scrollTop
+    if (!Number.isFinite(relativeTop)) relativeTop = scrollTop
+    relativeTop = Math.max(0, relativeTop)
+    let caretHeight = rect.height
+    if (!caretHeight || !Number.isFinite(caretHeight)) caretHeight = lineHeight
+    const relativeBottom = relativeTop + caretHeight
+    const contentHeight = Math.max(caretHeight, editorRef.scrollHeight - paddingTop - paddingBottom)
+    const threshold = Math.max(2, lineHeight / 2)
+
+    return {
+      collapsed: selection.isCollapsed,
+      onFirstLine: relativeTop <= threshold,
+      onLastLine: contentHeight - relativeBottom <= threshold,
+    }
+  }
 
   createEffect(() => {
     session.id
     editorRef.focus()
+    if (session.id) return
+    const interval = setInterval(() => {
+      setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
+    }, 6500)
+    onCleanup(() => clearInterval(interval))
   })
 
   const isFocused = createFocusSignal(() => editorRef)
@@ -129,17 +181,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
   }
 
-  const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({
+  const { flat, active, onInput, onKeyDown } = useFilteredList<string>({
     items: local.file.searchFilesAndDirectories,
     key: (x) => x,
     onSelect: handleFileSelect,
   })
 
-  createEffect(() => {
-    local.model.recent()
-    refetch()
-  })
-
   createEffect(
     on(
       () => session.prompt.current(),
@@ -221,6 +268,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       setStore("popoverIsOpen", false)
     }
 
+    if (store.historyIndex >= 0) {
+      setStore("historyIndex", -1)
+      setStore("savedPrompt", null)
+    }
+
     session.prompt.set(rawParts, cursorPosition)
   }
 
@@ -296,12 +348,100 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       sessionID: session.id!,
     })
 
+  const addToHistory = (prompt: Prompt) => {
+    const text = prompt
+      .map((p) => p.content)
+      .join("")
+      .trim()
+    if (!text) return
+
+    const entry = clonePromptParts(prompt)
+    const lastEntry = history.entries[0]
+    if (lastEntry) {
+      const lastText = lastEntry.map((p) => p.content).join("")
+      if (lastText === text) return
+    }
+
+    setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
+  }
+
+  const navigateHistory = (direction: "up" | "down") => {
+    const entries = history.entries
+    const current = store.historyIndex
+
+    if (direction === "up") {
+      if (entries.length === 0) return false
+      if (current === -1) {
+        setStore("savedPrompt", clonePromptParts(session.prompt.current()))
+        setStore("historyIndex", 0)
+        applyHistoryPrompt(entries[0], "start")
+        return true
+      }
+      if (current < entries.length - 1) {
+        const next = current + 1
+        setStore("historyIndex", next)
+        applyHistoryPrompt(entries[next], "start")
+        return true
+      }
+      return false
+    }
+
+    if (current > 0) {
+      const next = current - 1
+      setStore("historyIndex", next)
+      applyHistoryPrompt(entries[next], "end")
+      return true
+    }
+    if (current === 0) {
+      setStore("historyIndex", -1)
+      const saved = store.savedPrompt
+      if (saved) {
+        applyHistoryPrompt(saved, "end")
+        setStore("savedPrompt", null)
+        return true
+      }
+      applyHistoryPrompt(DEFAULT_PROMPT, "end")
+      return true
+    }
+
+    return false
+  }
+
   const handleKeyDown = (event: KeyboardEvent) => {
     if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
       onKeyDown(event)
       event.preventDefault()
       return
     }
+
+    if (event.key === "ArrowUp" || event.key === "ArrowDown") {
+      const { collapsed, onFirstLine, onLastLine } = getCaretLineState()
+      if (!collapsed) return
+      const cursorPos = getCursorPosition(editorRef)
+      const textLength = promptLength(session.prompt.current())
+      const inHistory = store.historyIndex >= 0
+      const isStart = cursorPos === 0
+      const isEnd = cursorPos === textLength
+      const atAbsoluteStart = onFirstLine && isStart
+      const atAbsoluteEnd = onLastLine && isEnd
+      const allowUp = (inHistory && isEnd) || atAbsoluteStart
+      const allowDown = (inHistory && isStart) || atAbsoluteEnd
+
+      if (event.key === "ArrowUp") {
+        if (!allowUp) return
+        if (navigateHistory("up")) {
+          event.preventDefault()
+        }
+        return
+      }
+
+      if (!allowDown) return
+      if (navigateHistory("down")) {
+        event.preventDefault()
+      }
+      return
+    }
+
     if (event.key === "Enter" && !event.shiftKey) {
       handleSubmit(event)
     }
@@ -323,6 +463,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       return
     }
 
+    addToHistory(prompt)
+    setStore("historyIndex", -1)
+    setStore("savedPrompt", null)
+
     let existing = session.info()
     if (!existing) {
       const created = await sdk.client.session.create()
@@ -461,7 +605,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           />
           <Show when={!session.prompt.dirty()}>
             <div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
-              Ask anything... "{PLACEHOLDERS[placeholder()]}"
+              Ask anything... "{PLACEHOLDERS[store.placeholder]}"
             </div>
           </Show>
         </div>
@@ -507,6 +651,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                         items={models}
                         current={local.model.current()}
                         filterKeys={["provider.name", "name", "id"]}
+                        sortBy={(a, b) => a.name.localeCompare(b.name)}
                         // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
                         groupBy={(x) => x.provider.name}
                         sortGroupsBy={(a, b) => {

+ 14 - 5
packages/desktop/src/components/terminal.tsx

@@ -1,8 +1,9 @@
 import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
-import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
+import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
 import { useSDK } from "@/context/sdk"
 import { SerializeAddon } from "@/addons/serialize"
 import { LocalPTY } from "@/context/session"
+import { usePrefersDark } from "@solid-primitives/media"
 
 export interface TerminalProps extends ComponentProps<"div"> {
   pty: LocalPTY
@@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
   let serializeAddon: SerializeAddon
   let fitAddon: FitAddon
   let handleResize: () => void
+  const prefersDark = usePrefersDark()
 
   onMount(async () => {
     ghostty = await Ghostty.load()
@@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
       fontSize: 14,
       fontFamily: "TX-02, monospace",
       allowTransparency: true,
-      theme: {
-        background: "#191515",
-        foreground: "#d4d4d4",
-      },
+      theme: prefersDark()
+        ? {
+            background: "#191515",
+            foreground: "#d4d4d4",
+            cursor: "#d4d4d4",
+          }
+        : {
+            background: "#fcfcfc",
+            foreground: "#211e1e",
+            cursor: "#211e1e",
+          },
       scrollback: 10_000,
       ghostty,
     })

+ 40 - 35
packages/desktop/src/pages/layout.tsx

@@ -189,11 +189,13 @@ export default function Layout(props: ParentProps) {
     const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
     const name = createMemo(() => getFilename(props.project.worktree))
     const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
+    const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
+
     return (
       <div class="relative size-6 shrink-0">
         <Avatar
           fallback={name()}
-          src={props.project.icon?.url}
+          src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
           {...getAvatarColors(props.project.icon?.color)}
           class={`size-full ${props.class ?? ""}`}
           style={
@@ -318,22 +320,20 @@ export default function Layout(props: ParentProps) {
                         )
                       }
                       return (
-                        <A
-                          href={`${slug()}/session/${session.id}`}
-                          class="group/session focus:outline-none cursor-default"
+                        <div
+                          class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
+                                 hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
                         >
-                          <Tooltip placement="right" value={session.title}>
-                            <div
-                              class="relative w-full pl-4 pr-1 py-1 rounded-md
-                                     group-[.active]/session:bg-surface-raised-base-hover
-                                     group-hover/session:bg-surface-raised-base-hover
-                                     group-focus/session:bg-surface-raised-base-hover"
+                          <Tooltip placement="right" value={session.title} gutter={10}>
+                            <A
+                              href={`${slug()}/session/${session.id}`}
+                              class="flex flex-col min-w-0 text-left w-full focus:outline-none"
                             >
-                              <div class="flex items-center self-stretch gap-6 justify-between">
+                              <div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
                                 <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
                                   {session.title}
                                 </span>
-                                <div class="shrink-0 group-hover/session:hidden mr-1">
+                                <div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
                                   <Switch>
                                     <Match when={hasError()}>
                                       <div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
@@ -358,12 +358,6 @@ export default function Layout(props: ParentProps) {
                                     </Match>
                                   </Switch>
                                 </div>
-                                <div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
-                                  {/* <IconButton icon="dot-grid" variant="ghost" /> */}
-                                  <Tooltip placement="right" value="Archive session">
-                                    <IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
-                                  </Tooltip>
-                                </div>
                               </div>
                               <Show when={session.summary?.files}>
                                 <div class="flex justify-between items-center self-stretch">
@@ -371,29 +365,40 @@ export default function Layout(props: ParentProps) {
                                   <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
                                 </div>
                               </Show>
-                            </div>
+                            </A>
                           </Tooltip>
-                        </A>
+                          <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
+                            {/* <IconButton icon="dot-grid" variant="ghost" /> */}
+                            <Tooltip placement="right" value="Archive session">
+                              <IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
+                            </Tooltip>
+                          </div>
+                        </div>
                       )
                     }}
                   </For>
                   <Show when={sessions().length === 0}>
-                    <A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default">
-                      <Tooltip placement="right" value="New session">
-                        <div
-                          class="relative w-full pl-4 pr-1 py-1 rounded-md
-                                 group-[.active]/session:bg-surface-raised-base-hover
-                                 group-hover/session:bg-surface-raised-base-hover
-                                 group-focus/session:bg-surface-raised-base-hover"
-                        >
-                          <div class="flex items-center self-stretch gap-6 justify-between">
-                            <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
-                              New session
-                            </span>
-                          </div>
+                    <div
+                      class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
+                             hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
+                    >
+                      <div class="flex items-center self-stretch w-full">
+                        <div class="flex-1 min-w-0">
+                          <Tooltip placement="right" value="New session">
+                            <A
+                              href={`${slug()}/session`}
+                              class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
+                            >
+                              <div class="flex items-center self-stretch gap-6 justify-between">
+                                <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
+                                  New session
+                                </span>
+                              </div>
+                            </A>
+                          </Tooltip>
                         </div>
-                      </Tooltip>
-                    </A>
+                      </div>
+                    </div>
                   </Show>
                 </nav>
               </Collapsible.Content>

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "private": true,
   "type": "module",
   "scripts": {

+ 6 - 6
packages/extensions/zed/extension.toml

@@ -1,7 +1,7 @@
 id = "opencode"
 name = "OpenCode"
 description = "The open source coding agent."
-version = "1.0.152"
+version = "1.0.153"
 schema_version = 1
 authors = ["Anomaly"]
 repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
 icon = "./icons/opencode.svg"
 
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-arm64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-arm64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.darwin-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-arm64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-x64.tar.gz"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-x64.tar.gz"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.windows-x86_64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-windows-x64.zip"
+archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 1 - 1
packages/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/function",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 10 - 2
packages/opencode/Dockerfile

@@ -1,10 +1,18 @@
-FROM alpine
+FROM alpine AS base
 
 # Disable the runtime transpiler cache by default inside Docker containers.
 # On ephemeral containers, the cache is not useful
 ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
 ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
 RUN apk add libgcc libstdc++ ripgrep
-ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+
+FROM base AS build-amd64
+COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
+
+FROM base AS build-arm64
+COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
+
+ARG TARGETARCH
+FROM build-${TARGETARCH}
 RUN opencode --version
 ENTRYPOINT ["opencode"]

+ 1 - 1
packages/opencode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "name": "opencode",
   "type": "module",
   "private": true,

+ 3 - 0
packages/opencode/script/build.ts

@@ -117,6 +117,9 @@ for (const item of targets) {
     compile: {
       autoloadBunfig: false,
       autoloadDotenv: false,
+      //@ts-ignore (bun types aren't up to date)
+      autoloadTsconfig: true,
+      autoloadPackageJson: true,
       target: name.replace(pkg.name, "bun") as any,
       outfile: `dist/${name}/bin/opencode`,
       execArgv: [`--user-agent=opencode/${Script.version}`, "--"],

+ 4 - 4
packages/opencode/script/publish.ts

@@ -244,8 +244,8 @@ if (!Script.preview) {
   await $`cd ./dist/homebrew-tap && git push`
 
   const image = "ghcr.io/sst/opencode"
-  await $`docker build -t ${image}:${Script.version} .`
-  await $`docker push ${image}:${Script.version}`
-  await $`docker tag ${image}:${Script.version} ${image}:latest`
-  await $`docker push ${image}:latest`
+  const platforms = "linux/amd64,linux/arm64"
+  const tags = [`${image}:${Script.version}`, `${image}:latest`]
+  const tagFlags = tags.flatMap((t) => ["-t", t])
+  await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
 }

+ 10 - 41
packages/opencode/src/bun/index.ts

@@ -85,47 +85,16 @@ export namespace BunProc {
       version,
     })
 
-    const total = 3
-    const wait = 500
-
-    const runInstall = async (count: number = 1): Promise<void> => {
-      log.info("bun install attempt", {
-        pkg,
-        version,
-        attempt: count,
-        total,
-      })
-      await BunProc.run(args, {
-        cwd: Global.Path.cache,
-      }).catch(async (error) => {
-        log.warn("bun install failed", {
-          pkg,
-          version,
-          attempt: count,
-          total,
-          error,
-        })
-        if (count >= total) {
-          throw new InstallFailedError(
-            { pkg, version },
-            {
-              cause: error,
-            },
-          )
-        }
-        const delay = wait * count
-        log.info("bun install retrying", {
-          pkg,
-          version,
-          next: count + 1,
-          delay,
-        })
-        await Bun.sleep(delay)
-        return runInstall(count + 1)
-      })
-    }
-
-    await runInstall()
+    await BunProc.run(args, {
+      cwd: Global.Path.cache,
+    }).catch((e) => {
+      throw new InstallFailedError(
+        { pkg, version },
+        {
+          cause: e,
+        },
+      )
+    })
 
     // Resolve actual version from installed package when using "latest"
     // This ensures subsequent starts use the cached version until explicitly updated

+ 28 - 5
packages/opencode/src/cli/cmd/github.ts

@@ -411,17 +411,30 @@ export const GithubRunCommand = cmd({
       let exitCode = 0
       type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
       const triggerCommentId = payload.comment.id
+      const useGithubToken = normalizeUseGithubToken()
 
       try {
-        const actionToken = isMock ? args.token! : await getOidcToken()
-        appToken = await exchangeForAppToken(actionToken)
+        if (useGithubToken) {
+          const githubToken = process.env["GITHUB_TOKEN"]
+          if (!githubToken) {
+            throw new Error(
+              "GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
+            )
+          }
+          appToken = githubToken
+        } else {
+          const actionToken = isMock ? args.token! : await getOidcToken()
+          appToken = await exchangeForAppToken(actionToken)
+        }
         octoRest = new Octokit({ auth: appToken })
         octoGraph = graphql.defaults({
           headers: { authorization: `token ${appToken}` },
         })
 
         const { userPrompt, promptFiles } = await getUserPrompt()
-        await configureGit(appToken)
+        if (!useGithubToken) {
+          await configureGit(appToken)
+        }
         await assertPermissions()
 
         await addReaction()
@@ -514,8 +527,10 @@ export const GithubRunCommand = cmd({
         // Also output the clean error message for the action to capture
         //core.setOutput("prepare_error", e.message);
       } finally {
-        await restoreGitConfig()
-        await revokeAppToken()
+        if (!useGithubToken) {
+          await restoreGitConfig()
+          await revokeAppToken()
+        }
       }
       process.exit(exitCode)
 
@@ -544,6 +559,14 @@ export const GithubRunCommand = cmd({
         throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
       }
 
+      function normalizeUseGithubToken() {
+        const value = process.env["USE_GITHUB_TOKEN"]
+        if (!value) return false
+        if (value === "true") return true
+        if (value === "false") return false
+        throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
+      }
+
       function isIssueCommentEvent(
         event: IssueCommentEvent | PullRequestReviewCommentEvent,
       ): event is IssueCommentEvent {

+ 1 - 1
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -218,7 +218,7 @@ function App() {
   let continued = false
   createEffect(() => {
     if (continued || sync.status !== "complete" || !args.continue) return
-    const match = sync.data.session.at(0)?.id
+    const match = sync.data.session.find((x) => x.parentID === undefined)?.id
     if (match) {
       continued = true
       route.navigate({ type: "session", sessionID: match })

+ 6 - 1
packages/opencode/src/cli/cmd/tui/attach.ts

@@ -14,12 +14,17 @@ export const AttachCommand = cmd({
       .option("dir", {
         type: "string",
         description: "directory to run in",
+      })
+      .option("session", {
+        alias: ["s"],
+        type: "string",
+        describe: "session id to continue",
       }),
   handler: async (args) => {
     if (args.dir) process.chdir(args.dir)
     await tui({
       url: args.url,
-      args: {},
+      args: { sessionID: args.session },
     })
   },
 })

+ 8 - 16
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -705,8 +705,8 @@ export function Prompt(props: PromptProps) {
           >
             <textarea
               placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
-              textColor={theme.text}
-              focusedTextColor={theme.text}
+              textColor={keybind.leader ? theme.textMuted : theme.text}
+              focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
               minHeight={1}
               maxHeight={6}
               onContentChange={() => {
@@ -854,7 +854,7 @@ export function Prompt(props: PromptProps) {
               </text>
               <Show when={store.mode === "normal"}>
                 <box flexDirection="row" gap={1}>
-                  <text flexShrink={0} fg={theme.text}>
+                  <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
                     {local.model.parsed().model}
                   </text>
                   <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
@@ -869,25 +869,17 @@ export function Prompt(props: PromptProps) {
           borderColor={highlight()}
           customBorderChars={{
             ...EmptyBorder,
-            // when the background is transparent, don't draw the vertical line
-            vertical: theme.background.a != 0 ? "╹" : " ",
+            vertical: "╹",
           }}
         >
           <box
             height={1}
             border={["bottom"]}
             borderColor={theme.backgroundElement}
-            customBorderChars={
-              theme.background.a != 0
-                ? {
-                    ...EmptyBorder,
-                    horizontal: "▀",
-                  }
-                : {
-                    ...EmptyBorder,
-                    horizontal: " ",
-                  }
-            }
+            customBorderChars={{
+              ...EmptyBorder,
+              horizontal: "▀",
+            }}
           />
         </box>
         <box flexDirection="row" justifyContent="space-between">

+ 4 - 1
packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx

@@ -277,7 +277,10 @@ export function Sidebar(props: { sessionID: string }) {
               </box>
             </box>
           </Show>
-          <text fg={theme.text}>{directory()}</text>
+          <text>
+            <span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
+            <span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
+          </text>
           <text fg={theme.textMuted}>
             <span style={{ fg: theme.success }}>•</span> <b>Open</b>
             <span style={{ fg: theme.text }}>

+ 18 - 0
packages/opencode/src/format/formatter.ts

@@ -275,3 +275,21 @@ export const terraform: Info = {
     return Bun.which("terraform") !== null
   },
 }
+
+export const latexindent: Info = {
+  name: "latexindent",
+  command: ["latexindent", "-w", "-s", "$FILE"],
+  extensions: [".tex"],
+  async enabled() {
+    return Bun.which("latexindent") !== null
+  },
+}
+
+export const gleam: Info = {
+  name: "gleam",
+  command: ["gleam", "format", "$FILE"],
+  extensions: [".gleam"],
+  async enabled() {
+    return Bun.which("gleam") !== null
+  },
+}

+ 1 - 0
packages/opencode/src/lsp/language.ts

@@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
   ".gitrebase": "git-rebase",
   ".go": "go",
   ".groovy": "groovy",
+  ".gleam": "gleam",
   ".hbs": "handlebars",
   ".handlebars": "handlebars",
   ".hs": "haskell",

+ 141 - 0
packages/opencode/src/lsp/server.ts

@@ -1386,4 +1386,145 @@ export namespace LSPServer {
       }
     },
   }
+
+  export const TexLab: Info = {
+    id: "texlab",
+    extensions: [".tex", ".bib"],
+    root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
+    async spawn(root) {
+      let bin = Bun.which("texlab", {
+        PATH: process.env["PATH"] + ":" + Global.Path.bin,
+      })
+
+      if (!bin) {
+        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+        log.info("downloading texlab from GitHub releases")
+
+        const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
+        if (!response.ok) {
+          log.error("Failed to fetch texlab release info")
+          return
+        }
+
+        const release = (await response.json()) as {
+          tag_name?: string
+          assets?: { name?: string; browser_download_url?: string }[]
+        }
+        const version = release.tag_name?.replace("v", "")
+        if (!version) {
+          log.error("texlab release did not include a version tag")
+          return
+        }
+
+        const platform = process.platform
+        const arch = process.arch
+
+        const texArch = arch === "arm64" ? "aarch64" : "x86_64"
+        const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
+        const ext = platform === "win32" ? "zip" : "tar.gz"
+        const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
+
+        const assets = release.assets ?? []
+        const asset = assets.find((a) => a.name === assetName)
+        if (!asset?.browser_download_url) {
+          log.error(`Could not find asset ${assetName} in texlab release`)
+          return
+        }
+
+        const downloadResponse = await fetch(asset.browser_download_url)
+        if (!downloadResponse.ok) {
+          log.error("Failed to download texlab")
+          return
+        }
+
+        const tempPath = path.join(Global.Path.bin, assetName)
+        await Bun.file(tempPath).write(downloadResponse)
+
+        if (ext === "zip") {
+          await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
+        }
+        if (ext === "tar.gz") {
+          await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
+        }
+
+        await fs.rm(tempPath, { force: true })
+
+        bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
+
+        if (!(await Bun.file(bin).exists())) {
+          log.error("Failed to extract texlab binary")
+          return
+        }
+
+        if (platform !== "win32") {
+          await $`chmod +x ${bin}`.nothrow()
+        }
+
+        log.info("installed texlab", { bin })
+      }
+
+      return {
+        process: spawn(bin, {
+          cwd: root,
+        }),
+      }
+    },
+  }
+
+  export const DockerfileLS: Info = {
+    id: "dockerfile",
+    extensions: [".dockerfile", "Dockerfile"],
+    root: async () => Instance.directory,
+    async spawn(root) {
+      let binary = Bun.which("docker-langserver")
+      const args: string[] = []
+      if (!binary) {
+        const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
+        if (!(await Bun.file(js).exists())) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
+      }
+      args.push("--stdio")
+      const proc = spawn(binary, args, {
+        cwd: root,
+        env: {
+          ...process.env,
+          BUN_BE_BUN: "1",
+        },
+      })
+      return {
+        process: proc,
+      }
+    },
+  }
+
+  export const Gleam: Info = {
+    id: "gleam",
+    extensions: [".gleam"],
+    root: NearestRoot(["gleam.toml"]),
+    async spawn(root) {
+      const gleam = Bun.which("gleam")
+      if (!gleam) {
+        log.info("gleam not found, please install gleam first")
+        return
+      }
+      return {
+        process: spawn(gleam, ["lsp"], {
+          cwd: root,
+        }),
+      }
+    },
+  }
 }

+ 20 - 5
packages/opencode/src/provider/transform.ts

@@ -199,14 +199,29 @@ export namespace ProviderTransform {
   }
 
   export function temperature(model: Provider.Model) {
-    if (model.api.id.toLowerCase().includes("qwen")) return 0.55
-    if (model.api.id.toLowerCase().includes("claude")) return undefined
-    if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0
-    return 0
+    const id = model.id.toLowerCase()
+    if (id.includes("qwen")) return 0.55
+    if (id.includes("claude")) return undefined
+    if (id.includes("gemini-3-pro")) return 1.0
+    if (id.includes("glm-4.6")) return 1.0
+    if (id.includes("minimax-m2")) return 1.0
+    // if (id.includes("kimi-k2")) {
+    //   if (id.includes("thinking")) return 1.0
+    //   return 0.6
+    // }
+    return undefined
   }
 
   export function topP(model: Provider.Model) {
-    if (model.api.id.toLowerCase().includes("qwen")) return 1
+    const id = model.id.toLowerCase()
+    if (id.includes("qwen")) return 1
+    if (id.includes("minimax-m2")) return 0.95
+    return undefined
+  }
+
+  export function topK(model: Provider.Model) {
+    const id = model.id.toLowerCase()
+    if (id.includes("minimax-m2")) return 40
     return undefined
   }
 

+ 2 - 0
packages/opencode/src/session/llm.ts

@@ -10,6 +10,7 @@ import type { MessageV2 } from "./message-v2"
 import { Plugin } from "@/plugin"
 import { SystemPrompt } from "./system"
 import { ToolRegistry } from "@/tool/registry"
+import { Flag } from "@/flag/flag"
 
 export namespace LLM {
   const log = Log.create({ service: "llm" })
@@ -133,6 +134,7 @@ export namespace LLM {
               "x-opencode-project": Instance.project.id,
               "x-opencode-session": input.sessionID,
               "x-opencode-request": input.user.id,
+              "x-opencode-client": Flag.OPENCODE_CLIENT,
             }
           : undefined),
         ...input.model.headers,

+ 9 - 0
packages/opencode/src/session/retry.ts

@@ -68,6 +68,15 @@ export namespace SessionRetry {
         if (json.code === "Some resource has been exhausted") {
           return "Provider is overloaded"
         }
+        if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
+          return "Rate Limited"
+        }
+        if (
+          json.error?.message?.includes("no_kv_space") ||
+          (json.type === "error" && json.error?.type === "server_error")
+        ) {
+          return "Provider Server Error"
+        }
       } catch {}
     }
 

+ 7 - 4
packages/opencode/src/tool/edit.ts

@@ -18,6 +18,8 @@ import { Instance } from "../project/instance"
 import { Agent } from "../agent/agent"
 import { Snapshot } from "@/snapshot"
 
+const MAX_DIAGNOSTICS_PER_FILE = 20
+
 function normalizeLineEndings(text: string): string {
   return text.replaceAll("\r\n", "\n")
 }
@@ -141,10 +143,11 @@ export const EditTool = Tool.define("edit", {
     for (const [file, issues] of Object.entries(diagnostics)) {
       if (issues.length === 0) continue
       if (file === filePath) {
-        output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues
-          .filter((item) => item.severity === 1)
-          .map(LSP.Diagnostic.pretty)
-          .join("\n")}\n</file_diagnostics>\n`
+        const errors = issues.filter((item) => item.severity === 1)
+        const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
+        const suffix =
+          errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
+        output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
         continue
       }
     }

+ 12 - 2
packages/opencode/src/tool/write.ts

@@ -11,6 +11,9 @@ import { Filesystem } from "../util/filesystem"
 import { Instance } from "../project/instance"
 import { Agent } from "../agent/agent"
 
+const MAX_DIAGNOSTICS_PER_FILE = 20
+const MAX_PROJECT_DIAGNOSTICS_FILES = 5
+
 export const WriteTool = Tool.define("write", {
   description: DESCRIPTION,
   parameters: z.object({
@@ -77,13 +80,20 @@ export const WriteTool = Tool.define("write", {
     let output = ""
     await LSP.touchFile(filepath, true)
     const diagnostics = await LSP.diagnostics()
+    let projectDiagnosticsCount = 0
     for (const [file, issues] of Object.entries(diagnostics)) {
       if (issues.length === 0) continue
+      const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4))
+      const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE)
+      const suffix =
+        issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
       if (file === filepath) {
-        output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
+        output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
         continue
       }
-      output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
+      if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
+      projectDiagnosticsCount++
+      output += `\n<project_diagnostics>\n${file}\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</project_diagnostics>\n`
     }
 
     return {

+ 2 - 2
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/plugin",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",
@@ -24,4 +24,4 @@
     "typescript": "catalog:",
     "@typescript/native-preview": "catalog:"
   }
-}
+}

+ 2 - 2
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/sdk",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo --noEmit",
@@ -29,4 +29,4 @@
   "publishConfig": {
     "directory": "dist"
   }
-}
+}

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/slack",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "scripts": {
     "dev": "bun run src/index.ts",

+ 1 - 1
packages/tauri/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/tauri",
   "private": true,
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "scripts": {
     "typecheck": "tsgo -b",

+ 2 - 2
packages/tauri/src-tauri/tauri.conf.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://schema.tauri.app/config/2",
   "productName": "OpenCode",
-  "mainBinaryName": "OpenCode Desktop",
+  "mainBinaryName": "OpenCode",
   "version": "../package.json",
   "identifier": "ai.opencode.desktop",
   "build": {
@@ -19,7 +19,7 @@
   },
   "bundle": {
     "active": true,
-    "targets": ["deb", "rpm", "dmg", "nsis"],
+    "targets": ["deb", "rpm", "dmg", "nsis", "appimage"],
     "icon": ["icons/32x32.png", "icons/128x128.png", "icons/[email protected]", "icons/icon.icns", "icons/icon.ico"],
     "externalBin": ["sidecars/opencode"],
     "createUpdaterArtifacts": true,

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/ui",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "type": "module",
   "exports": {
     "./*": "./src/components/*.tsx",

+ 3 - 1
packages/ui/src/components/basic-tool.tsx

@@ -13,7 +13,9 @@ export type TriggerTitle = {
 }
 
 const isTriggerTitle = (val: any): val is TriggerTitle => {
-  return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node)
+  return (
+    typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
+  )
 }
 
 export interface BasicToolProps {

+ 1 - 1
packages/ui/src/components/button.css

@@ -148,7 +148,7 @@
       padding: 0 12px 0 8px;
     }
 
-    gap: 4px;
+    gap: 8px;
 
     /* text-14-medium */
     font-family: var(--font-family-sans);

+ 0 - 1
packages/ui/src/components/message-part.tsx

@@ -321,7 +321,6 @@ ToolRegistry.register({
   render(props) {
     return (
       <BasicTool
-        defaultOpen
         icon="console"
         trigger={{
           title: "Shell",

+ 18 - 12
packages/ui/src/components/session-turn.css

@@ -1,5 +1,6 @@
 [data-component="session-turn"] {
   /* flex: 1; */
+  --scroll-y: 0px;
   height: 100%;
   min-height: 0;
   min-width: 0;
@@ -26,18 +27,27 @@
     align-items: flex-start;
     align-self: stretch;
     min-width: 0;
-    gap: 32px;
+    gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
+    overflow-anchor: none;
   }
 
-  [data-slot="session-turn-sticky-header"] {
+  [data-slot="session-turn-sticky-title"] {
     width: 100%;
     position: sticky;
     top: 0;
     background-color: var(--background-stronger);
+    z-index: 21;
+    /* padding-bottom: clamp(0px, calc(8px - var(--scroll-y) * 0.16), 8px); */
+  }
+
+  [data-slot="session-turn-response-trigger"] {
+    position: sticky;
+    top: 32px;
+    background-color: var(--background-stronger);
     z-index: 20;
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
+    width: calc(100% + 9px);
+    margin-left: -9px;
+    padding-left: 9px;
     padding-bottom: 8px;
   }
 
@@ -49,13 +59,8 @@
     height: 32px;
   }
 
-  /* [data-slot="session-turn-message-content"] { */
-  /* } */
-
-  [data-slot="session-turn-response-trigger"] {
-    width: calc(100% + 9px);
-    margin-left: -9px;
-    padding-left: 9px;
+  [data-slot="session-turn-message-content"] {
+    margin-top: -24px;
   }
 
   [data-slot="session-turn-message-title"] {
@@ -292,6 +297,7 @@
   [data-slot="session-turn-collapsible"] {
     gap: 32px;
     overflow: visible;
+    /* margin-top: clamp(8px, calc(24px - var(--scroll-y) * 0.32), 24px); */
   }
 
   [data-slot="session-turn-collapsible-trigger-content"] {

+ 81 - 55
packages/ui/src/components/session-turn.tsx

@@ -3,18 +3,7 @@ import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { checksum } from "@opencode-ai/util/encode"
-import {
-  createEffect,
-  createMemo,
-  createSignal,
-  For,
-  Match,
-  onCleanup,
-  onMount,
-  ParentProps,
-  Show,
-  Switch,
-} from "solid-js"
+import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
@@ -60,45 +49,77 @@ export function SessionTurn(
   const working = createMemo(() => status()?.type !== "idle")
 
   let scrollRef: HTMLDivElement | undefined
-  const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
-  const [stickyHeaderRef, setStickyHeaderRef] = createSignal<HTMLDivElement>()
-  const [userScrolled, setUserScrolled] = createSignal(false)
-  const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0)
+  const [state, setState] = createStore({
+    contentRef: undefined as HTMLDivElement | undefined,
+    stickyTitleRef: undefined as HTMLDivElement | undefined,
+    stickyTriggerRef: undefined as HTMLDivElement | undefined,
+    userScrolled: false,
+    stickyHeaderHeight: 0,
+    scrollY: 0,
+    autoScrolling: false,
+  })
 
   function handleScroll() {
     if (!scrollRef) return
+    setState("scrollY", scrollRef.scrollTop)
+    if (state.autoScrolling) return
     const { scrollTop, scrollHeight, clientHeight } = scrollRef
     const atBottom = scrollHeight - scrollTop - clientHeight < 50
     if (!atBottom && working()) {
-      setUserScrolled(true)
+      setState("userScrolled", true)
     }
   }
 
   function handleInteraction() {
     if (working()) {
-      setUserScrolled(true)
+      setState("userScrolled", true)
     }
   }
 
+  function scrollToBottom() {
+    if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
+    setState("autoScrolling", true)
+    requestAnimationFrame(() => {
+      scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "auto" })
+      requestAnimationFrame(() => {
+        setState("autoScrolling", false)
+      })
+    })
+  }
+
   createEffect(() => {
     if (!working()) {
-      setUserScrolled(false)
+      setState("userScrolled", false)
     }
   })
 
-  createResizeObserver(contentRef, () => {
-    if (!scrollRef || userScrolled() || !working()) return
-    scrollRef.scrollTop = scrollRef.scrollHeight
-  })
+  createResizeObserver(
+    () => state.contentRef,
+    () => {
+      scrollToBottom()
+    },
+  )
 
-  createResizeObserver(stickyHeaderRef, ({ height }) => {
-    setStickyHeaderHeight(height + 8)
-  })
+  createResizeObserver(
+    () => state.stickyTitleRef,
+    ({ height }) => {
+      const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
+      setState("stickyHeaderHeight", height + triggerHeight + 8)
+    },
+  )
+
+  createResizeObserver(
+    () => state.stickyTriggerRef,
+    ({ height }) => {
+      const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
+      setState("stickyHeaderHeight", titleHeight + height + 8)
+    },
+  )
 
   return (
-    <div data-component="session-turn" class={props.classes?.root}>
+    <div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
       <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
-        <div ref={setContentRef} onClick={handleInteraction}>
+        <div ref={(el) => setState("contentRef", el)} onClick={handleInteraction}>
           <Show when={message()}>
             {(message) => {
               const assistantMessages = createMemo(() => {
@@ -175,6 +196,9 @@ export function SessionTurn(
                       break
                   }
                 } else if (last.type === "reasoning") {
+                  const text = last.text ?? ""
+                  const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
+                  if (match) return `Thinking · ${match[1].trim()}`
                   return "Thinking"
                 } else if (last.type === "text") {
                   return "Gathering thoughts"
@@ -237,7 +261,7 @@ export function SessionTurn(
 
               createEffect((prev) => {
                 const isWorking = working()
-                if (prev && !isWorking && !userScrolled()) {
+                if (prev && !isWorking && !state.userScrolled) {
                   setStore("stepsExpanded", false)
                 }
                 return isWorking
@@ -248,10 +272,10 @@ export function SessionTurn(
                   data-message={message().id}
                   data-slot="session-turn-message-container"
                   class={props.classes?.container}
-                  style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }}
+                  style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
                 >
-                  {/* Sticky Header */}
-                  <div ref={setStickyHeaderRef} data-slot="session-turn-sticky-header">
+                  {/* Title (sticky) */}
+                  <div ref={(el) => setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
                     <div data-slot="session-turn-message-header">
                       <div data-slot="session-turn-message-title">
                         <Switch>
@@ -264,29 +288,31 @@ export function SessionTurn(
                         </Switch>
                       </div>
                     </div>
-                    <div data-slot="session-turn-message-content">
-                      <Message message={message()} parts={parts()} />
-                    </div>
-                    <div data-slot="session-turn-response-trigger">
-                      <Button
-                        data-slot="session-turn-collapsible-trigger-content"
-                        variant="ghost"
-                        size="small"
-                        onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
-                      >
-                        <Show when={working()}>
-                          <Spinner />
-                        </Show>
-                        <Switch>
-                          <Match when={working()}>{store.status ?? "Considering next steps..."}</Match>
-                          <Match when={store.stepsExpanded}>Hide steps</Match>
-                          <Match when={!store.stepsExpanded}>Show steps</Match>
-                        </Switch>
-                        <span>·</span>
-                        <span>{store.duration}</span>
-                        <Icon name="chevron-grabber-vertical" size="small" />
-                      </Button>
-                    </div>
+                  </div>
+                  {/* User Message */}
+                  <div data-slot="session-turn-message-content">
+                    <Message message={message()} parts={parts()} />
+                  </div>
+                  {/* Trigger (sticky) */}
+                  <div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
+                    <Button
+                      data-slot="session-turn-collapsible-trigger-content"
+                      variant="ghost"
+                      size="small"
+                      onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
+                    >
+                      <Show when={working()}>
+                        <Spinner />
+                      </Show>
+                      <Switch>
+                        <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
+                        <Match when={store.stepsExpanded}>Hide steps</Match>
+                        <Match when={!store.stepsExpanded}>Show steps</Match>
+                      </Switch>
+                      <span>·</span>
+                      <span>{store.duration}</span>
+                      <Icon name="chevron-grabber-vertical" size="small" />
+                    </Button>
                   </div>
                   {/* Response */}
                   <Show when={store.stepsExpanded}>

+ 2 - 2
packages/ui/src/styles/animations.css

@@ -5,7 +5,7 @@
 @keyframes pulse-opacity {
   0%,
   100% {
-    opacity: 0;
+    opacity: 0.4;
   }
   50% {
     opacity: 1;
@@ -18,7 +18,7 @@
     opacity: 0;
   }
   50% {
-    opacity: 0.3;
+    opacity: 0.2;
   }
 }
 

+ 1 - 1
packages/util/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/util",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "private": true,
   "type": "module",
   "exports": {

+ 1 - 0
packages/web/astro.config.mjs

@@ -54,6 +54,7 @@ export default defineConfig({
         "",
         "config",
         "providers",
+        "network",
         "enterprise",
         "troubleshooting",
         "1-0",

+ 1 - 1
packages/web/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@opencode-ai/web",
   "type": "module",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "scripts": {
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

+ 19 - 0
packages/web/src/content/docs/acp.mdx

@@ -67,6 +67,25 @@ You can also bind a keyboard shortcut by editing your `keymap.json`:
 
 ---
 
+### JetBrains IDEs
+
+Add to your [JetBrains IDE](https://www.jetbrains.com/) acp.json according to the [documentation](https://www.jetbrains.com/help/ai-assistant/acp.html):
+
+```json title="acp.json"
+{
+  "agent_servers": {
+    "OpenCode": {
+      "command": "/absolute/path/bin/opencode",
+      "args": ["acp"]
+    }
+  }
+}
+```
+
+To open it, use the new 'OpenCode' agent in the AI Chat agent selector.
+
+---
+
 ### Avante.nvim
 
 Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration:

+ 15 - 11
packages/web/src/content/docs/ecosystem.mdx

@@ -15,17 +15,21 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
 
 ## Plugins
 
-| Name                                                                                              | Description                                                           |
-| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
-| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session)                 | Automatically inject Helicone session headers for request grouping    |
-| [opencode-skills](https://github.com/malhashemi/opencode-skills)                                  | Manage and organize OpenCode skills and capabilities                  |
-| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject)                           | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
-| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth)            | Use your ChatGPT Plus/Pro subscription instead of API credits         |
-| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth)                           | Use your existing Gemini plan instead of API billing                  |
-| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth)               | Use Antigravity's free models instead of API billing                  |
-| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs                 |
-| [opencode-wakatime](https://github.com/angristan/opencode-wakatime)                               | Track OpenCode usage with Wakatime                                    |
-| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main)   | Clean up markdown tables produced by LLMs                             |
+| Name                                                                                              | Description                                                                                   |
+| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session)                 | Automatically inject Helicone session headers for request grouping                            |
+| [opencode-skills](https://github.com/malhashemi/opencode-skills)                                  | Manage and organize OpenCode skills and capabilities                                          |
+| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject)                           | Auto-inject TypeScript/Svelte types into file reads with lookup tools                         |
+| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth)            | Use your ChatGPT Plus/Pro subscription instead of API credits                                 |
+| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth)                           | Use your existing Gemini plan instead of API billing                                          |
+| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth)               | Use Antigravity's free models instead of API billing                                          |
+| [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-goggle-antigravity-auth)  | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
+| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs                                         |
+| [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git)                | Add native websearch support for supported providers with Google grounded style               |
+| [opencode-pty](https://github.com/shekohex/opencode-pty.git)                                      | Enables AI agents to run background processes in a PTY, send interactive input to them.       |
+| [opencode-wakatime](https://github.com/angrister/opencode-wakatime)                               | Track OpenCode usage with Wakatime                                                            |
+| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main)   | Clean up markdown tables produced by LLMs                                                     |
+| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode)                                  | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible        |
 
 ---
 

+ 1 - 0
packages/web/src/content/docs/formatters.mdx

@@ -29,6 +29,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
 | dart           | .dart                                                                                                    | `dart` command available                                       |
 | ocamlformat    | .ml, .mli                                                                                                | `ocamlformat` command available and `.ocamlformat` config file |
 | terraform      | .tf, .tfvars                                                                                             | `terraform` command available                                  |
+| gleam          | .gleam                                                                                                   | `gleam` command available                                      |
 
 So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.
 

+ 1 - 0
packages/web/src/content/docs/lsp.mdx

@@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
 | ocaml-lsp          | .ml, .mli                                            | `ocamllsp` command available                                 |
 | terraform          | .tf, .tfvars                                         | Auto-installs from GitHub releases                           |
 | bash               | .sh, .bash, .zsh, .ksh                               | Auto-installs bash-language-server                           |
+| gleam              | .gleam                                               | `gleam` command available                                    |
 
 LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
 

+ 57 - 0
packages/web/src/content/docs/network.mdx

@@ -0,0 +1,57 @@
+---
+title: Network
+description: Configure proxies and custom certificates.
+---
+
+OpenCode supports standard proxy environment variables and custom certificates for enterprise network environments.
+
+---
+
+## Proxy
+
+OpenCode respects standard proxy environment variables.
+
+```bash
+# HTTPS proxy (recommended)
+export HTTPS_PROXY=https://proxy.example.com:8080
+
+# HTTP proxy (if HTTPS not available)
+export HTTP_PROXY=http://proxy.example.com:8080
+
+# Bypass proxy for local server (required)
+export NO_PROXY=localhost,127.0.0.1
+```
+
+:::caution
+The TUI communicates with a local HTTP server. You must bypass the proxy for this connection to prevent routing loops.
+:::
+
+You can configure the server's port and hostname using [CLI flags](/docs/cli#run).
+
+---
+
+### Authenticate
+
+If your proxy requires basic authentication, include credentials in the URL.
+
+```bash
+export HTTPS_PROXY=http://username:[email protected]:8080
+```
+
+:::caution
+Avoid hardcoding passwords. Use environment variables or secure credential storage.
+:::
+
+For proxies requiring advanced authentication like NTLM or Kerberos, consider using an LLM Gateway that supports your authentication method.
+
+---
+
+## Custom certificates
+
+If your enterprise uses custom CAs for HTTPS connections, configure OpenCode to trust them.
+
+```bash
+export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
+```
+
+This works for both proxy connections and direct API access.

+ 1 - 1
sdks/vscode/package.json

@@ -2,7 +2,7 @@
   "name": "opencode",
   "displayName": "opencode",
   "description": "opencode for VS Code",
-  "version": "1.0.152",
+  "version": "1.0.153",
   "publisher": "sst-dev",
   "repository": {
     "type": "git",