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",
|
variant: error ? "warning" : "success",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.show({ message: "Failed to export session", variant: "error" })
|
toast.show({ message: `Failed to export session: ${errorMessage(error)}`, variant: "error" })
|
||||||
}
|
}
|
||||||
dialog.clear()
|
dialog.clear()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { join } from "node:path"
|
||||||
import type { CliRenderer } from "@opentui/core"
|
import type { CliRenderer } from "@opentui/core"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Process } from "@/util/process"
|
import { Process } from "@/util/process"
|
||||||
|
import { errorMessage } from "@/util/error"
|
||||||
import systemOpen from "open"
|
import systemOpen from "open"
|
||||||
|
|
||||||
export async function open(opts: { value: string; renderer: CliRenderer; cwd: string }): Promise<string | undefined> {
|
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 }) {
|
async function openPath(filepath: string, opts: { renderer: CliRenderer; cwd: string }) {
|
||||||
const editor = configuredEditor()
|
const editor = configuredEditor()
|
||||||
if (editor) {
|
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
|
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
|
return "system" as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ const originalVisual = process.env.VISUAL
|
||||||
const originalEditor = process.env.EDITOR
|
const originalEditor = process.env.EDITOR
|
||||||
const systemOpened: string[] = []
|
const systemOpened: string[] = []
|
||||||
const retained = new Set<string>()
|
const retained = new Set<string>()
|
||||||
|
let systemOpenError: Error | undefined
|
||||||
|
|
||||||
void mock.module("open", () => ({
|
void mock.module("open", () => ({
|
||||||
default: async (filepath: string) => {
|
default: async (filepath: string) => {
|
||||||
|
if (systemOpenError) throw systemOpenError
|
||||||
systemOpened.push(filepath)
|
systemOpened.push(filepath)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
@ -24,6 +26,7 @@ afterEach(async () => {
|
||||||
if (originalEditor === undefined) delete process.env.EDITOR
|
if (originalEditor === undefined) delete process.env.EDITOR
|
||||||
else process.env.EDITOR = originalEditor
|
else process.env.EDITOR = originalEditor
|
||||||
systemOpened.length = 0
|
systemOpened.length = 0
|
||||||
|
systemOpenError = undefined
|
||||||
await Promise.all([...retained].map((dir) => rm(dir, { force: true, recursive: true })))
|
await Promise.all([...retained].map((dir) => rm(dir, { force: true, recursive: true })))
|
||||||
retained.clear()
|
retained.clear()
|
||||||
})
|
})
|
||||||
|
|
@ -156,6 +159,24 @@ test("openFile uses the platform application when no editor is configured", asyn
|
||||||
expect(systemOpened).toEqual([target])
|
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 () => {
|
test("openFile restores the renderer when the editor exits unsuccessfully", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
process.env.VISUAL = await editor(tmp.path, "editor", "process.exit(7)")
|
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)
|
.then(() => undefined)
|
||||||
.catch(errorMessage)
|
.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"])
|
expect(render.events).toEqual(["suspend", "clear", "clear", "resume", "render"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue