Просмотр исходного кода

Merge public/dev: Update permission system for webgui

- Event type changed from permission.updated to permission.asked
- Permission structure now uses 'permission' field instead of 'type'
- pattern changed to patterns (array of strings)
- permissionID changed to requestID
- response changed to reply
- New endpoint /permission/:requestID/reply
- callID now accessed via tool.callID
- Added always field for future approval patterns
paviko 1 месяц назад
Родитель
Сommit
74faae7864
100 измененных файлов с 2771 добавлено и 974 удалено
  1. 4 0
      .github/CODEOWNERS
  2. 8 0
      .github/ISSUE_TEMPLATE/bug-report.yml
  3. 2 2
      .github/workflows/duplicate-issues.yml
  4. 63 0
      .github/workflows/duplicate-prs.yml
  5. 35 0
      .github/workflows/nix-desktop.yml
  6. 1 1
      .github/workflows/opencode.yml
  7. 1 1
      .github/workflows/publish.yml
  8. 1 1
      .github/workflows/review.yml
  9. 24 0
      .opencode/agent/duplicate-pr.md
  10. 1 1
      .opencode/command/issues.md
  11. 1 5
      .opencode/opencode.jsonc
  12. 52 0
      .opencode/tool/github-pr-search.ts
  13. 10 0
      .opencode/tool/github-pr-search.txt
  14. 1 1
      .opencode/tool/github-triage.ts
  15. 27 4
      CONTRIBUTING.md
  16. 3 3
      README.md
  17. 4 4
      README.zh-TW.md
  18. 28 25
      bun.lock
  19. 3 3
      flake.lock
  20. 16 4
      flake.nix
  21. 2 2
      github/README.md
  22. 1 1
      github/action.yml
  23. 1 1
      github/index.ts
  24. 1 0
      github/package.json
  25. 1 0
      infra/console.ts
  26. 5 5
      install
  27. 145 0
      nix/desktop.nix
  28. 1 1
      nix/hashes.json
  29. 1 1
      nix/opencode.nix
  30. 1 1
      package.json
  31. 1 1
      packages/app/AGENTS.md
  32. 1 1
      packages/app/index.html
  33. 1 1
      packages/app/package.json
  34. 302 376
      packages/app/src/components/prompt-input.tsx
  35. 7 1
      packages/app/src/components/session/session-context-tab.tsx
  36. 8 11
      packages/app/src/components/session/session-header.tsx
  37. 46 2
      packages/app/src/components/session/session-new-view.tsx
  38. 0 1
      packages/app/src/context/global-sdk.tsx
  39. 5 1
      packages/app/src/context/layout.tsx
  40. 13 6
      packages/app/src/context/local.tsx
  41. 1 1
      packages/app/src/context/platform.tsx
  42. 0 1
      packages/app/src/context/sdk.tsx
  43. 2 3
      packages/app/src/context/server.tsx
  44. 2 2
      packages/app/src/pages/home.tsx
  45. 105 76
      packages/app/src/pages/layout.tsx
  46. 192 232
      packages/app/src/pages/session.tsx
  47. 2 1
      packages/console/app/package.json
  48. 1 1
      packages/console/app/src/config.ts
  49. 1 1
      packages/console/app/src/routes/[...404].tsx
  50. 1 1
      packages/console/app/src/routes/download/[platform].ts
  51. 1 1
      packages/console/app/src/routes/openapi.json.ts
  52. 1 1
      packages/console/app/src/routes/temp.tsx
  53. 15 9
      packages/console/app/src/routes/zen/util/handler.ts
  54. 1 0
      packages/console/core/migrations/0040_broken_gamora.sql
  55. 1059 0
      packages/console/core/migrations/meta/0040_snapshot.json
  56. 7 0
      packages/console/core/migrations/meta/_journal.json
  57. 2 1
      packages/console/core/package.json
  58. 4 1
      packages/console/core/script/promote-models.ts
  59. 4 1
      packages/console/core/script/pull-models.ts
  60. 11 3
      packages/console/core/script/update-models.ts
  61. 2 1
      packages/console/core/src/model.ts
  62. 2 2
      packages/console/core/src/schema/billing.sql.ts
  63. 4 0
      packages/console/core/sst-env.d.ts
  64. 2 1
      packages/console/function/package.json
  65. 4 0
      packages/console/function/sst-env.d.ts
  66. 3 2
      packages/console/mail/package.json
  67. 1 0
      packages/console/resource/package.json
  68. 4 0
      packages/console/resource/sst-env.d.ts
  69. 0 1
      packages/desktop/index.html
  70. 2 1
      packages/desktop/package.json
  71. 127 0
      packages/desktop/src-tauri/release/appstream.metainfo.xml
  72. 8 1
      packages/desktop/src-tauri/tauri.prod.conf.json
  73. 2 2
      packages/desktop/src/index.tsx
  74. 2 1
      packages/enterprise/package.json
  75. 1 1
      packages/enterprise/src/routes/share/[shareID].tsx
  76. 4 0
      packages/enterprise/sst-env.d.ts
  77. 7 7
      packages/extensions/zed/extension.toml
  78. 2 1
      packages/function/package.json
  79. 4 0
      packages/function/sst-env.d.ts
  80. 2 2
      packages/opencode/AGENTS.md
  81. 8 6
      packages/opencode/package.json
  82. 10 10
      packages/opencode/script/publish-registries.ts
  83. 1 1
      packages/opencode/script/publish.ts
  84. 46 8
      packages/opencode/src/acp/agent.ts
  85. 7 0
      packages/opencode/src/agent/agent.ts
  86. 1 1
      packages/opencode/src/cli/cmd/auth.ts
  87. 55 13
      packages/opencode/src/cli/cmd/github.ts
  88. 27 11
      packages/opencode/src/cli/cmd/mcp.ts
  89. 6 0
      packages/opencode/src/cli/cmd/run.ts
  90. 11 3
      packages/opencode/src/cli/cmd/tui/app.tsx
  91. 2 0
      packages/opencode/src/cli/cmd/tui/attach.ts
  92. 0 1
      packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
  93. 14 7
      packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  94. 35 1
      packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
  95. 4 67
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  96. 73 0
      packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts
  97. 1 1
      packages/opencode/src/cli/cmd/tui/component/tips.ts
  98. 2 1
      packages/opencode/src/cli/cmd/tui/context/sdk.tsx
  99. 8 1
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  100. 37 15
      packages/opencode/src/cli/cmd/tui/context/theme.tsx

+ 4 - 0
.github/CODEOWNERS

@@ -0,0 +1,4 @@
+# web + desktop packages
+packages/app/      @adamdotdevin
+packages/tauri/    @adamdotdevin
+packages/desktop/  @adamdotdevin

+ 8 - 0
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -11,6 +11,14 @@ body:
     validations:
       required: true
 
+  - type: input
+    id: plugins
+    attributes:
+      label: Plugins
+      description: What plugins are you using?
+    validations:
+      required: false
+
   - type: input
     id: opencode-version
     attributes:

+ 2 - 2
.github/workflows/duplicate-issues.yml

@@ -28,8 +28,8 @@ jobs:
           OPENCODE_PERMISSION: |
             {
               "bash": {
-                "gh issue*": "allow",
-                "*": "deny"
+                "*": "deny",
+                "gh issue*": "allow"
               },
               "webfetch": "deny"
             }

+ 63 - 0
.github/workflows/duplicate-prs.yml

@@ -0,0 +1,63 @@
+name: Duplicate PR Check
+
+on:
+  pull_request_target:
+    types: [opened]
+
+jobs:
+  check-duplicates:
+    if: |
+      github.event.pull_request.user.login != 'actions-user' &&
+      github.event.pull_request.user.login != 'opencode' &&
+      github.event.pull_request.user.login != 'rekram1-node' &&
+      github.event.pull_request.user.login != 'thdxr' &&
+      github.event.pull_request.user.login != 'kommander' &&
+      github.event.pull_request.user.login != 'jayair' &&
+      github.event.pull_request.user.login != 'fwang' &&
+      github.event.pull_request.user.login != 'adamdotdevin' &&
+      github.event.pull_request.user.login != 'iamdavidhill' &&
+      github.event.pull_request.user.login != 'opencode-agent[bot]'
+    runs-on: blacksmith-4vcpu-ubuntu-2404
+    permissions:
+      contents: read
+      pull-requests: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      - name: Setup Bun
+        uses: ./.github/actions/setup-bun
+
+      - name: Install dependencies
+        run: bun install
+
+      - name: Install opencode
+        run: curl -fsSL https://opencode.ai/install | bash
+
+      - name: Build prompt
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          PR_NUMBER: ${{ github.event.pull_request.number }}
+        run: |
+          {
+            echo "Check for duplicate PRs related to this new PR:"
+            echo ""
+            echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)"
+            echo ""
+            echo "Description:"
+            gh pr view "$PR_NUMBER" --json body --jq .body
+          } > pr_info.txt
+
+      - name: Check for duplicate PRs
+        env:
+          OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          PR_NUMBER: ${{ github.event.pull_request.number }}
+        run: |
+          COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates")
+
+          gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_
+
+          $COMMENT"

+ 35 - 0
.github/workflows/nix-desktop.yml

@@ -0,0 +1,35 @@
+name: nix desktop
+
+on:
+  push:
+    branches: [dev]
+    paths:
+      - "flake.nix"
+      - "flake.lock"
+      - "nix/**"
+      - "packages/app/**"
+      - "packages/desktop/**"
+  workflow_dispatch:
+
+jobs:
+  build-desktop:
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - blacksmith-4vcpu-ubuntu-2404
+          - macos-latest
+    runs-on: ${{ matrix.os }}
+    timeout-minutes: 60
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v6
+
+      - name: Setup Nix
+        uses: DeterminateSystems/nix-installer-action@v21
+
+      - name: Build desktop via flake
+        run: |
+          set -euo pipefail
+          nix --version
+          nix build .#desktop -L

+ 1 - 1
.github/workflows/opencode.yml

@@ -26,7 +26,7 @@ jobs:
       - uses: ./.github/actions/setup-bun
 
       - name: Run opencode
-        uses: sst/opencode/github@latest
+        uses: anomalyco/opencode/github@latest
         env:
           OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
           OPENCODE_PERMISSION: '{"bash": "deny"}'

+ 1 - 1
.github/workflows/publish.yml

@@ -31,7 +31,7 @@ permissions:
 jobs:
   publish:
     runs-on: blacksmith-4vcpu-ubuntu-2404
-    if: github.repository == 'sst/opencode'
+    if: github.repository == 'anomalyco/opencode'
     steps:
       - uses: actions/checkout@v3
         with:

+ 1 - 1
.github/workflows/review.yml

@@ -47,7 +47,7 @@ jobs:
         env:
           ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh pr review*": "deny", "*": "deny" } }'
+          OPENCODE_PERMISSION: '{ "bash": {  "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }'
           PR_TITLE: ${{ steps.pr-details.outputs.title }}
         run: |
           PR_BODY=$(jq -r .body pr_data.json)

+ 24 - 0
.opencode/agent/duplicate-pr.md

@@ -0,0 +1,24 @@
+---
+mode: primary
+hidden: true
+model: opencode/claude-haiku-4-5
+color: "#E67E22"
+tools:
+  "*": false
+  "github-pr-search": true
+---
+
+You are a duplicate PR detection agent. When a PR is opened, your job is to search for potentially duplicate or related open PRs.
+
+Use the github-pr-search tool to search for PRs that might be addressing the same issue or feature.
+
+Search using keywords from the PR title and description. Try multiple searches with different relevant terms.
+
+If you find potential duplicates:
+
+- List them with their titles and URLs
+- Briefly explain why they might be related
+
+If no duplicates are found, say so clearly.
+
+Keep your response concise and actionable.

+ 1 - 1
.opencode/command/issues.md

@@ -3,7 +3,7 @@ description: "find issue(s) on github"
 model: opencode/claude-haiku-4-5
 ---
 
-Search through existing issues in sst/opencode using the gh cli to find issues matching this query:
+Search through existing issues in anomalyco/opencode using the gh cli to find issues matching this query:
 
 $ARGUMENTS
 

+ 1 - 5
.opencode/opencode.jsonc

@@ -10,11 +10,6 @@
       "options": {},
     },
   },
-  "permission": {
-    "bash": {
-      "ls foo": "ask",
-    },
-  },
   "mcp": {
     "context7": {
       "type": "remote",
@@ -23,5 +18,6 @@
   },
   "tools": {
     "github-triage": false,
+    "github-pr-search": false,
   },
 }

+ 52 - 0
.opencode/tool/github-pr-search.ts

@@ -0,0 +1,52 @@
+/// <reference path="../env.d.ts" />
+import { tool } from "@opencode-ai/plugin"
+import DESCRIPTION from "./github-pr-search.txt"
+
+async function githubFetch(endpoint: string, options: RequestInit = {}) {
+  const response = await fetch(`https://api.github.com${endpoint}`, {
+    ...options,
+    headers: {
+      Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
+      Accept: "application/vnd.github+json",
+      "Content-Type": "application/json",
+      ...options.headers,
+    },
+  })
+  if (!response.ok) {
+    throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
+  }
+  return response.json()
+}
+
+interface PR {
+  title: string
+  html_url: string
+}
+
+export default tool({
+  description: DESCRIPTION,
+  args: {
+    query: tool.schema.string().describe("Search query for PR titles and descriptions"),
+    limit: tool.schema.number().describe("Maximum number of results to return").default(10),
+    offset: tool.schema.number().describe("Number of results to skip for pagination").default(0),
+  },
+  async execute(args) {
+    const owner = "anomalyco"
+    const repo = "opencode"
+
+    const page = Math.floor(args.offset / args.limit) + 1
+    const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`)
+    const result = await githubFetch(
+      `/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`,
+    )
+
+    if (result.total_count === 0) {
+      return `No PRs found matching "${args.query}"`
+    }
+
+    const prs = result.items as PR[]
+    const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n")
+
+    return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}`
+  },
+})

+ 10 - 0
.opencode/tool/github-pr-search.txt

@@ -0,0 +1,10 @@
+Use this tool to search GitHub pull requests by title and description.
+
+This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including:
+- PR number and title
+- Author
+- State (open/closed/merged)
+- Labels
+- Description snippet
+
+Use the query parameter to search for keywords that might appear in PR titles or descriptions.

+ 1 - 1
.opencode/tool/github-triage.ts

