opencode-tui-utils 1.0.0 → 1.1.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 CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  Thank you for taking the time to improve `opencode-tui-utils`.
4
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.
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
- - 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.
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 Scope
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
- Good fits for this package:
154
+ These are verified gaps based on the plugin API. Feel free to pick one and open an issue before starting.
139
155
 
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. |
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
- Poor fits:
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
- | 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. |
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
  [![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
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
12
 
13
- [opencode](https://opencode.ai) のTUIで使う小さなユーティリティ集です。最初のコマンドは、接続済みプロバイダーを安全に解除する `/disconnect` です。
13
+ opencodeに欠けているエッセンシャルなTUIコマンド。一度インストールすれば、JSONを手動で編集する必要はありません。
14
14
 
15
- このプロジェクトは、コマンドを1つずつ追加して育てる前提で構成されています。各ユーティリティは、opencode TUI内の明確な問題を1つ解決し、レビューとテストがしやすい形に保ちます。
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
  [![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
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
12
 
13
- [opencode](https://opencode.ai) TUI에서 자주 필요한 작업을 작은 명령어로 추가하는 플러그인 패키지입니다. 기능은 연결된 프로바이더를 안전하게 해제하는 `/disconnect` 명령입니다.
13
+ opencode 없는 필수 TUI 명령어를 제공합니다. 설치하고 JSON 파일 직접 편집은 그만하세요.
14
14
 
15
- 이 프로젝트는 명령어를 하나씩 확장할 수 있도록 구성되어 있습니다. 각 유틸리티는 opencode TUI 안에서 가지 문제를 명확하게 해결하고, 검토와 테스트가 쉬운 형태를 유지하는 것을 기준으로 합니다.
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
  [![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
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
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.
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 should solve one focused opencode TUI problem and stay easy to review, test, and remove.
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
  [![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
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
12
12
 
13
- 面向 [opencode](https://opencode.ai) TUI 的小型工具集。第一个命令是 `/disconnect`,用于安全地断开一个已连接的 provider,不需要手动编辑 `auth.json`。
13
+ opencode 缺少的必备 TUI 命令。安装一次,再也不用亲手编辑 JSON。
14
14
 
15
- 这个项目按“一个命令解决一个问题”的方式扩展。每个工具都应该聚焦于一个 opencode TUI 场景,并保持容易审查、测试和移除。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-tui-utils",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Add /disconnect command to opencode - safely disconnect providers without editing auth.json",
5
5
  "main": "src/index.tsx",
6
6
  "type": "module",
@@ -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
  /**
@@ -94,5 +98,9 @@ export function createWrappedAPI(api: TuiPluginApi): WrappedAPI {
94
98
  keymap: createKeymapAPI(api),
95
99
  ui: createUIAPI(api),
96
100
  kv: createKVAPI(api),
101
+ plugins: api.plugins,
102
+ client: api.client,
103
+ route: api.route,
104
+ state: api.state,
97
105
  }
98
106
  }
package/src/index.tsx CHANGED
@@ -5,8 +5,12 @@
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"
8
12
 
9
- const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin]
13
+ const plugins: TuiPluginModule[] = [disconnectPlugin, lspTogglePlugin, pluginListPlugin, exportChatPlugin, sessionDiffPlugin, sessionTodosPlugin]
10
14
 
11
15
  export async function initializePlugins(...args: Parameters<TuiPluginModule["tui"]>) {
12
16
  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,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