opencode-tui-utils 1.0.0 → 1.2.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.
- package/CONTRIBUTING.md +29 -18
- package/README.ja.md +18 -13
- package/README.ko.md +18 -13
- package/README.md +20 -13
- package/README.zh.md +18 -13
- package/package.json +1 -1
- package/src/core/api-wrapper.ts +9 -0
- package/src/index.tsx +6 -1
- package/src/plugins/export-chat.tsx +98 -0
- package/src/plugins/permissions.tsx +398 -0
- package/src/plugins/plugin-list.tsx +58 -0
- package/src/plugins/session-diff.tsx +69 -0
- package/src/plugins/session-todos.tsx +69 -0
package/CONTRIBUTING.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Thank you for taking the time to improve `opencode-tui-utils`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Goal:** Provide essential TUI commands missing from opencode. Install once, stop editing JSON by hand.
|
|
6
|
+
|
|
7
|
+
A good contribution feels native to opencode's TUI, solves one clear problem, and never changes how opencode itself works.
|
|
6
8
|
|
|
7
9
|
## Before You Start
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
1. **Check opencode built-ins first.** Native slash commands include `/connect`, `/init`, `/undo`, `/redo`, `/share`, `/models`. Native shortcuts include `Ctrl+T` (variant cycle) and `Ctrl+X` then `M` (model switch). We only add what is missing.
|
|
12
|
+
2. **Check existing issues and pull requests** for similar work.
|
|
13
|
+
3. **Keep the first version small.** One focused command is easier to review and test than a bundle.
|
|
14
|
+
4. **Avoid new runtime dependencies.** The project ships zero deps for a reason.
|
|
13
15
|
|
|
14
16
|
## Project Layout
|
|
15
17
|
|
|
@@ -133,23 +135,32 @@ fix: handle missing auth file in /disconnect
|
|
|
133
135
|
docs: clarify plugin installation
|
|
134
136
|
```
|
|
135
137
|
|
|
136
|
-
## Command
|
|
138
|
+
## What Makes a Good Command
|
|
139
|
+
|
|
140
|
+
The best commands expose data the TUI already has but offers no quick slash-command shortcut for. Think of the pain point behind [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494): the UI showed a connected provider, but there was no way to disconnect it without hand-editing JSON.
|
|
141
|
+
|
|
142
|
+
**Great fit checklist:**
|
|
143
|
+
- **TUI-native end-to-end.** No external terminal windows. Use palettes, dialogs, toasts, and selects.
|
|
144
|
+
- **One clear frustration.** "I have to open a config file to toggle this," or "I can't see X without clicking three menus."
|
|
145
|
+
- **Read-only or local-only.** Prefer commands that read from `api.state.*`, `api.plugins.list()`, or local config files. Avoid network calls when possible.
|
|
146
|
+
- **Doesn't touch opencode core.** We only change user configs or session state that opencode already exposes to plugins.
|
|
147
|
+
|
|
148
|
+
**Not a great fit:**
|
|
149
|
+
- Agent orchestration, prompt packs, or provider backends — these belong in dedicated plugins or opencode core.
|
|
150
|
+
- Heavy external services or large new dependencies.
|
|
151
|
+
|
|
152
|
+
## Command Ideas (Up for Grabs)
|
|
137
153
|
|
|
138
|
-
|
|
154
|
+
These are verified gaps based on the plugin API. Feel free to pick one and open an issue before starting.
|
|
139
155
|
|
|
140
|
-
|
|
|
141
|
-
| --- | --- |
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
| Config sanity checks | Useful local diagnostics with clear output. |
|
|
156
|
+
| Command | Problem it Solves | API Source |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| `/provider-list` | "What's connected right now?" before or after `/disconnect` | `api.state.provider` |
|
|
159
|
+
| `/session-info` | "How many messages in this session? What's the status?" | `api.state.session.*` |
|
|
145
160
|
|
|
146
|
-
|
|
161
|
+
**Already covered by opencode built-ins — do not propose:** theme switch, MCP/LSP status viewers, clipboard copy. The TUI already provides these.
|
|
147
162
|
|
|
148
|
-
|
|
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. |
|
|
163
|
+
If you have a new idea, open an issue with the problem statement first. We'll confirm it doesn't duplicate a built-in before you start coding.
|
|
153
164
|
|
|
154
165
|
## License
|
|
155
166
|
|
package/README.ja.md
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
[](https://github.com/Blue-B/opencode-tui-utils/actions)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
opencodeに欠けているエッセンシャルなTUIコマンド。一度インストールすれば、JSONを手動で編集する必要はありません。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
このプロジェクトはコマンドを1つずつ追加します。各ユーティリティは、opencode TUIが既に持っているがスラッシュコマンドでアクセスできないデータを公開することを目的としています(opencode issue #10494のパターン)。
|
|
16
16
|
|
|
17
17
|
## プレビュー
|
|
18
18
|
|
|
@@ -60,10 +60,25 @@ opencodeを再起動して実行します。
|
|
|
60
60
|
|
|
61
61
|
## コマンド
|
|
62
62
|
|
|
63
|
+
### 利用可能
|
|
64
|
+
|
|
63
65
|
| コマンド | エイリアス | 説明 |
|
|
64
66
|
| --- | --- | --- |
|
|
65
67
|
| `/disconnect` | `/dc` | 接続済みプロバイダーを1つ選び、opencodeの認証ストレージから削除します。新しいセションを開くと変更が反映されます。 |
|
|
66
68
|
| `/lsp-toggle` | — | `~/.config/opencode/opencode.json` の `lsp` 設定を true/false に切り替えます。opencodeの再起動が必要です。 |
|
|
69
|
+
| `/plugin-list` | — | インストールされたプラグインとその有効状態を表示します。 |
|
|
70
|
+
| `/export-chat` | — | 現在のセッションのチャットをプロジェクトディレクトリにマークダウンとして保存します。 |
|
|
71
|
+
| `/session-diff` | — | 現在のセッションで変更されたファイル一覧を表示します。 |
|
|
72
|
+
| `/session-todos` | — | 現在のセッションのタスク一覧を表示します。 |
|
|
73
|
+
|
|
74
|
+
### アイデア・コントリビューション歓迎
|
|
75
|
+
|
|
76
|
+
TUIが既に持っているがスラッシュコマンドでアクセスできないデータを公開する候補です。気に入ったアイデアがあればIssueを立ててください。
|
|
77
|
+
|
|
78
|
+
| コマンド | 解決する問題 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `/provider-list` | "今何が接続されている?" — `/disconnect` の読み取り専用版 |
|
|
81
|
+
| `/session-info` | "このセッションに何通のメッセージがある?状態は?" |
|
|
67
82
|
|
|
68
83
|
## `/disconnect` の動作
|
|
69
84
|
|
|
@@ -97,7 +112,7 @@ opencodeを再起動して実行します。
|
|
|
97
112
|
|
|
98
113
|
## ユーティリティの追加
|
|
99
114
|
|
|
100
|
-
新しいコマンドは `src/plugins/` 配下に個別のプラグインモジュールとして追加し、`src/index.tsx
|
|
115
|
+
新しいコマンドは `src/plugins/` 配下に個別のプラグインモジュールとして追加し、`src/index.tsx`から登録します。追加したいアイデアがあれば [CONTRIBUTING.md](CONTRIBUTING.md) の Command Ideas リストを確認してください。
|
|
101
116
|
|
|
102
117
|
```text
|
|
103
118
|
src/
|
|
@@ -121,16 +136,6 @@ npm install
|
|
|
121
136
|
npm run build
|
|
122
137
|
```
|
|
123
138
|
|
|
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
139
|
## コントリビューション
|
|
135
140
|
|
|
136
141
|
IssueとPRを歓迎します。新しいコマンドを追加する前に[CONTRIBUTING.md](CONTRIBUTING.md)を確認してください。小さく、目的がはっきりした変更を歓迎します。
|
package/README.ko.md
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
[](https://github.com/Blue-B/opencode-tui-utils/actions)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
opencode에 없는 필수 TUI 명령어를 제공합니다. 한 번 설치하고 JSON 파일 직접 편집은 그만하세요.
|
|
14
14
|
|
|
15
|
-
이 프로젝트는 명령어를 하나씩
|
|
15
|
+
이 프로젝트는 명령어를 하나씩 확장합니다. 각 유틸리티는 opencode TUI가 이미 가지고 있지만 슬래시 명령어로 접근할 수 없는 데이터를 노출하는 것을 목표로 합니다(opencode issue #10494의 패턴).
|
|
16
16
|
|
|
17
17
|
## 미리보기
|
|
18
18
|
|
|
@@ -60,10 +60,25 @@ opencode를 재시작한 뒤 실행합니다.
|
|
|
60
60
|
|
|
61
61
|
## 명령어
|
|
62
62
|
|
|
63
|
+
### 사용 가능
|
|
64
|
+
|
|
63
65
|
| 명령어 | 별칭 | 설명 |
|
|
64
66
|
| --- | --- | --- |
|
|
65
67
|
| `/disconnect` | `/dc` | 연결된 프로바이더 중 하나를 선택해 opencode 인증 저장소에서 제거합니다. 새 세션을 열어야 변경 사항이 반영됩니다. |
|
|
66
68
|
| `/lsp-toggle` | — | `~/.config/opencode/opencode.json`의 `lsp` 설정을 true/false로 전환합니다. opencode 재시작 필요. |
|
|
69
|
+
| `/plugin-list` | — | 설치된 플러그인과 활성화 상태를 보여줍니다. |
|
|
70
|
+
| `/export-chat` | — | 현재 세션 대화를 프로젝트 디렉토리에 마크다운 파일로 저장합니다. |
|
|
71
|
+
| `/session-diff` | — | 현재 세션에서 변경된 파일 목록을 보여줍니다. |
|
|
72
|
+
| `/session-todos` | — | 현재 세션의 할 일 목록을 보여줍니다. |
|
|
73
|
+
|
|
74
|
+
### 아이디어 및 기여 환영
|
|
75
|
+
|
|
76
|
+
TUI가 이미 가지고 있지만 슬래시 명령어가 없는 데이터를 노출하는 후보입니다. 마음에 드는 아이디어가 있으면 이슈를 열어주세요.
|
|
77
|
+
|
|
78
|
+
| 명령어 | 해결하는 문제 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `/provider-list` | "지금 뭐가 연결되어 있지?" — `/disconnect`의 read-only 버전 |
|
|
81
|
+
| `/session-info` | "이 세션에 메시지가 몇 개 있지? 상태는?" |
|
|
67
82
|
|
|
68
83
|
## `/disconnect` 동작 방식
|
|
69
84
|
|
|
@@ -97,7 +112,7 @@ opencode를 재시작한 뒤 실행합니다.
|
|
|
97
112
|
|
|
98
113
|
## 유틸리티 확장 방법
|
|
99
114
|
|
|
100
|
-
새 명령어는 `src/plugins/` 아래에 별도 플러그인 모듈로 추가하고, `src/index.tsx`에서 등록합니다.
|
|
115
|
+
새 명령어는 `src/plugins/` 아래에 별도 플러그인 모듈로 추가하고, `src/index.tsx`에서 등록합니다. 추가할 아이디어가 있다면 [CONTRIBUTING.md](CONTRIBUTING.md)의 Command Ideas 목록을 확인해 주세요.
|
|
101
116
|
|
|
102
117
|
```text
|
|
103
118
|
src/
|
|
@@ -172,16 +187,6 @@ npm run build
|
|
|
172
187
|
|
|
173
188
|
`tui.json` 변경 후에는 opencode를 재시작해야 합니다.
|
|
174
189
|
|
|
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
190
|
## 기여
|
|
186
191
|
|
|
187
192
|
이슈와 PR은 환영합니다. 새 명령어를 추가하기 전에는 [CONTRIBUTING.md](CONTRIBUTING.md)를 확인해 주세요. 작고 명확한 변경을 선호합니다.
|
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ English | [한국어](./README.ko.md) | [日本語](./README.ja.md) | [中文](.
|
|
|
10
10
|
[](https://github.com/Blue-B/opencode-tui-utils/actions)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Essential TUI commands missing from [opencode](https://opencode.ai). Install once, stop editing JSON by hand.
|
|
14
14
|
|
|
15
|
-
This project is built to grow command by command. Each utility
|
|
15
|
+
This project is built to grow command by command. Each utility exposes data the TUI already holds but lacks a quick slash-command shortcut for (pattern established by [opencode issue #10494](https://github.com/anomalyco/opencode/issues/10494)).
|
|
16
16
|
|
|
17
17
|
## Preview
|
|
18
18
|
|
|
@@ -33,7 +33,7 @@ The command uses opencode's own TUI dialog components, so it appears inside the
|
|
|
33
33
|
| Remove one provider safely | Select a provider in the TUI and remove only that auth entry |
|
|
34
34
|
| Avoid manual JSON edits | No need to open or hand-edit `~/.local/share/opencode/auth.json` |
|
|
35
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 |
|
|
36
|
+
| Add more small commands | Shared plugin loader and API wrapper make new utilities straightforward. [See command ideas →](CONTRIBUTING.md#command-ideas-up-for-grabs) |
|
|
37
37
|
|
|
38
38
|
## Quick Start
|
|
39
39
|
|
|
@@ -60,10 +60,25 @@ Restart opencode and run:
|
|
|
60
60
|
|
|
61
61
|
## Commands
|
|
62
62
|
|
|
63
|
+
### Available
|
|
64
|
+
|
|
63
65
|
| Command | Alias | Description |
|
|
64
66
|
| --- | --- | --- |
|
|
65
67
|
| `/disconnect` | `/dc` | Pick one connected provider and remove it from opencode auth storage. Open a new session to reflect the change. |
|
|
66
68
|
| `/lsp-toggle` | — | Toggle `lsp: true/false` in `~/.config/opencode/opencode.json`. Requires opencode restart. |
|
|
69
|
+
| `/plugin-list` | — | Show installed plugins and their activation state. |
|
|
70
|
+
| `/export-chat` | — | Export the current session chat to a markdown file in the project directory. |
|
|
71
|
+
| `/session-diff` | — | List files changed in the current session. |
|
|
72
|
+
| `/session-todos` | — | Show pending todos for the current session. |
|
|
73
|
+
|
|
74
|
+
### Ideas & Contributions Welcome
|
|
75
|
+
|
|
76
|
+
These are verified gaps where the TUI already holds the data but offers no slash-command shortcut. Pick one and open an issue.
|
|
77
|
+
|
|
78
|
+
| Command | Problem it Solves |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `/provider-list` | "What's connected right now?" — read-only counterpart to `/disconnect` |
|
|
81
|
+
| `/session-info` | "How many messages in this session? What's the status?" |
|
|
67
82
|
|
|
68
83
|
## How `/disconnect` works
|
|
69
84
|
|
|
@@ -110,6 +125,8 @@ src/
|
|
|
110
125
|
index.tsx Public plugin entry point
|
|
111
126
|
```
|
|
112
127
|
|
|
128
|
+
**Want to build one?** Check the [Command Ideas](CONTRIBUTING.md#command-ideas-up-for-grabs) list in `CONTRIBUTING.md`. Each idea exposes data the TUI already has but lacks a slash-command shortcut for.
|
|
129
|
+
|
|
113
130
|
Minimal command shape:
|
|
114
131
|
|
|
115
132
|
```typescript
|
|
@@ -178,16 +195,6 @@ Local source-file testing:
|
|
|
178
195
|
|
|
179
196
|
Restart opencode after changing `tui.json`.
|
|
180
197
|
|
|
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
198
|
## Contributing
|
|
192
199
|
|
|
193
200
|
Issues and PRs are welcome. Please keep changes small and focused. See [CONTRIBUTING.md](CONTRIBUTING.md) before adding a new command.
|
package/README.zh.md
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
[](https://github.com/Blue-B/opencode-tui-utils/actions)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
opencode 缺少的必备 TUI 命令。安装一次,再也不用亲手编辑 JSON。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
这个项目逐个添加命令。每个工具的目标都是公开 opencode TUI 已经拥有但无法通过斜杠命令访问的数据(opencode issue #10494 的模式)。
|
|
16
16
|
|
|
17
17
|
## 预览
|
|
18
18
|
|
|
@@ -60,10 +60,25 @@ opencode plugin opencode-tui-utils
|
|
|
60
60
|
|
|
61
61
|
## 命令
|
|
62
62
|
|
|
63
|
+
### 可用
|
|
64
|
+
|
|
63
65
|
| 命令 | 别名 | 说明 |
|
|
64
66
|
| --- | --- | --- |
|
|
65
67
|
| `/disconnect` | `/dc` | 选择一个已连接的 provider,并从 opencode 的认证存储中移除。需要打开新会话才能看到变更。 |
|
|
66
68
|
| `/lsp-toggle` | — | 切换 `~/.config/opencode/opencode.json` 中的 `lsp` 设置。需要重启 opencode。 |
|
|
69
|
+
| `/plugin-list` | — | 显示已安装的插件及其激活状态。 |
|
|
70
|
+
| `/export-chat` | — | 将当前会话的聊天记录导出为项目目录中的 Markdown 文件。 |
|
|
71
|
+
| `/session-diff` | — | 列出当前会话中已更改的文件。 |
|
|
72
|
+
| `/session-todos` | — | 显示当前会话的待办事项列表。 |
|
|
73
|
+
|
|
74
|
+
### 欢迎提交想法与贡献
|
|
75
|
+
|
|
76
|
+
这些候选命令公开 TUI 已有但缺少斜杠命令快捷方式的数据。有喜欢的想法请开 Issue。
|
|
77
|
+
|
|
78
|
+
| 命令 | 解决的问题 |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `/provider-list` | "现在连接了什么?" — `/disconnect` 的只读版本 |
|
|
81
|
+
| `/session-info` | "这个会话有多少条消息?状态是什么?" |
|
|
67
82
|
|
|
68
83
|
## `/disconnect` 如何工作
|
|
69
84
|
|
|
@@ -97,7 +112,7 @@ opencode plugin opencode-tui-utils
|
|
|
97
112
|
|
|
98
113
|
## 添加更多工具
|
|
99
114
|
|
|
100
|
-
新命令作为单独的插件模块放在 `src/plugins/` 下,并从 `src/index.tsx`
|
|
115
|
+
新命令作为单独的插件模块放在 `src/plugins/` 下,并从 `src/index.tsx` 注册。如果有想添加的想法,请查看 [CONTRIBUTING.md](CONTRIBUTING.md) 中的 Command Ideas 列表。
|
|
101
116
|
|
|
102
117
|
```text
|
|
103
118
|
src/
|
|
@@ -121,16 +136,6 @@ npm install
|
|
|
121
136
|
npm run build
|
|
122
137
|
```
|
|
123
138
|
|
|
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
139
|
## 贡献
|
|
135
140
|
|
|
136
141
|
欢迎 issue 和 PR。添加新命令前请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。更推荐小而清晰的改动。
|
package/package.json
CHANGED
package/src/core/api-wrapper.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface WrappedAPI {
|
|
|
11
11
|
keymap: ReturnType<typeof createKeymapAPI>
|
|
12
12
|
ui: ReturnType<typeof createUIAPI>
|
|
13
13
|
kv: ReturnType<typeof createKVAPI>
|
|
14
|
+
plugins: TuiPluginApi["plugins"]
|
|
15
|
+
client: TuiPluginApi["client"]
|
|
16
|
+
route: TuiPluginApi["route"]
|
|
17
|
+
state: TuiPluginApi["state"]
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
@@ -44,6 +48,7 @@ export function createUIAPI(api: TuiPluginApi) {
|
|
|
44
48
|
DialogSelect: api.ui.DialogSelect,
|
|
45
49
|
DialogConfirm: api.ui.DialogConfirm,
|
|
46
50
|
DialogAlert: api.ui.DialogAlert,
|
|
51
|
+
DialogPrompt: api.ui.DialogPrompt,
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
@@ -94,5 +99,9 @@ export function createWrappedAPI(api: TuiPluginApi): WrappedAPI {
|
|
|
94
99
|
keymap: createKeymapAPI(api),
|
|
95
100
|
ui: createUIAPI(api),
|
|
96
101
|
kv: createKVAPI(api),
|
|
102
|
+
plugins: api.plugins,
|
|
103
|
+
client: api.client,
|
|
104
|
+
route: api.route,
|
|
105
|
+
state: api.state,
|
|
97
106
|
}
|
|
98
107
|
}
|
package/src/index.tsx
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
6
6
|
import disconnectPlugin from "./plugins/disconnect"
|
|
7
7
|
import lspTogglePlugin from "./plugins/lsp-toggle"
|
|
8
|
+
import pluginListPlugin from "./plugins/plugin-list"
|
|
9
|
+
import exportChatPlugin from "./plugins/export-chat"
|
|
10
|
+
import sessionDiffPlugin from "./plugins/session-diff"
|
|
11
|
+
import sessionTodosPlugin from "./plugins/session-todos"
|
|
12
|
+
import permissionsPlugin from "./plugins/permissions"
|
|
8
13
|
|
|
9
|
-
const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin]
|
|
14
|
+
const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin, pluginListPlugin, exportChatPlugin, sessionDiffPlugin, sessionTodosPlugin, permissionsPlugin]
|
|
10
15
|
|
|
11
16
|
export async function initializePlugins(...args: Parameters<TuiPluginModule["tui"]>) {
|
|
12
17
|
for (const plugin of plugins) {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import type { Part, TextPart } from "@opencode-ai/sdk/v2"
|
|
4
|
+
import { writeFile } from "node:fs/promises"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
import { createWrappedAPI } from "../core/api-wrapper"
|
|
7
|
+
|
|
8
|
+
function isTextPart(part: Part): part is TextPart {
|
|
9
|
+
return part.type === "text"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
13
|
+
id: "opencode-tui-utils.export-chat",
|
|
14
|
+
async tui(rawApi) {
|
|
15
|
+
const api = createWrappedAPI(rawApi)
|
|
16
|
+
|
|
17
|
+
api.keymap.registerLayer({
|
|
18
|
+
commands: [
|
|
19
|
+
{
|
|
20
|
+
name: "opencode-tui-utils.export-chat",
|
|
21
|
+
title: "Export Chat",
|
|
22
|
+
category: "Session",
|
|
23
|
+
namespace: "palette",
|
|
24
|
+
slashName: "export-chat",
|
|
25
|
+
async run() {
|
|
26
|
+
const route = api.route.current
|
|
27
|
+
if (route.name !== "session" || !route.params?.sessionID) {
|
|
28
|
+
api.ui.toast({
|
|
29
|
+
title: "No session",
|
|
30
|
+
message: "You must be in a session to export.",
|
|
31
|
+
})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sessionID = String(route.params.sessionID)
|
|
36
|
+
const messages = api.state.session.messages(sessionID)
|
|
37
|
+
|
|
38
|
+
if (messages.length === 0) {
|
|
39
|
+
api.ui.toast({
|
|
40
|
+
title: "Empty",
|
|
41
|
+
message: "No messages to export.",
|
|
42
|
+
})
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines: string[] = ["# OpenCode Chat Export\n"]
|
|
47
|
+
|
|
48
|
+
for (const msg of messages) {
|
|
49
|
+
if (msg.role === "user") {
|
|
50
|
+
lines.push(
|
|
51
|
+
`\n## User (${msg.agent} / ${msg.model.providerID} / ${msg.model.modelID})`,
|
|
52
|
+
)
|
|
53
|
+
lines.push(`Time: ${new Date(msg.time.created).toISOString()}\n`)
|
|
54
|
+
const parts = api.state.part(msg.id)
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (isTextPart(part)) {
|
|
57
|
+
lines.push(part.text)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if (msg.role === "assistant") {
|
|
61
|
+
lines.push(`\n## Assistant (${msg.agent} / ${msg.modelID})`)
|
|
62
|
+
lines.push(`Time: ${new Date(msg.time.created).toISOString()}\n`)
|
|
63
|
+
const parts = api.state.part(msg.id)
|
|
64
|
+
for (const part of parts) {
|
|
65
|
+
if (isTextPart(part)) {
|
|
66
|
+
lines.push(part.text)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
lines.push("")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const markdown = lines.join("\n")
|
|
74
|
+
const filename = `opencode-chat-${Date.now()}.md`
|
|
75
|
+
const filepath = join(api.state.path.directory, filename)
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await writeFile(filepath, markdown, "utf-8")
|
|
79
|
+
api.ui.toast({
|
|
80
|
+
variant: "success",
|
|
81
|
+
title: "Exported",
|
|
82
|
+
message: `Saved to ${filepath}`,
|
|
83
|
+
})
|
|
84
|
+
} catch (err) {
|
|
85
|
+
api.ui.toast({
|
|
86
|
+
variant: "error",
|
|
87
|
+
title: "Export failed",
|
|
88
|
+
message: String(err),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export default plugin
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
/**
|
|
3
|
+
* /permissions
|
|
4
|
+
*
|
|
5
|
+
* Manage opencode permission settings via TUI instead of hand-editing
|
|
6
|
+
* ~/.config/opencode/opencode.json.
|
|
7
|
+
*
|
|
8
|
+
* Supports:
|
|
9
|
+
* - View current permission config
|
|
10
|
+
* - Set global permission mode (ask / allow / deny)
|
|
11
|
+
* - Toggle per-tool permission
|
|
12
|
+
* - Manage pattern-based permissions (e.g. external_directory)
|
|
13
|
+
* - Reset (remove) permission config
|
|
14
|
+
*/
|
|
15
|
+
import type { TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
16
|
+
import { homedir } from "node:os"
|
|
17
|
+
import { join } from "node:path"
|
|
18
|
+
import { readFile, writeFile } from "node:fs/promises"
|
|
19
|
+
import { createWrappedAPI } from "../core/api-wrapper"
|
|
20
|
+
|
|
21
|
+
function getConfigPath() {
|
|
22
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
23
|
+
return join(process.env.OPENCODE_CONFIG_DIR, "opencode.json")
|
|
24
|
+
}
|
|
25
|
+
return join(homedir(), ".config", "opencode", "opencode.json")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(getConfigPath(), "utf-8")
|
|
31
|
+
return JSON.parse(content) as Record<string, any>
|
|
32
|
+
} catch {
|
|
33
|
+
return {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function saveConfig(data: Record<string, any>) {
|
|
38
|
+
await writeFile(getConfigPath(), JSON.stringify(data, null, 2))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TOOLS = [
|
|
42
|
+
"read",
|
|
43
|
+
"edit",
|
|
44
|
+
"glob",
|
|
45
|
+
"grep",
|
|
46
|
+
"list",
|
|
47
|
+
"bash",
|
|
48
|
+
"task",
|
|
49
|
+
"external_directory",
|
|
50
|
+
"todowrite",
|
|
51
|
+
"question",
|
|
52
|
+
"webfetch",
|
|
53
|
+
"websearch",
|
|
54
|
+
"codesearch",
|
|
55
|
+
"repo_clone",
|
|
56
|
+
"repo_overview",
|
|
57
|
+
"lsp",
|
|
58
|
+
"doom_loop",
|
|
59
|
+
"skill",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const PATTERN_TOOLS = [
|
|
63
|
+
"read",
|
|
64
|
+
"edit",
|
|
65
|
+
"glob",
|
|
66
|
+
"grep",
|
|
67
|
+
"list",
|
|
68
|
+
"bash",
|
|
69
|
+
"task",
|
|
70
|
+
"external_directory",
|
|
71
|
+
"repo_clone",
|
|
72
|
+
"repo_overview",
|
|
73
|
+
"lsp",
|
|
74
|
+
"skill",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const ACTIONS: Array<{ title: string; value: string }> = [
|
|
78
|
+
{ title: "ask - always prompt", value: "ask" },
|
|
79
|
+
{ title: "allow - auto approve", value: "allow" },
|
|
80
|
+
{ title: "deny - always reject", value: "deny" },
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
function getPermission(config: Record<string, any>) {
|
|
84
|
+
return config.permission
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getToolCurrent(config: Record<string, any>, tool: string): string {
|
|
88
|
+
const perm = getPermission(config)
|
|
89
|
+
if (typeof perm === "string") return perm
|
|
90
|
+
if (perm && typeof perm === "object") {
|
|
91
|
+
const val = perm[tool]
|
|
92
|
+
if (typeof val === "string") return val
|
|
93
|
+
if (typeof val === "object" && val !== null) return "(pattern-based)"
|
|
94
|
+
}
|
|
95
|
+
return "not set (default: ask)"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ensureObjectPermission(config: Record<string, any>) {
|
|
99
|
+
if (!config.permission || typeof config.permission !== "object") {
|
|
100
|
+
config.permission = {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function setToolAction(config: Record<string, any>, tool: string, action: string) {
|
|
105
|
+
ensureObjectPermission(config)
|
|
106
|
+
config.permission[tool] = action
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setToolPattern(config: Record<string, any>, tool: string, pattern: string, action: string) {
|
|
110
|
+
ensureObjectPermission(config)
|
|
111
|
+
if (!config.permission[tool] || typeof config.permission[tool] !== "object") {
|
|
112
|
+
config.permission[tool] = {}
|
|
113
|
+
}
|
|
114
|
+
config.permission[tool][pattern] = action
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function removeToolPattern(config: Record<string, any>, tool: string, pattern: string) {
|
|
118
|
+
ensureObjectPermission(config)
|
|
119
|
+
const toolCfg = config.permission[tool]
|
|
120
|
+
if (toolCfg && typeof toolCfg === "object") {
|
|
121
|
+
delete toolCfg[pattern]
|
|
122
|
+
if (Object.keys(toolCfg).length === 0) {
|
|
123
|
+
delete config.permission[tool]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatPermission(config: Record<string, any>): string {
|
|
129
|
+
const perm = getPermission(config)
|
|
130
|
+
if (perm === undefined) return "No permission config set."
|
|
131
|
+
return JSON.stringify(perm, null, 2)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
135
|
+
id: "opencode-tui-utils.permissions",
|
|
136
|
+
async tui(rawApi) {
|
|
137
|
+
const api = createWrappedAPI(rawApi)
|
|
138
|
+
const { DialogSelect, DialogConfirm, DialogAlert, DialogPrompt } = api.ui
|
|
139
|
+
|
|
140
|
+
const mainMenu = async () => {
|
|
141
|
+
const config = await loadConfig()
|
|
142
|
+
|
|
143
|
+
api.ui.dialog.replace(() => (
|
|
144
|
+
<DialogSelect
|
|
145
|
+
title="Permission Manager"
|
|
146
|
+
options={[
|
|
147
|
+
{ title: "View current permissions", value: "view" },
|
|
148
|
+
{ title: "Set global permission mode", value: "global" },
|
|
149
|
+
{ title: "Toggle per-tool permission", value: "tool" },
|
|
150
|
+
{ title: "Manage pattern permissions", value: "pattern" },
|
|
151
|
+
{ title: "Reset permissions", value: "reset" },
|
|
152
|
+
]}
|
|
153
|
+
onSelect={(option) => {
|
|
154
|
+
if (!option) return
|
|
155
|
+
switch (option.value) {
|
|
156
|
+
case "view":
|
|
157
|
+
viewPermissions(config)
|
|
158
|
+
break
|
|
159
|
+
case "global":
|
|
160
|
+
setGlobalMode(config)
|
|
161
|
+
break
|
|
162
|
+
case "tool":
|
|
163
|
+
selectTool(config)
|
|
164
|
+
break
|
|
165
|
+
case "pattern":
|
|
166
|
+
selectPatternTool(config)
|
|
167
|
+
break
|
|
168
|
+
case "reset":
|
|
169
|
+
confirmReset(config)
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const viewPermissions = (config: Record<string, any>) => {
|
|
178
|
+
api.ui.dialog.replace(() => (
|
|
179
|
+
<DialogAlert
|
|
180
|
+
title="Current Permissions"
|
|
181
|
+
message={formatPermission(config)}
|
|
182
|
+
/>
|
|
183
|
+
))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const setGlobalMode = (config: Record<string, any>) => {
|
|
187
|
+
api.ui.dialog.replace(() => (
|
|
188
|
+
<DialogSelect
|
|
189
|
+
title="Set global permission mode"
|
|
190
|
+
placeholder="This overrides every tool unless individually configured"
|
|
191
|
+
options={ACTIONS}
|
|
192
|
+
onSelect={(option) => {
|
|
193
|
+
if (!option) return
|
|
194
|
+
void (async () => {
|
|
195
|
+
config.permission = option.value
|
|
196
|
+
await saveConfig(config)
|
|
197
|
+
api.ui.dialog.clear()
|
|
198
|
+
api.ui.toast({
|
|
199
|
+
variant: "success",
|
|
200
|
+
title: "Global permission set",
|
|
201
|
+
message: `All tools default to "${option.value}".`,
|
|
202
|
+
})
|
|
203
|
+
})()
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const selectTool = (config: Record<string, any>) => {
|
|
210
|
+
const options = TOOLS.map((t) => ({
|
|
211
|
+
title: `${t} [${getToolCurrent(config, t)}]`,
|
|
212
|
+
value: t,
|
|
213
|
+
}))
|
|
214
|
+
|
|
215
|
+
api.ui.dialog.replace(() => (
|
|
216
|
+
<DialogSelect
|
|
217
|
+
title="Select tool to configure"
|
|
218
|
+
options={options}
|
|
219
|
+
onSelect={(option) => {
|
|
220
|
+
if (!option) return
|
|
221
|
+
setToolMode(config, option.value)
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const setToolMode = (config: Record<string, any>, tool: string) => {
|
|
228
|
+
api.ui.dialog.replace(() => (
|
|
229
|
+
<DialogSelect
|
|
230
|
+
title={`Set permission for "${tool}"`}
|
|
231
|
+
options={ACTIONS}
|
|
232
|
+
onSelect={(option) => {
|
|
233
|
+
if (!option) return
|
|
234
|
+
void (async () => {
|
|
235
|
+
setToolAction(config, tool, option.value)
|
|
236
|
+
await saveConfig(config)
|
|
237
|
+
api.ui.dialog.clear()
|
|
238
|
+
api.ui.toast({
|
|
239
|
+
variant: "success",
|
|
240
|
+
title: "Permission updated",
|
|
241
|
+
message: `"${tool}" is now "${option.value}".`,
|
|
242
|
+
})
|
|
243
|
+
})()
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const selectPatternTool = (config: Record<string, any>) => {
|
|
250
|
+
const options = PATTERN_TOOLS.map((t) => ({
|
|
251
|
+
title: `${t} [${getToolCurrent(config, t)}]`,
|
|
252
|
+
value: t,
|
|
253
|
+
}))
|
|
254
|
+
|
|
255
|
+
api.ui.dialog.replace(() => (
|
|
256
|
+
<DialogSelect
|
|
257
|
+
title="Select tool for pattern management"
|
|
258
|
+
options={options}
|
|
259
|
+
onSelect={(option) => {
|
|
260
|
+
if (!option) return
|
|
261
|
+
managePatterns(config, option.value)
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const managePatterns = (config: Record<string, any>, tool: string) => {
|
|
268
|
+
const toolCfg = config.permission?.[tool]
|
|
269
|
+
const patterns =
|
|
270
|
+
toolCfg && typeof toolCfg === "object"
|
|
271
|
+
? Object.entries(toolCfg).map(([k, v]) => ({ title: `${k} -> ${v}`, value: k }))
|
|
272
|
+
: []
|
|
273
|
+
|
|
274
|
+
const options = [
|
|
275
|
+
{ title: "Add new pattern", value: "__add__" },
|
|
276
|
+
...patterns,
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
api.ui.dialog.replace(() => (
|
|
280
|
+
<DialogSelect
|
|
281
|
+
title={`Patterns for "${tool}"${patterns.length === 0 ? " (none set)" : ""}`}
|
|
282
|
+
options={options}
|
|
283
|
+
onSelect={(option) => {
|
|
284
|
+
if (!option) return
|
|
285
|
+
if (option.value === "__add__") {
|
|
286
|
+
addPattern(config, tool)
|
|
287
|
+
} else {
|
|
288
|
+
confirmRemovePattern(config, tool, option.value)
|
|
289
|
+
}
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const addPattern = (config: Record<string, any>, tool: string) => {
|
|
296
|
+
api.ui.dialog.replace(() => (
|
|
297
|
+
<DialogPrompt
|
|
298
|
+
title={`Add pattern for "${tool}"`}
|
|
299
|
+
placeholder="/tmp/workspace/* or /home/shell/*"
|
|
300
|
+
onConfirm={(patternValue: string) => {
|
|
301
|
+
const trimmed = patternValue.trim()
|
|
302
|
+
if (!trimmed) {
|
|
303
|
+
api.ui.dialog.clear()
|
|
304
|
+
api.ui.toast({ title: "Cancelled", message: "Empty pattern." })
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
api.ui.dialog.replace(() => (
|
|
308
|
+
<DialogSelect
|
|
309
|
+
title={`Action for "${trimmed}"`}
|
|
310
|
+
options={ACTIONS}
|
|
311
|
+
onSelect={(actionOpt) => {
|
|
312
|
+
if (!actionOpt) {
|
|
313
|
+
api.ui.dialog.clear()
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
void (async () => {
|
|
317
|
+
setToolPattern(config, tool, trimmed, actionOpt.value)
|
|
318
|
+
await saveConfig(config)
|
|
319
|
+
api.ui.dialog.clear()
|
|
320
|
+
api.ui.toast({
|
|
321
|
+
variant: "success",
|
|
322
|
+
title: "Pattern added",
|
|
323
|
+
message: `"${trimmed}" -> ${actionOpt.value}`,
|
|
324
|
+
})
|
|
325
|
+
})()
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
))
|
|
329
|
+
}}
|
|
330
|
+
onCancel={() => {
|
|
331
|
+
api.ui.dialog.clear()
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const confirmRemovePattern = (config: Record<string, any>, tool: string, pattern: string) => {
|
|
338
|
+
api.ui.dialog.replace(() => (
|
|
339
|
+
<DialogConfirm
|
|
340
|
+
title="Remove pattern?"
|
|
341
|
+
message={`Delete "${pattern}" from "${tool}"?`}
|
|
342
|
+
onConfirm={async () => {
|
|
343
|
+
removeToolPattern(config, tool, pattern)
|
|
344
|
+
await saveConfig(config)
|
|
345
|
+
api.ui.dialog.clear()
|
|
346
|
+
api.ui.toast({
|
|
347
|
+
variant: "success",
|
|
348
|
+
title: "Pattern removed",
|
|
349
|
+
message: `"${pattern}" deleted from "${tool}".`,
|
|
350
|
+
})
|
|
351
|
+
}}
|
|
352
|
+
onCancel={() => {
|
|
353
|
+
api.ui.dialog.clear()
|
|
354
|
+
}}
|
|
355
|
+
/>
|
|
356
|
+
))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const confirmReset = (config: Record<string, any>) => {
|
|
360
|
+
api.ui.dialog.replace(() => (
|
|
361
|
+
<DialogConfirm
|
|
362
|
+
title="Reset permissions?"
|
|
363
|
+
message="This removes the entire 'permission' field from opencode.json."
|
|
364
|
+
onConfirm={async () => {
|
|
365
|
+
delete config.permission
|
|
366
|
+
await saveConfig(config)
|
|
367
|
+
api.ui.dialog.clear()
|
|
368
|
+
api.ui.toast({
|
|
369
|
+
variant: "success",
|
|
370
|
+
title: "Permissions reset",
|
|
371
|
+
message: "Permission config removed. Default behavior restored.",
|
|
372
|
+
})
|
|
373
|
+
}}
|
|
374
|
+
onCancel={() => {
|
|
375
|
+
api.ui.dialog.clear()
|
|
376
|
+
}}
|
|
377
|
+
/>
|
|
378
|
+
))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
api.keymap.registerLayer({
|
|
382
|
+
commands: [
|
|
383
|
+
{
|
|
384
|
+
name: "opencode-tui-utils.permissions",
|
|
385
|
+
title: "Manage Permissions",
|
|
386
|
+
category: "Config",
|
|
387
|
+
namespace: "palette",
|
|
388
|
+
slashName: "permissions",
|
|
389
|
+
async run() {
|
|
390
|
+
await mainMenu()
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
})
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default plugin
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginModule, TuiPluginStatus } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { createWrappedAPI } from "../core/api-wrapper"
|
|
4
|
+
|
|
5
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
6
|
+
id: "opencode-tui-utils.plugin-list",
|
|
7
|
+
async tui(rawApi) {
|
|
8
|
+
const api = createWrappedAPI(rawApi)
|
|
9
|
+
const { DialogSelect, DialogAlert } = api.ui
|
|
10
|
+
|
|
11
|
+
api.keymap.registerLayer({
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "opencode-tui-utils.plugin-list",
|
|
15
|
+
title: "List Plugins",
|
|
16
|
+
category: "System",
|
|
17
|
+
namespace: "palette",
|
|
18
|
+
slashName: "plugin-list",
|
|
19
|
+
async run() {
|
|
20
|
+
const plugins = api.plugins.list()
|
|
21
|
+
if (plugins.length === 0) {
|
|
22
|
+
api.ui.toast({
|
|
23
|
+
title: "No plugins",
|
|
24
|
+
message: "No plugins installed.",
|
|
25
|
+
})
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const options = plugins.map((p: TuiPluginStatus) => ({
|
|
30
|
+
title: `${p.id} (${p.enabled ? "enabled" : "disabled"}, ${p.active ? "active" : "inactive"})`,
|
|
31
|
+
value: p.id,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
api.ui.dialog.replace(() => (
|
|
35
|
+
<DialogSelect
|
|
36
|
+
title="Installed plugins"
|
|
37
|
+
options={options}
|
|
38
|
+
onSelect={(option) => {
|
|
39
|
+
if (!option) return
|
|
40
|
+
const plugin = plugins.find((p: TuiPluginStatus) => p.id === option.value)
|
|
41
|
+
if (!plugin) return
|
|
42
|
+
api.ui.dialog.replace(() => (
|
|
43
|
+
<DialogAlert
|
|
44
|
+
title={plugin.id}
|
|
45
|
+
message={`Source: ${plugin.source}\nEnabled: ${plugin.enabled ? "Yes" : "No"}\nActive: ${plugin.active ? "Yes" : "No"}\nSpec: ${plugin.spec}`}
|
|
46
|
+
/>
|
|
47
|
+
))
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
))
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default plugin
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginModule, TuiSidebarFileItem } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { createWrappedAPI } from "../core/api-wrapper"
|
|
4
|
+
|
|
5
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
6
|
+
id: "opencode-tui-utils.session-diff",
|
|
7
|
+
async tui(rawApi) {
|
|
8
|
+
const api = createWrappedAPI(rawApi)
|
|
9
|
+
const { DialogSelect, DialogAlert } = api.ui
|
|
10
|
+
|
|
11
|
+
api.keymap.registerLayer({
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "opencode-tui-utils.session-diff",
|
|
15
|
+
title: "Session Diff",
|
|
16
|
+
category: "Session",
|
|
17
|
+
namespace: "palette",
|
|
18
|
+
slashName: "session-diff",
|
|
19
|
+
async run() {
|
|
20
|
+
const route = api.route.current
|
|
21
|
+
if (route.name !== "session" || !route.params?.sessionID) {
|
|
22
|
+
api.ui.toast({
|
|
23
|
+
title: "No session",
|
|
24
|
+
message: "You must be in a session to view diffs.",
|
|
25
|
+
})
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sessionID = String(route.params.sessionID)
|
|
30
|
+
const diffs = api.state.session.diff(sessionID)
|
|
31
|
+
|
|
32
|
+
if (diffs.length === 0) {
|
|
33
|
+
api.ui.toast({
|
|
34
|
+
title: "No changes",
|
|
35
|
+
message: "No file changes in this session.",
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const options = diffs.map((d: TuiSidebarFileItem) => ({
|
|
41
|
+
title: `${d.file} (+${d.additions}/-${d.deletions})`,
|
|
42
|
+
value: d.file,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
api.ui.dialog.replace(() => (
|
|
46
|
+
<DialogSelect
|
|
47
|
+
title={`Changed files (${diffs.length})`}
|
|
48
|
+
options={options}
|
|
49
|
+
onSelect={(option) => {
|
|
50
|
+
if (!option) return
|
|
51
|
+
const diff = diffs.find((d: TuiSidebarFileItem) => d.file === option.value)
|
|
52
|
+
if (!diff) return
|
|
53
|
+
api.ui.dialog.replace(() => (
|
|
54
|
+
<DialogAlert
|
|
55
|
+
title={diff.file}
|
|
56
|
+
message={`Additions: +${diff.additions}\nDeletions: -${diff.deletions}`}
|
|
57
|
+
/>
|
|
58
|
+
))
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
))
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default plugin
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type { TuiPluginModule, TuiSidebarTodoItem } from "@opencode-ai/plugin/tui"
|
|
3
|
+
import { createWrappedAPI } from "../core/api-wrapper"
|
|
4
|
+
|
|
5
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
6
|
+
id: "opencode-tui-utils.session-todos",
|
|
7
|
+
async tui(rawApi) {
|
|
8
|
+
const api = createWrappedAPI(rawApi)
|
|
9
|
+
const { DialogSelect, DialogAlert } = api.ui
|
|
10
|
+
|
|
11
|
+
api.keymap.registerLayer({
|
|
12
|
+
commands: [
|
|
13
|
+
{
|
|
14
|
+
name: "opencode-tui-utils.session-todos",
|
|
15
|
+
title: "Session Todos",
|
|
16
|
+
category: "Session",
|
|
17
|
+
namespace: "palette",
|
|
18
|
+
slashName: "session-todos",
|
|
19
|
+
async run() {
|
|
20
|
+
const route = api.route.current
|
|
21
|
+
if (route.name !== "session" || !route.params?.sessionID) {
|
|
22
|
+
api.ui.toast({
|
|
23
|
+
title: "No session",
|
|
24
|
+
message: "You must be in a session to view todos.",
|
|
25
|
+
})
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sessionID = String(route.params.sessionID)
|
|
30
|
+
const todos = api.state.session.todo(sessionID)
|
|
31
|
+
|
|
32
|
+
if (todos.length === 0) {
|
|
33
|
+
api.ui.toast({
|
|
34
|
+
title: "No todos",
|
|
35
|
+
message: "No todos in this session.",
|
|
36
|
+
})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const options = todos.map((t: TuiSidebarTodoItem) => ({
|
|
41
|
+
title: `${t.content} (${t.status})`,
|
|
42
|
+
value: t.content,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
api.ui.dialog.replace(() => (
|
|
46
|
+
<DialogSelect
|
|
47
|
+
title={`Todos (${todos.length})`}
|
|
48
|
+
options={options}
|
|
49
|
+
onSelect={(option) => {
|
|
50
|
+
if (!option) return
|
|
51
|
+
const todo = todos.find((t: TuiSidebarTodoItem) => t.content === option.value)
|
|
52
|
+
if (!todo) return
|
|
53
|
+
api.ui.dialog.replace(() => (
|
|
54
|
+
<DialogAlert
|
|
55
|
+
title="Todo"
|
|
56
|
+
message={`${todo.content}\nStatus: ${todo.status}`}
|
|
57
|
+
/>
|
|
58
|
+
))
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
))
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default plugin
|