@@ -40,7 +40,7 @@ export default tool({
   async execute(args) {
     const issue = getIssueNumber()
     // const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
-    const owner = "sst"
+    const owner = "anomalyco"
     const repo = "opencode"
 
     const results: string[] = []

+ 27 - 4
CONTRIBUTING.md

@@ -14,10 +14,10 @@ However, any UI or core product feature must go through a design review with the
 
 If you are unsure if a PR would be accepted, feel free to ask a maintainer or look for issues with any of the following labels:
 
-- [`help wanted`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
-- [`good first issue`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
-- [`bug`](https://github.com/sst/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
-- [`perf`](https://github.com/sst/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
+- [`help wanted`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Ahelp-wanted)
+- [`good first issue`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22)
+- [`bug`](https://github.com/anomalyco/opencode/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug)
+- [`perf`](https://github.com/anomalyco/opencode/issues?q=is%3Aopen%20is%3Aissue%20label%3A%22perf%22)
 
 > [!NOTE]
 > PRs that ignore these guardrails will likely be closed.
@@ -67,8 +67,31 @@ Replace `<platform>` with your platform (e.g., `darwin-arm64`, `linux-x64`).
 - Core pieces:
   - `packages/opencode`: OpenCode core business logic & server.
   - `packages/opencode/src/cli/cmd/tui/`: The TUI code, written in SolidJS with [opentui](https://github.com/sst/opentui)
+  - `packages/app`: The shared web UI components, written in SolidJS
+  - `packages/desktop`: The native desktop app, built with Tauri (wraps `packages/app`)
   - `packages/plugin`: Source for `@opencode-ai/plugin`
 
+### Running the Web App
+
+To test UI changes during development, run the web app:
+
+```bash
+bun run --cwd packages/app dev
+```
+
+This starts a local dev server at http://localhost:5173 (or similar port shown in output). Most UI changes can be tested here.
+
+### Running the Desktop App
+
+The desktop app is a native Tauri application that wraps the web UI. To run it:
+
+```bash
+bun run --cwd packages/desktop dev
+```
+
+> [!NOTE]
+> Running the desktop app requires additional Tauri dependencies (Rust toolchain, platform-specific libraries). See the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for setup instructions.
+
 > [!NOTE]
 > If you make changes to the API or SDK (e.g. `packages/opencode/src/server/server.ts`), run `./script/generate.ts` to regenerate the SDK and related files.
 

+ 3 - 3
README.md

@@ -21,7 +21,7 @@
 <p align="center">
   <a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
   <a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
-  <a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
+  <a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
 </p>
 
 [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -41,7 +41,7 @@ choco install opencode             # Windows
 brew install opencode              # macOS and Linux
 paru -S opencode-bin               # Arch Linux
 mise use -g opencode               # Any OS
-nix run nixpkgs#opencode           # or github:sst/opencode for latest dev branch
+nix run nixpkgs#opencode           # or github:anomalyco/opencode for latest dev branch
 ```
 
 > [!TIP]
@@ -49,7 +49,7 @@ nix run nixpkgs#opencode           # or github:sst/opencode for latest dev branc
 
 ### Desktop App (BETA)
 
-OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
+OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
 
 | Platform              | Download                              |
 | --------------------- | ------------------------------------- |

+ 4 - 4
README.zh-TW.md

@@ -11,7 +11,7 @@
 <p align="center">
   <a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
   <a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
-  <a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
+  <a href="https://github.com/anomalyco/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/anomalyco/opencode/publish.yml?style=flat-square&branch=dev" /></a>
 </p>
 
 [![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai)
@@ -30,8 +30,8 @@ scoop bucket add extras; scoop install extras/opencode  # Windows
 choco install opencode             # Windows
 brew install opencode              # macOS 與 Linux
 paru -S opencode-bin               # Arch Linux
-mise use -g github:sst/opencode    # 任何作業系統
-nix run nixpkgs#opencode           # 或使用 github:sst/opencode 以取得最新開發分支
+mise use -g github:anomalyco/opencode    # 任何作業系統
+nix run nixpkgs#opencode           # 或使用 github:anomalyco/opencode 以取得最新開發分支
 ```
 
 > [!TIP]
@@ -39,7 +39,7 @@ nix run nixpkgs#opencode           # 或使用 github:sst/opencode 以取得最
 
 ### 桌面應用程式 (BETA)
 
-OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
+OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/anomalyco/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
 
 | 平台                  | 下載連結                              |
 | --------------------- | ------------------------------------- |

+ 28 - 25
bun.lock

@@ -23,7 +23,7 @@
     },
     "packages/app": {
       "name": "@opencode-ai/app",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -71,7 +71,7 @@
     },
     "packages/console/app": {
       "name": "@opencode-ai/console-app",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@cloudflare/vite-plugin": "1.15.2",
         "@ibm/plex": "6.4.1",
@@ -99,7 +99,7 @@
     },
     "packages/console/core": {
       "name": "@opencode-ai/console-core",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "@jsx-email/render": "1.1.1",
@@ -126,7 +126,7 @@
     },
     "packages/console/function": {
       "name": "@opencode-ai/console-function",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -150,7 +150,7 @@
     },
     "packages/console/mail": {
       "name": "@opencode-ai/console-mail",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@jsx-email/all": "2.2.3",
         "@jsx-email/cli": "1.4.3",
@@ -174,7 +174,7 @@
     },
     "packages/desktop": {
       "name": "@opencode-ai/desktop",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@opencode-ai/app": "workspace:*",
         "@solid-primitives/storage": "catalog:",
@@ -202,7 +202,7 @@
     },
     "packages/enterprise": {
       "name": "@opencode-ai/enterprise",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@opencode-ai/ui": "workspace:*",
         "@opencode-ai/util": "workspace:*",
@@ -231,7 +231,7 @@
     },
     "packages/function": {
       "name": "@opencode-ai/function",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "catalog:",
@@ -247,7 +247,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -287,11 +287,12 @@
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
         "@openrouter/ai-sdk-provider": "1.5.2",
-        "@opentui/core": "0.1.67",
-        "@opentui/solid": "0.1.67",
+        "@opentui/core": "0.1.68",
+        "@opentui/solid": "0.1.68",
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
+        "@solid-primitives/scheduled": "1.5.2",
         "@standard-schema/spec": "1.0.0",
         "@zip.js/zip.js": "2.7.62",
         "ai": "catalog:",
@@ -396,7 +397,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "zod": "catalog:",
@@ -416,7 +417,7 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "devDependencies": {
         "@hey-api/openapi-ts": "0.88.1",
         "@tsconfig/node22": "catalog:",
@@ -427,7 +428,7 @@
     },
     "packages/slack": {
       "name": "@opencode-ai/slack",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
         "@slack/bolt": "^3.17.1",
@@ -440,7 +441,7 @@
     },
     "packages/ui": {
       "name": "@opencode-ai/ui",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@kobalte/core": "catalog:",
         "@opencode-ai/sdk": "workspace:*",
@@ -478,7 +479,7 @@
     },
     "packages/util": {
       "name": "@opencode-ai/util",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "zod": "catalog:",
       },
@@ -489,7 +490,7 @@
     },
     "packages/web": {
       "name": "@opencode-ai/web",
-      "version": "1.0.224",
+      "version": "1.1.2",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -1338,21 +1339,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/[email protected]7", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.67", "@opentui/core-darwin-x64": "0.1.67", "@opentui/core-linux-arm64": "0.1.67", "@opentui/core-linux-x64": "0.1.67", "@opentui/core-win32-arm64": "0.1.67", "@opentui/core-win32-x64": "0.1.67", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-zmfyA10QUbzT6ohacPoHmGiYzuJrDSCfQWRWrKtao0BrHj9bii73qWy3V/eR4ibVueoRREwxJs5GlBOSvK6IoA=="],
+    "@opentui/core": ["@opentui/[email protected]8", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.68", "@opentui/core-darwin-x64": "0.1.68", "@opentui/core-linux-arm64": "0.1.68", "@opentui/core-linux-x64": "0.1.68", "@opentui/core-win32-arm64": "0.1.68", "@opentui/core-win32-x64": "0.1.68", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-SZz5qNO+2lJ8jDEoTSieyXH23t49myu6NetLex+xzqOf67XsU6QKlDcw5oMmc3zrKvETXhgbBvlSnbyJNQoBMg=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/[email protected]7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LtOcTlFD+kO7neItmkiF77H8cnjTYzBOZe8JQGwRSt9aaCke3UzMvLxmQnj4BP/kPC3hi9V6NRnFdptz0sJZIQ=="],
+    "@opentui/core-darwin-arm64": ["@opentui/[email protected]8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ipPX2gavBLVtw3d8L4ZPJDLlEwIjIRNdlNlxu07rqSEGSfxD5s29yc+33wLAlYXbmnJDajOqm0Dx6HnlY1Y9Fg=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/[email protected]7", "", { "os": "darwin", "cpu": "x64" }, "sha512-9i+awVWgpEVqZhFLaLq8usNGyCiyT5QxMLy6eH7JmRic79S34u23HfxiniGRtdYh3aqpm9SbLzo60v0nRIUkCA=="],
+    "@opentui/core-darwin-x64": ["@opentui/[email protected]8", "", { "os": "darwin", "cpu": "x64" }, "sha512-9dW0S9HINnuVjvC9QLj+S+329H7qEBQQtyJ9WHpykemokiJ5k4rnuDkfws5FxgTHIf/ddoYYTyPoGCS7WN5gsQ=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/[email protected]7", "", { "os": "linux", "cpu": "arm64" }, "sha512-WLjnTM3Ig//SRo0FUZYZJ5TITVbR6dKDVg6axU2D+sMoUzJMBP/Xo04q/TvZ3wP764Yca9l7oVMKWDxHlygyjQ=="],
+    "@opentui/core-linux-arm64": ["@opentui/[email protected]8", "", { "os": "linux", "cpu": "arm64" }, "sha512-/el6TbSQriBUfPhIa6SBfCCc7tjU98Bnhf2+w0zKwQFBjf3F3kmnI42++YxedMGFmL7bRt3EUawGOkQRZZzFAg=="],
 
-    "@opentui/core-linux-x64": ["@opentui/[email protected]7", "", { "os": "linux", "cpu": "x64" }, "sha512-5UbZ/TqWi/DAmHIZL4NvhdpgTwglszRiddkRiQ8cT0IbnE4lutd4XxWUWcLKwsNT1YJv32TtcGWkuthluLiriQ=="],
+    "@opentui/core-linux-x64": ["@opentui/[email protected]8", "", { "os": "linux", "cpu": "x64" }, "sha512-9NzVI3GZzmICoIu3YhWBdkEt0KvY27m++tu/MqW+xb6fnvN74jZkRWzlgjTdM70obL4eUGQdvU08sDHgZjsIJw=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/[email protected]7", "", { "os": "win32", "cpu": "arm64" }, "sha512-KNam5rObhN8/U9+GVVuvtAlGXp3MfdMHnw4W2P6YH7xp8HTsLvABUT91SJEyJ/ktVe9e1itLDG2fDHSoA5NbUg=="],
+    "@opentui/core-win32-arm64": ["@opentui/[email protected]8", "", { "os": "win32", "cpu": "arm64" }, "sha512-wrAeotyotOplUjQVBSxOGA8GCr9FWXSd6xCEo1PEGo/NjuAOtvHmKoENzyFEP0GzFsjvoUOyy2dZb987oFAn9A=="],
 
-    "@opentui/core-win32-x64": ["@opentui/[email protected]7", "", { "os": "win32", "cpu": "x64" }, "sha512-740lkOw42zLNh9YfahXjCwV2DS/amH2uMDh3tCADDCLckrMhemIhqArXDiMlalDxDqYspoaZCpBsFVsG9dMS6A=="],
+    "@opentui/core-win32-x64": ["@opentui/[email protected]8", "", { "os": "win32", "cpu": "x64" }, "sha512-w0yBjvzs/oMIwVdWICL4XlUrfsPoVXd4+RDqiuu+Xi/zD0UgANSTRY2asXca+gPe5zPHLsxvz1bAG0Z7uGtmyw=="],
 
-    "@opentui/solid": ["@opentui/[email protected]7", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.67", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-dVNq0+PJIdNb63D0T7vcbyVF/ZvLCihGvivTU50zDOzd0Sk5prbrIfpG8+DjMErFubXfdZQvdy/PqFdtw0rjtQ=="],
+    "@opentui/solid": ["@opentui/[email protected]8", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.68", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-S1oHvCQaY+gCQu2kiiksPIScP8i0FiDOlAlLjtfwcRlgeSjzT0wRwFkvoh4uVUPuAlyigox7vMCE3j04SYSGKg=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 
@@ -1762,6 +1763,8 @@
 
     "@solid-primitives/rootless": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ=="],
 
+    "@solid-primitives/scheduled": ["@solid-primitives/[email protected]", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="],
+
     "@solid-primitives/scroll": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-Ejq/Z7zKo/6eIEFr1bFLzXFxiGBCMLuqCM8QB8urr3YdPzjSETFLzYRWUyRiDWaBQN0F7k0SY6S7ig5nWOP7vg=="],
 
     "@solid-primitives/static-store": ["@solid-primitives/[email protected]", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw=="],

+ 3 - 3
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1767242400,
-        "narHash": "sha256-knFaYjeg7swqG1dljj1hOxfg39zrIy8pfGuicjm9s+o=",
+        "lastModified": 1767364772,
+        "narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "c04833a1e584401bb63c1a63ddc51a71e6aa457a",
+        "rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
         "type": "github"
       },
       "original": {

+ 16 - 4
flake.nix

@@ -66,10 +66,10 @@
           mkNodeModules = pkgs.callPackage ./nix/node-modules.nix {
             hash = nodeModulesHash;
           };
-          mkPackage = pkgs.callPackage ./nix/opencode.nix { };
-        in
-        {
-          default = mkPackage {
+          mkOpencode = pkgs.callPackage ./nix/opencode.nix { };
+          mkDesktop = pkgs.callPackage ./nix/desktop.nix { };
+
+          opencodePkg = mkOpencode {
             inherit (packageJson) version;
             src = ./.;
             scripts = ./nix/scripts;
@@ -77,6 +77,18 @@
             modelsDev = "${modelsDev.${system}}/dist/_api.json";
             inherit mkNodeModules;
           };
+
+          desktopPkg = mkDesktop {
+            inherit (packageJson) version;
+            src = ./.;
+            scripts = ./nix/scripts;
+            mkNodeModules = mkNodeModules;
+            opencode = opencodePkg;
+          };
+        in
+        {
+          default = opencodePkg;
+          desktop = desktopPkg;
         }
       );
 

+ 2 - 2
github/README.md

@@ -87,7 +87,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
              fetch-depth: 1
 
          - name: Run opencode
-           uses: sst/opencode/github@latest
+           uses: anomalyco/opencode/github@latest
            env:
              ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
            with:
@@ -98,7 +98,7 @@ This will walk you through installing the GitHub app, creating the workflow, and
 
 ## Support
 
-This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/sst/opencode/issues.
+This is an early release. If you encounter issues or have feedback, please create an issue at https://github.com/anomalyco/opencode/issues.
 
 ## Development
 

+ 1 - 1
github/action.yml

@@ -41,7 +41,7 @@ runs:
       id: version
       shell: bash
       run: |
-        VERSION=$(curl -sf https://api.github.com/repos/sst/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
+        VERSION=$(curl -sf https://api.github.com/repos/anomalyco/opencode/releases/latest | grep -o '"tag_name": *"[^"]*"' | cut -d'"' -f4)
         echo "version=${VERSION:-latest}" >> $GITHUB_OUTPUT
 
     - name: Cache opencode

+ 1 - 1
github/index.ts

@@ -281,7 +281,7 @@ async function assertOpencodeConnected() {
       connected = true
       break
     } catch (e) {}
-    await new Promise((resolve) => setTimeout(resolve, 300))
+    await Bun.sleep(300)
   } while (retry++ < 30)
 
   if (!connected) {

+ 1 - 0
github/package.json

@@ -3,6 +3,7 @@
   "module": "index.ts",
   "type": "module",
   "private": true,
+  "license": "MIT",
   "devDependencies": {
     "@types/bun": "catalog:"
   },

+ 1 - 0
infra/console.ts

@@ -104,6 +104,7 @@ const ZEN_MODELS = [
   new sst.Secret("ZEN_MODELS4"),
   new sst.Secret("ZEN_MODELS5"),
   new sst.Secret("ZEN_MODELS6"),
+  new sst.Secret("ZEN_MODELS7"),
 ]
 const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
 const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {

+ 5 - 5
install

@@ -147,8 +147,8 @@ INSTALL_DIR=$HOME/.opencode/bin
 mkdir -p "$INSTALL_DIR"
 
 if [ -z "$requested_version" ]; then
-    url="https://github.com/sst/opencode/releases/latest/download/$filename"
-    specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
+    url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
+    specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
 
     if [[ $? -ne 0 || -z "$specific_version" ]]; then
         echo -e "${RED}Failed to fetch version information${NC}"
@@ -157,14 +157,14 @@ if [ -z "$requested_version" ]; then
 else
     # Strip leading 'v' if present
     requested_version="${requested_version#v}"
-    url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename"
+    url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
     specific_version=$requested_version
     
     # Verify the release exists before downloading
-    http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/sst/opencode/releases/tag/v${requested_version}")
+    http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
     if [ "$http_status" = "404" ]; then
         echo -e "${RED}Error: Release v${requested_version} not found${NC}"
-        echo -e "${MUTED}Available releases: https://github.com/sst/opencode/releases${NC}"
+        echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
         exit 1
     fi
 fi

+ 145 - 0
nix/desktop.nix

@@ -0,0 +1,145 @@
+{
+  lib,
+  stdenv,
+  rustPlatform,
+  bun,
+  pkg-config,
+  dbus ? null,
+  openssl,
+  glib ? null,
+  gtk3 ? null,
+  libsoup_3 ? null,
+  webkitgtk_4_1 ? null,
+  librsvg ? null,
+  libappindicator-gtk3 ? null,
+  cargo,
+  rustc,
+  makeBinaryWrapper,
+  nodejs,
+  jq,
+}:
+args:
+let
+  scripts = args.scripts;
+  mkModules =
+    attrs:
+    args.mkNodeModules (
+      attrs
+      // {
+        canonicalizeScript = scripts + "/canonicalize-node-modules.ts";
+        normalizeBinsScript = scripts + "/normalize-bun-binaries.ts";
+      }
+    );
+in
+rustPlatform.buildRustPackage rec {
+  pname = "opencode-desktop";
+  version = args.version;
+
+  src = args.src;
+
+  # We need to set the root for cargo, but we also need access to the whole repo.
+  postUnpack = ''
+    # Update sourceRoot to point to the tauri app
+    sourceRoot+=/packages/desktop/src-tauri
+  '';
+
+  cargoLock = {
+    lockFile = ../packages/desktop/src-tauri/Cargo.lock;
+    allowBuiltinFetchGit = true;
+  };
+
+  node_modules = mkModules {
+    version = version;
+    src = src;
+  };
+
+  nativeBuildInputs = [
+    pkg-config
+    bun
+    makeBinaryWrapper
+    cargo
+    rustc
+    nodejs
+    jq
+  ];
+
+  buildInputs = [
+    openssl
+  ]
+  ++ lib.optionals stdenv.isLinux [
+    dbus
+    glib
+    gtk3
+    libsoup_3
+    webkitgtk_4_1
+    librsvg
+    libappindicator-gtk3
+  ];
+
+  preBuild = ''
+    # Restore node_modules
+    pushd ../../..
+
+    # Copy node_modules from the fixed-output derivation
+    # We use cp -r --no-preserve=mode to ensure we can write to them if needed,
+    # though we usually just read.
+    cp -r ${node_modules}/node_modules .
+    cp -r ${node_modules}/packages .
+
+    # Ensure node_modules is writable so patchShebangs can update script headers
+    chmod -R u+w node_modules
+    # Ensure workspace packages are writable for tsgo incremental outputs (.tsbuildinfo)
+    chmod -R u+w packages
+    # Patch shebangs so scripts can run
+    patchShebangs node_modules
+
+    # Copy sidecar
+    mkdir -p packages/desktop/src-tauri/sidecars
+    targetTriple=${stdenv.hostPlatform.rust.rustcTarget}
+    cp ${args.opencode}/bin/opencode packages/desktop/src-tauri/sidecars/opencode-cli-$targetTriple
+
+    # Merge prod config into tauri.conf.json
+    if ! jq -s '.[0] * .[1]' \
+      packages/desktop/src-tauri/tauri.conf.json \
+      packages/desktop/src-tauri/tauri.prod.conf.json \
+      > packages/desktop/src-tauri/tauri.conf.json.tmp; then
+      echo "Error: failed to merge tauri.conf.json with tauri.prod.conf.json" >&2
+      exit 1
+    fi
+    mv packages/desktop/src-tauri/tauri.conf.json.tmp packages/desktop/src-tauri/tauri.conf.json
+
+    # Build the frontend
+    cd packages/desktop
+
+    # The 'build' script runs 'bun run typecheck && vite build'.
+    bun run build
+
+    popd
+  '';
+
+  # Tauri bundles the assets during the rust build phase (which happens after preBuild).
+  # It looks for them in the location specified in tauri.conf.json.
+
+  postInstall = lib.optionalString stdenv.isLinux ''
+    # Wrap the binary to ensure it finds the libraries
+    wrapProgram $out/bin/opencode-desktop \
+      --prefix LD_LIBRARY_PATH : ${
+        lib.makeLibraryPath [
+          gtk3
+          webkitgtk_4_1
+          librsvg
+          glib
+          libsoup_3
+        ]
+      }
+  '';
+
+  meta = with lib; {
+    description = "OpenCode Desktop App";
+    homepage = "https://opencode.ai";
+    license = licenses.mit;
+    maintainers = with maintainers; [ ];
+    mainProgram = "opencode-desktop";
+    platforms = platforms.linux ++ platforms.darwin;
+  };
+}

+ 1 - 1
nix/hashes.json

@@ -1,3 +1,3 @@
 {
-  "nodeModules": "sha256-uJDhOieOdMQLORyuOWtgtjLoMnNEQPrDcyij9TX0aTw="
+  "nodeModules": "sha256-OJ3C4RMzfbbG1Fwa/5yru0rlISj+28UPITMNBEU5AeM="
 }

+ 1 - 1
nix/opencode.nix

@@ -125,7 +125,7 @@ stdenvNoCC.mkDerivation (finalAttrs: {
       It combines a TypeScript/JavaScript core with a Go-based TUI
       to provide an interactive AI coding experience.
     '';
-    homepage = "https://github.com/sst/opencode";
+    homepage = "https://github.com/anomalyco/opencode";
     license = lib.licenses.mit;
     platforms = [
       "aarch64-linux"

+ 1 - 1
package.json

@@ -78,7 +78,7 @@
   },
   "repository": {
     "type": "git",
-    "url": "https://github.com/sst/opencode"
+    "url": "https://github.com/anomalyco/opencode"
   },
   "license": "MIT",
   "prettier": {

+ 1 - 1
packages/app/AGENTS.md

@@ -1,6 +1,6 @@
 ## Debugging
 
-- To test the opencode app, use the playwrite mcp server, the app is already
+- To test the opencode app, use the playwright MCP server, the app is already
   running at http://localhost:3000
 - NEVER try to restart the app, or the server process, EVER.
 

+ 1 - 1
packages/app/index.html

@@ -47,7 +47,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-screen"></div>
+    <div id="root" class="flex flex-col h-dvh"></div>
     <script src="/src/entry.tsx" type="module"></script>
   </body>
 </html>

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/app",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "description": "",
   "type": "module",
   "exports": {

+ 302 - 376
packages/app/src/components/prompt-input.tsx

@@ -12,6 +12,7 @@ import {
   usePrompt,
   ImageAttachmentPart,
   AgentPart,
+  FileAttachmentPart,
 } from "@/context/prompt"
 import { useLayout } from "@/context/layout"
 import { useSDK } from "@/context/sdk"
@@ -33,6 +34,12 @@ import { persisted } from "@/utils/persist"
 import { Identifier } from "@/utils/id"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { usePermission } from "@/context/permission"
+import { useGlobalSync } from "@/context/global-sync"
+import { usePlatform } from "@/context/platform"
+import { createOpencodeClient, type Message, type Part } from "@opencode-ai/sdk/v2/client"
+import { Binary } from "@opencode-ai/util/binary"
+import { showToast } from "@opencode-ai/ui/toast"
+import { base64Encode } from "@opencode-ai/util/encode"
 
 const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
 const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -40,6 +47,8 @@ const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
 interface PromptInputProps {
   class?: string
   ref?: (el: HTMLDivElement) => void
+  newSessionWorktree?: string
+  onNewSessionWorktreeReset?: () => void
 }
 
 const PLACEHOLDERS = [
@@ -83,6 +92,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const navigate = useNavigate()
   const sdk = useSDK()
   const sync = useSync()
+  const globalSync = useGlobalSync()
+  const platform = usePlatform()
   const local = useLocal()
   const files = useFile()
   const prompt = usePrompt()
@@ -95,6 +106,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   let editorRef!: HTMLDivElement
   let fileInputRef!: HTMLInputElement
   let scrollRef!: HTMLDivElement
+  let slashPopoverRef!: HTMLDivElement
 
   const scrollCursorIntoView = () => {
     const container = scrollRef
@@ -151,7 +163,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     imageAttachments: ImageAttachmentPart[]
     mode: "normal" | "shell"
     applyingHistory: boolean
-    killBuffer: string
   }>({
     popover: null,
     historyIndex: -1,
@@ -161,7 +172,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     imageAttachments: [],
     mode: "normal",
     applyingHistory: false,
-    killBuffer: "",
   })
 
   const MAX_HISTORY = 100
@@ -292,6 +302,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   }
 
   const handleGlobalDragOver = (event: DragEvent) => {
+    if (dialog.active) return
+
     event.preventDefault()
     const hasFiles = event.dataTransfer?.types.includes("Files")
     if (hasFiles) {
@@ -300,6 +312,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   }
 
   const handleGlobalDragLeave = (event: DragEvent) => {
+    if (dialog.active) return
+
     // relatedTarget is null when leaving the document window
     if (!event.relatedTarget) {
       setStore("dragging", false)
@@ -307,6 +321,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   }
 
   const handleGlobalDrop = async (event: DragEvent) => {
+    if (dialog.active) return
+
     event.preventDefault()
     setStore("dragging", false)
 
@@ -430,6 +446,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     active: slashActive,
     onInput: slashOnInput,
     onKeyDown: slashOnKeyDown,
+    refetch: slashRefetch,
   } = useFilteredList<SlashCommand>({
     items: slashCommands,
     key: (x) => x?.id,
@@ -437,32 +454,78 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     onSelect: handleSlashSelect,
   })
 
+  const createPill = (part: FileAttachmentPart | AgentPart) => {
+    const pill = document.createElement("span")
+    pill.textContent = part.content
+    pill.setAttribute("data-type", part.type)
+    if (part.type === "file") pill.setAttribute("data-path", part.path)
+    if (part.type === "agent") pill.setAttribute("data-name", part.name)
+    pill.setAttribute("contenteditable", "false")
+    pill.style.userSelect = "text"
+    pill.style.cursor = "default"
+    return pill
+  }
+
+  const isNormalizedEditor = () =>
+    Array.from(editorRef.childNodes).every((node) => {
+      if (node.nodeType === Node.TEXT_NODE) {
+        const text = node.textContent ?? ""
+        if (!text.includes("\u200B")) return true
+        if (text !== "\u200B") return false
+
+        const prev = node.previousSibling
+        const next = node.nextSibling
+        const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
+        const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
+        if (!prevIsBr && !nextIsBr) return false
+        if (nextIsBr && !prevIsBr && prev) return false
+        return true
+      }
+      if (node.nodeType !== Node.ELEMENT_NODE) return false
+      const el = node as HTMLElement
+      if (el.dataset.type === "file") return true
+      if (el.dataset.type === "agent") return true
+      return el.tagName === "BR"
+    })
+
+  const renderEditor = (parts: Prompt) => {
+    editorRef.innerHTML = ""
+    for (const part of parts) {
+      if (part.type === "text") {
+        editorRef.appendChild(createTextFragment(part.content))
+        continue
+      }
+      if (part.type === "file" || part.type === "agent") {
+        editorRef.appendChild(createPill(part))
+      }
+    }
+  }
+
+  createEffect(
+    on(
+      () => sync.data.command,
+      () => slashRefetch(),
+      { defer: true },
+    ),
+  )
+
+  // Auto-scroll active command into view when navigating with keyboard
+  createEffect(() => {
+    const activeId = slashActive()
+    if (!activeId || !slashPopoverRef) return
+
+    requestAnimationFrame(() => {
+      const element = slashPopoverRef.querySelector(`[data-slash-id="${activeId}"]`)
+      element?.scrollIntoView({ block: "nearest", behavior: "smooth" })
+    })
+  })
+
   createEffect(
     on(
       () => prompt.current(),
       (currentParts) => {
         const domParts = parseFromDOM()
-        const normalized = Array.from(editorRef.childNodes).every((node) => {
-          if (node.nodeType === Node.TEXT_NODE) {
-            const text = node.textContent ?? ""
-            if (!text.includes("\u200B")) return true
-            if (text !== "\u200B") return false
-
-            const prev = node.previousSibling
-            const next = node.nextSibling
-            const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
-            const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
-            if (!prevIsBr && !nextIsBr) return false
-            if (nextIsBr && !prevIsBr && prev) return false
-            return true
-          }
-          if (node.nodeType !== Node.ELEMENT_NODE) return false
-          const el = node as HTMLElement
-          if (el.dataset.type === "file") return true
-          if (el.dataset.type === "agent") return true
-          return el.tagName === "BR"
-        })
-        if (normalized && isPromptEqual(currentParts, domParts)) return
+        if (isNormalizedEditor() && isPromptEqual(currentParts, domParts)) return
 
         const selection = window.getSelection()
         let cursorPosition: number | null = null
@@ -470,30 +533,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           cursorPosition = getCursorPosition(editorRef)
         }
 
-        editorRef.innerHTML = ""
-        currentParts.forEach((part) => {
-          if (part.type === "text") {
-            editorRef.appendChild(createTextFragment(part.content))
-          } else if (part.type === "file") {
-            const pill = document.createElement("span")
-            pill.textContent = part.content
-            pill.setAttribute("data-type", "file")
-            pill.setAttribute("data-path", part.path)
-            pill.setAttribute("contenteditable", "false")
-            pill.style.userSelect = "text"
-            pill.style.cursor = "default"
-            editorRef.appendChild(pill)
-          } else if (part.type === "agent") {
-            const pill = document.createElement("span")
-            pill.textContent = part.content
-            pill.setAttribute("data-type", "agent")
-            pill.setAttribute("data-name", part.name)
-            pill.setAttribute("contenteditable", "false")
-            pill.style.userSelect = "text"
-            pill.style.cursor = "default"
-            editorRef.appendChild(pill)
-          }
-        })
+        renderEditor(currentParts)
 
         if (cursorPosition !== null) {
           setCursorPosition(editorRef, cursorPosition)
@@ -671,40 +711,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     const textBeforeCursor = rawText.substring(0, cursorPosition)
     const atMatch = textBeforeCursor.match(/@(\S*)$/)
 
-    if (part.type === "file") {
-      const pill = document.createElement("span")
-      pill.textContent = part.content
-      pill.setAttribute("data-type", "file")
-      pill.setAttribute("data-path", part.path)
-      pill.setAttribute("contenteditable", "false")
-      pill.style.userSelect = "text"
-      pill.style.cursor = "default"
-
-      const gap = document.createTextNode(" ")
-      const range = selection.getRangeAt(0)
-
-      if (atMatch) {
-        const start = atMatch.index ?? cursorPosition - atMatch[0].length
-        setRangeEdge(range, "start", start)
-        setRangeEdge(range, "end", cursorPosition)
-      }
-
-      range.deleteContents()
-      range.insertNode(gap)
-      range.insertNode(pill)
-      range.setStartAfter(gap)
-      range.collapse(true)
-      selection.removeAllRanges()
-      selection.addRange(range)
-    } else if (part.type === "agent") {
-      const pill = document.createElement("span")
-      pill.textContent = part.content
-      pill.setAttribute("data-type", "agent")
-      pill.setAttribute("data-name", part.name)
-      pill.setAttribute("contenteditable", "false")
-      pill.style.userSelect = "text"
-      pill.style.cursor = "default"
-
+    if (part.type === "file" || part.type === "agent") {
+      const pill = createPill(part)
       const gap = document.createTextNode(" ")
       const range = selection.getRangeAt(0)
 
@@ -750,77 +758,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     setStore("popover", null)
   }
 
-  const setSelectionOffsets = (start: number, end: number) => {
-    const selection = window.getSelection()
-    if (!selection) return false
-
-    const length = promptLength(prompt.current())
-    const a = Math.max(0, Math.min(start, length))
-    const b = Math.max(0, Math.min(end, length))
-    const rangeStart = Math.min(a, b)
-    const rangeEnd = Math.max(a, b)
-
-    const range = document.createRange()
-    range.selectNodeContents(editorRef)
-
-    const setEdge = (edge: "start" | "end", offset: number) => {
-      let remaining = offset
-      const nodes = Array.from(editorRef.childNodes)
-
-      for (const node of nodes) {
-        const length = getNodeLength(node)
-        const isText = node.nodeType === Node.TEXT_NODE
-        const isFile = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type === "file"
-        const isBreak = node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === "BR"
-
-        if (isText && remaining <= length) {
-          if (edge === "start") range.setStart(node, remaining)
-          if (edge === "end") range.setEnd(node, remaining)
-          return
-        }
-
-        if ((isFile || isBreak) && remaining <= length) {
-          if (edge === "start" && remaining === 0) range.setStartBefore(node)
-          if (edge === "start" && remaining > 0) range.setStartAfter(node)
-          if (edge === "end" && remaining === 0) range.setEndBefore(node)
-          if (edge === "end" && remaining > 0) range.setEndAfter(node)
-          return
-        }
-
-        remaining -= length
-      }
-
-      const last = editorRef.lastChild
-      if (!last) {
-        if (edge === "start") range.setStart(editorRef, 0)
-        if (edge === "end") range.setEnd(editorRef, 0)
-        return
-      }
-      if (edge === "start") range.setStartAfter(last)
-      if (edge === "end") range.setEndAfter(last)
-    }
-
-    setEdge("start", rangeStart)
-    setEdge("end", rangeEnd)
-    selection.removeAllRanges()
-    selection.addRange(range)
-    return true
-  }
-
-  const replaceOffsets = (start: number, end: number, content: string) => {
-    if (!setSelectionOffsets(start, end)) return false
-    addPart({ type: "text", content, start: 0, end: 0 })
-    return true
-  }
-
-  const killText = (start: number, end: number) => {
-    if (start === end) return
-    const current = prompt.current()
-    if (!current.every((part) => part.type === "text")) return
-    const text = current.map((part) => part.content).join("")
-    setStore("killBuffer", text.slice(start, end))
-  }
-
   const abort = () =>
     sdk.client.session
       .abort({
@@ -942,7 +879,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     }
 
     const ctrl = event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey
-    const alt = event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey
 
     if (ctrl && event.code === "KeyG") {
       if (store.popover) {
@@ -957,148 +893,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       return
     }
 
-    if (ctrl || alt) {
-      const { collapsed, cursorPosition, textLength } = getCaretState()
-      if (collapsed) {
-        const current = prompt.current()
-        const text = current.map((part) => ("content" in part ? part.content : "")).join("")
-
-        if (ctrl) {
-          if (event.code === "KeyA") {
-            const pos = text.lastIndexOf("\n", cursorPosition - 1) + 1
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyE") {
-            const next = text.indexOf("\n", cursorPosition)
-            const pos = next === -1 ? textLength : next
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyB") {
-            const pos = Math.max(0, cursorPosition - 1)
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyF") {
-            const pos = Math.min(textLength, cursorPosition + 1)
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyD") {
-            if (store.mode === "shell" && cursorPosition === 0 && textLength === 0) {
-              setStore("mode", "normal")
-              event.preventDefault()
-              return
-            }
-            if (cursorPosition >= textLength) return
-            replaceOffsets(cursorPosition, cursorPosition + 1, "")
-            event.preventDefault()
-            return
-          }
-
-          if (event.code === "KeyK") {
-            const next = text.indexOf("\n", cursorPosition)
-            const lineEnd = next === -1 ? textLength : next
-            const end = lineEnd === cursorPosition && lineEnd < textLength ? lineEnd + 1 : lineEnd
-            if (end === cursorPosition) return
-            killText(cursorPosition, end)
-            replaceOffsets(cursorPosition, end, "")
-            event.preventDefault()
-            return
-          }
-
-          if (event.code === "KeyU") {
-            const start = text.lastIndexOf("\n", cursorPosition - 1) + 1
-            if (start === cursorPosition) return
-            killText(start, cursorPosition)
-            replaceOffsets(start, cursorPosition, "")
-            event.preventDefault()
-            return
-          }
-
-          if (event.code === "KeyW") {
-            let start = cursorPosition
-            while (start > 0 && /\s/.test(text[start - 1])) start -= 1
-            while (start > 0 && !/\s/.test(text[start - 1])) start -= 1
-            if (start === cursorPosition) return
-            killText(start, cursorPosition)
-            replaceOffsets(start, cursorPosition, "")
-            event.preventDefault()
-            return
-          }
-
-          if (event.code === "KeyY") {
-            if (!store.killBuffer) return
-            addPart({ type: "text", content: store.killBuffer, start: 0, end: 0 })
-            event.preventDefault()
-            return
-          }
-
-          if (event.code === "KeyT") {
-            if (!current.every((part) => part.type === "text")) return
-            if (textLength < 2) return
-            if (cursorPosition === 0) return
-
-            const atEnd = cursorPosition === textLength
-            const first = atEnd ? cursorPosition - 2 : cursorPosition - 1
-            const second = atEnd ? cursorPosition - 1 : cursorPosition
-
-            if (text[first] === "\n" || text[second] === "\n") return
-
-            replaceOffsets(first, second + 1, `${text[second]}${text[first]}`)
-            event.preventDefault()
-            return
-          }
-        }
-
-        if (alt) {
-          if (event.code === "KeyB") {
-            let pos = cursorPosition
-            while (pos > 0 && /\s/.test(text[pos - 1])) pos -= 1
-            while (pos > 0 && !/\s/.test(text[pos - 1])) pos -= 1
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyF") {
-            let pos = cursorPosition
-            while (pos < textLength && /\s/.test(text[pos])) pos += 1
-            while (pos < textLength && !/\s/.test(text[pos])) pos += 1
-            setCursorPosition(editorRef, pos)
-            event.preventDefault()
-            queueScroll()
-            return
-          }
-
-          if (event.code === "KeyD") {
-            let end = cursorPosition
-            while (end < textLength && /\s/.test(text[end])) end += 1
-            while (end < textLength && !/\s/.test(text[end])) end += 1
-            if (end === cursorPosition) return
-            killText(cursorPosition, end)
-            replaceOffsets(cursorPosition, end, "")
-            event.preventDefault()
-            return
-          }
-        }
-      }
-    }
-
     if (event.key === "ArrowUp" || event.key === "ArrowDown") {
       if (event.altKey || event.ctrlKey || event.metaKey) return
       const { collapsed } = getCaretState()
@@ -1152,30 +946,169 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
 
   const handleSubmit = async (event: Event) => {
     event.preventDefault()
+
     const currentPrompt = prompt.current()
     const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
-    const hasImageAttachments = store.imageAttachments.length > 0
-    if (text.trim().length === 0 && !hasImageAttachments) {
+    const images = store.imageAttachments.slice()
+    const mode = store.mode
+
+    if (text.trim().length === 0 && images.length === 0) {
       if (working()) abort()
       return
     }
 
-    addToHistory(currentPrompt, store.mode)
+    const currentModel = local.model.current()
+    const currentAgent = local.agent.current()
+    if (!currentModel || !currentAgent) {
+      showToast({
+        title: "Select an agent and model",
+        description: "Choose an agent and model before sending a prompt.",
+      })
+      return
+    }
+
+    const errorMessage = (err: unknown) => {
+      if (err && typeof err === "object" && "data" in err) {
+        const data = (err as { data?: { message?: string } }).data
+        if (data?.message) return data.message
+      }
+      if (err instanceof Error) return err.message
+      return "Request failed"
+    }
+
+    addToHistory(currentPrompt, mode)
     setStore("historyIndex", -1)
     setStore("savedPrompt", null)
 
-    let existing = info()
-    if (!existing) {
-      const created = await sdk.client.session.create()
-      existing = created.data ?? undefined
-      if (existing) navigate(existing.id)
+    const projectDirectory = sdk.directory
+    const isNewSession = !params.id
+    const worktreeSelection = props.newSessionWorktree ?? "main"
+
+    let sessionDirectory = projectDirectory
+    let client = sdk.client
+
+    if (isNewSession) {
+      if (worktreeSelection === "create") {
+        const createdWorktree = await client.worktree
+          .create({ directory: projectDirectory })
+          .then((x) => x.data)
+          .catch((err) => {
+            showToast({
+              title: "Failed to create worktree",
+              description: errorMessage(err),
+            })
+            return undefined
+          })
+
+        if (!createdWorktree?.directory) {
+          showToast({
+            title: "Failed to create worktree",
+            description: "Request failed",
+          })
+          return
+        }
+        sessionDirectory = createdWorktree.directory
+      }
+
+      if (worktreeSelection !== "main" && worktreeSelection !== "create") {
+        sessionDirectory = worktreeSelection
+      }
+
+      if (sessionDirectory !== projectDirectory) {
+        client = createOpencodeClient({
+          baseUrl: sdk.url,
+          fetch: platform.fetch,
+          directory: sessionDirectory,
+          throwOnError: true,
+        })
+        globalSync.child(sessionDirectory)
+      }
+
+      props.onNewSessionWorktreeReset?.()
+    }
+
+    let session = info()
+    if (!session && isNewSession) {
+      session = await client.session.create().then((x) => x.data ?? undefined)
+      if (session) navigate(`/${base64Encode(sessionDirectory)}/session/${session.id}`)
+    }
+    if (!session) return
+
+    const model = {
+      modelID: currentModel.id,
+      providerID: currentModel.provider.id,
+    }
+    const agent = currentAgent.name
+    const variant = local.model.variant.current()
+
+    const clearInput = () => {
+      prompt.reset()
+      setStore("imageAttachments", [])
+      setStore("mode", "normal")
+      setStore("popover", null)
     }
-    if (!existing) return
 
-    const toAbsolutePath = (path: string) => (path.startsWith("/") ? path : sync.absolute(path))
-    const fileAttachments = currentPrompt.filter(
-      (part) => part.type === "file",
-    ) as import("@/context/prompt").FileAttachmentPart[]
+    const restoreInput = () => {
+      prompt.set(currentPrompt, promptLength(currentPrompt))
+      setStore("imageAttachments", images)
+      setStore("mode", mode)
+      setStore("popover", null)
+      requestAnimationFrame(() => {
+        editorRef.focus()
+        setCursorPosition(editorRef, promptLength(currentPrompt))
+        queueScroll()
+      })
+    }
+
+    if (mode === "shell") {
+      clearInput()
+      client.session
+        .shell({
+          sessionID: session.id,
+          agent,
+          model,
+          command: text,
+        })
+        .catch((err) => {
+          showToast({
+            title: "Failed to send shell command",
+            description: errorMessage(err),
+          })
+          restoreInput()
+        })
+      return
+    }
+
+    if (text.startsWith("/")) {
+      const [cmdName, ...args] = text.split(" ")
+      const commandName = cmdName.slice(1)
+      const customCommand = sync.data.command.find((c) => c.name === commandName)
+      if (customCommand) {
+        clearInput()
+        client.session
+          .command({
+            sessionID: session.id,
+            command: commandName,
+            arguments: args.join(" "),
+            agent,
+            model: `${model.providerID}/${model.modelID}`,
+            variant,
+          })
+          .catch((err) => {
+            showToast({
+              title: "Failed to send command",
+              description: errorMessage(err),
+            })
+            restoreInput()
+          })
+        return
+      }
+    }
+
+    const toAbsolutePath = (path: string) =>
+      path.startsWith("/") ? path : (sessionDirectory + "/" + path).replace("//", "/")
+
+    const fileAttachments = currentPrompt.filter((part) => part.type === "file") as FileAttachmentPart[]
     const agentAttachments = currentPrompt.filter((part) => part.type === "agent") as AgentPart[]
 
     const fileAttachmentParts = fileAttachments.map((attachment) => {
@@ -1247,7 +1180,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       addContextFile(item.path, item.selection)
     }
 
-    const imageAttachmentParts = store.imageAttachments.map((attachment) => ({
+    const imageAttachmentParts = images.map((attachment) => ({
       id: Identifier.ascending("part"),
       type: "file" as const,
       mime: attachment.mime,
@@ -1255,60 +1188,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       filename: attachment.filename,
     }))
 
-    const isShellMode = store.mode === "shell"
-    editorRef.innerHTML = ""
-    prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
-    setStore("imageAttachments", [])
-    setStore("mode", "normal")
-
-    const currentModel = local.model.current()
-    const currentAgent = local.agent.current()
-    if (!currentModel || !currentAgent) {
-      console.warn("No agent or model available for prompt submission")
-      return
-    }
-    const model = {
-      modelID: currentModel.id,
-      providerID: currentModel.provider.id,
-    }
-    const agent = currentAgent.name
-    const variant = local.model.variant.current()
-
-    if (isShellMode) {
-      sdk.client.session
-        .shell({
-          sessionID: existing.id,
-          agent,
-          model,
-          command: text,
-        })
-        .catch((e) => {
-          console.error("Failed to send shell command", e)
-        })
-      return
-    }
-
-    if (text.startsWith("/")) {
-      const [cmdName, ...args] = text.split(" ")
-      const commandName = cmdName.slice(1)
-      const customCommand = sync.data.command.find((c) => c.name === commandName)
-      if (customCommand) {
-        sdk.client.session
-          .command({
-            sessionID: existing.id,
-            command: commandName,
-            arguments: args.join(" "),
-            agent,
-            model: `${model.providerID}/${model.modelID}`,
-            variant,
-          })
-          .catch((e) => {
-            console.error("Failed to send command", e)
-          })
-        return
-      }
-    }
-
     const messageID = Identifier.ascending("message")
     const textPart = {
       id: Identifier.ascending("part"),
@@ -1322,31 +1201,74 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       ...agentAttachmentParts,
       ...imageAttachmentParts,
     ]
+
     const optimisticParts = requestParts.map((part) => ({
       ...part,
-      sessionID: existing.id,
+      sessionID: session.id,
       messageID,
-    }))
+    })) as unknown as Part[]
 
-    sync.session.addOptimisticMessage({
-      sessionID: existing.id,
-      messageID,
-      parts: optimisticParts,
+    const optimisticMessage: Message = {
+      id: messageID,
+      sessionID: session.id,
+      role: "user",
+      time: { created: Date.now() },
       agent,
       model,
-    })
+    }
 
-    sdk.client.session
+    const setSyncStore = sessionDirectory === projectDirectory ? sync.set : globalSync.child(sessionDirectory)[1]
+
+    const addOptimisticMessage = () => {
+      setSyncStore(
+        produce((draft) => {
+          const messages = draft.message[session.id]
+          if (!messages) {
+            draft.message[session.id] = [optimisticMessage]
+          } else {
+            const result = Binary.search(messages, messageID, (m) => m.id)
+            messages.splice(result.index, 0, optimisticMessage)
+          }
+          draft.part[messageID] = optimisticParts
+            .filter((p) => !!p?.id)
+            .slice()
+            .sort((a, b) => a.id.localeCompare(b.id))
+        }),
+      )
+    }
+
+    const removeOptimisticMessage = () => {
+      setSyncStore(
+        produce((draft) => {
+          const messages = draft.message[session.id]
+          if (messages) {
+            const result = Binary.search(messages, messageID, (m) => m.id)
+            if (result.found) messages.splice(result.index, 1)
+          }
+          delete draft.part[messageID]
+        }),
+      )
+    }
+
+    clearInput()
+    addOptimisticMessage()
+
+    client.session
       .prompt({
-        sessionID: existing.id,
+        sessionID: session.id,
         agent,
         model,
         messageID,
         parts: requestParts,
         variant,
       })
-      .catch((e) => {
-        console.error("Failed to send prompt", e)
+      .catch((err) => {
+        showToast({
+          title: "Failed to send prompt",
+          description: errorMessage(err),
+        })
+        removeOptimisticMessage()
+        restoreInput()
       })
   }
 
@@ -1354,6 +1276,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     <div class="relative size-full _max-h-[320px] flex flex-col gap-3">
       <Show when={store.popover}>
         <div
+          ref={(el) => {
+            if (store.popover === "slash") slashPopoverRef = el
+          }}
           class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
                  overflow-auto no-scrollbar flex flex-col p-2 rounded-md
                  border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
@@ -1412,6 +1337,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                 <For each={slashFlat()}>
                   {(cmd) => (
                     <button
+                      data-slash-id={cmd.id}
                       classList={{
                         "w-full flex items-center justify-between gap-4 rounded-md px-2 py-1": true,
                         "bg-surface-raised-base-hover": slashActive() === cmd.id,
@@ -1665,7 +1591,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <input
               ref={fileInputRef}
               type="file"
-              accept={ACCEPTED_IMAGE_TYPES.join(",")}
+              accept={ACCEPTED_FILE_TYPES.join(",")}
               class="hidden"
               onChange={(e) => {
                 const file = e.currentTarget.files?.[0]
@@ -1676,7 +1602,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
             <div class="flex items-center gap-2">
               <SessionContextUsage />
               <Show when={store.mode === "normal"}>
-                <Tooltip placement="top" value="Attach image">
+                <Tooltip placement="top" value="Attach file">
                   <Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}>
                     <Icon name="photo" class="size-4.5" />
                   </Button>

+ 7 - 1
packages/app/src/components/session/session-context-tab.tsx

@@ -305,13 +305,19 @@ export function SessionContextTab(props: SessionContextTabProps) {
   let frame: number | undefined
   let pending: { x: number; y: number } | undefined
 
-  const restoreScroll = () => {
+  const restoreScroll = (retries = 0) => {
     const el = scroll
     if (!el) return
 
     const s = props.view()?.scroll("context")
     if (!s) return
 
+    // Wait for content to be scrollable - content may not have rendered yet
+    if (el.scrollHeight <= el.clientHeight && retries < 10) {
+      requestAnimationFrame(() => restoreScroll(retries + 1))
+      return
+    }
+
     if (el.scrollTop !== s.y) el.scrollTop = s.y
     if (el.scrollLeft !== s.x) el.scrollLeft = s.x
   }

+ 8 - 11
packages/app/src/components/session/session-header.tsx

@@ -1,4 +1,4 @@
-import { createMemo, createResource, Show } from "solid-js"
+import { createEffect, createMemo, createResource, Show } from "solid-js"
 import { A, useNavigate, useParams } from "@solidjs/router"
 import { useLayout } from "@/context/layout"
 import { useCommand } from "@/context/command"
@@ -7,7 +7,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { useSync } from "@/context/sync"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { getFilename } from "@opencode-ai/util/path"
-import { base64Encode } from "@opencode-ai/util/encode"
+import { base64Decode, base64Encode } from "@opencode-ai/util/encode"
 import { iife } from "@opencode-ai/util/iife"
 import { Icon } from "@opencode-ai/ui/icon"
 import { IconButton } from "@opencode-ai/ui/icon-button"
@@ -31,10 +31,11 @@ export function SessionHeader() {
   const dialog = useDialog()
   const sync = useSync()
 
+  const projectDirectory = createMemo(() => base64Decode(params.dir ?? ""))
+
   const sessions = createMemo(() => (sync.data.session ?? []).filter((s) => !s.parentID))
   const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
   const shareEnabled = createMemo(() => sync.data.config.share !== "disabled")
-  const branch = createMemo(() => sync.data.vcs?.branch)
 
   function navigateToProject(directory: string) {
     navigate(`/${base64Encode(directory)}`)
@@ -46,7 +47,7 @@ export function SessionHeader() {
   }
 
   return (
-    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
+    <header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex">
       <button
         type="button"
         class="xl:hidden w-12 shrink-0 flex items-center justify-center border-r border-border-weak-base hover:bg-surface-raised-base-hover active:bg-surface-raised-base-active transition-colors"
@@ -60,12 +61,8 @@ export function SessionHeader() {
             <div class="hidden xl:flex items-center gap-2">
               <Select
                 options={layout.projects.list().map((project) => project.worktree)}
-                current={sync.directory}
-                label={(x) => {
-                  const name = getFilename(x)
-                  const b = x === sync.directory ? branch() : undefined
-                  return b ? `${name}:${b}` : name
-                }}
+                current={sync.project?.worktree ?? projectDirectory()}
+                label={(x) => getFilename(x)}
                 onSelect={(x) => (x ? navigateToProject(x) : undefined)}
                 class="text-14-regular text-text-base"
                 variant="ghost"
@@ -191,7 +188,7 @@ export function SessionHeader() {
                     let shareURL = session.share?.url
                     if (!shareURL) {
                       shareURL = await globalSDK.client.session
-                        .share({ sessionID: session.id, directory: sync.directory })
+                        .share({ sessionID: session.id, directory: projectDirectory() })
                         .then((r) => r.data?.share?.url)
                         .catch((e) => {
                           console.error("Failed to share session", e)

+ 46 - 2
packages/app/src/components/session/session-new-view.tsx

@@ -1,12 +1,41 @@
-import { Show } from "solid-js"
+import { Show, createMemo } from "solid-js"
 import { DateTime } from "luxon"
 import { useSync } from "@/context/sync"
 import { Icon } from "@opencode-ai/ui/icon"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
+import { Select } from "@opencode-ai/ui/select"
 
-export function NewSessionView() {
+const MAIN_WORKTREE = "main"
+const CREATE_WORKTREE = "create"
+
+interface NewSessionViewProps {
+  worktree: string
+  onWorktreeChange: (value: string) => void
+}
+
+export function NewSessionView(props: NewSessionViewProps) {
   const sync = useSync()
 
+  const sandboxes = createMemo(() => sync.project?.sandboxes ?? [])
+  const options = createMemo(() => [MAIN_WORKTREE, ...sandboxes(), CREATE_WORKTREE])
+  const current = createMemo(() => {
+    const selection = props.worktree
+    if (options().includes(selection)) return selection
+    return MAIN_WORKTREE
+  })
+
+  const label = (value: string) => {
+    if (value === MAIN_WORKTREE) {
+      const branch = sync.data.vcs?.branch
+      if (branch) return `Current branch (${branch})`
+      return "Main branch"
+    }
+
+    if (value === CREATE_WORKTREE) return "Create new worktree"
+
+    return getFilename(value)
+  }
+
   return (
     <div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
       <div class="text-20-medium text-text-weaker">New session</div>
@@ -17,6 +46,21 @@ export function NewSessionView() {
           <span class="text-text-strong">{getFilename(sync.data.path.directory)}</span>
         </div>
       </div>
+      <div class="flex justify-center items-center gap-1">
+        <Icon name="branch" size="small" />
+        <Select
+          options={options()}
+          current={current()}
+          value={(x) => x}
+          label={label}
+          onSelect={(value) => {
+            props.onWorktreeChange(value ?? MAIN_WORKTREE)
+          }}
+          size="normal"
+          variant="ghost"
+          class="text-12-medium"
+        />
+      </div>
       <Show when={sync.project}>
         {(project) => (
           <div class="flex justify-center items-center gap-3">

+ 0 - 1
packages/app/src/context/global-sdk.tsx

@@ -31,7 +31,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
     const platform = usePlatform()
     const sdk = createOpencodeClient({
       baseUrl: server.url,
-      signal: AbortSignal.timeout(1000 * 60 * 10),
       fetch: platform.fetch,
       throwOnError: true,
     })

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

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

+ 13 - 6
packages/app/src/context/local.tsx

@@ -160,6 +160,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
         ),
       )
 
+      const latestSet = createMemo(() => new Set(latest().map((x) => `${x.providerID}:${x.modelID}`)))
+
+      const userVisibilityMap = createMemo(() => {
+        const map = new Map<string, "show" | "hide">()
+        for (const item of store.user) {
+          map.set(`${item.providerID}:${item.modelID}`, item.visibility)
+        }
+        return map
+      })
+
       const list = createMemo(() =>
         available().map((m) => ({
           ...m,
@@ -264,12 +274,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           })
         },
         visible(model: ModelKey) {
-          const user = store.user.find((x) => x.modelID === model.modelID && x.providerID === model.providerID)
-          return (
-            user?.visibility !== "hide" &&
-            (latest().find((x) => x.modelID === model.modelID && x.providerID === model.providerID) ||
-              user?.visibility === "show")
-          )
+          const key = `${model.providerID}:${model.modelID}`
+          const visibility = userVisibilityMap().get(key)
+          return visibility !== "hide" && (latestSet().has(key) || visibility === "show")
         },
         setVisibility(model: ModelKey, visible: boolean) {
           updateVisibility(model, visible ? "show" : "hide")

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

@@ -3,7 +3,7 @@ import { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
 
 export type Platform = {
   /** Platform discriminator */
-  platform: "web" | "tauri"
+  platform: "web" | "desktop"
 
   /** App version */
   version?: string

+ 0 - 1
packages/app/src/context/sdk.tsx

@@ -11,7 +11,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
     const globalSDK = useGlobalSDK()
     const sdk = createOpencodeClient({
       baseUrl: globalSDK.url,
-      signal: AbortSignal.timeout(1000 * 60 * 10),
       fetch: platform.fetch,
       directory: props.directory,
       throwOnError: true,

+ 2 - 3
packages/app/src/context/server.tsx

@@ -11,8 +11,7 @@ export function normalizeServerUrl(input: string) {
   const trimmed = input.trim()
   if (!trimmed) return
   const withProtocol = /^https?:\/\//.test(trimmed) ? trimmed : `http://${trimmed}`
-  const cleaned = withProtocol.replace(/\/+$/, "")
-  return cleaned.replace(/^(https?:\/\/[^/]+).*/, "$1")
+  return withProtocol.replace(/\/+$/, "")
 }
 
 export function serverDisplayName(url: string) {
@@ -100,7 +99,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
         const sdk = createOpencodeClient({
           baseUrl: url,
           fetch: platform.fetch,
-          signal: AbortSignal.timeout(2000),
+          signal: AbortSignal.timeout(3000),
         })
         return sdk.global
           .health()

+ 2 - 2
packages/app/src/pages/home.tsx

@@ -53,8 +53,8 @@ export default function Home() {
   }
 
   return (
-    <div class="mx-auto mt-55">
-      <Logo class="w-xl opacity-12" />
+    <div class="mx-auto mt-55 w-full md:w-auto px-4">
+      <Logo class="md:w-xl opacity-12" />
       <Button
         size="large"
         variant="ghost"

+ 105 - 76
packages/app/src/pages/layout.tsx

@@ -172,9 +172,9 @@ export default function Layout(props: ParentProps) {
       const perm = e.details.properties
       if (permission.autoResponds(perm)) return
 
-      const sessionKey = `${directory}:${perm.sessionID}`
       const [store] = globalSync.child(directory)
       const session = store.session.find((s) => s.id === perm.sessionID)
+      const sessionKey = `${directory}:${perm.sessionID}`
 
       const sessionTitle = session?.title ?? "New session"
       const projectName = getFilename(directory)
@@ -665,14 +665,13 @@ export default function Layout(props: ParentProps) {
       <>
         <div
           data-session-id={props.session.id}
-          class="group/session relative w-full pr-2 py-1 rounded-md cursor-default transition-colors
+          class="group/session relative w-full 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"
-          style={{ "padding-left": "16px" }}
         >
           <Tooltip placement={props.mobile ? "bottom" : "right"} value={props.session.title} gutter={10}>
             <A
               href={`${props.slug}/session/${props.session.id}`}
-              class="flex flex-col min-w-0 text-left w-full focus:outline-none"
+              class="flex flex-col min-w-0 text-left w-full focus:outline-none pl-4 pr-2 py-1"
             >
               <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
@@ -740,10 +739,17 @@ export default function Layout(props: ParentProps) {
   const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => {
     const sortable = createSortable(props.project.worktree)
     const showExpanded = createMemo(() => props.mobile || layout.sidebar.opened())
-    const slug = createMemo(() => base64Encode(props.project.worktree))
+    const defaultWorktree = createMemo(() => base64Encode(props.project.worktree))
     const name = createMemo(() => props.project.name || getFilename(props.project.worktree))
     const [store, setProjectStore] = globalSync.child(props.project.worktree)
-    const sessions = createMemo(() => store.session.toSorted(sortSessions))
+    const stores = createMemo(() =>
+      [props.project.worktree, ...(props.project.sandboxes ?? [])].map((dir) => globalSync.child(dir)[0]),
+    )
+    const sessions = createMemo(() =>
+      stores()
+        .flatMap((store) => store.session.filter((session) => session.directory === store.path.directory))
+        .toSorted(sortSessions),
+    )
     const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
     const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
     const loadMoreSessions = async () => {
@@ -799,7 +805,7 @@ export default function Layout(props: ParentProps) {
                     </DropdownMenu.Portal>
                   </DropdownMenu>
                   <TooltipKeybind placement="top" title="New session" keybind={command.keybind("session.new")}>
-                    <IconButton as={A} href={`${slug()}/session`} icon="plus-small" variant="ghost" />
+                    <IconButton as={A} href={`${defaultWorktree()}/session`} icon="plus-small" variant="ghost" />
                   </TooltipKeybind>
                 </div>
               </Button>
@@ -807,7 +813,12 @@ export default function Layout(props: ParentProps) {
                 <nav class="hidden @[4rem]:flex w-full flex-col gap-1.5">
                   <For each={rootSessions()}>
                     {(session) => (
-                      <SessionItem session={session} slug={slug()} project={props.project} mobile={props.mobile} />
+                      <SessionItem
+                        session={session}
+                        slug={base64Encode(session.directory)}
+                        project={props.project}
+                        mobile={props.mobile}
+                      />
                     )}
                   </For>
                   <Show when={rootSessions().length === 0}>
@@ -819,7 +830,7 @@ export default function Layout(props: ParentProps) {
                         <div class="flex-1 min-w-0">
                           <Tooltip placement={props.mobile ? "bottom" : "right"} value="New session">
                             <A
-                              href={`${slug()}/session`}
+                              href={`${defaultWorktree()}/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">
@@ -875,76 +886,85 @@ export default function Layout(props: ParentProps) {
   const SidebarContent = (sidebarProps: { mobile?: boolean }) => {
     const expanded = () => sidebarProps.mobile || layout.sidebar.opened()
     return (
-      <>
-        <div class="flex flex-col items-start self-stretch gap-4 p-2 min-h-0 overflow-hidden">
+      <div class="flex flex-col self-stretch h-full items-center justify-between overflow-hidden min-h-0">
+        <div class="flex flex-col items-start self-stretch gap-4 min-h-0">
           <Show when={!sidebarProps.mobile}>
-            <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2" data-tauri-drag-region>
-              <Mark class="shrink-0" />
-            </A>
-          </Show>
-          <Show when={!sidebarProps.mobile}>
-            <TooltipKeybind
-              class="shrink-0"
-              placement="right"
-              title="Toggle sidebar"
-              keybind={command.keybind("sidebar.toggle")}
-              inactive={expanded()}
-            >
-              <Button
-                variant="ghost"
-                size="large"
-                class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
-                onClick={layout.sidebar.toggle}
-              >
-                <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
-                  <Icon
-                    name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
-                    size="small"
-                    class="group-hover/sidebar-toggle:hidden"
-                  />
-                  <Icon
-                    name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
-                    size="small"
-                    class="hidden group-hover/sidebar-toggle:inline-block"
-                  />
-                  <Icon
-                    name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
-                    size="small"
-                    class="hidden group-active/sidebar-toggle:inline-block"
-                  />
-                </div>
-                <Show when={layout.sidebar.opened()}>
-                  <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
-                    Toggle sidebar
-                  </div>
-                </Show>
-              </Button>
-            </TooltipKeybind>
-          </Show>
-          <DragDropProvider
-            onDragStart={handleDragStart}
-            onDragEnd={handleDragEnd}
-            onDragOver={handleDragOver}
-            collisionDetector={closestCenter}
-          >
-            <DragDropSensors />
-            <ConstrainDragXAxis />
             <div
-              ref={(el) => {
-                if (!sidebarProps.mobile) scrollContainerRef = el
+              classList={{
+                "border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0": true,
+                "justify-start": expanded(),
               }}
-              class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
             >
-              <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
-                <For each={layout.projects.list()}>
-                  {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
-                </For>
-              </SortableProvider>
+              <A href="/" class="shrink-0 h-8 flex items-center justify-start px-2 w-full" data-tauri-drag-region>
+                <Mark class="shrink-0" />
+              </A>
             </div>
-            <DragOverlay>
-              <ProjectDragOverlay />
-            </DragOverlay>
-          </DragDropProvider>
+          </Show>
+          <div class="flex flex-col items-start self-stretch gap-4 px-2 overflow-hidden min-h-0">
+            <Show when={!sidebarProps.mobile}>
+              <TooltipKeybind
+                class="shrink-0"
+                placement="right"
+                title="Toggle sidebar"
+                keybind={command.keybind("sidebar.toggle")}
+                inactive={expanded()}
+              >
+                <Button
+                  variant="ghost"
+                  size="large"
+                  class="group/sidebar-toggle shrink-0 w-full text-left justify-start rounded-lg px-2"
+                  onClick={layout.sidebar.toggle}
+                >
+                  <div class="relative -ml-px flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0">
+                    <Icon
+                      name={layout.sidebar.opened() ? "layout-left" : "layout-right"}
+                      size="small"
+                      class="group-hover/sidebar-toggle:hidden"
+                    />
+                    <Icon
+                      name={layout.sidebar.opened() ? "layout-left-partial" : "layout-right-partial"}
+                      size="small"
+                      class="hidden group-hover/sidebar-toggle:inline-block"
+                    />
+                    <Icon
+                      name={layout.sidebar.opened() ? "layout-left-full" : "layout-right-full"}
+                      size="small"
+                      class="hidden group-active/sidebar-toggle:inline-block"
+                    />
+                  </div>
+                  <Show when={layout.sidebar.opened()}>
+                    <div class="hidden group-hover/sidebar-toggle:block group-active/sidebar-toggle:block text-text-base">
+                      Toggle sidebar
+                    </div>
+                  </Show>
+                </Button>
+              </TooltipKeybind>
+            </Show>
+            <DragDropProvider
+              onDragStart={handleDragStart}
+              onDragEnd={handleDragEnd}
+              onDragOver={handleDragOver}
+              collisionDetector={closestCenter}
+            >
+              <DragDropSensors />
+              <ConstrainDragXAxis />
+              <div
+                ref={(el) => {
+                  if (!sidebarProps.mobile) scrollContainerRef = el
+                }}
+                class="w-full min-w-8 flex flex-col gap-2 min-h-0 overflow-y-auto no-scrollbar"
+              >
+                <SortableProvider ids={layout.projects.list().map((p) => p.worktree)}>
+                  <For each={layout.projects.list()}>
+                    {(project) => <SortableProject project={project} mobile={sidebarProps.mobile} />}
+                  </For>
+                </SortableProvider>
+              </div>
+              <DragOverlay>
+                <ProjectDragOverlay />
+              </DragOverlay>
+            </DragDropProvider>
+          </div>
         </div>
         <div class="flex flex-col gap-1.5 self-stretch items-start shrink-0 px-2 py-3">
           <Switch>
@@ -1017,7 +1037,7 @@ export default function Layout(props: ParentProps) {
             </Button>
           </Tooltip>
         </div>
-      </>
+      </div>
     )
   }
 
@@ -1065,12 +1085,21 @@ export default function Layout(props: ParentProps) {
           />
           <div
             classList={{
-              "@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pt-12 pb-5 transition-transform duration-200 ease-out": true,
+              "@container fixed inset-y-0 left-0 z-50 w-72 bg-background-base border-r border-border-weak-base flex flex-col gap-5.5 items-start self-stretch justify-between pb-5 transition-transform duration-200 ease-out": true,
               "translate-x-0": layout.mobileSidebar.opened(),
               "-translate-x-full": !layout.mobileSidebar.opened(),
             }}
             onClick={(e) => e.stopPropagation()}
           >
+            <div class="border-b border-border-weak-base w-full h-12 ml-px flex items-center pl-1.75 shrink-0">
+              <A
+                href="/"
+                class="shrink-0 h-8 flex items-center justify-start px-2 w-full"
+                onClick={() => layout.mobileSidebar.hide()}
+              >
+                <Mark class="shrink-0" />
+              </A>
+            </div>
             <SidebarContent mobile />
           </div>
         </div>

+ 192 - 232
packages/app/src/pages/session.tsx

@@ -1,4 +1,4 @@
-import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on, createRenderEffect, batch } from "solid-js"
+import { For, onCleanup, Show, Match, Switch, createMemo, createEffect, on, batch } from "solid-js"
 import { createMediaQuery } from "@solid-primitives/media"
 import { Dynamic } from "solid-js/web"
 import { useLocal } from "@/context/local"
@@ -24,7 +24,7 @@ import { useSync } from "@/context/sync"
 import { useTerminal, type LocalPTY } from "@/context/terminal"
 import { useLayout } from "@/context/layout"
 import { Terminal } from "@/components/terminal"
-import { checksum } from "@opencode-ai/util/encode"
+import { checksum, base64Decode } from "@opencode-ai/util/encode"
 import { useDialog } from "@opencode-ai/ui/context/dialog"
 import { DialogSelectFile } from "@/components/dialog-select-file"
 import { DialogSelectModel } from "@/components/dialog-select-model"
@@ -47,6 +47,7 @@ import {
   SortableTerminalTab,
   NewSessionView,
 } from "@/components/session"
+import { usePlatform } from "@/context/platform"
 
 function same<T>(a: readonly T[], b: readonly T[]) {
   if (a === b) return true
@@ -73,13 +74,19 @@ function SessionReviewTab(props: SessionReviewTabProps) {
   let frame: number | undefined
   let pending: { x: number; y: number } | undefined
 
-  const restoreScroll = () => {
+  const restoreScroll = (retries = 0) => {
     const el = scroll
     if (!el) return
 
     const s = props.view().scroll("review")
     if (!s) return
 
+    // Wait for content to be scrollable - content may not have rendered yet
+    if (el.scrollHeight <= el.clientHeight && retries < 10) {
+      requestAnimationFrame(() => restoreScroll(retries + 1))
+      return
+    }
+
     if (el.scrollTop !== s.y) el.scrollTop = s.y
     if (el.scrollLeft !== s.x) el.scrollLeft = s.x
   }
@@ -147,6 +154,7 @@ export default function Page() {
   const dialog = useDialog()
   const codeComponent = useCodeComponent()
   const command = useCommand()
+  const platform = usePlatform()
   const params = useParams()
   const navigate = useNavigate()
   const sdk = useSDK()
@@ -218,20 +226,12 @@ export default function Page() {
     return sync.data.message[id] !== undefined
   })
   const emptyUserMessages: UserMessage[] = []
-  const userMessages = createMemo(
-    () => messages().filter((m) => m.role === "user") as UserMessage[],
-    emptyUserMessages,
-    { equals: same },
-  )
-  const visibleUserMessages = createMemo(
-    () => {
-      const revert = revertMessageID()
-      if (!revert) return userMessages()
-      return userMessages().filter((m) => m.id < revert)
-    },
-    emptyUserMessages,
-    { equals: same },
-  )
+  const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages)
+  const visibleUserMessages = createMemo(() => {
+    const revert = revertMessageID()
+    if (!revert) return userMessages()
+    return userMessages().filter((m) => m.id < revert)
+  }, emptyUserMessages)
   const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
 
   createEffect(
@@ -249,13 +249,10 @@ export default function Page() {
   const [store, setStore] = createStore({
     activeDraggable: undefined as string | undefined,
     activeTerminalDraggable: undefined as string | undefined,
-    userInteracted: false,
-    stepsExpanded: true,
-    mobileStepsExpanded: {} as Record<string, boolean>,
+    expanded: {} as Record<string, boolean>,
     messageId: undefined as string | undefined,
     mobileTab: "session" as "session" | "review",
-    ignoreScrollSpy: false,
-    initialScrollDone: !params.id,
+    newSessionWorktree: "main",
   })
 
   const activeMessage = createMemo(() => {
@@ -316,47 +313,24 @@ export default function Page() {
     ),
   )
 
-  createEffect(
-    on(
-      () => params.id,
-      (id) => {
-        const status = sync.data.session_status[id ?? ""] ?? idle
-        batch(() => {
-          setStore("userInteracted", false)
-          setStore("stepsExpanded", status.type !== "idle")
-        })
-      },
-    ),
-  )
-
   const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
 
   createEffect(
     on(
-      () => status().type,
-      (type) => {
-        if (type !== "idle") return
-        batch(() => {
-          setStore("userInteracted", false)
-          setStore("stepsExpanded", false)
-        })
+      () => params.id,
+      () => {
+        setStore("messageId", undefined)
+        setStore("expanded", {})
       },
       { defer: true },
     ),
   )
 
-  const working = createMemo(() => status().type !== "idle" && activeMessage()?.id === lastUserMessage()?.id)
-
-  createRenderEffect((prev) => {
-    const isWorking = working()
-    if (!prev && isWorking) {
-      setStore("stepsExpanded", true)
-    }
-    if (prev && !isWorking && !store.userInteracted) {
-      setStore("stepsExpanded", false)
-    }
-    return isWorking
-  }, working())
+  createEffect(() => {
+    const id = lastUserMessage()?.id
+    if (!id) return
+    setStore("expanded", id, status().type !== "idle")
+  })
 
   command.register(() => [
     {
@@ -405,12 +379,16 @@ export default function Page() {
     {
       id: "steps.toggle",
       title: "Toggle steps",
-      description: "Show or hide the steps",
+      description: "Show or hide steps for the current message",
       category: "View",
       keybind: "mod+e",
       slash: "steps",
       disabled: !params.id,
-      onSelect: () => setStore("stepsExpanded", (x) => !x),
+      onSelect: () => {
+        const msg = activeMessage()
+        if (!msg) return
+        setStore("expanded", msg.id, (open: boolean | undefined) => !open)
+      },
     },
     {
       id: "message.previous",
@@ -555,6 +533,31 @@ export default function Page() {
         setActiveMessage(priorMsg)
       },
     },
+    {
+      id: "session.compact",
+      title: "Compact session",
+      description: "Summarize the session to reduce context size",
+      category: "Session",
+      slash: "compact",
+      disabled: !params.id || visibleUserMessages().length === 0,
+      onSelect: async () => {
+        const sessionID = params.id
+        if (!sessionID) return
+        const model = local.model.current()
+        if (!model) {
+          showToast({
+            title: "No model selected",
+            description: "Connect a provider to summarize this session",
+          })
+          return
+        }
+        await sdk.client.session.summarize({
+          sessionID,
+          modelID: model.id,
+          providerID: model.provider.id,
+        })
+      },
+    },
   ])
 
   const handleKeyDown = (event: KeyboardEvent) => {
@@ -655,204 +658,76 @@ export default function Page() {
   const isWorking = createMemo(() => status().type !== "idle")
   const autoScroll = createAutoScroll({
     working: isWorking,
-    onUserInteracted: () => setStore("userInteracted", true),
   })
 
-  let scrollContainer: HTMLDivElement | undefined
-  let initialScrollFrame: number | undefined
-  let initialScrollTarget: string | undefined
-
-  const cancelInitialScroll = () => {
-    if (initialScrollFrame === undefined) return
-    cancelAnimationFrame(initialScrollFrame)
-    initialScrollFrame = undefined
-  }
+  let scrollSpyFrame: number | undefined
+  let scrollSpyTarget: HTMLDivElement | undefined
 
-  const ensureInitialScroll = () => {
-    cancelInitialScroll()
-    initialScrollFrame = requestAnimationFrame(() => {
-      initialScrollFrame = undefined
-      if (!params.id) {
-        initialScrollTarget = undefined
-        setStore("initialScrollDone", true)
-        return
-      }
-      const msgs = visibleUserMessages()
-      if (msgs.length === 0) {
-        if (!messagesReady()) {
-          ensureInitialScroll()
-          return
-        }
-        initialScrollTarget = undefined
-        setStore("initialScrollDone", true)
-        return
-      }
-      const last = msgs[msgs.length - 1]
-      const el = messageRefs.get(last.id)
-      if (!el || !scrollContainer) {
-        ensureInitialScroll()
-        return
-      }
-      scrollToMessage(last, "auto")
-      initialScrollTarget = last.id
-      setStore("initialScrollDone", true)
-    })
-  }
+  const anchor = (id: string) => `message-${id}`
 
   const setScrollRef = (el: HTMLDivElement | undefined) => {
-    scrollContainer = el
     autoScroll.scrollRef(el)
   }
 
-  const messageRefs = new Map<string, HTMLDivElement>()
-  let scrollTimer: number | undefined
-
-  createEffect(() => {
-    const msgs = visibleUserMessages()
-    if (msgs.length === 0) {
-      messageRefs.clear()
-      return
-    }
-    const ids = new Set(msgs.map((m) => m.id))
-    for (const id of messageRefs.keys()) {
-      if (ids.has(id)) continue
-      messageRefs.delete(id)
-    }
-  })
-
-  let scrollSpyIndex = 0
+  const updateHash = (id: string) => {
+    window.history.replaceState(null, "", `#${anchor(id)}`)
+  }
 
   const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
-    setStore("ignoreScrollSpy", true)
     setActiveMessage(message)
 
-    const msgs = visibleUserMessages()
-    const idx = msgs.findIndex((m) => m.id === message.id)
-    if (idx >= 0) scrollSpyIndex = idx
+    const el = document.getElementById(anchor(message.id))
+    if (el) el.scrollIntoView({ behavior, block: "start" })
+    updateHash(message.id)
+  }
 
-    const el = messageRefs.get(message.id)
-    if (el) {
-      el.scrollIntoView({ behavior, block: "start" })
+  const getActiveMessageId = (container: HTMLDivElement) => {
+    const cutoff = container.scrollTop + 100
+    const nodes = container.querySelectorAll<HTMLElement>("[data-message-id]")
+    let id: string | undefined
+
+    for (const node of nodes) {
+      const next = node.dataset.messageId
+      if (!next) continue
+      if (node.offsetTop > cutoff) break
+      id = next
     }
 
-    if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
-    scrollTimer = window.setTimeout(() => setStore("ignoreScrollSpy", false), 1000)
+    return id
   }
 
-  let scrollSpyFrame: number | undefined
-  let scrollSpyTarget: HTMLDivElement | undefined
-
   const scheduleScrollSpy = (container: HTMLDivElement) => {
-    if (store.ignoreScrollSpy) return
     scrollSpyTarget = container
     if (scrollSpyFrame !== undefined) return
 
     scrollSpyFrame = requestAnimationFrame(() => {
       scrollSpyFrame = undefined
+
       const target = scrollSpyTarget
       scrollSpyTarget = undefined
       if (!target) return
-      if (store.ignoreScrollSpy) return
-
-      const msgs = visibleUserMessages()
-      const scrollTop = target.scrollTop
-      const threshold = 100
-      const cutoff = scrollTop + threshold
-
-      if (msgs.length === 0) return
-
-      if (scrollSpyIndex >= msgs.length) scrollSpyIndex = msgs.length - 1
-      if (scrollSpyIndex < 0) scrollSpyIndex = 0
-
-      while (scrollSpyIndex + 1 < msgs.length) {
-        const next = msgs[scrollSpyIndex + 1]
-        if (!next) break
-
-        const el = messageRefs.get(next.id)
-        if (!el) break
-        if (el.offsetTop <= cutoff) {
-          scrollSpyIndex += 1
-          continue
-        }
-        break
-      }
-
-      while (scrollSpyIndex > 0) {
-        const cur = msgs[scrollSpyIndex]
-        if (!cur) break
-
-        const el = messageRefs.get(cur.id)
-        if (!el) break
-        if (el.offsetTop > cutoff) {
-          scrollSpyIndex -= 1
-          continue
-        }
-        break
-      }
 
-      const msg = msgs[scrollSpyIndex]
-      if (!msg) return
-      if (msg.id === activeMessage()?.id) return
+      const id = getActiveMessageId(target)
+      if (!id) return
+      if (id === store.messageId) return
 
-      setActiveMessage(msg)
+      setStore("messageId", id)
     })
   }
 
-  createEffect(
-    on(
-      () => params.id,
-      (id) => {
-        cancelInitialScroll()
-        if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
-        scrollTimer = undefined
-        if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
-        scrollSpyFrame = undefined
-        scrollSpyTarget = undefined
-        messageRefs.clear()
-        scrollSpyIndex = 0
-        initialScrollTarget = undefined
-        setStore("initialScrollDone", !id)
-      },
-      { defer: true },
-    ),
-  )
-
   createEffect(() => {
-    const msgs = visibleUserMessages()
-    const target = msgs.at(-1)?.id
+    const sessionID = params.id
     const ready = messagesReady()
+    if (!sessionID || !ready) return
 
-    if (!params.id) {
-      setStore("initialScrollDone", true)
-      initialScrollTarget = undefined
-      return
-    }
-
-    if (!ready) {
-      setStore("initialScrollDone", false)
-      ensureInitialScroll()
-      return
-    }
-
-    if (!store.initialScrollDone) {
-      ensureInitialScroll()
-      return
-    }
-
-    if (!initialScrollTarget && target) {
-      setStore("initialScrollDone", false)
-      ensureInitialScroll()
-    }
-  })
-
-  createEffect(() => {
-    const msgs = visibleUserMessages()
-    if (msgs.length === 0) return
     requestAnimationFrame(() => {
-      if (!scrollContainer) return
-      if (!isDesktop()) return
-      // Manually trigger spy once to set initial active message based on scroll position
-      scheduleScrollSpy(scrollContainer)
+      const id = window.location.hash.slice(1)
+      const hashTarget = id ? document.getElementById(id) : undefined
+      if (hashTarget) {
+        hashTarget.scrollIntoView({ behavior: "auto", block: "start" })
+        return
+      }
+      autoScroll.forceScrollToBottom()
     })
   })
 
@@ -862,8 +737,6 @@ export default function Page() {
 
   onCleanup(() => {
     document.removeEventListener("keydown", handleKeyDown)
-    cancelInitialScroll()
-    if (scrollTimer !== undefined) window.clearTimeout(scrollTimer)
     if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
   })
 
@@ -944,13 +817,10 @@ export default function Page() {
                         }}
                         onClick={autoScroll.handleInteraction}
                         class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
-                        classList={{
-                          "opacity-0 pointer-events-none": !store.initialScrollDone,
-                        }}
                       >
                         <div
                           ref={autoScroll.contentRef}
-                          class="flex flex-col gap-45 items-start justify-start pb-32 md:pb-40 transition-[margin]"
+                          class="flex flex-col gap-32 items-start justify-start pb-32 md:pb-40 transition-[margin]"
                           classList={{
                             "mt-0.5": !showTabs(),
                             "mt-0": showTabs(),
@@ -959,16 +829,24 @@ export default function Page() {
                           <For each={visibleUserMessages()}>
                             {(message) => (
                               <div
-                                ref={(el) => messageRefs.set(message.id, el)}
-                                class="min-w-0 w-full max-w-full last:min-h-[80vh]"
+                                id={anchor(message.id)}
+                                data-message-id={message.id}
+                                classList={{
+                                  "min-w-0 w-full max-w-full": true,
+                                  "last:min-h-[calc(100vh-13.5rem)] md:last:min-h-[calc(100vh-14.5rem)]":
+                                    platform.platform !== "desktop",
+                                  "last:min-h-[calc(100vh-15rem)] md:last:min-h-[calc(100vh-16rem)]":
+                                    platform.platform === "desktop",
+                                }}
                               >
                                 <SessionTurn
                                   sessionID={params.id!}
                                   messageID={message.id}
                                   lastUserMessageID={lastUserMessage()?.id}
-                                  stepsExpanded={store.mobileStepsExpanded[message.id] ?? false}
-                                  onStepsExpandedToggle={() => setStore("mobileStepsExpanded", message.id, (x) => !x)}
-                                  onUserInteracted={() => setStore("userInteracted", true)}
+                                  stepsExpanded={store.expanded[message.id] ?? false}
+                                  onStepsExpandedToggle={() =>
+                                    setStore("expanded", message.id, (open: boolean | undefined) => !open)
+                                  }
                                   classes={{
                                     root: "min-w-0 w-full relative",
                                     content:
@@ -992,7 +870,10 @@ export default function Page() {
                 </Show>
               </Match>
               <Match when={true}>
-                <NewSessionView />
+                <NewSessionView
+                  worktree={store.newSessionWorktree}
+                  onWorktreeChange={(value) => setStore("newSessionWorktree", value)}
+                />
               </Match>
             </Switch>
           </div>
@@ -1009,6 +890,8 @@ export default function Page() {
                 ref={(el) => {
                   inputRef = el
                 }}
+                newSessionWorktree={store.newSessionWorktree}
+                onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
               />
             </div>
           </div>
@@ -1128,6 +1011,35 @@ export default function Page() {
                     })
                     const contents = createMemo(() => state()?.content?.content ?? "")
                     const cacheKey = createMemo(() => checksum(contents()))
+                    const isImage = createMemo(() => {
+                      const c = state()?.content
+                      return (
+                        c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml"
+                      )
+                    })
+                    const isSvg = createMemo(() => {
+                      const c = state()?.content
+                      return c?.mimeType === "image/svg+xml"
+                    })
+                    const svgContent = createMemo(() => {
+                      if (!isSvg()) return
+                      const c = state()?.content
+                      if (!c) return
+                      if (c.encoding === "base64") return base64Decode(c.content)
+                      return c.content
+                    })
+                    const svgPreviewUrl = createMemo(() => {
+                      if (!isSvg()) return
+                      const c = state()?.content
+                      if (!c) return
+                      if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}`
+                      return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}`
+                    })
+                    const imageDataUrl = createMemo(() => {
+                      if (!isImage()) return
+                      const c = state()?.content
+                      return `data:${c?.mimeType};base64,${c?.content}`
+                    })
                     const selectedLines = createMemo(() => {
                       const p = path()
                       if (!p) return null
@@ -1145,13 +1057,19 @@ export default function Page() {
                       return `L${sel.startLine}-${sel.endLine}`
                     })
 
-                    const restoreScroll = () => {
+                    const restoreScroll = (retries = 0) => {
                       const el = scroll
                       if (!el) return
 
                       const s = view()?.scroll(tab)
                       if (!s) return
 
+                      // Wait for content to be scrollable - content may not have rendered yet
+                      if (el.scrollHeight <= el.clientHeight && retries < 10) {
+                        requestAnimationFrame(() => restoreScroll(retries + 1))
+                        return
+                      }
+
                       if (el.scrollTop !== s.y) el.scrollTop = s.y
                       if (el.scrollLeft !== s.x) el.scrollLeft = s.x
                     }
@@ -1196,6 +1114,17 @@ export default function Page() {
                       ),
                     )
 
+                    createEffect(
+                      on(
+                        () => tabs().active() === tab,
+                        (active) => {
+                          if (!active) return
+                          if (!state()?.loaded) return
+                          requestAnimationFrame(restoreScroll)
+                        },
+                      ),
+                    )
+
                     onCleanup(() => {
                       if (scrollFrame === undefined) return
                       cancelAnimationFrame(scrollFrame)
@@ -1230,6 +1159,37 @@ export default function Page() {
                           )}
                         </Show>
                         <Switch>
+                          <Match when={state()?.loaded && isImage()}>
+                            <div class="px-6 py-4 pb-40">
+                              <img src={imageDataUrl()} alt={path()} class="max-w-full" />
+                            </div>
+                          </Match>
+                          <Match when={state()?.loaded && isSvg()}>
+                            <div class="flex flex-col gap-4 px-6 py-4">
+                              <Dynamic
+                                component={codeComponent}
+                                file={{
+                                  name: path() ?? "",
+                                  contents: svgContent() ?? "",
+                                  cacheKey: cacheKey(),
+                                }}
+                                enableLineSelection
+                                selectedLines={selectedLines()}
+                                onLineSelected={(range: SelectedLineRange | null) => {
+                                  const p = path()
+                                  if (!p) return
+                                  file.setSelectedLines(p, range)
+                                }}
+                                overflow="scroll"
+                                class="select-text"
+                              />
+                              <Show when={svgPreviewUrl()}>
+                                <div class="flex justify-center pb-40">
+                                  <img src={svgPreviewUrl()} alt={path()} class="max-w-full max-h-96" />
+                                </div>
+                              </Show>
+                            </div>
+                          </Match>
                           <Match when={state()?.loaded}>
                             <Dynamic
                               component={codeComponent}

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

@@ -1,7 +1,8 @@
 {
   "name": "@opencode-ai/console-app",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "type": "module",
+  "license": "MIT",
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "dev": "vite dev --host 0.0.0.0",

+ 1 - 1
packages/console/app/src/config.ts

@@ -7,7 +7,7 @@ export const config = {
 
   // GitHub
   github: {
-    repoUrl: "https://github.com/sst/opencode",
+    repoUrl: "https://github.com/anomalyco/opencode",
     starsFormatted: {
       compact: "45K",
       full: "45,000",

+ 1 - 1
packages/console/app/src/routes/[...404].tsx

@@ -26,7 +26,7 @@ export default function NotFound() {
             <a href="/docs">Docs</a>
           </div>
           <div data-slot="action">
-            <a href="https://github.com/sst/opencode">GitHub</a>
+            <a href="https://github.com/anomalyco/opencode">GitHub</a>
           </div>
           <div data-slot="action">
             <a href="/discord">Discord</a>

+ 1 - 1
packages/console/app/src/routes/download/[platform].ts

@@ -21,7 +21,7 @@ export async function GET({ params: { platform } }: APIEvent) {
   const assetName = assetNames[platform]
   if (!assetName) return new Response("Not Found", { status: 404 })
 
-  const resp = await fetch(`https://github.com/sst/opencode/releases/latest/download/${assetName}`, {
+  const resp = await fetch(`https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`, {
     cf: {
       // in case gh releases has rate limits
       cacheTtl: 60 * 60 * 24,

+ 1 - 1
packages/console/app/src/routes/openapi.json.ts

@@ -1,6 +1,6 @@
 export async function GET() {
   const response = await fetch(
-    "https://raw.githubusercontent.com/sst/opencode/refs/heads/dev/packages/sdk/openapi.json",
+    "https://raw.githubusercontent.com/anomalyco/opencode/refs/heads/dev/packages/sdk/openapi.json",
   )
   const json = await response.json()
   return json

+ 1 - 1
packages/console/app/src/routes/temp.tsx

@@ -151,7 +151,7 @@ export default function Home() {
             <a href="https://x.com/opencode">X.com</a>
           </div>
           <div data-slot="cell">
-            <a href="https://github.com/sst/opencode">GitHub</a>
+            <a href="https://github.com/anomalyco/opencode">GitHub</a>
           </div>
           <div data-slot="cell">
             <a href="https://opencode.ai/discord">Discord</a>

+ 15 - 9
packages/console/app/src/routes/zen/util/handler.ts

@@ -172,8 +172,8 @@ export async function handler(
       const tokensInfo = providerInfo.normalizeUsage(json.usage)
       await trialLimiter?.track(tokensInfo)
       await rateLimiter?.track()
-      await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
-      await reload(authInfo)
+      const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
+      await reload(authInfo, costInfo)
       return new Response(body, {
         status: resStatus,
         statusText: res.statusText,
@@ -206,8 +206,8 @@ export async function handler(
                 if (usage) {
                   const tokensInfo = providerInfo.normalizeUsage(usage)
                   await trialLimiter?.track(tokensInfo)
-                  await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
-                  await reload(authInfo)
+                  const costInfo = await trackUsage(authInfo, modelInfo, providerInfo, tokensInfo)
+                  await reload(authInfo, costInfo)
                 }
                 c.close()
                 return
@@ -392,6 +392,7 @@ export async function handler(
             monthlyUsage: BillingTable.monthlyUsage,
             timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
             reloadTrigger: BillingTable.reloadTrigger,
+            timeReloadLockedTill: BillingTable.timeReloadLockedTill,
           },
           user: {
             id: UserTable.id,
@@ -608,13 +609,21 @@ export async function handler(
         .set({ timeUsed: sql`now()` })
         .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
     )
+
+    return { costInMicroCents: cost }
   }
 
-  async function reload(authInfo: AuthInfo) {
+  async function reload(authInfo: AuthInfo, costInfo: Awaited<ReturnType<typeof trackUsage>>) {
     if (!authInfo) return
     if (authInfo.isFree) return
     if (authInfo.provider?.credentials) return
 
+    if (!costInfo) return
+
+    const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100)
+    if (authInfo.billing.balance - costInfo.costInMicroCents >= reloadTrigger) return
+    if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return
+
     const lock = await Database.use((tx) =>
       tx
         .update(BillingTable)
@@ -625,10 +634,7 @@ export async function handler(
           and(
             eq(BillingTable.workspaceID, authInfo.workspaceID),
             eq(BillingTable.reload, true),
-            lt(
-              BillingTable.balance,
-              centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100),
-            ),
+            lt(BillingTable.balance, reloadTrigger),
             or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)),
           ),
         ),

+ 1 - 0
packages/console/core/migrations/0040_broken_gamora.sql

@@ -0,0 +1 @@
+CREATE INDEX `usage_time_created` ON `usage` (`workspace_id`,`time_created`);

+ 1059 - 0
packages/console/core/migrations/meta/0040_snapshot.json

@@ -0,0 +1,1059 @@
+{
+  "version": "5",
+  "dialect": "mysql",
+  "id": "bf19cd74-71f9-4bdf-b50e-67c2436f3408",
+  "prevId": "49a1ac05-78ab-4aae-908e-d4aeeb8196fc",
+  "tables": {
+    "account": {
+      "name": "account",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "account_id_pk": {
+          "name": "account_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "auth": {
+      "name": "auth",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "enum('email','github','google')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "subject": {
+          "name": "subject",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "provider": {
+          "name": "provider",
+          "columns": ["provider", "subject"],
+          "isUnique": true
+        },
+        "account_id": {
+          "name": "account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "auth_id_pk": {
+          "name": "auth_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "benchmark": {
+      "name": "benchmark",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "agent": {
+          "name": "agent",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "result": {
+          "name": "result",
+          "type": "mediumtext",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "time_created": {
+          "name": "time_created",
+          "columns": ["time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "benchmark_id_pk": {
+          "name": "benchmark_id_pk",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "billing": {
+      "name": "billing",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_id": {
+          "name": "payment_method_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_type": {
+          "name": "payment_method_type",
+          "type": "varchar(32)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_method_last4": {
+          "name": "payment_method_last4",
+          "type": "varchar(4)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "balance": {
+          "name": "balance",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload": {
+          "name": "reload",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_trigger": {
+          "name": "reload_trigger",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_amount": {
+          "name": "reload_amount",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "reload_error": {
+          "name": "reload_error",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_error": {
+          "name": "time_reload_error",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_reload_locked_till": {
+          "name": "time_reload_locked_till",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_customer_id": {
+          "name": "global_customer_id",
+          "columns": ["customer_id"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "billing_workspace_id_id_pk": {
+          "name": "billing_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "payment": {
+      "name": "payment",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "customer_id": {
+          "name": "customer_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "invoice_id": {
+          "name": "invoice_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "payment_id": {
+          "name": "payment_id",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "amount": {
+          "name": "amount",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_refunded": {
+          "name": "time_refunded",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "payment_workspace_id_id_pk": {
+          "name": "payment_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "usage": {
+      "name": "usage",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "input_tokens": {
+          "name": "input_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "output_tokens": {
+          "name": "output_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "reasoning_tokens": {
+          "name": "reasoning_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_read_tokens": {
+          "name": "cache_read_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_5m_tokens": {
+          "name": "cache_write_5m_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cache_write_1h_tokens": {
+          "name": "cache_write_1h_tokens",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "cost": {
+          "name": "cost",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key_id": {
+          "name": "key_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "usage_time_created": {
+          "name": "usage_time_created",
+          "columns": ["workspace_id", "time_created"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "usage_workspace_id_id_pk": {
+          "name": "usage_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "ip": {
+      "name": "ip",
+      "columns": {
+        "ip": {
+          "name": "ip",
+          "type": "varchar(45)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "usage": {
+          "name": "usage",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "ip_ip_pk": {
+          "name": "ip_ip_pk",
+          "columns": ["ip"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "key": {
+      "name": "key",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_used": {
+          "name": "time_used",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "global_key": {
+          "name": "global_key",
+          "columns": ["key"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "key_workspace_id_id_pk": {
+          "name": "key_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "model": {
+      "name": "model",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "model": {
+          "name": "model",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "model_workspace_model": {
+          "name": "model_workspace_model",
+          "columns": ["workspace_id", "model"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "model_workspace_id_id_pk": {
+          "name": "model_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "provider": {
+      "name": "provider",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "varchar(64)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "credentials": {
+          "name": "credentials",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "workspace_provider": {
+          "name": "workspace_provider",
+          "columns": ["workspace_id", "provider"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "provider_workspace_id_id_pk": {
+          "name": "provider_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "user": {
+      "name": "user",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "workspace_id": {
+          "name": "workspace_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "account_id": {
+          "name": "account_id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_seen": {
+          "name": "time_seen",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "color": {
+          "name": "color",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "role": {
+          "name": "role",
+          "type": "enum('admin','member')",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "monthly_limit": {
+          "name": "monthly_limit",
+          "type": "int",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "monthly_usage": {
+          "name": "monthly_usage",
+          "type": "bigint",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "time_monthly_usage_updated": {
+          "name": "time_monthly_usage_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "user_account_id": {
+          "name": "user_account_id",
+          "columns": ["workspace_id", "account_id"],
+          "isUnique": true
+        },
+        "user_email": {
+          "name": "user_email",
+          "columns": ["workspace_id", "email"],
+          "isUnique": true
+        },
+        "global_account_id": {
+          "name": "global_account_id",
+          "columns": ["account_id"],
+          "isUnique": false
+        },
+        "global_email": {
+          "name": "global_email",
+          "columns": ["email"],
+          "isUnique": false
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "user_workspace_id_id_pk": {
+          "name": "user_workspace_id_id_pk",
+          "columns": ["workspace_id", "id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    },
+    "workspace": {
+      "name": "workspace",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "varchar(30)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "slug": {
+          "name": "slug",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar(255)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "time_created": {
+          "name": "time_created",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "(now())"
+        },
+        "time_updated": {
+          "name": "time_updated",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false,
+          "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
+        },
+        "time_deleted": {
+          "name": "time_deleted",
+          "type": "timestamp(3)",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "slug": {
+          "name": "slug",
+          "columns": ["slug"],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "workspace_id": {
+          "name": "workspace_id",
+          "columns": ["id"]
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraint": {}
+    }
+  },
+  "views": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "tables": {},
+    "indexes": {}
+  }
+}

+ 7 - 0
packages/console/core/migrations/meta/_journal.json

@@ -281,6 +281,13 @@
       "when": 1766946179892,
       "tag": "0039_striped_forge",
       "breakpoints": true
+    },
+    {
+      "idx": 40,
+      "version": "5",
+      "when": 1767584617316,
+      "tag": "0040_broken_gamora",
+      "breakpoints": true
     }
   ]
 }

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

@@ -1,9 +1,10 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-core",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "private": true,
   "type": "module",
+  "license": "MIT",
   "dependencies": {
     "@aws-sdk/client-sts": "3.782.0",
     "@jsx-email/render": "1.1.1",

+ 4 - 1
packages/console/core/script/promote-models.ts

@@ -18,15 +18,17 @@ const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
+const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
 if (!value5) throw new Error("ZEN_MODELS5 not found")
 if (!value6) throw new Error("ZEN_MODELS6 not found")
+if (!value7) throw new Error("ZEN_MODELS7 not found")
 
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
+ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
 
 // update the secret
 await $`bun sst secret set ZEN_MODELS1 ${value1} --stage ${stage}`
@@ -35,3 +37,4 @@ await $`bun sst secret set ZEN_MODELS3 ${value3} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS4 ${value4} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS5 ${value5} --stage ${stage}`
 await $`bun sst secret set ZEN_MODELS6 ${value6} --stage ${stage}`
+await $`bun sst secret set ZEN_MODELS7 ${value7} --stage ${stage}`

+ 4 - 1
packages/console/core/script/pull-models.ts

@@ -18,14 +18,16 @@ const value3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=")[
 const value4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const value5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 const value6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
+const value7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
 if (!value1) throw new Error("ZEN_MODELS1 not found")
 if (!value2) throw new Error("ZEN_MODELS2 not found")
 if (!value3) throw new Error("ZEN_MODELS3 not found")
 if (!value4) throw new Error("ZEN_MODELS4 not found")
 if (!value5) throw new Error("ZEN_MODELS5 not found")
 if (!value6) throw new Error("ZEN_MODELS6 not found")
+if (!value7) throw new Error("ZEN_MODELS7 not found")
 // validate value
-ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6))
+ZenData.validate(JSON.parse(value1 + value2 + value3 + value4 + value5 + value6 + value7))
 
 // update the secret
 await $`bun sst secret set ZEN_MODELS1 ${value1}`
@@ -34,3 +36,4 @@ await $`bun sst secret set ZEN_MODELS3 ${value3}`
 await $`bun sst secret set ZEN_MODELS4 ${value4}`
 await $`bun sst secret set ZEN_MODELS5 ${value5}`
 await $`bun sst secret set ZEN_MODELS6 ${value6}`
+await $`bun sst secret set ZEN_MODELS7 ${value7}`

+ 11 - 3
packages/console/core/script/update-models.ts

@@ -16,18 +16,24 @@ const oldValue3 = lines.find((line) => line.startsWith("ZEN_MODELS3"))?.split("=
 const oldValue4 = lines.find((line) => line.startsWith("ZEN_MODELS4"))?.split("=")[1]
 const oldValue5 = lines.find((line) => line.startsWith("ZEN_MODELS5"))?.split("=")[1]
 const oldValue6 = lines.find((line) => line.startsWith("ZEN_MODELS6"))?.split("=")[1]
+const oldValue7 = lines.find((line) => line.startsWith("ZEN_MODELS7"))?.split("=")[1]
 if (!oldValue1) throw new Error("ZEN_MODELS1 not found")
 if (!oldValue2) throw new Error("ZEN_MODELS2 not found")
 if (!oldValue3) throw new Error("ZEN_MODELS3 not found")
 if (!oldValue4) throw new Error("ZEN_MODELS4 not found")
 if (!oldValue5) throw new Error("ZEN_MODELS5 not found")
 if (!oldValue6) throw new Error("ZEN_MODELS6 not found")
+if (!oldValue7) throw new Error("ZEN_MODELS7 not found")
 
 // store the prettified json to a temp file
 const filename = `models-${Date.now()}.json`
 const tempFile = Bun.file(path.join(os.tmpdir(), filename))
 await tempFile.write(
-  JSON.stringify(JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6), null, 2),
+  JSON.stringify(
+    JSON.parse(oldValue1 + oldValue2 + oldValue3 + oldValue4 + oldValue5 + oldValue6 + oldValue7),
+    null,
+    2,
+  ),
 )
 console.log("tempFile", tempFile.name)
 
@@ -37,13 +43,14 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
 ZenData.validate(JSON.parse(newValue))
 
 // update the secret
-const chunk = Math.ceil(newValue.length / 6)
+const chunk = Math.ceil(newValue.length / 7)
 const newValue1 = newValue.slice(0, chunk)
 const newValue2 = newValue.slice(chunk, chunk * 2)
 const newValue3 = newValue.slice(chunk * 2, chunk * 3)
 const newValue4 = newValue.slice(chunk * 3, chunk * 4)
 const newValue5 = newValue.slice(chunk * 4, chunk * 5)
-const newValue6 = newValue.slice(chunk * 5)
+const newValue6 = newValue.slice(chunk * 5, chunk * 6)
+const newValue7 = newValue.slice(chunk * 6)
 
 await $`bun sst secret set ZEN_MODELS1 ${newValue1}`
 await $`bun sst secret set ZEN_MODELS2 ${newValue2}`
@@ -51,3 +58,4 @@ await $`bun sst secret set ZEN_MODELS3 ${newValue3}`
 await $`bun sst secret set ZEN_MODELS4 ${newValue4}`
 await $`bun sst secret set ZEN_MODELS5 ${newValue5}`
 await $`bun sst secret set ZEN_MODELS6 ${newValue6}`
+await $`bun sst secret set ZEN_MODELS7 ${newValue7}`

+ 2 - 1
packages/console/core/src/model.ts

@@ -73,7 +73,8 @@ export namespace ZenData {
         Resource.ZEN_MODELS3.value +
         Resource.ZEN_MODELS4.value +
         Resource.ZEN_MODELS5.value +
-        Resource.ZEN_MODELS6.value,
+        Resource.ZEN_MODELS6.value +
+        Resource.ZEN_MODELS7.value,
     )
     return ModelsSchema.parse(json)
   })

+ 2 - 2
packages/console/core/src/schema/billing.sql.ts

@@ -1,4 +1,4 @@
-import { bigint, boolean, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
+import { bigint, boolean, index, int, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
 import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
 import { workspaceIndexes } from "./workspace.sql"
 
@@ -55,5 +55,5 @@ export const UsageTable = mysqlTable(
     cost: bigint("cost", { mode: "number" }).notNull(),
     keyID: ulid("key_id"),
   },
-  (table) => [...workspaceIndexes(table)],
+  (table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
 )

+ 4 - 0
packages/console/core/sst-env.d.ts

@@ -122,6 +122,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS7": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

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

@@ -1,9 +1,10 @@
 {
   "name": "@opencode-ai/console-function",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",
+  "license": "MIT",
   "scripts": {
     "typecheck": "tsgo --noEmit"
   },

+ 4 - 0
packages/console/function/sst-env.d.ts

@@ -122,6 +122,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS7": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

+ 3 - 2
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@opencode-ai/console-mail",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",
@@ -17,5 +17,6 @@
   "scripts": {
     "dev": "email preview emails/templates"
   },
-  "type": "module"
+  "type": "module",
+  "license": "MIT"
 }

+ 1 - 0
packages/console/resource/package.json

@@ -1,6 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@opencode-ai/console-resource",
+  "license": "MIT",
   "dependencies": {
     "@cloudflare/workers-types": "catalog:"
   },

+ 4 - 0
packages/console/resource/sst-env.d.ts

@@ -122,6 +122,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS7": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

+ 0 - 1
packages/desktop/index.html

@@ -13,7 +13,6 @@
     <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
     <meta property="og:image" content="/social-share.png" />
     <meta property="twitter:image" content="/social-share.png" />
-    <!-- Theme preload script - applies cached theme to avoid FOUC -->
     <script id="oc-theme-preload-script">
       ;(function () {
         var themeId = localStorage.getItem("opencode-theme-id")

+ 2 - 1
packages/desktop/package.json

@@ -1,8 +1,9 @@
 {
   "name": "@opencode-ai/desktop",
   "private": true,
-  "version": "1.0.224",
+  "version": "1.1.2",
   "type": "module",
+  "license": "MIT",
   "scripts": {
     "typecheck": "tsgo -b",
     "predev": "bun ./scripts/predev.ts",

+ 127 - 0
packages/desktop/src-tauri/release/appstream.metainfo.xml

@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+  <id>ai.opencode.opencode</id>
+
+  <metadata_license>CC0-1.0</metadata_license>
+  <project_license>MIT</project_license>
+
+  <name>OpenCode</name>
+  <summary>Open source AI coding agent</summary>
+
+  <developer id="ly.anoma">
+    <name>Anomaly Innovations Inc.</name>
+  </developer>
+
+  <description>
+    <p>
+      OpenCode is an open source agent that helps you write and run code with any AI model.
+    </p>
+  </description>
+
+  <launchable type="desktop-id">ai.opencode.opencode.desktop</launchable>
+
+  <content_rating type="oars-1.1" />
+
+  <url type="bugtracker">https://github.com/anomalyco/opencode/issues</url>
+  <url type="homepage">https://opencode.ai</url>
+  <url type="vcs-browser">https://github.com/anomalyco/opencode</url>
+
+  <screenshots>
+    <screenshot type="default">
+      <image>https://opencode.ai/docs/_astro/screenshot.Bs5D4atL_ZvsvFu.webp</image>
+    </screenshot>
+  </screenshots>
+
+  <releases>
+    <release version="1.0.223" date="2026-01-01">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.223</url>
+    </release>
+    <release version="1.0.222" date="2026-01-01">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.222</url>
+    </release>
+    <release version="1.0.221" date="2025-12-31">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.221</url>
+    </release>
+    <release version="1.0.220" date="2025-12-31">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.220</url>
+    </release>
+    <release version="1.0.219" date="2025-12-31">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.219</url>
+    </release>
+    <release version="1.0.218" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.218</url>
+    </release>
+    <release version="1.0.217" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.217</url>
+    </release>
+    <release version="1.0.216" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.216</url>
+    </release>
+    <release version="1.0.215" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.215</url>
+    </release>
+    <release version="1.0.214" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.214</url>
+    </release>
+    <release version="1.0.213" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.213</url>
+    </release>
+    <release version="1.0.212" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.212</url>
+    </release>
+    <release version="1.0.211" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.211</url>
+    </release>
+    <release version="1.0.210" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.210</url>
+    </release>
+    <release version="1.0.209" date="2025-12-30">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.209</url>
+    </release>
+    <release version="1.0.208" date="2025-12-29">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.208</url>
+    </release>
+    <release version="1.0.207" date="2025-12-29">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.207</url>
+    </release>
+    <release version="1.0.206" date="2025-12-28">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.206</url>
+    </release>
+    <release version="1.0.205" date="2025-12-28">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.205</url>
+    </release>
+    <release version="1.0.204" date="2025-12-27">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.204</url>
+    </release>
+    <release version="1.0.203" date="2025-12-26">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.203</url>
+    </release>
+    <release version="1.0.202" date="2025-12-26">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.202</url>
+    </release>
+    <release version="1.0.201" date="2025-12-25">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.201</url>
+    </release>
+    <release version="1.0.200" date="2025-12-25">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.200</url>
+    </release>
+    <release version="1.0.199" date="2025-12-25">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.199</url>
+    </release>
+    <release version="1.0.198" date="2025-12-24">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.198</url>
+    </release>
+    <release version="1.0.195" date="2025-12-24">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.195</url>
+    </release>
+    <release version="1.0.194" date="2025-12-24">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.194</url>
+    </release>
+    <release version="1.0.193" date="2025-12-23">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.193</url>
+    </release>
+    <release version="1.0.191" date="2025-12-23">
+      <url type="details">https://github.com/anomalyco/opencode/releases/tag/v1.0.191</url>
+    </release>
+  </releases>
+</component>

+ 8 - 1
packages/desktop/src-tauri/tauri.prod.conf.json

@@ -15,12 +15,19 @@
       "nsis": {
         "installerIcon": "icons/prod/icon.ico"
       }
+    },
+    "linux": {
+      "deb": {
+        "files": {
+          "/usr/share/metainfo/ai.opencode.opencode.metainfo.xml": "release/appstream.metainfo.xml"
+        }
+      }
     }
   },
   "plugins": {
     "updater": {
       "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYwMDM5Nzg5OUMzOUExMDQKUldRRW9UbWNpWmNEOENYT01CV0lhOXR1UFhpaXJsK1Z3aU9lZnNtNzE0TDROWVMwVW9XQnFOelkK",
-      "endpoints": ["https://github.com/sst/opencode/releases/latest/download/latest.json"]
+      "endpoints": ["https://github.com/anomalyco/opencode/releases/latest/download/latest.json"]
     }
   }
 }

+ 2 - 2
packages/desktop/src/index.tsx

@@ -27,7 +27,7 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
 let update: Update | null = null
 
 const platform: Platform = {
-  platform: "tauri",
+  platform: "desktop",
   version: pkg.version,
 
   async openDirectoryPickerDialog(opts) {
@@ -198,7 +198,7 @@ render(() => {
   return (
     <PlatformProvider value={platform}>
       {ostype() === "macos" && (
-        <div class="bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
+        <div class="mx-px bg-background-base border-b border-border-weak-base h-8" data-tauri-drag-region />
       )}
       <App />
     </PlatformProvider>

+ 2 - 1
packages/enterprise/package.json

@@ -1,8 +1,9 @@
 {
   "name": "@opencode-ai/enterprise",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "private": true,
   "type": "module",
+  "license": "MIT",
   "scripts": {
     "typecheck": "tsgo --noEmit",
     "dev": "vite dev",

+ 1 - 1
packages/enterprise/src/routes/share/[shareID].tsx

@@ -328,7 +328,7 @@ export default function () {
                               <div class="flex gap-3 items-center">
                                 <IconButton
                                   as={"a"}
-                                  href="https://github.com/sst/opencode"
+                                  href="https://github.com/anomalyco/opencode"
                                   target="_blank"
                                   icon="github"
                                   variant="ghost"

+ 4 - 0
packages/enterprise/sst-env.d.ts

@@ -122,6 +122,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS7": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

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

@@ -1,36 +1,36 @@
 id = "opencode"
 name = "OpenCode"
 description = "The open source coding agent."
-version = "1.0.224"
+version = "1.1.2"
 schema_version = 1
 authors = ["Anomaly"]
-repository = "https://github.com/sst/opencode"
+repository = "https://github.com/anomalyco/opencode"
 
 [agent_servers.opencode]
 name = "OpenCode"
 icon = "./icons/opencode.svg"
 
 [agent_servers.opencode.targets.darwin-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.224/opencode-darwin-arm64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-darwin-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/opencode-darwin-x64.zip"
 cmd = "./opencode"
 args = ["acp"]
 
 [agent_servers.opencode.targets.linux-aarch64]
-archive = "https://github.com/sst/opencode/releases/download/v1.0.224/opencode-linux-arm64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-linux-x64.tar.gz"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/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.224/opencode-windows-x64.zip"
+archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.2/opencode-windows-x64.zip"
 cmd = "./opencode.exe"
 args = ["acp"]

+ 2 - 1
packages/function/package.json

@@ -1,9 +1,10 @@
 {
   "name": "@opencode-ai/function",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",
+  "license": "MIT",
   "devDependencies": {
     "@cloudflare/workers-types": "catalog:",
     "@tsconfig/node22": "22.0.2",

+ 4 - 0
packages/function/sst-env.d.ts

@@ -122,6 +122,10 @@ declare module "sst" {
       "type": "sst.sst.Secret"
       "value": string
     }
+    "ZEN_MODELS7": {
+      "type": "sst.sst.Secret"
+      "value": string
+    }
   }
 }
 // cloudflare 

+ 2 - 2
packages/opencode/AGENTS.md

@@ -3,7 +3,7 @@
 ## Build/Test Commands
 
 - **Install**: `bun install`
-- **Run**: `bun run index.ts`
+- **Run**: `bun run --conditions=browser ./src/index.ts`
 - **Typecheck**: `bun run typecheck` (npm run typecheck)
 - **Test**: `bun test` (runs all tests)
 - **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
@@ -24,4 +24,4 @@
 - **Validation**: All inputs validated with Zod schemas
 - **Logging**: Use `Log.create({ service: "name" })` pattern
 - **Storage**: Use `Storage` namespace for persistence
-- **API Client**: Go TUI communicates with TypeScript server via stainless SDK. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, ask the user to generate a new client SDK to proceed with client-side changes.
+- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.

+ 8 - 6
packages/opencode/package.json

@@ -1,8 +1,9 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "1.0.224",
+  "version": "1.1.2",
   "name": "opencode",
   "type": "module",
+  "license": "MIT",
   "private": true,
   "scripts": {
     "typecheck": "tsgo --noEmit",
@@ -28,6 +29,7 @@
     "@babel/preset-typescript": "7.27.1",
     "babel-preset-solid": "1.9.10",
     "@octokit/webhooks-types": "7.6.1",
+    "@opencode-ai/script": "workspace:*",
     "@parcel/watcher-darwin-arm64": "2.5.1",
     "@parcel/watcher-darwin-x64": "2.5.1",
     "@parcel/watcher-linux-arm64-glibc": "2.5.1",
@@ -41,12 +43,11 @@
     "@types/bun": "catalog:",
     "@types/turndown": "5.0.5",
     "@types/yargs": "17.0.33",
-    "typescript": "catalog:",
     "@typescript/native-preview": "catalog:",
+    "typescript": "catalog:",
     "vscode-languageserver-types": "3.17.5",
     "why-is-node-running": "3.2.2",
-    "zod-to-json-schema": "3.24.5",
-    "@opencode-ai/script": "workspace:*"
+    "zod-to-json-schema": "3.24.5"
   },
   "dependencies": {
     "@actions/core": "1.11.1",
@@ -84,11 +85,12 @@
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/util": "workspace:*",
     "@openrouter/ai-sdk-provider": "1.5.2",
-    "@opentui/core": "0.1.67",
-    "@opentui/solid": "0.1.67",
+    "@opentui/core": "0.1.68",
+    "@opentui/solid": "0.1.68",
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",
+    "@solid-primitives/scheduled": "1.5.2",
     "@standard-schema/spec": "1.0.0",
     "@zip.js/zip.js": "2.7.62",
     "ai": "catalog:",

+ 10 - 10
packages/opencode/script/publish-registries.ts

@@ -22,17 +22,17 @@ if (!Script.preview) {
     "options=('!debug' '!strip')",
     "pkgrel=1",
     "pkgdesc='The AI coding agent built for the terminal.'",
-    "url='https://github.com/sst/opencode'",
+    "url='https://github.com/anomalyco/opencode'",
     "arch=('aarch64' 'x86_64')",
     "license=('MIT')",
     "provides=('opencode')",
     "conflicts=('opencode')",
     "depends=('ripgrep')",
     "",
-    `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
+    `source_aarch64=("\${pkgname}_\${pkgver}_aarch64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-arm64.tar.gz")`,
     `sha256sums_aarch64=('${arm64Sha}')`,
 
-    `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/sst/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
+    `source_x86_64=("\${pkgname}_\${pkgver}_x86_64.tar.gz::https://github.com/anomalyco/opencode/releases/download/v\${pkgver}\${_subver}/opencode-linux-x64.tar.gz")`,
     `sha256sums_x86_64=('${x64Sha}')`,
     "",
     "package() {",
@@ -52,7 +52,7 @@ if (!Script.preview) {
     "options=('!debug' '!strip')",
     "pkgrel=1",
     "pkgdesc='The AI coding agent built for the terminal.'",
-    "url='https://github.com/sst/opencode'",
+    "url='https://github.com/anomalyco/opencode'",
     "arch=('aarch64' 'x86_64')",
     "license=('MIT')",
     "provides=('opencode')",
@@ -60,7 +60,7 @@ if (!Script.preview) {
     "depends=('ripgrep')",
     "makedepends=('git' 'bun' 'go')",
     "",
-    `source=("opencode-\${pkgver}.tar.gz::https://github.com/sst/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
+    `source=("opencode-\${pkgver}.tar.gz::https://github.com/anomalyco/opencode/archive/v\${pkgver}\${_subver}.tar.gz")`,
     `sha256sums=('SKIP')`,
     "",
     "build() {",
@@ -133,14 +133,14 @@ if (!Script.preview) {
     "# This file was generated by GoReleaser. DO NOT EDIT.",
     "class Opencode < Formula",
     `  desc "The AI coding agent built for the terminal."`,
-    `  homepage "https://github.com/sst/opencode"`,
+    `  homepage "https://github.com/anomalyco/opencode"`,
     `  version "${Script.version.split("-")[0]}"`,
     "",
     `  depends_on "ripgrep"`,
     "",
     "  on_macos do",
     "    if Hardware::CPU.intel?",
-    `      url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
+    `      url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-x64.zip"`,
     `      sha256 "${macX64Sha}"`,
     "",
     "      def install",
@@ -148,7 +148,7 @@ if (!Script.preview) {
     "      end",
     "    end",
     "    if Hardware::CPU.arm?",
-    `      url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
+    `      url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-darwin-arm64.zip"`,
     `      sha256 "${macArm64Sha}"`,
     "",
     "      def install",
@@ -159,14 +159,14 @@ if (!Script.preview) {
     "",
     "  on_linux do",
     "    if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
-    `      url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
+    `      url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-x64.tar.gz"`,
     `      sha256 "${x64Sha}"`,
     "      def install",
     '        bin.install "opencode"',
     "      end",
     "    end",
     "    if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
-    `      url "https://github.com/sst/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
+    `      url "https://github.com/anomalyco/opencode/releases/download/v${Script.version}/opencode-linux-arm64.tar.gz"`,
     `      sha256 "${arm64Sha}"`,
     "      def install",
     '        bin.install "opencode"',

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

@@ -62,7 +62,7 @@ if (!Script.preview) {
     }
   }
 
-  const image = "ghcr.io/sst/opencode"
+  const image = "ghcr.io/anomalyco/opencode"
   const platforms = "linux/amd64,linux/arm64"
   const tags = [`${image}:${Script.version}`, `${image}:latest`]
   const tagFlags = tags.flatMap((t) => ["-t", t])

+ 46 - 8
packages/opencode/src/acp/agent.ts

@@ -34,13 +34,9 @@ import type { OpencodeClient, SessionMessageResponse } from "@opencode-ai/sdk/v2
 export namespace ACP {
   const log = Log.create({ service: "acp-agent" })
 
-  export async function init({ sdk }: { sdk: OpencodeClient }) {
-    const model = await defaultModel({ sdk })
+  export async function init({ sdk: _sdk }: { sdk: OpencodeClient }) {
     return {
       create: (connection: AgentSideConnection, fullConfig: ACPConfig) => {
-        if (!fullConfig.defaultModel) {
-          fullConfig.defaultModel = model
-        }
         return new Agent(connection, fullConfig)
       },
     }
@@ -988,8 +984,10 @@ export namespace ACP {
     const configured = config.defaultModel
     if (configured) return configured
 
-    const model = await sdk.config
-      .get({ directory: cwd }, { throwOnError: true })
+    const directory = cwd ?? process.cwd()
+
+    const specified = await sdk.config
+      .get({ directory }, { throwOnError: true })
       .then((resp) => {
         const cfg = resp.data
         if (!cfg || !cfg.model) return undefined
@@ -1004,7 +1002,47 @@ export namespace ACP {
         return undefined
       })
 
-    return model ?? { providerID: "opencode", modelID: "big-pickle" }
+    const providers = await sdk.config
+      .providers({ directory }, { throwOnError: true })
+      .then((x) => x.data?.providers ?? [])
+      .catch((error) => {
+        log.error("failed to list providers for default model", { error })
+        return []
+      })
+
+    if (specified && providers.length) {
+      const provider = providers.find((p) => p.id === specified.providerID)
+      if (provider && provider.models[specified.modelID]) return specified
+    }
+
+    if (specified && !providers.length) return specified
+
+    const opencodeProvider = providers.find((p) => p.id === "opencode")
+    if (opencodeProvider) {
+      if (opencodeProvider.models["big-pickle"]) {
+        return { providerID: "opencode", modelID: "big-pickle" }
+      }
+      const [best] = Provider.sort(Object.values(opencodeProvider.models))
+      if (best) {
+        return {
+          providerID: best.providerID,
+          modelID: best.id,
+        }
+      }
+    }
+
+    const models = providers.flatMap((p) => Object.values(p.models))
+    const [best] = Provider.sort(models)
+    if (best) {
+      return {
+        providerID: best.providerID,
+        modelID: best.id,
+      }
+    }
+
+    if (specified) return specified
+
+    return { providerID: "opencode", modelID: "big-pickle" }
   }
 
   function parseUri(

+ 7 - 0
packages/opencode/src/agent/agent.ts

@@ -47,6 +47,13 @@ export namespace Agent {
       "*": "allow",
       doom_loop: "ask",
       external_directory: "ask",
+      // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
+      read: {
+        "*": "allow",
+        "*.env": "deny",
+        "*.env.*": "deny",
+        "*.env.example": "allow",
+      },
     })
     const user = PermissionNext.fromConfig(cfg.permission ?? {})
 

+ 1 - 1
packages/opencode/src/cli/cmd/auth.ts

@@ -36,7 +36,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
   const method = plugin.auth.methods[index]
 
   // Handle prompts for all auth types
-  await new Promise((resolve) => setTimeout(resolve, 10))
+  await Bun.sleep(10)
   const inputs: Record<string, string> = {}
   if (method.prompts) {
     for (const prompt of method.prompts) {

+ 55 - 13
packages/opencode/src/cli/cmd/github.ts

@@ -348,7 +348,7 @@ export const GithubInstallCommand = cmd({
               }
 
               retries++
-              await new Promise((resolve) => setTimeout(resolve, 1000))
+              await Bun.sleep(1000)
             } while (true)
 
             s.stop("Installed GitHub app")
@@ -396,7 +396,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Run opencode
-        uses: sst/opencode/github@latest${envStr}
+        uses: anomalyco/opencode/github@latest${envStr}
         with:
           model: ${provider}/${model}`,
             )
@@ -994,12 +994,16 @@ export const GithubRunCommand = cmd({
 
         console.log("Configuring git...")
         const config = "http.https://github.com/.extraheader"
-        const ret = await $`git config --local --get ${config}`
-        gitConfig = ret.stdout.toString().trim()
+        // actions/checkout@v6 no longer stores credentials in .git/config,
+        // so this may not exist - use nothrow() to handle gracefully
+        const ret = await $`git config --local --get ${config}`.nothrow()
+        if (ret.exitCode === 0) {
+          gitConfig = ret.stdout.toString().trim()
+          await $`git config --local --unset-all ${config}`
+        }
 
         const newCredentials = Buffer.from(`x-access-token:${appToken}`, "utf8").toString("base64")
 
-        await $`git config --local --unset-all ${config}`
         await $`git config --local ${config} "AUTHORIZATION: basic ${newCredentials}"`
         await $`git config --global user.name "${AGENT_USERNAME}"`
         await $`git config --global user.email "${AGENT_USERNAME}@users.noreply.github.com"`
@@ -1233,17 +1237,55 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
 
       async function createPR(base: string, branch: string, title: string, body: string) {
         console.log("Creating pull request...")
-        const pr = await octoRest.rest.pulls.create({
-          owner,
-          repo,
-          head: branch,
-          base,
-          title,
-          body,
-        })
+
+        // Check if an open PR already exists for this head→base combination
+        // This handles the case where the agent created a PR via gh pr create during its run
+        try {
+          const existing = await withRetry(() =>
+            octoRest.rest.pulls.list({
+              owner,
+              repo,
+              head: `${owner}:${branch}`,
+              base,
+              state: "open",
+            }),
+          )
+
+          if (existing.data.length > 0) {
+            console.log(`PR #${existing.data[0].number} already exists for branch ${branch}`)
+            return existing.data[0].number
+          }
+        } catch (e) {
+          // If the check fails, proceed to create - we'll get a clear error if a PR already exists
+          console.log(`Failed to check for existing PR: ${e}`)
+        }
+
+        const pr = await withRetry(() =>
+          octoRest.rest.pulls.create({
+            owner,
+            repo,
+            head: branch,
+            base,
+            title,
+            body,
+          }),
+        )
         return pr.data.number
       }
 
+      async function withRetry<T>(fn: () => Promise<T>, retries = 1, delayMs = 5000): Promise<T> {
+        try {
+          return await fn()
+        } catch (e) {
+          if (retries > 0) {
+            console.log(`Retrying after ${delayMs}ms...`)
+            await Bun.sleep(delayMs)
+            return withRetry(fn, retries - 1, delayMs)
+          }
+          throw e
+        }
+      }
+
       function footer(opts?: { image?: boolean }) {
         const image = (() => {
           if (!shareId) return ""

+ 27 - 11
packages/opencode/src/cli/cmd/mcp.ts

@@ -36,6 +36,18 @@ function getAuthStatusText(status: MCP.AuthStatus): string {
   }
 }
 
+type McpEntry = NonNullable<Config.Info["mcp"]>[string]
+
+type McpConfigured = Config.Mcp
+function isMcpConfigured(config: McpEntry): config is McpConfigured {
+  return typeof config === "object" && config !== null && "type" in config
+}
+
+type McpRemote = Extract<McpConfigured, { type: "remote" }>
+function isMcpRemote(config: McpEntry): config is McpRemote {
+  return isMcpConfigured(config) && config.type === "remote"
+}
+
 export const McpCommand = cmd({
   command: "mcp",
   builder: (yargs) =>
@@ -64,15 +76,19 @@ export const McpListCommand = cmd({
         const mcpServers = config.mcp ?? {}
         const statuses = await MCP.status()
 
-        if (Object.keys(mcpServers).length === 0) {
+        const servers = Object.entries(mcpServers).filter((entry): entry is [string, McpConfigured] =>
+          isMcpConfigured(entry[1]),
+        )
+
+        if (servers.length === 0) {
           prompts.log.warn("No MCP servers configured")
           prompts.outro("Add servers with: opencode mcp add")
           return
         }
 
-        for (const [name, serverConfig] of Object.entries(mcpServers)) {
+        for (const [name, serverConfig] of servers) {
           const status = statuses[name]
-          const hasOAuth = serverConfig.type === "remote" && !!serverConfig.oauth
+          const hasOAuth = isMcpRemote(serverConfig) && !!serverConfig.oauth
           const hasStoredTokens = await MCP.hasStoredTokens(name)
 
           let statusIcon: string
@@ -110,7 +126,7 @@ export const McpListCommand = cmd({
           )
         }
 
-        prompts.outro(`${Object.keys(mcpServers).length} server(s)`)
+        prompts.outro(`${servers.length} server(s)`)
       },
     })
   },
@@ -138,7 +154,7 @@ export const McpAuthCommand = cmd({
 
         // Get OAuth-capable servers (remote servers with oauth not explicitly disabled)
         const oauthServers = Object.entries(mcpServers).filter(
-          ([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
+          (entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
         )
 
         if (oauthServers.length === 0) {
@@ -163,7 +179,7 @@ export const McpAuthCommand = cmd({
               const authStatus = await MCP.getAuthStatus(name)
               const icon = getAuthStatusIcon(authStatus)
               const statusText = getAuthStatusText(authStatus)
-              const url = cfg.type === "remote" ? cfg.url : ""
+              const url = cfg.url
               return {
                 label: `${icon} ${name} (${statusText})`,
                 value: name,
@@ -187,8 +203,8 @@ export const McpAuthCommand = cmd({
           return
         }
 
-        if (serverConfig.type !== "remote" || serverConfig.oauth === false) {
-          prompts.log.error(`MCP server ${serverName} does not support OAuth (oauth is disabled)`)
+        if (!isMcpRemote(serverConfig) || serverConfig.oauth === false) {
+          prompts.log.error(`MCP server ${serverName} is not an OAuth-capable remote server`)
           prompts.outro("Done")
           return
         }
@@ -263,7 +279,7 @@ export const McpAuthListCommand = cmd({
 
         // Get OAuth-capable servers
         const oauthServers = Object.entries(mcpServers).filter(
-          ([_, cfg]) => cfg.type === "remote" && cfg.oauth !== false,
+          (entry): entry is [string, McpRemote] => isMcpRemote(entry[1]) && entry[1].oauth !== false,
         )
 
         if (oauthServers.length === 0) {
@@ -276,7 +292,7 @@ export const McpAuthListCommand = cmd({
           const authStatus = await MCP.getAuthStatus(name)
           const icon = getAuthStatusIcon(authStatus)
           const statusText = getAuthStatusText(authStatus)
-          const url = serverConfig.type === "remote" ? serverConfig.url : ""
+          const url = serverConfig.url
 
           prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${statusText}\n    ${UI.Style.TEXT_DIM}${url}`)
         }
@@ -506,7 +522,7 @@ export const McpDebugCommand = cmd({
           return
         }
 
-        if (serverConfig.type !== "remote") {
+        if (!isMcpRemote(serverConfig)) {
           prompts.log.error(`MCP server ${serverName} is not a remote server`)
           prompts.outro("Done")
           return

+ 6 - 0
packages/opencode/src/cli/cmd/run.ts

@@ -87,6 +87,10 @@ export const RunCommand = cmd({
         type: "number",
         describe: "port for the local server (defaults to random port if no value provided)",
       })
+      .option("variant", {
+        type: "string",
+        describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
+      })
   },
   handler: async (args) => {
     let message = [...args.message, ...(args["--"] || [])]
@@ -254,6 +258,7 @@ export const RunCommand = cmd({
           model: args.model,
           command: args.command,
           arguments: message,
+          variant: args.variant,
         })
       } else {
         const modelParam = args.model ? Provider.parseModel(args.model) : undefined
@@ -261,6 +266,7 @@ export const RunCommand = cmd({
           sessionID,
           agent: resolvedAgent,
           model: modelParam,
+          variant: args.variant,
           parts: [...fileParts, { type: "text", text: message }],
         })
       }

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

@@ -96,7 +96,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
   })
 }
 
-export function tui(input: { url: string; args: Args; onExit?: () => Promise<void> }) {
+export function tui(input: { url: string; args: Args; directory?: string; onExit?: () => Promise<void> }) {
   // promise to prevent immediate exit
   return new Promise<void>(async (resolve) => {
     const mode = await getTerminalBackgroundColor()
@@ -116,7 +116,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
                 <KVProvider>
                   <ToastProvider>
                     <RouteProvider>
-                      <SDKProvider url={input.url}>
+                      <SDKProvider url={input.url} directory={input.directory}>
                         <SyncProvider>
                           <ThemeProvider mode={mode}>
                             <LocalProvider>
@@ -412,6 +412,7 @@ function App() {
     {
       title: "Switch theme",
       value: "theme.switch",
+      keybind: "theme_list",
       onSelect: () => {
         dialog.replace(() => <DialogThemeList />)
       },
@@ -549,6 +550,13 @@ function App() {
     })
   })
 
+  sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
+    route.navigate({
+      type: "session",
+      sessionID: evt.properties.sessionID,
+    })
+  })
+
   sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
     if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
       route.navigate({ type: "home" })
@@ -648,7 +656,7 @@ function ErrorComponent(props: {
   })
   const [copied, setCopied] = createSignal(false)
 
-  const issueURL = new URL("https://github.com/sst/opencode/issues/new?template=bug-report.yml")
+  const issueURL = new URL("https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml")
 
   // Choose safe fallback colors per mode since theme context may not be available
   const isLight = props.mode === "light"

+ 2 - 0
packages/opencode/src/cli/cmd/tui/attach.ts

@@ -22,9 +22,11 @@ export const AttachCommand = cmd({
       }),
   handler: async (args) => {
     if (args.dir) process.chdir(args.dir)
+    const directory = process.cwd()
     await tui({
       url: args.url,
       args: { sessionID: args.session },
+      directory,
     })
   },
 })

+ 0 - 1
packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

@@ -17,7 +17,6 @@ const PROVIDER_PRIORITY: Record<string, number> = {
   "github-copilot": 2,
   openai: 3,
   google: 4,
-  openrouter: 5,
 }
 
 export function createDialogProviderOptions() {

+ 14 - 7
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -2,13 +2,14 @@ import { useDialog } from "@tui/ui/dialog"
 import { DialogSelect } from "@tui/ui/dialog-select"
 import { useRoute } from "@tui/context/route"
 import { useSync } from "@tui/context/sync"
-import { createEffect, createMemo, createSignal, onMount, Show } from "solid-js"
+import { createMemo, createSignal, createResource, onMount, Show } from "solid-js"
 import { Locale } from "@/util/locale"
 import { Keybind } from "@/util/keybind"
 import { useTheme } from "../context/theme"
 import { useSDK } from "../context/sdk"
 import { DialogSessionRename } from "./dialog-session-rename"
 import { useKV } from "../context/kv"
+import { createDebouncedSignal } from "../util/signal"
 import "opentui-spinner/solid"
 
 export function DialogSessionList() {
@@ -20,6 +21,13 @@ export function DialogSessionList() {
   const kv = useKV()
 
   const [toDelete, setToDelete] = createSignal<string>()
+  const [search, setSearch] = createDebouncedSignal("", 150)
+
+  const [searchResults] = createResource(search, async (query) => {
+    if (!query) return undefined
+    const result = await sdk.client.session.list({ search: query, limit: 30 })
+    return result.data ?? []
+  })
 
   const deleteKeybind = "ctrl+d"
 
@@ -27,9 +35,11 @@ export function DialogSessionList() {
 
   const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
 
+  const sessions = createMemo(() => searchResults() ?? sync.data.session)
+
   const options = createMemo(() => {
     const today = new Date().toDateString()
-    return sync.data.session
+    return sessions()
       .filter((x) => x.parentID === undefined)
       .toSorted((a, b) => b.time.updated - a.time.updated)
       .map((x) => {
@@ -54,11 +64,6 @@ export function DialogSessionList() {
           ) : undefined,
         }
       })
-      .slice(0, 150)
-  })
-
-  createEffect(() => {
-    console.log("session count", sync.data.session.length)
   })
 
   onMount(() => {
@@ -69,7 +74,9 @@ export function DialogSessionList() {
     <DialogSelect
       title="Sessions"
       options={options()}
+      skipFilter={true}
       current={currentSessionID()}
+      onFilter={setSearch}
       onMove={() => {
         setToDelete(undefined)
       }}

+ 35 - 1
packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

@@ -231,6 +231,40 @@ export function Autocomplete(props: {
     },
   )
 
+  const mcpResources = createMemo(() => {
+    if (!store.visible || store.visible === "/") return []
+
+    const options: AutocompleteOption[] = []
+    const width = props.anchor().width - 4
+
+    for (const res of Object.values(sync.data.mcp_resource)) {
+      options.push({
+        display: Locale.truncateMiddle(`${res.name} (${res.uri})`, width),
+        description: res.description,
+        onSelect: () => {
+          insertPart(res.name, {
+            type: "file",
+            mime: res.mimeType ?? "text/plain",
+            filename: res.name,
+            url: res.uri,
+            source: {
+              type: "resource",
+              text: {
+                start: 0,
+                end: 0,
+                value: "",
+              },
+              clientName: res.client,
+              uri: res.uri,
+            },
+          })
+        },
+      })
+    }
+
+    return options
+  })
+
   const agents = createMemo(() => {
     const agents = sync.data.agent
     return agents
@@ -416,7 +450,7 @@ export function Autocomplete(props: {
     const commandsValue = commands()
 
     const mixed: AutocompleteOption[] = (
-      store.visible === "@" ? [...agentsValue, ...(filesValue || [])] : [...commandsValue]
+      store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
     ).filter((x) => x.disabled !== true)
 
     const currentFilter = filter()

+ 4 - 67
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -1,4 +1,4 @@
-import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg, type KeyBinding } from "@opentui/core"
+import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core"
 import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, Show, Switch, Match } from "solid-js"
 import "opentui-spinner/solid"
 import { useLocal } from "@tui/context/local"
@@ -10,7 +10,6 @@ import { useSync } from "@tui/context/sync"
 import { Identifier } from "@/id/id"
 import { createStore, produce } from "solid-js/store"
 import { useKeybind } from "@tui/context/keybind"
-import { Keybind } from "@/util/keybind"
 import { usePromptHistory, type PromptInfo } from "./history"
 import { usePromptStash } from "./stash"
 import { DialogStash } from "../dialog-stash"
@@ -30,6 +29,7 @@ import { DialogProvider as DialogProviderConnect } from "../dialog-provider"
 import { DialogAlert } from "../../ui/dialog-alert"
 import { useToast } from "../../ui/toast"
 import { useKV } from "../../context/kv"
+import { useTextareaKeybindings } from "../textarea-keybindings"
 
 export type PromptProps = {
   sessionID?: string
@@ -53,61 +53,6 @@ export type PromptRef = {
 
 const PLACEHOLDERS = ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"]
 
-const TEXTAREA_ACTIONS = [
-  "submit",
-  "newline",
-  "move-left",
-  "move-right",
-  "move-up",
-  "move-down",
-  "select-left",
-  "select-right",
-  "select-up",
-  "select-down",
-  "line-home",
-  "line-end",
-  "select-line-home",
-  "select-line-end",
-  "visual-line-home",
-  "visual-line-end",
-  "select-visual-line-home",
-  "select-visual-line-end",
-  "buffer-home",
-  "buffer-end",
-  "select-buffer-home",
-  "select-buffer-end",
-  "delete-line",
-  "delete-to-line-end",
-  "delete-to-line-start",
-  "backspace",
-  "delete",
-  "undo",
-  "redo",
-  "word-forward",
-  "word-backward",
-  "select-word-forward",
-  "select-word-backward",
-  "delete-word-forward",
-  "delete-word-backward",
-] as const
-
-function mapTextareaKeybindings(
-  keybinds: Record<string, Keybind.Info[]>,
-  action: (typeof TEXTAREA_ACTIONS)[number],
-): KeyBinding[] {
-  const configKey = `input_${action.replace(/-/g, "_")}`
-  const bindings = keybinds[configKey]
-  if (!bindings) return []
-  return bindings.map((binding) => ({
-    name: binding.name,
-    ctrl: binding.ctrl || undefined,
-    meta: binding.meta || undefined,
-    shift: binding.shift || undefined,
-    super: binding.super || undefined,
-    action,
-  }))
-}
-
 export function Prompt(props: PromptProps) {
   let input: TextareaRenderable
   let anchor: BoxRenderable
@@ -139,15 +84,7 @@ export function Prompt(props: PromptProps) {
     }
   }
 
-  const textareaKeybindings = createMemo(() => {
-    const keybinds = keybind.all
-
-    return [
-      { name: "return", action: "submit" },
-      { name: "return", meta: true, action: "newline" },
-      ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
-    ] satisfies KeyBinding[]
-  })
+  const textareaKeybindings = useTextareaKeybindings()
 
   const fileStyleId = syntax().getStyleId("extmark.file")!
   const agentStyleId = syntax().getStyleId("extmark.agent")!
@@ -812,7 +749,7 @@ export function Prompt(props: PromptProps) {
         >
           <box
             paddingLeft={2}
-            paddingRight={1}
+            paddingRight={2}
             paddingTop={1}
             flexShrink={0}
             backgroundColor={theme.backgroundElement}

+ 73 - 0
packages/opencode/src/cli/cmd/tui/component/textarea-keybindings.ts

@@ -0,0 +1,73 @@
+import { createMemo } from "solid-js"
+import type { KeyBinding } from "@opentui/core"
+import { useKeybind } from "../context/keybind"
+import { Keybind } from "@/util/keybind"
+
+const TEXTAREA_ACTIONS = [
+  "submit",
+  "newline",
+  "move-left",
+  "move-right",
+  "move-up",
+  "move-down",
+  "select-left",
+  "select-right",
+  "select-up",
+  "select-down",
+  "line-home",
+  "line-end",
+  "select-line-home",
+  "select-line-end",
+  "visual-line-home",
+  "visual-line-end",
+  "select-visual-line-home",
+  "select-visual-line-end",
+  "buffer-home",
+  "buffer-end",
+  "select-buffer-home",
+  "select-buffer-end",
+  "delete-line",
+  "delete-to-line-end",
+  "delete-to-line-start",
+  "backspace",
+  "delete",
+  "undo",
+  "redo",
+  "word-forward",
+  "word-backward",
+  "select-word-forward",
+  "select-word-backward",
+  "delete-word-forward",
+  "delete-word-backward",
+] as const
+
+function mapTextareaKeybindings(
+  keybinds: Record<string, Keybind.Info[]>,
+  action: (typeof TEXTAREA_ACTIONS)[number],
+): KeyBinding[] {
+  const configKey = `input_${action.replace(/-/g, "_")}`
+  const bindings = keybinds[configKey]
+  if (!bindings) return []
+  return bindings.map((binding) => ({
+    name: binding.name,
+    ctrl: binding.ctrl || undefined,
+    meta: binding.meta || undefined,
+    shift: binding.shift || undefined,
+    super: binding.super || undefined,
+    action,
+  }))
+}
+
+export function useTextareaKeybindings() {
+  const keybind = useKeybind()
+
+  return createMemo(() => {
+    const keybinds = keybind.all
+
+    return [
+      { name: "return", action: "submit" },
+      { name: "return", meta: true, action: "newline" },
+      ...TEXTAREA_ACTIONS.flatMap((action) => mapTextareaKeybindings(keybinds, action)),
+    ] satisfies KeyBinding[]
+  })
+}

+ 1 - 1
packages/opencode/src/cli/cmd/tui/component/tips.ts

@@ -92,7 +92,7 @@ export const TIPS = [
   "Press {highlight}Ctrl+X S{/highlight} or {highlight}/status{/highlight} to see system status info.",
   "Enable {highlight}tui.scroll_acceleration{/highlight} for smooth macOS-style scrolling.",
   "Toggle username display in chat via command palette ({highlight}Ctrl+P{/highlight}).",
-  "Run {highlight}docker run -it --rm ghcr.io/sst/opencode{/highlight} for containerized use.",
+  "Run {highlight}docker run -it --rm ghcr.io/anomalyco/opencode{/highlight} for containerized use.",
   "Use {highlight}/connect{/highlight} with OpenCode Zen for curated, tested models.",
   "Commit your project's {highlight}AGENTS.md{/highlight} file to Git for team sharing.",
   "Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs.",

+ 2 - 1
packages/opencode/src/cli/cmd/tui/context/sdk.tsx

@@ -5,11 +5,12 @@ import { batch, onCleanup, onMount } from "solid-js"
 
 export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
   name: "SDK",
-  init: (props: { url: string }) => {
+  init: (props: { url: string; directory?: string }) => {
     const abort = new AbortController()
     const sdk = createOpencodeClient({
       baseUrl: props.url,
       signal: abort.signal,
+      directory: props.directory,
     })
 
     const emitter = createGlobalEmitter<{

+ 8 - 1
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -10,6 +10,7 @@ import type {
   PermissionRequest,
   LspStatus,
   McpStatus,
+  McpResource,
   FormatterStatus,
   SessionStatus,
   ProviderListResponse,
@@ -62,6 +63,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       mcp: {
         [key: string]: McpStatus
       }
+      mcp_resource: {
+        [key: string]: McpResource
+      }
       formatter: FormatterStatus[]
       vcs: VcsInfo | undefined
       path: Path
@@ -87,6 +91,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       part: {},
       lsp: [],
       mcp: {},
+      mcp_resource: {},
       formatter: [],
       vcs: undefined,
       path: { state: "", config: "", worktree: "", directory: "" },
@@ -264,8 +269,9 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
 
     async function bootstrap() {
       console.log("bootstrapping")
+      const start = Date.now() - 30 * 24 * 60 * 60 * 1000
       const sessionListPromise = sdk.client.session
-        .list()
+        .list({ start: start })
         .then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))))
 
       // blocking - include session.list when continuing a session
@@ -295,6 +301,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
             sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
             sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
+            sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
             sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))),
             sdk.client.session.status().then((x) => {
               setStore("session_status", reconcile(x.data!))

+ 37 - 15
packages/opencode/src/cli/cmd/tui/context/theme.tsx

@@ -22,6 +22,7 @@ import mercury from "./theme/mercury.json" with { type: "json" }
 import monokai from "./theme/monokai.json" with { type: "json" }
 import nightowl from "./theme/nightowl.json" with { type: "json" }
 import nord from "./theme/nord.json" with { type: "json" }
+import osakaJade from "./theme/osaka-jade.json" with { type: "json" }
 import onedark from "./theme/one-dark.json" with { type: "json" }
 import opencode from "./theme/opencode.json" with { type: "json" }
 import orng from "./theme/orng.json" with { type: "json" }
@@ -155,6 +156,7 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
   nightowl,
   nord,
   ["one-dark"]: onedark,
+  ["osaka-jade"]: osakaJade,
   opencode,
   orng,
   ["lucent-orng"]: lucentOrng,
@@ -405,25 +407,45 @@ async function getCustomThemes() {
 function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
   const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
   const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
-  const palette = colors.palette.filter((x) => x !== null).map((x) => RGBA.fromHex(x))
   const isDark = mode == "dark"
 
+  const col = (i: number) => {
+    const value = colors.palette[i]
+    if (value) return RGBA.fromHex(value)
+    return ansiToRgba(i)
+  }
+
+  const tint = (base: RGBA, overlay: RGBA, alpha: number) => {
+    const r = base.r + (overlay.r - base.r) * alpha
+    const g = base.g + (overlay.g - base.g) * alpha
+    const b = base.b + (overlay.b - base.b) * alpha
+    return RGBA.fromInts(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255))
+  }
+
   // Generate gray scale based on terminal background
   const grays = generateGrayScale(bg, isDark)
   const textMuted = generateMutedTextColor(bg, isDark)
 
   // ANSI color references
   const ansiColors = {
-    black: palette[0],
-    red: palette[1],
-    green: palette[2],
-    yellow: palette[3],
-    blue: palette[4],
-    magenta: palette[5],
-    cyan: palette[6],
-    white: palette[7],
+    black: col(0),
+    red: col(1),
+    green: col(2),
+    yellow: col(3),
+    blue: col(4),
+    magenta: col(5),
+    cyan: col(6),
+    white: col(7),
+    redBright: col(9),
+    greenBright: col(10),
   }
 
+  const diffAlpha = isDark ? 0.22 : 0.14
+  const diffAddedBg = tint(bg, ansiColors.green, diffAlpha)
+  const diffRemovedBg = tint(bg, ansiColors.red, diffAlpha)
+  const diffAddedLineNumberBg = tint(grays[3], ansiColors.green, diffAlpha)
+  const diffRemovedLineNumberBg = tint(grays[3], ansiColors.red, diffAlpha)
+
   return {
     theme: {
       // Primary colors using ANSI
@@ -458,14 +480,14 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs
       diffRemoved: ansiColors.red,
       diffContext: grays[7],
       diffHunkHeader: grays[7],
-      diffHighlightAdded: ansiColors.green,
-      diffHighlightRemoved: ansiColors.red,
-      diffAddedBg: grays[2],
-      diffRemovedBg: grays[2],
+      diffHighlightAdded: ansiColors.greenBright,
+      diffHighlightRemoved: ansiColors.redBright,
+      diffAddedBg,
+      diffRemovedBg,
       diffContextBg: grays[1],
       diffLineNumber: grays[6],
-      diffAddedLineNumberBg: grays[3],
-      diffRemovedLineNumberBg: grays[3],
+      diffAddedLineNumberBg,
+      diffRemovedLineNumberBg,
 
       // Markdown colors
       markdownText: fg,

Некоторые файлы не были показаны из-за большого количества измененных файлов