mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-07 03:23:23 +00:00
fix(tui): surface editor open errors
This commit is contained in:
parent
b3f83eb104
commit
2a392f76b8
4 changed files with 67 additions and 5 deletions
34
SUMMARY.md
Normal file
34
SUMMARY.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# opencode
|
||||
|
||||
**Open source AI coding agent** — a monorepo for the `opencode` CLI/agent and its surrounding products.
|
||||
|
||||
## What it is
|
||||
|
||||
- AI-powered terminal coding agent (`opencode` CLI, npm: `opencode-ai`), plus a desktop app and web/console sites.
|
||||
- Built on **Bun**, **TypeScript**, **Effect v4** (smol), **SolidJS**, **Hono**, **Drizzle** (SQLite), **OpenTUI/Solid** for the TUI, and the Vercel **AI SDK** for LMs.
|
||||
- Two built-in agents: `build` (default, full-access) and `plan` (read-only), with a `general` subagent.
|
||||
|
||||
## Environment variables
|
||||
|
||||
- `OPENCODE_INSTALL_DIR`, `XDG_BIN_DIR` — installer location (see README).
|
||||
|
||||
## Repo layout (Bun workspaces + Turborepo)
|
||||
|
||||
- `packages/opencode` — main CLI/agent (`bun run dev`).
|
||||
- `packages/app` — web app, `packages/web` — marketing/lander, `packages/desktop` — Electron desktop app.
|
||||
- `packages/console` + `packages/console/app` + `packages/identity` — hosted console (auth, billing-ish UI) deployed via SST (`sst.config.ts`).
|
||||
- `packages/stats` — stats service (Cloudflare Worker, see `wrangler.jsonc`).
|
||||
- `packages/core` — shared core logic (config, sessions, tools, providers, MCP).
|
||||
- `packages/llm` — model provider adapters on top of the AI SDK.
|
||||
- `packages/plugin` — plugin/extension system (`@opencode-ai/plugin`).
|
||||
- `packages/sdk` (+ `sdks/vscode`, `packages/slack`) — JS SDK and integrations.
|
||||
- `packages/script` — build/CI scripts.
|
||||
- Supporting libs: `packages/function`, `packages/ui`, `packages/containers`, `packages/extensions`, `packages/docs`, `packages/storybook`, `packages/enterprise`, `packages/cli`, `packages/effect-drizzle-sqlite`, `packages/effect-sqlite-node`, `packages/http-recorder`.
|
||||
- `infra/` — SST infra; `github/` — GitHub workflow assets; `patches/` — patched deps; `nix/` + `flake.nix` — Nix packaging; `specs/` — specs; `install` — install script.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
# or: npm i -g opencode-ai@latest, brew install anomalyco/tap/opencode, etc.
|
||||
```
|
||||
|
|
@ -999,8 +999,8 @@ export function Session() {
|
|||
variant: error ? "warning" : "success",
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
toast.show({ message: "Failed to export session", variant: "error" })
|
||||
} catch (error) {
|
||||
toast.show({ message: `Failed to export session: ${errorMessage(error)}`, variant: "error" })
|
||||
}
|
||||
dialog.clear()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { join } from "node:path"
|
|||
import type { CliRenderer } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Process } from "@/util/process"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import systemOpen from "open"
|
||||
|
||||
export async function open(opts: { value: string; renderer: CliRenderer; cwd: string }): Promise<string | undefined> {
|
||||
|
|
@ -48,10 +49,14 @@ async function createDraft(value: string) {
|
|||
async function openPath(filepath: string, opts: { renderer: CliRenderer; cwd: string }) {
|
||||
const editor = configuredEditor()
|
||||
if (editor) {
|
||||
await openEditor(filepath, opts, editor)
|
||||
await openEditor(filepath, opts, editor).catch((error) => {
|
||||
throw new Error(`Failed to open file with ${editor}: ${errorMessage(error)}. Check $VISUAL or $EDITOR.`)
|
||||
})
|
||||
return "editor" as const
|
||||
}
|
||||
await systemOpen(filepath)
|
||||
await systemOpen(filepath).catch((error) => {
|
||||
throw new Error(`Failed to open file: ${errorMessage(error)}. Set $VISUAL or $EDITOR.`)
|
||||
})
|
||||
return "system" as const
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ const originalVisual = process.env.VISUAL
|
|||
const originalEditor = process.env.EDITOR
|
||||
const systemOpened: string[] = []
|
||||
const retained = new Set<string>()
|
||||
let systemOpenError: Error | undefined
|
||||
|
||||
void mock.module("open", () => ({
|
||||
default: async (filepath: string) => {
|
||||
if (systemOpenError) throw systemOpenError
|
||||
systemOpened.push(filepath)
|
||||
},
|
||||
}))
|
||||
|
|
@ -24,6 +26,7 @@ afterEach(async () => {
|
|||
if (originalEditor === undefined) delete process.env.EDITOR
|
||||
else process.env.EDITOR = originalEditor
|
||||
systemOpened.length = 0
|
||||
systemOpenError = undefined
|
||||
await Promise.all([...retained].map((dir) => rm(dir, { force: true, recursive: true })))
|
||||
retained.clear()
|
||||
})
|
||||
|
|
@ -156,6 +159,24 @@ test("openFile uses the platform application when no editor is configured", asyn
|
|||
expect(systemOpened).toEqual([target])
|
||||
})
|
||||
|
||||
test("openFile suggests configuring an editor when the platform application fails", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
delete process.env.VISUAL
|
||||
delete process.env.EDITOR
|
||||
systemOpenError = new Error("spawn xdg-open ENOENT")
|
||||
|
||||
const message = await Editor.openFile({
|
||||
filepath: "target.md",
|
||||
renderer: renderer().value,
|
||||
cwd: tmp.path,
|
||||
directory: tmp.path,
|
||||
})
|
||||
.then(() => undefined)
|
||||
.catch(errorMessage)
|
||||
|
||||
expect(message).toBe("Failed to open file: spawn xdg-open ENOENT. Set $VISUAL or $EDITOR.")
|
||||
})
|
||||
|
||||
test("openFile restores the renderer when the editor exits unsuccessfully", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
process.env.VISUAL = await editor(tmp.path, "editor", "process.exit(7)")
|
||||
|
|
@ -171,7 +192,9 @@ test("openFile restores the renderer when the editor exits unsuccessfully", asyn
|
|||
})
|
||||
.then(() => undefined)
|
||||
.catch(errorMessage)
|
||||
expect(message).toBe("Editor exited with code 7")
|
||||
expect(message).toBe(
|
||||
`Failed to open file with ${process.execPath} ${join(tmp.path, "editor.ts")}: Editor exited with code 7. Check $VISUAL or $EDITOR.`,
|
||||
)
|
||||
expect(render.events).toEqual(["suspend", "clear", "clear", "resume", "render"])
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue