diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 77d7bb30e..993682142 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -80,8 +80,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_PROVENANCE: false
outputs:
- releaseId: ${{ steps.publish.outputs.releaseId }}
- tagName: ${{ steps.publish.outputs.tagName }}
+ release: ${{ steps.publish.outputs.release }}
+ tag: ${{ steps.publish.outputs.tag }}
+ version: ${{ steps.publish.outputs.version }}
publish-tauri:
needs: publish
@@ -103,7 +104,7 @@ jobs:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- ref: ${{ needs.publish.outputs.tagName }}
+ ref: ${{ needs.publish.outputs.tag }}
- uses: apple-actions/import-codesign-certs@v2
if: ${{ runner.os == 'macOS' }}
@@ -146,20 +147,18 @@ jobs:
shared-key: ${{ matrix.settings.target }}
- name: Prepare
+ if: inputs.bump || inputs.version
run: |
cd packages/tauri
bun ./scripts/prepare.ts
env:
- OPENCODE_BUMP: ${{ inputs.bump }}
- OPENCODE_VERSION: ${{ inputs.version }}
- OPENCODE_CHANNEL: latest
+ OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
- OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- name: Install tauri-cli from portable appimage branch
@@ -189,8 +188,8 @@ jobs:
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
updaterJsonPreferNsis: true
- releaseId: ${{ needs.publish.outputs.releaseId }}
- tagName: ${{ needs.publish.outputs.tagName }}
+ releaseId: ${{ needs.publish.outputs.release }}
+ tagName: ${{ needs.publish.outputs.tag }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
@@ -198,13 +197,13 @@ jobs:
needs:
- publish
- publish-tauri
- if: needs.publish.outputs.tagName
+ if: needs.publish.outputs.tag
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- ref: ${{ needs.publish.outputs.tagName }}
+ ref: ${{ needs.publish.outputs.tag }}
- uses: ./.github/actions/setup-bun
@@ -221,8 +220,6 @@ jobs:
- run: ./script/publish-complete.ts
env:
- OPENCODE_BUMP: ${{ inputs.bump }}
- OPENCODE_VERSION: ${{ inputs.version }}
+ OPENCODE_VERSION: ${{ needs.publish.outputs.version }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
- OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml
index 36f6df54f..c0e3a5deb 100644
--- a/.github/workflows/review.yml
+++ b/.github/workflows/review.yml
@@ -67,6 +67,8 @@ jobs:
When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simpliest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts)
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
+ If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors.
+ Generally, write a comment instead of writing suggested change if you can help it.
Command MUST be like this.
\`\`\`
diff --git a/README.zh-TW.md b/README.zh-TW.md
new file mode 100644
index 000000000..d3cbb263c
--- /dev/null
+++ b/README.zh-TW.md
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+開源的 AI Coding Agent。
+
+
+
+
+
+
+[](https://opencode.ai)
+
+---
+
+### 安裝
+
+```bash
+# 直接安裝 (YOLO)
+curl -fsSL https://opencode.ai/install | bash
+
+# 套件管理員
+npm i -g opencode-ai@latest # 也可使用 bun/pnpm/yarn
+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 以取得最新開發分支
+```
+
+> [!TIP]
+> 安裝前請先移除 0.1.x 以前的舊版本。
+
+### 桌面應用程式 (BETA)
+
+OpenCode 也提供桌面版應用程式。您可以直接從 [發佈頁面 (releases page)](https://github.com/sst/opencode/releases) 或 [opencode.ai/download](https://opencode.ai/download) 下載。
+
+| 平台 | 下載連結 |
+| --------------------- | ------------------------------------- |
+| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
+| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
+| Windows | `opencode-desktop-windows-x64.exe` |
+| Linux | `.deb`, `.rpm`, 或 AppImage |
+
+```bash
+# macOS (Homebrew Cask)
+brew install --cask opencode-desktop
+```
+
+#### 安裝目錄
+
+安裝腳本會依據以下優先順序決定安裝路徑:
+
+1. `$OPENCODE_INSTALL_DIR` - 自定義安裝目錄
+2. `$XDG_BIN_DIR` - 符合 XDG 基礎目錄規範的路徑
+3. `$HOME/bin` - 標準使用者執行檔目錄 (若存在或可建立)
+4. `$HOME/.opencode/bin` - 預設備用路徑
+
+```bash
+# 範例
+OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
+XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
+```
+
+### Agents
+
+OpenCode 內建了兩種 Agent,您可以使用 `Tab` 鍵快速切換。
+
+- **build** - 預設模式,具備完整權限的 Agent,適用於開發工作。
+- **plan** - 唯讀模式,適用於程式碼分析與探索。
+ - 預設禁止修改檔案。
+ - 執行 bash 指令前會詢問權限。
+ - 非常適合用來探索陌生的程式碼庫或規劃變更。
+
+此外,OpenCode 還包含一個 **general** 子 Agent,用於處理複雜搜尋與多步驟任務。此 Agent 供系統內部使用,亦可透過在訊息中輸入 `@general` 來呼叫。
+
+了解更多關於 [Agents](https://opencode.ai/docs/agents) 的資訊。
+
+### 線上文件
+
+關於如何設定 OpenCode 的詳細資訊,請參閱我們的 [**官方文件**](https://opencode.ai/docs)。
+
+### 參與貢獻
+
+如果您有興趣參與 OpenCode 的開發,請在提交 Pull Request 前先閱讀我們的 [貢獻指南 (Contributing Docs)](./CONTRIBUTING.md)。
+
+### 基於 OpenCode 進行開發
+
+如果您正在開發與 OpenCode 相關的專案,並在名稱中使用了 "opencode"(例如 "opencode-dashboard" 或 "opencode-mobile"),請在您的 README 中加入聲明,說明該專案並非由 OpenCode 團隊開發,且與我們沒有任何隸屬關係。
+
+### 常見問題 (FAQ)
+
+#### 這跟 Claude Code 有什麼不同?
+
+在功能面上與 Claude Code 非常相似。以下是關鍵差異:
+
+- 100% 開源。
+- 不綁定特定的服務提供商。雖然我們推薦使用透過 [OpenCode Zen](https://opencode.ai/zen) 提供的模型,但 OpenCode 也可搭配 Claude, OpenAI, Google 甚至本地模型使用。隨著模型不斷演進,彼此間的差距會縮小且價格會下降,因此具備「不限廠商 (provider-agnostic)」的特性至關重要。
+- 內建 LSP (語言伺服器協定) 支援。
+- 專注於終端機介面 (TUI)。OpenCode 由 Neovim 愛好者與 [terminal.shop](https://terminal.shop) 的創作者打造;我們將不斷挑戰終端機介面的極限。
+- 客戶端/伺服器架構 (Client/Server Architecture)。這讓 OpenCode 能夠在您的電腦上運行的同時,由行動裝置進行遠端操控。這意味著 TUI 前端只是眾多可能的客戶端之一。
+
+#### 另一個同名的 Repo 是什麼?
+
+另一個名稱相近的儲存庫與本專案無關。您可以點此[閱讀背後的故事](https://x.com/thdxr/status/1933561254481666466)。
+
+---
+
+**加入我們的社群** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode)
diff --git a/bun.lock b/bun.lock
index d62d9013a..2871be933 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -48,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -75,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -99,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -123,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -171,7 +171,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -200,7 +200,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -216,7 +216,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.0.176",
+ "version": "1.0.184",
"bin": {
"opencode": "./bin/opencode",
},
@@ -308,7 +308,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -328,7 +328,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.0.176",
+ "version": "1.0.184",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -339,7 +339,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -352,7 +352,7 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
@@ -379,7 +379,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -414,7 +414,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"zod": "catalog:",
},
@@ -425,7 +425,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/flake.lock b/flake.lock
index 127262668..e1c4419dc 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1766025857,
- "narHash": "sha256-Lav5jJazCW4mdg1iHcROpuXqmM94BWJvabLFWaJVJp0=",
+ "lastModified": 1766125104,
+ "narHash": "sha256-l/YGrEpLromL4viUo5GmFH3K5M1j0Mb9O+LiaeCPWEM=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "def3da69945bbe338c373fddad5a1bb49cf199ce",
+ "rev": "7d853e518814cca2a657b72eeba67ae20ebf7059",
"type": "github"
},
"original": {
diff --git a/github/action.yml b/github/action.yml
index cf276b51c..57e26d856 100644
--- a/github/action.yml
+++ b/github/action.yml
@@ -9,6 +9,10 @@ inputs:
description: "Model to use"
required: true
+ agent:
+ description: "Agent to use. Must be a primary agent. Falls back to default_agent from config or 'build' if not found."
+ required: false
+
share:
description: "Share the opencode session (defaults to true for public repos)"
required: false
@@ -62,6 +66,7 @@ runs:
run: opencode github run
env:
MODEL: ${{ inputs.model }}
+ AGENT: ${{ inputs.agent }}
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
diff --git a/github/index.ts b/github/index.ts
index 6d826326e..7f6018232 100644
--- a/github/index.ts
+++ b/github/index.ts
@@ -318,6 +318,10 @@ function useEnvRunUrl() {
return `/${repo.owner}/${repo.repo}/actions/runs/${runId}`
}
+function useEnvAgent() {
+ return process.env["AGENT"] || undefined
+}
+
function useEnvShare() {
const value = process.env["SHARE"]
if (!value) return undefined
@@ -578,16 +582,38 @@ async function summarize(response: string) {
}
}
+async function resolveAgent(): Promise {
+ const envAgent = useEnvAgent()
+ if (!envAgent) return undefined
+
+ // Validate the agent exists and is a primary agent
+ const agents = await client.agent.list()
+ const agent = agents.data?.find((a) => a.name === envAgent)
+
+ if (!agent) {
+ console.warn(`agent "${envAgent}" not found. Falling back to default agent`)
+ return undefined
+ }
+
+ if (agent.mode === "subagent") {
+ console.warn(`agent "${envAgent}" is a subagent, not a primary agent. Falling back to default agent`)
+ return undefined
+ }
+
+ return envAgent
+}
+
async function chat(text: string, files: PromptFiles = []) {
console.log("Sending message to opencode...")
const { providerID, modelID } = useEnvModel()
+ const agent = await resolveAgent()
const chat = await client.session.chat({
path: session,
body: {
providerID,
modelID,
- agent: "build",
+ agent,
parts: [
{
type: "text",
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 10a516ed6..a243c8123 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.0.176",
+ "version": "1.0.184",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 87cc8f746..84e244943 100644
--- a/packages/console/core/package.json
+++ b/packages/console/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
- "version": "1.0.176",
+ "version": "1.0.184",
"private": true,
"type": "module",
"dependencies": {
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index e3d1eba98..d129804d9 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "1.0.176",
+ "version": "1.0.184",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json
index 4d6195235..1abe693c1 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "1.0.176",
+ "version": "1.0.184",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 91eec2039..5e70efcd0 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
- "version": "1.0.176",
+ "version": "1.0.184",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/desktop/src/components/header.tsx b/packages/desktop/src/components/header.tsx
index fd4b2c439..6e0ea96dc 100644
--- a/packages/desktop/src/components/header.tsx
+++ b/packages/desktop/src/components/header.tsx
@@ -10,6 +10,7 @@ import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
+import { useCommand } from "@/context/command"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
@@ -24,6 +25,7 @@ export function Header(props: {
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
+ const command = useCommand()
return (
@@ -80,9 +82,18 @@ export function Header(props: {
/>
-
+
+ New session
+ {command.keybind("session.new")}
+
+ }
+ >
+
+
@@ -91,7 +102,7 @@ export function Header(props: {
value={
Toggle terminal
- Ctrl `
+ {command.keybind("terminal.toggle")}
}
>
diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx
index 78415cd2b..c548cea0e 100644
--- a/packages/desktop/src/components/prompt-input.tsx
+++ b/packages/desktop/src/components/prompt-input.tsx
@@ -19,7 +19,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
-import { useCommand, formatKeybind } from "@/context/command"
+import { useCommand } from "@/context/command"
import { persisted } from "@/utils/persist"
import { Identifier } from "@/utils/id"
@@ -889,8 +889,8 @@ export const PromptInput: Component
= (props) => {
custom
-
- {formatKeybind(cmd.keybind!)}
+
+ {command.keybind(cmd.id)}
@@ -990,26 +990,46 @@ export const PromptInput: Component = (props) => {
-
diff --git a/packages/desktop/src/context/command.tsx b/packages/desktop/src/context/command.tsx
index 8fd76ee21..362f35b97 100644
--- a/packages/desktop/src/context/command.tsx
+++ b/packages/desktop/src/context/command.tsx
@@ -226,6 +226,11 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
}
}
},
+ keybind(id: string) {
+ const option = options().find((x) => x.id === id || x.id === "suggested." + id)
+ if (!option?.keybind) return ""
+ return formatKeybind(option.keybind)
+ },
show: showPalette,
keybinds(enabled: boolean) {
setSuspendCount((count) => count + (enabled ? -1 : 1))
diff --git a/packages/desktop/src/context/layout.tsx b/packages/desktop/src/context/layout.tsx
index 0d3de5683..8bfc8aa21 100644
--- a/packages/desktop/src/context/layout.tsx
+++ b/packages/desktop/src/context/layout.tsx
@@ -46,8 +46,8 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
opened: false,
height: 280,
},
- review: {
- state: "pane" as "pane" | "tab",
+ session: {
+ width: 600,
},
sessionTabs: {} as Record,
}),
@@ -156,13 +156,14 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
setStore("terminal", "height", height)
},
},
- review: {
- state: createMemo(() => store.review?.state ?? "closed"),
- pane() {
- setStore("review", "state", "pane")
- },
- tab() {
- setStore("review", "state", "tab")
+ session: {
+ width: createMemo(() => store.session?.width ?? 600),
+ resize(width: number) {
+ if (!store.session) {
+ setStore("session", { width })
+ } else {
+ setStore("session", "width", width)
+ }
},
},
tabs(sessionKey: string) {
@@ -186,14 +187,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
},
async open(tab: string) {
- if (tab === "chat") {
- if (!store.sessionTabs[sessionKey]) {
- setStore("sessionTabs", sessionKey, { all: [], active: undefined })
- } else {
- setStore("sessionTabs", sessionKey, "active", undefined)
- }
- return
- }
const current = store.sessionTabs[sessionKey] ?? { all: [] }
if (tab !== "review") {
if (!current.all.includes(tab)) {
diff --git a/packages/desktop/src/context/local.tsx b/packages/desktop/src/context/local.tsx
index f56973835..69807a2f4 100644
--- a/packages/desktop/src/context/local.tsx
+++ b/packages/desktop/src/context/local.tsx
@@ -360,7 +360,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const init = async (path: string) => {
const relativePath = relative(path)
if (!store.node[relativePath]) await fetch(path)
- if (store.node[relativePath].loaded) return
+ if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
@@ -380,7 +380,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
context.addActive()
if (options?.pinned) setStore("node", path, "pinned", true)
if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view)
- if (store.node[relativePath].loaded) return
+ if (store.node[relativePath]?.loaded) return
return load(relativePath)
}
diff --git a/packages/desktop/src/pages/error.tsx b/packages/desktop/src/pages/error.tsx
index 352b9f3e8..c7330c298 100644
--- a/packages/desktop/src/pages/error.tsx
+++ b/packages/desktop/src/pages/error.tsx
@@ -62,12 +62,32 @@ function formatInitError(error: InitError): string {
}
}
-function formatError(error: unknown): string {
+function formatErrorChain(error: unknown, depth = 0): string {
if (!error) return "Unknown error"
- if (isInitError(error)) return formatInitError(error)
- if (error instanceof Error) return `${error.name}: ${error.message}\n\n${error.stack}`
- if (typeof error === "string") return error
- return JSON.stringify(error, null, 2)
+
+ const indent = depth > 0 ? `\n${"─".repeat(40)}\nCaused by:\n` : ""
+
+ if (isInitError(error)) {
+ return indent + formatInitError(error)
+ }
+
+ if (error instanceof Error) {
+ const parts = [indent + `${error.name}: ${error.message}`]
+ if (error.stack) {
+ parts.push(error.stack)
+ }
+ if (error.cause) {
+ parts.push(formatErrorChain(error.cause, depth + 1))
+ }
+ return parts.join("\n\n")
+ }
+
+ if (typeof error === "string") return indent + error
+ return indent + JSON.stringify(error, null, 2)
+}
+
+function formatError(error: unknown): string {
+ return formatErrorChain(error, 0)
}
interface ErrorPageProps {
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index ade510069..1c8bb615c 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -674,7 +674,17 @@ export default function Layout(props: ParentProps) {
/>
-
+
+ Toggle sidebar
+ {command.keybind("sidebar.toggle")}
+
+ }
+ inactive={layout.sidebar.opened()}
+ >