opencode-tui-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ # Contributing to opencode-tui-utils
2
+
3
+ Thank you for taking the time to improve `opencode-tui-utils`.
4
+
5
+ This package is intentionally small. A good contribution should feel native to opencode's TUI, solve one clear problem, and avoid changing how opencode itself works.
6
+
7
+ ## Before You Start
8
+
9
+ - Check whether the feature already exists in opencode.
10
+ - Check existing issues and pull requests for similar work.
11
+ - Keep the first version of a command small enough to review and test manually.
12
+ - Avoid new runtime dependencies unless they are clearly necessary.
13
+
14
+ ## Project Layout
15
+
16
+ ```text
17
+ src/
18
+ core/
19
+ api-wrapper.ts Shared wrapper around the opencode TUI API
20
+ plugins/
21
+ disconnect.tsx Current provider disconnect command
22
+ your-feature.tsx New commands belong here
23
+ index.tsx Public plugin entry point
24
+ ```
25
+
26
+ ## Adding A Command
27
+
28
+ Create a file in `src/plugins/`:
29
+
30
+ ```typescript
31
+ /** @jsxImportSource @opentui/solid */
32
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
33
+ import { createWrappedAPI } from "../core/api-wrapper"
34
+
35
+ const plugin: TuiPluginModule & { id: string } = {
36
+ id: "opencode-tui-utils.your-feature",
37
+ async tui(rawApi) {
38
+ const api = createWrappedAPI(rawApi)
39
+
40
+ api.keymap.registerLayer({
41
+ commands: [
42
+ {
43
+ name: "opencode-tui-utils.your-feature",
44
+ title: "Your Feature",
45
+ category: "Utility",
46
+ namespace: "palette",
47
+ slashName: "your-command",
48
+ async run() {
49
+ api.ui.toast({
50
+ title: "Ready",
51
+ message: "Your command ran successfully.",
52
+ })
53
+ },
54
+ },
55
+ ],
56
+ })
57
+ },
58
+ }
59
+
60
+ export default plugin
61
+ ```
62
+
63
+ Register it in `src/index.tsx`:
64
+
65
+ ```typescript
66
+ import yourPlugin from "./plugins/your-feature"
67
+
68
+ const plugins: TuiPluginModule[] = [disconnectPlugin, yourPlugin]
69
+ ```
70
+
71
+ ## API Wrapper
72
+
73
+ Use `createWrappedAPI(rawApi)` in plugin files instead of calling the raw opencode API directly. This keeps API-specific changes isolated to `src/core/api-wrapper.ts` when possible.
74
+
75
+ Example:
76
+
77
+ ```typescript
78
+ const api = createWrappedAPI(rawApi)
79
+
80
+ api.ui.toast({
81
+ variant: "success",
82
+ message: "Done",
83
+ })
84
+ ```
85
+
86
+ ## KV Storage
87
+
88
+ The wrapper currently supports `get`, `set`, `getJSON`, and `setJSON`.
89
+
90
+ ```typescript
91
+ await api.kv.setJSON("my-feature.settings", { enabled: true })
92
+
93
+ const settings = await api.kv.getJSON<{ enabled: boolean }>(
94
+ "my-feature.settings",
95
+ )
96
+ ```
97
+
98
+ The current opencode TUI KV API does not expose delete. If you need deletion semantics, store an updated object without the removed field.
99
+
100
+ ## Local Testing
101
+
102
+ Type-check the package:
103
+
104
+ ```bash
105
+ npm install
106
+ npm run build
107
+ ```
108
+
109
+ To test a command in opencode before publishing, reference the source file from `~/.config/opencode/tui.json`:
110
+
111
+ ```json
112
+ {
113
+ "$schema": "https://opencode.ai/tui.json",
114
+ "plugin": ["/absolute/path/to/opencode-tui-utils/src/plugins/your-feature.tsx"]
115
+ }
116
+ ```
117
+
118
+ Then restart opencode and run the slash command.
119
+
120
+ ## Pull Requests
121
+
122
+ - Describe the user-facing problem the change solves.
123
+ - Include manual testing steps.
124
+ - Update `README.md` if the command is user-facing.
125
+ - Update localized README files when changing install, compatibility, or command behavior.
126
+ - Keep unrelated formatting and refactors out of feature PRs.
127
+
128
+ Suggested PR title format:
129
+
130
+ ```text
131
+ feat: add /your-command
132
+ fix: handle missing auth file in /disconnect
133
+ docs: clarify plugin installation
134
+ ```
135
+
136
+ ## Command Scope
137
+
138
+ Good fits for this package:
139
+
140
+ | Idea | Why |
141
+ | --- | --- |
142
+ | Provider quick switch | Small TUI-native provider utility. |
143
+ | Session favorites | Helps navigate opencode sessions without changing core behavior. |
144
+ | Config sanity checks | Useful local diagnostics with clear output. |
145
+
146
+ Poor fits:
147
+
148
+ | Idea | Why |
149
+ | --- | --- |
150
+ | Agent orchestration | Better handled by dedicated workflow plugins. |
151
+ | Prompt packs | Better as opencode commands or separate repositories. |
152
+ | Provider backends | Should be maintained as provider-specific plugins. |
153
+
154
+ ## License
155
+
156
+ By contributing, you agree that your contribution will be released under the MIT License.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Blue-B
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,140 @@
1
+ # opencode TUI Utils
2
+
3
+ [English](./README.md) | [한국어](./README.ko.md) | 日本語 | [中文](./README.zh.md)
4
+
5
+ <p align="center">
6
+ <img src="docs/banner.png" alt="opencode-tui-utils" width="860">
7
+ </p>
8
+
9
+ [![npm version](https://img.shields.io/npm/v/opencode-tui-utils?style=flat-square)](https://www.npmjs.com/package/opencode-tui-utils)
10
+ [![Build](https://img.shields.io/github/actions/workflow/status/Blue-B/opencode-tui-utils/test.yml?branch=main&style=flat-square)](https://github.com/Blue-B/opencode-tui-utils/actions)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
+
13
+ [opencode](https://opencode.ai) のTUIで使う小さなユーティリティ集です。最初のコマンドは、接続済みプロバイダーを安全に解除する `/disconnect` です。
14
+
15
+ このプロジェクトは、コマンドを1つずつ追加して育てる前提で構成されています。各ユーティリティは、opencode TUI内の明確な問題を1つ解決し、レビューとテストがしやすい形に保ちます。
16
+
17
+ ## プレビュー
18
+
19
+ <p align="center">
20
+ <img src="docs/preview-command.png" alt="/disconnect コマンド選択" width="600">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="docs/preview-result.png" alt="プロバイダー切断完了" width="600">
25
+ </p>
26
+
27
+ 実際の画面ではopencodeのTUI dialogコンポーネントを使います。外部スクリプトではなく、opencodeのコマンドパレットとdialogの流れの中で動作します。
28
+
29
+ ## なぜ使うのか
30
+
31
+ | 必要なこと | 得られるもの |
32
+ | --- | --- |
33
+ | 1つのプロバイダーだけ安全に削除 | TUIで選んだ認証項目だけを削除できます |
34
+ | JSONの手動編集を避ける | `~/.local/share/opencode/auth.json`を直接編集する必要がありません |
35
+ | トークンを表示しない | プロバイダー名と認証タイプだけを表示し、トークン値は出力しません |
36
+ | 小さな機能を追加しやすい | 共通ローダーとAPIラッパーで新しいユーティリティを追加しやすくしています |
37
+
38
+ ## クイックスタート
39
+
40
+ opencodeのプラグインインストーラーでnpmからインストールします。
41
+
42
+ ```bash
43
+ opencode plugin opencode-tui-utils
44
+ ```
45
+
46
+ このコマンドはパッケージをインストールし、opencodeの設定を更新します。設定を手動で管理する場合は、`~/.config/opencode/tui.json`に次の項目があることを確認してください。
47
+
48
+ ```json
49
+ {
50
+ "$schema": "https://opencode.ai/tui.json",
51
+ "plugin": ["opencode-tui-utils"]
52
+ }
53
+ ```
54
+
55
+ opencodeを再起動して実行します。
56
+
57
+ ```text
58
+ /disconnect
59
+ ```
60
+
61
+ ## コマンド
62
+
63
+ | コマンド | エイリアス | 説明 |
64
+ | --- | --- | --- |
65
+ | `/disconnect` | `/dc` | 接続済みプロバイダーを1つ選び、opencodeの認証ストレージから削除します。新しいセションを開くと変更が反映されます。 |
66
+ | `/lsp-toggle` | — | `~/.config/opencode/opencode.json` の `lsp` 設定を true/false に切り替えます。opencodeの再起動が必要です。 |
67
+
68
+ ## `/disconnect` の動作
69
+
70
+ `/disconnect` はopencodeの認証ファイルを読み、トップレベルのプロバイダーキーを一覧表示し、選択したプロバイダーだけを削除して同じファイルへ保存します。
71
+
72
+ 例:
73
+
74
+ ```json
75
+ {
76
+ "github": { "type": "copilot-free" },
77
+ "anthropic": { "type": "api-key" },
78
+ "openai": { "type": "api-key" }
79
+ }
80
+ ```
81
+
82
+ `github`を選択した場合、トップレベルの`github`キーだけが削除されます。`anthropic`や`openai`など他のプロバイダーは残ります。
83
+
84
+ > **注意:** opencodeはセション開始時に認証ファイルを読み込みます。切断後は新しいopencodeウィンドウまたはセションを開くと、更新されたプロバイダー一覧が反映されます。
85
+
86
+ 出発点は [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494) で話されていたプロバイダー解除の不便さでした。
87
+
88
+ ## データ保存
89
+
90
+ | データ | 場所 | 説明 |
91
+ | --- | --- | --- |
92
+ | プロバイダー認証 | `~/.local/share/opencode/auth.json` | 選択したプロバイダーキーだけを削除します |
93
+ | カスタム認証パス | `OPENCODE_AUTH_PATH=/path/to/auth.json` | 標準以外の場所を使う場合の上書きです |
94
+ | プラグインソース | npm package / opencode plugin cache | `/disconnect`は外部サービスへリクエストしません |
95
+
96
+ `/disconnect`はネットワークリクエストを送らず、トークン値をコピーしたりUIに出力したりしません。
97
+
98
+ ## ユーティリティの追加
99
+
100
+ 新しいコマンドは `src/plugins/` 配下に個別のプラグインモジュールとして追加し、`src/index.tsx`から登録します。
101
+
102
+ ```text
103
+ src/
104
+ core/
105
+ api-wrapper.ts opencode TUI APIの共通ラッパー
106
+ plugins/
107
+ disconnect.tsx プロバイダー解除コマンド
108
+ lsp-toggle.tsx LSP切り替えコマンド
109
+ your-command.tsx 新しいユーティリティの追加先
110
+ index.tsx 公開プラグインエントリ
111
+ ```
112
+
113
+ opencode TUI APIを使う場合は `createWrappedAPI(rawApi)` を通してください。opencodeのプラグインAPIが変わった場合の修正箇所を減らすためです。
114
+
115
+ ## 開発
116
+
117
+ ```bash
118
+ git clone https://github.com/Blue-B/opencode-tui-utils.git
119
+ cd opencode-tui-utils
120
+ npm install
121
+ npm run build
122
+ ```
123
+
124
+ ## 互換性
125
+
126
+ テスト済みの環境です。
127
+
128
+ | ツール | バージョン |
129
+ | --- | --- |
130
+ | opencode | 1.14.46 |
131
+
132
+ このパッケージはopencodeのTUI Plugin APIを使うため、peer dependencyとして`@opencode-ai/plugin >=1.14.42`を指定しています。
133
+
134
+ ## コントリビューション
135
+
136
+ IssueとPRを歓迎します。新しいコマンドを追加する前に[CONTRIBUTING.md](CONTRIBUTING.md)を確認してください。小さく、目的がはっきりした変更を歓迎します。
137
+
138
+ ## ライセンス
139
+
140
+ MIT
package/README.ko.md ADDED
@@ -0,0 +1,191 @@
1
+ # opencode TUI Utils
2
+
3
+ [English](./README.md) | 한국어 | [日本語](./README.ja.md) | [中文](./README.zh.md)
4
+
5
+ <p align="center">
6
+ <img src="docs/banner.png" alt="opencode-tui-utils" width="860">
7
+ </p>
8
+
9
+ [![npm version](https://img.shields.io/npm/v/opencode-tui-utils?style=flat-square)](https://www.npmjs.com/package/opencode-tui-utils)
10
+ [![Build](https://img.shields.io/github/actions/workflow/status/Blue-B/opencode-tui-utils/test.yml?branch=main&style=flat-square)](https://github.com/Blue-B/opencode-tui-utils/actions)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
+
13
+ [opencode](https://opencode.ai) TUI에서 자주 필요한 작업을 작은 명령어로 추가하는 플러그인 패키지입니다. 첫 기능은 연결된 프로바이더를 안전하게 해제하는 `/disconnect` 명령입니다.
14
+
15
+ 이 프로젝트는 명령어를 하나씩 확장할 수 있도록 구성되어 있습니다. 각 유틸리티는 opencode TUI 안에서 한 가지 문제를 명확하게 해결하고, 검토와 테스트가 쉬운 형태를 유지하는 것을 기준으로 합니다.
16
+
17
+ ## 미리보기
18
+
19
+ <p align="center">
20
+ <img src="docs/preview-command.png" alt="/disconnect 명령 선택" width="600">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="docs/preview-result.png" alt="프로바이더 해제 완료" width="600">
25
+ </p>
26
+
27
+ 실제 화면은 opencode의 TUI dialog 컴포넌트를 사용합니다. 별도 스크립트를 실행하는 방식이 아니라 opencode 안의 명령 팔레트와 dialog 흐름에서 자연스럽게 동작합니다.
28
+
29
+ ## 왜 필요한가요?
30
+
31
+ | 필요 | 제공 가치 |
32
+ | --- | --- |
33
+ | 프로바이더 하나만 안전하게 제거 | TUI에서 선택한 인증 항목 하나만 삭제합니다 |
34
+ | 직접 JSON 수정 방지 | `~/.local/share/opencode/auth.json`을 손으로 열어 수정하지 않아도 됩니다 |
35
+ | 토큰 노출 방지 | 프로바이더 이름과 인증 타입만 표시하고 토큰 값은 출력하지 않습니다 |
36
+ | 기능 확장 | 공용 플러그인 로더와 API 래퍼로 새 유틸리티를 추가하기 쉽습니다 |
37
+
38
+ ## 시작하기
39
+
40
+ opencode의 플러그인 설치 명령으로 npm에서 설치합니다.
41
+
42
+ ```bash
43
+ opencode plugin opencode-tui-utils
44
+ ```
45
+
46
+ 이 명령은 패키지를 설치하고 opencode 설정을 갱신합니다. 직접 설정을 관리한다면 `~/.config/opencode/tui.json`에 아래 항목이 있어야 합니다.
47
+
48
+ ```json
49
+ {
50
+ "$schema": "https://opencode.ai/tui.json",
51
+ "plugin": ["opencode-tui-utils"]
52
+ }
53
+ ```
54
+
55
+ opencode를 재시작한 뒤 실행합니다.
56
+
57
+ ```text
58
+ /disconnect
59
+ ```
60
+
61
+ ## 명령어
62
+
63
+ | 명령어 | 별칭 | 설명 |
64
+ | --- | --- | --- |
65
+ | `/disconnect` | `/dc` | 연결된 프로바이더 중 하나를 선택해 opencode 인증 저장소에서 제거합니다. 새 세션을 열어야 변경 사항이 반영됩니다. |
66
+ | `/lsp-toggle` | — | `~/.config/opencode/opencode.json`의 `lsp` 설정을 true/false로 전환합니다. opencode 재시작 필요. |
67
+
68
+ ## `/disconnect` 동작 방식
69
+
70
+ `/disconnect`는 opencode 인증 파일을 읽고, 최상위 프로바이더 키 목록을 보여준 뒤, 사용자가 선택한 프로바이더 하나만 제거한 상태로 같은 파일을 다시 저장합니다.
71
+
72
+ 예시 인증 구조:
73
+
74
+ ```json
75
+ {
76
+ "github": { "type": "copilot-free" },
77
+ "anthropic": { "type": "api-key" },
78
+ "openai": { "type": "api-key" }
79
+ }
80
+ ```
81
+
82
+ 여기서 `github`를 선택하면 최상위 `github` 키만 제거됩니다. `anthropic`, `openai` 같은 다른 프로바이더 항목은 그대로 유지됩니다.
83
+
84
+ > **참고:** opencode는 세션 시작 시 인증 파일을 읽습니다. 해제 후에는 새로운 opencode 창이나 세션을 열어야 업데이트된 프로바이더 목록이 반영됩니다.
85
+
86
+ 처음 시작점은 [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494)에서 다뤄진 프로바이더 해제 불편함이었습니다.
87
+
88
+ ## 데이터 저장
89
+
90
+ | 데이터 | 위치 | 설명 |
91
+ | --- | --- | --- |
92
+ | 프로바이더 인증 | `~/.local/share/opencode/auth.json` | 선택한 프로바이더 키만 이 파일에서 제거합니다 |
93
+ | 사용자 지정 인증 경로 | `OPENCODE_AUTH_PATH=/path/to/auth.json` | 기본 위치가 아닌 경우 사용할 수 있습니다 |
94
+ | 플러그인 소스 | npm 패키지 / opencode plugin cache | `/disconnect`는 외부 서비스에 요청하지 않습니다 |
95
+
96
+ `/disconnect`는 네트워크 요청을 보내지 않고, 토큰 값을 복사하거나 UI에 출력하지 않습니다.
97
+
98
+ ## 유틸리티 확장 방법
99
+
100
+ 새 명령어는 `src/plugins/` 아래에 별도 플러그인 모듈로 추가하고, `src/index.tsx`에서 등록합니다.
101
+
102
+ ```text
103
+ src/
104
+ core/
105
+ api-wrapper.ts opencode TUI API 공용 래퍼
106
+ plugins/
107
+ disconnect.tsx 프로바이더 해제 명령어
108
+ lsp-toggle.tsx LSP 전환 명령어
109
+ your-command.tsx 새 유틸리티 추가 위치
110
+ index.tsx 공개 플러그인 진입점
111
+ ```
112
+
113
+ 최소 명령어 형태:
114
+
115
+ ```typescript
116
+ /** @jsxImportSource @opentui/solid */
117
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
118
+ import { createWrappedAPI } from "../core/api-wrapper"
119
+
120
+ const plugin: TuiPluginModule & { id: string } = {
121
+ id: "opencode-tui-utils.your-command",
122
+ async tui(rawApi) {
123
+ const api = createWrappedAPI(rawApi)
124
+
125
+ api.keymap.registerLayer({
126
+ commands: [
127
+ {
128
+ name: "opencode-tui-utils.your-command",
129
+ title: "Your Command",
130
+ category: "Utility",
131
+ namespace: "palette",
132
+ slashName: "your-command",
133
+ async run() {
134
+ api.ui.toast({ message: "Command ran successfully." })
135
+ },
136
+ },
137
+ ],
138
+ })
139
+ },
140
+ }
141
+
142
+ export default plugin
143
+ ```
144
+
145
+ 그 다음 `src/index.tsx`에 등록합니다.
146
+
147
+ ```typescript
148
+ import yourCommand from "./plugins/your-command"
149
+
150
+ const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin, yourCommand]
151
+ ```
152
+
153
+ opencode TUI API를 사용할 때는 `createWrappedAPI(rawApi)`를 거치도록 합니다. opencode 플러그인 API가 바뀌었을 때 수정 지점을 줄이기 위한 구조입니다.
154
+
155
+ ## 개발
156
+
157
+ ```bash
158
+ git clone https://github.com/Blue-B/opencode-tui-utils.git
159
+ cd opencode-tui-utils
160
+ npm install
161
+ npm run build
162
+ ```
163
+
164
+ 로컬 소스 파일 테스트 예시:
165
+
166
+ ```json
167
+ {
168
+ "$schema": "https://opencode.ai/tui.json",
169
+ "plugin": ["/absolute/path/to/opencode-tui-utils/src/plugins/disconnect.tsx"]
170
+ }
171
+ ```
172
+
173
+ `tui.json` 변경 후에는 opencode를 재시작해야 합니다.
174
+
175
+ ## 호환성
176
+
177
+ 테스트한 환경입니다.
178
+
179
+ | 도구 | 버전 |
180
+ | --- | --- |
181
+ | opencode | 1.14.46 |
182
+
183
+ 이 패키지는 opencode의 TUI Plugin API를 사용하므로 peer dependency로 `@opencode-ai/plugin >=1.14.42`를 선언합니다.
184
+
185
+ ## 기여
186
+
187
+ 이슈와 PR은 환영합니다. 새 명령어를 추가하기 전에는 [CONTRIBUTING.md](CONTRIBUTING.md)를 확인해 주세요. 작고 명확한 변경을 선호합니다.
188
+
189
+ ## 라이선스
190
+
191
+ MIT
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # opencode TUI Utils
2
+
3
+ English | [한국어](./README.ko.md) | [日本語](./README.ja.md) | [中文](./README.zh.md)
4
+
5
+ <p align="center">
6
+ <img src="docs/banner.png" alt="opencode-tui-utils" width="860">
7
+ </p>
8
+
9
+ [![npm version](https://img.shields.io/npm/v/opencode-tui-utils?style=flat-square)](https://www.npmjs.com/package/opencode-tui-utils)
10
+ [![Build](https://img.shields.io/github/actions/workflow/status/Blue-B/opencode-tui-utils/test.yml?branch=main&style=flat-square)](https://github.com/Blue-B/opencode-tui-utils/actions)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
+
13
+ Small native-feeling TUI utilities for [opencode](https://opencode.ai). The first command adds a safe provider disconnect flow, so you can remove one connected provider without editing `auth.json` by hand.
14
+
15
+ This project is built to grow command by command. Each utility should solve one focused opencode TUI problem and stay easy to review, test, and remove.
16
+
17
+ ## Preview
18
+
19
+ <p align="center">
20
+ <img src="docs/preview-command.png" alt="/disconnect command selection" width="600">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="docs/preview-result.png" alt="Provider disconnected confirmation" width="600">
25
+ </p>
26
+
27
+ The command uses opencode's own TUI dialog components, so it appears inside the same command palette and dialog flow instead of launching a separate script.
28
+
29
+ ## Why use opencode TUI Utils
30
+
31
+ | Need | What you get |
32
+ | --- | --- |
33
+ | Remove one provider safely | Select a provider in the TUI and remove only that auth entry |
34
+ | Avoid manual JSON edits | No need to open or hand-edit `~/.local/share/opencode/auth.json` |
35
+ | Keep tokens private | Provider names and auth types are shown, token values are never printed |
36
+ | Add more small commands | Shared plugin loader and API wrapper make new utilities straightforward |
37
+
38
+ ## Quick Start
39
+
40
+ Install from npm with opencode's plugin installer:
41
+
42
+ ```bash
43
+ opencode plugin opencode-tui-utils
44
+ ```
45
+
46
+ That command installs the package and updates your opencode config. If you manage the config manually, make sure `~/.config/opencode/tui.json` includes:
47
+
48
+ ```json
49
+ {
50
+ "$schema": "https://opencode.ai/tui.json",
51
+ "plugin": ["opencode-tui-utils"]
52
+ }
53
+ ```
54
+
55
+ Restart opencode and run:
56
+
57
+ ```text
58
+ /disconnect
59
+ ```
60
+
61
+ ## Commands
62
+
63
+ | Command | Alias | Description |
64
+ | --- | --- | --- |
65
+ | `/disconnect` | `/dc` | Pick one connected provider and remove it from opencode auth storage. Open a new session to reflect the change. |
66
+ | `/lsp-toggle` | — | Toggle `lsp: true/false` in `~/.config/opencode/opencode.json`. Requires opencode restart. |
67
+
68
+ ## How `/disconnect` works
69
+
70
+ `/disconnect` reads your opencode auth file, lists the provider keys, and rewrites the same file after removing the one provider you selected.
71
+
72
+ Example auth shape:
73
+
74
+ ```json
75
+ {
76
+ "github": { "type": "copilot-free" },
77
+ "anthropic": { "type": "api-key" },
78
+ "openai": { "type": "api-key" }
79
+ }
80
+ ```
81
+
82
+ If you select `github`, only the top-level `github` key is removed. Other provider entries stay untouched.
83
+
84
+ > **Note:** opencode reads the auth file at session start. After disconnecting, open a new opencode window or session to see the updated provider list.
85
+
86
+ This started from the provider disconnect pain point discussed in [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494).
87
+
88
+ ## Data Storage
89
+
90
+ | Data | Location | Notes |
91
+ | --- | --- | --- |
92
+ | Provider auth | `~/.local/share/opencode/auth.json` | The selected provider key is removed from this file |
93
+ | Custom auth path | `OPENCODE_AUTH_PATH=/path/to/auth.json` | Optional override for non-standard setups |
94
+ | Plugin source | npm package / opencode plugin cache | No external service is contacted by `/disconnect` |
95
+
96
+ `/disconnect` does not send network requests, copy token values, or print token values to the UI.
97
+
98
+ ## Adding More Utilities
99
+
100
+ New commands are added as separate plugin modules under `src/plugins/` and registered from `src/index.tsx`.
101
+
102
+ ```text
103
+ src/
104
+ core/
105
+ api-wrapper.ts Shared wrapper around the opencode TUI API
106
+ plugins/
107
+ disconnect.tsx Provider disconnect command
108
+ lsp-toggle.tsx LSP toggle command
109
+ your-command.tsx Add new utilities here
110
+ index.tsx Public plugin entry point
111
+ ```
112
+
113
+ Minimal command shape:
114
+
115
+ ```typescript
116
+ /** @jsxImportSource @opentui/solid */
117
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
118
+ import { createWrappedAPI } from "../core/api-wrapper"
119
+
120
+ const plugin: TuiPluginModule & { id: string } = {
121
+ id: "opencode-tui-utils.your-command",
122
+ async tui(rawApi) {
123
+ const api = createWrappedAPI(rawApi)
124
+
125
+ api.keymap.registerLayer({
126
+ commands: [
127
+ {
128
+ name: "opencode-tui-utils.your-command",
129
+ title: "Your Command",
130
+ category: "Utility",
131
+ namespace: "palette",
132
+ slashName: "your-command",
133
+ async run() {
134
+ api.ui.toast({ message: "Command ran successfully." })
135
+ },
136
+ },
137
+ ],
138
+ })
139
+ },
140
+ }
141
+
142
+ export default plugin
143
+ ```
144
+
145
+ Then register it in `src/index.tsx`:
146
+
147
+ ```typescript
148
+ import yourCommand from "./plugins/your-command"
149
+
150
+ const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin, yourCommand]
151
+ ```
152
+
153
+ Use `createWrappedAPI(rawApi)` for opencode TUI APIs. It keeps API-specific changes easier to isolate when opencode updates its plugin API.
154
+
155
+ ## Tips
156
+
157
+ ### Switching model variants (xhigh / high / max)
158
+
159
+ Use `Ctrl + T` in the TUI to cycle through reasoning variants for the current model. To switch models entirely, use `Ctrl + X` then `M`, or type `/models`.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ git clone https://github.com/Blue-B/opencode-tui-utils.git
165
+ cd opencode-tui-utils
166
+ npm install
167
+ npm run build
168
+ ```
169
+
170
+ Local source-file testing:
171
+
172
+ ```json
173
+ {
174
+ "$schema": "https://opencode.ai/tui.json",
175
+ "plugin": ["/absolute/path/to/opencode-tui-utils/src/plugins/disconnect.tsx"]
176
+ }
177
+ ```
178
+
179
+ Restart opencode after changing `tui.json`.
180
+
181
+ ## Compatibility
182
+
183
+ Tested with:
184
+
185
+ | Tool | Version |
186
+ | --- | --- |
187
+ | opencode | 1.14.46 |
188
+
189
+ The package declares `@opencode-ai/plugin >=1.14.42` as a peer dependency because it uses opencode's TUI Plugin API.
190
+
191
+ ## Contributing
192
+
193
+ Issues and PRs are welcome. Please keep changes small and focused. See [CONTRIBUTING.md](CONTRIBUTING.md) before adding a new command.
194
+
195
+ ## License
196
+
197
+ [MIT](LICENSE) — Blue-B © 2026
198
+
199
+ You are free to use, modify, and distribute this project, including for commercial purposes. The only requirement is to keep the copyright notice and license text included in any copies or substantial portions.
package/README.zh.md ADDED
@@ -0,0 +1,140 @@
1
+ # opencode TUI Utils
2
+
3
+ [English](./README.md) | [한국어](./README.ko.md) | [日本語](./README.ja.md) | 中文
4
+
5
+ <p align="center">
6
+ <img src="docs/banner.png" alt="opencode-tui-utils" width="860">
7
+ </p>
8
+
9
+ [![npm version](https://img.shields.io/npm/v/opencode-tui-utils?style=flat-square)](https://www.npmjs.com/package/opencode-tui-utils)
10
+ [![Build](https://img.shields.io/github/actions/workflow/status/Blue-B/opencode-tui-utils/test.yml?branch=main&style=flat-square)](https://github.com/Blue-B/opencode-tui-utils/actions)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
+
13
+ 面向 [opencode](https://opencode.ai) TUI 的小型工具集。第一个命令是 `/disconnect`,用于安全地断开一个已连接的 provider,不需要手动编辑 `auth.json`。
14
+
15
+ 这个项目按“一个命令解决一个问题”的方式扩展。每个工具都应该聚焦于一个 opencode TUI 场景,并保持容易审查、测试和移除。
16
+
17
+ ## 预览
18
+
19
+ <p align="center">
20
+ <img src="docs/preview-command.png" alt="/disconnect 命令选择" width="600">
21
+ </p>
22
+
23
+ <p align="center">
24
+ <img src="docs/preview-result.png" alt="Provider 断开确认" width="600">
25
+ </p>
26
+
27
+ 实际界面使用 opencode 的 TUI dialog 组件,所以它在 opencode 的命令面板和 dialog 流程中运行,而不是额外启动脚本。
28
+
29
+ ## 为什么使用它
30
+
31
+ | 需求 | 提供的能力 |
32
+ | --- | --- |
33
+ | 安全移除一个 provider | 在 TUI 中选择并只删除对应认证项 |
34
+ | 避免手动编辑 JSON | 不需要直接打开 `~/.local/share/opencode/auth.json` |
35
+ | 避免 token 暴露 | 只显示 provider 名称和认证类型,不输出 token 值 |
36
+ | 方便继续扩展 | 共享插件加载器和 API wrapper 让新增工具更简单 |
37
+
38
+ ## 快速开始
39
+
40
+ 使用 opencode 的插件安装器从 npm 安装:
41
+
42
+ ```bash
43
+ opencode plugin opencode-tui-utils
44
+ ```
45
+
46
+ 这个命令会安装包并更新 opencode 配置。如果你手动管理配置,请确保 `~/.config/opencode/tui.json` 包含:
47
+
48
+ ```json
49
+ {
50
+ "$schema": "https://opencode.ai/tui.json",
51
+ "plugin": ["opencode-tui-utils"]
52
+ }
53
+ ```
54
+
55
+ 重启 opencode,然后运行:
56
+
57
+ ```text
58
+ /disconnect
59
+ ```
60
+
61
+ ## 命令
62
+
63
+ | 命令 | 别名 | 说明 |
64
+ | --- | --- | --- |
65
+ | `/disconnect` | `/dc` | 选择一个已连接的 provider,并从 opencode 的认证存储中移除。需要打开新会话才能看到变更。 |
66
+ | `/lsp-toggle` | — | 切换 `~/.config/opencode/opencode.json` 中的 `lsp` 设置。需要重启 opencode。 |
67
+
68
+ ## `/disconnect` 如何工作
69
+
70
+ `/disconnect` 会读取 opencode 的认证文件,列出顶层 provider key,然后只删除你选择的那一个 provider,并把结果写回同一个文件。
71
+
72
+ 示例:
73
+
74
+ ```json
75
+ {
76
+ "github": { "type": "copilot-free" },
77
+ "anthropic": { "type": "api-key" },
78
+ "openai": { "type": "api-key" }
79
+ }
80
+ ```
81
+
82
+ 如果选择 `github`,只会删除顶层的 `github` key。`anthropic`、`openai` 等其他 provider 会保持不变。
83
+
84
+ > **注意:** opencode 在会话启动时读取认证文件。断开连接后,需要打开新的 opencode 窗口或会话才能看到更新后的 provider 列表。
85
+
86
+ 它最初来自 [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494) 中提到的 provider 断开问题。
87
+
88
+ ## 数据存储
89
+
90
+ | 数据 | 位置 | 说明 |
91
+ | --- | --- | --- |
92
+ | Provider 认证 | `~/.local/share/opencode/auth.json` | 只从这里删除选择的 provider key |
93
+ | 自定义认证路径 | `OPENCODE_AUTH_PATH=/path/to/auth.json` | 用于非默认数据目录 |
94
+ | 插件源码 | npm package / opencode plugin cache | `/disconnect` 不会访问外部服务 |
95
+
96
+ `/disconnect` 不会发送网络请求,不会复制 token 值,也不会把 token 值显示在 UI 中。
97
+
98
+ ## 添加更多工具
99
+
100
+ 新命令作为单独的插件模块放在 `src/plugins/` 下,并从 `src/index.tsx` 注册。
101
+
102
+ ```text
103
+ src/
104
+ core/
105
+ api-wrapper.ts opencode TUI API 的共享 wrapper
106
+ plugins/
107
+ disconnect.tsx Provider 断开命令
108
+ lsp-toggle.tsx LSP 切换命令
109
+ your-command.tsx 新工具放在这里
110
+ index.tsx 公开插件入口
111
+ ```
112
+
113
+ 使用 opencode TUI API 时请通过 `createWrappedAPI(rawApi)`。这样当 opencode 插件 API 变化时,更新位置会更集中。
114
+
115
+ ## 开发
116
+
117
+ ```bash
118
+ git clone https://github.com/Blue-B/opencode-tui-utils.git
119
+ cd opencode-tui-utils
120
+ npm install
121
+ npm run build
122
+ ```
123
+
124
+ ## 兼容性
125
+
126
+ 已测试环境:
127
+
128
+ | 工具 | 版本 |
129
+ | --- | --- |
130
+ | opencode | 1.14.46 |
131
+
132
+ 这个包使用 opencode 的 TUI Plugin API,因此将 `@opencode-ai/plugin >=1.14.42` 声明为 peer dependency。
133
+
134
+ ## 贡献
135
+
136
+ 欢迎 issue 和 PR。添加新命令前请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。更推荐小而清晰的改动。
137
+
138
+ ## 许可证
139
+
140
+ MIT
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "opencode-tui-utils",
3
+ "version": "1.0.0",
4
+ "description": "Add /disconnect command to opencode - safely disconnect providers without editing auth.json",
5
+ "main": "src/index.tsx",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc --noEmit",
9
+ "dev": "tsc --watch",
10
+ "test": "npm run build",
11
+ "prepublishOnly": "npm run build"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "docs",
16
+ "README.md",
17
+ "README.*.md",
18
+ "CONTRIBUTING.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "opencode",
23
+ "tui",
24
+ "plugin",
25
+ "disconnect",
26
+ "provider",
27
+ "auth",
28
+ "copilot"
29
+ ],
30
+ "author": "Blue-B <source_vs@naver.com>",
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "@opencode-ai/plugin": ">=1.14.42",
34
+ "@opentui/solid": ">=0.2.6"
35
+ },
36
+ "devDependencies": {
37
+ "@opencode-ai/plugin": "1.14.46",
38
+ "@opentui/solid": "^0.2.6",
39
+ "@types/node": "^20.0.0",
40
+ "typescript": "^5.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/Blue-B/opencode-tui-utils.git"
48
+ },
49
+ "bugs": {
50
+ "url": "https://github.com/Blue-B/opencode-tui-utils/issues"
51
+ },
52
+ "homepage": "https://github.com/Blue-B/opencode-tui-utils#readme"
53
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * OpenCode TUI API Wrapper
3
+ *
4
+ * Keeps opencode TUI API calls behind a small local layer so API-specific
5
+ * changes are easier to isolate.
6
+ */
7
+
8
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
9
+
10
+ export interface WrappedAPI {
11
+ keymap: ReturnType<typeof createKeymapAPI>
12
+ ui: ReturnType<typeof createUIAPI>
13
+ kv: ReturnType<typeof createKVAPI>
14
+ }
15
+
16
+ /**
17
+ * Keymap API wrapper for slash commands and palette commands.
18
+ */
19
+ export function createKeymapAPI(api: TuiPluginApi) {
20
+ return {
21
+ registerLayer: (config: Parameters<typeof api.keymap.registerLayer>[0]) => {
22
+ return api.keymap.registerLayer(config)
23
+ },
24
+ }
25
+ }
26
+
27
+ /**
28
+ * UI API wrapper for dialogs and toasts.
29
+ */
30
+ export function createUIAPI(api: TuiPluginApi) {
31
+ return {
32
+ dialog: {
33
+ replace: (component: Parameters<typeof api.ui.dialog.replace>[0]) => {
34
+ return api.ui.dialog.replace(component)
35
+ },
36
+ clear: () => {
37
+ return api.ui.dialog.clear()
38
+ },
39
+ },
40
+ toast: (options: Parameters<typeof api.ui.toast>[0]) => {
41
+ return api.ui.toast(options)
42
+ },
43
+ // Re-export UI components used by plugins.
44
+ DialogSelect: api.ui.DialogSelect,
45
+ DialogConfirm: api.ui.DialogConfirm,
46
+ DialogAlert: api.ui.DialogAlert,
47
+ }
48
+ }
49
+
50
+ /**
51
+ * KV storage wrapper for future commands that need persistence.
52
+ */
53
+ export function createKVAPI(api: TuiPluginApi) {
54
+ return {
55
+ get: async (key: string): Promise<string | undefined> => {
56
+ try {
57
+ return api.kv.get<string | undefined>(key)
58
+ } catch (error) {
59
+ console.error(`Failed to get KV key "${key}":`, error)
60
+ return undefined
61
+ }
62
+ },
63
+ set: async (key: string, value: string): Promise<void> => {
64
+ try {
65
+ api.kv.set(key, value)
66
+ } catch (error) {
67
+ console.error(`Failed to set KV key "${key}":`, error)
68
+ }
69
+ },
70
+ getJSON: async <T = unknown>(key: string): Promise<T | undefined> => {
71
+ try {
72
+ const value = api.kv.get<string | undefined>(key)
73
+ return value ? JSON.parse(value) : undefined
74
+ } catch (error) {
75
+ console.error(`Failed to parse JSON from KV key "${key}":`, error)
76
+ return undefined
77
+ }
78
+ },
79
+ setJSON: async <T = unknown>(key: string, value: T): Promise<void> => {
80
+ try {
81
+ api.kv.set(key, JSON.stringify(value))
82
+ } catch (error) {
83
+ console.error(`Failed to set JSON to KV key "${key}":`, error)
84
+ }
85
+ },
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Main wrapper used by plugins.
91
+ */
92
+ export function createWrappedAPI(api: TuiPluginApi): WrappedAPI {
93
+ return {
94
+ keymap: createKeymapAPI(api),
95
+ ui: createUIAPI(api),
96
+ kv: createKVAPI(api),
97
+ }
98
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * OpenCode TUI Utils - Plugin Loader
3
+ */
4
+
5
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
6
+ import disconnectPlugin from "./plugins/disconnect"
7
+ import lspTogglePlugin from "./plugins/lsp-toggle"
8
+
9
+ const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin]
10
+
11
+ export async function initializePlugins(...args: Parameters<TuiPluginModule["tui"]>) {
12
+ for (const plugin of plugins) {
13
+ try {
14
+ await plugin.tui(...args)
15
+ } catch (error) {
16
+ console.error(`Failed to initialize plugin:`, error)
17
+ }
18
+ }
19
+ }
20
+
21
+ const mainPlugin: TuiPluginModule & { id: string } = {
22
+ id: "opencode-tui-utils",
23
+ async tui(...args: Parameters<TuiPluginModule["tui"]>) {
24
+ await initializePlugins(...args)
25
+ },
26
+ }
27
+
28
+ export default mainPlugin
@@ -0,0 +1,113 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
3
+ import { homedir } from "node:os"
4
+ import { join } from "node:path"
5
+ import { readFile, writeFile } from "node:fs/promises"
6
+ import { createWrappedAPI } from "../core/api-wrapper"
7
+
8
+ function getAuthPath() {
9
+ if (process.env.OPENCODE_AUTH_PATH) return process.env.OPENCODE_AUTH_PATH
10
+
11
+ const dataHome = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share")
12
+ return join(dataHome, "opencode", "auth.json")
13
+ }
14
+
15
+ async function loadAuth() {
16
+ try {
17
+ const content = await readFile(getAuthPath(), "utf-8")
18
+ return JSON.parse(content) as Record<string, { type: string }>
19
+ } catch (error) {
20
+ console.error("Failed to load auth:", error)
21
+ return null
22
+ }
23
+ }
24
+
25
+ async function saveAuth(data: Record<string, unknown>) {
26
+ try {
27
+ await writeFile(getAuthPath(), JSON.stringify(data, null, 2))
28
+ } catch (error) {
29
+ console.error("Failed to save auth:", error)
30
+ throw error
31
+ }
32
+ }
33
+
34
+ const plugin: TuiPluginModule & { id: string } = {
35
+ id: "opencode-tui-utils.disconnect",
36
+ async tui(rawApi) {
37
+ const api = createWrappedAPI(rawApi)
38
+ const { DialogSelect, DialogAlert } = api.ui
39
+
40
+ api.keymap.registerLayer({
41
+ commands: [
42
+ {
43
+ name: "opencode-tui-utils.disconnect",
44
+ title: "Disconnect Provider",
45
+ category: "Provider",
46
+ namespace: "palette",
47
+ slashName: "disconnect",
48
+ slashAliases: ["dc"],
49
+ async run() {
50
+ const data = await loadAuth()
51
+ if (!data) {
52
+ api.ui.dialog.replace(() => (
53
+ <DialogAlert
54
+ title="Error"
55
+ message="Could not read authentication file."
56
+ />
57
+ ))
58
+ return
59
+ }
60
+
61
+ const providers = Object.keys(data)
62
+ if (providers.length === 0) {
63
+ api.ui.toast({
64
+ title: "No providers",
65
+ message: "No providers connected.",
66
+ })
67
+ return
68
+ }
69
+
70
+ const options = providers.map((p) => ({
71
+ title: `${p} (${data[p]?.type ?? "unknown"})`,
72
+ value: p,
73
+ }))
74
+
75
+ api.ui.dialog.replace(() => (
76
+ <DialogSelect
77
+ title="Select provider to disconnect"
78
+ options={options}
79
+ onSelect={(option) => {
80
+ if (!option) return
81
+
82
+ void (async () => {
83
+ const nextData = { ...data }
84
+ delete nextData[option.value]
85
+
86
+ try {
87
+ await saveAuth(nextData)
88
+ api.ui.dialog.clear()
89
+ api.ui.toast({
90
+ variant: "success",
91
+ title: "Disconnected",
92
+ message: `Removed ${option.value}. Open a new session to reflect the change.`,
93
+ })
94
+ } catch {
95
+ api.ui.dialog.replace(() => (
96
+ <DialogAlert
97
+ title="Error"
98
+ message="Could not update authentication file."
99
+ />
100
+ ))
101
+ }
102
+ })()
103
+ }}
104
+ />
105
+ ))
106
+ },
107
+ },
108
+ ],
109
+ })
110
+ },
111
+ }
112
+
113
+ export default plugin
@@ -0,0 +1,96 @@
1
+ /**
2
+ * /lsp-toggle
3
+ *
4
+ * Toggles the `lsp` field in ~/.config/opencode/opencode.json between true/false.
5
+ * This controls whether opencode starts Language Server Protocol support.
6
+ *
7
+ * Important: opencode reads config at startup. Changing the JSON file does NOT
8
+ * hot-reload the setting. You must restart opencode after toggling for the
9
+ * change to take effect.
10
+ *
11
+ * Usage:
12
+ * /lsp-toggle Toggle LSP on/off
13
+ */
14
+
15
+ /** @jsxImportSource @opentui/solid */
16
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
17
+ import { homedir } from "node:os"
18
+ import { join } from "node:path"
19
+ import { readFile, writeFile } from "node:fs/promises"
20
+ import { createWrappedAPI } from "../core/api-wrapper"
21
+
22
+ /** Resolve opencode.json path. Honors OPENCODE_CONFIG_DIR if set. */
23
+ function getConfigPath() {
24
+ if (process.env.OPENCODE_CONFIG_DIR) {
25
+ return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json")
26
+ }
27
+ return join(homedir(), ".config", "opencode", "opencode.json")
28
+ }
29
+
30
+ async function loadConfig() {
31
+ try {
32
+ const content = await readFile(getConfigPath(), "utf-8")
33
+ return JSON.parse(content) as Record<string, unknown>
34
+ } catch {
35
+ // If file is missing or unreadable, start with an empty object.
36
+ return {}
37
+ }
38
+ }
39
+
40
+ async function saveConfig(data: Record<string, unknown>) {
41
+ await writeFile(getConfigPath(), JSON.stringify(data, null, 2))
42
+ }
43
+
44
+ const plugin: TuiPluginModule & { id: string } = {
45
+ id: "opencode-tui-utils.lsp-toggle",
46
+ async tui(rawApi) {
47
+ const api = createWrappedAPI(rawApi)
48
+
49
+ api.keymap.registerLayer({
50
+ commands: [
51
+ {
52
+ name: "opencode-tui-utils.lsp-toggle",
53
+ title: "Toggle LSP",
54
+ category: "Config",
55
+ namespace: "palette",
56
+ slashName: "lsp-toggle",
57
+ async run() {
58
+ const config = await loadConfig()
59
+ const isEnabled = !!config.lsp
60
+ const DialogConfirm = api.ui.DialogConfirm
61
+
62
+ api.ui.dialog.replace(() => (
63
+ <DialogConfirm
64
+ title="Toggle LSP"
65
+ message={
66
+ isEnabled
67
+ ? "LSP is currently enabled. Disable it?"
68
+ : "LSP is currently disabled. Enable it?"
69
+ }
70
+ onConfirm={async () => {
71
+ config.lsp = !isEnabled
72
+ await saveConfig(config)
73
+ api.ui.dialog.clear()
74
+
75
+ const nextEnabled = !isEnabled
76
+ api.ui.toast({
77
+ variant: nextEnabled ? "success" : "info",
78
+ title: nextEnabled ? "LSP Enabled" : "LSP Disabled",
79
+ message: nextEnabled
80
+ ? "LSP turned on. Restart opencode to apply."
81
+ : "LSP turned off. Restart opencode to apply.",
82
+ })
83
+ }}
84
+ onCancel={() => {
85
+ api.ui.dialog.clear()
86
+ }}
87
+ />
88
+ ))
89
+ },
90
+ },
91
+ ],
92
+ })
93
+ },
94
+ }
95
+
96
+ export default plugin